fix(tabs): reconcile rename flow with current main
- commit
- bc467a6b69a284b13db72518fdb2685ab1a12023
- author
- brice · · 2026-07-09 18:34 UTC
- parent
- dee642a
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
12 files · +199 -38
M internal/persist/persist.go +3 -1
| @@ -71,14 +71,16 @@ func (p *Persister) Touch(name, cwd string, at int64) error { | |||
| 71 | 71 | ||
| 72 | 72 | createdAt := at | |
| 73 | 73 | lastUsedSeq := uint64(0) | |
| 74 | + | var tabNames []string | |
| 74 | 75 | if v, ok := p.store.Get([]byte(name)); ok { | |
| 75 | 76 | if r, err := decodeRecordValue(name, v); err == nil { | |
| 76 | 77 | createdAt = r.CreatedAt | |
| 77 | 78 | lastUsedSeq = r.LastUsedSeq | |
| 79 | + | tabNames = r.TabNames | |
| 78 | 80 | } | |
| 79 | 81 | } | |
| 80 | 82 | ||
| 81 | - | value, err := encodeRecordValue(Record{Name: name, Cwd: cwd, CreatedAt: createdAt, UpdatedAt: at, LastUsedSeq: lastUsedSeq}) | |
| 83 | + | value, err := encodeRecordValue(Record{Name: name, Cwd: cwd, CreatedAt: createdAt, UpdatedAt: at, LastUsedSeq: lastUsedSeq, TabNames: tabNames}) | |
| 82 | 84 | if err != nil { | |
| 83 | 85 | return err | |
| 84 | 86 | } | |
| @@ -71,14 +71,16 @@ func (p *Persister) Touch(name, cwd string, at int64) error { | |||||
| 71 | 71 | ||||
| 72 | createdAt := at | 72 | createdAt := at | ||
| 73 | lastUsedSeq := uint64(0) | 73 | lastUsedSeq := uint64(0) | ||
| 74 | + | var tabNames []string | |||
| 74 | if v, ok := p.store.Get([]byte(name)); ok { | 75 | if v, ok := p.store.Get([]byte(name)); ok { | ||
| 75 | if r, err := decodeRecordValue(name, v); err == nil { | 76 | if r, err := decodeRecordValue(name, v); err == nil { | ||
| 76 | createdAt = r.CreatedAt | 77 | createdAt = r.CreatedAt | ||
| 77 | lastUsedSeq = r.LastUsedSeq | 78 | lastUsedSeq = r.LastUsedSeq | ||
| 79 | + | tabNames = r.TabNames | |||
| 78 | } | 80 | } | ||
| 79 | } | 81 | } | ||
| 80 | 82 | ||||
| 81 | - | value, err := encodeRecordValue(Record{Name: name, Cwd: cwd, CreatedAt: createdAt, UpdatedAt: at, LastUsedSeq: lastUsedSeq}) | 83 | + | value, err := encodeRecordValue(Record{Name: name, Cwd: cwd, CreatedAt: createdAt, UpdatedAt: at, LastUsedSeq: lastUsedSeq, TabNames: tabNames}) |
| 82 | if err != nil { | 84 | if err != nil { | ||
| 83 | return err | 85 | return err | ||
| 84 | } | 86 | } | ||
M internal/persist/persist_test.go +4 -5
| @@ -1,7 +1,6 @@ | |||
| 1 | 1 | package persist | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | - | "encoding/binary" | |
| 5 | 4 | "errors" | |
| 6 | 5 | "path/filepath" | |
| 7 | 6 | "testing" | |
| @@ -18,7 +17,7 @@ func privateDir(t *testing.T) string { | |||
| 18 | 17 | } | |
| 19 | 18 | ||
| 20 | 19 | func TestRecordCodecRoundTrip(t *testing.T) { | |
| 21 | - | r := Record{Name: "work", Cwd: "/tmp/project", CreatedAt: 11, UpdatedAt: 22, LastUsedSeq: 33} | |
| 20 | + | r := Record{Name: "work", Cwd: "/tmp/project", CreatedAt: 11, UpdatedAt: 22, LastUsedSeq: 33, TabNames: []string{"shell", "logs"}} | |
| 22 | 21 | value, err := encodeRecordValue(r) | |
| 23 | 22 | require.NoError(t, err) | |
| 24 | 23 | ||
| @@ -82,7 +81,7 @@ func TestPersisterSaveTouchDeleteLoadAll(t *testing.T) { | |||
| 82 | 81 | p, err := Open(dir) | |
| 83 | 82 | require.NoError(t, err) | |
| 84 | 83 | ||
| 85 | - | require.NoError(t, p.Save(Record{Name: "b", Cwd: "/b", CreatedAt: 10, UpdatedAt: 10, LastUsedSeq: 40})) | |
| 84 | + | require.NoError(t, p.Save(Record{Name: "b", Cwd: "/b", CreatedAt: 10, UpdatedAt: 10, LastUsedSeq: 40, TabNames: []string{"shell"}})) | |
| 86 | 85 | require.NoError(t, p.Save(Record{Name: "a", Cwd: "/a", CreatedAt: 20, UpdatedAt: 20, LastUsedSeq: 50})) | |
| 87 | 86 | require.NoError(t, p.Touch("b", "/b/next", 30)) | |
| 88 | 87 | require.NoError(t, p.TouchMRU("b", 60)) | |
| @@ -91,13 +90,13 @@ func TestPersisterSaveTouchDeleteLoadAll(t *testing.T) { | |||
| 91 | 90 | require.NoError(t, err) | |
| 92 | 91 | require.Equal(t, []Record{ | |
| 93 | 92 | {Name: "a", Cwd: "/a", CreatedAt: 20, UpdatedAt: 20, LastUsedSeq: 50}, | |
| 94 | - | {Name: "b", Cwd: "/b/next", CreatedAt: 10, UpdatedAt: 30, LastUsedSeq: 60}, | |
| 93 | + | {Name: "b", Cwd: "/b/next", CreatedAt: 10, UpdatedAt: 30, LastUsedSeq: 60, TabNames: []string{"shell"}}, | |
| 95 | 94 | }, records) | |
| 96 | 95 | ||
| 97 | 96 | require.NoError(t, p.Delete("a")) | |
| 98 | 97 | records, err = p.LoadAll() | |
| 99 | 98 | require.NoError(t, err) | |
| 100 | - | require.Equal(t, []Record{{Name: "b", Cwd: "/b/next", CreatedAt: 10, UpdatedAt: 30, LastUsedSeq: 60}}, records) | |
| 99 | + | require.Equal(t, []Record{{Name: "b", Cwd: "/b/next", CreatedAt: 10, UpdatedAt: 30, LastUsedSeq: 60, TabNames: []string{"shell"}}}, records) | |
| 101 | 100 | require.NoError(t, p.Close()) | |
| 102 | 101 | } | |
| 103 | 102 | ||
| @@ -1,7 +1,6 @@ | |||||
| 1 | package persist | 1 | package persist | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | - | "encoding/binary" | |||
| 5 | "errors" | 4 | "errors" | ||
| 6 | "path/filepath" | 5 | "path/filepath" | ||
| 7 | "testing" | 6 | "testing" | ||
| @@ -18,7 +17,7 @@ func privateDir(t *testing.T) string { | |||||
| 18 | } | 17 | } | ||
| 19 | 18 | ||||
| 20 | func TestRecordCodecRoundTrip(t *testing.T) { | 19 | func TestRecordCodecRoundTrip(t *testing.T) { | ||
| 21 | - | r := Record{Name: "work", Cwd: "/tmp/project", CreatedAt: 11, UpdatedAt: 22, LastUsedSeq: 33} | 20 | + | r := Record{Name: "work", Cwd: "/tmp/project", CreatedAt: 11, UpdatedAt: 22, LastUsedSeq: 33, TabNames: []string{"shell", "logs"}} |
| 22 | value, err := encodeRecordValue(r) | 21 | value, err := encodeRecordValue(r) | ||
| 23 | require.NoError(t, err) | 22 | require.NoError(t, err) | ||
| 24 | 23 | ||||
| @@ -82,7 +81,7 @@ func TestPersisterSaveTouchDeleteLoadAll(t *testing.T) { | |||||
| 82 | p, err := Open(dir) | 81 | p, err := Open(dir) | ||
| 83 | require.NoError(t, err) | 82 | require.NoError(t, err) | ||
| 84 | 83 | ||||
| 85 | - | require.NoError(t, p.Save(Record{Name: "b", Cwd: "/b", CreatedAt: 10, UpdatedAt: 10, LastUsedSeq: 40})) | 84 | + | require.NoError(t, p.Save(Record{Name: "b", Cwd: "/b", CreatedAt: 10, UpdatedAt: 10, LastUsedSeq: 40, TabNames: []string{"shell"}})) |
| 86 | require.NoError(t, p.Save(Record{Name: "a", Cwd: "/a", CreatedAt: 20, UpdatedAt: 20, LastUsedSeq: 50})) | 85 | require.NoError(t, p.Save(Record{Name: "a", Cwd: "/a", CreatedAt: 20, UpdatedAt: 20, LastUsedSeq: 50})) | ||
| 87 | require.NoError(t, p.Touch("b", "/b/next", 30)) | 86 | require.NoError(t, p.Touch("b", "/b/next", 30)) | ||
| 88 | require.NoError(t, p.TouchMRU("b", 60)) | 87 | require.NoError(t, p.TouchMRU("b", 60)) | ||
| @@ -91,13 +90,13 @@ func TestPersisterSaveTouchDeleteLoadAll(t *testing.T) { | |||||
| 91 | require.NoError(t, err) | 90 | require.NoError(t, err) | ||
| 92 | require.Equal(t, []Record{ | 91 | require.Equal(t, []Record{ | ||
| 93 | {Name: "a", Cwd: "/a", CreatedAt: 20, UpdatedAt: 20, LastUsedSeq: 50}, | 92 | {Name: "a", Cwd: "/a", CreatedAt: 20, UpdatedAt: 20, LastUsedSeq: 50}, | ||
| 94 | - | {Name: "b", Cwd: "/b/next", CreatedAt: 10, UpdatedAt: 30, LastUsedSeq: 60}, | 93 | + | {Name: "b", Cwd: "/b/next", CreatedAt: 10, UpdatedAt: 30, LastUsedSeq: 60, TabNames: []string{"shell"}}, |
| 95 | }, records) | 94 | }, records) | ||
| 96 | 95 | ||||
| 97 | require.NoError(t, p.Delete("a")) | 96 | require.NoError(t, p.Delete("a")) | ||
| 98 | records, err = p.LoadAll() | 97 | records, err = p.LoadAll() | ||
| 99 | require.NoError(t, err) | 98 | require.NoError(t, err) | ||
| 100 | - | require.Equal(t, []Record{{Name: "b", Cwd: "/b/next", CreatedAt: 10, UpdatedAt: 30, LastUsedSeq: 60}}, records) | 99 | + | require.Equal(t, []Record{{Name: "b", Cwd: "/b/next", CreatedAt: 10, UpdatedAt: 30, LastUsedSeq: 60, TabNames: []string{"shell"}}}, records) |
| 101 | require.NoError(t, p.Close()) | 100 | require.NoError(t, p.Close()) | ||
| 102 | } | 101 | } | ||
| 103 | 102 | ||||
M internal/persist/record.go +50 -3
| @@ -14,6 +14,7 @@ type Record struct { | |||
| 14 | 14 | CreatedAt int64 | |
| 15 | 15 | UpdatedAt int64 | |
| 16 | 16 | LastUsedSeq uint64 | |
| 17 | + | TabNames []string | |
| 17 | 18 | } | |
| 18 | 19 | ||
| 19 | 20 | var ( | |
| @@ -39,13 +40,22 @@ func encodeRecordValue(r Record) ([]byte, error) { | |||
| 39 | 40 | tabNamesSize += 4 + len(name) | |
| 40 | 41 | } | |
| 41 | 42 | ||
| 42 | - | buf := make([]byte, 4+len(r.Cwd)+8+8+8) | |
| 43 | + | buf := make([]byte, 4+len(r.Cwd)+8+8+8+tabNamesSize) | |
| 43 | 44 | binary.BigEndian.PutUint32(buf[:4], uint32(len(r.Cwd))) | |
| 44 | 45 | copy(buf[4:], r.Cwd) | |
| 45 | 46 | off := 4 + len(r.Cwd) | |
| 46 | 47 | binary.BigEndian.PutUint64(buf[off:off+8], uint64(r.CreatedAt)) | |
| 47 | 48 | binary.BigEndian.PutUint64(buf[off+8:off+16], uint64(r.UpdatedAt)) | |
| 48 | 49 | binary.BigEndian.PutUint64(buf[off+16:off+24], r.LastUsedSeq) | |
| 50 | + | off += 24 | |
| 51 | + | binary.BigEndian.PutUint32(buf[off:off+4], uint32(len(r.TabNames))) | |
| 52 | + | off += 4 | |
| 53 | + | for _, name := range r.TabNames { | |
| 54 | + | binary.BigEndian.PutUint32(buf[off:off+4], uint32(len(name))) | |
| 55 | + | off += 4 | |
| 56 | + | copy(buf[off:], name) | |
| 57 | + | off += len(name) | |
| 58 | + | } | |
| 49 | 59 | return buf, nil | |
| 50 | 60 | } | |
| 51 | 61 | ||
| @@ -62,7 +72,8 @@ func decodeRecordValue(name string, value []byte) (Record, error) { | |||
| 62 | 72 | return Record{}, errMalformedRecord | |
| 63 | 73 | } | |
| 64 | 74 | cwdLen := int(cwdLen32) | |
| 65 | - | if len(value) != 4+cwdLen+8+8 && len(value) != 4+cwdLen+8+8+8 { | |
| 75 | + | baseLen := 4 + cwdLen + 8 + 8 | |
| 76 | + | if len(value) < baseLen { | |
| 66 | 77 | return Record{}, errMalformedRecord | |
| 67 | 78 | } | |
| 68 | 79 | off := 4 + cwdLen | |
| @@ -72,8 +83,44 @@ func decodeRecordValue(name string, value []byte) (Record, error) { | |||
| 72 | 83 | CreatedAt: int64(binary.BigEndian.Uint64(value[off : off+8])), | |
| 73 | 84 | UpdatedAt: int64(binary.BigEndian.Uint64(value[off+8 : off+16])), | |
| 74 | 85 | } | |
| 75 | - | if len(value) == 4+cwdLen+8+8+8 { | |
| 86 | + | if len(value) == baseLen { | |
| 87 | + | return r, nil | |
| 88 | + | } | |
| 89 | + | if len(value) < baseLen+8 { | |
| 90 | + | return Record{}, errMalformedRecord | |
| 91 | + | } | |
| 92 | + | if len(value) == baseLen+8 { | |
| 76 | 93 | r.LastUsedSeq = binary.BigEndian.Uint64(value[off+16 : off+24]) | |
| 94 | + | return r, nil | |
| 95 | + | } | |
| 96 | + | r.LastUsedSeq = binary.BigEndian.Uint64(value[off+16 : off+24]) | |
| 97 | + | off += 24 | |
| 98 | + | if len(value)-off < 4 { | |
| 99 | + | return Record{}, errMalformedRecord | |
| 100 | + | } | |
| 101 | + | tabCount32 := binary.BigEndian.Uint32(value[off : off+4]) | |
| 102 | + | off += 4 | |
| 103 | + | if uint64(tabCount32) > uint64(len(value)-off)/4 { | |
| 104 | + | return Record{}, errMalformedRecord | |
| 105 | + | } | |
| 106 | + | if tabCount32 > 0 { | |
| 107 | + | r.TabNames = make([]string, 0, int(tabCount32)) | |
| 108 | + | } | |
| 109 | + | for range tabCount32 { | |
| 110 | + | if len(value)-off < 4 { | |
| 111 | + | return Record{}, errMalformedRecord | |
| 112 | + | } | |
| 113 | + | nameLen32 := binary.BigEndian.Uint32(value[off : off+4]) | |
| 114 | + | off += 4 | |
| 115 | + | if uint64(nameLen32) > uint64(len(value)-off) { | |
| 116 | + | return Record{}, errMalformedRecord | |
| 117 | + | } | |
| 118 | + | nameLen := int(nameLen32) | |
| 119 | + | r.TabNames = append(r.TabNames, string(value[off:off+nameLen])) | |
| 120 | + | off += nameLen | |
| 121 | + | } | |
| 122 | + | if off != len(value) { | |
| 123 | + | return Record{}, errMalformedRecord | |
| 77 | 124 | } | |
| 78 | 125 | return r, nil | |
| 79 | 126 | } | |
| @@ -14,6 +14,7 @@ type Record struct { | |||||
| 14 | CreatedAt int64 | 14 | CreatedAt int64 | ||
| 15 | UpdatedAt int64 | 15 | UpdatedAt int64 | ||
| 16 | LastUsedSeq uint64 | 16 | LastUsedSeq uint64 | ||
| 17 | + | TabNames []string | |||
| 17 | } | 18 | } | ||
| 18 | 19 | ||||
| 19 | var ( | 20 | var ( | ||
| @@ -39,13 +40,22 @@ func encodeRecordValue(r Record) ([]byte, error) { | |||||
| 39 | tabNamesSize += 4 + len(name) | 40 | tabNamesSize += 4 + len(name) | ||
| 40 | } | 41 | } | ||
| 41 | 42 | ||||
| 42 | - | buf := make([]byte, 4+len(r.Cwd)+8+8+8) | 43 | + | buf := make([]byte, 4+len(r.Cwd)+8+8+8+tabNamesSize) |
| 43 | binary.BigEndian.PutUint32(buf[:4], uint32(len(r.Cwd))) | 44 | binary.BigEndian.PutUint32(buf[:4], uint32(len(r.Cwd))) | ||
| 44 | copy(buf[4:], r.Cwd) | 45 | copy(buf[4:], r.Cwd) | ||
| 45 | off := 4 + len(r.Cwd) | 46 | off := 4 + len(r.Cwd) | ||
| 46 | binary.BigEndian.PutUint64(buf[off:off+8], uint64(r.CreatedAt)) | 47 | binary.BigEndian.PutUint64(buf[off:off+8], uint64(r.CreatedAt)) | ||
| 47 | binary.BigEndian.PutUint64(buf[off+8:off+16], uint64(r.UpdatedAt)) | 48 | binary.BigEndian.PutUint64(buf[off+8:off+16], uint64(r.UpdatedAt)) | ||
| 48 | binary.BigEndian.PutUint64(buf[off+16:off+24], r.LastUsedSeq) | 49 | binary.BigEndian.PutUint64(buf[off+16:off+24], r.LastUsedSeq) | ||
| 50 | + | off += 24 | |||
| 51 | + | binary.BigEndian.PutUint32(buf[off:off+4], uint32(len(r.TabNames))) | |||
| 52 | + | off += 4 | |||
| 53 | + | for _, name := range r.TabNames { | |||
| 54 | + | binary.BigEndian.PutUint32(buf[off:off+4], uint32(len(name))) | |||
| 55 | + | off += 4 | |||
| 56 | + | copy(buf[off:], name) | |||
| 57 | + | off += len(name) | |||
| 58 | + | } | |||
| 49 | return buf, nil | 59 | return buf, nil | ||
| 50 | } | 60 | } | ||
| 51 | 61 | ||||
| @@ -62,7 +72,8 @@ func decodeRecordValue(name string, value []byte) (Record, error) { | |||||
| 62 | return Record{}, errMalformedRecord | 72 | return Record{}, errMalformedRecord | ||
| 63 | } | 73 | } | ||
| 64 | cwdLen := int(cwdLen32) | 74 | cwdLen := int(cwdLen32) | ||
| 65 | - | if len(value) != 4+cwdLen+8+8 && len(value) != 4+cwdLen+8+8+8 { | 75 | + | baseLen := 4 + cwdLen + 8 + 8 |
| 76 | + | if len(value) < baseLen { | |||
| 66 | return Record{}, errMalformedRecord | 77 | return Record{}, errMalformedRecord | ||
| 67 | } | 78 | } | ||
| 68 | off := 4 + cwdLen | 79 | off := 4 + cwdLen | ||
| @@ -72,8 +83,44 @@ func decodeRecordValue(name string, value []byte) (Record, error) { | |||||
| 72 | CreatedAt: int64(binary.BigEndian.Uint64(value[off : off+8])), | 83 | CreatedAt: int64(binary.BigEndian.Uint64(value[off : off+8])), | ||
| 73 | UpdatedAt: int64(binary.BigEndian.Uint64(value[off+8 : off+16])), | 84 | UpdatedAt: int64(binary.BigEndian.Uint64(value[off+8 : off+16])), | ||
| 74 | } | 85 | } | ||
| 75 | - | if len(value) == 4+cwdLen+8+8+8 { | 86 | + | if len(value) == baseLen { |
| 87 | + | return r, nil | |||
| 88 | + | } | |||
| 89 | + | if len(value) < baseLen+8 { | |||
| 90 | + | return Record{}, errMalformedRecord | |||
| 91 | + | } | |||
| 92 | + | if len(value) == baseLen+8 { | |||
| 76 | r.LastUsedSeq = binary.BigEndian.Uint64(value[off+16 : off+24]) | 93 | r.LastUsedSeq = binary.BigEndian.Uint64(value[off+16 : off+24]) | ||
| 94 | + | return r, nil | |||
| 95 | + | } | |||
| 96 | + | r.LastUsedSeq = binary.BigEndian.Uint64(value[off+16 : off+24]) | |||
| 97 | + | off += 24 | |||
| 98 | + | if len(value)-off < 4 { | |||
| 99 | + | return Record{}, errMalformedRecord | |||
| 100 | + | } | |||
| 101 | + | tabCount32 := binary.BigEndian.Uint32(value[off : off+4]) | |||
| 102 | + | off += 4 | |||
| 103 | + | if uint64(tabCount32) > uint64(len(value)-off)/4 { | |||
| 104 | + | return Record{}, errMalformedRecord | |||
| 105 | + | } | |||
| 106 | + | if tabCount32 > 0 { | |||
| 107 | + | r.TabNames = make([]string, 0, int(tabCount32)) | |||
| 108 | + | } | |||
| 109 | + | for range tabCount32 { | |||
| 110 | + | if len(value)-off < 4 { | |||
| 111 | + | return Record{}, errMalformedRecord | |||
| 112 | + | } | |||
| 113 | + | nameLen32 := binary.BigEndian.Uint32(value[off : off+4]) | |||
| 114 | + | off += 4 | |||
| 115 | + | if uint64(nameLen32) > uint64(len(value)-off) { | |||
| 116 | + | return Record{}, errMalformedRecord | |||
| 117 | + | } | |||
| 118 | + | nameLen := int(nameLen32) | |||
| 119 | + | r.TabNames = append(r.TabNames, string(value[off:off+nameLen])) | |||
| 120 | + | off += nameLen | |||
| 121 | + | } | |||
| 122 | + | if off != len(value) { | |||
| 123 | + | return Record{}, errMalformedRecord | |||
| 77 | } | 124 | } | ||
| 78 | return r, nil | 125 | return r, nil | ||
| 79 | } | 126 | } | ||
M internal/usecase/command/context_mock_test.go +44 -0
| @@ -739,6 +739,50 @@ func (_c *MockContext_RenameSession_Call) RunAndReturn(run func() error) *MockCo | |||
| 739 | 739 | return _c | |
| 740 | 740 | } | |
| 741 | 741 | ||
| 742 | + | // RenameTab provides a mock function for the type MockContext | |
| 743 | + | func (_mock *MockContext) RenameTab() error { | |
| 744 | + | ret := _mock.Called() | |
| 745 | + | ||
| 746 | + | if len(ret) == 0 { | |
| 747 | + | panic("no return value specified for RenameTab") | |
| 748 | + | } | |
| 749 | + | ||
| 750 | + | var r0 error | |
| 751 | + | if returnFunc, ok := ret.Get(0).(func() error); ok { | |
| 752 | + | r0 = returnFunc() | |
| 753 | + | } else { | |
| 754 | + | r0 = ret.Error(0) | |
| 755 | + | } | |
| 756 | + | return r0 | |
| 757 | + | } | |
| 758 | + | ||
| 759 | + | // MockContext_RenameTab_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RenameTab' | |
| 760 | + | type MockContext_RenameTab_Call struct { | |
| 761 | + | *mock.Call | |
| 762 | + | } | |
| 763 | + | ||
| 764 | + | // RenameTab is a helper method to define mock.On call | |
| 765 | + | func (_e *MockContext_Expecter) RenameTab() *MockContext_RenameTab_Call { | |
| 766 | + | return &MockContext_RenameTab_Call{Call: _e.mock.On("RenameTab")} | |
| 767 | + | } | |
| 768 | + | ||
| 769 | + | func (_c *MockContext_RenameTab_Call) Run(run func()) *MockContext_RenameTab_Call { | |
| 770 | + | _c.Call.Run(func(args mock.Arguments) { | |
| 771 | + | run() | |
| 772 | + | }) | |
| 773 | + | return _c | |
| 774 | + | } | |
| 775 | + | ||
| 776 | + | func (_c *MockContext_RenameTab_Call) Return(err error) *MockContext_RenameTab_Call { | |
| 777 | + | _c.Call.Return(err) | |
| 778 | + | return _c | |
| 779 | + | } | |
| 780 | + | ||
| 781 | + | func (_c *MockContext_RenameTab_Call) RunAndReturn(run func() error) *MockContext_RenameTab_Call { | |
| 782 | + | _c.Call.Return(run) | |
| 783 | + | return _c | |
| 784 | + | } | |
| 785 | + | ||
| 742 | 786 | // SplitDown provides a mock function for the type MockContext | |
| 743 | 787 | func (_mock *MockContext) SplitDown() error { | |
| 744 | 788 | ret := _mock.Called() | |
| @@ -739,6 +739,50 @@ func (_c *MockContext_RenameSession_Call) RunAndReturn(run func() error) *MockCo | |||||
| 739 | return _c | 739 | return _c | ||
| 740 | } | 740 | } | ||
| 741 | 741 | ||||
| 742 | + | // RenameTab provides a mock function for the type MockContext | |||
| 743 | + | func (_mock *MockContext) RenameTab() error { | |||
| 744 | + | ret := _mock.Called() | |||
| 745 | + | ||||
| 746 | + | if len(ret) == 0 { | |||
| 747 | + | panic("no return value specified for RenameTab") | |||
| 748 | + | } | |||
| 749 | + | ||||
| 750 | + | var r0 error | |||
| 751 | + | if returnFunc, ok := ret.Get(0).(func() error); ok { | |||
| 752 | + | r0 = returnFunc() | |||
| 753 | + | } else { | |||
| 754 | + | r0 = ret.Error(0) | |||
| 755 | + | } | |||
| 756 | + | return r0 | |||
| 757 | + | } | |||
| 758 | + | ||||
| 759 | + | // MockContext_RenameTab_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RenameTab' | |||
| 760 | + | type MockContext_RenameTab_Call struct { | |||
| 761 | + | *mock.Call | |||
| 762 | + | } | |||
| 763 | + | ||||
| 764 | + | // RenameTab is a helper method to define mock.On call | |||
| 765 | + | func (_e *MockContext_Expecter) RenameTab() *MockContext_RenameTab_Call { | |||
| 766 | + | return &MockContext_RenameTab_Call{Call: _e.mock.On("RenameTab")} | |||
| 767 | + | } | |||
| 768 | + | ||||
| 769 | + | func (_c *MockContext_RenameTab_Call) Run(run func()) *MockContext_RenameTab_Call { | |||
| 770 | + | _c.Call.Run(func(args mock.Arguments) { | |||
| 771 | + | run() | |||
| 772 | + | }) | |||
| 773 | + | return _c | |||
| 774 | + | } | |||
| 775 | + | ||||
| 776 | + | func (_c *MockContext_RenameTab_Call) Return(err error) *MockContext_RenameTab_Call { | |||
| 777 | + | _c.Call.Return(err) | |||
| 778 | + | return _c | |||
| 779 | + | } | |||
| 780 | + | ||||
| 781 | + | func (_c *MockContext_RenameTab_Call) RunAndReturn(run func() error) *MockContext_RenameTab_Call { | |||
| 782 | + | _c.Call.Return(run) | |||
| 783 | + | return _c | |||
| 784 | + | } | |||
| 785 | + | ||||
| 742 | // SplitDown provides a mock function for the type MockContext | 786 | // SplitDown provides a mock function for the type MockContext | ||
| 743 | func (_mock *MockContext) SplitDown() error { | 787 | func (_mock *MockContext) SplitDown() error { | ||
| 744 | ret := _mock.Called() | 788 | ret := _mock.Called() | ||
M internal/usecase/command/registry.go +1 -0
| @@ -26,6 +26,7 @@ func Registry() []Command { | |||
| 26 | 26 | {Slug: "session-picker", Code: "SSP", Name: "Session picker", Desc: "Open the session picker", Run: func(ctx Context) error { return ctx.OpenSessionPicker() }}, | |
| 27 | 27 | {Slug: "visual-mode", Code: "VIS", Name: "Visual mode", Desc: "Enter visual mode", Run: func(ctx Context) error { return ctx.EnterVisualMode() }}, | |
| 28 | 28 | {Slug: "rename-session", Code: "RNS", Name: "Rename session", Desc: "Rename the session (an ephemeral session becomes named)", Run: func(ctx Context) error { return ctx.RenameSession() }}, | |
| 29 | + | {Slug: "rename-tab", Code: "RNT", Name: "Rename tab", Desc: "Rename the current tab", Run: func(ctx Context) error { return ctx.RenameTab() }}, | |
| 29 | 30 | {Slug: "detach", Code: "DET", Name: "Detach", Desc: "Detach from the session", Run: func(ctx Context) error { return ctx.Detach() }}, | |
| 30 | 31 | } | |
| 31 | 32 | ||
| @@ -26,6 +26,7 @@ func Registry() []Command { | |||||
| 26 | {Slug: "session-picker", Code: "SSP", Name: "Session picker", Desc: "Open the session picker", Run: func(ctx Context) error { return ctx.OpenSessionPicker() }}, | 26 | {Slug: "session-picker", Code: "SSP", Name: "Session picker", Desc: "Open the session picker", Run: func(ctx Context) error { return ctx.OpenSessionPicker() }}, | ||
| 27 | {Slug: "visual-mode", Code: "VIS", Name: "Visual mode", Desc: "Enter visual mode", Run: func(ctx Context) error { return ctx.EnterVisualMode() }}, | 27 | {Slug: "visual-mode", Code: "VIS", Name: "Visual mode", Desc: "Enter visual mode", Run: func(ctx Context) error { return ctx.EnterVisualMode() }}, | ||
| 28 | {Slug: "rename-session", Code: "RNS", Name: "Rename session", Desc: "Rename the session (an ephemeral session becomes named)", Run: func(ctx Context) error { return ctx.RenameSession() }}, | 28 | {Slug: "rename-session", Code: "RNS", Name: "Rename session", Desc: "Rename the session (an ephemeral session becomes named)", Run: func(ctx Context) error { return ctx.RenameSession() }}, | ||
| 29 | + | {Slug: "rename-tab", Code: "RNT", Name: "Rename tab", Desc: "Rename the current tab", Run: func(ctx Context) error { return ctx.RenameTab() }}, | |||
| 29 | {Slug: "detach", Code: "DET", Name: "Detach", Desc: "Detach from the session", Run: func(ctx Context) error { return ctx.Detach() }}, | 30 | {Slug: "detach", Code: "DET", Name: "Detach", Desc: "Detach from the session", Run: func(ctx Context) error { return ctx.Detach() }}, | ||
| 30 | } | 31 | } | ||
| 31 | 32 | ||||
M internal/usecase/command/registry_test.go +2 -1
| @@ -7,7 +7,7 @@ import ( | |||
| 7 | 7 | ||
| 8 | 8 | func TestRegistryCodesAndSlugsAreUniqueInOrder(t *testing.T) { | |
| 9 | 9 | commands := Registry() | |
| 10 | - | wantCodes := []string{"CNT", "CNS", "CLT", "SPR", "SPL", "SPU", "SPD", "STP", "TST", "CLP", "FPL", "FPR", "FPU", "FPD", "NXT", "PVT", "BSK", "FSK", "SSP", "VIS", "RNS", "DET"} | |
| 10 | + | wantCodes := []string{"CNT", "CNS", "CLT", "SPR", "SPL", "SPU", "SPD", "STP", "TST", "CLP", "FPL", "FPR", "FPU", "FPD", "NXT", "PVT", "BSK", "FSK", "SSP", "VIS", "RNS", "RNT", "DET"} | |
| 11 | 11 | ||
| 12 | 12 | if len(commands) != len(wantCodes) { | |
| 13 | 13 | t.Fatalf("Registry() returned %d commands, want %d", len(commands), len(wantCodes)) | |
| @@ -79,6 +79,7 @@ func TestCommandRunCallsMatchingContextMethod(t *testing.T) { | |||
| 79 | 79 | {code: "SSP", expect: func(ctx *MockContext) { ctx.EXPECT().OpenSessionPicker().Return(nil).Once() }}, | |
| 80 | 80 | {code: "VIS", expect: func(ctx *MockContext) { ctx.EXPECT().EnterVisualMode().Return(nil).Once() }}, | |
| 81 | 81 | {code: "RNS", expect: func(ctx *MockContext) { ctx.EXPECT().RenameSession().Return(nil).Once() }}, | |
| 82 | + | {code: "RNT", expect: func(ctx *MockContext) { ctx.EXPECT().RenameTab().Return(nil).Once() }}, | |
| 82 | 83 | {code: "DET", expect: func(ctx *MockContext) { ctx.EXPECT().Detach().Return(nil).Once() }}, | |
| 83 | 84 | } | |
| 84 | 85 | ||
| @@ -7,7 +7,7 @@ import ( | |||||
| 7 | 7 | ||||
| 8 | func TestRegistryCodesAndSlugsAreUniqueInOrder(t *testing.T) { | 8 | func TestRegistryCodesAndSlugsAreUniqueInOrder(t *testing.T) { | ||
| 9 | commands := Registry() | 9 | commands := Registry() | ||
| 10 | - | wantCodes := []string{"CNT", "CNS", "CLT", "SPR", "SPL", "SPU", "SPD", "STP", "TST", "CLP", "FPL", "FPR", "FPU", "FPD", "NXT", "PVT", "BSK", "FSK", "SSP", "VIS", "RNS", "DET"} | 10 | + | wantCodes := []string{"CNT", "CNS", "CLT", "SPR", "SPL", "SPU", "SPD", "STP", "TST", "CLP", "FPL", "FPR", "FPU", "FPD", "NXT", "PVT", "BSK", "FSK", "SSP", "VIS", "RNS", "RNT", "DET"} |
| 11 | 11 | ||||
| 12 | if len(commands) != len(wantCodes) { | 12 | if len(commands) != len(wantCodes) { | ||
| 13 | t.Fatalf("Registry() returned %d commands, want %d", len(commands), len(wantCodes)) | 13 | t.Fatalf("Registry() returned %d commands, want %d", len(commands), len(wantCodes)) | ||
| @@ -79,6 +79,7 @@ func TestCommandRunCallsMatchingContextMethod(t *testing.T) { | |||||
| 79 | {code: "SSP", expect: func(ctx *MockContext) { ctx.EXPECT().OpenSessionPicker().Return(nil).Once() }}, | 79 | {code: "SSP", expect: func(ctx *MockContext) { ctx.EXPECT().OpenSessionPicker().Return(nil).Once() }}, | ||
| 80 | {code: "VIS", expect: func(ctx *MockContext) { ctx.EXPECT().EnterVisualMode().Return(nil).Once() }}, | 80 | {code: "VIS", expect: func(ctx *MockContext) { ctx.EXPECT().EnterVisualMode().Return(nil).Once() }}, | ||
| 81 | {code: "RNS", expect: func(ctx *MockContext) { ctx.EXPECT().RenameSession().Return(nil).Once() }}, | 81 | {code: "RNS", expect: func(ctx *MockContext) { ctx.EXPECT().RenameSession().Return(nil).Once() }}, | ||
| 82 | + | {code: "RNT", expect: func(ctx *MockContext) { ctx.EXPECT().RenameTab().Return(nil).Once() }}, | |||
| 82 | {code: "DET", expect: func(ctx *MockContext) { ctx.EXPECT().Detach().Return(nil).Once() }}, | 83 | {code: "DET", expect: func(ctx *MockContext) { ctx.EXPECT().Detach().Return(nil).Once() }}, | ||
| 83 | } | 84 | } | ||
| 84 | 85 | ||||
M internal/usecase/daemon/daemon.go +15 -4
| @@ -31,6 +31,7 @@ import ( | |||
| 31 | 31 | "errors" | |
| 32 | 32 | "log/slog" | |
| 33 | 33 | "os" | |
| 34 | + | "slices" | |
| 34 | 35 | "sort" | |
| 35 | 36 | "sync" | |
| 36 | 37 | "sync/atomic" | |
| @@ -163,9 +164,19 @@ type stoppedSession struct { | |||
| 163 | 164 | cwd string | |
| 164 | 165 | createdAt int64 | |
| 165 | 166 | lastUsedSeq uint64 | |
| 167 | + | tabNames []string | |
| 166 | 168 | purging bool | |
| 167 | 169 | } | |
| 168 | 170 | ||
| 171 | + | func (s stoppedSession) same(other stoppedSession) bool { | |
| 172 | + | return s.name == other.name && | |
| 173 | + | s.cwd == other.cwd && | |
| 174 | + | s.createdAt == other.createdAt && | |
| 175 | + | s.lastUsedSeq == other.lastUsedSeq && | |
| 176 | + | s.purging == other.purging && | |
| 177 | + | slices.Equal(s.tabNames, other.tabNames) | |
| 178 | + | } | |
| 179 | + | ||
| 169 | 180 | type Option func(*Daemon) | |
| 170 | 181 | ||
| 171 | 182 | // WithShell overrides the command (and its args) each session spawns. The | |
| @@ -320,7 +331,7 @@ func New(ptys ports.PTYFactory, clock ports.Clock, log *slog.Logger, opts ...Opt | |||
| 320 | 331 | } else { | |
| 321 | 332 | var maxSeq uint64 | |
| 322 | 333 | for _, r := range records { | |
| 323 | - | d.stopped[r.Name] = stoppedSession{name: r.Name, cwd: r.Cwd, createdAt: r.CreatedAt, lastUsedSeq: r.LastUsedSeq} | |
| 334 | + | d.stopped[r.Name] = stoppedSession{name: r.Name, cwd: r.Cwd, createdAt: r.CreatedAt, lastUsedSeq: r.LastUsedSeq, tabNames: r.TabNames} | |
| 324 | 335 | if r.LastUsedSeq > maxSeq { | |
| 325 | 336 | maxSeq = r.LastUsedSeq | |
| 326 | 337 | } | |
| @@ -537,7 +548,7 @@ func (d *Daemon) handleKill(tr ports.Transport, f ports.Frame) { | |||
| 537 | 548 | return | |
| 538 | 549 | } | |
| 539 | 550 | d.mu.Lock() | |
| 540 | - | if cur, ok := d.stopped[k.Name]; ok && cur == stopped { | |
| 551 | + | if cur, ok := d.stopped[k.Name]; ok && cur.same(stopped) { | |
| 541 | 552 | delete(d.stopped, k.Name) | |
| 542 | 553 | } | |
| 543 | 554 | d.mu.Unlock() | |
| @@ -647,7 +658,7 @@ func (d *Daemon) route(h ports.Hello, tr ports.Transport) (*session, *attachedCl | |||
| 647 | 658 | } | |
| 648 | 659 | cwd := d.dirOrHome(stopped.cwd) | |
| 649 | 660 | var err error | |
| 650 | - | sess, err = d.createSessionLocked(h.Name, false, cwd, sz, term) | |
| 661 | + | sess, err = d.createSessionLocked(h.Name, false, cwd, sz, term, stopped.tabNames) | |
| 651 | 662 | if err != nil { | |
| 652 | 663 | d.mu.Unlock() | |
| 653 | 664 | return nil, nil, err | |
| @@ -715,7 +726,7 @@ func (d *Daemon) route(h ports.Hello, tr ports.Transport) (*session, *attachedCl | |||
| 715 | 726 | } | |
| 716 | 727 | cwd := d.dirOrHome(stopped.cwd) | |
| 717 | 728 | var err error | |
| 718 | - | sess, err = d.createSessionLocked(h.Name, false, cwd, sz, term) | |
| 729 | + | sess, err = d.createSessionLocked(h.Name, false, cwd, sz, term, stopped.tabNames) | |
| 719 | 730 | if err != nil { | |
| 720 | 731 | d.mu.Unlock() | |
| 721 | 732 | return nil, nil, err | |
| @@ -31,6 +31,7 @@ import ( | |||||
| 31 | "errors" | 31 | "errors" | ||
| 32 | "log/slog" | 32 | "log/slog" | ||
| 33 | "os" | 33 | "os" | ||
| 34 | + | "slices" | |||
| 34 | "sort" | 35 | "sort" | ||
| 35 | "sync" | 36 | "sync" | ||
| 36 | "sync/atomic" | 37 | "sync/atomic" | ||
| @@ -163,9 +164,19 @@ type stoppedSession struct { | |||||
| 163 | cwd string | 164 | cwd string | ||
| 164 | createdAt int64 | 165 | createdAt int64 | ||
| 165 | lastUsedSeq uint64 | 166 | lastUsedSeq uint64 | ||
| 167 | + | tabNames []string | |||
| 166 | purging bool | 168 | purging bool | ||
| 167 | } | 169 | } | ||
| 168 | 170 | ||||
| 171 | + | func (s stoppedSession) same(other stoppedSession) bool { | |||
| 172 | + | return s.name == other.name && | |||
| 173 | + | s.cwd == other.cwd && | |||
| 174 | + | s.createdAt == other.createdAt && | |||
| 175 | + | s.lastUsedSeq == other.lastUsedSeq && | |||
| 176 | + | s.purging == other.purging && | |||
| 177 | + | slices.Equal(s.tabNames, other.tabNames) | |||
| 178 | + | } | |||
| 179 | + | ||||
| 169 | type Option func(*Daemon) | 180 | type Option func(*Daemon) | ||
| 170 | 181 | ||||
| 171 | // WithShell overrides the command (and its args) each session spawns. The | 182 | // WithShell overrides the command (and its args) each session spawns. The | ||
| @@ -320,7 +331,7 @@ func New(ptys ports.PTYFactory, clock ports.Clock, log *slog.Logger, opts ...Opt | |||||
| 320 | } else { | 331 | } else { | ||
| 321 | var maxSeq uint64 | 332 | var maxSeq uint64 | ||
| 322 | for _, r := range records { | 333 | for _, r := range records { | ||
| 323 | - | d.stopped[r.Name] = stoppedSession{name: r.Name, cwd: r.Cwd, createdAt: r.CreatedAt, lastUsedSeq: r.LastUsedSeq} | 334 | + | d.stopped[r.Name] = stoppedSession{name: r.Name, cwd: r.Cwd, createdAt: r.CreatedAt, lastUsedSeq: r.LastUsedSeq, tabNames: r.TabNames} |
| 324 | if r.LastUsedSeq > maxSeq { | 335 | if r.LastUsedSeq > maxSeq { | ||
| 325 | maxSeq = r.LastUsedSeq | 336 | maxSeq = r.LastUsedSeq | ||
| 326 | } | 337 | } | ||
| @@ -537,7 +548,7 @@ func (d *Daemon) handleKill(tr ports.Transport, f ports.Frame) { | |||||
| 537 | return | 548 | return | ||
| 538 | } | 549 | } | ||
| 539 | d.mu.Lock() | 550 | d.mu.Lock() | ||
| 540 | - | if cur, ok := d.stopped[k.Name]; ok && cur == stopped { | 551 | + | if cur, ok := d.stopped[k.Name]; ok && cur.same(stopped) { |
| 541 | delete(d.stopped, k.Name) | 552 | delete(d.stopped, k.Name) | ||
| 542 | } | 553 | } | ||
| 543 | d.mu.Unlock() | 554 | d.mu.Unlock() | ||
| @@ -647,7 +658,7 @@ func (d *Daemon) route(h ports.Hello, tr ports.Transport) (*session, *attachedCl | |||||
| 647 | } | 658 | } | ||
| 648 | cwd := d.dirOrHome(stopped.cwd) | 659 | cwd := d.dirOrHome(stopped.cwd) | ||
| 649 | var err error | 660 | var err error | ||
| 650 | - | sess, err = d.createSessionLocked(h.Name, false, cwd, sz, term) | 661 | + | sess, err = d.createSessionLocked(h.Name, false, cwd, sz, term, stopped.tabNames) |
| 651 | if err != nil { | 662 | if err != nil { | ||
| 652 | d.mu.Unlock() | 663 | d.mu.Unlock() | ||
| 653 | return nil, nil, err | 664 | return nil, nil, err | ||
| @@ -715,7 +726,7 @@ func (d *Daemon) route(h ports.Hello, tr ports.Transport) (*session, *attachedCl | |||||
| 715 | } | 726 | } | ||
| 716 | cwd := d.dirOrHome(stopped.cwd) | 727 | cwd := d.dirOrHome(stopped.cwd) | ||
| 717 | var err error | 728 | var err error | ||
| 718 | - | sess, err = d.createSessionLocked(h.Name, false, cwd, sz, term) | 729 | + | sess, err = d.createSessionLocked(h.Name, false, cwd, sz, term, stopped.tabNames) |
| 719 | if err != nil { | 730 | if err != nil { | ||
| 720 | d.mu.Unlock() | 731 | d.mu.Unlock() | ||
| 721 | return nil, nil, err | 732 | return nil, nil, err | ||
M internal/usecase/daemon/input_test.go +2 -2
| @@ -654,7 +654,7 @@ func TestRNTOpensPromptAndRenamesActiveTab(t *testing.T) { | |||
| 654 | 654 | awaitFrame(t, sends, ports.MsgOutput) | |
| 655 | 655 | d.handleInput(sess, ac, []byte("RNT\r")) | |
| 656 | 656 | out := awaitFrame(t, sends, ports.MsgOutput) | |
| 657 | - | require.True(t, ac.promptActive()) | |
| 657 | + | require.True(t, ac.overlays.promptActive()) | |
| 658 | 658 | msg, err := ports.UnmarshalOutput(out.Payload) | |
| 659 | 659 | require.NoError(t, err) | |
| 660 | 660 | require.Contains(t, string(msg.Data), "> 2") | |
| @@ -662,7 +662,7 @@ func TestRNTOpensPromptAndRenamesActiveTab(t *testing.T) { | |||
| 662 | 662 | d.handleInput(sess, ac, []byte("\x7flogs\r")) | |
| 663 | 663 | awaitFrame(t, sends, ports.MsgOutput) | |
| 664 | 664 | ||
| 665 | - | require.False(t, ac.promptActive()) | |
| 665 | + | require.False(t, ac.overlays.promptActive()) | |
| 666 | 666 | require.Equal(t, "logs", sess.tabs[1].name) | |
| 667 | 667 | require.Empty(t, sess.tabs[0].name) | |
| 668 | 668 | } | |
| @@ -654,7 +654,7 @@ func TestRNTOpensPromptAndRenamesActiveTab(t *testing.T) { | |||||
| 654 | awaitFrame(t, sends, ports.MsgOutput) | 654 | awaitFrame(t, sends, ports.MsgOutput) | ||
| 655 | d.handleInput(sess, ac, []byte("RNT\r")) | 655 | d.handleInput(sess, ac, []byte("RNT\r")) | ||
| 656 | out := awaitFrame(t, sends, ports.MsgOutput) | 656 | out := awaitFrame(t, sends, ports.MsgOutput) | ||
| 657 | - | require.True(t, ac.promptActive()) | 657 | + | require.True(t, ac.overlays.promptActive()) |
| 658 | msg, err := ports.UnmarshalOutput(out.Payload) | 658 | msg, err := ports.UnmarshalOutput(out.Payload) | ||
| 659 | require.NoError(t, err) | 659 | require.NoError(t, err) | ||
| 660 | require.Contains(t, string(msg.Data), "> 2") | 660 | require.Contains(t, string(msg.Data), "> 2") | ||
| @@ -662,7 +662,7 @@ func TestRNTOpensPromptAndRenamesActiveTab(t *testing.T) { | |||||
| 662 | d.handleInput(sess, ac, []byte("\x7flogs\r")) | 662 | d.handleInput(sess, ac, []byte("\x7flogs\r")) | ||
| 663 | awaitFrame(t, sends, ports.MsgOutput) | 663 | awaitFrame(t, sends, ports.MsgOutput) | ||
| 664 | 664 | ||||
| 665 | - | require.False(t, ac.promptActive()) | 665 | + | require.False(t, ac.overlays.promptActive()) |
| 666 | require.Equal(t, "logs", sess.tabs[1].name) | 666 | require.Equal(t, "logs", sess.tabs[1].name) | ||
| 667 | require.Empty(t, sess.tabs[0].name) | 667 | require.Empty(t, sess.tabs[0].name) | ||
| 668 | } | 668 | } | ||
M internal/usecase/daemon/picker.go +1 -1
| @@ -424,7 +424,7 @@ func (d *Daemon) resumeStoppedAndSwitch(from *session, ac *attachedClient, targe | |||
| 424 | 424 | } | |
| 425 | 425 | term := from.terminal | |
| 426 | 426 | cwd := d.dirOrHome(stopped.cwd) | |
| 427 | - | targetSess, err := d.createSessionLocked(target.Name, false, cwd, ac.size, term) | |
| 427 | + | targetSess, err := d.createSessionLocked(target.Name, false, cwd, ac.size, term, stopped.tabNames) | |
| 428 | 428 | if err != nil { | |
| 429 | 429 | from.mu.Unlock() | |
| 430 | 430 | d.mu.Unlock() | |
| @@ -424,7 +424,7 @@ func (d *Daemon) resumeStoppedAndSwitch(from *session, ac *attachedClient, targe | |||||
| 424 | } | 424 | } | ||
| 425 | term := from.terminal | 425 | term := from.terminal | ||
| 426 | cwd := d.dirOrHome(stopped.cwd) | 426 | cwd := d.dirOrHome(stopped.cwd) | ||
| 427 | - | targetSess, err := d.createSessionLocked(target.Name, false, cwd, ac.size, term) | 427 | + | targetSess, err := d.createSessionLocked(target.Name, false, cwd, ac.size, term, stopped.tabNames) |
| 428 | if err != nil { | 428 | if err != nil { | ||
| 429 | from.mu.Unlock() | 429 | from.mu.Unlock() | ||
| 430 | d.mu.Unlock() | 430 | d.mu.Unlock() | ||
M internal/usecase/daemon/session.go +66 -17
| @@ -29,6 +29,7 @@ import ( | |||
| 29 | 29 | "errors" | |
| 30 | 30 | "fmt" | |
| 31 | 31 | "os" | |
| 32 | + | "slices" | |
| 32 | 33 | "strconv" | |
| 33 | 34 | "strings" | |
| 34 | 35 | "sync" | |
| @@ -76,6 +77,7 @@ type tab struct { | |||
| 76 | 77 | mu sync.Mutex // guards tree, panes, nextPaneID, size, previewClient, and pane map membership | |
| 77 | 78 | ||
| 78 | 79 | stableID string | |
| 80 | + | name string | |
| 79 | 81 | tree *layout.Tree | |
| 80 | 82 | panes map[layout.PaneID]*pane | |
| 81 | 83 | nextPaneID int | |
| @@ -127,25 +129,40 @@ func (d *Daemon) touchMRU(sess *session) { | |||
| 127 | 129 | } | |
| 128 | 130 | } | |
| 129 | 131 | ||
| 130 | - | func (d *Daemon) createSessionLocked(name string, ephemeral bool, cwd string, sz domain.Size, term terminalEnv) (*session, error) { | |
| 132 | + | func (d *Daemon) createSessionLocked(name string, ephemeral bool, cwd string, sz domain.Size, term terminalEnv, restoredTabNames ...[]string) (*session, error) { | |
| 131 | 133 | tbSize := tabSize(sz) | |
| 132 | - | tabStableID, paneStableID, err := d.newTabPaneStableIDs() | |
| 133 | - | if err != nil { | |
| 134 | - | return nil, err | |
| 135 | - | } | |
| 136 | - | pty, err := d.ptys.Open(d.shell, d.shellArgs, d.childEnv(name, tabStableID, paneStableID, term), cwd, tbSize) | |
| 137 | - | if err != nil { | |
| 138 | - | d.log.Warn("pty spawn failed", "err", err, "session", name, "kind", "session") | |
| 139 | - | return nil, fmt.Errorf("daemon: spawning session %q: %w", name, err) | |
| 134 | + | var names []string | |
| 135 | + | if len(restoredTabNames) > 0 { | |
| 136 | + | names = append([]string(nil), restoredTabNames[0]...) | |
| 137 | + | } | |
| 138 | + | tabs := make([]*tab, 0, max(1, len(names))) | |
| 139 | + | for i := range max(1, len(names)) { | |
| 140 | + | tabStableID, paneStableID, err := d.newTabPaneStableIDs() | |
| 141 | + | if err != nil { | |
| 142 | + | closeTabs(tabs) | |
| 143 | + | return nil, err | |
| 144 | + | } | |
| 145 | + | pty, err := d.ptys.Open(d.shell, d.shellArgs, d.childEnv(name, tabStableID, paneStableID, term), cwd, tbSize) | |
| 146 | + | if err != nil { | |
| 147 | + | closeTabs(tabs) | |
| 148 | + | d.log.Warn("pty spawn failed", "err", err, "session", name, "kind", "session") | |
| 149 | + | return nil, fmt.Errorf("daemon: spawning session %q: %w", name, err) | |
| 150 | + | } | |
| 151 | + | tb := newTabWithStableID(tabStableID, paneStableID, pty, tbSize) | |
| 152 | + | if i < len(names) { | |
| 153 | + | tb.name = names[i] | |
| 154 | + | } | |
| 155 | + | tabs = append(tabs, tb) | |
| 140 | 156 | } | |
| 141 | 157 | ||
| 142 | 158 | id := domain.SessionID(fmt.Sprintf("sess-%d", d.nextID)) | |
| 143 | 159 | d.nextID++ | |
| 144 | 160 | createdAt := time.Now().UnixNano() | |
| 145 | 161 | ||
| 146 | - | tb := newTabWithStableID(tabStableID, paneStableID, pty, tbSize) | |
| 147 | 162 | sctx, cancel := context.WithCancel(d.serveCtx) | |
| 148 | - | tb.ctx, tb.cancel = context.WithCancel(sctx) | |
| 163 | + | for _, tb := range tabs { | |
| 164 | + | tb.ctx, tb.cancel = context.WithCancel(sctx) | |
| 165 | + | } | |
| 149 | 166 | lastUsedSeq := uint64(0) | |
| 150 | 167 | if !ephemeral { | |
| 151 | 168 | if stopped, ok := d.stopped[name]; ok { | |
| @@ -168,8 +185,8 @@ func (d *Daemon) createSessionLocked(name string, ephemeral bool, cwd string, sz | |||
| 168 | 185 | } | |
| 169 | 186 | sess.snapEligible.Store(!ephemeral && name != "") | |
| 170 | 187 | if !ephemeral { | |
| 171 | - | if err := d.persist.Save(persist.Record{Name: name, Cwd: cwd, CreatedAt: createdAt, UpdatedAt: createdAt, LastUsedSeq: lastUsedSeq}); err != nil { | |
| 172 | - | _ = pty.Close() | |
| 188 | + | if err := d.persist.Save(persist.Record{Name: name, Cwd: cwd, CreatedAt: createdAt, UpdatedAt: createdAt, LastUsedSeq: lastUsedSeq, TabNames: names}); err != nil { | |
| 189 | + | closeTabs(tabs) | |
| 173 | 190 | cancel() | |
| 174 | 191 | return nil, err | |
| 175 | 192 | } | |
| @@ -180,11 +197,21 @@ func (d *Daemon) createSessionLocked(name string, ephemeral bool, cwd string, sz | |||
| 180 | 197 | d.touchMRU(sess) | |
| 181 | 198 | } | |
| 182 | 199 | d.log.Info("session created", "session", name, "id", id, "ephemeral", ephemeral) | |
| 183 | - | d.log.Info("tab created", "session", name, "tab", 0) | |
| 184 | - | d.startTabGoroutines(sess, tb) | |
| 200 | + | for i, tb := range tabs { | |
| 201 | + | d.log.Info("tab created", "session", name, "tab", i) | |
| 202 | + | d.startTabGoroutines(sess, tb) | |
| 203 | + | } | |
| 185 | 204 | return sess, nil | |
| 186 | 205 | } | |
| 187 | 206 | ||
| 207 | + | func closeTabs(tabs []*tab) { | |
| 208 | + | for _, tb := range tabs { | |
| 209 | + | for _, p := range tb.panes { | |
| 210 | + | _ = p.pty.Close() | |
| 211 | + | } | |
| 212 | + | } | |
| 213 | + | } | |
| 214 | + | ||
| 188 | 215 | func (d *Daemon) createSessionAndSwitch(from *session, ac *attachedClient, name string) error { | |
| 189 | 216 | if name == "" { | |
| 190 | 217 | return errors.New("name required") | |
| @@ -284,6 +311,21 @@ func (d *Daemon) createTab(sess *session, sz domain.Size) error { | |||
| 284 | 311 | sess.tabs = append(sess.tabs, tb) | |
| 285 | 312 | sess.active = len(sess.tabs) - 1 | |
| 286 | 313 | tabIndex := sess.active | |
| 314 | + | record := sess.persistRecordLocked(time.Now().UnixNano()) | |
| 315 | + | ephemeral := sess.ephemeral | |
| 316 | + | if !ephemeral { | |
| 317 | + | if err := d.persist.Save(record); err != nil { | |
| 318 | + | sess.tabs = sess.tabs[:len(sess.tabs)-1] | |
| 319 | + | sess.active = oldActive | |
| 320 | + | if tb.cancel != nil { | |
| 321 | + | tb.cancel() | |
| 322 | + | } | |
| 323 | + | sess.mu.Unlock() | |
| 324 | + | d.mu.Unlock() | |
| 325 | + | _ = pty.Close() | |
| 326 | + | return err | |
| 327 | + | } | |
| 328 | + | } | |
| 287 | 329 | sess.mu.Unlock() | |
| 288 | 330 | d.log.Info("tab created", "session", name, "tab", tabIndex) | |
| 289 | 331 | d.startTabGoroutines(sess, tb) | |
| @@ -522,7 +564,7 @@ func (s *session) persistRecordLocked(updatedAt int64) persist.Record { | |||
| 522 | 564 | } else { | |
| 523 | 565 | tabNames = tabNames[:lastCustom+1] | |
| 524 | 566 | } | |
| 525 | - | return persist.Record{Name: s.name, Cwd: s.cwd, CreatedAt: createdAt, UpdatedAt: updatedAt, TabNames: tabNames} | |
| 567 | + | return persist.Record{Name: s.name, Cwd: s.cwd, CreatedAt: createdAt, UpdatedAt: updatedAt, LastUsedSeq: s.mruAt.Load(), TabNames: tabNames} | |
| 526 | 568 | } | |
| 527 | 569 | ||
| 528 | 570 | func (d *Daemon) closeTab(sess *session, tb *tab, repaint bool) { | |
| @@ -560,6 +602,13 @@ func (d *Daemon) closeTab(sess *session, tb *tab, repaint bool) { | |||
| 560 | 602 | } | |
| 561 | 603 | ac := sess.client | |
| 562 | 604 | name := sess.name | |
| 605 | + | record := sess.persistRecordLocked(time.Now().UnixNano()) | |
| 606 | + | ephemeral := sess.ephemeral | |
| 607 | + | if !ephemeral { | |
| 608 | + | if err := d.persist.Save(record); err != nil { | |
| 609 | + | d.log.Warn("persisting closed tab failed", "err", err, "session", name) | |
| 610 | + | } | |
| 611 | + | } | |
| 563 | 612 | sess.mu.Unlock() | |
| 564 | 613 | d.log.Info("tab closed", "session", name) | |
| 565 | 614 | markSnapshotDirty(sess) | |
| @@ -618,7 +667,7 @@ func (d *Daemon) killSession(sess *session, reason uint8, purge bool) error { | |||
| 618 | 667 | ephemeral := sess.ephemeral | |
| 619 | 668 | sess.mu.Unlock() | |
| 620 | 669 | if !ephemeral { | |
| 621 | - | stopped := stoppedSession{name: stoppedName, cwd: stoppedCwd, createdAt: createdAt, lastUsedSeq: sess.mruAt.Load(), purging: purge} | |
| 670 | + | stopped := stoppedSession{name: stoppedName, cwd: stoppedCwd, createdAt: createdAt, lastUsedSeq: sess.mruAt.Load(), tabNames: tabNames, purging: purge} | |
| 622 | 671 | d.stopped[stoppedName] = stopped | |
| 623 | 672 | } | |
| 624 | 673 | empty := len(d.sessions) == 0 | |
| @@ -29,6 +29,7 @@ import ( | |||||
| 29 | "errors" | 29 | "errors" | ||
| 30 | "fmt" | 30 | "fmt" | ||
| 31 | "os" | 31 | "os" | ||
| 32 | + | "slices" | |||
| 32 | "strconv" | 33 | "strconv" | ||
| 33 | "strings" | 34 | "strings" | ||
| 34 | "sync" | 35 | "sync" | ||
| @@ -76,6 +77,7 @@ type tab struct { | |||||
| 76 | mu sync.Mutex // guards tree, panes, nextPaneID, size, previewClient, and pane map membership | 77 | mu sync.Mutex // guards tree, panes, nextPaneID, size, previewClient, and pane map membership | ||
| 77 | 78 | ||||
| 78 | stableID string | 79 | stableID string | ||
| 80 | + | name string | |||
| 79 | tree *layout.Tree | 81 | tree *layout.Tree | ||
| 80 | panes map[layout.PaneID]*pane | 82 | panes map[layout.PaneID]*pane | ||
| 81 | nextPaneID int | 83 | nextPaneID int | ||
| @@ -127,25 +129,40 @@ func (d *Daemon) touchMRU(sess *session) { | |||||
| 127 | } | 129 | } | ||
| 128 | } | 130 | } | ||
| 129 | 131 | ||||
| 130 | - | func (d *Daemon) createSessionLocked(name string, ephemeral bool, cwd string, sz domain.Size, term terminalEnv) (*session, error) { | 132 | + | func (d *Daemon) createSessionLocked(name string, ephemeral bool, cwd string, sz domain.Size, term terminalEnv, restoredTabNames ...[]string) (*session, error) { |
| 131 | tbSize := tabSize(sz) | 133 | tbSize := tabSize(sz) | ||
| 132 | - | tabStableID, paneStableID, err := d.newTabPaneStableIDs() | 134 | + | var names []string |
| 133 | - | if err != nil { | 135 | + | if len(restoredTabNames) > 0 { |
| 134 | - | return nil, err | 136 | + | names = append([]string(nil), restoredTabNames[0]...) |
| 135 | - | } | 137 | + | } |
| 136 | - | pty, err := d.ptys.Open(d.shell, d.shellArgs, d.childEnv(name, tabStableID, paneStableID, term), cwd, tbSize) | 138 | + | tabs := make([]*tab, 0, max(1, len(names))) |
| 137 | - | if err != nil { | 139 | + | for i := range max(1, len(names)) { |
| 138 | - | d.log.Warn("pty spawn failed", "err", err, "session", name, "kind", "session") | 140 | + | tabStableID, paneStableID, err := d.newTabPaneStableIDs() |
| 139 | - | return nil, fmt.Errorf("daemon: spawning session %q: %w", name, err) | 141 | + | if err != nil { |
| 142 | + | closeTabs(tabs) | |||
| 143 | + | return nil, err | |||
| 144 | + | } | |||
| 145 | + | pty, err := d.ptys.Open(d.shell, d.shellArgs, d.childEnv(name, tabStableID, paneStableID, term), cwd, tbSize) | |||
| 146 | + | if err != nil { | |||
| 147 | + | closeTabs(tabs) | |||
| 148 | + | d.log.Warn("pty spawn failed", "err", err, "session", name, "kind", "session") | |||
| 149 | + | return nil, fmt.Errorf("daemon: spawning session %q: %w", name, err) | |||
| 150 | + | } | |||
| 151 | + | tb := newTabWithStableID(tabStableID, paneStableID, pty, tbSize) | |||
| 152 | + | if i < len(names) { | |||
| 153 | + | tb.name = names[i] | |||
| 154 | + | } | |||
| 155 | + | tabs = append(tabs, tb) | |||
| 140 | } | 156 | } | ||
| 141 | 157 | ||||
| 142 | id := domain.SessionID(fmt.Sprintf("sess-%d", d.nextID)) | 158 | id := domain.SessionID(fmt.Sprintf("sess-%d", d.nextID)) | ||
| 143 | d.nextID++ | 159 | d.nextID++ | ||
| 144 | createdAt := time.Now().UnixNano() | 160 | createdAt := time.Now().UnixNano() | ||
| 145 | 161 | ||||
| 146 | - | tb := newTabWithStableID(tabStableID, paneStableID, pty, tbSize) | |||
| 147 | sctx, cancel := context.WithCancel(d.serveCtx) | 162 | sctx, cancel := context.WithCancel(d.serveCtx) | ||
| 148 | - | tb.ctx, tb.cancel = context.WithCancel(sctx) | 163 | + | for _, tb := range tabs { |
| 164 | + | tb.ctx, tb.cancel = context.WithCancel(sctx) | |||
| 165 | + | } | |||
| 149 | lastUsedSeq := uint64(0) | 166 | lastUsedSeq := uint64(0) | ||
| 150 | if !ephemeral { | 167 | if !ephemeral { | ||
| 151 | if stopped, ok := d.stopped[name]; ok { | 168 | if stopped, ok := d.stopped[name]; ok { | ||
| @@ -168,8 +185,8 @@ func (d *Daemon) createSessionLocked(name string, ephemeral bool, cwd string, sz | |||||
| 168 | } | 185 | } | ||
| 169 | sess.snapEligible.Store(!ephemeral && name != "") | 186 | sess.snapEligible.Store(!ephemeral && name != "") | ||
| 170 | if !ephemeral { | 187 | if !ephemeral { | ||
| 171 | - | if err := d.persist.Save(persist.Record{Name: name, Cwd: cwd, CreatedAt: createdAt, UpdatedAt: createdAt, LastUsedSeq: lastUsedSeq}); err != nil { | 188 | + | if err := d.persist.Save(persist.Record{Name: name, Cwd: cwd, CreatedAt: createdAt, UpdatedAt: createdAt, LastUsedSeq: lastUsedSeq, TabNames: names}); err != nil { |
| 172 | - | _ = pty.Close() | 189 | + | closeTabs(tabs) |
| 173 | cancel() | 190 | cancel() | ||
| 174 | return nil, err | 191 | return nil, err | ||
| 175 | } | 192 | } | ||
| @@ -180,11 +197,21 @@ func (d *Daemon) createSessionLocked(name string, ephemeral bool, cwd string, sz | |||||
| 180 | d.touchMRU(sess) | 197 | d.touchMRU(sess) | ||
| 181 | } | 198 | } | ||
| 182 | d.log.Info("session created", "session", name, "id", id, "ephemeral", ephemeral) | 199 | d.log.Info("session created", "session", name, "id", id, "ephemeral", ephemeral) | ||
| 183 | - | d.log.Info("tab created", "session", name, "tab", 0) | 200 | + | for i, tb := range tabs { |
| 184 | - | d.startTabGoroutines(sess, tb) | 201 | + | d.log.Info("tab created", "session", name, "tab", i) |
| 202 | + | d.startTabGoroutines(sess, tb) | |||
| 203 | + | } | |||
| 185 | return sess, nil | 204 | return sess, nil | ||
| 186 | } | 205 | } | ||
| 187 | 206 | ||||
| 207 | + | func closeTabs(tabs []*tab) { | |||
| 208 | + | for _, tb := range tabs { | |||
| 209 | + | for _, p := range tb.panes { | |||
| 210 | + | _ = p.pty.Close() | |||
| 211 | + | } | |||
| 212 | + | } | |||
| 213 | + | } | |||
| 214 | + | ||||
| 188 | func (d *Daemon) createSessionAndSwitch(from *session, ac *attachedClient, name string) error { | 215 | func (d *Daemon) createSessionAndSwitch(from *session, ac *attachedClient, name string) error { | ||
| 189 | if name == "" { | 216 | if name == "" { | ||
| 190 | return errors.New("name required") | 217 | return errors.New("name required") | ||
| @@ -284,6 +311,21 @@ func (d *Daemon) createTab(sess *session, sz domain.Size) error { | |||||
| 284 | sess.tabs = append(sess.tabs, tb) | 311 | sess.tabs = append(sess.tabs, tb) | ||
| 285 | sess.active = len(sess.tabs) - 1 | 312 | sess.active = len(sess.tabs) - 1 | ||
| 286 | tabIndex := sess.active | 313 | tabIndex := sess.active | ||
| 314 | + | record := sess.persistRecordLocked(time.Now().UnixNano()) | |||
| 315 | + | ephemeral := sess.ephemeral | |||
| 316 | + | if !ephemeral { | |||
| 317 | + | if err := d.persist.Save(record); err != nil { | |||
| 318 | + | sess.tabs = sess.tabs[:len(sess.tabs)-1] | |||
| 319 | + | sess.active = oldActive | |||
| 320 | + | if tb.cancel != nil { | |||
| 321 | + | tb.cancel() | |||
| 322 | + | } | |||
| 323 | + | sess.mu.Unlock() | |||
| 324 | + | d.mu.Unlock() | |||
| 325 | + | _ = pty.Close() | |||
| 326 | + | return err | |||
| 327 | + | } | |||
| 328 | + | } | |||
| 287 | sess.mu.Unlock() | 329 | sess.mu.Unlock() | ||
| 288 | d.log.Info("tab created", "session", name, "tab", tabIndex) | 330 | d.log.Info("tab created", "session", name, "tab", tabIndex) | ||
| 289 | d.startTabGoroutines(sess, tb) | 331 | d.startTabGoroutines(sess, tb) | ||
| @@ -522,7 +564,7 @@ func (s *session) persistRecordLocked(updatedAt int64) persist.Record { | |||||
| 522 | } else { | 564 | } else { | ||
| 523 | tabNames = tabNames[:lastCustom+1] | 565 | tabNames = tabNames[:lastCustom+1] | ||
| 524 | } | 566 | } | ||
| 525 | - | return persist.Record{Name: s.name, Cwd: s.cwd, CreatedAt: createdAt, UpdatedAt: updatedAt, TabNames: tabNames} | 567 | + | return persist.Record{Name: s.name, Cwd: s.cwd, CreatedAt: createdAt, UpdatedAt: updatedAt, LastUsedSeq: s.mruAt.Load(), TabNames: tabNames} |
| 526 | } | 568 | } | ||
| 527 | 569 | ||||
| 528 | func (d *Daemon) closeTab(sess *session, tb *tab, repaint bool) { | 570 | func (d *Daemon) closeTab(sess *session, tb *tab, repaint bool) { | ||
| @@ -560,6 +602,13 @@ func (d *Daemon) closeTab(sess *session, tb *tab, repaint bool) { | |||||
| 560 | } | 602 | } | ||
| 561 | ac := sess.client | 603 | ac := sess.client | ||
| 562 | name := sess.name | 604 | name := sess.name | ||
| 605 | + | record := sess.persistRecordLocked(time.Now().UnixNano()) | |||
| 606 | + | ephemeral := sess.ephemeral | |||
| 607 | + | if !ephemeral { | |||
| 608 | + | if err := d.persist.Save(record); err != nil { | |||
| 609 | + | d.log.Warn("persisting closed tab failed", "err", err, "session", name) | |||
| 610 | + | } | |||
| 611 | + | } | |||
| 563 | sess.mu.Unlock() | 612 | sess.mu.Unlock() | ||
| 564 | d.log.Info("tab closed", "session", name) | 613 | d.log.Info("tab closed", "session", name) | ||
| 565 | markSnapshotDirty(sess) | 614 | markSnapshotDirty(sess) | ||
| @@ -618,7 +667,7 @@ func (d *Daemon) killSession(sess *session, reason uint8, purge bool) error { | |||||
| 618 | ephemeral := sess.ephemeral | 667 | ephemeral := sess.ephemeral | ||
| 619 | sess.mu.Unlock() | 668 | sess.mu.Unlock() | ||
| 620 | if !ephemeral { | 669 | if !ephemeral { | ||
| 621 | - | stopped := stoppedSession{name: stoppedName, cwd: stoppedCwd, createdAt: createdAt, lastUsedSeq: sess.mruAt.Load(), purging: purge} | 670 | + | stopped := stoppedSession{name: stoppedName, cwd: stoppedCwd, createdAt: createdAt, lastUsedSeq: sess.mruAt.Load(), tabNames: tabNames, purging: purge} |
| 622 | d.stopped[stoppedName] = stopped | 671 | d.stopped[stoppedName] = stopped | ||
| 623 | } | 672 | } | ||
| 624 | empty := len(d.sessions) == 0 | 673 | empty := len(d.sessions) == 0 | ||
M internal/usecase/daemon/session_test.go +3 -3
| @@ -825,7 +825,7 @@ func TestRenameTabPersistsForNamedSession(t *testing.T) { | |||
| 825 | 825 | d := newTestDaemon(t, newFactory(t, p), stubClock{}) | |
| 826 | 826 | WithStore(store)(d) | |
| 827 | 827 | ||
| 828 | - | sess, err := d.createSessionLocked("work", false, "/tmp/work", sz, nil) | |
| 828 | + | sess, err := d.createSessionLocked("work", false, "/tmp/work", sz, terminalEnv{}) | |
| 829 | 829 | require.NoError(t, err) | |
| 830 | 830 | require.NoError(t, d.renameTab(sess, sess.tabs[0], "shell")) | |
| 831 | 831 | ||
| @@ -862,7 +862,7 @@ func TestTabNamePersistenceTracksTabIndexShifts(t *testing.T) { | |||
| 862 | 862 | WithStore(store)(d) | |
| 863 | 863 | sz := domain.Size{Cols: 80, Rows: 24} | |
| 864 | 864 | ||
| 865 | - | sess, err := d.createSessionLocked("work", false, "/tmp/work", sz, nil) | |
| 865 | + | sess, err := d.createSessionLocked("work", false, "/tmp/work", sz, terminalEnv{}) | |
| 866 | 866 | require.NoError(t, err) | |
| 867 | 867 | require.NoError(t, d.createTab(sess, sz)) | |
| 868 | 868 | require.NoError(t, d.createTab(sess, sz)) | |
| @@ -887,7 +887,7 @@ func TestRenameTabDoesNotPersistForEphemeralSession(t *testing.T) { | |||
| 887 | 887 | d := newTestDaemon(t, newFactory(t, p), stubClock{}) | |
| 888 | 888 | WithStore(store)(d) | |
| 889 | 889 | ||
| 890 | - | sess, err := d.createSessionLocked("0", true, "/tmp/work", sz, nil) | |
| 890 | + | sess, err := d.createSessionLocked("0", true, "/tmp/work", sz, terminalEnv{}) | |
| 891 | 891 | require.NoError(t, err) | |
| 892 | 892 | require.NoError(t, d.renameTab(sess, sess.tabs[0], "shell")) | |
| 893 | 893 | require.Equal(t, "shell", sess.tabs[0].name) | |
| @@ -825,7 +825,7 @@ func TestRenameTabPersistsForNamedSession(t *testing.T) { | |||||
| 825 | d := newTestDaemon(t, newFactory(t, p), stubClock{}) | 825 | d := newTestDaemon(t, newFactory(t, p), stubClock{}) | ||
| 826 | WithStore(store)(d) | 826 | WithStore(store)(d) | ||
| 827 | 827 | ||||
| 828 | - | sess, err := d.createSessionLocked("work", false, "/tmp/work", sz, nil) | 828 | + | sess, err := d.createSessionLocked("work", false, "/tmp/work", sz, terminalEnv{}) |
| 829 | require.NoError(t, err) | 829 | require.NoError(t, err) | ||
| 830 | require.NoError(t, d.renameTab(sess, sess.tabs[0], "shell")) | 830 | require.NoError(t, d.renameTab(sess, sess.tabs[0], "shell")) | ||
| 831 | 831 | ||||
| @@ -862,7 +862,7 @@ func TestTabNamePersistenceTracksTabIndexShifts(t *testing.T) { | |||||
| 862 | WithStore(store)(d) | 862 | WithStore(store)(d) | ||
| 863 | sz := domain.Size{Cols: 80, Rows: 24} | 863 | sz := domain.Size{Cols: 80, Rows: 24} | ||
| 864 | 864 | ||||
| 865 | - | sess, err := d.createSessionLocked("work", false, "/tmp/work", sz, nil) | 865 | + | sess, err := d.createSessionLocked("work", false, "/tmp/work", sz, terminalEnv{}) |
| 866 | require.NoError(t, err) | 866 | require.NoError(t, err) | ||
| 867 | require.NoError(t, d.createTab(sess, sz)) | 867 | require.NoError(t, d.createTab(sess, sz)) | ||
| 868 | require.NoError(t, d.createTab(sess, sz)) | 868 | require.NoError(t, d.createTab(sess, sz)) | ||
| @@ -887,7 +887,7 @@ func TestRenameTabDoesNotPersistForEphemeralSession(t *testing.T) { | |||||
| 887 | d := newTestDaemon(t, newFactory(t, p), stubClock{}) | 887 | d := newTestDaemon(t, newFactory(t, p), stubClock{}) | ||
| 888 | WithStore(store)(d) | 888 | WithStore(store)(d) | ||
| 889 | 889 | ||||
| 890 | - | sess, err := d.createSessionLocked("0", true, "/tmp/work", sz, nil) | 890 | + | sess, err := d.createSessionLocked("0", true, "/tmp/work", sz, terminalEnv{}) |
| 891 | require.NoError(t, err) | 891 | require.NoError(t, err) | ||
| 892 | require.NoError(t, d.renameTab(sess, sess.tabs[0], "shell")) | 892 | require.NoError(t, d.renameTab(sess, sess.tabs[0], "shell")) | ||
| 893 | require.Equal(t, "shell", sess.tabs[0].name) | 893 | require.Equal(t, "shell", sess.tabs[0].name) | ||
M internal/usecase/daemon/status.go +8 -1
| @@ -204,7 +204,7 @@ func (s *session) statusSegments() statusSnapshot { | |||
| 204 | 204 | } | |
| 205 | 205 | snap := statusSnapshot{session: name, tabs: make([]statusTab, len(s.tabs))} | |
| 206 | 206 | for i, tb := range s.tabs { | |
| 207 | - | name := strconv.Itoa(i + 1) | |
| 207 | + | name := tabDisplayName(tb, i) | |
| 208 | 208 | active := i == s.active | |
| 209 | 209 | attention := tb.attention && (!active || tb.attentionVisiblePaint) | |
| 210 | 210 | snap.tabs[i] = statusTab{name: name, active: active, attention: attention} | |
| @@ -212,6 +212,13 @@ func (s *session) statusSegments() statusSnapshot { | |||
| 212 | 212 | return snap | |
| 213 | 213 | } | |
| 214 | 214 | ||
| 215 | + | func tabDisplayName(tb *tab, index int) string { | |
| 216 | + | if tb.name != "" { | |
| 217 | + | return tb.name | |
| 218 | + | } | |
| 219 | + | return strconv.Itoa(index + 1) | |
| 220 | + | } | |
| 221 | + | ||
| 215 | 222 | func (d *Daemon) barStateForClient(cur *session, ac *attachedClient, copyFeedback string) barState { | |
| 216 | 223 | state := d.barStateFor(cur, copyFeedback) | |
| 217 | 224 | state.history = d.historyNavBarState(ac) | |
| @@ -204,7 +204,7 @@ func (s *session) statusSegments() statusSnapshot { | |||||
| 204 | } | 204 | } | ||
| 205 | snap := statusSnapshot{session: name, tabs: make([]statusTab, len(s.tabs))} | 205 | snap := statusSnapshot{session: name, tabs: make([]statusTab, len(s.tabs))} | ||
| 206 | for i, tb := range s.tabs { | 206 | for i, tb := range s.tabs { | ||
| 207 | - | name := strconv.Itoa(i + 1) | 207 | + | name := tabDisplayName(tb, i) |
| 208 | active := i == s.active | 208 | active := i == s.active | ||
| 209 | attention := tb.attention && (!active || tb.attentionVisiblePaint) | 209 | attention := tb.attention && (!active || tb.attentionVisiblePaint) | ||
| 210 | snap.tabs[i] = statusTab{name: name, active: active, attention: attention} | 210 | snap.tabs[i] = statusTab{name: name, active: active, attention: attention} | ||
| @@ -212,6 +212,13 @@ func (s *session) statusSegments() statusSnapshot { | |||||
| 212 | return snap | 212 | return snap | ||
| 213 | } | 213 | } | ||
| 214 | 214 | ||||
| 215 | + | func tabDisplayName(tb *tab, index int) string { | |||
| 216 | + | if tb.name != "" { | |||
| 217 | + | return tb.name | |||
| 218 | + | } | |||
| 219 | + | return strconv.Itoa(index + 1) | |||
| 220 | + | } | |||
| 221 | + | ||||
| 215 | func (d *Daemon) barStateForClient(cur *session, ac *attachedClient, copyFeedback string) barState { | 222 | func (d *Daemon) barStateForClient(cur *session, ac *attachedClient, copyFeedback string) barState { | ||
| 216 | state := d.barStateFor(cur, copyFeedback) | 223 | state := d.barStateFor(cur, copyFeedback) | ||
| 217 | state.history = d.historyNavBarState(ac) | 224 | state.history = d.historyNavBarState(ac) | ||