-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #21 from lunarmodules/terminal
- Loading branch information
Showing
26 changed files
with
4,037 additions
and
11 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
# 3. Terminal functionality | ||
|
||
Terminals are fundamentally different on Windows and Posix. So even though | ||
`luasystem` provides primitives to manipulate both the Windows and Posix terminals, | ||
the user will still have to write platform specific code. | ||
|
||
To mitigate this a little, all functions are available on all platforms. They just | ||
will be a no-op if invoked on another platform. This means that no platform specific | ||
branching is required (but still possible) in user code. The user must simply set | ||
up both platforms to make it work. | ||
|
||
## 3.1 Backup and Restore terminal settings | ||
|
||
Since there are a myriad of settings available; | ||
|
||
- `system.setconsoleflags` (Windows) | ||
- `system.setconsolecp` (Windows) | ||
- `system.setconsoleoutputcp` (Windows) | ||
- `system.setnonblock` (Posix) | ||
- `system.tcsetattr` (Posix) | ||
|
||
Some helper functions are available to backup and restore them all at once. | ||
See `termbackup`, `termrestore`, `autotermrestore` and `termwrap`. | ||
|
||
|
||
## 3.1 Terminal ANSI sequences | ||
|
||
Windows is catching up with this. In Windows 10 (since 2019), the Windows Terminal application (not to be | ||
mistaken for the `cmd` console application) supports ANSI sequences. However this | ||
might not be enabled by default. | ||
|
||
ANSI processing can be set up both on the input (key sequences, reading cursor position) | ||
as well as on the output (setting colors and cursor shapes). | ||
|
||
To enable it use `system.setconsoleflags` like this: | ||
|
||
-- setup Windows console to handle ANSI processing on output | ||
sys.setconsoleflags(io.stdout, sys.getconsoleflags(io.stdout) + sys.COF_VIRTUAL_TERMINAL_PROCESSING) | ||
sys.setconsoleflags(io.stderr, sys.getconsoleflags(io.stderr) + sys.COF_VIRTUAL_TERMINAL_PROCESSING) | ||
|
||
-- setup Windows console to handle ANSI processing on input | ||
sys.setconsoleflags(io.stdin, sys.getconsoleflags(io.stdin) + sys.CIF_VIRTUAL_TERMINAL_INPUT) | ||
|
||
|
||
## 3.2 UTF-8 in/output and display width | ||
|
||
### 3.2.1 UTF-8 in/output | ||
|
||
Where (most) Posix systems use UTF-8 by default, Windows internally uses UTF-16. More | ||
recent versions of Lua also have UTF-8 support. So `luasystem` also focusses on UTF-8. | ||
|
||
On Windows UTF-8 output can be enabled by setting the output codepage like this: | ||
|
||
-- setup Windows output codepage to UTF-8 | ||
sys.setconsoleoutputcp(sys.CODEPAGE_UTF8) | ||
|
||
Terminal input is handled by the [`_getwchar()`](https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/getchar-getwchar) function on Windows which returns | ||
UTF-16 surrogate pairs. `luasystem` will automatically convert those to UTF-8. | ||
So when using `readkey` or `readansi` to read keyboard input no additional changes | ||
are required. | ||
|
||
### 3.2.2 UTF-8 display width | ||
|
||
Typical western characters and symbols are single width characters and will use only | ||
a single column when displayed on a terminal. However many characters from other | ||
languages/cultures or emojis require 2 columns for display. | ||
|
||
Typically the `wcwidth` function is used on Posix to check the number of columns | ||
required for display. However since Windows doesn't provide this functionality a | ||
custom implementation is included based on [the work by Markus Kuhn](http://www.cl.cam.ac.uk/~mgk25/ucs/wcwidth.c). | ||
|
||
2 functions are provided, `system.utf8cwidth` for a single character, and `system.utf8swidth` for | ||
a string. When writing terminal applications the display width is relevant to | ||
positioning the cursor properly. For an example see the [`examples/readline.lua`](../examples/readline.lua.html) file. | ||
|
||
|
||
## 3.3 reading keyboard input | ||
|
||
### 3.3.1 Non-blocking | ||
|
||
There are 2 functions for keyboard input (actually 3, if taking `system._readkey` into | ||
account): `readkey` and `readansi`. | ||
|
||
`readkey` is a low level function and should preferably not be used, it returns | ||
a byte at a time, and hence can leave stray/invalid byte sequences in the buffer if | ||
only the start of a UTF-8 or ANSI sequence is consumed. | ||
|
||
The preferred way is to use `readansi` which will parse and return entire characters in | ||
single or multiple bytes, or a full ANSI sequence. | ||
|
||
On Windows the input is read using [`_getwchar()`](https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/getchar-getwchar) which bypasses the terminal and reads | ||
the input directly from the keyboard buffer. This means however that the character is | ||
also not being echoed to the terminal (independent of the echo settings used with | ||
`system.setconsoleflags`). | ||
|
||
On Posix the traditional file approach is used, which: | ||
|
||
- is blocking by default | ||
- echoes input to the terminal | ||
- requires enter to be pressed to pass the input (canonical mode) | ||
|
||
To use non-blocking input here's how to set it up: | ||
|
||
-- setup Windows console to disable echo and line input (not required since _getwchar is used, just for consistency) | ||
sys.setconsoleflags(io.stdin, sys.getconsoleflags(io.stdin) - sys.CIF_ECHO_INPUT - sys.CIF_LINE_INPUT) | ||
|
||
-- setup Posix by disabling echo, canonical mode, and making non-blocking | ||
local of_attr = sys.tcgetattr(io.stdin) | ||
sys.tcsetattr(io.stdin, sys.TCSANOW, { | ||
lflag = of_attr.lflag - sys.L_ICANON - sys.L_ECHO, | ||
}) | ||
sys.setnonblock(io.stdin, true) | ||
|
||
|
||
Both functions require a timeout to be provided which allows for proper asynchronous | ||
code to be written. Since the underlying sleep method used is `system.sleep`, just patching | ||
that function with a coroutine based yielding one should be all that is needed to make | ||
the result work with asynchroneous coroutine schedulers. | ||
|
||
### 3.3.2 Blocking input | ||
|
||
When using traditional input method like `io.stdin:read()` (which is blocking) the echo | ||
and newline properties should be set on Windows similar to Posix. | ||
For an example see [`examples/password_input.lua`](../examples/password_input.lua.html). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
-- This example shows how to remove platform differences to create a | ||
-- cross-platform level playing field. | ||
|
||
local sys = require "system" | ||
|
||
|
||
|
||
if sys.windows then | ||
-- Windows holds multiple copies of environment variables, to ensure `getenv` | ||
-- returns what `setenv` sets we need to use the `system.getenv` instead of | ||
-- `os.getenv`. | ||
os.getenv = sys.getenv -- luacheck: ignore | ||
|
||
-- Set console output to UTF-8 encoding. | ||
sys.setconsoleoutputcp(sys.CODEPAGE_UTF8) | ||
|
||
-- Set up the terminal to handle ANSI escape sequences on Windows. | ||
if sys.isatty(io.stdout) then | ||
sys.setconsoleflags(io.stdout, sys.getconsoleflags(io.stdout) + sys.COF_VIRTUAL_TERMINAL_PROCESSING) | ||
end | ||
if sys.isatty(io.stderr) then | ||
sys.setconsoleflags(io.stderr, sys.getconsoleflags(io.stderr) + sys.COF_VIRTUAL_TERMINAL_PROCESSING) | ||
end | ||
if sys.isatty(io.stdin) then | ||
sys.setconsoleflags(io.stdin, sys.getconsoleflags(io.stdout) + sys.ENABLE_VIRTUAL_TERMINAL_INPUT) | ||
end | ||
|
||
|
||
else | ||
-- On Posix, one can set a variable to an empty string, but on Windows, this | ||
-- will remove the variable from the environment. To make this consistent | ||
-- across platforms, we will remove the variable from the environment if the | ||
-- value is an empty string. | ||
local old_setenv = sys.setenv | ||
function sys.setenv(name, value) | ||
if value == "" then value = nil end | ||
return old_setenv(name, value) | ||
end | ||
end | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
local sys = require "system" | ||
|
||
-- Print the Windows Console flags for stdin | ||
sys.listconsoleflags(io.stdin) | ||
|
||
-- Print the Posix termios flags for stdin | ||
sys.listtermflags(io.stdin) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
local sys = require "system" | ||
|
||
print [[ | ||
This example shows how to disable the "echo" of characters read to the console, | ||
useful for reading secrets from the user. | ||
]] | ||
|
||
--- Function to read from stdin without echoing the input (for secrets etc). | ||
-- It will (in a platform agnostic way) disable echo on the terminal, read the | ||
-- input, and then re-enable echo. | ||
-- @param ... Arguments to pass to `io.stdin:read()` | ||
-- @return the results of `io.stdin:read(...)` | ||
local function read_secret(...) | ||
local w_oldflags, p_oldflags | ||
|
||
if sys.isatty(io.stdin) then | ||
-- backup settings, configure echo flags | ||
w_oldflags = sys.getconsoleflags(io.stdin) | ||
p_oldflags = sys.tcgetattr(io.stdin) | ||
-- set echo off to not show password on screen | ||
assert(sys.setconsoleflags(io.stdin, w_oldflags - sys.CIF_ECHO_INPUT)) | ||
assert(sys.tcsetattr(io.stdin, sys.TCSANOW, { lflag = p_oldflags.lflag - sys.L_ECHO })) | ||
end | ||
|
||
local secret, err = io.stdin:read(...) | ||
|
||
-- restore settings | ||
if sys.isatty(io.stdin) then | ||
io.stdout:write("\n") -- Add newline after reading the password | ||
sys.setconsoleflags(io.stdin, w_oldflags) | ||
sys.tcsetattr(io.stdin, sys.TCSANOW, p_oldflags) | ||
end | ||
|
||
return secret, err | ||
end | ||
|
||
|
||
|
||
-- Get username | ||
io.write("Username: ") | ||
local username = io.stdin:read("*l") | ||
|
||
-- Get the secret | ||
io.write("Password: ") | ||
local password = read_secret("*l") | ||
|
||
-- Get domainname | ||
io.write("Domain : ") | ||
local domain = io.stdin:read("*l") | ||
|
||
|
||
-- Print the results | ||
print("") | ||
print("Here's what we got:") | ||
print(" username: " .. username) | ||
print(" password: " .. password) | ||
print(" domain : " .. domain) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
local sys = require "system" | ||
|
||
print [[ | ||
This example shows how to do a non-blocking read from the cli. | ||
]] | ||
|
||
-- setup Windows console to handle ANSI processing | ||
local of_in = sys.getconsoleflags(io.stdin) | ||
local of_out = sys.getconsoleflags(io.stdout) | ||
sys.setconsoleflags(io.stdout, sys.getconsoleflags(io.stdout) + sys.COF_VIRTUAL_TERMINAL_PROCESSING) | ||
sys.setconsoleflags(io.stdin, sys.getconsoleflags(io.stdin) + sys.CIF_VIRTUAL_TERMINAL_INPUT) | ||
|
||
-- setup Posix terminal to use non-blocking mode, and disable line-mode | ||
local of_attr = sys.tcgetattr(io.stdin) | ||
local of_block = sys.getnonblock(io.stdin) | ||
sys.setnonblock(io.stdin, true) | ||
sys.tcsetattr(io.stdin, sys.TCSANOW, { | ||
lflag = of_attr.lflag - sys.L_ICANON - sys.L_ECHO, -- disable canonical mode and echo | ||
}) | ||
|
||
-- cursor sequences | ||
local get_cursor_pos = "\27[6n" | ||
|
||
|
||
|
||
print("Press a key, or 'A' to get cursor position, 'ESC' to exit") | ||
while true do | ||
local key, keytype | ||
|
||
-- wait for a key | ||
while not key do | ||
key, keytype = sys.readansi(math.huge) | ||
end | ||
|
||
if key == "A" then io.write(get_cursor_pos); io.flush() end | ||
|
||
-- check if we got a key or ANSI sequence | ||
if keytype == "char" then | ||
-- just a key | ||
local b = key:byte() | ||
if b < 32 then | ||
key = "." -- replace control characters with a simple "." to not mess up the screen | ||
end | ||
|
||
print("you pressed: " .. key .. " (" .. b .. ")") | ||
if b == 27 then | ||
print("Escape pressed, exiting") | ||
break | ||
end | ||
|
||
elseif keytype == "ansi" then | ||
-- we got an ANSI sequence | ||
local seq = { key:byte(1, #key) } | ||
print("ANSI sequence received: " .. key:sub(2,-1), "(bytes: " .. table.concat(seq, ", ")..")") | ||
|
||
else | ||
print("unknown key type received: " .. tostring(keytype)) | ||
end | ||
end | ||
|
||
|
||
|
||
-- Clean up afterwards | ||
sys.setnonblock(io.stdin, false) | ||
sys.setconsoleflags(io.stdout, of_out) | ||
sys.setconsoleflags(io.stdin, of_in) | ||
sys.tcsetattr(io.stdin, sys.TCSANOW, of_attr) | ||
sys.setnonblock(io.stdin, of_block) |
Oops, something went wrong.