lua-debug-tui/cursed.lua

493 lines
15 KiB
Lua
Raw Normal View History

2018-03-13 04:20:46 -07:00
local C = require("curses")
local repr = require('repr')
2018-03-13 23:53:06 -07:00
local COLORS = { }
2018-03-14 16:10:17 -07:00
local run_debugger, guard, stdscr
2018-03-14 18:45:32 -07:00
local AUTO = { }
2018-03-13 04:20:46 -07:00
local log = io.open("output.log", "w")
local callstack_range
callstack_range = function()
local min, max = 0, -1
for i = 1, 999 do
local info = debug.getinfo(i, 'f')
if not info then
min = i - 1
break
end
2018-03-14 16:10:17 -07:00
if info.func == run_debugger then
2018-03-13 04:20:46 -07:00
min = i + 1
break
end
end
for i = min, 999 do
local info = debug.getinfo(i, 'f')
if not info or info.func == guard then
2018-03-15 15:30:43 -07:00
max = i - 0
2018-03-13 04:20:46 -07:00
break
end
end
return min, max
end
local wrap_text
wrap_text = function(text, width)
local lines = { }
for line in text:gmatch("[^\n]*") do
while #line > width do
table.insert(lines, line:sub(1, width))
line = line:sub(width + 1, -1)
end
if #line > 0 then
table.insert(lines, line)
end
end
return lines
end
2018-03-13 23:53:06 -07:00
local default_colors = { }
2018-03-13 04:20:46 -07:00
local Pad
do
local _class_0
local _base_0 = {
2018-03-13 23:53:06 -07:00
set_active = function(self, active)
2018-03-14 16:10:17 -07:00
if active == self.active then
return
end
2018-03-13 23:53:06 -07:00
self.active = active
2018-03-14 18:45:32 -07:00
return self._frame:attrset(active and self.colors.active_frame or self.colors.inactive_frame)
2018-03-13 23:53:06 -07:00
end,
2018-03-13 04:20:46 -07:00
select = function(self, i)
2018-03-14 18:45:32 -07:00
if #self.lines == 0 then
i = nil
end
if i == self.selected then
2018-03-13 15:51:28 -07:00
return self.selected
2018-03-13 04:20:46 -07:00
end
if i ~= nil then
i = math.max(1, math.min(#self.lines, i))
end
if self.selected then
local j = self.selected
2018-03-14 18:45:32 -07:00
local attr = self.colors.line_colors[j]
2018-03-13 23:53:06 -07:00
self.chstrs[j]:set_str(0, self.lines[j], attr)
self.chstrs[j]:set_str(#self.lines[j], ' ', attr, self.chstrs[j]:len() - #self.lines[j])
2018-03-14 18:45:32 -07:00
self._pad:mvaddchstr(j - 1, 0, self.chstrs[j])
2018-03-13 04:20:46 -07:00
end
if i then
2018-03-13 23:53:06 -07:00
local attr = self.active and self.colors.active or self.colors.highlight
self.chstrs[i]:set_str(0, self.lines[i], attr)
self.chstrs[i]:set_str(#self.lines[i], ' ', attr, self.chstrs[i]:len() - #self.lines[i])
2018-03-14 18:45:32 -07:00
self._pad:mvaddchstr(i - 1, 0, self.chstrs[i])
local scrolloff = 3
if i > self.scroll_y + (self.height - 2) - scrolloff then
self.scroll_y = i - (self.height - 2) + scrolloff
elseif i < self.scroll_y + scrolloff then
self.scroll_y = i - scrolloff
2018-03-13 04:20:46 -07:00
end
2018-03-14 18:45:32 -07:00
self.scroll_y = math.max(1, math.min(#self.lines, self.scroll_y))
2018-03-13 04:20:46 -07:00
end
2018-03-14 18:45:32 -07:00
self.selected = i
2018-03-13 04:20:46 -07:00
self:refresh()
if self.on_select then
self:on_select(self.selected)
end
return self.selected
end,
2018-03-14 18:45:32 -07:00
scroll = function(self, dy, dx)
if self.selected ~= nil then
self:select(self.selected + (dy or 0))
else
self.scroll_y = math.max(1, math.min(self._height - self.height, self.scroll_y + (dy or 0)))
2018-03-14 16:10:17 -07:00
end
2018-03-14 18:45:32 -07:00
self.scroll_x = math.max(1, math.min(self._width - self.width, self.scroll_x + (dx or 0)))
return self:refresh()
2018-03-14 16:10:17 -07:00
end,
2018-03-13 04:20:46 -07:00
refresh = function(self)
2018-03-14 18:45:32 -07:00
self._frame:border(C.ACS_VLINE, C.ACS_VLINE, C.ACS_HLINE, C.ACS_HLINE, C.ACS_ULCORNER, C.ACS_URCORNER, C.ACS_LLCORNER, C.ACS_LRCORNER)
2018-03-13 23:53:06 -07:00
if self.label then
2018-03-14 18:45:32 -07:00
self._frame:mvaddstr(0, math.floor((self.width - #self.label - 2) / 2), " " .. tostring(self.label) .. " ")
2018-03-13 23:53:06 -07:00
end
2018-03-14 18:45:32 -07:00
self._frame:refresh()
local h, w = math.min(self.height - 2, self._height), math.min(self.width - 2, self._width)
return self._pad:pnoutrefresh(self.scroll_y - 1, self.scroll_x - 1, self.y + 1, self.x + 1, self.y + h, self.x + w)
2018-03-13 04:20:46 -07:00
end,
erase = function(self)
2018-03-14 18:45:32 -07:00
self._frame:erase()
return self._frame:refresh()
2018-03-13 04:20:46 -07:00
end,
2018-03-14 18:45:32 -07:00
__gc = function(self)
2018-03-15 15:30:43 -07:00
self._frame:close()
return self._pad:close()
2018-03-13 04:20:46 -07:00
end
}
_base_0.__index = _base_0
_class_0 = setmetatable({
2018-03-13 23:53:06 -07:00
__init = function(self, y, x, height, width, lines, label, colors)
if colors == nil then
colors = default_colors
2018-03-13 04:20:46 -07:00
end
2018-03-13 23:53:06 -07:00
self.y, self.x, self.height, self.width, self.lines, self.label, self.colors = y, x, height, width, lines, label, colors
if self.colors and self.colors ~= default_colors then
setmetatable(self.colors, {
__index = default_colors
})
2018-03-13 04:20:46 -07:00
end
2018-03-14 18:45:32 -07:00
self.scroll_y, self.scroll_x = 1, 1
2018-03-13 04:20:46 -07:00
self.selected = nil
2018-03-14 18:45:32 -07:00
self._height = #self.lines
2018-03-13 04:20:46 -07:00
if self.height == AUTO then
2018-03-14 18:45:32 -07:00
self.height = self._height + 2
2018-03-13 04:20:46 -07:00
end
2018-03-14 18:45:32 -07:00
self._width = 0
local _list_0 = self.lines
for _index_0 = 1, #_list_0 do
local x = _list_0[_index_0]
self._width = math.max(self._width, #x + 2)
end
if self.width == AUTO then
self.width = self._width + 2
end
self._frame = C.newwin(self.height, self.width, self.y, self.x)
2018-03-13 04:20:46 -07:00
self._pad = C.newpad(self._height, self._width)
self._pad:scrollok(true)
2018-03-13 23:53:06 -07:00
self:set_active(false)
2018-03-13 04:20:46 -07:00
self.chstrs = { }
for i, line in ipairs(self.lines) do
2018-03-14 18:45:32 -07:00
local attr = self.colors.line_colors[i]
local chstr = C.new_chstr(self._width)
2018-03-13 04:20:46 -07:00
self.chstrs[i] = chstr
2018-03-13 23:53:06 -07:00
if #line >= chstr:len() then
line = line:sub(1, chstr:len())
else
line = line .. (" "):rep(chstr:len() - #line)
end
2018-03-13 04:20:46 -07:00
chstr:set_str(0, line, attr)
2018-03-14 18:45:32 -07:00
self._pad:mvaddchstr(i - 1, 0, chstr)
2018-03-13 04:20:46 -07:00
end
return self:refresh()
end,
__base = _base_0,
__name = "Pad"
}, {
__index = _base_0,
__call = function(cls, ...)
local _self_0 = setmetatable({}, _base_0)
cls.__init(_self_0, ...)
return _self_0
end
})
_base_0.__class = _class_0
Pad = _class_0
end
local ok, to_lua = pcall(function()
return require('moonscript.base').to_lua
end)
if not ok then
to_lua = function()
return nil
end
end
local file_cache = setmetatable({ }, {
__index = function(self, filename)
local file = io.open(filename)
if not file then
return nil
end
local contents = file:read("*a")
self[filename] = contents
return contents
end
})
local line_tables = setmetatable({ }, {
__index = function(self, filename)
local file = file_cache[filename]
2018-03-13 15:51:28 -07:00
if not file then
return nil
end
2018-03-13 04:20:46 -07:00
local line_table
ok, line_table = to_lua(file)
if ok then
self[filename] = line_table
return line_table
end
end
})
2018-03-14 16:10:17 -07:00
run_debugger = function(err_msg)
2018-03-15 15:30:43 -07:00
log:write(err_msg .. "\n\n")
2018-03-14 16:10:17 -07:00
stdscr = C.initscr()
SCREEN_H, SCREEN_W = stdscr:getmaxyx()
C.cbreak()
C.echo(false)
C.nl(false)
C.curs_set(0)
C.start_color()
C.use_default_colors()
local _
_, COLORS.REGULAR = C.init_pair(1, C.COLOR_WHITE, -1), C.color_pair(1)
_, COLORS.INVERTED = C.init_pair(2, C.COLOR_WHITE, C.COLOR_BLACK), C.color_pair(2)
_, COLORS.YELLOW_BG = C.init_pair(3, C.COLOR_BLACK, C.COLOR_YELLOW), C.color_pair(3)
_, COLORS.RED = C.init_pair(4, C.COLOR_RED, -1), C.color_pair(4)
_, COLORS.BLUE = C.init_pair(5, C.COLOR_BLUE, -1), C.color_pair(5) | C.A_BOLD
_, COLORS.WHITE = C.init_pair(6, C.COLOR_WHITE, -1), C.color_pair(6)
_, COLORS.WHITE_BG = C.init_pair(7, C.COLOR_BLACK, C.COLOR_WHITE), C.color_pair(7)
_, COLORS.BROWN = C.init_pair(8, C.COLOR_BLACK, -1), C.color_pair(8) | C.A_BOLD
_, COLORS.RED_BG = C.init_pair(9, C.COLOR_YELLOW, C.COLOR_RED), C.color_pair(9) | C.A_BOLD | C.A_DIM
_, COLORS.GREEN = C.init_pair(10, C.COLOR_GREEN, -1), C.color_pair(10)
default_colors = {
active_frame = COLORS.BLUE,
inactive_frame = COLORS.BROWN,
2018-03-14 18:45:32 -07:00
line_colors = setmetatable({ }, {
__index = function(self, i)
return (i % 2 == 0 and COLORS.INVERTED or COLORS.REGULAR)
end
}),
2018-03-14 16:10:17 -07:00
highlight = COLORS.WHITE_BG,
active = COLORS.YELLOW_BG
}
2018-03-15 15:30:43 -07:00
do
stdscr:wbkgd(COLORS.RED_BG)
stdscr:clear()
stdscr:refresh()
local lines = wrap_text("ERROR!\n \n " .. err_msg .. "\n \npress any key...", math.floor(SCREEN_W / 2))
for i, line in ipairs(lines) do
stdscr:mvaddstr(math.floor(SCREEN_H / 2 - #lines / 2) + i, math.floor((SCREEN_W - #line) / 2), line)
end
stdscr:refresh()
C.doupdate()
stdscr:getch()
end
stdscr:wbkgd(COLORS.REGULAR)
2018-03-14 16:10:17 -07:00
stdscr:clear()
stdscr:refresh()
local pads = { }
do
2018-03-13 04:48:17 -07:00
local err_msg_lines = wrap_text(err_msg, SCREEN_W - 4)
2018-03-13 23:53:06 -07:00
for i, line in ipairs(err_msg_lines) do
err_msg_lines[i] = (" "):rep(math.floor((SCREEN_W - 2 - #line) / 2)) .. line
end
2018-03-14 16:10:17 -07:00
pads.err = Pad(0, 0, AUTO, SCREEN_W, err_msg_lines, "Error Message", {
2018-03-14 18:45:32 -07:00
line_colors = setmetatable({ }, {
__index = function()
return COLORS.RED | C.A_BOLD
end
}),
2018-03-13 23:53:06 -07:00
inactive_frame = COLORS.RED | C.A_DIM
})
2018-03-13 04:48:17 -07:00
end
2018-03-15 15:30:43 -07:00
local stack_locations = { }
2018-03-14 16:10:17 -07:00
do
local stack_names = { }
local max_filename = 0
local stack_min, stack_max = callstack_range()
for i = stack_min, stack_max do
local _continue_0 = false
repeat
local info = debug.getinfo(i)
if not info then
break
end
table.insert(stack_names, info.name or "<unnamed function>")
if not info.short_src then
_continue_0 = true
break
end
local line_table = line_tables[info.short_src]
local line
if line_table then
local char = line_table[info.currentline]
local line_num = 1
local file = file_cache[info.short_src]
for _ in file:sub(1, char):gmatch("\n") do
line_num = line_num + 1
end
line = tostring(info.short_src) .. ":" .. tostring(line_num)
else
line = info.short_src .. ":" .. info.currentline
end
table.insert(stack_locations, line)
max_filename = math.max(max_filename, #line)
_continue_0 = true
until true
if not _continue_0 then
break
end
end
local callstack = { }
for i = 1, #stack_names do
callstack[i] = stack_locations[i] .. (" "):rep(max_filename - #stack_locations[i]) .. " | " .. stack_names[i] .. " "
end
pads.stack = Pad(pads.err.height, 0, math.max(#callstack + 2, 20), AUTO, callstack, "(C)allstack")
pads.stack:set_active(true)
pads.stack:refresh()
end
local show_src
show_src = function(filename, line_no)
local file = file_cache[filename]
local src_lines = { }
2018-03-14 18:45:32 -07:00
local err_line = nil
2018-03-14 16:10:17 -07:00
if file then
2018-03-13 23:53:06 -07:00
local i = 0
for line in file:gmatch("[^\n]*") do
2018-03-14 18:45:32 -07:00
i = i + 1
table.insert(src_lines, line)
if i == line_no then
err_line = #src_lines
2018-03-13 23:53:06 -07:00
end
end
2018-03-14 18:45:32 -07:00
while #src_lines < pads.stack.height do
table.insert(src_lines, "")
end
2018-03-14 16:10:17 -07:00
else
table.insert(src_lines, "<no source code found>")
2018-03-13 23:53:06 -07:00
end
2018-03-14 16:10:17 -07:00
if pads.src then
pads.src:erase()
end
pads.src = Pad(pads.err.height, pads.stack.x + pads.stack.width, pads.stack.height, SCREEN_W - pads.stack.x - pads.stack.width - 0, src_lines, "(S)ource Code", {
2018-03-14 18:45:32 -07:00
line_colors = setmetatable({
2018-03-15 15:30:43 -07:00
[err_line or -1] = COLORS.RED_BG
2018-03-14 18:45:32 -07:00
}, {
__index = function(self, i)
return (i % 2 == 0) and INVERTED or REGULAR
end
})
2018-03-14 16:10:17 -07:00
})
2018-03-14 18:45:32 -07:00
return pads.src:select(err_line)
2018-03-13 04:48:17 -07:00
end
2018-03-14 16:10:17 -07:00
local show_vars
show_vars = function(stack_index)
if pads.vars then
pads.vars:erase()
2018-03-13 04:20:46 -07:00
end
2018-03-14 16:10:17 -07:00
if pads.values then
pads.values:erase()
end
local callstack_min
callstack_min, _ = callstack_range()
local var_names, values = { }, { }
for loc = 1, 999 do
local name, value = debug.getlocal(callstack_min + stack_index - 1, loc)
if value == nil then
break
end
table.insert(var_names, tostring(name))
if type(value) == 'function' then
local info = debug.getinfo(value, 'nS')
table.insert(values, repr(info))
else
table.insert(values, repr(value))
end
end
local var_y = pads.stack.y + pads.stack.height
local var_x = 0
pads.vars = Pad(var_y, var_x, math.min(2 + #var_names, SCREEN_H - pads.err.height - pads.stack.height), AUTO, var_names, "(V)ars")
pads.vars.on_select = function(self, var_index)
local value_x = pads.vars.x + pads.vars.width
local value_w = SCREEN_W - (value_x)
if var_index then
pads.values = Pad(var_y, value_x, pads.vars.height, value_w, wrap_text(values[var_index], value_w - 2), "Values")
else
pads.values = Pad(var_y, value_x, pads.vars.height, value_w, values, "Values")
end
2018-03-15 15:30:43 -07:00
collectgarbage()
return collectgarbage()
2018-03-13 04:20:46 -07:00
end
2018-03-14 16:10:17 -07:00
return pads.vars:select(1)
2018-03-13 04:20:46 -07:00
end
2018-03-14 16:10:17 -07:00
pads.stack.on_select = function(self, stack_index)
local filename, line_no = pads.stack.lines[stack_index]:match("([^:]*):(%d*).*")
line_no = tonumber(line_no)
show_src(filename, line_no)
return show_vars(stack_index)
2018-03-13 04:20:46 -07:00
end
2018-03-14 16:10:17 -07:00
pads.stack:select(1)
pads.stack:set_active(true)
local selected_pad = pads.stack
local select_pad
select_pad = function(pad)
if selected_pad ~= pad then
selected_pad:set_active(false)
selected_pad:refresh()
selected_pad = pad
selected_pad:set_active(true)
return selected_pad:refresh()
end
2018-03-13 04:20:46 -07:00
end
while true do
C.doupdate()
local c = stdscr:getch()
local _exp_0 = c
if C.KEY_DOWN == _exp_0 or C.KEY_SF == _exp_0 or ("j"):byte() == _exp_0 then
2018-03-14 18:45:32 -07:00
selected_pad:scroll(1, 0)
2018-03-13 04:20:46 -07:00
elseif ('J'):byte() == _exp_0 then
2018-03-14 18:45:32 -07:00
selected_pad:scroll(10, 0)
elseif C.KEY_UP == _exp_0 or C.KEY_SR == _exp_0 or ("k"):byte() == _exp_0 then
selected_pad:scroll(-1, 0)
2018-03-13 04:20:46 -07:00
elseif ('K'):byte() == _exp_0 then
2018-03-14 18:45:32 -07:00
selected_pad:scroll(-10, 0)
elseif C.KEY_RIGHT == _exp_0 or ("l"):byte() == _exp_0 then
selected_pad:scroll(0, 1)
elseif ("L"):byte() == _exp_0 then
selected_pad:scroll(0, 10)
elseif C.KEY_LEFT == _exp_0 or ("h"):byte() == _exp_0 then
selected_pad:scroll(0, -1)
elseif ("H"):byte() == _exp_0 then
selected_pad:scroll(0, -10)
2018-03-14 16:10:17 -07:00
elseif ('c'):byte() == _exp_0 then
select_pad(pads.stack)
elseif ('s'):byte() == _exp_0 then
select_pad(pads.src)
elseif ('v'):byte() == _exp_0 then
select_pad(pads.vars)
2018-03-13 04:20:46 -07:00
elseif ('o'):byte() == _exp_0 then
2018-03-14 16:10:17 -07:00
local file = stack_locations[pads.stack.selected]
2018-03-13 04:20:46 -07:00
local filename, line_no = file:match("([^:]*):(.*)")
2018-03-15 15:30:43 -07:00
C.endwin()
2018-03-13 04:20:46 -07:00
os.execute((os.getenv("EDITOR") or "nano") .. " +" .. line_no .. " " .. filename)
2018-03-15 15:30:43 -07:00
stdscr = C.initscr()
C.cbreak()
C.echo(false)
C.nl(false)
C.curs_set(0)
C.start_color()
C.use_default_colors()
stdscr:clear()
stdscr:refresh()
for _, pad in pairs(pads) do
pad:refresh()
end
2018-03-13 04:20:46 -07:00
elseif ('q'):byte() == _exp_0 or ("Q"):byte() == _exp_0 then
2018-03-15 15:30:43 -07:00
pads = { }
C.endwin()
return
2018-03-13 04:20:46 -07:00
end
end
2018-03-13 15:55:14 -07:00
return C.endwin()
2018-03-13 04:20:46 -07:00
end
guard = function(fn, ...)
local err_hand
err_hand = function(err)
2018-03-15 15:30:43 -07:00
log:write(err .. "\n\n\n")
2018-03-13 04:20:46 -07:00
C.endwin()
print("Caught an error:")
print(debug.traceback(err, 2))
return os.exit(2)
end
return xpcall(fn, (function(err_msg)
return xpcall(run_debugger, err_hand, err_msg)
end), ...)
end
local breakpoint
breakpoint = function()
local err_hand
err_hand = function(err)
2018-03-15 15:30:43 -07:00
log:write(err .. "\n\n\n")
2018-03-13 04:20:46 -07:00
C.endwin()
print("Caught an error:")
print(debug.traceback(err, 2))
return os.exit(2)
end
return xpcall(run_debugger, err_hand, "Breakpoint triggered!")
end
return {
guard = guard,
breakpoint = breakpoint
}