raiguard/tcom

git clone https://git.raiguard.me/raiguard/tcom

Caleb HeuerNo idea what I was working on...7992681

trunk
8.4 KiB299 linesraw
1#+build linux
2#+vet explicit-allocators
3package tcom
4
5import rt "base:runtime"
6import "core:bytes"
7import hm "core:container/handle_map"
8import "core:fmt"
9import "core:log"
10import "core:strings"
11import "core:sys/linux"
12import "core:sys/posix"
13
14// ---------------------------------------------------------------------------------------------------------------------
15// BOOTSTRAP
16
17// Not found in standard POSIX, but is essentially everywhere.
18_SIGWINCH :: posix.Signal(28)
19
20_sys_orig_termios: posix.termios
21
22_sys_init :: proc() {
23	// Setup signal handlers
24	// TODO: Do we even need SIGCHLD? Kakoune doesn't use it.
25	// TODO: There is a modern escape sequence for resizes as an alternative to SIGWINCH.
26	{
27		sa: posix.sigaction_t
28		posix.sigemptyset(&sa.sa_mask)
29		sa.sa_flags += {.SIGINFO, .RESTART}
30		sa.sa_sigaction = _sigchld_handler
31		ensure(posix.sigaction(.SIGCHLD, &sa, nil) == .OK)
32	}
33	{
34		sa: posix.sigaction_t
35		posix.sigemptyset(&sa.sa_mask)
36		sa.sa_flags += {.RESTART}
37		sa.sa_handler = _sigwinch_handler
38		ensure(posix.sigaction(_SIGWINCH, &sa, nil) == .OK)
39	}
40
41	// Setup terminal
42	{
43		ensure(posix.tcgetattr(posix.STDIN_FILENO, &_sys_orig_termios) == .OK)
44		fmt.print("\x1b[?1049h") // Enter alternative buffer
45
46		// Enable "raw mode"
47		// https://viewsourcecode.org/snaptoken/kilo/02.enteringRawMode.html
48		tio := _sys_orig_termios
49		tio.c_iflag -= {.ICRNL, .IXON}
50		tio.c_lflag -= {.ECHO, .ICANON, .ISIG, .IEXTEN}
51		tio.c_oflag -= {.OPOST}
52		posix.tcsetattr(posix.STDIN_FILENO, .TCSAFLUSH, &tio)
53	}
54}
55
56_sys_fini :: proc() {
57	// Reset terminal
58	fmt.print("\x1b[?1049l") // Exit alternative buffer
59	posix.tcsetattr(posix.STDIN_FILENO, .TCSAFLUSH, &_sys_orig_termios) // Restore original termios
60}
61
62// ---------------------------------------------------------------------------------------------------------------------
63// CHILD
64
65_Sys_Child_Info :: struct {
66	fd: linux.Fd,
67	pid: posix.pid_t,
68	died: bool,
69}
70
71// TODO: Improve error handling.
72_sys_child_start :: proc(cmd: []string, dim: TTY_Pos, allocator: rt.Allocator) -> (info: _Sys_Child_Info, err: rt.Allocator_Error) {
73	ensure(len(cmd) > 0, "Cannot create child with empty command.")
74	info.pid = posix.pid_t(_sys_forkpty(cast(^i32)&info.fd, nil, nil, nil))
75	if info.pid < 0 {
76		fmt.panicf("forkpty error: {}", posix.errno())
77	}
78	if info.pid == 0 { // Child
79		argv := make([]cstring, len(cmd) + 1, allocator)
80		for part, i in cmd {
81			err: rt.Allocator_Error
82			argv[i], err = strings.clone_to_cstring(part, allocator)
83			ensure(err == nil, "Failed to clone command line string")
84		}
85		argv[len(argv) - 1] = nil
86		posix.execvp(argv[0], raw_data(argv))
87		fmt.panicf("Child execvp failed with error: {}", posix.errno())
88		// End of execution.
89	}
90	return
91}
92
93_sys_child_write :: proc(info: _Sys_Child_Info, str: string) -> (ok: bool) {
94	written, errno := linux.write(info.fd, transmute([]u8) str)
95	if errno != .NONE {
96		log.errorf("write() syscall failed: {}", errno)
97		return
98	}
99	if written < len(str) {
100		log.errorf("failed to write entire output string, expected {}, got {}", len(str), written)
101		return
102	}
103	ok = true
104	return
105}
106
107// ---------------------------------------------------------------------------------------------------------------------
108// EVENTS
109
110_Sys_Event_Iterator_Info :: struct {
111	fds: []linux.Poll_Fd, // Owned.
112	fds_i: int,
113
114	fd_to_pane: map[linux.Fd]Pane_Handle, // Owned.
115
116	buf: [8192]byte,
117	reader: TTY_Reader,
118
119	pane_died_it: Maybe(Pane_Iterator),
120
121	allocator: rt.Allocator,
122}
123
124// TODO: Remove this if it ends up being unnecessary.
125_sys_event_iterator_create :: proc(it: ^Sys_Event_Iterator) {}
126
127_sys_event_iterator_iterate :: proc(it: ^Sys_Event_Iterator) -> (e: Sys_Event, cond: bool) {
128	it_info := &it._sys_info
129
130	// Initialize and grab events from the OS.
131	if it_info.fds == nil {
132		panes_len := hm.len(g_panes)
133		it_info.fds = make([]linux.Poll_Fd, panes_len + 1, allocator=it.allocator) // +1 for stdin
134		it_info.fd_to_pane = make(type_of(it_info.fd_to_pane), panes_len, allocator=it.allocator)
135
136		EVENTS :: linux.Fd_Poll_Events{.IN, .PRI, .ERR}
137
138		pane_it := hm.iterator_make(&g_panes)
139		i: int
140		for pane, _ in hm.iterate(&pane_it) {
141			it_info.fds[i] = {fd = pane.child.fd, events = EVENTS}
142			i += 1
143		}
144		it_info.fds[len(it_info.fds)-1] = {fd = posix.STDIN_FILENO, events = EVENTS} // STDIN
145
146		_, errno := linux.poll(it_info.fds[:], -1)
147		// If EINTR is thrown then we received a signal which we likely care about.
148		if errno != .NONE {
149			if errno == .EINTR {
150				log.debug("Received signal during poll()")
151			} else {
152				log.errorf("Error during poll: {}", errno)
153			}
154		}
155	}
156
157	r := &it_info.reader
158
159	// Buf has nothing to read
160	if !tty_reader_has_data(r) {
161		// Iterate grabbed events.
162		for it_info.fds_i < len(it_info.fds) {
163			fd_data := it_info.fds[it_info.fds_i]
164			it_info.fds_i += 1 // Increment now for convenience of continue.
165
166			// TODO: Check other events as well.
167			if .IN not_in fd_data.revents {
168				continue
169			}
170
171			it_info.buf = {}
172			n_read, errno := linux.read(fd_data.fd, it_info.buf[:])
173			// TODO: Check through possible errors to see if we need to handle them.
174			if errno != .NONE {
175				log.errorf("Error during read: {}", errno)
176				continue
177			}
178			if n_read == 0 {
179				continue
180			}
181			// TODO: Do we need to keep these reader states around per-client and expand the buf as needed?
182			tty_reader_init(&it_info.reader, it_info.buf[:n_read])
183			log.debug("input:", it_info.buf[:n_read])
184			break
185		}
186	}
187
188	// is_stdin := it_info.fds_i >= len(it_info.fds)
189	tty_event, tty_err := tty_reader_get_event(r)
190	if tty_err != nil {
191		return tty_err, true
192	}
193	if tty_event != nil {
194		return tty_event, true
195	}
196
197	// TODO: Put this and death handling before fd handling and remove from fds if needed.
198	// TODO: Deduplicate if we receive the resize escape code.
199	// Handle terminal resize.
200	if _sys_sigwinch_received {
201		_sys_sigwinch_received = false
202		new_size := sys_term_get_dims()
203		return TTY_Event(TTY_Resized{}), true
204	}
205
206	// Handle child deaths
207	// TODO: Test case where client dies during drawing.
208	if it_info.pane_died_it == nil {
209		it_info.pane_died_it = hm.iterator_make(&g_panes)
210		for pane, pane_handle in hm.iterate(&it_info.pane_died_it.?) {
211			if pane.child.died {
212				return Sys_Event_Pane_Died{pane_handle}, true
213			}
214		}
215	}
216
217	return
218}
219
220_sys_event_iterator_destroy :: proc(it: ^Sys_Event_Iterator) {
221	delete(it._sys_info.fds, allocator=context.allocator)
222	delete(it._sys_info.fd_to_pane)
223}
224
225// ---------------------------------------------------------------------------------------------------------------------
226// FORKPTY
227
228// Bindings for libc forkpty, since it is missing in core.
229
230_sys_winsize :: struct {
231	ws_row, ws_col: u16,
232	ws_xpixel, ws_ypixel: u16, // unused
233}
234
235@(default_calling_convention="c")
236foreign {
237	// Create child process and establish the slave pseudo terminal as the child's controlling terminal.
238	@(link_name="forkpty")
239	_sys_forkpty :: proc(amaster: ^i32, name: cstring, termp: ^posix.termios, winp: ^_sys_winsize) -> i32 ---
240}
241
242// ---------------------------------------------------------------------------------------------------------------------
243// TERMINAL
244
245_SYS_TERM_TIOCSWINSZ :: 0x5414 // TIOCGWINSZ + 1
246
247_Sys_Term_Winsize :: struct {
248	ws_row, ws_col: u16,
249	ws_xpixel, ws_ypixel: u16, // Unused
250}
251
252_sys_term_get_dims :: proc() -> (dim: TTY_Pos, ok: bool) {
253	ws: _Sys_Term_Winsize
254	// TODO: ms-edit does some wacky shenanigans with a 10ms timeout to work better on misbehaving terminals.
255	res := linux.ioctl(1, linux.TIOCGWINSZ, uintptr(&ws))
256	if res != 0 {
257		log.errorf("Failed to get terminal size:", linux.Errno(res))
258		return
259	}
260	dim.x = ws.ws_col
261	dim.y = ws.ws_row
262	ok = true
263	return
264}
265
266_sys_term_set_dims :: proc(dim: TTY_Pos, info: Sys_Child_Info) {
267	ws := _Sys_Term_Winsize {
268		ws_row = dim.y,
269		ws_col = dim.x,
270	}
271	// TODO: Error handling
272	linux.ioctl(info.fd, _SYS_TERM_TIOCSWINSZ, uintptr(&ws))
273}
274
275// ---------------------------------------------------------------------------------------------------------------------
276// SIGNALS
277
278_sys_sigwinch_received: bool
279
280_sigchld_handler :: proc "c" (sig: posix.Signal, info: ^posix.siginfo_t, _: rawptr) {
281	if sig != .SIGCHLD {
282		return
283	}
284	// TODO: Somehow remove the use of g_panes here.
285	// We could maybe store a global set of fds which died, allocated to [len(children)]linux.Fd size.
286	it := hm.iterator_make(&g_panes)
287	for pane, _ in hm.iterate(&it) {
288		if pane.child.pid == info.si_pid {
289			pane.child.died = true
290			break
291		}
292	}
293}
294
295_sigwinch_handler :: proc "c" (sig: posix.Signal) {
296	context = rt.default_context()
297	log.debugf("Received signal", sig)
298	_sys_sigwinch_received = true
299}