#+build linux #+vet explicit-allocators package tcom import rt "base:runtime" import "core:bytes" import hm "core:container/handle_map" import "core:fmt" import "core:log" import "core:strings" import "core:sys/linux" import "core:sys/posix" // --------------------------------------------------------------------------------------------------------------------- // BOOTSTRAP // Not found in standard POSIX, but is essentially everywhere. _SIGWINCH :: posix.Signal(28) _sys_orig_termios: posix.termios _sys_init :: proc() { // Setup signal handlers // TODO: Do we even need SIGCHLD? Kakoune doesn't use it. // TODO: There is a modern escape sequence for resizes as an alternative to SIGWINCH. { sa: posix.sigaction_t posix.sigemptyset(&sa.sa_mask) sa.sa_flags += {.SIGINFO, .RESTART} sa.sa_sigaction = _sigchld_handler ensure(posix.sigaction(.SIGCHLD, &sa, nil) == .OK) } { sa: posix.sigaction_t posix.sigemptyset(&sa.sa_mask) sa.sa_flags += {.RESTART} sa.sa_handler = _sigwinch_handler ensure(posix.sigaction(_SIGWINCH, &sa, nil) == .OK) } // Setup terminal { ensure(posix.tcgetattr(posix.STDIN_FILENO, &_sys_orig_termios) == .OK) fmt.print("\x1b[?1049h") // Enter alternative buffer // Enable "raw mode" // https://viewsourcecode.org/snaptoken/kilo/02.enteringRawMode.html tio := _sys_orig_termios tio.c_iflag -= {.ICRNL, .IXON} tio.c_lflag -= {.ECHO, .ICANON, .ISIG, .IEXTEN} tio.c_oflag -= {.OPOST} posix.tcsetattr(posix.STDIN_FILENO, .TCSAFLUSH, &tio) } } _sys_fini :: proc() { // Reset terminal fmt.print("\x1b[?1049l") // Exit alternative buffer posix.tcsetattr(posix.STDIN_FILENO, .TCSAFLUSH, &_sys_orig_termios) // Restore original termios } // --------------------------------------------------------------------------------------------------------------------- // CHILD _Sys_Child_Info :: struct { fd: linux.Fd, pid: posix.pid_t, died: bool, } // TODO: Improve error handling. _sys_child_start :: proc(cmd: []string, dim: TTY_Pos, allocator: rt.Allocator) -> (info: _Sys_Child_Info, err: rt.Allocator_Error) { ensure(len(cmd) > 0, "Cannot create child with empty command.") info.pid = posix.pid_t(_sys_forkpty(cast(^i32)&info.fd, nil, nil, nil)) if info.pid < 0 { fmt.panicf("forkpty error: {}", posix.errno()) } if info.pid == 0 { // Child argv := make([]cstring, len(cmd) + 1, allocator) for part, i in cmd { err: rt.Allocator_Error argv[i], err = strings.clone_to_cstring(part, allocator) ensure(err == nil, "Failed to clone command line string") } argv[len(argv) - 1] = nil posix.execvp(argv[0], raw_data(argv)) fmt.panicf("Child execvp failed with error: {}", posix.errno()) // End of execution. } return } _sys_child_write :: proc(info: _Sys_Child_Info, str: string) -> (ok: bool) { written, errno := linux.write(info.fd, transmute([]u8) str) if errno != .NONE { log.errorf("write() syscall failed: {}", errno) return } if written < len(str) { log.errorf("failed to write entire output string, expected {}, got {}", len(str), written) return } ok = true return } // --------------------------------------------------------------------------------------------------------------------- // EVENTS _Sys_Event_Iterator_Info :: struct { fds: []linux.Poll_Fd, // Owned. fds_i: int, fd_to_pane: map[linux.Fd]Pane_Handle, // Owned. buf: [8192]byte, reader: TTY_Reader, pane_died_it: Maybe(Pane_Iterator), allocator: rt.Allocator, } // TODO: Remove this if it ends up being unnecessary. _sys_event_iterator_create :: proc(it: ^Sys_Event_Iterator) {} _sys_event_iterator_iterate :: proc(it: ^Sys_Event_Iterator) -> (e: Sys_Event, cond: bool) { it_info := &it._sys_info // Initialize and grab events from the OS. if it_info.fds == nil { panes_len := hm.len(g_panes) it_info.fds = make([]linux.Poll_Fd, panes_len + 1, allocator=it.allocator) // +1 for stdin it_info.fd_to_pane = make(type_of(it_info.fd_to_pane), panes_len, allocator=it.allocator) EVENTS :: linux.Fd_Poll_Events{.IN, .PRI, .ERR} pane_it := hm.iterator_make(&g_panes) i: int for pane, _ in hm.iterate(&pane_it) { it_info.fds[i] = {fd = pane.child.fd, events = EVENTS} i += 1 } it_info.fds[len(it_info.fds)-1] = {fd = posix.STDIN_FILENO, events = EVENTS} // STDIN _, errno := linux.poll(it_info.fds[:], -1) // If EINTR is thrown then we received a signal which we likely care about. if errno != .NONE { if errno == .EINTR { log.debug("Received signal during poll()") } else { log.errorf("Error during poll: {}", errno) } } } r := &it_info.reader // Buf has nothing to read if !tty_reader_has_data(r) { // Iterate grabbed events. for it_info.fds_i < len(it_info.fds) { fd_data := it_info.fds[it_info.fds_i] it_info.fds_i += 1 // Increment now for convenience of continue. // TODO: Check other events as well. if .IN not_in fd_data.revents { continue } it_info.buf = {} n_read, errno := linux.read(fd_data.fd, it_info.buf[:]) // TODO: Check through possible errors to see if we need to handle them. if errno != .NONE { log.errorf("Error during read: {}", errno) continue } if n_read == 0 { continue } // TODO: Do we need to keep these reader states around per-client and expand the buf as needed? tty_reader_init(&it_info.reader, it_info.buf[:n_read]) log.debug("input:", it_info.buf[:n_read]) break } } // is_stdin := it_info.fds_i >= len(it_info.fds) tty_event, tty_err := tty_reader_get_event(r) if tty_err != nil { return tty_err, true } if tty_event != nil { return tty_event, true } // TODO: Put this and death handling before fd handling and remove from fds if needed. // TODO: Deduplicate if we receive the resize escape code. // Handle terminal resize. if _sys_sigwinch_received { _sys_sigwinch_received = false new_size := sys_term_get_dims() return TTY_Event(TTY_Resized{}), true } // Handle child deaths // TODO: Test case where client dies during drawing. if it_info.pane_died_it == nil { it_info.pane_died_it = hm.iterator_make(&g_panes) for pane, pane_handle in hm.iterate(&it_info.pane_died_it.?) { if pane.child.died { return Sys_Event_Pane_Died{pane_handle}, true } } } return } _sys_event_iterator_destroy :: proc(it: ^Sys_Event_Iterator) { delete(it._sys_info.fds, allocator=context.allocator) delete(it._sys_info.fd_to_pane) } // --------------------------------------------------------------------------------------------------------------------- // FORKPTY // Bindings for libc forkpty, since it is missing in core. _sys_winsize :: struct { ws_row, ws_col: u16, ws_xpixel, ws_ypixel: u16, // unused } @(default_calling_convention="c") foreign { // Create child process and establish the slave pseudo terminal as the child's controlling terminal. @(link_name="forkpty") _sys_forkpty :: proc(amaster: ^i32, name: cstring, termp: ^posix.termios, winp: ^_sys_winsize) -> i32 --- } // --------------------------------------------------------------------------------------------------------------------- // TERMINAL _SYS_TERM_TIOCSWINSZ :: 0x5414 // TIOCGWINSZ + 1 _Sys_Term_Winsize :: struct { ws_row, ws_col: u16, ws_xpixel, ws_ypixel: u16, // Unused } _sys_term_get_dims :: proc() -> (dim: TTY_Pos, ok: bool) { ws: _Sys_Term_Winsize // TODO: ms-edit does some wacky shenanigans with a 10ms timeout to work better on misbehaving terminals. res := linux.ioctl(1, linux.TIOCGWINSZ, uintptr(&ws)) if res != 0 { log.errorf("Failed to get terminal size:", linux.Errno(res)) return } dim.x = ws.ws_col dim.y = ws.ws_row ok = true return } _sys_term_set_dims :: proc(dim: TTY_Pos, info: Sys_Child_Info) { ws := _Sys_Term_Winsize { ws_row = dim.y, ws_col = dim.x, } // TODO: Error handling linux.ioctl(info.fd, _SYS_TERM_TIOCSWINSZ, uintptr(&ws)) } // --------------------------------------------------------------------------------------------------------------------- // SIGNALS _sys_sigwinch_received: bool _sigchld_handler :: proc "c" (sig: posix.Signal, info: ^posix.siginfo_t, _: rawptr) { if sig != .SIGCHLD { return } // TODO: Somehow remove the use of g_panes here. // We could maybe store a global set of fds which died, allocated to [len(children)]linux.Fd size. it := hm.iterator_make(&g_panes) for pane, _ in hm.iterate(&it) { if pane.child.pid == info.si_pid { pane.child.died = true break } } } _sigwinch_handler :: proc "c" (sig: posix.Signal) { context = rt.default_context() log.debugf("Received signal", sig) _sys_sigwinch_received = true }