refactor(term): split pkg/linuxterm into platform-neutral pkg/rawterm
- commit
- 70bf452c115e2acf5e77bb63689c1e07bb161829
- author
- brice · · 2026-07-16 17:41 UTC
- parent
- 1beec74
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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 ( | |||
| 15 | 15 | "syscall" | |
| 16 | 16 | "time" | |
| 17 | 17 | ||
| 18 | - | "github.com/bnema/vev/pkg/linuxterm" | |
| 18 | + | "github.com/bnema/vev/pkg/rawterm" | |
| 19 | 19 | "github.com/bnema/vev/pkg/safedir" | |
| 20 | 20 | ) | |
| 21 | 21 | ||
| @@ -731,11 +731,11 @@ func openPTY() (*os.File, *os.File, error) { | |||
| 731 | 731 | return nil, nil, err | |
| 732 | 732 | } | |
| 733 | 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 | 735 | if err != nil { | |
| 736 | 736 | return closeMaster(err) | |
| 737 | 737 | } | |
| 738 | - | if err := linuxterm.UnlockPt(masterFD); err != nil { | |
| 738 | + | if err := rawterm.UnlockPt(masterFD); err != nil { | |
| 739 | 739 | return closeMaster(err) | |
| 740 | 740 | } | |
| 741 | 741 | 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, err | 731 | 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 @@ | |||
| 2 | 2 | ||
| 3 | 3 | package pty | |
| 4 | 4 | ||
| 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. | |
| 9 | 8 | ||
| 10 | 9 | import ( | |
| 11 | 10 | "context" | |
| @@ -21,7 +20,7 @@ import ( | |||
| 21 | 20 | "github.com/bnema/vev/internal/domain" | |
| 22 | 21 | "github.com/bnema/vev/internal/platform" | |
| 23 | 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 | 26 | // 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 | 59 | 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) | |
| 73 | 61 | } | |
| 74 | 62 | // The child dups the slave into its std fds and holds its own copies, so the | |
| 75 | 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 | 177 | var pgid int | |
| 190 | 178 | var ioctlErr error | |
| 191 | 179 | if err := rc.Control(func(fd uintptr) { | |
| 192 | - | pgid, ioctlErr = linuxterm.ForegroundProcessGroup(int(fd)) | |
| 180 | + | pgid, ioctlErr = rawterm.ForegroundProcessGroup(int(fd)) | |
| 193 | 181 | }); err != nil { | |
| 194 | 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 | 229 | // setWinsize applies sz to the terminal referenced by fd via TIOCSWINSZ. | |
| 242 | 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 | } | |
| @@ -2,10 +2,9 @@ | |||||
| 2 | 2 | ||||
| 3 | package pty | 3 | package pty | ||
| 4 | 4 | ||||
| 5 | - | // The Linux adapter drives /dev/ptmx directly (open master, TIOCGPTN to learn | 5 | + | // The Linux adapter drives /dev/ptmx directly and delegates slave preparation |
| 6 | - | // the slave index, TIOCSPTLCK to unlock, then open /dev/pts/<N>) rather than | 6 | + | // 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 at | 7 | + | // 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 before | 26 | // 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 the | 62 | // 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 int | 177 | var pgid int | ||
| 190 | var ioctlErr error | 178 | 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 @@ | |||
| 3 | 3 | package term | |
| 4 | 4 | ||
| 5 | 5 | import ( | |
| 6 | - | "fmt" | |
| 7 | 6 | "io" | |
| 8 | 7 | "os" | |
| 9 | 8 | "syscall" | |
| 10 | 9 | "testing" | |
| 11 | 10 | "time" | |
| 12 | 11 | ||
| 13 | - | "github.com/bnema/vev/pkg/linuxterm" | |
| 12 | + | "github.com/bnema/vev/pkg/rawterm" | |
| 14 | 13 | ) | |
| 15 | 14 | ||
| 16 | 15 | // 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 | 23 | t.Fatalf("open /dev/ptmx: %v", err) | |
| 25 | 24 | } | |
| 26 | 25 | ||
| 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())) | |
| 40 | 27 | if err != nil { | |
| 41 | 28 | _ = m.Close() | |
| 42 | - | t.Fatalf("open slave %s: %v", slavePath, err) | |
| 29 | + | t.Fatalf("prepare pty: %v", err) | |
| 43 | 30 | } | |
| 44 | 31 | ||
| 45 | 32 | return m, s | |
| @@ -54,7 +41,7 @@ func TestTerminal_RealPTY_RawModeSizeAndEscapes(t *testing.T) { | |||
| 54 | 41 | defer func() { _ = master.Close() }() | |
| 55 | 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 | 45 | t.Fatalf("set winsize: %v", err) | |
| 59 | 46 | } | |
| 60 | 47 | ||
| @@ -3,14 +3,13 @@ | |||||
| 3 | package term | 3 | 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 via | 15 | // 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, s | 32 | 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 ( | |||
| 11 | 11 | ||
| 12 | 12 | "github.com/bnema/vev/internal/domain" | |
| 13 | 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 | 17 | // Compile-time check that Terminal satisfies ports.Terminal. | |
| @@ -49,10 +49,10 @@ type Terminal struct { | |||
| 49 | 49 | bw *batchWriter | |
| 50 | 50 | ||
| 51 | 51 | 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 | |
| 56 | 56 | ||
| 57 | 57 | // Resize watcher state, also guarded by mu so watcher start | |
| 58 | 58 | // (ResizeEvents) and watcher stop (restore) are strictly ordered: | |
| @@ -95,9 +95,9 @@ func (t *Terminal) EnterRaw() (func() error, error) { | |||
| 95 | 95 | return t.restoreFn, nil | |
| 96 | 96 | } | |
| 97 | 97 | ||
| 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) | |
| 101 | 101 | if err != nil { | |
| 102 | 102 | return nil, fmt.Errorf("term: make raw: %w", err) | |
| 103 | 103 | } | |
| @@ -170,14 +170,14 @@ func (t *Terminal) restoreRawLocked() error { | |||
| 170 | 170 | if t.orig == nil { | |
| 171 | 171 | return nil | |
| 172 | 172 | } | |
| 173 | - | err := linuxterm.Restore(t.fd, t.orig) | |
| 173 | + | err := rawterm.Restore(t.fd, t.orig) | |
| 174 | 174 | t.orig = nil | |
| 175 | 175 | return err | |
| 176 | 176 | } | |
| 177 | 177 | ||
| 178 | 178 | // Size returns the current terminal dimensions. | |
| 179 | 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 | 181 | if err != nil { | |
| 182 | 182 | return domain.Size{}, fmt.Errorf("term: get size: %w", err) | |
| 183 | 183 | } | |
| @@ -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 *batchWriter | 49 | bw *batchWriter | ||
| 50 | 50 | ||||
| 51 | mu sync.Mutex | 51 | 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 restore | 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 | 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 | 55 | + | restoreFn func() error // the single idempotent restore closure for the current session |
| 56 | 56 | ||||
| 57 | // Resize watcher state, also guarded by mu so watcher start | 57 | // 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, nil | 95 | return t.restoreFn, nil | ||
| 96 | } | 96 | } | ||
| 97 | 97 | ||||
| 98 | - | var old *linuxterm.State | 98 | + | 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 nil | 171 | return nil | ||
| 172 | } | 172 | } | ||
| 173 | - | err := linuxterm.Restore(t.fd, t.orig) | 173 | + | err := rawterm.Restore(t.fd, t.orig) |
| 174 | t.orig = nil | 174 | t.orig = nil | ||
| 175 | return err | 175 | 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 | |
| 7 | 7 | ||
| 8 | 8 | import ( | |
| 9 | 9 | "fmt" | |
| @@ -40,7 +40,7 @@ func ioctl(fd int, req uintptr, arg unsafe.Pointer) error { | |||
| 40 | 40 | // IsTerminal reports whether fd refers to a terminal device. | |
| 41 | 41 | func IsTerminal(fd int) bool { | |
| 42 | 42 | var t syscall.Termios | |
| 43 | - | return ioctl(fd, syscall.TCGETS, unsafe.Pointer(&t)) == nil | |
| 43 | + | return ioctl(fd, reqGetTermios, unsafe.Pointer(&t)) == nil | |
| 44 | 44 | } | |
| 45 | 45 | ||
| 46 | 46 | // MakeRaw puts the terminal referenced by fd into raw mode and returns its | |
| @@ -48,8 +48,8 @@ func IsTerminal(fd int) bool { | |||
| 48 | 48 | // changes applied match golang.org/x/term's MakeRaw semantics exactly. | |
| 49 | 49 | func MakeRaw(fd int) (*State, error) { | |
| 50 | 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) | |
| 51 | + | if err := ioctl(fd, reqGetTermios, unsafe.Pointer(&oldState.termios)); err != nil { | |
| 52 | + | return nil, fmt.Errorf("rawterm: get termios: %w", err) | |
| 53 | 53 | } | |
| 54 | 54 | ||
| 55 | 55 | raw := oldState.termios | |
| @@ -61,8 +61,8 @@ func MakeRaw(fd int) (*State, error) { | |||
| 61 | 61 | raw.Cc[syscall.VMIN] = 1 | |
| 62 | 62 | raw.Cc[syscall.VTIME] = 0 | |
| 63 | 63 | ||
| 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) | |
| 66 | 66 | } | |
| 67 | 67 | ||
| 68 | 68 | return &oldState, nil | |
| @@ -71,8 +71,8 @@ func MakeRaw(fd int) (*State, error) { | |||
| 71 | 71 | // Restore applies a previously captured State back to the terminal | |
| 72 | 72 | // referenced by fd. | |
| 73 | 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) | |
| 74 | + | if err := ioctl(fd, reqSetTermios, unsafe.Pointer(&s.termios)); err != nil { | |
| 75 | + | return fmt.Errorf("rawterm: set termios: %w", err) | |
| 76 | 76 | } | |
| 77 | 77 | return nil | |
| 78 | 78 | } | |
| @@ -81,7 +81,7 @@ func Restore(fd int, s *State) error { | |||
| 81 | 81 | func GetSize(fd int) (cols, rows int, err error) { | |
| 82 | 82 | var ws winsize | |
| 83 | 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 | 86 | return int(ws.Col), int(ws.Row), nil | |
| 87 | 87 | } | |
| @@ -90,27 +90,7 @@ func GetSize(fd int) (cols, rows int, err error) { | |||
| 90 | 90 | func SetWinsize(fd int, cols, rows uint16) error { | |
| 91 | 91 | ws := winsize{Row: rows, Col: cols} | |
| 92 | 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) | |
| 93 | + | return fmt.Errorf("rawterm: set winsize: %w", err) | |
| 114 | 94 | } | |
| 115 | 95 | return nil | |
| 116 | 96 | } | |
| @@ -120,7 +100,7 @@ func UnlockPt(masterFD int) error { | |||
| 120 | 100 | func ForegroundProcessGroup(fd int) (int, error) { | |
| 121 | 101 | var pgid int32 | |
| 122 | 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 | 105 | return int(pgid), nil | |
| 126 | 106 | } | |
| @@ -1,9 +1,9 @@ | |||||
| 1 | - | // Package linuxterm provides the small set of terminal-control primitives | 1 | + | //go:build linux || darwin |
| 2 | - | // vev needs (raw mode, window size, pty allocation) directly on top of the | 2 | + | |
| 3 | - | // frozen stdlib syscall package, so the module does not depend on | 3 | + | // 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 Linux | 4 | + | // 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 linuxterm | 6 | + | 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.Termios | 42 | var t syscall.Termios | ||
| 43 | - | return ioctl(fd, syscall.TCGETS, unsafe.Pointer(&t)) == nil | 43 | + | 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 its | 46 | // 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 State | 50 | 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.termios | 55 | raw := oldState.termios | ||
| @@ -61,8 +61,8 @@ func MakeRaw(fd int) (*State, error) { | |||||
| 61 | raw.Cc[syscall.VMIN] = 1 | 61 | raw.Cc[syscall.VMIN] = 1 | ||
| 62 | raw.Cc[syscall.VTIME] = 0 | 62 | 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, nil | 68 | return &oldState, nil | ||
| @@ -71,8 +71,8 @@ func MakeRaw(fd int) (*State, error) { | |||||
| 71 | // Restore applies a previously captured State back to the terminal | 71 | // 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 nil | 77 | 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 winsize | 82 | 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), nil | 86 | 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 nil | 95 | 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 int32 | 101 | 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), nil | 105 | 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 | |
| 2 | 4 | ||
| 3 | 5 | import ( | |
| 4 | 6 | "errors" | |
| 5 | - | "fmt" | |
| 6 | 7 | "os" | |
| 7 | 8 | "syscall" | |
| 8 | 9 | "testing" | |
| @@ -11,7 +12,7 @@ import ( | |||
| 11 | 12 | ||
| 12 | 13 | // openPTYPair opens a real Linux PTY master/slave pair directly via | |
| 13 | 14 | // /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 | 16 | func openPTYPair(t *testing.T) (master, slave *os.File) { | |
| 16 | 17 | t.Helper() | |
| 17 | 18 | ||
| @@ -21,22 +22,10 @@ func openPTYPair(t *testing.T) (master, slave *os.File) { | |||
| 21 | 22 | } | |
| 22 | 23 | master = os.NewFile(uintptr(masterFD), "pty-master") | |
| 23 | 24 | ||
| 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) | |
| 37 | 26 | if err != nil { | |
| 38 | 27 | _ = master.Close() | |
| 39 | - | t.Fatalf("open slave %s: %v", slavePath, err) | |
| 28 | + | t.Fatalf("PreparePty: %v", err) | |
| 40 | 29 | } | |
| 41 | 30 | ||
| 42 | 31 | return master, slave | |
| @@ -110,8 +99,8 @@ func TestMakeRawRestore(t *testing.T) { | |||
| 110 | 99 | } | |
| 111 | 100 | ||
| 112 | 101 | var current syscall.Termios | |
| 113 | - | if err := ioctl(fd, syscall.TCGETS, unsafe.Pointer(¤t)); err != nil { | |
| 114 | - | t.Fatalf("TCGETS after MakeRaw: %v", err) | |
| 102 | + | if err := ioctl(fd, reqGetTermios, unsafe.Pointer(¤t)); err != nil { | |
| 103 | + | t.Fatalf("get termios after MakeRaw: %v", err) | |
| 115 | 104 | } | |
| 116 | 105 | if current.Lflag&syscall.ECHO != 0 { | |
| 117 | 106 | t.Errorf("ECHO still set after MakeRaw") | |
| @@ -1,8 +1,9 @@ | |||||
| 1 | - | package linuxterm | 1 | + | //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 via | 13 | // 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 | // /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, slave | 31 | return master, slave | ||
| @@ -110,8 +99,8 @@ func TestMakeRawRestore(t *testing.T) { | |||||
| 110 | } | 99 | } | ||
| 111 | 100 | ||||
| 112 | var current syscall.Termios | 101 | var current syscall.Termios | ||
| 113 | - | if err := ioctl(fd, syscall.TCGETS, unsafe.Pointer(¤t)); err != nil { | 102 | + | if err := ioctl(fd, reqGetTermios, unsafe.Pointer(¤t)); 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 | + | } | |||