From a28e8d6f1e5dec55e35af219f19106e257800f3b Mon Sep 17 00:00:00 2001 From: Pierre Martin Date: Mon, 23 Mar 2026 21:29:09 +0100 Subject: [PATCH] test(04-02): add i3bar formatting with TDD - I3BarBlock struct for i3bar protocol JSON - formatI3BarBlocks: compact format D-06/D-07/D-08 - 10 tests covering mixed states, colors, labels, markers Co-Authored-By: Claude Opus 4.6 (1M context) --- i3bar.go | 52 ++++++++++++++++++++ i3bar_test.go | 129 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 181 insertions(+) create mode 100644 i3bar.go create mode 100644 i3bar_test.go diff --git a/i3bar.go b/i3bar.go new file mode 100644 index 0000000..a4eab12 --- /dev/null +++ b/i3bar.go @@ -0,0 +1,52 @@ +package main + +import ( + "fmt" + "strings" +) + +// I3BarBlock represents a single block in the i3bar protocol. +type I3BarBlock struct { + FullText string `json:"full_text"` + ShortText string `json:"short_text,omitempty"` + Color string `json:"color"` + Name string `json:"name"` + Markup string `json:"markup,omitempty"` +} + +// formatI3BarBlocks builds the vmux status block for i3bar from session data. +func formatI3BarBlocks(sessions []SessionInfo) []I3BarBlock { + if len(sessions) == 0 { + return []I3BarBlock{{FullText: "vmux: no sessions", Color: "#00ff00", Name: "vmux"}} + } + + hasWaiting := false + parts := make([]string, 0, len(sessions)) + + for _, s := range sessions { + name := shortName(s) + switch s.State { + case "Needs Input": + parts = append(parts, name+"[!]") + hasWaiting = true + case "Working": + parts = append(parts, name+"[W]") + case "Idle": + parts = append(parts, name+"[I]") + } + } + + var text string + if !hasWaiting { + text = fmt.Sprintf("vmux: all working (%d)", len(sessions)) + } else { + text = "vmux: " + strings.Join(parts, " ") + } + + color := "#00ff00" + if hasWaiting { + color = "#ff0000" + } + + return []I3BarBlock{{FullText: text, Color: color, Name: "vmux"}} +} diff --git a/i3bar_test.go b/i3bar_test.go new file mode 100644 index 0000000..27d161e --- /dev/null +++ b/i3bar_test.go @@ -0,0 +1,129 @@ +package main + +import ( + "testing" +) + +func TestFormatI3BarBlocks_MixedStates(t *testing.T) { + sessions := []SessionInfo{ + {Cwd: "/home/pierre/Code/auth", State: "Needs Input"}, + {Cwd: "/home/pierre/Code/portal", State: "Working"}, + {Cwd: "/home/pierre/Code/neia", State: "Idle"}, + } + blocks := formatI3BarBlocks(sessions) + if len(blocks) != 1 { + t.Fatalf("expected 1 block, got %d", len(blocks)) + } + want := "vmux: auth[!] portal[W] neia[I]" + if blocks[0].FullText != want { + t.Errorf("full_text = %q, want %q", blocks[0].FullText, want) + } + if blocks[0].Color != "#ff0000" { + t.Errorf("color = %q, want #ff0000", blocks[0].Color) + } +} + +func TestFormatI3BarBlocks_AllWorking(t *testing.T) { + sessions := []SessionInfo{ + {Cwd: "/a/b/one", State: "Working"}, + {Cwd: "/a/b/two", State: "Working"}, + {Cwd: "/a/b/three", State: "Working"}, + } + blocks := formatI3BarBlocks(sessions) + want := "vmux: all working (3)" + if blocks[0].FullText != want { + t.Errorf("full_text = %q, want %q", blocks[0].FullText, want) + } + if blocks[0].Color != "#00ff00" { + t.Errorf("color = %q, want #00ff00", blocks[0].Color) + } +} + +func TestFormatI3BarBlocks_NoSessions(t *testing.T) { + blocks := formatI3BarBlocks(nil) + want := "vmux: no sessions" + if blocks[0].FullText != want { + t.Errorf("full_text = %q, want %q", blocks[0].FullText, want) + } + if blocks[0].Color != "#00ff00" { + t.Errorf("color = %q, want #00ff00", blocks[0].Color) + } +} + +func TestFormatI3BarBlocks_ColorRed(t *testing.T) { + sessions := []SessionInfo{ + {Cwd: "/a/b/x", State: "Working"}, + {Cwd: "/a/b/y", State: "Needs Input"}, + } + blocks := formatI3BarBlocks(sessions) + if blocks[0].Color != "#ff0000" { + t.Errorf("color = %q, want #ff0000", blocks[0].Color) + } +} + +func TestFormatI3BarBlocks_ColorGreen(t *testing.T) { + sessions := []SessionInfo{ + {Cwd: "/a/b/x", State: "Working"}, + {Cwd: "/a/b/y", State: "Idle"}, + } + blocks := formatI3BarBlocks(sessions) + if blocks[0].Color != "#00ff00" { + t.Errorf("color = %q, want #00ff00", blocks[0].Color) + } +} + +func TestFormatI3BarBlocks_UsesLabel(t *testing.T) { + sessions := []SessionInfo{ + {Cwd: "/home/pierre/Code/auth-service", Label: "auth", State: "Working"}, + } + blocks := formatI3BarBlocks(sessions) + want := "vmux: all working (1)" + if blocks[0].FullText != want { + t.Errorf("full_text = %q, want %q", blocks[0].FullText, want) + } +} + +func TestFormatI3BarBlocks_UsesLabelInMixed(t *testing.T) { + sessions := []SessionInfo{ + {Cwd: "/home/pierre/Code/auth-service", Label: "auth", State: "Needs Input"}, + } + blocks := formatI3BarBlocks(sessions) + want := "vmux: auth[!]" + if blocks[0].FullText != want { + t.Errorf("full_text = %q, want %q", blocks[0].FullText, want) + } +} + +func TestFormatI3BarBlocks_UsesCwdBase(t *testing.T) { + sessions := []SessionInfo{ + {Cwd: "/home/pierre/Code/vibe/vmux", State: "Needs Input"}, + } + blocks := formatI3BarBlocks(sessions) + want := "vmux: vmux[!]" + if blocks[0].FullText != want { + t.Errorf("full_text = %q, want %q", blocks[0].FullText, want) + } +} + +func TestFormatI3BarBlocks_NeedsInputMarker(t *testing.T) { + sessions := []SessionInfo{ + {Cwd: "/a/proj", State: "Needs Input"}, + } + blocks := formatI3BarBlocks(sessions) + want := "vmux: proj[!]" + if blocks[0].FullText != want { + t.Errorf("full_text = %q, want %q", blocks[0].FullText, want) + } +} + +func TestFormatI3BarBlocks_IdleMarker(t *testing.T) { + sessions := []SessionInfo{ + {Cwd: "/a/proj", State: "Idle"}, + {Cwd: "/a/other", State: "Needs Input"}, + } + blocks := formatI3BarBlocks(sessions) + want := "vmux: proj[I] other[!]" + if blocks[0].FullText != want { + t.Errorf("full_text = %q, want %q", blocks[0].FullText, want) + } +}