diff --git a/cmd/sst/mosaic/multiplexer/multiplexer.go b/cmd/sst/mosaic/multiplexer/multiplexer.go index d59a65c5a7..b5d0db8ebc 100644 --- a/cmd/sst/mosaic/multiplexer/multiplexer.go +++ b/cmd/sst/mosaic/multiplexer/multiplexer.go @@ -31,8 +31,11 @@ type Multiplexer struct { main *views.ViewPort stack *views.BoxLayout - dragging bool - click *tcell.EventMouse + dragging bool + click *tcell.EventMouse + scrollDirection int // -1 for up, 0 for none, 1 for down + scrollStop chan struct{} + lastMouseX int } func New() (*Multiplexer, error) { @@ -102,6 +105,21 @@ func (s *Multiplexer) Start() { shouldBreak = true return + case *EventScrollTick: + if s.dragging && s.scrollDirection != 0 { + if selected != nil && selected.scrollable() { + if s.scrollDirection < 0 { + selected.scrollUp(1) + selected.vt.SelectEnd(s.lastMouseX, 0) + } else { + selected.scrollDown(1) + selected.vt.SelectEnd(s.lastMouseX, s.height-1) + } + s.draw() + } + } + return + case *EventProcess: for _, p := range s.processes { if p.key == evt.Key { @@ -154,6 +172,7 @@ func (s *Multiplexer) Start() { s.copy() } s.dragging = false + s.stopScrollTimer() return } if evt.Buttons()&tcell.ButtonPrimary != 0 { @@ -193,8 +212,18 @@ func (s *Multiplexer) Start() { } s.click = evt offsetX := x - SIDEBAR_WIDTH - 1 + s.lastMouseX = offsetX if s.dragging { selected.vt.SelectEnd(offsetX, y) + if y <= 0 && selected.scrollable() { + selected.scrollUp(1) + s.startScrollTimer(-1) + } else if y >= s.height-1 { + selected.scrollDown(1) + s.startScrollTimer(1) + } else { + s.stopScrollTimer() + } } if !s.dragging { s.dragging = true @@ -335,10 +364,48 @@ func (e *EventExit) When() time.Time { return e.when } +type EventScrollTick struct { + when time.Time +} + +func (e *EventScrollTick) When() time.Time { + return e.when +} + func (s *Multiplexer) Exit() { s.screen.PostEvent(&EventExit{}) } +func (s *Multiplexer) startScrollTimer(direction int) { + if s.scrollDirection == direction && s.scrollStop != nil { + return + } + s.stopScrollTimer() + s.scrollDirection = direction + s.scrollStop = make(chan struct{}) + + go func() { + ticker := time.NewTicker(50 * time.Millisecond) // ~20 scrolls per second + defer ticker.Stop() + for { + select { + case <-s.scrollStop: + return + case <-ticker.C: + s.screen.PostEvent(&EventScrollTick{when: time.Now()}) + } + } + }() +} + +func (s *Multiplexer) stopScrollTimer() { + if s.scrollStop != nil { + close(s.scrollStop) + s.scrollStop = nil + } + s.scrollDirection = 0 +} + func (s *Multiplexer) scrollDown(n int) { selected := s.selectedProcess() if selected == nil { diff --git a/cmd/sst/mosaic/multiplexer/tcell-term/vt.go b/cmd/sst/mosaic/multiplexer/tcell-term/vt.go index 34f951381e..0251c64353 100644 --- a/cmd/sst/mosaic/multiplexer/tcell-term/vt.go +++ b/cmd/sst/mosaic/multiplexer/tcell-term/vt.go @@ -67,11 +67,10 @@ type VT struct { } type selection struct { - content strings.Builder - startX int - startY int - endX int - endY int + startX int + startY int + endX int + endY int } type cursorState struct { @@ -523,7 +522,6 @@ func (vt *VT) Draw() { return } offset := 0 - vt.selection.content = strings.Builder{} if vt.IsScrolling() { for x := vt.scroll; x < len(vt.primaryScrollback); x += 1 { if offset >= vt.height() { @@ -547,7 +545,6 @@ func (vt *VT) drawRow(row int, cols []cell) { if vt.scroll != -1 { scrollOffset = vt.scroll } - builder := strings.Builder{} for col := 0; col < len(cols); { cell := cols[col] w := cell.width @@ -559,7 +556,6 @@ func (vt *VT) drawRow(row int, cols []cell) { style := cell.attrs if vt.selection != nil && isCellSelected(col, row+scrollOffset, vt.selection.startX, vt.selection.startY, vt.selection.endX, vt.selection.endY) { style = style.Reverse(true) - builder.WriteRune(content) } vt.surface.SetContent(col, row, content, cell.combining, style) if w == 0 { @@ -567,10 +563,6 @@ func (vt *VT) drawRow(row int, cols []cell) { } col += 1 } - if builder.Len() > 0 { - vt.selection.content.WriteString(strings.TrimRight(builder.String(), " ")) - vt.selection.content.WriteRune('\n') - } } func (vt *VT) HandleEvent(e tcell.Event) bool { @@ -603,7 +595,72 @@ func (vt *VT) Clear() { } func (vt *VT) Copy() string { - return strings.TrimRightFunc(vt.selection.content.String(), unicode.IsSpace) + if !vt.HasSelection() { + return "" + } + + // Normalize selection coordinates + startX, startY, endX, endY := vt.selection.startX, vt.selection.startY, vt.selection.endX, vt.selection.endY + if endY < startY || (endY == startY && endX < startX) { + startX, startY, endX, endY = endX, endY, startX, startY + } + + var builder strings.Builder + + // Determine which rows to iterate through + totalScrollback := len(vt.primaryScrollback) + + for y := startY; y <= endY; y++ { + var row []cell + + // Get the row from either scrollback or active screen + if y < totalScrollback { + row = vt.primaryScrollback[y] + } else { + screenY := y - totalScrollback + if screenY < len(vt.activeScreen) { + row = vt.activeScreen[screenY] + } else { + continue + } + } + + // Determine the column range for this row + colStart := 0 + colEnd := len(row) - 1 + + if y == startY { + colStart = startX + } + if y == endY { + colEnd = endX + } + + // Extract text from this row + rowBuilder := strings.Builder{} + for x := colStart; x <= colEnd && x < len(row); x++ { + cell := row[x] + content := cell.content + if content == '\x00' { + content = ' ' + } + rowBuilder.WriteRune(content) + for _, comb := range cell.combining { + rowBuilder.WriteRune(comb) + } + } + + // Add the row text, trimming trailing spaces + rowText := strings.TrimRight(rowBuilder.String(), " ") + if rowText != "" || y < endY { + builder.WriteString(rowText) + if y < endY { + builder.WriteRune('\n') + } + } + } + + return strings.TrimRightFunc(builder.String(), unicode.IsSpace) } func (vt *VT) SelectStart(x int, y int) {