Marquee long sidebar names; truncate with ellipsis otherwise
Sidebar rows that overflow the rail width used to spill characters into the main viewport. They now truncate with a trailing "…" when unfocused (or when the focused name still fits). The focused row whose name overflows runs a pause-scroll-pause marquee: 1 s hold on the head, ~150 ms per cell scroll, 1 s hold on the tail, snap back. The row's geometry never moves while it animates, so nothing below shifts. A dedicated 150 ms goroutine flips sidebarDirty only while a row is actively animating; the chrome ticker does the actual repaint. Idle is a single cheap wakeup. focus / spawn / exit / restart all reset the marquee state so the new focused row starts from frame zero. When the row's budget is tight, the trailing timer indicator drops before the name ellipses since the name is the only identifier the row carries. clampVisible() is a defensive net inside write(): even if a row's decoration size were mis-computed, it will not spill past the sidebar band into the PTY area.
This commit is contained in:
@@ -306,6 +306,28 @@ func Run(ctx context.Context, opts Options) error {
|
||||
}
|
||||
}()
|
||||
|
||||
// Marquee ticker: while a focused sidebar row's name overflows the
|
||||
// rail width, advance the pause-scroll-pause animation by marking
|
||||
// the sidebar dirty every marqueeStep. The chrome ticker above does
|
||||
// the actual repaint. When no row is animating, this is a single
|
||||
// cheap wakeup with no work.
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
ticker := time.NewTicker(marqueeStep)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
}
|
||||
if st.marquee.active() {
|
||||
st.markSidebarDirty()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// External termination: SPEC §2 step 4 (SIGTERM/SIGHUP → graceful exit).
|
||||
wg.Add(1)
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
@@ -436,6 +458,11 @@ type uiState struct {
|
||||
sidebarDirty atomic.Bool
|
||||
chromeWake chan struct{}
|
||||
|
||||
// marquee animates the focused sidebar row's name when it overflows
|
||||
// the rail width. The dedicated 150ms ticker below flips
|
||||
// sidebarDirty while a row is animating; idle case is free.
|
||||
marquee marqueeState
|
||||
|
||||
// padsCacheMu guards the cached scratchpad listing. The sidebar
|
||||
// and palette/sidebar nav helpers read it on every chunk-driven
|
||||
// repaint; the cache invalidates in scratchpadsChanged() which is
|
||||
@@ -476,6 +503,7 @@ func (st *uiState) focusProcess(processID string) {
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
st.marquee.reset()
|
||||
layout := st.layoutSnapshot()
|
||||
onAlt := childIsOnAlt(c)
|
||||
st.mu.Lock()
|
||||
@@ -543,6 +571,7 @@ func (st *uiState) focusScratchpad(name string) {
|
||||
if name == "" {
|
||||
return
|
||||
}
|
||||
st.marquee.reset()
|
||||
st.mu.Lock()
|
||||
if st.padOffsetName != name {
|
||||
st.padOffset = 0
|
||||
@@ -586,6 +615,7 @@ func (st *uiState) restartFocusedCommand(processID string) {
|
||||
if c == nil || c.Kind != KindCommand {
|
||||
return
|
||||
}
|
||||
st.marquee.reset()
|
||||
layout := st.layoutSnapshot()
|
||||
renderer := newViewportRenderer(layout)
|
||||
st.mu.Lock()
|
||||
@@ -672,6 +702,7 @@ func (st *uiState) scratchpadsChanged() {
|
||||
|
||||
// OnChildSpawned auto-focuses the new child.
|
||||
func (st *uiState) OnChildSpawned(c *Child) {
|
||||
st.marquee.reset()
|
||||
layout := st.layoutSnapshot()
|
||||
onAlt := childIsOnAlt(c)
|
||||
st.mu.Lock()
|
||||
@@ -733,6 +764,7 @@ func (st *uiState) OnChildStateChanged(string, IdleState) {
|
||||
// focused child.
|
||||
func (st *uiState) OnChildExited(c *Child) {
|
||||
st.lastExit.Store(int32(c.ExitCode()))
|
||||
st.marquee.reset()
|
||||
layout := st.layoutSnapshot()
|
||||
renderEmpty := false
|
||||
st.mu.Lock()
|
||||
|
||||
Reference in New Issue
Block a user