Skip to content

Instantly share code, notes, and snippets.

@jmiskovic
Created February 15, 2026 11:39
Show Gist options
  • Select an option

  • Save jmiskovic/2a9e1a8eafe4b09398778b7c4d82f673 to your computer and use it in GitHub Desktop.

Select an option

Save jmiskovic/2a9e1a8eafe4b09398778b7c4d82f673 to your computer and use it in GitHub Desktop.
REPL debugger integrated into LÖVR error handler
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