package modes import ( "fmt" "strings" "github.com/patriceckhart/zot/packages/core" "github.com/patriceckhart/zot/packages/tui" ) // sessionTreeDialog renders the branch topology for the current // cwd (parents, children, siblings) and lets the user pick another // session file to switch into. Reached via /session tree. // // Layout is a single flat list with indentation-based nesting so // we can reuse the simple up/down cursor model the other dialogs // use. The current session is tagged with a muted "[current]". type sessionTreeDialog struct { active bool items []treeItem cursor int current string // path of the session we're currently in (not selectable) } // treeItem is one visible row. Depth drives the indent; Path is the // on-disk session file to swap to when the user hits enter. type treeItem struct { label string path string depth int isCur bool } type sessionTreeAction struct { Select bool Path string Close bool } func newSessionTreeDialog() *sessionTreeDialog { return &sessionTreeDialog{} } // Open flattens the given forest into indented rows. currentPath // is highlighted and non-selectable (enter on it closes the dialog). func (d *sessionTreeDialog) Open(roots []*core.TreeNode, currentPath string) bool { items := flattenTree(roots, currentPath) if len(items) == 0 { return false } d.items = items d.current = currentPath d.cursor = indexOfCurrent(items, currentPath) d.active = true return true } // Close hides the dialog. func (d *sessionTreeDialog) Close() { d.active = false } // Active reports whether the dialog consumes input. func (d *sessionTreeDialog) Active() bool { return d != nil && d.active } // HandleKey advances the cursor or resolves the selection. func (d *sessionTreeDialog) HandleKey(k tui.Key) sessionTreeAction { switch k.Kind { case tui.KeyUp: if d.cursor > 0 { d.cursor-- } case tui.KeyDown: if d.cursor < len(d.items)-1 { d.cursor++ } case tui.KeyEsc: d.Close() return sessionTreeAction{Close: true} case tui.KeyEnter: if len(d.items) == 0 || d.cursor < 0 || d.cursor >= len(d.items) { d.Close() return sessionTreeAction{Close: true} } it := d.items[d.cursor] d.Close() if it.isCur { return sessionTreeAction{Close: true} } return sessionTreeAction{Select: true, Path: it.path} } return sessionTreeAction{} } // Render returns the dialog lines. func (d *sessionTreeDialog) Render(th tui.Theme, width int) []string { if !d.Active() { return nil } var lines []string lines = append(lines, frameHeader(th, "session tree", width)) lines = append(lines, th.FG256(th.Muted, "pick a branch to switch to (\u2191/\u2193, enter, esc to cancel):")) for i, it := range d.items { indent := strings.Repeat(" ", it.depth) label := " " + indent + it.label if it.isCur { label += " " + th.FG256(th.Muted, "[current]") } if i == d.cursor { lines = append(lines, th.PadHighlight(label, width)) } else { lines = append(lines, th.FG256(th.Muted, label)) } } lines = append(lines, frameRule(th, width)) return lines } // flattenTree walks the forest depth-first and returns one treeItem // per node. Each label has the shape " (N msgs)". func flattenTree(roots []*core.TreeNode, currentPath string) []treeItem { var out []treeItem var walk func(n *core.TreeNode, depth int) walk = func(n *core.TreeNode, depth int) { label := formatTreeRow(n) out = append(out, treeItem{ label: label, path: n.Summary.Path, depth: depth, isCur: n.Summary.Path == currentPath, }) for _, c := range n.Children { walk(c, depth+1) } } for _, r := range roots { walk(r, 0) } return out } // formatTreeRow renders one node's label line. Tries to fit a // compact "date preview (N msgs, $cost)" shape; falls back to // the meta id suffix when everything else is empty. func formatTreeRow(n *core.TreeNode) string { when := formatRelative(n.Summary.Started) preview := strings.TrimSpace(n.Summary.FirstUserText) if preview == "" { if n.Meta.ID != "" && len(n.Meta.ID) >= 8 { preview = "(" + n.Meta.ID[:8] + ")" } else { preview = "(empty)" } } if len(preview) > 50 { preview = preview[:47] + "..." } return fmt.Sprintf("%-14s %s %d msgs", when, preview, n.Summary.MessageCount) } // indexOfCurrent returns the flat-list index of the row whose path // matches currentPath. -1 when not found; the caller defaults to 0. func indexOfCurrent(items []treeItem, currentPath string) int { for i, it := range items { if it.path == currentPath { return i } } return 0 }