local posix = { termio = require("posix.termio"), poll = require("posix.poll"), unistd = require("posix.unistd"), } local cutie = { esc = string.char(27) .. "[", terminal_size = nil, buffer = "", input = EventTarget() } -- colors local function color_from_hue(hue) local h = hue / 60 local x = (1 - math.abs(h % 2 - 1)) local i = math.floor(h) if i == 0 then return {1, x, 0} elseif i == 1 then return {x, 1, 0} elseif i == 2 then return {0, 1, x} elseif i == 3 then return {0, x, 1} elseif i == 4 then return {x, 0, 1} else return {1, 0, x} end end function cutie.to_color(color) local t = type(color) if t == "number" then return color_from_hue(color) elseif t == "string" then local color = color:gsub("#", "") return { tonumber(color:sub(1, 2), 16) / 255, tonumber(color:sub(3, 4), 16) / 255, tonumber(color:sub(5, 6), 16) / 255, } else return color end end function make_color(color, bg) color = cutie.to_color(color) return cutie.esc .. (bg and "48" or "38") .. ";2" .. ";" .. math.clamp(math.floor(color[1] * 255), 0, 255) .. ";" .. math.clamp(math.floor(color[2] * 255), 0, 255) .. ";" .. math.clamp(math.floor(color[3] * 255), 0, 255) .. "m" end function cutie.color(color) return make_color(color, false) end function cutie.background_color(color) return make_color(color, true) end function cutie.set_color(color) cutie.render(cutie.color(color)) end function cutie.set_background(color) cutie.render(cutie.background_color(color)) end -- simple effects cutie.bold = cutie.esc .. "1m" cutie.no_effects = cutie.esc .. "0m" function cutie.set_bold() cutie.render(cutie.bold) end function cutie.clear_effects() cutie.render(cutie.no_effects) end function cutie.move_cursor(x, y) cutie.render_escape(math.floor(y) .. ";" .. math.floor(x) .. "H") end function cutie.set_alternate_buffer(enabled) cutie.render_escape("?1049" .. (enabled and "h" or "l")) end function cutie.set_cursor_shown(enabled) cutie.render_escape("?25" .. (enabled and "h" or "l")) end -- input function cutie.set_canon_input(enabled) local termios = posix.termio.tcgetattr(0) if enabled then termios.lflag = bit32.bor(termios.lflag, posix.termio.ICANON, posix.termio.ECHO ) else termios.lflag = bit32.band(termios.lflag, bit32.bnot(bit32.bor( posix.termio.ICANON, posix.termio.ECHO ))) end posix.termio.tcsetattr(0, posix.termio.TCSANOW, termios) end function cutie.set_input_buffer(enabled) lua_async.poll_functions[cutie.poll_input] = enabled or nil cutie.input.buffer = enabled and "" or nil cutie.input.history = enabled and {} or nil cutie.input.cursor = nil end local function getchar() return posix.unistd.read(0, 1) end function cutie.poll_input() local pfd = {[0] = {events = {IN = true}}} posix.poll.poll(pfd, 0) if pfd[0].revents and pfd[0].revents.IN then local char = getchar() if char == "\n" then if input ~= "" then cutie.input:dispatchEvent(Event("input", {input = cutie.input.buffer})) table.insert(cutie.input.history, cutie.input.buffer) cutie.input.buffer = "" cutie.input.cursor = nil end elseif char == cutie.esc:sub(1, 1) then local char2 = getchar() if char2 == cutie.esc:sub(2, 2) then local char3 = getchar() if char3 == "A" or char3 == "B" then cutie.input.cursor = (cutie.input.cursor or #cutie.input.history + 1) + (char3 == "A" and -1 or 1) if cutie.input.cursor > #cutie.input.history then cutie.input.cursor = #cutie.input.history + 1 end if cutie.input.cursor < 1 then cutie.input.cursor = 1 end cutie.input.buffer = cutie.input.history[cutie.input.cursor] or "" end end elseif char == string.char(127) then cutie.input.buffer = cutie.input.buffer:sub(1, #cutie.input.buffer - 1) else cutie.input.buffer = cutie.input.buffer .. char end end end -- rendering function cutie.clear_screen() cutie.render_escape("2J") end function cutie.empty_screen() cutie.move_cursor(1, 1) local size = cutie.get_terminal_size() local str = (string.rep(" ", size[1]) .. "\n"):rep(size[2]) str = str:sub(1, #str - 1) cutie.render(str) end function cutie.render(text) cutie.buffer = cutie.buffer .. text end function cutie.render_escape(text) cutie.render(cutie.esc .. text) end function cutie.render_at(array, x, y) for i, line in ipairs(array) do cutie.move_cursor(x + 1, y + i) cutie.render(line) end end function cutie.get_dimensions(array) local width = 0 for _, line in pairs(array) do width = math.max(width, #line) end return {width, #array} end function cutie.flush_buffer() io.write(cutie.buffer) io.stdout:flush() cutie.buffer = "" end function cutie.box(size) return { "┌" .. string.rep("─", size[1]) .. "┐", string.rep( "│" .. string.rep(" ", size[1]) .. "│", size[2]), "└" .. string.rep("─", size[1]) .. "┘", } end -- terminal size function cutie.handle_resize() local pf = io.popen("echo -ne \"cols\\nlines\" | tput -S", "r") local size = pf:read("*all"):split("\n") pf:close() cutie.terminal_size = size end function cutie.get_terminal_size() return cutie.terminal_size end return cutie