feat(daemon): emit stable vev pane identity
- commit
- 884ab90b5ce0dd25442f0afd307cf004ed9f5008
- author
- brice · · 2026-07-06 07:40 UTC
- parent
- 5ebf319
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
5 files · +84 -11
A internal/usecase/daemon/identity.go +26 -0
| @@ -0,0 +1,26 @@ | |||
| 1 | + | package daemon | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "crypto/rand" | |
| 5 | + | "encoding/base32" | |
| 6 | + | "io" | |
| 7 | + | "strings" | |
| 8 | + | ) | |
| 9 | + | ||
| 10 | + | var stableIDRand io.Reader = rand.Reader | |
| 11 | + | ||
| 12 | + | func newStableID() (string, error) { | |
| 13 | + | var b [16]byte | |
| 14 | + | if _, err := io.ReadFull(stableIDRand, b[:]); err != nil { | |
| 15 | + | return "", err | |
| 16 | + | } | |
| 17 | + | return strings.ToLower(base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(b[:])), nil | |
| 18 | + | } | |
| 19 | + | ||
| 20 | + | func mustTestStableID(prefix string) string { | |
| 21 | + | id, err := newStableID() | |
| 22 | + | if err != nil { | |
| 23 | + | return prefix + "-unknown" | |
| 24 | + | } | |
| 25 | + | return id | |
| 26 | + | } | |
| @@ -0,0 +1,26 @@ | |||||
| 1 | + | package daemon | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "crypto/rand" | |||
| 5 | + | "encoding/base32" | |||
| 6 | + | "io" | |||
| 7 | + | "strings" | |||
| 8 | + | ) | |||
| 9 | + | ||||
| 10 | + | var stableIDRand io.Reader = rand.Reader | |||
| 11 | + | ||||
| 12 | + | func newStableID() (string, error) { | |||
| 13 | + | var b [16]byte | |||
| 14 | + | if _, err := io.ReadFull(stableIDRand, b[:]); err != nil { | |||
| 15 | + | return "", err | |||
| 16 | + | } | |||
| 17 | + | return strings.ToLower(base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(b[:])), nil | |||
| 18 | + | } | |||
| 19 | + | ||||
| 20 | + | func mustTestStableID(prefix string) string { | |||
| 21 | + | id, err := newStableID() | |||
| 22 | + | if err != nil { | |||
| 23 | + | return prefix + "-unknown" | |||
| 24 | + | } | |||
| 25 | + | return id | |||
| 26 | + | } | |||
M internal/usecase/daemon/pane.go +6 -0
| @@ -18,6 +18,7 @@ import ( | |||
| 18 | 18 | // The PTY reader takes only pane.mu, so child output never waits on client IO. | |
| 19 | 19 | type pane struct { | |
| 20 | 20 | id layout.PaneID | |
| 21 | + | stableID string | |
| 21 | 22 | pty ports.PTY | |
| 22 | 23 | mu sync.Mutex // guards screen, scrollback, syncGen, rect, and title | |
| 23 | 24 | screen *vt.Screen | |
| @@ -34,11 +35,16 @@ type pane struct { | |||
| 34 | 35 | } | |
| 35 | 36 | ||
| 36 | 37 | func newPane(id layout.PaneID, pty ports.PTY, sz domain.Size) *pane { | |
| 38 | + | return newPaneWithStableID(id, mustTestStableID("pane"), pty, sz) | |
| 39 | + | } | |
| 40 | + | ||
| 41 | + | func newPaneWithStableID(id layout.PaneID, stableID string, pty ports.PTY, sz domain.Size) *pane { | |
| 37 | 42 | sb := scopy.NewScrollback(defaultScrollbackRows) | |
| 38 | 43 | screen := vt.NewScreen(sz.Cols, sz.Rows) | |
| 39 | 44 | screen.OnLineEvicted = sb.Append | |
| 40 | 45 | return &pane{ | |
| 41 | 46 | id: id, | |
| 47 | + | stableID: stableID, | |
| 42 | 48 | pty: pty, | |
| 43 | 49 | screen: screen, | |
| 44 | 50 | scrollback: sb, | |
| @@ -18,6 +18,7 @@ import ( | |||||
| 18 | // The PTY reader takes only pane.mu, so child output never waits on client IO. | 18 | // The PTY reader takes only pane.mu, so child output never waits on client IO. | ||
| 19 | type pane struct { | 19 | type pane struct { | ||
| 20 | id layout.PaneID | 20 | id layout.PaneID | ||
| 21 | + | stableID string | |||
| 21 | pty ports.PTY | 22 | pty ports.PTY | ||
| 22 | mu sync.Mutex // guards screen, scrollback, syncGen, rect, and title | 23 | mu sync.Mutex // guards screen, scrollback, syncGen, rect, and title | ||
| 23 | screen *vt.Screen | 24 | screen *vt.Screen | ||
| @@ -34,11 +35,16 @@ type pane struct { | |||||
| 34 | } | 35 | } | ||
| 35 | 36 | ||||
| 36 | func newPane(id layout.PaneID, pty ports.PTY, sz domain.Size) *pane { | 37 | func newPane(id layout.PaneID, pty ports.PTY, sz domain.Size) *pane { | ||
| 38 | + | return newPaneWithStableID(id, mustTestStableID("pane"), pty, sz) | |||
| 39 | + | } | |||
| 40 | + | ||||
| 41 | + | func newPaneWithStableID(id layout.PaneID, stableID string, pty ports.PTY, sz domain.Size) *pane { | |||
| 37 | sb := scopy.NewScrollback(defaultScrollbackRows) | 42 | sb := scopy.NewScrollback(defaultScrollbackRows) | ||
| 38 | screen := vt.NewScreen(sz.Cols, sz.Rows) | 43 | screen := vt.NewScreen(sz.Cols, sz.Rows) | ||
| 39 | screen.OnLineEvicted = sb.Append | 44 | screen.OnLineEvicted = sb.Append | ||
| 40 | return &pane{ | 45 | return &pane{ | ||
| 41 | id: id, | 46 | id: id, | ||
| 47 | + | stableID: stableID, | |||
| 42 | pty: pty, | 48 | pty: pty, | ||
| 43 | screen: screen, | 49 | screen: screen, | ||
| 44 | scrollback: sb, | 50 | scrollback: sb, | ||
M internal/usecase/daemon/pane_actions.go +11 -2
| @@ -57,9 +57,18 @@ func (d *Daemon) spawnPaneOp( | |||
| 57 | 57 | return layout.ErrTooSmall | |
| 58 | 58 | } | |
| 59 | 59 | newRect := placementContent(placements, newID) | |
| 60 | + | tabStableID := tb.stableID | |
| 60 | 61 | tb.mu.Unlock() | |
| 61 | 62 | ||
| 62 | - | pty, err := d.ptys.Open(d.shell, d.shellArgs, d.childEnv(name), cwd, rectSize(newRect)) | |
| 63 | + | paneStableID, err := newStableID() | |
| 64 | + | if err != nil { | |
| 65 | + | tb.mu.Lock() | |
| 66 | + | _ = tb.tree.Close(newID) | |
| 67 | + | tb.tree.Focus = oldFocus | |
| 68 | + | tb.mu.Unlock() | |
| 69 | + | return fmt.Errorf("daemon: generating pane identity: %w", err) | |
| 70 | + | } | |
| 71 | + | pty, err := d.ptys.Open(d.shell, d.shellArgs, d.childEnv(name, tabStableID, paneStableID), cwd, rectSize(newRect)) | |
| 63 | 72 | if err != nil { | |
| 64 | 73 | d.log.Warn("pty spawn failed", "err", err, "session", name, "pane", newID, "kind", "pane") | |
| 65 | 74 | tb.mu.Lock() | |
| @@ -70,7 +79,7 @@ func (d *Daemon) spawnPaneOp( | |||
| 70 | 79 | } | |
| 71 | 80 | ||
| 72 | 81 | pctx, cancel := context.WithCancel(tb.ctx) | |
| 73 | - | p := newPane(newID, pty, rectSize(newRect)) | |
| 82 | + | p := newPaneWithStableID(newID, paneStableID, pty, rectSize(newRect)) | |
| 74 | 83 | p.ctx, p.cancel = pctx, cancel | |
| 75 | 84 | p.rect = newRect | |
| 76 | 85 | ||
| @@ -57,9 +57,18 @@ func (d *Daemon) spawnPaneOp( | |||||
| 57 | return layout.ErrTooSmall | 57 | return layout.ErrTooSmall | ||
| 58 | } | 58 | } | ||
| 59 | newRect := placementContent(placements, newID) | 59 | newRect := placementContent(placements, newID) | ||
| 60 | + | tabStableID := tb.stableID | |||
| 60 | tb.mu.Unlock() | 61 | tb.mu.Unlock() | ||
| 61 | 62 | ||||
| 62 | - | pty, err := d.ptys.Open(d.shell, d.shellArgs, d.childEnv(name), cwd, rectSize(newRect)) | 63 | + | paneStableID, err := newStableID() |
| 64 | + | if err != nil { | |||
| 65 | + | tb.mu.Lock() | |||
| 66 | + | _ = tb.tree.Close(newID) | |||
| 67 | + | tb.tree.Focus = oldFocus | |||
| 68 | + | tb.mu.Unlock() | |||
| 69 | + | return fmt.Errorf("daemon: generating pane identity: %w", err) | |||
| 70 | + | } | |||
| 71 | + | pty, err := d.ptys.Open(d.shell, d.shellArgs, d.childEnv(name, tabStableID, paneStableID), cwd, rectSize(newRect)) | |||
| 63 | if err != nil { | 72 | if err != nil { | ||
| 64 | d.log.Warn("pty spawn failed", "err", err, "session", name, "pane", newID, "kind", "pane") | 73 | d.log.Warn("pty spawn failed", "err", err, "session", name, "pane", newID, "kind", "pane") | ||
| 65 | tb.mu.Lock() | 74 | tb.mu.Lock() | ||
| @@ -70,7 +79,7 @@ func (d *Daemon) spawnPaneOp( | |||||
| 70 | } | 79 | } | ||
| 71 | 80 | ||||
| 72 | pctx, cancel := context.WithCancel(tb.ctx) | 81 | pctx, cancel := context.WithCancel(tb.ctx) | ||
| 73 | - | p := newPane(newID, pty, rectSize(newRect)) | 82 | + | p := newPaneWithStableID(newID, paneStableID, pty, rectSize(newRect)) |
| 74 | p.ctx, p.cancel = pctx, cancel | 83 | p.ctx, p.cancel = pctx, cancel | ||
| 75 | p.rect = newRect | 84 | p.rect = newRect | ||
| 76 | 85 | ||||
M internal/usecase/daemon/session.go +28 -6
| @@ -70,6 +70,7 @@ type session struct { | |||
| 70 | 70 | type tab struct { | |
| 71 | 71 | mu sync.Mutex // guards tree, panes, nextPaneID, size, previewClient, and pane map membership | |
| 72 | 72 | ||
| 73 | + | stableID string | |
| 73 | 74 | tree *layout.Tree | |
| 74 | 75 | panes map[layout.PaneID]*pane | |
| 75 | 76 | nextPaneID int | |
| @@ -122,7 +123,15 @@ func (d *Daemon) touchMRU(sess *session) { | |||
| 122 | 123 | ||
| 123 | 124 | func (d *Daemon) createSessionLocked(name string, ephemeral bool, cwd string, sz domain.Size) (*session, error) { | |
| 124 | 125 | tbSize := tabSize(sz) | |
| 125 | - | pty, err := d.ptys.Open(d.shell, d.shellArgs, d.childEnv(name), cwd, tbSize) | |
| 126 | + | tabStableID, err := newStableID() | |
| 127 | + | if err != nil { | |
| 128 | + | return nil, fmt.Errorf("daemon: generating tab identity: %w", err) | |
| 129 | + | } | |
| 130 | + | paneStableID, err := newStableID() | |
| 131 | + | if err != nil { | |
| 132 | + | return nil, fmt.Errorf("daemon: generating pane identity: %w", err) | |
| 133 | + | } | |
| 134 | + | pty, err := d.ptys.Open(d.shell, d.shellArgs, d.childEnv(name, tabStableID, paneStableID), cwd, tbSize) | |
| 126 | 135 | if err != nil { | |
| 127 | 136 | d.log.Warn("pty spawn failed", "err", err, "session", name, "kind", "session") | |
| 128 | 137 | return nil, fmt.Errorf("daemon: spawning session %q: %w", name, err) | |
| @@ -234,12 +243,20 @@ func (d *Daemon) createTab(sess *session, sz domain.Size) error { | |||
| 234 | 243 | cwd := sess.cwd | |
| 235 | 244 | client := sess.client | |
| 236 | 245 | sess.mu.Unlock() | |
| 237 | - | pty, err := d.ptys.Open(d.shell, d.shellArgs, d.childEnv(name), cwd, tbSize) | |
| 246 | + | tabStableID, err := newStableID() | |
| 247 | + | if err != nil { | |
| 248 | + | return fmt.Errorf("daemon: generating tab identity: %w", err) | |
| 249 | + | } | |
| 250 | + | paneStableID, err := newStableID() | |
| 251 | + | if err != nil { | |
| 252 | + | return fmt.Errorf("daemon: generating pane identity: %w", err) | |
| 253 | + | } | |
| 254 | + | pty, err := d.ptys.Open(d.shell, d.shellArgs, d.childEnv(name, tabStableID, paneStableID), cwd, tbSize) | |
| 238 | 255 | if err != nil { | |
| 239 | 256 | d.log.Warn("pty spawn failed", "err", err, "session", name, "kind", "tab") | |
| 240 | 257 | return fmt.Errorf("daemon: spawning tab for session %q: %w", name, err) | |
| 241 | 258 | } | |
| 242 | - | tb := newTab(pty, tbSize) | |
| 259 | + | tb := newTabWithStableID(tabStableID, paneStableID, pty, tbSize) | |
| 243 | 260 | if client != nil { | |
| 244 | 261 | t := d.effectiveTheme(client.getClientTheme()) | |
| 245 | 262 | tb.mu.Lock() | |
| @@ -274,9 +291,14 @@ func (d *Daemon) createTab(sess *session, sz domain.Size) error { | |||
| 274 | 291 | } | |
| 275 | 292 | ||
| 276 | 293 | func newTab(pty ports.PTY, sz domain.Size) *tab { | |
| 294 | + | return newTabWithStableID(mustTestStableID("tab"), mustTestStableID("pane"), pty, sz) | |
| 295 | + | } | |
| 296 | + | ||
| 297 | + | func newTabWithStableID(tabStableID, paneStableID string, pty ports.PTY, sz domain.Size) *tab { | |
| 277 | 298 | id := layout.PaneID("pane-1") | |
| 278 | - | p := newPane(id, pty, sz) | |
| 299 | + | p := newPaneWithStableID(id, paneStableID, pty, sz) | |
| 279 | 300 | return &tab{ | |
| 301 | + | stableID: tabStableID, | |
| 280 | 302 | tree: layout.NewTree(id), | |
| 281 | 303 | panes: map[layout.PaneID]*pane{id: p}, | |
| 282 | 304 | nextPaneID: 2, | |
| @@ -707,7 +729,7 @@ func (d *Daemon) refreshSessionCwd(sess *session) { | |||
| 707 | 729 | ||
| 708 | 730 | // childEnv builds the session child's environment: the daemon's own, with TERM | |
| 709 | 731 | // and VEV forced to well-known values. | |
| 710 | - | func (d *Daemon) childEnv(name string) []string { | |
| 732 | + | func (d *Daemon) childEnv(name, tabStableID, paneStableID string) []string { | |
| 711 | 733 | out := make([]string, 0, len(d.baseEnv)+2) | |
| 712 | 734 | for _, e := range d.baseEnv { | |
| 713 | 735 | if strings.HasPrefix(e, "TERM=") || strings.HasPrefix(e, "VEV=") { | |
| @@ -715,5 +737,5 @@ func (d *Daemon) childEnv(name string) []string { | |||
| 715 | 737 | } | |
| 716 | 738 | out = append(out, e) | |
| 717 | 739 | } | |
| 718 | - | return append(out, "TERM=xterm-256color", "VEV="+name) | |
| 740 | + | return append(out, "TERM=xterm-direct", "VEV=session="+name+",tab="+tabStableID+",pane="+paneStableID) | |
| 719 | 741 | } | |
| @@ -70,6 +70,7 @@ type session struct { | |||||
| 70 | type tab struct { | 70 | type tab struct { | ||
| 71 | mu sync.Mutex // guards tree, panes, nextPaneID, size, previewClient, and pane map membership | 71 | mu sync.Mutex // guards tree, panes, nextPaneID, size, previewClient, and pane map membership | ||
| 72 | 72 | ||||
| 73 | + | stableID string | |||
| 73 | tree *layout.Tree | 74 | tree *layout.Tree | ||
| 74 | panes map[layout.PaneID]*pane | 75 | panes map[layout.PaneID]*pane | ||
| 75 | nextPaneID int | 76 | nextPaneID int | ||
| @@ -122,7 +123,15 @@ func (d *Daemon) touchMRU(sess *session) { | |||||
| 122 | 123 | ||||
| 123 | func (d *Daemon) createSessionLocked(name string, ephemeral bool, cwd string, sz domain.Size) (*session, error) { | 124 | func (d *Daemon) createSessionLocked(name string, ephemeral bool, cwd string, sz domain.Size) (*session, error) { | ||
| 124 | tbSize := tabSize(sz) | 125 | tbSize := tabSize(sz) | ||
| 125 | - | pty, err := d.ptys.Open(d.shell, d.shellArgs, d.childEnv(name), cwd, tbSize) | 126 | + | tabStableID, err := newStableID() |
| 127 | + | if err != nil { | |||
| 128 | + | return nil, fmt.Errorf("daemon: generating tab identity: %w", err) | |||
| 129 | + | } | |||
| 130 | + | paneStableID, err := newStableID() | |||
| 131 | + | if err != nil { | |||
| 132 | + | return nil, fmt.Errorf("daemon: generating pane identity: %w", err) | |||
| 133 | + | } | |||
| 134 | + | pty, err := d.ptys.Open(d.shell, d.shellArgs, d.childEnv(name, tabStableID, paneStableID), cwd, tbSize) | |||
| 126 | if err != nil { | 135 | if err != nil { | ||
| 127 | d.log.Warn("pty spawn failed", "err", err, "session", name, "kind", "session") | 136 | d.log.Warn("pty spawn failed", "err", err, "session", name, "kind", "session") | ||
| 128 | return nil, fmt.Errorf("daemon: spawning session %q: %w", name, err) | 137 | return nil, fmt.Errorf("daemon: spawning session %q: %w", name, err) | ||
| @@ -234,12 +243,20 @@ func (d *Daemon) createTab(sess *session, sz domain.Size) error { | |||||
| 234 | cwd := sess.cwd | 243 | cwd := sess.cwd | ||
| 235 | client := sess.client | 244 | client := sess.client | ||
| 236 | sess.mu.Unlock() | 245 | sess.mu.Unlock() | ||
| 237 | - | pty, err := d.ptys.Open(d.shell, d.shellArgs, d.childEnv(name), cwd, tbSize) | 246 | + | tabStableID, err := newStableID() |
| 247 | + | if err != nil { | |||
| 248 | + | return fmt.Errorf("daemon: generating tab identity: %w", err) | |||
| 249 | + | } | |||
| 250 | + | paneStableID, err := newStableID() | |||
| 251 | + | if err != nil { | |||
| 252 | + | return fmt.Errorf("daemon: generating pane identity: %w", err) | |||
| 253 | + | } | |||
| 254 | + | pty, err := d.ptys.Open(d.shell, d.shellArgs, d.childEnv(name, tabStableID, paneStableID), cwd, tbSize) | |||
| 238 | if err != nil { | 255 | if err != nil { | ||
| 239 | d.log.Warn("pty spawn failed", "err", err, "session", name, "kind", "tab") | 256 | d.log.Warn("pty spawn failed", "err", err, "session", name, "kind", "tab") | ||
| 240 | return fmt.Errorf("daemon: spawning tab for session %q: %w", name, err) | 257 | return fmt.Errorf("daemon: spawning tab for session %q: %w", name, err) | ||
| 241 | } | 258 | } | ||
| 242 | - | tb := newTab(pty, tbSize) | 259 | + | tb := newTabWithStableID(tabStableID, paneStableID, pty, tbSize) |
| 243 | if client != nil { | 260 | if client != nil { | ||
| 244 | t := d.effectiveTheme(client.getClientTheme()) | 261 | t := d.effectiveTheme(client.getClientTheme()) | ||
| 245 | tb.mu.Lock() | 262 | tb.mu.Lock() | ||
| @@ -274,9 +291,14 @@ func (d *Daemon) createTab(sess *session, sz domain.Size) error { | |||||
| 274 | } | 291 | } | ||
| 275 | 292 | ||||
| 276 | func newTab(pty ports.PTY, sz domain.Size) *tab { | 293 | func newTab(pty ports.PTY, sz domain.Size) *tab { | ||
| 294 | + | return newTabWithStableID(mustTestStableID("tab"), mustTestStableID("pane"), pty, sz) | |||
| 295 | + | } | |||
| 296 | + | ||||
| 297 | + | func newTabWithStableID(tabStableID, paneStableID string, pty ports.PTY, sz domain.Size) *tab { | |||
| 277 | id := layout.PaneID("pane-1") | 298 | id := layout.PaneID("pane-1") | ||
| 278 | - | p := newPane(id, pty, sz) | 299 | + | p := newPaneWithStableID(id, paneStableID, pty, sz) |
| 279 | return &tab{ | 300 | return &tab{ | ||
| 301 | + | stableID: tabStableID, | |||
| 280 | tree: layout.NewTree(id), | 302 | tree: layout.NewTree(id), | ||
| 281 | panes: map[layout.PaneID]*pane{id: p}, | 303 | panes: map[layout.PaneID]*pane{id: p}, | ||
| 282 | nextPaneID: 2, | 304 | nextPaneID: 2, | ||
| @@ -707,7 +729,7 @@ func (d *Daemon) refreshSessionCwd(sess *session) { | |||||
| 707 | 729 | ||||
| 708 | // childEnv builds the session child's environment: the daemon's own, with TERM | 730 | // childEnv builds the session child's environment: the daemon's own, with TERM | ||
| 709 | // and VEV forced to well-known values. | 731 | // and VEV forced to well-known values. | ||
| 710 | - | func (d *Daemon) childEnv(name string) []string { | 732 | + | func (d *Daemon) childEnv(name, tabStableID, paneStableID string) []string { |
| 711 | out := make([]string, 0, len(d.baseEnv)+2) | 733 | out := make([]string, 0, len(d.baseEnv)+2) | ||
| 712 | for _, e := range d.baseEnv { | 734 | for _, e := range d.baseEnv { | ||
| 713 | if strings.HasPrefix(e, "TERM=") || strings.HasPrefix(e, "VEV=") { | 735 | if strings.HasPrefix(e, "TERM=") || strings.HasPrefix(e, "VEV=") { | ||
| @@ -715,5 +737,5 @@ func (d *Daemon) childEnv(name string) []string { | |||||
| 715 | } | 737 | } | ||
| 716 | out = append(out, e) | 738 | out = append(out, e) | ||
| 717 | } | 739 | } | ||
| 718 | - | return append(out, "TERM=xterm-256color", "VEV="+name) | 740 | + | return append(out, "TERM=xterm-direct", "VEV=session="+name+",tab="+tabStableID+",pane="+paneStableID) |
| 719 | } | 741 | } | ||
M internal/usecase/daemon/snapshot.go +13 -3
| @@ -92,7 +92,12 @@ func (d *Daemon) restoreSession(ctx context.Context, snap snapcodec.Session) err | |||
| 92 | 92 | for _, pl := range placements { | |
| 93 | 93 | placementByPane[pl.ID] = pl.Content | |
| 94 | 94 | } | |
| 95 | - | tb := &tab{tree: tabSnap.Tree.Clone(), panes: make(map[layout.PaneID]*pane, len(tabSnap.Panes)), nextPaneID: int(tabSnap.NextPaneID), size: tbSize} | |
| 95 | + | tabStableID, err := newStableID() | |
| 96 | + | if err != nil { | |
| 97 | + | closeOpened() | |
| 98 | + | return fmt.Errorf("snapshot: generating tab identity: %w", err) | |
| 99 | + | } | |
| 100 | + | tb := &tab{stableID: tabStableID, tree: tabSnap.Tree.Clone(), panes: make(map[layout.PaneID]*pane, len(tabSnap.Panes)), nextPaneID: int(tabSnap.NextPaneID), size: tbSize} | |
| 96 | 101 | if tb.nextPaneID <= 0 { | |
| 97 | 102 | tb.nextPaneID = 1 | |
| 98 | 103 | } | |
| @@ -109,12 +114,17 @@ func (d *Daemon) restoreSession(ctx context.Context, snap snapcodec.Session) err | |||
| 109 | 114 | return fmt.Errorf("snapshot: missing pane placement") | |
| 110 | 115 | } | |
| 111 | 116 | contentSize := restorePTYSize(contentRect, tbSize) | |
| 112 | - | pty, err := d.ptys.Open(d.shell, d.shellArgs, d.childEnv(snap.Name), paneSnap.Cwd, contentSize) | |
| 117 | + | paneStableID, err := newStableID() | |
| 118 | + | if err != nil { | |
| 119 | + | closeOpened() | |
| 120 | + | return fmt.Errorf("snapshot: generating pane identity: %w", err) | |
| 121 | + | } | |
| 122 | + | pty, err := d.ptys.Open(d.shell, d.shellArgs, d.childEnv(snap.Name, tabStableID, paneStableID), paneSnap.Cwd, contentSize) | |
| 113 | 123 | if err != nil { | |
| 114 | 124 | closeOpened() | |
| 115 | 125 | return err | |
| 116 | 126 | } | |
| 117 | - | p := newPane(paneSnap.ID, pty, contentSize) | |
| 127 | + | p := newPaneWithStableID(paneSnap.ID, paneStableID, pty, contentSize) | |
| 118 | 128 | p.ctx, p.cancel = context.WithCancel(tb.ctx) | |
| 119 | 129 | p.rect = contentRect | |
| 120 | 130 | seedPaneRows(p, paneSnap.Scrollback, paneSnap.Visible) | |
| @@ -92,7 +92,12 @@ func (d *Daemon) restoreSession(ctx context.Context, snap snapcodec.Session) err | |||||
| 92 | for _, pl := range placements { | 92 | for _, pl := range placements { | ||
| 93 | placementByPane[pl.ID] = pl.Content | 93 | placementByPane[pl.ID] = pl.Content | ||
| 94 | } | 94 | } | ||
| 95 | - | tb := &tab{tree: tabSnap.Tree.Clone(), panes: make(map[layout.PaneID]*pane, len(tabSnap.Panes)), nextPaneID: int(tabSnap.NextPaneID), size: tbSize} | 95 | + | tabStableID, err := newStableID() |
| 96 | + | if err != nil { | |||
| 97 | + | closeOpened() | |||
| 98 | + | return fmt.Errorf("snapshot: generating tab identity: %w", err) | |||
| 99 | + | } | |||
| 100 | + | tb := &tab{stableID: tabStableID, tree: tabSnap.Tree.Clone(), panes: make(map[layout.PaneID]*pane, len(tabSnap.Panes)), nextPaneID: int(tabSnap.NextPaneID), size: tbSize} | |||
| 96 | if tb.nextPaneID <= 0 { | 101 | if tb.nextPaneID <= 0 { | ||
| 97 | tb.nextPaneID = 1 | 102 | tb.nextPaneID = 1 | ||
| 98 | } | 103 | } | ||
| @@ -109,12 +114,17 @@ func (d *Daemon) restoreSession(ctx context.Context, snap snapcodec.Session) err | |||||
| 109 | return fmt.Errorf("snapshot: missing pane placement") | 114 | return fmt.Errorf("snapshot: missing pane placement") | ||
| 110 | } | 115 | } | ||
| 111 | contentSize := restorePTYSize(contentRect, tbSize) | 116 | contentSize := restorePTYSize(contentRect, tbSize) | ||
| 112 | - | pty, err := d.ptys.Open(d.shell, d.shellArgs, d.childEnv(snap.Name), paneSnap.Cwd, contentSize) | 117 | + | paneStableID, err := newStableID() |
| 118 | + | if err != nil { | |||
| 119 | + | closeOpened() | |||
| 120 | + | return fmt.Errorf("snapshot: generating pane identity: %w", err) | |||
| 121 | + | } | |||
| 122 | + | pty, err := d.ptys.Open(d.shell, d.shellArgs, d.childEnv(snap.Name, tabStableID, paneStableID), paneSnap.Cwd, contentSize) | |||
| 113 | if err != nil { | 123 | if err != nil { | ||
| 114 | closeOpened() | 124 | closeOpened() | ||
| 115 | return err | 125 | return err | ||
| 116 | } | 126 | } | ||
| 117 | - | p := newPane(paneSnap.ID, pty, contentSize) | 127 | + | p := newPaneWithStableID(paneSnap.ID, paneStableID, pty, contentSize) |
| 118 | p.ctx, p.cancel = context.WithCancel(tb.ctx) | 128 | p.ctx, p.cancel = context.WithCancel(tb.ctx) | ||
| 119 | p.rect = contentRect | 129 | p.rect = contentRect | ||
| 120 | seedPaneRows(p, paneSnap.Scrollback, paneSnap.Visible) | 130 | seedPaneRows(p, paneSnap.Scrollback, paneSnap.Visible) | ||