Created
February 15, 2026 11:39
-
-
Save jmiskovic/2a9e1a8eafe4b09398778b7c4d82f673 to your computer and use it in GitHub Desktop.
REPL debugger integrated into LÖVR error handler
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| local lovr = lovr | |
| local dbg = require'debugger' -- https://codeberg.org/slembcke/debugger.lua - tested against 101cab55a4 | |
| dbg.auto_where = 4 | |
| dbg.use_color(true) | |
| local DEFAULT_COLOR = {1, 1, 1} | |
| local ANSI_COLORS = { | |
| ['0'] = {0.933, 0.867, 0.722}, | |
| ['97'] = {0.933, 0.867, 0.722}, | |
| ['30'] = {0.098, 0.090, 0.118}, | |
| ['31'] = {0.773, 0.267, 0.231}, | |
| ['32'] = {0.349, 0.482, 0.384}, | |
| ['33'] = {0.631, 0.373, 0.302}, | |
| ['34'] = {0.478, 0.537, 0.600}, | |
| ['35'] = {0.882, 0.420, 0.357}, | |
| ['36'] = {0.596, 0.678, 0.718}, | |
| ['37'] = {0.933, 0.867, 0.722}, | |
| ['90'] = {0.165, 0.122, 0.133}, | |
| ['91'] = {0.965, 0.714, 0.294}, | |
| ['92'] = {0.247, 0.161, 0.169}, | |
| ['93'] = {0.792, 0.525, 0.416}, | |
| ['94'] = {0.380, 0.231, 0.216}, | |
| ['95'] = {0.890, 714, 0.553}, | |
| ['96'] = {0.455, 0.624, 0.455} | |
| } | |
| local ESC = string.char(27) | |
| local output_lines = {} -- list of { type='text'|'newline', chunks={color, string, ...} } | |
| local current_parse_color = DEFAULT_COLOR | |
| local history = {} | |
| local history_index = 0 | |
| local temp_line = "" | |
| local function parse_ansi(str) | |
| local result = {} | |
| local last_pos = 1 | |
| for start_idx, code, end_idx in str:gmatch("()" .. ESC .. "%[(%d+)m()") do | |
| if start_idx > last_pos then | |
| local color = current_parse_color | |
| if type(color) ~= 'table' then color = DEFAULT_COLOR end | |
| table.insert(result, color) | |
| table.insert(result, str:sub(last_pos, start_idx - 1)) | |
| end | |
| local new_color = ANSI_COLORS[code] | |
| if new_color then | |
| current_parse_color = new_color | |
| elseif code == '0' then | |
| current_parse_color = ANSI_COLORS['0'] or DEFAULT_COLOR | |
| end | |
| last_pos = end_idx | |
| end | |
| if last_pos <= #str then | |
| local color = current_parse_color | |
| if type(color) ~= 'table' then color = DEFAULT_COLOR end | |
| table.insert(result, color) | |
| table.insert(result, str:sub(last_pos)) | |
| end | |
| return result | |
| end | |
| function dbg.write(str) | |
| local parts = {} | |
| local last_idx = 1 | |
| for idx in str:gmatch("()\n") do | |
| table.insert(parts, str:sub(last_idx, idx - 1)) | |
| table.insert(parts, "\n") | |
| last_idx = idx + 1 | |
| end | |
| if last_idx <= #str then | |
| table.insert(parts, str:sub(last_idx)) | |
| end | |
| for _, part in ipairs(parts) do | |
| if part == "\n" then | |
| table.insert(output_lines, {type='newline'}) | |
| else | |
| local colored_chunks = parse_ansi(part) | |
| if #output_lines == 0 or output_lines[#output_lines].type == 'newline' then | |
| table.insert(output_lines, {type='text', chunks=colored_chunks}) | |
| else | |
| local last = output_lines[#output_lines] | |
| for _, chunk in ipairs(colored_chunks) do | |
| table.insert(last.chunks, chunk) | |
| end | |
| end | |
| end | |
| end | |
| while #output_lines > 60 do table.remove(output_lines, 1) end | |
| end | |
| local function get_render_text(line, cursor_pos) | |
| local full_text_table = {} | |
| local current_render_color = ANSI_COLORS['0'] or DEFAULT_COLOR | |
| local function add_chunk(color, text) | |
| if type(text) ~= 'string' or text == "" then return end | |
| if type(color) ~= 'table' then color = current_render_color end | |
| if type(color) ~= 'table' then color = DEFAULT_COLOR end | |
| current_render_color = color | |
| local n = #full_text_table | |
| if n > 1 and full_text_table[n-1] == color then | |
| full_text_table[n] = full_text_table[n] .. text | |
| else | |
| table.insert(full_text_table, {color, text}) | |
| end | |
| end | |
| for _, line_obj in ipairs(output_lines) do | |
| if line_obj.type == 'text' then | |
| local n = #line_obj.chunks | |
| for i = 1, n, 2 do | |
| local c = line_obj.chunks[i] | |
| local t = line_obj.chunks[i+1] | |
| add_chunk(c, t) | |
| end | |
| elseif line_obj.type == 'newline' then | |
| add_chunk(current_render_color, "\n") | |
| end | |
| end | |
| local left = line:sub(1, cursor_pos - 1) | |
| local right = line:sub(cursor_pos) | |
| add_chunk(ANSI_COLORS['0'] or DEFAULT_COLOR, left) | |
| add_chunk({1, 1, 0}, "|") | |
| add_chunk(DEFAULT_COLOR, right) | |
| return full_text_table | |
| end | |
| local function render_console(line, cursor_pos) | |
| if lovr.headset and lovr.headset.isActive() then | |
| lovr.headset.update() | |
| local pass = lovr.headset.getPass() | |
| if pass then | |
| pass:setDepthTest(nil) | |
| pass:setDepthWrite(false) | |
| pass:setColor(0.1, 0.1, 0.12, 1.0) | |
| pass:plane(0, 1.7, -2.01, 2.0, 1.4) | |
| pass:setColor(1, 1, 1, 1) | |
| local colored_text = get_render_text(line, cursor_pos) | |
| pass:text(colored_text, -0.9, 1.1, -2.0, 0.02, 0, 0, 0, 0, 90, 'left', 'bottom') | |
| lovr.graphics.submit(pass) | |
| lovr.headset.submit() | |
| end | |
| end | |
| if lovr.system.isWindowOpen() then | |
| local pass = lovr.graphics.getWindowPass() | |
| if pass then | |
| pass:setDepthTest(nil) | |
| pass:setDepthWrite(false) | |
| pass:setColor(0.05, 0.05, 0.05) | |
| local w, h = lovr.system.getWindowDimensions() | |
| pass:setProjection(1, mat4():orthographic(w, h)) | |
| pass:plane(0, 0, 0.5, w, h) | |
| pass:setColor(1, 1, 1, 1) | |
| local colored_text = get_render_text(line, cursor_pos) | |
| local fontSize = 20 | |
| pass:text(colored_text, 100, h - 100, 0, fontSize, 0, 0, 0, 0, w - 20, 'left', 'bottom') | |
| lovr.graphics.submit(pass) | |
| lovr.graphics.present() | |
| end | |
| end | |
| end | |
| function dbg.read(prompt) | |
| dbg.write('\n' .. prompt) | |
| local line = "" | |
| local pos = 1 | |
| lovr.system.pollEvents() | |
| while true do | |
| if lovr.system then lovr.system.pollEvents() end | |
| if lovr.headset then lovr.headset.pollEvents() end | |
| for name, a in lovr.event.poll() do | |
| local ctrl = lovr.system.isKeyDown('lctrl') or lovr.system.isKeyDown('rctrl') | |
| if name == 'quit' or lovr.system.isKeyDown('escape') then return 'q' | |
| elseif name == 'restart' or name == 'filechanged' or (name == 'keypressed' and a == 'f5') then | |
| lovr.restart() | |
| elseif name == 'keypressed' then | |
| if a == 'return' then | |
| dbg.write(line .. "\n") | |
| if #line > 0 and line ~= history[#history] then | |
| table.insert(history, line) | |
| end | |
| history_index = 0 | |
| return line | |
| elseif a == 'backspace' then | |
| if pos > 1 then | |
| line = line:sub(1, pos - 2) .. line:sub(pos) | |
| pos = pos - 1 | |
| end | |
| elseif a == 'delete' then | |
| if pos <= #line then | |
| line = line:sub(1, pos - 1) .. line:sub(pos + 1) | |
| end | |
| elseif a == 'left' then | |
| pos = math.max(1, pos - 1) | |
| elseif a == 'right' then | |
| pos = math.min(#line + 1, pos + 1) | |
| elseif a == 'home' or (a == 'a' and ctrl) then | |
| pos = 1 | |
| elseif a == 'end' or (a == 'e' and ctrl) then | |
| pos = #line + 1 | |
| elseif a == 'w' and ctrl then | |
| local left = line:sub(1, pos - 1) | |
| local right = line:sub(pos) | |
| local new_left = left:gsub("%s*[%w_]+%s*$", "") | |
| line = new_left .. right | |
| pos = #new_left + 1 | |
| elseif a == 'u' and ctrl then | |
| line = line:sub(pos) | |
| pos = 1 | |
| elseif a == 'k' and ctrl then | |
| line = line:sub(1, pos - 1) | |
| elseif a == 'up' then | |
| if history_index < #history then | |
| if history_index == 0 then temp_line = line end | |
| history_index = history_index + 1 | |
| line = history[#history - history_index + 1] | |
| pos = #line + 1 | |
| end | |
| elseif a == 'down' then | |
| if history_index > 0 then | |
| history_index = history_index - 1 | |
| if history_index == 0 then | |
| line = temp_line | |
| else | |
| line = history[#history - history_index + 1] | |
| end | |
| pos = #line + 1 | |
| end | |
| elseif a == 'v' and ctrl then | |
| if lovr.system.getClipboardText then | |
| local text = lovr.system.getClipboardText() | |
| if text then | |
| line = line:sub(1, pos - 1) .. text .. line:sub(pos) | |
| pos = pos + #text | |
| end | |
| end | |
| end | |
| elseif name == 'textinput' then | |
| line = line:sub(1, pos - 1) .. a .. line:sub(pos) | |
| pos = pos + #a | |
| end | |
| end | |
| render_console(line, pos) | |
| lovr.timer.sleep(0.01) | |
| end | |
| end | |
| function lovr.errhand(msg) | |
| local trace = debug.traceback(msg, 4) | |
| print(trace) | |
| dbg.write(ESC .. "[91m" .. "Error:\n" .. ESC .. "[0m") | |
| dbg.write(trace .. "\n\n") | |
| dbg(false, 3, "error") | |
| lovr.event.push('quit') | |
| end | |
| return dbg |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment