detached 3d26bab

fix(copy): report oversized selections

commit
3d26bab6e1c787ec6b89703fda3ab348d70ecd26
author
brice · · 2026-07-03 04:51 UTC
parent
3b80c55
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +27 -2
M internal/usecase/daemon/daemon.go +4 -2
@@ -954,11 +954,13 @@ func (d *Daemon) handleCopyInput(ac *attachedClient, data []byte) {
954954 return
955955 }
956956 }
957+ ac.copyMu.Lock()
957958 if len(chunks) > 0 {
958- ac.copyMu.Lock()
959959 ac.copyFeedback = "copied " + strconv.Itoa(len([]rune(text))) + " chars to clipboard"
960- ac.copyMu.Unlock()
960+ } else {
961+ ac.copyFeedback = "selection too large to copy"
961962 }
963+ ac.copyMu.Unlock()
962964 }
963965 if exit {
964966 d.paint(sess, ac, true)
@@ -954,11 +954,13 @@ func (d *Daemon) handleCopyInput(ac *attachedClient, data []byte) {
954 return954 return
955 }955 }
956 }956 }
957+ ac.copyMu.Lock()
957 if len(chunks) > 0 {958 if len(chunks) > 0 {
958- ac.copyMu.Lock()
959 ac.copyFeedback = "copied " + strconv.Itoa(len([]rune(text))) + " chars to clipboard"959 ac.copyFeedback = "copied " + strconv.Itoa(len([]rune(text))) + " chars to clipboard"
960- ac.copyMu.Unlock()960+ } else {
961+ ac.copyFeedback = "selection too large to copy"
961 }962 }
963+ ac.copyMu.Unlock()
962 }964 }
963 if exit {965 if exit {
964 d.paint(sess, ac, true)966 d.paint(sess, ac, true)
M internal/usecase/daemon/daemon_test.go +23 -0
@@ -513,6 +513,29 @@ func TestCopyModeSplitArrowDoesNotExit(t *testing.T) {
513513 }
514514 }
515515
516+func TestCopyModeOversizedYankShowsTooLargeFeedback(t *testing.T) {
517+ p, _ := newBlockingPTY(t)
518+ d, sess, ac, sends := newManualSessionWithPTYs(t, p)
519+ sess.tabs[0].scrollback = scopy.NewScrollback(1)
520+ longLine := strings.Repeat("x", scopy.OSC52MaxPayloadBytes+1)
521+ sess.tabs[0].screen.Frame = renderer.NewFrame(len(longLine), 1)
522+ copy(sess.tabs[0].screen.Frame.Row(0), testRow(longLine))
523+
524+ d.enterCopyMode(sess, ac)
525+ awaitFrame(t, sends, ports.MsgOutput)
526+ d.handleInput(sess, ac, []byte{' ', 'y'})
527+
528+ out := awaitFrame(t, sends, ports.MsgOutput)
529+ msg, err := ports.UnmarshalOutput(out.Payload)
530+ require.NoError(t, err)
531+ if strings.Contains(string(msg.Data), "\x1b]52;") {
532+ t.Fatalf("oversized yank emitted OSC52: %q", string(msg.Data))
533+ }
534+ if !strings.Contains(string(msg.Data), "selection too large to copy") {
535+ t.Fatalf("oversized yank repaint = %q, want too-large feedback", string(msg.Data))
536+ }
537+}
538+
516539 func TestCopyModeEmptyYankDoesNotClearClipboard(t *testing.T) {
517540 cases := []struct {
518541 name string
@@ -513,6 +513,29 @@ func TestCopyModeSplitArrowDoesNotExit(t *testing.T) {
513 }513 }
514 }514 }
515 515
516+func TestCopyModeOversizedYankShowsTooLargeFeedback(t *testing.T) {
517+ p, _ := newBlockingPTY(t)
518+ d, sess, ac, sends := newManualSessionWithPTYs(t, p)
519+ sess.tabs[0].scrollback = scopy.NewScrollback(1)
520+ longLine := strings.Repeat("x", scopy.OSC52MaxPayloadBytes+1)
521+ sess.tabs[0].screen.Frame = renderer.NewFrame(len(longLine), 1)
522+ copy(sess.tabs[0].screen.Frame.Row(0), testRow(longLine))
523+
524+ d.enterCopyMode(sess, ac)
525+ awaitFrame(t, sends, ports.MsgOutput)
526+ d.handleInput(sess, ac, []byte{' ', 'y'})
527+
528+ out := awaitFrame(t, sends, ports.MsgOutput)
529+ msg, err := ports.UnmarshalOutput(out.Payload)
530+ require.NoError(t, err)
531+ if strings.Contains(string(msg.Data), "\x1b]52;") {
532+ t.Fatalf("oversized yank emitted OSC52: %q", string(msg.Data))
533+ }
534+ if !strings.Contains(string(msg.Data), "selection too large to copy") {
535+ t.Fatalf("oversized yank repaint = %q, want too-large feedback", string(msg.Data))
536+ }
537+}
538+
516 func TestCopyModeEmptyYankDoesNotClearClipboard(t *testing.T) {539 func TestCopyModeEmptyYankDoesNotClearClipboard(t *testing.T) {
517 cases := []struct {540 cases := []struct {
518 name string541 name string

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close