detached 70bf452

refactor(term): split pkg/linuxterm into platform-neutral pkg/rawterm

commit
70bf452c115e2acf5e77bb63689c1e07bb161829
author
brice · · 2026-07-16 17:41 UTC
parent
1beec74
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
7 files · +101 -105
M cmd/vev-perf-harness/launcher.go +3 -3
@@ -15,7 +15,7 @@ import (
1515 "syscall"
1616 "time"
1717
18- "github.com/bnema/vev/pkg/linuxterm"
18+ "github.com/bnema/vev/pkg/rawterm"
1919 "github.com/bnema/vev/pkg/safedir"
2020 )
2121
@@ -731,11 +731,11 @@ func openPTY() (*os.File, *os.File, error) {
731731 return nil, nil, err
732732 }
733733 closeMaster := func(err error) (*os.File, *os.File, error) { _ = syscall.Close(masterFD); return nil, nil, err }
734- number, err := linuxterm.PtsNumber(masterFD)
734+ number, err := rawterm.PtsNumber(masterFD)
735735 if err != nil {
736736 return closeMaster(err)
737737 }
738- if err := linuxterm.UnlockPt(masterFD); err != nil {
738+ if err := rawterm.UnlockPt(masterFD); err != nil {
739739 return closeMaster(err)
740740 }
741741 slaveFD, err := syscall.Open(fmt.Sprintf("/dev/pts/%d", number), syscall.O_RDWR|syscall.O_NOCTTY, 0)
@@ -15,7 +15,7 @@ import (
15 "syscall"15 "syscall"
16 "time"16 "time"
17 17
18- "github.com/bnema/vev/pkg/linuxterm"18+ "github.com/bnema/vev/pkg/rawterm"
19 "github.com/bnema/vev/pkg/safedir"19 "github.com/bnema/vev/pkg/safedir"
20 )20 )
21 21
@@ -731,11 +731,11 @@ func openPTY() (*os.File, *os.File, error) {
731 return nil, nil, err731 return nil, nil, err
732 }732 }
733 closeMaster := func(err error) (*os.File, *os.File, error) { _ = syscall.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 := linuxterm.PtsNumber(masterFD)734+ number, err := rawterm.PtsNumber(masterFD)
735 if err != nil {735 if err != nil {
736 return closeMaster(err)736 return closeMaster(err)
737 }737 }
738- if err := linuxterm.UnlockPt(masterFD); err != nil {738+ if err := rawterm.UnlockPt(masterFD); err != nil {
739 return closeMaster(err)739 return closeMaster(err)
740 }740 }
741 slaveFD, err := syscall.Open(fmt.Sprintf("/dev/pts/%d", number), syscall.O_RDWR|syscall.O_NOCTTY, 0)741 slaveFD, err := syscall.Open(fmt.Sprintf("/dev/pts/%d", number), syscall.O_RDWR|syscall.O_NOCTTY, 0)
M internal/adapters/pty/pty_linux.go +8 -20
@@ -2,10 +2,9 @@
22
33 package pty
44
5-// 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 than
7-// depending on a third-party pty package, keeping the dependency surface at
8-// the standard library alone.
5+// The Linux adapter drives /dev/ptmx directly and delegates slave preparation
6+// to rawterm rather than depending on a third-party pty package, keeping the
7+// dependency surface at the standard library alone.
98
109 import (
1110 "context"
@@ -21,7 +20,7 @@ import (
2120 "github.com/bnema/vev/internal/domain"
2221 "github.com/bnema/vev/internal/platform"
2322 "github.com/bnema/vev/internal/ports"
24- "github.com/bnema/vev/pkg/linuxterm"
23+ "github.com/bnema/vev/pkg/rawterm"
2524 )
2625
2726 // killGracePeriod is how long Close waits for a signalled child to exit before
@@ -56,20 +55,9 @@ func (Factory) Open(ctx context.Context, command string, args []string, env []st
5655 }
5756 }()
5857
59- ptn, err := linuxterm.PtsNumber(masterFd)
58+ slave, err := rawterm.PreparePty(masterFd)
6059 if err != nil {
61- return nil, fmt.Errorf("pty: TIOCGPTN: %w", err)
62- }
63-
64- // Unlock the slave so it can be opened.
65- if err := linuxterm.UnlockPt(masterFd); err != nil {
66- return nil, fmt.Errorf("pty: TIOCSPTLCK: %w", err)
67- }
68-
69- slaveName := fmt.Sprintf("/dev/pts/%d", ptn)
70- slave, err := os.OpenFile(slaveName, os.O_RDWR|syscall.O_NOCTTY, 0)
71- if err != nil {
72- return nil, fmt.Errorf("pty: open %s: %w", slaveName, err)
60+ return nil, fmt.Errorf("pty: prepare pty: %w", err)
7361 }
7462 // The child dups the slave into its std fds and holds its own copies, so the
7563 // parent always drops the slave once Start has run (or on any error).
@@ -189,7 +177,7 @@ func (p *linuxPTY) ForegroundPgid() (int, error) {
189177 var pgid int
190178 var ioctlErr error
191179 if err := rc.Control(func(fd uintptr) {
192- pgid, ioctlErr = linuxterm.ForegroundProcessGroup(int(fd))
180+ pgid, ioctlErr = rawterm.ForegroundProcessGroup(int(fd))
193181 }); err != nil {
194182 return 0, fmt.Errorf("pty: foreground pgid: %w", err)
195183 }
@@ -240,5 +228,5 @@ func signalProcessGroup(pid int, signal syscall.Signal) error {
240228
241229 // setWinsize applies sz to the terminal referenced by fd via TIOCSWINSZ.
242230 func setWinsize(fd int, sz domain.Size) error {
243- return linuxterm.SetWinsize(fd, uint16(sz.Cols), uint16(sz.Rows))
231+ return rawterm.SetWinsize(fd, uint16(sz.Cols), uint16(sz.Rows))
244232 }
@@ -2,10 +2,9 @@
2 2
3 package pty3 package pty
4 4
5-// The Linux adapter drives /dev/ptmx directly (open master, TIOCGPTN to learn5+// The Linux adapter drives /dev/ptmx directly and delegates slave preparation
6-// the slave index, TIOCSPTLCK to unlock, then open /dev/pts/<N>) rather than6+// to rawterm rather than depending on a third-party pty package, keeping the
7-// depending on a third-party pty package, keeping the dependency surface at7+// dependency surface at the standard library alone.
8-// the standard library alone.
9 8
10 import (9 import (
11 "context"10 "context"
@@ -21,7 +20,7 @@ import (
21 "github.com/bnema/vev/internal/domain"20 "github.com/bnema/vev/internal/domain"
22 "github.com/bnema/vev/internal/platform"21 "github.com/bnema/vev/internal/platform"
23 "github.com/bnema/vev/internal/ports"22 "github.com/bnema/vev/internal/ports"
24- "github.com/bnema/vev/pkg/linuxterm"23+ "github.com/bnema/vev/pkg/rawterm"
25 )24 )
26 25
27 // killGracePeriod is how long Close waits for a signalled child to exit before26 // killGracePeriod is how long Close waits for a signalled child to exit before
@@ -56,20 +55,9 @@ func (Factory) Open(ctx context.Context, command string, args []string, env []st
56 }55 }
57 }()56 }()
58 57
59- ptn, err := linuxterm.PtsNumber(masterFd)58+ slave, err := rawterm.PreparePty(masterFd)
60 if err != nil {59 if err != nil {
61- return nil, fmt.Errorf("pty: TIOCGPTN: %w", err)60+ return nil, fmt.Errorf("pty: prepare pty: %w", err)
62- }
63-
64- // Unlock the slave so it can be opened.
65- if err := linuxterm.UnlockPt(masterFd); err != nil {
66- return nil, fmt.Errorf("pty: TIOCSPTLCK: %w", err)
67- }
68-
69- slaveName := fmt.Sprintf("/dev/pts/%d", ptn)
70- slave, err := os.OpenFile(slaveName, os.O_RDWR|syscall.O_NOCTTY, 0)
71- if err != nil {
72- return nil, fmt.Errorf("pty: open %s: %w", slaveName, err)
73 }61 }
74 // The child dups the slave into its std fds and holds its own copies, so the62 // The child dups the slave into its std fds and holds its own copies, so the
75 // parent always drops the slave once Start has run (or on any error).63 // parent always drops the slave once Start has run (or on any error).
@@ -189,7 +177,7 @@ func (p *linuxPTY) ForegroundPgid() (int, error) {
189 var pgid int177 var pgid int
190 var ioctlErr error178 var ioctlErr error
191 if err := rc.Control(func(fd uintptr) {179 if err := rc.Control(func(fd uintptr) {
192- pgid, ioctlErr = linuxterm.ForegroundProcessGroup(int(fd))180+ pgid, ioctlErr = rawterm.ForegroundProcessGroup(int(fd))
193 }); err != nil {181 }); err != nil {
194 return 0, fmt.Errorf("pty: foreground pgid: %w", err)182 return 0, fmt.Errorf("pty: foreground pgid: %w", err)
195 }183 }
@@ -240,5 +228,5 @@ func signalProcessGroup(pid int, signal syscall.Signal) error {
240 228
241 // setWinsize applies sz to the terminal referenced by fd via TIOCSWINSZ.229 // setWinsize applies sz to the terminal referenced by fd via TIOCSWINSZ.
242 func setWinsize(fd int, sz domain.Size) error {230 func setWinsize(fd int, sz domain.Size) error {
243- return linuxterm.SetWinsize(fd, uint16(sz.Cols), uint16(sz.Rows))231+ return rawterm.SetWinsize(fd, uint16(sz.Cols), uint16(sz.Rows))
244 }232 }
M internal/adapters/term/pty_integration_test.go +4 -17
@@ -3,14 +3,13 @@
33 package term
44
55 import (
6- "fmt"
76 "io"
87 "os"
98 "syscall"
109 "testing"
1110 "time"
1211
13- "github.com/bnema/vev/pkg/linuxterm"
12+ "github.com/bnema/vev/pkg/rawterm"
1413 )
1514
1615 // openPTYPair opens a real Linux PTY master/slave pair directly via
@@ -24,22 +23,10 @@ func openPTYPair(t *testing.T) (master, slave *os.File) {
2423 t.Fatalf("open /dev/ptmx: %v", err)
2524 }
2625
27- if err := linuxterm.UnlockPt(int(m.Fd())); err != nil {
28- _ = m.Close()
29- t.Fatalf("unlock pty (TIOCSPTLCK): %v", err)
30- }
31-
32- n, err := linuxterm.PtsNumber(int(m.Fd()))
33- if err != nil {
34- _ = m.Close()
35- t.Fatalf("get pty number (TIOCGPTN): %v", err)
36- }
37-
38- slavePath := fmt.Sprintf("/dev/pts/%d", n)
39- s, err := os.OpenFile(slavePath, os.O_RDWR|syscall.O_NOCTTY, 0)
26+ s, err := rawterm.PreparePty(int(m.Fd()))
4027 if err != nil {
4128 _ = m.Close()
42- t.Fatalf("open slave %s: %v", slavePath, err)
29+ t.Fatalf("prepare pty: %v", err)
4330 }
4431
4532 return m, s
@@ -54,7 +41,7 @@ func TestTerminal_RealPTY_RawModeSizeAndEscapes(t *testing.T) {
5441 defer func() { _ = master.Close() }()
5542 defer func() { _ = slave.Close() }()
5643
57- if err := linuxterm.SetWinsize(int(slave.Fd()), 80, 24); err != nil {
44+ if err := rawterm.SetWinsize(int(slave.Fd()), 80, 24); err != nil {
5845 t.Fatalf("set winsize: %v", err)
5946 }
6047
@@ -3,14 +3,13 @@
3 package term3 package term
4 4
5 import (5 import (
6- "fmt"
7 "io"6 "io"
8 "os"7 "os"
9 "syscall"8 "syscall"
10 "testing"9 "testing"
11 "time"10 "time"
12 11
13- "github.com/bnema/vev/pkg/linuxterm"12+ "github.com/bnema/vev/pkg/rawterm"
14 )13 )
15 14
16 // openPTYPair opens a real Linux PTY master/slave pair directly via15 // openPTYPair opens a real Linux PTY master/slave pair directly via
@@ -24,22 +23,10 @@ func openPTYPair(t *testing.T) (master, slave *os.File) {
24 t.Fatalf("open /dev/ptmx: %v", err)23 t.Fatalf("open /dev/ptmx: %v", err)
25 }24 }
26 25
27- if err := linuxterm.UnlockPt(int(m.Fd())); err != nil {26+ s, err := rawterm.PreparePty(int(m.Fd()))
28- _ = m.Close()
29- t.Fatalf("unlock pty (TIOCSPTLCK): %v", err)
30- }
31-
32- n, err := linuxterm.PtsNumber(int(m.Fd()))
33- if err != nil {
34- _ = m.Close()
35- t.Fatalf("get pty number (TIOCGPTN): %v", err)
36- }
37-
38- slavePath := fmt.Sprintf("/dev/pts/%d", n)
39- s, err := os.OpenFile(slavePath, os.O_RDWR|syscall.O_NOCTTY, 0)
40 if err != nil {27 if err != nil {
41 _ = m.Close()28 _ = m.Close()
42- t.Fatalf("open slave %s: %v", slavePath, err)29+ t.Fatalf("prepare pty: %v", err)
43 }30 }
44 31
45 return m, s32 return m, s
@@ -54,7 +41,7 @@ func TestTerminal_RealPTY_RawModeSizeAndEscapes(t *testing.T) {
54 defer func() { _ = master.Close() }()41 defer func() { _ = master.Close() }()
55 defer func() { _ = slave.Close() }()42 defer func() { _ = slave.Close() }()
56 43
57- if err := linuxterm.SetWinsize(int(slave.Fd()), 80, 24); err != nil {44+ if err := rawterm.SetWinsize(int(slave.Fd()), 80, 24); err != nil {
58 t.Fatalf("set winsize: %v", err)45 t.Fatalf("set winsize: %v", err)
59 }46 }
60 47
M internal/adapters/term/terminal.go +10 -10
@@ -11,7 +11,7 @@ import (
1111
1212 "github.com/bnema/vev/internal/domain"
1313 "github.com/bnema/vev/internal/ports"
14- "github.com/bnema/vev/pkg/linuxterm"
14+ "github.com/bnema/vev/pkg/rawterm"
1515 )
1616
1717 // Compile-time check that Terminal satisfies ports.Terminal.
@@ -49,10 +49,10 @@ type Terminal struct {
4949 bw *batchWriter
5050
5151 mu sync.Mutex
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
52+ orig *rawterm.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
5656
5757 // Resize watcher state, also guarded by mu so watcher start
5858 // (ResizeEvents) and watcher stop (restore) are strictly ordered:
@@ -95,9 +95,9 @@ func (t *Terminal) EnterRaw() (func() error, error) {
9595 return t.restoreFn, nil
9696 }
9797
98- var old *linuxterm.State
99- if linuxterm.IsTerminal(t.fd) {
100- s, err := linuxterm.MakeRaw(t.fd)
98+ var old *rawterm.State
99+ if rawterm.IsTerminal(t.fd) {
100+ s, err := rawterm.MakeRaw(t.fd)
101101 if err != nil {
102102 return nil, fmt.Errorf("term: make raw: %w", err)
103103 }
@@ -170,14 +170,14 @@ func (t *Terminal) restoreRawLocked() error {
170170 if t.orig == nil {
171171 return nil
172172 }
173- err := linuxterm.Restore(t.fd, t.orig)
173+ err := rawterm.Restore(t.fd, t.orig)
174174 t.orig = nil
175175 return err
176176 }
177177
178178 // Size returns the current terminal dimensions.
179179 func (t *Terminal) Size() (domain.Size, error) {
180- cols, rows, err := linuxterm.GetSize(t.fd)
180+ cols, rows, err := rawterm.GetSize(t.fd)
181181 if err != nil {
182182 return domain.Size{}, fmt.Errorf("term: get size: %w", err)
183183 }
@@ -11,7 +11,7 @@ import (
11 11
12 "github.com/bnema/vev/internal/domain"12 "github.com/bnema/vev/internal/domain"
13 "github.com/bnema/vev/internal/ports"13 "github.com/bnema/vev/internal/ports"
14- "github.com/bnema/vev/pkg/linuxterm"14+ "github.com/bnema/vev/pkg/rawterm"
15 )15 )
16 16
17 // Compile-time check that Terminal satisfies ports.Terminal.17 // Compile-time check that Terminal satisfies ports.Terminal.
@@ -49,10 +49,10 @@ type Terminal struct {
49 bw *batchWriter49 bw *batchWriter
50 50
51 mu sync.Mutex51 mu sync.Mutex
52- orig *linuxterm.State // saved terminal state; nil when raw mode wasn't entered (or was restored)52+ orig *rawterm.State // saved terminal state; nil when raw mode wasn't entered (or was restored)
53- entered bool // true between a successful EnterRaw and its restore53+ entered bool // true between a successful EnterRaw and its restore
54- 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
55- restoreFn func() error // the single idempotent restore closure for the current session55+ restoreFn func() error // the single idempotent restore closure for the current session
56 56
57 // Resize watcher state, also guarded by mu so watcher start57 // Resize watcher state, also guarded by mu so watcher start
58 // (ResizeEvents) and watcher stop (restore) are strictly ordered:58 // (ResizeEvents) and watcher stop (restore) are strictly ordered:
@@ -95,9 +95,9 @@ func (t *Terminal) EnterRaw() (func() error, error) {
95 return t.restoreFn, nil95 return t.restoreFn, nil
96 }96 }
97 97
98- var old *linuxterm.State98+ var old *rawterm.State
99- if linuxterm.IsTerminal(t.fd) {99+ if rawterm.IsTerminal(t.fd) {
100- s, err := linuxterm.MakeRaw(t.fd)100+ s, err := rawterm.MakeRaw(t.fd)
101 if err != nil {101 if err != nil {
102 return nil, fmt.Errorf("term: make raw: %w", err)102 return nil, fmt.Errorf("term: make raw: %w", err)
103 }103 }
@@ -170,14 +170,14 @@ func (t *Terminal) restoreRawLocked() error {
170 if t.orig == nil {170 if t.orig == nil {
171 return nil171 return nil
172 }172 }
173- err := linuxterm.Restore(t.fd, t.orig)173+ err := rawterm.Restore(t.fd, t.orig)
174 t.orig = nil174 t.orig = nil
175 return err175 return err
176 }176 }
177 177
178 // Size returns the current terminal dimensions.178 // Size returns the current terminal dimensions.
179 func (t *Terminal) Size() (domain.Size, error) {179 func (t *Terminal) Size() (domain.Size, error) {
180- cols, rows, err := linuxterm.GetSize(t.fd)180+ cols, rows, err := rawterm.GetSize(t.fd)
181 if err != nil {181 if err != nil {
182 return domain.Size{}, fmt.Errorf("term: get size: %w", err)182 return domain.Size{}, fmt.Errorf("term: get size: %w", err)
183 }183 }
R pkg/linuxterm/linuxterm.go → pkg/rawterm/rawterm.go +16 -36 renamed, 61% similar
@@ -1,9 +1,9 @@
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
1+//go:build linux || darwin
2+
3+// Package rawterm provides the small set of terminal-control primitives vev
4+// needs directly on top of the frozen stdlib syscall package. The common raw
5+// mode and terminal-size operations use platform-specific termios requests.
6+package rawterm
77
88 import (
99 "fmt"
@@ -40,7 +40,7 @@ func ioctl(fd int, req uintptr, arg unsafe.Pointer) error {
4040 // IsTerminal reports whether fd refers to a terminal device.
4141 func IsTerminal(fd int) bool {
4242 var t syscall.Termios
43- return ioctl(fd, syscall.TCGETS, unsafe.Pointer(&t)) == nil
43+ return ioctl(fd, reqGetTermios, unsafe.Pointer(&t)) == nil
4444 }
4545
4646 // MakeRaw puts the terminal referenced by fd into raw mode and returns its
@@ -48,8 +48,8 @@ func IsTerminal(fd int) bool {
4848 // changes applied match golang.org/x/term's MakeRaw semantics exactly.
4949 func MakeRaw(fd int) (*State, error) {
5050 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)
51+ if err := ioctl(fd, reqGetTermios, unsafe.Pointer(&oldState.termios)); err != nil {
52+ return nil, fmt.Errorf("rawterm: get termios: %w", err)
5353 }
5454
5555 raw := oldState.termios
@@ -61,8 +61,8 @@ func MakeRaw(fd int) (*State, error) {
6161 raw.Cc[syscall.VMIN] = 1
6262 raw.Cc[syscall.VTIME] = 0
6363
64- if err := ioctl(fd, syscall.TCSETS, unsafe.Pointer(&raw)); err != nil {
65- return nil, fmt.Errorf("linuxterm: set termios: %w", err)
64+ if err := ioctl(fd, reqSetTermios, unsafe.Pointer(&raw)); err != nil {
65+ return nil, fmt.Errorf("rawterm: set termios: %w", err)
6666 }
6767
6868 return &oldState, nil
@@ -71,8 +71,8 @@ func MakeRaw(fd int) (*State, error) {
7171 // Restore applies a previously captured State back to the terminal
7272 // referenced by fd.
7373 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)
74+ if err := ioctl(fd, reqSetTermios, unsafe.Pointer(&s.termios)); err != nil {
75+ return fmt.Errorf("rawterm: set termios: %w", err)
7676 }
7777 return nil
7878 }
@@ -81,7 +81,7 @@ func Restore(fd int, s *State) error {
8181 func GetSize(fd int) (cols, rows int, err error) {
8282 var ws winsize
8383 if err := ioctl(fd, syscall.TIOCGWINSZ, unsafe.Pointer(&ws)); err != nil {
84- return 0, 0, fmt.Errorf("linuxterm: get winsize: %w", err)
84+ return 0, 0, fmt.Errorf("rawterm: get winsize: %w", err)
8585 }
8686 return int(ws.Col), int(ws.Row), nil
8787 }
@@ -90,27 +90,7 @@ func GetSize(fd int) (cols, rows int, err error) {
9090 func SetWinsize(fd int, cols, rows uint16) error {
9191 ws := winsize{Row: rows, Col: cols}
9292 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)
93+ return fmt.Errorf("rawterm: set winsize: %w", err)
11494 }
11595 return nil
11696 }
@@ -120,7 +100,7 @@ func UnlockPt(masterFD int) error {
120100 func ForegroundProcessGroup(fd int) (int, error) {
121101 var pgid int32
122102 if err := ioctl(fd, syscall.TIOCGPGRP, unsafe.Pointer(&pgid)); err != nil {
123- return 0, fmt.Errorf("linuxterm: get foreground process group: %w", err)
103+ return 0, fmt.Errorf("rawterm: get foreground process group: %w", err)
124104 }
125105 return int(pgid), nil
126106 }
@@ -1,9 +1,9 @@
1-// Package linuxterm provides the small set of terminal-control primitives1+//go:build linux || darwin
2-// vev needs (raw mode, window size, pty allocation) directly on top of the2+
3-// frozen stdlib syscall package, so the module does not depend on3+// Package rawterm provides the small set of terminal-control primitives vev
4-// golang.org/x/term or golang.org/x/sys. vev is Linux-only, and the Linux4+// needs directly on top of the frozen stdlib syscall package. The common raw
5-// TIOC*/TCGETS ioctl numbers and Termios layout syscall exposes are stable.5+// mode and terminal-size operations use platform-specific termios requests.
6-package linuxterm6+package rawterm
7 7
8 import (8 import (
9 "fmt"9 "fmt"
@@ -40,7 +40,7 @@ func ioctl(fd int, req uintptr, arg unsafe.Pointer) error {
40 // IsTerminal reports whether fd refers to a terminal device.40 // IsTerminal reports whether fd refers to a terminal device.
41 func IsTerminal(fd int) bool {41 func IsTerminal(fd int) bool {
42 var t syscall.Termios42 var t syscall.Termios
43- return ioctl(fd, syscall.TCGETS, unsafe.Pointer(&t)) == nil43+ return ioctl(fd, reqGetTermios, unsafe.Pointer(&t)) == nil
44 }44 }
45 45
46 // MakeRaw puts the terminal referenced by fd into raw mode and returns its46 // MakeRaw puts the terminal referenced by fd into raw mode and returns its
@@ -48,8 +48,8 @@ func IsTerminal(fd int) bool {
48 // changes applied match golang.org/x/term's MakeRaw semantics exactly.48 // changes applied match golang.org/x/term's MakeRaw semantics exactly.
49 func MakeRaw(fd int) (*State, error) {49 func MakeRaw(fd int) (*State, error) {
50 var oldState State50 var oldState State
51- if err := ioctl(fd, syscall.TCGETS, unsafe.Pointer(&oldState.termios)); err != nil {51+ if err := ioctl(fd, reqGetTermios, unsafe.Pointer(&oldState.termios)); err != nil {
52- return nil, fmt.Errorf("linuxterm: get termios: %w", err)52+ return nil, fmt.Errorf("rawterm: get termios: %w", err)
53 }53 }
54 54
55 raw := oldState.termios55 raw := oldState.termios
@@ -61,8 +61,8 @@ func MakeRaw(fd int) (*State, error) {
61 raw.Cc[syscall.VMIN] = 161 raw.Cc[syscall.VMIN] = 1
62 raw.Cc[syscall.VTIME] = 062 raw.Cc[syscall.VTIME] = 0
63 63
64- if err := ioctl(fd, syscall.TCSETS, unsafe.Pointer(&raw)); err != nil {64+ if err := ioctl(fd, reqSetTermios, unsafe.Pointer(&raw)); err != nil {
65- return nil, fmt.Errorf("linuxterm: set termios: %w", err)65+ return nil, fmt.Errorf("rawterm: set termios: %w", err)
66 }66 }
67 67
68 return &oldState, nil68 return &oldState, nil
@@ -71,8 +71,8 @@ func MakeRaw(fd int) (*State, error) {
71 // Restore applies a previously captured State back to the terminal71 // Restore applies a previously captured State back to the terminal
72 // referenced by fd.72 // referenced by fd.
73 func Restore(fd int, s *State) error {73 func Restore(fd int, s *State) error {
74- if err := ioctl(fd, syscall.TCSETS, unsafe.Pointer(&s.termios)); err != nil {74+ if err := ioctl(fd, reqSetTermios, unsafe.Pointer(&s.termios)); err != nil {
75- return fmt.Errorf("linuxterm: set termios: %w", err)75+ return fmt.Errorf("rawterm: set termios: %w", err)
76 }76 }
77 return nil77 return nil
78 }78 }
@@ -81,7 +81,7 @@ func Restore(fd int, s *State) error {
81 func GetSize(fd int) (cols, rows int, err error) {81 func GetSize(fd int) (cols, rows int, err error) {
82 var ws winsize82 var ws winsize
83 if err := ioctl(fd, syscall.TIOCGWINSZ, unsafe.Pointer(&ws)); err != nil {83 if err := ioctl(fd, syscall.TIOCGWINSZ, unsafe.Pointer(&ws)); err != nil {
84- return 0, 0, fmt.Errorf("linuxterm: get winsize: %w", err)84+ return 0, 0, fmt.Errorf("rawterm: get winsize: %w", err)
85 }85 }
86 return int(ws.Col), int(ws.Row), nil86 return int(ws.Col), int(ws.Row), nil
87 }87 }
@@ -90,27 +90,7 @@ func GetSize(fd int) (cols, rows int, err error) {
90 func SetWinsize(fd int, cols, rows uint16) error {90 func SetWinsize(fd int, cols, rows uint16) error {
91 ws := winsize{Row: rows, Col: cols}91 ws := winsize{Row: rows, Col: cols}
92 if err := ioctl(fd, syscall.TIOCSWINSZ, unsafe.Pointer(&ws)); err != nil {92 if err := ioctl(fd, syscall.TIOCSWINSZ, unsafe.Pointer(&ws)); err != nil {
93- return fmt.Errorf("linuxterm: set winsize: %w", err)93+ return fmt.Errorf("rawterm: 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 }94 }
115 return nil95 return nil
116 }96 }
@@ -120,7 +100,7 @@ func UnlockPt(masterFD int) error {
120 func ForegroundProcessGroup(fd int) (int, error) {100 func ForegroundProcessGroup(fd int) (int, error) {
121 var pgid int32101 var pgid int32
122 if err := ioctl(fd, syscall.TIOCGPGRP, unsafe.Pointer(&pgid)); err != nil {102 if err := ioctl(fd, syscall.TIOCGPGRP, unsafe.Pointer(&pgid)); err != nil {
123- return 0, fmt.Errorf("linuxterm: get foreground process group: %w", err)103+ return 0, fmt.Errorf("rawterm: get foreground process group: %w", err)
124 }104 }
125 return int(pgid), nil105 return int(pgid), nil
126 }106 }
R pkg/linuxterm/linuxterm_test.go → pkg/rawterm/rawterm_test.go +8 -19 renamed, 84% similar
@@ -1,8 +1,9 @@
1-package linuxterm
1+//go:build linux
2+
3+package rawterm
24
35 import (
46 "errors"
5- "fmt"
67 "os"
78 "syscall"
89 "testing"
@@ -11,7 +12,7 @@ import (
1112
1213 // openPTYPair opens a real Linux PTY master/slave pair directly via
1314 // /dev/ptmx using the package under test, so these tests double as an
14-// integration check of PtsNumber/UnlockPt against the kernel.
15+// integration check of PreparePty against the kernel.
1516 func openPTYPair(t *testing.T) (master, slave *os.File) {
1617 t.Helper()
1718
@@ -21,22 +22,10 @@ func openPTYPair(t *testing.T) (master, slave *os.File) {
2122 }
2223 master = os.NewFile(uintptr(masterFD), "pty-master")
2324
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)
25+ slave, err = PreparePty(masterFD)
3726 if err != nil {
3827 _ = master.Close()
39- t.Fatalf("open slave %s: %v", slavePath, err)
28+ t.Fatalf("PreparePty: %v", err)
4029 }
4130
4231 return master, slave
@@ -110,8 +99,8 @@ func TestMakeRawRestore(t *testing.T) {
11099 }
111100
112101 var current syscall.Termios
113- if err := ioctl(fd, syscall.TCGETS, unsafe.Pointer(&current)); err != nil {
114- t.Fatalf("TCGETS after MakeRaw: %v", err)
102+ if err := ioctl(fd, reqGetTermios, unsafe.Pointer(&current)); err != nil {
103+ t.Fatalf("get termios after MakeRaw: %v", err)
115104 }
116105 if current.Lflag&syscall.ECHO != 0 {
117106 t.Errorf("ECHO still set after MakeRaw")
@@ -1,8 +1,9 @@
1-package linuxterm1+//go:build linux
2+
3+package rawterm
2 4
3 import (5 import (
4 "errors"6 "errors"
5- "fmt"
6 "os"7 "os"
7 "syscall"8 "syscall"
8 "testing"9 "testing"
@@ -11,7 +12,7 @@ import (
11 12
12 // openPTYPair opens a real Linux PTY master/slave pair directly via13 // openPTYPair opens a real Linux PTY master/slave pair directly via
13 // /dev/ptmx using the package under test, so these tests double as an14 // /dev/ptmx using the package under test, so these tests double as an
14-// integration check of PtsNumber/UnlockPt against the kernel.15+// integration check of PreparePty against the kernel.
15 func openPTYPair(t *testing.T) (master, slave *os.File) {16 func openPTYPair(t *testing.T) (master, slave *os.File) {
16 t.Helper()17 t.Helper()
17 18
@@ -21,22 +22,10 @@ func openPTYPair(t *testing.T) (master, slave *os.File) {
21 }22 }
22 master = os.NewFile(uintptr(masterFD), "pty-master")23 master = os.NewFile(uintptr(masterFD), "pty-master")
23 24
24- if err := UnlockPt(masterFD); err != nil {25+ slave, err = PreparePty(masterFD)
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 {26 if err != nil {
38 _ = master.Close()27 _ = master.Close()
39- t.Fatalf("open slave %s: %v", slavePath, err)28+ t.Fatalf("PreparePty: %v", err)
40 }29 }
41 30
42 return master, slave31 return master, slave
@@ -110,8 +99,8 @@ func TestMakeRawRestore(t *testing.T) {
110 }99 }
111 100
112 var current syscall.Termios101 var current syscall.Termios
113- if err := ioctl(fd, syscall.TCGETS, unsafe.Pointer(&current)); err != nil {102+ if err := ioctl(fd, reqGetTermios, unsafe.Pointer(&current)); err != nil {
114- t.Fatalf("TCGETS after MakeRaw: %v", err)103+ t.Fatalf("get termios after MakeRaw: %v", err)
115 }104 }
116 if current.Lflag&syscall.ECHO != 0 {105 if current.Lflag&syscall.ECHO != 0 {
117 t.Errorf("ECHO still set after MakeRaw")106 t.Errorf("ECHO still set after MakeRaw")
A pkg/rawterm/term_linux.go +52 -0
@@ -0,0 +1,52 @@
1+//go:build linux
2+
3+package rawterm
4+
5+import (
6+ "fmt"
7+ "os"
8+ "syscall"
9+ "unsafe"
10+)
11+
12+const (
13+ reqGetTermios = syscall.TCGETS
14+ reqSetTermios = syscall.TCSETS
15+)
16+
17+// PtsNumber returns the pty slave number for the master referenced by
18+// masterFD (TIOCGPTN).
19+func PtsNumber(masterFD int) (int, error) {
20+ var n int32
21+ if err := ioctl(masterFD, syscall.TIOCGPTN, unsafe.Pointer(&n)); err != nil {
22+ return 0, fmt.Errorf("rawterm: get pts number: %w", err)
23+ }
24+ return int(n), nil
25+}
26+
27+// UnlockPt unlocks the pty slave associated with masterFD so it can be opened
28+// (TIOCSPTLCK).
29+func UnlockPt(masterFD int) error {
30+ var unlock int32
31+ if err := ioctl(masterFD, syscall.TIOCSPTLCK, unsafe.Pointer(&unlock)); err != nil {
32+ return fmt.Errorf("rawterm: unlock pty: %w", err)
33+ }
34+ return nil
35+}
36+
37+// PreparePty unlocks the slave associated with masterFD and opens it.
38+func PreparePty(masterFD int) (*os.File, error) {
39+ n, err := PtsNumber(masterFD)
40+ if err != nil {
41+ return nil, err
42+ }
43+ if err := UnlockPt(masterFD); err != nil {
44+ return nil, err
45+ }
46+ slaveName := fmt.Sprintf("/dev/pts/%d", n)
47+ slave, err := os.OpenFile(slaveName, os.O_RDWR|syscall.O_NOCTTY, 0)
48+ if err != nil {
49+ return nil, fmt.Errorf("rawterm: open %s: %w", slaveName, err)
50+ }
51+ return slave, nil
52+}
@@ -0,0 +1,52 @@
1+//go:build linux
2+
3+package rawterm
4+
5+import (
6+ "fmt"
7+ "os"
8+ "syscall"
9+ "unsafe"
10+)
11+
12+const (
13+ reqGetTermios = syscall.TCGETS
14+ reqSetTermios = syscall.TCSETS
15+)
16+
17+// PtsNumber returns the pty slave number for the master referenced by
18+// masterFD (TIOCGPTN).
19+func PtsNumber(masterFD int) (int, error) {
20+ var n int32
21+ if err := ioctl(masterFD, syscall.TIOCGPTN, unsafe.Pointer(&n)); err != nil {
22+ return 0, fmt.Errorf("rawterm: get pts number: %w", err)
23+ }
24+ return int(n), nil
25+}
26+
27+// UnlockPt unlocks the pty slave associated with masterFD so it can be opened
28+// (TIOCSPTLCK).
29+func UnlockPt(masterFD int) error {
30+ var unlock int32
31+ if err := ioctl(masterFD, syscall.TIOCSPTLCK, unsafe.Pointer(&unlock)); err != nil {
32+ return fmt.Errorf("rawterm: unlock pty: %w", err)
33+ }
34+ return nil
35+}
36+
37+// PreparePty unlocks the slave associated with masterFD and opens it.
38+func PreparePty(masterFD int) (*os.File, error) {
39+ n, err := PtsNumber(masterFD)
40+ if err != nil {
41+ return nil, err
42+ }
43+ if err := UnlockPt(masterFD); err != nil {
44+ return nil, err
45+ }
46+ slaveName := fmt.Sprintf("/dev/pts/%d", n)
47+ slave, err := os.OpenFile(slaveName, os.O_RDWR|syscall.O_NOCTTY, 0)
48+ if err != nil {
49+ return nil, fmt.Errorf("rawterm: open %s: %w", slaveName, err)
50+ }
51+ return slave, nil
52+}

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