detached 875a6f7

refactor(term,pty): drop golang.org/x deps for stdlib syscalls

Add pkg/linuxterm, a small Linux-only wrapper over the frozen stdlib
syscall package covering what vev used from golang.org/x/term and
golang.org/x/sys/unix: raw mode (identical MakeRaw flag semantics),
winsize get/set, pty slave number and unlock ioctls, and the
foreground process group query. Migrate the terminal adapter, the
pty adapter, and the perf harness; call-site behavior, open flags,
and error wrapping are unchanged.

The ioctl helper takes an unsafe.Pointer and converts to uintptr
inside the Syscall argument list, keeping the referent live across
stack moves.

The vev binary now depends on the standard library alone; testify
remains test-only.
commit
875a6f7c71ca6a234c655d255f2794d39aa63768
author
brice · · 2026-07-14 14:19 UTC
parent
abde31c
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
8 files · +304 -47
M cmd/vev-perf-harness/launcher.go +6 -6
@@ -15,8 +15,8 @@ import (
1515 "syscall"
1616 "time"
1717
18+ "github.com/bnema/vev/pkg/linuxterm"
1819 "github.com/bnema/vev/pkg/safedir"
19- "golang.org/x/sys/unix"
2020 )
2121
2222 type launchedProcess interface {
@@ -726,19 +726,19 @@ func (p *cliProcess) forceProcessGroupKill() {
726726 // openPTY is the small Linux PTY boundary needed to drive the public terminal
727727 // client. No daemon or adapter implementation is imported by this command.
728728 func openPTY() (*os.File, *os.File, error) {
729- masterFD, err := unix.Open("/dev/ptmx", unix.O_RDWR|unix.O_NOCTTY, 0)
729+ masterFD, err := syscall.Open("/dev/ptmx", syscall.O_RDWR|syscall.O_NOCTTY, 0)
730730 if err != nil {
731731 return nil, nil, err
732732 }
733- closeMaster := func(err error) (*os.File, *os.File, error) { _ = unix.Close(masterFD); return nil, nil, err }
734- number, err := unix.IoctlGetInt(masterFD, unix.TIOCGPTN)
733+ closeMaster := func(err error) (*os.File, *os.File, error) { _ = syscall.Close(masterFD); return nil, nil, err }
734+ number, err := linuxterm.PtsNumber(masterFD)
735735 if err != nil {
736736 return closeMaster(err)
737737 }
738- if err := unix.IoctlSetPointerInt(masterFD, unix.TIOCSPTLCK, 0); err != nil {
738+ if err := linuxterm.UnlockPt(masterFD); err != nil {
739739 return closeMaster(err)
740740 }
741- slaveFD, err := unix.Open(fmt.Sprintf("/dev/pts/%d", number), unix.O_RDWR|unix.O_NOCTTY, 0)
741+ slaveFD, err := syscall.Open(fmt.Sprintf("/dev/pts/%d", number), syscall.O_RDWR|syscall.O_NOCTTY, 0)
742742 if err != nil {
743743 return closeMaster(err)
744744 }
@@ -15,8 +15,8 @@ import (
15 "syscall"15 "syscall"
16 "time"16 "time"
17 17
18+ "github.com/bnema/vev/pkg/linuxterm"
18 "github.com/bnema/vev/pkg/safedir"19 "github.com/bnema/vev/pkg/safedir"
19- "golang.org/x/sys/unix"
20 )20 )
21 21
22 type launchedProcess interface {22 type launchedProcess interface {
@@ -726,19 +726,19 @@ func (p *cliProcess) forceProcessGroupKill() {
726 // openPTY is the small Linux PTY boundary needed to drive the public terminal726 // openPTY is the small Linux PTY boundary needed to drive the public terminal
727 // client. No daemon or adapter implementation is imported by this command.727 // client. No daemon or adapter implementation is imported by this command.
728 func openPTY() (*os.File, *os.File, error) {728 func openPTY() (*os.File, *os.File, error) {
729- masterFD, err := unix.Open("/dev/ptmx", unix.O_RDWR|unix.O_NOCTTY, 0)729+ masterFD, err := syscall.Open("/dev/ptmx", syscall.O_RDWR|syscall.O_NOCTTY, 0)
730 if err != nil {730 if err != nil {
731 return nil, nil, err731 return nil, nil, err
732 }732 }
733- closeMaster := func(err error) (*os.File, *os.File, error) { _ = unix.Close(masterFD); return nil, nil, err }733+ closeMaster := func(err error) (*os.File, *os.File, error) { _ = syscall.Close(masterFD); return nil, nil, err }
734- number, err := unix.IoctlGetInt(masterFD, unix.TIOCGPTN)734+ number, err := linuxterm.PtsNumber(masterFD)
735 if err != nil {735 if err != nil {
736 return closeMaster(err)736 return closeMaster(err)
737 }737 }
738- if err := unix.IoctlSetPointerInt(masterFD, unix.TIOCSPTLCK, 0); err != nil {738+ if err := linuxterm.UnlockPt(masterFD); err != nil {
739 return closeMaster(err)739 return closeMaster(err)
740 }740 }
741- slaveFD, err := unix.Open(fmt.Sprintf("/dev/pts/%d", number), unix.O_RDWR|unix.O_NOCTTY, 0)741+ slaveFD, err := syscall.Open(fmt.Sprintf("/dev/pts/%d", number), syscall.O_RDWR|syscall.O_NOCTTY, 0)
742 if err != nil {742 if err != nil {
743 return closeMaster(err)743 return closeMaster(err)
744 }744 }
M go.mod +1 -5
@@ -2,11 +2,7 @@ module github.com/bnema/vev
22
33 go 1.26
44
5-require (
6- github.com/stretchr/testify v1.11.1
7- golang.org/x/sys v0.46.0
8- golang.org/x/term v0.44.0
9-)
5+require github.com/stretchr/testify v1.11.1
106
117 require (
128 github.com/davecgh/go-spew v1.1.1 // indirect
@@ -2,11 +2,7 @@ module github.com/bnema/vev
2 2
3 go 1.263 go 1.26
4 4
5-require (5+require github.com/stretchr/testify v1.11.1
6- github.com/stretchr/testify v1.11.1
7- golang.org/x/sys v0.46.0
8- golang.org/x/term v0.44.0
9-)
10 6
11 require (7 require (
12 github.com/davecgh/go-spew v1.1.1 // indirect8 github.com/davecgh/go-spew v1.1.1 // indirect
M go.sum +0 -4 generated file: collapsed
M internal/adapters/pty/pty_linux.go +11 -14
@@ -5,7 +5,7 @@ package pty
55 // The Linux adapter drives /dev/ptmx directly (open master, TIOCGPTN to learn
66 // the slave index, TIOCSPTLCK to unlock, then open /dev/pts/<N>) rather than
77 // depending on a third-party pty package, keeping the dependency surface at
8-// x/sys/unix plus the standard library.
8+// the standard library alone.
99
1010 import (
1111 "context"
@@ -21,7 +21,7 @@ import (
2121 "github.com/bnema/vev/internal/domain"
2222 "github.com/bnema/vev/internal/platform"
2323 "github.com/bnema/vev/internal/ports"
24- "golang.org/x/sys/unix"
24+ "github.com/bnema/vev/pkg/linuxterm"
2525 )
2626
2727 // killGracePeriod is how long Close waits for a signalled child to exit before
@@ -43,7 +43,7 @@ func (Factory) Open(ctx context.Context, command string, args []string, env []st
4343 if err := ctx.Err(); err != nil {
4444 return nil, err
4545 }
46- masterFd, err := unix.Open("/dev/ptmx", unix.O_RDWR|unix.O_NOCTTY|unix.O_CLOEXEC, 0)
46+ masterFd, err := syscall.Open("/dev/ptmx", syscall.O_RDWR|syscall.O_NOCTTY|syscall.O_CLOEXEC, 0)
4747 if err != nil {
4848 return nil, fmt.Errorf("pty: open /dev/ptmx: %w", err)
4949 }
@@ -52,22 +52,22 @@ func (Factory) Open(ctx context.Context, command string, args []string, env []st
5252 ok := false
5353 defer func() {
5454 if !ok {
55- _ = unix.Close(masterFd)
55+ _ = syscall.Close(masterFd)
5656 }
5757 }()
5858
59- ptn, err := unix.IoctlGetInt(masterFd, unix.TIOCGPTN)
59+ ptn, err := linuxterm.PtsNumber(masterFd)
6060 if err != nil {
6161 return nil, fmt.Errorf("pty: TIOCGPTN: %w", err)
6262 }
6363
6464 // Unlock the slave so it can be opened.
65- if err := unix.IoctlSetPointerInt(masterFd, unix.TIOCSPTLCK, 0); err != nil {
65+ if err := linuxterm.UnlockPt(masterFd); err != nil {
6666 return nil, fmt.Errorf("pty: TIOCSPTLCK: %w", err)
6767 }
6868
6969 slaveName := fmt.Sprintf("/dev/pts/%d", ptn)
70- slave, err := os.OpenFile(slaveName, os.O_RDWR|unix.O_NOCTTY, 0)
70+ slave, err := os.OpenFile(slaveName, os.O_RDWR|syscall.O_NOCTTY, 0)
7171 if err != nil {
7272 return nil, fmt.Errorf("pty: open %s: %w", slaveName, err)
7373 }
@@ -141,7 +141,7 @@ var _ ports.PTY = (*linuxPTY)(nil)
141141 // normal end of stream, so EIO is mapped to io.EOF here.
142142 func (p *linuxPTY) Read(b []byte) (int, error) {
143143 n, err := p.master.Read(b)
144- if err != nil && errors.Is(err, unix.EIO) {
144+ if err != nil && errors.Is(err, syscall.EIO) {
145145 return n, io.EOF
146146 }
147147 return n, err
@@ -189,7 +189,7 @@ func (p *linuxPTY) ForegroundPgid() (int, error) {
189189 var pgid int
190190 var ioctlErr error
191191 if err := rc.Control(func(fd uintptr) {
192- pgid, ioctlErr = unix.IoctlGetInt(int(fd), unix.TIOCGPGRP)
192+ pgid, ioctlErr = linuxterm.ForegroundProcessGroup(int(fd))
193193 }); err != nil {
194194 return 0, fmt.Errorf("pty: foreground pgid: %w", err)
195195 }
@@ -235,13 +235,10 @@ func (p *linuxPTY) Close() error {
235235 // child as its own session and process-group leader, so a negative pid reaches
236236 // both the command and every descendant that remains in its process group.
237237 func signalProcessGroup(pid int, signal syscall.Signal) error {
238- return unix.Kill(-pid, signal)
238+ return syscall.Kill(-pid, signal)
239239 }
240240
241241 // setWinsize applies sz to the terminal referenced by fd via TIOCSWINSZ.
242242 func setWinsize(fd int, sz domain.Size) error {
243- return unix.IoctlSetWinsize(fd, unix.TIOCSWINSZ, &unix.Winsize{
244- Row: uint16(sz.Rows),
245- Col: uint16(sz.Cols),
246- })
243+ return linuxterm.SetWinsize(fd, uint16(sz.Cols), uint16(sz.Rows))
247244 }
@@ -5,7 +5,7 @@ package pty
5 // The Linux adapter drives /dev/ptmx directly (open master, TIOCGPTN to learn5 // The Linux adapter drives /dev/ptmx directly (open master, TIOCGPTN to learn
6 // the slave index, TIOCSPTLCK to unlock, then open /dev/pts/<N>) rather than6 // the slave index, TIOCSPTLCK to unlock, then open /dev/pts/<N>) rather than
7 // depending on a third-party pty package, keeping the dependency surface at7 // depending on a third-party pty package, keeping the dependency surface at
8-// x/sys/unix plus the standard library.8+// the standard library alone.
9 9
10 import (10 import (
11 "context"11 "context"
@@ -21,7 +21,7 @@ import (
21 "github.com/bnema/vev/internal/domain"21 "github.com/bnema/vev/internal/domain"
22 "github.com/bnema/vev/internal/platform"22 "github.com/bnema/vev/internal/platform"
23 "github.com/bnema/vev/internal/ports"23 "github.com/bnema/vev/internal/ports"
24- "golang.org/x/sys/unix"24+ "github.com/bnema/vev/pkg/linuxterm"
25 )25 )
26 26
27 // killGracePeriod is how long Close waits for a signalled child to exit before27 // killGracePeriod is how long Close waits for a signalled child to exit before
@@ -43,7 +43,7 @@ func (Factory) Open(ctx context.Context, command string, args []string, env []st
43 if err := ctx.Err(); err != nil {43 if err := ctx.Err(); err != nil {
44 return nil, err44 return nil, err
45 }45 }
46- masterFd, err := unix.Open("/dev/ptmx", unix.O_RDWR|unix.O_NOCTTY|unix.O_CLOEXEC, 0)46+ masterFd, err := syscall.Open("/dev/ptmx", syscall.O_RDWR|syscall.O_NOCTTY|syscall.O_CLOEXEC, 0)
47 if err != nil {47 if err != nil {
48 return nil, fmt.Errorf("pty: open /dev/ptmx: %w", err)48 return nil, fmt.Errorf("pty: open /dev/ptmx: %w", err)
49 }49 }
@@ -52,22 +52,22 @@ func (Factory) Open(ctx context.Context, command string, args []string, env []st
52 ok := false52 ok := false
53 defer func() {53 defer func() {
54 if !ok {54 if !ok {
55- _ = unix.Close(masterFd)55+ _ = syscall.Close(masterFd)
56 }56 }
57 }()57 }()
58 58
59- ptn, err := unix.IoctlGetInt(masterFd, unix.TIOCGPTN)59+ ptn, err := linuxterm.PtsNumber(masterFd)
60 if err != nil {60 if err != nil {
61 return nil, fmt.Errorf("pty: TIOCGPTN: %w", err)61 return nil, fmt.Errorf("pty: TIOCGPTN: %w", err)
62 }62 }
63 63
64 // Unlock the slave so it can be opened.64 // Unlock the slave so it can be opened.
65- if err := unix.IoctlSetPointerInt(masterFd, unix.TIOCSPTLCK, 0); err != nil {65+ if err := linuxterm.UnlockPt(masterFd); err != nil {
66 return nil, fmt.Errorf("pty: TIOCSPTLCK: %w", err)66 return nil, fmt.Errorf("pty: TIOCSPTLCK: %w", err)
67 }67 }
68 68
69 slaveName := fmt.Sprintf("/dev/pts/%d", ptn)69 slaveName := fmt.Sprintf("/dev/pts/%d", ptn)
70- slave, err := os.OpenFile(slaveName, os.O_RDWR|unix.O_NOCTTY, 0)70+ slave, err := os.OpenFile(slaveName, os.O_RDWR|syscall.O_NOCTTY, 0)
71 if err != nil {71 if err != nil {
72 return nil, fmt.Errorf("pty: open %s: %w", slaveName, err)72 return nil, fmt.Errorf("pty: open %s: %w", slaveName, err)
73 }73 }
@@ -141,7 +141,7 @@ var _ ports.PTY = (*linuxPTY)(nil)
141 // normal end of stream, so EIO is mapped to io.EOF here.141 // normal end of stream, so EIO is mapped to io.EOF here.
142 func (p *linuxPTY) Read(b []byte) (int, error) {142 func (p *linuxPTY) Read(b []byte) (int, error) {
143 n, err := p.master.Read(b)143 n, err := p.master.Read(b)
144- if err != nil && errors.Is(err, unix.EIO) {144+ if err != nil && errors.Is(err, syscall.EIO) {
145 return n, io.EOF145 return n, io.EOF
146 }146 }
147 return n, err147 return n, err
@@ -189,7 +189,7 @@ func (p *linuxPTY) ForegroundPgid() (int, error) {
189 var pgid int189 var pgid int
190 var ioctlErr error190 var ioctlErr error
191 if err := rc.Control(func(fd uintptr) {191 if err := rc.Control(func(fd uintptr) {
192- pgid, ioctlErr = unix.IoctlGetInt(int(fd), unix.TIOCGPGRP)192+ pgid, ioctlErr = linuxterm.ForegroundProcessGroup(int(fd))
193 }); err != nil {193 }); err != nil {
194 return 0, fmt.Errorf("pty: foreground pgid: %w", err)194 return 0, fmt.Errorf("pty: foreground pgid: %w", err)
195 }195 }
@@ -235,13 +235,10 @@ func (p *linuxPTY) Close() error {
235 // child as its own session and process-group leader, so a negative pid reaches235 // child as its own session and process-group leader, so a negative pid reaches
236 // both the command and every descendant that remains in its process group.236 // both the command and every descendant that remains in its process group.
237 func signalProcessGroup(pid int, signal syscall.Signal) error {237 func signalProcessGroup(pid int, signal syscall.Signal) error {
238- return unix.Kill(-pid, signal)238+ return syscall.Kill(-pid, signal)
239 }239 }
240 240
241 // setWinsize applies sz to the terminal referenced by fd via TIOCSWINSZ.241 // setWinsize applies sz to the terminal referenced by fd via TIOCSWINSZ.
242 func setWinsize(fd int, sz domain.Size) error {242 func setWinsize(fd int, sz domain.Size) error {
243- return unix.IoctlSetWinsize(fd, unix.TIOCSWINSZ, &unix.Winsize{243+ return linuxterm.SetWinsize(fd, uint16(sz.Cols), uint16(sz.Rows))
244- Row: uint16(sz.Rows),
245- Col: uint16(sz.Cols),
246- })
247 }244 }
M internal/adapters/term/pty_integration_test.go +7 -7
@@ -6,10 +6,11 @@ import (
66 "fmt"
77 "io"
88 "os"
9+ "syscall"
910 "testing"
1011 "time"
1112
12- "golang.org/x/sys/unix"
13+ "github.com/bnema/vev/pkg/linuxterm"
1314 )
1415
1516 // openPTYPair opens a real Linux PTY master/slave pair directly via
@@ -18,24 +19,24 @@ import (
1819 func openPTYPair(t *testing.T) (master, slave *os.File) {
1920 t.Helper()
2021
21- m, err := os.OpenFile("/dev/ptmx", os.O_RDWR|unix.O_NOCTTY, 0)
22+ m, err := os.OpenFile("/dev/ptmx", os.O_RDWR|syscall.O_NOCTTY, 0)
2223 if err != nil {
2324 t.Fatalf("open /dev/ptmx: %v", err)
2425 }
2526
26- if err := unix.IoctlSetPointerInt(int(m.Fd()), unix.TIOCSPTLCK, 0); err != nil {
27+ if err := linuxterm.UnlockPt(int(m.Fd())); err != nil {
2728 _ = m.Close()
2829 t.Fatalf("unlock pty (TIOCSPTLCK): %v", err)
2930 }
3031
31- n, err := unix.IoctlGetInt(int(m.Fd()), unix.TIOCGPTN)
32+ n, err := linuxterm.PtsNumber(int(m.Fd()))
3233 if err != nil {
3334 _ = m.Close()
3435 t.Fatalf("get pty number (TIOCGPTN): %v", err)
3536 }
3637
3738 slavePath := fmt.Sprintf("/dev/pts/%d", n)
38- s, err := os.OpenFile(slavePath, os.O_RDWR|unix.O_NOCTTY, 0)
39+ s, err := os.OpenFile(slavePath, os.O_RDWR|syscall.O_NOCTTY, 0)
3940 if err != nil {
4041 _ = m.Close()
4142 t.Fatalf("open slave %s: %v", slavePath, err)
@@ -53,8 +54,7 @@ func TestTerminal_RealPTY_RawModeSizeAndEscapes(t *testing.T) {
5354 defer func() { _ = master.Close() }()
5455 defer func() { _ = slave.Close() }()
5556
56- ws := &unix.Winsize{Row: 24, Col: 80}
57- if err := unix.IoctlSetWinsize(int(slave.Fd()), unix.TIOCSWINSZ, ws); err != nil {
57+ if err := linuxterm.SetWinsize(int(slave.Fd()), 80, 24); err != nil {
5858 t.Fatalf("set winsize: %v", err)
5959 }
6060
@@ -6,10 +6,11 @@ import (
6 "fmt"6 "fmt"
7 "io"7 "io"
8 "os"8 "os"
9+ "syscall"
9 "testing"10 "testing"
10 "time"11 "time"
11 12
12- "golang.org/x/sys/unix"13+ "github.com/bnema/vev/pkg/linuxterm"
13 )14 )
14 15
15 // openPTYPair opens a real Linux PTY master/slave pair directly via16 // openPTYPair opens a real Linux PTY master/slave pair directly via
@@ -18,24 +19,24 @@ import (
18 func openPTYPair(t *testing.T) (master, slave *os.File) {19 func openPTYPair(t *testing.T) (master, slave *os.File) {
19 t.Helper()20 t.Helper()
20 21
21- m, err := os.OpenFile("/dev/ptmx", os.O_RDWR|unix.O_NOCTTY, 0)22+ m, err := os.OpenFile("/dev/ptmx", os.O_RDWR|syscall.O_NOCTTY, 0)
22 if err != nil {23 if err != nil {
23 t.Fatalf("open /dev/ptmx: %v", err)24 t.Fatalf("open /dev/ptmx: %v", err)
24 }25 }
25 26
26- if err := unix.IoctlSetPointerInt(int(m.Fd()), unix.TIOCSPTLCK, 0); err != nil {27+ if err := linuxterm.UnlockPt(int(m.Fd())); err != nil {
27 _ = m.Close()28 _ = m.Close()
28 t.Fatalf("unlock pty (TIOCSPTLCK): %v", err)29 t.Fatalf("unlock pty (TIOCSPTLCK): %v", err)
29 }30 }
30 31
31- n, err := unix.IoctlGetInt(int(m.Fd()), unix.TIOCGPTN)32+ n, err := linuxterm.PtsNumber(int(m.Fd()))
32 if err != nil {33 if err != nil {
33 _ = m.Close()34 _ = m.Close()
34 t.Fatalf("get pty number (TIOCGPTN): %v", err)35 t.Fatalf("get pty number (TIOCGPTN): %v", err)
35 }36 }
36 37
37 slavePath := fmt.Sprintf("/dev/pts/%d", n)38 slavePath := fmt.Sprintf("/dev/pts/%d", n)
38- s, err := os.OpenFile(slavePath, os.O_RDWR|unix.O_NOCTTY, 0)39+ s, err := os.OpenFile(slavePath, os.O_RDWR|syscall.O_NOCTTY, 0)
39 if err != nil {40 if err != nil {
40 _ = m.Close()41 _ = m.Close()
41 t.Fatalf("open slave %s: %v", slavePath, err)42 t.Fatalf("open slave %s: %v", slavePath, err)
@@ -53,8 +54,7 @@ func TestTerminal_RealPTY_RawModeSizeAndEscapes(t *testing.T) {
53 defer func() { _ = master.Close() }()54 defer func() { _ = master.Close() }()
54 defer func() { _ = slave.Close() }()55 defer func() { _ = slave.Close() }()
55 56
56- ws := &unix.Winsize{Row: 24, Col: 80}57+ if err := linuxterm.SetWinsize(int(slave.Fd()), 80, 24); err != nil {
57- if err := unix.IoctlSetWinsize(int(slave.Fd()), unix.TIOCSWINSZ, ws); err != nil {
58 t.Fatalf("set winsize: %v", err)58 t.Fatalf("set winsize: %v", err)
59 }59 }
60 60
M internal/adapters/term/terminal.go +10 -11
@@ -9,10 +9,9 @@ import (
99 "sync"
1010 "syscall"
1111
12- "golang.org/x/term"
13-
1412 "github.com/bnema/vev/internal/domain"
1513 "github.com/bnema/vev/internal/ports"
14+ "github.com/bnema/vev/pkg/linuxterm"
1615 )
1716
1817 // Compile-time check that Terminal satisfies ports.Terminal.
@@ -50,10 +49,10 @@ type Terminal struct {
5049 bw *batchWriter
5150
5251 mu sync.Mutex
53- orig *term.State // saved terminal state; nil when raw mode wasn't entered (or was restored)
54- entered bool // true between a successful EnterRaw and its restore
55- rawSkipped bool // true if fd wasn't a tty, so MakeRaw/Restore were skipped
56- restoreFn func() error // the single idempotent restore closure for the current session
52+ orig *linuxterm.State // saved terminal state; nil when raw mode wasn't entered (or was restored)
53+ entered bool // true between a successful EnterRaw and its restore
54+ rawSkipped bool // true if fd wasn't a tty, so MakeRaw/Restore were skipped
55+ restoreFn func() error // the single idempotent restore closure for the current session
5756
5857 // Resize watcher state, also guarded by mu so watcher start
5958 // (ResizeEvents) and watcher stop (restore) are strictly ordered:
@@ -96,9 +95,9 @@ func (t *Terminal) EnterRaw() (func() error, error) {
9695 return t.restoreFn, nil
9796 }
9897
99- var old *term.State
100- if term.IsTerminal(t.fd) {
101- s, err := term.MakeRaw(t.fd)
98+ var old *linuxterm.State
99+ if linuxterm.IsTerminal(t.fd) {
100+ s, err := linuxterm.MakeRaw(t.fd)
102101 if err != nil {
103102 return nil, fmt.Errorf("term: make raw: %w", err)
104103 }
@@ -171,14 +170,14 @@ func (t *Terminal) restoreRawLocked() error {
171170 if t.orig == nil {
172171 return nil
173172 }
174- err := term.Restore(t.fd, t.orig)
173+ err := linuxterm.Restore(t.fd, t.orig)
175174 t.orig = nil
176175 return err
177176 }
178177
179178 // Size returns the current terminal dimensions.
180179 func (t *Terminal) Size() (domain.Size, error) {
181- cols, rows, err := term.GetSize(t.fd)
180+ cols, rows, err := linuxterm.GetSize(t.fd)
182181 if err != nil {
183182 return domain.Size{}, fmt.Errorf("term: get size: %w", err)
184183 }
@@ -9,10 +9,9 @@ import (
9 "sync"9 "sync"
10 "syscall"10 "syscall"
11 11
12- "golang.org/x/term"
13-
14 "github.com/bnema/vev/internal/domain"12 "github.com/bnema/vev/internal/domain"
15 "github.com/bnema/vev/internal/ports"13 "github.com/bnema/vev/internal/ports"
14+ "github.com/bnema/vev/pkg/linuxterm"
16 )15 )
17 16
18 // Compile-time check that Terminal satisfies ports.Terminal.17 // Compile-time check that Terminal satisfies ports.Terminal.
@@ -50,10 +49,10 @@ type Terminal struct {
50 bw *batchWriter49 bw *batchWriter
51 50
52 mu sync.Mutex51 mu sync.Mutex
53- orig *term.State // saved terminal state; nil when raw mode wasn't entered (or was restored)52+ orig *linuxterm.State // saved terminal state; nil when raw mode wasn't entered (or was restored)
54- entered bool // true between a successful EnterRaw and its restore53+ entered bool // true between a successful EnterRaw and its restore
55- rawSkipped bool // true if fd wasn't a tty, so MakeRaw/Restore were skipped54+ rawSkipped bool // true if fd wasn't a tty, so MakeRaw/Restore were skipped
56- restoreFn func() error // the single idempotent restore closure for the current session55+ restoreFn func() error // the single idempotent restore closure for the current session
57 56
58 // Resize watcher state, also guarded by mu so watcher start57 // Resize watcher state, also guarded by mu so watcher start
59 // (ResizeEvents) and watcher stop (restore) are strictly ordered:58 // (ResizeEvents) and watcher stop (restore) are strictly ordered:
@@ -96,9 +95,9 @@ func (t *Terminal) EnterRaw() (func() error, error) {
96 return t.restoreFn, nil95 return t.restoreFn, nil
97 }96 }
98 97
99- var old *term.State98+ var old *linuxterm.State
100- if term.IsTerminal(t.fd) {99+ if linuxterm.IsTerminal(t.fd) {
101- s, err := term.MakeRaw(t.fd)100+ s, err := linuxterm.MakeRaw(t.fd)
102 if err != nil {101 if err != nil {
103 return nil, fmt.Errorf("term: make raw: %w", err)102 return nil, fmt.Errorf("term: make raw: %w", err)
104 }103 }
@@ -171,14 +170,14 @@ func (t *Terminal) restoreRawLocked() error {
171 if t.orig == nil {170 if t.orig == nil {
172 return nil171 return nil
173 }172 }
174- err := term.Restore(t.fd, t.orig)173+ err := linuxterm.Restore(t.fd, t.orig)
175 t.orig = nil174 t.orig = nil
176 return err175 return err
177 }176 }
178 177
179 // Size returns the current terminal dimensions.178 // Size returns the current terminal dimensions.
180 func (t *Terminal) Size() (domain.Size, error) {179 func (t *Terminal) Size() (domain.Size, error) {
181- cols, rows, err := term.GetSize(t.fd)180+ cols, rows, err := linuxterm.GetSize(t.fd)
182 if err != nil {181 if err != nil {
183 return domain.Size{}, fmt.Errorf("term: get size: %w", err)182 return domain.Size{}, fmt.Errorf("term: get size: %w", err)
184 }183 }
A pkg/linuxterm/linuxterm.go +126 -0
@@ -0,0 +1,126 @@
1+// Package linuxterm provides the small set of terminal-control primitives
2+// vev needs (raw mode, window size, pty allocation) directly on top of the
3+// frozen stdlib syscall package, so the module does not depend on
4+// golang.org/x/term or golang.org/x/sys. vev is Linux-only, and the Linux
5+// TIOC*/TCGETS ioctl numbers and Termios layout syscall exposes are stable.
6+package linuxterm
7+
8+import (
9+ "fmt"
10+ "syscall"
11+ "unsafe"
12+)
13+
14+// State is an opaque snapshot of a terminal's termios settings, captured by
15+// MakeRaw and consumed by Restore.
16+type State struct {
17+ termios syscall.Termios
18+}
19+
20+// winsize mirrors the kernel's struct winsize for TIOCGWINSZ/TIOCSWINSZ.
21+type winsize struct {
22+ Row uint16
23+ Col uint16
24+ Xpixel uint16
25+ Ypixel uint16
26+}
27+
28+// ioctl issues a SYS_IOCTL syscall against fd with the given request and
29+// argument pointer, returning a Go error on failure. The pointer-to-uintptr
30+// conversion must happen inside the Syscall argument list (unsafe.Pointer
31+// rule 4), so arg stays a live pointer until the call.
32+func ioctl(fd int, req uintptr, arg unsafe.Pointer) error {
33+ _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), req, uintptr(arg))
34+ if errno != 0 {
35+ return errno
36+ }
37+ return nil
38+}
39+
40+// IsTerminal reports whether fd refers to a terminal device.
41+func IsTerminal(fd int) bool {
42+ var t syscall.Termios
43+ return ioctl(fd, syscall.TCGETS, unsafe.Pointer(&t)) == nil
44+}
45+
46+// MakeRaw puts the terminal referenced by fd into raw mode and returns its
47+// pre-modification state, so a caller can later restore it. The flag
48+// changes applied match golang.org/x/term's MakeRaw semantics exactly.
49+func MakeRaw(fd int) (*State, error) {
50+ var oldState State
51+ if err := ioctl(fd, syscall.TCGETS, unsafe.Pointer(&oldState.termios)); err != nil {
52+ return nil, fmt.Errorf("linuxterm: get termios: %w", err)
53+ }
54+
55+ raw := oldState.termios
56+ raw.Iflag &^= syscall.IGNBRK | syscall.BRKINT | syscall.PARMRK | syscall.ISTRIP | syscall.INLCR | syscall.IGNCR | syscall.ICRNL | syscall.IXON
57+ raw.Oflag &^= syscall.OPOST
58+ raw.Lflag &^= syscall.ECHO | syscall.ECHONL | syscall.ICANON | syscall.ISIG | syscall.IEXTEN
59+ raw.Cflag &^= syscall.CSIZE | syscall.PARENB
60+ raw.Cflag |= syscall.CS8
61+ raw.Cc[syscall.VMIN] = 1
62+ raw.Cc[syscall.VTIME] = 0
63+
64+ if err := ioctl(fd, syscall.TCSETS, unsafe.Pointer(&raw)); err != nil {
65+ return nil, fmt.Errorf("linuxterm: set termios: %w", err)
66+ }
67+
68+ return &oldState, nil
69+}
70+
71+// Restore applies a previously captured State back to the terminal
72+// referenced by fd.
73+func Restore(fd int, s *State) error {
74+ if err := ioctl(fd, syscall.TCSETS, unsafe.Pointer(&s.termios)); err != nil {
75+ return fmt.Errorf("linuxterm: set termios: %w", err)
76+ }
77+ return nil
78+}
79+
80+// GetSize returns the terminal's current column and row count.
81+func GetSize(fd int) (cols, rows int, err error) {
82+ var ws winsize
83+ if err := ioctl(fd, syscall.TIOCGWINSZ, unsafe.Pointer(&ws)); err != nil {
84+ return 0, 0, fmt.Errorf("linuxterm: get winsize: %w", err)
85+ }
86+ return int(ws.Col), int(ws.Row), nil
87+}
88+
89+// SetWinsize sets the terminal's column and row count.
90+func SetWinsize(fd int, cols, rows uint16) error {
91+ ws := winsize{Row: rows, Col: cols}
92+ if err := ioctl(fd, syscall.TIOCSWINSZ, unsafe.Pointer(&ws)); err != nil {
93+ return fmt.Errorf("linuxterm: set winsize: %w", err)
94+ }
95+ return nil
96+}
97+
98+// PtsNumber returns the pty slave number for the master referenced by
99+// masterFD (TIOCGPTN).
100+func PtsNumber(masterFD int) (int, error) {
101+ var n int32
102+ if err := ioctl(masterFD, syscall.TIOCGPTN, unsafe.Pointer(&n)); err != nil {
103+ return 0, fmt.Errorf("linuxterm: get pts number: %w", err)
104+ }
105+ return int(n), nil
106+}
107+
108+// UnlockPt unlocks the pty slave associated with masterFD so it can be
109+// opened (TIOCSPTLCK).
110+func UnlockPt(masterFD int) error {
111+ var unlock int32
112+ if err := ioctl(masterFD, syscall.TIOCSPTLCK, unsafe.Pointer(&unlock)); err != nil {
113+ return fmt.Errorf("linuxterm: unlock pty: %w", err)
114+ }
115+ return nil
116+}
117+
118+// ForegroundProcessGroup returns the process group id currently in the
119+// foreground for the terminal referenced by fd (TIOCGPGRP).
120+func ForegroundProcessGroup(fd int) (int, error) {
121+ var pgid int32
122+ if err := ioctl(fd, syscall.TIOCGPGRP, unsafe.Pointer(&pgid)); err != nil {
123+ return 0, fmt.Errorf("linuxterm: get foreground process group: %w", err)
124+ }
125+ return int(pgid), nil
126+}
@@ -0,0 +1,126 @@
1+// Package linuxterm provides the small set of terminal-control primitives
2+// vev needs (raw mode, window size, pty allocation) directly on top of the
3+// frozen stdlib syscall package, so the module does not depend on
4+// golang.org/x/term or golang.org/x/sys. vev is Linux-only, and the Linux
5+// TIOC*/TCGETS ioctl numbers and Termios layout syscall exposes are stable.
6+package linuxterm
7+
8+import (
9+ "fmt"
10+ "syscall"
11+ "unsafe"
12+)
13+
14+// State is an opaque snapshot of a terminal's termios settings, captured by
15+// MakeRaw and consumed by Restore.
16+type State struct {
17+ termios syscall.Termios
18+}
19+
20+// winsize mirrors the kernel's struct winsize for TIOCGWINSZ/TIOCSWINSZ.
21+type winsize struct {
22+ Row uint16
23+ Col uint16
24+ Xpixel uint16
25+ Ypixel uint16
26+}
27+
28+// ioctl issues a SYS_IOCTL syscall against fd with the given request and
29+// argument pointer, returning a Go error on failure. The pointer-to-uintptr
30+// conversion must happen inside the Syscall argument list (unsafe.Pointer
31+// rule 4), so arg stays a live pointer until the call.
32+func ioctl(fd int, req uintptr, arg unsafe.Pointer) error {
33+ _, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), req, uintptr(arg))
34+ if errno != 0 {
35+ return errno
36+ }
37+ return nil
38+}
39+
40+// IsTerminal reports whether fd refers to a terminal device.
41+func IsTerminal(fd int) bool {
42+ var t syscall.Termios
43+ return ioctl(fd, syscall.TCGETS, unsafe.Pointer(&t)) == nil
44+}
45+
46+// MakeRaw puts the terminal referenced by fd into raw mode and returns its
47+// pre-modification state, so a caller can later restore it. The flag
48+// changes applied match golang.org/x/term's MakeRaw semantics exactly.
49+func MakeRaw(fd int) (*State, error) {
50+ var oldState State
51+ if err := ioctl(fd, syscall.TCGETS, unsafe.Pointer(&oldState.termios)); err != nil {
52+ return nil, fmt.Errorf("linuxterm: get termios: %w", err)
53+ }
54+
55+ raw := oldState.termios
56+ raw.Iflag &^= syscall.IGNBRK | syscall.BRKINT | syscall.PARMRK | syscall.ISTRIP | syscall.INLCR | syscall.IGNCR | syscall.ICRNL | syscall.IXON
57+ raw.Oflag &^= syscall.OPOST
58+ raw.Lflag &^= syscall.ECHO | syscall.ECHONL | syscall.ICANON | syscall.ISIG | syscall.IEXTEN
59+ raw.Cflag &^= syscall.CSIZE | syscall.PARENB
60+ raw.Cflag |= syscall.CS8
61+ raw.Cc[syscall.VMIN] = 1
62+ raw.Cc[syscall.VTIME] = 0
63+
64+ if err := ioctl(fd, syscall.TCSETS, unsafe.Pointer(&raw)); err != nil {
65+ return nil, fmt.Errorf("linuxterm: set termios: %w", err)
66+ }
67+
68+ return &oldState, nil
69+}
70+
71+// Restore applies a previously captured State back to the terminal
72+// referenced by fd.
73+func Restore(fd int, s *State) error {
74+ if err := ioctl(fd, syscall.TCSETS, unsafe.Pointer(&s.termios)); err != nil {
75+ return fmt.Errorf("linuxterm: set termios: %w", err)
76+ }
77+ return nil
78+}
79+
80+// GetSize returns the terminal's current column and row count.
81+func GetSize(fd int) (cols, rows int, err error) {
82+ var ws winsize
83+ if err := ioctl(fd, syscall.TIOCGWINSZ, unsafe.Pointer(&ws)); err != nil {
84+ return 0, 0, fmt.Errorf("linuxterm: get winsize: %w", err)
85+ }
86+ return int(ws.Col), int(ws.Row), nil
87+}
88+
89+// SetWinsize sets the terminal's column and row count.
90+func SetWinsize(fd int, cols, rows uint16) error {
91+ ws := winsize{Row: rows, Col: cols}
92+ if err := ioctl(fd, syscall.TIOCSWINSZ, unsafe.Pointer(&ws)); err != nil {
93+ return fmt.Errorf("linuxterm: set winsize: %w", err)
94+ }
95+ return nil
96+}
97+
98+// PtsNumber returns the pty slave number for the master referenced by
99+// masterFD (TIOCGPTN).
100+func PtsNumber(masterFD int) (int, error) {
101+ var n int32
102+ if err := ioctl(masterFD, syscall.TIOCGPTN, unsafe.Pointer(&n)); err != nil {
103+ return 0, fmt.Errorf("linuxterm: get pts number: %w", err)
104+ }
105+ return int(n), nil
106+}
107+
108+// UnlockPt unlocks the pty slave associated with masterFD so it can be
109+// opened (TIOCSPTLCK).
110+func UnlockPt(masterFD int) error {
111+ var unlock int32
112+ if err := ioctl(masterFD, syscall.TIOCSPTLCK, unsafe.Pointer(&unlock)); err != nil {
113+ return fmt.Errorf("linuxterm: unlock pty: %w", err)
114+ }
115+ return nil
116+}
117+
118+// ForegroundProcessGroup returns the process group id currently in the
119+// foreground for the terminal referenced by fd (TIOCGPGRP).
120+func ForegroundProcessGroup(fd int) (int, error) {
121+ var pgid int32
122+ if err := ioctl(fd, syscall.TIOCGPGRP, unsafe.Pointer(&pgid)); err != nil {
123+ return 0, fmt.Errorf("linuxterm: get foreground process group: %w", err)
124+ }
125+ return int(pgid), nil
126+}
A pkg/linuxterm/linuxterm_test.go +143 -0
@@ -0,0 +1,143 @@
1+package linuxterm
2+
3+import (
4+ "errors"
5+ "fmt"
6+ "os"
7+ "syscall"
8+ "testing"
9+ "unsafe"
10+)
11+
12+// openPTYPair opens a real Linux PTY master/slave pair directly via
13+// /dev/ptmx using the package under test, so these tests double as an
14+// integration check of PtsNumber/UnlockPt against the kernel.
15+func openPTYPair(t *testing.T) (master, slave *os.File) {
16+ t.Helper()
17+
18+ masterFD, err := syscall.Open("/dev/ptmx", syscall.O_RDWR|syscall.O_NOCTTY|syscall.O_CLOEXEC, 0)
19+ if err != nil {
20+ t.Skipf("open /dev/ptmx: %v", err)
21+ }
22+ master = os.NewFile(uintptr(masterFD), "pty-master")
23+
24+ if err := UnlockPt(masterFD); err != nil {
25+ _ = master.Close()
26+ t.Fatalf("UnlockPt: %v", err)
27+ }
28+
29+ n, err := PtsNumber(masterFD)
30+ if err != nil {
31+ _ = master.Close()
32+ t.Fatalf("PtsNumber: %v", err)
33+ }
34+
35+ slavePath := fmt.Sprintf("/dev/pts/%d", n)
36+ slave, err = os.OpenFile(slavePath, os.O_RDWR|syscall.O_NOCTTY, 0)
37+ if err != nil {
38+ _ = master.Close()
39+ t.Fatalf("open slave %s: %v", slavePath, err)
40+ }
41+
42+ return master, slave
43+}
44+
45+func TestIsTerminal(t *testing.T) {
46+ master, slave := openPTYPair(t)
47+ defer func() { _ = master.Close() }()
48+ defer func() { _ = slave.Close() }()
49+
50+ if !IsTerminal(int(slave.Fd())) {
51+ t.Errorf("IsTerminal(pty slave) = false, want true")
52+ }
53+
54+ r, w, err := os.Pipe()
55+ if err != nil {
56+ t.Fatalf("os.Pipe: %v", err)
57+ }
58+ defer func() { _ = r.Close() }()
59+ defer func() { _ = w.Close() }()
60+
61+ if IsTerminal(int(r.Fd())) {
62+ t.Errorf("IsTerminal(pipe) = true, want false")
63+ }
64+}
65+
66+func TestSetWinsizeGetSizeRoundtrip(t *testing.T) {
67+ master, slave := openPTYPair(t)
68+ defer func() { _ = master.Close() }()
69+ defer func() { _ = slave.Close() }()
70+
71+ tests := []struct {
72+ name string
73+ cols uint16
74+ rows uint16
75+ }{
76+ {name: "typical", cols: 80, rows: 24},
77+ {name: "wide", cols: 220, rows: 60},
78+ {name: "minimal", cols: 1, rows: 1},
79+ }
80+
81+ for _, tt := range tests {
82+ t.Run(tt.name, func(t *testing.T) {
83+ if err := SetWinsize(int(slave.Fd()), tt.cols, tt.rows); err != nil {
84+ t.Fatalf("SetWinsize: %v", err)
85+ }
86+ gotCols, gotRows, err := GetSize(int(slave.Fd()))
87+ if err != nil {
88+ t.Fatalf("GetSize: %v", err)
89+ }
90+ if gotCols != int(tt.cols) || gotRows != int(tt.rows) {
91+ t.Fatalf("GetSize = (%d, %d), want (%d, %d)", gotCols, gotRows, tt.cols, tt.rows)
92+ }
93+ })
94+ }
95+}
96+
97+func TestMakeRawRestore(t *testing.T) {
98+ master, slave := openPTYPair(t)
99+ defer func() { _ = master.Close() }()
100+ defer func() { _ = slave.Close() }()
101+
102+ fd := int(slave.Fd())
103+
104+ old, err := MakeRaw(fd)
105+ if err != nil {
106+ t.Fatalf("MakeRaw: %v", err)
107+ }
108+ if old == nil {
109+ t.Fatalf("MakeRaw returned nil state")
110+ }
111+
112+ var current syscall.Termios
113+ if err := ioctl(fd, syscall.TCGETS, unsafe.Pointer(&current)); err != nil {
114+ t.Fatalf("TCGETS after MakeRaw: %v", err)
115+ }
116+ if current.Lflag&syscall.ECHO != 0 {
117+ t.Errorf("ECHO still set after MakeRaw")
118+ }
119+ if current.Lflag&syscall.ICANON != 0 {
120+ t.Errorf("ICANON still set after MakeRaw")
121+ }
122+ if current.Cc[syscall.VMIN] != 1 {
123+ t.Errorf("Cc[VMIN] = %d, want 1", current.Cc[syscall.VMIN])
124+ }
125+
126+ if err := Restore(fd, old); err != nil {
127+ t.Fatalf("Restore: %v", err)
128+ }
129+}
130+
131+func TestForegroundProcessGroup(t *testing.T) {
132+ master, slave := openPTYPair(t)
133+ defer func() { _ = master.Close() }()
134+ defer func() { _ = slave.Close() }()
135+
136+ // No session has been established on this pty (no process ever made it
137+ // a controlling terminal via Setsid+Setctty), so the kernel legitimately
138+ // has no foreground process group to report and returns ENOTTY. That
139+ // still exercises the ioctl path; only an unexpected error is a failure.
140+ if _, err := ForegroundProcessGroup(int(slave.Fd())); err != nil && !errors.Is(err, syscall.ENOTTY) {
141+ t.Fatalf("ForegroundProcessGroup: %v", err)
142+ }
143+}
@@ -0,0 +1,143 @@
1+package linuxterm
2+
3+import (
4+ "errors"
5+ "fmt"
6+ "os"
7+ "syscall"
8+ "testing"
9+ "unsafe"
10+)
11+
12+// openPTYPair opens a real Linux PTY master/slave pair directly via
13+// /dev/ptmx using the package under test, so these tests double as an
14+// integration check of PtsNumber/UnlockPt against the kernel.
15+func openPTYPair(t *testing.T) (master, slave *os.File) {
16+ t.Helper()
17+
18+ masterFD, err := syscall.Open("/dev/ptmx", syscall.O_RDWR|syscall.O_NOCTTY|syscall.O_CLOEXEC, 0)
19+ if err != nil {
20+ t.Skipf("open /dev/ptmx: %v", err)
21+ }
22+ master = os.NewFile(uintptr(masterFD), "pty-master")
23+
24+ if err := UnlockPt(masterFD); err != nil {
25+ _ = master.Close()
26+ t.Fatalf("UnlockPt: %v", err)
27+ }
28+
29+ n, err := PtsNumber(masterFD)
30+ if err != nil {
31+ _ = master.Close()
32+ t.Fatalf("PtsNumber: %v", err)
33+ }
34+
35+ slavePath := fmt.Sprintf("/dev/pts/%d", n)
36+ slave, err = os.OpenFile(slavePath, os.O_RDWR|syscall.O_NOCTTY, 0)
37+ if err != nil {
38+ _ = master.Close()
39+ t.Fatalf("open slave %s: %v", slavePath, err)
40+ }
41+
42+ return master, slave
43+}
44+
45+func TestIsTerminal(t *testing.T) {
46+ master, slave := openPTYPair(t)
47+ defer func() { _ = master.Close() }()
48+ defer func() { _ = slave.Close() }()
49+
50+ if !IsTerminal(int(slave.Fd())) {
51+ t.Errorf("IsTerminal(pty slave) = false, want true")
52+ }
53+
54+ r, w, err := os.Pipe()
55+ if err != nil {
56+ t.Fatalf("os.Pipe: %v", err)
57+ }
58+ defer func() { _ = r.Close() }()
59+ defer func() { _ = w.Close() }()
60+
61+ if IsTerminal(int(r.Fd())) {
62+ t.Errorf("IsTerminal(pipe) = true, want false")
63+ }
64+}
65+
66+func TestSetWinsizeGetSizeRoundtrip(t *testing.T) {
67+ master, slave := openPTYPair(t)
68+ defer func() { _ = master.Close() }()
69+ defer func() { _ = slave.Close() }()
70+
71+ tests := []struct {
72+ name string
73+ cols uint16
74+ rows uint16
75+ }{
76+ {name: "typical", cols: 80, rows: 24},
77+ {name: "wide", cols: 220, rows: 60},
78+ {name: "minimal", cols: 1, rows: 1},
79+ }
80+
81+ for _, tt := range tests {
82+ t.Run(tt.name, func(t *testing.T) {
83+ if err := SetWinsize(int(slave.Fd()), tt.cols, tt.rows); err != nil {
84+ t.Fatalf("SetWinsize: %v", err)
85+ }
86+ gotCols, gotRows, err := GetSize(int(slave.Fd()))
87+ if err != nil {
88+ t.Fatalf("GetSize: %v", err)
89+ }
90+ if gotCols != int(tt.cols) || gotRows != int(tt.rows) {
91+ t.Fatalf("GetSize = (%d, %d), want (%d, %d)", gotCols, gotRows, tt.cols, tt.rows)
92+ }
93+ })
94+ }
95+}
96+
97+func TestMakeRawRestore(t *testing.T) {
98+ master, slave := openPTYPair(t)
99+ defer func() { _ = master.Close() }()
100+ defer func() { _ = slave.Close() }()
101+
102+ fd := int(slave.Fd())
103+
104+ old, err := MakeRaw(fd)
105+ if err != nil {
106+ t.Fatalf("MakeRaw: %v", err)
107+ }
108+ if old == nil {
109+ t.Fatalf("MakeRaw returned nil state")
110+ }
111+
112+ var current syscall.Termios
113+ if err := ioctl(fd, syscall.TCGETS, unsafe.Pointer(&current)); err != nil {
114+ t.Fatalf("TCGETS after MakeRaw: %v", err)
115+ }
116+ if current.Lflag&syscall.ECHO != 0 {
117+ t.Errorf("ECHO still set after MakeRaw")
118+ }
119+ if current.Lflag&syscall.ICANON != 0 {
120+ t.Errorf("ICANON still set after MakeRaw")
121+ }
122+ if current.Cc[syscall.VMIN] != 1 {
123+ t.Errorf("Cc[VMIN] = %d, want 1", current.Cc[syscall.VMIN])
124+ }
125+
126+ if err := Restore(fd, old); err != nil {
127+ t.Fatalf("Restore: %v", err)
128+ }
129+}
130+
131+func TestForegroundProcessGroup(t *testing.T) {
132+ master, slave := openPTYPair(t)
133+ defer func() { _ = master.Close() }()
134+ defer func() { _ = slave.Close() }()
135+
136+ // No session has been established on this pty (no process ever made it
137+ // a controlling terminal via Setsid+Setctty), so the kernel legitimately
138+ // has no foreground process group to report and returns ENOTTY. That
139+ // still exercises the ioctl path; only an unexpected error is a failure.
140+ if _, err := ForegroundProcessGroup(int(slave.Fd())); err != nil && !errors.Is(err, syscall.ENOTTY) {
141+ t.Fatalf("ForegroundProcessGroup: %v", err)
142+ }
143+}

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