3
-- Lua console implemented using Gtk widgets.
5
local lgi = require 'lgi'
7
local Gtk = lgi.require('Gtk', '3.0')
8
local Gdk = lgi.require('Gdk', '3.0')
9
local Pango = lgi.Pango
10
local GtkSource = lgi.GtkSource
12
-- Creates new console instance.
13
local function Console()
14
-- Define console object actions.
16
execute = Gtk.Action {
17
name = 'execute', stock_id = Gtk.STOCK_OK, is_important = true,
18
label = "_Execute", tooltip = "Execute",
21
multiline = Gtk.ToggleAction {
22
name = 'multiline', stock_id = Gtk.STOCK_JUSTIFY_LEFT,
25
tooltip = "Switches command entry to multiline mode",
29
name = 'up', stock_id = Gtk.STOCK_GO_UP,
30
label = "_Previous", tooltip = "Previous command in history",
35
name = 'down', stock_id = Gtk.STOCK_GO_DOWN,
36
label = "_Next", tooltip = "Next command in history",
41
name = 'clear', stock_id = Gtk.STOCK_CLEAR,
42
label = "_Clear", tooltip = "Clear output window",
46
name = 'about', stock_id = Gtk.STOCK_ABOUT,
47
label = "_About", tooltip = "About",
51
name = 'quit', stock_id = Gtk.STOCK_QUIT,
52
label = "_Quit", tooltip = "Quit",
56
-- Define the widget tree.
57
local widget = Gtk.Grid {
58
orientation = Gtk.Orientation.VERTICAL,
60
Gtk.ToolButton { related_action = actions.clear },
61
Gtk.SeparatorToolItem {},
62
Gtk.ToolItem { Gtk.FontButton { id = 'font_button' } },
63
Gtk.SeparatorToolItem {},
64
Gtk.ToolButton { related_action = actions.about },
65
Gtk.ToolButton { related_action = actions.quit },
68
orientation = Gtk.Orientation.VERTICAL,
71
shadow_type = Gtk.ShadowType.IN,
73
id = 'output', expand = true,
74
buffer = Gtk.TextBuffer {
75
tag_table = Gtk.TextTagTable {
76
Gtk.TextTag { name = 'command', foreground = 'blue' },
77
Gtk.TextTag { name = 'log' },
78
Gtk.TextTag { name = 'result',
79
style = Pango.Style.ITALIC },
80
Gtk.TextTag { name = 'error',
81
weight = Pango.Weight.BOLD,
91
orientation = Gtk.Orientation.HORIZONTAL,
94
shadow_type = Gtk.ShadowType.IN,
98
wrap_mode = Gtk.WrapMode.CHAR,
101
buffer = GtkSource.Buffer {
102
language = GtkSource.LanguageManager.get_default():
110
orientation = Gtk.Orientation.VERTICAL,
111
toolbar_style = Gtk.ToolbarStyle.ICONS,
113
Gtk.ToolButton { related_action = actions.execute },
114
Gtk.ToggleToolButton { related_action = actions.multiline },
115
Gtk.SeparatorToolItem {},
116
Gtk.ToolButton { related_action = actions.up },
117
Gtk.ToolButton { related_action = actions.down },
123
single_line_mode = true,
124
justify = Gtk.Justification.RIGHT,
135
-- Cache important widgets in local variables
136
local entry = widget.child.entry
137
local output = widget.child.output
138
local indicator = widget.child.indicator
139
local font_button = widget.child.font_button
141
-- When font changes, apply it to both views.
142
font_button.on_notify['font-name'] = function(button)
143
local desc = Pango.FontDescription.from_string(button.font_name)
144
output:override_font(desc)
145
entry:override_font(desc)
148
-- Initialize font. Get preferred font from system settings, if
149
-- they are installed. GSettings crash the application if the
150
-- schema is not found, so better check first if we can use it.
151
font_button.font_name = 'Monospace'
152
for _, schema in pairs(Gio.Settings.list_schemas()) do
153
if schema == 'org.gnome.desktop.interface' then
154
font_button.font_name = Gio.Settings(
155
{ schema = schema }):get_string('monospace-font-name')
160
-- Change indicator text when position in the entry changes.
161
entry.buffer.on_notify['cursor-position'] = function(buffer)
162
local iter = buffer:get_iter_at_mark(buffer:get_insert())
164
iter:get_line() + 1 .. ':' .. iter:get_line_offset() + 1
167
local output_end_mark = output.buffer:create_mark(
168
nil, output.buffer:get_end_iter(), false)
170
-- Appends text to the output window, optionally with specified tag.
171
local function append_output(text, tag)
173
local end_iter = output.buffer:get_end_iter()
174
local offset = end_iter:get_offset()
175
output.buffer:insert(end_iter, text, -1)
176
end_iter = output.buffer:get_end_iter()
179
tag = output.buffer.tag_table.tag[tag]
181
output.buffer:apply_tag(tag, output.buffer:get_iter_at_offset(offset),
185
-- Scroll so that the end of the buffer is visible, but only in
186
-- case that cursor is at the very end of the view. This avoids
187
-- autoscroll when user tries to select something in the output
189
local cursor = output.buffer:get_iter_at_mark(output.buffer:get_insert())
190
if end_iter:get_offset() == cursor:get_offset() then
191
output:scroll_mark_onscreen(output_end_mark)
195
-- Define history buffer and operations with it.
196
local history = { '', position = 1 }
197
local function history_select(new_position)
198
history[history.position] = entry.buffer.text
199
history.position = new_position
200
entry.buffer.text = history[history.position]
201
entry.buffer:place_cursor(entry.buffer:get_end_iter())
202
actions.up.sensitive = history.position > 1
203
actions.down.sensitive = history.position < #history
206
-- History navigation actions.
207
function actions.up:on_activate()
208
history_select(history.position - 1)
210
function actions.down:on_activate()
211
history_select(history.position + 1)
214
-- Execute Lua command from entry and log result into output.
215
function actions.execute:on_activate()
216
-- Get contents of the entry.
217
local text = entry.buffer.text:gsub('^%s?(=)%s*', 'return ')
218
if text == '' then return end
220
-- Add command to the output view.
221
append_output(text:gsub('\n*$', '\n', 1), 'command')
223
-- Try to execute the command.
224
local chunk, answer = (loadstring or load)(text, '=stdin')
227
answer = answer:gsub('\n*$', '\n', 1)
231
answer = tostring(...):gsub('\n*$', '\n', 1)
233
-- Stringize the results.
235
for i = 1, select('#', ...) do
236
answer[#answer + 1] = tostring(select(i, ...))
238
answer = #answer > 0 and table.concat(answer, '\t') .. '\n'
244
-- Add answer to the output pane.
245
if answer then append_output(answer, tag) end
247
if tag == 'error' then
248
-- Try to parse the error and find line to place the cursor
249
local line = answer:match('^stdin:(%d+):')
251
entry.buffer:place_cursor(entry.buffer:get_iter_at_line_offset(
255
-- Store current text as the last item in the history, but
256
-- avoid duplicating items.
257
history[#history] = (history[#history - 1] ~= text) and text or nil
259
-- Add new empty item to the history, point position to it.
260
history.position = #history + 1
261
history[history.position] = ''
263
-- Enable/disable history navigation actions.
264
actions.up.sensitive = history.position > 1
265
actions.down.sensitive = false
267
-- Clear contents of the entry buffer.
268
entry.buffer.text = ''
272
-- Intercept assorted keys in order to implement history
273
-- navigation. Ideally, this should be implemented using
274
-- Gtk.BindingKey mechanism, but lgi still lacks possibility to
275
-- derive classes and install new signals, which is needed in order
276
-- to implement this.
278
[Gdk.KEY_Return] = actions.execute,
279
[Gdk.KEY_Up] = actions.up,
280
[Gdk.KEY_Down] = actions.down,
283
function entry:on_key_press_event(event)
284
-- Lookup action to be activated for specified key combination.
285
local action = keytable[event.keyval]
286
local state = event.state
287
local without_control = not state.CONTROL_MASK
288
if not action or state.SHIFT_MASK
289
or actions.multiline.active == without_control then
293
-- Ask textview whether it still wants to consume the key.
294
if self:im_context_filter_keypress(event) then return true end
296
-- Activate specified action.
299
-- Do not continue distributing the signal to the view.
303
function actions.about:on_activate()
304
local about = Gtk.AboutDialog {
305
program_name = 'LGI Lua Console',
306
copyright = '(C) Copyright 2011 Pavel Holejšovský',
307
authors = { 'Pavel Holejšovský' },
309
about.license_type = Gtk.License.MIT_X11
314
function actions.clear:on_activate()
315
output.buffer.text = ''
318
-- Return public object.
320
widget = widget, output = output, entry = entry,
322
append_output = append_output
326
local old_print = print
327
local old_write = io.write
329
-- On activation, create and wire the whole widget hierarchy.
330
local app = Gtk.Application { application_id = 'org.lgi.gtkconsole' }
331
function app:on_activate()
333
local console = Console()
335
-- Create application window with console widget in it.
336
console.window = Gtk.Window {
338
title = "LGI Lua Console",
339
default_width = 800, default_height = 600,
343
-- Make everything visible
344
console.entry.has_focus = true
345
console.window:show_all()
347
-- Map quit action to window destroy.
348
function console.actions.quit:on_activate()
349
console.window:destroy()
352
-- Inject 'lgi' symbol into global namespace, for convenience.
353
-- Also add 'console' table containing important elements of this
354
-- console, so that it can be manipulated live.
358
-- Override global 'print' and 'io.write' handlers, so that output
359
-- goes to our output window (with special text style).
360
function _G.print(...)
362
for i = 1, select('#', ...) do
363
outs[#outs + 1] = tostring(select(i, ...))
365
console.append_output(table.concat(outs, '\t') .. '\n', 'log')
368
function _G.io.write(...)
369
for i = 1, select('#', ...) do
370
console.append_output(select(i, ...), 'log')
375
-- Run the whole application
376
app:run { arg[0], ... }
378
-- Revert to old printing routines.