package app import ( "fmt" "os" "strings" ) const ( sidebarCols = 28 statusRows = 1 ) // drawSidebar paints the right-rail session tree + scratchpad list. // SPEC §4: the rail is the active session's child hierarchy on top and // the scratchpad list (with preview) on the bottom. // // Implementation note: the PTY child's winsize is constrained to the // computed main viewport, so the sidebar region is outside the child's // cursor range. We can redraw freely without fighting the child for cells. func (st *uiState) drawSidebar() { st.mu.Lock() palOpen := st.palette != nil focus := st.focusedID st.mu.Unlock() if palOpen { return } layout := st.layoutSnapshot() if !layout.sidebarVisible || layout.hostRows < 4 { return } left := int(layout.sidebarLeft) width := int(layout.sidebarWidth) - 1 maxRow := int(layout.statusRow) - statusRows var b strings.Builder // Border column at left-1: a single vertical pipe. for r := 1; r <= maxRow; r++ { fmt.Fprintf(&b, "\x1b[%d;%dH\x1b[2m│\x1b[0m", r, left-1) } row := 1 writeLine := func(s string, style string) { if row > maxRow { return } if len(s) > width { s = s[:width] } fmt.Fprintf(&b, "\x1b[%d;%dH%s%s\x1b[0m\x1b[K", row, left, style, padRight(s, width)) row++ } writeLine(" Session tree", "\x1b[1m") writeLine(strings.Repeat("─", width-1), "\x1b[2m") children := visibleSessionTree(st.sess.Children(), focus) if len(children) == 0 { writeLine(" (empty)", "\x1b[2m") } for _, c := range children { glyph := "◉" marker := " " if c.ID == focus { marker = "▶ " } indent := "" if c.ParentID != "" { indent = " " } line := fmt.Sprintf(" %s%s%s %s", marker, indent, glyph, c.Name) style := "" if c.ID == focus { style = "\x1b[1m" } writeLine(line, style) } // Scratchpads list — pick the most-recently-modified one as the // preview target. SPEC §4. var previewName string if row+2 <= maxRow { row++ writeLine(" Scratchpads", "\x1b[1m") writeLine(strings.Repeat("─", width-1), "\x1b[2m") entries, err := st.pads.List() if err == nil { if len(entries) == 0 { writeLine(" (none)", "\x1b[2m") } var newest string var newestTS string for _, e := range entries { if e.ModifiedAt > newestTS { newestTS = e.ModifiedAt newest = e.Name } } previewName = newest for _, e := range entries { if row > maxRow { break } marker := " " style := "" if e.Name == previewName { marker = " ▸ " style = "\x1b[1m" } writeLine(marker+e.Name, style) } } } // Preview pane at the bottom of the rail. Reserve up to 8 rows. if previewName != "" && row+2 <= maxRow { row++ writeLine(strings.Repeat("─", width-1), "\x1b[2m") writeLine(" "+previewName, "\x1b[1m") content, _, err := st.pads.Read(previewName) if err == nil { for _, line := range strings.Split(content, "\n") { if row > maxRow { break } writeLine(" "+line, "\x1b[2m") } } } // Blank-fill any rows the rail content didn't cover so stale // content from a previous redraw doesn't linger. for row <= maxRow { writeLine("", "") } st.outMu.Lock() // Save cursor; emit the sidebar; restore. fmt.Fprintf(os.Stdout, "\x1b7%s\x1b8", b.String()) st.outMu.Unlock() }