package app import ( "fmt" "os" "strings" "unicode/utf8" ) // Three-row tab bar: labels row, subtitle row, underline row. The PTY // viewport's top row is therefore mainTop == tabBarRows + 1. const tabBarRows = 3 // drawTabBar renders the top tab strip across the full host width. The // strip has three rows: labels (with horizontal padding), a dim // subtitle showing each child's argv, and an underline that's thick + // accent for the focused tab and faint for the rest. Subtitles are // truncated with `…` to the tab's width. func (st *uiState) drawTabBar() { st.mu.Lock() palOpen := st.palette != nil focus := st.focusedID st.mu.Unlock() if palOpen { return } layout := st.layoutSnapshot() width := int(layout.childCols()) if width < 8 { return } var sessions []*Child for _, c := range st.sess.Children() { if c.ParentID == "" && c.Status() == StatusRunning { sessions = append(sessions, c) } } type tabRect struct { startCol int width int label string subtitle string active bool } const ( leadingPad = 2 // host columns before the first tab tabPad = 2 // spaces on each side of the label inside the tab tabGap = 1 // gap columns between adjacent tabs tailReserve = 8 // reserve room for the trailing "+ new" hint ) tabs := make([]tabRect, 0, len(sessions)) cur := leadingPad + 1 for _, c := range sessions { label := c.Name labelW := utf8.RuneCountInString(label) tabW := labelW + tabPad*2 // If the tab won't fit, try truncating the label down to whatever // space is left (label still has to leave room for "…"). if cur+tabW+tabGap+tailReserve > width+1 { avail := width + 1 - cur - tabGap - tailReserve - tabPad*2 if avail < 3 { break } label = clipRunes(label, avail-1) + "…" labelW = utf8.RuneCountInString(label) tabW = labelW + tabPad*2 tabs = append(tabs, tabRect{ startCol: cur, width: tabW, label: label, subtitle: strings.Join(c.Argv, " "), active: c.ID == focus, }) cur += tabW + tabGap break } tabs = append(tabs, tabRect{ startCol: cur, width: tabW, label: label, subtitle: strings.Join(c.Argv, " "), active: c.ID == focus, }) cur += tabW + tabGap } var b strings.Builder // Clear all three rows up front so a stale label from the previous // frame can't bleed through. b.WriteString("\x1b[1;1H\x1b[2K") b.WriteString("\x1b[2;1H\x1b[2K") b.WriteString("\x1b[3;1H\x1b[2K") for _, t := range tabs { // Row 1: label fmt.Fprintf(&b, "\x1b[1;%dH", t.startCol) if t.active { b.WriteString(styleActive) } else { b.WriteString(styleHint) } b.WriteString(strings.Repeat(" ", tabPad)) b.WriteString(t.label) b.WriteString(strings.Repeat(" ", tabPad)) b.WriteString(styleReset) // Row 2: subtitle, truncated to tab width and dimmed. sub := t.subtitle if utf8.RuneCountInString(sub) > t.width { if t.width > 1 { sub = clipRunes(sub, t.width-1) + "…" } else { sub = "" } } padR := t.width - utf8.RuneCountInString(sub) if padR < 0 { padR = 0 } fmt.Fprintf(&b, "\x1b[2;%dH%s%s%s%s", t.startCol, styleDim, sub, strings.Repeat(" ", padR), styleReset) // Row 3: underline. Thick accent for the active tab, faint // border for the rest. fmt.Fprintf(&b, "\x1b[3;%dH", t.startCol) if t.active { b.WriteString(styleAccent) b.WriteString(strings.Repeat("━", t.width)) } else { b.WriteString(styleBorder) b.WriteString(strings.Repeat("─", t.width)) } b.WriteString(styleReset) } // "+ new" hint at the end of the labels row, in dim. if cur+3 <= width { fmt.Fprintf(&b, "\x1b[1;%dH%s+ new%s", cur+1, styleDim, styleReset) } // Extend the faint underline across the rest of the host width so // the tab strip reads as one continuous divider. if cur <= width { remain := width - cur + 1 if remain > 0 { fmt.Fprintf(&b, "\x1b[3;%dH%s%s%s", cur, styleBorder, strings.Repeat("─", remain), styleReset) } } if leadingPad > 0 { fmt.Fprintf(&b, "\x1b[3;1H%s%s%s", styleBorder, strings.Repeat("─", leadingPad), styleReset) } st.outMu.Lock() defer st.outMu.Unlock() fmt.Fprintf(os.Stdout, "\x1b7%s\x1b8", b.String()) }