From 0b936c940fde9fc91b68892d5c03b44e26616e2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Mon, 5 Oct 2026 14:38:18 -0400 Subject: [PATCH] Terminal & Code: components 321-340 --- breakpoint_list.go | 126 ++++++ breakpoint_list_test.go | 114 ++++++ call_stack.go | 140 +++++++ call_stack_test.go | 104 +++++ code_shared.go | 176 +++++++++ code_viewer.go | 253 ++++++++++++ code_viewer_test.go | 151 ++++++++ command_history.go | 119 ++++++ command_history_test.go | 117 ++++++ completion_menu.go | 129 +++++++ completion_menu_test.go | 103 +++++ debug_toolbar.go | 141 +++++++ debug_toolbar_test.go | 113 ++++++ docs/components/321-terminal.md | 52 +++ docs/components/322-terminal-tabs.md | 52 +++ docs/components/323-terminal-search.md | 48 +++ docs/components/324-command-history.md | 50 +++ docs/components/325-log-viewer.md | 48 +++ docs/components/326-process-list.md | 50 +++ docs/components/327-code-viewer.md | 46 +++ docs/components/328-minimap.md | 45 +++ docs/components/329-find-widget.md | 51 +++ docs/components/330-search-panel.md | 51 +++ docs/components/331-problems-panel.md | 49 +++ docs/components/332-output-panel.md | 48 +++ docs/components/333-symbol-outline.md | 53 +++ docs/components/334-completion-menu.md | 50 +++ docs/components/335-debug-toolbar.md | 48 +++ docs/components/336-breakpoint-list.md | 49 +++ docs/components/337-call-stack.md | 52 +++ docs/components/338-variables-panel.md | 49 +++ docs/components/339-hex-viewer.md | 44 +++ docs/components/340-flamegraph.md | 50 +++ example_code_test.go | 276 +++++++++++++ examples/gallery/pages_code.go | 244 +++++++++++- examples/gallery/pages_code_debug.go | 238 ++++++++++++ examples/gallery/pages_code_snippets.go | 230 +++++++++++ examples/gallery/pages_code_tools.go | 264 +++++++++++++ find_widget.go | 181 +++++++++ find_widget_test.go | 117 ++++++ flamegraph.go | 276 +++++++++++++ flamegraph_test.go | 143 +++++++ hex_viewer.go | 157 ++++++++ hex_viewer_test.go | 115 ++++++ log_viewer.go | 130 +++++++ log_viewer_test.go | 129 +++++++ messages_code.go | 101 +++++ minimap.go | 134 +++++++ minimap_test.go | 116 ++++++ output_panel.go | 93 +++++ output_panel_test.go | 118 ++++++ problems_panel.go | 194 ++++++++++ problems_panel_test.go | 106 +++++ process_list.go | 185 +++++++++ process_list_test.go | 124 ++++++ search_panel.go | 202 ++++++++++ search_panel_test.go | 129 +++++++ symbol_outline.go | 235 ++++++++++++ symbol_outline_test.go | 107 ++++++ terminal.go | 308 +++++++++++++++ terminal_search.go | 77 ++++ terminal_search_test.go | 105 +++++ terminal_state.go | 361 ++++++++++++++++++ terminal_tabs.go | 181 +++++++++ terminal_tabs_test.go | 122 ++++++ terminal_test.go | 192 ++++++++++ .../snapshots/darwin/breakpoint_list-dark.png | Bin 0 -> 8797 bytes .../darwin/breakpoint_list-light.png | Bin 0 -> 8592 bytes testdata/snapshots/darwin/call_stack-dark.png | Bin 0 -> 16607 bytes .../snapshots/darwin/call_stack-light.png | Bin 0 -> 15794 bytes .../snapshots/darwin/code_viewer-dark.png | Bin 0 -> 16193 bytes .../snapshots/darwin/code_viewer-light.png | Bin 0 -> 15572 bytes .../snapshots/darwin/command_history-dark.png | Bin 0 -> 13577 bytes .../darwin/command_history-light.png | Bin 0 -> 13177 bytes .../snapshots/darwin/completion_menu-dark.png | Bin 0 -> 14386 bytes .../darwin/completion_menu-light.png | Bin 0 -> 15566 bytes .../snapshots/darwin/debug_toolbar-dark.png | Bin 0 -> 2158 bytes .../snapshots/darwin/debug_toolbar-light.png | Bin 0 -> 2145 bytes .../snapshots/darwin/find_widget-dark.png | Bin 0 -> 9014 bytes .../snapshots/darwin/find_widget-light.png | Bin 0 -> 8713 bytes testdata/snapshots/darwin/flamegraph-dark.png | Bin 0 -> 13502 bytes .../snapshots/darwin/flamegraph-light.png | Bin 0 -> 13200 bytes testdata/snapshots/darwin/hex_viewer-dark.png | Bin 0 -> 15629 bytes .../snapshots/darwin/hex_viewer-light.png | Bin 0 -> 14906 bytes testdata/snapshots/darwin/log_viewer-dark.png | Bin 0 -> 27349 bytes .../snapshots/darwin/log_viewer-light.png | Bin 0 -> 26128 bytes testdata/snapshots/darwin/minimap-dark.png | Bin 0 -> 1549 bytes testdata/snapshots/darwin/minimap-light.png | Bin 0 -> 1589 bytes .../snapshots/darwin/output_panel-dark.png | Bin 0 -> 7462 bytes .../snapshots/darwin/output_panel-light.png | Bin 0 -> 7084 bytes .../snapshots/darwin/problems_panel-dark.png | Bin 0 -> 23450 bytes .../snapshots/darwin/problems_panel-light.png | Bin 0 -> 22802 bytes .../snapshots/darwin/process_list-dark.png | Bin 0 -> 15261 bytes .../snapshots/darwin/process_list-light.png | Bin 0 -> 14823 bytes .../snapshots/darwin/search_panel-dark.png | Bin 0 -> 21859 bytes .../snapshots/darwin/search_panel-light.png | Bin 0 -> 21072 bytes .../snapshots/darwin/symbol_outline-dark.png | Bin 0 -> 13607 bytes .../snapshots/darwin/symbol_outline-light.png | Bin 0 -> 13268 bytes testdata/snapshots/darwin/terminal-dark.png | Bin 0 -> 11844 bytes testdata/snapshots/darwin/terminal-light.png | Bin 0 -> 11584 bytes .../snapshots/darwin/terminal_search-dark.png | Bin 0 -> 8269 bytes .../darwin/terminal_search-light.png | Bin 0 -> 7606 bytes .../snapshots/darwin/terminal_tabs-dark.png | Bin 0 -> 5920 bytes .../snapshots/darwin/terminal_tabs-light.png | Bin 0 -> 5571 bytes .../snapshots/darwin/variables_panel-dark.png | Bin 0 -> 8891 bytes .../darwin/variables_panel-light.png | Bin 0 -> 8556 bytes variables_panel.go | 158 ++++++++ variables_panel_test.go | 115 ++++++ 108 files changed, 8733 insertions(+), 1 deletion(-) create mode 100644 breakpoint_list.go create mode 100644 breakpoint_list_test.go create mode 100644 call_stack.go create mode 100644 call_stack_test.go create mode 100644 code_shared.go create mode 100644 code_viewer.go create mode 100644 code_viewer_test.go create mode 100644 command_history.go create mode 100644 command_history_test.go create mode 100644 completion_menu.go create mode 100644 completion_menu_test.go create mode 100644 debug_toolbar.go create mode 100644 debug_toolbar_test.go create mode 100644 docs/components/321-terminal.md create mode 100644 docs/components/322-terminal-tabs.md create mode 100644 docs/components/323-terminal-search.md create mode 100644 docs/components/324-command-history.md create mode 100644 docs/components/325-log-viewer.md create mode 100644 docs/components/326-process-list.md create mode 100644 docs/components/327-code-viewer.md create mode 100644 docs/components/328-minimap.md create mode 100644 docs/components/329-find-widget.md create mode 100644 docs/components/330-search-panel.md create mode 100644 docs/components/331-problems-panel.md create mode 100644 docs/components/332-output-panel.md create mode 100644 docs/components/333-symbol-outline.md create mode 100644 docs/components/334-completion-menu.md create mode 100644 docs/components/335-debug-toolbar.md create mode 100644 docs/components/336-breakpoint-list.md create mode 100644 docs/components/337-call-stack.md create mode 100644 docs/components/338-variables-panel.md create mode 100644 docs/components/339-hex-viewer.md create mode 100644 docs/components/340-flamegraph.md create mode 100644 example_code_test.go create mode 100644 examples/gallery/pages_code_debug.go create mode 100644 examples/gallery/pages_code_snippets.go create mode 100644 examples/gallery/pages_code_tools.go create mode 100644 find_widget.go create mode 100644 find_widget_test.go create mode 100644 flamegraph.go create mode 100644 flamegraph_test.go create mode 100644 hex_viewer.go create mode 100644 hex_viewer_test.go create mode 100644 log_viewer.go create mode 100644 log_viewer_test.go create mode 100644 messages_code.go create mode 100644 minimap.go create mode 100644 minimap_test.go create mode 100644 output_panel.go create mode 100644 output_panel_test.go create mode 100644 problems_panel.go create mode 100644 problems_panel_test.go create mode 100644 process_list.go create mode 100644 process_list_test.go create mode 100644 search_panel.go create mode 100644 search_panel_test.go create mode 100644 symbol_outline.go create mode 100644 symbol_outline_test.go create mode 100644 terminal.go create mode 100644 terminal_search.go create mode 100644 terminal_search_test.go create mode 100644 terminal_state.go create mode 100644 terminal_tabs.go create mode 100644 terminal_tabs_test.go create mode 100644 terminal_test.go create mode 100644 testdata/snapshots/darwin/breakpoint_list-dark.png create mode 100644 testdata/snapshots/darwin/breakpoint_list-light.png create mode 100644 testdata/snapshots/darwin/call_stack-dark.png create mode 100644 testdata/snapshots/darwin/call_stack-light.png create mode 100644 testdata/snapshots/darwin/code_viewer-dark.png create mode 100644 testdata/snapshots/darwin/code_viewer-light.png create mode 100644 testdata/snapshots/darwin/command_history-dark.png create mode 100644 testdata/snapshots/darwin/command_history-light.png create mode 100644 testdata/snapshots/darwin/completion_menu-dark.png create mode 100644 testdata/snapshots/darwin/completion_menu-light.png create mode 100644 testdata/snapshots/darwin/debug_toolbar-dark.png create mode 100644 testdata/snapshots/darwin/debug_toolbar-light.png create mode 100644 testdata/snapshots/darwin/find_widget-dark.png create mode 100644 testdata/snapshots/darwin/find_widget-light.png create mode 100644 testdata/snapshots/darwin/flamegraph-dark.png create mode 100644 testdata/snapshots/darwin/flamegraph-light.png create mode 100644 testdata/snapshots/darwin/hex_viewer-dark.png create mode 100644 testdata/snapshots/darwin/hex_viewer-light.png create mode 100644 testdata/snapshots/darwin/log_viewer-dark.png create mode 100644 testdata/snapshots/darwin/log_viewer-light.png create mode 100644 testdata/snapshots/darwin/minimap-dark.png create mode 100644 testdata/snapshots/darwin/minimap-light.png create mode 100644 testdata/snapshots/darwin/output_panel-dark.png create mode 100644 testdata/snapshots/darwin/output_panel-light.png create mode 100644 testdata/snapshots/darwin/problems_panel-dark.png create mode 100644 testdata/snapshots/darwin/problems_panel-light.png create mode 100644 testdata/snapshots/darwin/process_list-dark.png create mode 100644 testdata/snapshots/darwin/process_list-light.png create mode 100644 testdata/snapshots/darwin/search_panel-dark.png create mode 100644 testdata/snapshots/darwin/search_panel-light.png create mode 100644 testdata/snapshots/darwin/symbol_outline-dark.png create mode 100644 testdata/snapshots/darwin/symbol_outline-light.png create mode 100644 testdata/snapshots/darwin/terminal-dark.png create mode 100644 testdata/snapshots/darwin/terminal-light.png create mode 100644 testdata/snapshots/darwin/terminal_search-dark.png create mode 100644 testdata/snapshots/darwin/terminal_search-light.png create mode 100644 testdata/snapshots/darwin/terminal_tabs-dark.png create mode 100644 testdata/snapshots/darwin/terminal_tabs-light.png create mode 100644 testdata/snapshots/darwin/variables_panel-dark.png create mode 100644 testdata/snapshots/darwin/variables_panel-light.png create mode 100644 variables_panel.go create mode 100644 variables_panel_test.go diff --git a/breakpoint_list.go b/breakpoint_list.go new file mode 100644 index 0000000..1aab2b1 --- /dev/null +++ b/breakpoint_list.go @@ -0,0 +1,126 @@ +package mujica + +import ( + "fmt" + "log/slog" + "path" + + "github.com/egoist/mygo/ui" + + "mujicaui/icons" + "mujicaui/internal" + "mujicaui/theme" +) + +// Breakpoint stops at File:Line when Condition holds. +type Breakpoint struct { + ID string + File string + Line int + Enabled bool + Condition string +} + +// BreakpointListOptions configure a BreakpointList. +type BreakpointListOptions struct { + Disabled bool +} + +// BreakpointListResult fields hold breakpoint IDs. +type BreakpointListResult struct { + Element *ui.Element + Toggled, Edited, Opened string +} + +type breakpointEdit struct{ id, draft string } + +// BreakpointList lists breakpoints with toggles and conditions. +func BreakpointList(c *ui.Context, bps *[]Breakpoint, opts BreakpointListOptions) BreakpointListResult { + if bps == nil { + panic("mujica: BreakpointList needs breakpoints") + } + seen := map[string]bool{} + for _, b := range *bps { + if b.ID == "" || seen[b.ID] { + panic("mujica: Breakpoint IDs must be set and unique") + } + seen[b.ID] = true + } + k := Tokens(c) + d := Density(c) + u := d.Unit() + var res BreakpointListResult + res.Element = codePanel(c).Disabled(opts.Disabled).Label(msg(c, "breakpoint_list.label")) + edit := ui.Local(res.Element, "breakpoint_list.edit", func() breakpointEdit { return breakpointEdit{} }) + disabled := res.Element.IsDisabled() + if disabled { + edit.id = "" + } + res.Element.Children(func() { + if len(*bps) == 0 { + codeCaption(c, msg(c, "breakpoint_list.empty")).Padding(u * 4) + return + } + ScrollArea(c, nil, ScrollAreaOptions{}, func() { + for i := range *bps { + b := &(*bps)[i] + ui.Column(c).Key(b.ID).Padding(u, u*2).Gap(u).BorderWidth(0, 0, theme.BorderWidth, 0).BorderColor(k.Border).Children(func() { + ui.Row(c).MinHeight(d.ControlHeight()).Gap(u * 2).AlignItems(ui.Center).Children(func() { + on := b.Enabled + Checkbox(c, &on, "", CheckboxOptions{Disabled: opts.Disabled}).Label(fmt.Sprintf(msg(c, "breakpoint_list.enable"), path.Base(b.File), b.Line)) + if on != b.Enabled && !disabled { + b.Enabled, res.Toggled = on, b.ID + slog.Debug("mujica: breakpoint_list toggle", "id", b.ID, "enabled", on) + } + if breakpointLocation(c, *b).Clicked() && !disabled { + res.Opened = b.ID + slog.Debug("mujica: breakpoint_list open", "id", b.ID) + } + if iconAction(c, icons.Must("funnel"), fmt.Sprintf(msg(c, "breakpoint_list.edit"), path.Base(b.File), b.Line)).Clicked() && !disabled { + *edit = breakpointEdit{b.ID, b.Condition} + } + }) + switch { + case edit.id == b.ID: + in := TextInput(c, &edit.draft, TextInputOptions{Placeholder: msg(c, "breakpoint_list.placeholder"), Label: msg(c, "breakpoint_list.condition")}) + in.Input.AutoFocus() + if in.Input.Shortcut(0, ui.KeyEscape) { + edit.id = "" + } else if in.Submitted() && !disabled { + b.Condition, res.Edited, edit.id = edit.draft, b.ID, "" + slog.Debug("mujica: breakpoint_list condition", "id", b.ID) + } + case b.Condition != "": + codeMono(c, fmt.Sprintf(msg(c, "breakpoint_list.when"), b.Condition)).TextColor(k.Warning).Padding(0, 0, 0, 28) + } + }) + } + }).Grow(1) + }) + return res +} + +// breakpointLocation is a button opening the location. +func breakpointLocation(c *ui.Context, b Breakpoint) *ui.Element { + k := Tokens(c) + d := Density(c) + e := ui.ButtonBase(c).Grow(1).MinWidth(0).MinHeight(theme.MinHitSize).Justify(ui.Start).Gap(d.Unit()*2).Padding(0, d.Unit()). + Radius(theme.ControlRadius).Label(fmt.Sprintf("%s:%d", b.File, b.Line)) + internal.Ring(e, k.Focus, theme.ControlRadius) + if e.Hovered() { + e.Background(k.SurfaceHover) + } + fg := k.Text + if !b.Enabled { + fg = k.TextMuted + } + return e.Children(func() { + // A filled dot is enabled, a ring disabled. + dot := ui.Box(c).Size(10, 10).Radius(5).Shrink(0).Role(ui.RoleNone).Border(theme.BorderWidth, k.Danger) + if b.Enabled { + dot.Background(k.Danger) + } + ui.Text(c, path.Base(b.File)).TextColor(fg).SingleLine() + codeCaption(c, fmt.Sprintf("%s:%d", path.Dir(b.File), b.Line)).MinWidth(0) + }) +} diff --git a/breakpoint_list_test.go b/breakpoint_list_test.go new file mode 100644 index 0000000..0b02a10 --- /dev/null +++ b/breakpoint_list_test.go @@ -0,0 +1,114 @@ +package mujica + +import ( + "testing" + + "github.com/egoist/mygo/ui" +) + +type breakpointProbe struct { + bps []Breakpoint + disabled bool + toggled, edited, opened []string +} + +func (p *breakpointProbe) view(c *ui.Context) { + Use(c, Settings{}) + r := BreakpointList(c, &p.bps, BreakpointListOptions{Disabled: p.disabled}) + r.Element.Height(300) + for _, e := range []struct { + id string + out *[]string + }{{r.Toggled, &p.toggled}, {r.Edited, &p.edited}, {r.Opened, &p.opened}} { + if e.id != "" { + *e.out = append(*e.out, e.id) + } + } +} + +func newBreakpointProbe() *breakpointProbe { + return &breakpointProbe{bps: []Breakpoint{ + {ID: "a", File: "court/seal.go", Line: 12, Enabled: true}, + {ID: "b", File: "court/ledger.go", Line: 40, Enabled: false, Condition: "n > 3"}, + }} +} + +func TestBreakpointListToggleAndOpen(t *testing.T) { + p := newBreakpointProbe() + tt := ui.NewTester(p.view, 520, 320) + if err := tt.Click("Enable ledger.go:40"); err != nil { + t.Fatal(err) + } + if !p.bps[1].Enabled || len(p.toggled) != 1 || p.toggled[0] != "b" { + t.Fatalf("toggle: %+v %v", p.bps[1], p.toggled) + } + if err := tt.Click("court/seal.go:12"); err != nil { + t.Fatal(err) + } + if len(p.opened) != 1 || p.opened[0] != "a" { + t.Fatalf("opened %v", p.opened) + } + if !tt.HasText("when n > 3") { + t.Fatalf("condition not shown: %q", tt.Texts()) + } +} + +func TestBreakpointListEditCondition(t *testing.T) { + p := newBreakpointProbe() + tt := ui.NewTester(p.view, 520, 320) + if err := tt.Click("Edit condition of seal.go:12"); err != nil { + t.Fatal(err) + } + if !tt.Focused("Condition") { + t.Fatal("editor did not take the focus") + } + tt.Type("i == 7") + tt.Key(0, ui.KeyEnter) + if p.bps[0].Condition != "i == 7" || len(p.edited) != 1 { + t.Fatalf("condition %q edited %v", p.bps[0].Condition, p.edited) + } + _ = tt.Click("Edit condition of ledger.go:40") + tt.Type("x") + tt.Key(0, ui.KeyEscape) + if p.bps[1].Condition != "n > 3" { + t.Fatalf("Escape kept %q", p.bps[1].Condition) + } +} + +func TestBreakpointListKeyboardToggle(t *testing.T) { + p := newBreakpointProbe() + tt := ui.NewTester(p.view, 520, 320) + tt.Key(0, ui.KeyTab) + tt.Key(0, ui.KeySpace) + if p.bps[0].Enabled { + t.Fatal("Space did not disable the first breakpoint") + } +} + +func TestBreakpointListDisabledAndDuplicate(t *testing.T) { + p := newBreakpointProbe() + p.disabled = true + tt := ui.NewTester(p.view, 520, 320) + _ = tt.Click("Enable ledger.go:40") + _ = tt.Click("court/seal.go:12") + if p.bps[1].Enabled || len(p.opened) != 0 { + t.Fatal("disabled list changed") + } + defer func() { + if recover() == nil { + t.Fatal("duplicate IDs did not panic") + } + }() + p = newBreakpointProbe() + p.bps[1].ID = "a" + ui.NewTester(p.view, 520, 320) +} + +func TestBreakpointListSnapshot(t *testing.T) { + snapshot(t, "breakpoint_list", 460, 170, func(c *ui.Context) { + p := ui.Local(c.Root(), "snap", newBreakpointProbe) + ui.Column(c).Padding(12).Children(func() { + BreakpointList(c, &(*p).bps, BreakpointListOptions{}).Element.Height(146) + }) + }) +} diff --git a/call_stack.go b/call_stack.go new file mode 100644 index 0000000..4a060d5 --- /dev/null +++ b/call_stack.go @@ -0,0 +1,140 @@ +package mujica + +import ( + "fmt" + "log/slog" + "path" + + "github.com/egoist/mygo/ui" + + "mujicaui/icons" +) + +// StackFrame is one call of a thread's stack, innermost first. +type StackFrame struct { + ID, Name, File string + Line int +} + +// DebugThread is a thread and its stack. +type DebugThread struct { + ID, Name string + Frames []StackFrame +} + +// CallStackState is the current frame and folded threads. +type CallStackState struct { + // Current is the chosen frame's ID. + Current string + // Collapsed holds folded thread IDs. + Collapsed ui.Selection[string] + rows ui.ListState + selected int + flat []callRow + seen string + folds int + started bool +} + +type callRow struct{ thread, frame int } + +// CallStackOptions configure a CallStack. +type CallStackOptions struct { + Disabled bool +} + +// CallStack lists frames by thread; choosing sets Current. +func CallStack(c *ui.Context, threads []DebugThread, s *CallStackState, opts CallStackOptions) ChoiceResult { + if s == nil { + panic("mujica: CallStack needs a state") + } + s.flat = s.flat[:0] + at := -1 + for ti, t := range threads { + s.flat = append(s.flat, callRow{ti, -1}) + if s.Collapsed.Has(t.ID) { + continue + } + for fi, f := range t.Frames { + if f.ID == s.Current { + at = len(s.flat) + } + s.flat = append(s.flat, callRow{ti, fi}) + } + } + // Choose the row again after outside changes. + if !s.started || s.seen != s.Current || s.folds != s.Collapsed.Len() || s.selected >= len(s.flat) { + s.started, s.seen, s.selected, s.folds = true, s.Current, at, s.Collapsed.Len() + } + d := Density(c) + var res ChoiceResult + res.Element = codePanel(c).Disabled(opts.Disabled) + disabled := res.Element.IsDisabled() + res.Element.Children(func() { + if len(threads) == 0 { + codeCaption(c, msg(c, "call_stack.empty")).Padding(d.Unit() * 4) + return + } + was := s.selected + s.rows.Selected = &s.selected + s.rows.Key = func(i int) any { return s.flat[i] } + s.rows.Label = func(i int) string { return callLabel(threads, s.flat[i]) } + list := codeRows(c, &s.rows, len(s.flat), func(i int) { callRowView(c, threads, s, s.flat[i], disabled) }) + if disabled { + s.selected = was + return + } + if s.selected < 0 || s.selected >= len(s.flat) { + return + } + r := s.flat[s.selected] + t := threads[r.thread] + switch { + case r.frame >= 0 && s.selected != was: + s.Current = t.Frames[r.frame].ID + s.seen, res.changed = s.Current, true + slog.Debug("mujica: call_stack frame", "thread", t.ID, "depth", r.frame) + case r.frame < 0 && (list.Submitted() || + !s.Collapsed.Has(t.ID) && list.Shortcut(0, ui.KeyLeft) || s.Collapsed.Has(t.ID) && list.Shortcut(0, ui.KeyRight)): + callFold(s, t.ID) + } + }) + return res +} + +func callFold(s *CallStackState, id string) { + if s.Collapsed.Has(id) { + s.Collapsed.Remove(id) + } else { + s.Collapsed.Add(id) + } + slog.Debug("mujica: call_stack fold", "thread", id, "folded", s.Collapsed.Has(id)) +} + +func callLabel(threads []DebugThread, r callRow) string { + t := threads[r.thread] + if r.frame < 0 { + return t.Name + } + f := t.Frames[r.frame] + return fmt.Sprintf("%s %s:%d", f.Name, path.Base(f.File), f.Line) +} + +func callRowView(c *ui.Context, threads []DebugThread, s *CallStackState, r callRow, disabled bool) { + t := threads[r.thread] + if r.frame < 0 { + chev, key := "chevron-down", "search_panel.fold" + if s.Collapsed.Has(t.ID) { + chev, key = "chevron-right", "search_panel.unfold" + } + if iconAction(c, icons.Must(chev), fmt.Sprintf(msg(c, key), t.Name)).Clicked() && !disabled { + callFold(s, t.ID) + } + ui.Text(c, t.Name).FontWeight(600).SingleLine().Grow(1).MinWidth(0) + codeCaption(c, fmt.Sprintf(msg(c, "call_stack.frames"), len(t.Frames))).Shrink(0) + return + } + f := t.Frames[r.frame] + codeMono(c, f.Name).Grow(1).MinWidth(0).Margin(0, 0, 0, 20) + codeCaption(c, fmt.Sprintf("%s:%d", path.Base(f.File), f.Line)).Shrink(0) +} diff --git a/call_stack_test.go b/call_stack_test.go new file mode 100644 index 0000000..ff3610e --- /dev/null +++ b/call_stack_test.go @@ -0,0 +1,104 @@ +package mujica + +import ( + "testing" + + "github.com/egoist/mygo/ui" +) + +func callStackSample() []DebugThread { + return []DebugThread{ + {ID: "t1", Name: "main", Frames: []StackFrame{ + {"f1", "court.Seal", "court/seal.go", 14}, + {"f2", "court.Decree", "court/decree.go", 88}, + {"f3", "main.main", "main.go", 9}, + }}, + {ID: "t2", Name: "herald worker", Frames: []StackFrame{{"f4", "herald.Run", "herald/run.go", 31}}}, + } +} + +type callProbe struct { + threads []DebugThread + s CallStackState + disabled bool + changed int +} + +func (p *callProbe) view(c *ui.Context) { + Use(c, Settings{}) + r := CallStack(c, p.threads, &p.s, CallStackOptions{Disabled: p.disabled}) + r.Element.Height(320) + if r.Changed() { + p.changed++ + } +} + +func TestCallStackChooseFrame(t *testing.T) { + p := &callProbe{threads: callStackSample(), s: CallStackState{Current: "f1"}} + tt := ui.NewTester(p.view, 460, 340) + if p.s.selected != 1 { + t.Fatalf("current frame row %d, want 1", p.s.selected) + } + if err := tt.Click("court.Decree"); err != nil { + t.Fatal(err) + } + if p.s.Current != "f2" || p.changed != 1 { + t.Fatalf("current %q changed %d", p.s.Current, p.changed) + } + tt.Key(0, ui.KeyDown) + tt.Key(0, ui.KeyDown) + if p.s.Current != "f3" || p.s.selected != 4 { + t.Fatalf("keys: current %q row %d", p.s.Current, p.s.selected) + } + tt.Key(0, ui.KeyDown) + tt.Key(0, ui.KeyDown) + if p.s.Current != "f4" { + t.Fatalf("keys did not pass the thread header: %q", p.s.Current) + } +} + +func TestCallStackFoldThread(t *testing.T) { + p := &callProbe{threads: callStackSample(), s: CallStackState{Current: "f4"}} + tt := ui.NewTester(p.view, 460, 340) + if err := tt.Click("Fold main"); err != nil { + t.Fatal(err) + } + if tt.HasText("court.Seal") || !tt.HasText("herald.Run") || p.s.Current != "f4" { + t.Fatalf("fold: %q current %q", tt.Texts(), p.s.Current) + } + if !tt.HasText("depth 3") { + t.Fatal("thread header lacks its count") + } +} + +func TestCallStackCallerSetsCurrent(t *testing.T) { + p := &callProbe{threads: callStackSample()} + tt := ui.NewTester(p.view, 460, 340) + p.s.Current = "f3" + tt.Frame() + if p.s.selected != 3 || p.changed != 0 { + t.Fatalf("row %d changed %d", p.s.selected, p.changed) + } +} + +func TestCallStackDisabledAndEmpty(t *testing.T) { + p := &callProbe{threads: callStackSample(), s: CallStackState{Current: "f1"}, disabled: true} + tt := ui.NewTester(p.view, 460, 340) + _ = tt.Click("court.Decree") + if p.s.Current != "f1" { + t.Fatal("disabled stack changed frame") + } + tt = ui.NewTester((&callProbe{}).view, 460, 340) + if !tt.HasText("Not paused") { + t.Fatalf("texts %q", tt.Texts()) + } +} + +func TestCallStackSnapshot(t *testing.T) { + snapshot(t, "call_stack", 420, 230, func(c *ui.Context) { + p := ui.Local(c.Root(), "snap", func() *callProbe { return &callProbe{threads: callStackSample(), s: CallStackState{Current: "f2"}} }) + ui.Column(c).Padding(12).Children(func() { + CallStack(c, (*p).threads, &(*p).s, CallStackOptions{}).Element.Height(206) + }) + }) +} diff --git a/code_shared.go b/code_shared.go new file mode 100644 index 0000000..9a99563 --- /dev/null +++ b/code_shared.go @@ -0,0 +1,176 @@ +package mujica + +import ( + "math" + "strings" + + "github.com/egoist/mygo/ui" + + "mujicaui/internal" + "mujicaui/theme" +) + +// codeSize is the design size of code and terminal text. +const codeSize float32 = 13 + +// codeFont is the scaled monospace code font. +func codeFont(c *ui.Context) ui.Font { + return ui.Font{Family: theme.MonoFont, Size: FontSize(c, codeSize)} +} + +// codeMono is a single-line monospace text. +func codeMono(c *ui.Context, s string) *ui.Element { + return ui.Text(c, s).Font(theme.MonoFont).FontSize(FontSize(c, codeSize)).SingleLine() +} + +// codeCaption is muted caption text. +func codeCaption(c *ui.Context, s string) *ui.Element { + return ui.Text(c, s).FontSize(FontSize(c, theme.CaptionSize)).TextColor(Tokens(c).TextMuted).SingleLine() +} + +// codeTokenColor colors a syntax token kind. +func codeTokenColor(k theme.Tokens, kind internal.TokenKind) ui.Color { + switch kind { + case internal.TokKeyword: + return k.AccentText + case internal.TokString: + return k.Success + case internal.TokNumber: + return k.Warning + case internal.TokComment: + return k.TextMuted + case internal.TokType: + return k.Info + } + return k.Text +} + +// codeAnsiColor maps a terminal color onto the theme. +func codeAnsiColor(k theme.Tokens, a internal.AnsiColor) ui.Color { + switch a.Kind { + case internal.AnsiRGB: + return ui.RGB(a.R, a.G, a.B) + case internal.AnsiIndexed: + if a.Index < 16 { + base := [8]ui.Color{k.TextMuted, k.Danger, k.Success, k.Warning, k.Info, k.AccentText, k.Info.Mix(k.Success, 0.5), k.Text} + return base[a.Index%8] + } + if a.Index >= 232 { + v := uint8(8 + 10*int(a.Index-232)) + return ui.RGB(v, v, v) + } + i := int(a.Index) - 16 + step := func(n int) uint8 { + if n == 0 { + return 0 + } + return uint8(55 + 40*n) + } + return ui.RGB(step(i/36), step(i/6%6), step(i%6)) + } + return ui.Color{} +} + +// codeAnsiSpans turns ANSI text into themed spans. +func codeAnsiSpans(c *ui.Context, line string) []ui.Span { + k := Tokens(c) + parts, _ := internal.ParseAnsi(line, internal.AnsiStyle{}) + spans := make([]ui.Span, 0, len(parts)) + for _, p := range parts { + s := ui.Span{Text: p.Text, Color: codeAnsiColor(k, p.Style.FG), Underline: p.Style.Underline, Strikethrough: p.Style.Strike, Italic: p.Style.Italic} + if p.Style.Bold { + s.Weight = 700 + } + if bg := codeAnsiColor(k, p.Style.BG); bg.A > 0 { + s.Background = bg.Alpha(0.3) + } + spans = append(spans, s) + } + return spans +} + +// codeMatchSpans marks each query match in text. +func codeMatchSpans(c *ui.Context, text, query string) []ui.Span { + if query == "" { + return []ui.Span{{Text: text}} + } + k := Tokens(c) + lower, q := strings.ToLower(text), strings.ToLower(query) + var spans []ui.Span + for { + i := strings.Index(lower, q) + if i < 0 { + break + } + if i > 0 { + spans = append(spans, ui.Span{Text: text[:i]}) + } + spans = append(spans, ui.Span{Text: text[i : i+len(q)], Background: k.Selection, Underline: true, DecorationColor: k.AccentText, Weight: 600}) + text, lower = text[i+len(q):], lower[i+len(q):] + } + if text != "" { + spans = append(spans, ui.Span{Text: text}) + } + return spans +} + +// codePanel is a framed surface holding a code tool. +func codePanel(c *ui.Context) *ui.Element { + k := Tokens(c) + return ui.Column(c).MinHeight(0).Background(k.Surface).Border(theme.BorderWidth, k.Border).Radius(theme.ControlRadius).Clip() +} + +// codeHeader is a panel's top strip of tools. +func codeHeader(c *ui.Context, fn func()) *ui.Element { + k := Tokens(c) + u := Density(c).Unit() + return ui.Row(c).Shrink(0).MinHeight(theme.MinHitSize+2*u).Padding(u, u*2).Gap(u*2).AlignItems(ui.Center). + BorderWidth(0, 0, theme.BorderWidth, 0).BorderColor(k.Border).Children(fn) +} + +// codeSelectMark draws a chosen row's diamond. +func codeSelectMark(c *ui.Context, on bool) { + k := Tokens(c) + ui.Box(c).Size(8, 8).Shrink(0).Role(ui.RoleNone).Draw(func(p *ui.Painter, r ui.Rect) { + if on { + p.FillPath(internal.Diamond(internal.Snap(p, r.X+r.W/2), internal.Snap(p, r.Y+r.H/2), r.W), k.AccentText) + } + }) +} + +// codeRows builds a growing virtual list with marks. +func codeRows(c *ui.Context, s *ui.ListState, n int, row func(i int)) *ui.Element { + d := Density(c) + restore := dataSelectTheme(c, 0) + defer restore() + e := ui.List(c, s, n, func(i int) { + ui.Row(c).MinHeight(d.ControlHeight()).Padding(0, d.Unit()*2).Gap(d.Unit() * 2).AlignItems(ui.Center).Children(func() { + codeSelectMark(c, s.Selected != nil && *s.Selected == i) + row(i) + }) + }) + k := Tokens(c) + internal.Ring(e, k.Focus, theme.ControlRadius) + return e.Grow(1).MinHeight(d.ControlHeight() * 3) +} + +// codeBadge is a kind letter in a framed square. +func codeBadge(c *ui.Context, letter string, col ui.Color) { + ui.Box(c).Size(18, 18).Shrink(0).Center().Radius(theme.ControlRadius).Border(theme.BorderWidth, col). + Role(ui.RoleNone).Children(func() { + ui.Text(c, letter).Font(theme.MonoFont).FontSize(FontSize(c, 11)).FontWeight(700).TextColor(col) + }) +} + +// codeRule draws a pixel-aligned 1 DIP line. +func codeRule(p *ui.Painter, x0, x1, y float32, c ui.Color) { + s := float64(p.Scale()) + yy := float32((math.Floor(float64(y)*s) + 0.5) / s) + p.Line(x0, yy, x1, yy, 1, c) +} + +// codeTextWidth returns the width of s in the code font. +func codeTextWidth(c *ui.Context, s string) float32 { + w, _ := c.MeasureText(0, ui.Span{Text: s, Font: theme.MonoFont, Size: FontSize(c, codeSize)}) + return w +} diff --git a/code_viewer.go b/code_viewer.go new file mode 100644 index 0000000..cfa9001 --- /dev/null +++ b/code_viewer.go @@ -0,0 +1,253 @@ +package mujica + +import ( + "fmt" + "log/slog" + "runtime" + "slices" + "strings" + + "github.com/egoist/mygo/ui" + + "mujicaui/icons" + "mujicaui/internal" + "mujicaui/theme" +) + +// CodeViewerOptions configure a CodeViewer. +type CodeViewerOptions struct { + // Language picks the highlighting; empty is plain. + Language string + // Label names the view for assistive technology. + Label string + Disabled bool +} + +// CodeViewerState is a CodeViewer's current line, selection, folds and place. +type CodeViewerState struct { + // Selection holds the selected line indices. + Selection ui.Selection[int] + // Folded holds the first lines of folded blocks. + Folded ui.Selection[int] + rows ui.ListState + cursor int + shown []int + inBlock []bool + ends []int + data *string + n int + lang string + folds int + started bool +} + +// Line returns the current line index, or -1. +func (s *CodeViewerState) Line() int { + if s.cursor < 0 || s.cursor >= len(s.shown) { + return -1 + } + return s.shown[s.cursor] +} + +// Visible returns the first and last lines in view. +func (s *CodeViewerState) Visible() (first, last int) { + a, b := s.rows.Visible() + if len(s.shown) == 0 || a < 0 { + return 0, -1 + } + return s.shown[min(a, len(s.shown)-1)], s.shown[min(max(b, a), len(s.shown)-1)] +} + +// ScrollTo brings line to the top. +func (s *CodeViewerState) ScrollTo(line int) { + row, _ := slices.BinarySearch(s.shown, line) + s.rows.ScrollTo(row, ui.Start) +} + +// GoTo makes line current, unfolding and scrolling to it. +func (s *CodeViewerState) GoTo(line int) { + for start := range s.Folded.All() { + if start < line && line <= s.ends[start] { + s.Folded.Remove(start) + } + } + s.folds = -1 + s.Selection.Clear() + s.Selection.Add(line) + s.started, s.cursor = true, -2-line // resolved to a row once the rows are known +} + +// CodeViewer is a read-only, foldable, virtualized code view. +func CodeViewer(c *ui.Context, lines []string, s *CodeViewerState, opts CodeViewerOptions) ChoiceResult { + if s == nil { + panic("mujica: CodeViewer needs a state") + } + if !s.started { + s.started, s.cursor = true, -1 + } + codeViewerPrepare(lines, s, opts.Language) + k := Tokens(c) + d := Density(c) + var res ChoiceResult + res.Element = codePanel(c).Disabled(opts.Disabled) + res.Element.Children(func() { + if len(lines) == 0 { + codeCaption(c, msg(c, "code_viewer.empty")).Padding(d.Unit() * 4) + return + } + digits := len(fmt.Sprint(len(lines))) + gutter := codeTextWidth(c, strings.Repeat("9", digits)) + s.rows.Selected = &s.cursor + s.rows.Selection = &s.Selection + s.rows.Key = func(i int) any { return s.shown[i] } + s.rows.Label = func(i int) string { return fmt.Sprintf("%d %s", s.shown[i]+1, lines[s.shown[i]]) } + restore := dataSelectTheme(c, 0) + list := ui.List(c, &s.rows, len(s.shown), func(i int) { + li := s.shown[i] + ui.Row(c).MinHeight(d.ControlHeight()).Padding(0, d.Unit()*2, 0, 0).Gap(d.Unit() * 2).AlignItems(ui.Center).Children(func() { + codeSelectMark(c, i == s.cursor) + if s.ends[li] > li { + label := msg(c, "code_viewer.fold") + if s.Folded.Has(li) { + label = msg(c, "code_viewer.unfold") + } + chev := "chevron-down" + if s.Folded.Has(li) { + chev = "chevron-right" + } + if iconAction(c, icons.Must(chev), fmt.Sprintf(label, li+1)).Clicked() && !res.Element.IsDisabled() { + codeViewerToggle(s, li) + } + } else { + ui.Box(c).Size(theme.MinHitSize, theme.MinHitSize).Shrink(0) + } + codeMono(c, fmt.Sprint(li+1)).Width(gutter).TextAlign(ui.End).TextColor(k.TextMuted).Shrink(0) + codeViewerLine(c, k, opts.Language, lines[li], s.inBlock[li]) + }) + }) + restore() + list.Grow(1).MinHeight(d.ControlHeight() * 3).Label(opts.Label) + internal.Ring(list, k.Focus, theme.ControlRadius) + if list.Changed() { + res.changed = true + slog.Debug("mujica: code_viewer line", "line", s.Line()) + } + codeViewerKeys(c, list, lines, s) + }) + return res +} + +// codeViewerKeys copies the selection and folds with Left and Right. +func codeViewerKeys(c *ui.Context, list *ui.Element, lines []string, s *CodeViewerState) { + if list.IsDisabled() { + return + } + copyMods := ui.Cmd + if runtime.GOOS != "darwin" { + copyMods = ui.Ctrl + } + line := s.Line() + switch { + case list.Shortcut(copyMods, ui.KeyC): + picked := slices.Sorted(s.Selection.All()) + if len(picked) == 0 && line >= 0 { + picked = []int{line} + } + out := make([]string, len(picked)) + for i, l := range picked { + out[i] = lines[l] + } + c.WriteClipboard(strings.Join(out, "\n")) + slog.Debug("mujica: code_viewer copy", "lines", len(out)) + case list.Shortcut(0, ui.KeyLeft) && line >= 0 && s.ends[line] > line && !s.Folded.Has(line): + codeViewerToggle(s, line) + case list.Shortcut(0, ui.KeyRight) && line >= 0 && s.Folded.Has(line): + codeViewerToggle(s, line) + } +} + +func codeViewerToggle(s *CodeViewerState, line int) { + if s.Folded.Has(line) { + s.Folded.Remove(line) + } else { + s.Folded.Add(line) + } + s.folds = -1 + slog.Debug("mujica: code_viewer fold", "line", line, "folded", s.Folded.Has(line)) +} + +// codeViewerLine draws one line's syntax tokens. +func codeViewerLine(c *ui.Context, k theme.Tokens, lang, line string, inBlock bool) { + toks, _ := internal.Highlight(lang, line, inBlock) + spans := make([]ui.Span, len(toks)) + for i, t := range toks { + spans[i] = ui.Span{Text: t.Text, Color: codeTokenColor(k, t.Kind), Italic: t.Kind == internal.TokComment} + } + ui.RichText(c, spans...).Font(theme.MonoFont).FontSize(FontSize(c, codeSize)).SingleLine().Grow(1).MinWidth(0) +} + +// codeViewerPrepare rescans code and folds when they change. +func codeViewerPrepare(lines []string, s *CodeViewerState, lang string) { + var first *string + if len(lines) > 0 { + first = &lines[0] + } + if s.data != first || s.n != len(lines) || s.lang != lang { + s.data, s.n, s.lang, s.folds = first, len(lines), lang, -1 + s.inBlock = slices.Grow(s.inBlock[:0], len(lines))[:len(lines)] + in := false + for i, l := range lines { + s.inBlock[i] = in + _, in = internal.Highlight(lang, l, in) + } + s.ends = codeViewerFolds(lines, s.ends) + } + if s.folds == s.Folded.Len() { + return + } + s.folds = s.Folded.Len() + s.shown = s.shown[:0] + for i := 0; i < len(lines); i++ { + s.shown = append(s.shown, i) + if s.Folded.Has(i) { + i = s.ends[i] + } + } + if s.cursor <= -2 { + s.cursor = max(slices.Index(s.shown, -2-s.cursor), 0) + s.rows.ScrollIntoView(s.cursor) + } + s.cursor = min(s.cursor, len(s.shown)-1) +} + +// codeViewerFolds returns each line's indented block end. +func codeViewerFolds(lines []string, ends []int) []int { + ends = slices.Grow(ends[:0], len(lines))[:len(lines)] + indent := func(l string) int { + if strings.TrimSpace(l) == "" { + return -1 + } + return len(l) - len(strings.TrimLeft(l, " \t")) + } + type open struct{ line, indent int } + var stack []open + last := -1 + for i, l := range lines { + ends[i] = i + in := indent(l) + if in < 0 { + continue + } + for len(stack) > 0 && stack[len(stack)-1].indent >= in { + top := stack[len(stack)-1] + stack = stack[:len(stack)-1] + ends[top.line] = last + } + stack = append(stack, open{i, in}) + last = i + } + for _, o := range stack { + ends[o.line] = last + } + return ends +} diff --git a/code_viewer_test.go b/code_viewer_test.go new file mode 100644 index 0000000..8e493c2 --- /dev/null +++ b/code_viewer_test.go @@ -0,0 +1,151 @@ +package mujica + +import ( + "fmt" + "strings" + "testing" + + "github.com/egoist/mygo/ui" + + "mujicaui/internal" +) + +var codeViewerSample = []string{ + "package court", + "", + "// Seal stamps a decree.", + "func Seal(d string) int {", + "\tif d == \"\" {", + "\t\treturn 0", + "\t}", + "\treturn 42", + "}", +} + +func codeViewerTester(lines []string, s *CodeViewerState, disabled bool) *ui.Tester { + return ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + CodeViewer(c, lines, s, CodeViewerOptions{Language: "go", Label: "Source", Disabled: disabled}).Element.Height(400) + }, 520, 420) +} + +func TestCodeViewerFoldRanges(t *testing.T) { + ends := codeViewerFolds(codeViewerSample, nil) + if ends[3] != 7 || ends[4] != 5 || ends[0] != 0 || ends[8] != 8 { + t.Fatalf("fold ends %v", ends) + } +} + +func TestCodeViewerFoldsAndNumbers(t *testing.T) { + s := &CodeViewerState{} + tt := codeViewerTester(codeViewerSample, s, false) + if !tt.HasText("9") || !tt.HasText("\treturn 42") { + t.Fatalf("texts %q", tt.Texts()) + } + if err := tt.Click("Fold line 4"); err != nil { + t.Fatal(err) + } + if tt.HasText("\treturn 42") || !tt.HasText("}") || len(s.shown) != 5 { + t.Fatalf("fold kept %d lines: %q", len(s.shown), tt.Texts()) + } + if err := tt.Click("Unfold line 4"); err != nil { + t.Fatal(err) + } + if len(s.shown) != 9 { + t.Fatalf("unfold shows %d lines", len(s.shown)) + } +} + +func TestCodeViewerKeysCurrentLineAndCopy(t *testing.T) { + s := &CodeViewerState{} + tt := codeViewerTester(codeViewerSample, s, false) + for !tt.Focused("Source") { + tt.Key(0, ui.KeyTab) + } + tt.Key(0, ui.KeyDown) + tt.Key(ui.Shift, ui.KeyDown) + tt.Key(ui.Shift, ui.KeyDown) + tt.Key(ui.Cmd, ui.KeyC) + if want := "package court\n\n// Seal stamps a decree."; tt.Clipboard() != want { + t.Fatalf("copied %q", tt.Clipboard()) + } + tt.Key(0, ui.KeyDown) + if s.Line() != 3 { + t.Fatalf("current line %d, want 3", s.Line()) + } + tt.Key(0, ui.KeyLeft) + if !s.Folded.Has(3) || len(s.shown) != 5 { + t.Fatal("Left did not fold the block") + } + tt.Key(0, ui.KeyRight) + if s.Folded.Has(3) { + t.Fatal("Right did not unfold the block") + } +} + +func TestCodeViewerGoToOpensFold(t *testing.T) { + s := &CodeViewerState{} + tt := codeViewerTester(codeViewerSample, s, false) + _ = tt.Click("Fold line 4") + s.GoTo(7) + tt.Frame() + if s.Line() != 7 || s.Folded.Has(3) { + t.Fatalf("GoTo gave line %d folded %v", s.Line(), s.Folded.Has(3)) + } +} + +func TestCodeViewerHighlightColors(t *testing.T) { + ui.NewTester(func(c *ui.Context) { + Use(c, Settings{Mode: Light}) + k := Tokens(c) + kw, str := codeTokenColor(k, internal.TokKeyword), codeTokenColor(k, internal.TokString) + if kw != k.AccentText || str != k.Success { + t.Errorf("keyword %v string %v", kw, str) + } + }, 10, 10) +} + +func TestCodeViewerHundredThousandLines(t *testing.T) { + lines := make([]string, 100_000) + for i := range lines { + lines[i] = fmt.Sprintf("x%d := %d // line", i, i) + } + s := &CodeViewerState{} + tt := codeViewerTester(lines, s, false) + rows := 0 + for _, t := range tt.Texts() { + if strings.HasPrefix(t, "x") { + rows++ + } + } + if rows == 0 || rows > 40 { + t.Fatalf("built %d rows of 100000", rows) + } + s.GoTo(99_998) + tt.Frame() + if !tt.HasText("x99998 := 99998 // line") { + t.Fatal("GoTo did not scroll to the end") + } +} + +func TestCodeViewerDisabledFold(t *testing.T) { + s := &CodeViewerState{} + tt := codeViewerTester(codeViewerSample, s, true) + _ = tt.Click("Fold line 4") + if s.Folded.Len() != 0 { + t.Fatal("disabled viewer folded") + } +} + +func TestCodeViewerSnapshot(t *testing.T) { + snapshot(t, "code_viewer", 480, 300, func(c *ui.Context) { + s := ui.Local(c.Root(), "snap", func() *CodeViewerState { + s := &CodeViewerState{} + s.GoTo(5) + return s + }) + ui.Column(c).Padding(12).Children(func() { + CodeViewer(c, codeViewerSample, *s, CodeViewerOptions{Language: "go"}).Element.Height(276) + }) + }) +} diff --git a/command_history.go b/command_history.go new file mode 100644 index 0000000..d469546 --- /dev/null +++ b/command_history.go @@ -0,0 +1,119 @@ +package mujica + +import ( + "fmt" + "log/slog" + "strings" + "time" + + "github.com/egoist/mygo/ui" + + "mujicaui/icons" +) + +// HistoryEntry is one command a shell ran. +type HistoryEntry struct { + Command string + Time time.Time + ExitCode int +} + +// CommandHistoryState is the filter and place of a CommandHistory. +type CommandHistoryState struct { + Query string + rows ui.ListState + selected int + shown []int + query string + n int + ran bool +} + +// CommandHistoryOptions configure a CommandHistory. +type CommandHistoryOptions struct { + Disabled bool +} + +// CommandHistoryResult carries the entry to replay. +type CommandHistoryResult struct { + Element *ui.Element + // Replayed is the entry to run again, or nil. + Replayed *HistoryEntry +} + +// CommandHistory lists commands newest first with search and replay. +func CommandHistory(c *ui.Context, entries []HistoryEntry, s *CommandHistoryState, opts CommandHistoryOptions) CommandHistoryResult { + if s == nil { + panic("mujica: CommandHistory needs a state") + } + var res CommandHistoryResult + res.Element = codePanel(c).Disabled(opts.Disabled) + disabled := res.Element.IsDisabled() + replay := func(i int) { + if disabled { + return + } + e := entries[i] + res.Replayed = &e + slog.Debug("mujica: command_history replay", "index", i) + } + res.Element.Children(func() { + codeHeader(c, func() { + SearchInput(c, &s.Query, SearchInputOptions{Disabled: opts.Disabled, Placeholder: msg(c, "command_history.search")}).Element.Grow(1) + }) + commandHistoryFilter(entries, s) + if len(s.shown) == 0 { + text := msg(c, "command_history.empty") + if len(entries) > 0 { + text = msg(c, "command_history.none") + } + codeCaption(c, text).Padding(Density(c).Unit() * 4) + return + } + s.rows.Selected = &s.selected + s.rows.Key = func(i int) any { return s.shown[i] } + s.rows.Label = func(i int) string { return entries[s.shown[i]].Command } + list := codeRows(c, &s.rows, len(s.shown), func(i int) { + e := entries[s.shown[i]] + commandHistoryRow(c, e) + if iconAction(c, icons.Must("refresh-cw"), fmt.Sprintf(msg(c, "command_history.replay"), e.Command)).Clicked() { + replay(s.shown[i]) + } + }) + if list.Submitted() && s.selected >= 0 && s.selected < len(s.shown) { + replay(s.shown[s.selected]) + } + }) + return res +} + +// commandHistoryFilter refilters when entries or query change. +func commandHistoryFilter(entries []HistoryEntry, s *CommandHistoryState) { + if s.ran && s.n == len(entries) && s.query == s.Query { + return + } + s.ran, s.n, s.query = true, len(entries), s.Query + s.shown = s.shown[:0] + q := strings.ToLower(strings.TrimSpace(s.Query)) + for i := len(entries) - 1; i >= 0; i-- { + if q == "" || strings.Contains(strings.ToLower(entries[i].Command), q) { + s.shown = append(s.shown, i) + } + } + s.selected = min(s.selected, len(s.shown)-1) +} + +func commandHistoryRow(c *ui.Context, e HistoryEntry) { + k := Tokens(c) + ui.Column(c).Grow(1).MinWidth(0).Children(func() { + codeMono(c, e.Command) + ui.Row(c).Gap(Density(c).Unit() * 2).Children(func() { + if !e.Time.IsZero() { + codeCaption(c, e.Time.Format("15:04:05")) + } + if e.ExitCode != 0 { + codeCaption(c, fmt.Sprintf(msg(c, "command_history.exit"), e.ExitCode)).TextColor(k.Danger) + } + }) + }) +} diff --git a/command_history_test.go b/command_history_test.go new file mode 100644 index 0000000..1f5f599 --- /dev/null +++ b/command_history_test.go @@ -0,0 +1,117 @@ +package mujica + +import ( + "fmt" + "strings" + "testing" + "time" + + "github.com/egoist/mygo/ui" +) + +func commandHistorySample() []HistoryEntry { + at := time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC) + return []HistoryEntry{ + {Command: "git status", Time: at}, + {Command: "go test ./...", Time: at.Add(time.Minute), ExitCode: 1}, + {Command: "go build", Time: at.Add(2 * time.Minute)}, + } +} + +func commandHistoryTester(entries []HistoryEntry, s *CommandHistoryState, disabled bool, got *[]string) *ui.Tester { + return ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + r := CommandHistory(c, entries, s, CommandHistoryOptions{Disabled: disabled}) + r.Element.Grow(1) + if r.Replayed != nil { + *got = append(*got, r.Replayed.Command) + } + }, 420, 320) +} + +func TestCommandHistoryNewestFirstAndReplay(t *testing.T) { + s := &CommandHistoryState{} + var got []string + tt := commandHistoryTester(commandHistorySample(), s, false, &got) + first, _ := tt.Find("go build") + last, _ := tt.Find("git status") + if first.Y >= last.Y { + t.Fatal("newest command is not on top") + } + if !tt.HasText("exit 1") { + t.Fatal("failed command shows no exit code") + } + if err := tt.Click("Run git status again"); err != nil { + t.Fatal(err) + } + tt.ClickAt(first.X, first.Y+2) + tt.ClickAt(first.X, first.Y+2) + if len(got) != 2 || got[0] != "git status" || got[1] != "go build" { + t.Fatalf("replayed %q", got) + } +} + +func TestCommandHistorySearchAndKeys(t *testing.T) { + s := &CommandHistoryState{} + var got []string + tt := commandHistoryTester(commandHistorySample(), s, false, &got) + tt.Key(0, ui.KeyTab) + tt.Type("go ") + if tt.HasText("git status") || !tt.HasText("go build") || !tt.HasText("go test ./...") { + t.Fatalf("filter shows %q", tt.Texts()) + } + tt.Key(0, ui.KeyTab) + tt.Key(0, ui.KeyTab) + tt.Key(0, ui.KeyDown) + tt.Key(0, ui.KeyEnter) + if len(got) != 1 || got[0] != "go test ./..." { + t.Fatalf("keyboard replay %q", got) + } + s.Query = "zzz" + tt.Frame() + if !tt.HasText("No matching commands") { + t.Fatalf("texts %q", tt.Texts()) + } +} + +func TestCommandHistoryDisabledAndEmpty(t *testing.T) { + var got []string + tt := commandHistoryTester(commandHistorySample(), &CommandHistoryState{}, true, &got) + _ = tt.Click("Run git status again") + if len(got) != 0 { + t.Fatalf("disabled replayed %q", got) + } + tt = commandHistoryTester(nil, &CommandHistoryState{}, false, &got) + if !tt.HasText("No commands yet") { + t.Fatalf("texts %q", tt.Texts()) + } +} + +func TestCommandHistoryLargeIsVirtual(t *testing.T) { + entries := make([]HistoryEntry, 100_000) + for i := range entries { + entries[i].Command = fmt.Sprintf("echo %d", i) + } + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + CommandHistory(c, entries, &CommandHistoryState{}, CommandHistoryOptions{}) + }, 420, 320) + rows := 0 + for _, s := range tt.Texts() { + if strings.HasPrefix(s, "echo ") { + rows++ + } + } + if !tt.HasText("echo 99999") || rows == 0 || rows > 60 { + t.Fatalf("built %d rows of 100000", rows) + } +} + +func TestCommandHistorySnapshot(t *testing.T) { + snapshot(t, "command_history", 420, 240, func(c *ui.Context) { + s := ui.Local(c.Root(), "snap", func() *CommandHistoryState { return &CommandHistoryState{} }) + ui.Column(c).Padding(12).Height(240).Children(func() { + CommandHistory(c, commandHistorySample(), *s, CommandHistoryOptions{}).Element.Grow(1) + }) + }) +} diff --git a/completion_menu.go b/completion_menu.go new file mode 100644 index 0000000..60c6ab9 --- /dev/null +++ b/completion_menu.go @@ -0,0 +1,129 @@ +package mujica + +import ( + "log/slog" + + "github.com/egoist/mygo/ui" + + "mujicaui/theme" +) + +// Completion is one candidate of a CompletionMenu. +type Completion struct { + Label string + Kind SymbolKind + Detail string + // Doc describes the chosen candidate below the list. + Doc string +} + +// CompletionMenuState is the highlighted candidate and place of a CompletionMenu. +type CompletionMenuState struct { + Selected int + rows ui.ListState + pending ui.Key +} + +// CompletionMenuOptions configure a CompletionMenu. +type CompletionMenuOptions struct { + Disabled bool +} + +// CompletionMenuResult carries the accepted candidate. +type CompletionMenuResult struct { + Element *ui.Element + Accepted *Completion +} + +// completionRows is how many candidates show before the list scrolls. +const completionRows = 8 + +// CompletionMenu shows candidates under the editor anchor. +func CompletionMenu(c *ui.Context, anchor *ui.Element, open *bool, items []Completion, s *CompletionMenuState, opts CompletionMenuOptions) CompletionMenuResult { + if anchor == nil || open == nil || s == nil { + panic("mujica: CompletionMenu needs an anchor, open and a state") + } + for _, it := range items { + if it.Kind < SymbolFunction || it.Kind > SymbolModule { + panic("mujica: Completion kind out of range") + } + } + var res CompletionMenuResult + if opts.Disabled || anchor.IsDisabled() || len(items) == 0 { + *open = false + } + if !*open { + return res + } + s.Selected = max(0, min(s.Selected, len(items)-1)) + accept := func(i int) { + it := items[i] + res.Accepted = &it + *open = false + slog.Debug("mujica: completion_menu accept", "index", i) + } + move := func(d int) { + s.Selected = (s.Selected + d + len(items)) % len(items) + s.rows.ScrollIntoView(s.Selected) + } + // Take the menu keys before the editor does. + anchor.HandleInput(func(ev ui.InputEvent) bool { + if ev.Kind != ui.InputKeyDown || ev.Mods != 0 || !*open || anchor.IsDisabled() { + return false + } + switch ev.Key { + case ui.KeyDown, ui.KeyUp, ui.KeyTab, ui.KeyEnter, ui.KeyEscape: + s.pending = ev.Key + return true + } + return false + }) + key := s.pending + s.pending = 0 + switch key { + case ui.KeyDown: + move(1) + case ui.KeyUp: + move(-1) + case ui.KeyTab, ui.KeyEnter: + accept(s.Selected) + return res + case ui.KeyEscape: + *open = false + slog.Debug("mujica: completion_menu close") + return res + } + k := Tokens(c) + d := Density(c) + b := anchor.Bounds() + overlayLayer(c, open) + res.Element = ui.PopoverBase(c, anchor, open, func(panel *ui.Element) { + overlayStyle(c, panel, overlayAnchored(c, panel, b)) + panel.Margin(4, 0, 0, 0).Padding(d.Unit()).Width(popoverWide(c, 360)).MaxHeight(popoverRoom(c, b)). + Label(msg(c, "completion_menu.label")).Role(ui.RoleList) + chosen := s.Selected + s.rows.Selected = &chosen + s.rows.Label = func(i int) string { return items[i].Label } + restore := dataSelectTheme(c, 0) + list := ui.List(c, &s.rows, len(items), func(i int) { + it := items[i] + ui.Row(c).MinHeight(d.ControlHeight()).Padding(0, d.Unit()*2).Gap(d.Unit() * 2).AlignItems(ui.Center).Children(func() { + codeSelectMark(c, i == s.Selected) + codeBadge(c, symbolBadges[it.Kind], k.AccentText) + codeMono(c, it.Label).Grow(1).MinWidth(0) + if it.Detail != "" { + codeCaption(c, it.Detail).MinWidth(0).Shrink(1) + } + }) + }).MaxHeight(d.ControlHeight() * completionRows).Shrink(1) + restore() + if list.Changed() && chosen >= 0 { + accept(chosen) + } + if doc := items[s.Selected].Doc; doc != "" { + ui.Text(c, doc).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).Padding(d.Unit()*2). + BorderWidth(theme.BorderWidth, 0, 0, 0).BorderColor(k.Border).Shrink(0) + } + }) + return res +} diff --git a/completion_menu_test.go b/completion_menu_test.go new file mode 100644 index 0000000..1ebe98a --- /dev/null +++ b/completion_menu_test.go @@ -0,0 +1,103 @@ +package mujica + +import ( + "strings" + "testing" + + "github.com/egoist/mygo/ui" +) + +func completionSample() []Completion { + return []Completion{ + {Label: "Seal", Kind: SymbolMethod, Detail: "func(d Decree) error", Doc: "Seal stamps a decree."}, + {Label: "Search", Kind: SymbolFunction, Detail: "func(q string) []Decree"}, + {Label: "Season", Kind: SymbolConstant, Detail: "= 4"}, + } +} + +type completionProbe struct { + text string + open bool + s CompletionMenuState + disabled bool + accepted []string +} + +func (p *completionProbe) view(c *ui.Context) { + Use(c, Settings{}) + ui.Column(c).Padding(16).Children(func() { + in := TextInput(c, &p.text, TextInputOptions{Label: "Code"}) + if in.Changed() { + p.open = strings.HasPrefix(strings.ToLower(p.text), "se") + } + r := CompletionMenu(c, in.Input, &p.open, completionSample(), &p.s, CompletionMenuOptions{Disabled: p.disabled}) + if r.Accepted != nil { + p.accepted = append(p.accepted, r.Accepted.Label) + p.text = r.Accepted.Label + } + }) +} + +func TestCompletionMenuKeys(t *testing.T) { + p := &completionProbe{} + tt := ui.NewTester(p.view, 480, 400) + tt.Key(0, ui.KeyTab) + tt.Type("se") + if !p.open || !tt.HasText("Seal") || !tt.HasText("Seal stamps a decree.") { + t.Fatalf("menu did not open: %q", tt.Texts()) + } + tt.Key(0, ui.KeyDown) + tt.Key(0, ui.KeyDown) + tt.Key(0, ui.KeyDown) + if p.s.Selected != 0 { + t.Fatalf("Down wrapped to %d, want 0", p.s.Selected) + } + tt.Key(0, ui.KeyUp) + tt.Key(0, ui.KeyTab) + if len(p.accepted) != 1 || p.accepted[0] != "Season" || p.open { + t.Fatalf("accepted %v open %v", p.accepted, p.open) + } + if !tt.Focused("Code") { + t.Fatal("accepting moved the focus away from the editor") + } +} + +func TestCompletionMenuEnterClickEscape(t *testing.T) { + p := &completionProbe{} + tt := ui.NewTester(p.view, 480, 400) + tt.Key(0, ui.KeyTab) + tt.Type("se") + tt.Key(0, ui.KeyEnter) + if len(p.accepted) != 1 || p.accepted[0] != "Seal" { + t.Fatalf("Enter accepted %v", p.accepted) + } + p.open = true + tt.Frame() + if err := tt.Click("Search"); err != nil { + t.Fatal(err) + } + if len(p.accepted) != 2 || p.accepted[1] != "Search" { + t.Fatalf("click accepted %v", p.accepted) + } + p.open = true + tt.Frame() + tt.Key(0, ui.KeyEscape) + if p.open || len(p.accepted) != 2 { + t.Fatalf("Escape: open %v accepted %v", p.open, p.accepted) + } +} + +func TestCompletionMenuDisabledClosed(t *testing.T) { + p := &completionProbe{open: true, disabled: true} + tt := ui.NewTester(p.view, 480, 400) + if p.open || tt.HasText("Search") { + t.Fatal("disabled menu shows") + } +} + +func TestCompletionMenuSnapshot(t *testing.T) { + snapshot(t, "completion_menu", 420, 260, func(c *ui.Context) { + p := ui.Local(c.Root(), "snap", func() *completionProbe { return &completionProbe{text: "se", open: true} }) + (*p).view(c) + }) +} diff --git a/debug_toolbar.go b/debug_toolbar.go new file mode 100644 index 0000000..01d88fd --- /dev/null +++ b/debug_toolbar.go @@ -0,0 +1,141 @@ +package mujica + +import ( + "fmt" + "log/slog" + "runtime" + + "github.com/egoist/mygo/ui" +) + +// DebugState is where a debug session is. +type DebugState int + +const ( + DebugStopped DebugState = iota + DebugRunning + DebugPaused +) + +// DebugAction is what the user asked the debugger to do. +type DebugAction int + +const ( + DebugNone DebugAction = iota + // DebugContinue resumes a paused session or starts a stopped one. + DebugContinue + DebugPause + DebugStepOver + DebugStepInto + DebugStepOut + DebugRestart + DebugStop +) + +// DebugToolbarOptions configure a DebugToolbar. +type DebugToolbarOptions struct { + Disabled bool +} + +// DebugToolbarResult carries the requested action. +type DebugToolbarResult struct { + Element *ui.Element + Action DebugAction +} + +// debugIcon draws a toolbar glyph in a 24-unit box. +func debugIcon(body string) *ui.SVG { + return ui.MustParseSVG([]byte(`` + body + ``)) +} + +var debugIcons = map[DebugAction]*ui.SVG{ + DebugContinue: debugIcon(``), + DebugPause: debugIcon(``), + DebugStepOver: debugIcon(``), + DebugStepInto: debugIcon(``), + DebugStepOut: debugIcon(``), + DebugRestart: debugIcon(``), + DebugStop: debugIcon(``), +} + +// debugShortcut is an action's key, as in common IDEs. +type debugShortcut struct { + mods ui.Modifiers + key ui.Key + name string +} + +func debugShortcuts() map[DebugAction]debugShortcut { + cmd := "Ctrl" + if runtime.GOOS == "darwin" { + cmd = "⌘" + } + return map[DebugAction]debugShortcut{ + DebugContinue: {0, ui.KeyF5, "F5"}, + DebugPause: {0, ui.KeyF6, "F6"}, + DebugStepOver: {0, ui.KeyF10, "F10"}, + DebugStepInto: {0, ui.KeyF11, "F11"}, + DebugStepOut: {ui.Shift, ui.KeyF11, "Shift+F11"}, + DebugRestart: {ui.Cmd | ui.Shift, ui.KeyF5, cmd + "+Shift+F5"}, + DebugStop: {ui.Shift, ui.KeyF5, "Shift+F5"}, + } +} + +// DebugEnabled reports whether action applies in state. +func DebugEnabled(state DebugState, a DebugAction) bool { + switch a { + case DebugContinue: + return state != DebugRunning + case DebugPause: + return state == DebugRunning + case DebugStepOver, DebugStepInto, DebugStepOut: + return state == DebugPaused + case DebugRestart, DebugStop: + return state != DebugStopped + } + return false +} + +// DebugToolbar shows debugger actions enabled by state. +func DebugToolbar(c *ui.Context, state DebugState, opts DebugToolbarOptions) DebugToolbarResult { + if state < DebugStopped || state > DebugPaused { + panic("mujica: DebugState out of range") + } + keys := debugShortcuts() + var res DebugToolbarResult + res.Element = Toolbar(c, ToolbarOptions{Label: msg(c, "debug_toolbar.label")}, func() { + first := DebugContinue + if state == DebugRunning { + first = DebugPause + } + for _, a := range []DebugAction{first, DebugStepOver, DebugStepInto, DebugStepOut, DebugRestart, DebugStop} { + name := msg(c, debugKeys[a]) + if a == DebugContinue && state == DebugStopped { + name = msg(c, "debug_toolbar.start") + } + on := DebugEnabled(state, a) && !opts.Disabled + b := ToolbarButton(c, name, ToolbarButtonOptions{Icon: debugIcons[a], Disabled: !on}) + b.Tooltip(fmt.Sprintf("%s (%s)", name, keys[a].name)) + if b.Clicked() && on && !b.IsDisabled() { + res.Action = a + } + } + }).Disabled(opts.Disabled) + if res.Action == DebugNone && !res.Element.IsDisabled() { + for _, a := range []DebugAction{DebugContinue, DebugPause, DebugStepOver, DebugStepInto, DebugStepOut, DebugRestart, DebugStop} { + if k := keys[a]; DebugEnabled(state, a) && c.Shortcut(k.mods, k.key) { + res.Action = a + break + } + } + } + if res.Action != DebugNone { + slog.Debug("mujica: debug_toolbar action", "action", res.Action, "state", state) + } + return res +} + +var debugKeys = map[DebugAction]string{ + DebugContinue: "debug_toolbar.continue", DebugPause: "debug_toolbar.pause", DebugStepOver: "debug_toolbar.stepOver", + DebugStepInto: "debug_toolbar.stepInto", DebugStepOut: "debug_toolbar.stepOut", DebugRestart: "debug_toolbar.restart", DebugStop: "debug_toolbar.stop", +} diff --git a/debug_toolbar_test.go b/debug_toolbar_test.go new file mode 100644 index 0000000..be2afe1 --- /dev/null +++ b/debug_toolbar_test.go @@ -0,0 +1,113 @@ +package mujica + +import ( + "testing" + + "github.com/egoist/mygo/ui" +) + +func debugTester(state *DebugState, disabled bool, got *[]DebugAction) *ui.Tester { + return ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + if a := DebugToolbar(c, *state, DebugToolbarOptions{Disabled: disabled}).Action; a != DebugNone { + *got = append(*got, a) + } + }, 480, 120) +} + +func TestDebugEnabledRules(t *testing.T) { + cases := []struct { + state DebugState + on []DebugAction + }{ + {DebugStopped, []DebugAction{DebugContinue}}, + {DebugRunning, []DebugAction{DebugPause, DebugRestart, DebugStop}}, + {DebugPaused, []DebugAction{DebugContinue, DebugStepOver, DebugStepInto, DebugStepOut, DebugRestart, DebugStop}}, + } + for _, tc := range cases { + n := 0 + for a := DebugContinue; a <= DebugStop; a++ { + if DebugEnabled(tc.state, a) { + n++ + } + } + if n != len(tc.on) { + t.Fatalf("state %d enables %d actions, want %d", tc.state, n, len(tc.on)) + } + for _, a := range tc.on { + if !DebugEnabled(tc.state, a) { + t.Fatalf("state %d disables %d", tc.state, a) + } + } + } +} + +func TestDebugToolbarClicksFollowState(t *testing.T) { + state := DebugPaused + var got []DebugAction + tt := debugTester(&state, false, &got) + for _, name := range []string{"Step over", "Step into", "Step out", "Continue"} { + if err := tt.Click(name); err != nil { + t.Fatal(err) + } + } + want := []DebugAction{DebugStepOver, DebugStepInto, DebugStepOut, DebugContinue} + if len(got) != 4 || got[0] != want[0] || got[3] != want[3] { + t.Fatalf("actions %v", got) + } + state = DebugRunning + tt.Frame() + _ = tt.Click("Step over") + if err := tt.Click("Pause"); err != nil { + t.Fatal(err) + } + if len(got) != 5 || got[4] != DebugPause { + t.Fatalf("running: actions %v", got) + } + state = DebugStopped + tt.Frame() + if _, ok := tt.Find("Start"); !ok { + t.Fatal("stopped toolbar does not offer Start") + } +} + +func TestDebugToolbarShortcuts(t *testing.T) { + state := DebugPaused + var got []DebugAction + tt := debugTester(&state, false, &got) + tt.Key(0, ui.KeyF10) + tt.Key(ui.Shift, ui.KeyF11) + tt.Key(ui.Shift, ui.KeyF5) + state = DebugStopped + tt.Frame() + tt.Key(0, ui.KeyF10) + tt.Key(0, ui.KeyF5) + want := []DebugAction{DebugStepOver, DebugStepOut, DebugStop, DebugContinue} + if len(got) != len(want) { + t.Fatalf("actions %v, want %v", got, want) + } + for i := range want { + if got[i] != want[i] { + t.Fatalf("actions %v, want %v", got, want) + } + } +} + +func TestDebugToolbarDisabled(t *testing.T) { + state := DebugPaused + var got []DebugAction + tt := debugTester(&state, true, &got) + _ = tt.Click("Continue") + tt.Key(0, ui.KeyF10) + if len(got) != 0 { + t.Fatalf("disabled toolbar acted: %v", got) + } +} + +func TestDebugToolbarSnapshot(t *testing.T) { + snapshot(t, "debug_toolbar", 340, 70, func(c *ui.Context) { + ui.Row(c).Padding(12).Children(func() { + DebugToolbar(c, DebugPaused, DebugToolbarOptions{}) + }) + }) +} diff --git a/docs/components/321-terminal.md b/docs/components/321-terminal.md new file mode 100644 index 0000000..9c06f2c --- /dev/null +++ b/docs/components/321-terminal.md @@ -0,0 +1,52 @@ +# 321 Terminal · 终端 + +## 用途 + +显示终端网格:调用方把 PTY 输出写入 `TerminalState.Write`,组件解析 ANSI 样式(`internal.ParseAnsi`)、光标移动与清屏,保留滚动缓冲,支持鼠标选择复制,并把键盘输入编码后交给调用方写回 PTY。PTY 本身由调用方提供。 + +## 最小示例 + +```go +var term mujica.TerminalState +term.Write("\x1b[1m~/court\x1b[0m $ ls\r\n\x1b[34mdecrees\x1b[0m ledger.txt\r\n$ ") +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.Terminal(c, &term, mujica.TerminalOptions{ + Input: func(data string) { fmt.Printf("to pty: %q\n", data) }, + Resized: func(cols, rows int) { fmt.Println("pty size", cols, rows) }, + }).Size(380, 200) +} +ui.Render(view, 400, 300, 1) +``` + +## 参数 + +`func Terminal(c *ui.Context, s *TerminalState, opts TerminalOptions) *ui.Element`。`TerminalState` 零值是 80×24 空屏,由调用方持有;`Write(data)` 写入输出(须在界面线程,例如经 `Window.Update`),`Resize(cols, rows)`、`Lines()`、`LineText(i)`、`Select(a, b)`、`Selection()`、`ScrollBack(n)`、`Offset()`、`Cursor()`、`Size()` 供程序读取与控制。 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `Input` | `func(data string)` | nil | 用户输入编码后的字节:文字原样,Enter 为 `\r`,退格 `\x7f`,方向键 `ESC [ A–D`,Ctrl+字母为控制码。 | +| `Resized` | `func(cols, rows int)` | nil | 网格尺寸随元素大小变化时回调,用于同步 PTY 窗口大小。 | +| `Scrollback` | `int` | 1000 | 滚动缓冲最多保留的行数;负数 panic。 | +| `Label` | `string` | 内置“终端” | 无障碍名称。 | +| `Disabled` | `bool` | false | 不接收任何输入。 | + +## 状态 + +默认(Surface 底、细边框)、聚焦(外侧 2 DIP 焦点环,光标为 AccentText 实心块并反白字符)、未聚焦(光标只描边)、选择(Selection 背景)、回看滚动缓冲(隐藏光标)、输入法组合(光标处显示带下划线的组合文字)、禁用。 + +## 事件 + +`Input`:每次按键、输入文字或粘贴;`Resized`:网格尺寸变化。复制写入系统剪贴板。 + +## 键盘操作 + +- 焦点在终端时,文字、Enter、Backspace、Tab、Shift+Tab、Escape、方向键、Home/End、Delete、PageUp/PageDown 与 Ctrl+字母都发送给 PTY。 +- 复制:macOS 上 Cmd+C,其他平台 Ctrl+Shift+C(复制选中文字);粘贴:Cmd+V / Ctrl+Shift+V。 +- Shift+PageUp / Shift+PageDown:翻看滚动缓冲;滚轮同样可用;任意输入回到底部。 + +## 限制 + +- 状态机覆盖常用序列:SGR 样式、CUU/CUD/CUF/CUB、CHA、CUP、ED(0/1/2/3)、EL、CR/LF/BS/TAB 与 OSC(忽略)。其余 CSI 序列只记录 Debug 日志后忽略;不支持备用屏幕、滚动区域、鼠标上报与宽字符占两格。 +- 需要完整终端仿真时请使用 MyGo 的 `plugins/terminal`(基于 libghostty-vt)。 +- 字形用 `ui.Shape` 逐格排布并按行缓存,等宽字体由系统回退决定。 diff --git a/docs/components/322-terminal-tabs.md b/docs/components/322-terminal-tabs.md new file mode 100644 index 0000000..47e9d2d --- /dev/null +++ b/docs/components/322-terminal-tabs.md @@ -0,0 +1,52 @@ +# 322 TerminalTabs · 终端标签 + +## 用途 + +管理多个终端的标签:切换、重命名、关闭;仍有进程运行(`Busy`)的终端关闭前弹出确认。 + +## 最小示例 + +```go +tabs := []mujica.TerminalTab{{ID: "a", Title: "zsh"}, {ID: "b", Title: "build", Busy: true}} +active := "a" +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + r := mujica.TerminalTabs(c, &tabs, &active, mujica.TerminalTabsOptions{}, func(id string) { + ui.Text(c, "terminal "+id) + }) + if r.Closed != "" { + fmt.Println("close pty", r.Closed) + } +} +ui.Render(view, 400, 300, 1) +``` + +## 参数 + +`func TerminalTabs(c *ui.Context, tabs *[]TerminalTab, active *string, opts TerminalTabsOptions, content func(id string)) TerminalTabsResult`。`*tabs` 与 `*active` 由调用方持有,组件直接修改:重命名改 `Title`,关闭删除元素并把 `active` 移到相邻标签。`active` 不是任何标签 ID 时 panic。 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `TerminalTab.ID` | `string` | 必填 | 稳定标识,也是事件值。 | +| `TerminalTab.Title` | `string` | 必填 | 标签文字。 | +| `TerminalTab.Busy` | `bool` | false | 进程仍在运行:显示小圆点并在关闭前确认。 | +| `Disabled` | `bool` | false | 不响应切换、重命名与关闭。 | +| `content` | `func(id string)` | 必填 | 构建当前标签的内容。 | + +## 状态 + +当前标签(Selection 背景、粗体与底部 AccentText 线)、悬停、键盘焦点环、正在运行(圆点与无障碍描述“进程运行中”)、重命名输入框、关闭确认对话框、无标签时的空状态、禁用。 + +## 事件 + +`Changed`:当前标签变化;`Renamed`:被重命名的标签 ID;`Closed`:被关闭的标签 ID(调用方应关闭对应 PTY)。 + +## 键盘操作 + +- ←/→、Home/End:在标签间移动并切换。 +- F2 或双击:重命名;Enter 确认,Escape 取消。 +- Delete:关闭焦点所在标签;运行中的标签先确认,确认框默认聚焦“取消”。 + +## 限制 + +- 不提供新建按钮与拖动排序;新建由调用方向 `*tabs` 追加。 diff --git a/docs/components/323-terminal-search.md b/docs/components/323-terminal-search.md new file mode 100644 index 0000000..36c840b --- /dev/null +++ b/docs/components/323-terminal-search.md @@ -0,0 +1,48 @@ +# 323 TerminalSearch · 终端搜索 + +## 用途 + +在终端的滚动缓冲与屏幕中查找文字,显示匹配数,上下跳转并把当前匹配滚动到可见处。 + +## 最小示例 + +```go +var term mujica.TerminalState +term.Write("build ok\r\nerror: missing seal\r\n") +var search mujica.TerminalSearchState +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + ui.Column(c).Gap(8).Children(func() { + mujica.TerminalSearch(c, &term, &search, mujica.TerminalSearchOptions{}) + mujica.Terminal(c, &term, mujica.TerminalOptions{}).Height(160) + }) +} +ui.Render(view, 400, 300, 1) +``` + +## 参数 + +`func TerminalSearch(c *ui.Context, term *TerminalState, s *TerminalSearchState, opts TerminalSearchOptions) ChoiceResult`。匹配在查询或终端内容变化时重算一次,不逐帧计算;匹配结果写入 `term`,由同一 `TerminalState` 的 `Terminal` 绘制下划线,当前匹配加框。 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `TerminalSearchState.Query` | `string` | "" | 查找文字,不区分大小写。 | +| `TerminalSearchState.Current` | `int` | 0 | 当前匹配序号,从 0 开始。 | +| `Disabled` | `bool` | false | 不能输入与跳转。 | + +## 状态 + +无查询、有匹配(“第 n 项,共 m 项”)、无匹配(“无匹配”)、禁用;终端中其余匹配为 AccentText 下划线,当前匹配加 AccentText 框。 + +## 事件 + +`Changed()`:查询变化或跳到另一匹配。 + +## 键盘操作 + +- Enter / Shift+Enter:下一项 / 上一项;macOS 上 Cmd+G / Shift+Cmd+G,其他平台 F3 / Shift+F3。 +- 上一项、下一项按钮可用 Tab 到达。 + +## 限制 + +- 只做纯文字匹配,不支持正则与整词;跨行文字不会匹配。 diff --git a/docs/components/324-command-history.md b/docs/components/324-command-history.md new file mode 100644 index 0000000..69be25f --- /dev/null +++ b/docs/components/324-command-history.md @@ -0,0 +1,50 @@ +# 324 CommandHistory · 命令历史 + +## 用途 + +按时间倒序列出执行过的命令,可搜索,并通过回调重新运行。 + +## 最小示例 + +```go +history := []mujica.HistoryEntry{{Command: "git status"}, {Command: "go test ./...", ExitCode: 1}} +var state mujica.CommandHistoryState +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + r := mujica.CommandHistory(c, history, &state, mujica.CommandHistoryOptions{}) + r.Element.Height(240) + if r.Replayed != nil { + fmt.Println("run", r.Replayed.Command) + } +} +ui.Render(view, 400, 300, 1) +``` + +## 参数 + +`func CommandHistory(c *ui.Context, entries []HistoryEntry, s *CommandHistoryState, opts CommandHistoryOptions) CommandHistoryResult`。列表虚拟化,筛选只在条目数或查询变化时重算。 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `HistoryEntry.Command` | `string` | 必填 | 命令文字。 | +| `HistoryEntry.Time` | `time.Time` | 零值不显示 | 执行时间。 | +| `HistoryEntry.ExitCode` | `int` | 0 | 非零时显示“退出码 n”。 | +| `CommandHistoryState.Query` | `string` | "" | 搜索文字,不区分大小写。 | +| `Disabled` | `bool` | false | 不能搜索与重放。 | + +## 状态 + +默认、选中(Selection 背景与菱形标记)、失败命令(Danger 退出码)、无历史、无匹配、禁用。 + +## 事件 + +`Replayed`:要重新运行的条目,没有则为 nil。 + +## 键盘操作 + +- Tab 依次到达搜索框与列表;↑/↓、Home/End 选择;输入字母按命令定位。 +- Enter 或双击:重放当前条目;每行的重放按钮可用 Tab 到达。 + +## 限制 + +- 只追加或整体替换 `entries` 会被识别;原地修改中间条目后请传入新切片。 diff --git a/docs/components/325-log-viewer.md b/docs/components/325-log-viewer.md new file mode 100644 index 0000000..720a92e --- /dev/null +++ b/docs/components/325-log-viewer.md @@ -0,0 +1,48 @@ +# 325 LogViewer · 日志查看器 + +## 用途 + +显示日志:按级别过滤、跟随尾部、关键字高亮,十万行也只构建可见行。 + +## 最小示例 + +```go +lines := []mujica.LogLine{{Level: mujica.LogInfo, Text: "court opened"}, {Level: mujica.LogError, Text: "seal missing"}} +var state mujica.LogViewerState +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.LogViewer(c, lines, &state, mujica.LogViewerOptions{}).Element.Height(240) +} +ui.Render(view, 400, 300, 1) +``` + +## 参数 + +`func LogViewer(c *ui.Context, lines []LogLine, s *LogViewerState, opts LogViewerOptions) ChoiceResult`。新追加的行增量过滤;级别变化时重新过滤一次,不逐帧计算。 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `LogLine.Level` | `LogLevel` | `LogDebug` | `LogDebug`、`LogInfo`、`LogWarn`、`LogError`;越界 panic。 | +| `LogLine.Time`, `Text` | `time.Time`, `string` | — | 时间(零值不显示)与内容。 | +| `LogViewerState.Hidden` | `[4]bool` | 全显示 | 被隐藏的级别。 | +| `LogViewerState.Query` | `string` | "" | 高亮关键字,不区分大小写。 | +| `LogViewerState.Follow` | `bool` | 首次为 true | 跟随尾部;向上滚动后 MyGo 暂停跟随,滚回底部恢复。 | +| `Disabled` | `bool` | false | 不能改过滤与跟随。 | + +## 状态 + +级别按钮按下表示显示;级别同时用文字(DEBUG/INFO/WARN/ERROR)与颜色区分;高亮为 Selection 背景、粗体与下划线;无日志空状态;禁用。 + +## 事件 + +`Changed()`:级别过滤或跟随开关变化。 + +## 键盘操作 + +- Tab 依次到达级别按钮、高亮输入框、跟随按钮与日志列表;Space 切换按钮。 +- 列表中方向键、PageUp/PageDown、Home/End 滚动。 + +## 限制 + +- 只识别向末尾追加;替换为更短的切片会重新过滤。 +- 高亮不过滤行,只标出匹配。 diff --git a/docs/components/326-process-list.md b/docs/components/326-process-list.md new file mode 100644 index 0000000..d0523e5 --- /dev/null +++ b/docs/components/326-process-list.md @@ -0,0 +1,50 @@ +# 326 ProcessList · 进程列表 + +## 用途 + +以表格列出进程,可按名称、PID、CPU、内存排序;结束进程前必须确认,确认后通过结果交给调用方执行。 + +## 最小示例 + +```go +procs := []mujica.Process{{PID: 7, Name: "archivist", CPU: 42, Memory: 50 << 20}, {PID: 9, Name: "herald", CPU: 3, Memory: 8 << 20}} +var state mujica.ProcessListState +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + r := mujica.ProcessList(c, procs, &state, mujica.ProcessListOptions{}) + r.Element.Height(240) + if r.End != nil { + fmt.Println("kill", r.End.PID) + } +} +ui.Render(view, 400, 300, 1) +``` + +## 参数 + +`func ProcessList(c *ui.Context, procs []Process, s *ProcessListState, opts ProcessListOptions) ProcessListResult`。排序只在切片或排序方式变化时进行;刷新数据请传入新切片。 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `Process.PID`, `Name` | `int`, `string` | — | 进程号与名称;PID 是行的稳定键。 | +| `Process.CPU` | `float64` | 0 | 占用百分比。 | +| `Process.Memory` | `uint64` | 0 | 常驻内存字节数,显示为 KB/MB/GB。 | +| `ProcessListState.Sort` | `ui.SortOrder` | CPU 降序 | 列为 `name`、`pid`、`cpu`、`memory`,其他值 panic。 | +| `Disabled` | `bool` | false | 不能排序与结束进程。 | + +## 状态 + +选中行(Selection 背景与菱形)、排序列箭头、确认对话框(默认聚焦“取消”)、无进程、禁用。 + +## 事件 + +`End`:用户确认要结束的进程,没有则为 nil。 + +## 键盘操作 + +- Tab 到达表格;↑/↓、Home/End 选择;Delete 结束所选进程(先确认)。 +- 点击列头排序,再点反向;每行结束按钮可用 Tab 到达。 + +## 限制 + +- 组件不发送信号,只回报确认结果。 diff --git a/docs/components/327-code-viewer.md b/docs/components/327-code-viewer.md new file mode 100644 index 0000000..b91d213 --- /dev/null +++ b/docs/components/327-code-viewer.md @@ -0,0 +1,46 @@ +# 327 CodeViewer · 代码查看器 + +## 用途 + +只读代码视图:行号、基础语法着色(`internal.Highlight`)、按缩进折叠、当前行、选择复制;大文件只构建可见行。 + +## 最小示例 + +```go +lines := []string{"func Seal(d string) int {", "\treturn len(d)", "}"} +var state mujica.CodeViewerState +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.CodeViewer(c, lines, &state, mujica.CodeViewerOptions{Language: "go"}).Element.Height(240) +} +ui.Render(view, 400, 300, 1) +``` + +## 参数 + +`func CodeViewer(c *ui.Context, lines []string, s *CodeViewerState, opts CodeViewerOptions) ChoiceResult`。块注释状态与折叠范围在代码变化时扫描一次。`s.Line()` 返回当前行,`s.GoTo(line)` 跳转并展开遮住它的折叠,`s.Visible()` 返回可见行范围,`s.ScrollTo(line)` 只滚动;`s.Selection` 保存选中行,`s.Folded` 保存折叠起始行。 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `Language` | `string` | ""(纯文本) | `go`、`javascript`、`typescript`、`python`、`json`、`shell`、`sql`。 | +| `Label` | `string` | "" | 无障碍名称。 | +| `Disabled` | `bool` | false | 不能折叠。 | + +## 状态 + +当前行(Selection 背景与菱形)、选中行(Selection 背景)、可折叠行(向下箭头)、已折叠(向右箭头)、空代码、禁用。关键字 AccentText、字符串 Success、数字 Warning、类型 Info、注释 TextMuted 斜体。 + +## 事件 + +`Changed()`:当前行或选择变化。 + +## 键盘操作 + +- ↑/↓、Home/End:移动当前行;Shift+方向键扩展选择;Cmd/Ctrl+A 全选。 +- Cmd+C(macOS)/ Ctrl+C:复制选中行,未选则复制当前行。 +- ←:折叠当前行开始的块;→:展开。折叠按钮可用 Tab 到达。 + +## 限制 + +- 折叠按缩进推断,不解析语法;空行不结束块。 +- 只读,不提供编辑与行内(字符级)选择。 diff --git a/docs/components/328-minimap.md b/docs/components/328-minimap.md new file mode 100644 index 0000000..60a0a9e --- /dev/null +++ b/docs/components/328-minimap.md @@ -0,0 +1,45 @@ +# 328 Minimap · 代码缩略图 + +## 用途 + +以细条绘制代码缩略图,框出编辑器可见区域;点击或拖动跳转,键盘可移动。 + +## 最小示例 + +```go +lines := []string{"package court", "", "func Seal() {}", "func Herald() {}"} +view := mujica.MinimapView{Top: 0, Visible: 2} +draw := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + if mujica.Minimap(c, lines, &view, mujica.MinimapOptions{Language: "go"}).Changed() { + fmt.Println("scroll editor to", view.Top) + } +} +ui.Render(draw, 400, 300, 1) +``` + +## 参数 + +`func Minimap(c *ui.Context, lines []string, view *MinimapView, opts MinimapOptions) ChoiceResult`。`MinimapView{Top, Visible}` 是编辑器当前首行与可见行数,由调用方与编辑器同步(可用 `CodeViewerState.Visible` 与 `ScrollTo`)。每行 2 DIP、每字符 1 DIP;文件比缩略图高时缩略图随视图滚动,只绘制可见的行。 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `Language` | `string` | "" | 着色语言,同 CodeViewer。 | +| `Width` | `float32` | 96 | 宽度(DIP);负数 panic。 | +| `Disabled` | `bool` | false | 不能跳转。 | + +## 状态 + +可见区为 Selection 半透明填充加 AccentText 边框;聚焦时外侧焦点环;禁用。 + +## 事件 + +`Changed()`:用户移动了视图,读取 `view.Top`。 + +## 键盘操作 + +- Tab 聚焦;↑/↓ 移动一行,PageUp/PageDown 移动一屏,Home/End 到开头与结尾。 + +## 限制 + +- 缩略图按行独立着色,不跟踪跨行的块注释。 diff --git a/docs/components/329-find-widget.md b/docs/components/329-find-widget.md new file mode 100644 index 0000000..2617d9b --- /dev/null +++ b/docs/components/329-find-widget.md @@ -0,0 +1,51 @@ +# 329 FindWidget · 查找替换 + +## 用途 + +在文本中查找与替换:区分大小写、整词、正则表达式,显示匹配数并上下跳转。 + +## 最小示例 + +```go +text := "the seal and the seal" +state := mujica.FindState{Query: "seal", Replacement: "signet", ShowReplace: true} +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + if mujica.FindWidget(c, &text, &state, mujica.FindWidgetOptions{}).Replaced > 0 { + fmt.Println(text) + } +} +ui.Render(view, 400, 300, 1) +``` + +## 参数 + +`func FindWidget(c *ui.Context, text *string, s *FindState, opts FindWidgetOptions) FindWidgetResult`。匹配在文本、查询或选项变化时重算;`s.Matches()` 返回各匹配的字节范围。替换直接修改 `*text`。 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `FindState.Query`, `Replacement` | `string` | "" | 查找与替换文字;正则模式下替换支持 `$1` 分组。 | +| `MatchCase`, `WholeWord`, `Regex` | `bool` | false | 区分大小写、整词、正则。 | +| `ShowReplace` | `bool` | false | 展开替换行。 | +| `Current` | `int` | 0 | 当前匹配。 | +| `ReadOnly` | `bool` | false | 隐藏替换。 | +| `Disabled` | `bool` | false | 不能输入与替换。 | + +## 状态 + +选项按钮按下状态、匹配计数、无匹配、正则错误(Danger 文字说明具体错误)、替换按钮在无匹配时禁用、禁用。 + +## 事件 + +`Moved`:查询、选项或当前匹配变化;`Replaced`:本帧替换的数量。 + +## 键盘操作 + +- Enter / Shift+Enter:下一项 / 上一项;macOS Cmd+G,其他平台 F3。 +- 替换输入框中 Enter:替换当前项。 +- Tab 依次到达展开按钮、查找框、上下按钮、三个选项与替换控件。 + +## 限制 + +- 使用 Go 的 RE2 正则语法,不支持回溯引用。 +- 不在编辑器中绘制高亮,调用方用 `Matches()` 自行标记。 diff --git a/docs/components/330-search-panel.md b/docs/components/330-search-panel.md new file mode 100644 index 0000000..c9dc6e9 --- /dev/null +++ b/docs/components/330-search-panel.md @@ -0,0 +1,51 @@ +# 330 SearchPanel · 多文件搜索 + +## 用途 + +展示多文件搜索结果:按文件分组、可折叠,匹配预览带高亮;提供包含与排除模式输入。 + +## 最小示例 + +```go +results := []mujica.SearchFileResult{{Path: "court/seal.go", Matches: []mujica.SearchMatch{{Line: 12, Text: "func Seal() {", Start: 5, End: 9}}}} +state := mujica.SearchPanelState{Query: "seal"} +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + r := mujica.SearchPanel(c, results, &state, mujica.SearchPanelOptions{}) + r.Element.Height(280) + if r.Opened != nil { + fmt.Println("open", r.Opened.Path, r.Opened.Line) + } +} +ui.Render(view, 400, 300, 1) +``` + +## 参数 + +`func SearchPanel(c *ui.Context, results []SearchFileResult, s *SearchPanelState, opts SearchPanelOptions) SearchPanelResult`。搜索由调用方执行:查询或模式变化时 `Changed` 为 true,完成后传入新的 `results`。行列表虚拟化。 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `SearchFileResult.Path` | `string` | 必填 | 文件路径,也是折叠键。 | +| `SearchMatch.Line`, `Text` | `int`, `string` | — | 行号与行文字。 | +| `SearchMatch.Start`, `End` | `int` | — | 匹配在 `Text` 中的字节范围;越界 panic。 | +| `SearchPanelState.Query`, `Include`, `Exclude` | `string` | "" | 查询与包含、排除模式(格式由调用方解释)。 | +| `Status`, `Error` | `DataStatus`, `string` | `DataReady` | 搜索中或失败。 | +| `Disabled` | `bool` | false | 不能输入、折叠与打开。 | + +## 状态 + +结果摘要、文件标题(折叠箭头与匹配数)、匹配行(Selection 背景与粗体下划线标出匹配)、选中行(Selection 与菱形)、搜索中、失败(可重试)、未输入、无结果、禁用。 + +## 事件 + +`Changed`:查询或模式变化,或点击重试;`Opened`:用户打开的匹配(路径与行号)。 + +## 键盘操作 + +- Tab 依次到达查询、包含、排除与结果列表。 +- ↑/↓ 选择;Enter 或双击:打开匹配,或折叠/展开文件;文件行上 ← 折叠、→ 展开。 + +## 限制 + +- 长行只显示匹配前 40 个字节。 diff --git a/docs/components/331-problems-panel.md b/docs/components/331-problems-panel.md new file mode 100644 index 0000000..cea7623 --- /dev/null +++ b/docs/components/331-problems-panel.md @@ -0,0 +1,49 @@ +# 331 ProblemsPanel · 问题面板 + +## 用途 + +列出错误、警告与信息,按文件分组并按行列排序,可按级别过滤,打开问题时回调跳转。 + +## 最小示例 + +```go +problems := []mujica.Problem{{Severity: mujica.ProblemError, File: "court/seal.go", Line: 12, Col: 5, Message: "undefined: Decree"}} +var state mujica.ProblemsPanelState +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + r := mujica.ProblemsPanel(c, problems, &state, mujica.ProblemsPanelOptions{}) + r.Element.Height(240) + if r.Opened != nil { + fmt.Println("go to", r.Opened.File, r.Opened.Line) + } +} +ui.Render(view, 400, 300, 1) +``` + +## 参数 + +`func ProblemsPanel(c *ui.Context, problems []Problem, s *ProblemsPanelState, opts ProblemsPanelOptions) ProblemsPanelResult`。分组排序只在数据、过滤或折叠变化时进行。 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `Problem.Severity` | `ProblemSeverity` | `ProblemError` | `ProblemError`、`ProblemWarning`、`ProblemInfo`;越界 panic。 | +| `Problem.File`, `Line`, `Col` | `string`, `int` | — | 位置。 | +| `Problem.Message`, `Source` | `string` | — | 说明与来源(如编译器名)。 | +| `ProblemsPanelState.Hidden` | `[3]bool` | 全显示 | 被隐藏的级别。 | +| `Disabled` | `bool` | false | 不能过滤、折叠与打开。 | + +## 状态 + +级别按钮带图标与计数;问题行用不同形状的图标(圆叉、三角、圆 i)与颜色共同标识级别,并有无障碍名称;选中行 Selection 与菱形;无问题;禁用。 + +## 事件 + +`Opened`:用户打开的问题。 + +## 键盘操作 + +- Tab 到达级别按钮与列表;↑/↓ 选择;Enter 或双击打开问题,在文件行上折叠/展开;← 折叠、→ 展开。 + +## 限制 + +- 同一文件的问题按行、列排序,文件按路径排序。 diff --git a/docs/components/332-output-panel.md b/docs/components/332-output-panel.md new file mode 100644 index 0000000..5349439 --- /dev/null +++ b/docs/components/332-output-panel.md @@ -0,0 +1,48 @@ +# 332 OutputPanel · 输出面板 + +## 用途 + +显示多个输出通道之一,跟随尾部,支持 ANSI 颜色;清空请求通过结果交给调用方。 + +## 最小示例 + +```go +channels := []mujica.OutputChannel{{Name: "Build", Lines: []string{"compiling court", "\x1b[32mok\x1b[0m"}}} +var state mujica.OutputPanelState +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + r := mujica.OutputPanel(c, channels, &state, mujica.OutputPanelOptions{}) + r.Element.Height(240) + if r.Cleared != "" { + channels[state.Channel].Lines = nil + } +} +ui.Render(view, 400, 300, 1) +``` + +## 参数 + +`func OutputPanel(c *ui.Context, channels []OutputChannel, s *OutputPanelState, opts OutputPanelOptions) OutputPanelResult`。通道为空或 `Channel` 越界时 panic。行列表虚拟化。 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `OutputChannel.Name`, `Lines` | `string`, `[]string` | — | 通道名与内容,行内可含 ANSI 样式。 | +| `OutputPanelState.Channel` | `int` | 0 | 当前通道。 | +| `OutputPanelState.Follow` | `bool` | 首次为 true | 跟随尾部。 | +| `Disabled` | `bool` | false | 不能切换、清空与改跟随。 | + +## 状态 + +通道下拉、跟随按钮按下、清空按钮、无输出、禁用。 + +## 事件 + +`Changed`:切换了通道;`Cleared`:要清空的通道名,调用方清空其 `Lines`。 + +## 键盘操作 + +- Tab 到达通道下拉、跟随按钮、清空按钮与输出列表;下拉中方向键选择。 + +## 限制 + +- 只渲染 SGR 样式,其余控制序列原样保留(与 `internal.ParseAnsi` 一致)。 diff --git a/docs/components/333-symbol-outline.md b/docs/components/333-symbol-outline.md new file mode 100644 index 0000000..f04dbfa --- /dev/null +++ b/docs/components/333-symbol-outline.md @@ -0,0 +1,53 @@ +# 333 SymbolOutline · 符号大纲 + +## 用途 + +以树显示文件中的符号,带类型标记,可筛选,并跟随编辑器光标选中所在符号。 + +## 最小示例 + +```go +symbols := []mujica.Symbol{{Name: "Court", Kind: mujica.SymbolType, Line: 3, EndLine: 40, Children: []mujica.Symbol{ + {Name: "Seal", Kind: mujica.SymbolMethod, Line: 10, EndLine: 20}, +}}} +var state mujica.SymbolOutlineState +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + r := mujica.SymbolOutline(c, symbols, &state, mujica.SymbolOutlineOptions{CursorLine: 12}) + r.Element.Height(240) + if r.Jumped != nil { + fmt.Println("go to line", r.Jumped.Line) + } +} +ui.Render(view, 400, 300, 1) +``` + +## 参数 + +`func SymbolOutline(c *ui.Context, symbols []Symbol, s *SymbolOutlineState, opts SymbolOutlineOptions) SymbolOutlineResult`。符号树在数据变化时编号一次。 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `Symbol.Name`, `Detail` | `string` | — | 名称与补充说明(如签名)。 | +| `Symbol.Kind` | `SymbolKind` | `SymbolFunction` | 函数 f、方法 m、类型 T、接口 I、字段 p、变量 v、常量 c、模块 M;越界 panic。 | +| `Symbol.Line`, `EndLine` | `int` | — | 所在行范围,从 1 开始。 | +| `SymbolOutlineState.Query` | `string` | "" | 筛选文字;匹配项与其祖先保留并自动展开。 | +| `CursorLine` | `int` | 0(不跟随) | 变化时选中包含该行的最内层符号并展开祖先。 | +| `Disabled` | `bool` | false | 不能跳转。 | + +## 状态 + +类型以字母徽记加颜色区分;选中行 Selection 与菱形;筛选高亮;无符号;禁用。 + +## 事件 + +`Jumped`:用户要跳转的符号。 + +## 键盘操作 + +- Tab 到达筛选框与树;↑/↓ 移动,→ 展开或进入子项,← 折叠或回到父项;输入字母按名称定位。 +- Enter 或双击:跳转到符号。 + +## 限制 + +- 光标跟随只在 `CursorLine` 变化时生效,之后用户可自由移动选择。 diff --git a/docs/components/334-completion-menu.md b/docs/components/334-completion-menu.md new file mode 100644 index 0000000..cbc27a1 --- /dev/null +++ b/docs/components/334-completion-menu.md @@ -0,0 +1,50 @@ +# 334 CompletionMenu · 补全菜单 + +## 用途 + +在编辑器下方显示补全候选:类型徽记、详情与说明;方向键选择,Tab 或 Enter 接受,焦点始终留在编辑器。 + +## 最小示例 + +```go +text := "se" +open := true +items := []mujica.Completion{{Label: "Seal", Kind: mujica.SymbolMethod, Detail: "func()"}, {Label: "Search", Kind: mujica.SymbolFunction}} +var state mujica.CompletionMenuState +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + in := mujica.TextInput(c, &text, mujica.TextInputOptions{Label: "Code"}) + if r := mujica.CompletionMenu(c, in.Input, &open, items, &state, mujica.CompletionMenuOptions{}); r.Accepted != nil { + text = r.Accepted.Label + } +} +ui.Render(view, 400, 300, 1) +``` + +## 参数 + +`func CompletionMenu(c *ui.Context, anchor *ui.Element, open *bool, items []Completion, s *CompletionMenuState, opts CompletionMenuOptions) CompletionMenuResult`。`anchor` 是编辑器元素(如 `InputResult.Input`);打开期间组件替换它的 `HandleInput` 以先拿到菜单按键。候选为空、禁用或编辑器禁用时自动关闭。列表虚拟化,最多显示 8 行后滚动;窗口下方空间不足时在上方弹出。 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `Completion.Label` | `string` | 必填 | 候选文字。 | +| `Completion.Kind` | `SymbolKind` | `SymbolFunction` | 类型徽记;越界 panic。 | +| `Completion.Detail`, `Doc` | `string` | "" | 右侧详情与当前候选的说明。 | +| `CompletionMenuState.Selected` | `int` | 0 | 当前候选。 | +| `Disabled` | `bool` | false | 关闭菜单。 | + +## 状态 + +当前候选 Selection 背景与菱形;弹层 160 ms 淡入(减少动画时直接显示)。 + +## 事件 + +`Accepted`:被接受的候选,同时 `*open` 置为 false。 + +## 键盘操作 + +- ↓/↑:循环移动;Tab 或 Enter:接受;Escape:关闭。点击候选也接受。 + +## 限制 + +- 打开期间编辑器自身设置的 `HandleInput` 会被覆盖。 diff --git a/docs/components/335-debug-toolbar.md b/docs/components/335-debug-toolbar.md new file mode 100644 index 0000000..7560c0b --- /dev/null +++ b/docs/components/335-debug-toolbar.md @@ -0,0 +1,48 @@ +# 335 DebugToolbar · 调试工具栏 + +## 用途 + +调试会话的继续/暂停、单步跳过、单步进入、单步跳出、重启与停止,按会话状态禁用不适用的按钮,并注册常用快捷键。 + +## 最小示例 + +```go +state := mujica.DebugPaused +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + switch mujica.DebugToolbar(c, state, mujica.DebugToolbarOptions{}).Action { + case mujica.DebugContinue: + state = mujica.DebugRunning + case mujica.DebugStop: + state = mujica.DebugStopped + } +} +ui.Render(view, 400, 300, 1) +``` + +## 参数 + +`func DebugToolbar(c *ui.Context, state DebugState, opts DebugToolbarOptions) DebugToolbarResult`。`DebugEnabled(state, action)` 给出禁用规则:运行中只能暂停、重启、停止;暂停时可继续、单步、重启、停止;停止时只能“开始”(`DebugContinue`)。 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `state` | `DebugState` | — | `DebugStopped`、`DebugRunning`、`DebugPaused`;越界 panic。 | +| `Disabled` | `bool` | false | 全部按钮与快捷键无效。 | + +## 状态 + +运行中第一个按钮显示暂停,否则显示继续(停止时名为“开始”);不适用的按钮禁用;按钮悬停、按下、焦点环与带快捷键的提示。 + +## 事件 + +`Action`:本帧请求的 `DebugAction`,无则为 `DebugNone`。 + +## 键盘操作 + +- F5 继续/开始,F6 暂停,F10 单步跳过,F11 单步进入,Shift+F11 单步跳出,Cmd/Ctrl+Shift+F5 重启,Shift+F5 停止(窗口内全局,焦点控件优先处理)。 +- 工具栏是一个 Tab 停靠点,←/→ 在按钮间移动。 + +## 限制 + +- 快捷键固定,不可配置;同一窗口只放一个调试工具栏。 +- 图标为自绘 SVG。 diff --git a/docs/components/336-breakpoint-list.md b/docs/components/336-breakpoint-list.md new file mode 100644 index 0000000..488efca --- /dev/null +++ b/docs/components/336-breakpoint-list.md @@ -0,0 +1,49 @@ +# 336 BreakpointList · 断点列表 + +## 用途 + +列出断点:启用开关、跳到源码位置、行内编辑条件断点。 + +## 最小示例 + +```go +bps := []mujica.Breakpoint{{ID: "a", File: "court/seal.go", Line: 12, Enabled: true}, {ID: "b", File: "main.go", Line: 9, Condition: "n > 3"}} +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + r := mujica.BreakpointList(c, &bps, mujica.BreakpointListOptions{}) + r.Element.Height(200) + if r.Toggled != "" || r.Edited != "" { + fmt.Println("sync breakpoints with the debugger") + } +} +ui.Render(view, 400, 300, 1) +``` + +## 参数 + +`func BreakpointList(c *ui.Context, bps *[]Breakpoint, opts BreakpointListOptions) BreakpointListResult`。组件直接修改 `*bps` 中的 `Enabled` 与 `Condition`;ID 为空或重复时 panic。 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `Breakpoint.ID` | `string` | 必填 | 稳定标识。 | +| `Breakpoint.File`, `Line` | `string`, `int` | — | 位置。 | +| `Breakpoint.Enabled` | `bool` | false | 是否启用。 | +| `Breakpoint.Condition` | `string` | "" | 条件表达式,非空时显示“当 …”。 | +| `Disabled` | `bool` | false | 不能修改与跳转。 | + +## 状态 + +启用为实心圆点,停用为空心圆点并降低文字色;条件以 Warning 等宽文字显示;编辑中的输入框;无断点;禁用。 + +## 事件 + +`Toggled`、`Edited`、`Opened`:本帧被切换、改条件、要打开的断点 ID。 + +## 键盘操作 + +- Tab 依次到达复选框、位置按钮与编辑条件按钮;Space 切换启用;Enter 打开位置。 +- 条件输入框:Enter 确认,Escape 取消。 + +## 限制 + +- 不提供删除与新增;由调用方修改 `*bps`。 diff --git a/docs/components/337-call-stack.md b/docs/components/337-call-stack.md new file mode 100644 index 0000000..a5a2b69 --- /dev/null +++ b/docs/components/337-call-stack.md @@ -0,0 +1,52 @@ +# 337 CallStack · 调用栈 + +## 用途 + +按线程分组显示调用栈帧,标出当前帧;选择帧即切换当前帧。 + +## 最小示例 + +```go +threads := []mujica.DebugThread{{ID: "t1", Name: "main", Frames: []mujica.StackFrame{ + {ID: "f1", Name: "court.Seal", File: "court/seal.go", Line: 14}, + {ID: "f2", Name: "main.main", File: "main.go", Line: 9}, +}}} +state := mujica.CallStackState{Current: "f1"} +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + r := mujica.CallStack(c, threads, &state, mujica.CallStackOptions{}) + r.Element.Height(200) + if r.Changed() { + fmt.Println("show frame", state.Current) + } +} +ui.Render(view, 400, 300, 1) +``` + +## 参数 + +`func CallStack(c *ui.Context, threads []DebugThread, s *CallStackState, opts CallStackOptions) ChoiceResult`。调用方修改 `s.Current` 时选中行随之移动。 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `DebugThread.ID`, `Name` | `string` | — | 线程标识与名称。 | +| `StackFrame.ID`, `Name`, `File`, `Line` | `string`, `int` | — | 帧标识、函数名与位置,最内层在前。 | +| `CallStackState.Current` | `string` | "" | 当前帧 ID。 | +| `CallStackState.Collapsed` | `ui.Selection[string]` | 空 | 折叠的线程 ID。 | +| `Disabled` | `bool` | false | 不能切换与折叠。 | + +## 状态 + +线程标题(折叠箭头与栈深度)、当前帧(Selection 背景与菱形)、未暂停空状态、禁用。 + +## 事件 + +`Changed()`:用户选了另一帧,读取 `s.Current`。 + +## 键盘操作 + +- Tab 到达列表;↑/↓ 移动,到达帧即切换当前帧;线程行上 Enter 折叠/展开,← 折叠、→ 展开。 + +## 限制 + +- 不区分线程的运行状态;需要时写入线程名。 diff --git a/docs/components/338-variables-panel.md b/docs/components/338-variables-panel.md new file mode 100644 index 0000000..c68a88c --- /dev/null +++ b/docs/components/338-variables-panel.md @@ -0,0 +1,49 @@ +# 338 VariablesPanel · 变量面板 + +## 用途 + +以树显示调试变量,可展开结构,标出自上次停止以来变化的值,并在行内修改叶子变量的值。 + +## 最小示例 + +```go +vars := []mujica.Variable{{Name: "decree", Value: "{…}", Children: []mujica.Variable{{Name: "id", Value: "42", Changed: true}}}, {Name: "count", Value: "7"}} +var state mujica.VariablesPanelState +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + r := mujica.VariablesPanel(c, vars, &state, mujica.VariablesPanelOptions{}) + r.Element.Height(200) + if r.Edited != nil { + fmt.Println("set", r.Edited.Path, "=", r.Edited.Value) + } +} +ui.Render(view, 400, 300, 1) +``` + +## 参数 + +`func VariablesPanel(c *ui.Context, vars []Variable, s *VariablesPanelState, opts VariablesPanelOptions) VariablesPanelResult`。变量按名称以点连接成路径作为键,同层重名 panic。修改值通过结果交给调用方写回调试器并传入新值。 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `Variable.Name`, `Value`, `Type` | `string` | — | 名称、显示值与类型。 | +| `Variable.Changed` | `bool` | false | 值自上次停止后变化。 | +| `Variable.Children` | `[]Variable` | nil(叶子) | 成员;非 nil 时可展开。 | +| `Disabled` | `bool` | false | 不能编辑。 | + +## 状态 + +变化的值以菱形标记、粗体与 AccentText 显示并带无障碍描述“已变化”;编辑中的输入框;选中行 Selection;无变量;禁用。 + +## 事件 + +`Edited`:新值与变量路径。 + +## 键盘操作 + +- Tab 到达树;↑/↓ 移动,→ 展开,← 折叠。 +- 叶子上 Enter、F2 或双击:编辑;Enter 确认,Escape 取消。 + +## 限制 + +- 子项不支持按需加载;需要时调用方在展开后补充 `Children`。 diff --git a/docs/components/339-hex-viewer.md b/docs/components/339-hex-viewer.md new file mode 100644 index 0000000..3a4a448 --- /dev/null +++ b/docs/components/339-hex-viewer.md @@ -0,0 +1,44 @@ +# 339 HexViewer · 十六进制查看器 + +## 用途 + +以偏移、十六进制与 ASCII 三列对照显示字节;两列共享同一选择与光标,只构建可见行。 + +## 最小示例 + +```go +data := []byte("MUJICA court ledger\x00\x01") +var state mujica.HexViewerState +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.HexViewer(c, data, &state, mujica.HexViewerOptions{}).Element.Height(160) +} +ui.Render(view, 400, 300, 1) +``` + +## 参数 + +`func HexViewer(c *ui.Context, data []byte, s *HexViewerState, opts HexViewerOptions) ChoiceResult`。每行 16 字节;`s.Selection()` 返回选中范围;光标或锚点越界 panic。 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `HexViewerState.Cursor`, `Anchor` | `int` | 0 | 光标字节与 Shift 选择的起点。 | +| `Disabled` | `bool` | false | 不能移动与选择。 | + +## 状态 + +选中字节在两列都显示 Selection 背景,光标字节在两列都加 AccentText 框;不可打印字节显示为点;聚焦时外侧焦点环;空数据;禁用。 + +## 事件 + +`Changed()`:光标或选择变化。 + +## 键盘操作 + +- Tab 聚焦;←/→ 移动一字节,↑/↓ 移动一行,Home/End 到行首行尾;加 Shift 扩展选择。 +- Cmd+C(macOS)/ Ctrl+C:以空格分隔的十六进制复制选中字节。 +- 点击任一列选择字节,Shift+点击扩展。 + +## 限制 + +- 只读,不支持编辑与搜索。 diff --git a/docs/components/340-flamegraph.md b/docs/components/340-flamegraph.md new file mode 100644 index 0000000..21f12c8 --- /dev/null +++ b/docs/components/340-flamegraph.md @@ -0,0 +1,50 @@ +# 340 Flamegraph · 火焰图 + +## 用途 + +以层叠框绘制性能剖析:点击放大子树,搜索高亮匹配帧,悬停或键盘移动时显示并朗读耗时读数。 + +## 最小示例 + +```go +root := &mujica.FlameNode{Name: "main", Value: 100, Children: []mujica.FlameNode{ + {Name: "court.Seal", Value: 60}, {Name: "ledger.Write", Value: 30}, +}} +var state mujica.FlamegraphState +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.Flamegraph(c, root, &state, mujica.FlamegraphOptions{Unit: "ms"}) +} +ui.Render(view, 400, 300, 1) +``` + +## 参数 + +`func Flamegraph(c *ui.Context, root *FlameNode, s *FlamegraphState, opts FlamegraphOptions) ChoiceResult`。帧宽按耗时占比;放大的帧占满宽度,祖先以淡色整宽显示。子帧耗时之和大于父帧或出现负值时 panic。帧颜色按名称取自主题调色板(AccentText、Info、Success、Warning、加深的 Ornament、TextMuted,均对 Surface 至少 3:1)。 + +| 字段 | 类型 | 默认值 | 含义 | +| --- | --- | --- | --- | +| `FlameNode.Name`, `Value`, `Children` | `string`, `float64`, `[]FlameNode` | — | 帧名、含子帧的总耗时与子帧。 | +| `FlamegraphState.Zoom`, `Focus` | `[]int` | nil(根) | 放大与键盘焦点所在帧的子序号路径。 | +| `FlamegraphState.Query` | `string` | "" | 搜索高亮。 | +| `Unit` | `string` | "" | 读数中的单位。 | +| `Status`, `Error` | `DataStatus`, `string` | `DataReady` | 加载中或失败,与图同高显示。 | +| `Disabled` | `bool` | false | 不能放大与移动。 | + +## 状态 + +帧为浅色底加同色边框与名称;搜索匹配加 AccentText 粗框,其余帧变淡;焦点帧带焦点环;读数行显示名称、耗时与占比;加载、失败、无数据。 + +## 事件 + +`Changed()`:放大或焦点变化(失败状态下点击“重试”也为 true)。 + +## 键盘操作 + +- Tab 聚焦图表;↑ 到父帧,↓ 到第一个子帧,←/→ 在兄弟帧间移动;Enter 放大当前帧,Escape 缩小一级。 +- 读数随焦点更新并通过屏幕阅读器朗读。 + +## 限制 + +- 窄于 1 DIP 的帧不绘制,窄于 24 DIP 的帧不写名称;用键盘可到达所有帧。 +- 帧行高 28 DIP,窄帧的点击区域可能小于 28 DIP,请用键盘或先放大。 diff --git a/example_code_test.go b/example_code_test.go new file mode 100644 index 0000000..10451d2 --- /dev/null +++ b/example_code_test.go @@ -0,0 +1,276 @@ +package mujica_test + +import ( + "fmt" + + "github.com/egoist/mygo/ui" + + mujica "mujicaui" +) + +func ExampleTerminal() { + var term mujica.TerminalState + term.Write("\x1b[1m~/court\x1b[0m $ ls\r\n\x1b[34mdecrees\x1b[0m ledger.txt\r\n$ ") + view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.Terminal(c, &term, mujica.TerminalOptions{ + Input: func(data string) { fmt.Printf("to pty: %q\n", data) }, + Resized: func(cols, rows int) { fmt.Println("pty size", cols, rows) }, + }).Size(380, 200) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleTerminalTabs() { + tabs := []mujica.TerminalTab{{ID: "a", Title: "zsh"}, {ID: "b", Title: "build", Busy: true}} + active := "a" + view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + r := mujica.TerminalTabs(c, &tabs, &active, mujica.TerminalTabsOptions{}, func(id string) { + ui.Text(c, "terminal "+id) + }) + if r.Closed != "" { + fmt.Println("close pty", r.Closed) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleTerminalSearch() { + var term mujica.TerminalState + term.Write("build ok\r\nerror: missing seal\r\n") + var search mujica.TerminalSearchState + view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + ui.Column(c).Gap(8).Children(func() { + mujica.TerminalSearch(c, &term, &search, mujica.TerminalSearchOptions{}) + mujica.Terminal(c, &term, mujica.TerminalOptions{}).Height(160) + }) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleCommandHistory() { + history := []mujica.HistoryEntry{{Command: "git status"}, {Command: "go test ./...", ExitCode: 1}} + var state mujica.CommandHistoryState + view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + r := mujica.CommandHistory(c, history, &state, mujica.CommandHistoryOptions{}) + r.Element.Height(240) + if r.Replayed != nil { + fmt.Println("run", r.Replayed.Command) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleLogViewer() { + lines := []mujica.LogLine{{Level: mujica.LogInfo, Text: "court opened"}, {Level: mujica.LogError, Text: "seal missing"}} + var state mujica.LogViewerState + view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.LogViewer(c, lines, &state, mujica.LogViewerOptions{}).Element.Height(240) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleProcessList() { + procs := []mujica.Process{{PID: 7, Name: "archivist", CPU: 42, Memory: 50 << 20}, {PID: 9, Name: "herald", CPU: 3, Memory: 8 << 20}} + var state mujica.ProcessListState + view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + r := mujica.ProcessList(c, procs, &state, mujica.ProcessListOptions{}) + r.Element.Height(240) + if r.End != nil { + fmt.Println("kill", r.End.PID) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleCodeViewer() { + lines := []string{"func Seal(d string) int {", "\treturn len(d)", "}"} + var state mujica.CodeViewerState + view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.CodeViewer(c, lines, &state, mujica.CodeViewerOptions{Language: "go"}).Element.Height(240) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleMinimap() { + lines := []string{"package court", "", "func Seal() {}", "func Herald() {}"} + view := mujica.MinimapView{Top: 0, Visible: 2} + draw := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + if mujica.Minimap(c, lines, &view, mujica.MinimapOptions{Language: "go"}).Changed() { + fmt.Println("scroll editor to", view.Top) + } + } + ui.Render(draw, 400, 300, 1) +} + +func ExampleFindWidget() { + text := "the seal and the seal" + state := mujica.FindState{Query: "seal", Replacement: "signet", ShowReplace: true} + view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + if mujica.FindWidget(c, &text, &state, mujica.FindWidgetOptions{}).Replaced > 0 { + fmt.Println(text) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleSearchPanel() { + results := []mujica.SearchFileResult{{Path: "court/seal.go", Matches: []mujica.SearchMatch{{Line: 12, Text: "func Seal() {", Start: 5, End: 9}}}} + state := mujica.SearchPanelState{Query: "seal"} + view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + r := mujica.SearchPanel(c, results, &state, mujica.SearchPanelOptions{}) + r.Element.Height(280) + if r.Opened != nil { + fmt.Println("open", r.Opened.Path, r.Opened.Line) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleProblemsPanel() { + problems := []mujica.Problem{{Severity: mujica.ProblemError, File: "court/seal.go", Line: 12, Col: 5, Message: "undefined: Decree"}} + var state mujica.ProblemsPanelState + view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + r := mujica.ProblemsPanel(c, problems, &state, mujica.ProblemsPanelOptions{}) + r.Element.Height(240) + if r.Opened != nil { + fmt.Println("go to", r.Opened.File, r.Opened.Line) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleOutputPanel() { + channels := []mujica.OutputChannel{{Name: "Build", Lines: []string{"compiling court", "\x1b[32mok\x1b[0m"}}} + var state mujica.OutputPanelState + view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + r := mujica.OutputPanel(c, channels, &state, mujica.OutputPanelOptions{}) + r.Element.Height(240) + if r.Cleared != "" { + channels[state.Channel].Lines = nil + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleSymbolOutline() { + symbols := []mujica.Symbol{{Name: "Court", Kind: mujica.SymbolType, Line: 3, EndLine: 40, Children: []mujica.Symbol{ + {Name: "Seal", Kind: mujica.SymbolMethod, Line: 10, EndLine: 20}, + }}} + var state mujica.SymbolOutlineState + view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + r := mujica.SymbolOutline(c, symbols, &state, mujica.SymbolOutlineOptions{CursorLine: 12}) + r.Element.Height(240) + if r.Jumped != nil { + fmt.Println("go to line", r.Jumped.Line) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleCompletionMenu() { + text := "se" + open := true + items := []mujica.Completion{{Label: "Seal", Kind: mujica.SymbolMethod, Detail: "func()"}, {Label: "Search", Kind: mujica.SymbolFunction}} + var state mujica.CompletionMenuState + view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + in := mujica.TextInput(c, &text, mujica.TextInputOptions{Label: "Code"}) + if r := mujica.CompletionMenu(c, in.Input, &open, items, &state, mujica.CompletionMenuOptions{}); r.Accepted != nil { + text = r.Accepted.Label + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleDebugToolbar() { + state := mujica.DebugPaused + view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + switch mujica.DebugToolbar(c, state, mujica.DebugToolbarOptions{}).Action { + case mujica.DebugContinue: + state = mujica.DebugRunning + case mujica.DebugStop: + state = mujica.DebugStopped + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleBreakpointList() { + bps := []mujica.Breakpoint{{ID: "a", File: "court/seal.go", Line: 12, Enabled: true}, {ID: "b", File: "main.go", Line: 9, Condition: "n > 3"}} + view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + r := mujica.BreakpointList(c, &bps, mujica.BreakpointListOptions{}) + r.Element.Height(200) + if r.Toggled != "" || r.Edited != "" { + fmt.Println("sync breakpoints with the debugger") + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleCallStack() { + threads := []mujica.DebugThread{{ID: "t1", Name: "main", Frames: []mujica.StackFrame{ + {ID: "f1", Name: "court.Seal", File: "court/seal.go", Line: 14}, + {ID: "f2", Name: "main.main", File: "main.go", Line: 9}, + }}} + state := mujica.CallStackState{Current: "f1"} + view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + r := mujica.CallStack(c, threads, &state, mujica.CallStackOptions{}) + r.Element.Height(200) + if r.Changed() { + fmt.Println("show frame", state.Current) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleVariablesPanel() { + vars := []mujica.Variable{{Name: "decree", Value: "{…}", Children: []mujica.Variable{{Name: "id", Value: "42", Changed: true}}}, {Name: "count", Value: "7"}} + var state mujica.VariablesPanelState + view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + r := mujica.VariablesPanel(c, vars, &state, mujica.VariablesPanelOptions{}) + r.Element.Height(200) + if r.Edited != nil { + fmt.Println("set", r.Edited.Path, "=", r.Edited.Value) + } + } + ui.Render(view, 400, 300, 1) +} + +func ExampleHexViewer() { + data := []byte("MUJICA court ledger\x00\x01") + var state mujica.HexViewerState + view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.HexViewer(c, data, &state, mujica.HexViewerOptions{}).Element.Height(160) + } + ui.Render(view, 400, 300, 1) +} + +func ExampleFlamegraph() { + root := &mujica.FlameNode{Name: "main", Value: 100, Children: []mujica.FlameNode{ + {Name: "court.Seal", Value: 60}, {Name: "ledger.Write", Value: 30}, + }} + var state mujica.FlamegraphState + view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.Flamegraph(c, root, &state, mujica.FlamegraphOptions{Unit: "ms"}) + } + ui.Render(view, 400, 300, 1) +} diff --git a/examples/gallery/pages_code.go b/examples/gallery/pages_code.go index 028688b..9f9be1e 100644 --- a/examples/gallery/pages_code.go +++ b/examples/gallery/pages_code.go @@ -1,3 +1,245 @@ package main -var codePages = []page{} +import ( + "fmt" + "strings" + "time" + + "github.com/egoist/mygo/ui" + + mujica "mujicaui" +) + +const codeCategory = "Terminal & Code" + +var codePages = []page{ + {ID: "321", Name: "Terminal", ZhName: "终端", Category: codeCategory, Demo: codeTerminalDemo, Code: codeTerminalCode, + Keys: "Focused: keys and text go to the PTY; Ctrl+letters send control codes; Cmd+C/V (Ctrl+Shift+C/V elsewhere) copy and paste; Shift+PageUp/PageDown scroll back; drag selects."}, + {ID: "322", Name: "TerminalTabs", ZhName: "终端标签", Category: codeCategory, Demo: codeTerminalTabsDemo, Code: codeTerminalTabsCode, + Keys: "Left/Right, Home/End switch tabs; F2 or double-click renames (Enter saves, Escape cancels); Delete closes, asking first while a process runs."}, + {ID: "323", Name: "TerminalSearch", ZhName: "终端搜索", Category: codeCategory, Demo: codeTerminalSearchDemo, Code: codeTerminalSearchCode, + Keys: "Enter / Shift+Enter go to the next / previous match; Cmd+G (F3 elsewhere) too."}, + {ID: "324", Name: "CommandHistory", ZhName: "命令历史", Category: codeCategory, Demo: codeCommandHistoryDemo, Code: codeCommandHistoryCode, + Keys: "Up/Down choose; Enter or double-click replays; typing in the list jumps by command."}, + {ID: "325", Name: "LogViewer", ZhName: "日志查看器", Category: codeCategory, Demo: codeLogViewerDemo, Code: codeLogViewerCode, + Keys: "Space toggles a level or Follow; arrows, PageUp/PageDown, Home/End scroll the log."}, + {ID: "326", Name: "ProcessList", ZhName: "进程列表", Category: codeCategory, Demo: codeProcessListDemo, Code: codeProcessListCode, + Keys: "Up/Down choose; Delete ends the chosen process after confirming; click a header to sort, again to reverse."}, + {ID: "327", Name: "CodeViewer", ZhName: "代码查看器", Category: codeCategory, Demo: codeCodeViewerDemo, Code: codeCodeViewerCode, + Keys: "Up/Down move the current line; Shift extends; Cmd/Ctrl+C copies; Left folds, Right unfolds."}, + {ID: "328", Name: "Minimap", ZhName: "代码缩略图", Category: codeCategory, Demo: codeMinimapDemo, Code: codeMinimapCode, + Keys: "Up/Down move a line, PageUp/PageDown a screen, Home/End to the ends; click or drag jumps."}, + {ID: "329", Name: "FindWidget", ZhName: "查找替换", Category: codeCategory, Demo: codeFindWidgetDemo, Code: codeFindWidgetCode, + Keys: "Enter / Shift+Enter next / previous match; Enter in the replace field replaces the current match."}, + {ID: "330", Name: "SearchPanel", ZhName: "多文件搜索", Category: codeCategory, Demo: codeSearchPanelDemo, Code: codeSearchPanelCode, + Keys: "Up/Down choose; Enter opens a match or folds a file; Left folds, Right unfolds a file."}, + {ID: "331", Name: "ProblemsPanel", ZhName: "问题面板", Category: codeCategory, Demo: codeProblemsDemo, Code: codeProblemsPanelCode, + Keys: "Space toggles a severity; Up/Down choose; Enter opens a problem or folds a file; Left/Right fold and unfold."}, + {ID: "332", Name: "OutputPanel", ZhName: "输出面板", Category: codeCategory, Demo: codeOutputDemo, Code: codeOutputPanelCode, + Keys: "Tab reaches the channel menu, Follow and Clear; arrows choose a channel in the open menu."}, + {ID: "333", Name: "SymbolOutline", ZhName: "符号大纲", Category: codeCategory, Demo: codeSymbolDemo, Code: codeSymbolOutlineCode, + Keys: "Up/Down move; Right opens, Left closes; Enter or double-click jumps; typing jumps by name."}, + {ID: "334", Name: "CompletionMenu", ZhName: "补全菜单", Category: codeCategory, Demo: codeCompletionDemo, Code: codeCompletionMenuCode, + Keys: "While typing: Down/Up move, Tab or Enter accepts, Escape closes; the editor keeps the focus."}, + {ID: "335", Name: "DebugToolbar", ZhName: "调试工具栏", Category: codeCategory, Demo: codeDebugDemo, Code: codeDebugToolbarCode, + Keys: "F5 continue, F6 pause, F10 step over, F11 step into, Shift+F11 step out, Cmd/Ctrl+Shift+F5 restart, Shift+F5 stop; Left/Right inside the toolbar."}, + {ID: "336", Name: "BreakpointList", ZhName: "断点列表", Category: codeCategory, Demo: codeBreakpointDemo, Code: codeBreakpointListCode, + Keys: "Space toggles; Enter on a location opens it; the condition field saves with Enter, cancels with Escape."}, + {ID: "337", Name: "CallStack", ZhName: "调用栈", Category: codeCategory, Demo: codeCallStackDemo, Code: codeCallStackCode, + Keys: "Up/Down choose a frame; Enter folds a thread; Left/Right fold and unfold."}, + {ID: "338", Name: "VariablesPanel", ZhName: "变量面板", Category: codeCategory, Demo: codeVariablesDemo, Code: codeVariablesPanelCode, + Keys: "Up/Down move; Right opens, Left closes; Enter, F2 or double-click edits a leaf; Enter saves, Escape cancels."}, + {ID: "339", Name: "HexViewer", ZhName: "十六进制查看器", Category: codeCategory, Demo: codeHexDemo, Code: codeHexViewerCode, + Keys: "Arrows move by byte and row; Shift extends; Home/End go to the row ends; Cmd/Ctrl+C copies hex."}, + {ID: "340", Name: "Flamegraph", ZhName: "火焰图", Category: codeCategory, Demo: codeFlamegraphDemo, Code: codeFlamegraphCode, + Keys: "Up parent, Down first child, Left/Right siblings; Enter zooms in, Escape zooms out; the readout is announced."}, +} + +// codeDemoCheck is a switch that logs its changes. +func codeDemoCheck(c *ui.Context, v *bool, label string, log func(string)) { + if mujica.Switch(c, v, label, mujica.SwitchOptions{}).Changed() { + log(fmt.Sprintf("%s: %v", label, *v)) + } +} + +func codeDemoControls(c *ui.Context, fn func()) { + ui.Row(c).Gap(16).Wrap().AlignItems(ui.Center).Margin(0, 0, 16, 0).Children(fn) +} + +func codeTerminalDemo(c *ui.Context, log func(string)) { + type state struct { + term mujica.TerminalState + line string + disabled bool + } + s := ui.Local(c.Root(), "321", func() *state { + s := &state{} + s.term.Write("Welcome to the \x1b[1;35mcourt\x1b[0m shell. Type and press Enter.\r\n$ ") + return s + }) + st := *s + codeDemoControls(c, func() { + codeDemoCheck(c, &st.disabled, "Disabled", log) + if mujica.Button(c, "Clear screen", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + st.term.Write("\x1b[2J\x1b[H$ ") + log("clear screen") + } + }) + // A tiny echo shell stands in for the PTY. + mujica.Terminal(c, &st.term, mujica.TerminalOptions{Disabled: st.disabled, + Input: func(data string) { + log(fmt.Sprintf("input %q", data)) + for _, r := range data { + switch r { + case '\r': + st.term.Write("\r\n" + codeShell(st.line) + "$ ") + st.line = "" + case 0x7f: + if st.line != "" { + st.line = st.line[:len(st.line)-1] + st.term.Write("\b \b") + } + default: + if r >= ' ' { + st.line += string(r) + st.term.Write(string(r)) + } + } + } + }, + Resized: func(cols, rows int) { log(fmt.Sprintf("resized %d×%d", cols, rows)) }, + }).Size(640, 260) +} + +// codeShell answers a few commands for the demo terminal. +func codeShell(line string) string { + switch f := strings.Fields(line); { + case len(f) == 0: + return "" + case f[0] == "ls": + return "\x1b[34mchronicles\x1b[0m decrees.txt \x1b[32mseal.sh\x1b[0m\r\n" + case f[0] == "echo": + return strings.Join(f[1:], " ") + "\r\n" + case f[0] == "date": + return time.Now().Format(time.RFC1123) + "\r\n" + } + return "\x1b[31m" + line + ": command not found\x1b[0m\r\n" +} + +func codeTerminalTabsDemo(c *ui.Context, log func(string)) { + type state struct { + tabs []mujica.TerminalTab + active string + next int + disabled bool + } + s := ui.Local(c.Root(), "322", func() *state { + return &state{tabs: []mujica.TerminalTab{{ID: "1", Title: "zsh"}, {ID: "2", Title: "build", Busy: true}, {ID: "3", Title: "logs"}}, active: "1", next: 4} + }) + st := *s + codeDemoControls(c, func() { + codeDemoCheck(c, &st.disabled, "Disabled", log) + if mujica.Button(c, "New terminal", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + id := fmt.Sprint(st.next) + st.next++ + st.tabs = append(st.tabs, mujica.TerminalTab{ID: id, Title: "shell " + id}) + st.active = id + log("new " + id) + } + }) + r := mujica.TerminalTabs(c, &st.tabs, &st.active, mujica.TerminalTabsOptions{Disabled: st.disabled}, func(id string) { + ui.Text(c, "Terminal "+id+" content").TextColor(mujica.Tokens(c).TextMuted) + }) + r.Element.Width(560) + if r.Changed { + log("active " + st.active) + } + if r.Renamed != "" { + log("renamed " + r.Renamed) + } + if r.Closed != "" { + log("closed " + r.Closed) + } +} + +func codeTerminalSearchDemo(c *ui.Context, log func(string)) { + type state struct { + term mujica.TerminalState + search mujica.TerminalSearchState + disabled bool + } + s := ui.Local(c.Root(), "323", func() *state { + s := &state{} + for i := range 60 { + if i%9 == 4 { + s.term.Write(fmt.Sprintf("[%02d] \x1b[31merror\x1b[0m: seal %d missing\r\n", i, i)) + } else { + s.term.Write(fmt.Sprintf("[%02d] ledger page %d copied\r\n", i, i)) + } + } + return s + }) + st := *s + codeDemoControls(c, func() { codeDemoCheck(c, &st.disabled, "Disabled", log) }) + ui.Column(c).Width(600).Gap(8).Children(func() { + if mujica.TerminalSearch(c, &st.term, &st.search, mujica.TerminalSearchOptions{Disabled: st.disabled}).Changed() { + log(fmt.Sprintf("query %q, match %d", st.search.Query, st.search.Current+1)) + } + mujica.Terminal(c, &st.term, mujica.TerminalOptions{}).Height(220) + }) +} + +func codeCommandHistoryDemo(c *ui.Context, log func(string)) { + type state struct { + entries []mujica.HistoryEntry + s mujica.CommandHistoryState + disabled bool + } + s := ui.Local(c.Root(), "324", func() *state { + at := time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC) + var e []mujica.HistoryEntry + for i, cmd := range []string{"git pull", "go build ./...", "go test ./...", "make seal", "ls -la", "git status"} { + e = append(e, mujica.HistoryEntry{Command: cmd, Time: at.Add(time.Duration(i) * time.Minute)}) + } + e[3].ExitCode = 2 + return &state{entries: e} + }) + st := *s + codeDemoControls(c, func() { codeDemoCheck(c, &st.disabled, "Disabled", log) }) + r := mujica.CommandHistory(c, st.entries, &st.s, mujica.CommandHistoryOptions{Disabled: st.disabled}) + r.Element.Size(480, 300) + if r.Replayed != nil { + log("replay " + r.Replayed.Command) + st.entries = append(st.entries, mujica.HistoryEntry{Command: r.Replayed.Command, Time: time.Now()}) + } +} + +func codeLogViewerDemo(c *ui.Context, log func(string)) { + type state struct { + lines []mujica.LogLine + s mujica.LogViewerState + disabled bool + } + s := ui.Local(c.Root(), "325", func() *state { + at := time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC) + lines := make([]mujica.LogLine, 100_000) + words := []string{"ledger synced", "seal applied", "herald dispatched", "archive rotated"} + for i := range lines { + lines[i] = mujica.LogLine{Time: at.Add(time.Duration(i) * time.Second), Level: mujica.LogLevel(i % 7 % 4), Text: fmt.Sprintf("%s #%d", words[i%4], i)} + } + return &state{lines: lines} + }) + st := *s + codeDemoControls(c, func() { + codeDemoCheck(c, &st.disabled, "Disabled", log) + if mujica.Button(c, "Append line", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + st.lines = append(st.lines, mujica.LogLine{Time: time.Now(), Level: mujica.LogWarn, Text: fmt.Sprintf("late seal #%d", len(st.lines))}) + log("append") + } + }) + r := mujica.LogViewer(c, st.lines, &st.s, mujica.LogViewerOptions{Disabled: st.disabled}) + r.Element.Size(720, 360) + if r.Changed() { + log(fmt.Sprintf("hidden %v, follow %v", st.s.Hidden, st.s.Follow)) + } +} diff --git a/examples/gallery/pages_code_debug.go b/examples/gallery/pages_code_debug.go new file mode 100644 index 0000000..a6e91a2 --- /dev/null +++ b/examples/gallery/pages_code_debug.go @@ -0,0 +1,238 @@ +package main + +import ( + "fmt" + "strings" + + "github.com/egoist/mygo/ui" + + mujica "mujicaui" +) + +func codeSymbolDemo(c *ui.Context, log func(string)) { + type state struct { + s mujica.SymbolOutlineState + cursor float64 + disabled bool + } + s := ui.Local(c.Root(), "333", func() *state { return &state{cursor: 15} }) + st := *s + codeDemoControls(c, func() { + codeDemoCheck(c, &st.disabled, "Disabled", log) + ui.Text(c, fmt.Sprintf("Cursor line %.0f", st.cursor)) + if mujica.Slider(c, &st.cursor, mujica.SliderOptions{Min: 1, Max: 34, Step: 1}).Changed() { + log(fmt.Sprintf("cursor %.0f", st.cursor)) + } + }) + symbols := []mujica.Symbol{ + {Name: "Decree", Kind: mujica.SymbolType, Line: 6, EndLine: 9, Detail: "struct", Children: []mujica.Symbol{ + {Name: "ID", Kind: mujica.SymbolField, Line: 7, EndLine: 7, Detail: "int"}, + {Name: "Title", Kind: mujica.SymbolField, Line: 8, EndLine: 8, Detail: "string"}, + }}, + {Name: "Seal", Kind: mujica.SymbolFunction, Line: 13, EndLine: 21, Detail: "(d Decree) (string, error)"}, + {Name: "Herald", Kind: mujica.SymbolFunction, Line: 23, EndLine: 31, Detail: "(ds []Decree) int"}, + } + r := mujica.SymbolOutline(c, symbols, &st.s, mujica.SymbolOutlineOptions{CursorLine: int(st.cursor), Disabled: st.disabled}) + r.Element.Size(420, 320) + if r.Jumped != nil { + log(fmt.Sprintf("jump to %s, line %d", r.Jumped.Name, r.Jumped.Line)) + } +} + +func codeCompletionDemo(c *ui.Context, log func(string)) { + type state struct { + text string + open bool + s mujica.CompletionMenuState + disabled bool + } + s := ui.Local(c.Root(), "334", func() *state { return &state{} }) + st := *s + codeDemoControls(c, func() { codeDemoCheck(c, &st.disabled, "Disabled", log) }) + all := []mujica.Completion{ + {Label: "Seal", Kind: mujica.SymbolFunction, Detail: "func(d Decree) (string, error)", Doc: "Seal stamps a decree with the royal wax."}, + {Label: "Search", Kind: mujica.SymbolFunction, Detail: "func(q string) []Decree"}, + {Label: "Season", Kind: mujica.SymbolConstant, Detail: "= 4"}, + {Label: "Herald", Kind: mujica.SymbolFunction, Detail: "func(ds []Decree) int"}, + {Label: "Decree", Kind: mujica.SymbolType, Detail: "struct", Doc: "Decree is an order of the court."}, + } + word := st.text[strings.LastIndexAny(st.text, " .(")+1:] + var items []mujica.Completion + for _, it := range all { + if word != "" && strings.HasPrefix(strings.ToLower(it.Label), strings.ToLower(word)) { + items = append(items, it) + } + } + ui.Column(c).Width(420).Gap(8).Children(func() { + ui.Text(c, "Type “se” or “de” to see suggestions.").TextColor(mujica.Tokens(c).TextMuted) + in := mujica.TextInput(c, &st.text, mujica.TextInputOptions{Label: "Code", Placeholder: "court.", Disabled: st.disabled}) + if in.Changed() { + st.open = len(items) > 0 + } + r := mujica.CompletionMenu(c, in.Input, &st.open, items, &st.s, mujica.CompletionMenuOptions{Disabled: st.disabled}) + if r.Accepted != nil { + st.text = st.text[:len(st.text)-len(word)] + r.Accepted.Label + log("accept " + r.Accepted.Label) + } + }) +} + +func codeDebugDemo(c *ui.Context, log func(string)) { + type state struct { + dbg mujica.DebugState + disabled bool + } + s := ui.Local(c.Root(), "335", func() *state { return &state{dbg: mujica.DebugPaused} }) + st := *s + names := []string{"Stopped", "Running", "Paused"} + codeDemoControls(c, func() { + codeDemoCheck(c, &st.disabled, "Disabled", log) + ui.Text(c, "State: "+names[st.dbg]) + }) + a := mujica.DebugToolbar(c, st.dbg, mujica.DebugToolbarOptions{Disabled: st.disabled}).Action + switch a { + case mujica.DebugNone: + return + case mujica.DebugContinue, mujica.DebugRestart: + st.dbg = mujica.DebugRunning + case mujica.DebugPause, mujica.DebugStepOver, mujica.DebugStepInto, mujica.DebugStepOut: + st.dbg = mujica.DebugPaused + case mujica.DebugStop: + st.dbg = mujica.DebugStopped + } + log(fmt.Sprintf("action %d, now %s", a, names[st.dbg])) +} + +func codeBreakpointDemo(c *ui.Context, log func(string)) { + type state struct { + bps []mujica.Breakpoint + disabled bool + } + s := ui.Local(c.Root(), "336", func() *state { + return &state{bps: []mujica.Breakpoint{ + {ID: "1", File: "court/seal.go", Line: 14, Enabled: true}, + {ID: "2", File: "court/ledger.go", Line: 40, Condition: "n > 3"}, + {ID: "3", File: "main.go", Line: 9, Enabled: true}, + }} + }) + st := *s + codeDemoControls(c, func() { codeDemoCheck(c, &st.disabled, "Disabled", log) }) + r := mujica.BreakpointList(c, &st.bps, mujica.BreakpointListOptions{Disabled: st.disabled}) + r.Element.Size(480, 240) + for _, e := range []struct{ what, id string }{{"toggled", r.Toggled}, {"condition", r.Edited}, {"open", r.Opened}} { + if e.id != "" { + log(e.what + " " + e.id) + } + } +} + +func codeCallStackDemo(c *ui.Context, log func(string)) { + type state struct { + s mujica.CallStackState + disabled bool + } + s := ui.Local(c.Root(), "337", func() *state { return &state{s: mujica.CallStackState{Current: "f1"}} }) + st := *s + codeDemoControls(c, func() { codeDemoCheck(c, &st.disabled, "Disabled", log) }) + threads := []mujica.DebugThread{ + {ID: "t1", Name: "main", Frames: []mujica.StackFrame{ + {ID: "f1", Name: "court.Seal", File: "court/seal.go", Line: 14}, + {ID: "f2", Name: "court.Herald", File: "court/seal.go", Line: 27}, + {ID: "f3", Name: "main.main", File: "main.go", Line: 9}, + }}, + {ID: "t2", Name: "ledger writer", Frames: []mujica.StackFrame{{ID: "f4", Name: "ledger.Flush", File: "ledger/write.go", Line: 51}}}, + } + r := mujica.CallStack(c, threads, &st.s, mujica.CallStackOptions{Disabled: st.disabled}) + r.Element.Size(440, 300) + if r.Changed() { + log("frame " + st.s.Current) + } +} + +func codeVariablesDemo(c *ui.Context, log func(string)) { + type state struct { + vars []mujica.Variable + s mujica.VariablesPanelState + disabled bool + } + s := ui.Local(c.Root(), "338", func() *state { + return &state{vars: []mujica.Variable{ + {Name: "d", Type: "court.Decree", Value: "{…}", Children: []mujica.Variable{ + {Name: "ID", Value: "42", Type: "int", Changed: true}, + {Name: "Title", Value: `"Of seals"`, Type: "string"}, + }}, + {Name: "i", Value: "2", Type: "int", Changed: true}, + {Name: "err", Value: "nil", Type: "error"}, + }} + }) + st := *s + codeDemoControls(c, func() { codeDemoCheck(c, &st.disabled, "Disabled", log) }) + r := mujica.VariablesPanel(c, st.vars, &st.s, mujica.VariablesPanelOptions{Disabled: st.disabled}) + r.Element.Size(440, 260) + if r.Edited != nil { + log(fmt.Sprintf("set %s = %s", r.Edited.Path, r.Edited.Value)) + codeDemoSet(st.vars, strings.Split(r.Edited.Path, "."), r.Edited.Value) + } +} + +// codeDemoSet writes value at the dotted path. +func codeDemoSet(vars []mujica.Variable, path []string, value string) { + for i := range vars { + if vars[i].Name != path[0] { + continue + } + if len(path) == 1 { + vars[i].Value, vars[i].Changed = value, true + return + } + codeDemoSet(vars[i].Children, path[1:], value) + } +} + +func codeHexDemo(c *ui.Context, log func(string)) { + type state struct { + s mujica.HexViewerState + disabled bool + } + s := ui.Local(c.Root(), "339", func() *state { return &state{} }) + st := *s + codeDemoControls(c, func() { codeDemoCheck(c, &st.disabled, "Disabled", log) }) + data := []byte(strings.Join(codeDemoSource, "\n")) + r := mujica.HexViewer(c, data, &st.s, mujica.HexViewerOptions{Disabled: st.disabled}) + r.Element.Size(720, 300) + if r.Changed() { + a, b := st.s.Selection() + log(fmt.Sprintf("bytes %d..%d", a, b)) + } +} + +func codeFlamegraphDemo(c *ui.Context, log func(string)) { + type state struct { + s mujica.FlamegraphState + status string + disabled bool + } + s := ui.Local(c.Root(), "340", func() *state { return &state{status: "Ready"} }) + st := *s + codeDemoControls(c, func() { + codeDemoCheck(c, &st.disabled, "Disabled", log) + opts := []mujica.SelectOption[string]{{Value: "Ready", Label: "Ready"}, {Value: "Loading", Label: "Loading"}, {Value: "Failed", Label: "Failed"}} + if mujica.Select(c, &st.status, opts, mujica.SelectOptions{Label: "Status"}).Changed() { + log("status " + st.status) + } + }) + root := &mujica.FlameNode{Name: "main", Value: 240, Children: []mujica.FlameNode{ + {Name: "court.Herald", Value: 150, Children: []mujica.FlameNode{ + {Name: "court.Seal", Value: 110, Children: []mujica.FlameNode{{Name: "wax.Melt", Value: 70}, {Name: "ink.Dry", Value: 25}}}, + {Name: "ledger.Write", Value: 30}, + }}, + {Name: "archive.Load", Value: 60, Children: []mujica.FlameNode{{Name: "json.Decode", Value: 45}}}, + {Name: "gc", Value: 20}, + }} + status := map[string]mujica.DataStatus{"Ready": mujica.DataReady, "Loading": mujica.DataLoading, "Failed": mujica.DataFailed}[st.status] + ui.Column(c).Width(680).Children(func() { + if mujica.Flamegraph(c, root, &st.s, mujica.FlamegraphOptions{Unit: "ms", Status: status, Error: "Profile could not be read", Disabled: st.disabled}).Changed() { + log(fmt.Sprintf("zoom %v focus %v", st.s.Zoom, st.s.Focus)) + } + }) +} diff --git a/examples/gallery/pages_code_snippets.go b/examples/gallery/pages_code_snippets.go new file mode 100644 index 0000000..3cff0a4 --- /dev/null +++ b/examples/gallery/pages_code_snippets.go @@ -0,0 +1,230 @@ +package main + +// Snippets are the bodies of the Example functions in example_code_test.go. + +const codeTerminalCode = `var term mujica.TerminalState +term.Write("\x1b[1m~/court\x1b[0m $ ls\r\n\x1b[34mdecrees\x1b[0m ledger.txt\r\n$ ") +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.Terminal(c, &term, mujica.TerminalOptions{ + Input: func(data string) { fmt.Printf("to pty: %q\n", data) }, + Resized: func(cols, rows int) { fmt.Println("pty size", cols, rows) }, + }).Size(380, 200) +} +ui.Render(view, 400, 300, 1)` + +const codeTerminalTabsCode = `tabs := []mujica.TerminalTab{{ID: "a", Title: "zsh"}, {ID: "b", Title: "build", Busy: true}} +active := "a" +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + r := mujica.TerminalTabs(c, &tabs, &active, mujica.TerminalTabsOptions{}, func(id string) { + ui.Text(c, "terminal "+id) + }) + if r.Closed != "" { + fmt.Println("close pty", r.Closed) + } +} +ui.Render(view, 400, 300, 1)` + +const codeTerminalSearchCode = `var term mujica.TerminalState +term.Write("build ok\r\nerror: missing seal\r\n") +var search mujica.TerminalSearchState +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + ui.Column(c).Gap(8).Children(func() { + mujica.TerminalSearch(c, &term, &search, mujica.TerminalSearchOptions{}) + mujica.Terminal(c, &term, mujica.TerminalOptions{}).Height(160) + }) +} +ui.Render(view, 400, 300, 1)` + +const codeCommandHistoryCode = `history := []mujica.HistoryEntry{{Command: "git status"}, {Command: "go test ./...", ExitCode: 1}} +var state mujica.CommandHistoryState +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + r := mujica.CommandHistory(c, history, &state, mujica.CommandHistoryOptions{}) + r.Element.Height(240) + if r.Replayed != nil { + fmt.Println("run", r.Replayed.Command) + } +} +ui.Render(view, 400, 300, 1)` + +const codeLogViewerCode = `lines := []mujica.LogLine{{Level: mujica.LogInfo, Text: "court opened"}, {Level: mujica.LogError, Text: "seal missing"}} +var state mujica.LogViewerState +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.LogViewer(c, lines, &state, mujica.LogViewerOptions{}).Element.Height(240) +} +ui.Render(view, 400, 300, 1)` + +const codeProcessListCode = `procs := []mujica.Process{{PID: 7, Name: "archivist", CPU: 42, Memory: 50 << 20}, {PID: 9, Name: "herald", CPU: 3, Memory: 8 << 20}} +var state mujica.ProcessListState +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + r := mujica.ProcessList(c, procs, &state, mujica.ProcessListOptions{}) + r.Element.Height(240) + if r.End != nil { + fmt.Println("kill", r.End.PID) + } +} +ui.Render(view, 400, 300, 1)` + +const codeCodeViewerCode = `lines := []string{"func Seal(d string) int {", "\treturn len(d)", "}"} +var state mujica.CodeViewerState +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.CodeViewer(c, lines, &state, mujica.CodeViewerOptions{Language: "go"}).Element.Height(240) +} +ui.Render(view, 400, 300, 1)` + +const codeMinimapCode = `lines := []string{"package court", "", "func Seal() {}", "func Herald() {}"} +view := mujica.MinimapView{Top: 0, Visible: 2} +draw := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + if mujica.Minimap(c, lines, &view, mujica.MinimapOptions{Language: "go"}).Changed() { + fmt.Println("scroll editor to", view.Top) + } +} +ui.Render(draw, 400, 300, 1)` + +const codeFindWidgetCode = `text := "the seal and the seal" +state := mujica.FindState{Query: "seal", Replacement: "signet", ShowReplace: true} +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + if mujica.FindWidget(c, &text, &state, mujica.FindWidgetOptions{}).Replaced > 0 { + fmt.Println(text) + } +} +ui.Render(view, 400, 300, 1)` + +const codeSearchPanelCode = `results := []mujica.SearchFileResult{{Path: "court/seal.go", Matches: []mujica.SearchMatch{{Line: 12, Text: "func Seal() {", Start: 5, End: 9}}}} +state := mujica.SearchPanelState{Query: "seal"} +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + r := mujica.SearchPanel(c, results, &state, mujica.SearchPanelOptions{}) + r.Element.Height(280) + if r.Opened != nil { + fmt.Println("open", r.Opened.Path, r.Opened.Line) + } +} +ui.Render(view, 400, 300, 1)` + +const codeProblemsPanelCode = `problems := []mujica.Problem{{Severity: mujica.ProblemError, File: "court/seal.go", Line: 12, Col: 5, Message: "undefined: Decree"}} +var state mujica.ProblemsPanelState +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + r := mujica.ProblemsPanel(c, problems, &state, mujica.ProblemsPanelOptions{}) + r.Element.Height(240) + if r.Opened != nil { + fmt.Println("go to", r.Opened.File, r.Opened.Line) + } +} +ui.Render(view, 400, 300, 1)` + +const codeOutputPanelCode = `channels := []mujica.OutputChannel{{Name: "Build", Lines: []string{"compiling court", "\x1b[32mok\x1b[0m"}}} +var state mujica.OutputPanelState +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + r := mujica.OutputPanel(c, channels, &state, mujica.OutputPanelOptions{}) + r.Element.Height(240) + if r.Cleared != "" { + channels[state.Channel].Lines = nil + } +} +ui.Render(view, 400, 300, 1)` + +const codeSymbolOutlineCode = `symbols := []mujica.Symbol{{Name: "Court", Kind: mujica.SymbolType, Line: 3, EndLine: 40, Children: []mujica.Symbol{ + {Name: "Seal", Kind: mujica.SymbolMethod, Line: 10, EndLine: 20}, +}}} +var state mujica.SymbolOutlineState +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + r := mujica.SymbolOutline(c, symbols, &state, mujica.SymbolOutlineOptions{CursorLine: 12}) + r.Element.Height(240) + if r.Jumped != nil { + fmt.Println("go to line", r.Jumped.Line) + } +} +ui.Render(view, 400, 300, 1)` + +const codeCompletionMenuCode = `text := "se" +open := true +items := []mujica.Completion{{Label: "Seal", Kind: mujica.SymbolMethod, Detail: "func()"}, {Label: "Search", Kind: mujica.SymbolFunction}} +var state mujica.CompletionMenuState +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + in := mujica.TextInput(c, &text, mujica.TextInputOptions{Label: "Code"}) + if r := mujica.CompletionMenu(c, in.Input, &open, items, &state, mujica.CompletionMenuOptions{}); r.Accepted != nil { + text = r.Accepted.Label + } +} +ui.Render(view, 400, 300, 1)` + +const codeDebugToolbarCode = `state := mujica.DebugPaused +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + switch mujica.DebugToolbar(c, state, mujica.DebugToolbarOptions{}).Action { + case mujica.DebugContinue: + state = mujica.DebugRunning + case mujica.DebugStop: + state = mujica.DebugStopped + } +} +ui.Render(view, 400, 300, 1)` + +const codeBreakpointListCode = `bps := []mujica.Breakpoint{{ID: "a", File: "court/seal.go", Line: 12, Enabled: true}, {ID: "b", File: "main.go", Line: 9, Condition: "n > 3"}} +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + r := mujica.BreakpointList(c, &bps, mujica.BreakpointListOptions{}) + r.Element.Height(200) + if r.Toggled != "" || r.Edited != "" { + fmt.Println("sync breakpoints with the debugger") + } +} +ui.Render(view, 400, 300, 1)` + +const codeCallStackCode = `threads := []mujica.DebugThread{{ID: "t1", Name: "main", Frames: []mujica.StackFrame{ + {ID: "f1", Name: "court.Seal", File: "court/seal.go", Line: 14}, + {ID: "f2", Name: "main.main", File: "main.go", Line: 9}, +}}} +state := mujica.CallStackState{Current: "f1"} +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + r := mujica.CallStack(c, threads, &state, mujica.CallStackOptions{}) + r.Element.Height(200) + if r.Changed() { + fmt.Println("show frame", state.Current) + } +} +ui.Render(view, 400, 300, 1)` + +const codeVariablesPanelCode = `vars := []mujica.Variable{{Name: "decree", Value: "{…}", Children: []mujica.Variable{{Name: "id", Value: "42", Changed: true}}}, {Name: "count", Value: "7"}} +var state mujica.VariablesPanelState +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + r := mujica.VariablesPanel(c, vars, &state, mujica.VariablesPanelOptions{}) + r.Element.Height(200) + if r.Edited != nil { + fmt.Println("set", r.Edited.Path, "=", r.Edited.Value) + } +} +ui.Render(view, 400, 300, 1)` + +const codeHexViewerCode = `data := []byte("MUJICA court ledger\x00\x01") +var state mujica.HexViewerState +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.HexViewer(c, data, &state, mujica.HexViewerOptions{}).Element.Height(160) +} +ui.Render(view, 400, 300, 1)` + +const codeFlamegraphCode = `root := &mujica.FlameNode{Name: "main", Value: 100, Children: []mujica.FlameNode{ + {Name: "court.Seal", Value: 60}, {Name: "ledger.Write", Value: 30}, +}} +var state mujica.FlamegraphState +view := func(c *ui.Context) { + mujica.Use(c, mujica.Settings{}) + mujica.Flamegraph(c, root, &state, mujica.FlamegraphOptions{Unit: "ms"}) +} +ui.Render(view, 400, 300, 1)` diff --git a/examples/gallery/pages_code_tools.go b/examples/gallery/pages_code_tools.go new file mode 100644 index 0000000..16357dc --- /dev/null +++ b/examples/gallery/pages_code_tools.go @@ -0,0 +1,264 @@ +package main + +import ( + "fmt" + "strings" + + "github.com/egoist/mygo/ui" + + mujica "mujicaui" +) + +func codeProcessListDemo(c *ui.Context, log func(string)) { + type state struct { + procs []mujica.Process + s mujica.ProcessListState + disabled bool + } + s := ui.Local(c.Root(), "326", func() *state { + return &state{procs: []mujica.Process{ + {PID: 101, Name: "chancellor", CPU: 3.5, Memory: 200 << 20}, {PID: 7, Name: "archivist", CPU: 42.1, Memory: 50 << 20}, + {PID: 330, Name: "herald", CPU: 0.2, Memory: 2 << 30}, {PID: 412, Name: "scribe", CPU: 12.8, Memory: 96 << 20}, + }} + }) + st := *s + codeDemoControls(c, func() { codeDemoCheck(c, &st.disabled, "Disabled", log) }) + r := mujica.ProcessList(c, st.procs, &st.s, mujica.ProcessListOptions{Disabled: st.disabled}) + r.Element.Size(560, 260) + if r.End != nil { + log(fmt.Sprintf("end %s (PID %d)", r.End.Name, r.End.PID)) + var kept []mujica.Process + for _, p := range st.procs { + if p.PID != r.End.PID { + kept = append(kept, p) + } + } + st.procs = kept + } +} + +var codeDemoSource = strings.Split(`package court + +import "errors" + +// Decree is an order of the court. +type Decree struct { + ID int + Title string +} + +/* Seal stamps a decree + with the royal wax. */ +func Seal(d Decree) (string, error) { + if d.Title == "" { + return "", errors.New("untitled decree") + } + for i := 0; i < 3; i++ { + d.ID += i + } + return "sealed: " + d.Title, nil +} + +func Herald(ds []Decree) int { + n := 0 + for _, d := range ds { + if _, err := Seal(d); err == nil { + n++ + } + } + return n +}`, "\n") + +func codeCodeViewerDemo(c *ui.Context, log func(string)) { + type state struct { + s mujica.CodeViewerState + lang string + disabled bool + } + s := ui.Local(c.Root(), "327", func() *state { return &state{lang: "go"} }) + st := *s + codeDemoControls(c, func() { + codeDemoCheck(c, &st.disabled, "Disabled", log) + langs := []mujica.SelectOption[string]{{Value: "go", Label: "Go"}, {Value: "", Label: "Plain"}} + if mujica.Select(c, &st.lang, langs, mujica.SelectOptions{Label: "Language"}).Changed() { + log("language " + st.lang) + } + if mujica.Button(c, "Go to line 20", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + st.s.GoTo(19) + log("go to 20") + } + }) + r := mujica.CodeViewer(c, codeDemoSource, &st.s, mujica.CodeViewerOptions{Language: st.lang, Label: "Source", Disabled: st.disabled}) + r.Element.Size(600, 360) + if r.Changed() { + log(fmt.Sprintf("line %d", st.s.Line()+1)) + } +} + +func codeMinimapDemo(c *ui.Context, log func(string)) { + type state struct { + s mujica.CodeViewerState + view mujica.MinimapView + lines []string + disabled bool + } + s := ui.Local(c.Root(), "328", func() *state { + var lines []string + for range 12 { + lines = append(lines, codeDemoSource...) + } + return &state{lines: lines} + }) + st := *s + codeDemoControls(c, func() { codeDemoCheck(c, &st.disabled, "Disabled", log) }) + first, last := st.s.Visible() + st.view = mujica.MinimapView{Top: first, Visible: max(last-first+1, 1)} + ui.Row(c).Gap(8).Height(360).AlignItems(ui.Stretch).Children(func() { + mujica.CodeViewer(c, st.lines, &st.s, mujica.CodeViewerOptions{Language: "go"}).Element.Width(520) + if mujica.Minimap(c, st.lines, &st.view, mujica.MinimapOptions{Language: "go", Disabled: st.disabled}).Changed() { + st.s.ScrollTo(st.view.Top) + log(fmt.Sprintf("jump to line %d", st.view.Top+1)) + } + }) +} + +func codeFindWidgetDemo(c *ui.Context, log func(string)) { + type state struct { + text string + s mujica.FindState + readOnly, disabled bool + } + s := ui.Local(c.Root(), "329", func() *state { + return &state{text: "The seal of the court; seals are kept sealed. Seal 42 and seal 7.", s: mujica.FindState{Query: "seal"}} + }) + st := *s + codeDemoControls(c, func() { + codeDemoCheck(c, &st.readOnly, "Read-only", log) + codeDemoCheck(c, &st.disabled, "Disabled", log) + }) + ui.Column(c).Width(620).Gap(12).Children(func() { + r := mujica.FindWidget(c, &st.text, &st.s, mujica.FindWidgetOptions{ReadOnly: st.readOnly, Disabled: st.disabled}) + if r.Moved { + log(fmt.Sprintf("%d matches, at %d", len(st.s.Matches()), st.s.Current+1)) + } + if r.Replaced > 0 { + log(fmt.Sprintf("replaced %d", r.Replaced)) + } + var spans []ui.Span + at := 0 + for i, m := range st.s.Matches() { + spans = append(spans, ui.Span{Text: st.text[at:m[0]]}) + sp := ui.Span{Text: st.text[m[0]:m[1]], Background: mujica.Tokens(c).Selection, Underline: true} + if i == st.s.Current { + sp.Weight = 700 + } + spans = append(spans, sp) + at = m[1] + } + spans = append(spans, ui.Span{Text: st.text[at:]}) + ui.RichText(c, spans...) + }) +} + +func codeSearchPanelDemo(c *ui.Context, log func(string)) { + type state struct { + s mujica.SearchPanelState + results []mujica.SearchFileResult + disabled bool + } + s := ui.Local(c.Root(), "330", func() *state { return &state{s: mujica.SearchPanelState{Query: "seal"}} }) + st := *s + codeDemoControls(c, func() { codeDemoCheck(c, &st.disabled, "Disabled", log) }) + // The demo searches its own two files. + files := map[string][]string{"court/seal.go": codeDemoSource, "docs/ledger.md": {"# Ledger", "The seal is kept by the chancellor.", "Seals expire after a year."}} + if st.results == nil || st.s.Query != "" && len(st.results) == 0 { + st.results = codeDemoSearch(files, st.s.Query, st.s.Include, st.s.Exclude) + } + r := mujica.SearchPanel(c, st.results, &st.s, mujica.SearchPanelOptions{Disabled: st.disabled}) + r.Element.Size(560, 380) + if r.Changed { + st.results = codeDemoSearch(files, st.s.Query, st.s.Include, st.s.Exclude) + log(fmt.Sprintf("search %q include %q exclude %q", st.s.Query, st.s.Include, st.s.Exclude)) + } + if r.Opened != nil { + log(fmt.Sprintf("open %s:%d", r.Opened.Path, r.Opened.Line)) + } +} + +// codeDemoSearch finds q in the demo files. +func codeDemoSearch(files map[string][]string, q, include, exclude string) []mujica.SearchFileResult { + out := []mujica.SearchFileResult{} + if q == "" { + return out + } + for _, path := range []string{"court/seal.go", "docs/ledger.md"} { + if include != "" && !strings.Contains(path, strings.Trim(include, "*")) || exclude != "" && strings.Contains(path, strings.Trim(exclude, "*")) { + continue + } + f := mujica.SearchFileResult{Path: path} + for i, line := range files[path] { + if at := strings.Index(strings.ToLower(line), strings.ToLower(q)); at >= 0 { + f.Matches = append(f.Matches, mujica.SearchMatch{Line: i + 1, Text: line, Start: at, End: at + len(q)}) + } + } + if len(f.Matches) > 0 { + out = append(out, f) + } + } + return out +} + +func codeProblemsDemo(c *ui.Context, log func(string)) { + type state struct { + s mujica.ProblemsPanelState + disabled bool + } + s := ui.Local(c.Root(), "331", func() *state { return &state{} }) + st := *s + codeDemoControls(c, func() { codeDemoCheck(c, &st.disabled, "Disabled", log) }) + problems := []mujica.Problem{ + {Severity: mujica.ProblemError, File: "court/seal.go", Line: 12, Col: 5, Message: "undefined: Decree", Source: "compiler"}, + {Severity: mujica.ProblemWarning, File: "court/seal.go", Line: 30, Col: 2, Message: "unused variable wax", Source: "vet"}, + {Severity: mujica.ProblemInfo, File: "docs/ledger.md", Line: 4, Col: 1, Message: "line is longer than 80", Source: "lint"}, + {Severity: mujica.ProblemError, File: "main.go", Line: 9, Col: 14, Message: "missing return", Source: "compiler"}, + } + r := mujica.ProblemsPanel(c, problems, &st.s, mujica.ProblemsPanelOptions{Disabled: st.disabled}) + r.Element.Size(620, 320) + if r.Opened != nil { + log(fmt.Sprintf("go to %s:%d:%d", r.Opened.File, r.Opened.Line, r.Opened.Col)) + } +} + +func codeOutputDemo(c *ui.Context, log func(string)) { + type state struct { + channels []mujica.OutputChannel + s mujica.OutputPanelState + disabled bool + } + s := ui.Local(c.Root(), "332", func() *state { + var build []string + for i := range 300 { + build = append(build, fmt.Sprintf("compiling unit %d of 300", i+1)) + } + return &state{channels: []mujica.OutputChannel{{Name: "Build", Lines: build}, + {Name: "Tests", Lines: []string{"\x1b[32mPASS\x1b[0m court/seal", "\x1b[31mFAIL\x1b[0m court/ledger: expected 3 seals", "\x1b[1mok\x1b[0m 2 of 3 packages"}}}} + }) + st := *s + codeDemoControls(c, func() { + codeDemoCheck(c, &st.disabled, "Disabled", log) + if mujica.Button(c, "Write line", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() { + ch := &st.channels[st.s.Channel] + ch.Lines = append(ch.Lines, fmt.Sprintf("line %d", len(ch.Lines)+1)) + log("write to " + ch.Name) + } + }) + r := mujica.OutputPanel(c, st.channels, &st.s, mujica.OutputPanelOptions{Disabled: st.disabled}) + r.Element.Size(560, 300) + if r.Changed { + log("channel " + st.channels[st.s.Channel].Name) + } + if r.Cleared != "" { + st.channels[st.s.Channel].Lines = nil + log("cleared " + r.Cleared) + } +} diff --git a/find_widget.go b/find_widget.go new file mode 100644 index 0000000..97f8629 --- /dev/null +++ b/find_widget.go @@ -0,0 +1,181 @@ +package mujica + +import ( + "fmt" + "log/slog" + "regexp" + + "github.com/egoist/mygo/ui" + + "mujicaui/icons" +) + +// FindState is a FindWidget's query, options and current match. +type FindState struct { + Query, Replacement string + // MatchCase, WholeWord and Regex refine the query. + MatchCase, WholeWord, Regex bool + // ShowReplace opens the replace row. + ShowReplace bool + // Current is the chosen match, from 0. + Current int + matches [][]int + err string + key findKey +} + +type findKey struct { + text, query string + matchCase, word, regex bool + ran bool +} + +// Matches returns the byte ranges of matches. +func (s *FindState) Matches() [][2]int { + out := make([][2]int, len(s.matches)) + for i, m := range s.matches { + out[i] = [2]int{m[0], m[1]} + } + return out +} + +// FindWidgetOptions configure a FindWidget. +type FindWidgetOptions struct { + // ReadOnly hides replacing. + ReadOnly bool + Disabled bool +} + +// FindWidgetResult is a FindWidget and what happened in this frame. +type FindWidgetResult struct { + Element *ui.Element + // Moved reports navigation; Replaced counts replacements. + Moved bool + Replaced int +} + +// FindWidget finds and replaces in *text. +func FindWidget(c *ui.Context, text *string, s *FindState, opts FindWidgetOptions) FindWidgetResult { + if text == nil || s == nil { + panic("mujica: FindWidget needs text and a state") + } + k := Tokens(c) + u := Density(c).Unit() + var res FindWidgetResult + res.Element = codePanel(c).Padding(u * 2).Gap(u * 2).Disabled(opts.Disabled) + disabled := res.Element.IsDisabled() + findRun(*text, s) + res.Element.Children(func() { + ui.Row(c).Gap(u * 2).AlignItems(ui.Center).Children(func() { + if !opts.ReadOnly { + chev := "chevron-right" + if s.ShowReplace { + chev = "chevron-down" + } + if iconAction(c, icons.Must(chev), msg(c, "find_widget.toggleReplace")).Clicked() && !disabled { + s.ShowReplace = !s.ShowReplace + } + } + r := SearchInput(c, &s.Query, SearchInputOptions{Find: true, Matches: len(s.matches), Current: &s.Current, Disabled: opts.Disabled}) + r.Element.Grow(1).MinWidth(0) + changed := r.Changed() || r.Moved() + for _, o := range []struct { + on *bool + short string + key string + }{{&s.MatchCase, "Aa", "find_widget.case"}, {&s.WholeWord, "ab", "find_widget.word"}, {&s.Regex, ".*", "find_widget.regex"}} { + if Toggle(c, o.on, o.short, ToggleOptions{Label: msg(c, o.key), Disabled: opts.Disabled}).Tooltip(msg(c, o.key)).Changed() { + changed = true + } + } + if changed { + findRun(*text, s) + res.Moved = true + slog.Debug("mujica: find_widget move", "matches", len(s.matches), "current", s.Current) + } + }) + if s.err != "" { + ui.Text(c, fmt.Sprintf(msg(c, "find_widget.invalid"), s.err)).TextColor(k.Danger).Role(ui.RoleStatus). + FontSize(FontSize(c, 12)) + } + if !s.ShowReplace || opts.ReadOnly { + return + } + ui.Row(c).Gap(u*2).AlignItems(ui.Center).Padding(0, 0, 0, 28+u*2).Children(func() { + in := TextInput(c, &s.Replacement, TextInputOptions{Placeholder: msg(c, "find_widget.replace"), Label: msg(c, "find_widget.replace"), Disabled: opts.Disabled}) + in.Element.Grow(1).MinWidth(0) + none := len(s.matches) == 0 + if (Button(c, msg(c, "find_widget.replaceOne"), ButtonOptions{Variant: Secondary, Disabled: none || opts.Disabled}).Clicked() || in.Submitted()) && !none && !disabled { + res.Replaced = findReplace(text, s, false) + } + if Button(c, msg(c, "find_widget.replaceAll"), ButtonOptions{Variant: Secondary, Disabled: none || opts.Disabled}).Clicked() && !disabled { + res.Replaced = findReplace(text, s, true) + } + }) + }) + return res +} + +// findPattern compiles the query with its options. +func findPattern(s *FindState) (*regexp.Regexp, error) { + q := s.Query + if !s.Regex { + q = regexp.QuoteMeta(q) + } + if s.WholeWord { + q = `\b(?:` + q + `)\b` + } + if !s.MatchCase { + q = "(?i)" + q + } + return regexp.Compile(q) +} + +// findRun refinds matches when inputs change. +func findRun(text string, s *FindState) { + key := findKey{text, s.Query, s.MatchCase, s.WholeWord, s.Regex, true} + if key == s.key { + return + } + s.key, s.err, s.matches = key, "", nil + if s.Query != "" { + re, err := findPattern(s) + if err != nil { + s.err = err.Error() + } else { + for _, m := range re.FindAllStringSubmatchIndex(text, -1) { + if m[1] > m[0] { + s.matches = append(s.matches, m) + } + } + } + } + s.Current = max(0, min(s.Current, len(s.matches)-1)) +} + +// findReplace replaces one or all matches. +func findReplace(text *string, s *FindState, all bool) int { + re, err := findPattern(s) + if err != nil { + panic("mujica: FindWidget replaced with an invalid pattern") + } + targets := s.matches + if !all { + targets = s.matches[s.Current : s.Current+1] + } + out := []byte{} + last := 0 + for _, m := range targets { + out = append(out, (*text)[last:m[0]]...) + if s.Regex { + out = re.ExpandString(out, s.Replacement, *text, m) + } else { + out = append(out, s.Replacement...) + } + last = m[1] + } + *text = string(append(out, (*text)[last:]...)) + findRun(*text, s) + slog.Debug("mujica: find_widget replace", "count", len(targets)) + return len(targets) +} diff --git a/find_widget_test.go b/find_widget_test.go new file mode 100644 index 0000000..5a94aef --- /dev/null +++ b/find_widget_test.go @@ -0,0 +1,117 @@ +package mujica + +import ( + "testing" + + "github.com/egoist/mygo/ui" +) + +func findTester(text *string, s *FindState, opts FindWidgetOptions, replaced *int) *ui.Tester { + return ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + *replaced += FindWidget(c, text, s, opts).Replaced + }, 640, 200) +} + +func TestFindWidgetOptions(t *testing.T) { + text := "Seal the seal; sealing wax. SEAL" + s := &FindState{Query: "seal"} + n := 0 + tt := findTester(&text, s, FindWidgetOptions{}, &n) + if len(s.Matches()) != 4 || !tt.HasText("1 of 4") { + t.Fatalf("plain matches %d", len(s.Matches())) + } + if err := tt.Click("Match case"); err != nil { + t.Fatal(err) + } + if len(s.Matches()) != 2 { + t.Fatalf("case-sensitive matches %d, want 2", len(s.Matches())) + } + _ = tt.Click("Whole word") + if len(s.Matches()) != 1 || s.Matches()[0][0] != 9 { + t.Fatalf("whole-word matches %v", s.Matches()) + } + _ = tt.Click("Match case") + _ = tt.Click("Whole word") + _ = tt.Click("Regular expression") + s.Query = `seal(ing)?` + tt.Frame() + if len(s.Matches()) != 4 || s.Matches()[2] != [2]int{15, 22} { + t.Fatalf("regex matches %v", s.Matches()) + } +} + +func TestFindWidgetInvalidRegex(t *testing.T) { + text := "abc" + s := &FindState{Query: "a(", Regex: true} + n := 0 + tt := findTester(&text, s, FindWidgetOptions{}, &n) + if len(s.Matches()) != 0 || !tt.HasText("Invalid pattern: error parsing regexp: missing closing ): `(?i)a(`") { + t.Fatalf("texts %q", tt.Texts()) + } +} + +func TestFindWidgetReplace(t *testing.T) { + text := "one cat, two cat, red cat" + s := &FindState{Query: "cat", Replacement: "owl", ShowReplace: true, Current: 1} + n := 0 + tt := findTester(&text, s, FindWidgetOptions{}, &n) + if err := tt.Click("Replace"); err != nil { + t.Fatal(err) + } + if text != "one cat, two owl, red cat" || n != 1 { + t.Fatalf("replace one gave %q (%d)", text, n) + } + if err := tt.Click("Replace all"); err != nil { + t.Fatal(err) + } + if text != "one owl, two owl, red owl" || n != 3 || len(s.Matches()) != 0 { + t.Fatalf("replace all gave %q (%d)", text, n) + } + text, s.Query, s.Regex, s.Replacement = "a1 b2", `([a-z])(\d)`, true, "$2$1" + tt.Frame() + _ = tt.Click("Replace all") + if text != "1a 2b" { + t.Fatalf("regex replace gave %q", text) + } +} + +func TestFindWidgetKeysAndReadOnly(t *testing.T) { + text := "x y x y x" + s := &FindState{} + n := 0 + tt := findTester(&text, s, FindWidgetOptions{ReadOnly: true}, &n) + if tt.HasText("Replace all") { + t.Fatal("read-only widget offers replace") + } + tt.Key(0, ui.KeyTab) + tt.Type("x") + tt.Key(0, ui.KeyEnter) + if s.Current != 1 || !tt.HasText("2 of 3") { + t.Fatalf("Enter went to %d", s.Current) + } +} + +func TestFindWidgetDisabled(t *testing.T) { + text := "cat" + s := &FindState{Query: "cat", Replacement: "dog", ShowReplace: true} + n := 0 + tt := findTester(&text, s, FindWidgetOptions{Disabled: true}, &n) + _ = tt.Click("Replace all") + _ = tt.Click("Match case") + if text != "cat" || s.MatchCase { + t.Fatalf("disabled widget changed %q case=%v", text, s.MatchCase) + } +} + +func TestFindWidgetSnapshot(t *testing.T) { + snapshot(t, "find_widget", 640, 130, func(c *ui.Context) { + st := ui.Local(c.Root(), "snap", func() *FindState { + return &FindState{Query: "seal", Replacement: "signet", ShowReplace: true, WholeWord: true} + }) + text := "the seal and the seal" + ui.Column(c).Padding(12).Children(func() { + FindWidget(c, &text, *st, FindWidgetOptions{}) + }) + }) +} diff --git a/flamegraph.go b/flamegraph.go new file mode 100644 index 0000000..e3e3673 --- /dev/null +++ b/flamegraph.go @@ -0,0 +1,276 @@ +package mujica + +import ( + "fmt" + "hash/fnv" + "log/slog" + "slices" + "strings" + + "github.com/egoist/mygo/ui" + + "mujicaui/internal" + "mujicaui/theme" +) + +// FlameNode is a profile frame; Value includes children. +type FlameNode struct { + Name string + Value float64 + Children []FlameNode +} + +// FlamegraphState holds zoom and focus paths and the search. +type FlamegraphState struct { + Zoom, Focus []int + Query string + said string +} + +// FlamegraphOptions configure a Flamegraph. +type FlamegraphOptions struct { + // Unit follows values in the readout, as "ms". + Unit string + // Status shows loading or failure in place of the graph. + Status DataStatus + Error string + Disabled bool +} + +// flameRow is a frame row's height, a full hit target. +const flameRow = theme.MinHitSize + +// flamegraphPalette colors frames at 3:1 on Surface. +func flamegraphPalette(k theme.Tokens) []ui.Color { + // Ornament alone is too faint on ivory. + return []ui.Color{k.AccentText, k.Info, k.Success, k.Warning, k.Ornament.Mix(k.Text, 0.2), k.TextMuted} +} + +// Flamegraph draws a zoomable, searchable profile. +func Flamegraph(c *ui.Context, root *FlameNode, s *FlamegraphState, opts FlamegraphOptions) ChoiceResult { + if s == nil { + panic("mujica: Flamegraph needs a state") + } + k := Tokens(c) + d := Density(c) + var res ChoiceResult + res.Element = codePanel(c).Disabled(opts.Disabled) + res.Element.Children(func() { + codeHeader(c, func() { + SearchInput(c, &s.Query, SearchInputOptions{Placeholder: msg(c, "flamegraph.search"), Disabled: opts.Disabled}).Element.Grow(1) + }) + if root == nil || opts.Status != DataReady { + ui.Column(c).Height(flameRow * 6).Children(func() { + res.changed = dataPlaceholder(c, opts.Status, opts.Error, msg(c, "flamegraph.empty"), msg(c, "flamegraph.empty")) + }) + return + } + flameCheck(root) + if flameAt(root, s.Zoom) == nil { + s.Zoom = nil + } + if flameAt(root, s.Focus) == nil { + s.Focus = nil + } + q := strings.ToLower(strings.TrimSpace(s.Query)) + depth := flameDepth(root) + g := ui.Box(c).Height(float32(depth)*flameRow).Shrink(0).Margin(d.Unit()*2, d.Unit()*3, 0, d.Unit()*3).Focusable().Label(msg(c, "flamegraph.label")) + internal.Ring(g, k.Focus, theme.ControlRadius) + hover := ui.Local(g, "flamegraph.hover", func() []int { return nil }) + zoom, focus := s.Zoom, s.Focus + if !g.IsDisabled() { + if flameInput(c, g, root, s, hover) { + res.changed = true + } + } + b := g.Bounds() + g.Draw(func(p *ui.Painter, r ui.Rect) { + if r.W != b.W { + c.Invalidate() + } + flamePaint(p, r, k, root, zoom, focus, q, g.Focused(), FontSize(c, theme.CaptionSize)) + }) + readout := s.Focus + if *hover != nil { + readout = *hover + } + text := flameReadout(c, root, readout, opts.Unit) + ui.Text(c, text).FontSize(FontSize(c, theme.CaptionSize)).TextColor(k.TextMuted).Padding(d.Unit(), d.Unit()*3). + Role(ui.RoleStatus).Shrink(0).SingleLine() + if g.Focused() && text != s.said { + s.said = text + c.Announce(text) + } + }) + return res +} + +// flameInput zooms, moves focus and tracks hover. +func flameInput(c *ui.Context, g *ui.Element, root *FlameNode, s *FlamegraphState, hover *[]int) bool { + b := g.Bounds() + x, y, over := g.PointerPosition() + *hover = nil + if over && b.W > 0 { + *hover = flameHit(root, s.Zoom, x/b.W, int(y/flameRow)) + } + changed := false + zoomTo := func(path []int) { + s.Zoom, s.Focus, changed = slices.Clone(path), slices.Clone(path), true + slog.Debug("mujica: flamegraph zoom", "depth", len(path)) + } + if g.Clicked() && *hover != nil { + zoomTo(*hover) + } + f := s.Focus + switch { + case g.Shortcut(0, ui.KeyEnter): + zoomTo(f) + case g.Shortcut(0, ui.KeyEscape) && len(s.Zoom) > 0: + zoomTo(s.Zoom[:len(s.Zoom)-1]) + case g.Shortcut(0, ui.KeyUp) && len(f) > 0: + s.Focus, changed = f[:len(f)-1], true + case g.Shortcut(0, ui.KeyDown): + if n := flameAt(root, f); len(n.Children) > 0 { + s.Focus, changed = append(slices.Clone(f), 0), true + } + case g.Shortcut(0, ui.KeyLeft) && len(f) > 0 && f[len(f)-1] > 0: + s.Focus, changed = append(slices.Clone(f[:len(f)-1]), f[len(f)-1]-1), true + case g.Shortcut(0, ui.KeyRight) && len(f) > 0 && f[len(f)-1] < len(flameAt(root, f[:len(f)-1]).Children)-1: + s.Focus, changed = append(slices.Clone(f[:len(f)-1]), f[len(f)-1]+1), true + } + return changed +} + +// flameCheck panics on impossible values. +func flameCheck(n *FlameNode) { + sum := 0.0 + for i := range n.Children { + flameCheck(&n.Children[i]) + sum += n.Children[i].Value + } + if n.Value < 0 || sum > n.Value*(1+1e-9) { + panic("mujica: FlameNode " + n.Name + " has a negative value or children larger than itself") + } +} + +func flameAt(root *FlameNode, path []int) *FlameNode { + n := root + for _, i := range path { + if i < 0 || i >= len(n.Children) { + return nil + } + n = &n.Children[i] + } + return n +} + +func flameDepth(n *FlameNode) int { + d := 0 + for i := range n.Children { + d = max(d, flameDepth(&n.Children[i])) + } + return d + 1 +} + +// flameSpan returns a frame's fractional left and width. +func flameSpan(root *FlameNode, zoom, path []int) (x, w float64) { + if len(path) <= len(zoom) { + return 0, 1 + } + z := flameAt(root, zoom) + x, w = 0, 1 + n := z + for _, i := range path[len(zoom):] { + for j := range i { + x += w * n.Children[j].Value / max(n.Value, 1e-12) + } + w *= n.Children[i].Value / max(n.Value, 1e-12) + n = &n.Children[i] + } + return x, w +} + +// flameHit finds the frame at fx in row. +func flameHit(root *FlameNode, zoom []int, fx float32, row int) []int { + if row < len(zoom) { + return slices.Clone(zoom[:row]) + } + path := slices.Clone(zoom) + for len(path) < row { + n := flameAt(root, path) + next := -1 + for i := range n.Children { + x, w := flameSpan(root, zoom, append(path, i)) + if float64(fx) >= x && float64(fx) < x+w { + next = i + } + } + if next < 0 { + return nil + } + path = append(path, next) + } + if x, w := flameSpan(root, zoom, path); float64(fx) < x || float64(fx) >= x+w { + return nil + } + return path +} + +func flameColor(k theme.Tokens, name string) ui.Color { + h := fnv.New32a() + h.Write([]byte(name)) + pal := flamegraphPalette(k) + return pal[h.Sum32()%uint32(len(pal))] +} + +// flamePaint draws visible frames with search and focus marks. +func flamePaint(p *ui.Painter, r ui.Rect, k theme.Tokens, root *FlameNode, zoom, focus []int, q string, focused bool, size float32) { + var walk func(n *FlameNode, path []int) + walk = func(n *FlameNode, path []int) { + fx, fw := flameSpan(root, zoom, path) + w := float32(fw) * r.W + if w < 1 { + return + } + box := ui.Rect{X: internal.Snap(p, r.X+float32(fx)*r.W), Y: r.Y + float32(len(path))*flameRow, W: internal.Snap(p, w) - 1, H: flameRow - 2} + col := flameColor(k, n.Name) + face := col.Mix(k.Surface, 0.78) + match := q != "" && strings.Contains(strings.ToLower(n.Name), q) + above := len(path) < len(zoom) + if q != "" && !match || above { + face, col = k.Surface, k.Border + } + p.Fill(box, face, 2) + p.Stroke(box, col, 2, 1) + if match { + p.Stroke(box, k.AccentText, 2, 2) + } + if focused && slices.Equal(path, focus) { + internal.FocusRing(p, box, 2, k.Focus) + } + if box.W > 24 { + fg := k.Text + if above || q != "" && !match { + fg = k.TextMuted + } + p.Clip(box, 2, func() { p.Text(box.X+6, box.Y+(box.H-size*1.3)/2, n.Name, size, fg) }) + } + for i := range n.Children { + if len(path) < len(zoom) && i != zoom[len(path)] { + continue + } + walk(&n.Children[i], append(slices.Clone(path), i)) + } + } + walk(root, nil) +} + +// flameReadout names a frame's time and share. +func flameReadout(c *ui.Context, root *FlameNode, path []int, unit string) string { + n := flameAt(root, path) + if n == nil { + return msg(c, "flamegraph.hint") + } + share := 100 * n.Value / max(root.Value, 1e-12) + return fmt.Sprintf(msg(c, "flamegraph.readout"), n.Name, n.Value, unit, share) +} diff --git a/flamegraph_test.go b/flamegraph_test.go new file mode 100644 index 0000000..ff6cda6 --- /dev/null +++ b/flamegraph_test.go @@ -0,0 +1,143 @@ +package mujica + +import ( + "testing" + + "github.com/egoist/mygo/ui" + + "mujicaui/theme" +) + +func flameSample() *FlameNode { + return &FlameNode{Name: "main", Value: 100, Children: []FlameNode{ + {Name: "court.Seal", Value: 60, Children: []FlameNode{ + {Name: "wax.Melt", Value: 40}, + {Name: "ink.Dry", Value: 15}, + }}, + {Name: "ledger.Write", Value: 30}, + }} +} + +type flameProbe struct { + root *FlameNode + s FlamegraphState + opts FlamegraphOptions + changed int +} + +func (p *flameProbe) view(c *ui.Context) { + Use(c, Settings{}) + if Flamegraph(c, p.root, &p.s, p.opts).Changed() { + p.changed++ + } +} + +func TestFlamegraphPaletteContrast(t *testing.T) { + for _, k := range []theme.Tokens{theme.Light(), theme.Dark()} { + for i, col := range flamegraphPalette(k) { + if r := theme.Contrast(col, k.Surface); r < 3 { + t.Errorf("dark=%v color %d contrast %.2f < 3", k.Dark, i, r) + } + } + } +} + +func TestFlamegraphClickZoomAndEscape(t *testing.T) { + p := &flameProbe{root: flameSample(), opts: FlamegraphOptions{Unit: "ms"}} + tt := ui.NewTester(p.view, 600, 300) + g, _ := tt.Find("Flame graph") + // Row 1, at 30% of the width, is court.Seal. + tt.ClickAt(g.X+g.W*0.3, g.Y+flameRow*1.5) + if len(p.s.Zoom) != 1 || p.s.Zoom[0] != 0 || p.changed != 1 { + t.Fatalf("zoom %v changed %d", p.s.Zoom, p.changed) + } + if x, w := flameSpan(p.root, p.s.Zoom, []int{0, 0}); x != 0 || w < 0.66 || w > 0.67 { + t.Fatalf("wax.Melt spans %v+%v of the zoomed width", x, w) + } + if !tt.HasText("court.Seal: 60.0 ms, 60.0% of total") { + t.Fatalf("readout %q", tt.Texts()) + } + tt.Key(0, ui.KeyTab) + tt.Key(0, ui.KeyTab) + tt.Key(0, ui.KeyEscape) + if len(p.s.Zoom) != 0 { + t.Fatalf("Escape left zoom %v", p.s.Zoom) + } +} + +func TestFlamegraphKeyboardReadout(t *testing.T) { + p := &flameProbe{root: flameSample(), opts: FlamegraphOptions{Unit: "ms"}} + tt := ui.NewTester(p.view, 600, 300) + tt.Key(0, ui.KeyTab) + tt.Key(0, ui.KeyTab) + if !tt.Focused("Flame graph") { + t.Fatal("Tab did not reach the graph") + } + tt.Key(0, ui.KeyDown) + tt.Key(0, ui.KeyDown) + tt.Key(0, ui.KeyRight) + if len(p.s.Focus) != 2 || p.s.Focus[1] != 1 { + t.Fatalf("focus %v", p.s.Focus) + } + want := "ink.Dry: 15.0 ms, 15.0% of total" + if !tt.HasText(want) { + t.Fatalf("readout %q", tt.Texts()) + } + found := false + for _, a := range tt.Announcements() { + found = found || a == want + } + if !found { + t.Fatalf("readout not announced: %q", tt.Announcements()) + } + tt.Key(0, ui.KeyUp) + tt.Key(0, ui.KeyEnter) + if len(p.s.Zoom) != 1 { + t.Fatalf("Enter zoomed to %v", p.s.Zoom) + } +} + +func TestFlamegraphSearchAndStates(t *testing.T) { + p := &flameProbe{root: flameSample(), s: FlamegraphState{Query: "wax"}} + tt := ui.NewTester(p.view, 600, 300) + if hit := flameHit(p.root, nil, 0.2, 2); len(hit) != 2 || flameAt(p.root, hit).Name != "wax.Melt" { + t.Fatalf("hit %v", hit) + } + _ = tt + p = &flameProbe{opts: FlamegraphOptions{Status: DataFailed, Error: "profile unreadable"}} + tt = ui.NewTester(p.view, 600, 300) + if !tt.HasText("profile unreadable") { + t.Fatalf("texts %q", tt.Texts()) + } + p = &flameProbe{} + tt = ui.NewTester(p.view, 600, 300) + if !tt.HasText("No profile") { + t.Fatalf("texts %q", tt.Texts()) + } +} + +func TestFlamegraphInvalidAndDisabled(t *testing.T) { + p := &flameProbe{root: flameSample(), opts: FlamegraphOptions{Disabled: true}} + tt := ui.NewTester(p.view, 600, 300) + g, _ := tt.Find("Flame graph") + tt.ClickAt(g.X+g.W*0.3, g.Y+flameRow*1.5) + if len(p.s.Zoom) != 0 { + t.Fatal("disabled graph zoomed") + } + defer func() { + if recover() == nil { + t.Fatal("children above their parent did not panic") + } + }() + bad := &FlameNode{Name: "x", Value: 1, Children: []FlameNode{{Name: "y", Value: 2}}} + ui.NewTester((&flameProbe{root: bad}).view, 600, 300) +} + +func TestFlamegraphSnapshot(t *testing.T) { + snapshot(t, "flamegraph", 560, 200, func(c *ui.Context) { + p := ui.Local(c.Root(), "snap", func() *flameProbe { + return &flameProbe{root: flameSample(), s: FlamegraphState{Focus: []int{0, 1}}, opts: FlamegraphOptions{Unit: "ms"}} + }) + ui.Column(c).Padding(12).Children(func() { (*p).view(c) }) + }) +} diff --git a/hex_viewer.go b/hex_viewer.go new file mode 100644 index 0000000..7bce36c --- /dev/null +++ b/hex_viewer.go @@ -0,0 +1,157 @@ +package mujica + +import ( + "fmt" + "log/slog" + "runtime" + "strings" + + "github.com/egoist/mygo/ui" + + "mujicaui/internal" + "mujicaui/theme" +) + +// hexPerRow is how many bytes a row shows. +const hexPerRow = 16 + +// HexViewerState is the cursor and selection anchor. +type HexViewerState struct { + // Anchor is where a Shift selection started. + Cursor, Anchor int + rows ui.ListState +} + +// Selection returns the selected byte range, start inclusive, end exclusive. +func (s *HexViewerState) Selection() (start, end int) { + return min(s.Cursor, s.Anchor), max(s.Cursor, s.Anchor) + 1 +} + +// HexViewerOptions configure a HexViewer. +type HexViewerOptions struct { + Disabled bool +} + +// HexViewer shows bytes as synced hex and ASCII columns. +func HexViewer(c *ui.Context, data []byte, s *HexViewerState, opts HexViewerOptions) ChoiceResult { + if s == nil { + panic("mujica: HexViewer needs a state") + } + n := len(data) + if n > 0 && (s.Cursor < 0 || s.Cursor >= n || s.Anchor < 0 || s.Anchor >= n) { + panic("mujica: HexViewer cursor outside the data") + } + k := Tokens(c) + d := Density(c) + cw := codeTextWidth(c, strings.Repeat("0", hexPerRow*3)) / (hexPerRow * 3) + var res ChoiceResult + frame := codePanel(c).Focusable().Disabled(opts.Disabled).Label(msg(c, "hex_viewer.label")) + res.Element = frame + internal.Ring(frame, k.Focus, theme.ControlRadius) + move := func(to int, extend bool) { + to = max(0, min(to, n-1)) + if to == s.Cursor && (extend || s.Anchor == to) { + return + } + s.Cursor = to + if !extend { + s.Anchor = to + } + s.rows.ScrollIntoView(to / hexPerRow) + res.changed = true + slog.Debug("mujica: hex_viewer select", "cursor", s.Cursor, "anchor", s.Anchor) + } + if n > 0 && !frame.IsDisabled() { + hexKeys(c, frame, data, s, move) + } + a, b := s.Selection() + frame.Description(fmt.Sprintf(msg(c, "hex_viewer.at"), s.Cursor, b-a)) + frame.Children(func() { + if n == 0 { + codeCaption(c, msg(c, "hex_viewer.empty")).Padding(d.Unit() * 4) + return + } + rows := (n + hexPerRow - 1) / hexPerRow + ui.List(c, &s.rows, rows, func(r int) { + start := r * hexPerRow + end := min(start+hexPerRow, n) + ui.Row(c).MinHeight(d.ControlHeight()).Padding(0, d.Unit()*3).Gap(cw * 2).AlignItems(ui.Center).Children(func() { + codeMono(c, fmt.Sprintf("%08x", start)).TextColor(k.TextMuted).Shrink(0) + hexColumn(c, s, data, start, end, cw, true, move) + hexColumn(c, s, data, start, end, cw, false, move) + }) + }).Grow(1).MinHeight(d.ControlHeight() * 3) + }) + return res +} + +// hexColumn draws a row's hex or ASCII with marks. +func hexColumn(c *ui.Context, s *HexViewerState, data []byte, start, end int, cw float32, hex bool, move func(int, bool)) { + k := Tokens(c) + var b strings.Builder + step := cw + if hex { + step = 3 * cw + } + for i := start; i < end; i++ { + if hex { + fmt.Fprintf(&b, "%02x ", data[i]) + } else if data[i] >= 0x20 && data[i] < 0x7f { + b.WriteByte(data[i]) + } else { + b.WriteByte('.') + } + } + box := ui.Box(c).Shrink(0).Width(step * hexPerRow).Cursor(ui.CursorText) + lo, hi := s.Selection() + // A hex mark covers the two digits. + mark := step + if hex { + mark = 2 * cw + } + box.Draw(func(p *ui.Painter, r ui.Rect) { + for i := max(lo, start); i < min(hi, end); i++ { + x := r.X + float32(i-start)*step + p.Fill(ui.Rect{X: x - 1, Y: r.Y, W: mark + 2, H: r.H}, k.Selection, 2) + } + if s.Cursor >= start && s.Cursor < end { + x := r.X + float32(s.Cursor-start)*step + p.Stroke(ui.Rect{X: x - 1, Y: r.Y, W: mark + 2, H: r.H}, k.AccentText, 2, 1.5) + } + }) + if box.Clicked() && !box.IsDisabled() { + x, _, _ := box.PointerPosition() + move(start+min(int(x/step), end-start-1), box.ClickModifiers()&ui.Shift != 0) + } + box.Children(func() { codeMono(c, strings.TrimRight(b.String(), " ")) }) +} + +// hexKeys moves, extends and copies the selection. +func hexKeys(c *ui.Context, frame *ui.Element, data []byte, s *HexViewerState, move func(int, bool)) { + copyMods := ui.Cmd + if runtime.GOOS != "darwin" { + copyMods = ui.Ctrl + } + for _, mods := range []ui.Modifiers{0, ui.Shift} { + ext := mods == ui.Shift + switch { + case frame.Shortcut(mods, ui.KeyRight): + move(s.Cursor+1, ext) + case frame.Shortcut(mods, ui.KeyLeft): + move(s.Cursor-1, ext) + case frame.Shortcut(mods, ui.KeyDown): + move(s.Cursor+hexPerRow, ext) + case frame.Shortcut(mods, ui.KeyUp): + move(s.Cursor-hexPerRow, ext) + case frame.Shortcut(mods, ui.KeyHome): + move(s.Cursor/hexPerRow*hexPerRow, ext) + case frame.Shortcut(mods, ui.KeyEnd): + move(s.Cursor/hexPerRow*hexPerRow+hexPerRow-1, ext) + } + } + if frame.Shortcut(copyMods, ui.KeyC) { + a, b := s.Selection() + c.WriteClipboard(fmt.Sprintf("% x", data[a:b])) + slog.Debug("mujica: hex_viewer copy", "bytes", b-a) + } +} diff --git a/hex_viewer_test.go b/hex_viewer_test.go new file mode 100644 index 0000000..609c656 --- /dev/null +++ b/hex_viewer_test.go @@ -0,0 +1,115 @@ +package mujica + +import ( + "strings" + "testing" + + "github.com/egoist/mygo/ui" +) + +type hexProbe struct { + data []byte + s HexViewerState + disabled bool + changed int +} + +func (p *hexProbe) view(c *ui.Context) { + Use(c, Settings{}) + r := HexViewer(c, p.data, &p.s, HexViewerOptions{Disabled: p.disabled}) + r.Element.Height(300) + if r.Changed() { + p.changed++ + } +} + +func TestHexViewerColumns(t *testing.T) { + p := &hexProbe{data: []byte("Court seal\x00\x01 ledger of the realm")} + tt := ui.NewTester(p.view, 760, 320) + for _, s := range []string{"00000000", "00000010", "43 6f 75 72 74 20 73 65 61 6c 00 01 20 6c 65 64", "Court seal.. led"} { + if !tt.HasText(s) { + t.Fatalf("missing %q in %q", s, tt.Texts()) + } + } +} + +func TestHexViewerKeysSelectAndCopy(t *testing.T) { + p := &hexProbe{data: []byte("0123456789abcdefGHIJ")} + tt := ui.NewTester(p.view, 760, 320) + tt.Key(0, ui.KeyTab) + tt.Key(0, ui.KeyRight) + tt.Key(ui.Shift, ui.KeyRight) + tt.Key(ui.Shift, ui.KeyRight) + if a, b := p.s.Selection(); a != 1 || b != 4 { + t.Fatalf("selection %d..%d", a, b) + } + tt.Key(ui.Cmd, ui.KeyC) + if tt.Clipboard() != "31 32 33" { + t.Fatalf("copied %q", tt.Clipboard()) + } + tt.Key(0, ui.KeyDown) + if p.s.Cursor != 19 || p.s.Anchor != 19 { + t.Fatalf("Down clamped to %d", p.s.Cursor) + } + tt.Key(0, ui.KeyHome) + if p.s.Cursor != 16 { + t.Fatalf("Home gave %d", p.s.Cursor) + } +} + +func TestHexViewerClickSyncsColumns(t *testing.T) { + p := &hexProbe{data: []byte("0123456789abcdef")} + tt := ui.NewTester(p.view, 760, 320) + r, _ := tt.Find("0123456789abcdef") + cw := r.W / 16 + tt.ClickAt(r.X+cw*5.5, r.Y+r.H/2) + if p.s.Cursor != 5 || p.changed != 1 { + t.Fatalf("ASCII click chose %d", p.s.Cursor) + } + h, _ := tt.Find("30 31 32 33 34 35 36 37 38 39 61 62 63 64 65 66") + tt.ClickAt(h.X+cw*3*9+cw, h.Y+h.H/2) + if p.s.Cursor != 9 { + t.Fatalf("hex click chose %d", p.s.Cursor) + } +} + +func TestHexViewerLargeIsVirtual(t *testing.T) { + p := &hexProbe{data: make([]byte, 1_600_000)} + tt := ui.NewTester(p.view, 760, 320) + rows := 0 + for _, s := range tt.Texts() { + if len(s) == 8 && strings.Trim(s, "0123456789abcdef") == "" { + rows++ + } + } + if rows == 0 || rows > 40 { + t.Fatalf("built %d of 100000 rows", rows) + } +} + +func TestHexViewerDisabledAndBadCursor(t *testing.T) { + p := &hexProbe{data: []byte("abc"), disabled: true} + tt := ui.NewTester(p.view, 760, 320) + tt.Key(0, ui.KeyTab) + tt.Key(0, ui.KeyRight) + if p.s.Cursor != 0 { + t.Fatal("disabled viewer moved") + } + defer func() { + if recover() == nil { + t.Fatal("cursor outside data did not panic") + } + }() + ui.NewTester((&hexProbe{data: []byte("a"), s: HexViewerState{Cursor: 4}}).view, 760, 320) +} + +func TestHexViewerSnapshot(t *testing.T) { + snapshot(t, "hex_viewer", 720, 170, func(c *ui.Context) { + p := ui.Local(c.Root(), "snap", func() *hexProbe { + return &hexProbe{data: []byte("MUJICA\x00\x01court ledger\nseal: 42\xff\xfe"), s: HexViewerState{Cursor: 9, Anchor: 6}} + }) + ui.Column(c).Padding(12).Children(func() { + HexViewer(c, (*p).data, &(*p).s, HexViewerOptions{}).Element.Height(146) + }) + }) +} diff --git a/log_viewer.go b/log_viewer.go new file mode 100644 index 0000000..33099db --- /dev/null +++ b/log_viewer.go @@ -0,0 +1,130 @@ +package mujica + +import ( + "fmt" + "log/slog" + "time" + + "github.com/egoist/mygo/ui" + + "mujicaui/theme" +) + +// LogLevel is a log line's severity. +type LogLevel int + +const ( + LogDebug LogLevel = iota + LogInfo + LogWarn + LogError +) + +// LogLine is one entry of a LogViewer. +type LogLine struct { + Time time.Time + Level LogLevel + Text string +} + +// LogViewerState is a LogViewer's filters, highlight and place. +type LogViewerState struct { + // Hidden levels are filtered out. + Hidden [4]bool + // Query is highlighted in the shown lines. + Query string + // Follow keeps the newest line in view; it starts on. + Follow bool + rows ui.ListState + shown []int + scanned int + hidden [4]bool + started bool +} + +// LogViewerOptions configure a LogViewer. +type LogViewerOptions struct { + Disabled bool +} + +// LogViewer shows filtered, highlighted log lines, virtualized. +func LogViewer(c *ui.Context, lines []LogLine, s *LogViewerState, opts LogViewerOptions) ChoiceResult { + if s == nil { + panic("mujica: LogViewer needs a state") + } + if !s.started { + s.started, s.Follow = true, true + } + var res ChoiceResult + res.Element = codePanel(c).Disabled(opts.Disabled) + res.Element.Children(func() { + codeHeader(c, func() { + for lv := LogDebug; lv <= LogError; lv++ { + on := !s.Hidden[lv] + if Toggle(c, &on, logViewerName(c, lv), ToggleOptions{Disabled: opts.Disabled}).Changed() { + s.Hidden[lv] = !on + res.changed = true + slog.Debug("mujica: log_viewer level", "level", lv, "shown", on) + } + } + SearchInput(c, &s.Query, SearchInputOptions{Disabled: opts.Disabled, Placeholder: msg(c, "log_viewer.highlight")}).Element.Grow(1).MinWidth(0) + was := s.Follow + if Toggle(c, &s.Follow, msg(c, "log_viewer.follow"), ToggleOptions{Disabled: opts.Disabled}).Changed() { + res.changed = true + slog.Debug("mujica: log_viewer follow", "on", s.Follow) + if s.Follow && !was { + s.rows.ScrollToEnd() + } + } + }) + logViewerFilter(lines, s) + if len(s.shown) == 0 { + codeCaption(c, msg(c, "log_viewer.empty")).Padding(Density(c).Unit() * 4) + return + } + s.rows.FollowEnd = s.Follow + s.rows.Key = func(i int) any { return s.shown[i] } + s.rows.Label = func(i int) string { return lines[s.shown[i]].Text } + d := Density(c) + ui.List(c, &s.rows, len(s.shown), func(i int) { + logViewerRow(c, lines[s.shown[i]], s.Query) + }).Grow(1).MinHeight(d.ControlHeight()*3).Padding(d.Unit(), 0) + }) + return res +} + +// logViewerFilter extends or redoes the shown lines. +func logViewerFilter(lines []LogLine, s *LogViewerState) { + if s.hidden != s.Hidden || s.scanned > len(lines) { + s.hidden, s.scanned, s.shown = s.Hidden, 0, s.shown[:0] + } + for i := s.scanned; i < len(lines); i++ { + lv := lines[i].Level + if lv < LogDebug || lv > LogError { + panic("mujica: LogLine level out of range") + } + if !s.Hidden[lv] { + s.shown = append(s.shown, i) + } + } + s.scanned = len(lines) +} + +func logViewerName(c *ui.Context, lv LogLevel) string { + return msg(c, [...]string{"log_viewer.debug", "log_viewer.info", "log_viewer.warn", "log_viewer.error"}[lv]) +} + +func logViewerRow(c *ui.Context, l LogLine, query string) { + k := Tokens(c) + d := Density(c) + col := [...]ui.Color{k.TextMuted, k.Info, k.Warning, k.Danger}[l.Level] + ui.Row(c).MinHeight(d.ControlHeight()).Padding(0, d.Unit()*3).Gap(d.Unit() * 3).AlignItems(ui.Center).Children(func() { + if !l.Time.IsZero() { + codeMono(c, l.Time.Format("15:04:05.000")).TextColor(k.TextMuted).Shrink(0) + } + ui.Text(c, fmt.Sprintf("%-5s", logViewerName(c, l.Level))).Font(theme.MonoFont).FontSize(FontSize(c, codeSize)). + FontWeight(700).TextColor(col).SingleLine().Shrink(0) + ui.RichText(c, codeMatchSpans(c, l.Text, query)...).Font(theme.MonoFont).FontSize(FontSize(c, codeSize)). + SingleLine().Grow(1).MinWidth(0) + }) +} diff --git a/log_viewer_test.go b/log_viewer_test.go new file mode 100644 index 0000000..7d8c01c --- /dev/null +++ b/log_viewer_test.go @@ -0,0 +1,129 @@ +package mujica + +import ( + "fmt" + "strings" + "testing" + "time" + + "github.com/egoist/mygo/ui" +) + +func logViewerLines(n int) []LogLine { + at := time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC) + lines := make([]LogLine, n) + for i := range lines { + lines[i] = LogLine{Time: at.Add(time.Duration(i) * time.Millisecond), Level: LogLevel(i % 4), Text: fmt.Sprintf("event %d", i)} + } + return lines +} + +func logViewerTester(lines *[]LogLine, s *LogViewerState, disabled bool) *ui.Tester { + return ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + LogViewer(c, *lines, s, LogViewerOptions{Disabled: disabled}).Element.Height(400) + }, 760, 420) +} + +func logViewerRows(tt *ui.Tester) int { + n := 0 + for _, s := range tt.Texts() { + if strings.HasPrefix(s, "event ") { + n++ + } + } + return n +} + +func TestLogViewerHundredThousandLines(t *testing.T) { + lines := logViewerLines(100_000) + s := &LogViewerState{} + tt := logViewerTester(&lines, s, false) + if !tt.HasText("event 99999") { + t.Fatal("follow did not start at the tail") + } + if n := logViewerRows(tt); n == 0 || n > 40 { + t.Fatalf("built %d rows of 100000", n) + } + // Hiding a level refilters every line once. + if err := tt.Click("ERROR"); err != nil { + t.Fatal(err) + } + if len(s.shown) != 75_000 || tt.HasText("event 99999") { + t.Fatalf("shown %d after hiding ERROR", len(s.shown)) + } + if n := logViewerRows(tt); n > 40 { + t.Fatalf("built %d rows after filtering", n) + } +} + +func TestLogViewerFollowsAppends(t *testing.T) { + lines := logViewerLines(50) + s := &LogViewerState{} + tt := logViewerTester(&lines, s, false) + lines = append(lines, LogLine{Level: LogInfo, Text: "event fresh"}) + tt.Frame() + if !tt.HasText("event fresh") || len(s.shown) != 51 { + t.Fatalf("appended line not followed; shown %d", len(s.shown)) + } + if err := tt.Click("Follow"); err != nil { + t.Fatal(err) + } + if s.Follow { + t.Fatal("Follow toggle did not turn off") + } + tt.Scroll(300, 200, 0, -2000) + lines = append(lines, LogLine{Level: LogInfo, Text: "event later"}) + tt.Frame() + if tt.HasText("event later") { + t.Fatal("paused viewer jumped to the new line") + } +} + +func TestLogViewerHighlight(t *testing.T) { + lines := []LogLine{{Level: LogWarn, Text: "disk nearly full"}} + s := &LogViewerState{Query: "FULL"} + tt := logViewerTester(&lines, s, false) + if !tt.HasText("disk nearly full") || !tt.HasText("WARN") { + t.Fatalf("texts %q", tt.Texts()) + } + var spans []ui.Span + ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + spans = codeMatchSpans(c, "disk nearly full", "FULL") + }, 10, 10) + if len(spans) != 2 || spans[1].Text != "full" || !spans[1].Underline || spans[1].Background.A == 0 { + t.Fatalf("highlight spans %+v", spans) + } +} + +func TestLogViewerDisabledAndEmpty(t *testing.T) { + lines := logViewerLines(8) + s := &LogViewerState{} + tt := logViewerTester(&lines, s, true) + _ = tt.Click("ERROR") + if s.Hidden[LogError] { + t.Fatal("disabled viewer changed its filter") + } + var none []LogLine + tt = logViewerTester(&none, &LogViewerState{}, false) + if !tt.HasText("No log lines") { + t.Fatalf("texts %q", tt.Texts()) + } +} + +func TestLogViewerSnapshot(t *testing.T) { + snapshot(t, "log_viewer", 640, 230, func(c *ui.Context) { + s := ui.Local(c.Root(), "snap", func() *LogViewerState { return &LogViewerState{Query: "seal"} }) + at := time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC) + lines := []LogLine{ + {at, LogDebug, "loading ledger index"}, + {at.Add(12 * time.Millisecond), LogInfo, "court opened by the chancellor"}, + {at.Add(40 * time.Millisecond), LogWarn, "seal of decree 41 is faded"}, + {at.Add(91 * time.Millisecond), LogError, "seal of decree 42 missing"}, + } + ui.Column(c).Padding(12).Children(func() { + LogViewer(c, lines, *s, LogViewerOptions{}).Element.Height(206) + }) + }) +} diff --git a/messages_code.go b/messages_code.go new file mode 100644 index 0000000..3f210e4 --- /dev/null +++ b/messages_code.go @@ -0,0 +1,101 @@ +package mujica + +func init() { + addMessages(map[string][2]string{ + "terminal.label": {"Terminal", "终端"}, + "terminal_search.label": {"Find in terminal", "在终端中查找"}, + "terminal_tabs.label": {"Terminals", "终端"}, + "terminal_tabs.empty": {"No terminals open", "没有打开的终端"}, + "terminal_tabs.confirm": {"Close “%s”?", "关闭“%s”?"}, + "terminal_tabs.busy": {"A process is still running in this terminal and will be ended.", "此终端中仍有进程在运行,关闭后将结束它。"}, + "terminal_tabs.close": {"Close", "关闭"}, + "terminal_tabs.running": {"Process running", "进程运行中"}, + "terminal_tabs.rename": {"Terminal name", "终端名称"}, + "terminal_tabs.closeTab": {"Close %s", "关闭 %s"}, + "command_history.search": {"Search history", "搜索历史"}, + "command_history.empty": {"No commands yet", "暂无命令"}, + "command_history.none": {"No matching commands", "没有匹配的命令"}, + "command_history.replay": {"Run %s again", "重新运行 %s"}, + "command_history.exit": {"exit %d", "退出码 %d"}, + "log_viewer.highlight": {"Highlight", "高亮关键字"}, + "log_viewer.follow": {"Follow", "跟随"}, + "log_viewer.empty": {"No log lines", "没有日志"}, + "log_viewer.debug": {"DEBUG", "调试"}, + "log_viewer.info": {"INFO", "信息"}, + "log_viewer.warn": {"WARN", "警告"}, + "log_viewer.error": {"ERROR", "错误"}, + "process_list.empty": {"No processes", "没有进程"}, + "process_list.name": {"Name", "名称"}, + "process_list.pid": {"PID", "PID"}, + "process_list.cpu": {"CPU", "CPU"}, + "process_list.memory": {"Memory", "内存"}, + "process_list.actions": {"End", "结束"}, + "process_list.end": {"End %s", "结束 %s"}, + "process_list.confirm": {"End “%s” (PID %d)?", "结束“%s”(PID %d)?"}, + "process_list.warning": {"Unsaved work in this process will be lost.", "该进程中未保存的内容将会丢失。"}, + "process_list.endButton": {"End process", "结束进程"}, + "code_viewer.empty": {"No code", "没有代码"}, + "code_viewer.fold": {"Fold line %d", "折叠第 %d 行"}, + "code_viewer.unfold": {"Unfold line %d", "展开第 %d 行"}, + "minimap.label": {"Minimap", "代码缩略图"}, + "minimap.range": {"Lines %d to %d of %d", "第 %d 至 %d 行,共 %d 行"}, + "find_widget.toggleReplace": {"Toggle replace", "切换替换"}, + "find_widget.case": {"Match case", "区分大小写"}, + "find_widget.word": {"Whole word", "全字匹配"}, + "find_widget.regex": {"Regular expression", "正则表达式"}, + "find_widget.invalid": {"Invalid pattern: %s", "表达式无效:%s"}, + "find_widget.replace": {"Replace with", "替换为"}, + "find_widget.replaceOne": {"Replace", "替换"}, + "find_widget.replaceAll": {"Replace all", "全部替换"}, + "search_panel.include": {"Files to include", "包含的文件"}, + "search_panel.exclude": {"Files to exclude", "排除的文件"}, + "search_panel.idle": {"Type to search in files", "输入内容以在文件中搜索"}, + "search_panel.none": {"No results", "没有结果"}, + "search_panel.summary": {"%d results in %d files", "%d 个结果,位于 %d 个文件"}, + "search_panel.fold": {"Fold %s", "收起 %s"}, + "search_panel.unfold": {"Unfold %s", "展开 %s"}, + "problems_panel.errors": {"Errors %d", "错误 %d"}, + "problems_panel.warnings": {"Warnings %d", "警告 %d"}, + "problems_panel.infos": {"Info %d", "信息 %d"}, + "problems_panel.error": {"Error", "错误"}, + "problems_panel.warning": {"Warning", "警告"}, + "problems_panel.info": {"Info", "信息"}, + "problems_panel.empty": {"No problems", "没有问题"}, + "problems_panel.at": {"Ln %d, Col %d", "第 %d 行,第 %d 列"}, + "output_panel.channel": {"Output channel", "输出通道"}, + "output_panel.clear": {"Clear output", "清空输出"}, + "output_panel.empty": {"No output", "没有输出"}, + "symbol_outline.filter": {"Filter symbols", "筛选符号"}, + "symbol_outline.empty": {"No symbols", "没有符号"}, + "completion_menu.label": {"Suggestions", "补全建议"}, + "debug_toolbar.label": {"Debug", "调试"}, + "debug_toolbar.start": {"Start", "开始"}, + "debug_toolbar.continue": {"Continue", "继续"}, + "debug_toolbar.pause": {"Pause", "暂停"}, + "debug_toolbar.stepOver": {"Step over", "单步跳过"}, + "debug_toolbar.stepInto": {"Step into", "单步进入"}, + "debug_toolbar.stepOut": {"Step out", "单步跳出"}, + "debug_toolbar.restart": {"Restart", "重启"}, + "debug_toolbar.stop": {"Stop", "停止"}, + "breakpoint_list.label": {"Breakpoints", "断点"}, + "breakpoint_list.empty": {"No breakpoints", "没有断点"}, + "breakpoint_list.enable": {"Enable %s:%d", "启用 %s:%d"}, + "breakpoint_list.edit": {"Edit condition of %s:%d", "编辑 %s:%d 的条件"}, + "breakpoint_list.condition": {"Condition", "条件"}, + "breakpoint_list.placeholder": {"Break when expression is true", "表达式为真时中断"}, + "breakpoint_list.when": {"when %s", "当 %s"}, + "call_stack.empty": {"Not paused", "未暂停"}, + "call_stack.frames": {"depth %d", "深度 %d"}, + "variables_panel.empty": {"No variables", "没有变量"}, + "variables_panel.value": {"Value", "值"}, + "variables_panel.changed": {"Changed", "已变化"}, + "hex_viewer.label": {"Hex view", "十六进制视图"}, + "hex_viewer.empty": {"No bytes", "没有数据"}, + "hex_viewer.at": {"Offset %d, %d bytes selected", "偏移 %d,已选 %d 字节"}, + "flamegraph.search": {"Highlight frames", "高亮帧"}, + "flamegraph.empty": {"No profile", "没有性能数据"}, + "flamegraph.label": {"Flame graph", "火焰图"}, + "flamegraph.hint": {"Point at a frame, or focus the graph and use the arrows", "指向一帧,或聚焦图表后用方向键"}, + "flamegraph.readout": {"%s: %.1f %s, %.1f%% of total", "%s:%.1f %s,占总计 %.1f%%"}, + }) +} diff --git a/minimap.go b/minimap.go new file mode 100644 index 0000000..3ba6490 --- /dev/null +++ b/minimap.go @@ -0,0 +1,134 @@ +package mujica + +import ( + "fmt" + "log/slog" + "strings" + + "github.com/egoist/mygo/ui" + + "mujicaui/internal" + "mujicaui/theme" +) + +// MinimapView is the editor's first line and line count. +type MinimapView struct{ Top, Visible int } + +// MinimapOptions configure a Minimap. +type MinimapOptions struct { + // Language colors the strokes as CodeViewer does. + Language string + // Width is the map's width in DIPs; zero is 96. + Width float32 + Disabled bool +} + +// Line height and character width on the map. +const ( + minimapLine float32 = 2 + minimapChar float32 = 1 +) + +// Minimap draws code small and moves the visible frame. +func Minimap(c *ui.Context, lines []string, view *MinimapView, opts MinimapOptions) ChoiceResult { + if view == nil { + panic("mujica: Minimap needs a view") + } + if opts.Width < 0 || view.Visible < 0 || view.Top < 0 { + panic("mujica: Minimap width, Top and Visible must not be negative") + } + k := Tokens(c) + w := opts.Width + if w == 0 { + w = 96 + } + n := len(lines) + var res ChoiceResult + move := func(top int) { + top = max(0, min(top, n-view.Visible)) + if top != view.Top { + view.Top = top + res.changed = true + slog.Debug("mujica: minimap move", "top", top) + } + } + e := ui.Box(c).Width(w).Shrink(0).MinHeight(theme.MinHitSize*2).Focusable().Clip().Cursor(ui.CursorPointer). + Background(k.Surface).Border(theme.BorderWidth, k.Border).Radius(theme.ControlRadius).Disabled(opts.Disabled). + Label(msg(c, "minimap.label")).Description(fmt.Sprintf(msg(c, "minimap.range"), min(view.Top+1, n), min(view.Top+view.Visible, n), n)) + res.Element = e + h := e.Bounds().H + offset := minimapOffset(n, *view, h) + if !e.IsDisabled() { + if _, _, held := e.Dragged(); held { + _, y, _ := e.PointerPosition() + move(int((y+offset)/minimapLine) - view.Visible/2) + } + switch { + case e.Shortcut(0, ui.KeyDown): + move(view.Top + 1) + case e.Shortcut(0, ui.KeyUp): + move(view.Top - 1) + case e.Shortcut(0, ui.KeyPageDown): + move(view.Top + max(view.Visible, 1)) + case e.Shortcut(0, ui.KeyPageUp): + move(view.Top - max(view.Visible, 1)) + case e.Shortcut(0, ui.KeyHome): + move(0) + case e.Shortcut(0, ui.KeyEnd): + move(n) + } + } + internal.Ring(e, k.Focus, theme.ControlRadius) + v := *view + e.Draw(func(p *ui.Painter, r ui.Rect) { + if r.H != h { + c.Invalidate() + } + p.Clip(r, theme.ControlRadius, func() { + off := minimapOffset(n, v, r.H) + first := int(off / minimapLine) + for i := first; i < n && float32(i-first)*minimapLine < r.H+minimapLine; i++ { + y := internal.Snap(p, r.Y+float32(i)*minimapLine-off) + minimapStrokes(p, k, opts.Language, lines[i], r.X+6, y, r.X+r.W-6) + } + fy := internal.Snap(p, r.Y+float32(v.Top)*minimapLine-off) + frame := ui.Rect{X: r.X + 1, Y: fy, W: r.W - 2, H: max(internal.Snap(p, float32(v.Visible)*minimapLine), 4)} + p.Fill(frame, k.Selection.Alpha(0.55), 0) + p.Stroke(frame, k.AccentText, 0, 1) + }) + }) + return res +} + +// minimapOffset scrolls a tall map with the view. +func minimapOffset(n int, v MinimapView, h float32) float32 { + total := float32(n) * minimapLine + if total <= h || n <= v.Visible { + return 0 + } + return float32(v.Top) / float32(n-v.Visible) * (total - h) +} + +// minimapStrokes draws a line's tokens as short bars, skipping blanks. +func minimapStrokes(p *ui.Painter, k theme.Tokens, lang, line string, x0, y, x1 float32) { + line = strings.ReplaceAll(line, "\t", " ") + toks, _ := internal.Highlight(lang, line, false) + col := 0 + for _, t := range toks { + runes := []rune(t.Text) + start := -1 + for i, r := range append(runes, ' ') { + if r != ' ' && start < 0 { + start = i + } + if r == ' ' && start >= 0 { + x := x0 + float32(col+start)*minimapChar + if x < x1 { + p.Fill(ui.Rect{X: x, Y: y, W: min(float32(i-start)*minimapChar, x1-x), H: minimapLine * 0.7}, codeTokenColor(k, t.Kind).Alpha(0.75), 0) + } + start = -1 + } + } + col += len(runes) + } +} diff --git a/minimap_test.go b/minimap_test.go new file mode 100644 index 0000000..c538fd0 --- /dev/null +++ b/minimap_test.go @@ -0,0 +1,116 @@ +package mujica + +import ( + "fmt" + "testing" + + "github.com/egoist/mygo/ui" +) + +func minimapLines(n int) []string { + lines := make([]string, n) + for i := range lines { + lines[i] = fmt.Sprintf("\tvalue%d := compute(%d) // note", i, i) + } + return lines +} + +func minimapTester(lines []string, v *MinimapView, disabled bool, moves *int) *ui.Tester { + return ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + r := Minimap(c, lines, v, MinimapOptions{Language: "go", Disabled: disabled}) + r.Element.Height(200) + if r.Changed() { + *moves++ + } + }, 200, 220) +} + +func TestMinimapClickAndDragJump(t *testing.T) { + v := &MinimapView{Visible: 20} + moves := 0 + tt := minimapTester(minimapLines(80), v, false, &moves) + // y=100 is line 50; centered, top is 40. + tt.Press(40, 100) + if v.Top != 40 { + t.Fatalf("press moved top to %d, want 40", v.Top) + } + tt.Move(40, 150) + tt.Release(40, 150) + if v.Top != 60 { + t.Fatalf("drag clamped top to %d, want 60 (80-20)", v.Top) + } + if moves < 2 { + t.Fatalf("Changed reported %d times", moves) + } +} + +func TestMinimapKeys(t *testing.T) { + v := &MinimapView{Visible: 20} + moves := 0 + tt := minimapTester(minimapLines(500), v, false, &moves) + tt.Key(0, ui.KeyTab) + if !tt.Focused("Minimap") { + t.Fatal("Tab did not focus the minimap") + } + tt.Key(0, ui.KeyPageDown) + tt.Key(0, ui.KeyDown) + if v.Top != 21 { + t.Fatalf("PageDown+Down gave %d", v.Top) + } + tt.Key(0, ui.KeyEnd) + if v.Top != 480 { + t.Fatalf("End gave %d", v.Top) + } + tt.Key(0, ui.KeyHome) + if v.Top != 0 { + t.Fatalf("Home gave %d", v.Top) + } +} + +func TestMinimapScrollsWhenTaller(t *testing.T) { + if off := minimapOffset(1000, MinimapView{Top: 980, Visible: 20}, 200); off != 1800 { + t.Fatalf("offset at the end %v, want 1800", off) + } + if off := minimapOffset(50, MinimapView{Top: 10, Visible: 20}, 200); off != 0 { + t.Fatalf("short map offset %v", off) + } +} + +func TestMinimapDisabled(t *testing.T) { + v := &MinimapView{Visible: 20} + moves := 0 + tt := minimapTester(minimapLines(80), v, true, &moves) + tt.Press(40, 100) + tt.Release(40, 100) + if v.Top != 0 || moves != 0 { + t.Fatalf("disabled minimap moved to %d", v.Top) + } +} + +func TestMinimapNegativePanics(t *testing.T) { + defer func() { + if recover() == nil { + t.Fatal("negative Visible did not panic") + } + }() + minimapTester(nil, &MinimapView{Visible: -1}, false, new(int)) +} + +func TestMinimapSnapshot(t *testing.T) { + snapshot(t, "minimap", 140, 220, func(c *ui.Context) { + v := ui.Local(c.Root(), "snap", func() *MinimapView { return &MinimapView{Top: 12, Visible: 24} }) + ui.Column(c).Padding(12).Children(func() { + Minimap(c, codeMinimapSnapshotLines(), *v, MinimapOptions{Language: "go"}).Element.Height(196) + }) + }) +} + +func codeMinimapSnapshotLines() []string { + var lines []string + for i := range 12 { + lines = append(lines, codeViewerSample...) + lines = append(lines, fmt.Sprintf("// section %d", i), "") + } + return lines +} diff --git a/output_panel.go b/output_panel.go new file mode 100644 index 0000000..e7c00df --- /dev/null +++ b/output_panel.go @@ -0,0 +1,93 @@ +package mujica + +import ( + "log/slog" + + "github.com/egoist/mygo/ui" + + "mujicaui/icons" + "mujicaui/theme" +) + +// OutputChannel is a named stream of ANSI lines. +type OutputChannel struct { + Name string + Lines []string +} + +// OutputPanelState is the channel, following and place. +type OutputPanelState struct { + Channel int + // Follow keeps the newest line in view; it starts on. + Follow bool + rows ui.ListState + shown int + started bool +} + +// OutputPanelOptions configure an OutputPanel. +type OutputPanelOptions struct { + Disabled bool +} + +// OutputPanelResult is an OutputPanel and what happened in this frame. +type OutputPanelResult struct { + Element *ui.Element + // Cleared names a channel to clear. + Changed bool + Cleared string +} + +// OutputPanel shows one channel, following its tail. +func OutputPanel(c *ui.Context, channels []OutputChannel, s *OutputPanelState, opts OutputPanelOptions) OutputPanelResult { + if s == nil || len(channels) == 0 { + panic("mujica: OutputPanel needs a state and a channel") + } + if s.Channel < 0 || s.Channel >= len(channels) { + panic("mujica: OutputPanel Channel out of range") + } + if !s.started { + s.started, s.Follow, s.shown = true, true, s.Channel + } + var res OutputPanelResult + res.Element = codePanel(c).Disabled(opts.Disabled) + disabled := res.Element.IsDisabled() + res.Element.Children(func() { + codeHeader(c, func() { + options := make([]SelectOption[int], len(channels)) + for i, ch := range channels { + options[i] = SelectOption[int]{Value: i, Label: ch.Name} + } + if Select(c, &s.Channel, options, SelectOptions{Label: msg(c, "output_panel.channel"), Disabled: opts.Disabled}).Changed() { + res.Changed = true + slog.Debug("mujica: output_panel channel", "channel", s.Channel) + } + ui.Spacer(c) + was := s.Follow + if Toggle(c, &s.Follow, msg(c, "log_viewer.follow"), ToggleOptions{Disabled: opts.Disabled}).Changed() && s.Follow && !was { + s.rows.ScrollToEnd() + } + if iconAction(c, icons.Must("trash-2"), msg(c, "output_panel.clear")).Disabled(opts.Disabled).Clicked() && !disabled { + res.Cleared = channels[s.Channel].Name + slog.Debug("mujica: output_panel clear", "channel", s.Channel) + } + }) + if s.shown != s.Channel { + s.shown, s.rows = s.Channel, ui.ListState{} + } + lines := channels[s.Channel].Lines + if len(lines) == 0 { + codeCaption(c, msg(c, "output_panel.empty")).Padding(Density(c).Unit() * 4) + return + } + d := Density(c) + s.rows.FollowEnd = s.Follow + s.rows.Label = func(i int) string { return lines[i] } + ui.List(c, &s.rows, len(lines), func(i int) { + ui.Row(c).MinHeight(d.ControlHeight()).Padding(0, d.Unit()*3).AlignItems(ui.Center).Children(func() { + ui.RichText(c, codeAnsiSpans(c, lines[i])...).Font(theme.MonoFont).FontSize(FontSize(c, codeSize)).Grow(1).MinWidth(0) + }) + }).Grow(1).MinHeight(d.ControlHeight()*3).Padding(d.Unit(), 0) + }) + return res +} diff --git a/output_panel_test.go b/output_panel_test.go new file mode 100644 index 0000000..e6cad68 --- /dev/null +++ b/output_panel_test.go @@ -0,0 +1,118 @@ +package mujica + +import ( + "fmt" + "testing" + + "github.com/egoist/mygo/ui" +) + +type outputProbe struct { + channels []OutputChannel + s OutputPanelState + disabled bool + changed int + cleared []string +} + +func (p *outputProbe) view(c *ui.Context) { + Use(c, Settings{}) + r := OutputPanel(c, p.channels, &p.s, OutputPanelOptions{Disabled: p.disabled}) + r.Element.Height(300) + if r.Changed { + p.changed++ + } + if r.Cleared != "" { + p.cleared = append(p.cleared, r.Cleared) + for i := range p.channels { + if p.channels[i].Name == r.Cleared { + p.channels[i].Lines = nil + } + } + } +} + +func newOutputProbe() *outputProbe { + var build []string + for i := range 200 { + build = append(build, fmt.Sprintf("compile unit %d", i)) + } + return &outputProbe{channels: []OutputChannel{{"Build", build}, {"Tests", []string{"\x1b[32mPASS\x1b[0m court", "\x1b[31mFAIL\x1b[0m ledger"}}}} +} + +func TestOutputPanelFollowsTail(t *testing.T) { + p := newOutputProbe() + tt := ui.NewTester(p.view, 520, 320) + if !tt.HasText("compile unit 199") || tt.HasText("compile unit 0") { + t.Fatal("panel did not start at the tail") + } + p.channels[0].Lines = append(p.channels[0].Lines, "linked") + tt.Frame() + if !tt.HasText("linked") { + t.Fatal("new output not followed") + } +} + +func TestOutputPanelSwitchAndClear(t *testing.T) { + p := newOutputProbe() + tt := ui.NewTester(p.view, 520, 320) + if err := tt.Click("Build"); err != nil { + t.Fatal(err) + } + if err := tt.Click("Tests"); err != nil { + t.Fatal(err) + } + if p.s.Channel != 1 || p.changed != 1 || !tt.HasText("PASS court") { + t.Fatalf("channel %d changed %d texts %q", p.s.Channel, p.changed, tt.Texts()) + } + if err := tt.Click("Clear output"); err != nil { + t.Fatal(err) + } + if len(p.cleared) != 1 || p.cleared[0] != "Tests" || !tt.HasText("No output") { + t.Fatalf("cleared %v", p.cleared) + } +} + +func TestOutputPanelAnsiColors(t *testing.T) { + var spans []ui.Span + var ok ui.Color + ui.NewTester(func(c *ui.Context) { + Use(c, Settings{Mode: Light}) + ok = Tokens(c).Success + spans = codeAnsiSpans(c, "\x1b[1;32mPASS\x1b[0m court") + }, 10, 10) + if len(spans) != 2 || spans[0].Color != ok || spans[0].Weight != 700 || spans[1].Color.A != 0 { + t.Fatalf("spans %+v", spans) + } +} + +func TestOutputPanelDisabledAndBadChannel(t *testing.T) { + p := newOutputProbe() + p.disabled = true + tt := ui.NewTester(p.view, 520, 320) + _ = tt.Click("Clear output") + if len(p.cleared) != 0 { + t.Fatal("disabled panel cleared") + } + defer func() { + if recover() == nil { + t.Fatal("channel out of range did not panic") + } + }() + p = newOutputProbe() + p.s.Channel = 5 + ui.NewTester(p.view, 520, 320) +} + +func TestOutputPanelSnapshot(t *testing.T) { + snapshot(t, "output_panel", 520, 200, func(c *ui.Context) { + p := ui.Local(c.Root(), "snap", func() *outputProbe { + p := newOutputProbe() + p.s.Channel = 1 + return p + }) + ui.Column(c).Padding(12).Children(func() { + OutputPanel(c, (*p).channels, &(*p).s, OutputPanelOptions{}).Element.Height(176) + }) + }) +} diff --git a/problems_panel.go b/problems_panel.go new file mode 100644 index 0000000..5602f75 --- /dev/null +++ b/problems_panel.go @@ -0,0 +1,194 @@ +package mujica + +import ( + "cmp" + "fmt" + "log/slog" + "slices" + + "github.com/egoist/mygo/ui" + + "mujicaui/icons" +) + +// ProblemSeverity ranks a Problem. +type ProblemSeverity int + +const ( + ProblemError ProblemSeverity = iota + ProblemWarning + ProblemInfo +) + +// Problem is one diagnostic. +type Problem struct { + Severity ProblemSeverity + File string + Line, Col int + Message, Source string +} + +// ProblemsPanelState is a ProblemsPanel's filters, folded files and place. +type ProblemsPanelState struct { + // Hidden severities are filtered out. + Hidden [3]bool + // Collapsed holds folded file paths. + Collapsed ui.Selection[string] + rows ui.ListState + selected int + flat []problemRow + data *Problem + n, folded int + hidden [3]bool + started bool +} + +// problemRow is a file header or a problem. +type problemRow struct { + file string + problem int +} + +// ProblemsPanelOptions configure a ProblemsPanel. +type ProblemsPanelOptions struct { + Disabled bool +} + +// ProblemsPanelResult carries the problem opened. +type ProblemsPanelResult struct { + Element *ui.Element + Opened *Problem +} + +// ProblemsPanel lists diagnostics grouped by file. +func ProblemsPanel(c *ui.Context, problems []Problem, s *ProblemsPanelState, opts ProblemsPanelOptions) ProblemsPanelResult { + if s == nil { + panic("mujica: ProblemsPanel needs a state") + } + if !s.started { + s.started, s.selected = true, -1 + } + var counts [3]int + for _, p := range problems { + if p.Severity < ProblemError || p.Severity > ProblemInfo { + panic("mujica: Problem severity out of range") + } + counts[p.Severity]++ + } + var res ProblemsPanelResult + res.Element = codePanel(c).Disabled(opts.Disabled) + disabled := res.Element.IsDisabled() + res.Element.Children(func() { + codeHeader(c, func() { + for sv := ProblemError; sv <= ProblemInfo; sv++ { + on := !s.Hidden[sv] + name := fmt.Sprintf(msg(c, problemsKeys[sv]), counts[sv]) + if Toggle(c, &on, name, ToggleOptions{Icon: icons.Must(problemsIcons[sv]), Disabled: opts.Disabled}).Changed() { + s.Hidden[sv] = !on + slog.Debug("mujica: problems_panel filter", "severity", sv, "shown", on) + } + } + }) + problemsGroup(problems, s) + if len(s.flat) == 0 { + codeCaption(c, msg(c, "problems_panel.empty")).Padding(Density(c).Unit() * 4) + return + } + s.rows.Selected = &s.selected + s.rows.Key = func(i int) any { return s.flat[i] } + s.rows.Label = func(i int) string { + if r := s.flat[i]; r.problem >= 0 { + return problems[r.problem].Message + } + return s.flat[i].file + } + list := codeRows(c, &s.rows, len(s.flat), func(i int) { problemsRow(c, problems, s, s.flat[i], disabled) }) + if disabled || s.selected < 0 { + return + } + r := s.flat[s.selected] + switch { + case list.Submitted() && r.problem >= 0: + p := problems[r.problem] + res.Opened = &p + slog.Debug("mujica: problems_panel open", "line", p.Line) + case list.Submitted(), + r.problem < 0 && !s.Collapsed.Has(r.file) && list.Shortcut(0, ui.KeyLeft), + r.problem < 0 && s.Collapsed.Has(r.file) && list.Shortcut(0, ui.KeyRight): + problemsFold(s, r.file) + } + }) + return res +} + +var ( + problemsKeys = [3]string{"problems_panel.errors", "problems_panel.warnings", "problems_panel.infos"} + problemsIcons = [3]string{"circle-x", "triangle-alert", "info"} +) + +func problemsFold(s *ProblemsPanelState, file string) { + if s.Collapsed.Has(file) { + s.Collapsed.Remove(file) + } else { + s.Collapsed.Add(file) + } + s.folded = -1 + slog.Debug("mujica: problems_panel fold", "folded", s.Collapsed.Has(file)) +} + +// problemsGroup regroups when data or filters change. +func problemsGroup(problems []Problem, s *ProblemsPanelState) { + var first *Problem + if len(problems) > 0 { + first = &problems[0] + } + if s.data == first && s.n == len(problems) && s.folded == s.Collapsed.Len() && s.hidden == s.Hidden { + return + } + s.data, s.n, s.folded, s.hidden = first, len(problems), s.Collapsed.Len(), s.Hidden + var idx []int + for i, p := range problems { + if !s.Hidden[p.Severity] { + idx = append(idx, i) + } + } + slices.SortStableFunc(idx, func(a, b int) int { + pa, pb := problems[a], problems[b] + return cmp.Or(cmp.Compare(pa.File, pb.File), cmp.Compare(pa.Line, pb.Line), cmp.Compare(pa.Col, pb.Col)) + }) + s.flat = s.flat[:0] + for j, i := range idx { + f := problems[i].File + if j == 0 || problems[idx[j-1]].File != f { + s.flat = append(s.flat, problemRow{f, -1}) + } + if !s.Collapsed.Has(f) { + s.flat = append(s.flat, problemRow{f, i}) + } + } + s.selected = min(s.selected, len(s.flat)-1) +} + +func problemsRow(c *ui.Context, problems []Problem, s *ProblemsPanelState, r problemRow, disabled bool) { + k := Tokens(c) + if r.problem < 0 { + chev, key := "chevron-down", "search_panel.fold" + if s.Collapsed.Has(r.file) { + chev, key = "chevron-right", "search_panel.unfold" + } + if iconAction(c, icons.Must(chev), fmt.Sprintf(msg(c, key), r.file)).Clicked() && !disabled { + problemsFold(s, r.file) + } + ui.Text(c, r.file).FontWeight(600).SingleLine().Grow(1).MinWidth(0) + return + } + p := problems[r.problem] + col := [3]ui.Color{k.Danger, k.Warning, k.Info}[p.Severity] + ui.Icon(c, icons.Must(problemsIcons[p.Severity])).Size(16, 16).Shrink(0).TextColor(col).Margin(0, 0, 0, 28). + Label(msg(c, [3]string{"problems_panel.error", "problems_panel.warning", "problems_panel.info"}[p.Severity])) + ui.Text(c, p.Message).SingleLine().Grow(1).MinWidth(0) + if p.Source != "" { + codeCaption(c, p.Source).Shrink(0) + } + codeCaption(c, fmt.Sprintf(msg(c, "problems_panel.at"), p.Line, p.Col)).FontFeatures("tnum").Shrink(0) +} diff --git a/problems_panel_test.go b/problems_panel_test.go new file mode 100644 index 0000000..2ecf38d --- /dev/null +++ b/problems_panel_test.go @@ -0,0 +1,106 @@ +package mujica + +import ( + "testing" + + "github.com/egoist/mygo/ui" +) + +func problemsSample() []Problem { + return []Problem{ + {ProblemWarning, "court/seal.go", 30, 2, "unused variable wax", "vet"}, + {ProblemError, "court/seal.go", 12, 5, "undefined: Decree", "compiler"}, + {ProblemInfo, "docs/ledger.md", 4, 1, "line is long", "lint"}, + } +} + +type problemsProbe struct { + problems []Problem + s ProblemsPanelState + disabled bool + opened []Problem +} + +func (p *problemsProbe) view(c *ui.Context) { + Use(c, Settings{}) + r := ProblemsPanel(c, p.problems, &p.s, ProblemsPanelOptions{Disabled: p.disabled}) + r.Element.Height(360) + if r.Opened != nil { + p.opened = append(p.opened, *r.Opened) + } +} + +func TestProblemsPanelGroupsSortedByLine(t *testing.T) { + p := &problemsProbe{problems: problemsSample()} + tt := ui.NewTester(p.view, 640, 380) + if !tt.HasText("Errors 1") || !tt.HasText("Warnings 1") || !tt.HasText("Info 1") { + t.Fatalf("counts missing: %q", tt.Texts()) + } + a, _ := tt.Find("undefined: Decree") + b, _ := tt.Find("unused variable wax") + if a.Y >= b.Y { + t.Fatal("problems are not sorted by line within the file") + } + if len(p.s.flat) != 5 || p.s.flat[0].file != "court/seal.go" || p.s.flat[3].file != "docs/ledger.md" { + t.Fatalf("rows %v", p.s.flat) + } +} + +func TestProblemsPanelFilterAndOpen(t *testing.T) { + p := &problemsProbe{problems: problemsSample()} + tt := ui.NewTester(p.view, 640, 380) + if err := tt.Click("Warnings 1"); err != nil { + t.Fatal(err) + } + if tt.HasText("unused variable wax") || !tt.HasText("undefined: Decree") { + t.Fatalf("filter kept warnings: %q", tt.Texts()) + } + r, _ := tt.Find("undefined: Decree") + tt.ClickAt(r.X+10, r.Y+r.H/2) + tt.ClickAt(r.X+10, r.Y+r.H/2) + if len(p.opened) != 1 || p.opened[0].Line != 12 || p.opened[0].Col != 5 { + t.Fatalf("opened %v", p.opened) + } +} + +func TestProblemsPanelKeyboardFold(t *testing.T) { + p := &problemsProbe{problems: problemsSample()} + tt := ui.NewTester(p.view, 640, 380) + for range 4 { + tt.Key(0, ui.KeyTab) + } + tt.Key(0, ui.KeyDown) + tt.Key(0, ui.KeyLeft) + if !p.s.Collapsed.Has("court/seal.go") || tt.HasText("undefined: Decree") { + t.Fatal("Left did not fold the file") + } + tt.Key(0, ui.KeyRight) + tt.Key(0, ui.KeyDown) + tt.Key(0, ui.KeyEnter) + if len(p.opened) != 1 || p.opened[0].Message != "undefined: Decree" { + t.Fatalf("Enter opened %v", p.opened) + } +} + +func TestProblemsPanelDisabledAndEmpty(t *testing.T) { + p := &problemsProbe{problems: problemsSample(), disabled: true} + tt := ui.NewTester(p.view, 640, 380) + _ = tt.Click("Errors 1") + _ = tt.Click("Fold court/seal.go") + if p.s.Hidden[ProblemError] || p.s.Collapsed.Len() != 0 { + t.Fatal("disabled panel changed") + } + tt = ui.NewTester((&problemsProbe{}).view, 640, 380) + if !tt.HasText("No problems") { + t.Fatalf("texts %q", tt.Texts()) + } +} + +func TestProblemsPanelSnapshot(t *testing.T) { + snapshot(t, "problems_panel", 600, 240, func(c *ui.Context) { + s := ui.Local(c.Root(), "snap", func() *ProblemsPanelState { return &ProblemsPanelState{} }) + ui.Column(c).Padding(12).Children(func() { + ProblemsPanel(c, problemsSample(), *s, ProblemsPanelOptions{}).Element.Height(216) + }) + }) +} diff --git a/process_list.go b/process_list.go new file mode 100644 index 0000000..73b608e --- /dev/null +++ b/process_list.go @@ -0,0 +1,185 @@ +package mujica + +import ( + "cmp" + "fmt" + "log/slog" + "slices" + "strings" + + "github.com/egoist/mygo/ui" + + "mujicaui/icons" + "mujicaui/internal" + "mujicaui/theme" +) + +// Process is one row of a ProcessList. +type Process struct { + PID int + Name string + // CPU is percent of one core; Memory is resident bytes. + CPU float64 + Memory uint64 +} + +// ProcessListState is a ProcessList's sort, selection and pending end. +type ProcessListState struct { + // Sort is by name, pid, cpu or memory. + Sort ui.SortOrder + rows ui.ListState + selected int + order []int + sorted ui.SortOrder + data *Process + n int + confirm bool + ending Process + started bool +} + +// ProcessListOptions configure a ProcessList. +type ProcessListOptions struct { + Disabled bool +} + +// ProcessListResult carries a confirmed end request. +type ProcessListResult struct { + Element *ui.Element + // End is the confirmed process to end, or nil. + End *Process +} + +// ProcessList is a sortable process table with confirmed ending. +func ProcessList(c *ui.Context, procs []Process, s *ProcessListState, opts ProcessListOptions) ProcessListResult { + if s == nil { + panic("mujica: ProcessList needs a state") + } + if !s.started { + s.started, s.selected, s.Sort = true, -1, ui.SortOrder{Column: "cpu", Descending: true} + } + processListSort(procs, s) + k := Tokens(c) + var res ProcessListResult + res.Element = codePanel(c).Disabled(opts.Disabled) + disabled := res.Element.IsDisabled() + if disabled { + s.confirm = false + } + ask := func(p Process) { + if !disabled { + s.ending, s.confirm = p, true + } + } + res.Element.Children(func() { + if len(procs) == 0 { + codeCaption(c, msg(c, "process_list.empty")).Padding(Density(c).Unit() * 4) + return + } + cols := []ui.TableColumn{ + {Title: msg(c, "process_list.name"), ID: "name", Sortable: true, MinWidth: 120}, + {Title: msg(c, "process_list.pid"), ID: "pid", Width: 80, Align: ui.End, Sortable: true}, + {Title: msg(c, "process_list.cpu"), ID: "cpu", Width: 90, Align: ui.End, Sortable: true}, + {Title: msg(c, "process_list.memory"), ID: "memory", Width: 100, Align: ui.End, Sortable: true}, + {Title: msg(c, "process_list.actions"), ID: "end", Width: 56, Fixed: true, Align: ui.Center}, + } + s.rows.Sort = &s.Sort + s.rows.Selected = &s.selected + s.rows.Key = func(i int) any { return procs[s.order[i]].PID } + s.rows.Label = func(i int) string { return procs[s.order[i]].Name } + restore := dataSelectTheme(c, 0) + table := ui.Table(c, &s.rows, cols, len(procs), func(row, col int) { + p := procs[s.order[row]] + switch col { + case 0: + ui.Row(c).Gap(Density(c).Unit() * 2).AlignItems(ui.Center).Children(func() { + codeSelectMark(c, s.selected == row) + ui.Text(c, p.Name).SingleLine() + }) + case 1: + codeMono(c, fmt.Sprint(p.PID)) + case 2: + codeMono(c, fmt.Sprintf("%.1f%%", p.CPU)) + case 3: + codeMono(c, processListBytes(p.Memory)) + case 4: + if iconAction(c, icons.Must("x"), fmt.Sprintf(msg(c, "process_list.end"), p.Name)).Clicked() { + ask(p) + } + } + }).Grow(1).MinHeight(Density(c).ControlHeight() * 4) + restore() + internal.Ring(table, k.Focus, theme.ControlRadius) + if table.Changed() { + processListSort(procs, s) + } + if table.Shortcut(0, ui.KeyDelete) && s.selected >= 0 && s.selected < len(procs) { + ask(procs[s.order[s.selected]]) + } + }) + if s.confirm { + r := AlertDialog(c, &s.confirm, AlertDialogOptions{ + Title: fmt.Sprintf(msg(c, "process_list.confirm"), s.ending.Name, s.ending.PID), + Description: msg(c, "process_list.warning"), + ConfirmLabel: msg(c, "process_list.endButton"), + }) + if r.Confirmed && !disabled { + p := s.ending + res.End = &p + slog.Debug("mujica: process_list end", "pid", p.PID) + } + } + return res +} + +// processListSort reorders when data or sort change. +func processListSort(procs []Process, s *ProcessListState) { + var first *Process + if len(procs) > 0 { + first = &procs[0] + } + if s.data == first && s.n == len(procs) && s.sorted == s.Sort && len(s.order) == len(procs) { + return + } + s.data, s.n, s.sorted = first, len(procs), s.Sort + s.order = s.order[:0] + for i := range procs { + s.order = append(s.order, i) + } + by := func(a, b int) int { + pa, pb := procs[a], procs[b] + switch s.Sort.Column { + case "name": + return strings.Compare(strings.ToLower(pa.Name), strings.ToLower(pb.Name)) + case "pid": + return cmp.Compare(pa.PID, pb.PID) + case "cpu": + return cmp.Compare(pa.CPU, pb.CPU) + case "memory": + return cmp.Compare(pa.Memory, pb.Memory) + case "": + return 0 + } + panic("mujica: ProcessList sorts by name, pid, cpu or memory, not " + s.Sort.Column) + } + slices.SortStableFunc(s.order, func(a, b int) int { + if s.Sort.Descending { + return by(b, a) + } + return by(a, b) + }) + slog.Debug("mujica: process_list sort", "by", s.Sort.Column, "descending", s.Sort.Descending) +} + +func processListBytes(b uint64) string { + const unit = 1024 + switch { + case b >= unit*unit*unit: + return fmt.Sprintf("%.1f GB", float64(b)/(unit*unit*unit)) + case b >= unit*unit: + return fmt.Sprintf("%.1f MB", float64(b)/(unit*unit)) + case b >= unit: + return fmt.Sprintf("%.0f KB", float64(b)/unit) + } + return fmt.Sprintf("%d B", b) +} diff --git a/process_list_test.go b/process_list_test.go new file mode 100644 index 0000000..a8a89cf --- /dev/null +++ b/process_list_test.go @@ -0,0 +1,124 @@ +package mujica + +import ( + "testing" + + "github.com/egoist/mygo/ui" +) + +func processListSample() []Process { + return []Process{ + {PID: 101, Name: "chancellor", CPU: 3.5, Memory: 200 << 20}, + {PID: 7, Name: "archivist", CPU: 42.1, Memory: 50 << 20}, + {PID: 330, Name: "Herald", CPU: 0.2, Memory: 2 << 30}, + } +} + +func processListTester(procs []Process, s *ProcessListState, disabled bool, ended *[]int) *ui.Tester { + return ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + r := ProcessList(c, procs, s, ProcessListOptions{Disabled: disabled}) + r.Element.Height(300) + if r.End != nil { + *ended = append(*ended, r.End.PID) + } + }, 560, 340) +} + +func processListOrder(procs []Process, s *ProcessListState) []int { + var out []int + for _, i := range s.order { + out = append(out, procs[i].PID) + } + return out +} + +func TestProcessListSortsByCPUThenMemory(t *testing.T) { + procs := processListSample() + s := &ProcessListState{} + var ended []int + tt := processListTester(procs, s, false, &ended) + if got := processListOrder(procs, s); got[0] != 7 || got[2] != 330 { + t.Fatalf("default CPU order %v", got) + } + if err := tt.Click("Memory"); err != nil { + t.Fatal(err) + } + if got := processListOrder(procs, s); got[0] != 7 || got[2] != 330 || s.Sort.Descending { + t.Fatalf("memory ascending %v", got) + } + _ = tt.Click("Memory") + if got := processListOrder(procs, s); got[0] != 330 { + t.Fatalf("memory descending %v", got) + } + if !tt.HasText("2.0 GB") || !tt.HasText("42.1%") { + t.Fatalf("texts %q", tt.Texts()) + } +} + +func TestProcessListEndAsksFirst(t *testing.T) { + procs := processListSample() + s := &ProcessListState{} + var ended []int + tt := processListTester(procs, s, false, &ended) + if err := tt.Click("End Herald"); err != nil { + t.Fatal(err) + } + if len(ended) != 0 || !tt.HasText("End “Herald” (PID 330)?") { + t.Fatalf("ended %v before confirmation", ended) + } + if err := tt.Click("End process"); err != nil { + t.Fatal(err) + } + if len(ended) != 1 || ended[0] != 330 { + t.Fatalf("ended %v", ended) + } +} + +func TestProcessListKeyboardEnd(t *testing.T) { + procs := processListSample() + s := &ProcessListState{} + var ended []int + tt := processListTester(procs, s, false, &ended) + tt.Key(0, ui.KeyTab) + tt.Key(0, ui.KeyDown) + tt.Key(0, ui.KeyDelete) + if !tt.HasText("End “archivist” (PID 7)?") { + t.Fatalf("Delete did not ask for the chosen row: %q", tt.Texts()) + } + tt.Key(0, ui.KeyEscape) + if len(ended) != 0 { + t.Fatal("Escape ended the process") + } +} + +func TestProcessListDisabledAndEmpty(t *testing.T) { + var ended []int + tt := processListTester(processListSample(), &ProcessListState{}, true, &ended) + _ = tt.Click("End Herald") + if tt.HasText("End process") { + t.Fatal("disabled list asked to end a process") + } + tt = processListTester(nil, &ProcessListState{}, false, &ended) + if !tt.HasText("No processes") { + t.Fatalf("texts %q", tt.Texts()) + } +} + +func TestProcessListBadSortPanics(t *testing.T) { + defer func() { + if recover() == nil { + t.Fatal("unknown sort column did not panic") + } + }() + processListSort(processListSample(), &ProcessListState{Sort: ui.SortOrder{Column: "owner"}}) +} + +func TestProcessListSnapshot(t *testing.T) { + snapshot(t, "process_list", 560, 220, func(c *ui.Context) { + s := ui.Local(c.Root(), "snap", func() *ProcessListState { return &ProcessListState{} }) + ui.Column(c).Padding(12).Children(func() { + ProcessList(c, processListSample(), *s, ProcessListOptions{}).Element.Height(196) + }) + }) +} diff --git a/search_panel.go b/search_panel.go new file mode 100644 index 0000000..6e14471 --- /dev/null +++ b/search_panel.go @@ -0,0 +1,202 @@ +package mujica + +import ( + "fmt" + "log/slog" + "strings" + + "github.com/egoist/mygo/ui" + + "mujicaui/icons" +) + +// SearchMatch is a hit; Start and End index Text. +type SearchMatch struct { + Line int + Text string + Start, End int +} + +// SearchFileResult is a file and its hits. +type SearchFileResult struct { + Path string + Matches []SearchMatch +} + +// SearchPanelState is a SearchPanel's query, patterns, folded files and place. +type SearchPanelState struct { + Query, Include, Exclude string + // Collapsed holds the paths of folded files. + Collapsed ui.Selection[string] + rows ui.ListState + selected int + flat []searchRow + data *SearchFileResult + n, folded int + started bool +} + +type searchRow struct{ file, match int } + +// SearchPanelOptions configure a SearchPanel. +type SearchPanelOptions struct { + // Status shows a search in progress or failed. + Status DataStatus + Error string + Disabled bool +} + +// SearchOpen is a hit the user opened. +type SearchOpen struct { + Path string + Line int +} + +// SearchPanelResult is a SearchPanel and what happened in this frame. +type SearchPanelResult struct { + Element *ui.Element + // Changed asks the caller to search again. + Changed bool + Opened *SearchOpen +} + +// SearchPanel shows search results grouped by file. +func SearchPanel(c *ui.Context, results []SearchFileResult, s *SearchPanelState, opts SearchPanelOptions) SearchPanelResult { + if s == nil { + panic("mujica: SearchPanel needs a state") + } + if !s.started { + s.started, s.selected = true, -1 + } + d := Density(c) + var res SearchPanelResult + res.Element = codePanel(c).Disabled(opts.Disabled) + disabled := res.Element.IsDisabled() + res.Element.Children(func() { + ui.Column(c).Shrink(0).Padding(d.Unit() * 2).Gap(d.Unit() * 2).Children(func() { + res.Changed = SearchInput(c, &s.Query, SearchInputOptions{Disabled: opts.Disabled}).Changed() + ui.Row(c).Gap(d.Unit() * 2).Children(func() { + for _, f := range []struct { + v *string + key string + }{{&s.Include, "search_panel.include"}, {&s.Exclude, "search_panel.exclude"}} { + r := TextInput(c, f.v, TextInputOptions{Placeholder: msg(c, f.key), Label: msg(c, f.key), Disabled: opts.Disabled}) + r.Element.Grow(1).MinWidth(0) + res.Changed = res.Changed || r.Changed() + } + }) + }) + if res.Changed { + slog.Debug("mujica: search_panel query", "query", s.Query, "include", s.Include, "exclude", s.Exclude) + } + searchPanelFlatten(results, s) + if len(results) == 0 || opts.Status != DataReady { + text := msg(c, "search_panel.idle") + if s.Query != "" { + text = msg(c, "search_panel.none") + } + res.Changed = dataPlaceholder(c, opts.Status, opts.Error, text, text) || res.Changed + return + } + total := 0 + for _, f := range results { + total += len(f.Matches) + } + codeCaption(c, fmt.Sprintf(msg(c, "search_panel.summary"), total, len(results))).Padding(0, d.Unit()*3, d.Unit(), d.Unit()*3) + s.rows.Selected = &s.selected + s.rows.Key = func(i int) any { return s.flat[i] } + s.rows.Label = func(i int) string { return searchPanelLabel(results, s.flat[i]) } + list := codeRows(c, &s.rows, len(s.flat), func(i int) { searchPanelRow(c, results, s, s.flat[i], disabled) }) + if list.Submitted() && !disabled && s.selected >= 0 { + r := s.flat[s.selected] + f := results[r.file] + if r.match < 0 { + searchPanelFold(s, f.Path, !s.Collapsed.Has(f.Path)) + } else { + res.Opened = &SearchOpen{Path: f.Path, Line: f.Matches[r.match].Line} + slog.Debug("mujica: search_panel open", "line", res.Opened.Line) + } + } + if !disabled && s.selected >= 0 && s.flat[s.selected].match < 0 { + path := results[s.flat[s.selected].file].Path + switch { + case list.Shortcut(0, ui.KeyLeft): + searchPanelFold(s, path, true) + case list.Shortcut(0, ui.KeyRight): + searchPanelFold(s, path, false) + } + } + }) + return res +} + +func searchPanelFold(s *SearchPanelState, path string, fold bool) { + if fold { + s.Collapsed.Add(path) + } else { + s.Collapsed.Remove(path) + } + s.folded = -1 + slog.Debug("mujica: search_panel fold", "folded", fold) +} + +// searchPanelFlatten relists rows when results or folds change. +func searchPanelFlatten(results []SearchFileResult, s *SearchPanelState) { + var first *SearchFileResult + if len(results) > 0 { + first = &results[0] + } + if s.data == first && s.n == len(results) && s.folded == s.Collapsed.Len() { + return + } + s.data, s.n, s.folded = first, len(results), s.Collapsed.Len() + s.flat = s.flat[:0] + for fi, f := range results { + s.flat = append(s.flat, searchRow{fi, -1}) + if !s.Collapsed.Has(f.Path) { + for mi := range f.Matches { + s.flat = append(s.flat, searchRow{fi, mi}) + } + } + } + s.selected = min(s.selected, len(s.flat)-1) +} + +func searchPanelLabel(results []SearchFileResult, r searchRow) string { + f := results[r.file] + if r.match < 0 { + return f.Path + } + m := f.Matches[r.match] + return fmt.Sprintf("%d %s", m.Line, strings.TrimSpace(m.Text)) +} + +func searchPanelRow(c *ui.Context, results []SearchFileResult, s *SearchPanelState, r searchRow, disabled bool) { + k := Tokens(c) + f := results[r.file] + if r.match < 0 { + chev, key := "chevron-down", "search_panel.fold" + if s.Collapsed.Has(f.Path) { + chev, key = "chevron-right", "search_panel.unfold" + } + if iconAction(c, icons.Must(chev), fmt.Sprintf(msg(c, key), f.Path)).Clicked() && !disabled { + searchPanelFold(s, f.Path, !s.Collapsed.Has(f.Path)) + } + ui.Icon(c, icons.Must("file-text")).Size(16, 16).Shrink(0).TextColor(k.TextMuted) + ui.Text(c, f.Path).FontWeight(600).SingleLine().Grow(1).MinWidth(0) + codeCaption(c, fmt.Sprint(len(f.Matches))).Shrink(0) + return + } + m := f.Matches[r.match] + if m.Start < 0 || m.End > len(m.Text) || m.Start > m.End { + panic("mujica: SearchMatch range outside its Text") + } + codeMono(c, fmt.Sprint(m.Line)).Width(codeTextWidth(c, "99999")).TextColor(k.TextMuted).Shrink(0).Margin(0, 0, 0, 28) + from := max(0, m.Start-40) + before := strings.TrimLeft(m.Text[from:m.Start], " \t") + if from > 0 { + before = "…" + before + } + spans := []ui.Span{{Text: before}, {Text: m.Text[m.Start:m.End], Background: k.Selection, Underline: true, DecorationColor: k.AccentText, Weight: 600}, {Text: m.Text[m.End:]}} + ui.RichText(c, spans...).SingleLine().Grow(1).MinWidth(0) +} diff --git a/search_panel_test.go b/search_panel_test.go new file mode 100644 index 0000000..23e9102 --- /dev/null +++ b/search_panel_test.go @@ -0,0 +1,129 @@ +package mujica + +import ( + "testing" + + "github.com/egoist/mygo/ui" +) + +func searchPanelSample() []SearchFileResult { + return []SearchFileResult{ + {Path: "court/seal.go", Matches: []SearchMatch{{Line: 12, Text: "func Seal(d Decree) {", Start: 5, End: 9}, {Line: 30, Text: "\treturn Seal(next)", Start: 8, End: 12}}}, + {Path: "docs/ledger.md", Matches: []SearchMatch{{Line: 4, Text: "The seal is kept by the chancellor.", Start: 4, End: 8}}}, + } +} + +type searchPanelProbe struct { + results []SearchFileResult + s SearchPanelState + opts SearchPanelOptions + changed int + opened []SearchOpen +} + +func (p *searchPanelProbe) view(c *ui.Context) { + Use(c, Settings{}) + r := SearchPanel(c, p.results, &p.s, p.opts) + r.Element.Height(400) + if r.Changed { + p.changed++ + } + if r.Opened != nil { + p.opened = append(p.opened, *r.Opened) + } +} + +func TestSearchPanelGroupsAndOpens(t *testing.T) { + p := &searchPanelProbe{results: searchPanelSample()} + tt := ui.NewTester(p.view, 560, 420) + if !tt.HasText("3 results in 2 files") || !tt.HasText("court/seal.go") { + t.Fatalf("texts %q", tt.Texts()) + } + r, _ := tt.Find("12") + tt.ClickAt(r.X+60, r.Y+r.H/2) + tt.ClickAt(r.X+60, r.Y+r.H/2) + if len(p.opened) != 1 || p.opened[0] != (SearchOpen{"court/seal.go", 12}) { + t.Fatalf("opened %v", p.opened) + } + if err := tt.Click("Fold court/seal.go"); err != nil { + t.Fatal(err) + } + if tt.HasText("12") || !tt.HasText("docs/ledger.md") { + t.Fatalf("fold kept hits: %q", tt.Texts()) + } + _ = tt.Click("Unfold court/seal.go") + if !tt.HasText("30") { + t.Fatal("unfold did not show the hits") + } +} + +func TestSearchPanelQueryAndPatternsReport(t *testing.T) { + p := &searchPanelProbe{} + tt := ui.NewTester(p.view, 560, 420) + if !tt.HasText("Type to search in files") { + t.Fatalf("texts %q", tt.Texts()) + } + tt.Key(0, ui.KeyTab) + tt.Type("seal") + tt.Key(0, ui.KeyTab) + tt.Type("*.go") + if p.changed != 2 || p.s.Query != "seal" || p.s.Include != "*.go" { + t.Fatalf("changed %d query %q include %q", p.changed, p.s.Query, p.s.Include) + } + if !tt.HasText("No results") { + t.Fatal("empty search with a query says nothing") + } +} + +func TestSearchPanelKeyboard(t *testing.T) { + p := &searchPanelProbe{results: searchPanelSample()} + tt := ui.NewTester(p.view, 560, 420) + for range 5 { + tt.Key(0, ui.KeyTab) + } + tt.Key(0, ui.KeyDown) + tt.Key(0, ui.KeyLeft) + if !p.s.Collapsed.Has("court/seal.go") { + t.Fatal("Left on a file did not fold it") + } + tt.Key(0, ui.KeyRight) + tt.Key(0, ui.KeyDown) + tt.Key(0, ui.KeyDown) + tt.Key(0, ui.KeyEnter) + if len(p.opened) != 1 || p.opened[0].Line != 30 { + t.Fatalf("Enter opened %v", p.opened) + } +} + +func TestSearchPanelStatusAndDisabled(t *testing.T) { + p := &searchPanelProbe{opts: SearchPanelOptions{Status: DataFailed, Error: "index unavailable"}} + tt := ui.NewTester(p.view, 560, 420) + if !tt.HasText("index unavailable") { + t.Fatalf("texts %q", tt.Texts()) + } + p = &searchPanelProbe{results: searchPanelSample(), opts: SearchPanelOptions{Disabled: true}} + tt = ui.NewTester(p.view, 560, 420) + _ = tt.Click("Fold court/seal.go") + if p.s.Collapsed.Len() != 0 { + t.Fatal("disabled panel folded") + } +} + +func TestSearchPanelBadRangePanics(t *testing.T) { + defer func() { + if recover() == nil { + t.Fatal("match outside its text did not panic") + } + }() + p := &searchPanelProbe{results: []SearchFileResult{{Path: "a", Matches: []SearchMatch{{Text: "ab", Start: 1, End: 9}}}}} + ui.NewTester(p.view, 560, 420) +} + +func TestSearchPanelSnapshot(t *testing.T) { + snapshot(t, "search_panel", 520, 300, func(c *ui.Context) { + s := ui.Local(c.Root(), "snap", func() *SearchPanelState { return &SearchPanelState{Query: "seal", Include: "*.go, *.md"} }) + ui.Column(c).Padding(12).Children(func() { + SearchPanel(c, searchPanelSample(), *s, SearchPanelOptions{}).Element.Height(276) + }) + }) +} diff --git a/symbol_outline.go b/symbol_outline.go new file mode 100644 index 0000000..e82bba1 --- /dev/null +++ b/symbol_outline.go @@ -0,0 +1,235 @@ +package mujica + +import ( + "log/slog" + "strings" + + "github.com/egoist/mygo/ui" + + "mujicaui/internal" + "mujicaui/theme" +) + +// SymbolKind is what a Symbol declares. +type SymbolKind int + +const ( + SymbolFunction SymbolKind = iota + SymbolMethod + SymbolType + SymbolInterface + SymbolField + SymbolVariable + SymbolConstant + SymbolModule +) + +// Symbol is a declaration spanning Line to EndLine, from 1. +type Symbol struct { + Name, Detail string + Kind SymbolKind + Line, EndLine int + Children []Symbol +} + +// SymbolOutlineState is a SymbolOutline's filter, open symbols and choice. +type SymbolOutlineState struct { + Query string + outline ui.OutlineState[int] + cursor int + nodes []symbolNode + roots []int + data *Symbol + n int + line int + started bool +} + +type symbolNode struct { + sym *Symbol + parent int + kids []int +} + +// SymbolOutlineOptions configure a SymbolOutline. +type SymbolOutlineOptions struct { + // CursorLine chooses its symbol when it changes; 0 is off. + CursorLine int + Disabled bool +} + +// SymbolOutlineResult carries the symbol to jump to. +type SymbolOutlineResult struct { + Element *ui.Element + Jumped *Symbol +} + +var symbolBadges = [...]string{"f", "m", "T", "I", "p", "v", "c", "M"} + +// SymbolOutline shows a filterable symbol tree. +func SymbolOutline(c *ui.Context, symbols []Symbol, s *SymbolOutlineState, opts SymbolOutlineOptions) SymbolOutlineResult { + if s == nil { + panic("mujica: SymbolOutline needs a state") + } + if !s.started { + s.started, s.cursor = true, -1 + } + symbolIndex(symbols, s) + k := Tokens(c) + d := Density(c) + var res SymbolOutlineResult + res.Element = codePanel(c).Disabled(opts.Disabled) + disabled := res.Element.IsDisabled() + res.Element.Children(func() { + codeHeader(c, func() { + SearchInput(c, &s.Query, SearchInputOptions{Placeholder: msg(c, "symbol_outline.filter"), Disabled: opts.Disabled}).Element.Grow(1) + }) + q := strings.ToLower(strings.TrimSpace(s.Query)) + keep := symbolFilter(s, q) + roots := symbolKept(s.roots, keep) + if len(roots) == 0 { + codeCaption(c, msg(c, "symbol_outline.empty")).Padding(d.Unit() * 4) + return + } + follow := opts.CursorLine != s.line + s.line = opts.CursorLine + if follow { + symbolFollow(s, opts.CursorLine, keep) + } + s.outline.List.Selected = &s.cursor + s.outline.List.Label = func(i int) string { return s.nodes[s.outline.Item(i)].sym.Name } + children := func(i int) []int { + if len(s.nodes[i].kids) == 0 { + return nil + } + if q != "" { + s.outline.Open.Add(i) + } + return symbolKept(s.nodes[i].kids, keep) + } + restore := dataSelectTheme(c, 0) + e := ui.Outline(c, &s.outline, roots, children, func(i int) { + sym := s.nodes[i].sym + if sym.Kind < SymbolFunction || sym.Kind > SymbolModule { + panic("mujica: Symbol kind out of range") + } + ui.Row(c).MinHeight(d.ControlHeight() - 2*c.Theme().Space(1)).Gap(d.Unit() * 2).AlignItems(ui.Center).Grow(1).MinWidth(0).Children(func() { + codeBadge(c, symbolBadges[sym.Kind], [...]ui.Color{k.AccentText, k.AccentText, k.Info, k.Info, k.TextMuted, k.TextMuted, k.Warning, k.Success}[sym.Kind]) + ui.RichText(c, codeMatchSpans(c, sym.Name, q)...).SingleLine().MinWidth(0) + if sym.Detail != "" { + codeCaption(c, sym.Detail).MinWidth(0) + } + ui.Spacer(c) + codeSelectMark(c, s.cursor >= 0 && s.cursor < s.outline.Rows() && s.outline.Item(s.cursor) == i) + }) + }) + restore() + e.Grow(1).MinHeight(d.ControlHeight() * 3) + internal.Ring(e, k.Focus, theme.ControlRadius) + if follow && s.cursor >= 0 { + s.outline.List.ScrollIntoView(s.cursor) + } + if !disabled && s.cursor >= 0 && s.cursor < s.outline.Rows() && e.Submitted() { + sym := *s.nodes[s.outline.Item(s.cursor)].sym + res.Jumped = &sym + slog.Debug("mujica: symbol_outline jump", "line", sym.Line) + } + }) + return res +} + +// symbolIndex numbers the symbols depth first when the tree changes. +func symbolIndex(symbols []Symbol, s *SymbolOutlineState) { + var first *Symbol + if len(symbols) > 0 { + first = &symbols[0] + } + if s.data == first && s.n == len(symbols) { + return + } + s.data, s.n, s.nodes, s.roots = first, len(symbols), s.nodes[:0], s.roots[:0] + var walk func(list []Symbol, parent int) []int + walk = func(list []Symbol, parent int) []int { + var ids []int + for i := range list { + id := len(s.nodes) + s.nodes = append(s.nodes, symbolNode{sym: &list[i], parent: parent}) + s.nodes[id].kids = walk(list[i].Children, id) + ids = append(ids, id) + } + return ids + } + s.roots = walk(symbols, -1) +} + +// symbolFilter marks matches and ancestors; nil keeps all. +func symbolFilter(s *SymbolOutlineState, q string) []bool { + if q == "" { + return nil + } + keep := make([]bool, len(s.nodes)) + for i, n := range s.nodes { + if strings.Contains(strings.ToLower(n.sym.Name), q) { + for j := i; j >= 0 && !keep[j]; j = s.nodes[j].parent { + keep[j] = true + } + } + } + return keep +} + +func symbolKept(ids []int, keep []bool) []int { + if keep == nil { + return ids + } + var out []int + for _, id := range ids { + if keep[id] { + out = append(out, id) + } + } + return out +} + +// symbolFollow opens and chooses the innermost symbol spanning line. +func symbolFollow(s *SymbolOutlineState, line int, keep []bool) { + best := -1 + for i, n := range s.nodes { + if keep != nil && !keep[i] { + continue + } + if n.sym.Line <= line && line <= max(n.sym.EndLine, n.sym.Line) { + best = i + } + } + if best < 0 { + return + } + for p := s.nodes[best].parent; p >= 0; p = s.nodes[p].parent { + s.outline.Open.Add(p) + } + s.cursor = symbolRow(s, best, keep) + slog.Debug("mujica: symbol_outline follow", "line", line) +} + +// symbolRow returns the row the shown node id is at. +func symbolRow(s *SymbolOutlineState, id int, keep []bool) int { + row := 0 + var walk func(ids []int) bool + walk = func(ids []int) bool { + for _, n := range symbolKept(ids, keep) { + if n == id { + return true + } + row++ + if s.outline.Open.Has(n) && walk(s.nodes[n].kids) { + return true + } + } + return false + } + if walk(s.roots) { + return row + } + return -1 +} diff --git a/symbol_outline_test.go b/symbol_outline_test.go new file mode 100644 index 0000000..106bb33 --- /dev/null +++ b/symbol_outline_test.go @@ -0,0 +1,107 @@ +package mujica + +import ( + "testing" + + "github.com/egoist/mygo/ui" +) + +func symbolSample() []Symbol { + return []Symbol{ + {Name: "Court", Kind: SymbolType, Line: 3, EndLine: 40, Detail: "struct", Children: []Symbol{ + {Name: "Seal", Kind: SymbolMethod, Line: 10, EndLine: 20, Detail: "(d Decree) error"}, + {Name: "ledger", Kind: SymbolField, Line: 5, EndLine: 5}, + }}, + {Name: "Herald", Kind: SymbolFunction, Line: 44, EndLine: 60}, + {Name: "MaxSeals", Kind: SymbolConstant, Line: 1, EndLine: 1}, + } +} + +type symbolProbe struct { + symbols []Symbol + s SymbolOutlineState + opts SymbolOutlineOptions + jumped []string +} + +func (p *symbolProbe) view(c *ui.Context) { + Use(c, Settings{}) + r := SymbolOutline(c, p.symbols, &p.s, p.opts) + r.Element.Height(320) + if r.Jumped != nil { + p.jumped = append(p.jumped, r.Jumped.Name) + } +} + +func TestSymbolOutlineFollowsCursor(t *testing.T) { + p := &symbolProbe{symbols: symbolSample(), opts: SymbolOutlineOptions{CursorLine: 12}} + tt := ui.NewTester(p.view, 400, 340) + if !tt.HasText("Seal") || p.s.cursor < 0 || p.s.nodes[p.s.outline.Item(p.s.cursor)].sym.Name != "Seal" { + t.Fatalf("cursor at line 12 did not choose Seal: %q", tt.Texts()) + } + p.opts.CursorLine = 50 + tt.Frame() + if name := p.s.nodes[p.s.outline.Item(p.s.cursor)].sym.Name; name != "Herald" { + t.Fatalf("line 50 chose %q", name) + } +} + +func TestSymbolOutlineFilter(t *testing.T) { + p := &symbolProbe{symbols: symbolSample()} + tt := ui.NewTester(p.view, 400, 340) + tt.Key(0, ui.KeyTab) + tt.Type("led") + if !tt.HasText("Court") || !tt.HasText("ledger") || tt.HasText("Herald") || tt.HasText("Seal") { + t.Fatalf("filter shows %q", tt.Texts()) + } + tt.Key(ui.Cmd, ui.KeyA) + tt.Type("zz") + if !tt.HasText("No symbols") { + t.Fatalf("texts %q", tt.Texts()) + } +} + +func TestSymbolOutlineJumpByKeyboard(t *testing.T) { + p := &symbolProbe{symbols: symbolSample()} + tt := ui.NewTester(p.view, 400, 340) + tt.Key(0, ui.KeyTab) + tt.Key(0, ui.KeyTab) + tt.Key(0, ui.KeyDown) + tt.Key(0, ui.KeyDown) + tt.Key(0, ui.KeyEnter) + if len(p.jumped) != 1 || p.jumped[0] != "Herald" { + t.Fatalf("jumped %v", p.jumped) + } + tt.Key(0, ui.KeyUp) + tt.Key(0, ui.KeyRight) + if !tt.HasText("Seal") { + t.Fatal("Right did not open Court") + } +} + +func TestSymbolOutlineDisabledAndBadKind(t *testing.T) { + p := &symbolProbe{symbols: symbolSample(), opts: SymbolOutlineOptions{Disabled: true}} + tt := ui.NewTester(p.view, 400, 340) + tt.Key(0, ui.KeyTab) + tt.Key(0, ui.KeyEnter) + if len(p.jumped) != 0 { + t.Fatal("disabled outline jumped") + } + defer func() { + if recover() == nil { + t.Fatal("bad kind did not panic") + } + }() + ui.NewTester((&symbolProbe{symbols: []Symbol{{Name: "x", Kind: 99}}}).view, 400, 340) +} + +func TestSymbolOutlineSnapshot(t *testing.T) { + snapshot(t, "symbol_outline", 360, 260, func(c *ui.Context) { + p := ui.Local(c.Root(), "snap", func() *symbolProbe { + return &symbolProbe{symbols: symbolSample(), opts: SymbolOutlineOptions{CursorLine: 12}} + }) + ui.Column(c).Padding(12).Children(func() { + SymbolOutline(c, (*p).symbols, &(*p).s, (*p).opts).Element.Height(236) + }) + }) +} diff --git a/terminal.go b/terminal.go new file mode 100644 index 0000000..deee353 --- /dev/null +++ b/terminal.go @@ -0,0 +1,308 @@ +package mujica + +import ( + "log/slog" + "math" + "runtime" + + "github.com/egoist/mygo/ui" + + "mujicaui/internal" + "mujicaui/theme" +) + +// TerminalOptions configure a Terminal. +type TerminalOptions struct { + // Input receives what the user types, encoded for the PTY. + Input func(data string) + // Resized reports a new grid size for the PTY. + Resized func(cols, rows int) + // Scrollback caps lines kept above; zero keeps 1000. + Scrollback int + Label string + Disabled bool +} + +// terminalPad is the space around the grid in DIPs. +const terminalPad float32 = 6 + +// terminalCtrl maps letter keys to their Ctrl codes. +var terminalCtrl = map[ui.Key]byte{ui.KeyA: 1, ui.KeyB: 2, ui.KeyC: 3, ui.KeyD: 4, ui.KeyE: 5, ui.KeyF: 6, ui.KeyG: 7, + ui.KeyH: 8, ui.KeyI: 9, ui.KeyJ: 10, ui.KeyK: 11, ui.KeyL: 12, ui.KeyM: 13, ui.KeyN: 14, ui.KeyO: 15, ui.KeyP: 16, + ui.KeyQ: 17, ui.KeyR: 18, ui.KeyS: 19, ui.KeyT: 20, ui.KeyU: 21, ui.KeyV: 22, ui.KeyW: 23, ui.KeyX: 24, ui.KeyY: 25, ui.KeyZ: 26} + +// terminalKeys are the sequences of keys that type no text. +var terminalKeys = map[ui.Key]string{ui.KeyEnter: "\r", ui.KeyBackspace: "\x7f", ui.KeyTab: "\t", ui.KeyEscape: "\x1b", + ui.KeyUp: "\x1b[A", ui.KeyDown: "\x1b[B", ui.KeyRight: "\x1b[C", ui.KeyLeft: "\x1b[D", ui.KeyHome: "\x1b[H", ui.KeyEnd: "\x1b[F", + ui.KeyDelete: "\x1b[3~", ui.KeyPageUp: "\x1b[5~", ui.KeyPageDown: "\x1b[6~"} + +// terminalCell size and baseline for the code font. +func terminalMetrics(f ui.Font) (cw, ch, base float32) { + m := f.Metrics() + g := ui.Shape("M", f) + cw = f.Size * 0.6 + if len(g) > 0 { + cw = g[0].Advance + } + ch = float32(math.Ceil(float64((m.Ascent + m.Descent + m.LineGap) * 1.15))) + return cw, ch, (ch-m.Ascent-m.Descent)/2 + m.Ascent +} + +// Terminal draws a TerminalState and sends input to the PTY. +func Terminal(c *ui.Context, s *TerminalState, opts TerminalOptions) *ui.Element { + if s == nil { + panic("mujica: Terminal needs a TerminalState") + } + if opts.Scrollback < 0 { + panic("mujica: Terminal Scrollback is negative") + } + s.init() + if opts.Scrollback > 0 { + s.limit = opts.Scrollback + } + k := Tokens(c) + font := codeFont(c) + cw, ch, base := terminalMetrics(font) + label := opts.Label + if label == "" { + label = msg(c, "terminal.label") + } + e := ui.Box(c).Focusable().Cursor(ui.CursorText).Clip().Label(label).Background(k.Surface). + Border(theme.BorderWidth, k.Border).Radius(theme.ControlRadius).Disabled(opts.Disabled).MinHeight(ch * 4) + b := e.Bounds() + if b.W > 0 { + cols := max(int((b.W-2*terminalPad)/cw), 1) + rows := max(int((b.H-2*terminalPad)/ch), 1) + if cols != s.cols || rows != s.rows { + s.Resize(cols, rows) + slog.Debug("mujica: terminal resize", "cols", cols, "rows", rows) + if opts.Resized != nil { + opts.Resized(cols, rows) + } + } + } + send := func(data string) { + s.offset, s.selOn, s.preedit = 0, false, "" + if opts.Input != nil { + opts.Input(data) + } + } + cell := func(x, y float32) TerminalPos { + col := max(0, min(int((x-terminalPad)/cw), s.cols-1)) + row := max(0, min(int((y-terminalPad)/ch), s.rows-1)) + return TerminalPos{Line: len(s.back) - s.offset + row, Col: col} + } + anchor := ui.Local(e, "terminal.anchor", func() TerminalPos { return TerminalPos{} }) + e.HandleInput(func(ev ui.InputEvent) bool { + if e.IsDisabled() { + return false + } + switch ev.Kind { + case ui.InputKeyDown: + return terminalKey(c, s, ev, send) + case ui.InputText: + send(ev.Text) + return true + case ui.InputCompose: + s.preedit = ev.Text + return true + case ui.InputScroll: + s.scrolled -= ev.DY / ch + n := int(s.scrolled) + s.scrolled -= float32(n) + s.ScrollBack(n) + return true + case ui.InputPointerDown: + if ev.Button != 0 { + return false + } + e.Focus() + *anchor = cell(ev.X, ev.Y) + s.selOn = false + return true + case ui.InputPointerMove: + if e.Pressed() { + s.Select(*anchor, cell(ev.X, ev.Y)) + } + return true + } + return false + }) + cy := float32(s.cy+s.offset) * ch + e.TextCaret(ui.Rect{X: terminalPad + float32(s.cx)*cw, Y: terminalPad + cy, W: cw, H: ch}) + focused := e.Focused() + internal.Ring(e, k.Focus, theme.ControlRadius) + e.Draw(func(p *ui.Painter, r ui.Rect) { + if r.W != b.W || r.H != b.H { + // Laid out at another size: rebuild to fit the grid. + c.Invalidate() + } + terminalPaint(p, r, s, k, font, cw, ch, base, focused) + }) + return e +} + +// terminalKey encodes a key, handling copy, paste and scrollback. +func terminalKey(c *ui.Context, s *TerminalState, ev ui.InputEvent, send func(string)) bool { + clip := ui.Super + if runtime.GOOS != "darwin" { + clip = ui.Ctrl | ui.Shift + } + switch { + case ev.Mods == clip && ev.Key == ui.KeyC: + if t := s.Selection(); t != "" { + c.WriteClipboard(t) + slog.Debug("mujica: terminal copy", "runes", len([]rune(t))) + } + return true + case ev.Mods == clip && ev.Key == ui.KeyV: + if t := c.ReadClipboard(); t != "" { + send(t) + } + return true + case ev.Mods == ui.Shift && (ev.Key == ui.KeyPageUp || ev.Key == ui.KeyPageDown): + n := s.rows - 1 + if ev.Key == ui.KeyPageDown { + n = -n + } + s.ScrollBack(n) + return true + case ev.Mods == ui.Ctrl && terminalCtrl[ev.Key] != 0: + send(string([]byte{terminalCtrl[ev.Key]})) + return true + } + if seq, ok := terminalKeys[ev.Key]; ok && ev.Mods&^ui.Shift == 0 { + if ev.Key == ui.KeyTab && ev.Mods == ui.Shift { + seq = "\x1b[Z" + } + send(seq) + return true + } + return false +} + +// terminalPaint draws the visible rows, marks and cursor. +func terminalPaint(p *ui.Painter, r ui.Rect, s *TerminalState, k theme.Tokens, font ui.Font, cw, ch, base float32, focused bool) { + ox, oy := r.X+terminalPad, r.Y+terminalPad + top := len(s.back) - s.offset + for row := range s.rows { + li := top + row + cells := s.line(li) + y := oy + float32(row)*ch + for col := 0; col < len(cells); { + st := cells[col].st + end := col + 1 + for end < len(cells) && cells[end].st == st { + end++ + } + fg, bg := codeAnsiColor(k, st.FG), codeAnsiColor(k, st.BG) + if fg.A == 0 { + fg = k.Text + } + if st.Inverse { + if bg.A == 0 { + bg = k.Surface + } + fg, bg = bg, fg + } + if st.Dim { + fg = fg.Alpha(0.6) + } + x0 := ox + float32(col)*cw + if bg.A > 0 { + p.Fill(ui.Rect{X: x0, Y: y, W: float32(end-col) * cw, H: ch}, bg, 0) + } + for i := col; i < end; i++ { + if s.selected(TerminalPos{li, i}) { + p.Fill(ui.Rect{X: ox + float32(i)*cw, Y: y, W: cw, H: ch}, k.Selection, 0) + } + } + terminalRun(p, s, cells[col:end], st, font, x0, y+base, cw, fg) + if st.Underline || st.Strike { + ly := y + base + 2 + if st.Strike { + ly = y + ch/2 + } + codeRule(p, x0, x0+float32(end-col)*cw, ly, fg) + } + col = end + } + for mi, m := range s.matches { + if m.pos.Line != li { + continue + } + mr := ui.Rect{X: ox + float32(m.pos.Col)*cw, Y: y, W: float32(m.n) * cw, H: ch} + codeRule(p, mr.X, mr.X+mr.W, y+ch-1, k.AccentText) + if mi == s.current { + p.Stroke(mr, k.AccentText, 1, 1.5) + } + } + } + if s.offset > 0 || s.cy >= s.rows { + return + } + cr := ui.Rect{X: ox + float32(s.cx)*cw, Y: oy + float32(s.cy)*ch, W: cw, H: ch} + if s.cx >= s.cols { + cr.X = ox + float32(s.cols-1)*cw + } + if s.preedit != "" { + g := terminalShape(s, s.preedit, font) + p.Glyphs(g, cr.X, cr.Y+base, k.AccentText) + codeRule(p, cr.X, cr.X+float32(len([]rune(s.preedit)))*cw, cr.Y+ch-1, k.AccentText) + return + } + if !focused { + p.Stroke(cr, k.AccentText, 0, 1) + return + } + p.Fill(cr, k.AccentText, 0) + if c := min(s.cx, s.cols-1); s.screen[s.cy][c].r > ' ' { + p.Glyphs(terminalShape(s, string(s.screen[s.cy][c].r), font), cr.X, cr.Y+base, k.Surface) + } +} + +// terminalRun draws same-style cells, one glyph per cell. +func terminalRun(p *ui.Painter, s *TerminalState, cells []terminalCell, st internal.AnsiStyle, font ui.Font, x, y, cw float32, fg ui.Color) { + runes := make([]rune, len(cells)) + blank := true + for i, cell := range cells { + runes[i] = cell.r + if cell.r == 0 { + runes[i] = ' ' + } else if cell.r != ' ' { + blank = false + } + } + if blank { + return + } + if st.Bold { + font.Weight = 700 + } + font.Italic = st.Italic + p.Glyphs(terminalShape(s, string(runes), font), x, y, fg) +} + +// terminalShape shapes text into cells, cached across frames. +func terminalShape(s *TerminalState, text string, f ui.Font) []ui.Glyph { + key := string(rune(f.Weight)) + string(rune(int(f.Size*4))) + text + if f.Italic { + key = "i" + key + } + if g, ok := s.shaped[key]; ok { + return g + } + if s.shaped == nil || len(s.shaped) > 4096 { + s.shaped = map[string][]ui.Glyph{} + } + g := ui.Shape(text, f) + cw := f.Size * 0.6 + if m := ui.Shape("M", ui.Font{Family: f.Family, Size: f.Size}); len(m) > 0 { + cw = m[0].Advance + } + for i := range g { + g[i].X = float32(g[i].Cluster) * cw + } + s.shaped[key] = g + return g +} diff --git a/terminal_search.go b/terminal_search.go new file mode 100644 index 0000000..c7bba0a --- /dev/null +++ b/terminal_search.go @@ -0,0 +1,77 @@ +package mujica + +import ( + "log/slog" + "strings" + + "github.com/egoist/mygo/ui" +) + +// TerminalSearchState is a search through a terminal's scrollback and screen. +type TerminalSearchState struct { + Query string + // Current is the chosen match, from 0. + Current int + query string + version int + ran bool +} + +// TerminalSearchOptions configure a TerminalSearch. +type TerminalSearchOptions struct { + Disabled bool +} + +// TerminalSearch finds Query in term and shows the current match. +func TerminalSearch(c *ui.Context, term *TerminalState, s *TerminalSearchState, opts TerminalSearchOptions) ChoiceResult { + if term == nil || s == nil { + panic("mujica: TerminalSearch needs a terminal and a state") + } + term.init() + fresh := terminalSearchRun(term, s) + r := SearchInput(c, &s.Query, SearchInputOptions{Find: true, Matches: len(term.matches), Current: &s.Current, + Disabled: opts.Disabled, Label: msg(c, "terminal_search.label")}) + var res ChoiceResult + res.Element = r.Element + if r.Changed() { + s.Current = 0 + terminalSearchRun(term, s) + res.changed = true + } + if r.Moved() { + res.changed = true + } + if res.changed || fresh { + term.current = s.Current + if s.Current < len(term.matches) { + term.reveal(term.matches[s.Current].pos.Line) + } + slog.Debug("mujica: terminal_search move", "matches", len(term.matches), "current", s.Current) + } + return res +} + +// terminalSearchRun refinds matches; it reports a new query. +func terminalSearchRun(term *TerminalState, s *TerminalSearchState) bool { + if s.ran && s.query == s.Query && s.version == term.version { + return false + } + fresh := !s.ran || s.query != s.Query + s.ran, s.query, s.version = true, s.Query, term.version + term.matches = term.matches[:0] + q := []rune(strings.ToLower(s.Query)) + if len(q) > 0 { + for i := range term.Lines() { + line := []rune(strings.ToLower(term.LineText(i))) + for col := 0; col+len(q) <= len(line); col++ { + if string(line[col:col+len(q)]) == string(q) { + term.matches = append(term.matches, terminalMatch{TerminalPos{i, col}, len(q)}) + col += len(q) - 1 + } + } + } + } + s.Current = max(0, min(s.Current, len(term.matches)-1)) + term.current = s.Current + return fresh +} diff --git a/terminal_search_test.go b/terminal_search_test.go new file mode 100644 index 0000000..584e293 --- /dev/null +++ b/terminal_search_test.go @@ -0,0 +1,105 @@ +package mujica + +import ( + "fmt" + "testing" + + "github.com/egoist/mygo/ui" +) + +func terminalSearchTester(term *TerminalState, s *TerminalSearchState, opts TerminalSearchOptions, moves *int) *ui.Tester { + return ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + ui.Column(c).Children(func() { + if TerminalSearch(c, term, s, opts).Changed() { + *moves++ + } + Terminal(c, term, TerminalOptions{}).Size(400, 120) + }) + }, 420, 220) +} + +func terminalSearchFill() *TerminalState { + term := &TerminalState{} + term.Resize(40, 5) + for i := range 30 { + if i%10 == 3 { + term.Write(fmt.Sprintf("step %d: Error found\r\n", i)) + } else { + term.Write(fmt.Sprintf("step %d: ok\r\n", i)) + } + } + return term +} + +func TestTerminalSearchCountsAndJumps(t *testing.T) { + term := terminalSearchFill() + s := &TerminalSearchState{} + moves := 0 + tt := terminalSearchTester(term, s, TerminalSearchOptions{}, &moves) + tt.Key(0, ui.KeyTab) + tt.Type("error") + if len(term.matches) != 3 || !tt.HasText("1 of 3") { + t.Fatalf("matches %d, texts %q", len(term.matches), tt.Texts()) + } + first := term.matches[0].pos.Line + top := len(term.back) - term.Offset() + if first < top || first >= top+5 { + t.Fatalf("match line %d not on screen from %d", first, top) + } + tt.Key(0, ui.KeyEnter) + if s.Current != 1 || !tt.HasText("2 of 3") { + t.Fatalf("Enter moved to %d", s.Current) + } + tt.Key(ui.Shift, ui.KeyEnter) + tt.Key(ui.Shift, ui.KeyEnter) + if s.Current != 2 { + t.Fatalf("Shift+Enter twice from 1 gave %d, want 2 (wraps)", s.Current) + } + if line := term.matches[2].pos.Line; line < len(term.back)-term.Offset() { + t.Fatalf("current match line %d above the view", line) + } + if moves < 4 { + t.Fatalf("Changed reported %d times", moves) + } +} + +func TestTerminalSearchFollowsNewOutput(t *testing.T) { + term := terminalSearchFill() + s := &TerminalSearchState{Query: "error"} + moves := 0 + tt := terminalSearchTester(term, s, TerminalSearchOptions{}, &moves) + term.Write("late error\r\n") + tt.Frame() + if len(term.matches) != 4 { + t.Fatalf("%d matches after new output, want 4", len(term.matches)) + } +} + +func TestTerminalSearchNoMatchAndDisabled(t *testing.T) { + term := terminalSearchFill() + s := &TerminalSearchState{Query: "zebra"} + moves := 0 + tt := terminalSearchTester(term, s, TerminalSearchOptions{Disabled: true}, &moves) + if !tt.HasText("No matches") || len(term.matches) != 0 { + t.Fatalf("texts %q", tt.Texts()) + } + tt.Key(0, ui.KeyTab) + tt.Type("ok") + if s.Query != "zebra" || moves != 0 { + t.Fatalf("disabled search changed to %q", s.Query) + } +} + +func TestTerminalSearchSnapshot(t *testing.T) { + snapshot(t, "terminal_search", 420, 170, func(c *ui.Context) { + st := ui.Local(c.Root(), "snap", func() [2]any { + return [2]any{terminalSearchFill(), &TerminalSearchState{Query: "error", Current: 1}} + }) + term, s := (*st)[0].(*TerminalState), (*st)[1].(*TerminalSearchState) + ui.Column(c).Padding(12).Gap(8).Children(func() { + TerminalSearch(c, term, s, TerminalSearchOptions{}) + Terminal(c, term, TerminalOptions{}).Height(96) + }) + }) +} diff --git a/terminal_state.go b/terminal_state.go new file mode 100644 index 0000000..e3930fb --- /dev/null +++ b/terminal_state.go @@ -0,0 +1,361 @@ +package mujica + +import ( + "log/slog" + "strconv" + "strings" + "unicode/utf8" + + "github.com/egoist/mygo/ui" + + "mujicaui/internal" +) + +// terminalCell is one grid character; zero is blank. +type terminalCell struct { + r rune + st internal.AnsiStyle +} + +// TerminalPos is a cell; Line counts from the oldest line. +type TerminalPos struct{ Line, Col int } + +func (a TerminalPos) before(b TerminalPos) bool { + return a.Line < b.Line || a.Line == b.Line && a.Col < b.Col +} + +// TerminalState is the screen; zero is an empty 80×24 grid. +type TerminalState struct { + cols, rows int + screen [][]terminalCell + back [][]terminalCell + cx, cy int + style internal.AnsiStyle + pending string + limit int + // offset is how far the view scrolled back. + offset int + scrolled float32 + sel [2]TerminalPos + selOn bool + matches []terminalMatch + current int + version int + shaped map[string][]ui.Glyph + preedit string +} + +type terminalMatch struct { + pos TerminalPos + n int +} + +func (s *TerminalState) init() { + if s.screen == nil { + s.Resize(80, 24) + } + if s.limit == 0 { + s.limit = 1000 + } +} + +// Size returns the grid's columns and rows. +func (s *TerminalState) Size() (cols, rows int) { + s.init() + return s.cols, s.rows +} + +// Cursor returns the cursor's column and row on the screen. +func (s *TerminalState) Cursor() (col, row int) { return s.cx, s.cy } + +// Resize sets the grid size, keeping the cursor in view. +func (s *TerminalState) Resize(cols, rows int) { + if cols < 1 || rows < 1 { + panic("mujica: TerminalState size must be at least 1×1") + } + if s.limit == 0 { + s.limit = 1000 + } + old := s.screen + // Lines above a falling-off cursor go to the scrollback. + for drop := s.cy - (rows - 1); drop > 0 && len(old) > 0; drop-- { + s.pushBack(old[0]) + old = old[1:] + s.cy-- + if s.offset > 0 { + s.offset++ + } + } + s.screen = make([][]terminalCell, rows) + for y := range rows { + s.screen[y] = make([]terminalCell, cols) + if y < len(old) { + copy(s.screen[y], old[y]) + } + } + s.cols, s.rows = cols, rows + s.cx, s.cy = min(s.cx, cols-1), min(max(s.cy, 0), rows-1) + s.version++ +} + +// Write feeds PTY output; call it on the UI thread. +func (s *TerminalState) Write(data string) { + s.init() + data = s.pending + data + s.pending = "" + for i := 0; i < len(data); { + b := data[i] + switch { + case b == 0x1b: + n, ok := s.escape(data[i:]) + if !ok { + s.pending = data[i:] + s.version++ + return + } + i += n + case b == '\r': + s.cx = 0 + i++ + case b == '\n': + s.lineFeed() + i++ + case b == '\b': + s.cx = max(s.cx-1, 0) + i++ + case b == '\t': + s.cx = min((s.cx/8+1)*8, s.cols-1) + i++ + case b < 0x20 || b == 0x7f: + i++ + default: + r, n := utf8.DecodeRuneInString(data[i:]) + s.put(r) + i += n + } + } + s.version++ +} + +func (s *TerminalState) put(r rune) { + if s.cx >= s.cols { + s.cx = 0 + s.lineFeed() + } + s.screen[s.cy][s.cx] = terminalCell{r, s.style} + s.cx++ +} + +func (s *TerminalState) lineFeed() { + if s.cy < s.rows-1 { + s.cy++ + return + } + s.pushBack(s.screen[0]) + copy(s.screen, s.screen[1:]) + s.screen[s.rows-1] = make([]terminalCell, s.cols) + if s.offset > 0 { + s.offset = min(s.offset+1, len(s.back)) + } +} + +func (s *TerminalState) pushBack(line []terminalCell) { + s.back = append(s.back, line) + if over := len(s.back) - s.limit; over > 0 { + s.back = s.back[over:] + s.offset = min(s.offset, len(s.back)) + s.selOn = false + } +} + +// escape runs the sequence at d; false means incomplete. +func (s *TerminalState) escape(d string) (int, bool) { + if len(d) < 2 { + return 0, false + } + switch d[1] { + case '[': + case ']': + // OSC ends with BEL or ST; nothing to draw. + if end := strings.IndexAny(d, "\a"); end >= 0 { + return end + 1, true + } + if end := strings.Index(d, "\x1b\\"); end >= 0 { + return end + 2, true + } + return 0, false + default: + return 2, true + } + j := 2 + for j < len(d) && (d[j] < 0x40 || d[j] > 0x7e) { + j++ + } + if j >= len(d) { + return 0, false + } + params, final := d[2:j], d[j] + if final == 'm' { + _, s.style = internal.ParseAnsi(d[:j+1], s.style) + return j + 1, true + } + s.control(params, final) + return j + 1, true +} + +// control applies a CSI cursor or erase command. +func (s *TerminalState) control(params string, final byte) { + args := strings.Split(params, ";") + arg := func(i, def int) int { + if i >= len(args) || args[i] == "" { + return def + } + v, err := strconv.Atoi(args[i]) + if err != nil { + return def + } + return v + } + switch final { + case 'A': + s.cy = max(s.cy-max(arg(0, 1), 1), 0) + case 'B': + s.cy = min(s.cy+max(arg(0, 1), 1), s.rows-1) + case 'C': + s.cx = min(s.cx+max(arg(0, 1), 1), s.cols-1) + case 'D': + s.cx = max(min(s.cx, s.cols-1)-max(arg(0, 1), 1), 0) + case 'G': + s.cx = min(max(arg(0, 1), 1), s.cols) - 1 + case 'H', 'f': + s.cy = min(max(arg(0, 1), 1), s.rows) - 1 + s.cx = min(max(arg(1, 1), 1), s.cols) - 1 + case 'J': + s.eraseDisplay(arg(0, 0)) + case 'K': + s.eraseLine(s.cy, arg(0, 0)) + default: + slog.Debug("mujica: terminal unsupported sequence", "params", params, "final", string(final)) + } +} + +func (s *TerminalState) eraseLine(y, mode int) { + line := s.screen[y] + from, to := 0, s.cols + switch mode { + case 0: + from = min(s.cx, s.cols) + case 1: + to = min(s.cx+1, s.cols) + } + clear(line[from:to]) +} + +func (s *TerminalState) eraseDisplay(mode int) { + switch mode { + case 0: + s.eraseLine(s.cy, 0) + for y := s.cy + 1; y < s.rows; y++ { + clear(s.screen[y]) + } + case 1: + s.eraseLine(s.cy, 1) + for y := range s.cy { + clear(s.screen[y]) + } + default: + for y := range s.rows { + clear(s.screen[y]) + } + if mode == 3 { + s.back, s.offset = nil, 0 + } + } +} + +// Lines counts scrollback and screen lines. +func (s *TerminalState) Lines() int { + s.init() + return len(s.back) + s.rows +} + +func (s *TerminalState) line(i int) []terminalCell { + if i < len(s.back) { + return s.back[i] + } + return s.screen[i-len(s.back)] +} + +// LineText returns line i without trailing blanks. +func (s *TerminalState) LineText(i int) string { + var b strings.Builder + for _, cell := range s.line(i) { + r := cell.r + if r == 0 { + r = ' ' + } + b.WriteRune(r) + } + return strings.TrimRight(b.String(), " ") +} + +// Select marks the cells from a to b. +func (s *TerminalState) Select(a, b TerminalPos) { + if b.before(a) { + a, b = b, a + } + s.sel, s.selOn = [2]TerminalPos{a, b}, true +} + +// Selection returns the selected text, lines joined by newlines. +func (s *TerminalState) Selection() string { + if !s.selOn { + return "" + } + a, b := s.sel[0], s.sel[1] + var out []string + for y := a.Line; y <= b.Line && y < s.Lines(); y++ { + cells := s.line(y) + from, to := 0, len(cells) + if y == a.Line { + from = min(a.Col, len(cells)) + } + if y == b.Line { + to = min(b.Col+1, len(cells)) + } + var sb strings.Builder + for _, cell := range cells[from:max(to, from)] { + r := cell.r + if r == 0 { + r = ' ' + } + sb.WriteRune(r) + } + out = append(out, strings.TrimRight(sb.String(), " ")) + } + return strings.Join(out, "\n") +} + +func (s *TerminalState) selected(p TerminalPos) bool { + return s.selOn && !p.before(s.sel[0]) && !s.sel[1].before(p) +} + +// ScrollBack moves the view n lines up; negative goes down. +func (s *TerminalState) ScrollBack(n int) { + s.init() + s.offset = max(0, min(s.offset+n, len(s.back))) +} + +// Offset returns how many lines the view is scrolled back. +func (s *TerminalState) Offset() int { return s.offset } + +// reveal scrolls so that line shows on screen. +func (s *TerminalState) reveal(line int) { + top := len(s.back) - s.offset + switch { + case line < top: + s.offset = len(s.back) - line + case line >= top+s.rows: + s.offset = max(0, len(s.back)-(line-s.rows+1)) + } +} diff --git a/terminal_tabs.go b/terminal_tabs.go new file mode 100644 index 0000000..fbc4c87 --- /dev/null +++ b/terminal_tabs.go @@ -0,0 +1,181 @@ +package mujica + +import ( + "fmt" + "log/slog" + "slices" + "strings" + + "github.com/egoist/mygo/ui" + + "mujicaui/icons" + "mujicaui/internal" + "mujicaui/theme" +) + +// TerminalTab is one terminal of a TerminalTabs. +type TerminalTab struct { + ID, Title string + // Busy asks for confirmation before closing. + Busy bool +} + +// TerminalTabsOptions configure TerminalTabs. +type TerminalTabsOptions struct { + Disabled bool +} + +// TerminalTabsResult is TerminalTabs and what happened in this frame. +type TerminalTabsResult struct { + Element *ui.Element + // Renamed and Closed hold a tab ID. + Changed bool + Renamed string + Closed string +} + +type terminalTabsState struct { + editing, draft, closing string + confirm bool +} + +// TerminalTabs shows renamable, closable terminal tabs. +func TerminalTabs(c *ui.Context, tabs *[]TerminalTab, active *string, opts TerminalTabsOptions, content func(id string)) TerminalTabsResult { + if tabs == nil || active == nil || content == nil { + panic("mujica: TerminalTabs needs tabs, active and content") + } + at := slices.IndexFunc(*tabs, func(t TerminalTab) bool { return t.ID == *active }) + if len(*tabs) > 0 && at < 0 { + panic("mujica: TerminalTabs active is not a tab ID") + } + k := Tokens(c) + u := Density(c).Unit() + var res TerminalTabsResult + res.Element = ui.Column(c).MinHeight(0).Disabled(opts.Disabled) + st := ui.Local(res.Element, "terminal_tabs", func() terminalTabsState { return terminalTabsState{} }) + disabled := res.Element.IsDisabled() + if disabled { + st.editing, st.confirm = "", false + } + remove := func(id string) { + i := slices.IndexFunc(*tabs, func(t TerminalTab) bool { return t.ID == id }) + if i < 0 { + return + } + *tabs = slices.Delete(*tabs, i, i+1) + if *active == id && len(*tabs) > 0 { + *active = (*tabs)[min(i, len(*tabs)-1)].ID + res.Changed = true + } + res.Closed = id + slog.Debug("mujica: terminal_tabs close", "id", id) + } + askClose := func(t TerminalTab) { + if t.Busy { + st.closing, st.confirm = t.ID, true + return + } + remove(t.ID) + } + res.Element.Children(func() { + strip := ui.ScrollHorizontal(c).Shrink(0).MinWidth(0).Padding(theme.FocusGap+theme.FocusWidth, theme.FocusGap+theme.FocusWidth, 0, theme.FocusGap+theme.FocusWidth) + strip.Draw(func(p *ui.Painter, r ui.Rect) { p.Fill(ui.Rect{X: r.X, Y: r.Y + r.H - 1, W: r.W, H: 1}, k.Border, 0) }) + strip.Children(func() { + idx := max(at, 0) + base := ui.TabsBase(c, &idx, len(*tabs)) + base.List.Gap(u).Label(msg(c, "terminal_tabs.label")) + base.List.Children(func() { + for i, t := range *tabs { + rename := func(title string) { + (*tabs)[i].Title = title + res.Renamed = t.ID + slog.Debug("mujica: terminal_tabs rename", "id", t.ID) + } + terminalTabsTab(c, base.Tab(i), t, i == at, st, rename, askClose, disabled) + } + }) + if base.List.Changed() && !disabled { + *active = (*tabs)[idx].ID + res.Changed = true + slog.Debug("mujica: terminal_tabs select", "id", *active) + } + }) + if len(*tabs) == 0 { + codeCaption(c, msg(c, "terminal_tabs.empty")).Padding(u * 4) + return + } + ui.Column(c).Grow(1).MinHeight(0).Padding(u*2, 0, 0, 0).Children(func() { content(*active) }) + }) + if st.confirm { + title := "" + for _, t := range *tabs { + if t.ID == st.closing { + title = t.Title + } + } + r := AlertDialog(c, &st.confirm, AlertDialogOptions{Title: fmt.Sprintf(msg(c, "terminal_tabs.confirm"), title), + Description: msg(c, "terminal_tabs.busy"), ConfirmLabel: msg(c, "terminal_tabs.close")}) + if r.Confirmed && !disabled { + remove(st.closing) + } + } + return res +} + +// terminalTabsTab builds one tab with title and close. +func terminalTabsTab(c *ui.Context, tab *ui.Element, t TerminalTab, on bool, st *terminalTabsState, rename func(string), askClose func(TerminalTab), disabled bool) { + k := Tokens(c) + d := Density(c) + u := d.Unit() + tab.MinHeight(d.ControlHeight()).Padding(0, u, 0, u*3).Gap(u).AlignItems(ui.Center).Radius(theme.ControlRadius, theme.ControlRadius, 0, 0).Label(t.Title) + if t.Busy { + tab.Description(msg(c, "terminal_tabs.running")) + } + fg := k.TextMuted + switch { + case on: + tab.Background(k.Selection).FontWeight(600) + fg = k.Text + case tab.Hovered(): + tab.Background(k.SurfaceHover) + fg = k.Text + } + internal.Ease(tab, Motion(c, theme.StateDuration), fg) + tab.FocusRing(false).DrawOver(func(p *ui.Painter, r ui.Rect) { + if on { + p.Fill(ui.Rect{X: r.X, Y: r.Y + r.H - 2, W: r.W, H: 2}, k.AccentText, 0) + } + if tab.FocusVisible() { + internal.FocusRing(p, r, theme.ControlRadius, k.Focus) + } + }) + if !disabled && st.editing == "" && (tab.DoubleClicked() || tab.Shortcut(0, ui.KeyF2)) { + st.editing, st.draft = t.ID, t.Title + } + if !disabled && tab.Shortcut(0, ui.KeyDelete) { + askClose(t) + } + tab.Children(func() { + if st.editing == t.ID { + in := TextInput(c, &st.draft, TextInputOptions{Label: msg(c, "terminal_tabs.rename")}) + in.Element.Width(u * 36) + in.Input.AutoFocus() + switch { + case in.Input.Shortcut(0, ui.KeyEscape): + st.editing = "" + case in.Submitted() && strings.TrimSpace(st.draft) != "": + rename(strings.TrimSpace(st.draft)) + st.editing = "" + tab.Focus() + } + return + } + ui.Text(c, t.Title).SingleLine() + if t.Busy { + ui.Box(c).Size(6, 6).Shrink(0).Role(ui.RoleNone).Background(k.AccentText).Radius(3) + } + if iconAction(c, icons.Must("x"), fmt.Sprintf(msg(c, "terminal_tabs.closeTab"), t.Title)).Clicked() && !disabled { + askClose(t) + } + }) +} diff --git a/terminal_tabs_test.go b/terminal_tabs_test.go new file mode 100644 index 0000000..e3964c9 --- /dev/null +++ b/terminal_tabs_test.go @@ -0,0 +1,122 @@ +package mujica + +import ( + "testing" + + "github.com/egoist/mygo/ui" +) + +type terminalTabsProbe struct { + tabs []TerminalTab + active string + disabled bool + changed int + renamed, closed []string +} + +func (p *terminalTabsProbe) view(c *ui.Context) { + Use(c, Settings{}) + r := TerminalTabs(c, &p.tabs, &p.active, TerminalTabsOptions{Disabled: p.disabled}, func(id string) { + ui.Text(c, "Content of "+id) + }) + if r.Changed { + p.changed++ + } + if r.Renamed != "" { + p.renamed = append(p.renamed, r.Renamed) + } + if r.Closed != "" { + p.closed = append(p.closed, r.Closed) + } +} + +func newTerminalTabsProbe() *terminalTabsProbe { + return &terminalTabsProbe{tabs: []TerminalTab{{ID: "a", Title: "zsh"}, {ID: "b", Title: "build", Busy: true}, {ID: "c", Title: "logs"}}, active: "a"} +} + +func TestTerminalTabsSwitch(t *testing.T) { + p := newTerminalTabsProbe() + tt := ui.NewTester(p.view, 600, 400) + if err := tt.Click("logs"); err != nil { + t.Fatal(err) + } + if p.active != "c" || !tt.HasText("Content of c") || p.changed != 1 { + t.Fatalf("active %q changed %d", p.active, p.changed) + } + tt.Key(0, ui.KeyLeft) + if p.active != "b" { + t.Fatalf("Left gave %q", p.active) + } +} + +func TestTerminalTabsRename(t *testing.T) { + p := newTerminalTabsProbe() + tt := ui.NewTester(p.view, 600, 400) + tt.Key(0, ui.KeyTab) + tt.Key(0, ui.KeyF2) + tt.Key(ui.Cmd, ui.KeyA) + tt.Type("server") + tt.Key(0, ui.KeyEnter) + if p.tabs[0].Title != "server" || len(p.renamed) != 1 || p.renamed[0] != "a" { + t.Fatalf("title %q renamed %v", p.tabs[0].Title, p.renamed) + } + // Escape cancels a rename. + tt.Key(0, ui.KeyF2) + tt.Type("x") + tt.Key(0, ui.KeyEscape) + if p.tabs[0].Title != "server" || !tt.HasText("server") { + t.Fatalf("Escape kept %q", p.tabs[0].Title) + } +} + +func TestTerminalTabsCloseIdleAndBusy(t *testing.T) { + p := newTerminalTabsProbe() + tt := ui.NewTester(p.view, 600, 400) + if err := tt.Click("Close zsh"); err != nil { + t.Fatal(err) + } + if len(p.tabs) != 2 || p.active != "b" || p.closed[0] != "a" { + t.Fatalf("after closing idle: tabs %v active %q", p.tabs, p.active) + } + if err := tt.Click("Close build"); err != nil { + t.Fatal(err) + } + if len(p.tabs) != 2 || !tt.HasText("Close “build”?") { + t.Fatalf("busy tab closed without asking: %v", p.tabs) + } + if err := tt.Click("Cancel"); err != nil { + t.Fatal(err) + } + if len(p.tabs) != 2 { + t.Fatal("Cancel closed the tab") + } + _ = tt.Click("Close build") + if err := tt.Click("Close"); err != nil { + t.Fatal(err) + } + if len(p.tabs) != 1 || p.active != "c" || len(p.closed) != 2 { + t.Fatalf("after confirm: tabs %v active %q closed %v", p.tabs, p.active, p.closed) + } +} + +func TestTerminalTabsDisabled(t *testing.T) { + p := newTerminalTabsProbe() + p.disabled = true + tt := ui.NewTester(p.view, 600, 400) + _ = tt.Click("logs") + _ = tt.Click("Close zsh") + if p.active != "a" || len(p.tabs) != 3 { + t.Fatalf("disabled tabs changed: active %q tabs %d", p.active, len(p.tabs)) + } +} + +func TestTerminalTabsSnapshot(t *testing.T) { + snapshot(t, "terminal_tabs", 460, 120, func(c *ui.Context) { + p := ui.Local(c.Root(), "snap", newTerminalTabsProbe) + ui.Column(c).Padding(12).Children(func() { + TerminalTabs(c, &(*p).tabs, &(*p).active, TerminalTabsOptions{}, func(id string) { + codeMono(c, "~/court $ make seal") + }) + }) + }) +} diff --git a/terminal_test.go b/terminal_test.go new file mode 100644 index 0000000..88cad6b --- /dev/null +++ b/terminal_test.go @@ -0,0 +1,192 @@ +package mujica + +import ( + "runtime" + "strings" + "testing" + + "github.com/egoist/mygo/ui" +) + +func TestTerminalStateText(t *testing.T) { + var s TerminalState + s.Resize(10, 3) + s.Write("hello\r\nwor") + s.Write("ld\x1b[31m!\x1b[0m") + if got := s.LineText(0); got != "hello" { + t.Fatalf("line 0 %q", got) + } + if got := s.LineText(1); got != "world!" { + t.Fatalf("line 1 %q", got) + } + if st := s.screen[1][5].st; st.FG.Index != 1 || st.FG.Kind == 0 { + t.Fatalf("'!' style %+v, want red", st) + } + if st := s.screen[1][4].st; st.FG.Kind != 0 { + t.Fatalf("'d' style %+v, want default", st) + } +} + +func TestTerminalStateCursorAndErase(t *testing.T) { + var s TerminalState + s.Resize(10, 3) + s.Write("abcdef\x1b[3D") + if x, _ := s.Cursor(); x != 3 { + t.Fatalf("cursor after 3D at %d, want 3", x) + } + s.Write("\x1b[K") + if got := s.LineText(0); got != "abc" { + t.Fatalf("after erase-to-end %q", got) + } + s.Write("\x1b[2;4HX") + if got := s.LineText(1); got != " X" { + t.Fatalf("after CUP %q", got) + } + s.Write("\x1b[2J") + if s.LineText(0) != "" || s.LineText(1) != "" { + t.Fatal("2J left text") + } + // A sequence split across writes completes on the next one. + s.Write("\x1b[") + s.Write("1;1Hz") + if got := s.LineText(0); got != "z" { + t.Fatalf("split CUP gave %q", got) + } +} + +func TestTerminalStateScrollback(t *testing.T) { + var s TerminalState + s.Resize(8, 2) + for _, l := range []string{"one", "two", "three", "four"} { + s.Write(l + "\r\n") + } + if s.Lines() != 5 || s.LineText(0) != "one" || s.LineText(3) != "four" { + t.Fatalf("lines %d: %q %q", s.Lines(), s.LineText(0), s.LineText(3)) + } + s.ScrollBack(10) + if s.Offset() != 3 { + t.Fatalf("offset %d, want the 3 scrollback lines", s.Offset()) + } + s.ScrollBack(-1) + if s.Offset() != 2 { + t.Fatalf("offset %d after scrolling down one", s.Offset()) + } +} + +func TestTerminalStateWrapsLongLines(t *testing.T) { + var s TerminalState + s.Resize(4, 3) + s.Write("abcdefg") + if s.LineText(0) != "abcd" || s.LineText(1) != "efg" { + t.Fatalf("wrapped %q %q", s.LineText(0), s.LineText(1)) + } +} + +func terminalTester(s *TerminalState, opts *TerminalOptions, typed *[]string) *ui.Tester { + opts.Input = func(d string) { *typed = append(*typed, d) } + return ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + Terminal(c, s, *opts).Size(400, 200) + }, 420, 220) +} + +func TestTerminalInputAndResize(t *testing.T) { + var s TerminalState + var typed []string + var size [2]int + opts := TerminalOptions{Resized: func(cols, rows int) { size = [2]int{cols, rows} }} + tt := terminalTester(&s, &opts, &typed) + if size[0] < 30 || size[1] < 5 { + t.Fatalf("resize reported %v for a 400×200 view", size) + } + if cols, rows := s.Size(); cols != size[0] || rows != size[1] { + t.Fatalf("state %d×%d, reported %v", cols, rows, size) + } + tt.Key(0, ui.KeyTab) + if !tt.Focused("Terminal") { + t.Fatal("Tab did not focus the terminal") + } + tt.Type("ls") + tt.Key(0, ui.KeyEnter) + tt.Key(ui.Ctrl, ui.KeyC) + tt.Key(0, ui.KeyUp) + want := []string{"ls", "\r", "\x03", "\x1b[A"} + if strings.Join(typed, "|") != strings.Join(want, "|") { + t.Fatalf("typed %q, want %q", typed, want) + } +} + +func TestTerminalSelectCopyPaste(t *testing.T) { + var s TerminalState + var typed []string + tt := terminalTester(&s, &TerminalOptions{}, &typed) + s.Write("alpha beta") + tt.Frame() + cw, _, _ := terminalMetrics(ui.Font{Family: "monospace", Size: 13}) + y := float32(terminalPad + 5) + tt.Press(terminalPad+1+cw*0.5, y) + tt.Move(terminalPad+1+cw*4.5, y) + tt.Release(terminalPad+1+cw*4.5, y) + if got := s.Selection(); got != "alpha" { + t.Fatalf("selection %q, want alpha", got) + } + mod := ui.Super + if runtime.GOOS != "darwin" { + mod = ui.Ctrl | ui.Shift + } + tt.Key(mod, ui.KeyC) + if tt.Clipboard() != "alpha" { + t.Fatalf("clipboard %q", tt.Clipboard()) + } + tt.SetClipboard("echo hi") + tt.Key(mod, ui.KeyV) + if len(typed) != 1 || typed[0] != "echo hi" { + t.Fatalf("paste sent %q", typed) + } +} + +func TestTerminalDisabledSendsNothing(t *testing.T) { + var s TerminalState + var typed []string + tt := terminalTester(&s, &TerminalOptions{Disabled: true}, &typed) + tt.Key(0, ui.KeyTab) + tt.Type("x") + tt.Key(0, ui.KeyEnter) + if len(typed) != 0 { + t.Fatalf("disabled terminal sent %q", typed) + } +} + +func TestTerminalScrollbackKeys(t *testing.T) { + var s TerminalState + var typed []string + tt := terminalTester(&s, &TerminalOptions{}, &typed) + for range 40 { + s.Write("line\r\n") + } + tt.Frame() + tt.Key(0, ui.KeyTab) + tt.Key(ui.Shift, ui.KeyPageUp) + if s.Offset() == 0 { + t.Fatal("Shift+PageUp did not scroll back") + } + tt.Type("q") + if s.Offset() != 0 { + t.Fatal("typing did not return to the bottom") + } +} + +func TestTerminalSnapshot(t *testing.T) { + snapshot(t, "terminal", 420, 160, func(c *ui.Context) { + s := ui.Local(c.Root(), "snap", func() *TerminalState { + s := &TerminalState{} + s.Resize(50, 7) + s.Write("\x1b[1m~/court\x1b[0m $ ls\r\n\x1b[34mchronicles\x1b[0m decrees.txt \x1b[32mrun.sh\x1b[0m\r\n") + s.Write("\x1b[33mwarning:\x1b[0m ledger is \x1b[7mlocked\x1b[0m\r\n\x1b[31merror\x1b[0m: missing seal\r\n\x1b[1m~/court\x1b[0m $ ") + return s + }) + ui.Column(c).Padding(12).Children(func() { + Terminal(c, *s, TerminalOptions{}).Height(136) + }) + }) +} diff --git a/testdata/snapshots/darwin/breakpoint_list-dark.png b/testdata/snapshots/darwin/breakpoint_list-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..e11b0a072a9b2618d99c6334f9485f9c0ffe8a3b GIT binary patch literal 8797 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 OKv`Zxu1>}(>VE*6OZ*7{ literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/breakpoint_list-light.png b/testdata/snapshots/darwin/breakpoint_list-light.png new file mode 100644 index 0000000000000000000000000000000000000000..43ac81e00eb452f2a1a0edb9a933ec234f859585 GIT binary patch literal 8592 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

Y$wvxD3s$BI#Q#$da)tkr%aqqA-P#rJy3MBmRc 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 XlFu1uYz(L^Bs4jxPm)z2~ zWZ;oo?bYn*nb+CuyP7+rX)y2;6y+2dDJ)^H@&z3|f$Tgq6d(gXnQS2crdfH4K4Yg%Zw=kPb#G(>;wQB!<~F5Z20W?8+8flzpLPv*JuWb%^w+3 zD^;bDsEIVe=?F&`eLrBpiXOIibabk9vMI3(0V|d&FSK%b;dVeiiNd4&%qe9fF4YQZ zI9}w^?2jTLED3Q>aIBD#f`@RpN9%maMZp7}7&iJ1oMCn@7B>44;wZoVLH+#s^NtVk z|90a)|Bf@sFaevp!~WlBphm3=c?KqUoH>ho!y5?0FXIDOCRf#Dtm1PcEvQ3dk|PJ) zx2At;Yf`1o;5PW#F46L@>7;qMxs@WpllmbY3`xp+cU=R1iu2#M(zG|TlZrEW8D_SO zy$%Ka3Lh>Rrcd&GyL~0p<9^+X$M=^)E|((s)G1?9s74(=y?n2bi^E0BkTQ{HP`y~j zps4S|#2&#!5(>M)l4<{zvwY^rR;*lsK+aFw#zb-j#S zsQhr;AI6r+!46EXOFYC)M7VURjEI{G8~Qt{_Bedzpj%tl(n7|>HDcOW5}3BIwAAFO zkW{X!qN>^hd5u5YaBy~U2|Q?XQpzN#Q>QzdTPQ8h=W{-!A*HY|yIN{2=6Z*K_xHJ0 zcQCNi2L>fG(NCQYJEBj+bNC5XuPx*XRb!(#H6$}e_iIzhbHV|%J4qIw5KB>kTzQ> zqM@Mf`i!7lG;0Xz8a1hLj&2zM8{&mXDn#O)}N2gmPw?fX>3&1`q9(6jNxoHA28t| zORnyOhC=Hw^owm_eIZgc(9l!SQOHLmg`1RMGRp5NYE^LK->Er|+$w&ROw-cTSgTXi z1^o?bFcFp^ks=;|< zyxFxbaFm6Ac;d4(_*Imx*=;s1Up_KEI`4~*Q3ssT4eUbJZpxzRx9Tc81X38Ss3IeS zn~i-m1X7V7W(cI*a*n5~?-JWH^Os0TY>F0NM5*NA%Z#3skAr$GEa(2HmDk>9a?&R; zk?%gl-r>`JiM~I3ezBd7?S4kPxyh*Yhleig6Tv-b8l`j0AMRM1n&W4h+8mVHYFkvw z;my}_zSSOF{`VV@cR+t;qoJX;IyYlGd-V4*=16`pCJsWZnuu z&U2&6iGkfTCL7_m)fi`vg3OinR{J;Ay_41)27f1-wHdIRt`GXhROv8p;6>x|+sajo z=RGz%-Obo;TR=87+02`! z+~oDbB-TctB|W--TF0o=>H53_4Sn_0aVI~<*-a0c1zV>5rd`OF^dslWGnfu_tLG`} z);cEFLgl|MIl?3j-LfLp$Nzi+&)-70M@NWX9y<;SvR^e&5WV7d_-=AP&OFY!IW`Ex z9*b_*)YsKet3IBxIxs{kSCz8i+8qMzNWU?$=0UT z!Tl}M_kw#gy{FTW5zN9ZJHoWT8kDZBqtWay6FfIuS=VE-*~hV(c4%rSazmfUR(W&t zXunD<+Vzh8<=I{Zt%T6eZh1Rpv(^NsOk=j^*(iTQ$O_ss{&(CC&oS5ChShQGhT*Yk zy~l{#f)l~ws7-4}Z$OvT=}}4K1-e1Ro9tHW&o6~{%q{ErJ5T&^r(byD{QA|%c7N?* z>W}wc#m>phy#9`^TWRfZ0D47>lZBIlHg%TWI8%f++zuzCNG1CUz1F5 z@(#3W{nD9|cZKZBxKCScR@%;2$P**OKAE)j2|p1KO&QM_Rtd@p*~>aF#a3NKq!G~n z5GF|n9^dAMyf1zxZK1RXSSanP$?@;FaK{%Kc|YEEIy?#EZ$5ti=VMrYT)-YWRj|@? z{H?e$Vj^7j_e2z-=pjPc`AHFXgz_gg==Wllk5AqW;J;M`{7+56J=gfm>~{c#J<$4k z$=)1hVFJ-eAd`TRKriJ+lE9Fds`yFSci7xzr4)0yhif*5L|oiRNh6t`5n5IhOjBek zg&|_|Nfr82M~efA$LM=A16YU-$22KIaN_(SG1Ki}qdh3hN-!qSgVbKxGN z`{O8%HZ~4+c4){MhkKAF|2=B;t@EjkSE)IkDyTQXI;OZ3v@qXUOg;e?>i4boXf2wd z0R*L7irOvedfemeebhCKCpkukOCg8B{XC6bQEUy=3sB zdV0;}WFmjLBi}f8&JWbo^^fpb7KJX>kt(%va|zttTPj3J;DaTLRs8B_Lf{@3oy^rW zHPuitY|(9++J;v4qDxgF_4U|E6xicP4(Er52NE~ESj&}_m5v)NE3;r7s^s-wK34Dd z@0E3dh=3#8obmiRPZy#Otk%ciR^PiLhQ0xW|NI223~f(R7y`c2LVeS$dmynMBm+qE zeUl&`4wnp6RkhV#(ibqu{h*QZGQ;SHGdnIlTtFP*I(@2i`&vhD?UHovi^Md<9ZL?f z1(vFfa+N+TCUWd z{YpYgJJ2`bX{ihV$ltiO;hGSpCuI2Ub);uEgRS+dejMyVjxbn;p9fFJ9%ij_2UqND zD`p|?aaAK%4UeWjDd_e?g=ut~tn=#PKy1;Nd;wAE)j5ts+lm{Lj0GzR*&TRRRh#|& z*Vti2R3qGf$$&vZ`7yv{pr<52*hXjIP=y>m*qFYV;6zGdDCdc3n+7l??dr0`kAiZm zCEsCTDTrfc1vY2vUG^a1+sD`o{|E)jufv_!mWJI~8-I7y&IE4% zh4Q5TgVOgUdyqQa9#ZHRsX%i!T!V&YMmpL~muDux;rfgV0sGMdWH)7Wk*UO>Jh3DS zadmWjS=cDu_8)I72=|So{@+R1C}3(~p@-jI5TKor-ISV29xvGjYTd)Y2zPok4^172 zUG)Ori*sB-GjmN_u$RMJcn4Rvv7OjUsrHD#0@>Ae=H~1SvJ(P?n4bfQ0M@oP2^2oe z^jc>yztU5idF=DzK>JRDCJDpl=}hYdcZO22u`f>d;(^$g#Mo9tXyE;Trpt2(?tN8t zJ}PdJlexL`(e9Tc@q)UE%6cbv0=o~nLzY}9$;enMPt^ z+a2S7oirF58D^hbB(&wAc)3;(NJkJsFB6N!8be^6I<_R39;yMAh5K$5vklaEPO9mC z7BI*YURt5bKm8It;DHj0sHv%bD=6b&X?k*E9dvOxDGwVmoWL%dIbdt&ls;@eM;Zd~ zY;8pWfffw1jV&w$*H*|L*DMk%WT=v3i3GNvVv!%aEm~eQNe*>^WLVKCC^wjPQug+> zM%F4c81uK6b2$^eL9MOH6pfDKGg6fCU_t$%1Ek`(ld=`bLRfjPCL;5V zNltFoyd!Q}`!`@aTC}%+pPJ)({N-Gv6xCu41J-`)a5Uchx}wL_OJh zdQ!ZwP$v_mCEdNwciRm#|oUE#UfCr7iLPPt<%!>Ns?33G?XcS?~MpeX#^SNxO1gW zVq>Yb(GE3wek2S8fQAMqx6?5B%gPEYeYJ_+TkBf{czQCgbu5S(8sO?mJo4;wb_I7n zNt;KktF7JSKKzNE{(3mjtwgm_n{0Y!rlEzS4WDyn!L$D~#_y^ODm*j0VR@v(myZL_ zA?4H=;H|Fu<|}unT=6ECIk3`U}8JbJFGHDdAr4wfZ3Opp`AO(LnH>hKXtY0dmL0E&l55 z4}P-9chZ^ML71s4d4Be|c6`q}{Qw*4`$NWOw#+dwQ2SCG6F`KO);~ZE`|$jrcvs`| zCl|Nl*C^Z6U6bm>g}TelS?2}ZB2rhM9Ze0;KJt*5i5`o{cEyUWhW zVvVjSy10V@ct0rcgS>W+A$Np^oxa0;=U}@$XQXIZ3`M}wm$%IJcA3Sp7ZDb~Sb*1K zanjZ11CMfrP}u?q)N%~@(eo{&{Hd)4G)P6^>FH_M?y@SZ-H^5H=kBuorj5y6$#Cu5*5CJVYauuIhnYO@TZcqh@ zLqacVFDJM^^6C^tRc0rLl-^aZ#btilu>Z+o#<=~hNR>KVa4fC5rJx@)!v?6`xn8%AYwbXM)vQ<{LUU2zSdmM46<26XJ@fUT1e_`L7zEN-G zG`NQSs=2cJJ@v1m$>fuE_8r@PM>+D-rT(qOu^sO8=+~$$-vHr_nVF|6Hoi>x@Dj0^ zZr&0#;UkMsnbH}fkji?nZ`7_30FgLV@$T-ybk;Xho+?q=RFUc~LGg?9o$hh3nG@y7 z)YR05{5N@es4u$ZmeEyhHa!7)^G#|w}zF5biuv$<&c$z-W(B&qoc#+%W=4r+H zi1|dB2IHs}i|W&FaX<*A0z%n6SyjXmgGvj={DA3xo$j|FgLug}bUn(XDwRxF>z zy#Xz}1a3ULNf>0k<( zpW3eOM(Rh9XmwshPU9=o3npf!-he%>J#RIM@73|gJn@(El$KI{Qv2g$F?E9U&8eTQ z;Igzck|=0*5!Aj;XlbdddBD2k0N2;aD~|I&`2@AXClyTF%x%0!ll)x#D9E{A_5OOH09;vF!LHLUC@Yjq1&E+?;MKmrp0^6Eb5ly5x0Uw1SX(=V z;o{+UD7f}|K=%7CKDRNgyXWsZ8r#DICIp!}!hJ6zc2H0G`|YfA*yt0gE}x#hF=4_$ zI=Dy=nd8pYdFbT0{SW_b0{}XZ<;I9@+x)pt->b5Yl5UAtVu=+1iTMW3)(51dM$mxG zw_oh6RFIMOYQR6%<$$v!-uH}7Jiy(+8L5ow?{E47K@R)D#*Bouhas#Go3{7%W3@Ck zBmjtbi7HhV-xj~E_BDymC;#Z=a7=MBZq3E|bYb4{n}OO_CYFqthI!(cd%Q_=wyS0& z)KFp!1o42|e3D4RXE!RokCCL9jm}|}RoG}EVI$#gZtrO6xXAl_Q;ed^-pqD=aXYs# zUq@40z>brGPq01|>w*VXwM@lKHQ;koAxmUQ&FEo&k!MsGFm>4qiQs15n44P2AiPtw zkO{l3*wRXxy=uOGuClPSxT}Hac=~Fdbz{Hp^1t#s5ZFW9eH|^&q@NRAB22^l3ll37 zBkIe05(C8?z^5&%@x-b09qMW-$ zt*n9izWpMV92}m+p-PviT873?z-tpAv}{o zzrCQNh>D60v}^KHnj{M7&;Q!QA0_xL7su3)PZ4fp*6a_=V} zef|K}x4$lSdexly^}Z7C&qrv0C1C91s+3$cy&dXVWJ)9@v4c^4Y4I(h&`JR(rbeob zTkwkB>c^m-g>VoC3d z(22pxK<@UEmJ4Ao=fV^Dzc&a4FxxV~H;pKEPfb$cNA#2@f){#sX0cQ4|TzO?k#g7Y4j#Aq_z zPA8_NE6=sIs-~%`W@c<=Zp}A?>`Ha1VGD~AO>Sm(2CAUA+icLU4`6K0CjPTD1tWQB zW5G~U{i!WJJrkLIe0=PuAP(bEM^DLD+O{m{jhwTFY1GByI=Uo7n#|P5yLc3JbQEP9 zicU-BLeOjAxti+MZJ?RkYxz@};c1u}85x}79~8zYP~}WO-7V8S@g4mN=^`0gR&eG1 z;4SWf!f3OO*!2YsCtg|nE5UX=Q0S166lmeK2OSX|LRHh>vynTfXcs@9s>SMgvtAWI zA($jQJlMv4+dCAxAsQ@Zz1mWDuRUIiw+O@{owGUe7iL6_NN!~T+L?gZLlYE>&46CI zBp4hETzv89N5=V|qF?==q$E~tLMBM=+?W(rbf4}RDJEo*qfvnm5GadcHfg*TbA>=r z-R*fdEi8;(-44c8p=HZfT{SkpQ9$u!~C1R&^U0blwWJ@zR^(OvWDgVx#f~hMR=r$Cqi? zvFaHbb|p5cz>_k?+qJC{qr)j_XCqTF|465A4BN^Org5eO$-^cq76WhRxdRq&dlm^$ zBc}8YN4%n6`?;!X&lyC)3xCjejHA%7<~OUoB+>D1^a^GVgTcY91ZD&=45}j_vlC*> zKM8K#8a8;pHj?D8wCJB*-560A@ltia()78kEpFbgZnM|XTKUN6*jiROecvSi6Nm17 zxx8b7NrBMfmAO2F-m$tCN~%7)#!*z_wjm(h$9|nRh%V`yoE_Au@|A|y?+R{2l5$Kw zQsu9Mrk-{S!#TDOpThI>>i~QqjgPs$s(yK+T4Gg3vo}{CDe5_`I5J`e0eUhzd2C8r zYDywm;fD+#JtI0>MrsZ-KOVnd+WKF$tQ@U?unyGoF%sR<$EI!E z1p~Kh!gOGc(`|G)gI=!{2m3ByUQW6gMkHN+t&N}C>)(#14)yZ%v5Y0`NLepk;_x9O zr5`-OK4f){@lIOyUJq?9s4rA#WNmGqph0ChH>r6_Ll9`70e#d;HT!MvkO=}joV=snvi}`Bi9(OeM_c*VP@d=%8vEjY1w@^#;)&l%okfo zk{l5wFZqCjmMp!No&;Whz9cJ+fVeneadA$ML0}j%JOi&|?S0s?>m5uoQlEm0QnsOj zfJ_+P-y&{Pa`Fdqz3=FDb`^#b4&mTarASbUhj4h@W-zK;RoJj^JEdw?W~rM@n~2Pi zw5pD8InTJxQ2aB3LYkmT%qW#J<7-KL4vAyPc)`TFSGREpuM`;rts+ zicWPSh*Xt`mmSE+MXEVp!yk0SuQ2=5=4W41!pvwXRY+xN;Q)Acy^p0dUAEY?=s*4B z4x0-i50vzmm5h=;3Ao$6zvaBuC*lS&53N}^c5PN@Rz7~J-?g%&D5s8)4BepX)^NRJ z+cDNX(lehnsm!d5%0w{quU9k5RpqRH3R)5glwLCVkp#H&)-LU*@m*P2o>>|N&wd_i zJR`nb9A8Hv-PL75O_^Hd^*T0U6fAKeu<{*%4Fs(TtgWy4)G4r_vA=DJqlZrESs$_> z>+&FD!zpN^jAbHY#O>%4c`4_B_gIi;gR#(35U#6k79H3Hee!(FZ+LOo5)h`0n^7qGKXHT>X#!5Ou41ZB$ zIM|S(>_-Zrpi|XefKWe4R#`8jpqfu{k^pa$uk?~YI2?vC3(lIw(NnL3pUjuxEmssi z0fpI*=g>KrhR;h-CjifQD9;J%Zscn>ukyIEi=ky49AwydtAlL|Kr5} zbum_NiB+s$m;~KAW`N&wX*tuSO#&SmHiVc+0SyT#FfYc6nz7mUg1Ya;(u+@h4TL3) zD!f&nG<$;%n|9<}sTIos{17e(g4q%}cm02P%m1|ef8C(}r`kbePx>^W=T0xfJ!hV8 zB}V^sQ*IHHHb;fNnU}SK0%5L;(0e4<_?GoG?@Ad&ZlZXUn8>a!ewiQNKNy#)zhtMj z?fu`u!R*x37;N4}VBfz)&tDww>mwF?-GybKxL5&21ev~l6g#mHYVy#m)Y|;B_4?_f zrR4XL24NY1AAgOiDMqRXr@c)~*zL%cGdlE^GY`&w;@e-M2yo--sl5&V9nz;*B?Orn zYqrMHp&G5A$Kd?R1E#NU5z(tc8)jU3=%TBjE^I8UDD zcOREyYP2PnDb3=?miGX?T3do0mTXi>w42F<=tRqor#9=IZZ7XOu7<7P3zst?&6X62 z6uo7jvuoyFHV;n(8COl&ZLb5SWg0p*ea}N_muA&4bUHqteJ$p?+uJW~$9zkg`8PSa z2^jLZcs?gfHl4YFjuvlLeyCx?qBE9Myyu^LHxe>guY-h^wY+y9MlhqX_a{6xg%4*9 z;5Obbm`g1*e*E|f!Y!wUd8#)|HPZ30`k3sPRxx4j{*@)b9`^(jA~i(7G^9t7gK^k< zZ%it&%|IyIG!gDQX z_-o8jtDO|F^DdlvTb&N4gt`{=a2R_vqC+bqTF4HUjziqW;S>lNulgJ&k^K3yp$ zDF!NxmEc>%lU)5k+pS#G{uYkec>7m z@7JHqqSm7exq5A9WbnZli2LKC^4Xa;1L+d*g;!T}p~~*=-nLP}4#i5?OojpP;97W6 zAt9H(8Gd9iQ3D%cb)ZOr%4{L|P*y5kVuC(L&_v%KvT&2_T{Zp4$A87Ap1@PcmZHks z4bNkIyW64D$9r=Hy4I)7OBpM`8~)m__jE|-~c z_-(m7Jjp_7MjRm!m-UBQF8uf*33!Xr@;(0K{dpkRb#EMQz0iZuH#7mX-aba~Yf)A@ zi6_o(^*N8xy5*E<3FhH>y~nSfJ3OVlQec`3BSHq->p9IHQMR$c2J;G&~%bt8u=+g3kjTXya#rvG1egxL5D*l^xz>l zdHiyYjZBn}A!m22;Lk`>yXf zE(R6S<;~4S`omY~FCSAm@bhcc&C4{PBXn8i{_fX>`W*Ips=csit>ufz|KvTTZ_q!` zj~aD-_zgF@?73b@OmIMN?R$HvZBFXydA`*?(?#l(|0bc}An}zNjU0w%!dx zdK0aYZF@G)d%XH=>EM}lwk|ufC}&ozT0Or7ZK$=#ll2-bY(}}5)$+diMp<>Ny;|3) z?CKzMwpQwMP4Bdc>plKEq*u!rhbaDeE`_Fh+P%2w%6a;yW-FB_mTw!VuC5SLc_;jlzmfE_VCGO3}bdm@#tF;Rsjog{<+2yaB-QTFtpJK&QwKjZ@*E@Rz zO-CGq`VIR22YYe{njEZ6n6|Z5eUQ=EP5li5B04HqQF zIjtuf+e`%N*~99h(KHJAbaQj_>sIf%_5;Me!(=yn&{&d35_V@!L*sBIp_os1xnDWI zj?~lXM{Y^UF0@p3e9kE(Jn>M+EGDZsB(}1Wbe+Xj%#4LR2Ljb|$F&1l2}*J4;3Mwj z`Dqk9?j4=k_jZ#gB8nbbe=r2+>40!?S+Xh!e?s8EJAn#vKh?+~v?p}nbw>&5>l$L6 zTfBC5E41sygbit=vrTO4)U&d}PUb~cZ9K$&e`U!Wi)?^=H2xAT>5%KfIU9#>{JhgG z{V<$cW_R1CPef4FzOv<}MnUgwWn%*h;Pxjtz@G{qAA*g+AKe*nzV=uRXs}w83@yHrq7@vEXD z!!a)F=YO|)h{!s^_lWjfe)ki%D=S&Uw77n6zDrqVm%}fao}iufuCkV!lYr-V z5C{YV_c(!s7H~zOxj7FXv3@xs{`!1t@TB&IuE zeOXDYfQ1Axbn)Gyi-iR)9UWZeNtxExTfaM^JRr6Ce3bML;frmH%*@Qh($Y$0X~SRM z3Q~G%N?Lj!D-A`qp02rtMI6BjEj_)Oh6Xt!qlKB7&%+dx*Q9cXlS<>hw*6{H_<*U0 z*L5v}0`JZ^guH%c>hJvQyl0j%z8C$5#Wu8U)h7#7&N>`Si`7fk{yIzVSmzm{8mGWX=|4u8ai3wcGpaMcC5xHg- zfkBT?+77iJ$X8X_tpb6Pw6uy1%{|V0;R`q(K~C{iM=NVDpSl}YH22Hic|NQuWfM!r zIax=CR?LQZKfAXN_K2{)Hqh_02w+@1zVyta+2YFKV*4>Nxo71^to9p1o)m937_!^W zJlxRQar{05Y8VutNXYb|7xEV!@>%UJKXxDQkTYiwXGs|a{FAoTj}eu zk~upx7T6d;2Cy}|%E#O~*fRk6_^ift_y)W@PfD|qVOgy_FOJ*9xqm+$?j`Hz%F5BF z`S*km*&(7{r6wnLKZC*F3zS4H8EQU^4h;u~$HzY>VGPx-*CK^@6p*E5CklYSjl&5- zTsrJ!hKs3{tOO@`eUId|w775EM;keMZ^0oif2_RDglcN)8d=z}@=>wt)};MX0v-oX zKPoh_%3}2mJsb?QyzXa>5LbcXNHuqHapw~TvH>ZtX0loPI3B(^o0R8MV|9x_K*kpy zOaPN%_v76R)bG>tZ=s@WTOwC6%0WIpyqp@C8uQbD1`0OQ%OYo3F^ZnLI#5ka%Y!?Q z*PE@I8xlZ$ac*5s`YQA4vak15Lc}m`Jgo#P-hHUZ)Sd?wFk$d(Wo2PwWg+nNDOVzY zycF$j7cv`MR))3SxDpQQ>+8$6yezc3q|LXa*9HRpq#bK%V1fExbCR>Y{T1-b{n7I9 zrqZ%Z@&nR;X};yvB0mHH4OG5*0RiLyWdUcNee4NBD1eO(txB=aq#qM4FFA94!@Vnm z_C`%bbrpxRkHdCs!Nf#e#}h07rraP2vikhAyr@DSrQvP__8b%UQm#?v#(w-|IJj>fSpfR9pI zy0?AKo|2N1Od-SK+p}0(TiY!+XC~#zo;fx$l7-DI*V*})l$7*2t%3nz8cx#7i~IJ+ z^!)nzdBcNHp>oFMVPiz+XnB4p)bFsRV>~=t)VrK;*gBlf8ksu90AQhEgEm7jjO6t!Ia}N&`I=YdfQN0z|Zh{b8RlHyN5->hg$+YP3 zILP5iNn`+L9|h6Ei^Iz1_D6x$Wzj6g2Pf`Ru*m$b7QFm$%pMAPM+#|sQ;#Nc3+}?b;6kC zi4ZHjs0bUtGC!G20?(Cwdb%e`3`tL!lLuVnZA&JZ`$~G+>ZYZog@=X7cqkaSxFB$z zHtlQ2XcAHSGVE+20${=>VLm~vi}WT?v>z#Dhlv(`_{SdB(5H7_>T?UpnI?aLtP&kv)j+mgjsb8%FC$yo4h;3PH~mm({+itC z+3x)*ZQN3gy>c;`Y6E1(g42t^Z)d+-T51Ic^TdLKTqSUD(3LcE)ySt?{_ayyQu1eE z@GHP^@KxAIK>CcDf>_{gk}~njXc3mV(8TMTqUb-4^N)6PbgV#&@F+CwT|cTH;EXw! zf_k=Y2I%|V1ep|3DJ?MPTRrB;ZXUd>uqX4sb+bj0MCOz~qN&YP#-BcghaaW}eMmis zj?&k!YEUExHi0@4dm|N-cZ7T*QDRE4GNdx7Y!t$qv*?|Djo%PqC}gf41(L}H^EaR0 z1^m!H{Orv2GHU(b8gF7w6!h*s0XUPlkwUlW|1F&QF?}PXZs+sy{k8wDN`Zmc(!v4# zNn|sRm|y-UuQQqdggYZ63Q;}nn_*EzClaUTW{Q@2YMSQZp>IFeOxL7qWN zn-@c@Zl}j}x@MZCZ5177t4BK1kV-EK`@gg&)PR?4_7B1*B&g|Kxw26H8;tVa26T1# z)tpQSP}i70S$n-tr?i;c>*W&bHxAF*y%|f1VX&d~#P9yF2*v7eOw^;vNK2zbD=RVj z-eoZx3HflPQ4DVk?x!vEo(M=Yh9c!-ZYoG)aD$cFeMT1@i|UWlbB&0LjCwTSQCeAV zj*ViIOC2Vzg98dMH8pMR?9SIG@LQy18b7=gw=7$WABruwOW#dyRPW6ZE_?iA zm~XrKxPueOsj2Bn`)uv~uZdVP+TzppGf=AEe_76Y8h*I`@B9X2H+gOkYThB4>%Ay4 zNpr5|p8x-~D{H84A`6E?Xt=V2N-`(GROkTXg2ZCWn;S54W zNz)g_7tw+JsEuMLg32cweZl`fh~)oGZGH~pdA__E+wJ$f_LgDBy|WDoH<{oby2ryi zgc24b=cX}-T3=%(?F$?&63ji}4ROT52`0f}%mEv-Nd&!jwNsLkws#dp2}brA!=j0x zU%57oGuBebpo)2&13janu2fZ3-5nmjx&6b3@!=&tkI&6LIRyPkOfsdXqnm0K`D4zO zE-v{o2Hib8Xlee|>wfV$zI*k+_naDH%|H`;pR8*a=5(=id6|F?MnLcphKK%97J9)T zC@eBXMuS#ZFn~EX;3+qt265E0Ggv*nyXmf7&pV@~DFqofV(A(p3NEP98wk^*K_M|D zYKfuK#iRepPB6cd|Km&--9lubHG2Q<7iV-ZD7SV*s+k?++Buk9+#9gbY_v}FWxx#h&eSWPJ<+E8QW-=NNq2iles zRjDCPI_D7Q7c9lazBzJ{<2L&^g_@ohaQFp@S>Ho68*BTdqtM^KZBEpUW;fvpYky{+ zjr{JjlbSLrQ2dFohc3I7sKa646cuf)IsE%HJOTgq_x(?-qpG7zikj|^3|b_3F9&Iyngq^SZ{V{w%M}V+4ZvonDh_ z)h}uB@k(hg$D&74nPz4?bJSy`R#umz%mT-dEwb}pROw!-TSjU68;_6JK7t#DadFXl ztGh5hkW48z#ONN_??kR~zS z=Vs+7NnR5=<{!{0-=~(;Rqwi!mt54uD~eyY>+g+&9zLsNMWDvDJ3LpM#q7-(?gktXzkt4ZRt?oNg@|FUkZw<7IdzDU;BBsw$ZDi zTFfnu0{&$R3E;LY2%TOuz68&%ZgUBI#T6~h7sClJ^?yhU={~%m9-^bSTdcV&wG2${ z;$M%M`wRdk#)iR+qQi1UQ#N6mhKs71)5mNQscn+T@uqB%1+chSy`a!0B%DRb%PV;6 z#}$XHr^84zrE#V4CXeU6wXBMcl-t%t(W-dPfQT5 zhKEbVneWY5|H@YXW6rX~aJ`Ojshi8h4s=o$NO^eR`jJWr2!*a)Zeslkz*y{%W7do$ zJPqhh7i%0l>q@5EqA7It#&zEvu?>{Fx5SRSbF$X{kBcQAd?llu?oyE||8)BJlL~R{s7T zm9s`>>k1+uJh7A&1)#$-EW*^6Q8`tOvnE!*bmdZ-7Ajwix6gp?P;D(8Q;jP84}_yg zg@Q)+X^_EhQVzQ2yN|DrS}?8yYqPcB;jd zPAT2@gGMn*mGZDV7gjnu-@a+#WutaI6;(H@8TDRiPq++D>Rx;y6{%B22sas0%AJR} ztK(oXoiHuV;lD+o4seP^AblngCo9a&9lv}*Hj9WR`3^R@zoo5A?{9arQ2%t5fA3mf zvK|jyT8)j3orT=wwg$1Ddo#A*g7$*R!126+zQb7tBvn! zxw~g(Z3Jy1%{WO`iEwh}=*iBzx6aoSWZdfQx$%mD$&>L=0P;4ibeNeVgSsp%6-4S}$<7sQyLYvJzrn?j}dtgMWOCx+Q)2A@!y{UVse`|AV)ocpa-DQ zyY$D}`(5f&h?%7{9KiNg*~v)g7SWl&@2%YcQR4oo;TNYJaq6&cMP@rE7oVgj84+2& zB-HO&aB5cO1Qs@acfe|Ka~ksqvwB&XMH#`^KW7Fmi3ohtU2X?EatbNH8Ih{3-VF4b zv;LC*G0oqRaW_Ac6ive?0KMSkN)`c)MhF2C$mJ3p%tDmT z&f@%e-#^W_*5K4?*X*&hYAf*zcV%c+m)TRSI=a<_|8qa_cHN+pqpj_7>M!uJPM09Z zhyQMQHJag(A1{_FG4cnA?&@DZ7?JOqdF0QYf3Dwb_>u>V1W>ndmEvTE8 zf01$kdmGwYyUXi?lx8M^j2v8&Wnj04u%C{gu@^o8`2cRFG+u4!7 zG!;ZDa=#k5_a82K%dN)C@vkp4r0KgLjehs~>UpntEp3I)xo~{zyTjA_FD8C+<{(d| zpQWa}?R~Ge$8PQ9Uxd@@YBYd^8W>9wIsYSjaEKZ{_#@^vukE*oD&aah>bZw?EnVMU zf76>bhM2%h36TRs^xDoH9c_ZPCY^dNS9-AT-(9S&OPZRPNQ_}xrKYB*=NHTSZM=c= zspRmY^^3>uyFFi^6Y~;ZCsvum!lCo{0+_C^*RbydZ&#}Q;9y}V)Af;&a`y>+)K?&8 z(|D*SdAxgnk^TZIN5nyKm57B!FN3R+;IfJe_SG8zQCM&WT8*XuNo3k=Q+T-EjQayk3^-|I#zB0;2Akhb76-ZcG>gFm| zciZSl-MTq<8%`d$d(wPrF6Z#BTGdpR#^>BUu_~QbAjD(_iov3l}{{?K-wn_j1 literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/call_stack-light.png b/testdata/snapshots/darwin/call_stack-light.png new file mode 100644 index 0000000000000000000000000000000000000000..e0aa6b8e77d63a4112865184a9edff9e08b7ab32 GIT binary patch literal 15794 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 literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/code_viewer-light.png b/testdata/snapshots/darwin/code_viewer-light.png new file mode 100644 index 0000000000000000000000000000000000000000..c40631d39dc0478ab799618e39b2b3c416eaa1b6 GIT binary patch literal 15572 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 O0i?wh#HvJ$0{;i%6p?NK literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/command_history-light.png b/testdata/snapshots/darwin/command_history-light.png new file mode 100644 index 0000000000000000000000000000000000000000..4012a1dd1f8c7bbf0f5332c9e6c9772d4fbf11e1 GIT binary patch literal 13177 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 z zsf)2-L0A}mmjw?Ui40KyI-2@)COQ82;9GYM6pZbC5<qbTyCK^QpZq773JxRgJTH2W#TrGP~{`1M88SPAYdPIOIGkd}K z-ZO09@(s85U8$ZSdwq3I4s<0?{ga|14|@_K;G(*kv8iE?jF@w3s$k?#UrR>*8rOFa z=oxZiKQ~>YD;SFn2Lle4eZ^&OCMrsbT?LP>VTM@Y@|XR^y_^oy!mH<4GRhX(j^L-Y zj_NpfCe6qb>SeeIocQpN3cE|UThB*vmeiGRN+fj1%98UNFyXj+(%|AwNND+s+4P## zKs1XTW-tLii+fB3UPsqd*D6J~y~|fmO6)^BKeyy=tgWpb42@2?J}BWPCg+DjSBwdv z5QU`i0Gixml<-dP1dP}J#9N%B6^j1@6u%k#??Lgh6SyYpFX&fi=aeFZnRS`7$HKg5 zd}7{LRPFH*w)C{#+n3grvq#AIoR_L!liuf4tJfd@0K*x4%Ts`IveNk0E2Dgc8aE;@ zzl<&ELvY}d-({dqM6lPNKgGtH3X28nH)RjVgo-*U70{JTarWh3KqI49=hnp~g|M9J znlE9=grQRVCsO}ZWZ^a0l`j+#GS!)!;zBfERtGrP-g&i#Q%sQJ6W3tPwMg1Dk!N>q& zXXh-CPlle>@V4u7N}}k>igJeqH67iv#-l)+^{L{B*v;R%gA%*Cs%y!~WCw*o6K=R)qxVlZ$7IhuQ;Vk^}+) zpfD8BwHZIcy`py~2I%{DaXQmU(E<FEnB;{pvh|6tCOECgt)@z1^Q1<8aD^N?9jCHL21W)%tLa2-glj*P61 zL}zX34z(S@8lKM<65+LhvAY4`Ntt|L)V~ny|L_0blfVi-@OjjUqJ$b#+KkvSvJQ-o z?*9zKC0}}(g3I&`>vaee`DM83h{SgC2vzt|U+n*9?V16acvsHil9pCmbMa~_%iNQ^ zkgWp#$X8vx?Lk~9XfM*0TX)&KVG{{{H0M{EE9 literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/completion_menu-dark.png b/testdata/snapshots/darwin/completion_menu-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..d1dcdfe6b30a0309404d9580fa5d328f807bc9f6 GIT binary patch literal 14386 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 literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/debug_toolbar-dark.png b/testdata/snapshots/darwin/debug_toolbar-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..0090c932f12a886b220c200c8ebde955581a0357 GIT binary patch literal 2158 zcmbuBX;2af8^_UQi@fm6Ra)^XkJf9;BS}rXQd6_dLQ6Zm%1BZ>BozyfJW?|>a|0=l z^7a)nGB7H%*46UJi%=1dSd~<;06Aiv{r=AEJMYYIp80)yX8s?Z|C4_>*iT)>Pz3-0 zsQVxEJ^}zJ9o=+akn-l-Q7L^30Dv6*y*-bn;(1dUhjqC>Z(ZYZ67|=-zou%zW4sv# zK%Hubp7U8r$_2nY-@1>rcb}Re-kMwZ_XXzFjOeR`7S!dM#^Kw`_aY7js;MW$5666} zK8DP>^w~CNc1NCSQD63A1t zdwsS?;|5fG^Z$X(6ff{*j6x*?>R-W!#ggRYVr{rpY`iA0axc~xlzqQ{SJ#<(3camO zu^l#NDlY$?;-!rJw#7@-@*u7-_A5OL{)lHCU}b?D2<_Jh-J%1JE6$`omf!rYbUo`4 zMR5VgRNJ@%-VpXt++?fwgSzCWmWy%^WbV8`1z~d9P;|_VqRm~wiC}f>^!WDD!Wr&7 zo_Y3+8PM~w4vyL1yU!_EOLqD4(i>k8_K*eExWE2NvUDx_BJ_e3!JQyVA=nKQLzW7_nl5vI=*N#MmMlxp%5yQ4OWGd)596P9JITTbIwWIA%7cx6Qp@) zdO8gPNic*YY)W`YGrqxtzvdNaRkw^A+tJ{1X(fwYC7)yYS!* zMF$F96_BbQJ_NuIqQi$i9KQ-FV(@C`QcnFPo0)ml_~-CKN%zUbL^SuPn@P+#KZr7-aVjIPbySl7%x5g%>eLy|KI==nYzP`=Pqqa6yS9d#I zf+Ie5m#1@DiFGJ{)}4FZt;y+@b+FOO(o(JFGcAAokw;iM)E@t4XHT15sLga0!P(y4 z#oqq88w{17kmS!_=Fu#ztZdVfvAkzccPYCGLVS9jqOiQSVb(X+l7aoU1D`*To))Y` zPg!FH?7}f012)ih1r(J#_cayWOJT4mYnQZ@fo8uEI~0rV%yzYlH0ZltTw)B;Qe~7J zyVq!{UpiE8s{eD#FGYx-Y?mk|hXY{l^r~_~(`ZMDQ{J)hO=a;nuJGM2V^T;flN%)y ze?f+pJ}Cs_fdj}xaq6`7w0W4~xj;a|jMbuogV^}r@h>{a zM#SpK7^UO1TCbf;E`QN_K1uDY!^=fa;Yyo2w zxpzCEHTvwwn+96hBtZ?X>$G#^IgE)Jq38yi!7rJuFdhmF)AK7P4$^2oxZBaDB5eP9@GFxc3984cDWP=7$RC?4TB-cYy!u~H5cUxay zpl7{&Dvcf9GXB8Q>zxtTrDriqQ6@e8yU z7SYSwm(BR&Pd4)yp~wN`PGp9>cqbr+ktqFZ%fTu05GNxfs%!5BnmT zm?U7xxewU-vGM4^r|jsyaM>%&TasvS6r-!+_=D=dt)3};dOaJS14jqHw+s++pz^B$ zPH@c_*GFY4wxW6G${@TNzLMW8zP8C=!VTr3F`( zm#cccaoOkB4!p4w@Qcy}1!#QRmS6c#vyt0azP;rbBGgk;R5-5N`Xgp$>wiaI$D>M? oDwyjnvG~`LCdRQP^NIn$yiVK1#lH~$ELzfZ7tEBrL}Z=>sNEC2ui literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/debug_toolbar-light.png b/testdata/snapshots/darwin/debug_toolbar-light.png new file mode 100644 index 0000000000000000000000000000000000000000..77eaa79692dbc95a66bf8f5e6d97d272fecf1985 GIT binary patch literal 2145 zcmbuBc`zIL7RR-vwZ(E>Rkf9>v{kBA`;ywZG-_{ZNp+=ERBfq{Qlu!cl$4TGQ*v8% zQ7KYeD@C_Z+)!IAvD6Ytg)9|u^ZtJ`@4b05-#PQ0GjnFnocaCE=VYA+y8xsQN{fk! z0o+_2y~V_K`HH-=X!FZd{cw!4BWF>e~BB1_FRh4~&^3n@lP4}+9@UN{j z;+{D;x&<+^h|5MTk8~FTif*XPjs$wa?AEQdKW%N#pcl8`*j5xWgr9;#0p%OWBZdm% zcF#`gyxk>uC;xw`vPa9^Oz2r2-%7b0|NT`|b5V51Hyg>c36rm`m%D!p4?>h%d+8Jn z*o3A{g*VIZ+&F3*y1p&nm#vtix_3q5l}dexWNiDp+ca3PVb188NKNnjV@kcqv%FjT z?;yJ7K5;>=YLaQ<2QwA3j(CH#o^)H^X6-wJvWoQY=2j0XrXAuI@;)4sds3X&CEhvC zc&M1OBx#qQl*O~nVwOM>XMpOc6-3Y?-4euF5m@!09?-jFL2C>;&328kF9rr`I~r=+ zi^$_6g4l50e%2n)|03igIxJ#~Sj+IS+?_IwwiUd5M5>9cGK-pcnyb*e6pOXwds@z( zsx?3b2FeW;oNXA_uo!iZd5!tbkQ<}xgHld#|9%mbM=yI4nEmGM>Z|Tih`Y!vEbOZF(3j05i>Geb*k3tCf>GqTUAhUZgjkjd4A=9Wb2vfnkWsHx)% zixJZ%CQQa?J37yK@y$p`<&z5>RO~2o=VxM@dM{(_joC@=3wp2Tgs6jv5r1`a)+aSs zq~+j16P#ULRz|^Tgx9_tHd~vZ{!PSbDhs0310axFXUYixs48GlU%!SQB83r6t-^RM z>`$MgbJ7_I7l+^}iz|?Di^+cX&wqTe+DQ>qBs2+`lerrYiJ29fKybN$U1>Td6BC7F z?-`X4RKE8F?Y0&kd6yYrww|E!v}!(j3yMb6*K&SVk%%s)e*Qb0j5uN3F@^+4mK?;J zb%ujuiyY=o;`=l{ z1Oj>vfGXd_g8N+$r*A;p&cH(U-r5I(MZtqbui`m8zOQ^VJzs~?*y!UYN>;2b9+4&c z`nH{QF~eW>IsScsa>!Ztx7w{>{-j}jW1|XnA%<$18sd+-hJ1k8 zPS1aQK#^>8b2EEw2*-Z@lTk+bk|NaA$T*a;#NpvW2A?Nhxedw$Ib$#l)vZgQq2&dU%`H%?;HW3tyAQN){Gz=wkzWD`hVzk zLYh`T0n~)%Ml9;a5+3@4HidjaBF)cZMnV_R>!nj>>@A=mEln`obl+b123l8l5nC66 zts7pc3G;mL*u&v&a&ZXs+G)T5R^e1>=G)dJk;KiJC)Y7m)vuR}$5K=4!ukU+hJfE+ z=0N=YSQ`~}l08}^d$3t=y-ZxvDbdFL#%Nu5R(XSy*83w&){Wl*V~9SKl2BXA{$nIZ z5#39N#%`#lC)W2vzp$yz{_5wF)WWR>`;z$U%9D4h_TIm}q(ri?fXJcPS4Unrpf37F zejcucvAOlEo8vsM^t0?lxhl{KNM>$ z4sVWkPu@nQ=NA^<5Di4IT-qg1n_8QC+WyhH&QWLJrpd+HOoiFayG}ZGOf7s5L}0ju zH*GGNA}Y=-I!RpcBCevxq?JMSxcXK6MifXoqwJo=b2L46wexd3MUbwL<)K&b>4%6r^=Qekdwaur sGWYUQwf?=} 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 DbkWx< literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/find_widget-light.png b/testdata/snapshots/darwin/find_widget-light.png new file mode 100644 index 0000000000000000000000000000000000000000..7f8bfa421ea775cea83dc1b2e797ede40303b685 GIT binary patch literal 8713 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 RfaY-kG7^g7m7<1z{|l>)bdUf5 literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/flamegraph-dark.png b/testdata/snapshots/darwin/flamegraph-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..9da9b695006367835255ea129c9df83f51956c9c GIT binary patch literal 13502 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 literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/flamegraph-light.png b/testdata/snapshots/darwin/flamegraph-light.png new file mode 100644 index 0000000000000000000000000000000000000000..6a9f5c8c53f2d5e7de57ebcb43f1ba3ae074f430 GIT binary patch literal 13200 zcmdUV1yGzzm*|i{fB?ZI5G=S$a1DdIyG(F*_u%gC!7V^=ha`i$ySux~@Fw@adv|MV z_q|uORj=xOJ$0tLzn;@a`<&_SFaXzGR6SF zCv`~?Ar-gGqh(K(gxhEMGg}Nk@+H!rsG)eT-m>^xmLEmpAS2@P)|CV57dFvpvM&r+JclQmF^%i8J-0U{E@zPU$O!ND}_3 zMENLja)^k*VQ{d2QGWdo0LvRFgAv5t?G+^z{fe}WSyBEXgVpc%`dm}fxiDEC=xvU$ zCDW^d_&k3&`+j8+Wpfy)*wQrg@rnwoAznM<_Wo=uaal#$aDQm<7aKGCZDF+1_|pLK zxwur7R?AI@J{vxr^k6yxL4A9> zjaP>XCFVoKp6oxW+;9G>0@8LRVOPzcaYKUTBdXdFbCxqn)}Ym__sPo7C)0)d#{!-? zh03Zb7VE8BHE7)hjq4-I%bP_coQS9r%`MYuh9zcYEDXmv76=!mP<-pSAr#uSPzTugMi;=~&y*nKLs+ znkx(p34Di0IN^92ODjFdW-|ceS6eeZix%pjlVyVskOBSk2cDyAU6+pb}#jp zMM0~NsLGD{EzC@v+SA*+r=1SRTAuKp53hDMbzB-+T0N&*>H}kgA0C#5z83|Hp~}ak z!c}Yb%2|1x>blCy14GrypAC9PiWX69h)(Ror2Wa`^Wi6@wk~|KPO6A~urPUFw$*(E z?l{*_f;+7t1=@uz#cj$U?M6t}>a`i$KFIfSfC*=7d&>g}_HcFHO}%tTf_G_V;`jCi zIVmaPV6p>O=H|Y*6DpA%S7!JGt0#QOw&jY2xkZQF&3E&kY=$Og69d>d%T7ClC95s;N|``(eYBJ$Nt-rQWc z@p-VbjWCWRv)3^zrBdPY@bWmJ4*9k{YmFyCXNK)az)WtRFFxi?Nm{Nr$SdzlQ!d_Z z<8vKpbHA>4es6AG$CpYZ8zWXh8R+Ed{A_tE=;7h9k&pnD#MNt%tBVs}R8L5`gq~bw zamwr=Vnoy%u(~J2oEZrz zRrePkIW;vSCl!?;zF>VN*OL?WPDKX|qpod#|EDEKRKc*Ky8+b-wN^KC)`)JYGLPN(W`ZCgB~1w{`#oSY@?c&=SOQ-OjNF`U8r=1p>y z<}t%rH5JM*J#{?SX<`Tc`gUf1aN>bge85k0ZWvbaCAd^KFp7mGOFn>iZbAtf#q<#n1+A_NG zf*Ld}&&1ztUYxESDKV%Fkyw%vb|O>xA#A!?SyY|;JVt^Uc@@_BUYYemX_Pp#D{Yde z2{b+di)Vo!6;<828BnA0>Zd(xem*Ezcu1UWlE|K467Zp?`HD{$6RLXN-jQOVKmZZo zb+>yr^~7Xat;L7B1zxGbU2rK0X_xq#;k=Sr^}y!Ko5Sen=wv-+agbMzjf3yWC7pAD)tbSkYo+kgDZ|dX4SZm;B;nlg+XA6JEk%2ZvzTcY;|7+Nn9Ghr3ji8TuM_3JcL$CqzV$ zfLGxcNb!vn!;xMu(`RvENzW6h#)O`$ZYY< zGV9ovz1>yc$K(oInRG7wgTx-VsKdhIr`HFY+lQDO2lc|9w}f+9E4_6vp73pknGRvJ~v@tSM|#y;pO)aLKltk$rTGIpHM4AjmZm6Y@lAdq%Z z=Ch_?htG~~4HSNY_~my`8+z4yDPD!MZydPykMvEMv4MsC8G05!yNH>XOZFEu7MZi* zYbLArba`CeM5YVM4>g8rbK00Z`Oxzj)ODE3OUB<66%q1!k?ZT`sTJ32nETNWmU(o2 zjwmTDb+>!*>nZS;^w7zsbgTeMQzW}w%5=7IGEoPjh?_z7*c+igUh&OWPE*n!;zenw9>&38SWom zD49Q%;-g#e=u$0rw@CON)(%QMqMlc*$v2uUm3CpG7x3|s-q;-3&g!saMZv>b6Qurz zvYTe%ywE*p=vZ4LEq(A#=ueLNL<>uiA z(RC~heFOmZnyAAaDoTEi_iG~8O$rp`l9Kr@<~*eIlMmJ#AOiqA==f1t`3aX}Ys`aT zxby|2Lb<~Jz;0}Qau12C*gev5;k=M3Ieu~=RJpW5e~kA(6GR?3H9<7doteKu@ejpQ zK0uEdroGnyfLS_@z{D67i3n;K5ypSS`gg~;@Lw+RpRxWQ1W+SMI$v+&p1^(nT$v#0 zd6J~sSD*Pb+`sxaSk{+^s4dPryB@N5?QH!(H3?@Sz?|qW;MXS}Ip7{shjRWIUPLi5 z{t_-NBff%;+%owHisTEKRbbQ)bdG~Lj(Bp(LZxQ-tMWEOtk(@x0z2m5%e_I;*doQQwbT*WsOeX@TXZ>-4g+12}2J zqA21NkF{^*5zrCR2#r6X4c2D`N?f2*?d!O#}gr+-^ z0DW069nEI@Ri;j<_&T3S6W4Zu^V+-50i^oRZ$uT1G&_`SRIm!yS1#}2a+oxa78_54 z83aRJ;Q(J#zIM*@p8lY&bdAZ;6dRZr++ZYmC52m>j)#;SnwM(Xi_beg7X+)VLT=H9 z&u-B)UZO}UR!O0*g7xbw16rX(NqFchrQ{Kp#v$+I?e9(gae<)N?dP_8!;x0g5k7a5 z4xgOMZ1B({yReeT*wXxtiHV_jG&Mt2Dnt?h+!H3Qx{%@knt|rj0%KKZr)MTw?ARMB zb769(h9=~9HL5ja9D9=P0&BvhkWA#Cknk}6DN4L3{bN4?q*DRp*Lzn*uoDwgSO|d# zxp&%)`HW9mN@_)gr8K^R@sS_RU7KW5eRmOLIxF!tC&+Toh^o;t3Ovz8S$& zwB95ht-J$~+?|FJQ$`ayvLC2Stp{+SKsOwm#CaxT^}@SZ(}w)#YvCoudUbltq5a=@ zfe_CW53)#PMkeOreI?4?VxvAQR2OPQTo63Q;*X8WPtX4=Fg=r z<9n=ziSWZD6c&HTa9z9G+I$(*pY1D9%KXE|6bec_+ZUF$q9}0K?P%;9rgtCOxX>E^ zov6QRJm!B>wqYwSJ`x*m7*j+kVt+W#`u=Yb%hAn`vq`+hOr8yWAJ7BmN84IQtGyzX z`C8b6Q&>ti6rW8X)OZpVDw91}>hLnRj4u0N3rfD}}9Beai6s7@u_MX_yQzNV8UNu5#@M|5Lk?tRTBBq%kX$4;7Vf(wQX zzS6^25?a5cn~k?S#6Bupz_x6Jm(k?$6zc2RAB6s96SNYRBL|XbenD#ld>%%BE1Cg* zXV=8{2a#=9*}*~_(mj`U8P%~GiE`i{40dT5qv7_rO9TV%J0t#w(Hv!dWmA7$ZkJ_gz_E)Po1&ar{`W{3F? zoa*p_V@Y#e5iIBI_17!emA}ey_6{cUM|*3^Ez}t^0?=B#Z-sPzw-YYT=!D72c%}|+ zUqFU@>IAs<`l`+R6!5rQhqJSCXWBLH-j*~Kc+Gtw=I7@DnjI9dcprJxw>hok7gvsW z&wfue{l$tKrbiSR+Lnztqk3F65BcX>GvR@pIb&)ym5evLrQ@^-{~~RF~OlCDXFbF{jl$6-K*^ z>?nlsqXU%5b!|0u6}3~yjd&nh43*iI9rh=3zt1Y#>P+$%aE&KRuFln~x5FO9r#=Z( z51Lxcu0im%sa5TlUSpqc`#_9@+(cPf%AXYzQ$px;#kI*jnz@ev5yl;^yD1(XS5*zZ zA!scwn=U45K6lB}yNZz}T^wDjCf!G124xQrkRcqt%^`rp%_DyEYj0 zy1I6y)AMy!WrEXV$G1`hbo(nA$Y$|krxx~_px14qAPzsBD8lp%$C8YUUX9(u6L&)T zh?w39qb7F@8gT!8vrXkY;WzGurhSDeNtbhz%c7$D2)Qi}Z~PQWKHf(TwF#pu*1mep z5yJS$PeFEIg$#N5`W|B_cEvjQs^nYf;HGc23%X>y+&h;_#+#4hgP#5S`QkooHjzv~ zv*8Q(!C9LWiG`|4a zCaF&oI*?sylL<9#N$m9$-(et}Jd# zkcf7>*o-%GDxN?7fzV3%#FTa`&1u(XucbRWx&iue^_BOGO*XbP z3_?p6==QxE^Sk5en*>ulWrX&K*(`uFp{6IG_ngP1lTS@cS4?78_L3o`9@vp>Bi+B+ zoWkR97t$dN->07a>UHIE6Tj<@?|R1{KR*2;(M_?PjI(1jsSan(mTn*h(oqul*?O-_ z3H29@u!8TIQKJgob8`UV21Fq=KjmIHIYRY^ypIP~L)ou;bu%Baa8vA?+RhUM!R-o? z=0O_t^PX!WHW17-8Hn*BdI#~(6zlcDhUJ^Vh6;;+qOkRs%n$}0z=g@DVeY?qpx1qBGI(`kKVYuLNE-SOTZEZ#H>47g{WGrP`Q0kjF| z{7D=xv!M&cxr3{_c%dK@<6z&As@%er(>^WX&)A=_kl(Qy{O^y-)U~VURqH8xg87GK z%gYY2w5!W44-ovBO;gd>{K4icLu6G4=B5Tv9?yc$jhsa zv;6X{ck?_VT*^BHd2BNOaL?>?v3;fg*Snl$O1*5x6hlmOpi9HWesoN+j88H}^5>uyH!(*7m-%0etv4q%j!r+YBPayLK|c3NUS#VhnL(sFM62LsZpL*R zp>;v9jEF^PCG%~@YSVB0-cNzx zZ|CY5&gU35xIH_=?$vBQI^1Q%=klpI&u6&b^JCRQbL-PN$z*SNGuy`I@l9`l3=?X~ ztVgHnMfS;v{kdy4PLc$4@hcuVUGT)Z_5a&%3#zIJ8r-)b!c;Lid{xELsG99#eG03# zQ)_vqg@-^_MA(my9qnL_3K0a9_eYM`4;A-9ht75NeEd2VhxjVJK(nvvoh$A1MMGN7 z^c#l{!99cOn6IBgZW|hOh(FYM&)&i)C&w4Ok&Gv`#7X}og9g;*th{5~xcU_;VQpPO zfmHPV{lRGVx#Zjkcy{x2Zb4&VUaK?`6|nr4O!%3TedCmq{&akD+Y)TVI#0DJ&h9M2 z?-9!jp3!Z!Z*6L_dcb;_J8az23qfHZ+`@v#Iy>;WL{W_QocMd-6+h z?Wo<7MFQdF@vg7)yGFf$!EL$*W4c^wDQc|!J+tyQYawOj1afw}JZy4|(!2l}N@c8i zY{k`|>z8o!l)}cJs6z%PCkyEkute>fy}Z43pLhW!R8-(%YaY!r9My(G^uSmR`Zz^a z23grcX4DS??A8|=j~yI^Otd(R%T1P^KN?MIvk-zl`ioKXHX>ukLE06hI`-RVSp`5P z7cpTAAscENYANJ@Cz8$+$dd1E1wO+Hl{BFPL*-_Fg;?i#v8fU{kqLW4KT94#9V?bA z!f3Ef=uL48t%|KcQL=Cvbb&gp?Dm@gy*UEm=h*zd?Z&cJ~ex#bTv^X`bTKIJSf-Vt%%8ItIl1#x$ zrZmiFc;MlWxIgLW8ZiKxNc~&6ib39gndiMNDH&*w5d=r-yfymL#1%@~boMIuYg6Rc zP)=FKHzhP%Z#Rd3B=QVS8K!H5$EAg(@MI+eKN}oye<0`osuPW|5tI?>fGxuJ?I!?{ z3v7&aBVdU~jAazGrorqb2zp6<_PjZwh-kkb;(K(z`GgTv(6e1l|Jd3}f^Z=oXspT3 zh^k)rRwt>wQwqV{42WOF^u1G41e!hjzM~(UQN=!ZD`Te`eN%oh4x)I&j%=+E(4O&9 zxP>5Do2uUeKaUMK8&cT3aDtH$7fvfTr6s7x-QWA&mW2=Da+jj z0q*0qa%)fjY)Uf~Xz=O6s6 zvC?!_wR)gIKm_8~0+mcsvg!(9IQ*j$Ey=n!;e&QK;!Rjc3rg!XDXa_bSWHtj*&0^% zR{?*`!W5EM)kle;*y4zic}*BITVY3Y$AC9A#dj`sJe1B;p41#&E<29 zK)&{K|1eW{Xna-K%JlU0WE^a1T~-4CJNHKIxN+4>9i%Of)uOSBB7_`Y|FB6H|9Dt}5j26_hS1Obl?%MsU&66Htq!(XuDG zBN-hLr_A5*gRf`lbd>B0+1#wDNy)j{9q(Uk&(2U5ez?zO?LarJldT^iK6})p1+~gJ zSRdl;8AFKnVR#lssj?wSHgN@spn76W*?Te7;o#xcHk{5*`@xfimN#0-#K&3RWCLSk zW_<1G8N-tOXzd!WPvBw7d3ku05+-g=mRcX9f9Spa*6Z2LQ_&v(oP|l0Y=*fsnqf zDFfOb1=RmG zJea>($TXxLwQr#iXlJ;eRO#-Yhiu%epAfd9AfpKA5I^0$pGGCiwGU{D+fU2lF+RZR z1a$7JO>%vRo}`oG-mOI><;HcWFx9%ezP}>;%k!czNYC##hbhw%lT~p-rJh!=S|*n~ ziks`}zE7;oJV2f+y;r;M7?Ezy5W}ORPTNtj@P0HE6+w)}JumxTuVQQLom(h{x6Lx1 zI|Zk-TAmW89vonrMb*Q5-^atdU4=lyR8HN; zkZNF_Tf}iUujkq-$E8>@moVyUMu^L~+jis?tna|M)zq{u-5}-N9~|2g9=mlIFt542 zrCWi?DVJd(usFjbK&lGLu{A2mv1i=1GPzObohhag^FGTKT+Ygr8=l%e%tM4-mW{>6Jn1T0xdfL}RIJy#!8|G=LHN$EBpW|0j9*db!EOyR zrq?;=w8qnRYl*w|>dMJnb7h$-uI~Bb^u@{988=i)t#T!=X}Tb^<$((Y;2A_r z61Zi&n`nkKPG}acswc zXEM&CZ}sconj;1I0RJWbvn)?kLhh2%LP^U3(``=e+838d8+}AadVPV0WM=i9SKi>y z(3J;L*vm>zK(j+Tp7QJ$E|wpj4JXWxndcKKCnoiD1IbTh=Vm2 zI_|fuF@vh>j*-TiL@AF2p2B}{UGe^Vj-l{Rim^4CK8cyAs?KkwQ1OFaTgw})G5QsU zxdbKvpSfh6<_-H?>pe&+I+iWseNn~?z)fqaiBH!rEUWwp=50jgPIpm`g;o3HF!S>* z!S-2`jv@bRQ1MGXoJ?eQmxfHFcX(Je^coGqgp#I-> z=BrD*7@o@fOy`;8hh%-0AR#CHjPUrbNWmH%o!%48j42@Bx}w6mdqUK{7cOh1YRg@#6gb-D58WrQ72}e9~?HPEpbC(c8JXx$hpk z)`1G%*1A3;ay1yDJ>4Td?u_mQuKkca(5T8<(}*DR*|lgyiJD4{2_HL23h`adkA|J4 z2h;{GyJm#k@7>bV7VP*K;F+7}7>HQIbnD5}eZ03QbT4}47bNM*$yV^0y_C=SVJgq|;?EAwuB+)8!nsSPQ$d?p~7@vKpZAsET7+}j*b4~licT5exJWR&bYlQ1yL^E ztUTJ<`rKNnPS*#E)r)yotiOFZ?^Vvp*j{t9RDOnOeyWXeqznoq=gX2wMi~tL^`%Bb z24|4-Cx|iS#qDVEN)(B1V32@`6S)sQy;4uNfunq_R`-y@n%pivqn)3Rmep)yGpSgv zmmHyFN}ENtQ>MY|82_2@W|#GK>=EGyt2CJ`InMa>-7?>Z@Q6+B!yJ{Y%ygxisyZdo>D1?NriTu1KjBRd_g{3(8aG{6og#Dms)m4dyn6f{e;KI-5{Uom6;xU zyVU#NmqM6S@xGX_ul!Clu`auBjkDgJ9`28?H!0mLXZGulH$0iBqk-u#%dKl z9G!CO5Xr}8iX^9@2kXlR1}>Rdiz3iq4_tO;U7z1-w|NwXq!f!)K%G)Dr*iM0V`dKT zZq86vy(%gB0|xYl%FPTf%Xa4yKfkOS4Tg)AMnb(yf6a2v77eykjo2xpeQ|9$B#ak< z6ql2;R3uNWv`?zN5JOVd`8wiqx0`~3Xij@>#l^7yKve(&DWnKN147NowywblVticd zsCT#1r|6GKhTXR--ra40K!&8ysUlXWdQxUH; zEY^rbbGW^t7QCOF2bJ^j@jrTa#t?HqC@t{irG-`Mw5Zg)d|tW=oW*5=E?5gMqA<$~ z57*rhXh5doQD0ihV5?9{SomHU`L&Bo9D418wLPvHvgrX@?{#3{nDpgjk(dBGTfh5H zzd_yf`e5P@SJNl_sPU8k@@hcAIc$8j&hqkR0>jkv(?3|+MHK&1gK0^rX~dsclY{uIIPA}$IXr+4&8#xvE3$cf-L*Tw;b+n z?s2-q(v)#h9*;Zi=VD?zZ>trY)&_wIs;`l?wGF1f9*-fFsz|B5qZ1=JgKF4~N3(d6 zqPWP-&7B_`b9s3>3{43cd5vpg-$_1DZY4Rx@B?}@W133U*jcG20Vj0W_U@4ib?`3U zwPCD;RT?<|Bdd&37%b`4(^5(oFgGG&u`YElAn0);9>baEX|rPS!gYuC&6~a*kJ+qN zW(q#Ok30iRBwua@9SjsL6^h6QGB$TAlQ-m_%6iZySkXGgC`QxuZ% z|F-RM8FFf~!!_570DCSOW>n0w#Yt+nTol#yJCM7oLrGhCa$>3r^&L@1q>GE}tW7sm zT8V~Loj;&=EX{4(OXaux`N5uO{>2)XK z*^*VKWMG(7P)%|9;CO%7cadk-`M3ahP`Y3YFc-mg#mhlzuPc^Hln_{>XB*ufP|)bg z6)-&;qK_By1p4*eE-+?3&Ma4n_SRPsjgYvJozwIg{xpFK@K^i;6o|t}$lzFBO#Ltu z^7>V&djatY94l;TsllY`_h4f?CbPCOqaIpao>{W$Ys_$S(3o4_2!QeGKnW1?65q8u z7fQ$+lW64NOgW-cqDjJ!+UYt!^?G`u#tPR5=%b@UV*-+`OP^5@Y`D+-dyqJin ziS4|<-4-rhtQS1aN`FFeFt%4RNYm#n_{Z4U82|#CCD%xGIw?x`iLhB`az^YT2Mv!Q zbQJ;tF_fyj4T^7Alch|UcWc(aBW!MIOTDo;PXRq&9)hjjQ5_F+($j|s);uh<5L8$* z6?bwt1sW7n9W5utY*Ldsx;+z}V{$O$<+ppA-nb+w?XPcNYxTSmo|d=_2ykO$JXEjf zUuasf5=W9AOoja=cyo-46hu>N(nBFPI@;e&o%q8XeT(}u3EG}OkR-1!Ck=k-PVho+ zQCHN=28>l^U$eAsheoHXI;6;&c%skuX1Fqq!!k`Je3L6;m=ei`;Hy-riY1#!4@96q zf!d4>KkgPILS#Op@#%V#P-Mj1$$TokV?__D8-4PgQ-t30?2{7~Tq)>4BqStZCc{mx zO0{y76iX-6ckl3ftQyGBU(ki8dnI|6sTFbYkl%nH$ELOr@{7GyJkls*FvA_AchlWk zT1`z_ttr7O#qdhd@PeCnS0BvSo)dnS9MN*^_lk-r9OKYecTcUk=+#{QgRMy)Slz|( zf|rMD+7;vVgy&0Jjf2lqhh5k@3~m$6UkChJQ?$DOfxJ~o$PkK4P8Fr|>h?!JG|Po) z6nF)FDVRviPZE_K@9tKB+R!&kf`$N#ubk*<`R+}?uP}It|Im+TjLZWEy)Lb{}MF{05;Fl{KW6F)@_}*qiiJ$c-b_1Kml+@hF z`v%A0<4k?tUHJKt_gCVW!HVC_oRaxnmpbr}pvfP}Sf^H^ry0`UneKm>@=xOX|7mj~ j^?%6Ddzk%~H-K74)lHfd975GoJb|`i~s)uom=kv literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/hex_viewer-dark.png b/testdata/snapshots/darwin/hex_viewer-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..3296b9283dc06b32674e019575b55ee57c125bf2 GIT binary patch literal 15629 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 C07*{( literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/hex_viewer-light.png b/testdata/snapshots/darwin/hex_viewer-light.png new file mode 100644 index 0000000000000000000000000000000000000000..d01870b820081a59c296149be6f0e913364bbfab GIT binary patch literal 14906 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/gMeSm~+cxrKV(YhfKQXs+M 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 literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/minimap-dark.png b/testdata/snapshots/darwin/minimap-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..f9dfd84a10598864a6b7229bad9497bf4953fc29 GIT binary patch literal 1549 zcmeAS@N?(olHy`uVBq!ia0vp^JwSYig9%9L>fD>hz`%ON)5S5QV$Pen2fah0WsaZU zQy#m=U7+Knii?VgKyQTL%T*_j{t(~6og}tMh(k!zNiRmEqm83+#TloTvVW|*3}RQh zsQPiRxe5rXZCS-3-Zs~A`%LTa*XHhge$Tr2`Dg3=!gKGf@(ttbo>#y7^6*d3=Cg&4 zA?1G-t97(^U`HYTN|IThmSm#_!ROA3u&?ym)Z>;r{vdD`P5@ zj!MtAlaZ_W`_EUkD@pCLev3rZocI~DUr9^E&6q9Z^o5t_drxYMz-fN*)VRNuOCDXR z|G(YD^tW!%nX~I3$WCAX?B3o$?L``s#P#y_)~yVC7&_^9tY27YB9HDldpY0x^CLn# zSGY2WJy>^1HilMV)-kr#(F)WMpqUJCxIg)Bn)q9NSL8 zCk{)b1WsP~nwRoV;M`BA-#?lBS26k*MSGqQ?tT8 z;l*$K|43b5zlBqA-u_!tg)MbXA6oKO`DcAV@F7Djk&}OlbnkCUjhwNi_DF%TmdHx& zo9mV(>2AL~*KcL0U0B$4{kuC>hW)fCw+fqa|MtBXe#?PkSBqUPXtEmA zx4fojkH>k+wfr!^>=tTP8eK}>0iq+)Kb4zBXhuHA=&(~Y06)Bch_$L3E zhWPiVkJfwm#ie@9cxADF-`6-*0nMC=I&rH1ofY-DUVc>3IllNQi=xo9#}|v5ZoHdQ z)6skGx7+gnCGGr~Ule(7%i2BJ&Yvm3DgWtqyGc&@w*5`h|9x63)FIO5v5djb%i`(H zcTKn7bto`T%yLk1zP@|nTYH_teHV)M8JO&oUf87D^qd% z_q(<`FTeh=V_$NCX0-12yLYAJ3wK#=`f|vlQvK`OiJV(cxv%XEY(74ONceE7g%^2_P1s`?4BGiFbkJa1!9j$2q%x=W;^>Qx6-$$55fA5D9F zqFib5q!mjhdtG|q8PVr{n$t-oFXMWV^X~ogT-1%MCSL69@k=RDS}ZI%_rwbqH%y_l zmo9DU4?Qb_I_9PP2BKRj`;4dBP4d4g0!(39Hz%9SdG~6L*dnkUO=3e0u4)ncb@HC< zYBe4f28n%BwoRC@vE+qtY0mr*_xYL{Gbc;?naFfmX=FHk*&Y4PukeHXrka>L-0$K( zTzt=JE^q5P%W;YIp4Clai&`@jqdEqTiVNgkI_1+P3t8nqmf^0ilGdEodf#(+#pzYu zwfB5~`DEStdj9(N=fB+pMSeGb`2JtoDMByoa8h~6He0(rKQ4aYuC&}gG3Zs&{VlU= v3|<%?uXckE<&Cv=)^p=OJxkwMKS!fp)H}nVxGa%)z4*}Q$iB}uH3`5 literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/minimap-light.png b/testdata/snapshots/darwin/minimap-light.png new file mode 100644 index 0000000000000000000000000000000000000000..e061ab9125e2f867cf0ddd943186c24cf4e0c746 GIT binary patch literal 1589 zcmeAS@N?(olHy`uVBq!ia0vp^JwSYig9%9L>fD>hz`**))5S5QV$Penz8)d5636?C z;_pp2KgAKX<U8R zr^aNbDZ7nY+yoAZ2^T%P^Xze)^mDuNXXn0tzaw_Oa^1EW_RoLLH~wdF{MP?}o`MJa zq9!W{DJfz_mokM;8l5aG+%Vtk?c0A=e`fkE_kBC%c;J;QSN?r|UtcP&H_tLWJ!`74 z=GN`iEBAf;zxlbZqI%jd_bEZOmoMHsaUdsWmATP^DYJTgdHwpdm&VyiUH*E;zx|b` z{G0zjc5QyU-NW=~4fnp!ADx4+P1SA!tM9%(yv=bNk4nU zeb;vGGh3z3t}Bc4iU zIFU&oRdkLoemcb&LM^_#^QS=Xx$4D}-abY)YYx9@DPeJMod_jiYOr4*{-aa{Th*|mT%NBn1xS4@!LDC=J z{OW%ATX9E}=FA8)&E=BCvo=n@+>`#x{mYR{>>Iwhv6UEESgzAo)}E6-hx^>l2Z`3= zcdS0<&-pOr@rC;?VhdWErw8{1p6GTmn%DBQV#}POjw6u|>gKTO-FbJXro-*|-iw`` zE&M+pIy1fHlmB$PoyFx)`=4)$jb+DuQ|`DN!yp>>bn^Hgn*3o2SJkA)b|(vOr@WuZ z75{R(TI&2Gb}MIn_heeVJJ^rgaPkaKcVo^djv)qDh(x_Gul~M%mVp3!!5_T~D#6^1 zHi6%5a=z{J*q$frtPpVPA=k0V3VpNef9$+Jse8c#an^^2U&a(XZ(3icx1;h)M@fT6 z`H%HnQ5|B7S~C;}j;eCg6-gF!I%S=`^U}++HyxKuUp4QT;`=qHy)Ii!37xxrcHP~* zrfa|{Zl7~{K3L0@~ozh=^2Bx`2_i`kQ6@b+NgQu&X%Q~loCIC&F#tHxc literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/output_panel-dark.png b/testdata/snapshots/darwin/output_panel-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..09d8585cd84a571a98a75143e2eadb06dd4464a0 GIT binary patch literal 7462 zcmeHLWl&t(mTm|kKyVW1;I4rNf(5-mBTbOt!993z3y@B5C%D4}0)*hf-96B0+!{i# zAdNNJNDueDdtc3u@u`}cnwtINoPGAHy=t9r`HFn0u0%vYO#lD@h#<;fEdT%;?`}VY zhkf_N-i|8;0BDgAu&j=6_Fm4zm)iQ&gRrrO%5=XAGaej9FXt*hSZfV`qA>y5g8Ju%k~hGzy8P{9P)(qMYb&U+;E$j zW!KMVlzBB5G5xkrrLB%GJNmc6t)ucZUcMjlCd}&(vibo`vO2*H{#uXJf*4ppo>XA* zyQZp>Xy9dVRo7Y>Z9)`FW=D-OC&#xKB-36v7Mc*sViho;`1BACzu!Ns>*bGWt?k`= zVtI!okX(Q@D7&nQ4R)74;gaVN6tEWxHmzwXQka^TP& z>%A;fb#1LNPdY{^S!W2x`>gKpQ3TdA<#bs=7~^W}2T?`nGVorPobAV_=n^0i=I0$6 zVWj^Rr2*@JV)7n999dFpU(5cr$N(QG@()7+Hd~Tj`ijST7iSSG|`DnuG3>h-PVQ0$=*G}haHw;u7|DJ z+1PT*cFNzsq>M_VeHUYCwB#am^dLs`V^dx4@4E?*_O6lj3LQNX$;yGs$~`m*!HqtB zd*cUT;sx*riDf1Q))r?jx5s(f3B|t~ivVlA>LVRkoU~e;^|wb#cPzE>u}HXx@lny| z0e5z3c6QZ7Exf!%c=MA@L7f53yIX#K_C0NWa5<9Mf}azC!=;z^sqmS)yFWnv9E;M@ z!nNk#ZhnIqHA0h4x?5LQnVwQ6d3(Z+PGacHkh}AR)7}%Eq=b|rS?!kFmeMQiEE4fE zLwiHjeHTp_dP&uXfS9Ka)HId)x^PV@P3?ZB+obIKR%IJRe)?6}ye%r{Cvvb&1v1Td za;q(Y#S|x=P)z7EmX2GdinfNCFxyku?20mI2-YsR*U53|i_ddVF5uhLnhN1jygUa| zvi04ZB4pgf_9(WWFq>7W(a0R09tDKCp!!P~#KmF|wdXGt6i#_YsW|vdU%IMmnV2{V zQETLmJmup<)`!zSCd9g=VlB1jAYWU1trX_fDCe*cnVUuih=tDA z69>K{st;e_KTfX%qDPKx@Kbb!d7zj$LxkH&Ytn<;#=!k01B3ZXNqW&W)%{AugERMz z8)v6aI)+{q>*%R2Ktk2|8AxrJ81Ha*UOt{63>Q@|XBm)f?`q2;j0B%Q!~-Z&gA`zq zBo8@!I8#;P*R2(zgWqsH%A*zkUOIW>dmAwEMYp!^=L^xd7-fyBG5^U)d?sd5QH8Pz z4Nj+nUx()>=-VyhgjWA>JtI9MRF`bnJ>u}*jdb-{T4LMe=nt?)G6F=-^Jyb)r}hNDVKGEMPyI^~QR$o}S?wH&2q(*9T7mzEQvW>MG5&zIYJdOHkdT z+fxj41w6mXy`+x)(kvXpmxv zINEV^0ih9X)la%#kem6sL9Q20^BAPvvpdFe<=xTwa@$ z2>ZlS+%Tq@89h3>%z9c>aC-u0K!S@4~mcFbXUuB}< z0cz~gln>NU=b*$?$GU~A>1mXl+;b=I8XfeVn(!60J)$R0Qa}pY&njZI1W4u7bH?8~ z_eW=m-tb72Fj}{Wy;kpwt2#5`MSJaTIfBgi#M2Wuk-Y0iO&c4rse&1C?xxEdX?O&i;^hRA?&&>@*9Fpd!r@g(n zsB|HwCPYj5Yo~V9x%Ren?s~TDWna@tB;hl0XxM~>t7Xy96isf=R$$6!>2YLp$AN?c ztx#{+QLSHq<1Q%%lAnZ~%spLG>z6@3-2Sd`JH}dwnn-a?K2xG@;dp@>WScheWu(=T zGnMFIBS_IPv*&S3Yo<#WoG!n07ZJ9Kx3>3dYD-;3`Td21-@%L4gL2iSHi?utT)gQ1 zxrK#>`chAE#65)+mFfA?-OFS@RFH<5)Q`w#tS*G>T=CBhAT*e2w|jI%q+uUzotze? z91MY9H{ZS9@^>sqTrMocT9&Tz$MxoEA6{O27{wYwDmR3gZ@&7Sb6kS>N2l#Ulf2k+ z5F_<4-4Q9yc#!9J!Fp1n^uAB5A_CptK2dTE6lbuCZ+kAB*es}XAnlwJkJ!e9V#RY_ z#e#ynNr{G6fp9*ZA<+XVmu?wJD!|Md?H3861Hz6RTBDXV0P z&|$CCN_W(? zb*D8Lz5?eOVy%J!Jy6$T4_@y^bielL1I0AY%OpT3$SA4%9(RnT(a3?F)u~r}aSxzi2>%z&JID`tP3<3Y{ zluV81{^Gf=$))GJ+v^DB`J79(warWV=%P=>S`JHC&C@6txc$eF{?~Qz-=w79=O5zmL4#8=lk)L2$rBbg_61B2guNh2k}1g*Yu3gd3WZaL z$?Q^YM}o1&Oa~&Kb;eCkqY5y$o(k+qr8@Z{StbsEKViTFgz3g_jcbPzN|>N}*o zLRI8IFZwgvV)e?@b}YE@|A8WQy2_U;rH#Bsq~ys+Zi5Dq!Tqw{0+u#sl_BNzb-{re zwhYKbb8-WFZSBUwqRi518kM)%?I%7iqY}cLt&)RIvAE%lwU>i#F>C2{RCYisI9#)k z!TMWmH+$fd7$Co8R|FPIShJKRPz_qryaR52+w9$)*!lo1DN|E#z-5xx=r5XT!c}#i z-JdQfH211(D;xpV@oM`vVZUje4eRhcs=+7GZ%0q2=|TOAIHMk8+L~-vTIuL=4ay@9 ztM#F06JJtzfL7DOM4kS~*xPqJWI1AI&DI^#Z zVJsRh*{F$QKT)qcI2yru6)7BLUgS6gDX?>BYPr63!Q~1*!8o*3y0o5jhbF+#J69v; zBIU5LVQKGcPa~tjTA1|BNSrh1;NmeEa^Z4xofWwf{|FUyI5iM7>x0?M7z2^R5z#+; zdUe;;(pBp1R8R?ICtyWhvE$C6DxLgN^38V6cKo`2HA~qN9fW5Bn zLgK;Sdj&WVPyP%tyrjX}{ZA6wy0$hl9zxH`DzsInX5yFm53vE%h7lVFp!p zLhjk}YLfps10(a3L1!-Rn3$NuTIHA8lZUYzTm+$(v-^$95&>EclNl3&xm%)-rS#^?rJ@nxKlqz)n-u9&;}d#@fcK-o){$43R;uE1M4@l^@|a>y6+Mj(3B; z#HE=?`n_}e`gS8MyRBly*-?<#5I5koC@Vmi`t;|-#4(1?eM)qCT9iz4$|QkyaM~2@ zX`N7brnQz@&~pjbq^P8iF7Q2b3atxg#1m{3JdK8_G0f7VkCuC7pjS&~8u8_Pu7dOJ z8IWQ{`Ky$fGoFfK#b9<59(|lp#n25Ge<4LW4g$F=+lnoWcbJ=TfJ#tRhiACk#H5wF zn#m60Q(s?5`0NS{SiATDtU0xvC}qN&^X3VeK|3s~1+gFM_E0fbR1IQ+PT^T_I$NfB zZGQk|CdquOBJelIz>NXEzKpPN>MwCfX6mNWNcN$rqR>?GFp?Y4s(%w%Q^}@Cm)2PS z`|%t{B`7_qN@l zw~z$U1P9?=4-7I~`s@nVHs?h+IMSewI|^rMEG-LXH#U^h@-HqF;64h&)`ArxAHZ;P zA&AoF*4DHL(*TPX!`oWX{WHhSn&Lrs(8;41y!WNP3}4;eyWqfI$N#{3sJ$QM{sJe+ zH(#!Kj+@^8=DBB3t)rWRhS$Q3w>HU za@OA(T*I$pcyTx4S^8XwgYQ`(>l0tL(CxpQ^R;7{7qOHB*Zz~^kz zEDFu$aj!PLHizAVHdii-<-LtjdI&__ z&dF){tf%a4Ac(BKp#c^ZJv?GeO(uPFZA;bLTTCa7zU=}$Uf*J4>-WfZwV9bH|5kK= zP^QffRg6Lwp3&q8W_Wh@LPiBAD9B%9j=)S;?l`g^Bgvq75VeX~#pnEzZNGH_W_&Wh z8?^P6sj>U-#A+f?C*K>r;%fJ~b1g|ae)~Q-DN(>jK92z|TJP9~cB`Ug^5VuTUC^S_ z0n*SeGQHnOaWa6BL`i0!b!yw zdGy~$Do>(euL9@?e?FtWTMHp?{+n6u-zsAMcd!m~>-{kx?!)n{B#g5})rQ5N=P77c zIAc$$;ABn=E6?o2rnwANW@M;CaHLlaj*aqdxZ>f9>YENvs`I8Q%}w!yiZ7GzSHE?~9Ia zjHZAe3A0%00DUFt^I(mJ73KUu={%R(`MF{nNP2hQJEipjA`R{FmujsFR-i!?c4brca?8^^M{Zt&yC9f}VqCTPXrj z=&<5DSK9N`;(LG_9^gimE($}L*kjp&Z!yF)%2nhbq10v#-$SvXsfG}6lUx8TLnF8!SEfV-M&jY zXYf5ZPSd^f!eH#1!SwJ5^?;YYKVE+5>^x}TrC}q71&7B&jW0ycHkz!u=6% zFRH*oyw)lMB$N+Bdw%~-MrLK$?<#Jy(?2L8AdsVoSGA_fmxq~^ys9nGk3%C+1>7xr zTWdQQClEjHblX_jz2gfkn%>DZ?o0{f{j)C!tLpwfQ@D~0or1F!+8o;Z8+yi!y&g44 zlm&}q@$wp8BDzQ)tuxTl9>RyM&amKJfMIL$NF6yacglh(vsOx34!?5F>2del3>7Jy z-p{DIdW89kdiH9=+yee_17q6Yy z((!zaSkM)o&!CR=?nO^0B;-d^R(&aR+1sC~8XvDq=M{9>?zs;+59;XHXUAOK`;9*n}(Ctmd8zd zH-C@4RN|Z7p0l$)+n;|-O)VGKmX@akj+g2v)hmxrH(f6x6Mp?PHK6}+W2_4;G7y&j`QljD^ z92gy~t%C8hW`u{^?WV?VjRzhnl#VzxJX+tvf4(}ROS3OV^0zc$S7UMfFojzLUAYgu z6}93A5*13Vy76%n_S*^hjehF%vr1DeBqAn0IZC*B3zJyOcpyuK8^lZf~i5 z>%&FUFT2ob2VQO6#1re-@em+`o+Q|0#c^t$%j{@t=jNw>=!ohwz`r z(ItT&asT&^0@8d4@7=ps9?ggsq&gLYO1o?_|JC@aO|s7^zndt zdWI5h)wx@u*1yN83SIU_O{S%)>F9`qu literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/output_panel-light.png b/testdata/snapshots/darwin/output_panel-light.png new file mode 100644 index 0000000000000000000000000000000000000000..c95ef87614f1412c6e64bce3e7c73b43ebfd4f80 GIT binary patch literal 7084 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 literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/process_list-dark.png b/testdata/snapshots/darwin/process_list-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..a784437f0036296ee75754e11984b3bc1c55036c GIT binary patch literal 15261 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 literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/process_list-light.png b/testdata/snapshots/darwin/process_list-light.png new file mode 100644 index 0000000000000000000000000000000000000000..79711b44a0c000c5b67f468e15deb9c5af97dc7d GIT binary patch literal 14823 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

F}^5H#c`ds5v}b4)8`B+aq2OA0MBrEFw`< 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 literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/search_panel-light.png b/testdata/snapshots/darwin/search_panel-light.png new file mode 100644 index 0000000000000000000000000000000000000000..ac992e55eac780543915730b797119adf0ee0ece GIT binary patch literal 21072 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 literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/symbol_outline-dark.png b/testdata/snapshots/darwin/symbol_outline-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..b68cba4023df1bbe5eee231ce682689b91b8493f GIT binary patch literal 13607 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 ZCqN3GeOXINax4J+l9g1Fs1h>{{vX&w#8ChM literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/symbol_outline-light.png b/testdata/snapshots/darwin/symbol_outline-light.png new file mode 100644 index 0000000000000000000000000000000000000000..d46e177ec47a1db5c992f374c7354c2ab826b44d GIT binary patch literal 13268 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

Cb73}P-RS)zT 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 literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/terminal-light.png b/testdata/snapshots/darwin/terminal-light.png new file mode 100644 index 0000000000000000000000000000000000000000..31dc61419fed425b9677d79f8a0ab6240ca4aeef GIT binary patch literal 11584 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¨b&c+nHi4|R~|{%|60H%KDz}u!_e^9)^1&_ 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 literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/terminal_search-light.png b/testdata/snapshots/darwin/terminal_search-light.png new file mode 100644 index 0000000000000000000000000000000000000000..0af80e02ad0dbd7cc07b3d4d7bc08213f17d16bd GIT binary patch literal 7606 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 %o`| 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 H&4T|2@Mvn0 literal 0 HcmV?d00001 diff --git a/testdata/snapshots/darwin/variables_panel-light.png b/testdata/snapshots/darwin/variables_panel-light.png new file mode 100644 index 0000000000000000000000000000000000000000..a28c2ba4800838c52d142a3fd75322ce8dac63d9 GIT binary patch literal 8556 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 literal 0 HcmV?d00001 diff --git a/variables_panel.go b/variables_panel.go new file mode 100644 index 0000000..6e94759 --- /dev/null +++ b/variables_panel.go @@ -0,0 +1,158 @@ +package mujica + +import ( + "log/slog" + + "github.com/egoist/mygo/ui" + + "mujicaui/internal" + "mujicaui/theme" +) + +// Variable is a debugged value; Changed marks new values. +type Variable struct { + Name, Value, Type string + Changed bool + Children []Variable +} + +// VariablesPanelState is the open rows, choice and edit. +type VariablesPanelState struct { + outline ui.OutlineState[string] + cursor int + byPath map[string]*Variable + kids map[string][]string + roots []string + data *Variable + n int + editing string + draft string + started bool +} + +// VariablesPanelOptions configure a VariablesPanel. +type VariablesPanelOptions struct { + Disabled bool +} + +// VariableEdit is a typed value for a dotted Path. +type VariableEdit struct{ Path, Value string } + +// VariablesPanelResult carries a committed edit. +type VariablesPanelResult struct { + Element *ui.Element + Edited *VariableEdit +} + +// VariablesPanel shows an editable variable tree. +func VariablesPanel(c *ui.Context, vars []Variable, s *VariablesPanelState, opts VariablesPanelOptions) VariablesPanelResult { + if s == nil { + panic("mujica: VariablesPanel needs a state") + } + if !s.started { + s.started, s.cursor = true, -1 + } + variablesIndex(vars, s) + k := Tokens(c) + d := Density(c) + var res VariablesPanelResult + res.Element = codePanel(c).Disabled(opts.Disabled) + disabled := res.Element.IsDisabled() + if disabled { + s.editing = "" + } + res.Element.Children(func() { + if len(vars) == 0 { + codeCaption(c, msg(c, "variables_panel.empty")).Padding(d.Unit() * 4) + return + } + s.outline.List.Selected = &s.cursor + s.outline.List.Label = func(i int) string { return s.byPath[s.outline.Item(i)].Name } + children := func(p string) []string { return s.kids[p] } + restore := dataSelectTheme(c, 0) + e := ui.Outline(c, &s.outline, s.roots, children, func(p string) { + v := s.byPath[p] + ui.Row(c).MinHeight(d.ControlHeight() - 2*c.Theme().Space(1)).Gap(d.Unit() * 2).AlignItems(ui.Center).Grow(1).MinWidth(0).Children(func() { + codeMono(c, v.Name).TextColor(k.Info).Shrink(0) + if s.editing == p { + in := TextInput(c, &s.draft, TextInputOptions{Label: msg(c, "variables_panel.value")}) + in.Element.Grow(1).MinWidth(0) + in.Input.AutoFocus() + switch { + case in.Input.Shortcut(0, ui.KeyEscape): + s.editing = "" + case in.Submitted() && !disabled: + res.Edited = &VariableEdit{Path: p, Value: s.draft} + s.editing = "" + slog.Debug("mujica: variables_panel edit", "path", p) + } + return + } + variablesValue(c, v) + }) + }) + restore() + e.Grow(1).MinHeight(d.ControlHeight() * 3) + internal.Ring(e, k.Focus, theme.ControlRadius) + if disabled || s.editing != "" || s.cursor < 0 || s.cursor >= s.outline.Rows() { + return + } + p := s.outline.Item(s.cursor) + if len(s.kids[p]) == 0 && s.byPath[p].Children == nil && (e.Submitted() || e.Shortcut(0, ui.KeyF2)) { + s.editing, s.draft = p, s.byPath[p].Value + slog.Debug("mujica: variables_panel editing", "path", p) + } + }) + return res +} + +// variablesValue shows a value, marking changed ones. +func variablesValue(c *ui.Context, v *Variable) { + k := Tokens(c) + if v.Changed { + ui.Box(c).Size(7, 7).Shrink(0).Role(ui.RoleNone).Draw(func(p *ui.Painter, r ui.Rect) { + p.FillPath(internal.Diamond(r.X+r.W/2, r.Y+r.H/2, r.W), k.AccentText) + }) + } + val := codeMono(c, v.Value).MinWidth(0) + if v.Changed { + val.FontWeight(700).TextColor(k.AccentText).Description(msg(c, "variables_panel.changed")) + } + ui.Spacer(c) + if v.Type != "" { + codeCaption(c, v.Type).Shrink(0) + } +} + +// variablesIndex keys variables by dotted path. +func variablesIndex(vars []Variable, s *VariablesPanelState) { + var first *Variable + if len(vars) > 0 { + first = &vars[0] + } + if s.data == first && s.n == len(vars) && s.byPath != nil { + return + } + s.data, s.n = first, len(vars) + s.byPath, s.kids = map[string]*Variable{}, map[string][]string{} + var walk func(list []Variable, prefix string) []string + walk = func(list []Variable, prefix string) []string { + paths := []string{} + for i := range list { + p := list[i].Name + if prefix != "" { + p = prefix + "." + p + } + if _, dup := s.byPath[p]; dup { + panic("mujica: duplicate variable path " + p) + } + s.byPath[p] = &list[i] + if list[i].Children != nil { + s.kids[p] = walk(list[i].Children, p) + } + paths = append(paths, p) + } + return paths + } + s.roots = walk(vars, "") +} diff --git a/variables_panel_test.go b/variables_panel_test.go new file mode 100644 index 0000000..67bd40c --- /dev/null +++ b/variables_panel_test.go @@ -0,0 +1,115 @@ +package mujica + +import ( + "testing" + + "github.com/egoist/mygo/ui" +) + +func variablesSample() []Variable { + return []Variable{ + {Name: "decree", Type: "court.Decree", Value: "{…}", Children: []Variable{ + {Name: "id", Value: "42", Type: "int", Changed: true}, + {Name: "title", Value: `"Of seals"`, Type: "string"}, + }}, + {Name: "count", Value: "7", Type: "int"}, + } +} + +type variablesProbe struct { + vars []Variable + s VariablesPanelState + disabled bool + edits []VariableEdit +} + +func (p *variablesProbe) view(c *ui.Context) { + Use(c, Settings{}) + r := VariablesPanel(c, p.vars, &p.s, VariablesPanelOptions{Disabled: p.disabled}) + r.Element.Height(300) + if r.Edited != nil { + p.edits = append(p.edits, *r.Edited) + } +} + +func TestVariablesPanelExpandAndChanged(t *testing.T) { + p := &variablesProbe{vars: variablesSample()} + tt := ui.NewTester(p.view, 460, 320) + if tt.HasText("title") { + t.Fatal("children shown before expanding") + } + tt.Key(0, ui.KeyTab) + tt.Key(0, ui.KeyDown) + tt.Key(0, ui.KeyRight) + if !tt.HasText("title") || !tt.HasText("42") { + t.Fatalf("Right did not expand: %q", tt.Texts()) + } + if !p.s.byPath["decree.id"].Changed { + t.Fatal("changed flag lost") + } +} + +func TestVariablesPanelEditLeaf(t *testing.T) { + p := &variablesProbe{vars: variablesSample()} + tt := ui.NewTester(p.view, 460, 320) + tt.Key(0, ui.KeyTab) + tt.Key(0, ui.KeyEnd) + tt.Key(0, ui.KeyF2) + if !tt.Focused("Value") { + t.Fatal("F2 did not open the editor") + } + tt.Key(ui.Cmd, ui.KeyA) + tt.Type("8") + tt.Key(0, ui.KeyEnter) + if len(p.edits) != 1 || p.edits[0] != (VariableEdit{"count", "8"}) { + t.Fatalf("edits %v", p.edits) + } + // Enter on a branch does not edit it. + tt.Key(0, ui.KeyHome) + tt.Key(0, ui.KeyEnter) + if tt.Focused("Value") { + t.Fatal("a branch opened the editor") + } +} + +func TestVariablesPanelEscapeAndDisabled(t *testing.T) { + p := &variablesProbe{vars: variablesSample()} + tt := ui.NewTester(p.view, 460, 320) + tt.Key(0, ui.KeyTab) + tt.Key(0, ui.KeyEnd) + tt.Key(0, ui.KeyEnter) + tt.Type("x") + tt.Key(0, ui.KeyEscape) + if len(p.edits) != 0 || tt.Focused("Value") { + t.Fatal("Escape committed") + } + p = &variablesProbe{vars: variablesSample(), disabled: true} + tt = ui.NewTester(p.view, 460, 320) + tt.Key(0, ui.KeyTab) + tt.Key(0, ui.KeyF2) + if tt.Focused("Value") { + t.Fatal("disabled panel opened the editor") + } +} + +func TestVariablesPanelDuplicatePanics(t *testing.T) { + defer func() { + if recover() == nil { + t.Fatal("duplicate names did not panic") + } + }() + variablesIndex([]Variable{{Name: "a"}, {Name: "a"}}, &VariablesPanelState{}) +} + +func TestVariablesPanelSnapshot(t *testing.T) { + snapshot(t, "variables_panel", 420, 180, func(c *ui.Context) { + p := ui.Local(c.Root(), "snap", func() *variablesProbe { + p := &variablesProbe{vars: variablesSample()} + p.s.outline.Open.Add("decree") + return p + }) + ui.Column(c).Padding(12).Children(func() { + VariablesPanel(c, (*p).vars, &(*p).s, VariablesPanelOptions{}).Element.Height(156) + }) + }) +}