--[[ ____ / _____ ___ ____ / |_ / /| | / / | |_ |_ / /| /| | \ /-| | /| / | | | \ / |/ | | \ / | |/ |/ | |__ | \ / / | (v.2026-01-23) lua traslation layer that uses 'stty' to get the terminal into raw mode. Reasorces: https://man7.org/linux/man-pages/man1/stty.1.html https://viewsourcecode.org/snaptoken/kilo/02.rawInputAndOutput.html ]] local rawterm = {} -- colors ====================================================================== local color = {} local __color = {} function __color:__tostring() return self.value end function __color:__concat(other) return tostring(self) .. tostring(other) end function __color:__call(s) return self .. s .. color.reset end __color.__metatable = {} local function makecolor(value) return setmetatable( { value = string.char(27) .. '[' .. tostring(value) .. 'm' }, __color ) end local colors = { -- attributes -- foreground -- background reset = 0, black = 30, onblack = 40, clear = 0, red = 31, onred = 41, bright = 1, green = 32, ongreen = 42, dim = 2, yellow = 33, onyellow = 43, underscore = 4, blue = 34, onblue = 44, blink = 5, magenta = 35, onmagenta = 45, reverse = 7, cyan = 36, oncyan = 46, hidden = 8, white = 37, onwhite = 47, } for c, v in pairs(colors) do color[c] = makecolor(v) end rawterm.color = color -- the terminal ================================================================ local savedTerminalState = '' local function enterRawmode() os.execute('clear') io.write("\x1b[?25l") -- hide the cursor -- read the terminal settings with popen savedTerminalState = io.popen('stty -g'):read() -- use our sttings os.execute('stty -echo') -- DON'T: print keyboard input os.execute('stty -ignbrk') -- DON'T: ignore break characters os.execute('stty -brkint') -- DON'T: breaks cause an interrupt signal os.execute('stty -ignpar') -- DON'T: ignore characters with parity errors os.execute('stty -parmrk') -- DON'T: mark parity errors (with a 255-0-character sequence) os.execute('stty -inpck') -- DON'T: enable input parity checking os.execute('stty -istrip') -- DON'T: clear 8th bit os.execute('stty -inlcr') -- DON'T: translate newline to carriage return os.execute('stty -igncr') -- DON'T: ignore carriage return os.execute('stty -icrnl') -- DON'T: translate carriage return to newline os.execute('stty -ixon') -- DON'T: enable XON/XOFF flow control os.execute('stty -ixoff') -- DON'T: enable sending of start/stop characters os.execute('stty -icanon') -- DON'T: enable: erase, kill, werase, rprnt os.execute('stty -opost') -- DON'T: post prosess output os.execute('stty -isig') -- DON'T: enable interrupt, quit, and suspend special characters os.execute('stty -iuclc') -- DON'T: translate uppercase to lowercase. os.execute('stty -ixany') -- DON'T: let any character restart output, not only start character. os.execute('stty -imaxbel') -- DON'T: beep and do not flush a full input buffer on a character. os.execute('stty -xcase') -- DON'T: with icanon, escape with '\' for uppercase characters. os.execute('stty min 0') -- with -icanon, set N characters minimum for a completed read os.execute('stty time 0') -- with -icanon, set read timeout of N tenths of a second -- In theory we could just use os.execute('stty raw'). -- But this way we get to see what 'stty raw' would be doing. -- Also need turn 'echo' off. -- And for some reason 'min' needed to be set to 0. -- Then we use os.execute('sleep '..sleepTime) to keep the program from.. -- ..runing at max speed. end local function exitRawmode(reason) os.execute('clear') os.execute('stty '..savedTerminalState) io.write("\x1b[?25h") -- show the cursor io.write(color.reset..'') io.write('good bye :)\r\n') io.write( '-> ' ..color.black..color.ongreen..reason..color.clear ..' <-\r\n' ) end function rawterm.getSize() local values = {} local str = io.popen('stty size'):read() local start = 1 while true do local find = str:find(' ',start) table.insert(values, str:sub(start, (find or #str+1)-1)) if not find then break end start = find + 1 end return unpack(values) end --[[ local og_error = error function error(...) exitRawmode('ERROR:') og_error(...) end local og_assert = assert function assert(shouldBeFine, notFine) if not shouldBeFine then exitRawmode('ASSERT:') og_assert(shouldBeFine, notFine) end end ]] local esc = string.char(27) local translate = { [' '] = 'spacebar', [string.char(0)] = 'ctrl_spacebar', [string.char(1)] = 'ctrl_a', [string.char(2)] = 'ctrl_b', [string.char(3)] = 'ctrl_c', [string.char(4)] = 'ctrl_d', [string.char(5)] = 'ctrl_e', [string.char(6)] = 'ctrl_f', [string.char(7)] = 'ctrl_g', [string.char(8)] = 'ctrl_backspace', -- ctrl_h [string.char(9)] = 'tab', -- ctrl_i [string.char(10)] = 'ctrl_j', [string.char(11)] = 'ctrl_k', [string.char(12)] = 'ctrl_l', [string.char(13)] = 'return', [string.char(14)] = 'ctrl_n', [string.char(15)] = 'ctrl_o', [string.char(16)] = 'ctrl_p', [string.char(17)] = 'ctrl_q', [string.char(18)] = 'ctrl_r', [string.char(19)] = 'ctrl_s', [string.char(20)] = 'ctrl_t', [string.char(21)] = 'ctrl_u', [string.char(22)] = 'ctrl_v', [string.char(23)] = 'ctrl_w', [string.char(24)] = 'ctrl_x', [string.char(25)] = 'ctrl_y', [string.char(26)] = 'ctrl_z', [string.char(27)] = 'escape', [string.char(29)] = 'ctrl_]', [string.char(28)] = 'ctrl_\\', [string.char(31)] = 'ctrl_?', [string.char(32)] = 'spacebar', [string.char(127)] = 'backspace', [esc..'[A'] = 'up', [esc..'[B'] = 'down', [esc..'[D'] = 'left', [esc..'[C'] = 'right', [esc..'[1;2A'] = 'shift_up', [esc..'[1;2B'] = 'shift_down', [esc..'[1;2D'] = 'shift_left', [esc..'[1;2C'] = 'shift_right', [esc..'[1;3A'] = 'alt_up', [esc..'[1;3B'] = 'alt_down', [esc..'[1;3D'] = 'alt_left', [esc..'[1;3C'] = 'alt_right', [esc..'[1;4A'] = 'shift_alt_up', [esc..'[1;4B'] = 'shift_alt_down', [esc..'[1;4D'] = 'shift_alt_left', [esc..'[1;4C'] = 'shift_alt_right', [esc..'[1;5A'] = 'ctrl_up', [esc..'[1;5B'] = 'ctrl_down', [esc..'[1;5D'] = 'ctrl_left', [esc..'[1;5C'] = 'ctrl_right', [esc..'[1;6D'] = 'shift_ctrl_left', [esc..'[1;6C'] = 'shift_ctrl_right', [esc..'[1;7D'] = 'ctrl_alt_left', [esc..'[1;7C'] = 'ctrl_alt_right', [esc..'[1;7D'] = 'shift_ctrl_alt_left', [esc..'[1;7C'] = 'shift_ctrl_alt_right', [esc..'[Z'] = 'shift_tab', [esc..'[5~'] = 'pageup', [esc..'[5;3~'] = 'alt_pageup', [esc..'[5;5~'] = 'ctrl_pageup', [esc..'[5;7~'] = 'ctrl_alt_pageup', [esc..'[6~'] = 'pagedown', [esc..'[6;3~'] = 'alt_pagedown', [esc..'[6;5~'] = 'ctrl_pagedown', [esc..'[6;7~'] = 'ctrl_alt_pagedown', [esc..'[3~'] = 'delete', [esc..'[3;3~'] = 'alt_delete', [esc..'[3;5~'] = 'ctrl_delete', [esc..'[3;7~'] = 'ctrl_alt_delete', } local letters = 'qwertyuiopasdfghjklzxcvbnm' for i=1,#letters do local letter = letters:sub(i, i) translate[esc..letter] = 'alt_'..letter translate[esc..letter:upper()] = 'alt_'..letter:upper() end local buffer = '' local function backupFunc(key, read) io.write(color.ongreen..color.black'rawterm (v2026-01-22)\r\n') io.write(color.green'Here you can see how keys are being translated.\r\n') io.write(color.green'Ctrl+Q to quit.\r\n') io.write(color.green'Ctrl+E to throw an error.\r\n') if key == 'ctrl_q' then return 'pressed ctrl_q' end if key == 'ctrl_e' then error('error was thrown') end if #read > 1 and string.byte(read:sub(1,1)) == 27 then io.write( color.onred..color.black ..'esacpe sequence:'..read:sub(2,#read)..'\n\r' ..color.reset ) end local value = '' if #key == 1 then value = color.onred..color.black..(string.byte(read) or 'no value?') end io.write( value ..color.reset..':' ..color.onblue..color.black..key..'\n\r' ..color.reset ) io.write() buffer = buffer..key buffer = buffer:gsub('\n','\n\r') io.write(buffer..'\n\r') end local function run(func, read) os.execute('clear') local key = translate[read] or read or 'nil?' local status, rtn = pcall(func, key, read) if not status then exitRawmode('ERROR in the function you gave:') error(rtn) end if rtn then exitRawmode(rtn) end return rtn end rawterm.refreshrate = 1/100 function rawterm.run(func) func = func or backupFunc enterRawmode() if run(func, 'init') then return end while true do local read = io.read() if read then if run(func, read) then break end else os.execute("sleep "..rawterm.refreshrate) end end end return rawterm