raiguard/tcom
git clone https://git.raiguard.me/raiguard/tcom
7992681
trunk
1#+vet explicit-allocators 2package tcom 3 4import rt "base:runtime" 5import hm "core:container/handle_map" 6import "core:flags" 7import "core:fmt" 8import "core:mem" 9import "core:log" 10import "core:os" 11 12/* 13How do we store terminal contents? 14Terminals are line-buffered, so we definitely want to split by newlines. 15But UTF-8 is a variable-width encoding. 16We have a number of grapheme clusters (runes) visible at a given time but that does not correspond to characters. 17Take a note from kakoune's design: an array of lines can work well! So [][]rune? 18If we use an array of runes then that takes up four bytes per cell when we will only need one most of the time. 19But that's what foot does... so it's fine? 20 21Do we want a child to be visible on multiple panes? 22No, so we can safely tie a VT to a child. 23Except that a child can suspend itself to have another child take over the VT... what should happen in that case? 24 25Yeah, we need to do [][]Cell, and attach other properties to each Cell as well. 26 27There are some escape codes which we must "sanitize" and others we ought to pass through. 28For example, fish shell starts out by switching to the alternative buffer, doing some crap, then switching back. 29Currently this causes the upstrema terminal to switch away from the alt buffer after we enter it. 30Therefore we must "sanitize" these codes from the output and handle them in our vt. 31*/ 32 33EXE_NAME :: "tcom" 34 35VERSION :: #config ( VERSION , "" ) 36#assert ( VERSION != "" ) 37 38LOG_FILE :: EXE_NAME + ".log" 39VERSION_LINE :: EXE_NAME + " version " + VERSION 40 41Cli :: struct { 42cmd : string `usage:"Command to run on startup. Defaults to $SHELL."` , 43debug : bool `usage:"Enable debug logging."` , 44session : string `usage:"Set explicit session name. Defaults to PID."` , 45version : bool `usage:"Print version information."` , 46} 47 48Run_Error :: union { 49rt . Allocator_Error , 50string , 51} 52 53run :: proc () -> ( err : Run_Error ) { 54cli : Cli 55if flags_err := flags . parse ( & cli , os . args [ 1 :], allocator = context . allocator ); flags_err != nil { 56flags . print_errors ( Cli , flags_err , EXE_NAME ) 57_ , is_help_request := flags_err .( flags . Help_Request ) 58os . exit ( is_help_request ? 0 : 1 ) 59} 60 61if cli . version { 62fmt . println ( VERSION_LINE ) 63return 64} 65 66logger : log . Logger 67{ 68// TODO: State directory and custom log path. 69// TODO: Log "buffer" in tcom itself (inspired by kakoune) 70log_file , log_file_err := os . create ( LOG_FILE ) 71if log_file_err == nil { 72// TODO: I really hate the default logging syntax... I want to replace the default logging proc! 73opts := log . Options { 74. Level , 75. Short_File_Path , 76. Line , 77. Time , 78} 79lowest : log . Level = ( cli . debug || ODIN_DEBUG ) ? . Debug : . Info 80logger = log . create_file_logger ( log_file , lowest , opts , allocator = context . allocator ) 81} else { 82log . error ( "Failed to create log file:" , log_file_err ) 83fmt . eprintln ( "Failed to create log file:" , log_file_err ) 84} 85} 86context . logger = logger 87 88log . info ( VERSION_LINE ) 89 90env_term := os . lookup_env ( "TERM" , context . temp_allocator ) or_else "<unset>" 91env_tcom_session := os . lookup_env ( "TCOM_SESSION" , context . temp_allocator ) or_else "<unset>" 92log . infof ( "TERM={} TCOM_SESSION={}" , env_term , env_tcom_session ) 93 94log . infof ( "args: {}" , os . args ) 95log . debugf ( "cli: {}" , cli ) 96 97session : string 98if len ( cli . session ) > 0 { 99session = cli . session 100} else { 101pid := os . get_pid () 102session = fmt . tprint ( pid ) 103} 104os . set_env ( "TCOM_SESSION" , "debug" ) 105log . infof ( "session: {}" , session ) 106// TODO: tmux sets its own terminfo, but zellij appears to pass through the host terminal's terminfo. 107// But zellij will continue to persist that same $TERM throughout the whole session, even if you change 108// viewing terminals! So I am more inclined to *sigh* make my own terminfo for tcom, or fall back to 109// tmux-256color. 110// Making my own terminfo is probably good because then I can make sure children see *all* available features, 111// even ones which the host terminal doesn't support. i.e. if the host terminal doesn't support multicursor, 112// tcom could emulate it. 113// Ghostty ships xterm-ghostty because apparently many archaic programs simply search for 'xterm' in the 114// $TERM string. We could do something similar as xterm-tcom. Or maybe we just use xterm-ghostty directly 115// since it is already propagating? See https://ghostty.org/docs/help/terminfo 116// os.set_env("TERM", "tcom") 117 118startup_cmd : string 119if len ( cli . cmd ) > 0 { 120startup_cmd = cli . cmd 121} else { 122startup_cmd , _ = os . lookup_env_alloc ( "SHELL" , context . temp_allocator ) 123} 124if len ( startup_cmd ) == 0 { 125return "Could not determine startup command. Either pass a -cmd or set $SHELL in your environment." 126} 127 128// TODO: shlex startup_cmd 129 130// TODO: Fork into server for session persistence! 131 132if ! os . is_tty ( os . stdout ) { 133return "Must be run from a terminal" 134} 135 136if env_tcom_session != "<unset>" { 137return "Nesting sessions is not recommended; unset $TCOM_SESSION to force." 138} 139 140sys_init () 141 142panes_init ( context . allocator ) 143 144g_focused_pane = pane_init ({ startup_cmd }, sys_term_get_dims (), context . allocator ) or_return 145 146// Main loop 147for { 148// TODO: Frame arena allocator? 149// TODO: Consider multithreading? 150if hm . len ( g_panes ) == 0 { 151log . info ( "All children are dead." ) 152break 153} 154 155it := sys_event_iterator_create ( context . allocator ) 156for event in sys_event_iterator_iterate ( & it ) { 157log . debug ( "event:" , event ) 158 159switch e in event { 160case Sys_Event_Pane_Died : 161pane_fini ( e . pane ) 162case Sys_Event_TTY : 163pane_handle_event ( g_focused_pane , e ) 164case Sys_Event_TTY_Error : 165log . error ( e ) 166case Sys_Event_Input_Raw : 167// TODO: 168// assert(pane_write(g_focused_pane, e.text)) 169case Sys_Event_IO_Error : 170log . error ( e . err ) 171} 172} 173sys_event_iterator_destroy ( & it ) 174 175pane_draw ( g_focused_pane ) 176} 177 178sys_fini () 179 180log . info ( "Goodbye!" ) 181if logger != {} { 182log . destroy_file_logger ( logger , context . allocator ) 183} 184 185panes_fini () 186 187return 188} 189 190main :: proc () { 191// TODO: Consider reducing the scope of the tracking allocator so that we can exit quicker. 192when ODIN_DEBUG { 193track : mem . Tracking_Allocator 194mem . tracking_allocator_init ( & track , context . allocator , context . allocator ) 195context . allocator = mem . tracking_allocator ( & track ) 196defer { 197if len ( track . allocation_map ) > 0 { 198for _ , entry in track . allocation_map { 199fmt . eprintfln ( "{} leaked {} bytes" , entry . location , entry . size ) 200} 201} 202// There is a leak here, but since the tracking allocator doesn't track itself, it's fine! 203} 204} 205if err := run (); err != nil { 206fmt . eprintln ( err ) 207os . exit ( 1 ) 208} 209}