fix(copy): report oversized selections
- commit
- 3d26bab6e1c787ec6b89703fda3ab348d70ecd26
- author
- brice · · 2026-07-03 04:51 UTC
- parent
- 3b80c55
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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) { | |||
| 954 | 954 | return | |
| 955 | 955 | } | |
| 956 | 956 | } | |
| 957 | + | ac.copyMu.Lock() | |
| 957 | 958 | if len(chunks) > 0 { | |
| 958 | - | ac.copyMu.Lock() | |
| 959 | 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 | 965 | if exit { | |
| 964 | 966 | d.paint(sess, ac, true) | |
| @@ -954,11 +954,13 @@ func (d *Daemon) handleCopyInput(ac *attachedClient, data []byte) { | |||||
| 954 | return | 954 | 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) { | |||
| 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 | 539 | func TestCopyModeEmptyYankDoesNotClearClipboard(t *testing.T) { | |
| 517 | 540 | cases := []struct { | |
| 518 | 541 | 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 string | 541 | name string | ||