detached f38c91b

fix(copy): bound clipboard send latency

commit
f38c91b7234a35f91109131bc9660d5908597f21
author
brice · · 2026-07-03 04:59 UTC
parent
3d26bab
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
1 file · +15 -10
M internal/usecase/daemon/daemon.go +15 -10
@@ -226,18 +226,23 @@ func (ac *attachedClient) send(f ports.Frame) error {
226226 // write. Detach/kill/shutdown paths use this so they are never gated on a
227227 // client that has stopped draining its socket.
228228 func (d *Daemon) boundedSend(ac *attachedClient, f ports.Frame) {
229+ _ = d.boundedSendErr(ac, f)
230+}
231+
232+func (d *Daemon) boundedSendErr(ac *attachedClient, f ports.Frame) error {
229233 timer := d.clock.NewTimer(detachNotifyTimeout)
230- sent := make(chan struct{})
234+ result := make(chan error, 1)
231235 go func() {
232- select {
233- case <-timer.C():
234- _ = ac.tr.Close()
235- case <-sent:
236- }
236+ result <- ac.send(f)
237237 }()
238- _ = ac.send(f)
239- timer.Stop()
240- close(sent)
238+ select {
239+ case err := <-result:
240+ timer.Stop()
241+ return err
242+ case <-timer.C():
243+ _ = ac.tr.Close()
244+ return errors.New("send timed out")
245+ }
241246 }
242247
243248 // notifyDetachedAsync delivers a best-effort Detached notice off the caller's
@@ -949,7 +954,7 @@ func (d *Daemon) handleCopyInput(ac *attachedClient, data []byte) {
949954 if copyOut && text != "" {
950955 chunks := scopy.OSC52(text)
951956 for _, chunk := range chunks {
952- if err := ac.send(frameOutput(chunk)); err != nil {
957+ if err := d.boundedSendErr(ac, frameOutput(chunk)); err != nil {
953958 d.detachOnSendError(sess, ac)
954959 return
955960 }
@@ -226,18 +226,23 @@ func (ac *attachedClient) send(f ports.Frame) error {
226 // write. Detach/kill/shutdown paths use this so they are never gated on a226 // write. Detach/kill/shutdown paths use this so they are never gated on a
227 // client that has stopped draining its socket.227 // client that has stopped draining its socket.
228 func (d *Daemon) boundedSend(ac *attachedClient, f ports.Frame) {228 func (d *Daemon) boundedSend(ac *attachedClient, f ports.Frame) {
229+ _ = d.boundedSendErr(ac, f)
230+}
231+
232+func (d *Daemon) boundedSendErr(ac *attachedClient, f ports.Frame) error {
229 timer := d.clock.NewTimer(detachNotifyTimeout)233 timer := d.clock.NewTimer(detachNotifyTimeout)
230- sent := make(chan struct{})234+ result := make(chan error, 1)
231 go func() {235 go func() {
232- select {236+ result <- ac.send(f)
233- case <-timer.C():
234- _ = ac.tr.Close()
235- case <-sent:
236- }
237 }()237 }()
238- _ = ac.send(f)238+ select {
239- timer.Stop()239+ case err := <-result:
240- close(sent)240+ timer.Stop()
241+ return err
242+ case <-timer.C():
243+ _ = ac.tr.Close()
244+ return errors.New("send timed out")
245+ }
241 }246 }
242 247
243 // notifyDetachedAsync delivers a best-effort Detached notice off the caller's248 // notifyDetachedAsync delivers a best-effort Detached notice off the caller's
@@ -949,7 +954,7 @@ func (d *Daemon) handleCopyInput(ac *attachedClient, data []byte) {
949 if copyOut && text != "" {954 if copyOut && text != "" {
950 chunks := scopy.OSC52(text)955 chunks := scopy.OSC52(text)
951 for _, chunk := range chunks {956 for _, chunk := range chunks {
952- if err := ac.send(frameOutput(chunk)); err != nil {957+ if err := d.boundedSendErr(ac, frameOutput(chunk)); err != nil {
953 d.detachOnSendError(sess, ac)958 d.detachOnSendError(sess, ac)
954 return959 return
955 }960 }

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