fix(palette): use the compact rail for every floating width (#300)
* fix(palette): use the compact rail for every floating width * fix(demo): restore make demo and re-record the demo * docs(demo): slow key beats and show moving a live pane * docs(demo): show the floating pane once * docs(demo): move a live fzf pane and pause on STP * fix(demo): keep demo state in a volume and check session bootstrap
- commit
- 46d259b70ac4c63f6201ba49d5ee0f764bf2b207
- author
- bnema · · 2026-09-29 16:24 UTC
- committer
- GitHub · · 2026-09-29 16:24 UTC
- parent
- 7907dde
- signature
- GPG Unverified
B5690EEEBB952194The key is unknown
10 files · +106 -57
M Makefile +1 -0
| @@ -56,6 +56,7 @@ protocol-check: | |||
| 56 | 56 | test -z "$$(git status --porcelain -- internal/protocol/wire/*.pb.go)" | |
| 57 | 57 | ||
| 58 | 58 | demo: | |
| 59 | + | go build -o build/vev . | |
| 59 | 60 | docker build -f scripts/demo/Dockerfile -t vev-demo . | |
| 60 | 61 | ./scripts/demo/run.sh demo.tape | |
| 61 | 62 | # vhs cannot emit a transparent margin, so cut the window out after the | |
| @@ -56,6 +56,7 @@ protocol-check: | |||||
| 56 | test -z "$$(git status --porcelain -- internal/protocol/wire/*.pb.go)" | 56 | test -z "$$(git status --porcelain -- internal/protocol/wire/*.pb.go)" | ||
| 57 | 57 | ||||
| 58 | demo: | 58 | demo: | ||
| 59 | + | go build -o build/vev . | |||
| 59 | docker build -f scripts/demo/Dockerfile -t vev-demo . | 60 | docker build -f scripts/demo/Dockerfile -t vev-demo . | ||
| 60 | ./scripts/demo/run.sh demo.tape | 61 | ./scripts/demo/run.sh demo.tape | ||
| 61 | # vhs cannot emit a transparent margin, so cut the window out after the | 62 | # vhs cannot emit a transparent margin, so cut the window out after the | ||
M docs/assets/demo.gif +0 -0 binary file: no text diff
binary file: no text diff
M internal/usecase/daemon/palette.go +5 -13
| @@ -16,25 +16,17 @@ import ( | |||
| 16 | 16 | "github.com/bnema/vev/internal/usecase/ui" | |
| 17 | 17 | ) | |
| 18 | 18 | ||
| 19 | - | const ( | |
| 20 | - | paletteRailBreakpoint = 96 | |
| 21 | - | paletteRailWidth = 64 | |
| 22 | - | ) | |
| 23 | - | ||
| 24 | 19 | var ( | |
| 25 | - | paletteModal = ui.Modal{WidthPct: 100, MinWidth: 32, FixedHeight: 11, Title: " Commands ", Anchor: domain.AnchorBottom, Margins: ui.Margins{Top: 1, Right: 1, Bottom: 1, Left: 1}} | |
| 20 | + | // Floating palettes use a compact rail; below ui.ResponsiveDrawerBreakpoint | |
| 21 | + | // the presentation becomes a full-width drawer regardless of this width. | |
| 22 | + | paletteModal = ui.Modal{FixedWidth: 64, FixedHeight: 11, Title: " Commands ", Anchor: domain.AnchorBottomRight, Margins: ui.Margins{Top: 1, Right: 1, Bottom: 1, Left: 1}} | |
| 26 | 23 | errCreateDestinationUnavailable = errors.New("that destination is no longer available") | |
| 27 | 24 | ) | |
| 28 | 25 | ||
| 29 | - | func paletteModalFor(size domain.Size, cfg domain.PaletteConfig) ui.Modal { | |
| 26 | + | func paletteModalFor(cfg domain.PaletteConfig) ui.Modal { | |
| 30 | 27 | modal := paletteModal | |
| 31 | - | if size.Cols >= paletteRailBreakpoint { | |
| 32 | - | modal.FixedWidth = paletteRailWidth | |
| 33 | - | } | |
| 34 | 28 | if cfg.AnchorSet { | |
| 35 | 29 | modal.Anchor = cfg.Anchor | |
| 36 | - | } else if size.Cols >= paletteRailBreakpoint { | |
| 37 | - | modal.Anchor = domain.AnchorBottomRight | |
| 38 | 30 | } | |
| 39 | 31 | return modal | |
| 40 | 32 | } | |
| @@ -1173,7 +1165,7 @@ func (e paletteExec) NavigateRecentRoute(action protocol.RouteNavigationAction) | |||
| 1173 | 1165 | ||
| 1174 | 1166 | func composePaletteClientFrame(model *palette.Model, base renderer.Frame, cfg domain.PaletteConfig, guidance string, styles ...themeui.Styles) (renderer.Frame, []renderer.Damage) { | |
| 1175 | 1167 | styleSet := resolveStyles(styles) | |
| 1176 | - | modal := paletteModalFor(domain.Size{Cols: base.Width, Rows: base.Height}, cfg) | |
| 1168 | + | modal := paletteModalFor(cfg) | |
| 1177 | 1169 | return composeModalClientFrame(base, modal, styleSet, func(size domain.Size) renderer.Frame { | |
| 1178 | 1170 | return model.Render(size, palette.RenderOptions{Styles: palette.RenderStyles{Base: styleSet.PickerBase, Row: styleSet.PickerBase, Selection: styleSet.PickerSelection, Description: styleSet.PickerDescription, SelectionDescription: styleSet.PickerSelectionMuted}, Guidance: guidance}) | |
| 1179 | 1171 | }) | |
| @@ -16,25 +16,17 @@ import ( | |||||
| 16 | "github.com/bnema/vev/internal/usecase/ui" | 16 | "github.com/bnema/vev/internal/usecase/ui" | ||
| 17 | ) | 17 | ) | ||
| 18 | 18 | ||||
| 19 | - | const ( | |||
| 20 | - | paletteRailBreakpoint = 96 | |||
| 21 | - | paletteRailWidth = 64 | |||
| 22 | - | ) | |||
| 23 | - | ||||
| 24 | var ( | 19 | var ( | ||
| 25 | - | paletteModal = ui.Modal{WidthPct: 100, MinWidth: 32, FixedHeight: 11, Title: " Commands ", Anchor: domain.AnchorBottom, Margins: ui.Margins{Top: 1, Right: 1, Bottom: 1, Left: 1}} | 20 | + | // Floating palettes use a compact rail; below ui.ResponsiveDrawerBreakpoint |
| 21 | + | // the presentation becomes a full-width drawer regardless of this width. | |||
| 22 | + | paletteModal = ui.Modal{FixedWidth: 64, FixedHeight: 11, Title: " Commands ", Anchor: domain.AnchorBottomRight, Margins: ui.Margins{Top: 1, Right: 1, Bottom: 1, Left: 1}} | |||
| 26 | errCreateDestinationUnavailable = errors.New("that destination is no longer available") | 23 | errCreateDestinationUnavailable = errors.New("that destination is no longer available") | ||
| 27 | ) | 24 | ) | ||
| 28 | 25 | ||||
| 29 | - | func paletteModalFor(size domain.Size, cfg domain.PaletteConfig) ui.Modal { | 26 | + | func paletteModalFor(cfg domain.PaletteConfig) ui.Modal { |
| 30 | modal := paletteModal | 27 | modal := paletteModal | ||
| 31 | - | if size.Cols >= paletteRailBreakpoint { | |||
| 32 | - | modal.FixedWidth = paletteRailWidth | |||
| 33 | - | } | |||
| 34 | if cfg.AnchorSet { | 28 | if cfg.AnchorSet { | ||
| 35 | modal.Anchor = cfg.Anchor | 29 | modal.Anchor = cfg.Anchor | ||
| 36 | - | } else if size.Cols >= paletteRailBreakpoint { | |||
| 37 | - | modal.Anchor = domain.AnchorBottomRight | |||
| 38 | } | 30 | } | ||
| 39 | return modal | 31 | return modal | ||
| 40 | } | 32 | } | ||
| @@ -1173,7 +1165,7 @@ func (e paletteExec) NavigateRecentRoute(action protocol.RouteNavigationAction) | |||||
| 1173 | 1165 | ||||
| 1174 | func composePaletteClientFrame(model *palette.Model, base renderer.Frame, cfg domain.PaletteConfig, guidance string, styles ...themeui.Styles) (renderer.Frame, []renderer.Damage) { | 1166 | func composePaletteClientFrame(model *palette.Model, base renderer.Frame, cfg domain.PaletteConfig, guidance string, styles ...themeui.Styles) (renderer.Frame, []renderer.Damage) { | ||
| 1175 | styleSet := resolveStyles(styles) | 1167 | styleSet := resolveStyles(styles) | ||
| 1176 | - | modal := paletteModalFor(domain.Size{Cols: base.Width, Rows: base.Height}, cfg) | 1168 | + | modal := paletteModalFor(cfg) |
| 1177 | return composeModalClientFrame(base, modal, styleSet, func(size domain.Size) renderer.Frame { | 1169 | return composeModalClientFrame(base, modal, styleSet, func(size domain.Size) renderer.Frame { | ||
| 1178 | return model.Render(size, palette.RenderOptions{Styles: palette.RenderStyles{Base: styleSet.PickerBase, Row: styleSet.PickerBase, Selection: styleSet.PickerSelection, Description: styleSet.PickerDescription, SelectionDescription: styleSet.PickerSelectionMuted}, Guidance: guidance}) | 1170 | return model.Render(size, palette.RenderOptions{Styles: palette.RenderStyles{Base: styleSet.PickerBase, Row: styleSet.PickerBase, Selection: styleSet.PickerSelection, Description: styleSet.PickerDescription, SelectionDescription: styleSet.PickerSelectionMuted}, Guidance: guidance}) | ||
| 1179 | }) | 1171 | }) | ||
M internal/usecase/daemon/palette_test.go +11 -9
| @@ -1063,8 +1063,8 @@ func TestPaletteModalGeometry(t *testing.T) { | |||
| 1063 | 1063 | auto := domain.PaletteConfig{} | |
| 1064 | 1064 | tests := []testCase{ | |
| 1065 | 1065 | {name: "auto 79 column drawer", size: domain.Size{Cols: 79, Rows: 40}, cfg: auto, want: domain.Rect{X: 0, Y: 28, Width: 79, Height: 11}}, | |
| 1066 | - | {name: "auto 80 column shelf", size: domain.Size{Cols: 80, Rows: 40}, cfg: auto, want: domain.Rect{X: 0, Y: 28, Width: 80, Height: 11}}, | |
| 1067 | - | {name: "auto 95 column shelf", size: domain.Size{Cols: 95, Rows: 40}, cfg: auto, want: domain.Rect{X: 0, Y: 28, Width: 95, Height: 11}}, | |
| 1066 | + | {name: "auto 80 column rail", size: domain.Size{Cols: 80, Rows: 40}, cfg: auto, want: domain.Rect{X: 15, Y: 28, Width: 64, Height: 11}}, | |
| 1067 | + | {name: "auto 95 column rail", size: domain.Size{Cols: 95, Rows: 40}, cfg: auto, want: domain.Rect{X: 30, Y: 28, Width: 64, Height: 11}}, | |
| 1068 | 1068 | {name: "auto 96 column rail", size: domain.Size{Cols: 96, Rows: 40}, cfg: auto, want: domain.Rect{X: 31, Y: 28, Width: 64, Height: 11}}, | |
| 1069 | 1069 | {name: "auto 120 column rail", size: domain.Size{Cols: 120, Rows: 40}, cfg: auto, want: domain.Rect{X: 55, Y: 28, Width: 64, Height: 11}}, | |
| 1070 | 1070 | {name: "auto tiny terminal clamps", size: domain.Size{Cols: 20, Rows: 6}, cfg: auto, want: domain.Rect{X: 0, Y: 3, Width: 20, Height: 2}}, | |
| @@ -1075,12 +1075,13 @@ func TestPaletteModalGeometry(t *testing.T) { | |||
| 1075 | 1075 | tests = append(tests, testCase{name: anchor.String(), size: domain.Size{Cols: 120, Rows: 40}, cfg: domain.PaletteConfig{Anchor: anchor, AnchorSet: true}, want: domain.Rect{X: wantX, Y: wantY, Width: 64, Height: 11}}) | |
| 1076 | 1076 | } | |
| 1077 | 1077 | for _, anchor := range []domain.Anchor{domain.AnchorLeft, domain.AnchorCenter, domain.AnchorRight} { | |
| 1078 | - | tests = append(tests, testCase{name: "narrow " + anchor.String(), size: domain.Size{Cols: 95, Rows: 40}, cfg: domain.PaletteConfig{Anchor: anchor, AnchorSet: true}, want: domain.Rect{X: 0, Y: 14, Width: 95, Height: 11}}) | |
| 1078 | + | wantX := map[domain.Anchor]int{domain.AnchorLeft: 1, domain.AnchorCenter: 15, domain.AnchorRight: 30}[anchor] | |
| 1079 | + | tests = append(tests, testCase{name: "95 column " + anchor.String(), size: domain.Size{Cols: 95, Rows: 40}, cfg: domain.PaletteConfig{Anchor: anchor, AnchorSet: true}, want: domain.Rect{X: wantX, Y: 14, Width: 64, Height: 11}}) | |
| 1079 | 1080 | } | |
| 1080 | 1081 | ||
| 1081 | 1082 | for _, tt := range tests { | |
| 1082 | 1083 | t.Run(tt.name, func(t *testing.T) { | |
| 1083 | - | modal := paletteModalFor(tt.size, tt.cfg) | |
| 1084 | + | modal := paletteModalFor(tt.cfg) | |
| 1084 | 1085 | presentation := modal.Resolve(tt.size) | |
| 1085 | 1086 | require.Equal(t, tt.want, presentation.Bounds) | |
| 1086 | 1087 | if tt.size.Cols < ui.ResponsiveDrawerBreakpoint { | |
| @@ -1102,17 +1103,18 @@ func TestComposePaletteClientFrameUsesResponsivePresentation(t *testing.T) { | |||
| 1102 | 1103 | for _, tt := range []struct { | |
| 1103 | 1104 | name string | |
| 1104 | 1105 | width int | |
| 1106 | + | left int | |
| 1105 | 1107 | topLeft rune | |
| 1106 | 1108 | innerLeft rune | |
| 1107 | 1109 | }{ | |
| 1108 | - | {name: "79 column drawer", width: 79, topLeft: '─', innerLeft: '>'}, | |
| 1109 | - | {name: "80 column shelf", width: 80, topLeft: '┌', innerLeft: '│'}, | |
| 1110 | + | {name: "79 column drawer", width: 79, left: 0, topLeft: '─', innerLeft: '>'}, | |
| 1111 | + | {name: "80 column rail", width: 80, left: 15, topLeft: '┌', innerLeft: '│'}, | |
| 1110 | 1112 | } { | |
| 1111 | 1113 | t.Run(tt.name, func(t *testing.T) { | |
| 1112 | 1114 | frame, _ := composePaletteClientFrame(model, renderer.NewFrame(tt.width, 40), domain.PaletteConfig{}, "") | |
| 1113 | 1115 | ||
| 1114 | - | require.Equal(t, tt.topLeft, frame.At(0, 28).Rune) | |
| 1115 | - | require.Equal(t, tt.innerLeft, frame.At(0, 29).Rune) | |
| 1116 | + | require.Equal(t, tt.topLeft, frame.At(tt.left, 28).Rune) | |
| 1117 | + | require.Equal(t, tt.innerLeft, frame.At(tt.left, 29).Rune) | |
| 1116 | 1118 | }) | |
| 1117 | 1119 | } | |
| 1118 | 1120 | } | |
| @@ -1142,7 +1144,7 @@ func TestComposePaletteClientFrameUsesSelectedDescriptionTheme(t *testing.T) { | |||
| 1142 | 1144 | styles := themeui.Resolve(themeui.BuiltinDark, domain.ThemeAccent{Mode: domain.ThemeAccentAuto}).Styles | |
| 1143 | 1145 | ||
| 1144 | 1146 | frame, _ := composePaletteClientFrame(model, base, domain.PaletteConfig{}, "", styles) | |
| 1145 | - | inner := paletteModalFor(size, domain.PaletteConfig{}).Resolve(size).Inner | |
| 1147 | + | inner := paletteModalFor(domain.PaletteConfig{}).Resolve(size).Inner | |
| 1146 | 1148 | ||
| 1147 | 1149 | require.True(t, frame.At(inner.X+4, inner.Y+1).Style.Equal(styles.PickerSelectionMuted)) | |
| 1148 | 1150 | } | |
| @@ -1063,8 +1063,8 @@ func TestPaletteModalGeometry(t *testing.T) { | |||||
| 1063 | auto := domain.PaletteConfig{} | 1063 | auto := domain.PaletteConfig{} | ||
| 1064 | tests := []testCase{ | 1064 | tests := []testCase{ | ||
| 1065 | {name: "auto 79 column drawer", size: domain.Size{Cols: 79, Rows: 40}, cfg: auto, want: domain.Rect{X: 0, Y: 28, Width: 79, Height: 11}}, | 1065 | {name: "auto 79 column drawer", size: domain.Size{Cols: 79, Rows: 40}, cfg: auto, want: domain.Rect{X: 0, Y: 28, Width: 79, Height: 11}}, | ||
| 1066 | - | {name: "auto 80 column shelf", size: domain.Size{Cols: 80, Rows: 40}, cfg: auto, want: domain.Rect{X: 0, Y: 28, Width: 80, Height: 11}}, | 1066 | + | {name: "auto 80 column rail", size: domain.Size{Cols: 80, Rows: 40}, cfg: auto, want: domain.Rect{X: 15, Y: 28, Width: 64, Height: 11}}, |
| 1067 | - | {name: "auto 95 column shelf", size: domain.Size{Cols: 95, Rows: 40}, cfg: auto, want: domain.Rect{X: 0, Y: 28, Width: 95, Height: 11}}, | 1067 | + | {name: "auto 95 column rail", size: domain.Size{Cols: 95, Rows: 40}, cfg: auto, want: domain.Rect{X: 30, Y: 28, Width: 64, Height: 11}}, |
| 1068 | {name: "auto 96 column rail", size: domain.Size{Cols: 96, Rows: 40}, cfg: auto, want: domain.Rect{X: 31, Y: 28, Width: 64, Height: 11}}, | 1068 | {name: "auto 96 column rail", size: domain.Size{Cols: 96, Rows: 40}, cfg: auto, want: domain.Rect{X: 31, Y: 28, Width: 64, Height: 11}}, | ||
| 1069 | {name: "auto 120 column rail", size: domain.Size{Cols: 120, Rows: 40}, cfg: auto, want: domain.Rect{X: 55, Y: 28, Width: 64, Height: 11}}, | 1069 | {name: "auto 120 column rail", size: domain.Size{Cols: 120, Rows: 40}, cfg: auto, want: domain.Rect{X: 55, Y: 28, Width: 64, Height: 11}}, | ||
| 1070 | {name: "auto tiny terminal clamps", size: domain.Size{Cols: 20, Rows: 6}, cfg: auto, want: domain.Rect{X: 0, Y: 3, Width: 20, Height: 2}}, | 1070 | {name: "auto tiny terminal clamps", size: domain.Size{Cols: 20, Rows: 6}, cfg: auto, want: domain.Rect{X: 0, Y: 3, Width: 20, Height: 2}}, | ||
| @@ -1075,12 +1075,13 @@ func TestPaletteModalGeometry(t *testing.T) { | |||||
| 1075 | tests = append(tests, testCase{name: anchor.String(), size: domain.Size{Cols: 120, Rows: 40}, cfg: domain.PaletteConfig{Anchor: anchor, AnchorSet: true}, want: domain.Rect{X: wantX, Y: wantY, Width: 64, Height: 11}}) | 1075 | tests = append(tests, testCase{name: anchor.String(), size: domain.Size{Cols: 120, Rows: 40}, cfg: domain.PaletteConfig{Anchor: anchor, AnchorSet: true}, want: domain.Rect{X: wantX, Y: wantY, Width: 64, Height: 11}}) | ||
| 1076 | } | 1076 | } | ||
| 1077 | for _, anchor := range []domain.Anchor{domain.AnchorLeft, domain.AnchorCenter, domain.AnchorRight} { | 1077 | for _, anchor := range []domain.Anchor{domain.AnchorLeft, domain.AnchorCenter, domain.AnchorRight} { | ||
| 1078 | - | tests = append(tests, testCase{name: "narrow " + anchor.String(), size: domain.Size{Cols: 95, Rows: 40}, cfg: domain.PaletteConfig{Anchor: anchor, AnchorSet: true}, want: domain.Rect{X: 0, Y: 14, Width: 95, Height: 11}}) | 1078 | + | wantX := map[domain.Anchor]int{domain.AnchorLeft: 1, domain.AnchorCenter: 15, domain.AnchorRight: 30}[anchor] |
| 1079 | + | tests = append(tests, testCase{name: "95 column " + anchor.String(), size: domain.Size{Cols: 95, Rows: 40}, cfg: domain.PaletteConfig{Anchor: anchor, AnchorSet: true}, want: domain.Rect{X: wantX, Y: 14, Width: 64, Height: 11}}) | |||
| 1079 | } | 1080 | } | ||
| 1080 | 1081 | ||||
| 1081 | for _, tt := range tests { | 1082 | for _, tt := range tests { | ||
| 1082 | t.Run(tt.name, func(t *testing.T) { | 1083 | t.Run(tt.name, func(t *testing.T) { | ||
| 1083 | - | modal := paletteModalFor(tt.size, tt.cfg) | 1084 | + | modal := paletteModalFor(tt.cfg) |
| 1084 | presentation := modal.Resolve(tt.size) | 1085 | presentation := modal.Resolve(tt.size) | ||
| 1085 | require.Equal(t, tt.want, presentation.Bounds) | 1086 | require.Equal(t, tt.want, presentation.Bounds) | ||
| 1086 | if tt.size.Cols < ui.ResponsiveDrawerBreakpoint { | 1087 | if tt.size.Cols < ui.ResponsiveDrawerBreakpoint { | ||
| @@ -1102,17 +1103,18 @@ func TestComposePaletteClientFrameUsesResponsivePresentation(t *testing.T) { | |||||
| 1102 | for _, tt := range []struct { | 1103 | for _, tt := range []struct { | ||
| 1103 | name string | 1104 | name string | ||
| 1104 | width int | 1105 | width int | ||
| 1106 | + | left int | |||
| 1105 | topLeft rune | 1107 | topLeft rune | ||
| 1106 | innerLeft rune | 1108 | innerLeft rune | ||
| 1107 | }{ | 1109 | }{ | ||
| 1108 | - | {name: "79 column drawer", width: 79, topLeft: '─', innerLeft: '>'}, | 1110 | + | {name: "79 column drawer", width: 79, left: 0, topLeft: '─', innerLeft: '>'}, |
| 1109 | - | {name: "80 column shelf", width: 80, topLeft: '┌', innerLeft: '│'}, | 1111 | + | {name: "80 column rail", width: 80, left: 15, topLeft: '┌', innerLeft: '│'}, |
| 1110 | } { | 1112 | } { | ||
| 1111 | t.Run(tt.name, func(t *testing.T) { | 1113 | t.Run(tt.name, func(t *testing.T) { | ||
| 1112 | frame, _ := composePaletteClientFrame(model, renderer.NewFrame(tt.width, 40), domain.PaletteConfig{}, "") | 1114 | frame, _ := composePaletteClientFrame(model, renderer.NewFrame(tt.width, 40), domain.PaletteConfig{}, "") | ||
| 1113 | 1115 | ||||
| 1114 | - | require.Equal(t, tt.topLeft, frame.At(0, 28).Rune) | 1116 | + | require.Equal(t, tt.topLeft, frame.At(tt.left, 28).Rune) |
| 1115 | - | require.Equal(t, tt.innerLeft, frame.At(0, 29).Rune) | 1117 | + | require.Equal(t, tt.innerLeft, frame.At(tt.left, 29).Rune) |
| 1116 | }) | 1118 | }) | ||
| 1117 | } | 1119 | } | ||
| 1118 | } | 1120 | } | ||
| @@ -1142,7 +1144,7 @@ func TestComposePaletteClientFrameUsesSelectedDescriptionTheme(t *testing.T) { | |||||
| 1142 | styles := themeui.Resolve(themeui.BuiltinDark, domain.ThemeAccent{Mode: domain.ThemeAccentAuto}).Styles | 1144 | styles := themeui.Resolve(themeui.BuiltinDark, domain.ThemeAccent{Mode: domain.ThemeAccentAuto}).Styles | ||
| 1143 | 1145 | ||||
| 1144 | frame, _ := composePaletteClientFrame(model, base, domain.PaletteConfig{}, "", styles) | 1146 | frame, _ := composePaletteClientFrame(model, base, domain.PaletteConfig{}, "", styles) | ||
| 1145 | - | inner := paletteModalFor(size, domain.PaletteConfig{}).Resolve(size).Inner | 1147 | + | inner := paletteModalFor(domain.PaletteConfig{}).Resolve(size).Inner |
| 1146 | 1148 | ||||
| 1147 | require.True(t, frame.At(inner.X+4, inner.Y+1).Style.Equal(styles.PickerSelectionMuted)) | 1149 | require.True(t, frame.At(inner.X+4, inner.Y+1).Style.Equal(styles.PickerSelectionMuted)) | ||
| 1148 | } | 1150 | } | ||
M internal/usecase/daemon/render_pipeline.go +1 -1
| @@ -312,7 +312,7 @@ func captureOverlayLayers(state *capturedRenderState, snap *overlayRenderSnapsho | |||
| 312 | 312 | o.noticesOverlay.inner = snap.noticesOverlayModel.Render(rectSize(presentation.Inner), renderStyles) | |
| 313 | 313 | } | |
| 314 | 314 | if snap.paletteActive && snap.paletteModel != nil { | |
| 315 | - | modal := paletteModalFor(size, paletteCfg) | |
| 315 | + | modal := paletteModalFor(paletteCfg) | |
| 316 | 316 | presentation := modal.Resolve(size) | |
| 317 | 317 | o.palette = capturedModal{active: true, title: modal.Title, presentation: presentation, focused: true} | |
| 318 | 318 | guidance := "" | |
| @@ -312,7 +312,7 @@ func captureOverlayLayers(state *capturedRenderState, snap *overlayRenderSnapsho | |||||
| 312 | o.noticesOverlay.inner = snap.noticesOverlayModel.Render(rectSize(presentation.Inner), renderStyles) | 312 | o.noticesOverlay.inner = snap.noticesOverlayModel.Render(rectSize(presentation.Inner), renderStyles) | ||
| 313 | } | 313 | } | ||
| 314 | if snap.paletteActive && snap.paletteModel != nil { | 314 | if snap.paletteActive && snap.paletteModel != nil { | ||
| 315 | - | modal := paletteModalFor(size, paletteCfg) | 315 | + | modal := paletteModalFor(paletteCfg) |
| 316 | presentation := modal.Resolve(size) | 316 | presentation := modal.Resolve(size) | ||
| 317 | o.palette = capturedModal{active: true, title: modal.Title, presentation: presentation, focused: true} | 317 | o.palette = capturedModal{active: true, title: modal.Title, presentation: presentation, focused: true} | ||
| 318 | guidance := "" | 318 | guidance := "" | ||
M scripts/demo/Dockerfile +2 -2
| @@ -6,7 +6,7 @@ RUN --mount=type=cache,target=/var/cache/pacman/pkg,sharing=locked \ | |||
| 6 | 6 | && pacman -Syu --noconfirm \ | |
| 7 | 7 | vhs ttyd ffmpeg chromium \ | |
| 8 | 8 | htop fastfetch gifsicle imagemagick \ | |
| 9 | - | eza neovim man-db man-pages coreutils \ | |
| 9 | + | eza fzf neovim man-db man-pages coreutils \ | |
| 10 | 10 | ncurses openssh util-linux \ | |
| 11 | 11 | ttf-jetbrains-mono-nerd | |
| 12 | 12 | RUN useradd -m -u 1000 demo | |
| @@ -16,7 +16,7 @@ COPY build/vev /usr/local/bin/vev | |||
| 16 | 16 | COPY scripts/demo/ticker.sh /usr/local/bin/ticker | |
| 17 | 17 | COPY scripts/demo/demo-shell.sh /usr/local/bin/demo-shell | |
| 18 | 18 | RUN chmod +x /usr/local/bin/ticker /usr/local/bin/demo-shell \ | |
| 19 | - | && install -d -o demo -g demo -m 0700 /home/demo/.ssh \ | |
| 19 | + | && install -d -o demo -g demo -m 0700 /home/demo/.ssh /home/demo/.local \ | |
| 20 | 20 | && install -d -m 0755 /run/sshd \ | |
| 21 | 21 | && ssh-keygen -A \ | |
| 22 | 22 | && ssh-keygen -q -t ed25519 -N '' -C vev-demo -f /home/demo/.ssh/id_ed25519 \ | |
| @@ -6,7 +6,7 @@ RUN --mount=type=cache,target=/var/cache/pacman/pkg,sharing=locked \ | |||||
| 6 | && pacman -Syu --noconfirm \ | 6 | && pacman -Syu --noconfirm \ | ||
| 7 | vhs ttyd ffmpeg chromium \ | 7 | vhs ttyd ffmpeg chromium \ | ||
| 8 | htop fastfetch gifsicle imagemagick \ | 8 | htop fastfetch gifsicle imagemagick \ | ||
| 9 | - | eza neovim man-db man-pages coreutils \ | 9 | + | eza fzf neovim man-db man-pages coreutils \ |
| 10 | ncurses openssh util-linux \ | 10 | ncurses openssh util-linux \ | ||
| 11 | ttf-jetbrains-mono-nerd | 11 | ttf-jetbrains-mono-nerd | ||
| 12 | RUN useradd -m -u 1000 demo | 12 | RUN useradd -m -u 1000 demo | ||
| @@ -16,7 +16,7 @@ COPY build/vev /usr/local/bin/vev | |||||
| 16 | COPY scripts/demo/ticker.sh /usr/local/bin/ticker | 16 | COPY scripts/demo/ticker.sh /usr/local/bin/ticker | ||
| 17 | COPY scripts/demo/demo-shell.sh /usr/local/bin/demo-shell | 17 | COPY scripts/demo/demo-shell.sh /usr/local/bin/demo-shell | ||
| 18 | RUN chmod +x /usr/local/bin/ticker /usr/local/bin/demo-shell \ | 18 | RUN chmod +x /usr/local/bin/ticker /usr/local/bin/demo-shell \ | ||
| 19 | - | && install -d -o demo -g demo -m 0700 /home/demo/.ssh \ | 19 | + | && install -d -o demo -g demo -m 0700 /home/demo/.ssh /home/demo/.local \ |
| 20 | && install -d -m 0755 /run/sshd \ | 20 | && install -d -m 0755 /run/sshd \ | ||
| 21 | && ssh-keygen -A \ | 21 | && ssh-keygen -A \ | ||
| 22 | && ssh-keygen -q -t ed25519 -N '' -C vev-demo -f /home/demo/.ssh/id_ed25519 \ | 22 | && ssh-keygen -q -t ed25519 -N '' -C vev-demo -f /home/demo/.ssh/id_ed25519 \ | ||
M scripts/demo/README.md +2 -1
| @@ -1,6 +1,7 @@ | |||
| 1 | 1 | # Demo and sandbox scripts | |
| 2 | 2 | ||
| 3 | - | - `run.sh`, `demo.tape`, `smoke.tape`: VHS demo recording. | |
| 3 | + | - `run.sh`, `demo.tape`, `smoke.tape`, `compose.demo.yaml`: VHS demo recording | |
| 4 | + | (`make demo`). One remote container, `remote-a`. | |
| 4 | 5 | - `sandbox-run.sh`: automated acceptance. Client and two remotes all run in | |
| 5 | 6 | containers on an internal network. | |
| 6 | 7 | - `test-remotes.sh`: two containerised remotes for manual testing from your own | |
| @@ -1,6 +1,7 @@ | |||||
| 1 | # Demo and sandbox scripts | 1 | # Demo and sandbox scripts | ||
| 2 | 2 | ||||
| 3 | - | - `run.sh`, `demo.tape`, `smoke.tape`: VHS demo recording. | 3 | + | - `run.sh`, `demo.tape`, `smoke.tape`, `compose.demo.yaml`: VHS demo recording |
| 4 | + | (`make demo`). One remote container, `remote-a`. | |||
| 4 | - `sandbox-run.sh`: automated acceptance. Client and two remotes all run in | 5 | - `sandbox-run.sh`: automated acceptance. Client and two remotes all run in | ||
| 5 | containers on an internal network. | 6 | containers on an internal network. | ||
| 6 | - `test-remotes.sh`: two containerised remotes for manual testing from your own | 7 | - `test-remotes.sh`: two containerised remotes for manual testing from your own | ||
A scripts/demo/compose.demo.yaml +22 -0
| @@ -0,0 +1,22 @@ | |||
| 1 | + | # VHS demo recording. Driven by scripts/demo/run.sh (make demo). | |
| 2 | + | name: vev-demo | |
| 3 | + | ||
| 4 | + | services: | |
| 5 | + | remote-a: | |
| 6 | + | image: vev-demo | |
| 7 | + | hostname: remote-a | |
| 8 | + | user: root | |
| 9 | + | entrypoint: ["/usr/bin/sshd", "-D", "-e"] | |
| 10 | + | ||
| 11 | + | client: | |
| 12 | + | image: vev-demo | |
| 13 | + | depends_on: | |
| 14 | + | - remote-a | |
| 15 | + | volumes: | |
| 16 | + | - ../../scripts/demo:/tape:ro | |
| 17 | + | - ../../scripts/demo/out:/home/demo/out | |
| 18 | + | # Shared by the host-registration and recording runs; removed by `down -v`. | |
| 19 | + | - demo-local:/home/demo/.local | |
| 20 | + | ||
| 21 | + | volumes: | |
| 22 | + | demo-local: | |
| @@ -0,0 +1,22 @@ | |||||
| 1 | + | # VHS demo recording. Driven by scripts/demo/run.sh (make demo). | |||
| 2 | + | name: vev-demo | |||
| 3 | + | ||||
| 4 | + | services: | |||
| 5 | + | remote-a: | |||
| 6 | + | image: vev-demo | |||
| 7 | + | hostname: remote-a | |||
| 8 | + | user: root | |||
| 9 | + | entrypoint: ["/usr/bin/sshd", "-D", "-e"] | |||
| 10 | + | ||||
| 11 | + | client: | |||
| 12 | + | image: vev-demo | |||
| 13 | + | depends_on: | |||
| 14 | + | - remote-a | |||
| 15 | + | volumes: | |||
| 16 | + | - ../../scripts/demo:/tape:ro | |||
| 17 | + | - ../../scripts/demo/out:/home/demo/out | |||
| 18 | + | # Shared by the host-registration and recording runs; removed by `down -v`. | |||
| 19 | + | - demo-local:/home/demo/.local | |||
| 20 | + | ||||
| 21 | + | volumes: | |||
| 22 | + | demo-local: | |||
M scripts/demo/demo.tape +45 -19
| @@ -22,7 +22,7 @@ Set WaitTimeout 30s | |||
| 22 | 22 | ||
| 23 | 23 | # ── Local and remote history, shown immediately ────────── | |
| 24 | 24 | Type@180ms "vev" | |
| 25 | - | Sleep 600ms | |
| 25 | + | Sleep 1.2s | |
| 26 | 26 | Enter | |
| 27 | 27 | Sleep 1.1s | |
| 28 | 28 | # Keep the initial local shell so the final detach/attach cycle is meaningful. | |
| @@ -30,6 +30,7 @@ Alt+" " | |||
| 30 | 30 | Sleep 550ms | |
| 31 | 31 | Type "RNS " | |
| 32 | 32 | Type@95ms "local" | |
| 33 | + | Sleep 1s | |
| 33 | 34 | Enter | |
| 34 | 35 | Sleep 1s | |
| 35 | 36 | ||
| @@ -39,8 +40,10 @@ Sleep 550ms | |||
| 39 | 40 | Type "SSP" | |
| 40 | 41 | Enter | |
| 41 | 42 | Sleep 1.5s | |
| 42 | - | Down 2 | |
| 43 | - | Sleep 700ms | |
| 43 | + | Down | |
| 44 | + | Sleep 800ms | |
| 45 | + | Down | |
| 46 | + | Sleep 1.5s | |
| 44 | 47 | Enter | |
| 45 | 48 | Sleep 2.5s | |
| 46 | 49 | ||
| @@ -67,6 +70,10 @@ Alt+" " | |||
| 67 | 70 | Sleep 550ms | |
| 68 | 71 | Type "CNS " | |
| 69 | 72 | Type@95ms "agents" | |
| 73 | + | Sleep 400ms | |
| 74 | + | # From a remote session CNS offers two destinations; pick the local one. | |
| 75 | + | Down 2 | |
| 76 | + | Sleep 300ms | |
| 70 | 77 | Enter | |
| 71 | 78 | Sleep 900ms | |
| 72 | 79 | Type@80ms "sleep " | |
| @@ -93,6 +100,7 @@ Sleep 550ms | |||
| 93 | 100 | Alt+" " | |
| 94 | 101 | Sleep 550ms | |
| 95 | 102 | Type "STP" | |
| 103 | + | Sleep 1.5s | |
| 96 | 104 | Enter | |
| 97 | 105 | Sleep 650ms | |
| 98 | 106 | Type@85ms "nvim " | |
| @@ -106,9 +114,8 @@ Sleep 900ms | |||
| 106 | 114 | Alt+j | |
| 107 | 115 | Sleep 900ms | |
| 108 | 116 | ||
| 109 | - | # ── Session "work": floating pane preserves its program ── | |
| 110 | - | # Open a man page, scroll it, hide, then show again at the same | |
| 111 | - | # scroll position — proves the float keeps program + scroll state. | |
| 117 | + | # ── Session "work": floating pane ───────────────────── | |
| 118 | + | # Open a man page in the float, scroll it, then hide the float. | |
| 112 | 119 | Alt+f | |
| 113 | 120 | Sleep 550ms | |
| 114 | 121 | Type@90ms "man " | |
| @@ -116,15 +123,9 @@ Type@130ms "ls" | |||
| 116 | 123 | Enter | |
| 117 | 124 | Sleep 1.2s | |
| 118 | 125 | Space | |
| 119 | - | Sleep 700ms | |
| 120 | - | Alt+f | |
| 121 | - | Sleep 900ms | |
| 122 | - | Alt+f | |
| 123 | 126 | Sleep 1.2s | |
| 124 | - | Type "q" | |
| 125 | - | Sleep 550ms | |
| 126 | 127 | Alt+f | |
| 127 | - | Sleep 500ms | |
| 128 | + | Sleep 900ms | |
| 128 | 129 | ||
| 129 | 130 | # ── Session "monitor": fastfetch + htop, each full-width, | |
| 130 | 131 | # never split. ───────────────────────────────────────── | |
| @@ -132,6 +133,9 @@ Alt+" " | |||
| 132 | 133 | Sleep 550ms | |
| 133 | 134 | Type "CNS " | |
| 134 | 135 | Type@95ms "monitor" | |
| 136 | + | Sleep 400ms | |
| 137 | + | Down 2 | |
| 138 | + | Sleep 300ms | |
| 135 | 139 | Enter | |
| 136 | 140 | Sleep 900ms | |
| 137 | 141 | Type@80ms "fast" | |
| @@ -156,16 +160,39 @@ Sleep 900ms | |||
| 156 | 160 | Alt+a | |
| 157 | 161 | Sleep 1.9s | |
| 158 | 162 | ||
| 159 | - | # ── Session picker now lists local and remote sessions ─── | |
| 163 | + | # ── Move pane: start fzf beside the agents job, type a query, | |
| 164 | + | # then carry it live into the "local" session. ─────────── | |
| 160 | 165 | Alt+" " | |
| 161 | 166 | Sleep 550ms | |
| 162 | - | Type "SSP" | |
| 167 | + | Type "SPR" | |
| 168 | + | Enter | |
| 169 | + | Sleep 650ms | |
| 170 | + | Type@85ms "cd " | |
| 171 | + | Type@120ms "/etc " | |
| 172 | + | Type@80ms "&& " | |
| 173 | + | Type@110ms "fzf" | |
| 174 | + | Enter | |
| 175 | + | Sleep 1.2s | |
| 176 | + | Type@140ms "ssh" | |
| 177 | + | Sleep 1.2s | |
| 178 | + | Alt+" " | |
| 179 | + | Sleep 550ms | |
| 180 | + | Type "MFP" | |
| 181 | + | Sleep 800ms | |
| 182 | + | Enter | |
| 183 | + | Sleep 1.5s | |
| 184 | + | Down | |
| 185 | + | Sleep 800ms | |
| 186 | + | Down | |
| 187 | + | Sleep 1.5s | |
| 163 | 188 | Enter | |
| 189 | + | # The picker stays open after a pane move so more panes can follow; close it. | |
| 190 | + | # The final re-attach to "local" shows fzf still open with its query. | |
| 164 | 191 | Sleep 1.5s | |
| 165 | 192 | Escape | |
| 166 | - | Sleep 650ms | |
| 193 | + | Sleep 1s | |
| 167 | 194 | ||
| 168 | - | # ── Detach / re-attach: local sessions survive, ticker on ─ | |
| 195 | + | # ── Detach / re-attach: local sessions survive, fzf intact ─ | |
| 169 | 196 | Alt+" " | |
| 170 | 197 | Sleep 550ms | |
| 171 | 198 | Type "DET" | |
| @@ -181,5 +208,4 @@ Sleep 150ms | |||
| 181 | 208 | Type@80ms "attach " | |
| 182 | 209 | Type@140ms "local" | |
| 183 | 210 | Enter | |
| 184 | - | Sleep 2.2s | |
| 185 | - | Sleep 400ms | |
| 211 | + | Sleep 3.5s | |
| @@ -22,7 +22,7 @@ Set WaitTimeout 30s | |||||
| 22 | 22 | ||||
| 23 | # ── Local and remote history, shown immediately ────────── | 23 | # ── Local and remote history, shown immediately ────────── | ||
| 24 | Type@180ms "vev" | 24 | Type@180ms "vev" | ||
| 25 | - | Sleep 600ms | 25 | + | Sleep 1.2s |
| 26 | Enter | 26 | Enter | ||
| 27 | Sleep 1.1s | 27 | Sleep 1.1s | ||
| 28 | # Keep the initial local shell so the final detach/attach cycle is meaningful. | 28 | # Keep the initial local shell so the final detach/attach cycle is meaningful. | ||
| @@ -30,6 +30,7 @@ Alt+" " | |||||
| 30 | Sleep 550ms | 30 | Sleep 550ms | ||
| 31 | Type "RNS " | 31 | Type "RNS " | ||
| 32 | Type@95ms "local" | 32 | Type@95ms "local" | ||
| 33 | + | Sleep 1s | |||
| 33 | Enter | 34 | Enter | ||
| 34 | Sleep 1s | 35 | Sleep 1s | ||
| 35 | 36 | ||||
| @@ -39,8 +40,10 @@ Sleep 550ms | |||||
| 39 | Type "SSP" | 40 | Type "SSP" | ||
| 40 | Enter | 41 | Enter | ||
| 41 | Sleep 1.5s | 42 | Sleep 1.5s | ||
| 42 | - | Down 2 | 43 | + | Down |
| 43 | - | Sleep 700ms | 44 | + | Sleep 800ms |
| 45 | + | Down | |||
| 46 | + | Sleep 1.5s | |||
| 44 | Enter | 47 | Enter | ||
| 45 | Sleep 2.5s | 48 | Sleep 2.5s | ||
| 46 | 49 | ||||
| @@ -67,6 +70,10 @@ Alt+" " | |||||
| 67 | Sleep 550ms | 70 | Sleep 550ms | ||
| 68 | Type "CNS " | 71 | Type "CNS " | ||
| 69 | Type@95ms "agents" | 72 | Type@95ms "agents" | ||
| 73 | + | Sleep 400ms | |||
| 74 | + | # From a remote session CNS offers two destinations; pick the local one. | |||
| 75 | + | Down 2 | |||
| 76 | + | Sleep 300ms | |||
| 70 | Enter | 77 | Enter | ||
| 71 | Sleep 900ms | 78 | Sleep 900ms | ||
| 72 | Type@80ms "sleep " | 79 | Type@80ms "sleep " | ||
| @@ -93,6 +100,7 @@ Sleep 550ms | |||||
| 93 | Alt+" " | 100 | Alt+" " | ||
| 94 | Sleep 550ms | 101 | Sleep 550ms | ||
| 95 | Type "STP" | 102 | Type "STP" | ||
| 103 | + | Sleep 1.5s | |||
| 96 | Enter | 104 | Enter | ||
| 97 | Sleep 650ms | 105 | Sleep 650ms | ||
| 98 | Type@85ms "nvim " | 106 | Type@85ms "nvim " | ||
| @@ -106,9 +114,8 @@ Sleep 900ms | |||||
| 106 | Alt+j | 114 | Alt+j | ||
| 107 | Sleep 900ms | 115 | Sleep 900ms | ||
| 108 | 116 | ||||
| 109 | - | # ── Session "work": floating pane preserves its program ── | 117 | + | # ── Session "work": floating pane ───────────────────── |
| 110 | - | # Open a man page, scroll it, hide, then show again at the same | 118 | + | # Open a man page in the float, scroll it, then hide the float. |
| 111 | - | # scroll position — proves the float keeps program + scroll state. | |||
| 112 | Alt+f | 119 | Alt+f | ||
| 113 | Sleep 550ms | 120 | Sleep 550ms | ||
| 114 | Type@90ms "man " | 121 | Type@90ms "man " | ||
| @@ -116,15 +123,9 @@ Type@130ms "ls" | |||||
| 116 | Enter | 123 | Enter | ||
| 117 | Sleep 1.2s | 124 | Sleep 1.2s | ||
| 118 | Space | 125 | Space | ||
| 119 | - | Sleep 700ms | |||
| 120 | - | Alt+f | |||
| 121 | - | Sleep 900ms | |||
| 122 | - | Alt+f | |||
| 123 | Sleep 1.2s | 126 | Sleep 1.2s | ||
| 124 | - | Type "q" | |||
| 125 | - | Sleep 550ms | |||
| 126 | Alt+f | 127 | Alt+f | ||
| 127 | - | Sleep 500ms | 128 | + | Sleep 900ms |
| 128 | 129 | ||||
| 129 | # ── Session "monitor": fastfetch + htop, each full-width, | 130 | # ── Session "monitor": fastfetch + htop, each full-width, | ||
| 130 | # never split. ───────────────────────────────────────── | 131 | # never split. ───────────────────────────────────────── | ||
| @@ -132,6 +133,9 @@ Alt+" " | |||||
| 132 | Sleep 550ms | 133 | Sleep 550ms | ||
| 133 | Type "CNS " | 134 | Type "CNS " | ||
| 134 | Type@95ms "monitor" | 135 | Type@95ms "monitor" | ||
| 136 | + | Sleep 400ms | |||
| 137 | + | Down 2 | |||
| 138 | + | Sleep 300ms | |||
| 135 | Enter | 139 | Enter | ||
| 136 | Sleep 900ms | 140 | Sleep 900ms | ||
| 137 | Type@80ms "fast" | 141 | Type@80ms "fast" | ||
| @@ -156,16 +160,39 @@ Sleep 900ms | |||||
| 156 | Alt+a | 160 | Alt+a | ||
| 157 | Sleep 1.9s | 161 | Sleep 1.9s | ||
| 158 | 162 | ||||
| 159 | - | # ── Session picker now lists local and remote sessions ─── | 163 | + | # ── Move pane: start fzf beside the agents job, type a query, |
| 164 | + | # then carry it live into the "local" session. ─────────── | |||
| 160 | Alt+" " | 165 | Alt+" " | ||
| 161 | Sleep 550ms | 166 | Sleep 550ms | ||
| 162 | - | Type "SSP" | 167 | + | Type "SPR" |
| 168 | + | Enter | |||
| 169 | + | Sleep 650ms | |||
| 170 | + | Type@85ms "cd " | |||
| 171 | + | Type@120ms "/etc " | |||
| 172 | + | Type@80ms "&& " | |||
| 173 | + | Type@110ms "fzf" | |||
| 174 | + | Enter | |||
| 175 | + | Sleep 1.2s | |||
| 176 | + | Type@140ms "ssh" | |||
| 177 | + | Sleep 1.2s | |||
| 178 | + | Alt+" " | |||
| 179 | + | Sleep 550ms | |||
| 180 | + | Type "MFP" | |||
| 181 | + | Sleep 800ms | |||
| 182 | + | Enter | |||
| 183 | + | Sleep 1.5s | |||
| 184 | + | Down | |||
| 185 | + | Sleep 800ms | |||
| 186 | + | Down | |||
| 187 | + | Sleep 1.5s | |||
| 163 | Enter | 188 | Enter | ||
| 189 | + | # The picker stays open after a pane move so more panes can follow; close it. | |||
| 190 | + | # The final re-attach to "local" shows fzf still open with its query. | |||
| 164 | Sleep 1.5s | 191 | Sleep 1.5s | ||
| 165 | Escape | 192 | Escape | ||
| 166 | - | Sleep 650ms | 193 | + | Sleep 1s |
| 167 | 194 | ||||
| 168 | - | # ── Detach / re-attach: local sessions survive, ticker on ─ | 195 | + | # ── Detach / re-attach: local sessions survive, fzf intact ─ |
| 169 | Alt+" " | 196 | Alt+" " | ||
| 170 | Sleep 550ms | 197 | Sleep 550ms | ||
| 171 | Type "DET" | 198 | Type "DET" | ||
| @@ -181,5 +208,4 @@ Sleep 150ms | |||||
| 181 | Type@80ms "attach " | 208 | Type@80ms "attach " | ||
| 182 | Type@140ms "local" | 209 | Type@140ms "local" | ||
| 183 | Enter | 210 | Enter | ||
| 184 | - | Sleep 2.2s | 211 | + | Sleep 3.5s |
| 185 | - | Sleep 400ms | |||
M scripts/demo/run.sh +17 -12
| @@ -3,11 +3,8 @@ set -euo pipefail | |||
| 3 | 3 | cd "$(dirname "$0")/../.." | |
| 4 | 4 | ||
| 5 | 5 | TAPE="${1:-demo.tape}" | |
| 6 | - | COMPOSE_FILE="scripts/demo/compose.yaml" | |
| 6 | + | COMPOSE_FILE="scripts/demo/compose.demo.yaml" | |
| 7 | 7 | install -d -m 0777 scripts/demo/out | |
| 8 | - | # Demo containers use a rootless subuid mapping, so reset generated state | |
| 9 | - | # from the same container identity rather than relying on host ownership. | |
| 10 | - | docker run --rm --entrypoint sh -v "$PWD/scripts/demo/out:/out" vev-demo -c 'rm -rf /out/local && mkdir -m 0777 /out/local' | |
| 11 | 8 | ||
| 12 | 9 | compose() { | |
| 13 | 10 | docker compose -f "$COMPOSE_FILE" "$@" | |
| @@ -18,11 +15,13 @@ cleanup() { | |||
| 18 | 15 | } | |
| 19 | 16 | trap cleanup EXIT | |
| 20 | 17 | ||
| 21 | - | compose up -d remote | |
| 18 | + | # Start from a fresh named volume for the client's vev state. | |
| 19 | + | cleanup | |
| 20 | + | compose up -d remote-a | |
| 22 | 21 | ||
| 23 | 22 | ready="" | |
| 24 | 23 | for _ in $(seq 60); do | |
| 25 | - | if compose run --rm --entrypoint ssh client remote true >/dev/null 2>&1; then | |
| 24 | + | if compose run --rm --entrypoint ssh client remote-a true >/dev/null 2>&1; then | |
| 26 | 25 | ready=yes | |
| 27 | 26 | break | |
| 28 | 27 | fi | |
| @@ -30,19 +29,25 @@ for _ in $(seq 60); do | |||
| 30 | 29 | done | |
| 31 | 30 | if [ -z "$ready" ]; then | |
| 32 | 31 | echo "error: sshd on the remote container never accepted a connection" >&2 | |
| 33 | - | compose logs remote >&2 || true | |
| 32 | + | compose logs remote-a >&2 || true | |
| 34 | 33 | exit 1 | |
| 35 | 34 | fi | |
| 36 | 35 | ||
| 37 | 36 | start_remote_session() { | |
| 38 | 37 | local name=$1 scene=$2 | |
| 39 | - | if ! compose exec -u demo -T remote env VEV=demo-bootstrap SHELL=/usr/local/bin/demo-shell VEV_DEMO_SCENE="$scene" vev new "$name"; then | |
| 40 | - | echo "error: could not create remote session '$name'" >&2 | |
| 38 | + | # `vev new` needs a sized terminal. Attach briefly inside a pseudo-terminal; | |
| 39 | + | # the named session survives when the timeout ends the client. | |
| 40 | + | # timeout exits 124 when it ends the attached client, the expected path. | |
| 41 | + | local status=0 | |
| 42 | + | compose exec -u demo -T remote-a env TERM=xterm-256color VEV_DEMO_SCENE="$scene" SHELL=/bin/bash \ | |
| 43 | + | timeout 3 script -qec "stty cols 120 rows 40; SHELL=/usr/local/bin/demo-shell vev new $name" /dev/null >/dev/null 2>&1 || status=$? | |
| 44 | + | if [ "$status" -ne 124 ]; then | |
| 45 | + | echo "error: 'vev new $name' exited with status $status" >&2 | |
| 41 | 46 | return 1 | |
| 42 | 47 | fi | |
| 43 | - | if ! compose exec -u demo -T remote vev ls | grep -q "^$name"; then | |
| 48 | + | if ! compose exec -u demo -T remote-a vev ls | awk -v name="$name" '$1 == name && $2 == "up" { found = 1 } END { exit !found }'; then | |
| 44 | 49 | echo "error: remote session '$name' was not retained" >&2 | |
| 45 | - | compose exec -u demo -T remote vev ls >&2 || true | |
| 50 | + | compose exec -u demo -T remote-a vev ls >&2 || true | |
| 46 | 51 | return 1 | |
| 47 | 52 | fi | |
| 48 | 53 | } | |
| @@ -50,5 +55,5 @@ start_remote_session() { | |||
| 50 | 55 | start_remote_session work remote-deploys | |
| 51 | 56 | start_remote_session ticker remote-ticker | |
| 52 | 57 | ||
| 53 | - | compose run --rm --entrypoint vev client host add remote | |
| 58 | + | compose run --rm --entrypoint vev client host add remote-a | |
| 54 | 59 | compose run --rm client "/tape/$TAPE" | |
| @@ -3,11 +3,8 @@ set -euo pipefail | |||||
| 3 | cd "$(dirname "$0")/../.." | 3 | cd "$(dirname "$0")/../.." | ||
| 4 | 4 | ||||
| 5 | TAPE="${1:-demo.tape}" | 5 | TAPE="${1:-demo.tape}" | ||
| 6 | - | COMPOSE_FILE="scripts/demo/compose.yaml" | 6 | + | COMPOSE_FILE="scripts/demo/compose.demo.yaml" |
| 7 | install -d -m 0777 scripts/demo/out | 7 | install -d -m 0777 scripts/demo/out | ||
| 8 | - | # Demo containers use a rootless subuid mapping, so reset generated state | |||
| 9 | - | # from the same container identity rather than relying on host ownership. | |||
| 10 | - | docker run --rm --entrypoint sh -v "$PWD/scripts/demo/out:/out" vev-demo -c 'rm -rf /out/local && mkdir -m 0777 /out/local' | |||
| 11 | 8 | ||||
| 12 | compose() { | 9 | compose() { | ||
| 13 | docker compose -f "$COMPOSE_FILE" "$@" | 10 | docker compose -f "$COMPOSE_FILE" "$@" | ||
| @@ -18,11 +15,13 @@ cleanup() { | |||||
| 18 | } | 15 | } | ||
| 19 | trap cleanup EXIT | 16 | trap cleanup EXIT | ||
| 20 | 17 | ||||
| 21 | - | compose up -d remote | 18 | + | # Start from a fresh named volume for the client's vev state. |
| 19 | + | cleanup | |||
| 20 | + | compose up -d remote-a | |||
| 22 | 21 | ||||
| 23 | ready="" | 22 | ready="" | ||
| 24 | for _ in $(seq 60); do | 23 | for _ in $(seq 60); do | ||
| 25 | - | if compose run --rm --entrypoint ssh client remote true >/dev/null 2>&1; then | 24 | + | if compose run --rm --entrypoint ssh client remote-a true >/dev/null 2>&1; then |
| 26 | ready=yes | 25 | ready=yes | ||
| 27 | break | 26 | break | ||
| 28 | fi | 27 | fi | ||
| @@ -30,19 +29,25 @@ for _ in $(seq 60); do | |||||
| 30 | done | 29 | done | ||
| 31 | if [ -z "$ready" ]; then | 30 | if [ -z "$ready" ]; then | ||
| 32 | echo "error: sshd on the remote container never accepted a connection" >&2 | 31 | echo "error: sshd on the remote container never accepted a connection" >&2 | ||
| 33 | - | compose logs remote >&2 || true | 32 | + | compose logs remote-a >&2 || true |
| 34 | exit 1 | 33 | exit 1 | ||
| 35 | fi | 34 | fi | ||
| 36 | 35 | ||||
| 37 | start_remote_session() { | 36 | start_remote_session() { | ||
| 38 | local name=$1 scene=$2 | 37 | local name=$1 scene=$2 | ||
| 39 | - | if ! compose exec -u demo -T remote env VEV=demo-bootstrap SHELL=/usr/local/bin/demo-shell VEV_DEMO_SCENE="$scene" vev new "$name"; then | 38 | + | # `vev new` needs a sized terminal. Attach briefly inside a pseudo-terminal; |
| 40 | - | echo "error: could not create remote session '$name'" >&2 | 39 | + | # the named session survives when the timeout ends the client. |
| 40 | + | # timeout exits 124 when it ends the attached client, the expected path. | |||
| 41 | + | local status=0 | |||
| 42 | + | compose exec -u demo -T remote-a env TERM=xterm-256color VEV_DEMO_SCENE="$scene" SHELL=/bin/bash \ | |||
| 43 | + | timeout 3 script -qec "stty cols 120 rows 40; SHELL=/usr/local/bin/demo-shell vev new $name" /dev/null >/dev/null 2>&1 || status=$? | |||
| 44 | + | if [ "$status" -ne 124 ]; then | |||
| 45 | + | echo "error: 'vev new $name' exited with status $status" >&2 | |||
| 41 | return 1 | 46 | return 1 | ||
| 42 | fi | 47 | fi | ||
| 43 | - | if ! compose exec -u demo -T remote vev ls | grep -q "^$name"; then | 48 | + | if ! compose exec -u demo -T remote-a vev ls | awk -v name="$name" '$1 == name && $2 == "up" { found = 1 } END { exit !found }'; then |
| 44 | echo "error: remote session '$name' was not retained" >&2 | 49 | echo "error: remote session '$name' was not retained" >&2 | ||
| 45 | - | compose exec -u demo -T remote vev ls >&2 || true | 50 | + | compose exec -u demo -T remote-a vev ls >&2 || true |
| 46 | return 1 | 51 | return 1 | ||
| 47 | fi | 52 | fi | ||
| 48 | } | 53 | } | ||
| @@ -50,5 +55,5 @@ start_remote_session() { | |||||
| 50 | start_remote_session work remote-deploys | 55 | start_remote_session work remote-deploys | ||
| 51 | start_remote_session ticker remote-ticker | 56 | start_remote_session ticker remote-ticker | ||
| 52 | 57 | ||||
| 53 | - | compose run --rm --entrypoint vev client host add remote | 58 | + | compose run --rm --entrypoint vev client host add remote-a |
| 54 | compose run --rm client "/tape/$TAPE" | 59 | compose run --rm client "/tape/$TAPE" | ||