detached 621dedb

feat(domain,ports): core types, port interfaces, IPC frame surface

Adds internal/domain (Size, Rect, Session, Window) and internal/ports
(PTY, PTYFactory, Terminal, Transport, Listener, Clock, Timer plus the
Frame/MsgType IPC surface) that the pty, ipc, and term adapters will
implement in parallel.
commit
621dedb6bc639fe3564bed3ee7fff4667746fb9b
author
brice · · 2026-07-02 10:56 UTC
parent
538f8cc
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
6 files · +189 -0
A internal/domain/types.go +39 -0
@@ -0,0 +1,39 @@
1+package domain
2+
3+// Size is a terminal dimension in columns and rows.
4+type Size struct {
5+ Cols, Rows int
6+}
7+
8+// Valid reports whether sz has strictly positive columns and rows.
9+func (sz Size) Valid() bool {
10+ return sz.Cols > 0 && sz.Rows > 0
11+}
12+
13+// Rect is an axis-aligned rectangle in cell coordinates.
14+type Rect struct {
15+ X, Y, Width, Height int
16+}
17+
18+// SessionID uniquely identifies a session.
19+type SessionID string
20+
21+// WindowID uniquely identifies a window within a session.
22+type WindowID string
23+
24+// Window is a single window (tab) within a session.
25+type Window struct {
26+ ID WindowID
27+ Index int // 0-based; drives Alt+1..9 and status-bar order
28+ Name string
29+ Active bool
30+}
31+
32+// Session is a persistent multiplexer session containing one or more windows.
33+type Session struct {
34+ ID SessionID
35+ Name string // display name; numbered "0","1"... when ephemeral
36+ Ephemeral bool
37+ Windows []Window
38+ ActiveWin WindowID
39+}
@@ -0,0 +1,39 @@
1+package domain
2+
3+// Size is a terminal dimension in columns and rows.
4+type Size struct {
5+ Cols, Rows int
6+}
7+
8+// Valid reports whether sz has strictly positive columns and rows.
9+func (sz Size) Valid() bool {
10+ return sz.Cols > 0 && sz.Rows > 0
11+}
12+
13+// Rect is an axis-aligned rectangle in cell coordinates.
14+type Rect struct {
15+ X, Y, Width, Height int
16+}
17+
18+// SessionID uniquely identifies a session.
19+type SessionID string
20+
21+// WindowID uniquely identifies a window within a session.
22+type WindowID string
23+
24+// Window is a single window (tab) within a session.
25+type Window struct {
26+ ID WindowID
27+ Index int // 0-based; drives Alt+1..9 and status-bar order
28+ Name string
29+ Active bool
30+}
31+
32+// Session is a persistent multiplexer session containing one or more windows.
33+type Session struct {
34+ ID SessionID
35+ Name string // display name; numbered "0","1"... when ephemeral
36+ Ephemeral bool
37+ Windows []Window
38+ ActiveWin WindowID
39+}
A internal/domain/types_test.go +26 -0
@@ -0,0 +1,26 @@
1+package domain
2+
3+import "testing"
4+
5+func TestSizeValid(t *testing.T) {
6+ tests := []struct {
7+ name string
8+ sz Size
9+ want bool
10+ }{
11+ {"positive", Size{Cols: 80, Rows: 24}, true},
12+ {"zero cols", Size{Cols: 0, Rows: 24}, false},
13+ {"zero rows", Size{Cols: 80, Rows: 0}, false},
14+ {"zero both", Size{Cols: 0, Rows: 0}, false},
15+ {"negative cols", Size{Cols: -1, Rows: 24}, false},
16+ {"negative rows", Size{Cols: 80, Rows: -1}, false},
17+ }
18+
19+ for _, tt := range tests {
20+ t.Run(tt.name, func(t *testing.T) {
21+ if got := tt.sz.Valid(); got != tt.want {
22+ t.Errorf("Size%+v.Valid() = %v, want %v", tt.sz, got, tt.want)
23+ }
24+ })
25+ }
26+}
@@ -0,0 +1,26 @@
1+package domain
2+
3+import "testing"
4+
5+func TestSizeValid(t *testing.T) {
6+ tests := []struct {
7+ name string
8+ sz Size
9+ want bool
10+ }{
11+ {"positive", Size{Cols: 80, Rows: 24}, true},
12+ {"zero cols", Size{Cols: 0, Rows: 24}, false},
13+ {"zero rows", Size{Cols: 80, Rows: 0}, false},
14+ {"zero both", Size{Cols: 0, Rows: 0}, false},
15+ {"negative cols", Size{Cols: -1, Rows: 24}, false},
16+ {"negative rows", Size{Cols: 80, Rows: -1}, false},
17+ }
18+
19+ for _, tt := range tests {
20+ t.Run(tt.name, func(t *testing.T) {
21+ if got := tt.sz.Valid(); got != tt.want {
22+ t.Errorf("Size%+v.Valid() = %v, want %v", tt.sz, got, tt.want)
23+ }
24+ })
25+ }
26+}
M internal/ports/doc.go +2 -0
@@ -1,2 +1,4 @@
11 // Package ports defines the interfaces that decouple vev's core from its adapters.
22 package ports
3+
4+//go:generate mockery
@@ -1,2 +1,4 @@
1 // Package ports defines the interfaces that decouple vev's core from its adapters.1 // Package ports defines the interfaces that decouple vev's core from its adapters.
2 package ports2 package ports
3+
4+//go:generate mockery
A internal/ports/frame.go +32 -0
@@ -0,0 +1,32 @@
1+package ports
2+
3+// ProtocolVersion is the current vev IPC wire protocol version.
4+const ProtocolVersion uint16 = 1
5+
6+// MsgType identifies the kind of payload carried by a Frame.
7+type MsgType uint8
8+
9+// Frame message types. Client-originated messages are numbered from 1,
10+// server-originated messages from 16, leaving room for growth in each band.
11+const (
12+ MsgHello MsgType = 1
13+ MsgInput MsgType = 2
14+ MsgResize MsgType = 3
15+ MsgDetach MsgType = 4
16+ MsgPing MsgType = 5
17+
18+ MsgWelcome MsgType = 16
19+ MsgError MsgType = 17
20+ MsgOutput MsgType = 18
21+ MsgDetached MsgType = 19
22+ MsgPong MsgType = 20
23+)
24+
25+// Frame is the unit of exchange over a Transport: a typed, length-delimited
26+// message. It lives in ports (rather than an ipc adapter) so usecases and
27+// adapters can share it without either depending on a concrete transport
28+// implementation.
29+type Frame struct {
30+ Type MsgType
31+ Payload []byte
32+}
@@ -0,0 +1,32 @@
1+package ports
2+
3+// ProtocolVersion is the current vev IPC wire protocol version.
4+const ProtocolVersion uint16 = 1
5+
6+// MsgType identifies the kind of payload carried by a Frame.
7+type MsgType uint8
8+
9+// Frame message types. Client-originated messages are numbered from 1,
10+// server-originated messages from 16, leaving room for growth in each band.
11+const (
12+ MsgHello MsgType = 1
13+ MsgInput MsgType = 2
14+ MsgResize MsgType = 3
15+ MsgDetach MsgType = 4
16+ MsgPing MsgType = 5
17+
18+ MsgWelcome MsgType = 16
19+ MsgError MsgType = 17
20+ MsgOutput MsgType = 18
21+ MsgDetached MsgType = 19
22+ MsgPong MsgType = 20
23+)
24+
25+// Frame is the unit of exchange over a Transport: a typed, length-delimited
26+// message. It lives in ports (rather than an ipc adapter) so usecases and
27+// adapters can share it without either depending on a concrete transport
28+// implementation.
29+type Frame struct {
30+ Type MsgType
31+ Payload []byte
32+}
A internal/ports/frame_test.go +33 -0
@@ -0,0 +1,33 @@
1+package ports
2+
3+import "testing"
4+
5+func TestMsgTypeUnique(t *testing.T) {
6+ tests := []struct {
7+ name string
8+ typ MsgType
9+ }{
10+ {"MsgHello", MsgHello},
11+ {"MsgInput", MsgInput},
12+ {"MsgResize", MsgResize},
13+ {"MsgDetach", MsgDetach},
14+ {"MsgPing", MsgPing},
15+ {"MsgWelcome", MsgWelcome},
16+ {"MsgError", MsgError},
17+ {"MsgOutput", MsgOutput},
18+ {"MsgDetached", MsgDetached},
19+ {"MsgPong", MsgPong},
20+ }
21+
22+ seen := make(map[MsgType]string, len(tests))
23+ for _, tt := range tests {
24+ if prev, ok := seen[tt.typ]; ok {
25+ t.Errorf("MsgType value %d used by both %s and %s", tt.typ, prev, tt.name)
26+ }
27+ seen[tt.typ] = tt.name
28+ }
29+
30+ if len(seen) != len(tests) {
31+ t.Fatalf("expected %d distinct MsgType values, got %d", len(tests), len(seen))
32+ }
33+}
@@ -0,0 +1,33 @@
1+package ports
2+
3+import "testing"
4+
5+func TestMsgTypeUnique(t *testing.T) {
6+ tests := []struct {
7+ name string
8+ typ MsgType
9+ }{
10+ {"MsgHello", MsgHello},
11+ {"MsgInput", MsgInput},
12+ {"MsgResize", MsgResize},
13+ {"MsgDetach", MsgDetach},
14+ {"MsgPing", MsgPing},
15+ {"MsgWelcome", MsgWelcome},
16+ {"MsgError", MsgError},
17+ {"MsgOutput", MsgOutput},
18+ {"MsgDetached", MsgDetached},
19+ {"MsgPong", MsgPong},
20+ }
21+
22+ seen := make(map[MsgType]string, len(tests))
23+ for _, tt := range tests {
24+ if prev, ok := seen[tt.typ]; ok {
25+ t.Errorf("MsgType value %d used by both %s and %s", tt.typ, prev, tt.name)
26+ }
27+ seen[tt.typ] = tt.name
28+ }
29+
30+ if len(seen) != len(tests) {
31+ t.Fatalf("expected %d distinct MsgType values, got %d", len(tests), len(seen))
32+ }
33+}
A internal/ports/ports.go +57 -0
@@ -0,0 +1,57 @@
1+package ports
2+
3+import (
4+ "io"
5+ "time"
6+
7+ "github.com/bnema/vev/internal/domain"
8+)
9+
10+// PTY is a running pseudo-terminal-backed child process.
11+type PTY interface {
12+ io.ReadWriteCloser // Read = child output, Write = child input
13+ Resize(sz domain.Size) error
14+ Pid() int
15+}
16+
17+// PTYFactory creates PTYs by spawning a command attached to a new pseudo-terminal.
18+type PTYFactory interface {
19+ Open(cmd string, args []string, env []string, sz domain.Size) (PTY, error)
20+}
21+
22+// Terminal is the CLIENT-side controlling terminal.
23+type Terminal interface {
24+ EnterRaw() (restore func() error, err error)
25+ Size() (domain.Size, error)
26+ ResizeEvents() <-chan domain.Size
27+ In() io.Reader
28+ Out() io.Writer
29+ Flush() error
30+}
31+
32+// Transport is a framed message channel over a single connection.
33+type Transport interface {
34+ Send(Frame) error
35+ Recv() (Frame, error) // blocking; io.EOF on close
36+ Close() error
37+}
38+
39+// Listener accepts incoming Transport connections.
40+type Listener interface {
41+ Accept() (Transport, error)
42+ Close() error
43+ Addr() string
44+}
45+
46+// Clock abstracts time so usecases can be tested without real delays.
47+type Clock interface {
48+ Now() time.Time
49+ NewTimer(d time.Duration) Timer
50+}
51+
52+// Timer abstracts a cancellable, resettable time.Timer.
53+type Timer interface {
54+ C() <-chan time.Time
55+ Reset(d time.Duration) bool
56+ Stop() bool
57+}
@@ -0,0 +1,57 @@
1+package ports
2+
3+import (
4+ "io"
5+ "time"
6+
7+ "github.com/bnema/vev/internal/domain"
8+)
9+
10+// PTY is a running pseudo-terminal-backed child process.
11+type PTY interface {
12+ io.ReadWriteCloser // Read = child output, Write = child input
13+ Resize(sz domain.Size) error
14+ Pid() int
15+}
16+
17+// PTYFactory creates PTYs by spawning a command attached to a new pseudo-terminal.
18+type PTYFactory interface {
19+ Open(cmd string, args []string, env []string, sz domain.Size) (PTY, error)
20+}
21+
22+// Terminal is the CLIENT-side controlling terminal.
23+type Terminal interface {
24+ EnterRaw() (restore func() error, err error)
25+ Size() (domain.Size, error)
26+ ResizeEvents() <-chan domain.Size
27+ In() io.Reader
28+ Out() io.Writer
29+ Flush() error
30+}
31+
32+// Transport is a framed message channel over a single connection.
33+type Transport interface {
34+ Send(Frame) error
35+ Recv() (Frame, error) // blocking; io.EOF on close
36+ Close() error
37+}
38+
39+// Listener accepts incoming Transport connections.
40+type Listener interface {
41+ Accept() (Transport, error)
42+ Close() error
43+ Addr() string
44+}
45+
46+// Clock abstracts time so usecases can be tested without real delays.
47+type Clock interface {
48+ Now() time.Time
49+ NewTimer(d time.Duration) Timer
50+}
51+
52+// Timer abstracts a cancellable, resettable time.Timer.
53+type Timer interface {
54+ C() <-chan time.Time
55+ Reset(d time.Duration) bool
56+ Stop() bool
57+}

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