Skip to content

Commit 2c3a3a7

Browse files
committed
client: restore host terminal on session exit
1 parent 355a119 commit 2c3a3a7

3 files changed

Lines changed: 25 additions & 19 deletions

File tree

client/attach.go

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@ func (c *Client) RunAttach(name string, command []string, dk DetachKey, forwardE
206206
case *protocol.Output:
207207
os.Stdout.Write(m.Data)
208208
case *protocol.Exited:
209+
restoreHostTerminal(fd, oldState, "[hauntty] session exited\n")
209210
return &ExitError{Code: int(m.ExitCode)}
210211
case *protocol.Error:
211212
term.Restore(fd, oldState)
@@ -222,36 +223,40 @@ func (c *Client) RunAttach(name string, command []string, dk DetachKey, forwardE
222223
if err != nil {
223224
close(done)
224225
if err == io.EOF || isConnClosed(err) {
225-
// Use 1047 (not 1049) to exit alt screen: 1047
226-
// just switches the buffer without restoring the
227-
// saved cursor, so session content on the primary
228-
// screen stays intact. No-op when already on primary.
229-
//
230-
// Reset modes, show cursor, pop kitty keyboard,
231-
// reset SGR, erase from cursor to end of screen.
232-
// Session content above the cursor is preserved.
233-
os.Stdout.Write([]byte(
234-
"\x1b[?1047;1;1000;1002;1003;1006;1004;2004;2048;2026l" +
235-
"\x1b[?25h" +
236-
"\x1b[<u" +
237-
"\x1b[0m" +
238-
"\x1b[J"))
239-
drainStdin(fd, 20*time.Millisecond)
240-
term.Restore(fd, oldState)
241-
fmt.Fprintf(os.Stderr, "[hauntty] detached\n")
226+
restoreHostTerminal(fd, oldState, "[hauntty] detached\n")
242227
return nil
243228
}
244229
term.Restore(fd, oldState)
245230
return fmt.Errorf("read message: %w", err)
246231
}
247232
if err := handleMsg(msg); err != nil {
248233
close(done)
249-
term.Restore(fd, oldState)
250234
return err
251235
}
252236
}
253237
}
254238

239+
func restoreHostTerminal(fd int, oldState *term.State, message string) {
240+
// Use 1047 (not 1049) to exit alt screen: 1047 just switches the
241+
// buffer without restoring the saved cursor, so session content on
242+
// the primary screen stays intact. No-op when already on primary.
243+
//
244+
// Reset modes, show cursor, pop kitty keyboard, reset SGR, erase
245+
// from cursor to end of screen. Session content above the cursor is
246+
// preserved.
247+
os.Stdout.Write([]byte(
248+
"\x1b[?1047;1;1000;1002;1003;1006;1004;2004;2048;2026l" +
249+
"\x1b[?25h" +
250+
"\x1b[<u" +
251+
"\x1b[0m" +
252+
"\x1b[J"))
253+
drainStdin(fd, 20*time.Millisecond)
254+
term.Restore(fd, oldState)
255+
if message != "" {
256+
fmt.Fprint(os.Stderr, message)
257+
}
258+
}
259+
255260
// readCursorRow reads the DSR response (\x1b[{row};{col}R) from fd
256261
// and returns the cursor row. Must be called in raw mode after sending
257262
// \x1b[6n. Returns fallback if the response cannot be parsed.

cmd/ht/e2e_test/main_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import (
1818

1919
var (
2020
htBin string
21-
hostPromptRE = regexp.MustCompile(`\$ ?$`)
21+
hostPromptRE = regexp.MustCompile(`^\$ ?$`)
2222
)
2323

2424
type testEnv struct {

cmd/ht/e2e_test/terminal_safety_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,7 @@ func TestDetachFailurePathHostCleanup(t *testing.T) {
295295
kill := e.run("kill", "fail-cleanup")
296296
kill.Assert(t, icmd.Expected{ExitCode: 0, Out: "killed session \"fail-cleanup\"\n"})
297297

298+
sh.WaitFor("session exited")
298299
e.waitHostPrompt(sh)
299300
sh.Type("echo failure-cleanup-ok\n")
300301
sh.WaitFor("failure-cleanup-ok")

0 commit comments

Comments
 (0)