gtkterminal.lua 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. #! /usr/bin/env lua
  2. --
  3. -- Lua console using Vte widget. It uses homegrown poor-man's
  4. -- Lua-only readline implementation (most of the code of this sample,
  5. -- not really related to GLib/Gtk in any way).
  6. --
  7. local lgi = require 'lgi'
  8. local Gtk = lgi.require('Gtk', '3.0')
  9. local Vte = lgi.require('Vte', '2.90')
  10. -- Simple readline implementation with asynchronous interface.
  11. local ReadLine = {}
  12. ReadLine.__index = ReadLine
  13. function ReadLine.new()
  14. return setmetatable(
  15. {
  16. insert_mode = true,
  17. columns = 80,
  18. history = {},
  19. }, ReadLine)
  20. end
  21. function ReadLine:start_line(prompt)
  22. self.input = ''
  23. self.pos = 1
  24. self.prompt = prompt or ''
  25. self.history_pos = #self.history + 1
  26. self.display(self.prompt)
  27. end
  28. -- Translates input string position into line/column pair.
  29. local function getpos(rl, pos)
  30. local full, part = math.modf((pos + #rl.prompt - 1) / rl.columns)
  31. return full, math.floor(part * rl.columns + 0.5)
  32. end
  33. -- Redisplays currently edited line, moves cursor to newpos, assumes
  34. -- that rl.input is updated with new contents but rl.pos still holds
  35. -- old cursor position.
  36. local function redisplay(rl, newpos, modified)
  37. if newpos < rl.pos then
  38. -- Go back with the cursor
  39. local oldl, oldc = getpos(rl, rl.pos)
  40. local newl, newc = getpos(rl, newpos)
  41. if oldl ~= newl then
  42. rl.display(('\27[%dA'):format(oldl - newl))
  43. end
  44. if oldc ~= newc then
  45. rl.display(('\27[%d%s'):format(math.abs(newc - oldc),
  46. oldc < newc and 'C' or 'D'))
  47. end
  48. elseif newpos > rl.pos then
  49. -- Redraw portion between old and new cursor.
  50. rl.display(rl.input:sub(rl.pos, newpos - 1))
  51. end
  52. rl.pos = newpos
  53. if modified then
  54. -- Save cursor, redraw the rest of the string, clear the rest of
  55. -- the line and screen and restore cursor position back.
  56. rl.display('\27[s' .. rl.input:sub(newpos, -1) .. '\27[K\27[J\27[u')
  57. end
  58. end
  59. local bindings = {}
  60. function bindings.default(rl, key)
  61. if not key:match('%c') then
  62. rl.input = rl.input:sub(1, rl.pos - 1) .. key
  63. .. rl.input:sub(rl.pos + (rl.insert_mode and 0 or 1), -1)
  64. redisplay(rl, rl.pos + 1, rl.insert_mode)
  65. end
  66. end
  67. function bindings.enter(rl)
  68. redisplay(rl, #rl.input + 1)
  69. rl.display('\n')
  70. rl.commit(rl.input)
  71. end
  72. function bindings.back(rl)
  73. if rl.pos > 1 then redisplay(rl, rl.pos - 1) end
  74. end
  75. function bindings.forward(rl)
  76. if rl.pos <= #rl.input then redisplay(rl, rl.pos + 1) end
  77. end
  78. function bindings.home(rl)
  79. if rl.pos ~= 1 then redisplay(rl, 1) end
  80. end
  81. function bindings.goto_end(rl)
  82. if rl.pos ~= #rl.input then redisplay(rl, #rl.input + 1) end
  83. end
  84. function bindings.backspace(rl)
  85. if rl.pos > 1 then
  86. rl.input = rl.input:sub(1, rl.pos - 2) .. rl.input:sub(rl.pos, -1)
  87. redisplay(rl, rl.pos - 1, true)
  88. end
  89. end
  90. function bindings.delete(rl)
  91. if rl.pos <= #rl.input then
  92. rl.input = rl.input:sub(1, rl.pos - 1) .. rl.input:sub(rl.pos + 1, -1)
  93. redisplay(rl, rl.pos, true)
  94. end
  95. end
  96. function bindings.kill(rl)
  97. rl.input = rl.input:sub(1, rl.pos - 1)
  98. redisplay(rl, rl.pos, true)
  99. end
  100. function bindings.clear(rl)
  101. rl.input = ''
  102. rl.history_pos = #rl.history + 1
  103. redisplay(rl, 1, true)
  104. end
  105. local function set_history(rl)
  106. rl.input = rl.history[rl.history_pos] or ''
  107. redisplay(rl, 1, true)
  108. redisplay(rl, #rl.input + 1)
  109. end
  110. function bindings.up(rl)
  111. if rl.history_pos > 1 then
  112. rl.history_pos = rl.history_pos - 1
  113. set_history(rl)
  114. end
  115. end
  116. function bindings.down(rl)
  117. if rl.history_pos <= #rl.history then
  118. rl.history_pos = rl.history_pos + 1
  119. set_history(rl)
  120. end
  121. end
  122. -- Real keys are here bound to symbolic names.
  123. local function ctrl(char)
  124. return string.char(char:byte() - ('a'):byte() + 1)
  125. end
  126. bindings[ctrl'b'] = bindings.back
  127. bindings['\27[D'] = bindings.back
  128. bindings[ctrl'f'] = bindings.forward
  129. bindings['\27[C'] = bindings.forward
  130. bindings[ctrl'a'] = bindings.home
  131. bindings['\27OH'] = bindings.home
  132. bindings[ctrl'e'] = bindings.goto_end
  133. bindings['\27OF'] = bindings.goto_end
  134. bindings[ctrl'h'] = bindings.backspace
  135. bindings[ctrl'd'] = bindings.delete
  136. bindings['\127'] = bindings.delete
  137. bindings[ctrl'k'] = bindings.kill
  138. bindings[ctrl'c'] = bindings.clear
  139. bindings[ctrl'p'] = bindings.up
  140. bindings['\27[A'] = bindings.up
  141. bindings[ctrl'n'] = bindings.down
  142. bindings['\27[B'] = bindings.down
  143. bindings['\r'] = bindings.enter
  144. function ReadLine:receive(key)
  145. (bindings[key] or bindings.default)(self, key)
  146. end
  147. function ReadLine:add_line(line)
  148. -- Avoid duplicating lines in history.
  149. if self.history[#self.history] ~= line then
  150. self.history[#self.history + 1] = line
  151. end
  152. end
  153. -- Instantiate terminal widget and couple it with our custom readline.
  154. local terminal = Vte.Terminal {
  155. delete_binding = Vte.TerminalEraseBinding.ASCII_DELETE,
  156. }
  157. local readline = ReadLine.new()
  158. if Vte.Terminal.on_size_allocate then
  159. -- 'size_allocate' signal is not present in some older Gtk-3.0.gir files
  160. -- due to bug in older GI versions. Make sure that this does not trip us
  161. -- completely, it only means that readline will not react on the terminal
  162. -- resize events.
  163. function terminal:on_size_allocate(rect)
  164. readline.columns = self:get_column_count()
  165. end
  166. end
  167. function readline.display(str)
  168. -- Make sure that \n is always replaced with \r\n. Also make sure
  169. -- that after \n, kill-rest-of-line is always issued, so that
  170. -- random garbage does not stay on the screen.
  171. str = str:gsub('([^\r]?)\n', '%1\r\n'):gsub('\r\n', '\27[K\r\n')
  172. terminal:feed(str, #str)
  173. end
  174. function terminal:on_commit(str, length)
  175. readline.columns = self:get_column_count()
  176. readline:receive(str)
  177. end
  178. function readline.commit(line)
  179. -- Try to execute input line.
  180. line = line:gsub('^%s?(=)%s*', 'return ')
  181. local chunk, answer = (loadstring or load)(line, '=stdin')
  182. if chunk then
  183. (function(ok, ...)
  184. if not ok then
  185. answer = tostring(...)
  186. else
  187. answer = {}
  188. for i = 1, select('#', ...) do
  189. answer[#answer + 1] = tostring(select(i, ...))
  190. end
  191. answer = #answer > 0 and table.concat(answer, '\t')
  192. end
  193. end)(pcall(chunk))
  194. end
  195. if answer then
  196. readline.display(answer .. '\n')
  197. end
  198. -- Store the line into rl history and start reading new line.
  199. readline:add_line(line)
  200. readline:start_line(_PROMPT or '> ')
  201. end
  202. -- Create the application.
  203. local app = Gtk.Application { application_id = 'org.lgi.samples.gtkconsole' }
  204. -- Pack terminal into the window with scrollbar.
  205. function app:on_activate()
  206. local grid = Gtk.Grid { child = terminal }
  207. grid:add(Gtk.Scrollbar {
  208. orientation = Gtk.Orientation.VERTICAL,
  209. adjustment = terminal.adjustment,
  210. })
  211. terminal.expand = true
  212. readline.display [[
  213. This is terminal emulation of standard Lua console. Enter Lua
  214. commands as in interactive Lua console. The advantage over standard
  215. console is that in this context, GMainLoop is running, so this
  216. console is ideal for interactive toying with Gtk (and other
  217. mainloop-based) components. Try following:
  218. Gtk = lgi.Gtk <Enter>
  219. window = Gtk.Window { title = 'Test' } <Enter>
  220. window:show_all() <Enter>
  221. window.title = 'Different' <Enter>
  222. ]]
  223. local window = Gtk.Window {
  224. application = self,
  225. title = 'Lua Terminal',
  226. default_width = 640,
  227. default_height = 480,
  228. has_resize_grip = true,
  229. child = grid,
  230. }
  231. window:show_all()
  232. readline.columns = terminal:get_column_count()
  233. readline:start_line(_PROMPT or '> ')
  234. -- For convenience, propagate 'lgi' into the global namespace.
  235. _G.lgi = lgi
  236. end
  237. -- Start the application.
  238. app:run { arg[0], ... }