Module ui

User Interface.

The ui module lets you create user interfaces. This subject is covered in the user guide.

At the basis of the architecture are widgets. You can create widgets, and you can also access existing widgets created by MC itself. This ability is the one which lets you script the file manager and the editor.

C programmers: You can do in Lua anything practical that you can do in C. All the benefits of a “scripting” language are at your hands: you write but the fraction of the code you would in C, and you don’t need to worry about memory leaks and crashes.

As a quick reference, here’s a snippet that uses some common features:

local function order_pizza()

  local dlg = ui.Dialog(T"Place an order")

  local flavor = ui.Radios()
  flavor.items = {
    'Cheese',
    'Olive',
    'Anchobi',
    'Falafel',
  }

  local with_pepper = ui.Checkbox(T"With pepper")
  local with_ketchup = ui.Checkbox{T"With ketchup", checked=true}

  local send_address = ui.Input()

  dlg:add(
    ui.Label(T"Please fill in the details:"),
    ui.HBox():add(
      flavor,
      ui.Groupbox(T"Spices:"):add(
        with_pepper,
        with_ketchup
      )
    ),
    ui.Label(T"Send it to:"),
    send_address,
    ui.DefaultButtons()
  )

  flavor.on_change = function(self)
    -- It's abominable to add ketchup to Anchobi.
    with_ketchup.enabled = (self.value ~= "Anchobi")
  end

  if dlg:run() then
    alert(T"Great! I'll be delivering the %s pizza to %s!":format(
      flavor.value, send_address.text))
    if with_pepper.checked then
      alert(T"I too love pepper!")
    end
  end

end

keymap.bind('C-q', order_pizza)

Functions

Button(...) Creates a Button widget
Checkbox(...) Creates a Checkbox widget
Dialog(...) Creates a Dialog box
Editbox(...) Creates an Editbox widget.
Gauge(...) Creates a gauge widget
Groupbox(...) Creates a Groupbox widget
HBox(...) Creates an HBox container
HLine(...) Creates an HLine widget
Input(...) Creates a Input widget
Label(...) Creates a label widget
Listbox(...) Creates a Listbox widget
Radios(...) Creates a Radios widget
Space([cols[, rows]) Creates a Space widget.
VBox(...) Creates a VBox container
ZLine(...) Creates a ZLine widget.
current_widget([widget_type]) Returns the current widget.
open() Enters UI mode.
queue(fn) Queues code to run at the next event-loop iteration.

Widget methods and properties

widget.cols rw The width of the widget, in characters.
widget.data rw Custom user data.
widget.dialog r The dialog the widget is in.
widget.enabled rw Whether the widget is enabled or disabled.
widget.expandx rw Layout control.
widget.expandy rw Layout control.
widget.rows rw The height of the widget, in lines.
widget.widget_type r The name of the widget’s class.
widget:_send_message(msg[, parm[, sender]) Low-level message passing.
widget:command(command_name) Executes a widget command.
widget:fixate() Fixates a widget.
widget:focus() Focuses the widget.
widget:get_canvas() Returns a canvas object encompassing the widget’s area.
widget:is_alive() Whether the widget is alive.
widget:redraw() Redraws the widget.

Button widget

button.result rw The result value for clicking this button.
button.text rw The label shown on the button.
button.type w The type of the button.
button:on_click(self) handler Click handler.

Checkbox widget

checkbox.checked rw The state of the checkbox.
checkbox.text rw The label for the checkbox.
checkbox:on_change(self) handler Change handler.

Label widget

label.auto_size w Whether the text decides the size of the widget.
label.text rw The text displayed in the label.

Input widget

input.cursor_offs rw The cursor position.
input.history w The history bin.
input.mark rw The “mark” position.
input.password rw Whether to mask the input.
input.text rw The text being edited.
input:insert(s) Inserts text at the cursor location.
input:on_change(self) handler Change handler.

Groupbox widget

groupbox.padding rw Horizontal padding.
groupbox.text rw Caption.

Listbox widget

listbox.items rw The listbox items.
listbox.selected_index rw The index of the selected item.
listbox.value rw The selected item.
listbox:on_change(self) handler Change handler.
listbox:widest_item() Calculates the widest item.

Radios widget

radios.items rw The radio items.
radios.selected_index rw The index of the selected item.
radios.value rw The selected item.
radios:on_change(self) handler Change handler.

Gauge widget

gauge.max rw The maximal value.
gauge.shown rw The visibility of the gauge.
gauge.value rw The current value.

HLine widget

hline.text rw Optional caption.

Dialog widget

dialog.colorset rw The colors of the dialog.
dialog.compact rw Whether extra space is shown around the dialog’s frame.
dialog.current r The focused widget.
dialog.help_id rw The help ID.
dialog.mapped_children r The child widgets.
dialog.modal rw Whether the dialog is modal or modaless.
dialog.padding rw Horizontal padding.
dialog.result rw Holds the “result” of running the dialog.
dialog.state r The state of the dialog.
dialog.text rw The dialog window’s title.
dialog:close() Closes the dialog.
dialog:find([wtype,] [predicate,] [from]) Finds a widget among the children.
dialog:focus() Switches to the dialog.
dialog:gmatch([wtype,] [predicate,] [from]) Finds widgets among the children.
dialog:on_draw(self) handler Frame drawing handler.
dialog:on_help(self) handler Help handler.
dialog:on_idle(self) handler Idle handler.
dialog:on_init(self) handler Initialization handler.
dialog:on_key(self, keycode) handler Keypress handler.
dialog:on_post_key(self, keycode) handler Keypress handler.
dialog:on_resize(self) handler Resize handler.
dialog:on_title(self) handler Title handler.
dialog:on_validate(self) handler Closing validation handler.
dialog:popup([lstbx]) Runs the dialog.
dialog:redraw() Redraws the dialog.
dialog:redraw_cursor() Positions the cursor at the focused element.
dialog:refresh([do_redraw]) Updates the cursor on the physical screen.
dialog:run() Runs the dialog.
dialog:set_dimensions([x], [y], [cols], [rows]) Explicitly sets the dialog’s dimensions.
<<draw>> Triggered after a dialog had been painted.
<<layout>> Triggered after a dialog layouts itself.
<<open>> Triggered when a dialog is opened.
<<submit>> Triggered when a dialog is about to be closed “successfully”.
<<close>> Triggered when a dialog is about to be closed.

Static dialog properties

Dialog.screens r A list of all the modaless dialogs.
Dialog.top r The topmost dialog.

Containers

container:add(widget[, ...]) Adds widgets to a container.
container:preferred_cols() Calculates the preferred width.
container:preferred_rows() Calculates the preferred height.

HBox container

hbox.gap rw The horizontal gap between child widgets.

VBox container

vbox.gap rw The vertical gap between child widgets.

Stock buttons

Buttons() Creates a container for buttons.
CancelButton([props]) Creates a “Cancel” button.
DefaultButtons() Creates an “Ok” and “Cancel” buttons.
OkButton([props]) Creates an “OK” button.

Static widget functions

bind(keyseq_or_event, function) Binds functions to keys and events.
subclass(new_class_name) Creates a new widget class.


Functions

Button(...)
Creates a Button widget
Checkbox(...)
Creates a Checkbox widget
Dialog(...)
Creates a Dialog box
Editbox(...)
Creates an Editbox widget.

You'll usually access an already-existing Editbox (as in the editor), but you can create one yourself. Note, however, that since it was not foreseen by the core developers that this widget would be used outside the editor, it has a few problems when used in that fashion. See editbox_instance.mcs.

Gauge(...)
Creates a gauge widget
Groupbox(...)
Creates a Groupbox widget
HBox(...)
Creates an HBox container
HLine(...)
Creates an HLine widget
Input(...)
Creates a Input widget
Label(...)
Creates a label widget
Listbox(...)
Creates a Listbox widget
Radios(...)
Creates a Radios widget
Space([cols[, rows])
Creates a Space widget.

A Space widget is just an empty rectangle on the screen. It lets us space out other widgets. You can put it between or before/after widgets.

Together with the expandx and expandy properties it can also be used to flush other widgets to the right/bottom/center. (See discussion in the user guide.)

VBox(...)
Creates a VBox container
ZLine(...)
Creates a ZLine widget.

A “ZLine” widget is exactly like an HLine widget except that the line stretches all over the way to the dialog’s frame. It’s therefore a bit more aesthetically pleasing than an HLine.

current_widget([widget_type])
Returns the current widget.

The “current widget” is the widget that has the focus, in the active dialog box.

-- A useful debugging aid.
keymap.bind('F11', function()
  devel.view(ui.current_widget())
end)

-- An interesting way to close the active dialog.
keymap.bind('F12', function()
  local wgt = ui.current_widget()
  if wgt then
    wgt.dialog:close()
  end
  -- Or we could just do ui.Dialog.top:close()
end)

The widget doesn’t need to have been created in Lua. There’s no distinction between widgets created in Lua to widgets created by MC itself.

The function may return nil if there’s no current widget or if the widget doesn’t have a Lua counterpart. For example, if the pull-down menus are active, nil is returned.

The optional string argument widget_type makes the function return the widget only if the widget is of the specified type (otherwise nil is returned). While it’s trivial to do this check in Lua, it’s more efficient to have current_widget do it.

When you're using the filemanager, it’s the panel which has the focus there and therefore is what considered the current widget. The command input line, albeit showing the caret, isn’t really in focus.

current_widget knows about this MC idiosyncrasy and provides you with a little convenience device: if you specify an “Input” widget_type, the command input line will be returned, even though it isn’t technically the current widget.

open()
Enters UI mode.

Makes the terminal enter UI mode, where dialogs can be displayed.

This function can be called in standalone mode only. See there for details.

At the time of this writing, there’s no ui.close(). That is, once you enter UI mode you cannot go back to “line printer” mode.

queue(fn)
Queues code to run at the next event-loop iteration.

Feel free to ignore this function unless you're an advanced user.

Sometimes you want to postpone code a bit, till after some other things happen in the current event-loop iteration.

Example:

-- A failed example.

-- The "Pause" button, when clicked, should pass focus
-- to the "Resume" button.
pause_button.on_click = function()
  interval:stop()
  -- THIS FAILS: It so happens that MC focuses a button
  -- *after* its on_click gets called. So right after you
  -- focus the resume button, MC will take away its focus
  -- and give it to the pause button.
  resume_button:focus()
end

To fix this example we can postpone the focusing:

-- This works.

pause_button.on_click = function()
  interval:stop()
  ui.queue(function()
    resume_button:focus()
  end)
end

(Search our code base for “queue” to see more examples.)

ui.queue() is a very simple function. Its implementation is largely equivalent to:

function ui.queue(fn)
  timer.set_timeout(function()
    fn()
    tty.refresh()  -- When using timers, we have to refresh the screen ourselves (see explanation in tty module).
  end, 0)
end

This trick, of wrapping code in set_timeout(…, 0), is well-known in the JavaScript world.

Widget methods and properties

“Widget” is the base class for all widgets. All the widgets (buttons, listboxes, dialogs, etc.) inherit the methods and properties listed here.
widget.cols rw
The width of the widget, in characters.

You usually don’t need to set this property: for most widgets it’s set according to the widget’s preferred dimensions.

widget.data rw
Custom user data.

A table in which you can store your own data.

See example at dialog:on_validate.

There’s nothing really special in this property. You can store your data in however-named property, but then you'd have to use rawset to bypass the typo protection. In other words, this property is just an aid letting you do:

wgt.data.help_text = "whatever"

instead of:

rawset(wgt, "help_text", "whatever")
widget.dialog r
The dialog the widget is in.

closeButton.on_click = function(self)
  self.dialog:close()
end

(See another example at command.)

widget.enabled rw
Whether the widget is enabled or disabled.

local ftp = ui.Checkbox(T"Use FTP")
local ftp_server = ui.Input("ftp.midnight.org")

ftp.on_change = function(self)
  ftp_server.enabled = self.checked
end

ui.Dialog():add(ftp, ftp_server):run()

This property works for containers as well: You may group widgets in a groupbox and enable/disable the groupbox itself to affect all the widgets within.

widget.expandx rw
Layout control.

This property helps in laying out a widget, as explained in the user guide.

widget.expandy rw
Layout control.

This property helps in laying out a widget, as explained in the user guide.

widget.rows rw
The height of the widget, in lines.
widget.widget_type r
The name of the widget’s class.

This property can aid in debugging. It’s also a way for you to find the type of a widget. E.g., if w.widget_type == "Button" (although you can also do if getmetatable(w) == ui.Button.meta).

widget:_send_message(msg[, parm[, sender])
Low-level message passing.

This method calls the C function send_message().

This method is intended for advanced users only. One won’t normally use it (which is proven by the fact that this method isn’t used in our core). Its use is discouraged, which is why we don’t document its parameters here.

See usage examples at snippets/hotlist_right_as_enter.lua and skin-sampler.lua.

widget:command(command_name)
Executes a widget command.

Certain widgets respond to certain commands. For example, an Input widget responds to ‘WordLeft’, ‘Paste’, etc. An Editbox responds to ‘Undo’, ‘Redo’, ‘DeleteLine’, etc. Instead of providing a Lua method for every such command, this one method triggers any command by name.

Command names are case insensitive.

To see a list of available commands, check the C source code (e.g., keybind-defaults.c, but that list isn’t exhaustive).

Note that some commands are to be sent to the dialog containing the widget, not the widget itself (see example).

ui.Editbox.bind('C-x e', function(edt)
  edt:command "DeleteLine"
  edt.dialog:command "ShowMargin"
end)

ui.Panel.bind('C-x d', function(pnl)
  pnl:command "MiddleOnScreen"
  pnl:redraw() -- Some commands, such as MiddleOnScreen,
               -- don't automatically redraw the widget.
  pnl.dialog:command "Find"
end)

This method returns true if the command was handled.

widget:fixate()
Fixates a widget.

This section describes a convenience method that can be used in some special situations. It is seldom needed. Feel free to ignore this section unless you're an advanced user.

Introduction

The widgets you interact with in Lua are wrappers around “real” C widgets.

In some situations it can happen that you no longer have a Lua variable referencing a widget you're interested in. In such cases the Lua wrapper gets garbage collected. Usually there’s no problem in that: it’s the expected behavior. A problem may arise, however, when you store some data in the Lua wrapper: this data will be lost too.

The fixate method prevents the Lua wrapper from being garbage collected as long as the underlying C widget is still alive. This means that the next time you get your hands on a wrapper for this widget you'll get the same old wrapper — with your precious data on it.

Example

Let’s have an example. Suppose you want to implement a “read only” feature for the editor. As a first step you make C-n mark an editbox as read only:

ui.Editbox.bind('C-n', function(edt)
  alert(T'This editbox is now read-only!')
  edt.data.is_read_only = true
end)

As the next step you reject keys that occur in such marked editboxes:

local ESC = tty.keyname_to_keycode 'esc'

ui.Editbox.bind('any', function(edt, kcode)
  if edt.data.is_read_only and (kcode < 256 and kcode ~= ESC) then
    tty.beep()
  else
    return false  -- Let MC handle this key.
  end
end)

Will this work? Not quite. The read-only protection will last for a few seconds only: The Lua wrapper carrying data.is_read_only will get garbage collected at some point, and the edt wrapper seen at the keyboard handler code will be a new one, which doesn’t carry data.is_read_only.

To fix our code we need to fixate the wrapper:

ui.Editbox.bind('C-n', function(edt)
  alert(T'This editbox is now read-only!')
  edt.data.is_read_only = true
  edt:fixate()
end)

It’s trivial to change our system to make widgets “fixated” by default. Should we? Maybe. Maybe not. In the meantime we should observe our users to learn what they expect of the system.

Returns:

    The widget itself.
widget:focus()

Focuses the widget.

That is, moves the keyboard focus (the cursor) to it.

See example at on_validate.

Note that you can only focus a widget that has been "mapped" into a dialog. Mapping happens when you call run, not before. Therefore, a way to select the initial widget to get the focus is to use on_init, as follows:

local function test()
  local dlg = ui.Dialog()

  local name = ui.Input()
  local age = ui.Input()

  dlg.on_init = function() age:focus() end

  dlg:add(name, age)
  dlg:run()
end
widget:get_canvas()
Returns a canvas object encompassing the widget’s area.

This lets you draw inside the widget. You'd normally use this method with a ui.Custom widget only.

widget:is_alive()

Whether the widget is alive.

You'll seldom, if ever, use this method.

This method tells us whether the C widget associated with this Lua object has been destroyed.

To understand this method, let’s imagine the following code:

local the_editbox = nil

ui.Editbox.bind('C-a', function(edt)
  the_editbox = edt
end)

keymap.bind('C-b', function()
  if the_editbox then
    alert('The editbox edits the file ' .. the_editbox.filename)
  end
end)

We press C-a inside the editor. Then we close the editor. This destroys the editbox. Now we press C-b. What will happen? An exception will be raised, when we try to access the filename property. The error message says “A living widget was expected, but an already destroyed widget was provided”. That’s because the Lua object is now just a shell over a dead body. To fix our code we can change it to:

keymap.bind('C-b', function()
  if the_editbox and the_editbox:is_alive() then
    alert('The editbox edits the file ' .. the_editbox.filename)
  end
end)
widget:redraw()
Redraws the widget.

Draws (“paints”, if you will) the widget.

You won’t normally need to call this method yourself, because all properties that affect the visual appearance of a widget call :redraw() automatically for you upon setting. For example, if you change the text of an input box or the value of a gauge, you don’t need to call :redraw() afterwards.

A notable case where you do have to call :redraw() yourself is after you change the state of a ui.Custom widget. Only you know what affects the display of your custom widget, so only you can decide when to redraw it.

For further information on the mechanism of updating the screen, see Drawing and Refreshing the Screen.

Button widget

There are two ways to make a button do something: either by using the result property, or by using the on_click handler.
button.result rw
The result value for clicking this button.

Often, after running a dialog, we're interested in knowing which button was clicked.

While you yourself can keep a track of which button was clicked, by using the on_click handler, the “result” property offers a shortcut:

When a button is clicked which has the “result” property set, the dialog’s own “result” property gets set to this value and the dialog is then closed.

Example:

keymap.bind('C-f', function()

  local dlg = ui.Dialog(T"Open mode")

  dlg:add(ui.Button{T'Read', result='r'})
     :add(ui.Button{T'Write', result='w'})
     :add(ui.Button{T'Read/Write', result='rw'})

  alert(dlg:run())

  -- You'll usually do:

  --if dlg:run() then
  --  alert("I'll open the file in " .. dlg.result .. " mode")
  --end

  -- Or:

  --local mode = dlg:run()
  --if mode then
  --  alert("I'll open the file in " .. mode .. " mode")
  --end

end)

The result property is just a shortcut for doing:

local btn = ui.Button{T'Read/Write', on_click=function(self)
  self.dialog.result = 'rw'
  self.dialog:close()
end}

This property’s value isn’t limited to strings and numbers: it can be any complex object, including tables.

button.text rw
The label shown on the button.
button.type w
The type of the button.

Possible types: “normal”, “default”, “narrow”, “hidden”.

button:on_click(self) handler

Click handler.

Called when a button is “clicked”; that is, activated.

btn.on_click = function()
  alert(T"hi there!")
end

The name of this handler isn’t very accurate. It’s actually seldom that users click the mouse in a console application. The name was borrowed from the JavaScript world to make most programmers feel “at home,” and to make code snippets more self-documented. It was felt that the benefits outweigh the negatives.

You may, of course, close the dialog from the handler:

btn.on_click = function(self)
  alert(T"hi there!")
  self.dialog:close()
end

Often, however, using the result property is shorter:

local btn = ui.Button{T"Show greeting", result="greet"}
dlg:add(btn)

if dlg:run() == "greet" then
  alert(T"hi there!")
end

Checkbox widget

checkbox.checked rw
The state of the checkbox.

A checkbox is either checked or not.

See example in widget.enabled.

checkbox.text rw
The label for the checkbox.
checkbox:on_change(self) handler
Change handler.

Called when the user changes the state of a checkbox.

Label widget

label.auto_size w
Whether the text decides the size of the widget.

Normally, setting the label.text property sets the size of the label widget. When creating a label that shows a fixed string, this is what you what. If your label changes its text after creation, however, you want to turn off this feature so that the label doesn’t paint over neighboring widgets.

auto_size is initially true. Set it to false to disable it; you'll also want to set cols explicitly to make the label wide enough to display the bulk of its text, and/or to set expandx to true.

local function test()
  local lst = ui.Listbox {items={"one", "two",
                           "a very loooooong string"}}
  local lbl = ui.Label {cols=20, auto_size=false}

  lst.on_change = function()
    lbl.text = lst.value
  end

  ui.Dialog():add(ui.Groupbox():add(lst), lbl):run()
end

See another example in ui_filechooser.mcs.

label.text rw
The text displayed in the label.

If you wish to change this property after creation, see discussion at label.auto_size.

Input widget

input.cursor_offs rw
The cursor position.
input.history w

The history bin.

It is a string naming the bin. (As this string isn’t for human consumption, you don’t need to wrap it in T.)

local expr = ui.Input{history="calculator-expression"}
input.mark rw
The “mark” position.

The “mark” is the point where the selection starts. If there’s no selction active, it equals nil.

input.password rw

Whether to mask the input.

If you're inputting a password, set this property to true to show asterisks instead of the actual text.

-- Toggle masking for the current input field.
ui.Input.bind('C-r', function(ipt)
  ipt.password = not ipt.password
end)
input.text rw
The text being edited.

Setting this property to -1 sets the text to the last value stored in the history:

local filename = ui.Input{text=-1, history='scanimage-output'}
input:insert(s)

Inserts text at the cursor location.

-- Insert the current date and time into any input line.
ui.Input.bind("C-y", function(ipt)
  ipt:insert(os.date("%Y-%m-%d %H:%M:%S"))
end)
input:on_change(self) handler
Change handler.

Called when the user modifies the input box' text.

See example in ui_inputchange.mcs.

Groupbox widget

A Groupbox is a container that draws a frame around its child widgets. You add widgets to it using its :add() method. You can even add child groupboxes to it.

See example in ui_groupboxes.mcs.

groupbox.padding rw
Horizontal padding.

The amount of screen columns (“spaces”) to reserve on the left and right sides, inside the frame. Defaults to 1. If you want the child widgets to almost “touch” the frame, set it to 0.

groupbox.text rw
Caption.

Caption to print on the groupbox’s frame.

Listbox widget

A listbox dimensions by default are 12 characters wide (cols) and 6 lines high (rows). By default expandx=true so the width will stretch. You may change all this by modifying these properties (and expandy). There’s also :widest_item() to your help.

For aesthetic reasons it’s recommended that you wrap your listboxes in a groupbox. I.e., instead of dlg:add(lstbx) do dlg:add(ui.Groupbox(T"Pick a word"):add(lstbx)).

listbox.items rw
The listbox items.

In its simplest form, each item is a string:

lstbx.items = {
  'apple',
  'banana',
  'water melon',
}

Alternatively, any item may be a list whose first element is the string, plus two optional keyed elements: value and hotkey. The string is meant for humans whereas the value is what your program actually sees. This value can be any complex object; not just strings or numbers. The hotkey, if exists, lets you select the associated item and close the dialog by pressing a key.

local lstbx = ui.Listbox()

lstbx.items = {
  { T'Read only', value='r' },
  { T'Write only', value='w' },
  { T'Read/write', value='r+', hotkey='C-b' },
}

if ui.Dialog():add(lstbx):run() then
  local f = assert( fs.open('/etc/passwd', lstbx.value) )
  -- ...
end

An alternative to using hotkey elements is using dialog:on_key.

listbox.selected_index rw
The index of the selected item.

You may find using the listbox.value property easier.

listbox.value rw
The selected item.
listbox:on_change(self) handler
Change handler.

Called when the user changes the selection in the listbox.

See example in ui_filechooser.mcs.

listbox:widest_item()

Calculates the widest item.

lstbx.cols = lstbx:widest_item() + 2

Radios widget

The radios widget, for the sake of convenience, shares the same API with the listbox widget.
radios.items rw
The radio items.

Everything discussed at listbox.items applies here as well except that a hokey element for an item has no effect on radios.

radios.selected_index rw
The index of the selected item.

You may find using the radios.value property easier.

radios.value rw
The selected item.
radios:on_change(self) handler
Change handler.

Called when the user changes the selection in radio boxes.

Gauge widget

A gauge, also known as a progress-bar, shows a percentage in a graphic manner. The percentage is value / max. Typically you continuously change value to reflect the status of a task carried out.

The cols property (the gauge’s size) is 25 characters by default. You may change this and/or use expandx=true.

gauge.max rw
The maximal value.

This number is the 100% value. By default this is 100. You may change it if you think it'd make your calculations easier.

This number, as well as value, is stored internally as C-language’s int. Therefore setting max to 1.0 and moving value from 0.0 to 0.1 to 0.2 … to 1.0 won’t quite work.

gauge.shown rw
The visibility of the gauge.

Whether the gauge is shown or not. A gauge that is not shown still consumes space on the screen. You'd use this property when, for example, you wish to show the gauge only when some process is running.

A gauge is shown by default.

gauge.value rw
The current value.

As your task progresses, you'd update this property to anything between 0 and max. It is allowed for it to exceed max (in which case it'd be treated as if it were equal to max).

You'll usually have to call dialog:refresh to see the gauge updated on the screen, because screen refresh is only done automatically when a keyboard or mouse event is handled, something which probably doesn’t happen in your loop.

See example at dialog:on_idle.

HLine widget

An HLine widget is simply a horizontal line, possibly with text on it. It’s similar to HTML’s <hr> element. You can use it to separate sections in a dialog.

See also ZLine, which looks nicer.

hline.text rw
Optional caption.

Text to print centered on the line.

Dialog widget

dialog.colorset rw

The colors of the dialog.

The set of colors to be used to paint the dialog and the widgets within. Possible values:

  • "normal"
  • "alarm" (typically red dominated, for error boxes.)
  • "pmenu" (colors of popup menus, like the “User menu”.)

You'd usually set this property in the constructor call:

dlg = ui.Dialog{T"A frightening dialog", colorset="alarm"}

But you can set it anytime afterwards:

local answer = ui.Input()

answer.on_change = function(self)
  if self.text == "" then
    -- Missing data.
    self.dialog.colorset = "alarm"
  else
    self.dialog.colorset = "normal"
  end
end

You can also read this property:

-- Sound a beep when an error dialog is shown.

ui.Dialog.bind("<<open>>", function(dlg)
  if dlg.colorset == "alarm" then
    tty.beep()
  end
end)
dialog.compact rw
Whether extra space is shown around the dialog’s frame. A boolean flag.

The difference between this and padding is that padding governs the space inside the frame whereas compact governs the space outside the frame.

dialog.current r
The focused widget.

Note that this is a read-only property. To set the focused widget, use focus on the desired widget.

dialog.help_id rw
The help ID.

If the dialog has a section in the user manual, this is the name of that section.

dialog.mapped_children r
The child widgets.

Returns a list of all the widgets “mapped” into the dialog. Pseudo widgets (those used for layout: HBox, VBox, Space) aren’t included in the list.

find and gmatch are easy-to-use wrappers around this property.

A short explanation for the frightening word “mapped”:

When you use add() to add a widget to a dialog, it doesn’t get added yet to the dialog itself but to a layout manager. Only when you call run does the layout manager physically add the widgets to the dialog. The widgets are then called “mapped” (a term borrowed from Tk, which is not the only toolkit to use it).

dialog.modal rw
Whether the dialog is modal or modaless.

By default dialogs are modal: the user has to finish interacting with them in order to continue. In other words, dialog:run doesn’t return while the dialog is still open. But a dialog can also be modaless: the user can switch to some other dialog while working with them.

In MC, by default, switching between modaless dialogs (sometimes called “screens”, or “windows”) is done with the following key bindings:

  • M-{ (command name: “ScreenPrev”)
  • M-} (command name: “ScreenNext”)
  • M-` (command name: “ScreenList”)

To make a dialog modaless, simply set this property to true before calling dialog:run.

For best aesthetic results, make your modaless dialogs maximized.

That’s because MC won’t paint the dialog at the background (that is, the filemanager) when the need arises (see lib/widget/dialog.c:dlg_redraw(), called by do_refresh(): it only paints active dialogs). This is certainly an MC bug that should be fixed.

dialog.padding rw
Horizontal padding.

This property behaves just like groupbox.padding.

dialog.result rw
Holds the “result” of running the dialog. This value has no meaning except the one you yourself give it.

This value is conveniently returned by :run, but you can access it directly any time.

See example at button.result.

dialog.state r
The state of the dialog.

Use this property to inquire about the state of the dialog. Possible states:

  • “construct”: The dialog hasn’t been run yet.
  • “active”: The dialog is running.
  • “suspended”: A modaless dialog has been switched out of.
  • “closed”: The dialog has been closed.

This is a read-only property. To actually change the state of the dialog you use other methods; e.g., :run, :close.

dialog.text rw
The dialog window’s title.

We don’t have properties named “title” and “label” in our API. We always call it “text”. This way you don’t need to remember what property goes with what widget type.

dialog:close()
Closes the dialog. Normally you'd use this method from click handlers of buttons.

dlg = ui.Dialog()
dlg:add(ui.Button {T"say something nice", on_click = function()
  alert(T"something nice!")
  dlg:close()
end})
dlg:run()

Another way to close a dialog is by sending it the “cancel” command (i.e., dlg:command "cancel"). The difference between the two ways is explained in a note in tests/nonauto/close_current_dialog.lua.

dialog:find([wtype,] [predicate,] [from])
Finds a widget among the children.

This is a convenience interface for mapped_children.

There are three criteria to search by. Each is optional, and the order doesn’t matter:

  • wtype – the widget type (a string).

  • predicate – a function testing a widget and returning true if it matches.

  • from – either a number, meaning to return the n'th widget found, or a widget, meaning to start searching after this widget (in the tabbing order).

If no widget matches the criteria, nil is returned.

Examples:

dlg:find('Input')  -- find the first Input widget.
dlg:find('Input', 2)  -- find the second one.
dlg:find('Checkbox', function(w) return w.text == T'&Fake half tabs' end)  -- find the checkbox with that label.
dlg:find('Checkbox', dlg:find('Groupbox', 2))  -- find the first checkbox inside the second groupbox.

There’s no reason to use find with dialogs you create in Lua because you can simply store the desired widgets in variables, which you can refer to later. find is useful when you want to interact with dialogs created by MC itself.

See also gmatch.

dialog:focus()
Switches to the dialog.

Only works for modaless dialogs.

You won’t normally use this method. It can be used to implement special features like a windows switcher or tabs.

This method doesn’t return immediately (unless you switch to the filemanager): it starts an event loop. This is not a limitation in our Lua API but the way MC works.

dialog:gmatch([wtype,] [predicate,] [from])
Finds widgets among the children.

Like find, but iterates over all the matched children.

See example at Dialog.screens.

dialog:on_draw(self) handler
Frame drawing handler.

Called to draw the background and frame of the dialog.

You should return true from this handler to signal that you've done the job or else the default frame will then be drawn, overwriting yours.

You wouldn’t normally be interested in this handler. It is only useful for special applications (e.g., for drawing a wallpaper; although this is alternatively possible by adding a ui.Custom to the dialog).

dialog:on_help(self) handler
Help handler.

Called up when the user presses the help button (usually F1). Example:

dlg.on_help = function()
  local helpfile = assert(utils.path.module_path('mymodule', 'README.md'))
  mc.view(helpfile)
end

An alternative method for displaying help is to set help_id to some section name in the user manual. But since you cannot normally add sections there, on_help is the only practical way.

dialog:on_idle(self) handler

Idle handler.

Called while there’s no keyboard input.

You may use this handler, for example, to perform one slice of a lengthy task, repeatedly. This gives the impression of performing in the background: the user is able to interact with the dialog between the invocations of this handler.

keymap.bind('C-q', function()
  local dlg = ui.Dialog()
  local gg = ui.Gauge()
  gg.max = 5000000
  dlg.on_idle = function()
    -- ... imagine we perform a slice of a lengthy calculation here ...
    gg.value = gg.value + 1
    if gg.value > gg.max then
      gg.value = 0
    end
    dlg:refresh()  -- We have to refresh the terminal ourselves.
  end
  dlg:add(gg)
  dlg:add(ui.DefaultButtons())
  dlg:run()
end)

Comments:

  • If it happens that you no longer need this handler, set it to nil so it won’t get invoked and waste CPU cycles.

  • A alternative to on_idle is to use the timer. But you can’t quite close a dialog (if you need to) from a timed function (because right after MC executes the timers it waits for a key (see tty/key.c:tty_get_event), even if the dialog is closed), which is something you can do from on_idle.

  • If you wish to close a dialog from on_idle (let’s say when you finish your lengthy “background” task), you first need to set the handler to nil (or else MC will keep calling it, since there’s still no keyboard input):

dlg.on_idle = function()
  do_a_slice_of_some_task()
  if task_was_completed() then
    dlg.on_idle = nil
    dlg:close()
  end
end
dialog:on_init(self) handler
Initialization handler.

Called just before the dialog becomes active.

See usage example at widget:focus.

when this handler is called the dialog isn’t yet considered active. This means that some operations on the dialog, like drawing it on screen or closing it, can’t be done yet. If you do need such operations, put your code in on_idle instead (and in it also set the handler to nil so it'd run only once).

dialog:on_key(self, keycode) handler
Keypress handler.

Lets you respond to a key before any of the child widgets sees it. Return true from this handler to signal that you've consumed the key.

See examples at ui.Custom:on_key, which is used similarly.

Parameters:

  • self The dialog
  • keycode A number
dialog:on_post_key(self, keycode) handler
Keypress handler.

Lets you respond to a key after the child widgets had a chance to respond to it. Return true from this handler to signal that you've consumed the key.

See examples at ui.Custom:on_key, which is used similarly.

It happens that the system doesn’t really care what you return from this specific handler. But for “forward-compatibility” it won’t hurt that you do return true for keys you handled.

Parameters:

  • self The dialog
  • keycode A number
dialog:on_resize(self) handler

Resize handler.

Called when the screen changes its size.

Normally you don’t need to implement this handler: The default handler does an adequate job: it will re-center the dialog on the screen (unless it has figured out, by noticing that you've called set_dimensions earlier, that it’s not what you want).

You may implement this handler if you need to do some custom positioning. For example, here’s how to keep a dialog a fixed distance from the screen edges, similar to how the “Directory hotlist” behaves:

local dlg = ui.Dialog()

dlg.on_resize = function(self)
  self:set_dimensions(nil, nil, tty.get_cols() - 10, tty.get_rows() - 2)
end

dlg:on_resize() -- We have to call this explicitly in the beginning,
                -- as it only gets called when the screen changes size.
dlg:run()
dialog:on_title(self) handler

Title handler.

Called to generate a modaless dialog’s title. It’s not used for modal dialogs. The default handler returns the dialog’s title.

local dlg = ui.Dialog("bobo")

dlg.on_title = function()
  return "Clock: " .. os.date()
end

dlg.modal = false

dlg:add(ui.DefaultButtons())
dlg:run()
dialog:on_validate(self) handler
Closing validation handler.

Lets you decide whether it’s alright to close the dialog. This handler is called whenever an attempt is made to close the dialog. Return true from this handler to signal that it’s alright to close the dialog; else, the dialog will stay open.

-- Asks the user for his name and age. If the user
-- omits either name or age, we nag him.

local function test()
  local dlg = ui.Dialog(T"Tell me about yourself")

  local name = ui.Input()
  local age = ui.Input()
  local occupation = ui.Input()

  name.data = {
    required = true,
    errmsg = T"Missing name!",
  }

  age.data = {
    required = true,
    errmsg = T"Missing age!",
  }

  dlg.on_validate = function()
    if dlg.result then  -- We validate the input only if the
                        -- user pressed some positive button.
                        -- This excludes ESC and "Cancel".
      for widget in dlg:gmatch() do
        if widget.data.required and widget.text == "" then
          alert(widget.data.errmsg)
          widget:focus()  -- Move user to the rogue widget.
          return false    -- Don't let user close the dialog.
        end
      end
    end
    return true -- Allow closing.
  end

  dlg:add(
    ui.HBox():add(ui.Label(T"Name:"), name),
    ui.HBox():add(ui.Label(T"Age:"), age),
    ui.HBox():add(ui.Label(T"Occupation (optional):"), occupation),
    ui.DefaultButtons()
  )

  dlg:run()
end

If the user is exiting MC, and the dialog is modaless, the dialog will get closed anyway.

A special case: on_validate has no effect when you close the dialog from on_idle (see dialog.c:frontend_dlg_run).

dialog:popup([lstbx])
Runs the dialog.

This is like run except that the dialog is shown near the cursor, which is where users typically expect “popup” boxes to appear.

Popup dialogs often have a listbox, functioning as a menu. If you pass the optional lstbx argument, this listbox will be resized to show as many of its items as possible while taking care not to make the dialog exceed the screen’s size.

See examples at ui.Editbox.current_word and speller.lua.

dialog:redraw()
Redraws the dialog.

You won’t normally need to call this method.

This method is similar in principle to widget:redraw but works on the whole dialog: the dialog draws itself (frame and background), then its children. It also asks the widget in focus to reposition the cursor.

dialog:redraw_cursor()
Positions the cursor at the focused element.

You won’t normally need to call this method.

This method asks the widget in focus to reposition the cursor.

As to why this method has “redraw” in its name, see the section Drawing and Refreshing the Screen.

dialog:refresh([do_redraw])

Updates the cursor on the physical screen.

This is just a shorthand for calling :redraw_cursor() and then tty.refresh. It is implemented thus:

function ui.Dialog.meta:refresh(do_redraw)
  if do_redraw then
    self:redraw()
  else
    self:redraw_cursor()
  end
  tty.refresh()
end
dialog:run()
Runs the dialog.

The dialog is displayed. An “event loop” starts which lets the user interact with the dialog till it’s dismissed.

Returns:

As a convenience, this method returns dialog.result. A nil is returned (and stored in dialog.result) if the user cancels the dialog (e.g., by pressing ESC).

See examples at button.result (and elsewhere on this page).

Modaless dialogs

For modaless dialogs, :run returns also when the user switches to another dialog.

The implication is that for such dialogs you need to put your action in buttons' on_click handlers

dialog:set_dimensions([x], [y], [cols], [rows])
Explicitly sets the dialog’s dimensions.

Call this method if you wish to explicitly position or size the dialog. Usually you shouldn’t be interested in this method as the dialog by default will be decently positioned (centered on the screen).

You may omit x and/or y: if you do, they will be calculated such that the dialog will be centered on the screen.

You may omit cols and/or rows: if you do, they will be calculated based on the dialog’s contents (therefore, in this case, make sure to call this method after you've already added all the widgets to the dialog.)

local dlg = ui.Dialog()
dlg:add(ui.Label('Hi there!'))
-- push the dialog to the extreme right of the screen:
dlg:set_dimensions(tty.get_cols() - dlg:preferred_cols(), nil)
dlg:run()

To “maximize” a dialog, do:

dlg:set_dimensions(nil, nil, tty.get_cols(), tty.get_rows() - 2)

This function returns the dialog itself, thereby allowing for “fluent API”.

A fifth argument, send_msg_resize, makes the dialog receive a MSG_RESIZE message. For advanced users only.

<<draw>>
Triggered after a dialog had been painted.

You may use this event to add decoration to a dialog, like a drop shadow.

ui.Dialog.bind('<<draw>>', function(dlg)
  local c = dlg:get_canvas()
  c:set_style(tty.style('yellow, red'))
  c:goto_xy(0, 0)
  c:draw_string(T"hello!")
end)

The differences between this event and on_draw are:

  • This event is global: it’s triggered for every dialog box in the application, whereas on_draw is attached to a single dialog you yourself created.
  • This event is triggered after the child widgets had painted themselves, whereas on_draw is triggered before that.
<<layout>>
Triggered after a dialog layouts itself.

Triggered after the placement of child widgets has been set. You may use this event to inject your own widgets into a dialog.

This event is currently triggered only for MC’s filemanager and MC’s editor. It is used by the docker module to inject widgets there.

(In the future we may replace this event with <<resize>>.)

See Global events in the user guide.

<<open>>
Triggered when a dialog is opened.

You may use this event to notify the user with sound on alert boxes, text-to-speech the title, etc.

-- Read aloud dialogs' titles.

ui.Dialog.bind('<<open>>', function(dlg)
   -- Note: we run espeak in the background (&) or else we'll be
   -- blocked till it finishes voicing the text.
   os.execute(('espeak %q &'):format(dlg.text))
end)

You may also use it to set initial values for widgets of builtin dialogs:

-- Make 'xsel' the default command of 'Paste output of...'.
--
-- (This technique isn't very robust because dialog titles may
-- change between MC releases.)

ui.Dialog.bind('<<open>>', function(dlg)
  if dlg.text == T'Paste output of external command' then
    dlg:find('Input').text = 'xsel'
  end
end)

You can close or cancel a dialog from this event (i.e., by calling :close or :command 'cancel'). This lets you “automate”, or “auto submit”, forms.

See Global events in the user guide.

<<submit>>
Triggered when a dialog is about to be closed “successfully”.

When a dialog is closed by hitting Enter, or by clicking a positive button (that is, anything except the “Cancel” button), this event is triggered.

This event is not triggered for dialogs that are canceled (e.g., by pressing ESC).

You may use this event to read data from the widgets. E.g., the find_file_title.lua snippet uses this event to read the search parameters from the “Find File” dialog and put them in the title of the progress dialog that follows.

Right after this event is triggered, the <<close>> event too is triggered.

The name of this event was borrowed from the JavaScript world.

See Global events in the user guide.

<<close>>
Triggered when a dialog is about to be closed.

See Global events in the user guide.

Static dialog properties

Dialog.screens r

A list of all the modaless dialogs.

-- Show all the edited files.

local append = table.insert

keymap.bind('C-y', function()
  local edited_files = {}
  for _, dlg in ipairs(ui.Dialog.screens) do
    -- A single editor dialog may contain several editboxes (aka "windows").
    for edt in dlg:gmatch('Editbox') do
      append(edited_files, edt.filename)
    end
  end
  devel.view(edited_files)
end)
Dialog.top r

The topmost dialog.

This is the “current” dialog.

-- Closes the current dialog.
keymap.bind('C-y', function()
  ui.Dialog.top:close()
end)

Containers

Some widgets — like Groupbox, Dialog, HBox, VBox — are containers. They hold other widgets. The following methods are shared by all containers.

Containers can be nested. This way you can set up complex layouts.

container:add(widget[, ...])

Adds widgets to a container.

As a convenience, this method returns the container itself; this lets you save some typing.

dlg:add(w1)
dlg:add(w2, w3)
dlg:run()

is the same as:

dlg:add(w1, w2, w3):run()
dlg:add(w1):add(w2):add(w3):run()
container:preferred_cols()
Calculates the preferred width.

Calculates the width of the container based on its contents.

See example at dialog:set_dimensions.

container:preferred_rows()
Calculates the preferred height.

Calculates the height of the container based on its contents.

HBox container

An HBox is a container that helps you to layout widgets. You add widgets to it using its :add() method.
hbox.gap rw
The horizontal gap between child widgets. By default it is 1 (one space character).

VBox container

A VBox is a container that helps you to layout widgets. You add widgets to it using its :add() method.
vbox.gap rw
The vertical gap between child widgets. By default it is 0 (zero screen rows).

Stock buttons

Buttons()
Creates a container for buttons.

When the default OK/Cancel buttons that DefaultButtons() creates don’t satisfy you, you'll want to create the buttons yourself. You can add them to the dialog in however manner you wish, but for uniformity and conformity it’s recommended that you use this container. It displays the buttons centered horizontally with a line separating them from the preceding widgets. Example:

local dlg = ui.Dialog()

dlg:add(ui.Label(T"The target file exists. What to do?"))

dlg:add(ui.Buttons():add(
  ui.Button{T"&Skip", result="skip"},
  ui.Button{T"&Overwrite", result="overwrite"},
  ui.Button{T"H&elp", on_click=function() alert "hi" end},
  ui.CancelButton()
))

dlg:run()

The container is not limited to just buttons. E.g., we could have added ui.Space(), in the code above, before the cancel button to separate it visually from the main buttons.

The function accepts an optional first argument that tells it whether to drop the horizontal line. Use it when you're adding a second (or third etc.) line of buttons:

dlg:add(ui.Buttons():add( ...first line of buttons... ))
dlg:add(ui.Buttons(true):add( ...second line... ))

Remember: some terminals are limited in width so do break your buttons into several lines if there are many of them.

This container has a method, repack(), which you can use to re-layout the buttons after changing the text of one of them (and hence its size) during runtime.

CancelButton([props])
Creates a “Cancel” button.

Typically you'd use this function only if DefaultButtons doesn’t suit your needs. See example at Buttons.

This function is implemented thus:

function ui.CancelButton(props)
  return ui.Button { T"&Cancel", result = false }:assign_properties(props)
end
DefaultButtons()
Creates an “Ok” and “Cancel” buttons.

You're expected to add this to any normal dialog you create. Example:

local dlg = ui.Dialog()

dlg:add(ui.Label(T"Give me the head of Alfredo Garcia!"))

dlg:add(ui.DefaultButtons())

dlg:run()

This function is implemented thus:

function ui.DefaultButtons()
  return ui.Buttons():add(ui.OkButton(), ui.CancelButton())
end
OkButton([props])

Creates an “OK” button.

Typically you'd use this function only if DefaultButtons doesn’t suit your needs.

This function is implemented thus:

function ui.OkButton(props)
  return ui.Button { T"&OK", result = "ok", type = "default" }:assign_properties(props)
end

You can use the optional props argument to change the default label:

ui.Buttons():add(
  ui.OkButton(T"G&o!"),
  ui.CancelButton()
)

Static widget functions

You already know that for every widget class there exists a ui.WidgetClass() function that creates (“instantiates”) such widget. E.g., ui.Input().

But ui.WidgetClass is also a namespace that groups further functions and properties. E.g., ui.Input.bind(), ui.Panel.register_field(), ui.Editbox.options, etc.

This section describes functions common to all widget classes. These function aren’t methods: they're what called in OOP parlance “static”.

bind(keyseq_or_event, function)
Binds functions to keys and events.

Use this to execute a function when a certain key sequence is pressed in a certain class of widgets:

ui.Panel.bind('C-y', function(pnl)
  alert("You're standing on " .. pnl.current)
end)

Or when a certain event occurs:

ui.Panel.bind('<<load>>', function(pnl)
  alert("You're browsing " .. pnl.dir)
end)

In both cases the bound function is invoked with the widget as its first (and only) argument.

subclass(new_class_name)
Creates a new widget class.

For example, let’s suppose we want to create a widget that shows the current time. We can do it thus:

local clock = ui.Custom()

function clock:on_draw()
  self.canvas:draw_string(os.date("%H:%M:%S"))
end

ui.Dialog():add(clock):run()

However, this widget isn’t quite reusable. We can instead create is as a class,

local ClockMeta = ui.Custom.subclass("Clock")

function ClockMeta:on_draw()
  self.canvas:draw_string(os.date("%H:%M:%S"))
end

…and then re-use this class wherever we want:

ui.Dialog():add(ui.Clock(), ui.Clock(), ui.Clock()):run()

This function, subclass, returns the metatable of the new class. It also creates the namespace ui.NewClassName.

You can initialize your instances in a method called “init”. It’s like the constructor from other programming languages.

Your new class behaves just like any other widget class. You can even further inherit from it:

local RedClock = ui.Clock.subclass("RedClock")

The namespace also stores the widget’s metatable at meta. This is true for all widget classes. You can define new methods on a class easily:

-- Define a :word_left() method for Editboxes.
function ui.Editbox.meta:word_left()
  self:command "WordLeft"
end

This meta table is very similar to JavaScript’s prototype property:

// JavaScript code!
String.prototype.trim = function() { ... }
Array.prototype.some = function() { ... }
generated by LDoc 1.4.3 Last updated 2016-08-23 17:29:40