raiguard/tcom
git clone https://git.raiguard.me/raiguard/tcom
7992681
trunk
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{ 27sa : posix . sigaction_t 28posix . sigemptyset ( & sa . sa_mask ) 29sa . sa_flags += {. SIGINFO , . RESTART } 30sa . sa_sigaction = _sigchld_handler 31ensure ( posix . sigaction (. SIGCHLD , & sa , nil ) == . OK ) 32} 33{ 34sa : posix . sigaction_t 35posix . sigemptyset ( & sa . sa_mask ) 36sa . sa_flags += {. RESTART } 37sa . sa_handler = _sigwinch_handler 38ensure ( posix . sigaction ( _SIGWINCH , & sa , nil ) == . OK ) 39} 40 41// Setup terminal 42{ 43ensure ( posix . tcgetattr ( posix . STDIN_FILENO , & _sys_orig_termios ) == . OK ) 44fmt . ( "\x1b[?1049h" ) // Enter alternative buffer 45 46// Enable "raw mode" 47// https://viewsourcecode.org/snaptoken/kilo/02.enteringRawMode.html 48tio := _sys_orig_termios 49tio . c_iflag -= {. ICRNL , . IXON } 50tio . c_lflag -= {. ECHO , . ICANON , . ISIG , . IEXTEN } 51tio . c_oflag -= {. OPOST } 52posix . tcsetattr ( posix . STDIN_FILENO , . TCSAFLUSH , & tio ) 53} 54} 55 56_sys_fini :: proc () { 57// Reset terminal 58fmt . ( "\x1b[?1049l" ) // Exit alternative buffer 59posix . tcsetattr ( posix . STDIN_FILENO , . TCSAFLUSH , & _sys_orig_termios ) // Restore original termios 60} 61 62// --------------------------------------------------------------------------------------------------------------------- 63// CHILD 64 65_Sys_Child_Info :: struct { 66fd : linux . Fd , 67pid : posix . pid_t , 68died : 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 ) { 73ensure ( len ( cmd ) > 0 , "Cannot create child with empty command." ) 74info . pid = posix . pid_t ( _sys_forkpty ( cast ( ^ i32 ) & info . fd , nil , nil , nil )) 75if info . pid < 0 { 76fmt . panicf ( "forkpty error: {}" , posix . errno ()) 77} 78if info . pid == 0 { // Child 79argv := make ([] cstring , len ( cmd ) + 1 , allocator ) 80for part , i in cmd { 81err : rt . Allocator_Error 82argv [ i ], err = strings . clone_to_cstring ( part , allocator ) 83ensure ( err == nil , "Failed to clone command line string" ) 84} 85argv [ len ( argv ) - 1 ] = nil 86posix . execvp ( argv [ 0 ], raw_data ( argv )) 87fmt . panicf ( "Child execvp failed with error: {}" , posix . errno ()) 88// End of execution. 89} 90return 91} 92 93_sys_child_write :: proc ( info : _Sys_Child_Info , str : string ) -> ( ok : bool ) { 94written , errno := linux . write ( info . fd , transmute ([] u8 ) str ) 95if errno != . NONE { 96log . errorf ( "write() syscall failed: {}" , errno ) 97return 98} 99if written < len ( str ) { 100log . errorf ( "failed to write entire output string, expected {}, got {}" , len ( str ), written ) 101return 102} 103ok = true 104return 105} 106 107// --------------------------------------------------------------------------------------------------------------------- 108// EVENTS 109 110_Sys_Event_Iterator_Info :: struct { 111fds : [] linux . Poll_Fd , // Owned. 112fds_i : int , 113 114fd_to_pane : map [ linux . Fd ] Pane_Handle , // Owned. 115 116buf : [ 8192 ] byte , 117reader : TTY_Reader , 118 119pane_died_it : Maybe ( Pane_Iterator ), 120 121allocator : 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 ) { 128it_info := & it . _sys_info 129 130// Initialize and grab events from the OS. 131if it_info . fds == nil { 132panes_len := hm . len ( g_panes ) 133it_info . fds = make ([] linux . Poll_Fd , panes_len + 1 , allocator = it . allocator ) // +1 for stdin 134it_info . fd_to_pane = make ( type_of ( it_info . fd_to_pane ), panes_len , allocator = it . allocator ) 135 136EVENTS :: linux . Fd_Poll_Events {. IN , . PRI , . ERR } 137 138pane_it := hm . iterator_make ( & g_panes ) 139i : int 140for pane , _ in hm . iterate ( & pane_it ) { 141it_info . fds [ i ] = { fd = pane . child . fd , events = EVENTS } 142i += 1 143} 144it_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. 148if errno != . NONE { 149if errno == . EINTR { 150log . debug ( "Received signal during poll()" ) 151} else { 152log . errorf ( "Error during poll: {}" , errno ) 153} 154} 155} 156 157r := & it_info . reader 158 159// Buf has nothing to read 160if ! tty_reader_has_data ( r ) { 161// Iterate grabbed events. 162for it_info . fds_i < len ( it_info . fds ) { 163fd_data := it_info . fds [ it_info . fds_i ] 164it_info . fds_i += 1 // Increment now for convenience of continue. 165 166// TODO: Check other events as well. 167if . IN not_in fd_data . revents { 168continue 169} 170 171it_info . buf = {} 172n_read , errno := linux . read ( fd_data . fd , it_info . buf [:]) 173// TODO: Check through possible errors to see if we need to handle them. 174if errno != . NONE { 175log . errorf ( "Error during read: {}" , errno ) 176continue 177} 178if n_read == 0 { 179continue 180} 181// TODO: Do we need to keep these reader states around per-client and expand the buf as needed? 182tty_reader_init ( & it_info . reader , it_info . buf [: n_read ]) 183log . debug ( "input:" , it_info . buf [: n_read ]) 184break 185} 186} 187 188// is_stdin := it_info.fds_i >= len(it_info.fds) 189tty_event , tty_err := tty_reader_get_event ( r ) 190if tty_err != nil { 191return tty_err , true 192} 193if tty_event != nil { 194return 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. 200if _sys_sigwinch_received { 201_sys_sigwinch_received = false 202new_size := sys_term_get_dims () 203return TTY_Event ( TTY_Resized {}), true 204} 205 206// Handle child deaths 207// TODO: Test case where client dies during drawing. 208if it_info . pane_died_it == nil { 209it_info . pane_died_it = hm . iterator_make ( & g_panes ) 210for pane , pane_handle in hm . iterate ( & it_info . pane_died_it . ? ) { 211if pane . child . died { 212return Sys_Event_Pane_Died { pane_handle }, true 213} 214} 215} 216 217return 218} 219 220_sys_event_iterator_destroy :: proc ( it : ^ Sys_Event_Iterator ) { 221delete ( it . _sys_info . fds , allocator = context . allocator ) 222delete ( 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 { 231ws_row , ws_col : u16 , 232ws_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 { 248ws_row , ws_col : u16 , 249ws_xpixel , ws_ypixel : u16 , // Unused 250} 251 252_sys_term_get_dims :: proc () -> ( dim : TTY_Pos , ok : bool ) { 253ws : _Sys_Term_Winsize 254// TODO: ms-edit does some wacky shenanigans with a 10ms timeout to work better on misbehaving terminals. 255res := linux . ioctl ( 1 , linux . TIOCGWINSZ , uintptr ( & ws )) 256if res != 0 { 257log . errorf ( "Failed to get terminal size:" , linux . Errno ( res )) 258return 259} 260dim . x = ws . ws_col 261dim . y = ws . ws_row 262ok = true 263return 264} 265 266_sys_term_set_dims :: proc ( dim : TTY_Pos , info : Sys_Child_Info ) { 267ws := _Sys_Term_Winsize { 268ws_row = dim . y , 269ws_col = dim . x , 270} 271// TODO: Error handling 272linux . 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 ) { 281if sig != . SIGCHLD { 282return 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. 286it := hm . iterator_make ( & g_panes ) 287for pane , _ in hm . iterate ( & it ) { 288if pane . child . pid == info . si_pid { 289pane . child . died = true 290break 291} 292} 293} 294 295_sigwinch_handler :: proc "c" ( sig : posix . Signal ) { 296context = rt . default_context () 297log . debugf ( "Received signal" , sig ) 298_sys_sigwinch_received = true 299}