local C = require("curses") local re = require('re') local line_matcher = re.compile('lines<-{| line ("\n" line)* |} line<-{[^\n]*}') local ldb local AUTO = { } local PARENT = { } local _error = error local _assert = assert 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 if info.func == ldb.run_debugger then min = i + 2 break end end for i = min, 999 do local info = debug.getinfo(i, 'f') if not info or info.func == ldb.guard then max = i - 3 break end end return min, max end local wrap_text wrap_text = function(text, width) local lines = { } local _list_0 = line_matcher:match(text) for _index_0 = 1, #_list_0 do local line = _list_0[_index_0] while #line > width do table.insert(lines, line:sub(1, width)) line = line:sub(width + 1, -1) if #line == 0 then line = nil end end if line then table.insert(lines, line) end end return lines end local Color do local color_index = 0 local existing = { } local make_color make_color = function(fg, bg) if fg == nil then fg = -1 end if bg == nil then bg = -1 end local key = tostring(fg) .. "," .. tostring(bg) if not (existing[key]) then color_index = color_index + 1 C.init_pair(color_index, fg, bg) existing[key] = C.color_pair(color_index) end return existing[key] end local color_lang = re.compile([[ x <- {| {:attrs: {| {attr} (" " {attr})* |} :} / ( ({:bg: "on " {color} :} / ({:fg: color :} (" on " {:bg: color :})?)) {:attrs: {| (" " {attr})* |} :}) |} attr <- "blink" / "bold" / "dim" / "invis" / "normal" / "protect" / "reverse" / "standout" / "underline" / "altcharset" color <- "black" / "blue" / "cyan" / "green" / "magenta" / "red" / "white" / "yellow" / "default" ]]) C.COLOR_DEFAULT = -1 Color = function(s) if s == nil then s = "default" end local t = _assert(color_lang:match(s), "Invalid color: " .. tostring(s)) if t.fg then t.fg = C["COLOR_" .. t.fg:upper()] end if t.bg then t.bg = C["COLOR_" .. t.bg:upper()] end local c = make_color(t.fg, t.bg) local _list_0 = t.attrs for _index_0 = 1, #_list_0 do local a = _list_0[_index_0] c = c | C["A_" .. a:upper()] end return c end end local Pad do local _class_0 local _base_0 = { configure_size = function(self, height, width) self.height, self.width = height, width self._height = math.max(#self.columns[1], 1) if self.height == AUTO then self.height = self._height + 2 end self._width = #self.columns - 1 for i, col in ipairs(self.columns) do local col_width = 0 for _index_0 = 1, #col do local chunk = col[_index_0] col_width = math.max(col_width, #chunk) end self._width = self._width + col_width end self._width = math.max(self._width, 1) if self.width == AUTO then self.width = self._width + 2 end end, setup_chstr = function(self, i) local chstr = _assert(self.chstrs[i], "Failed to find chstrs[" .. tostring(i) .. "]") local x = 0 for c = 1, #self.columns do local attr = self.colors[c](self, i) local chunk = self.columns[c][i] chstr:set_str(x, chunk, attr) x = x + #chunk if #chunk < self.column_widths[c] then chstr:set_str(x, " ", attr, self.column_widths[c] - #chunk) x = x + (self.column_widths[c] - #chunk) end if c < #self.columns then chstr:set_ch(x, C.ACS_VLINE, Color("black bold")) x = x + 1 end end self._pad:mvaddchstr(i - 1, 0, chstr) self.dirty = true end, set_active = function(self, active) if active == self.active then return end self.active = active self._frame:attrset(active and self.active_frame or self.inactive_frame) self.dirty = true end, select = function(self, i) if #self.columns[1] == 0 then i = nil end if i == self.selected then return self.selected end local old_y, old_x = self.scroll_y, self.scroll_x if i ~= nil then i = math.max(1, math.min(#self.columns[1], i)) end local old_selected old_selected, self.selected = self.selected, i if old_selected then self:setup_chstr(old_selected) end if self.selected then self:setup_chstr(self.selected) local scrolloff = 3 if self.selected > self.scroll_y + (self.height - 2) - scrolloff then self.scroll_y = self.selected - (self.height - 2) + scrolloff elseif self.selected < self.scroll_y + scrolloff then self.scroll_y = self.selected - scrolloff end self.scroll_y = math.max(1, math.min(self._height, self.scroll_y)) end if self.scroll_y == old_y then local w = math.min(self.width - 2, self._width) if old_selected and self.scroll_y <= old_selected and old_selected <= self.scroll_y + self.height - 2 then self._pad:pnoutrefresh(old_selected - 1, self.scroll_x - 1, self.y + 1 + (old_selected - self.scroll_y), self.x + 1, self.y + 1 + (old_selected - self.scroll_y) + 1, self.x + w) end if self.selected and self.scroll_y <= self.selected and self.selected <= self.scroll_y + self.height - 2 then self._pad:pnoutrefresh(self.selected - 1, self.scroll_x - 1, self.y + 1 + (self.selected - self.scroll_y), self.x + 1, self.y + 1 + (self.selected - self.scroll_y) + 1, self.x + w) end else self.dirty = true end if self.on_select then self:on_select(self.selected) end return self.selected end, scroll = function(self, dy, dx) local old_y, old_x = self.scroll_y, self.scroll_x 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 - 2 - 1), self.scroll_y + (dy or 0))) end self.scroll_x = math.max(1, math.min(self._width - (self.width - 2 - 1), self.scroll_x + (dx or 0))) if self.scroll_y ~= old_y or self.scroll_x ~= old_x then self.dirty = true end end, refresh = function(self, force) if force == nil then force = false end if not force and not self.dirty then return end 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) if self.label then self._frame:mvaddstr(0, math.floor((self.width - #self.label - 2) / 2), " " .. tostring(self.label) .. " ") end self._frame:refresh() local h, w = math.min(self.height - 2, self._height), math.min(self.width - 2, self._width) self._pad:pnoutrefresh(self.scroll_y - 1, self.scroll_x - 1, self.y + 1, self.x + 1, self.y + h, self.x + w) self.dirty = false end, keypress = function(self, c) local _exp_0 = c if C.KEY_DOWN == _exp_0 or C.KEY_SR == _exp_0 or ("j"):byte() == _exp_0 then return self:scroll(1, 0) elseif ('J'):byte() == _exp_0 then return self:scroll(10, 0) elseif C.KEY_UP == _exp_0 or C.KEY_SF == _exp_0 or ("k"):byte() == _exp_0 then return self:scroll(-1, 0) elseif ('K'):byte() == _exp_0 then return self:scroll(-10, 0) elseif C.KEY_RIGHT == _exp_0 or ("l"):byte() == _exp_0 then return self:scroll(0, 1) elseif ("L"):byte() == _exp_0 then return self:scroll(0, 10) elseif C.KEY_LEFT == _exp_0 or ("h"):byte() == _exp_0 then return self:scroll(0, -1) elseif ("H"):byte() == _exp_0 then return self:scroll(0, -10) end end, erase = function(self) self.dirty = true self._frame:erase() return self._frame:refresh() end, __gc = function(self) self._frame:close() return self._pad:close() end } _base_0.__index = _base_0 _class_0 = setmetatable({ __init = function(self, label, y, x, height, width, ...) self.label, self.y, self.x = label, y, x self.scroll_y, self.scroll_x = 1, 1 self.selected = nil self.columns = { } self.column_widths = { } self.active_frame = Color("yellow bold") self.inactive_frame = Color("blue dim") self.colors = { } for i = 1, select('#', ...) - 1, 2 do local col = select(i, ...) table.insert(self.columns, col) local w = 0 for _index_0 = 1, #col do local chunk = col[_index_0] w = math.max(w, #chunk) end table.insert(self.column_widths, w) local color_fn = select(i + 1, ...) or (function(self, i) return Color() end) _assert(type(color_fn) == 'function', "Invalid color function type: " .. tostring(type(color_fn))) table.insert(self.colors, color_fn) end self:configure_size(height, width) self._frame = C.newwin(self.height, self.width, self.y, self.x) self._frame:immedok(true) self._pad = C.newpad(self._height, self._width) self._pad:scrollok(true) self:set_active(false) self.chstrs = { } for i = 1, #self.columns[1] do self.chstrs[i] = C.new_chstr(self._width) self:setup_chstr(i) end self.dirty = true 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 NumberedPad do local _class_0 local _parent_0 = Pad local _base_0 = { } _base_0.__index = _base_0 setmetatable(_base_0, _parent_0.__base) _class_0 = setmetatable({ __init = function(self, label, y, x, height, width, ...) self.label, self.y, self.x = label, y, x local col1 = select(1, ...) local fmt = "%" .. tostring(#tostring(#col1)) .. "d" local line_nums do local _accum_0 = { } local _len_0 = 1 for i = 1, #col1 do _accum_0[_len_0] = fmt:format(i) _len_0 = _len_0 + 1 end line_nums = _accum_0 end local cols = { line_nums, (function(self, i) return i == self.selected and Color() or Color("yellow") end), ... } return _class_0.__parent.__init(self, self.label, self.y, self.x, height, width, unpack(cols)) end, __base = _base_0, __name = "NumberedPad", __parent = _parent_0 }, { __index = function(cls, name) local val = rawget(_base_0, name) if val == nil then local parent = rawget(cls, "__parent") if parent then return parent[name] end else return val end end, __call = function(cls, ...) local _self_0 = setmetatable({}, _base_0) cls.__init(_self_0, ...) return _self_0 end }) _base_0.__class = _class_0 if _parent_0.__inherited then _parent_0.__inherited(_parent_0, _class_0) end NumberedPad = _class_0 end local expansions = { } local TOP_LOCATION, KEY, VALUE = { }, { }, { } local locations = { } local Location Location = function(old_loc, kind, key) if old_loc == nil then return TOP_LOCATION end if not (locations[old_loc]) then locations[old_loc] = { } end if not (locations[old_loc][kind]) then locations[old_loc][kind] = { } end if not (locations[old_loc][kind][key]) then locations[old_loc][kind][key] = { old_loc = old_loc, kind = kind, key = key } end return locations[old_loc][kind][key] end local expand expand = function(kind, key, location) expansions[Location(location, kind, key)] = true end local collapse collapse = function(kind, key, location) expansions[Location(location, kind, key)] = nil end local is_key_expanded is_key_expanded = function(location, key) return expansions[Location(location, KEY, key)] end local is_value_expanded is_value_expanded = function(location, key) return expansions[Location(location, VALUE, key)] end local TYPE_COLORS = setmetatable({ }, { __index = 0 }) local colored_repr colored_repr = function(x, width, depth) if depth == nil then depth = 2 end depth = depth - 1 local x_type = type(x) if x_type == 'table' then if next(x) == nil then return { "{}", TYPE_COLORS.table } end if depth == 0 then return { "{", TYPE_COLORS.table, "...", Color('white'), "}", TYPE_COLORS.table } end local ret = { "{", TYPE_COLORS.table } local i = 1 for k, v in pairs(x) do if k == i then local _list_0 = colored_repr(x[i], width, depth) for _index_0 = 1, #_list_0 do local s = _list_0[_index_0] ret[#ret + 1] = s end i = i + 1 else local _list_0 = colored_repr(k, width, depth) for _index_0 = 1, #_list_0 do local s = _list_0[_index_0] ret[#ret + 1] = s end ret[#ret + 1] = ' = ' ret[#ret + 1] = Color('white') local _list_1 = colored_repr(v, width, depth) for _index_0 = 1, #_list_1 do local s = _list_1[_index_0] ret[#ret + 1] = s end end ret[#ret + 1] = ', ' ret[#ret + 1] = Color('white') end if #ret > 2 then ret[#ret] = nil ret[#ret] = nil end local len = 0 for i = 1, #ret - 1, 2 do len = len + #ret[i] end for i = #ret - 1, 3, -2 do if len <= width - 1 then break end if ret[i + 2] then ret[i + 2], ret[i + 3] = nil, nil end ret[i] = '...' ret[i + 1] = Color('white') end ret[#ret + 1] = '}' ret[#ret + 1] = TYPE_COLORS.table return ret elseif x_type == 'string' then local ret = { (x:match('^[^\t\r\v\b\a\n]*')), TYPE_COLORS.string } for escape, line in x:gmatch('([\t\r\v\b\a\n])([^\t\r\v\b\a\n]*)') do ret[#ret + 1] = '\\' .. ({ ['\t'] = 't', ['\r'] = 'r', ['\v'] = 'v', ['\b'] = 'b', ['\a'] = 'a', ['\n'] = 'n' })[escape] ret[#ret + 1] = Color('white on black') ret[#ret + 1] = line ret[#ret + 1] = TYPE_COLORS.string end local len = 0 for i = 1, #ret - 1, 2 do len = len + #ret[i] end for i = #ret - 1, 1, -2 do if len <= width then break end if ret[i + 2] then ret[i + 2], ret[i + 3] = nil, nil end len = len - #ret[i] if len <= width then ret[i] = ret[i]:sub(1, width - len - 3) ret[i + 2] = '...' ret[i + 3] = Color('blue') break end end return ret else local ok, s = pcall(tostring, x) if not ok then return { "tostring error: " .. s, Color("red") } end if #s > width then return { s:sub(1, width - 3), TYPE_COLORS[type(x)], '...', Color('blue') } else return { s, TYPE_COLORS[type(x)] } end end end local make_lines make_lines = function(location, x, width) local _exp_0 = type(x) if 'string' == _exp_0 then local lines = { } local _list_0 = line_matcher:match(x) for _index_0 = 1, #_list_0 do local line = _list_0[_index_0] local wrapped = wrap_text(line, width - 1) for i, subline in ipairs(wrapped) do local _line = { location = location } if i > 1 then table.insert(_line, C.ACS_BULLET) table.insert(_line, Color('black bold altcharset')) end table.insert(_line, subline) table.insert(_line, Color('blue on black')) table.insert(lines, _line) end end if #lines == 0 then table.insert(lines, { location = location, "''", Color('blue') }) end return lines elseif 'table' == _exp_0 then local prepend prepend = function(line, ...) for i = 1, select('#', ...) do table.insert(line, i, (select(i, ...))) end end local lines = { } for k, v in pairs(x) do if is_key_expanded(location, k) and is_value_expanded(location, k) then table.insert(lines, { location = Location(location, KEY, k), 'key', Color('green bold'), '/', Color(), 'value', Color('blue bold'), ':', Color('white') }) local key_lines = make_lines(Location(location, KEY, k), k, width - 1) for i, key_line in ipairs(key_lines) do if i == 1 then prepend(key_line, ' ', Color(), C.ACS_DIAMOND, Color('green bold'), ' ', Color()) else prepend(key_line, ' ', Color()) end table.insert(lines, key_line) end local value_lines = make_lines(Location(location, VALUE, k), v, width - 2) for i, value_line in ipairs(value_lines) do if i == 1 then prepend(value_line, ' ', Color(), C.ACS_DIAMOND, Color('blue bold'), ' ', Color()) else prepend(value_line, ' ', Color()) end table.insert(lines, value_line) end elseif is_value_expanded(location, k) then local k_str = colored_repr(k, width - 1) table.insert(lines, { location = Location(location, KEY, k), '-', Color('red'), unpack(k_str) }) local v_lines = make_lines(Location(location, VALUE, k), v, width - 1) prepend(v_lines[1], ' ', Color()) for i = 2, #v_lines do prepend(v_lines[i], ' ', Color()) end for _index_0 = 1, #v_lines do local v_line = v_lines[_index_0] table.insert(lines, v_line) end elseif is_key_expanded(location, k) then local k_lines = make_lines(Location(location, KEY, k), k, width - 4) for i = 1, #k_lines do prepend(k_lines[i], ' ', Color()) end for _index_0 = 1, #k_lines do local k_line = k_lines[_index_0] table.insert(lines, k_line) end local v_str = colored_repr(v, width - 2) table.insert(lines, { location = Location(location, VALUE, k), ' ', Color(), unpack(v_str) }) else local k_space = math.floor((width - 4) / 3) local k_str = colored_repr(k, k_space) local v_space = (width - 4) - #k_str local v_str = colored_repr(v, v_space) local line = { location = Location(location, VALUE, k), '+', Color('green'), unpack(k_str) } table.insert(line, ' = ') table.insert(line, Color('white')) for _index_0 = 1, #v_str do local s = v_str[_index_0] table.insert(line, s) end table.insert(lines, line) end end if #lines == 0 then table.insert(lines, { location = location, '{}', TYPE_COLORS.table }) end return lines else if getmetatable(x) and getmetatable(x).__pairs then local lines = make_lines(location, (function() local _tbl_0 = { } for k, v in pairs(x) do _tbl_0[k] = v end return _tbl_0 end)(), width) if getmetatable(x).__tostring then local s_lines = { } local ok, s = pcall(tostring, x) if not ok then s = "tostring error: " .. s end local _list_0 = line_matcher:match(s) for _index_0 = 1, #_list_0 do local line = _list_0[_index_0] local wrapped = wrap_text(line, width) for i, subline in ipairs(wrapped) do table.insert(s_lines, { location = location, subline, ok and Color('yellow') or Color('red') }) end end for i = 1, #s_lines do table.insert(lines, i, s_lines[i]) end end return lines end local str = tostring(x) if #str > width then str = str:sub(1, width - 3) .. '...' end return { { location = location, str, TYPE_COLORS[type(x)] } } end end local DataViewer do local _class_0 local _parent_0 = Pad local _base_0 = { setup_chstr = function(self, i) end, configure_size = function(self, height, width) self.height, self.width = height, width self._height, self._width = #self.chstrs, self.width - 2 end, select = function(self, i) if #self.chstrs == 0 then i = nil end if i == self.selected then return self.selected end local old_y, old_x = self.scroll_y, self.scroll_x if i ~= nil then i = math.max(1, math.min(#self.chstrs, i)) end local old_selected old_selected, self.selected = self.selected, i if old_selected and self.chstrs[old_selected] then self.chstrs[old_selected]:set_str(0, ' ', Color('yellow bold')) self._pad:mvaddchstr(old_selected - 1, 0, self.chstrs[old_selected]) end if self.selected then self.chstrs[self.selected]:set_ch(0, C.ACS_RARROW, Color('yellow bold')) self._pad:mvaddchstr(self.selected - 1, 0, self.chstrs[self.selected]) local scrolloff = 3 if self.selected > self.scroll_y + (self.height - 2) - scrolloff then self.scroll_y = self.selected - (self.height - 2) + scrolloff elseif self.selected < self.scroll_y + scrolloff then self.scroll_y = self.selected - scrolloff end self.scroll_y = math.max(1, math.min(self._height, self.scroll_y)) end if self.scroll_y == old_y then local w = math.min(self.width - 2, self._width) if old_selected and self.scroll_y <= old_selected and old_selected <= self.scroll_y + self.height - 2 then self._pad:pnoutrefresh(old_selected - 1, self.scroll_x - 1, self.y + 1 + (old_selected - self.scroll_y), self.x + 1, self.y + 1 + (old_selected - self.scroll_y) + 1, self.x + w) end if self.selected and self.scroll_y <= self.selected and self.selected <= self.scroll_y + self.height - 2 then self._pad:pnoutrefresh(self.selected - 1, self.scroll_x - 1, self.y + 1 + (self.selected - self.scroll_y), self.x + 1, self.y + 1 + (self.selected - self.scroll_y) + 1, self.x + w) end else self.dirty = true end if self.on_select then self:on_select(self.selected) end return self.selected end, keypress = function(self, c) local _exp_0 = c if C.KEY_DOWN == _exp_0 or C.KEY_SR == _exp_0 or ("j"):byte() == _exp_0 then return self:scroll(1, 0) elseif ('J'):byte() == _exp_0 then return self:scroll(10, 0) elseif C.KEY_UP == _exp_0 or C.KEY_SF == _exp_0 or ("k"):byte() == _exp_0 then return self:scroll(-1, 0) elseif ('K'):byte() == _exp_0 then return self:scroll(-10, 0) elseif C.KEY_RIGHT == _exp_0 or ("l"):byte() == _exp_0 then expansions[self.chstr_locations[self.selected]] = true return self:full_refresh() elseif ("L"):byte() == _exp_0 then expansions[self.chstr_locations[self.selected]] = true return self:full_refresh() elseif C.KEY_LEFT == _exp_0 or ("h"):byte() == _exp_0 then local loc = self.chstr_locations[self.selected] if expansions[loc] == nil then loc = Location(loc.old_loc, (loc.kind == KEY and VALUE or KEY), loc.key) end while loc and expansions[loc] == nil do loc = loc.old_loc end if loc then expansions[loc] = nil end self:full_refresh() if loc and self.chstr_locations[self.selected] ~= loc then for i, chstr_loc in ipairs(self.chstr_locations) do if chstr_loc == loc then self:select(i) break end end elseif not loc then return self:select(1) end elseif ("H"):byte() == _exp_0 then local loc = self.chstr_locations[self.selected] if expansions[loc] == nil then loc = Location(loc.old_loc, (loc.kind == KEY and VALUE or KEY), loc.key) end while loc and expansions[loc] == nil do loc = loc.old_loc end if loc then expansions[loc] = nil end self:full_refresh() if loc and self.chstr_locations[self.selected] ~= loc then for i, chstr_loc in ipairs(self.chstr_locations) do if chstr_loc == loc then self:select(i) break end end elseif not loc then return self:select(1) end end end } _base_0.__index = _base_0 setmetatable(_base_0, _parent_0.__base) _class_0 = setmetatable({ __init = function(self, data, label, y, x, height, width) self.data, self.label, self.y, self.x = data, label, y, x self.scroll_y, self.scroll_x = 1, 1 self.selected = nil self.active_frame = Color("yellow bold") self.inactive_frame = Color("blue dim") self.full_refresh = function() local old_location = self.selected and self.chstr_locations and self.chstr_locations[self.selected] self.chstrs, self.chstr_locations = { }, { } local W = width - 3 local lines = make_lines(TOP_LOCATION, self.data, W) for i, line in ipairs(lines) do local chstr = C.new_chstr(W) if i == self.selected then chstr:set_ch(0, C.ACS_RARROW, Color('yellow bold')) else chstr:set_str(0, ' ', Color('yellow bold')) end local offset = 1 for j = 1, #line - 1, 2 do local chunk, attrs = line[j], line[j + 1] if type(chunk) == 'number' then chstr:set_ch(offset, chunk, attrs) offset = offset + 1 else chstr:set_str(offset, chunk, attrs) offset = offset + #chunk end end if offset < W then chstr:set_str(offset, ' ', attrs, W - offset) end table.insert(self.chstrs, chstr) table.insert(self.chstr_locations, line.location) end self._height, self._width = #self.chstrs, self.width - 2 self._pad:resize(self._height, self._width) for i, chstr in ipairs(self.chstrs) do self._pad:mvaddchstr(i - 1, 0, chstr) end self.dirty = true if old_location then for i, loc in ipairs(self.chstr_locations) do if loc == old_location then self:select(i) break end end end end self.height, self.width = height, width self._frame = C.newwin(self.height, self.width, self.y, self.x) self._frame:immedok(true) self._pad = C.newpad(self.height - 2, self.width - 2) self._pad:scrollok(true) self:set_active(false) self:full_refresh() return self:select(1) end, __base = _base_0, __name = "DataViewer", __parent = _parent_0 }, { __index = function(cls, name) local val = rawget(_base_0, name) if val == nil then local parent = rawget(cls, "__parent") if parent then return parent[name] end else return val end end, __call = function(cls, ...) local _self_0 = setmetatable({}, _base_0) cls.__init(_self_0, ...) return _self_0 end }) _base_0.__class = _class_0 if _parent_0.__inherited then _parent_0.__inherited(_parent_0, _class_0) end DataViewer = _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"):sub(1, -2) self[filename] = contents return contents end }) local line_tables = setmetatable({ }, { __index = function(self, filename) local file = file_cache[filename] if not file then return nil end local line_table ok, line_table = to_lua(file) if ok then self[filename] = line_table return line_table end end }) local err_hand err_hand = function(err) C.endwin() print("Error in debugger.") print(debug.traceback(err, 2)) return os.exit(2) end ldb = { run_debugger = function(err_msg) local select_pad err_msg = err_msg or '' if type(err_msg) ~= 'string' then err_msg = tostring(err_msg) end local stdscr = C.initscr() local 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() do TYPE_COLORS.string = Color('blue on black') TYPE_COLORS.number = Color('magenta') TYPE_COLORS.boolean = Color('cyan') TYPE_COLORS["nil"] = Color('cyan') TYPE_COLORS.table = Color('yellow') TYPE_COLORS["function"] = Color('green') TYPE_COLORS.userdata = Color('cyan bold') TYPE_COLORS.thread = Color('blue') end do stdscr:wbkgd(Color("yellow on red bold")) stdscr:clear() stdscr:refresh() local lines = wrap_text("ERROR!\n \n " .. err_msg .. "\n \npress any key...", math.floor(SCREEN_W - 2)) local max_line = 0 for _index_0 = 1, #lines do local line = lines[_index_0] max_line = math.max(max_line, #line) end for i, line in ipairs(lines) do if i == 1 or i == #lines then stdscr:mvaddstr(math.floor(SCREEN_H / 2 - #lines / 2) + i, math.floor((SCREEN_W - #line) / 2), line) else stdscr:mvaddstr(math.floor(SCREEN_H / 2 - #lines / 2) + i, math.floor((SCREEN_W - max_line) / 2), line) end end stdscr:refresh() C.doupdate() stdscr:getch() end stdscr:keypad() stdscr:wbkgd(Color()) stdscr:clear() stdscr:refresh() local pads = { } do local err_msg_lines = wrap_text(err_msg, SCREEN_W - 4) for i, line in ipairs(err_msg_lines) do err_msg_lines[i] = (" "):rep(2) .. line end local height = math.min(#err_msg_lines + 2, 7) pads.err = Pad("(E)rror Message", 0, 0, height, SCREEN_W, err_msg_lines, function(self, i) return Color("red bold") end) end local err_lines = { } local stack_sources = { } local stack_locations = { } local watch_exprs = setmetatable({ }, { __index = function(self, k) local t = { } self[k] = t return t end }) do local stack_names = { } local max_filename, max_fn_name = 0, 0 local stack_min, stack_max = callstack_range() for i = stack_min, stack_max do local info = debug.getinfo(i) if not info then break end local fn_name = info.name if not (fn_name) then if info.istailcall then fn_name = "" else fn_name = "" end end table.insert(stack_names, fn_name) local line if info.short_src then local line_table = line_tables[info.short_src] if line_table then local char = line_table[info.currentline] local line_num = 1 local file = file_cache[info.short_src] or info.source 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 else line = "???" end err_lines[line] = true table.insert(stack_locations, line) table.insert(stack_sources, info.source) max_filename = math.max(max_filename, #line) max_fn_name = math.max(max_fn_name, #fn_name) end max_fn_name, max_filename = 0, 0 for i = 1, #stack_names do max_fn_name = math.max(max_fn_name, #stack_names[i]) max_filename = math.max(max_filename, #stack_locations[i]) end local stack_h = math.floor(SCREEN_H * .6) local stack_w = math.min(max_fn_name + 3 + max_filename, math.floor(1 / 3 * SCREEN_W)) pads.stack = Pad("(C)allstack", pads.err.height, SCREEN_W - stack_w, stack_h, stack_w, stack_names, (function(self, i) return (i == self.selected) and Color("black on green") or Color("green bold") end), stack_locations, (function(self, i) return (i == self.selected) and Color("black on cyan") or Color("cyan bold") end)) end local show_src show_src = function(filename, line_no, file_contents) if file_contents == nil then file_contents = nil end if pads.src then if pads.src.filename == filename then pads.src:select(line_no) pads.src.colors[2] = function(self, i) if i == line_no and i == self.selected then return Color("yellow on red bold") elseif i == line_no then return Color("yellow on red") elseif err_lines[tostring(filename) .. ":" .. tostring(i)] == true then return Color("red on black bold") elseif i == self.selected then return Color("reverse") else return Color() end end for line, _ in pairs(err_lines) do local _filename, i = line:match("([^:]*):(%d*).*") if _filename == filename and tonumber(i) then pads.src:setup_chstr(tonumber(i)) end end pads.src:select(line_no) return else pads.src:erase() end end file_contents = file_contents or file_cache[filename] if file_contents then local src_lines = line_matcher:match(file_contents) pads.src = NumberedPad("(S)ource Code", pads.err.height, 0, pads.stack.height, pads.stack.x, src_lines, function(self, i) if i == line_no and i == self.selected then return Color("yellow on red bold") elseif i == line_no then return Color("yellow on red") elseif err_lines[tostring(filename) .. ":" .. tostring(i)] == true then return Color("red on black bold") elseif i == self.selected then return Color("reverse") else return Color() end end) pads.src:select(line_no) else local lines = { } for i = 1, math.floor(pads.stack.height / 2) - 1 do table.insert(lines, "") end local s = "" s = (" "):rep(math.floor((pads.stack.x - 2 - #s) / 2)) .. s table.insert(lines, s) pads.src = Pad("(S)ource Code", pads.err.height, 0, pads.stack.height, pads.stack.x, lines, function() return Color("red") end) end pads.src.filename = filename end local stack_env local show_vars show_vars = function(stack_index) if pads.vars then pads.vars:erase() end if pads.data then pads.data:erase() end local callstack_min, _ = callstack_range() local var_names, values = { }, { } stack_env = setmetatable({ }, { __index = _G }) for loc = 1, 999 do local name, value = debug.getlocal(callstack_min + stack_index - 1, loc) if name == nil then break end table.insert(var_names, tostring(name)) table.insert(values, value) stack_env[name] = value end local num_locals = #var_names local info = debug.getinfo(callstack_min + stack_index - 1, "uf") for upval = 1, info.nups do local _continue_0 = false repeat local name, value = debug.getupvalue(info.func, upval) if name == "_ENV" then _continue_0 = true break end table.insert(var_names, tostring(name)) table.insert(values, value) stack_env[name] = value _continue_0 = true until true if not _continue_0 then break end end local _list_0 = watch_exprs[stack_index] for _index_0 = 1, #_list_0 do local _continue_0 = false repeat local watch = _list_0[_index_0] if stack_env[watch.expr] ~= nil then _continue_0 = true break end table.insert(var_names, watch.expr) table.insert(values, watch.value) stack_env[watch.expr] = watch.value _continue_0 = true until true if not _continue_0 then break end end local var_y = pads.stack.y + pads.stack.height local var_x = 0 local height = SCREEN_H - (pads.err.height + pads.stack.height) pads.vars = Pad("(V)ars", var_y, var_x, height, AUTO, var_names, function(self, i) local color if i <= num_locals then color = Color() elseif i <= num_locals + info.nups - 1 then color = Color("blue") else color = Color("green") end if i == self.selected then color = color + C.A_REVERSE end return color end) pads.vars.keypress = function(self, key) if key == ('l'):byte() or key == C.KEY_RIGHT then return select_pad(pads.data) else return Pad.keypress(self, key) end end pads.vars.on_select = function(self, var_index) if var_index == nil then return end local value_x = pads.vars.x + pads.vars.width local value_w = SCREEN_W - (value_x) local value = stack_env[var_names[var_index]] local type_str = tostring(type(value)) pads.data = DataViewer(value, "(D)ata [" .. tostring(type_str) .. "]", var_y, value_x, pads.vars.height, value_w) pads.data.keypress = function(self, key) if (key == ('h'):byte() or key == C.KEY_LEFT) and self.selected == 1 then return select_pad(pads.vars) else return DataViewer.keypress(self, key) end end collectgarbage() return collectgarbage() end return pads.vars:select(1) end pads.stack.on_select = function(self, stack_index) local filename, line_no = pads.stack.columns[2][stack_index]:match("^(.*):(%d*)$") line_no = tonumber(line_no) show_src(filename, line_no, filename and file_cache[filename] or stack_sources[stack_index]) return show_vars(stack_index) end pads.stack:select(1) local selected_pad = nil select_pad = function(pad) if selected_pad ~= pad then if selected_pad then selected_pad:set_active(false) selected_pad:refresh() end selected_pad = pad if selected_pad then selected_pad:set_active(true) return selected_pad:refresh() end end end select_pad(pads.stack) while true do for _, p in pairs(pads) do p:refresh() end local s = " press 'q' to quit " stdscr:mvaddstr(math.floor(SCREEN_H - 1), math.floor((SCREEN_W - #s)), s) local c = stdscr:getch() local _exp_0 = c if (':'):byte() == _exp_0 or ('>'):byte() == _exp_0 or ('?'):byte() == _exp_0 then C.echo(true) local print_nil = false local user_input local code = '' if c == ('?'):byte() then stdscr:mvaddstr(SCREEN_H - 1, 0, "? " .. (' '):rep(SCREEN_W - 1)) stdscr:move(SCREEN_H - 1, 2) user_input = stdscr:getstr() code = 'return ' .. user_input print_nil = true elseif c == (':'):byte() or c == ('>'):byte() then local numlines = 1 stdscr:mvaddstr(SCREEN_H - 1, 0, "> " .. (' '):rep(SCREEN_W - 1)) stdscr:move(SCREEN_H - 1, 2) while true do local line = stdscr:getstr() if line == '' then break end code = code .. (line .. '\n') numlines = numlines + 1 stdscr:mvaddstr(SCREEN_H - numlines, 0, "> " .. ((' '):rep(SCREEN_W) .. '\n'):rep(numlines)) stdscr:mvaddstr(SCREEN_H - numlines, 2, code) stdscr:mvaddstr(SCREEN_H - 1, 0, (' '):rep(SCREEN_W)) stdscr:move(SCREEN_H - 1, 0) end end C.echo(false) local output = "" if not stack_env then stack_env = setmetatable({ }, { __index = _G }) end stack_env.print = function(...) for i = 1, select('#', ...) do if i > 1 then output = output .. '\t' end output = output .. tostring(select(i, ...)) end output = output .. "\n" end for _, p in pairs(pads) do p:refresh(true) end local run_fn run_fn, err_msg = load(code, 'user input', 't', stack_env) if not run_fn then stdscr:attrset(Color('red bold')) stdscr:addstr(err_msg) stdscr:attrset(Color()) else local ret ok, ret = pcall(run_fn) if not ok then stdscr:attrset(Color('red bold')) stdscr:addstr(ret) stdscr:attrset(Color()) elseif ret ~= nil or print_nil then local value_bits = { '= ', Color('yellow'), unpack(colored_repr(ret, SCREEN_W - 2, 4)) } local numlines = 1 for i = 1, #value_bits - 1, 2 do for nl in value_bits[i]:gmatch('\n') do numlines = numlines + 1 end end for nl in output:gmatch('\n') do numlines = numlines + 1 end local y, x = SCREEN_H - numlines, 0 if output ~= "" then stdscr:mvaddstr(SCREEN_H - numlines, 0, output) for nl in output:gmatch('\n') do y = y + 1 end end for i = 1, #value_bits - 1, 2 do stdscr:attrset(value_bits[i + 1]) local first_line = value_bits[i]:match('^[^\n]*') stdscr:mvaddstr(y, x, first_line) x = x + #first_line for line in value_bits[i]:gmatch('\n([^\n]*)') do stdscr:mvaddstr(y, x, (' '):rep(SCREEN_W - x)) y = y + 1 x = 0 stdscr:mvaddstr(y, x, line) end end stdscr:attrset(Color()) stdscr:mvaddstr(y, x, (' '):rep(SCREEN_W - x)) if c == ("?"):byte() and ret ~= nil then local replacing = false local watch_index = nil local watches = watch_exprs[pads.stack.selected] for i, w in ipairs(watches) do if w.expr == user_input then w.value = ret watch_index = i break end end if not (watch_index) then table.insert(watches, { expr = user_input, value = ret }) watch_index = #watches end show_vars(pads.stack.selected) for i, s in ipairs(pads.vars.columns[1]) do if s == user_input then pads.vars:select(i) break end end select_pad(pads.data) end else local numlines = 0 for nl in output:gmatch('\n') do numlines = numlines + 1 end stdscr:mvaddstr(SCREEN_H - numlines, 0, output) end end elseif ('o'):byte() == _exp_0 then local file = stack_locations[pads.stack.selected] local filename, line_no = file:match("([^:]*):(.*)") line_no = tostring(pads.src.selected) C.endwin() os.execute((os.getenv("EDITOR") or "nano") .. " +" .. line_no .. " " .. filename) 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(true) end elseif C.KEY_RESIZE == _exp_0 then SCREEN_H, SCREEN_W = stdscr:getmaxyx() stdscr:clear() stdscr:refresh() for _, pad in pairs(pads) do pad:refresh(true) end C.doupdate() elseif ('q'):byte() == _exp_0 or ("Q"):byte() == _exp_0 then pads = { } C.endwin() return 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) elseif ('d'):byte() == _exp_0 then select_pad(pads.data) elseif ('e'):byte() == _exp_0 then select_pad(pads.err) elseif C.KEY_DC == _exp_0 or C.KEY_DL == _exp_0 or C.KEY_BACKSPACE == _exp_0 then if selected_pad == pads.vars then local watches = watch_exprs[pads.stack.selected] local expr = pads.vars.columns[1][pads.vars.selected] for i, w in ipairs(watches) do if w.expr == expr then table.remove(watches, i) show_vars(pads.stack.selected) select_pad(pads.vars) break end end end else if selected_pad then selected_pad:keypress(c) end end end return C.endwin() end, guard = function(fn, ...) local handler handler = function(err_msg) print(debug.traceback(err_msg, 2)) return xpcall(ldb.run_debugger, err_hand, err_msg) end return xpcall(fn, handler, ...) end, breakpoint = function() return xpcall(ldb.run_debugger, err_hand, "Breakpoint triggered!") end, hijack = function() error = function(err_msg) print(debug.traceback(err_msg, 2)) xpcall(ldb.run_debugger, err_hand, err_msg) return os.exit(2) end assert = function(condition, err_msg) if not condition then err_msg = err_msg or 'Assertion failed!' print(debug.traceback(err_msg, 2)) xpcall(ldb.run_debugger, err_hand, err_msg) os.exit(2) end return condition end end } return ldb