Module tty
Terminal-related facilities.
Keyboard keys
keycode_to_keyname(keycode) | Converts a keycode to a keyname. |
keyname_to_keycode(keyname) | Converts a keyname to a keycode. |
Drawing and Refreshing the Screen
get_canvas() | Returns a canvas object encompassing the whole screen. |
redraw() | Redraws all the screen’s contents. |
refresh() | Refreshes the screen. |
Styles
destruct_style(style) | Destructures a style. |
is_color() | Whether it’s a color terminal. |
is_hicolor() | Whether it’s a rich color terminal. |
style(spec) | Creates a style. |
Text handling
text_align(s, width, align_mode) | Aligns (“justifies”) a string. |
text_cols(s, i [, j]) | Returns a “visual” substring of a string. |
text_width(s) | Calculates a string’s “visual” width. |
Misc functions
beep() | Sounds a beep. |
get_cols() | Returns the terminal width, in characters. |
get_rows() | Returns the terminal height, in lines. |
is_idle() | Whether the terminal is idle. |
is_ui_ready() | Whether the UI is ready. |
skin_change(skin_name[, force]) | Changes the active skin. |
skin_get(property_name, default) | Fetches a skin’s property. |
Encodings
conv(s, encoding_name) | Converts a string to the terminal’s encoding. |
is_utf8() | Whether the terminal is UTF-8 encoded. |
Keyboard keys
A key —for example q, R, Control-S, F4, ESC— is represented as a number. We call this number a keycode. We, as humans, would like to deal not with such a number but with a descriptive name. We call this name a keyname.
The TTY module provides us with two functions to deal with keys.
The foremost function is keyname_to_keycode, which translates a keyname to a keycode. The other function, keycode_to_keyname, does the opposite.
- keycode_to_keyname(keycode)
-
Converts a keycode to a keyname.
Throws an exception if the keycode is invalid.
Returns two values: the key’s “short” name, and its “long” name.
- keyname_to_keycode(keyname)
-
Converts a keyname to a keycode.
Throws an exception if the keyname is invalid.
See use example at ui.Custom:on_key.
Drawing and Refreshing the Screen
MC is a curses application. In such applications drawing (others may call it “painting”) is made to a virtual screen, which is simply a memory buffer in the program. This virtual screen is then written out to the physical screen.
Likewise in Lua. Any function (or method) dealing with the screen belongs to one, and one only, of the stages.
Functions belonging to the drawing stage have “redraw” in their names. They don’t affect the physical screen, only the virtual one. We'll call this the drawing stage.
Functions belonging to the second stage, which affects the physical screen, have “refresh” in their names. We'll call this the refresh stage.
Here’s a summary of the functions (or method) dealing with the screen:
The drawing stage
-
Positioning the cursor is just like a drawing operation: it occurs in the virtual screen, and doesn’t show on the physical screen unless one of the refresh stage functions is called.
The refresh stage
-
Copies the virtual screen onto the physical one.
-
A utility function that also positions the cursor.
When to call which function?
You may feel overwhelmed by the many functions you have at your hands. Don’t feel so.
In normal code you won’t need to call any of them. When you set a property of some widget, its :redraw() method will be called automatically. Then, as part of MC’s event loop, tty.refresh will be called. So the screen will be updated appropriately without your explicit intervention.
On the other hand, when using a ui.Custom you have to call its :redraw() yourself whenever its state changes in a way that affects its display because only you know when this happens.
Working with timers
We explained that you normally don’t need to call tty.refresh
yourself as this is done automatically. One exception, however, is when
working with timers. If your timed function affects the display (for
example, if it updates some widget, as in label.text = "new label"
)
then you need to call tty.refresh (or dialog:refresh)
yourself to refresh the screen:
timer.set_timeout(function() label.text = label.text .. "!" dlg:refresh() -- dialog:refresh() is like tty.refresh() except that -- it also puts the cursor at the focused widget. Had -- we called tty.refresh() instead, the cursor would have -- appeared at the last widget to draw itself (the label). end, 1000) -- another example: timer.set_timeout(function() alert('hi') tty.refresh() end, 1000)
The reason for this is the way MC’s event loop works. Here’s a schema of it:
event_loop: repeat: (A) while there's no keyboard or mouse events: execute timers (B) get keyboard or mouse event (C) process the event (D) position the cursor at the focused widget (E) refresh the screen
When our timer function returns, MC still sits there waiting (loop (A)) for a keyboard (or mouse) event. Step (E) isn’t arrived at, and hence the screen isn’t refreshed. That’s why we need to refresh the screen explicitly from our timer function.
- get_canvas()
-
Returns a canvas object encompassing the whole screen.
This lets you draw on the screen. Alternatively you can use the :get_canvas() method of a widget if you're interested in its limited screen area.
- redraw()
-
Redraws all the screen’s contents.
All the dialogs on screen are redrawn, from the bottom to the top.
keymap.bind('C-y', function() local dlg = ui.Dialog() local btn = ui.Button(T"Move this dialog to the right") btn.on_click = function() dlg:set_dimensions(dlg.x + 1, dlg.y) tty.redraw() -- comment this out to see what happens. end dlg:add(btn):run() end)
- refresh()
-
Refreshes the screen.
This copies the virtual screen onto the physical one.
Styles
- Foreground color
- Background color
- Attributes: underlined, italic, bold and/or reversed.
A style happens to be represented internally as a numeric handle. For example, on your system the style 64 may mean “red foreground, green background, italic.” We, as humans, don’t manipulate such numbers directly but instead use style() to convert a human-readable style description to this number.
- destruct_style(style)
-
Destructures a style.
Does the opposite of tty.style. Given a style, returns a table with the fields fg, bg, attr (and their numeric counterparts).
You'll not normally use this function. It can be used to implement exotic features like converting an ui.Editbox syntax-highlighted contents into HTML, or creating “screen shots”.
ui.Editbox.bind('C-y', function(edt) devel.view(tty.destruct_style( edt:get_style_at(edt.cursor_offs) )) end)
- is_color()
-
Whether it’s a color terminal.
Return true if the terminal supports colors, or false if it’s a monochrome terminal.
- is_hicolor()
-
Whether it’s a rich color terminal.
Return true if the terminal supports 256 colors (in this case is_color too will return true), or false otherwise.
- style(spec)
-
Creates a style.
The argument for this function is a description of a style. The function “compiles” this description and returns an integer which can be used wherever a style value is required.
There are three ways to describe a style.
(1) As a string with the three components (foreground color, background color, attributes — in this order) separated by commas or semicolons:
local style = tty.style('white, red; underline') local style = tty.style('white; red, underline') local style = tty.style('white, red, underline')
Type
mc --help-color
at the shell to see the valid names.Any of the components may be omitted:
local style = tty.style('white, red') local style = tty.style(', red') local style = tty.style(',, underline') local style = tty.style('')
(2) As a string naming a skin property:
local style = tty.style('editor.bookmark')
(3) You may also specify several style in a table, keyed by the terminal type:
local style = tty.style { mono = "reverse", color = "yellow, green", hicolor = "rgb452, rgb111" } -- 'hicolor' is for 256 color terminals.
For examples of using styles, see ui.Canvas and ui.Editbox:bookmark_set.
Text handling
- text_align(s, width, align_mode)
-
Aligns (“justifies”) a string.
Fits a string to width terminal columns by padding it with spaces or trimming it.
align_mode may be:
- “left”
- “right”
- “center”
- “center or left”
if the string is wider than width, the excess it cut off. You may instead append “~” to the align mode to shorten the string by replacing characters from its middle with a tilde character.
assert(tty.text_align("Alice", 10, "left") == "Alice ") assert(tty.text_align("Alice", 10, "right") == " Alice") assert(tty.text_align("Alice", 10, "center") == " Alice ") assert(tty.text_align("Alice in Wonderland", 10, "left") == "Alice in W") assert(tty.text_align("Alice in Wonderland", 10, "left~") == "Alice~land") assert(tty.text_align("Alice in Wonderland", 10, "center") == "") -- "center or left" means to center if there's enough room, and align -- to left otherwise. assert(tty.text_align("Alice in Wonderland", 10, "center or left") == "Alice in W") -- Multiple lines are not supported: assert(tty.text_align("one\ntwo", 8, "left") == "one.two ")
- text_cols(s, i [, j])
-
Returns a “visual” substring of a string.
Given a string in the terminal’s encoding, returns the substring falling within certain screen columns.
The arguments to this function are the same as string.sub’s. Indeed, you can think of this function as equivalent to string.sub except that the indices are interpreted to be screen columns instead of bytes.
(See discussion at tty.text_width.)
assert(tty.text_cols('ンab᷉c᷉d', 4, 5) == 'b᷉c᷉') -- ...and now, assuming this is a UTF-8 encoded source file, compare -- this with string.sub(), which is oblivious to characters and -- their properties: assert(string.sub('ンab᷉c᷉d', 4, 5) == 'ab')
(If the terminal’s encoding isn’t UTF-8, this function is identical to string.sub.)
- text_width(s)
-
Calculates a string’s “visual” width.
Given a string in the terminal’s encoding, returns the amount of columns needed to display it.
assert(tty.text_width 'ンab᷉c᷉d' == 6) -- ...and now, assuming this is a UTF-8 encoded source file, compare -- this with string.len(), which is oblivious to characters and -- their properties: assert(string.len 'ンab᷉c᷉d' == 13)
(If the terminal’s encoding isn’t UTF-8, this function is identical to string.len (except for handling multiple lines).)
if the string contains multiple lines, the width of the widest line is returned. Also returned is the number of lines:
assert(tty.text_width "once\nupon\na time" = 6) assert(select(2, tty.text_width "once\nupon\na time") = 3)
Misc functions
- beep()
- Sounds a beep.
- get_cols()
- Returns the terminal width, in characters.
- get_rows()
- Returns the terminal height, in lines.
- is_idle()
-
Whether the terminal is idle. That is, whether there are no pending
keyboard or mouse events.
This function can be used, for example, to early exist lengthy drawing tasks and thereby collapsing them into a final one, when the terminal becomes idle again. See examples at dialog-drag.lua and ruler.lua.
- is_ui_ready()
-
Whether the UI is ready.
It tells us if curses has taken control of the terminal. This is when you can use dialog boxes.
- skin_change(skin_name[, force])
-
Changes the active skin.
Example:
-- There are three ways to specify a skin. ui.Panel.bind('C-q', function() tty.skin_change('gotar') -- Or you may include the '.ini' suffix: tty.skin_change('gotar.ini') -- Or: if your skin file isn't located in one of the -- predefined places, you can use it by providing -- an absolute path (but not relative!) to it: tty.skin_change('/usr/share/mc/skins/gotar.ini') end)
Here’s how to change the skin depending on which directory you're in:
-- Use a special skin when we're in the /etc tree. local function on_chdir(pnl) if pnl == ui.Panel.current then -- <<load>> is fired for the "other" (inactive) panel as well. if pnl.dir:find '^/etc' then tty.skin_change('gotar') else tty.skin_change('default') end end end ui.Panel.bind("<<load>>", on_chdir) -- When the user navigates between directories. ui.Panel.bind("<<activate>>", on_chdir) -- When the user switches between panels. -- Note: if you're interested in this idea, see the 'dynamic-skin' -- module, which is more robust.
Parameters:
- skin_name The name of the skin (with or without the ending ‘.ini’), or an absolute path to a skin file.
- force
Normally the function only changes the skin if it’s not
already the active one. This saves you from worrying about performance
yourself if you call the function frequently. But you can pass true as
the
force
flag to always change the skin (useful if, for example, you've edited it on disk and want to reload it). (optional)
Returns:
-
Returns the name of the skin now active. It should match the name of the
skin you fed this function, unless some error occurred. You may call this
function with a nil
skin_name
if you're only interested in the active skin name. - skin_get(property_name, default)
-
Fetches a skin’s property.
Example:
tty.skin_get('invasion-from-mars.missile', '>===))>') tty.skin_get('chess.white-knight', tty.is_utf8() and '♘' or 'N') tty.skin_get('Lines.horiz', '-')
Skin files are, by convention, encoded in UTF-8, and the properties read from them are converted to the terminal’s encoding. Therefore you can directly use them in the UI (in widgets' data and drawing functions): there’s no need to re-encode them first.
Parameters:
- property_name A string of the form group.name.
- default A value to return if the property wasn’t found.
Encodings
This is the result of not living in a perfect world: The file you're editing, or a string you read from some data file, may be of a different encoding than your terminal’s. For example, your terminal’s encoding may be UTF-8 whereas your data may be encoded in ISO-8859-8.
This isn’t really an issue on modern systems where everything is encoded in UTF-8. It'd be legitimate for you, therefore, to decide to ignore this issue —especially if you're the only user of your script. Nevertheless, you should at least be aware of this issue in order to support your user-base.
There are two ways to convert a string to the terminal’s encoding:
(1) When you know the string’s encoding, use tty.conv:
-- Displaying the contents of a ISO-8859-8 encoded file. local s = assert(fs.read('data_file.txt')) alert(tty.conv(s, 'iso-5589-8'))
(2) When the string originates in some widget like ui.Editbox, which keeps its data encoded independently of the terminal, use the widget’s :to_tty method:
alert('The current word is ' .. edt:to_tty(edt.current_word))
(In the example above we could've used instead the hypothetical code
tty.conv(edt.current_word, edt.encoding)
but an ui.Editbox doesn’t
keep the name of its encoding.)
- conv(s, encoding_name)
-
Converts a string to the terminal’s encoding.
See example in the discussion above.
Parameters:
- s The string to convert.
- encoding_name The string’s encoding name. Like “ISO-8859-8”, “KOI8-R”, etc. This name is case-insensitive. If the encoding name is unknown to the system, an exception will be raised.
- is_utf8()
-
Whether the terminal is UTF-8 encoded.
See example in skin_get.