detached 46853d3

fix(copy): address PR review feedback

commit
46853d3f5de28f7053abe4c83730e02f045f292c
author
brice · · 2026-07-03 05:07 UTC
parent
f38c91b
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +25 -4
M README.md +1 -1
@@ -64,7 +64,7 @@ Scrollback mode freezes a view over history while the program keeps running unde
6464 | y or Enter | copy selection and exit |
6565 | q, Esc, Ctrl-C | exit without copying |
6666
67-Copying uses OSC 52, so the selection lands in your terminal's clipboard even across SSH. After a successful copy, the right side of the status bar reports `copied N chars to clipboard` until the next screen update.
67+Copying uses OSC 52, so the selection lands in your terminal's clipboard even across SSH. After a successful copy, the right side of the status bar reports `copied N chars to clipboard` until the next screen update. If the selection exceeds the OSC 52 copy limit, the status bar reports `selection too large to copy` instead.
6868
6969 ## Mouse
7070
@@ -64,7 +64,7 @@ Scrollback mode freezes a view over history while the program keeps running unde
64 | y or Enter | copy selection and exit |64 | y or Enter | copy selection and exit |
65 | q, Esc, Ctrl-C | exit without copying |65 | q, Esc, Ctrl-C | exit without copying |
66 66
67-Copying uses OSC 52, so the selection lands in your terminal's clipboard even across SSH. After a successful copy, the right side of the status bar reports `copied N chars to clipboard` until the next screen update.67+Copying uses OSC 52, so the selection lands in your terminal's clipboard even across SSH. After a successful copy, the right side of the status bar reports `copied N chars to clipboard` until the next screen update. If the selection exceeds the OSC 52 copy limit, the status bar reports `selection too large to copy` instead.
68 68
69 ## Mouse69 ## Mouse
70 70
M internal/usecase/daemon/daemon.go +8 -3
@@ -240,6 +240,11 @@ func (d *Daemon) boundedSendErr(ac *attachedClient, f ports.Frame) error {
240240 timer.Stop()
241241 return err
242242 case <-timer.C():
243+ select {
244+ case err := <-result:
245+ return err
246+ default:
247+ }
243248 _ = ac.tr.Close()
244249 return errors.New("send timed out")
245250 }
@@ -1801,11 +1806,11 @@ func drawStatus(row []renderer.Cell, sess *session, rightText string) {
18011806 return
18021807 }
18031808 style := renderer.DefaultStyle()
1804- start := max(len(row)-len([]rune(rightText))-1, x+1)
1805- if start >= len(row) {
1809+ rightWidth := len([]rune(rightText)) + 1
1810+ if len(row)-x-1 < rightWidth {
18061811 return
18071812 }
1808- x = start
1813+ x = len(row) - rightWidth
18091814 writeStatusText(row, &x, " "+rightText, style)
18101815 }
18111816
@@ -240,6 +240,11 @@ func (d *Daemon) boundedSendErr(ac *attachedClient, f ports.Frame) error {
240 timer.Stop()240 timer.Stop()
241 return err241 return err
242 case <-timer.C():242 case <-timer.C():
243+ select {
244+ case err := <-result:
245+ return err
246+ default:
247+ }
243 _ = ac.tr.Close()248 _ = ac.tr.Close()
244 return errors.New("send timed out")249 return errors.New("send timed out")
245 }250 }
@@ -1801,11 +1806,11 @@ func drawStatus(row []renderer.Cell, sess *session, rightText string) {
1801 return1806 return
1802 }1807 }
1803 style := renderer.DefaultStyle()1808 style := renderer.DefaultStyle()
1804- start := max(len(row)-len([]rune(rightText))-1, x+1)1809+ rightWidth := len([]rune(rightText)) + 1
1805- if start >= len(row) {1810+ if len(row)-x-1 < rightWidth {
1806 return1811 return
1807 }1812 }
1808- x = start1813+ x = len(row) - rightWidth
1809 writeStatusText(row, &x, " "+rightText, style)1814 writeStatusText(row, &x, " "+rightText, style)
1810 }1815 }
1811 1816
M internal/usecase/daemon/daemon_test.go +16 -0
@@ -1241,6 +1241,22 @@ func TestStatusCompositionGolden(t *testing.T) {
12411241 }
12421242 }
12431243
1244+func TestStatusCopyFeedbackRendersOnlyWhenFullyFits(t *testing.T) {
1245+ p, releasePTY := newBlockingPTY(t)
1246+ _, sess, _, _ := newManualSessionWithPTYs(t, p)
1247+ defer releasePTY()
1248+ sess.name = "work"
1249+ win := sess.activeTab()
1250+ win.screen = vt.NewScreen(30, 2)
1251+ win.size = domain.Size{Cols: 30, Rows: 2}
1252+
1253+ frame, _ := composeClientFrame(sess, win, true, "ok")
1254+ require.Equal(t, " work 1 ok", rowText(frame.Row(2)))
1255+
1256+ frame, _ = composeClientFrame(sess, win, true, "selection too large to copy")
1257+ require.Equal(t, " work 1 ", rowText(frame.Row(2)))
1258+}
1259+
12441260 func TestStatusRepaintsOnCreateSwitchAndResize(t *testing.T) {
12451261 p1, releasePTY1 := newBlockingPTY(t)
12461262 p2, releasePTY2 := newBlockingPTY(t)
@@ -1241,6 +1241,22 @@ func TestStatusCompositionGolden(t *testing.T) {
1241 }1241 }
1242 }1242 }
1243 1243
1244+func TestStatusCopyFeedbackRendersOnlyWhenFullyFits(t *testing.T) {
1245+ p, releasePTY := newBlockingPTY(t)
1246+ _, sess, _, _ := newManualSessionWithPTYs(t, p)
1247+ defer releasePTY()
1248+ sess.name = "work"
1249+ win := sess.activeTab()
1250+ win.screen = vt.NewScreen(30, 2)
1251+ win.size = domain.Size{Cols: 30, Rows: 2}
1252+
1253+ frame, _ := composeClientFrame(sess, win, true, "ok")
1254+ require.Equal(t, " work 1 ok", rowText(frame.Row(2)))
1255+
1256+ frame, _ = composeClientFrame(sess, win, true, "selection too large to copy")
1257+ require.Equal(t, " work 1 ", rowText(frame.Row(2)))
1258+}
1259+
1244 func TestStatusRepaintsOnCreateSwitchAndResize(t *testing.T) {1260 func TestStatusRepaintsOnCreateSwitchAndResize(t *testing.T) {
1245 p1, releasePTY1 := newBlockingPTY(t)1261 p1, releasePTY1 := newBlockingPTY(t)
1246 p2, releasePTY2 := newBlockingPTY(t)1262 p2, releasePTY2 := newBlockingPTY(t)

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 2

Tags 13

↑↓ moveenter openesc close