detached 9fab834

fix: repaint after pty eof window close

commit
9fab83440e3f7f074f37f532accd23f710d0d289
author
brice · · 2026-07-02 15:37 UTC
parent
d8fc808
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +69 -1
M internal/usecase/daemon/daemon.go +1 -1
@@ -804,7 +804,7 @@ func (d *Daemon) ptyReader(sess *session, win *window) {
804804 }
805805 }
806806 if err != nil {
807- d.closeWindow(sess, win, false)
807+ d.closeWindow(sess, win, true)
808808 return
809809 }
810810 }
@@ -804,7 +804,7 @@ func (d *Daemon) ptyReader(sess *session, win *window) {
804 }804 }
805 }805 }
806 if err != nil {806 if err != nil {
807- d.closeWindow(sess, win, false)807+ d.closeWindow(sess, win, true)
808 return808 return
809 }809 }
810 }810 }
M internal/usecase/daemon/daemon_test.go +68 -0
@@ -474,6 +474,74 @@ func TestAltXClosesFinalWindowAndDetaches(t *testing.T) {
474474 require.Equal(t, ports.ReasonSessionKilled, det.Reason)
475475 }
476476
477+func TestPTYEOFClosesActiveNonFinalWindowAndRepaintsRemaining(t *testing.T) {
478+ p1, releasePTY1 := newBlockingPTY(t)
479+ p2, releasePTY2 := newBlockingPTY(t)
480+ d, sess, _, sends := newManualSessionWithPTYs(t, p1, p2)
481+ defer releasePTY2()
482+ sess.active = 0
483+ sess.windows[1].screen.Write([]byte("remaining"))
484+
485+ d.sessWg.Add(1)
486+ go d.ptyReader(sess, sess.windows[0])
487+ releasePTY1()
488+
489+ require.Eventually(t, func() bool { return len(sess.windows) == 1 }, 2*time.Second, 5*time.Millisecond)
490+ require.Equal(t, 1, sessionCount(d))
491+ require.Equal(t, 0, activeWindowIndex(sess))
492+ f := awaitFrame(t, sends, ports.MsgOutput)
493+ out, err := ports.UnmarshalOutput(f.Payload)
494+ require.NoError(t, err)
495+ data := string(out.Data)
496+ require.Contains(t, data, "remaining")
497+ require.Contains(t, data, "work")
498+ require.Contains(t, data, ";7m")
499+
500+ d.sessWg.Wait()
501+}
502+
503+func TestPTYEOFClosesInactiveNonFinalWindowAndRepaintsStatus(t *testing.T) {
504+ p1, releasePTY1 := newBlockingPTY(t)
505+ p2, releasePTY2 := newBlockingPTY(t)
506+ d, sess, _, sends := newManualSessionWithPTYs(t, p1, p2)
507+ defer releasePTY1()
508+ sess.active = 0
509+ sess.windows[0].screen.Write([]byte("active"))
510+
511+ d.sessWg.Add(1)
512+ go d.ptyReader(sess, sess.windows[1])
513+ releasePTY2()
514+
515+ require.Eventually(t, func() bool { return len(sess.windows) == 1 }, 2*time.Second, 5*time.Millisecond)
516+ require.Equal(t, 1, sessionCount(d))
517+ require.Equal(t, 0, activeWindowIndex(sess))
518+ f := awaitFrame(t, sends, ports.MsgOutput)
519+ out, err := ports.UnmarshalOutput(f.Payload)
520+ require.NoError(t, err)
521+ data := string(out.Data)
522+ require.Contains(t, data, "active")
523+ require.Contains(t, data, "work")
524+ require.NotContains(t, data, " 2 ")
525+
526+ d.sessWg.Wait()
527+}
528+
529+func TestPTYEOFFinalWindowKillsSessionAndDetaches(t *testing.T) {
530+ d, sess, _, sends, releases := newManualWindowSession(t, 1)
531+
532+ d.sessWg.Add(1)
533+ go d.ptyReader(sess, sess.windows[0])
534+ releases[0]()
535+
536+ require.Eventually(t, func() bool { return sessionCount(d) == 0 }, 2*time.Second, 5*time.Millisecond)
537+ f := awaitFrame(t, sends, ports.MsgDetached)
538+ det, err := ports.UnmarshalDetached(f.Payload)
539+ require.NoError(t, err)
540+ require.Equal(t, ports.ReasonSessionKilled, det.Reason)
541+
542+ d.sessWg.Wait()
543+}
544+
477545 func TestAltDDetachesCurrentClient(t *testing.T) {
478546 d, sess, ac, sends, releases := newManualWindowSession(t, 1)
479547 defer releases[0]()
@@ -474,6 +474,74 @@ func TestAltXClosesFinalWindowAndDetaches(t *testing.T) {
474 require.Equal(t, ports.ReasonSessionKilled, det.Reason)474 require.Equal(t, ports.ReasonSessionKilled, det.Reason)
475 }475 }
476 476
477+func TestPTYEOFClosesActiveNonFinalWindowAndRepaintsRemaining(t *testing.T) {
478+ p1, releasePTY1 := newBlockingPTY(t)
479+ p2, releasePTY2 := newBlockingPTY(t)
480+ d, sess, _, sends := newManualSessionWithPTYs(t, p1, p2)
481+ defer releasePTY2()
482+ sess.active = 0
483+ sess.windows[1].screen.Write([]byte("remaining"))
484+
485+ d.sessWg.Add(1)
486+ go d.ptyReader(sess, sess.windows[0])
487+ releasePTY1()
488+
489+ require.Eventually(t, func() bool { return len(sess.windows) == 1 }, 2*time.Second, 5*time.Millisecond)
490+ require.Equal(t, 1, sessionCount(d))
491+ require.Equal(t, 0, activeWindowIndex(sess))
492+ f := awaitFrame(t, sends, ports.MsgOutput)
493+ out, err := ports.UnmarshalOutput(f.Payload)
494+ require.NoError(t, err)
495+ data := string(out.Data)
496+ require.Contains(t, data, "remaining")
497+ require.Contains(t, data, "work")
498+ require.Contains(t, data, ";7m")
499+
500+ d.sessWg.Wait()
501+}
502+
503+func TestPTYEOFClosesInactiveNonFinalWindowAndRepaintsStatus(t *testing.T) {
504+ p1, releasePTY1 := newBlockingPTY(t)
505+ p2, releasePTY2 := newBlockingPTY(t)
506+ d, sess, _, sends := newManualSessionWithPTYs(t, p1, p2)
507+ defer releasePTY1()
508+ sess.active = 0
509+ sess.windows[0].screen.Write([]byte("active"))
510+
511+ d.sessWg.Add(1)
512+ go d.ptyReader(sess, sess.windows[1])
513+ releasePTY2()
514+
515+ require.Eventually(t, func() bool { return len(sess.windows) == 1 }, 2*time.Second, 5*time.Millisecond)
516+ require.Equal(t, 1, sessionCount(d))
517+ require.Equal(t, 0, activeWindowIndex(sess))
518+ f := awaitFrame(t, sends, ports.MsgOutput)
519+ out, err := ports.UnmarshalOutput(f.Payload)
520+ require.NoError(t, err)
521+ data := string(out.Data)
522+ require.Contains(t, data, "active")
523+ require.Contains(t, data, "work")
524+ require.NotContains(t, data, " 2 ")
525+
526+ d.sessWg.Wait()
527+}
528+
529+func TestPTYEOFFinalWindowKillsSessionAndDetaches(t *testing.T) {
530+ d, sess, _, sends, releases := newManualWindowSession(t, 1)
531+
532+ d.sessWg.Add(1)
533+ go d.ptyReader(sess, sess.windows[0])
534+ releases[0]()
535+
536+ require.Eventually(t, func() bool { return sessionCount(d) == 0 }, 2*time.Second, 5*time.Millisecond)
537+ f := awaitFrame(t, sends, ports.MsgDetached)
538+ det, err := ports.UnmarshalDetached(f.Payload)
539+ require.NoError(t, err)
540+ require.Equal(t, ports.ReasonSessionKilled, det.Reason)
541+
542+ d.sessWg.Wait()
543+}
544+
477 func TestAltDDetachesCurrentClient(t *testing.T) {545 func TestAltDDetachesCurrentClient(t *testing.T) {
478 d, sess, ac, sends, releases := newManualWindowSession(t, 1)546 d, sess, ac, sends, releases := newManualWindowSession(t, 1)
479 defer releases[0]()547 defer releases[0]()

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