terminal based frontend for the mobdebug lua remote debugger
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

503 lines
11 KiB

  1. local tfx = require "termfx"
  2. local config
  3. local function resetconfig()
  4. config = {
  5. widget_fg = tfx.color.WHITE,
  6. widget_bg = tfx.color.BLUE,
  7. sel_fg = tfx.color.BLACK,
  8. sel_bg = tfx.color.CYAN,
  9. elem_fg = tfx.color.BLUE,
  10. elem_bg = tfx.color.WHITE,
  11. sep = '|'
  12. }
  13. end
  14. local function getconfig(n)
  15. return config[n]
  16. end
  17. -- helper
  18. -- augment for type that can recognize userdata with a named metatable
  19. local _type = type
  20. local type = function(v)
  21. local t = _type(v)
  22. if t == "userdata" or t == "table" then
  23. local mt = getmetatable(v)
  24. if mt then
  25. if mt.__type then
  26. if type(mt.__type) == "function" then
  27. return string.lower(mt.__type(t))
  28. else
  29. return string.lower(mt.__type)
  30. end
  31. elseif t == "userdata" then
  32. local reg = debug.getregistry()
  33. for k, v in pairs(reg) do
  34. if v == mt then
  35. return string.lower(k)
  36. end
  37. end
  38. end
  39. end
  40. end
  41. return t
  42. end
  43. -- api
  44. -- draw a frame for an ui element
  45. -- top left is x, y, dimensions are w, h, title is optional
  46. -- may resize frame if it leaves the screen somewhere.
  47. -- returns x, y, w, h of frame contents
  48. local function drawframe(x, y, w, h, title)
  49. local tw, th = tfx.size()
  50. local pw = 0
  51. if title then
  52. title = tostring(title)
  53. pw = #title
  54. if w < #title then w = #title end
  55. end
  56. if x < 2 then x = 2 end
  57. if y < 2 then y = 2 end
  58. if x + w >= tw then w = tw - x end
  59. if y + h >= th then h = th - y end
  60. local ccell = tfx.newcell('+')
  61. local hcell = tfx.newcell('-')
  62. local vcell = tfx.newcell('|')
  63. for i = x, x+w do
  64. tfx.setcell(i, y-1, hcell)
  65. tfx.setcell(i, y+h, hcell)
  66. end
  67. for i = y, y+h do
  68. tfx.setcell(x-1, i, vcell)
  69. tfx.setcell(x+w, i, vcell)
  70. end
  71. tfx.setcell(x-1, y-1, ccell)
  72. tfx.setcell(x-1, y+h, ccell)
  73. tfx.setcell(x+w, y-1, ccell)
  74. tfx.setcell(x+w, y+h, ccell)
  75. tfx.rect(x, y, w, h, ' ')
  76. if title then
  77. if w < pw then pw = w end
  78. tfx.printat(math.floor(x + (w - pw) / 2), y - 1, title, pw)
  79. end
  80. return x, y, w, h
  81. end
  82. -- helper
  83. -- draw a frame of width w and height h, centered on the screen
  84. -- title is optional
  85. local function frame(w, h, title)
  86. local tw, th = tfx.size()
  87. if w + 2 > tw then w = tw - 2 end
  88. if h + 2 > th then h = th - 2 end
  89. local x = math.floor((tw - w) / 2) + 1
  90. local y = math.floor((th - h) / 2) + 1
  91. return drawframe(x, y, w, h, title)
  92. end
  93. -- helper
  94. -- format a string to fit a certain width. Returns a table with the lines
  95. local function format(msg, w)
  96. if not w or w >= #msg then return { msg } end
  97. local pos, last = 1, #msg
  98. local res = {}
  99. repeat
  100. res[#res+1] = string.sub(msg, pos, pos + w - 1)
  101. pos = pos + w
  102. until pos > last
  103. return res
  104. end
  105. -- helper
  106. -- returns true if evt contains a keypress for what is considered an
  107. -- escape key, one that closes the current window. This can be forced to
  108. -- only be escape, or to also include enter.
  109. -- return true if evt contains an escape key press, false if not.
  110. local function is_escape_key(evt, onlyesc)
  111. if not evt then return false end
  112. if evt.key == tfx.key.ESC then
  113. return true
  114. end
  115. if not onlyesc and evt.key == tfx.key.ENTER then
  116. return true
  117. end
  118. return false
  119. end
  120. -- helper
  121. -- waits for an event that is a keypress, then returns.
  122. local function waitkeypress()
  123. local evt
  124. repeat
  125. evt = tfx.pollevent()
  126. until evt and evt.type == 'key'
  127. return evt.char or evt.key
  128. end
  129. -- helper
  130. -- draw a simple string s at pos x, y, width w, filling the rest between
  131. -- #s and w with f or blanks
  132. -- returns true
  133. local function drawfield(x, y, s, w, f)
  134. f = f or ' '
  135. s = tostring(s)
  136. tfx.printat(x, y, s, w)
  137. if #s < w then
  138. tfx.printat(x + #s, y, string.rep(f, w - #s))
  139. end
  140. return true
  141. end
  142. -- api
  143. -- draw a list of rows contained in tbl at position x, y, size w, h.
  144. -- first is first line to show, may be modified. renderrow, if present,
  145. -- is a function to render an individual row, which defaults to a simple
  146. -- function calling drawfield(). The functions signature is
  147. -- renderrow(row, s, x, y, w, extra)
  148. -- where row is the row number, s is the string, x, y is the position,
  149. -- w is the width and extra is what was passed to drawlist as rr_extra
  150. -- default renderrow function:
  151. local function default_renderrow(row, s, x, y, w, extra)
  152. if s then
  153. drawfield(x, y, tostring(s), w)
  154. end
  155. end
  156. local function drawlist(tbl, first, x, y, w, h, renderrow, rr_extra)
  157. local fg, bg = tfx.attributes()
  158. local tw, th = tfx.size()
  159. local sx, sy
  160. local fo, bo, hl
  161. local ntbl = #tbl
  162. if ntbl == 0 then return end
  163. renderrow = renderrow or default_renderrow
  164. if first < 1 then
  165. first = 1
  166. end
  167. if ntbl >= h then
  168. w = w - 1
  169. sx = x + w
  170. sy = y
  171. if ntbl - first + 1 <= h then
  172. first = ntbl - h + 1
  173. if first < 1 then
  174. first = 1
  175. h = ntbl < h and ntbl or h
  176. end
  177. end
  178. end
  179. if w < 1 or h < 1 or x > tw or y > th or x + w < 1 or y + h < 1 then
  180. return false
  181. end
  182. -- contents
  183. first = first - 1
  184. for i=1, h do
  185. local s = tbl[first + i]
  186. tfx.attributes(fg, bg)
  187. renderrow(first + i, s, x, y, w, rr_extra)
  188. y = y + 1
  189. end
  190. -- scrollbar
  191. if ntbl > h then
  192. local sh = math.floor(h * h / ntbl)
  193. local sf = math.floor(first * (h - sh) / (ntbl - h)) + sy
  194. if sf + sh > h then sf = h - sh + 1 end
  195. local sl = sf + sh
  196. for yy = sy, sy + h - 1 do
  197. if yy >= sf and yy <= sl then
  198. tfx.setcell(sx, yy, '#', config.elem_fg, config.elem_bg)
  199. else
  200. tfx.setcell(sx, yy, ' ', config.elem_fg, config.elem_bg)
  201. end
  202. end
  203. end
  204. return first + 1
  205. end
  206. ----- drawtext -----
  207. -- api
  208. -- draw some text. The argument table contains strings
  209. local function drawtext_renderrow(row, s, x, y, w, extra)
  210. if s then
  211. if extra.hr and row == extra.hr then
  212. tfx.attributes(config.sel_fg, config.sel_bg)
  213. end
  214. drawfield(x, y, s, w)
  215. end
  216. end
  217. local function drawtext(tbl, first, x, y, w, h, hr)
  218. local extra = { hr = hr }
  219. return drawlist(tbl, first, x, y, w, h, drawtext_renderrow, extra)
  220. end
  221. ----- text -----
  222. -- api widget
  223. -- show lines of text contained in tbl
  224. local function text(tbl, title)
  225. local first = 1
  226. local th = #tbl
  227. local w, h = 0, th
  228. local x, y
  229. local quit = false
  230. local evt
  231. for i = 1, #tbl do
  232. local lw = #tbl[i]
  233. if lw > w then w = lw end
  234. end
  235. tfx.attributes(config.widget_fg, config.widget_bg)
  236. x, y, w, h = frame(w, th, title)
  237. if th > h then
  238. x, y, w, h = frame(w+1, th, title)
  239. end
  240. repeat
  241. x, y, w, h = frame(w, h, title)
  242. tfx.attributes(config.widget_fg, config.widget_bg)
  243. first = drawtext(tbl, first, x, y, w, h)
  244. tfx.present()
  245. evt = tfx.pollevent()
  246. if evt.key == tfx.key.ARROW_UP then
  247. first = first - 1
  248. elseif evt.key == tfx.key.ARROW_DOWN then
  249. first = first + 1
  250. elseif evt.key == tfx.key.PGUP then
  251. first = first - h
  252. elseif evt.key == tfx.key.PGDN then
  253. first = first + h
  254. elseif is_escape_key(evt) then
  255. quit = true
  256. end
  257. until quit
  258. end
  259. -- api widget
  260. -- ask the user something, providing a table of buttons for answers.
  261. -- Default is { "Yes", "No" }
  262. -- Returns the number and the text of the selected button, or nil on abort
  263. local function ask(msg, btns, title)
  264. local sel = 1
  265. btns = btns or { "Yes", "No" }
  266. tfx.attributes(config.widget_fg, config.widget_bg)
  267. local bw = #btns[1]
  268. for i = 2, #btns do
  269. bw = bw + 1 + #btns[i]
  270. end
  271. local tw = tfx.width()
  272. local ma = format(msg, tw / 2)
  273. local mw = bw
  274. for i = 1, #ma do
  275. if #ma[i] > mw then mw = #ma[i] end
  276. end
  277. local x, y, w, h = frame(mw, #ma+1, title)
  278. drawlist(ma, 1, x, y, w, h)
  279. local evt
  280. repeat
  281. local bp = math.floor(w - bw) / 2
  282. if bp < 1 then bp = 1 end
  283. local bw = w - bp + 1
  284. for i = 1, #btns do
  285. if i == sel then
  286. tfx.attributes(config.sel_fg, config.sel_bg)
  287. else
  288. tfx.attributes(config.elem_fg, config.elem_bg)
  289. end
  290. tfx.printat(x - 1 + bp, y + #ma, btns[i], bw - bp + 1)
  291. bp = bp + 1 + #btns[i]
  292. if bp > bw then break end
  293. end
  294. tfx.present()
  295. evt = tfx.pollevent()
  296. if evt then
  297. if evt.key == tfx.key.ENTER then
  298. return sel
  299. elseif evt.key == tfx.key.TAB or evt.key == tfx.key.ARROW_RIGHT then
  300. sel = sel < #btns and sel + 1 or 1
  301. elseif evt.key == tfx.key.ARROW_LEFT then
  302. sel = sel > 1 and sel - 1 or #btns
  303. end
  304. end
  305. until is_escape_key(evt, true)
  306. return nil
  307. end
  308. -- api
  309. -- input a single value
  310. local function drawvalue(t, f, x, y, w)
  311. local m = #t
  312. if f + w - 1 >= m then m = f + w - 1 end
  313. for i = f, m do
  314. if i - f < w then
  315. local ch = t[i] or '_'
  316. tfx.setcell(x + i - f, y, ch)
  317. end
  318. end
  319. end
  320. local function input(x, y, w, orig)
  321. local f = 1
  322. local pos = 1
  323. local res = {}
  324. if orig then
  325. string.gsub(tostring(orig), "(.)", function(c) res[#res+1] = c end)
  326. pos = #res + 1
  327. end
  328. local evt
  329. repeat
  330. if pos - f >= w then
  331. f = pos - w + 1
  332. elseif pos < f then
  333. f = pos
  334. end
  335. drawvalue(res, f, x, y, w)
  336. tfx.setcursor(x + pos - f, y)
  337. tfx.present()
  338. evt = tfx.pollevent()
  339. local ch = evt.char
  340. if evt.key == tfx.key.SPACE then ch = " " end
  341. if ch >= ' ' then
  342. table.insert(res, pos, ch)
  343. pos = pos + 1
  344. elseif (evt.key == tfx.key.BACKSPACE or evt.key == tfx.key.BACKSPACE2) and pos > 1 then
  345. table.remove(res, pos-1)
  346. pos = pos - 1
  347. elseif evt.key == tfx.key.DELETE and pos <= #res then
  348. table.remove(res, pos)
  349. if pos > #res and pos > 1 then pos = pos - 1 end
  350. elseif evt.key == tfx.key.ARROW_LEFT and pos > 1 then
  351. pos = pos - 1
  352. elseif evt.key == tfx.key.ARROW_RIGHT and pos <= #res then
  353. pos = pos + 1
  354. elseif evt.key == tfx.key.HOME then
  355. pos = 1
  356. elseif evt.key == tfx.key.END then
  357. pos = #res + 1
  358. elseif evt.key == tfx.key.ESC then
  359. return nil
  360. end
  361. until is_escape_key(evt) or evt.key == tfx.key.TAB or evt.key == tfx.key.ARROW_UP or evt.key == tfx.key.ARROW_DOWN
  362. tfx.hidecursor()
  363. return table.concat(res), evt.key
  364. end
  365. -- api
  366. -- draw a status bar. The last element in tbl is always right aligned,
  367. -- the rest is left aligned.
  368. local function drawstatus(tbl, y, w, sep)
  369. sep = sep or config.sep
  370. tfx.attributes(config.elem_fg, config.elem_bg)
  371. local w = tfx.width()
  372. local tw = 1
  373. for i=2, #tbl - 1 do
  374. tw = tw + #sep + #tbl[i]
  375. end
  376. tfx.printat(1, y, string.rep(' ', w))
  377. tfx.printat(1, y, tbl[1])
  378. local p = #tbl[1] + 1
  379. for i = 2, #tbl - 1 do
  380. tfx.printat(p, y, sep)
  381. tfx.printat(p + #sep, y, tbl[i])
  382. p = p + #tbl[i] + #sep
  383. end
  384. tfx.setcell(w - #tbl[#tbl], y, ' ')
  385. tfx.printat(w + 1 - #tbl[#tbl], y, tbl[#tbl])
  386. end
  387. -- api
  388. -- change config options for the ui lib. see resetconfig() above for options
  389. local function configure(tbl)
  390. for k, v in pairs(tbl) do
  391. if config[k] then
  392. if type(v) == type(config[k]) then
  393. config[k] = v
  394. else
  395. error("invalid value type for config option '"..k.."': "..type(v), 2)
  396. end
  397. else
  398. error("invalid config option '"..k.."'", 2)
  399. end
  400. end
  401. end
  402. -- api
  403. -- overwrite tfx outputmode function: do the same but also reset all
  404. -- colors to default. This is because with the change of the output mode,
  405. -- the colors may also change.
  406. local function outputmode(m)
  407. local om = tfx.outputmode(m)
  408. if m then resetconfig() end
  409. return om
  410. end
  411. ----- initialize -----
  412. tfx.init()
  413. tfx.outputmode(tfx.output.NORMAL)
  414. tfx.inputmode(tfx.input.ESC)
  415. resetconfig()
  416. ----- return -----
  417. return setmetatable({
  418. -- utilities
  419. drawframe = drawframe,
  420. frame = frame,
  421. drawlist = drawlist,
  422. drawtext = drawtext,
  423. drawstatus = drawstatus,
  424. drawfield = drawfield,
  425. input = input,
  426. -- widgets
  427. text = text,
  428. ask = ask,
  429. -- misc
  430. -- configure = configure,
  431. getconfig = getconfig,
  432. outputmode = outputmode,
  433. formatwidth = format,
  434. waitkeypress = waitkeypress,
  435. }, { __index = tfx })