From e8e907bce24ffb79146cce8500b4d9a913b2d16b Mon Sep 17 00:00:00 2001 From: Kristian Larsson Date: Tue, 2 Apr 2024 09:19:37 +0200 Subject: [PATCH 1/5] Support term settings, canonical & echo The canonical mode is the normal mode for a terminal where input is line buffered and the buffer is managed by something else, so there might be editing capabilities. Only when enter is hit, the line is sent to us. If we want to react to individual characters, we must turn this off, which is now possible by doing `env.set_stdin(canonical=False)`. It is also possible to turn off the terminal echo with `env.set_stdin(echo=False)`. On startup, we read the terminal settings and store so that on shutdown we restore the terminal settings. --- base/builtin/env.c | 29 ++++++++++++++++++++++++++++- base/rts/rts.c | 7 +++++++ base/src/__builtin__.act | 3 +++ 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/base/builtin/env.c b/base/builtin/env.c index bda3e385..c7eecc94 100644 --- a/base/builtin/env.c +++ b/base/builtin/env.c @@ -21,7 +21,10 @@ #define GC_THREADS 1 #include +#include +#include #include + #include "env.h" #include "../rts/io.h" @@ -38,6 +41,30 @@ extern int return_val; return $R_CONT(c$cont, B_None); } +$R B_EnvD_set_stdinG_local (B_Env self, $Cont c$cont, B_bool canonical, B_bool echo) { + struct termios attr; + tcgetattr(STDIN_FILENO, &attr); + + if (canonical != NULL) { + if (fromB_bool(canonical) == true) { + attr.c_lflag |= ICANON; // Set ICANON flag + } else { + attr.c_lflag &= ~ICANON; // Remove ICANON flag + } + } + + if (echo != NULL) { + if (fromB_bool(echo) == true) { + attr.c_lflag |= ECHO; + } else { + attr.c_lflag &= ~ECHO; + } + } + + tcsetattr(STDIN_FILENO, TCSANOW, &attr); + return $R_CONT(c$cont, B_None); +} + void read_stdin(uv_stream_t *stream, ssize_t nread, const uv_buf_t *buf) { if (nread < 0){ if (nread == UV_EOF) { @@ -56,7 +83,7 @@ void read_stdin(uv_stream_t *stream, ssize_t nread, const uv_buf_t *buf) { // pin affinity here (and not earlier).. pin_actor_affinity(); uv_tty_t *tty = acton_malloc(sizeof(uv_tty_t)); - uv_tty_init(get_uv_loop(), tty, 0, 1); + uv_tty_init(get_uv_loop(), tty, STDIN_FILENO, 1); tty->data = cb; uv_read_start((uv_stream_t*)tty, alloc_buffer, read_stdin); return $R_CONT(c$cont, B_None); diff --git a/base/rts/rts.c b/base/rts/rts.c index e4160549..e394236a 100644 --- a/base/rts/rts.c +++ b/base/rts/rts.c @@ -21,6 +21,7 @@ #define GC_THREADS 1 #include +#include #include #include #include @@ -249,6 +250,8 @@ int64_t get_next_key() { remote_db_t * db = NULL; #endif +struct termios old_stdin_attr; + //////////////////////////////////////////////////////////////////////////////////////// /* @@ -2088,6 +2091,8 @@ void *$mon_socket_loop() { } void rts_shutdown() { + tcsetattr(STDIN_FILENO, TCSANOW, &old_stdin_attr); + rts_exit = 1; // 0 = main thread, rest is wthreads, thus +1 for (int i = 0; i < num_wthreads+1; i++) { @@ -2592,6 +2597,8 @@ int main(int argc, char **argv) { rqs[i].count = 0; } + tcgetattr(STDIN_FILENO, &old_stdin_attr); + // RTS startup and module is static stuff, in particular module constants // which are created during module init are static and do not need to be // scanned. We therefore use the real_malloc (not GC_malloc) so that it is diff --git a/base/src/__builtin__.act b/base/src/__builtin__.act index 614c3d57..4c8627d6 100644 --- a/base/src/__builtin__.act +++ b/base/src/__builtin__.act @@ -1057,6 +1057,9 @@ actor Env (wc: WorldCap, sc: SysCap, args: list[str]): sd = StringDecoder(on_stdin, encoding, on_error) _on_stdin_bytes(sd.decode) + action def set_stdin(canonical: ?bool, echo: ?bool) -> None: + NotImplemented + action def is_tty() -> bool: NotImplemented From e75b385586a5b24a69cb800f191ae9468581e068 Mon Sep 17 00:00:00 2001 From: Kristian Larsson Date: Thu, 4 Apr 2024 21:12:36 +0200 Subject: [PATCH 2/5] Add term.clear & term.top --- base/src/term.act | 3 +++ 1 file changed, 3 insertions(+) diff --git a/base/src/term.act b/base/src/term.act index ae0a0cd5..0b1187e4 100644 --- a/base/src/term.act +++ b/base/src/term.act @@ -50,6 +50,9 @@ bg_magenta = "\x1b[45m" bg_cyan = "\x1b[46m" bg_white = "\x1b[47m" +clear = "\x1b[0J" +top = "\x1b[H" + def up(n=1): """Move cursor up n lines. """ From fce3d5fedf1e363dae373bce39c46161c12b47e2 Mon Sep 17 00:00:00 2001 From: Kristian Larsson Date: Fri, 5 Apr 2024 12:02:44 +0200 Subject: [PATCH 3/5] AbE: explain env.set_stdin() --- CHANGELOG.md | 14 ++++++++-- docs/acton-by-example/src/SUMMARY.md | 1 + .../src/environment/interactive_stdin.md | 27 +++++++++++++++++++ 3 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 docs/acton-by-example/src/environment/interactive_stdin.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 854e6082..d5351e1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,10 +36,20 @@ to wait for the process to finish running before starting to parse its output as you'll get a single invokation and the full output rather than receive it piecemeal -- `env.is_tty()` to check if stdout is a TTY - `acton.rts.start_gc_performance_measurement()` to start a GC perf measurement - `acton.rts.get_gc_time()` to get the GC timings -- More grey shades in `term` +- `env.is_tty()` to check if stdout is a TTY +- `env.set_stdin(canonical: bool, echo: bool)` to set stdin options + - `canonical` is the default mode which is line buffered where the OS / + terminal offers line editing capabilities and we only receive the input ones + carriage return is hit + - setting `env.set_stdin(canonical=False)` turns stdin into non-canonical mode + where each character as entered is immediately forwarded to the stdin + callback so we can react to it, great for interactive applications! + - `echo` enables (the default) or disables echoing of characters +- `term` improvements: + - More grey shades + - `term.clear` && `term.top` to clear and move cursor to top ### Changed - The work dir and environment arguments of `process.Process` have been moved to diff --git a/docs/acton-by-example/src/SUMMARY.md b/docs/acton-by-example/src/SUMMARY.md index 3d06e266..edac164a 100644 --- a/docs/acton-by-example/src/SUMMARY.md +++ b/docs/acton-by-example/src/SUMMARY.md @@ -38,6 +38,7 @@ - [Environment](environment.md) - [Environment variables](environment/variables.md) - [Reading stdin input](environment/stdin.md) + - [Interactive stdin input](environment/interactive_stdin.md) - [Standard library](stdlib.md) - [Regular Expression](stdlib/re.md) diff --git a/docs/acton-by-example/src/environment/interactive_stdin.md b/docs/acton-by-example/src/environment/interactive_stdin.md new file mode 100644 index 00000000..3627edf3 --- /dev/null +++ b/docs/acton-by-example/src/environment/interactive_stdin.md @@ -0,0 +1,27 @@ +# Interactive stdin + +For interactive programs, like a text editor, input is not fed into the program +line by line, rather the program can react on individual key strokes. + +The default stdin mode is the *canonical* mode, which implies line buffering and +that there are typically line editing capabilities offered that are implemented +external to the Acton program. By setting stdin in non-canonical mode we can +instead get the raw key strokes directly fed to us. + +```python +actor main(env): + def interact(input): + print("Got some input:", input) + + # Set non-canonical mode, so we get each key stroke directly + env.set_stdin(canonical=False) + # Turn off terminal echo + env.set_stdin(echo=False) + env.stdin_install(interact) +``` + +We can also disable the echo mode with the echo option. + +The Acton run time system will copy the stdin terminal settings on startup and +restore them on exit, so you do not need to manually restore terminal echo for +example. From 186e70c62571f2cdd0481ddaf9a803a81f8e39ec Mon Sep 17 00:00:00 2001 From: Kristian Larsson Date: Fri, 5 Apr 2024 12:26:28 +0200 Subject: [PATCH 4/5] Fix StringDecoder for 1 byte input The previous code would not work for single byte input. It would get buffered, so having direct input from stdin in non-canonical didn't work properly. Now fixed! Should add some test cases though, ending is correct, right? --- base/src/__builtin__.act | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/base/src/__builtin__.act b/base/src/__builtin__.act index 4c8627d6..5d514949 100644 --- a/base/src/__builtin__.act +++ b/base/src/__builtin__.act @@ -946,10 +946,10 @@ actor StringDecoder(cb_out: action(str) -> None, encoding: ?str="utf-8", on_erro # Attempt to decode all of buf. If it fails we are likely in the middle # of a multi-byte character so we try again by removing the last bytes # iteratively until we succeed. UTF-8 has up to 4 bytes per character. - for i in range(1, MAX_UNICODE_CHAR_SIZE+1): + for i in range(len(buf), len(buf)-MAX_UNICODE_CHAR_SIZE, -1): try: - s = buf[:-i].decode() - buf = buf[-i:] + s = buf[:i].decode() + buf = buf[i:] cb_out(s) return except ValueError: From df4ab27e183640f14f1b17ba6b88ff44e2ac15f5 Mon Sep 17 00:00:00 2001 From: Kristian Larsson Date: Fri, 5 Apr 2024 12:27:41 +0200 Subject: [PATCH 5/5] Add inter.act An interactive terminal program! --- examples/inter.act | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 examples/inter.act diff --git a/examples/inter.act b/examples/inter.act new file mode 100644 index 00000000..95237169 --- /dev/null +++ b/examples/inter.act @@ -0,0 +1,37 @@ +# An interacive terminal program that increments and decrements a counter based +# on user input. The program will exit when the user types 'q'. There is also a +# periodic incrementation of the counter every 5 seconds. +# +# The actor based asynchronous I/O model of Acton makes it very natural to +# express event-driven reactive programs like this that can react to user input +# while not blocking on I/O, thus allowing for other tasks to be performed, in +# this case the periodic incrementation of the counter. + +actor main(env: Env): + var count = 0 + + def interact(input): + if input == "q": + print("Quitting!") + env.exit(0) + elif input == "i": + count += 1 + print("Incrementing! Count is now:", count) + elif input == "d": + count -= 1 + print("Decrementing! Count is now:", count) + else: + print("Unknown command:", input) + + # Set non-canonical mode, so we get each key stroke directly + env.set_stdin(canonical=False) + # Turn off terminal echo + env.set_stdin(echo=False) + env.stdin_install(interact) + print("Type 'q' to quit, 'i' to increment a counter and 'd' to decrement it.") + + def periodic(): + count += 1 + print("Periodic +1 increment. Count is now:", count) + after 5: periodic() + after 1: periodic()