From 09787553cbf1cb8207dee13c0d18c4582f807289 Mon Sep 17 00:00:00 2001 From: Kyle Evans Date: Thu, 19 Sep 2024 21:01:22 -0500 Subject: [PATCH] lib: add terminal resize capabilities This adds a new size() global, which actually lives on the process.term for each process. One or both dimensions can be changed at a time, or both arguments can be omitted to return the current size. --- lib/core/porch_lua.c | 1 + lib/core/porch_tty.c | 66 ++++++++++++++++++++++++++++++++++++++ lib/porch/scripter.lua | 6 ++++ lib/porch_lib.h | 3 ++ man/orch.5 | 21 +++++++++++- tests/resize_basic.orch | 30 +++++++++++++++++ tests/resize_optional.orch | 25 +++++++++++++++ tests/resized.sh | 22 +++++++++++++ 8 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 tests/resize_basic.orch create mode 100644 tests/resize_optional.orch create mode 100755 tests/resized.sh diff --git a/lib/core/porch_lua.c b/lib/core/porch_lua.c index 1965a66..f908e9d 100644 --- a/lib/core/porch_lua.c +++ b/lib/core/porch_lua.c @@ -663,6 +663,7 @@ porchlua_process_term_set(porch_ipc_t ipc __unused, struct porch_ipc_msg *msg, memcpy(parent_termios, child_termios, sizeof(*child_termios)); term->initialized = true; + term->winsz_valid = false; return (0); } diff --git a/lib/core/porch_tty.c b/lib/core/porch_tty.c index 9a02b73..fd41d99 100644 --- a/lib/core/porch_tty.c +++ b/lib/core/porch_tty.c @@ -319,10 +319,76 @@ porchlua_term_update(lua_State *L) return (2); } +static int +porchlua_term_size(lua_State *L) +{ + struct porch_term *self; + lua_Number val; + bool fetching; + + self = luaL_checkudata(L, 1, ORCHLUA_TERMHANDLE); + if (!self->winsz_valid) { + if (ioctl(self->proc->termctl, TIOCGWINSZ, &self->winsz) != 0) { + int error = errno; + + luaL_pushfail(L); + lua_pushstring(L, strerror(error)); + return (2); + } + + self->winsz_valid = true; + } + + /* + * If size doesn't have both width and height arguments, it simply + * return the current size. + */ + fetching = lua_isnoneornil(L, 2) && lua_isnoneornil(L, 3); + if (!fetching) { + if (!lua_isnoneornil(L, 2)) { + val = luaL_checknumber(L, 2); + if (val < 0 || val > USHRT_MAX) { + luaL_pushfail(L); + lua_pushfstring(L, "width out of bounds: %llu\n", + (uint64_t)val); + return (2); + } + + self->winsz.ws_col = val; + } + + if (!lua_isnoneornil(L, 3)) { + val = luaL_checknumber(L, 3); + if (val < 0 || val > USHRT_MAX) { + luaL_pushfail(L); + lua_pushfstring(L, "height out of bounds: %llu\n", + (uint64_t)val); + return (2); + } + + self->winsz.ws_row = val; + } + + if (ioctl(self->proc->termctl, TIOCSWINSZ, &self->winsz) != 0) { + int error = errno; + + luaL_pushfail(L); + lua_pushstring(L, strerror(error)); + return (2); + } + } + + lua_pushnumber(L, self->winsz.ws_col); + lua_pushnumber(L, self->winsz.ws_row); + + return (2); +} + #define ORCHTERM_SIMPLE(n) { #n, porchlua_term_ ## n } static const luaL_Reg porchlua_term[] = { ORCHTERM_SIMPLE(fetch), ORCHTERM_SIMPLE(update), + ORCHTERM_SIMPLE(size), { NULL, NULL }, }; diff --git a/lib/porch/scripter.lua b/lib/porch/scripter.lua index 57761ad..95c911f 100644 --- a/lib/porch/scripter.lua +++ b/lib/porch/scripter.lua @@ -376,6 +376,12 @@ function scripter.env.matcher(val) return true end +function scripter.env.size(w, h) + local current_process = current_ctx.process + + return current_process.term:size(w, h) +end + function scripter.env.timeout(val) if val == nil or val < 0 then error("Timeout must be >= 0") diff --git a/lib/porch_lib.h b/lib/porch_lib.h index 0185a77..b6c3a42 100644 --- a/lib/porch_lib.h +++ b/lib/porch_lib.h @@ -7,6 +7,7 @@ #pragma once #include +#include #include #include @@ -53,8 +54,10 @@ struct porch_process { struct porch_term { struct termios term; + struct winsize winsz; struct porch_process *proc; bool initialized; + bool winsz_valid; }; struct porchlua_tty_cntrl { diff --git a/man/orch.5 b/man/orch.5 index 1ce1589..2dc445d 100644 --- a/man/orch.5 +++ b/man/orch.5 @@ -3,7 +3,7 @@ .\" .\" SPDX-License-Identifier: BSD-2-Clause .\" -.Dd February 10, 2024 +.Dd September 19, 2024 .Dt ORCH 5 .Os .Sh NAME @@ -236,6 +236,25 @@ table's keys correspond to supported characters, e.g., and the associated values are all truthy to indicate that they are supported. .Pp This directive is enqueued, not processed immediately. +.It Fn size "width" "height" +Set or get the size of the terminal associated with the process. +If at least one of +.Fa width +or +.Fa height +are not nil, then +.Fn size +will resize that dimension of the window. +The new current size of the window is always returned. +.Pp +The window will start off on a fresh spawn with a width and height of 0. +The size of the window is never persisted across processes. +.Pp +This directive is always processed immediately, and thus should always be used +in either an +.Fn enqueue +or +fail context. .It Fn raw "boolean" Changes the raw .Fn write diff --git a/tests/resize_basic.orch b/tests/resize_basic.orch new file mode 100644 index 0000000..3963964 --- /dev/null +++ b/tests/resize_basic.orch @@ -0,0 +1,30 @@ +spawn("resized.sh") +match "ready" + +enqueue(function() + local w, h = assert(size()) + + -- pty sizes start at 0 when we spawn off. + assert(w == 0, "width is wrong, expected 0 got " .. w) + assert(h == 0, "height is wrong, expected 0 got " .. h) + + w, h = size(w + 25, h + 80) + + -- Make sure that it's returning the *new* width and height when we set + -- them. + assert(w == 25, "width is wrong, expected 25 got " .. w) + assert(h == 80, "height is wrong, expected 80 got " .. h) + + -- And not setting anything should still return the current dimensions. + w, h = size() + assert(w == 25, "width is wrong, expected 25 got " .. w) + assert(h == 80, "height is wrong, expected 80 got " .. h) + w, h = size(nil, nil) + assert(w == 25, "width is wrong, expected 25 got " .. w) + assert(h == 80, "height is wrong, expected 80 got " .. h) +end) + +match "resized" +write "^C" + +match "1" diff --git a/tests/resize_optional.orch b/tests/resize_optional.orch new file mode 100644 index 0000000..8129f2f --- /dev/null +++ b/tests/resize_optional.orch @@ -0,0 +1,25 @@ +spawn("resized.sh") +match "ready" + +enqueue(function() + local w, h = assert(size()) + + assert(w == 0, "width is wrong, expected 0 got " .. w) + assert(h == 0, "height is wrong, expected 0 got " .. h) + + w, h = size(nil, h + 80) + + assert(w == 0, "width is wrong, expected 0 got " .. w) + assert(h == 80, "height is wrong, expected 80 got " .. h) +end) + +match "resized" + +enqueue(function() + local w, h = size(25) + assert(w == 25, "width is wrong, expected 25 got " .. w) + assert(h == 80, "height is wrong, expected 80 got " .. h) +end) + +write "^C" +match "2" diff --git a/tests/resized.sh b/tests/resized.sh new file mode 100755 index 0000000..d931425 --- /dev/null +++ b/tests/resized.sh @@ -0,0 +1,22 @@ +#!/bin/sh + +loop=1 + +trap 'echo resized; caught=$((caught + 1))' WINCH +trap 'loop=0' INT + +# Release the hounds now that the signal handler is setup. +echo "ready" + +caught=0 + +while [ "$loop" -ne 0 ]; do + sleep 1 +done + +if [ "$caught" -eq 0 ]; then + 1>&2 echo "Did not observe SIGWINCH" + exit 1 +fi + +echo "$caught"