From 421f18fae39e1b954357bb9a17b07f1f8ebc5c80 Mon Sep 17 00:00:00 2001 From: maxlandon Date: Sun, 31 May 2026 02:24:15 +0200 Subject: [PATCH 01/12] example: demonstrate readline hint lanes and async completions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a "readline features" command group and a passive hint provider to the example console, exercising the new reeflective/readline APIs: - setupReadlineHints: registers rl.Hint.SetProvider, a passive hint computed from the current line (shows the matched command's short description). - notify: pushes async status updates into the transient hint lane from a goroutine (rl.Hint.SetTransient/ClearTransient) — the idle shell repaints on its own via the async-refresh wake. - hint set/clear: set or clear a sticky transient hint synchronously. - scan: async completions — a background "discovery" grows the candidate set and calls rl.RefreshCompletions() to rebuild the open menu in place, so hosts appear live with no keystroke. The feature commands are registered after the generic file-completion loop so their custom (async) completers are not overwritten. Includes cursor.cast, a screencast used while diagnosing the hint-area drift bug. Note: builds against the local readline via the go workspace; needs a readline release + go.mod bump to build standalone. Co-Authored-By: Claude Opus 4.8 (1M context) --- example/cursor.cast | 595 ++++++++++++++++++++++++++++++++++++ example/feature-commands.go | 213 +++++++++++++ example/main-commands.go | 8 + example/main.go | 4 + 4 files changed, 820 insertions(+) create mode 100644 example/cursor.cast create mode 100644 example/feature-commands.go diff --git a/example/cursor.cast b/example/cursor.cast new file mode 100644 index 0000000..1f63736 --- /dev/null +++ b/example/cursor.cast @@ -0,0 +1,595 @@ +{"version":3,"term":{"cols":150,"rows":40,"type":"xterm-256color","version":"XTerm(397)","theme":{"fg":"#ffffff","bg":"#1b1b1b","palette":"#3d3d3d:#e96d89:#3ea290:#b0ead9:#31658c:#596196:#8292b2:#c8cacc:#4d4d4d:#899aff:#52ad91:#98c9bb:#477ab3:#7882bf:#95a7cc:#edeff2"}},"timestamp":1780184942,"command":"go run .","env":{"SHELL":"/usr/sbin/zsh"}} +[0.22927, "o", "\r\n _____ __ _ _ _ _____ _\r\n | __ \\ / _| | | | (_) / ____| | |\r\n | |__) |___ ___| |_| | ___ ___| |_ ___ _____ | | ___ _ __ ___ ___ | | ___\r\n | _ // _ \\/ _ \\ _| |/ _ \\/ __| __| \\ \\ / / _ \\ | | / _ \\| '_ \\/ __|/ _ \\| |/ _ \\\r\n | | \\ \\ __/ __/ | | | __/ (__| |_| |\\ V / __/ | |___| (_) | | | \\__ \\ (_) | | __/\r\n |_| \\_\\___|\\___|_| |_|\\___|\\___|\\__|_| \\_/ \\___| \\_____\\___/|_| |_|___/\\___/|_|\\___|\r\n\r\n"] +[0.001154, "o", "\u001b[?2004h"] +[0.000029, "o", "\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\r\n"] +[0.000205, "o", "> "] +[0.0001, "o", "\u001b[5 q"] +[0.000486, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] +[0.000994, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:02.699\u001b[0m\u001b[150D\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C"] +[0.000116, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\r\r\n\u001b[2;3mtype a command — try 'notify', 'hint set ...', or 'scan '\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[1A\u001b[1A\u001b[2C"] +[0.000134, "o", "\u001b[150D\u001b[2C\u001b[?25h"] +[1.367838, "o", "\u001b[H\u001b[2J\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\r\n> \u001b[?25l\u001b[150D> \u001b[6n"] +[0.001171, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:04.068\u001b[0m\u001b[150D\u001b[2C\u001b[1B"] +[0.000166, "o", "\u001b[150D\u001b[0J\u001b[1A\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\r\r\n\u001b[2;3mtype a command — try 'notify', 'hint set ...', or 'scan '\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m"] +[0.000604, "o", "\u001b[1A\u001b[1A\u001b[2C\u001b[150D\u001b[2C\u001b[?25h"] +[0.400446, "o", "\u001b[2D\u001b[2C\u001b[6n"] +[0.000808, "o", "\u001b[150D\u001b[2C\u001b[0J \u001b[1;30m01:49:04.470\u001b[0m\u001b[150D\r\r\n"] +[0.000073, "o", "\u001b[0 q\u001b[?2004l"] +[0.000817, "o", "\u001b[?2004h\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\r\n> \u001b[5 q\u001b[?25l\u001b[150D> \u001b[6n"] +[0.001192, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:04.472\u001b[0m\u001b[150D\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A"] +[0.000069, "o", "\u001b[2C"] +[0.000097, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\r\r\n"] +[0.000168, "o", "\u001b[2;3mtype a command — try 'notify', 'hint set ...', or 'scan '\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[1A\u001b[1A\u001b[2C\u001b[150D\u001b[2C\u001b[?25h"] +[0.253221, "o", "\u001b[2D\u001b[2C\u001b[6n"] +[0.000741, "o", "\u001b[150D\u001b[2C\u001b[0J \u001b[1;30m01:49:04.727\u001b[0m\u001b[150D\r\r\n\u001b[0 q\u001b[?2004l"] +[0.001045, "o", "\u001b[?2004h\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\r\n> \u001b[5 q"] +[0.000095, "o", "\u001b[?25l\u001b[150D> "] +[0.000052, "o", "\u001b[6n"] +[0.001536, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] +[0.000061, "o", "\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:04.730\u001b[0m\u001b[150D\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C"] +[0.000126, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\r\r\n"] +[0.000038, "o", "\u001b[2;3mtype a command — try 'notify', 'hint set ...', or 'scan '\u001b[0m\u001b[0K\r\r\n"] +[0.000085, "o", "\u001b[0K\u001b[0m\u001b[1A"] +[0.00001, "o", "\u001b[1A"] +[0.00002, "o", "\u001b[2C"] +[0.000262, "o", "\u001b[150D\u001b[2C\u001b[?25h"] +[1.900381, "o", "\u001b[?25l\u001b[150D> "] +[0.00011, "o", "\u001b[6n"] +[0.000958, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] +[0.000206, "o", "n\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:06.632\u001b[0m\u001b[150D\u001b[3C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[3C"] +[0.002262, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[3C\r\r\n\u001b[0K\u001b[1m\u001b[33mreadline commands \u001b[0m\u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34mn\u001b[22m\u001b[39m\u001b[motify\u001b[0m \u001b[2m# Async status updates shown in the hint lane (transient hint + wake)\u001b[0m \u001b[0K\u001b[1A\u001b[1A\u001b[3C\u001b[150D\u001b[3C\u001b[?25h"] +[0.156857, "o", "\u001b[?25l\u001b[150D> "] +[0.000111, "o", "\u001b[6n"] +[0.000763, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2Cno\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:06.792\u001b[0m\u001b[150D\u001b[4C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[4C"] +[0.001335, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[4C\r\r\n\u001b[0K\u001b[1m\u001b[33mreadline commands \u001b[0m\u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34mno\u001b[22m\u001b[39m\u001b[mtify\u001b[0m \u001b[2m# Async status updates shown in the hint lane (transient hint + wake)\u001b[0m \u001b[0K\u001b[1A\u001b[1A\u001b[4C\u001b[150D\u001b[4C\u001b[?25h"] +[0.124228, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] +[0.000896, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] +[0.000392, "o", "not\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:06.918\u001b[0m\u001b[150D\u001b[5C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[5C"] +[0.001917, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[5C\r\r\n\u001b[0K\u001b[1m\u001b[33mreadline commands \u001b[0m\u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34mnot\u001b[22m\u001b[39m\u001b[mify\u001b[0m \u001b[2m# Async status updates shown in the hint lane (transient hint + wake)\u001b[0m \u001b[0K\u001b[1A\u001b[1A\u001b[5C\u001b[150D\u001b[5C\u001b[?25h"] +[0.158572, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] +[0.001049, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] +[0.00016, "o", "noti\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:07.080\u001b[0m\u001b[150D\u001b[6C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[6C"] +[0.002479, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[6C\r\r\n\u001b[0K\u001b[1m\u001b[33mreadline commands \u001b[0m\u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34mnoti\u001b[22m\u001b[39m\u001b[mfy\u001b[0m \u001b[2m# Async status updates shown in the hint lane (transient hint + wake)\u001b[0m \u001b[0K\u001b[1A\u001b[1A\u001b[6C\u001b[150D\u001b[6C\u001b[?25h"] +[0.137401, "o", "\u001b[?25l\u001b[150D> "] +[0.000172, "o", "\u001b[6n"] +[0.001107, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] +[0.000578, "o", "notif\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:07.222\u001b[0m\u001b[150D\u001b[7C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[7C"] +[0.004882, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[7C\r\r\n\u001b[0K\u001b[1m\u001b[33mreadline commands \u001b[0m\u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34mnotif\u001b[22m\u001b[39m\u001b[my\u001b[0m \u001b[2m# Async status updates shown in the hint lane (transient hint + wake)\u001b[0m \u001b[0K\u001b[1A\u001b[1A\u001b[7C\u001b[150D\u001b[7C\u001b[?25h"] +[0.233665, "o", "\u001b[5 q\u001b[5 q\u001b[5 q\u001b[?25l\u001b[150D> \u001b[6n"] +[0.001602, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] +[0.000122, "o", "\u001b[1m\u001b[32mnotify \u001b[39m\u001b[22m\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:07.462\u001b[0m\u001b[150D\u001b[9C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[9C"] +[0.001222, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[9C\r\r\n"] +[0.000645, "o", "\u001b[2;3m notify — Async status updates shown in the hint lane (transient hint + wake)\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[1A\u001b[1A\u001b[9C\u001b[150D\u001b[9C\u001b[?25h"] +[0.508538, "o", "\u001b[5 q\u001b[9D\u001b[2C\u001b[6n"] +[0.001335, "o", "\u001b[150D\u001b[9C\u001b[0J \u001b[1;30m01:49:07.974\u001b[0m\u001b[150D\r\r\n\u001b[0 q\u001b[?2004l"] +[0.000662, "o", "\r\n"] +[0.001799, "o", "Background job started — watch the hint line below the prompt update on its own (no keystroke needed).\r\n\r\n"] +[0.000482, "o", "\u001b[?2004h\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\r\n> \u001b[5 q"] +[0.000242, "o", "\u001b[?25l"] +[0.000219, "o", "\u001b[150D> \u001b[6n"] +[0.000715, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:07.978\u001b[0m\u001b[150D\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C"] +[0.000098, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\r\r\n\u001b[2;3mtype a command — try 'notify', 'hint set ...', or 'scan '\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[1A\u001b[1A\u001b[2C\u001b[150D\u001b[2C\u001b[?25h"] +[1.199173, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] +[0.001072, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:09.178\u001b[0m\u001b[150D\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C"] +[0.000121, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\r\r\n\u001b[2;3mtype a command — try 'notify', 'hint set ...', or 'scan '\u001b[0m\u001b[0K\r\r\n\u001b[33m⠋ connecting…\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m"] +[0.000266, "o", "\u001b[3A\u001b[1A\u001b[2C\u001b[150D\u001b[2C\u001b[?25h"] +[1.198578, "o", "\u001b[?25l\u001b[150D> "] +[0.000168, "o", "\u001b[6n"] +[0.000739, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:10.378\u001b[0m\u001b[150D\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\r\r\n\u001b[2;3mtype a command — try 'notify', 'hint set ...', or 'scan '\u001b[0m\u001b[0K\r\r\n\u001b[33m⠙ authenticating…\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[3A\u001b[1A\u001b[2C\u001b[150D\u001b[2C\u001b[?25h"] +[1.199802, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] +[0.001404, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:11.579\u001b[0m\u001b[150D\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A"] +[0.00025, "o", "\u001b[2C\r\r\n\u001b[2;3mtype a command — try 'notify', 'hint set ...', or 'scan '\u001b[0m\u001b[0K\r\r\n\u001b[33m⠹ transferring…\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m"] +[0.000127, "o", "\u001b[3A\u001b[1A\u001b[2C\u001b[150D\u001b[2C\u001b[?25h"] +[1.198192, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] +[0.000521, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n"] +[0.000073, "o", "\u001b[2C"] +[0.000427, "o", "\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:12.779\u001b[0m"] +[0.000064, "o", "\u001b[150D\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C"] +[0.000162, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\r\r\n\u001b[2;3mtype a command — try 'notify', 'hint set ...', or 'scan '\u001b[0m\u001b[0K\r\r\n\u001b[32m✓ transfer complete\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[3A"] +[0.000667, "o", "\u001b[1A\u001b[2C\u001b[150D\u001b[2C\u001b[?25h"] +[1.498669, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] +[0.000934, "o", "\u001b[150D"] +[0.000547, "o", "\u001b[1A"] +[0.000699, "o", "\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:14.281\u001b[0m\u001b[150D\u001b[2C"] +[0.000102, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C"] +[0.000351, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\r\r\n"] +[0.000102, "o", "\u001b[2;3mtype a command — try 'notify', 'hint set ...', or 'scan '\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[1A\u001b[1A\u001b[2C\u001b[150D\u001b[2C\u001b[?25h"] +[2.136457, "o", "\u001b[2D\u001b[2C\u001b[6n"] +[0.001127, "o", "\u001b[150D\u001b[2C\u001b[0J \u001b[1;30m01:49:16.419\u001b[0m\u001b[150D\r\r\n\u001b[0 q\u001b[?2004l"] +[0.000324, "o", "\u001b[?2004h"] +[0.000321, "o", "\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\r\n> \u001b[5 q"] +[0.000259, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] +[0.000581, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:16.421\u001b[0m\u001b[150D\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C"] +[0.000052, "o", "\u001b[1B"] +[0.000147, "o", "\u001b[150D"] +[0.000301, "o", "\u001b[0J\u001b[1A\u001b[2C\r\r\n\u001b[2;3mtype a command — try 'notify', 'hint set ...', or 'scan '\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[1A\u001b[1A\u001b[2C\u001b[150D\u001b[2C"] +[0.000097, "o", "\u001b[?25h"] +[0.303004, "o", "\u001b[2D\u001b[2C\u001b[6n"] +[0.001, "o", "\u001b[150D\u001b[2C\u001b[0J \u001b[1;30m01:49:16.725\u001b[0m\u001b[150D\r\r\n"] +[0.000262, "o", "\u001b[0 q\u001b[?2004l"] +[0.000431, "o", "\u001b[?2004h\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\r\n> \u001b[5 q"] +[0.000091, "o", "\u001b[?25l\u001b[150D"] +[0.000179, "o", "> \u001b[6n"] +[0.00114, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] +[0.000085, "o", "\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:16.727\u001b[0m\u001b[150D\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C"] +[0.000171, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\r\r\n\u001b[2;3mtype a command — try 'notify', 'hint set ...', or 'scan '\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[1A\u001b[1A\u001b[2C\u001b[150D\u001b[2C\u001b[?25h"] +[0.49948, "o", "\u001b[5 q"] +[0.001632, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] +[0.001218, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:17.229\u001b[0m\u001b[150D\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\r\r\n\u001b[2;3mtype a command — try 'notify', 'hint set ...', or 'scan '\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m"] +[0.001198, "o", "\u001b[0K\u001b[1m\u001b[33mfilesystem commands \u001b[0m\u001b[0K\r\r\n\u001b[mbackup\u001b[0m \u001b[2m# Create a backup of a file or directory\u001b[0m \u001b[0K\r\r\n\u001b[mconvert\u001b[0m \u001b[2m# Convert a file\u001b[0m \u001b[0K\r\r\n\u001b[mdownload\u001b[0m \u001b[2m# Download a file from a URL\u001b[0m \u001b[0K\r\r\n\u001b[mencrypt\u001b[0m \u001b[2m# Encrypt a file\u001b[0m \u001b[0K\r\r\n\u001b[mls\u001b[0m \u001b[2m# List directory contents\u001b[0m \u001b[0K\r\r\n\u001b[mmkdir\u001b[0m \u001b[2m# Create directories\u001b[0m \u001b[0K\r\r\n\u001b[mrename\u001b[0m \u001b[2m# Rename a file\u001b[0m \u001b[0K\r\r\n\u001b[msearch\u001b[0m \u001b[2m# Search for a query\u001b[0m \u001b[0K\r\r\n\u001b[1m\u001b[33mcore commands \u001b[0m\u001b[0K\r\r\n\u001b[mclient\u001b[0m \u001b[2m# Switch to the client menu (also works with CtrlC)\u001b[0m \u001b[0K\r\r\n\u001b[mexit\u001b[0m \u001b[2m# Exit the console application\u001b[0m \u001b[0K\r\r\n\u001b[mgreet\u001b[0m \u001b[2m# Greet a person\u001b[0m \u001b[0K\r\r\n\u001b[mhello\u001b[0m \u001b[2m# Say hello with customizable message\u001b[0m \u001b[0K\r\r\n\u001b[mhelp\u001b[0m \u001b[2m# Help about any command\u001b[0m \u001b[0K\r\r\n\u001b[1m\u001b[33mdeployment commands \u001b[0m\u001b[0K\r\r\n\u001b[mdeploy\u001b[0m \u001b[2m# Deploy a file\u001b[0m \u001b[0K\r\r\n\u001b[1m\u001b[33mtools commands \u001b[0m\u001b[0K\r\r\n\u001b[mgit\u001b[0m \u001b[2m# Git command\u001b[0m \u001b[0K\r\r\n\u001b[mssh\u001b[0m \u001b[2m# SSH client\u001b[0m \u001b[0K\r\r\n\u001b[1m\u001b[33mreadline commands \u001b[0m\u001b[0K\r\r\n\u001b[mhint\u001b[0m \u001b[2m# Set or clear a sticky transient hint immediately (non-async)\u001b[0m \u001b[0K\r\r\n\u001b[mnotify\u001b[0m \u001b[2m# Async status updates shown in the hint lane (transient hint + wake)\u001b[0m \u001b[0K\r\r\n\u001b[mscan\u001b[0m \u001b[2m# Async completions — press Tab after 'scan ' and watch hosts appear live\u001b[0m \u001b[0K\r\r\n\u001b[1m\u001b[33mother commands \u001b[0m\u001b[0K\r\r\n\u001b[mreadline\u001b[0m \u001b[2m# Manipulate readline options, keymaps and bindings\u001b[0m \u001b[0K\u001b[25A\u001b[1A\u001b[1A\u001b[2C\u001b[150D\u001b[2C\u001b[?25h"] +[1.027009, "o", "\u001b[5 q\u001b[?25l\u001b[150D> \u001b[6n"] +[0.001044, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] +[0.000109, "o", "s\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:18.259\u001b[0m\u001b[150D\u001b[3C\u001b[1B\u001b[150D\u001b[0J\u001b[1A"] +[0.000055, "o", "\u001b[3C"] +[0.002292, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[3C\r\r\n"] +[0.000132, "o", "\u001b[0K\u001b[1m\u001b[33mreadline commands \u001b[0m\u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34ms\u001b[22m\u001b[39m\u001b[mcan\u001b[0m \u001b[2m# Async completions — press Tab after 'scan ' and watch hosts appear live\u001b[0m \u001b[0K\r\r\n\u001b[1m\u001b[33mfilesystem commands \u001b[0m\u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34ms\u001b[22m\u001b[39m\u001b[mearch\u001b[0m \u001b[2m# Search for a query\u001b[0m \u001b[0K\r\r\n\u001b[1m\u001b[33mtools commands \u001b[0m\u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34ms\u001b[22m\u001b[39m\u001b[msh\u001b[0m \u001b[2m# SSH client\u001b[0m \u001b[0K\u001b[5A\u001b[1A\u001b[3C\u001b[150D\u001b[3C\u001b[?25h"] +[0.227561, "o", "\u001b[?25l\u001b[150D> "] +[0.000088, "o", "\u001b[6n"] +[0.000682, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] +[0.000377, "o", "sc\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:18.490\u001b[0m\u001b[150D"] +[0.000225, "o", "\u001b[4C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[4C"] +[0.001889, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[4C\r\r\n\u001b[0K\u001b[1m\u001b[33mreadline commands \u001b[0m\u001b[0K\r\r\n"] +[0.000095, "o", "\u001b[m\u001b[1m\u001b[34msc\u001b[22m\u001b[39m\u001b[man\u001b[0m \u001b[2m# Async completions — press Tab after 'scan ' and watch hosts appear live\u001b[0m \u001b[0K\u001b[1A\u001b[1A\u001b[4C\u001b[150D\u001b[4C\u001b[?25h"] +[0.112281, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] +[0.00088, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2Csca\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:18.606\u001b[0m\u001b[150D\u001b[5C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[5C"] +[0.00109, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[5C\r\r\n\u001b[0K\u001b[1m\u001b[33mreadline commands \u001b[0m\u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34msca\u001b[22m\u001b[39m\u001b[mn\u001b[0m \u001b[2m# Async completions — press Tab after 'scan ' and watch hosts appear live\u001b[0m \u001b[0K\u001b[1A\u001b[1A\u001b[5C\u001b[150D\u001b[5C\u001b[?25h"] +[0.208441, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] +[0.000975, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[1m\u001b[32mscan\u001b[39m\u001b[22m\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:18.816\u001b[0m\u001b[150D\u001b[6C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[6C"] +[0.001349, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[6C\r\r\n\u001b[2;3m scan — Async completions — press Tab after 'scan ' and watch hosts appear live\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[0K\u001b[1m\u001b[33mreadline commands \u001b[0m\u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34mscan\u001b[22m\u001b[39m\u001b[m\u001b[0m \u001b[2m# Async completions — press Tab after 'scan ' and watch hosts appear live\u001b[0m \u001b[0K\u001b[1A\u001b[1A\u001b[1A\u001b[6C\u001b[150D\u001b[6C\u001b[?25h"] +[0.557426, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] +[0.000435, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[1m\u001b[32mscan \u001b[39m\u001b[22m\u001b[0m\u001b[0K\u001b[49m"] +[0.000835, "o", " \u001b[1;30m01:49:19.376\u001b[0m\u001b[150D\u001b[7C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[7C"] +[0.000184, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[7C\r\r\n\u001b[2;3m scan — Async completions — press Tab after 'scan ' and watch hosts appear live\u001b[0m\u001b[0K\r\r\n\u001b[2mscan [HOST]\u001b[0m\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m"] +[0.000068, "o", "\u001b[0K\u001b[mgateway\u001b[0m \u001b[0m\u001b[mlocalhost\u001b[0m \u001b[0m\u001b[0K\u001b[3A\u001b[1A\u001b[7C\u001b[150D\u001b[7C\u001b[?25h"] +[0.900054, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] +[0.000945, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] +[0.000209, "o", "\u001b[1m\u001b[32mscan \u001b[39m\u001b[22m\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:20.278\u001b[0m\u001b[150D\u001b[7C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[7C"] +[0.002928, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[7C\r\r\n\u001b[2;3m scan — Async completions — press Tab after 'scan ' and watch hosts appear live\u001b[0m\u001b[0K\r\r\n\u001b[2mscan [HOST]\u001b[0m\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[0K\u001b[m10.0.0.1\u001b[0m \u001b[0m\u001b[mgateway\u001b[0m \u001b[0m\u001b[mlocalhost\u001b[0m \u001b[0m\u001b[0K\u001b[3A\u001b[1A\u001b[7C\u001b[150D\u001b[7C\u001b[?25h"] +[0.895915, "o", "\u001b[?25l\u001b[150D"] +[0.000118, "o", "> "] +[0.000494, "o", "\u001b[6n"] +[0.001276, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[1m\u001b[32mscan \u001b[39m\u001b[22m\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:21.178\u001b[0m\u001b[150D\u001b[7C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[7C"] +[0.00057, "o", "\u001b[1B\u001b[150D"] +[0.000192, "o", "\u001b[0J\u001b[1A\u001b[7C\r\r\n\u001b[2;3m scan — Async completions — press Tab after 'scan ' and watch hosts appear live\u001b[0m\u001b[0K\r\r\n\u001b[2mscan [HOST]\u001b[0m\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[0K\u001b[m10.0.0.1\u001b[0m \u001b[0m\u001b[m10.0.0.2\u001b[0m \u001b[0m\u001b[mgateway\u001b[0m \u001b[0m\u001b[mlocalhost\u001b[0m \u001b[0m\u001b[0K\u001b[3A\u001b[1A\u001b[7C\u001b[150D\u001b[7C\u001b[?25h"] +[0.898603, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] +[0.000936, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[1m\u001b[32mscan \u001b[39m\u001b[22m\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:22.079\u001b[0m\u001b[150D"] +[0.000107, "o", "\u001b[7C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[7C"] +[0.001268, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[7C\r\r\n\u001b[2;3m scan — Async completions — press Tab after 'scan ' and watch hosts appear live\u001b[0m\u001b[0K\r\r\n\u001b[2mscan [HOST]\u001b[0m\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m"] +[0.000149, "o", "\u001b[0K\u001b[m10.0.0.1\u001b[0m \u001b[0m\u001b[m10.0.0.2\u001b[0m \u001b[0m\u001b[m10.0.0.3\u001b[0m \u001b[0m\u001b[mgateway\u001b[0m \u001b[0m\u001b[mlocalhost\u001b[0m \u001b[0m\u001b[0K\u001b[3A\u001b[1A\u001b[7C\u001b[150D\u001b[7C\u001b[?25h"] +[0.897459, "o", "\u001b[?25l\u001b[150D> "] +[0.000191, "o", "\u001b[6n"] +[0.001205, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[1m\u001b[32mscan \u001b[39m\u001b[22m\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:22.979\u001b[0m\u001b[150D\u001b[7C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[7C"] +[0.001653, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[7C\r\r\n\u001b[2;3m scan — Async completions — press Tab after 'scan ' and watch hosts appear live\u001b[0m\u001b[0K\r\r\n\u001b[2mscan [HOST]\u001b[0m\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[0K\u001b[m10.0.0.1\u001b[0m \u001b[0m\u001b[m10.0.0.2\u001b[0m \u001b[0m\u001b[m10.0.0.3\u001b[0m \u001b[0m\u001b[m10.0.0.4\u001b[0m \u001b[0m\u001b[mgateway\u001b[0m \u001b[0m\u001b[mlocalhost\u001b[0m \u001b[0m\u001b[0K\u001b[3A\u001b[1A\u001b[7C\u001b[150D\u001b[7C\u001b[?25h"] +[0.897222, "o", "\u001b[?25l\u001b[150D> "] +[0.000216, "o", "\u001b[6n"] +[0.000942, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] +[0.000168, "o", "\u001b[1m\u001b[32mscan \u001b[39m\u001b[22m\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:23.879\u001b[0m\u001b[150D\u001b[7C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[7C"] +[0.001748, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[7C\r\r\n\u001b[2;3m scan — Async completions — press Tab after 'scan ' and watch hosts appear live\u001b[0m\u001b[0K\r\r\n\u001b[2mscan [HOST]\u001b[0m\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[0K\u001b[m10.0.0.1\u001b[0m \u001b[0m\u001b[m10.0.0.2\u001b[0m \u001b[0m\u001b[m10.0.0.3\u001b[0m \u001b[0m\u001b[m10.0.0.4\u001b[0m \u001b[0m\u001b[m10.0.0.5\u001b[0m \u001b[0m\u001b[mgateway\u001b[0m \u001b[0m\u001b[mlocalhost\u001b[0m \u001b[0m\u001b[0K\u001b[3A\u001b[1A\u001b[7C\u001b[150D\u001b[7C\u001b[?25h"] +[0.731148, "o", "\u001b[5 q\u001b[?25l\u001b[150D> \u001b[6n"] +[0.00111, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[1m\u001b[32mscan \u001b[39m\u001b[22m10.0.0.1 \u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:24.613\u001b[0m\u001b[150D\u001b[16C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[16C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[16C\r\r\n\u001b[2;3m scan — Async completions — press Tab after 'scan ' and watch hosts appear live\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[0K\u001b[48;05;255m\u001b[1;40m10.0.0.1 \u001b[0m\u001b[m10.0.0.2\u001b[0m \u001b[0m\u001b[m10.0.0.3\u001b[0m \u001b[0m\u001b[m10.0.0.4\u001b[0m \u001b[0m\u001b[m10.0.0.5\u001b[0m \u001b[0m\u001b[mgateway\u001b[0m \u001b[0m\u001b[mlocalhost\u001b[0m \u001b[0m\u001b[0K\u001b[1A\u001b[1A\u001b[16C\u001b[150D\u001b[16C\u001b[?25h"] +[0.164697, "o", "\u001b[?25l\u001b[150D"] +[0.000047, "o", "> \u001b[6n"] +[0.000874, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[1m\u001b[32mscan \u001b[39m\u001b[22m10.0.0.1 \u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:24.779\u001b[0m\u001b[150D\u001b[16C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[16C"] +[0.000054, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[16C\r\r\n"] +[0.000096, "o", "\u001b[2;3m scan — Async completions — press Tab after 'scan ' and watch hosts appear live\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[0K\u001b[48;05;255m\u001b[1;40m10.0.0.1 \u001b[0m\u001b[m10.0.0.2\u001b[0m \u001b[0m\u001b[m10.0.0.3\u001b[0m \u001b[0m\u001b[m10.0.0.4\u001b[0m \u001b[0m\u001b[m10.0.0.5\u001b[0m \u001b[0m\u001b[mgateway\u001b[0m \u001b[0m\u001b[mlocalhost\u001b[0m \u001b[0m\u001b[0K\u001b[1A\u001b[1A\u001b[16C\u001b[150D\u001b[16C\u001b[?25h"] +[0.899372, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] +[0.001166, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] +[0.000362, "o", "\u001b[1m\u001b[32mscan \u001b[39m\u001b[22m10.0.0.1 \u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:25.680\u001b[0m\u001b[150D\u001b[16C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[16C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[16C\r\r\n"] +[0.000246, "o", "\u001b[2;3m scan — Async completions — press Tab after 'scan ' and watch hosts appear live\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[0K\u001b[48;05;255m\u001b[1;40m10.0.0.1 \u001b[0m\u001b[m10.0.0.2\u001b[0m \u001b[0m\u001b[m10.0.0.3\u001b[0m \u001b[0m\u001b[m10.0.0.4\u001b[0m \u001b[0m\u001b[m10.0.0.5\u001b[0m \u001b[0m\u001b[mgateway\u001b[0m \u001b[0m\u001b[mlocalhost\u001b[0m \u001b[0m\u001b[0K\u001b[1A\u001b[1A\u001b[16C\u001b[150D\u001b[16C\u001b[?25h"] +[0.898206, "o", "\u001b[?25l\u001b[150D> "] +[0.000057, "o", "\u001b[6n"] +[0.001162, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[1m\u001b[32mscan \u001b[39m\u001b[22m10.0.0.1 \u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:26.580\u001b[0m\u001b[150D\u001b[16C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[16C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[16C\r\r\n"] +[0.000022, "o", "\u001b[2;3m scan — Async completions — press Tab after 'scan ' and watch hosts appear live\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[0K\u001b[48;05;255m\u001b[1;40m10.0.0.1 \u001b[0m\u001b[m10.0.0.2\u001b[0m \u001b[0m\u001b[m10.0.0.3\u001b[0m \u001b[0m\u001b[m10.0.0.4\u001b[0m \u001b[0m\u001b[m10.0.0.5\u001b[0m \u001b[0m\u001b[mgateway\u001b[0m \u001b[0m\u001b[mlocalhost\u001b[0m \u001b[0m\u001b[0K\u001b[1A\u001b[1A\u001b[16C\u001b[150D\u001b[16C\u001b[?25h"] +[1.505176, "o", "\u001b[5 q\u001b[?25l\u001b[150D> \u001b[6n"] +[0.000825, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[1m\u001b[32mscan \u001b[39m\u001b[22m10.0.0.\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:28.086\u001b[0m\u001b[150D\u001b[14C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[14C"] +[0.000518, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[14C\r\r\n\u001b[2;3m scan — Async completions — press Tab after 'scan ' and watch hosts appear live\u001b[0m\u001b[0K\r\r\n\u001b[2mscan [HOST]\u001b[0m\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m"] +[0.000029, "o", "\u001b[3A\u001b[1A\u001b[14C\u001b[150D\u001b[14C\u001b[?25h"] +[0.17032, "o", "\u001b[?25l"] +[0.000212, "o", "\u001b[150D> "] +[0.000062, "o", "\u001b[6n"] +[0.000978, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[1m\u001b[32mscan \u001b[39m\u001b[22m10.0.0\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:28.258\u001b[0m\u001b[150D"] +[0.0002, "o", "\u001b[13C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[13C"] +[0.00132, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[13C\r\r\n\u001b[2;3m scan — Async completions — press Tab after 'scan ' and watch hosts appear live\u001b[0m\u001b[0K\r\r\n\u001b[2mscan [HOST]\u001b[0m\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[3A\u001b[1A\u001b[13C\u001b[150D\u001b[13C\u001b[?25h"] +[0.174161, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] +[0.001698, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[1m\u001b[32mscan \u001b[39m\u001b[22m10.0.\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:28.435\u001b[0m\u001b[150D\u001b[12C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[12C"] +[0.0011, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[12C\r\r\n\u001b[2;3m scan — Async completions — press Tab after 'scan ' and watch hosts appear live\u001b[0m\u001b[0K\r\r\n\u001b[2mscan [HOST]\u001b[0m\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[3A\u001b[1A\u001b[12C\u001b[150D\u001b[12C\u001b[?25h"] +[0.162056, "o", "\u001b[?25l\u001b[150D> "] +[0.000136, "o", "\u001b[6n"] +[0.00081, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] +[0.000069, "o", "\u001b[1m\u001b[32mscan \u001b[39m\u001b[22m10.0\u001b[0m\u001b[0K\u001b[49m"] +[0.000277, "o", " \u001b[1;30m01:49:28.599\u001b[0m\u001b[150D\u001b[11C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[11C"] +[0.001347, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[11C\r\r\n\u001b[2;3m scan — Async completions — press Tab after 'scan ' and watch hosts appear live\u001b[0m\u001b[0K\r\r\n\u001b[2mscan [HOST]\u001b[0m\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[3A\u001b[1A\u001b[11C\u001b[150D\u001b[11C\u001b[?25h"] +[0.173528, "o", "\u001b[?25l\u001b[150D"] +[0.000113, "o", "> \u001b[6n"] +[0.000934, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[1m\u001b[32mscan \u001b[39m\u001b[22m10.\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:28.776\u001b[0m\u001b[150D"] +[0.000028, "o", "\u001b[10C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[10C"] +[0.00346, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[10C\r\r\n\u001b[2;3m scan — Async completions — press Tab after 'scan ' and watch hosts appear live\u001b[0m\u001b[0K\r\r\n\u001b[2mscan [HOST]\u001b[0m\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[3A\u001b[1A\u001b[10C\u001b[150D\u001b[10C\u001b[?25h"] +[0.176575, "o", "\u001b[?25l\u001b[150D> "] +[0.000341, "o", "\u001b[6n"] +[0.0013, "o", "\u001b[150D\u001b[1A"] +[0.000417, "o", "\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[1m\u001b[32mscan \u001b[39m\u001b[22m10\u001b[0m\u001b[0K\u001b[49m"] +[0.000101, "o", " \u001b[1;30m01:49:28.958\u001b[0m\u001b[150D\u001b[9C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[9C"] +[0.001305, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[9C\r\r\n\u001b[2;3m scan — Async completions — press Tab after 'scan ' and watch hosts appear live\u001b[0m\u001b[0K\r\r\n\u001b[2mscan [HOST]\u001b[0m\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[3A\u001b[1A\u001b[9C\u001b[150D\u001b[9C\u001b[?25h"] +[0.027352, "o", "\u001b[?25l\u001b[150D> "] +[0.000079, "o", "\u001b[6n"] +[0.000829, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[1m\u001b[32mscan \u001b[39m\u001b[22m10\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:28.987\u001b[0m\u001b[150D\u001b[9C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[9C"] +[0.000737, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[9C\r\r\n\u001b[2;3m scan — Async completions — press Tab after 'scan ' and watch hosts appear live\u001b[0m\u001b[0K\r\r\n\u001b[2mscan [HOST]\u001b[0m\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[0K\u001b[m\u001b[1m\u001b[34m10\u001b[22m\u001b[39m\u001b[m.0.0.1\u001b[0m \u001b[0m\u001b[0K\u001b[3A\u001b[1A\u001b[9C\u001b[150D\u001b[9C\u001b[?25h"] +[0.14891, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] +[0.00102, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[1m\u001b[32mscan \u001b[39m\u001b[22m\u001b[0m\u001b[0K\u001b[49m"] +[0.00016, "o", " \u001b[1;30m01:49:29.138\u001b[0m\u001b[150D\u001b[7C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[7C"] +[0.000913, "o", "\u001b[1B\u001b[150D\u001b[0J"] +[0.000328, "o", "\u001b[1A\u001b[7C\r\r\n\u001b[2;3m scan — Async completions — press Tab after 'scan ' and watch hosts appear live\u001b[0m\u001b[0K\r\r\n\u001b[2mscan [HOST]\u001b[0m\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[0K\u001b[m10.0.0.1\u001b[0m \u001b[0m\u001b[mgateway\u001b[0m \u001b[0m\u001b[mlocalhost\u001b[0m \u001b[0m\u001b[0K\u001b[3A\u001b[1A\u001b[7C\u001b[150D\u001b[7C\u001b[?25h"] +[0.192664, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] +[0.000779, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:29.333\u001b[0m\u001b[150D\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\r\r\n\u001b[2;3mtype a command — try 'notify', 'hint set ...', or 'scan '\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[1A\u001b[1A\u001b[2C\u001b[150D\u001b[2C\u001b[?25h"] +[0.205749, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] +[0.000744, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:29.539\u001b[0m\u001b[150D\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\r\r\n"] +[0.000078, "o", "\u001b[2;3mtype a command — try 'notify', 'hint set ...', or 'scan '\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[1A\u001b[1A\u001b[2C\u001b[150D\u001b[2C\u001b[?25h"] +[0.209697, "o", "\u001b[H\u001b[2J\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\r\n> \u001b[?25l\u001b[150D> \u001b[6n"] +[0.00108, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] +[0.000041, "o", "\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:29.750\u001b[0m\u001b[150D\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\u001b[1B"] +[0.000313, "o", "\u001b[150D\u001b[0J\u001b[1A\u001b[2C\r\r\n\u001b[2;3mtype a command — try 'notify', 'hint set ...', or 'scan '\u001b[0m\u001b[0K\r\r\n"] +[0.000164, "o", "\u001b[0K\u001b[0m"] +[0.000035, "o", "\u001b[1A"] +[0.000149, "o", "\u001b[1A"] +[0.000101, "o", "\u001b[2C\u001b[150D\u001b[2C\u001b[?25h"] +[0.13543, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] +[0.001023, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:29.887\u001b[0m\u001b[150D\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\r\r\n\u001b[2;3mtype a command — try 'notify', 'hint set ...', or 'scan '\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[1A\u001b[1A\u001b[2C\u001b[150D\u001b[2C\u001b[?25h"] +[0.259924, "o", "\u001b[2D\u001b[2C\u001b[6n"] +[0.001132, "o", "\u001b[150D\u001b[2C\u001b[0J \u001b[1;30m01:49:30.148\u001b[0m\u001b[150D\r\r\n\u001b[0 q\u001b[?2004l"] +[0.000501, "o", "\u001b[?2004h\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\r\n> "] +[0.000078, "o", "\u001b[5 q\u001b[?25l\u001b[150D"] +[0.000056, "o", "> \u001b[6n"] +[0.001296, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:30.150\u001b[0m\u001b[150D\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\u001b[1B\u001b[150D"] +[0.000052, "o", "\u001b[0J\u001b[1A\u001b[2C\r\r\n"] +[0.000104, "o", "\u001b[2;3mtype a command — try 'notify', 'hint set ...', or 'scan '\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m"] +[0.000227, "o", "\u001b[1A\u001b[1A\u001b[2C\u001b[150D\u001b[2C\u001b[?25h"] +[0.22294, "o", "\u001b[2D\u001b[2C\u001b[6n"] +[0.001, "o", "\u001b[150D\u001b[2C\u001b[0J \u001b[1;30m01:49:30.375\u001b[0m\u001b[150D\r\r\n\u001b[0 q\u001b[?2004l"] +[0.000787, "o", "\u001b[?2004h\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\r\n> \u001b[5 q\u001b[?25l"] +[0.00002, "o", "\u001b[150D"] +[0.000188, "o", "> \u001b[6n"] +[0.001014, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] +[0.00007, "o", "\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:30.377\u001b[0m\u001b[150D\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C"] +[0.000119, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\r\r\n\u001b[2;3mtype a command — try 'notify', 'hint set ...', or 'scan '\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m"] +[0.000065, "o", "\u001b[1A\u001b[1A\u001b[2C\u001b[150D\u001b[2C\u001b[?25h"] +[0.387653, "o", "\u001b[5 q"] +[0.00084, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] +[0.000944, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:30.766\u001b[0m\u001b[150D\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A"] +[0.000147, "o", "\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\r\r\n\u001b[2;3mtype a command — try 'notify', 'hint set ...', or 'scan '\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m"] +[0.000107, "o", "\u001b[0K\u001b[1m\u001b[33mfilesystem commands \u001b[0m\u001b[0K\r\r\n\u001b[mbackup\u001b[0m \u001b[2m# Create a backup of a file or directory\u001b[0m \u001b[0K\r\r\n\u001b[mconvert\u001b[0m \u001b[2m# Convert a file\u001b[0m \u001b[0K\r\r\n\u001b[mdownload\u001b[0m \u001b[2m# Download a file from a URL\u001b[0m \u001b[0K\r\r\n\u001b[mencrypt\u001b[0m \u001b[2m# Encrypt a file\u001b[0m \u001b[0K\r\r\n\u001b[mls\u001b[0m \u001b[2m# List directory contents\u001b[0m \u001b[0K\r\r\n\u001b[mmkdir\u001b[0m \u001b[2m# Create directories\u001b[0m \u001b[0K\r\r\n\u001b[mrename\u001b[0m \u001b[2m# Rename a file\u001b[0m \u001b[0K\r\r\n\u001b[msearch\u001b[0m \u001b[2m# Search for a query\u001b[0m \u001b[0K\r\r\n\u001b[1m\u001b[33mcore commands \u001b[0m\u001b[0K\r\r\n\u001b[mclient\u001b[0m \u001b[2m# Switch to the client menu (also works with CtrlC)\u001b[0m \u001b[0K\r\r\n\u001b[mexit\u001b[0m \u001b[2m# Exit the console application\u001b[0m \u001b[0K\r\r\n\u001b[mgreet\u001b[0m \u001b[2m# Greet a person\u001b[0m \u001b[0K\r\r\n\u001b[mhello\u001b[0m \u001b[2m# Say hello with customizable message\u001b[0m \u001b[0K\r\r\n\u001b[mhelp\u001b[0m \u001b[2m# Help about any command\u001b[0m \u001b[0K\r\r\n\u001b[1m\u001b[33mdeployment commands \u001b[0m\u001b[0K\r\r\n\u001b[mdeploy\u001b[0m \u001b[2m# Deploy a file\u001b[0m \u001b[0K\r\r\n\u001b[1m\u001b[33mtools commands \u001b[0m\u001b[0K\r\r\n\u001b[mgit\u001b[0m \u001b[2m# Git command\u001b[0m \u001b[0K\r\r\n\u001b[mssh\u001b[0m \u001b[2m# SSH client\u001b[0m \u001b[0K\r\r\n\u001b[1m\u001b[33mreadline commands \u001b[0m\u001b[0K\r\r\n\u001b[mhint\u001b[0m \u001b[2m# Set or clear a sticky transient hint immediately (non-async)\u001b[0m \u001b[0K\r\r\n\u001b[mnotify\u001b[0m \u001b[2m# Async status updates shown in the hint lane (transient hint + wake)\u001b[0m \u001b[0K\r\r\n\u001b[mscan\u001b[0m \u001b[2m# Async completions — press Tab after 'scan ' and watch hosts appear live\u001b[0m \u001b[0K\r\r\n\u001b[1m\u001b[33mother commands \u001b[0m\u001b[0K\r\r\n\u001b[mreadline\u001b[0m \u001b[2m# Manipulate readline options, keymaps and bindings\u001b[0m \u001b[0K\u001b[25A\u001b[1A\u001b[1A\u001b[2C\u001b[150D\u001b[2C\u001b[?25h"] +[0.020934, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] +[0.000959, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:30.788\u001b[0m\u001b[150D\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\r\r\n\u001b[2;3mtype a command — try 'notify', 'hint set ...', or 'scan '\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m"] +[0.000057, "o", "\u001b[0K\u001b[1m\u001b[33mfilesystem commands \u001b[0m\u001b[0K\r\r\n\u001b[mbackup\u001b[0m \u001b[2m# Create a backup of a file or directory\u001b[0m \u001b[0K\r\r\n\u001b[mconvert\u001b[0m \u001b[2m# Convert a file\u001b[0m \u001b[0K\r\r\n\u001b[mdownload\u001b[0m \u001b[2m# Download a file from a URL\u001b[0m \u001b[0K\r\r\n\u001b[mencrypt\u001b[0m \u001b[2m# Encrypt a file\u001b[0m \u001b[0K\r\r\n\u001b[mls\u001b[0m \u001b[2m# List directory contents\u001b[0m \u001b[0K\r\r\n\u001b[mmkdir\u001b[0m \u001b[2m# Create directories\u001b[0m \u001b[0K\r\r\n\u001b[mrename\u001b[0m \u001b[2m# Rename a file\u001b[0m \u001b[0K\r\r\n\u001b[msearch\u001b[0m \u001b[2m# Search for a query\u001b[0m \u001b[0K\r\r\n\u001b[1m\u001b[33mcore commands \u001b[0m\u001b[0K\r\r\n\u001b[mclient\u001b[0m \u001b[2m# Switch to the client menu (also works with CtrlC)\u001b[0m \u001b[0K\r\r\n\u001b[mexit\u001b[0m \u001b[2m# Exit the console application\u001b[0m \u001b[0K\r\r\n\u001b[mgreet\u001b[0m \u001b[2m# Greet a person\u001b[0m \u001b[0K\r\r\n\u001b[mhello\u001b[0m \u001b[2m# Say hello with customizable message\u001b[0m \u001b[0K\r\r\n\u001b[mhelp\u001b[0m \u001b[2m# Help about any command\u001b[0m \u001b[0K\r\r\n\u001b[1m\u001b[33mdeployment commands \u001b[0m\u001b[0K\r\r\n\u001b[mdeploy\u001b[0m \u001b[2m# Deploy a file\u001b[0m \u001b[0K\r\r\n\u001b[1m\u001b[33mtools commands \u001b[0m\u001b[0K\r\r\n\u001b[mgit\u001b[0m \u001b[2m# Git command\u001b[0m \u001b[0K\r\r\n\u001b[mssh\u001b[0m \u001b[2m# SSH client\u001b[0m \u001b[0K\r\r\n\u001b[1m\u001b[33mreadline commands \u001b[0m\u001b[0K\r\r\n\u001b[mhint\u001b[0m \u001b[2m# Set or clear a sticky transient hint immediately (non-async)\u001b[0m \u001b[0K\r\r\n\u001b[mnotify\u001b[0m \u001b[2m# Async status updates shown in the hint lane (transient hint + wake)\u001b[0m \u001b[0K\r\r\n\u001b[mscan\u001b[0m \u001b[2m# Async completions — press Tab after 'scan ' and watch hosts appear live\u001b[0m \u001b[0K\r\r\n\u001b[1m\u001b[33mother commands \u001b[0m\u001b[0K\r\r\n\u001b[mreadline\u001b[0m \u001b[2m# Manipulate readline options, keymaps and bindings\u001b[0m \u001b[0K\u001b[25A\u001b[1A\u001b[1A\u001b[2C\u001b[150D\u001b[2C\u001b[?25h"] +[0.900652, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] +[0.000975, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:31.690\u001b[0m\u001b[150D\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A"] +[0.000431, "o", "\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\r\r\n\u001b[2;3mtype a command — try 'notify', 'hint set ...', or 'scan '\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[0K\u001b[1m\u001b[33mfilesystem commands \u001b[0m\u001b[0K\r\r\n\u001b[mbackup\u001b[0m \u001b[2m# Create a backup of a file or directory\u001b[0m \u001b[0K\r\r\n\u001b[mconvert\u001b[0m \u001b[2m# Convert a file\u001b[0m \u001b[0K\r\r\n\u001b[mdownload\u001b[0m \u001b[2m# Download a file from a URL\u001b[0m \u001b[0K\r\r\n\u001b[mencrypt\u001b[0m \u001b[2m# Encrypt a file\u001b[0m \u001b[0K\r\r\n\u001b[mls\u001b[0m \u001b[2m# List directory contents\u001b[0m \u001b[0K\r\r\n\u001b[mmkdir\u001b[0m \u001b[2m# Create directories\u001b[0m \u001b[0K\r\r\n\u001b[mrename\u001b[0m \u001b[2m# Rename a file\u001b[0m \u001b[0K\r\r\n\u001b[msearch\u001b[0m \u001b[2m# Search for a query\u001b[0m \u001b[0K\r\r\n\u001b[1m\u001b[33mcore commands \u001b[0m\u001b[0K\r\r\n\u001b[mclient\u001b[0m \u001b[2m# Switch to the client menu (also works with CtrlC)\u001b[0m \u001b[0K\r\r\n\u001b[mexit\u001b[0m \u001b[2m# Exit the console application\u001b[0m \u001b[0K\r\r\n\u001b[mgreet\u001b[0m \u001b[2m# Greet a person\u001b[0m \u001b[0K\r\r\n\u001b[mhello\u001b[0m \u001b[2m# Say hello with customizable message\u001b[0m \u001b[0K\r\r\n\u001b[mhelp\u001b[0m \u001b[2m# Help about any command\u001b[0m \u001b[0K\r\r\n\u001b[1m\u001b[33mdeployment commands \u001b[0m\u001b[0K\r\r\n\u001b[mdeploy\u001b[0m \u001b[2m# Deploy a file\u001b[0m \u001b[0K\r\r\n\u001b[1m\u001b[33mtools commands \u001b[0m\u001b[0K\r\r\n\u001b[mgit\u001b[0m \u001b[2m# Git command\u001b[0m \u001b[0K\r\r\n\u001b[mssh\u001b[0m \u001b[2m# SSH client\u001b[0m \u001b[0K\r\r\n\u001b[1m\u001b[33mreadline commands \u001b[0m\u001b[0K\r\r\n\u001b[mhint\u001b[0m \u001b[2m# Set or clear a sticky transient hint immediately (non-async)\u001b[0m \u001b[0K\r\r\n\u001b[mnotify\u001b[0m \u001b[2m# Async status updates shown in the hint lane (transient hint + wake)\u001b[0m \u001b[0K\r\r\n\u001b[mscan\u001b[0m \u001b[2m# Async completions — press Tab after 'scan ' and watch hosts appear live\u001b[0m \u001b[0K\r\r\n\u001b[1m\u001b[33mother commands \u001b[0m\u001b[0K\r\r\n\u001b[mreadline\u001b[0m \u001b[2m# Manipulate readline options, keymaps and bindings\u001b[0m \u001b[0K\u001b[25A\u001b[1A\u001b[1A\u001b[2C"] +[0.000109, "o", "\u001b[150D\u001b[2C\u001b[?25h"] +[0.898254, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] +[0.001099, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n"] +[0.000267, "o", "\u001b[2C\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:32.590\u001b[0m\u001b[150D\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C"] +[0.000383, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\r\r\n\u001b[2;3mtype a command — try 'notify', 'hint set ...', or 'scan '\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m"] +[0.000422, "o", "\u001b[0K\u001b[1m\u001b[33mfilesystem commands \u001b[0m\u001b[0K\r\r\n\u001b[mbackup\u001b[0m \u001b[2m# Create a backup of a file or directory\u001b[0m \u001b[0K\r\r\n\u001b[mconvert\u001b[0m \u001b[2m# Convert a file\u001b[0m \u001b[0K\r\r\n\u001b[mdownload\u001b[0m \u001b[2m# Download a file from a URL\u001b[0m \u001b[0K\r\r\n\u001b[mencrypt\u001b[0m \u001b[2m# Encrypt a file\u001b[0m \u001b[0K\r\r\n\u001b[mls\u001b[0m \u001b[2m# List directory contents\u001b[0m \u001b[0K\r\r\n\u001b[mmkdir\u001b[0m \u001b[2m# Create directories\u001b[0m \u001b[0K\r\r\n\u001b[mrename\u001b[0m \u001b[2m# Rename a file\u001b[0m \u001b[0K\r\r\n\u001b[msearch\u001b[0m \u001b[2m# Search for a query\u001b[0m \u001b[0K\r\r\n\u001b[1m\u001b[33mcore commands \u001b[0m\u001b[0K\r\r\n\u001b[mclient\u001b[0m \u001b[2m# Switch to the client menu (also works with CtrlC)\u001b[0m \u001b[0K\r\r\n\u001b[mexit\u001b[0m \u001b[2m# Exit the console application\u001b[0m \u001b[0K\r\r\n\u001b[mgreet\u001b[0m \u001b[2m# Greet a person\u001b[0m \u001b[0K\r\r\n\u001b[mhello\u001b[0m \u001b[2m# Say hello with customizable message\u001b[0m \u001b[0K\r\r\n\u001b[mhelp\u001b[0m \u001b[2m# Help about any command\u001b[0m \u001b[0K\r\r\n\u001b[1m\u001b[33mdeployment commands \u001b[0m\u001b[0K\r\r\n\u001b[mdeploy\u001b[0m \u001b[2m# Deploy a file\u001b[0m \u001b[0K\r\r\n\u001b[1m\u001b[33mtools commands \u001b[0m\u001b[0K\r\r\n\u001b[mgit\u001b[0m \u001b[2m# Git command\u001b[0m \u001b[0K\r\r\n\u001b[mssh\u001b[0m \u001b[2m# SSH client\u001b[0m \u001b[0K\r\r\n\u001b[1m\u001b[33mreadline commands \u001b[0m\u001b[0K\r\r\n\u001b[mhint\u001b[0m \u001b[2m# Set or clear a sticky transient hint immediately (non-async)\u001b[0m \u001b[0K\r\r\n\u001b[mnotify\u001b[0m \u001b[2m# Async status updates shown in the hint lane (transient hint + wake)\u001b[0m \u001b[0K\r\r\n\u001b[mscan\u001b[0m \u001b[2m# Async completions — press Tab after 'scan ' and watch hosts appear live\u001b[0m \u001b[0K\r\r\n\u001b[1m\u001b[33mother commands \u001b[0m\u001b[0K\r\r\n\u001b[mreadline\u001b[0m \u001b[2m# Manipulate readline options, keymaps and bindings\u001b[0m \u001b[0K\u001b[25A\u001b[1A\u001b[1A\u001b[2C\u001b[150D\u001b[2C\u001b[?25h"] +[0.350238, "o", "\u001b[5 q"] +[0.000238, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] +[0.000737, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] +[0.000322, "o", "h\u001b[0m\u001b[2m\u001b[38;05;242melp client\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:32.943\u001b[0m\u001b[150D\u001b[13C\u001b[1B\u001b[150D\u001b[0J"] +[0.000103, "o", "\u001b[1A\u001b[13C"] +[0.005184, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[13C\r\r\n\u001b[0K\u001b[1m\u001b[33mcore commands \u001b[0m\u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34mh\u001b[22m\u001b[39m\u001b[mello\u001b[0m \u001b[2m# Say hello with customizable message\u001b[0m \u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34mh\u001b[22m\u001b[39m\u001b[melp\u001b[0m \u001b[2m# Help about any command\u001b[0m \u001b[0K\r\r\n\u001b[1m\u001b[33mreadline commands \u001b[0m\u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34mh\u001b[22m\u001b[39m\u001b[mint\u001b[0m \u001b[2m# Set or clear a sticky transient hint immediately (non-async)\u001b[0m \u001b[0K\u001b[4A\u001b[1A\u001b[13C\u001b[150D\u001b[3C\u001b[?25h"] +[0.144817, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] +[0.001008, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2Chi\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:33.094\u001b[0m\u001b[150D\u001b[4C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[4C"] +[0.001521, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[4C\r\r\n\u001b[0K\u001b[1m\u001b[33mreadline commands \u001b[0m\u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34mhi\u001b[22m\u001b[39m\u001b[mnt\u001b[0m \u001b[2m# Set or clear a sticky transient hint immediately (non-async)\u001b[0m \u001b[0K\u001b[1A\u001b[1A\u001b[4C\u001b[150D\u001b[4C\u001b[?25h"] +[0.173923, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] +[0.000996, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2Chin\u001b[0m\u001b[0K\u001b[49m"] +[0.000091, "o", " \u001b[1;30m01:49:33.270\u001b[0m\u001b[150D\u001b[5C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[5C"] +[0.001571, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[5C\r\r\n\u001b[0K\u001b[1m\u001b[33mreadline commands \u001b[0m\u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34mhin\u001b[22m\u001b[39m\u001b[mt\u001b[0m \u001b[2m# Set or clear a sticky transient hint immediately (non-async)\u001b[0m \u001b[0K\u001b[1A\u001b[1A\u001b[5C\u001b[150D\u001b[5C\u001b[?25h"] +[0.127583, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] +[0.000895, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[1m\u001b[32mhint\u001b[39m\u001b[22m\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:33.400\u001b[0m\u001b[150D\u001b[6C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[6C"] +[0.001077, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[6C\r\r\n\u001b[2;3m hint — Set or clear a sticky transient hint immediately (non-async)\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[0K\u001b[1m\u001b[33mreadline commands \u001b[0m\u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34mhint\u001b[22m\u001b[39m\u001b[m\u001b[0m \u001b[2m# Set or clear a sticky transient hint immediately (non-async)\u001b[0m \u001b[0K\u001b[1A\u001b[1A\u001b[1A\u001b[6C\u001b[150D\u001b[6C\u001b[?25h"] +[0.08543, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] +[0.001327, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] +[0.000105, "o", "\u001b[1m\u001b[32mhint\u001b[39m\u001b[22m\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:33.488\u001b[0m"] +[0.000395, "o", "\u001b[150D\u001b[6C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[6C"] +[0.001913, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[6C\r\r\n\u001b[2;3m hint — Set or clear a sticky transient hint immediately (non-async)\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m"] +[0.00031, "o", "\u001b[0K\u001b[1m\u001b[33mreadline commands \u001b[0m\u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34mhint\u001b[22m\u001b[39m\u001b[m\u001b[0m \u001b[2m# Set or clear a sticky transient hint immediately (non-async)\u001b[0m \u001b[0K\u001b[1A\u001b[1A\u001b[1A\u001b[6C\u001b[150D\u001b[6C\u001b[?25h"] +[0.326811, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] +[0.001452, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] +[0.000554, "o", "\u001b[1m\u001b[32mhint \u001b[39m\u001b[22m\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:33.820\u001b[0m"] +[0.000057, "o", "\u001b[150D"] +[0.000833, "o", "\u001b[7C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[7C"] +[0.001074, "o", "\u001b[1B\u001b[150D"] +[0.000181, "o", "\u001b[0J\u001b[1A\u001b[7C\r\r\n"] +[0.000165, "o", "\u001b[2;3m hint — Set or clear a sticky transient hint immediately (non-async)\u001b[0m\u001b[0K\r\r\n"] +[0.000264, "o", "\u001b[0K\u001b[0m"] +[0.000226, "o", "\u001b[0K\u001b[1m\u001b[33mcommands \u001b[0m\u001b[0K\r\r\n\u001b[mclear\u001b[0m \u001b[2m# Clear the transient hint lane\u001b[0m \u001b[0K\r\r\n\u001b[mset\u001b[0m \u001b[2m# Set the transient hint lane to a message (persists until cleared)\u001b[0m \u001b[0K\u001b[2A\u001b[1A\u001b[1A\u001b[7C\u001b[150D\u001b[7C\u001b[?25h"] +[0.56553, "o", "\u001b[?25l\u001b[150D> "] +[0.000075, "o", "\u001b[6n"] +[0.001102, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[1m\u001b[32mhint \u001b[39m\u001b[22m\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:34.389\u001b[0m\u001b[150D\u001b[7C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[7C"] +[0.001755, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[7C\r\r\n\u001b[2;3m hint — Set or clear a sticky transient hint immediately (non-async)\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[0K\u001b[1m\u001b[33mcommands \u001b[0m\u001b[0K\r\r\n\u001b[mclear\u001b[0m \u001b[2m# Clear the transient hint lane\u001b[0m \u001b[0K\r\r\n\u001b[mset\u001b[0m \u001b[2m# Set the transient hint lane to a message (persists until cleared)\u001b[0m \u001b[0K\u001b[2A\u001b[1A\u001b[1A\u001b[7C\u001b[150D\u001b[7C\u001b[?25h"] +[0.286319, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] +[0.001117, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] +[0.000057, "o", "\u001b[1m\u001b[32mhint \u001b[39m\u001b[22ms\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:34.679\u001b[0m\u001b[150D"] +[0.000111, "o", "\u001b[8C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[8C"] +[0.001574, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[8C\r\r\n\u001b[2;3m hint — Set or clear a sticky transient hint immediately (non-async)\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m"] +[0.000148, "o", "\u001b[0K\u001b[1m\u001b[33mcommands \u001b[0m\u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34ms\u001b[22m\u001b[39m\u001b[met\u001b[0m \u001b[2m# Set the transient hint lane to a message (persists until cleared)\u001b[0m \u001b[0K\u001b[1A\u001b[1A\u001b[1A\u001b[8C\u001b[150D\u001b[8C\u001b[?25h"] +[0.321767, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] +[0.000881, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[1m\u001b[32mhint \u001b[39m\u001b[22mse\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:35.003\u001b[0m\u001b[150D\u001b[9C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[9C"] +[0.001348, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[9C\r\r\n\u001b[2;3m hint — Set or clear a sticky transient hint immediately (non-async)\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[0K\u001b[1m\u001b[33mcommands \u001b[0m\u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34mse\u001b[22m\u001b[39m\u001b[mt\u001b[0m \u001b[2m# Set the transient hint lane to a message (persists until cleared)\u001b[0m \u001b[0K\u001b[1A\u001b[1A\u001b[1A\u001b[9C\u001b[150D\u001b[9C\u001b[?25h"] +[0.149185, "o", "\u001b[?25l\u001b[150D> "] +[0.000052, "o", "\u001b[6n"] +[0.00085, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n"] +[0.000336, "o", "\u001b[2C\u001b[1m\u001b[32mhint \u001b[39m\u001b[22mset\u001b[0m\u001b[0K\u001b[49m"] +[0.000301, "o", " \u001b[1;30m01:49:35.155\u001b[0m\u001b[150D\u001b[10C\u001b[1B\u001b[150D\u001b[0J\u001b[1A"] +[0.000738, "o", "\u001b[10C"] +[0.001821, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[10C\r\r\n\u001b[2;3m hint set — Set the transient hint lane to a message (persists until cleared)\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[0K\u001b[1m\u001b[33mcommands \u001b[0m\u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34mset\u001b[22m\u001b[39m\u001b[m\u001b[0m \u001b[2m# Set the transient hint lane to a message (persists until cleared)\u001b[0m \u001b[0K\u001b[1A\u001b[1A\u001b[1A\u001b[10C\u001b[150D\u001b[10C\u001b[?25h"] +[0.130431, "o", "\u001b[?25l\u001b[150D"] +[0.000121, "o", "> "] +[0.000352, "o", "\u001b[6n"] +[0.001064, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[1m\u001b[32mhint \u001b[39m\u001b[22mset\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:35.290\u001b[0m\u001b[150D\u001b[10C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[10C"] +[0.001526, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[10C\r\r\n"] +[0.000203, "o", "\u001b[2;3m hint set — Set the transient hint lane to a message (persists until cleared)\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[0K\u001b[1m\u001b[33mcommands \u001b[0m\u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34mset\u001b[22m\u001b[39m\u001b[m\u001b[0m \u001b[2m# Set the transient hint lane to a message (persists until cleared)\u001b[0m \u001b[0K\u001b[1A"] +[0.000426, "o", "\u001b[1A\u001b[1A\u001b[10C\u001b[150D\u001b[10C\u001b[?25h"] +[1.145172, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] +[0.001041, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] +[0.000019, "o", "\u001b[1m\u001b[32mhint \u001b[39m\u001b[22mset \u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:36.438\u001b[0m"] +[0.000135, "o", "\u001b[150D\u001b[11C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[11C"] +[0.001823, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[11C\r\r\n\u001b[2;3m hint set — Set the transient hint lane to a message (persists until cleared)\u001b[0m\u001b[0K\r\r\n\u001b[2mset MESSAGE...\u001b[0m\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[3A\u001b[1A\u001b[11C\u001b[150D\u001b[11C\u001b[?25h"] +[2.332968, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] +[0.000937, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[1m\u001b[32mhint \u001b[39m\u001b[22mset t\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:38.774\u001b[0m\u001b[150D\u001b[12C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[12C"] +[0.000833, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[12C\r\r\n\u001b[2;3m hint set — Set the transient hint lane to a message (persists until cleared)\u001b[0m\u001b[0K\r\r\n\u001b[2mset MESSAGE...\u001b[0m\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[3A\u001b[1A\u001b[12C\u001b[150D\u001b[12C\u001b[?25h"] +[0.088322, "o", "\u001b[?25l\u001b[150D> "] +[0.000663, "o", "\u001b[6n"] +[0.001058, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[1m\u001b[32mhint \u001b[39m\u001b[22mset te\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:38.865\u001b[0m\u001b[150D\u001b[13C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[13C"] +[0.001896, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[13C\r\r\n\u001b[2;3m hint set — Set the transient hint lane to a message (persists until cleared)\u001b[0m\u001b[0K\r\r\n\u001b[2mset MESSAGE...\u001b[0m\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m"] +[0.0001, "o", "\u001b[3A\u001b[1A\u001b[13C\u001b[150D\u001b[13C\u001b[?25h"] +[0.218064, "o", "\u001b[?25l\u001b[150D> "] +[0.000103, "o", "\u001b[6n"] +[0.000645, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] +[0.000185, "o", "\u001b[1m\u001b[32mhint \u001b[39m\u001b[22mset tes\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:39.086\u001b[0m\u001b[150D\u001b[14C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[14C"] +[0.000767, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[14C\r\r\n\u001b[2;3m hint set — Set the transient hint lane to a message (persists until cleared)\u001b[0m\u001b[0K\r\r\n\u001b[2mset MESSAGE...\u001b[0m\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[3A\u001b[1A\u001b[14C\u001b[150D\u001b[14C\u001b[?25h"] +[0.174302, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] +[0.001231, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[1m\u001b[32mhint \u001b[39m\u001b[22mset test\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:39.262\u001b[0m\u001b[150D\u001b[15C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[15C"] +[0.001111, "o", "\u001b[1B"] +[0.000907, "o", "\u001b[150D\u001b[0J\u001b[1A\u001b[15C\r\r\n\u001b[2;3m hint set — Set the transient hint lane to a message (persists until cleared)\u001b[0m\u001b[0K\r\r\n\u001b[2mset MESSAGE...\u001b[0m\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[3A\u001b[1A\u001b[15C\u001b[150D\u001b[15C\u001b[?25h"] +[0.459465, "o", "\u001b[5 q\u001b[15D\u001b[2C\u001b[6n"] +[0.001119, "o", "\u001b[150D\u001b[15C\u001b[0J \u001b[1;30m01:49:39.725\u001b[0m\u001b[150D\r\r\n\u001b[0 q\u001b[?2004l"] +[0.000195, "o", "\r\n"] +[0.009177, "o", "\r\n"] +[0.000253, "o", "\u001b[?2004h\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\r\n"] +[0.000425, "o", "> \u001b[5 q"] +[0.000029, "o", "\u001b[?25l\u001b[150D"] +[0.00012, "o", "> \u001b[6n"] +[0.000879, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:39.736\u001b[0m\u001b[150D\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C"] +[0.000105, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\r\r\n\u001b[2;3mtype a command — try 'notify', 'hint set ...', or 'scan '\u001b[0m\u001b[0K\r\r\n\u001b[36mtest\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m"] +[0.000063, "o", "\u001b[3A\u001b[1A\u001b[2C\u001b[150D\u001b[2C\u001b[?25h"] +[2.436322, "o", "\u001b[2D\u001b[2C\u001b[6n"] +[0.001215, "o", "\u001b[150D\u001b[2C\u001b[0J \u001b[1;30m01:49:42.173\u001b[0m\u001b[150D\r\r\n"] +[0.000039, "o", "\u001b[0 q\u001b[?2004l"] +[0.000445, "o", "\u001b[?2004h\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\r\n> \u001b[5 q"] +[0.000204, "o", "\u001b[?25l\u001b[150D"] +[0.000019, "o", "> \u001b[6n"] +[0.00087, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:42.175\u001b[0m\u001b[150D\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\u001b[1B"] +[0.000223, "o", "\u001b[150D\u001b[0J\u001b[1A\u001b[2C\r\r\n\u001b[2;3mtype a command — try 'notify', 'hint set ...', or 'scan '\u001b[0m\u001b[0K\r\r\n\u001b[36mtest\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[3A\u001b[1A\u001b[2C\u001b[150D\u001b[2C\u001b[?25h"] +[0.387877, "o", "\u001b[2D\u001b[2C\u001b[6n"] +[0.00094, "o", "\u001b[150D\u001b[2C\u001b[0J \u001b[1;30m01:49:42.564\u001b[0m\u001b[150D\r\r\n\u001b[0 q\u001b[?2004l"] +[0.001222, "o", "\u001b[?2004h\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\r\n> \u001b[5 q\u001b[?25l\u001b[150D> \u001b[6n"] +[0.001149, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:42.566\u001b[0m\u001b[150D\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\r\r\n\u001b[2;3mtype a command — try 'notify', 'hint set ...', or 'scan '\u001b[0m\u001b[0K\r\r\n\u001b[36mtest\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m"] +[0.000129, "o", "\u001b[3A\u001b[1A\u001b[2C\u001b[150D\u001b[2C\u001b[?25h"] +[0.309419, "o", "\u001b[2D\u001b[2C\u001b[6n"] +[0.000786, "o", "\u001b[150D\u001b[2C\u001b[0J \u001b[1;30m01:49:42.877\u001b[0m\u001b[150D\r\r\n\u001b[0 q\u001b[?2004l"] +[0.000658, "o", "\u001b[?2004h\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\r\n> \u001b[5 q\u001b[?25l\u001b[150D> \u001b[6n"] +[0.000883, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] +[0.000205, "o", "\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:42.879\u001b[0m\u001b[150D"] +[0.000034, "o", "\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C"] +[0.000174, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\r\r\n\u001b[2;3mtype a command — try 'notify', 'hint set ...', or 'scan '\u001b[0m\u001b[0K\r\r\n\u001b[36mtest\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m"] +[0.000241, "o", "\u001b[3A\u001b[1A\u001b[2C\u001b[150D\u001b[2C\u001b[?25h"] +[0.243154, "o", "\u001b[2D\u001b[2C\u001b[6n"] +[0.00116, "o", "\u001b[150D\u001b[2C\u001b[0J \u001b[1;30m01:49:43.123\u001b[0m\u001b[150D\r\r\n"] +[0.000113, "o", "\u001b[0 q\u001b[?2004l"] +[0.000796, "o", "\u001b[?2004h\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\r\n> \u001b[5 q"] +[0.000071, "o", "\u001b[?25l"] +[0.000177, "o", "\u001b[150D"] +[0.000145, "o", "> \u001b[6n"] +[0.000679, "o", "\u001b[150D"] +[0.000142, "o", "\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] +[0.000334, "o", "\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:43.126\u001b[0m"] +[0.000123, "o", "\u001b[150D\u001b[2C"] +[0.000126, "o", "\u001b[1B"] +[0.000063, "o", "\u001b[150D"] +[0.000147, "o", "\u001b[0J"] +[0.000349, "o", "\u001b[1A\u001b[2C"] +[0.000129, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C"] +[0.000545, "o", "\r\r\n\u001b[2;3mtype a command — try 'notify', 'hint set ...', or 'scan '\u001b[0m\u001b[0K\r\r\n\u001b[36mtest\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[3A\u001b[1A\u001b[2C\u001b[150D\u001b[2C\u001b[?25h"] +[2.347618, "o", "\u001b[?25l\u001b[150D> "] +[0.000257, "o", "\u001b[6n"] +[0.001382, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2Ch\u001b[0m\u001b[2m\u001b[38;05;242mint set test\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:45.476\u001b[0m\u001b[150D\u001b[15C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[15C"] +[0.001233, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[15C\r\r\n\u001b[36mtest\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[0K\u001b[1m\u001b[33mcore commands \u001b[0m\u001b[0K\r\r\n"] +[0.00009, "o", "\u001b[m\u001b[1m\u001b[34mh\u001b[22m\u001b[39m\u001b[mello\u001b[0m \u001b[2m# Say hello with customizable message\u001b[0m \u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34mh\u001b[22m\u001b[39m\u001b[melp\u001b[0m \u001b[2m# Help about any command\u001b[0m \u001b[0K\r\r\n\u001b[1m\u001b[33mreadline commands \u001b[0m\u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34mh\u001b[22m\u001b[39m\u001b[mint\u001b[0m \u001b[2m# Set or clear a sticky transient hint immediately (non-async)\u001b[0m \u001b[0K\u001b[4A\u001b[1A\u001b[1A\u001b[15C\u001b[150D\u001b[3C\u001b[?25h"] +[0.162658, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] +[0.000815, "o", "\u001b[150D"] +[0.00018, "o", "\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] +[0.000589, "o", "hi\u001b[0m\u001b[2m\u001b[38;05;242mnt set test\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:45.642\u001b[0m\u001b[150D\u001b[15C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[15C"] +[0.001696, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[15C\r\r\n\u001b[36mtest\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[0K\u001b[1m\u001b[33mreadline commands \u001b[0m\u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34mhi\u001b[22m\u001b[39m\u001b[mnt\u001b[0m \u001b[2m# Set or clear a sticky transient hint immediately (non-async)\u001b[0m \u001b[0K\u001b[1A\u001b[1A\u001b[1A\u001b[15C\u001b[150D\u001b[4C\u001b[?25h"] +[0.192963, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] +[0.001209, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2Chin\u001b[0m\u001b[2m\u001b[38;05;242mt set test\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:45.838\u001b[0m\u001b[150D\u001b[15C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[15C"] +[0.002552, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[15C\r\r\n\u001b[36mtest\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[0K\u001b[1m\u001b[33mreadline commands \u001b[0m\u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34mhin\u001b[22m\u001b[39m\u001b[mt\u001b[0m \u001b[2m# Set or clear a sticky transient hint immediately (non-async)\u001b[0m \u001b[0K\u001b[1A\u001b[1A"] +[0.000242, "o", "\u001b[1A\u001b[15C\u001b[150D"] +[0.000136, "o", "\u001b[5C"] +[0.000004, "o", "\u001b[?25h"] +[0.161097, "o", "\u001b[?25l\u001b[150D> "] +[0.000071, "o", "\u001b[6n"] +[0.000843, "o", "\u001b[150D"] +[0.000681, "o", "\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[1m\u001b[32mhint\u001b[39m\u001b[22m\u001b[0m\u001b[2m\u001b[38;05;242m set test\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:46.003\u001b[0m\u001b[150D\u001b[15C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[15C"] +[0.004536, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[15C\r\r\n\u001b[2;3m hint — Set or clear a sticky transient hint immediately (non-async)\u001b[0m\u001b[0K\r\r\n\u001b[36mtest\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[0K\u001b[1m\u001b[33mreadline commands \u001b[0m\u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34mhint\u001b[22m\u001b[39m\u001b[m\u001b[0m \u001b[2m# Set or clear a sticky transient hint immediately (non-async)\u001b[0m \u001b[0K\u001b[1A\u001b[3A\u001b[1A\u001b[15C\u001b[150D\u001b[6C\u001b[?25h"] +[0.205828, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] +[0.001139, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[1m\u001b[32mhint \u001b[39m\u001b[22m\u001b[0m\u001b[2m\u001b[38;05;242mset test\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:46.215\u001b[0m\u001b[150D\u001b[15C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[15C"] +[0.001466, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[15C\r\r\n\u001b[2;3m hint — Set or clear a sticky transient hint immediately (non-async)\u001b[0m\u001b[0K\r\r\n\u001b[36mtest\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[0K\u001b[1m\u001b[33mcommands \u001b[0m\u001b[0K\r\r\n\u001b[mclear\u001b[0m \u001b[2m# Clear the transient hint lane\u001b[0m \u001b[0K\r\r\n\u001b[mset\u001b[0m \u001b[2m# Set the transient hint lane to a message (persists until cleared)\u001b[0m \u001b[0K\u001b[2A\u001b[3A"] +[0.000061, "o", "\u001b[1A\u001b[15C\u001b[150D\u001b[7C\u001b[?25h"] +[0.942846, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] +[0.001183, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] +[0.000161, "o", "\u001b[1m\u001b[32mhint \u001b[39m\u001b[22ms\u001b[0m\u001b[2m\u001b[38;05;242met test\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:47.161\u001b[0m\u001b[150D\u001b[15C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[15C"] +[0.001706, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[15C\r\r\n\u001b[2;3m hint — Set or clear a sticky transient hint immediately (non-async)\u001b[0m\u001b[0K\r\r\n\u001b[36mtest\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[0K\u001b[1m\u001b[33mcommands \u001b[0m\u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34ms\u001b[22m\u001b[39m\u001b[met\u001b[0m \u001b[2m# Set the transient hint lane to a message (persists until cleared)\u001b[0m \u001b[0K\u001b[1A\u001b[3A\u001b[1A\u001b[15C\u001b[150D\u001b[8C\u001b[?25h"] +[0.208063, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] +[0.00088, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[1m\u001b[32mhint \u001b[39m\u001b[22mse\u001b[0m\u001b[2m\u001b[38;05;242mt test\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:47.371\u001b[0m\u001b[150D\u001b[15C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[15C"] +[0.001678, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[15C\r\r\n\u001b[2;3m hint — Set or clear a sticky transient hint immediately (non-async)\u001b[0m\u001b[0K\r\r\n\u001b[36mtest\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m"] +[0.000041, "o", "\u001b[0K\u001b[1m\u001b[33mcommands \u001b[0m\u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34mse\u001b[22m\u001b[39m\u001b[mt\u001b[0m \u001b[2m# Set the transient hint lane to a message (persists until cleared)\u001b[0m \u001b[0K\u001b[1A\u001b[3A\u001b[1A\u001b[15C\u001b[150D\u001b[9C\u001b[?25h"] +[0.599676, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] +[0.000934, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[1m\u001b[32mhint \u001b[39m\u001b[22m\u001b[0m\u001b[2m\u001b[38;05;242mset test\u001b[0m\u001b[0K\u001b[49m"] +[0.000068, "o", " \u001b[1;30m01:49:47.974\u001b[0m\u001b[150D\u001b[15C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[15C"] +[0.002124, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[15C\r\r\n"] +[0.000064, "o", "\u001b[2;3m hint — Set or clear a sticky transient hint immediately (non-async)\u001b[0m\u001b[0K\r\r\n\u001b[36mtest\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m"] +[0.000188, "o", "\u001b[0K\u001b[1m\u001b[33mcommands \u001b[0m\u001b[0K\r\r\n\u001b[mclear\u001b[0m \u001b[2m# Clear the transient hint lane\u001b[0m \u001b[0K\r\r\n\u001b[mset\u001b[0m \u001b[2m# Set the transient hint lane to a message (persists until cleared)\u001b[0m \u001b[0K\u001b[2A\u001b[3A\u001b[1A\u001b[15C\u001b[150D\u001b[7C\u001b[?25h"] +[0.430185, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] +[0.001401, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[1m\u001b[32mhint \u001b[39m\u001b[22mc\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:48.408\u001b[0m\u001b[150D\u001b[8C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[8C"] +[0.001734, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[8C\r\r\n\u001b[2;3m hint — Set or clear a sticky transient hint immediately (non-async)\u001b[0m\u001b[0K\r\r\n\u001b[36mtest\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[0K\u001b[1m\u001b[33mcommands \u001b[0m\u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34mc\u001b[22m\u001b[39m\u001b[mlear\u001b[0m \u001b[2m# Clear the transient hint lane\u001b[0m \u001b[0K\u001b[1A\u001b[3A\u001b[1A\u001b[8C\u001b[150D\u001b[8C\u001b[?25h"] +[0.131817, "o", "\u001b[?25l\u001b[150D> "] +[0.000127, "o", "\u001b[6n"] +[0.000762, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] +[0.000199, "o", "\u001b[1m\u001b[32mhint \u001b[39m\u001b[22mcl\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:48.542\u001b[0m\u001b[150D\u001b[9C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[9C"] +[0.001749, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[9C\r\r\n\u001b[2;3m hint — Set or clear a sticky transient hint immediately (non-async)\u001b[0m\u001b[0K\r\r\n\u001b[36mtest\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m"] +[0.000145, "o", "\u001b[0K\u001b[1m\u001b[33mcommands \u001b[0m\u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34mcl\u001b[22m\u001b[39m\u001b[mear\u001b[0m \u001b[2m# Clear the transient hint lane\u001b[0m \u001b[0K\u001b[1A\u001b[3A\u001b[1A\u001b[9C\u001b[150D\u001b[9C\u001b[?25h"] +[0.12261, "o", "\u001b[?25l\u001b[150D> "] +[0.00025, "o", "\u001b[6n"] +[0.000787, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[1m\u001b[32mhint \u001b[39m\u001b[22mcle\u001b[0m\u001b[0K\u001b[49m"] +[0.000187, "o", " \u001b[1;30m01:49:48.668\u001b[0m\u001b[150D\u001b[10C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[10C"] +[0.001258, "o", "\u001b[1B"] +[0.000108, "o", "\u001b[150D\u001b[0J\u001b[1A\u001b[10C\r\r\n"] +[0.000455, "o", "\u001b[2;3m hint — Set or clear a sticky transient hint immediately (non-async)\u001b[0m\u001b[0K\r\r\n\u001b[36mtest\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m"] +[0.00022, "o", "\u001b[0K\u001b[1m\u001b[33mcommands \u001b[0m\u001b[0K\r\r\n"] +[0.000458, "o", "\u001b[m\u001b[1m\u001b[34mcle\u001b[22m\u001b[39m\u001b[mar\u001b[0m \u001b[2m# Clear the transient hint lane\u001b[0m \u001b[0K\u001b[1A\u001b[3A\u001b[1A\u001b[10C\u001b[150D\u001b[10C\u001b[?25h"] +[0.178518, "o", "\u001b[?25l\u001b[150D> "] +[0.000246, "o", "\u001b[6n"] +[0.001105, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[1m\u001b[32mhint \u001b[39m\u001b[22mclea\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:48.850\u001b[0m\u001b[150D\u001b[11C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[11C"] +[0.001521, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[11C\r\r\n\u001b[2;3m hint — Set or clear a sticky transient hint immediately (non-async)\u001b[0m\u001b[0K\r\r\n\u001b[36mtest\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m"] +[0.000123, "o", "\u001b[0K\u001b[1m\u001b[33mcommands \u001b[0m\u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34mclea\u001b[22m\u001b[39m\u001b[mr\u001b[0m \u001b[2m# Clear the transient hint lane\u001b[0m \u001b[0K\u001b[1A\u001b[3A\u001b[1A\u001b[11C\u001b[150D\u001b[11C\u001b[?25h"] +[0.126152, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] +[0.000949, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] +[0.000485, "o", "\u001b[1m\u001b[32mhint \u001b[39m\u001b[22mclear\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:48.979\u001b[0m\u001b[150D\u001b[12C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[12C"] +[0.001279, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[12C\r\r\n\u001b[2;3m hint clear — Clear the transient hint lane\u001b[0m\u001b[0K\r\r\n\u001b[36mtest\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[0K\u001b[1m\u001b[33mcommands \u001b[0m\u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34mclear\u001b[22m\u001b[39m\u001b[m\u001b[0m \u001b[2m# Clear the transient hint lane\u001b[0m \u001b[0K\u001b[1A\u001b[3A"] +[0.00005, "o", "\u001b[1A\u001b[12C\u001b[150D\u001b[12C\u001b[?25h"] +[0.465806, "o", "\u001b[5 q\u001b[12D\u001b[2C\u001b[6n"] +[0.000901, "o", "\u001b[150D\u001b[12C\u001b[0J \u001b[1;30m01:49:49.448\u001b[0m\u001b[150D\r\r\n"] +[0.000259, "o", "\u001b[0 q\u001b[?2004l"] +[0.000087, "o", "\r\n"] +[0.003905, "o", "\r\n"] +[0.00071, "o", "\u001b[?2004h\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\r\n> \u001b[5 q\u001b[?25l\u001b[150D> "] +[0.000106, "o", "\u001b[6n"] +[0.000769, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] +[0.000239, "o", "\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:49.454\u001b[0m\u001b[150D"] +[0.000029, "o", "\u001b[2C\u001b[1B"] +[0.000127, "o", "\u001b[150D\u001b[0J\u001b[1A\u001b[2C"] +[0.000094, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\r\r\n\u001b[2;3mtype a command — try 'notify', 'hint set ...', or 'scan '\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m"] +[0.000027, "o", "\u001b[1A\u001b[1A\u001b[2C\u001b[150D\u001b[2C\u001b[?25h"] +[1.395294, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] +[0.001151, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] +[0.000038, "o", "a\u001b[0m\u001b[2m\u001b[38;05;242mnother test\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:50.851\u001b[0m\u001b[150D\u001b[14C"] +[0.000016, "o", "\u001b[1B"] +[0.000008, "o", "\u001b[150D\u001b[0J"] +[0.000459, "o", "\u001b[1A\u001b[14C"] +[0.003071, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[14C\u001b[150D\u001b[3C\u001b[?25h"] +[0.100847, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] +[0.000982, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2Can\u001b[0m\u001b[2m\u001b[38;05;242mother test\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:50.956\u001b[0m\u001b[150D\u001b[14C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[14C"] +[0.001623, "o", "\u001b[1B\u001b[150D"] +[0.000131, "o", "\u001b[0J\u001b[1A\u001b[14C\u001b[150D\u001b[4C\u001b[?25h"] +[0.173555, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] +[0.000807, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] +[0.000263, "o", "ano\u001b[0m\u001b[2m\u001b[38;05;242mther test\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:51.132\u001b[0m\u001b[150D\u001b[14C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[14C"] +[0.001608, "o", "\u001b[1B"] +[0.000037, "o", "\u001b[150D"] +[0.000111, "o", "\u001b[0J\u001b[1A\u001b[14C\u001b[150D\u001b[5C\u001b[?25h"] +[0.122813, "o", "\u001b[?25l\u001b[150D> "] +[0.000028, "o", "\u001b[6n"] +[0.001012, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] +[0.000102, "o", "anot\u001b[0m\u001b[2m\u001b[38;05;242mher test\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:51.258\u001b[0m\u001b[150D\u001b[14C\u001b[1B\u001b[150D\u001b[0J"] +[0.000037, "o", "\u001b[1A\u001b[14C"] +[0.001867, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[14C\u001b[150D\u001b[6C\u001b[?25h"] +[0.131601, "o", "\u001b[?25l\u001b[150D> "] +[0.000152, "o", "\u001b[6n"] +[0.00127, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2Canoth\u001b[0m\u001b[2m\u001b[38;05;242mer test\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:51.393\u001b[0m\u001b[150D\u001b[14C\u001b[1B\u001b[150D"] +[0.000026, "o", "\u001b[0J"] +[0.000127, "o", "\u001b[1A\u001b[14C"] +[0.001935, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[14C\u001b[150D\u001b[7C\u001b[?25h"] +[0.124085, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] +[0.001039, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2Canothe\u001b[0m\u001b[2m\u001b[38;05;242mr test\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:51.520\u001b[0m\u001b[150D\u001b[14C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[14C"] +[0.001216, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[14C\u001b[150D\u001b[8C\u001b[?25h"] +[0.081154, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] +[0.000851, "o", "\u001b[150D"] +[0.000012, "o", "\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] +[0.000353, "o", "another\u001b[0m\u001b[2m\u001b[38;05;242m test\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:51.604\u001b[0m\u001b[150D\u001b[14C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[14C"] +[0.001841, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[14C\u001b[150D\u001b[9C\u001b[?25h"] +[0.168706, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] +[0.000671, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2Canother \u001b[0m\u001b[2m\u001b[38;05;242mtest\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:51.775\u001b[0m\u001b[150D\u001b[14C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[14C"] +[0.000573, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[14C\u001b[150D\u001b[10C\u001b[?25h"] +[0.476079, "o", "\u001b[?25l\u001b[150D"] +[0.000022, "o", "> \u001b[6n"] +[0.00082, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] +[0.00005, "o", "\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:52.253\u001b[0m"] +[0.000004, "o", "\u001b[150D\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C"] +[0.000157, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\r\r\n\u001b[2;3mtype a command — try 'notify', 'hint set ...', or 'scan '\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m"] +[0.000092, "o", "\u001b[1A\u001b[1A\u001b[2C\u001b[150D\u001b[2C\u001b[?25h"] +[0.525942, "o", "\u001b[5 q"] +[0.001525, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] +[0.00112, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:52.781\u001b[0m\u001b[150D\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C"] +[0.000687, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\r\r\n\u001b[2;3mtype a command — try 'notify', 'hint set ...', or 'scan '\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[0K\u001b[1m\u001b[33mfilesystem commands \u001b[0m\u001b[0K\r\r\n\u001b[mbackup\u001b[0m \u001b[2m# Create a backup of a file or directory\u001b[0m \u001b[0K\r\r\n\u001b[mconvert\u001b[0m \u001b[2m# Convert a file\u001b[0m \u001b[0K\r\r\n\u001b[mdownload\u001b[0m \u001b[2m# Download a file from a URL\u001b[0m \u001b[0K\r\r\n\u001b[mencrypt\u001b[0m \u001b[2m# Encrypt a file\u001b[0m \u001b[0K\r\r\n\u001b[mls\u001b[0m \u001b[2m# List directory contents\u001b[0m \u001b[0K\r\r\n\u001b[mmkdir\u001b[0m \u001b[2m# Create directories\u001b[0m \u001b[0K\r\r\n\u001b[mrename\u001b[0m \u001b[2m# Rename a file\u001b[0m \u001b[0K\r\r\n\u001b[msearch\u001b[0m \u001b[2m# Search for a query\u001b[0m \u001b[0K\r\r\n\u001b[1m\u001b[33mcore commands \u001b[0m\u001b[0K\r\r\n\u001b[mclient\u001b[0m \u001b[2m# Switch to the client menu (also works with CtrlC)\u001b[0m \u001b[0K\r\r\n\u001b[mexit\u001b[0m \u001b[2m# Exit the console application\u001b[0m \u001b[0K\r\r\n\u001b[mgreet\u001b[0m \u001b[2m# Greet a person\u001b[0m \u001b[0K\r\r\n\u001b[mhello\u001b[0m \u001b[2m# Say hello with customizable message\u001b[0m \u001b[0K\r\r\n\u001b[mhelp\u001b[0m \u001b[2m# Help about any command\u001b[0m \u001b[0K\r\r\n\u001b[1m\u001b[33mdeployment commands \u001b[0m\u001b[0K\r\r\n\u001b[mdeploy\u001b[0m \u001b[2m# Deploy a file\u001b[0m \u001b[0K\r\r\n\u001b[1m\u001b[33mtools commands \u001b[0m\u001b[0K\r\r\n\u001b[mgit\u001b[0m \u001b[2m# Git command\u001b[0m \u001b[0K\r\r\n\u001b[mssh\u001b[0m \u001b[2m# SSH client\u001b[0m \u001b[0K\r\r\n\u001b[1m\u001b[33mreadline commands \u001b[0m\u001b[0K\r\r\n\u001b[mhint\u001b[0m \u001b[2m# Set or clear a sticky transient hint immediately (non-async)\u001b[0m \u001b[0K\r\r\n\u001b[mnotify\u001b[0m \u001b[2m# Async status updates shown in the hint lane (transient hint + wake)\u001b[0m \u001b[0K\r\r\n\u001b[mscan\u001b[0m \u001b[2m# Async completions — press Tab after 'scan ' and watch hosts appear live\u001b[0m \u001b[0K\r\r\n\u001b[1m\u001b[33mother commands \u001b[0m\u001b[0K\r\r\n\u001b[mreadline\u001b[0m \u001b[2m# Manipulate readline options, keymaps and bindings\u001b[0m \u001b[0K\u001b[25A\u001b[1A\u001b[1A\u001b[2C\u001b[150D\u001b[2C\u001b[?25h"] +[0.952817, "o", "\u001b[5 q\u001b[?25l\u001b[150D> \u001b[6n"] +[0.000766, "o", "\u001b[150D"] +[0.000198, "o", "\u001b[1A"] +[0.000164, "o", "\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n"] +[0.001603, "o", "\u001b[2Cm\u001b[0m\u001b[2m\u001b[38;05;242multiple-ambiguous 10.203.23.45 127.0.0.1\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:53.737\u001b[0m\u001b[150D\u001b[44C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[44C"] +[0.001045, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[44C\r\r\n"] +[0.000307, "o", "\u001b[0K\u001b[1m\u001b[33mfilesystem commands \u001b[0m\u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34mm\u001b[22m\u001b[39m\u001b[mkdir\u001b[0m \u001b[2m# Create directories\u001b[0m \u001b[0K\u001b[1A\u001b[1A\u001b[44C"] +[0.000313, "o", "\u001b[150D\u001b[3C\u001b[?25h"] +[0.195881, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] +[0.00126, "o", "\u001b[150D\u001b[1A"] +[0.000229, "o", "\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] +[0.000137, "o", "mk\u001b[0m\u001b[0K\u001b[49m"] +[0.000226, "o", " \u001b[1;30m01:49:53.937\u001b[0m"] +[0.000538, "o", "\u001b[150D\u001b[4C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[4C"] +[0.00386, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[4C\r\r\n\u001b[0K\u001b[1m\u001b[33mfilesystem commands \u001b[0m\u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34mmk\u001b[22m\u001b[39m\u001b[mdir\u001b[0m \u001b[2m# Create directories\u001b[0m \u001b[0K\u001b[1A\u001b[1A\u001b[4C\u001b[150D\u001b[4C\u001b[?25h"] +[0.110343, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] +[0.001108, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] +[0.000186, "o", "mkd\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:54.053\u001b[0m"] +[0.000057, "o", "\u001b[150D\u001b[5C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[5C"] +[0.002146, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[5C\r\r\n\u001b[0K\u001b[1m\u001b[33mfilesystem commands \u001b[0m\u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34mmkd\u001b[22m\u001b[39m\u001b[mir\u001b[0m \u001b[2m# Create directories\u001b[0m \u001b[0K\u001b[1A\u001b[1A\u001b[5C\u001b[150D\u001b[5C\u001b[?25h"] +[0.107865, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] +[0.001231, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2Cmkdi\u001b[0m\u001b[0K\u001b[49m"] +[0.00008, "o", " \u001b[1;30m01:49:54.164\u001b[0m\u001b[150D\u001b[6C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[6C"] +[0.001904, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[6C\r\r\n\u001b[0K\u001b[1m\u001b[33mfilesystem commands \u001b[0m\u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34mmkdi\u001b[22m\u001b[39m\u001b[mr\u001b[0m \u001b[2m# Create directories\u001b[0m \u001b[0K\u001b[1A\u001b[1A\u001b[6C"] +[0.000109, "o", "\u001b[150D\u001b[6C\u001b[?25h"] +[0.196706, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] +[0.000747, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] +[0.000019, "o", "\u001b[1m\u001b[32mmkdir\u001b[39m\u001b[22m\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:54.364\u001b[0m\u001b[150D\u001b[7C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[7C"] +[0.001616, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[7C\r\r\n\u001b[2;3m mkdir — Create directories\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[0K\u001b[1m\u001b[33mfilesystem commands \u001b[0m\u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34mmkdir\u001b[22m\u001b[39m\u001b[m\u001b[0m \u001b[2m# Create directories\u001b[0m \u001b[0K\u001b[1A\u001b[1A\u001b[1A\u001b[7C\u001b[150D\u001b[7C\u001b[?25h"] +[0.149241, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] +[0.000992, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] +[0.000126, "o", "\u001b[1m\u001b[32mmkdir \u001b[39m\u001b[22m\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:54.516\u001b[0m"] +[0.000231, "o", "\u001b[150D\u001b[8C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[8C"] +[0.002592, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[8C\r\r\n\u001b[2;3m mkdir — Create directories\u001b[0m\u001b[0K\r\r\n\u001b[2mmkdir [flags] DIRECTORY...\u001b[0m\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[0K\u001b[1m\u001b[33mfiles \u001b[0m\u001b[0K\r\r\n\u001b[mcursor.cast\u001b[0m \u001b[0m\u001b[1;32mexample\u001b[0m \u001b[0m\u001b[1;32mexample.exe\u001b[0m \u001b[0m\u001b[mfeature-commands.go\u001b[0m \u001b[0m\u001b[mhistory.go\u001b[0m \u001b[0m\u001b[minterrupt.go\u001b[0m \u001b[0m\u001b[mmain-commands.go\u001b[0m \u001b[0m\u001b[0K\r\r\n\u001b[mmain.go\u001b[0m \u001b[0m\u001b[mmenu.go\u001b[0m \u001b[0m\u001b[mprompt.omp.json\u001b[0m \u001b[0m\u001b[mREADME.md\u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m\u001b[0K\u001b[2A\u001b[3A\u001b[1A\u001b[8C\u001b[150D\u001b[8C\u001b[?25h"] +[3.288206, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] +[0.001043, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:57.808\u001b[0m"] +[0.000162, "o", "\u001b[150D\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\u001b[1B\u001b[150D\u001b[0J"] +[0.000057, "o", "\u001b[1A\u001b[2C"] +[0.000009, "o", "\r\r\n"] +[0.000026, "o", "\u001b[2;3mtype a command — try 'notify', 'hint set ...', or 'scan '\u001b[0m\u001b[0K\r"] +[0.000001, "o", "\r\n"] +[0.000007, "o", "\u001b[0K\u001b[0m"] +[0.000571, "o", "\u001b[1A\u001b[1A\u001b[2C\u001b[150D\u001b[2C\u001b[?25h"] +[0.184947, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] +[0.001192, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] +[0.000153, "o", "\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:57.995\u001b[0m\u001b[150D\u001b[2C\u001b[1B"] +[0.000432, "o", "\u001b[150D\u001b[0J\u001b[1A\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\r\r\n\u001b[2;3mtype a command — try 'notify', 'hint set ...', or 'scan '\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m"] +[0.000163, "o", "\u001b[1A\u001b[1A\u001b[2C\u001b[150D\u001b[2C\u001b[?25h"] +[0.289402, "o", "\u001b[2D\u001b[2C\u001b[6n"] +[0.001393, "o", "\u001b[150D\u001b[2C\u001b[0J \u001b[1;30m01:49:58.286\u001b[0m\u001b[150D\r\r\n\u001b[0 q\u001b[?2004lConfirm exit (Y/y): "] +[0.371704, "o", "\u001b[?2004h\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\r\n> \u001b[5 q"] +[0.00012, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] +[0.001395, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] +[0.000056, "o", "\u001b[0m\u001b[0K\u001b[49m"] +[0.000158, "o", " \u001b[1;30m01:49:58.660\u001b[0m\u001b[150D\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\u001b[1B\u001b[150D"] +[0.000038, "o", "\u001b[0J\u001b[1A"] +[0.000133, "o", "\u001b[2C\r\r\n\u001b[2;3mtype a command — try 'notify', 'hint set ...', or 'scan '\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m"] +[0.000772, "o", "\u001b[1A\u001b[1A\u001b[2C\u001b[150D\u001b[2C\u001b[?25h"] +[0.709851, "o", "\u001b[2D\u001b[2C\u001b[6n"] +[0.00118, "o", "\u001b[150D\u001b[2C\u001b[0J \u001b[1;30m01:49:59.372\u001b[0m\u001b[150D\r\r\n\u001b[0 q\u001b[?2004lConfirm exit (Y/y): "] +[0.290469, "o", "y"] +[0.321349, "o", "\r\n"] +[0.011871, "x", "0"] diff --git a/example/feature-commands.go b/example/feature-commands.go new file mode 100644 index 0000000..16bcd30 --- /dev/null +++ b/example/feature-commands.go @@ -0,0 +1,213 @@ +package main + +import ( + "fmt" + "strings" + "sync" + "time" + + "github.com/carapace-sh/carapace" + "github.com/spf13/cobra" + + "github.com/reeflective/console" +) + +// featureGroupID groups the commands that demonstrate the readline hint and +// async-completion features. +const featureGroupID = "readline" + +// setupReadlineHints registers a passive hint provider on the shell. The +// provider is recomputed from the current input line on every refresh and its +// result is shown below the input, in the dedicated "provided" hint lane. +// +// Here it resolves the command being typed and shows its short description. +// Because this lane is independent from completion hints (set by the completion +// engine) and from transient/async status messages (see the `notify`/`hint` +// commands), all three can be displayed at once without clobbering each other. +func setupReadlineHints(app *console.Console) { + dim := func(format string, args ...any) []rune { + return []rune("\x1b[2;3m" + fmt.Sprintf(format, args...) + "\x1b[0m") + } + + app.Shell().Hint.SetProvider(func(line []rune, _ int) []rune { + fields := strings.Fields(string(line)) + if len(fields) == 0 { + return dim("type a command — try 'notify', 'hint set ...', or 'scan '") + } + + menu := app.ActiveMenu() + if menu == nil || menu.Command == nil { + return nil + } + + // Find resolves the deepest command matched by the words typed so far. + cmd, _, err := menu.Find(fields) + if err != nil || cmd == nil || cmd == menu.Command { + return nil + } + + return dim("%s — %s", cmd.CommandPath(), cmd.Short) + }) +} + +// readlineFeatureCommands builds the commands demonstrating the hint lanes and +// async completion regeneration. They are added to the main menu. +func readlineFeatureCommands(app *console.Console) []*cobra.Command { + return []*cobra.Command{ + notifyCommand(app), + hintCommand(app), + scanCommand(app), + } +} + +// notifyCommand demonstrates ASYNC status updates in the transient hint lane. +// It starts a background job that pushes status messages from another goroutine +// with Hint.SetTransient; the shell repaints on its own (no keystroke), thanks +// to the async-refresh wake. +func notifyCommand(app *console.Console) *cobra.Command { + return &cobra.Command{ + Use: "notify", + Short: "Async status updates shown in the hint lane (transient hint + wake)", + GroupID: featureGroupID, + Run: func(_ *cobra.Command, _ []string) { + hint := app.Shell().Hint + stages := []string{ + "\x1b[33m⠋ connecting…\x1b[0m", + "\x1b[33m⠙ authenticating…\x1b[0m", + "\x1b[33m⠹ transferring…\x1b[0m", + "\x1b[32m✓ transfer complete\x1b[0m", + } + + go func() { + for _, stage := range stages { + time.Sleep(1200 * time.Millisecond) + hint.SetTransient(stage) + } + + time.Sleep(1500 * time.Millisecond) + hint.ClearTransient() + }() + + fmt.Println("Background job started — watch the hint line below the prompt update on its own (no keystroke needed).") + }, + } +} + +// hintCommand demonstrates SYNCHRONOUS use of the transient hint lane: setting a +// sticky status message that persists across keystrokes (unlike a completion +// hint) until it is cleared or replaced. +func hintCommand(app *console.Console) *cobra.Command { + hint := &cobra.Command{ + Use: "hint", + Short: "Set or clear a sticky transient hint immediately (non-async)", + GroupID: featureGroupID, + } + + hint.AddCommand(&cobra.Command{ + Use: "set MESSAGE...", + Short: "Set the transient hint lane to a message (persists until cleared)", + Args: cobra.MinimumNArgs(1), + Run: func(_ *cobra.Command, args []string) { + app.Shell().Hint.SetTransient("\x1b[36m" + strings.Join(args, " ") + "\x1b[0m") + }, + }) + + hint.AddCommand(&cobra.Command{ + Use: "clear", + Short: "Clear the transient hint lane", + Run: func(_ *cobra.Command, _ []string) { + app.Shell().Hint.ClearTransient() + }, + }) + + return hint +} + +// hostDiscovery is a process-wide singleton: the console rebuilds its command +// tree (and thus re-runs scanCommand) on each completion, so the discovery state +// must persist across those rebuilds rather than being recreated each time. +// +// Seeded with two known hosts so the menu opens and stays open — a single +// candidate would be auto-accepted, closing the menu before any async result +// could be shown. +var hostDiscovery = &discovery{base: []string{"localhost", "gateway"}} + +// scanCommand demonstrates ASYNC completions. Its argument completer returns a +// set of hosts that a background "discovery" grows over time; each time a host +// is found, the goroutine calls Shell().RefreshCompletions(), which rebuilds the +// already-open completion menu in place — so hosts appear live while the menu +// stays open, with no keystroke from the user. +func scanCommand(app *console.Console) *cobra.Command { + scan := &cobra.Command{ + Use: "scan [HOST]", + Short: "Async completions — press Tab after 'scan ' and watch hosts appear live", + GroupID: featureGroupID, + Args: cobra.MaximumNArgs(1), + Run: func(_ *cobra.Command, args []string) { + if len(args) == 0 { + fmt.Println("Usage: scan HOST (press Tab after 'scan ' and watch the menu fill in)") + return + } + + fmt.Println("Scanning host:", args[0]) + }, + } + + carapace.Gen(scan).PositionalCompletion( + carapace.ActionCallback(func(_ carapace.Context) carapace.Action { + hostDiscovery.start(app) + return carapace.ActionValues(hostDiscovery.snapshot()...) + }), + ) + + return scan +} + +// discovery simulates an asynchronous completion producer: a background routine +// appends "discovered" hosts to a cache and asks the shell to regenerate the +// open menu in place. +type discovery struct { + mu sync.Mutex + base []string + found []string + running bool +} + +// snapshot returns the current known + discovered hosts. +func (d *discovery) snapshot() []string { + d.mu.Lock() + defer d.mu.Unlock() + + return append(append([]string{}, d.base...), d.found...) +} + +// start kicks off one discovery run if none is in progress. Each newly found +// host triggers an in-place regeneration of the open completion menu. +func (d *discovery) start(app *console.Console) { + d.mu.Lock() + if d.running { + d.mu.Unlock() + return + } + + d.running = true + d.found = nil + d.mu.Unlock() + + go func() { + for i := 1; i <= 8; i++ { + time.Sleep(900 * time.Millisecond) + + d.mu.Lock() + d.found = append(d.found, fmt.Sprintf("10.0.0.%d", i)) + d.mu.Unlock() + + // Rebuild the open menu in place with the newly discovered host. + app.Shell().RefreshCompletions() + } + + d.mu.Lock() + d.running = false + d.mu.Unlock() + }() +} diff --git a/example/main-commands.go b/example/main-commands.go index b51f137..71ac7a6 100644 --- a/example/main-commands.go +++ b/example/main-commands.go @@ -27,6 +27,7 @@ func mainMenuCommands(app *console.Console) console.Commands { &cobra.Group{ID: "filesystem", Title: "filesystem"}, &cobra.Group{ID: "deployment", Title: "deployment"}, &cobra.Group{ID: "tools", Title: "tools"}, + &cobra.Group{ID: featureGroupID, Title: "readline features"}, ) // Readline subcommands @@ -608,6 +609,13 @@ func mainMenuCommands(app *console.Console) console.Commands { c.FlagCompletion(flagMap) } + // Add the readline-feature demo commands AFTER the generic completion + // loop above, so their custom (e.g. async) completers are not replaced + // by the default file completion applied to every command with args. + for _, cmd := range readlineFeatureCommands(app) { + rootCmd.AddCommand(cmd) + } + rootCmd.SetHelpCommandGroupID("core") rootCmd.InitDefaultHelpCmd() rootCmd.CompletionOptions.DisableDefaultCmd = true diff --git a/example/main.go b/example/main.go index 12f6061..9f9db44 100644 --- a/example/main.go +++ b/example/main.go @@ -41,6 +41,10 @@ func main() { // Set some custom prompt handlers for this menu. setupPrompt(menu) + // Register a passive hint provider on the shell, demonstrating the readline + // hint lanes (passive provider / async transient / completion hints). + setupReadlineHints(app) + // All menus currently each have a distinct, in-memory history source. // Replace the main (current) menu's history with one writing to our // application history file. The default history is named after its menu. From ff1ee28853215b794eecb2539632907787f62117 Mon Sep 17 00:00:00 2001 From: maxlandon Date: Mon, 1 Jun 2026 02:53:24 +0200 Subject: [PATCH 02/12] deps: upgrade readline to v1.2.0 and refresh dependencies Upgrade direct dependencies: - reeflective/readline v1.1.4 -> v1.2.0 (fixes the bottom-of-terminal prompt overlap reported in #78, via the NewlineBefore/After fix) - carapace-sh/carapace v1.7.1 -> v1.11.6 - spf13/cobra v1.8.1 -> v1.10.2 - spf13/pflag v1.0.6 -> v1.0.10 - mvdan.cc/sh/v3 v3.7.0 -> v3.13.1 - golang.org/x/exp -> latest The go directive moves to 1.25.0, required by both readline v1.2.0 and mvdan.cc/sh v3.13.1. Co-Authored-By: Claude Opus 4.8 (1M context) --- go.mod | 18 +++++++++--------- go.sum | 57 +++++++++++++++++++++++++++++---------------------------- 2 files changed, 38 insertions(+), 37 deletions(-) diff --git a/go.mod b/go.mod index becf4e0..020899e 100644 --- a/go.mod +++ b/go.mod @@ -1,22 +1,22 @@ module github.com/reeflective/console -go 1.24.0 +go 1.25.0 require ( - github.com/carapace-sh/carapace v1.7.1 + github.com/carapace-sh/carapace v1.11.6 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 - github.com/reeflective/readline v1.1.4 - github.com/spf13/cobra v1.8.1 - github.com/spf13/pflag v1.0.6 - golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac - mvdan.cc/sh/v3 v3.7.0 + github.com/reeflective/readline v1.2.0 + github.com/spf13/cobra v1.10.2 + github.com/spf13/pflag v1.0.10 + golang.org/x/exp v0.0.0-20260529124908-c761662dc8c9 + mvdan.cc/sh/v3 v3.13.1 ) require ( - github.com/carapace-sh/carapace-shlex v1.0.1 // indirect + github.com/carapace-sh/carapace-shlex v1.1.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect - golang.org/x/sys v0.40.0 // indirect + golang.org/x/sys v0.45.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 63fb960..9d429a5 100644 --- a/go.sum +++ b/go.sum @@ -1,12 +1,16 @@ -github.com/carapace-sh/carapace v1.7.1 h1:GjMjPNEMHhTstneZD2M3Ypjb+lW5YNEV1AfYmRhsG4c= -github.com/carapace-sh/carapace v1.7.1/go.mod h1:fHdo3nEFe1QnIXxeA/Z1O9dCI83sfCsKfxrogpHfgtM= -github.com/carapace-sh/carapace-shlex v1.0.1 h1:ww0JCgWpOVuqWG7k3724pJ18Lq8gh5pHQs9j3ojUs1c= -github.com/carapace-sh/carapace-shlex v1.0.1/go.mod h1:lJ4ZsdxytE0wHJ8Ta9S7Qq0XpjgjU0mdfCqiI2FHx7M= -github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/frankban/quicktest v1.14.5 h1:dfYrrRyLtiqT9GyKXgdh+k4inNeTvmGbuSgZ3lx3GhA= -github.com/frankban/quicktest v1.14.5/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/carapace-sh/carapace v1.11.6 h1:fUZv+oAMgbiDEpNPNis4n35tzqE3h8yshOohLJ2Mz4Y= +github.com/carapace-sh/carapace v1.11.6/go.mod h1:5MUSHyLN9GGb5/NY/j9VI68/TcZV4ApRCAHGg4WeU0s= +github.com/carapace-sh/carapace-shlex v1.1.1 h1:ccmNeetAYZOk4IcV36youFDsXusT9uCNW2Njkw+QS+Q= +github.com/carapace-sh/carapace-shlex v1.1.1/go.mod h1:lJ4ZsdxytE0wHJ8Ta9S7Qq0XpjgjU0mdfCqiI2FHx7M= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= +github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= +github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= +github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/hinshun/vt10x v0.0.0-20220301184237-5011da428d02 h1:AgcIVYPa6XJnU3phs104wLj8l5GEththEw6+F79YsIY= +github.com/hinshun/vt10x v0.0.0-20220301184237-5011da428d02/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= @@ -18,30 +22,27 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/reeflective/readline v1.1.3 h1:meGkuEmujZHmalJ9eT3pYkwtkufH5EwYFPTnaph0T0s= -github.com/reeflective/readline v1.1.3/go.mod h1:CwNkh9BmFBBCSO6mdDaNWb34rOqQsI9eYbxyqvOEazY= -github.com/reeflective/readline v1.1.4 h1:HEdVYiPZ7e2CrP3uU/l6wApQdpkY0MjR8lINNboVtFk= -github.com/reeflective/readline v1.1.4/go.mod h1:CwNkh9BmFBBCSO6mdDaNWb34rOqQsI9eYbxyqvOEazY= +github.com/reeflective/readline v1.2.0 h1:QuT4CbHTFnZfQF1aQpxGDTfmGUPmbr/7r+oS9JLbsKA= +github.com/reeflective/readline v1.2.0/go.mod h1:bOpqx2/VqGlIoobyWR1Vgt/p5FiMfIHj4OicPuw6RfU= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/rogpeppe/go-internal v1.10.1-0.20230524175051-ec119421bb97 h1:3RPlVWzZ/PDqmVuf/FKHARG5EMid/tl7cv54Sw/QRVY= -github.com/rogpeppe/go-internal v1.10.1-0.20230524175051-ec119421bb97/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= -github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= -github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac h1:l5+whBCLH3iH2ZNHYLbAe58bo7yrN4mVcnkHDYz5vvs= -golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac/go.mod h1:hH+7mtFmImwwcMvScyxUhjuVHR3HGaDPMn9rMSUUbxo= -golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= -golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= -golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/exp v0.0.0-20260529124908-c761662dc8c9 h1:4d4PbuBNwaxMXkXI8yiIYjydtMU+04RHeuSxJdgKftM= +golang.org/x/exp v0.0.0-20260529124908-c761662dc8c9/go.mod h1:d2fgXJLVs4dYDHUk5lwMIfzRzSrWCfGZb0ZqeLa/Vcw= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -mvdan.cc/sh/v3 v3.7.0 h1:lSTjdP/1xsddtaKfGg7Myu7DnlHItd3/M2tomOcNNBg= -mvdan.cc/sh/v3 v3.7.0/go.mod h1:K2gwkaesF/D7av7Kxl0HbF5kGOd2ArupNTX3X44+8l8= +mvdan.cc/sh/v3 v3.13.1 h1:DP3TfgZhDkT7lerUdnp6PTGKyxxzz6T+cOlY/xEvfWk= +mvdan.cc/sh/v3 v3.13.1/go.mod h1:lXJ8SexMvEVcHCoDvAGLZgFJ9Wsm2sulmoNEXGhYZD0= From 27149195ea03c3b9c2a35ae5374b4b6db06b51bb Mon Sep 17 00:00:00 2001 From: maxlandon Date: Mon, 1 Jun 2026 02:53:43 +0200 Subject: [PATCH 03/12] fix: data races, signal leak, deadlock risk, and alias highlighting Concurrency: - isExecuting was written under RLock and read with no lock at all; make it an atomic.Bool so all accesses are correct and lock-free. - Several maps/slices (filters, interruptHandlers, histories, cmds) were mutated under RLock, and HideCommands took no lock at all. Writers now take Lock; readers (Menu, ActiveFiltersFor) take RLock. - activeMenu() iterated the menus map with no lock. Cache the active menu in a pointer maintained under Lock, and read it under RLock (also drops the per-call linear scan). - handleInterrupt ranged the handler map unlocked while a handler could mutate it; snapshot matching handlers under RLock, then run them after releasing. Deadlock: - ActiveFiltersFor held a write lock and recursed into itself while holding it (latent self-deadlock on the non-reentrant mutex, plus per-command render contention). Snapshot the filters once under RLock, then walk the command tree lock-free. - SwitchMenu now releases c.mutex before resetPreRun, which reacquires it. Signal leak: - monitorSignals registered a channel via signal.Notify on every command execution and never released it, leaking a channel per command for the process lifetime. execute now defers signal.Stop(sigchan). Highlighting: - A command invoked through an alias was never highlighted because the alias branch broke out of the loop before the highlight block. Fold the alias check into cmdFound. Tests: - concurrency_test.go stresses the shared state from many goroutines and passes under -race. - internal/line/highlight_test.go covers alias highlighting. Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitignore | 1 + command.go | 11 +++-- concurrency_test.go | 68 ++++++++++++++++++++++++++++++ console.go | 73 ++++++++++++++++++--------------- internal/line/highlight.go | 11 ++--- internal/line/highlight_test.go | 38 +++++++++++++++++ interrupt.go | 32 ++++++++------- menu.go | 30 +++++++++----- run.go | 18 ++++---- 9 files changed, 204 insertions(+), 78 deletions(-) create mode 100644 concurrency_test.go create mode 100644 internal/line/highlight_test.go diff --git a/.gitignore b/.gitignore index 1f086c4..e1603b5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ console.wiki .gemini +.claude diff --git a/command.go b/command.go index d3858cd..10e0a89 100644 --- a/command.go +++ b/command.go @@ -20,8 +20,8 @@ type Commands func() *cobra.Command // SetCommands requires a function returning a tree of cobra commands to be used. func (m *Menu) SetCommands(cmds Commands) { - m.mutex.RLock() - defer m.mutex.RUnlock() + m.mutex.Lock() + defer m.mutex.Unlock() m.cmds = cmds } @@ -31,6 +31,9 @@ func (m *Menu) SetCommands(cmds Commands) { // If "windows" is used as the argument here, all windows commands for the current // menu are subsequently hidden, until ShowCommands("windows") is called. func (c *Console) HideCommands(filters ...string) { + c.mutex.Lock() + defer c.mutex.Unlock() + next: for _, filt := range filters { for _, filter := range c.filters { @@ -50,8 +53,8 @@ next: // Use this function if you have previously called HideCommands("filter") and want // these commands to be available back under their respective menu. func (c *Console) ShowCommands(filters ...string) { - c.mutex.RLock() - defer c.mutex.RUnlock() + c.mutex.Lock() + defer c.mutex.Unlock() updated := make([]string, 0) diff --git a/concurrency_test.go b/concurrency_test.go new file mode 100644 index 0000000..6920110 --- /dev/null +++ b/concurrency_test.go @@ -0,0 +1,68 @@ +package console + +import ( + "errors" + "sync" + "testing" + + "github.com/spf13/cobra" +) + +// TestConcurrentStateAccess stresses the console's shared state (filters, the +// menus map, and per-menu interrupt handlers) from many goroutines at once. +// +// It is meant to be run with the race detector (`go test -race`). Before the +// locking fixes, these paths mutated maps/slices under a read lock (or no lock +// at all), which the detector flags and which can panic on concurrent map +// writes in production. +func TestConcurrentStateAccess(t *testing.T) { + c := New("test") + + // Give the active menu a small command tree so that ActiveFiltersFor has + // something to recurse over while filters are being mutated concurrently. + menu := c.ActiveMenu() + menu.SetCommands(func() *cobra.Command { + root := &cobra.Command{Use: "root"} + child := &cobra.Command{ + Use: "child", + Annotations: map[string]string{CommandFilterKey: "filterA,filterB"}, + Run: func(*cobra.Command, []string) {}, + } + root.AddCommand(child) + return root + }) + menu.resetPreRun() + + errInt := errors.New("interrupt") + + const workers = 64 + + var wg sync.WaitGroup + wg.Add(workers) + + for i := 0; i < workers; i++ { + go func(i int) { + defer wg.Done() + + // Filters: concurrent writers (Hide/Show) and readers (ActiveFiltersFor). + c.HideCommands("filterA", "filterB") + c.ShowCommands("filterA") + + m := c.ActiveMenu() + for _, cmd := range m.Command.Commands() { + _ = m.ActiveFiltersFor(cmd) + } + + // Menus map: concurrent creation and lookup. + _ = c.NewMenu("menu") + _ = c.Menu("menu") + _ = c.ActiveMenu() + + // Interrupt handlers map: concurrent writers. + m.AddInterrupt(errInt, func(*Console) {}) + m.DelInterrupt(errInt) + }(i) + } + + wg.Wait() +} diff --git a/console.go b/console.go index a8c335f..5eb984d 100644 --- a/console.go +++ b/console.go @@ -4,6 +4,7 @@ import ( "fmt" "strings" "sync" + "sync/atomic" "github.com/reeflective/readline" @@ -21,8 +22,9 @@ type Console struct { cmdHighlight string // Ansi code for highlighting of command in default highlighter. Green by default. flagHighlight string // Ansi code for highlighting of flag in default highlighter. Grey by default. menus map[string]*Menu // Different command trees, prompt engines, etc. + current *Menu // Cached pointer to the active menu (guarded by mutex). filters []string // Hide commands based on their attributes and current context. - isExecuting bool // Used by log functions, which need to adapt behavior (print the prompt, etc.) + isExecuting atomic.Bool // Used by log functions, which need to adapt behavior (print the prompt, etc.) printed bool // Used to adjust asynchronous messages too. mutex *sync.RWMutex // Concurrency management. @@ -91,6 +93,7 @@ func New(app string) *Console { // Each menu is created with a default prompt engine. defaultMenu := console.NewMenu("") defaultMenu.active = true + console.current = defaultMenu // Set the history for this menu for _, name := range defaultMenu.historyNames { @@ -154,8 +157,8 @@ func (c *Console) SetDefaultFlagHighlight(seq string) { // well as some specific items like history sources, prompt // configurations, sets of expanded variables, and others. func (c *Console) NewMenu(name string) *Menu { - c.mutex.RLock() - defer c.mutex.RUnlock() + c.mutex.Lock() + defer c.mutex.Unlock() menu := newMenu(name, c) c.menus[name] = menu @@ -164,16 +167,13 @@ func (c *Console) NewMenu(name string) *Menu { // ActiveMenu - Return the currently used console menu. func (c *Console) ActiveMenu() *Menu { - c.mutex.Lock() - defer c.mutex.Unlock() - return c.activeMenu() } // Menu returns one of the console menus by name, or nil if no menu is found. func (c *Console) Menu(name string) *Menu { - c.mutex.Lock() - defer c.mutex.Unlock() + c.mutex.RLock() + defer c.mutex.RUnlock() return c.menus[name] } @@ -184,33 +184,39 @@ func (c *Console) Menu(name string) *Menu { // are bound to this menu name, the current menu is kept. func (c *Console) SwitchMenu(menu string) { c.mutex.Lock() + target, found := c.menus[menu] - c.mutex.Unlock() + current := c.current - if found && target != nil { - // Only switch if the target menu was found. - current := c.activeMenu() - if current != nil && target == current { - return - } + // Only switch if the target menu was found and is not already current. + if !found || target == nil || target == current { + c.mutex.Unlock() + return + } + + if current != nil { + current.active = false + } - if current != nil { - current.active = false - } + target.active = true + c.current = target - target.active = true + c.mutex.Unlock() - // Remove the currently bound history sources - // (old menu) and bind the ones peculiar to this one. - c.shell.History.Delete() + // The following touches the shell and regenerates the menu commands, + // which itself reacquires c.mutex (history/filters): it must run with + // the lock released to avoid a self-deadlock. - for _, name := range target.historyNames { - c.shell.History.Add(name, target.histories[name]) - } + // Remove the currently bound history sources + // (old menu) and bind the ones peculiar to this one. + c.shell.History.Delete() - // Regenerate the commands, outputs and everything related. - target.resetPreRun() + for _, name := range target.historyNames { + c.shell.History.Add(name, target.histories[name]) } + + // Regenerate the commands, outputs and everything related. + target.resetPreRun() } // @@ -224,7 +230,7 @@ func (c *Console) SwitchMenu(menu string) { // If this function is called while a command is running, the console will simply print the log // below the line, and will not print the prompt. In any other case this function works normally. func (c *Console) TransientPrintf(msg string, args ...any) (n int, err error) { - if c.isExecuting { + if c.isExecuting.Load() { return fmt.Printf(msg, args...) } @@ -250,7 +256,7 @@ func (c *Console) TransientPrintf(msg string, args ...any) (n int, err error) { // If this function is called while a command is running, the console will simply print the log // below the line, and will not print the prompt. In any other case this function works normally. func (c *Console) Printf(msg string, args ...any) (n int, err error) { - if c.isExecuting { + if c.isExecuting.Load() { return fmt.Printf(msg, args...) } @@ -294,10 +300,11 @@ func (c *Console) setupShell() { } func (c *Console) activeMenu() *Menu { - for _, menu := range c.menus { - if menu.active { - return menu - } + c.mutex.RLock() + defer c.mutex.RUnlock() + + if c.current != nil { + return c.current } // Else return the default menu. diff --git a/internal/line/highlight.go b/internal/line/highlight.go index b84ebd1..3a2a302 100644 --- a/internal/line/highlight.go +++ b/internal/line/highlight.go @@ -41,13 +41,10 @@ func HighlightCommand(done, args []string, root *cobra.Command, cmdColor string) // Highlight the root command when found, or any of its aliases. for _, cmd := range root.Commands() { - // Change 1: Highlight based on first arg in usage rather than the entire usage itself - cmdFound := strings.Split(cmd.Use, " ")[0] == strings.TrimSpace(args[0]) - - if slices.Contains(cmd.Aliases, strings.TrimSpace(args[0])) { - cmdFound = true - break - } + // Highlight based on first arg in usage rather than the entire usage itself, + // or on any of the command's aliases. + name := strings.TrimSpace(args[0]) + cmdFound := strings.Split(cmd.Use, " ")[0] == name || slices.Contains(cmd.Aliases, name) if cmdFound { highlighted = append(highlighted, Bold+cmdColor+args[0]+ResetFG+BoldReset) diff --git a/internal/line/highlight_test.go b/internal/line/highlight_test.go new file mode 100644 index 0000000..ea8e286 --- /dev/null +++ b/internal/line/highlight_test.go @@ -0,0 +1,38 @@ +package line + +import ( + "strings" + "testing" + + "github.com/spf13/cobra" +) + +// TestHighlightCommandAlias is a regression test for a bug where a command +// invoked through one of its aliases was never highlighted: the alias branch +// used to `break` out of the loop before reaching the highlight block. +func TestHighlightCommandAlias(t *testing.T) { + root := &cobra.Command{Use: "app"} + root.AddCommand(&cobra.Command{Use: "deploy host", Aliases: []string{"d", "dep"}}) + + tests := []struct { + name string + arg string + want bool // whether arg should be highlighted as a command + }{ + {"canonical name", "deploy", true}, + {"first alias", "d", true}, + {"second alias", "dep", true}, + {"unknown word", "nope", false}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + done, _ := HighlightCommand(nil, []string{tc.arg}, root, GreenFG) + + highlighted := len(done) > 0 && strings.Contains(done[0], GreenFG) + if highlighted != tc.want { + t.Fatalf("arg %q: highlighted=%v, want %v (got %q)", tc.arg, highlighted, tc.want, done) + } + }) + } +} diff --git a/interrupt.go b/interrupt.go index a70ba33..150ddf9 100644 --- a/interrupt.go +++ b/interrupt.go @@ -9,15 +9,15 @@ package console // Many will want to use this to switch menus. Note that these interrupt errors only // work when the console is NOT currently executing a command, only when reading input. func (m *Menu) AddInterrupt(err error, handler func(c *Console)) { - m.mutex.RLock() + m.mutex.Lock() m.interruptHandlers[err] = handler - m.mutex.RUnlock() + m.mutex.Unlock() } // DelInterrupt removes one or more interrupt handlers from the menu registered ones. // If no error is passed as argument, all handlers are removed. func (m *Menu) DelInterrupt(errs ...error) { - m.mutex.RLock() + m.mutex.Lock() if len(errs) == 0 { m.interruptHandlers = make(map[error]func(c *Console)) } else { @@ -25,19 +25,12 @@ func (m *Menu) DelInterrupt(errs ...error) { delete(m.interruptHandlers, err) } } - m.mutex.RUnlock() + m.mutex.Unlock() } func (m *Menu) handleInterrupt(err error) { - m.console.mutex.RLock() - m.console.isExecuting = true - m.console.mutex.RUnlock() - - defer func() { - m.console.mutex.RLock() - m.console.isExecuting = false - m.console.mutex.RUnlock() - }() + m.console.isExecuting.Store(true) + defer m.console.isExecuting.Store(false) // TODO: this is not a very, very safe way of comparing // errors. I'm not sure what to right now with this, but @@ -46,9 +39,20 @@ func (m *Menu) handleInterrupt(err error) { // the string itself is likely to change in the future. // // But if people use their own third-party errors... nothing is guaranteed. + // + // Snapshot the matching handlers under the lock, then run them once + // released: a handler is free to mutate the menu (e.g. SwitchMenu) + // without deadlocking, and the map can't be written mid-iteration. + m.mutex.RLock() + matched := make([]func(c *Console), 0, len(m.interruptHandlers)) for herr, handler := range m.interruptHandlers { if err.Error() == herr.Error() { - handler(m.console) + matched = append(matched, handler) } } + m.mutex.RUnlock() + + for _, handler := range matched { + handler(m.console) + } } diff --git a/menu.go b/menu.go index ada9bcd..ab63a78 100644 --- a/menu.go +++ b/menu.go @@ -101,8 +101,8 @@ func (m *Menu) Prompt() *Prompt { // AddHistorySource adds a source of history commands that will // be accessible to the shell when the menu is active. func (m *Menu) AddHistorySource(name string, source readline.History) { - m.mutex.RLock() - defer m.mutex.RUnlock() + m.mutex.Lock() + defer m.mutex.Unlock() if len(m.histories) == 1 && m.historyNames[0] == m.defaultHistoryName() { delete(m.histories, m.defaultHistoryName()) @@ -117,8 +117,8 @@ func (m *Menu) AddHistorySource(name string, source readline.History) { // to the specified "filepath" parameter. On the first call to this function, // the default in-memory history source is removed. func (m *Menu) AddHistorySourceFile(name string, filepath string) { - m.mutex.RLock() - defer m.mutex.RUnlock() + m.mutex.Lock() + defer m.mutex.Unlock() if len(m.histories) == 1 && m.historyNames[0] == m.defaultHistoryName() { delete(m.histories, m.defaultHistoryName()) @@ -242,23 +242,33 @@ func (m *Menu) CheckIsAvailable(cmd *cobra.Command) error { // ActiveFiltersFor returns all the active menu filters that a given command // does not declare as compliant with (added with console.Hide/ShowCommand()). func (m *Menu) ActiveFiltersFor(cmd *cobra.Command) []string { + // Snapshot the console filters once under a read lock, then walk the + // command tree lock-free. The previous version held a write lock and + // recursed into itself while holding it, which both serialized every + // completion/highlight render and risked a self-deadlock on the + // (non-reentrant) mutex whenever the parent-subtree branch was taken. + m.console.mutex.RLock() + consoleFilters := append([]string(nil), m.console.filters...) + m.console.mutex.RUnlock() + + return activeFiltersFor(cmd, consoleFilters) +} + +func activeFiltersFor(cmd *cobra.Command, consoleFilters []string) []string { if cmd.Annotations == nil { if cmd.HasParent() { - return m.ActiveFiltersFor(cmd.Parent()) + return activeFiltersFor(cmd.Parent(), consoleFilters) } return nil } - m.console.mutex.Lock() - defer m.console.mutex.Unlock() - // Get the filters on the command filterStr := cmd.Annotations[CommandFilterKey] var filters []string for _, cmdFilter := range strings.Split(filterStr, ",") { - for _, filter := range m.console.filters { + for _, filter := range consoleFilters { if cmdFilter != "" && cmdFilter == filter { filters = append(filters, cmdFilter) } @@ -270,7 +280,7 @@ func (m *Menu) ActiveFiltersFor(cmd *cobra.Command) []string { } // Any parent that is hidden make its whole subtree hidden also. - return m.ActiveFiltersFor(cmd.Parent()) + return activeFiltersFor(cmd.Parent(), consoleFilters) } // SetErrFilteredCommandTemplate sets the error template to be used diff --git a/run.go b/run.go index ab9340b..7d6e874 100644 --- a/run.go +++ b/run.go @@ -110,7 +110,7 @@ func (m *Menu) RunCommandArgs(ctx context.Context, args []string) (err error) { m.resetPreRun() // Run the command and associated helpers. - return m.console.execute(ctx, m, args, !m.console.isExecuting) + return m.console.execute(ctx, m, args, !m.console.isExecuting.Load()) } // RunCommandLine is the equivalent of menu.RunCommandArgs(), but accepts @@ -138,16 +138,10 @@ func (m *Menu) RunCommandLine(ctx context.Context, line string) (err error) { // command is running, the menu's root command will be overwritten. func (c *Console) execute(ctx context.Context, menu *Menu, args []string, async bool) error { if !async { - c.mutex.RLock() - c.isExecuting = true - c.mutex.RUnlock() + c.isExecuting.Store(true) } - defer func() { - c.mutex.RLock() - c.isExecuting = false - c.mutex.RUnlock() - }() + defer c.isExecuting.Store(false) // Our root command of interest, used throughout this function. cmd := menu.Command @@ -174,7 +168,11 @@ func (c *Console) execute(ctx context.Context, menu *Menu, args []string, async cmd.SetContext(ctx) // Start monitoring keyboard and OS signals. + // signal.Stop releases the channel registration once the command + // returns: without it, every command execution would leak a channel + // in the os/signal package for the lifetime of the process. sigchan := c.monitorSignals() + defer signal.Stop(sigchan) // And start the command execution. go c.executeCommand(cmd, cancel) @@ -278,7 +276,7 @@ func (c *Console) displayPostRun(lastLine string) { // monitorSignals - Monitor the signals that can be sent to the process // while a command is running. We want to be able to cancel the command. -func (c *Console) monitorSignals() <-chan os.Signal { +func (c *Console) monitorSignals() chan os.Signal { sigchan := make(chan os.Signal, 1) signal.Notify( From 93c1cc152861b9c99993c5a2661e0ac75aff4679 Mon Sep 17 00:00:00 2001 From: maxlandon Date: Mon, 1 Jun 2026 02:55:21 +0200 Subject: [PATCH 04/12] cleanup: remove dead code - Drop the unused completion.UnescapeValue; the live completion path uses line.UnescapeValue (completer.go). - Remove the leftover `// menu := app.activeMenu()` comment in the prompt. - Simplify the BindPrompt primary wrapper to a plain nil-guard and fix its stale comment: newline handling now lives in readline, not here. - Drop the redundant `buf == ""` branch (len(buf) == 0 already covers it). Co-Authored-By: Claude Opus 4.8 (1M context) --- internal/completion/line.go | 13 ------------- internal/ui/prompt.go | 11 +++-------- menu.go | 2 +- 3 files changed, 4 insertions(+), 22 deletions(-) diff --git a/internal/completion/line.go b/internal/completion/line.go index 45d3ec4..5da8f32 100644 --- a/internal/completion/line.go +++ b/internal/completion/line.go @@ -11,19 +11,6 @@ import ( "github.com/reeflective/console/internal/line" ) -// when the completer has returned us some completions, we sometimes -// needed to post-process them a little before passing them to our shell. -func UnescapeValue(prefixComp, prefixLine, val string) string { - quoted := strings.HasPrefix(prefixLine, "\"") || - strings.HasPrefix(prefixLine, "'") - - if quoted { - val = strings.ReplaceAll(val, "\\ ", " ") - } - - return val -} - // SplitArgs splits the line in valid words, prepares them in various ways before calling // the completer with them, and also determines which parts of them should be used as // prefixes, in the completions and/or in the line. diff --git a/internal/ui/prompt.go b/internal/ui/prompt.go index 750d38d..a7b590c 100644 --- a/internal/ui/prompt.go +++ b/internal/ui/prompt.go @@ -27,8 +27,6 @@ func NewPrompt(appName, menuName string, stdout *bytes.Buffer) *Prompt { prompt.Primary = func() string { promptStr := appName - // menu := app.activeMenu() - if menuName == "" { return promptStr + " > " } @@ -51,17 +49,14 @@ func NewPrompt(appName, menuName string, stdout *bytes.Buffer) *Prompt { func BindPrompt(p *Prompt, shell *readline.Shell) { prompt := shell.Prompt - // If the user has bound its own primary prompt and the shell - // must leave a newline after command/log output, wrap its function - // to add a newline before the prompt. + // Guard against a nil primary prompt, since the shell calls this on + // every render. Newlines around the prompt are handled by readline. primary := func() string { if p.Primary == nil { return "" } - prompt := p.Primary() - - return prompt + return p.Primary() } prompt.Primary(primary) diff --git a/menu.go b/menu.go index ab63a78..7df1e55 100644 --- a/menu.go +++ b/menu.go @@ -337,7 +337,7 @@ func (m *Menu) resetCmdOutput() { buf := strings.TrimSpace(m.out.String()) // If our command has printed everything to stdout, nothing to do. - if len(buf) == 0 || buf == "" { + if len(buf) == 0 { m.out.Reset() return } From 033a56ee72139c1eabceaf18681ea98ff6eff9e3 Mon Sep 17 00:00:00 2001 From: maxlandon Date: Mon, 1 Jun 2026 02:58:48 +0200 Subject: [PATCH 05/12] test: add parser test suite for internal/line and internal/completion Table-driven coverage for the shell-parsing core: internal/line: - Parse: comment stripping, quoting, whitespace collapse, unterminated quotes (error). - Split: words, quotes, escaped spaces, and the unterminated quote/escape remainder paths. - AcceptMultiline: complete vs. unterminated-quote/escape continuation. - IsEmpty, TrimSpaces, UnescapeValue. internal/completion: - splitCompWords: words/quotes plus remainder + error on unterminated quote/escape. - adjustQuotedPrefix: the quote/escape prefix derivation. - sanitizeArgs: newline/tab/escaped-space normalization. - SplitArgs: empty line, partial word, trailing-space new word, unterminated-quote prefix handling, and ANSI color stripping. Co-Authored-By: Claude Opus 4.8 (1M context) --- internal/completion/line_test.go | 109 +++++++++++++++++++++ internal/line/line_test.go | 156 +++++++++++++++++++++++++++++++ 2 files changed, 265 insertions(+) create mode 100644 internal/completion/line_test.go create mode 100644 internal/line/line_test.go diff --git a/internal/completion/line_test.go b/internal/completion/line_test.go new file mode 100644 index 0000000..090e4c1 --- /dev/null +++ b/internal/completion/line_test.go @@ -0,0 +1,109 @@ +package completion + +import ( + "reflect" + "testing" + + "github.com/reeflective/console/internal/line" +) + +func TestSplitCompWords(t *testing.T) { + tests := []struct { + name string + input string + wantWords []string + wantRemainder string + wantErr error + }{ + {"empty", "", []string{}, "", nil}, + {"two words", "echo hello", []string{"echo", "hello"}, "", nil}, + {"single quoted", "echo 'hello world'", []string{"echo", "hello world"}, "", nil}, + {"double quoted", `echo "hello world"`, []string{"echo", "hello world"}, "", nil}, + {"unterminated single", "echo 'foo", []string{"echo"}, "foo", line.ErrUnterminatedSingleQuote}, + {"unterminated double", `echo "foo`, []string{"echo"}, "foo", line.ErrUnterminatedDoubleQuote}, + {"trailing backslash", `echo foo\`, []string{"echo"}, `foo\`, line.ErrUnterminatedEscape}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + words, remainder, err := splitCompWords(tc.input) + if err != tc.wantErr { + t.Fatalf("splitCompWords(%q) err = %v, want %v", tc.input, err, tc.wantErr) + } + if !reflect.DeepEqual(words, tc.wantWords) { + t.Fatalf("splitCompWords(%q) words = %q, want %q", tc.input, words, tc.wantWords) + } + if remainder != tc.wantRemainder { + t.Fatalf("splitCompWords(%q) remainder = %q, want %q", tc.input, remainder, tc.wantRemainder) + } + }) + } +} + +func TestAdjustQuotedPrefix(t *testing.T) { + tests := []struct { + name string + remain string + err error + wantArg string + wantComp string + wantInput string + }{ + {"no error", "foo", nil, "foo", "", ""}, + {"double quote", "foo", line.ErrUnterminatedDoubleQuote, "foo", `"`, `"foo`}, + {"single quote", "foo", line.ErrUnterminatedSingleQuote, "foo", "'", "'foo"}, + {"escape strips backslashes", `fo\o`, line.ErrUnterminatedEscape, "foo", "", ""}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + arg, comp, input := adjustQuotedPrefix(tc.remain, tc.err) + if arg != tc.wantArg || comp != tc.wantComp || input != tc.wantInput { + t.Fatalf("adjustQuotedPrefix(%q) = (%q, %q, %q), want (%q, %q, %q)", + tc.remain, arg, comp, input, tc.wantArg, tc.wantComp, tc.wantInput) + } + }) + } +} + +func TestSanitizeArgs(t *testing.T) { + got := sanitizeArgs([]string{"a\nb", "c\td", `e\ f`}) + want := []string{"a b", "c d", "e f"} + if !reflect.DeepEqual(got, want) { + t.Fatalf("sanitizeArgs = %q, want %q", got, want) + } +} + +func TestSplitArgs(t *testing.T) { + tests := []struct { + name string + input string + wantArgs []string + wantPrefixComp string + wantPrefixLine string + }{ + {"empty line completes root", "", []string{""}, "", ""}, + {"partial word", "cmd", []string{"cmd"}, "", ""}, + {"trailing space starts new word", "cmd ", []string{"cmd", ""}, "", ""}, + {"two words", "cmd arg", []string{"cmd", "arg"}, "", ""}, + {"unterminated double quote", `cmd "foo`, []string{"cmd", "foo"}, `"`, `"foo`}, + {"unterminated single quote", "cmd 'foo", []string{"cmd", "foo"}, "'", "'foo"}, + {"color codes stripped", "\x1b[32mcmd\x1b[0m", []string{"cmd"}, "", ""}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + runes := []rune(tc.input) + args, prefixComp, prefixLine := SplitArgs(runes, len(runes)) + if !reflect.DeepEqual(args, tc.wantArgs) { + t.Fatalf("SplitArgs(%q) args = %q, want %q", tc.input, args, tc.wantArgs) + } + if prefixComp != tc.wantPrefixComp { + t.Fatalf("SplitArgs(%q) prefixComp = %q, want %q", tc.input, prefixComp, tc.wantPrefixComp) + } + if prefixLine != tc.wantPrefixLine { + t.Fatalf("SplitArgs(%q) prefixLine = %q, want %q", tc.input, prefixLine, tc.wantPrefixLine) + } + }) + } +} diff --git a/internal/line/line_test.go b/internal/line/line_test.go new file mode 100644 index 0000000..192e589 --- /dev/null +++ b/internal/line/line_test.go @@ -0,0 +1,156 @@ +package line + +import ( + "errors" + "reflect" + "testing" +) + +func TestParse(t *testing.T) { + tests := []struct { + name string + input string + want []string + wantErr bool + }{ + {"empty", "", nil, false}, + {"simple", "echo hello", []string{"echo", "hello"}, false}, + {"extra spaces collapse", "echo hello world", []string{"echo", "hello", "world"}, false}, + {"trailing comment", "echo hello # a comment", []string{"echo", "hello"}, false}, + {"comment only", "# just a comment", nil, false}, + {"single quotes", "echo 'hello world'", []string{"echo", "hello world"}, false}, + {"double quotes", `echo "hello world"`, []string{"echo", "hello world"}, false}, + {"quoted hash not a comment", "echo '# not a comment'", []string{"echo", "# not a comment"}, false}, + {"unterminated single quote", "echo 'oops", nil, true}, + {"unterminated double quote", `echo "oops`, nil, true}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + got, err := Parse(tc.input) + if tc.wantErr { + if err == nil { + t.Fatalf("Parse(%q): expected error, got nil (words=%q)", tc.input, got) + } + return + } + if err != nil { + t.Fatalf("Parse(%q): unexpected error: %v", tc.input, err) + } + if len(got) == 0 && len(tc.want) == 0 { + return + } + if !reflect.DeepEqual(got, tc.want) { + t.Fatalf("Parse(%q) = %q, want %q", tc.input, got, tc.want) + } + }) + } +} + +func TestSplit(t *testing.T) { + tests := []struct { + name string + input string + wantWords []string + wantErr error + }{ + {"empty", "", []string{}, nil}, + {"simple", "echo hello", []string{"echo", "hello"}, nil}, + {"single quotes", "echo 'hello world'", []string{"echo", "hello world"}, nil}, + {"double quotes", `echo "hello world"`, []string{"echo", "hello world"}, nil}, + {"escaped space", `echo foo\ bar`, []string{"echo", "foo bar"}, nil}, + {"unterminated single", "echo 'oops", []string{"echo"}, ErrUnterminatedSingleQuote}, + {"unterminated double", `echo "oops`, []string{"echo"}, ErrUnterminatedDoubleQuote}, + {"trailing backslash", `echo foo\`, []string{"echo"}, ErrUnterminatedEscape}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + words, _, err := Split(tc.input, false) + if !errors.Is(err, tc.wantErr) { + t.Fatalf("Split(%q) err = %v, want %v", tc.input, err, tc.wantErr) + } + if !reflect.DeepEqual(words, tc.wantWords) { + t.Fatalf("Split(%q) words = %q, want %q", tc.input, words, tc.wantWords) + } + }) + } +} + +func TestAcceptMultiline(t *testing.T) { + tests := []struct { + name string + input string + want bool + }{ + {"complete", "echo hello", true}, + {"complete quoted", `echo "hello world"`, true}, + {"unterminated single quote", "echo 'oops", false}, + {"unterminated double quote", `echo "oops`, false}, + {"trailing backslash", `echo foo\`, false}, + {"empty", "", true}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + if got := AcceptMultiline([]rune(tc.input)); got != tc.want { + t.Fatalf("AcceptMultiline(%q) = %v, want %v", tc.input, got, tc.want) + } + }) + } +} + +func TestIsEmpty(t *testing.T) { + empty := []rune{' ', '\t'} + + tests := []struct { + name string + input string + chars []rune + want bool + }{ + {"empty string", "", empty, true}, + {"only spaces", " ", empty, true}, + {"spaces and tabs", " \t \t ", empty, true}, + {"has content", " x ", empty, false}, + {"content no chars", "abc", nil, false}, + {"newline not in set", "\n", empty, false}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + if got := IsEmpty(tc.input, tc.chars...); got != tc.want { + t.Fatalf("IsEmpty(%q) = %v, want %v", tc.input, got, tc.want) + } + }) + } +} + +func TestTrimSpaces(t *testing.T) { + got := TrimSpaces([]string{" a ", "b\t", "\tc"}) + want := []string{"a", "b", "c"} + if !reflect.DeepEqual(got, want) { + t.Fatalf("TrimSpaces = %q, want %q", got, want) + } +} + +func TestUnescapeValue(t *testing.T) { + tests := []struct { + name string + prefixLine string + val string + want string + }{ + {"double-quoted unescapes spaces", `"foo`, `bar\ baz`, "bar baz"}, + {"single-quoted unescapes spaces", `'foo`, `bar\ baz`, "bar baz"}, + {"unquoted left as-is", "foo", `bar\ baz`, `bar\ baz`}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + if got := UnescapeValue("", tc.prefixLine, tc.val); got != tc.want { + t.Fatalf("UnescapeValue(%q, %q) = %q, want %q", tc.prefixLine, tc.val, got, tc.want) + } + }) + } +} From 7f226e09542b4575658e76c154f1af868091694c Mon Sep 17 00:00:00 2001 From: maxlandon Date: Mon, 1 Jun 2026 03:08:18 +0200 Subject: [PATCH 06/12] perf: trim work on the completion and highlight hot paths Two hot paths run on (nearly) every keystroke. Reduce their per-call cost without changing observable behavior: - Syntax highlighting: memoize the last result keyed on the input string. readline calls the highlighter on every render (including cursor-only moves), so an unchanged line no longer re-splits the input and re-walks the command tree. The cache is invalidated whenever the command tree is regenerated (Menu.regenerate), so a stale tree can't yield a stale highlight; the key is stored/loaded via atomic.Pointer to stay race-free. - Completion reset: complete() previously called the full resetPreRun() (which rebuilds the command tree, re-binds the prompt, and resets the output buffer) and then redundantly re-hid filtered commands. Split out a lighter resetCommands() that only regenerates + re-filters the tree: the prompt is already bound and no command output is produced during completion, so that extra work was wasted per keystroke. resetPreRun now delegates to the same shared regenerate() core. Co-Authored-By: Claude Opus 4.8 (1M context) --- completer.go | 23 ++++++++++++++++++++--- console.go | 13 +++++++++++++ menu.go | 34 +++++++++++++++++++++++++++------- 3 files changed, 60 insertions(+), 10 deletions(-) diff --git a/completer.go b/completer.go index bf33b1a..c60604d 100644 --- a/completer.go +++ b/completer.go @@ -81,10 +81,12 @@ func (c *Console) complete(input []rune, pos int) readline.Completions { comps = comps.Prefix(prefixComp) comps.PREFIX = prefixLine - // Finally, reset our command tree for the next call. + // Finally, reset our command tree for the next call. Only the commands need + // regenerating here: the prompt is already bound and no command output was + // produced, so the full resetPreRun would just be wasted work per keystroke. + // (resetCommands already re-hides filtered commands.) completer.ClearStorage() - menu.resetPreRun() - menu.hideFilteredCommands(menu.Command) + menu.resetCommands() return comps } @@ -115,6 +117,21 @@ func (c *Console) justifyCommandComps(comps readline.Completions) readline.Compl // highlightSyntax - Entrypoint to all input syntax highlighting in the Wiregost console. func (c *Console) highlightSyntax(input []rune) string { + // Serve a memoized result when the input has not changed since the last + // render. The cache is cleared whenever the command tree is regenerated, + // so a stale tree can never produce a stale highlight. + key := string(input) + if cached := c.hlCache.Load(); cached != nil && cached.input == key { + return cached.output + } + + highlighted := c.computeHighlight(input) + c.hlCache.Store(&highlightCache{input: key, output: highlighted}) + + return highlighted +} + +func (c *Console) computeHighlight(input []rune) string { // Split the line as shellwords args, unprocessed, err := line.Split(string(input), true) if err != nil { diff --git a/console.go b/console.go index 5eb984d..7e91b7f 100644 --- a/console.go +++ b/console.go @@ -13,6 +13,12 @@ import ( "github.com/reeflective/readline/inputrc" ) +// highlightCache holds a memoized syntax-highlighting result for one input. +type highlightCache struct { + input string + output string +} + // Console is an integrated console application instance. type Console struct { // Application @@ -28,6 +34,13 @@ type Console struct { printed bool // Used to adjust asynchronous messages too. mutex *sync.RWMutex // Concurrency management. + // hlCache memoizes the last syntax-highlighting result. The highlighter is + // called on every render (even when only the cursor moved), so caching the + // output for an unchanged input avoids re-splitting and re-walking the + // command tree. It is invalidated whenever the command tree is regenerated + // (see Menu.regenerate), so the input alone is a sufficient key. + hlCache atomic.Pointer[highlightCache] + // Execution // Leave an empty line before executing the command. diff --git a/menu.go b/menu.go index 7df1e55..a31fba5 100644 --- a/menu.go +++ b/menu.go @@ -297,6 +297,30 @@ func (m *Menu) resetPreRun() { defer m.mutex.Unlock() // Commands + m.regenerate() + + // Reset or adjust any buffered command output. + m.resetCmdOutput() + + // Prompt binding + prompt := (*ui.Prompt)(m.Prompt()) + ui.BindPrompt(prompt, m.console.shell) +} + +// resetCommands regenerates the menu command tree and re-applies filtering, +// without rebinding the prompt or touching the command-output buffer. It is the +// lighter reset used on the completion hot path, where the prompt is already +// bound and no command output was produced. +func (m *Menu) resetCommands() { + m.mutex.Lock() + defer m.mutex.Unlock() + + m.regenerate() +} + +// regenerate rebuilds the command tree and hides filtered commands. +// It assumes m.mutex is already held. +func (m *Menu) regenerate() { if m.cmds != nil { m.Command = m.cmds() } @@ -307,15 +331,11 @@ func (m *Menu) resetPreRun() { } } - // Hide commands that are not available + // Hide commands that are not available. m.hideFilteredCommands(m.Command) - // Reset or adjust any buffered command output. - m.resetCmdOutput() - - // Prompt binding - prompt := (*ui.Prompt)(m.Prompt()) - ui.BindPrompt(prompt, m.console.shell) + // The command tree just changed, so any memoized highlight is now stale. + m.console.hlCache.Store(nil) } // hide commands that are filtered so that they are not From 3e2be58e477d27a0ed6a5353781f87bea858c9dd Mon Sep 17 00:00:00 2001 From: maxlandon Date: Mon, 1 Jun 2026 03:35:20 +0200 Subject: [PATCH 07/12] chore: remove unused example/cursor.cast recording The cast recording is no longer used by the example app. --- example/cursor.cast | 595 -------------------------------------------- 1 file changed, 595 deletions(-) delete mode 100644 example/cursor.cast diff --git a/example/cursor.cast b/example/cursor.cast deleted file mode 100644 index 1f63736..0000000 --- a/example/cursor.cast +++ /dev/null @@ -1,595 +0,0 @@ -{"version":3,"term":{"cols":150,"rows":40,"type":"xterm-256color","version":"XTerm(397)","theme":{"fg":"#ffffff","bg":"#1b1b1b","palette":"#3d3d3d:#e96d89:#3ea290:#b0ead9:#31658c:#596196:#8292b2:#c8cacc:#4d4d4d:#899aff:#52ad91:#98c9bb:#477ab3:#7882bf:#95a7cc:#edeff2"}},"timestamp":1780184942,"command":"go run .","env":{"SHELL":"/usr/sbin/zsh"}} -[0.22927, "o", "\r\n _____ __ _ _ _ _____ _\r\n | __ \\ / _| | | | (_) / ____| | |\r\n | |__) |___ ___| |_| | ___ ___| |_ ___ _____ | | ___ _ __ ___ ___ | | ___\r\n | _ // _ \\/ _ \\ _| |/ _ \\/ __| __| \\ \\ / / _ \\ | | / _ \\| '_ \\/ __|/ _ \\| |/ _ \\\r\n | | \\ \\ __/ __/ | | | __/ (__| |_| |\\ V / __/ | |___| (_) | | | \\__ \\ (_) | | __/\r\n |_| \\_\\___|\\___|_| |_|\\___|\\___|\\__|_| \\_/ \\___| \\_____\\___/|_| |_|___/\\___/|_|\\___|\r\n\r\n"] -[0.001154, "o", "\u001b[?2004h"] -[0.000029, "o", "\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\r\n"] -[0.000205, "o", "> "] -[0.0001, "o", "\u001b[5 q"] -[0.000486, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] -[0.000994, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:02.699\u001b[0m\u001b[150D\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C"] -[0.000116, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\r\r\n\u001b[2;3mtype a command — try 'notify', 'hint set ...', or 'scan '\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[1A\u001b[1A\u001b[2C"] -[0.000134, "o", "\u001b[150D\u001b[2C\u001b[?25h"] -[1.367838, "o", "\u001b[H\u001b[2J\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\r\n> \u001b[?25l\u001b[150D> \u001b[6n"] -[0.001171, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:04.068\u001b[0m\u001b[150D\u001b[2C\u001b[1B"] -[0.000166, "o", "\u001b[150D\u001b[0J\u001b[1A\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\r\r\n\u001b[2;3mtype a command — try 'notify', 'hint set ...', or 'scan '\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m"] -[0.000604, "o", "\u001b[1A\u001b[1A\u001b[2C\u001b[150D\u001b[2C\u001b[?25h"] -[0.400446, "o", "\u001b[2D\u001b[2C\u001b[6n"] -[0.000808, "o", "\u001b[150D\u001b[2C\u001b[0J \u001b[1;30m01:49:04.470\u001b[0m\u001b[150D\r\r\n"] -[0.000073, "o", "\u001b[0 q\u001b[?2004l"] -[0.000817, "o", "\u001b[?2004h\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\r\n> \u001b[5 q\u001b[?25l\u001b[150D> \u001b[6n"] -[0.001192, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:04.472\u001b[0m\u001b[150D\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A"] -[0.000069, "o", "\u001b[2C"] -[0.000097, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\r\r\n"] -[0.000168, "o", "\u001b[2;3mtype a command — try 'notify', 'hint set ...', or 'scan '\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[1A\u001b[1A\u001b[2C\u001b[150D\u001b[2C\u001b[?25h"] -[0.253221, "o", "\u001b[2D\u001b[2C\u001b[6n"] -[0.000741, "o", "\u001b[150D\u001b[2C\u001b[0J \u001b[1;30m01:49:04.727\u001b[0m\u001b[150D\r\r\n\u001b[0 q\u001b[?2004l"] -[0.001045, "o", "\u001b[?2004h\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\r\n> \u001b[5 q"] -[0.000095, "o", "\u001b[?25l\u001b[150D> "] -[0.000052, "o", "\u001b[6n"] -[0.001536, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] -[0.000061, "o", "\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:04.730\u001b[0m\u001b[150D\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C"] -[0.000126, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\r\r\n"] -[0.000038, "o", "\u001b[2;3mtype a command — try 'notify', 'hint set ...', or 'scan '\u001b[0m\u001b[0K\r\r\n"] -[0.000085, "o", "\u001b[0K\u001b[0m\u001b[1A"] -[0.00001, "o", "\u001b[1A"] -[0.00002, "o", "\u001b[2C"] -[0.000262, "o", "\u001b[150D\u001b[2C\u001b[?25h"] -[1.900381, "o", "\u001b[?25l\u001b[150D> "] -[0.00011, "o", "\u001b[6n"] -[0.000958, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] -[0.000206, "o", "n\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:06.632\u001b[0m\u001b[150D\u001b[3C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[3C"] -[0.002262, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[3C\r\r\n\u001b[0K\u001b[1m\u001b[33mreadline commands \u001b[0m\u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34mn\u001b[22m\u001b[39m\u001b[motify\u001b[0m \u001b[2m# Async status updates shown in the hint lane (transient hint + wake)\u001b[0m \u001b[0K\u001b[1A\u001b[1A\u001b[3C\u001b[150D\u001b[3C\u001b[?25h"] -[0.156857, "o", "\u001b[?25l\u001b[150D> "] -[0.000111, "o", "\u001b[6n"] -[0.000763, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2Cno\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:06.792\u001b[0m\u001b[150D\u001b[4C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[4C"] -[0.001335, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[4C\r\r\n\u001b[0K\u001b[1m\u001b[33mreadline commands \u001b[0m\u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34mno\u001b[22m\u001b[39m\u001b[mtify\u001b[0m \u001b[2m# Async status updates shown in the hint lane (transient hint + wake)\u001b[0m \u001b[0K\u001b[1A\u001b[1A\u001b[4C\u001b[150D\u001b[4C\u001b[?25h"] -[0.124228, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] -[0.000896, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] -[0.000392, "o", "not\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:06.918\u001b[0m\u001b[150D\u001b[5C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[5C"] -[0.001917, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[5C\r\r\n\u001b[0K\u001b[1m\u001b[33mreadline commands \u001b[0m\u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34mnot\u001b[22m\u001b[39m\u001b[mify\u001b[0m \u001b[2m# Async status updates shown in the hint lane (transient hint + wake)\u001b[0m \u001b[0K\u001b[1A\u001b[1A\u001b[5C\u001b[150D\u001b[5C\u001b[?25h"] -[0.158572, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] -[0.001049, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] -[0.00016, "o", "noti\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:07.080\u001b[0m\u001b[150D\u001b[6C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[6C"] -[0.002479, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[6C\r\r\n\u001b[0K\u001b[1m\u001b[33mreadline commands \u001b[0m\u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34mnoti\u001b[22m\u001b[39m\u001b[mfy\u001b[0m \u001b[2m# Async status updates shown in the hint lane (transient hint + wake)\u001b[0m \u001b[0K\u001b[1A\u001b[1A\u001b[6C\u001b[150D\u001b[6C\u001b[?25h"] -[0.137401, "o", "\u001b[?25l\u001b[150D> "] -[0.000172, "o", "\u001b[6n"] -[0.001107, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] -[0.000578, "o", "notif\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:07.222\u001b[0m\u001b[150D\u001b[7C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[7C"] -[0.004882, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[7C\r\r\n\u001b[0K\u001b[1m\u001b[33mreadline commands \u001b[0m\u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34mnotif\u001b[22m\u001b[39m\u001b[my\u001b[0m \u001b[2m# Async status updates shown in the hint lane (transient hint + wake)\u001b[0m \u001b[0K\u001b[1A\u001b[1A\u001b[7C\u001b[150D\u001b[7C\u001b[?25h"] -[0.233665, "o", "\u001b[5 q\u001b[5 q\u001b[5 q\u001b[?25l\u001b[150D> \u001b[6n"] -[0.001602, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] -[0.000122, "o", "\u001b[1m\u001b[32mnotify \u001b[39m\u001b[22m\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:07.462\u001b[0m\u001b[150D\u001b[9C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[9C"] -[0.001222, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[9C\r\r\n"] -[0.000645, "o", "\u001b[2;3m notify — Async status updates shown in the hint lane (transient hint + wake)\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[1A\u001b[1A\u001b[9C\u001b[150D\u001b[9C\u001b[?25h"] -[0.508538, "o", "\u001b[5 q\u001b[9D\u001b[2C\u001b[6n"] -[0.001335, "o", "\u001b[150D\u001b[9C\u001b[0J \u001b[1;30m01:49:07.974\u001b[0m\u001b[150D\r\r\n\u001b[0 q\u001b[?2004l"] -[0.000662, "o", "\r\n"] -[0.001799, "o", "Background job started — watch the hint line below the prompt update on its own (no keystroke needed).\r\n\r\n"] -[0.000482, "o", "\u001b[?2004h\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\r\n> \u001b[5 q"] -[0.000242, "o", "\u001b[?25l"] -[0.000219, "o", "\u001b[150D> \u001b[6n"] -[0.000715, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:07.978\u001b[0m\u001b[150D\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C"] -[0.000098, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\r\r\n\u001b[2;3mtype a command — try 'notify', 'hint set ...', or 'scan '\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[1A\u001b[1A\u001b[2C\u001b[150D\u001b[2C\u001b[?25h"] -[1.199173, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] -[0.001072, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:09.178\u001b[0m\u001b[150D\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C"] -[0.000121, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\r\r\n\u001b[2;3mtype a command — try 'notify', 'hint set ...', or 'scan '\u001b[0m\u001b[0K\r\r\n\u001b[33m⠋ connecting…\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m"] -[0.000266, "o", "\u001b[3A\u001b[1A\u001b[2C\u001b[150D\u001b[2C\u001b[?25h"] -[1.198578, "o", "\u001b[?25l\u001b[150D> "] -[0.000168, "o", "\u001b[6n"] -[0.000739, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:10.378\u001b[0m\u001b[150D\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\r\r\n\u001b[2;3mtype a command — try 'notify', 'hint set ...', or 'scan '\u001b[0m\u001b[0K\r\r\n\u001b[33m⠙ authenticating…\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[3A\u001b[1A\u001b[2C\u001b[150D\u001b[2C\u001b[?25h"] -[1.199802, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] -[0.001404, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:11.579\u001b[0m\u001b[150D\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A"] -[0.00025, "o", "\u001b[2C\r\r\n\u001b[2;3mtype a command — try 'notify', 'hint set ...', or 'scan '\u001b[0m\u001b[0K\r\r\n\u001b[33m⠹ transferring…\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m"] -[0.000127, "o", "\u001b[3A\u001b[1A\u001b[2C\u001b[150D\u001b[2C\u001b[?25h"] -[1.198192, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] -[0.000521, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n"] -[0.000073, "o", "\u001b[2C"] -[0.000427, "o", "\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:12.779\u001b[0m"] -[0.000064, "o", "\u001b[150D\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C"] -[0.000162, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\r\r\n\u001b[2;3mtype a command — try 'notify', 'hint set ...', or 'scan '\u001b[0m\u001b[0K\r\r\n\u001b[32m✓ transfer complete\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[3A"] -[0.000667, "o", "\u001b[1A\u001b[2C\u001b[150D\u001b[2C\u001b[?25h"] -[1.498669, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] -[0.000934, "o", "\u001b[150D"] -[0.000547, "o", "\u001b[1A"] -[0.000699, "o", "\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:14.281\u001b[0m\u001b[150D\u001b[2C"] -[0.000102, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C"] -[0.000351, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\r\r\n"] -[0.000102, "o", "\u001b[2;3mtype a command — try 'notify', 'hint set ...', or 'scan '\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[1A\u001b[1A\u001b[2C\u001b[150D\u001b[2C\u001b[?25h"] -[2.136457, "o", "\u001b[2D\u001b[2C\u001b[6n"] -[0.001127, "o", "\u001b[150D\u001b[2C\u001b[0J \u001b[1;30m01:49:16.419\u001b[0m\u001b[150D\r\r\n\u001b[0 q\u001b[?2004l"] -[0.000324, "o", "\u001b[?2004h"] -[0.000321, "o", "\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\r\n> \u001b[5 q"] -[0.000259, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] -[0.000581, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:16.421\u001b[0m\u001b[150D\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C"] -[0.000052, "o", "\u001b[1B"] -[0.000147, "o", "\u001b[150D"] -[0.000301, "o", "\u001b[0J\u001b[1A\u001b[2C\r\r\n\u001b[2;3mtype a command — try 'notify', 'hint set ...', or 'scan '\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[1A\u001b[1A\u001b[2C\u001b[150D\u001b[2C"] -[0.000097, "o", "\u001b[?25h"] -[0.303004, "o", "\u001b[2D\u001b[2C\u001b[6n"] -[0.001, "o", "\u001b[150D\u001b[2C\u001b[0J \u001b[1;30m01:49:16.725\u001b[0m\u001b[150D\r\r\n"] -[0.000262, "o", "\u001b[0 q\u001b[?2004l"] -[0.000431, "o", "\u001b[?2004h\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\r\n> \u001b[5 q"] -[0.000091, "o", "\u001b[?25l\u001b[150D"] -[0.000179, "o", "> \u001b[6n"] -[0.00114, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] -[0.000085, "o", "\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:16.727\u001b[0m\u001b[150D\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C"] -[0.000171, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\r\r\n\u001b[2;3mtype a command — try 'notify', 'hint set ...', or 'scan '\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[1A\u001b[1A\u001b[2C\u001b[150D\u001b[2C\u001b[?25h"] -[0.49948, "o", "\u001b[5 q"] -[0.001632, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] -[0.001218, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:17.229\u001b[0m\u001b[150D\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\r\r\n\u001b[2;3mtype a command — try 'notify', 'hint set ...', or 'scan '\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m"] -[0.001198, "o", "\u001b[0K\u001b[1m\u001b[33mfilesystem commands \u001b[0m\u001b[0K\r\r\n\u001b[mbackup\u001b[0m \u001b[2m# Create a backup of a file or directory\u001b[0m \u001b[0K\r\r\n\u001b[mconvert\u001b[0m \u001b[2m# Convert a file\u001b[0m \u001b[0K\r\r\n\u001b[mdownload\u001b[0m \u001b[2m# Download a file from a URL\u001b[0m \u001b[0K\r\r\n\u001b[mencrypt\u001b[0m \u001b[2m# Encrypt a file\u001b[0m \u001b[0K\r\r\n\u001b[mls\u001b[0m \u001b[2m# List directory contents\u001b[0m \u001b[0K\r\r\n\u001b[mmkdir\u001b[0m \u001b[2m# Create directories\u001b[0m \u001b[0K\r\r\n\u001b[mrename\u001b[0m \u001b[2m# Rename a file\u001b[0m \u001b[0K\r\r\n\u001b[msearch\u001b[0m \u001b[2m# Search for a query\u001b[0m \u001b[0K\r\r\n\u001b[1m\u001b[33mcore commands \u001b[0m\u001b[0K\r\r\n\u001b[mclient\u001b[0m \u001b[2m# Switch to the client menu (also works with CtrlC)\u001b[0m \u001b[0K\r\r\n\u001b[mexit\u001b[0m \u001b[2m# Exit the console application\u001b[0m \u001b[0K\r\r\n\u001b[mgreet\u001b[0m \u001b[2m# Greet a person\u001b[0m \u001b[0K\r\r\n\u001b[mhello\u001b[0m \u001b[2m# Say hello with customizable message\u001b[0m \u001b[0K\r\r\n\u001b[mhelp\u001b[0m \u001b[2m# Help about any command\u001b[0m \u001b[0K\r\r\n\u001b[1m\u001b[33mdeployment commands \u001b[0m\u001b[0K\r\r\n\u001b[mdeploy\u001b[0m \u001b[2m# Deploy a file\u001b[0m \u001b[0K\r\r\n\u001b[1m\u001b[33mtools commands \u001b[0m\u001b[0K\r\r\n\u001b[mgit\u001b[0m \u001b[2m# Git command\u001b[0m \u001b[0K\r\r\n\u001b[mssh\u001b[0m \u001b[2m# SSH client\u001b[0m \u001b[0K\r\r\n\u001b[1m\u001b[33mreadline commands \u001b[0m\u001b[0K\r\r\n\u001b[mhint\u001b[0m \u001b[2m# Set or clear a sticky transient hint immediately (non-async)\u001b[0m \u001b[0K\r\r\n\u001b[mnotify\u001b[0m \u001b[2m# Async status updates shown in the hint lane (transient hint + wake)\u001b[0m \u001b[0K\r\r\n\u001b[mscan\u001b[0m \u001b[2m# Async completions — press Tab after 'scan ' and watch hosts appear live\u001b[0m \u001b[0K\r\r\n\u001b[1m\u001b[33mother commands \u001b[0m\u001b[0K\r\r\n\u001b[mreadline\u001b[0m \u001b[2m# Manipulate readline options, keymaps and bindings\u001b[0m \u001b[0K\u001b[25A\u001b[1A\u001b[1A\u001b[2C\u001b[150D\u001b[2C\u001b[?25h"] -[1.027009, "o", "\u001b[5 q\u001b[?25l\u001b[150D> \u001b[6n"] -[0.001044, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] -[0.000109, "o", "s\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:18.259\u001b[0m\u001b[150D\u001b[3C\u001b[1B\u001b[150D\u001b[0J\u001b[1A"] -[0.000055, "o", "\u001b[3C"] -[0.002292, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[3C\r\r\n"] -[0.000132, "o", "\u001b[0K\u001b[1m\u001b[33mreadline commands \u001b[0m\u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34ms\u001b[22m\u001b[39m\u001b[mcan\u001b[0m \u001b[2m# Async completions — press Tab after 'scan ' and watch hosts appear live\u001b[0m \u001b[0K\r\r\n\u001b[1m\u001b[33mfilesystem commands \u001b[0m\u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34ms\u001b[22m\u001b[39m\u001b[mearch\u001b[0m \u001b[2m# Search for a query\u001b[0m \u001b[0K\r\r\n\u001b[1m\u001b[33mtools commands \u001b[0m\u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34ms\u001b[22m\u001b[39m\u001b[msh\u001b[0m \u001b[2m# SSH client\u001b[0m \u001b[0K\u001b[5A\u001b[1A\u001b[3C\u001b[150D\u001b[3C\u001b[?25h"] -[0.227561, "o", "\u001b[?25l\u001b[150D> "] -[0.000088, "o", "\u001b[6n"] -[0.000682, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] -[0.000377, "o", "sc\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:18.490\u001b[0m\u001b[150D"] -[0.000225, "o", "\u001b[4C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[4C"] -[0.001889, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[4C\r\r\n\u001b[0K\u001b[1m\u001b[33mreadline commands \u001b[0m\u001b[0K\r\r\n"] -[0.000095, "o", "\u001b[m\u001b[1m\u001b[34msc\u001b[22m\u001b[39m\u001b[man\u001b[0m \u001b[2m# Async completions — press Tab after 'scan ' and watch hosts appear live\u001b[0m \u001b[0K\u001b[1A\u001b[1A\u001b[4C\u001b[150D\u001b[4C\u001b[?25h"] -[0.112281, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] -[0.00088, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2Csca\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:18.606\u001b[0m\u001b[150D\u001b[5C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[5C"] -[0.00109, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[5C\r\r\n\u001b[0K\u001b[1m\u001b[33mreadline commands \u001b[0m\u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34msca\u001b[22m\u001b[39m\u001b[mn\u001b[0m \u001b[2m# Async completions — press Tab after 'scan ' and watch hosts appear live\u001b[0m \u001b[0K\u001b[1A\u001b[1A\u001b[5C\u001b[150D\u001b[5C\u001b[?25h"] -[0.208441, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] -[0.000975, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[1m\u001b[32mscan\u001b[39m\u001b[22m\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:18.816\u001b[0m\u001b[150D\u001b[6C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[6C"] -[0.001349, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[6C\r\r\n\u001b[2;3m scan — Async completions — press Tab after 'scan ' and watch hosts appear live\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[0K\u001b[1m\u001b[33mreadline commands \u001b[0m\u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34mscan\u001b[22m\u001b[39m\u001b[m\u001b[0m \u001b[2m# Async completions — press Tab after 'scan ' and watch hosts appear live\u001b[0m \u001b[0K\u001b[1A\u001b[1A\u001b[1A\u001b[6C\u001b[150D\u001b[6C\u001b[?25h"] -[0.557426, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] -[0.000435, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[1m\u001b[32mscan \u001b[39m\u001b[22m\u001b[0m\u001b[0K\u001b[49m"] -[0.000835, "o", " \u001b[1;30m01:49:19.376\u001b[0m\u001b[150D\u001b[7C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[7C"] -[0.000184, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[7C\r\r\n\u001b[2;3m scan — Async completions — press Tab after 'scan ' and watch hosts appear live\u001b[0m\u001b[0K\r\r\n\u001b[2mscan [HOST]\u001b[0m\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m"] -[0.000068, "o", "\u001b[0K\u001b[mgateway\u001b[0m \u001b[0m\u001b[mlocalhost\u001b[0m \u001b[0m\u001b[0K\u001b[3A\u001b[1A\u001b[7C\u001b[150D\u001b[7C\u001b[?25h"] -[0.900054, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] -[0.000945, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] -[0.000209, "o", "\u001b[1m\u001b[32mscan \u001b[39m\u001b[22m\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:20.278\u001b[0m\u001b[150D\u001b[7C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[7C"] -[0.002928, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[7C\r\r\n\u001b[2;3m scan — Async completions — press Tab after 'scan ' and watch hosts appear live\u001b[0m\u001b[0K\r\r\n\u001b[2mscan [HOST]\u001b[0m\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[0K\u001b[m10.0.0.1\u001b[0m \u001b[0m\u001b[mgateway\u001b[0m \u001b[0m\u001b[mlocalhost\u001b[0m \u001b[0m\u001b[0K\u001b[3A\u001b[1A\u001b[7C\u001b[150D\u001b[7C\u001b[?25h"] -[0.895915, "o", "\u001b[?25l\u001b[150D"] -[0.000118, "o", "> "] -[0.000494, "o", "\u001b[6n"] -[0.001276, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[1m\u001b[32mscan \u001b[39m\u001b[22m\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:21.178\u001b[0m\u001b[150D\u001b[7C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[7C"] -[0.00057, "o", "\u001b[1B\u001b[150D"] -[0.000192, "o", "\u001b[0J\u001b[1A\u001b[7C\r\r\n\u001b[2;3m scan — Async completions — press Tab after 'scan ' and watch hosts appear live\u001b[0m\u001b[0K\r\r\n\u001b[2mscan [HOST]\u001b[0m\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[0K\u001b[m10.0.0.1\u001b[0m \u001b[0m\u001b[m10.0.0.2\u001b[0m \u001b[0m\u001b[mgateway\u001b[0m \u001b[0m\u001b[mlocalhost\u001b[0m \u001b[0m\u001b[0K\u001b[3A\u001b[1A\u001b[7C\u001b[150D\u001b[7C\u001b[?25h"] -[0.898603, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] -[0.000936, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[1m\u001b[32mscan \u001b[39m\u001b[22m\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:22.079\u001b[0m\u001b[150D"] -[0.000107, "o", "\u001b[7C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[7C"] -[0.001268, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[7C\r\r\n\u001b[2;3m scan — Async completions — press Tab after 'scan ' and watch hosts appear live\u001b[0m\u001b[0K\r\r\n\u001b[2mscan [HOST]\u001b[0m\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m"] -[0.000149, "o", "\u001b[0K\u001b[m10.0.0.1\u001b[0m \u001b[0m\u001b[m10.0.0.2\u001b[0m \u001b[0m\u001b[m10.0.0.3\u001b[0m \u001b[0m\u001b[mgateway\u001b[0m \u001b[0m\u001b[mlocalhost\u001b[0m \u001b[0m\u001b[0K\u001b[3A\u001b[1A\u001b[7C\u001b[150D\u001b[7C\u001b[?25h"] -[0.897459, "o", "\u001b[?25l\u001b[150D> "] -[0.000191, "o", "\u001b[6n"] -[0.001205, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[1m\u001b[32mscan \u001b[39m\u001b[22m\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:22.979\u001b[0m\u001b[150D\u001b[7C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[7C"] -[0.001653, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[7C\r\r\n\u001b[2;3m scan — Async completions — press Tab after 'scan ' and watch hosts appear live\u001b[0m\u001b[0K\r\r\n\u001b[2mscan [HOST]\u001b[0m\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[0K\u001b[m10.0.0.1\u001b[0m \u001b[0m\u001b[m10.0.0.2\u001b[0m \u001b[0m\u001b[m10.0.0.3\u001b[0m \u001b[0m\u001b[m10.0.0.4\u001b[0m \u001b[0m\u001b[mgateway\u001b[0m \u001b[0m\u001b[mlocalhost\u001b[0m \u001b[0m\u001b[0K\u001b[3A\u001b[1A\u001b[7C\u001b[150D\u001b[7C\u001b[?25h"] -[0.897222, "o", "\u001b[?25l\u001b[150D> "] -[0.000216, "o", "\u001b[6n"] -[0.000942, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] -[0.000168, "o", "\u001b[1m\u001b[32mscan \u001b[39m\u001b[22m\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:23.879\u001b[0m\u001b[150D\u001b[7C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[7C"] -[0.001748, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[7C\r\r\n\u001b[2;3m scan — Async completions — press Tab after 'scan ' and watch hosts appear live\u001b[0m\u001b[0K\r\r\n\u001b[2mscan [HOST]\u001b[0m\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[0K\u001b[m10.0.0.1\u001b[0m \u001b[0m\u001b[m10.0.0.2\u001b[0m \u001b[0m\u001b[m10.0.0.3\u001b[0m \u001b[0m\u001b[m10.0.0.4\u001b[0m \u001b[0m\u001b[m10.0.0.5\u001b[0m \u001b[0m\u001b[mgateway\u001b[0m \u001b[0m\u001b[mlocalhost\u001b[0m \u001b[0m\u001b[0K\u001b[3A\u001b[1A\u001b[7C\u001b[150D\u001b[7C\u001b[?25h"] -[0.731148, "o", "\u001b[5 q\u001b[?25l\u001b[150D> \u001b[6n"] -[0.00111, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[1m\u001b[32mscan \u001b[39m\u001b[22m10.0.0.1 \u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:24.613\u001b[0m\u001b[150D\u001b[16C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[16C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[16C\r\r\n\u001b[2;3m scan — Async completions — press Tab after 'scan ' and watch hosts appear live\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[0K\u001b[48;05;255m\u001b[1;40m10.0.0.1 \u001b[0m\u001b[m10.0.0.2\u001b[0m \u001b[0m\u001b[m10.0.0.3\u001b[0m \u001b[0m\u001b[m10.0.0.4\u001b[0m \u001b[0m\u001b[m10.0.0.5\u001b[0m \u001b[0m\u001b[mgateway\u001b[0m \u001b[0m\u001b[mlocalhost\u001b[0m \u001b[0m\u001b[0K\u001b[1A\u001b[1A\u001b[16C\u001b[150D\u001b[16C\u001b[?25h"] -[0.164697, "o", "\u001b[?25l\u001b[150D"] -[0.000047, "o", "> \u001b[6n"] -[0.000874, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[1m\u001b[32mscan \u001b[39m\u001b[22m10.0.0.1 \u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:24.779\u001b[0m\u001b[150D\u001b[16C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[16C"] -[0.000054, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[16C\r\r\n"] -[0.000096, "o", "\u001b[2;3m scan — Async completions — press Tab after 'scan ' and watch hosts appear live\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[0K\u001b[48;05;255m\u001b[1;40m10.0.0.1 \u001b[0m\u001b[m10.0.0.2\u001b[0m \u001b[0m\u001b[m10.0.0.3\u001b[0m \u001b[0m\u001b[m10.0.0.4\u001b[0m \u001b[0m\u001b[m10.0.0.5\u001b[0m \u001b[0m\u001b[mgateway\u001b[0m \u001b[0m\u001b[mlocalhost\u001b[0m \u001b[0m\u001b[0K\u001b[1A\u001b[1A\u001b[16C\u001b[150D\u001b[16C\u001b[?25h"] -[0.899372, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] -[0.001166, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] -[0.000362, "o", "\u001b[1m\u001b[32mscan \u001b[39m\u001b[22m10.0.0.1 \u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:25.680\u001b[0m\u001b[150D\u001b[16C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[16C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[16C\r\r\n"] -[0.000246, "o", "\u001b[2;3m scan — Async completions — press Tab after 'scan ' and watch hosts appear live\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[0K\u001b[48;05;255m\u001b[1;40m10.0.0.1 \u001b[0m\u001b[m10.0.0.2\u001b[0m \u001b[0m\u001b[m10.0.0.3\u001b[0m \u001b[0m\u001b[m10.0.0.4\u001b[0m \u001b[0m\u001b[m10.0.0.5\u001b[0m \u001b[0m\u001b[mgateway\u001b[0m \u001b[0m\u001b[mlocalhost\u001b[0m \u001b[0m\u001b[0K\u001b[1A\u001b[1A\u001b[16C\u001b[150D\u001b[16C\u001b[?25h"] -[0.898206, "o", "\u001b[?25l\u001b[150D> "] -[0.000057, "o", "\u001b[6n"] -[0.001162, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[1m\u001b[32mscan \u001b[39m\u001b[22m10.0.0.1 \u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:26.580\u001b[0m\u001b[150D\u001b[16C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[16C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[16C\r\r\n"] -[0.000022, "o", "\u001b[2;3m scan — Async completions — press Tab after 'scan ' and watch hosts appear live\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[0K\u001b[48;05;255m\u001b[1;40m10.0.0.1 \u001b[0m\u001b[m10.0.0.2\u001b[0m \u001b[0m\u001b[m10.0.0.3\u001b[0m \u001b[0m\u001b[m10.0.0.4\u001b[0m \u001b[0m\u001b[m10.0.0.5\u001b[0m \u001b[0m\u001b[mgateway\u001b[0m \u001b[0m\u001b[mlocalhost\u001b[0m \u001b[0m\u001b[0K\u001b[1A\u001b[1A\u001b[16C\u001b[150D\u001b[16C\u001b[?25h"] -[1.505176, "o", "\u001b[5 q\u001b[?25l\u001b[150D> \u001b[6n"] -[0.000825, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[1m\u001b[32mscan \u001b[39m\u001b[22m10.0.0.\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:28.086\u001b[0m\u001b[150D\u001b[14C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[14C"] -[0.000518, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[14C\r\r\n\u001b[2;3m scan — Async completions — press Tab after 'scan ' and watch hosts appear live\u001b[0m\u001b[0K\r\r\n\u001b[2mscan [HOST]\u001b[0m\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m"] -[0.000029, "o", "\u001b[3A\u001b[1A\u001b[14C\u001b[150D\u001b[14C\u001b[?25h"] -[0.17032, "o", "\u001b[?25l"] -[0.000212, "o", "\u001b[150D> "] -[0.000062, "o", "\u001b[6n"] -[0.000978, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[1m\u001b[32mscan \u001b[39m\u001b[22m10.0.0\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:28.258\u001b[0m\u001b[150D"] -[0.0002, "o", "\u001b[13C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[13C"] -[0.00132, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[13C\r\r\n\u001b[2;3m scan — Async completions — press Tab after 'scan ' and watch hosts appear live\u001b[0m\u001b[0K\r\r\n\u001b[2mscan [HOST]\u001b[0m\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[3A\u001b[1A\u001b[13C\u001b[150D\u001b[13C\u001b[?25h"] -[0.174161, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] -[0.001698, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[1m\u001b[32mscan \u001b[39m\u001b[22m10.0.\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:28.435\u001b[0m\u001b[150D\u001b[12C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[12C"] -[0.0011, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[12C\r\r\n\u001b[2;3m scan — Async completions — press Tab after 'scan ' and watch hosts appear live\u001b[0m\u001b[0K\r\r\n\u001b[2mscan [HOST]\u001b[0m\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[3A\u001b[1A\u001b[12C\u001b[150D\u001b[12C\u001b[?25h"] -[0.162056, "o", "\u001b[?25l\u001b[150D> "] -[0.000136, "o", "\u001b[6n"] -[0.00081, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] -[0.000069, "o", "\u001b[1m\u001b[32mscan \u001b[39m\u001b[22m10.0\u001b[0m\u001b[0K\u001b[49m"] -[0.000277, "o", " \u001b[1;30m01:49:28.599\u001b[0m\u001b[150D\u001b[11C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[11C"] -[0.001347, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[11C\r\r\n\u001b[2;3m scan — Async completions — press Tab after 'scan ' and watch hosts appear live\u001b[0m\u001b[0K\r\r\n\u001b[2mscan [HOST]\u001b[0m\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[3A\u001b[1A\u001b[11C\u001b[150D\u001b[11C\u001b[?25h"] -[0.173528, "o", "\u001b[?25l\u001b[150D"] -[0.000113, "o", "> \u001b[6n"] -[0.000934, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[1m\u001b[32mscan \u001b[39m\u001b[22m10.\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:28.776\u001b[0m\u001b[150D"] -[0.000028, "o", "\u001b[10C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[10C"] -[0.00346, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[10C\r\r\n\u001b[2;3m scan — Async completions — press Tab after 'scan ' and watch hosts appear live\u001b[0m\u001b[0K\r\r\n\u001b[2mscan [HOST]\u001b[0m\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[3A\u001b[1A\u001b[10C\u001b[150D\u001b[10C\u001b[?25h"] -[0.176575, "o", "\u001b[?25l\u001b[150D> "] -[0.000341, "o", "\u001b[6n"] -[0.0013, "o", "\u001b[150D\u001b[1A"] -[0.000417, "o", "\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[1m\u001b[32mscan \u001b[39m\u001b[22m10\u001b[0m\u001b[0K\u001b[49m"] -[0.000101, "o", " \u001b[1;30m01:49:28.958\u001b[0m\u001b[150D\u001b[9C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[9C"] -[0.001305, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[9C\r\r\n\u001b[2;3m scan — Async completions — press Tab after 'scan ' and watch hosts appear live\u001b[0m\u001b[0K\r\r\n\u001b[2mscan [HOST]\u001b[0m\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[3A\u001b[1A\u001b[9C\u001b[150D\u001b[9C\u001b[?25h"] -[0.027352, "o", "\u001b[?25l\u001b[150D> "] -[0.000079, "o", "\u001b[6n"] -[0.000829, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[1m\u001b[32mscan \u001b[39m\u001b[22m10\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:28.987\u001b[0m\u001b[150D\u001b[9C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[9C"] -[0.000737, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[9C\r\r\n\u001b[2;3m scan — Async completions — press Tab after 'scan ' and watch hosts appear live\u001b[0m\u001b[0K\r\r\n\u001b[2mscan [HOST]\u001b[0m\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[0K\u001b[m\u001b[1m\u001b[34m10\u001b[22m\u001b[39m\u001b[m.0.0.1\u001b[0m \u001b[0m\u001b[0K\u001b[3A\u001b[1A\u001b[9C\u001b[150D\u001b[9C\u001b[?25h"] -[0.14891, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] -[0.00102, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[1m\u001b[32mscan \u001b[39m\u001b[22m\u001b[0m\u001b[0K\u001b[49m"] -[0.00016, "o", " \u001b[1;30m01:49:29.138\u001b[0m\u001b[150D\u001b[7C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[7C"] -[0.000913, "o", "\u001b[1B\u001b[150D\u001b[0J"] -[0.000328, "o", "\u001b[1A\u001b[7C\r\r\n\u001b[2;3m scan — Async completions — press Tab after 'scan ' and watch hosts appear live\u001b[0m\u001b[0K\r\r\n\u001b[2mscan [HOST]\u001b[0m\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[0K\u001b[m10.0.0.1\u001b[0m \u001b[0m\u001b[mgateway\u001b[0m \u001b[0m\u001b[mlocalhost\u001b[0m \u001b[0m\u001b[0K\u001b[3A\u001b[1A\u001b[7C\u001b[150D\u001b[7C\u001b[?25h"] -[0.192664, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] -[0.000779, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:29.333\u001b[0m\u001b[150D\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\r\r\n\u001b[2;3mtype a command — try 'notify', 'hint set ...', or 'scan '\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[1A\u001b[1A\u001b[2C\u001b[150D\u001b[2C\u001b[?25h"] -[0.205749, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] -[0.000744, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:29.539\u001b[0m\u001b[150D\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\r\r\n"] -[0.000078, "o", "\u001b[2;3mtype a command — try 'notify', 'hint set ...', or 'scan '\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[1A\u001b[1A\u001b[2C\u001b[150D\u001b[2C\u001b[?25h"] -[0.209697, "o", "\u001b[H\u001b[2J\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\r\n> \u001b[?25l\u001b[150D> \u001b[6n"] -[0.00108, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] -[0.000041, "o", "\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:29.750\u001b[0m\u001b[150D\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\u001b[1B"] -[0.000313, "o", "\u001b[150D\u001b[0J\u001b[1A\u001b[2C\r\r\n\u001b[2;3mtype a command — try 'notify', 'hint set ...', or 'scan '\u001b[0m\u001b[0K\r\r\n"] -[0.000164, "o", "\u001b[0K\u001b[0m"] -[0.000035, "o", "\u001b[1A"] -[0.000149, "o", "\u001b[1A"] -[0.000101, "o", "\u001b[2C\u001b[150D\u001b[2C\u001b[?25h"] -[0.13543, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] -[0.001023, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:29.887\u001b[0m\u001b[150D\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\r\r\n\u001b[2;3mtype a command — try 'notify', 'hint set ...', or 'scan '\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[1A\u001b[1A\u001b[2C\u001b[150D\u001b[2C\u001b[?25h"] -[0.259924, "o", "\u001b[2D\u001b[2C\u001b[6n"] -[0.001132, "o", "\u001b[150D\u001b[2C\u001b[0J \u001b[1;30m01:49:30.148\u001b[0m\u001b[150D\r\r\n\u001b[0 q\u001b[?2004l"] -[0.000501, "o", "\u001b[?2004h\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\r\n> "] -[0.000078, "o", "\u001b[5 q\u001b[?25l\u001b[150D"] -[0.000056, "o", "> \u001b[6n"] -[0.001296, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:30.150\u001b[0m\u001b[150D\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\u001b[1B\u001b[150D"] -[0.000052, "o", "\u001b[0J\u001b[1A\u001b[2C\r\r\n"] -[0.000104, "o", "\u001b[2;3mtype a command — try 'notify', 'hint set ...', or 'scan '\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m"] -[0.000227, "o", "\u001b[1A\u001b[1A\u001b[2C\u001b[150D\u001b[2C\u001b[?25h"] -[0.22294, "o", "\u001b[2D\u001b[2C\u001b[6n"] -[0.001, "o", "\u001b[150D\u001b[2C\u001b[0J \u001b[1;30m01:49:30.375\u001b[0m\u001b[150D\r\r\n\u001b[0 q\u001b[?2004l"] -[0.000787, "o", "\u001b[?2004h\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\r\n> \u001b[5 q\u001b[?25l"] -[0.00002, "o", "\u001b[150D"] -[0.000188, "o", "> \u001b[6n"] -[0.001014, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] -[0.00007, "o", "\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:30.377\u001b[0m\u001b[150D\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C"] -[0.000119, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\r\r\n\u001b[2;3mtype a command — try 'notify', 'hint set ...', or 'scan '\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m"] -[0.000065, "o", "\u001b[1A\u001b[1A\u001b[2C\u001b[150D\u001b[2C\u001b[?25h"] -[0.387653, "o", "\u001b[5 q"] -[0.00084, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] -[0.000944, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:30.766\u001b[0m\u001b[150D\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A"] -[0.000147, "o", "\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\r\r\n\u001b[2;3mtype a command — try 'notify', 'hint set ...', or 'scan '\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m"] -[0.000107, "o", "\u001b[0K\u001b[1m\u001b[33mfilesystem commands \u001b[0m\u001b[0K\r\r\n\u001b[mbackup\u001b[0m \u001b[2m# Create a backup of a file or directory\u001b[0m \u001b[0K\r\r\n\u001b[mconvert\u001b[0m \u001b[2m# Convert a file\u001b[0m \u001b[0K\r\r\n\u001b[mdownload\u001b[0m \u001b[2m# Download a file from a URL\u001b[0m \u001b[0K\r\r\n\u001b[mencrypt\u001b[0m \u001b[2m# Encrypt a file\u001b[0m \u001b[0K\r\r\n\u001b[mls\u001b[0m \u001b[2m# List directory contents\u001b[0m \u001b[0K\r\r\n\u001b[mmkdir\u001b[0m \u001b[2m# Create directories\u001b[0m \u001b[0K\r\r\n\u001b[mrename\u001b[0m \u001b[2m# Rename a file\u001b[0m \u001b[0K\r\r\n\u001b[msearch\u001b[0m \u001b[2m# Search for a query\u001b[0m \u001b[0K\r\r\n\u001b[1m\u001b[33mcore commands \u001b[0m\u001b[0K\r\r\n\u001b[mclient\u001b[0m \u001b[2m# Switch to the client menu (also works with CtrlC)\u001b[0m \u001b[0K\r\r\n\u001b[mexit\u001b[0m \u001b[2m# Exit the console application\u001b[0m \u001b[0K\r\r\n\u001b[mgreet\u001b[0m \u001b[2m# Greet a person\u001b[0m \u001b[0K\r\r\n\u001b[mhello\u001b[0m \u001b[2m# Say hello with customizable message\u001b[0m \u001b[0K\r\r\n\u001b[mhelp\u001b[0m \u001b[2m# Help about any command\u001b[0m \u001b[0K\r\r\n\u001b[1m\u001b[33mdeployment commands \u001b[0m\u001b[0K\r\r\n\u001b[mdeploy\u001b[0m \u001b[2m# Deploy a file\u001b[0m \u001b[0K\r\r\n\u001b[1m\u001b[33mtools commands \u001b[0m\u001b[0K\r\r\n\u001b[mgit\u001b[0m \u001b[2m# Git command\u001b[0m \u001b[0K\r\r\n\u001b[mssh\u001b[0m \u001b[2m# SSH client\u001b[0m \u001b[0K\r\r\n\u001b[1m\u001b[33mreadline commands \u001b[0m\u001b[0K\r\r\n\u001b[mhint\u001b[0m \u001b[2m# Set or clear a sticky transient hint immediately (non-async)\u001b[0m \u001b[0K\r\r\n\u001b[mnotify\u001b[0m \u001b[2m# Async status updates shown in the hint lane (transient hint + wake)\u001b[0m \u001b[0K\r\r\n\u001b[mscan\u001b[0m \u001b[2m# Async completions — press Tab after 'scan ' and watch hosts appear live\u001b[0m \u001b[0K\r\r\n\u001b[1m\u001b[33mother commands \u001b[0m\u001b[0K\r\r\n\u001b[mreadline\u001b[0m \u001b[2m# Manipulate readline options, keymaps and bindings\u001b[0m \u001b[0K\u001b[25A\u001b[1A\u001b[1A\u001b[2C\u001b[150D\u001b[2C\u001b[?25h"] -[0.020934, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] -[0.000959, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:30.788\u001b[0m\u001b[150D\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\r\r\n\u001b[2;3mtype a command — try 'notify', 'hint set ...', or 'scan '\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m"] -[0.000057, "o", "\u001b[0K\u001b[1m\u001b[33mfilesystem commands \u001b[0m\u001b[0K\r\r\n\u001b[mbackup\u001b[0m \u001b[2m# Create a backup of a file or directory\u001b[0m \u001b[0K\r\r\n\u001b[mconvert\u001b[0m \u001b[2m# Convert a file\u001b[0m \u001b[0K\r\r\n\u001b[mdownload\u001b[0m \u001b[2m# Download a file from a URL\u001b[0m \u001b[0K\r\r\n\u001b[mencrypt\u001b[0m \u001b[2m# Encrypt a file\u001b[0m \u001b[0K\r\r\n\u001b[mls\u001b[0m \u001b[2m# List directory contents\u001b[0m \u001b[0K\r\r\n\u001b[mmkdir\u001b[0m \u001b[2m# Create directories\u001b[0m \u001b[0K\r\r\n\u001b[mrename\u001b[0m \u001b[2m# Rename a file\u001b[0m \u001b[0K\r\r\n\u001b[msearch\u001b[0m \u001b[2m# Search for a query\u001b[0m \u001b[0K\r\r\n\u001b[1m\u001b[33mcore commands \u001b[0m\u001b[0K\r\r\n\u001b[mclient\u001b[0m \u001b[2m# Switch to the client menu (also works with CtrlC)\u001b[0m \u001b[0K\r\r\n\u001b[mexit\u001b[0m \u001b[2m# Exit the console application\u001b[0m \u001b[0K\r\r\n\u001b[mgreet\u001b[0m \u001b[2m# Greet a person\u001b[0m \u001b[0K\r\r\n\u001b[mhello\u001b[0m \u001b[2m# Say hello with customizable message\u001b[0m \u001b[0K\r\r\n\u001b[mhelp\u001b[0m \u001b[2m# Help about any command\u001b[0m \u001b[0K\r\r\n\u001b[1m\u001b[33mdeployment commands \u001b[0m\u001b[0K\r\r\n\u001b[mdeploy\u001b[0m \u001b[2m# Deploy a file\u001b[0m \u001b[0K\r\r\n\u001b[1m\u001b[33mtools commands \u001b[0m\u001b[0K\r\r\n\u001b[mgit\u001b[0m \u001b[2m# Git command\u001b[0m \u001b[0K\r\r\n\u001b[mssh\u001b[0m \u001b[2m# SSH client\u001b[0m \u001b[0K\r\r\n\u001b[1m\u001b[33mreadline commands \u001b[0m\u001b[0K\r\r\n\u001b[mhint\u001b[0m \u001b[2m# Set or clear a sticky transient hint immediately (non-async)\u001b[0m \u001b[0K\r\r\n\u001b[mnotify\u001b[0m \u001b[2m# Async status updates shown in the hint lane (transient hint + wake)\u001b[0m \u001b[0K\r\r\n\u001b[mscan\u001b[0m \u001b[2m# Async completions — press Tab after 'scan ' and watch hosts appear live\u001b[0m \u001b[0K\r\r\n\u001b[1m\u001b[33mother commands \u001b[0m\u001b[0K\r\r\n\u001b[mreadline\u001b[0m \u001b[2m# Manipulate readline options, keymaps and bindings\u001b[0m \u001b[0K\u001b[25A\u001b[1A\u001b[1A\u001b[2C\u001b[150D\u001b[2C\u001b[?25h"] -[0.900652, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] -[0.000975, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:31.690\u001b[0m\u001b[150D\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A"] -[0.000431, "o", "\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\r\r\n\u001b[2;3mtype a command — try 'notify', 'hint set ...', or 'scan '\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[0K\u001b[1m\u001b[33mfilesystem commands \u001b[0m\u001b[0K\r\r\n\u001b[mbackup\u001b[0m \u001b[2m# Create a backup of a file or directory\u001b[0m \u001b[0K\r\r\n\u001b[mconvert\u001b[0m \u001b[2m# Convert a file\u001b[0m \u001b[0K\r\r\n\u001b[mdownload\u001b[0m \u001b[2m# Download a file from a URL\u001b[0m \u001b[0K\r\r\n\u001b[mencrypt\u001b[0m \u001b[2m# Encrypt a file\u001b[0m \u001b[0K\r\r\n\u001b[mls\u001b[0m \u001b[2m# List directory contents\u001b[0m \u001b[0K\r\r\n\u001b[mmkdir\u001b[0m \u001b[2m# Create directories\u001b[0m \u001b[0K\r\r\n\u001b[mrename\u001b[0m \u001b[2m# Rename a file\u001b[0m \u001b[0K\r\r\n\u001b[msearch\u001b[0m \u001b[2m# Search for a query\u001b[0m \u001b[0K\r\r\n\u001b[1m\u001b[33mcore commands \u001b[0m\u001b[0K\r\r\n\u001b[mclient\u001b[0m \u001b[2m# Switch to the client menu (also works with CtrlC)\u001b[0m \u001b[0K\r\r\n\u001b[mexit\u001b[0m \u001b[2m# Exit the console application\u001b[0m \u001b[0K\r\r\n\u001b[mgreet\u001b[0m \u001b[2m# Greet a person\u001b[0m \u001b[0K\r\r\n\u001b[mhello\u001b[0m \u001b[2m# Say hello with customizable message\u001b[0m \u001b[0K\r\r\n\u001b[mhelp\u001b[0m \u001b[2m# Help about any command\u001b[0m \u001b[0K\r\r\n\u001b[1m\u001b[33mdeployment commands \u001b[0m\u001b[0K\r\r\n\u001b[mdeploy\u001b[0m \u001b[2m# Deploy a file\u001b[0m \u001b[0K\r\r\n\u001b[1m\u001b[33mtools commands \u001b[0m\u001b[0K\r\r\n\u001b[mgit\u001b[0m \u001b[2m# Git command\u001b[0m \u001b[0K\r\r\n\u001b[mssh\u001b[0m \u001b[2m# SSH client\u001b[0m \u001b[0K\r\r\n\u001b[1m\u001b[33mreadline commands \u001b[0m\u001b[0K\r\r\n\u001b[mhint\u001b[0m \u001b[2m# Set or clear a sticky transient hint immediately (non-async)\u001b[0m \u001b[0K\r\r\n\u001b[mnotify\u001b[0m \u001b[2m# Async status updates shown in the hint lane (transient hint + wake)\u001b[0m \u001b[0K\r\r\n\u001b[mscan\u001b[0m \u001b[2m# Async completions — press Tab after 'scan ' and watch hosts appear live\u001b[0m \u001b[0K\r\r\n\u001b[1m\u001b[33mother commands \u001b[0m\u001b[0K\r\r\n\u001b[mreadline\u001b[0m \u001b[2m# Manipulate readline options, keymaps and bindings\u001b[0m \u001b[0K\u001b[25A\u001b[1A\u001b[1A\u001b[2C"] -[0.000109, "o", "\u001b[150D\u001b[2C\u001b[?25h"] -[0.898254, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] -[0.001099, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n"] -[0.000267, "o", "\u001b[2C\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:32.590\u001b[0m\u001b[150D\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C"] -[0.000383, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\r\r\n\u001b[2;3mtype a command — try 'notify', 'hint set ...', or 'scan '\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m"] -[0.000422, "o", "\u001b[0K\u001b[1m\u001b[33mfilesystem commands \u001b[0m\u001b[0K\r\r\n\u001b[mbackup\u001b[0m \u001b[2m# Create a backup of a file or directory\u001b[0m \u001b[0K\r\r\n\u001b[mconvert\u001b[0m \u001b[2m# Convert a file\u001b[0m \u001b[0K\r\r\n\u001b[mdownload\u001b[0m \u001b[2m# Download a file from a URL\u001b[0m \u001b[0K\r\r\n\u001b[mencrypt\u001b[0m \u001b[2m# Encrypt a file\u001b[0m \u001b[0K\r\r\n\u001b[mls\u001b[0m \u001b[2m# List directory contents\u001b[0m \u001b[0K\r\r\n\u001b[mmkdir\u001b[0m \u001b[2m# Create directories\u001b[0m \u001b[0K\r\r\n\u001b[mrename\u001b[0m \u001b[2m# Rename a file\u001b[0m \u001b[0K\r\r\n\u001b[msearch\u001b[0m \u001b[2m# Search for a query\u001b[0m \u001b[0K\r\r\n\u001b[1m\u001b[33mcore commands \u001b[0m\u001b[0K\r\r\n\u001b[mclient\u001b[0m \u001b[2m# Switch to the client menu (also works with CtrlC)\u001b[0m \u001b[0K\r\r\n\u001b[mexit\u001b[0m \u001b[2m# Exit the console application\u001b[0m \u001b[0K\r\r\n\u001b[mgreet\u001b[0m \u001b[2m# Greet a person\u001b[0m \u001b[0K\r\r\n\u001b[mhello\u001b[0m \u001b[2m# Say hello with customizable message\u001b[0m \u001b[0K\r\r\n\u001b[mhelp\u001b[0m \u001b[2m# Help about any command\u001b[0m \u001b[0K\r\r\n\u001b[1m\u001b[33mdeployment commands \u001b[0m\u001b[0K\r\r\n\u001b[mdeploy\u001b[0m \u001b[2m# Deploy a file\u001b[0m \u001b[0K\r\r\n\u001b[1m\u001b[33mtools commands \u001b[0m\u001b[0K\r\r\n\u001b[mgit\u001b[0m \u001b[2m# Git command\u001b[0m \u001b[0K\r\r\n\u001b[mssh\u001b[0m \u001b[2m# SSH client\u001b[0m \u001b[0K\r\r\n\u001b[1m\u001b[33mreadline commands \u001b[0m\u001b[0K\r\r\n\u001b[mhint\u001b[0m \u001b[2m# Set or clear a sticky transient hint immediately (non-async)\u001b[0m \u001b[0K\r\r\n\u001b[mnotify\u001b[0m \u001b[2m# Async status updates shown in the hint lane (transient hint + wake)\u001b[0m \u001b[0K\r\r\n\u001b[mscan\u001b[0m \u001b[2m# Async completions — press Tab after 'scan ' and watch hosts appear live\u001b[0m \u001b[0K\r\r\n\u001b[1m\u001b[33mother commands \u001b[0m\u001b[0K\r\r\n\u001b[mreadline\u001b[0m \u001b[2m# Manipulate readline options, keymaps and bindings\u001b[0m \u001b[0K\u001b[25A\u001b[1A\u001b[1A\u001b[2C\u001b[150D\u001b[2C\u001b[?25h"] -[0.350238, "o", "\u001b[5 q"] -[0.000238, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] -[0.000737, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] -[0.000322, "o", "h\u001b[0m\u001b[2m\u001b[38;05;242melp client\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:32.943\u001b[0m\u001b[150D\u001b[13C\u001b[1B\u001b[150D\u001b[0J"] -[0.000103, "o", "\u001b[1A\u001b[13C"] -[0.005184, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[13C\r\r\n\u001b[0K\u001b[1m\u001b[33mcore commands \u001b[0m\u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34mh\u001b[22m\u001b[39m\u001b[mello\u001b[0m \u001b[2m# Say hello with customizable message\u001b[0m \u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34mh\u001b[22m\u001b[39m\u001b[melp\u001b[0m \u001b[2m# Help about any command\u001b[0m \u001b[0K\r\r\n\u001b[1m\u001b[33mreadline commands \u001b[0m\u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34mh\u001b[22m\u001b[39m\u001b[mint\u001b[0m \u001b[2m# Set or clear a sticky transient hint immediately (non-async)\u001b[0m \u001b[0K\u001b[4A\u001b[1A\u001b[13C\u001b[150D\u001b[3C\u001b[?25h"] -[0.144817, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] -[0.001008, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2Chi\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:33.094\u001b[0m\u001b[150D\u001b[4C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[4C"] -[0.001521, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[4C\r\r\n\u001b[0K\u001b[1m\u001b[33mreadline commands \u001b[0m\u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34mhi\u001b[22m\u001b[39m\u001b[mnt\u001b[0m \u001b[2m# Set or clear a sticky transient hint immediately (non-async)\u001b[0m \u001b[0K\u001b[1A\u001b[1A\u001b[4C\u001b[150D\u001b[4C\u001b[?25h"] -[0.173923, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] -[0.000996, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2Chin\u001b[0m\u001b[0K\u001b[49m"] -[0.000091, "o", " \u001b[1;30m01:49:33.270\u001b[0m\u001b[150D\u001b[5C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[5C"] -[0.001571, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[5C\r\r\n\u001b[0K\u001b[1m\u001b[33mreadline commands \u001b[0m\u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34mhin\u001b[22m\u001b[39m\u001b[mt\u001b[0m \u001b[2m# Set or clear a sticky transient hint immediately (non-async)\u001b[0m \u001b[0K\u001b[1A\u001b[1A\u001b[5C\u001b[150D\u001b[5C\u001b[?25h"] -[0.127583, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] -[0.000895, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[1m\u001b[32mhint\u001b[39m\u001b[22m\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:33.400\u001b[0m\u001b[150D\u001b[6C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[6C"] -[0.001077, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[6C\r\r\n\u001b[2;3m hint — Set or clear a sticky transient hint immediately (non-async)\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[0K\u001b[1m\u001b[33mreadline commands \u001b[0m\u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34mhint\u001b[22m\u001b[39m\u001b[m\u001b[0m \u001b[2m# Set or clear a sticky transient hint immediately (non-async)\u001b[0m \u001b[0K\u001b[1A\u001b[1A\u001b[1A\u001b[6C\u001b[150D\u001b[6C\u001b[?25h"] -[0.08543, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] -[0.001327, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] -[0.000105, "o", "\u001b[1m\u001b[32mhint\u001b[39m\u001b[22m\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:33.488\u001b[0m"] -[0.000395, "o", "\u001b[150D\u001b[6C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[6C"] -[0.001913, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[6C\r\r\n\u001b[2;3m hint — Set or clear a sticky transient hint immediately (non-async)\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m"] -[0.00031, "o", "\u001b[0K\u001b[1m\u001b[33mreadline commands \u001b[0m\u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34mhint\u001b[22m\u001b[39m\u001b[m\u001b[0m \u001b[2m# Set or clear a sticky transient hint immediately (non-async)\u001b[0m \u001b[0K\u001b[1A\u001b[1A\u001b[1A\u001b[6C\u001b[150D\u001b[6C\u001b[?25h"] -[0.326811, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] -[0.001452, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] -[0.000554, "o", "\u001b[1m\u001b[32mhint \u001b[39m\u001b[22m\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:33.820\u001b[0m"] -[0.000057, "o", "\u001b[150D"] -[0.000833, "o", "\u001b[7C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[7C"] -[0.001074, "o", "\u001b[1B\u001b[150D"] -[0.000181, "o", "\u001b[0J\u001b[1A\u001b[7C\r\r\n"] -[0.000165, "o", "\u001b[2;3m hint — Set or clear a sticky transient hint immediately (non-async)\u001b[0m\u001b[0K\r\r\n"] -[0.000264, "o", "\u001b[0K\u001b[0m"] -[0.000226, "o", "\u001b[0K\u001b[1m\u001b[33mcommands \u001b[0m\u001b[0K\r\r\n\u001b[mclear\u001b[0m \u001b[2m# Clear the transient hint lane\u001b[0m \u001b[0K\r\r\n\u001b[mset\u001b[0m \u001b[2m# Set the transient hint lane to a message (persists until cleared)\u001b[0m \u001b[0K\u001b[2A\u001b[1A\u001b[1A\u001b[7C\u001b[150D\u001b[7C\u001b[?25h"] -[0.56553, "o", "\u001b[?25l\u001b[150D> "] -[0.000075, "o", "\u001b[6n"] -[0.001102, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[1m\u001b[32mhint \u001b[39m\u001b[22m\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:34.389\u001b[0m\u001b[150D\u001b[7C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[7C"] -[0.001755, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[7C\r\r\n\u001b[2;3m hint — Set or clear a sticky transient hint immediately (non-async)\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[0K\u001b[1m\u001b[33mcommands \u001b[0m\u001b[0K\r\r\n\u001b[mclear\u001b[0m \u001b[2m# Clear the transient hint lane\u001b[0m \u001b[0K\r\r\n\u001b[mset\u001b[0m \u001b[2m# Set the transient hint lane to a message (persists until cleared)\u001b[0m \u001b[0K\u001b[2A\u001b[1A\u001b[1A\u001b[7C\u001b[150D\u001b[7C\u001b[?25h"] -[0.286319, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] -[0.001117, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] -[0.000057, "o", "\u001b[1m\u001b[32mhint \u001b[39m\u001b[22ms\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:34.679\u001b[0m\u001b[150D"] -[0.000111, "o", "\u001b[8C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[8C"] -[0.001574, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[8C\r\r\n\u001b[2;3m hint — Set or clear a sticky transient hint immediately (non-async)\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m"] -[0.000148, "o", "\u001b[0K\u001b[1m\u001b[33mcommands \u001b[0m\u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34ms\u001b[22m\u001b[39m\u001b[met\u001b[0m \u001b[2m# Set the transient hint lane to a message (persists until cleared)\u001b[0m \u001b[0K\u001b[1A\u001b[1A\u001b[1A\u001b[8C\u001b[150D\u001b[8C\u001b[?25h"] -[0.321767, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] -[0.000881, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[1m\u001b[32mhint \u001b[39m\u001b[22mse\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:35.003\u001b[0m\u001b[150D\u001b[9C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[9C"] -[0.001348, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[9C\r\r\n\u001b[2;3m hint — Set or clear a sticky transient hint immediately (non-async)\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[0K\u001b[1m\u001b[33mcommands \u001b[0m\u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34mse\u001b[22m\u001b[39m\u001b[mt\u001b[0m \u001b[2m# Set the transient hint lane to a message (persists until cleared)\u001b[0m \u001b[0K\u001b[1A\u001b[1A\u001b[1A\u001b[9C\u001b[150D\u001b[9C\u001b[?25h"] -[0.149185, "o", "\u001b[?25l\u001b[150D> "] -[0.000052, "o", "\u001b[6n"] -[0.00085, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n"] -[0.000336, "o", "\u001b[2C\u001b[1m\u001b[32mhint \u001b[39m\u001b[22mset\u001b[0m\u001b[0K\u001b[49m"] -[0.000301, "o", " \u001b[1;30m01:49:35.155\u001b[0m\u001b[150D\u001b[10C\u001b[1B\u001b[150D\u001b[0J\u001b[1A"] -[0.000738, "o", "\u001b[10C"] -[0.001821, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[10C\r\r\n\u001b[2;3m hint set — Set the transient hint lane to a message (persists until cleared)\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[0K\u001b[1m\u001b[33mcommands \u001b[0m\u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34mset\u001b[22m\u001b[39m\u001b[m\u001b[0m \u001b[2m# Set the transient hint lane to a message (persists until cleared)\u001b[0m \u001b[0K\u001b[1A\u001b[1A\u001b[1A\u001b[10C\u001b[150D\u001b[10C\u001b[?25h"] -[0.130431, "o", "\u001b[?25l\u001b[150D"] -[0.000121, "o", "> "] -[0.000352, "o", "\u001b[6n"] -[0.001064, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[1m\u001b[32mhint \u001b[39m\u001b[22mset\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:35.290\u001b[0m\u001b[150D\u001b[10C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[10C"] -[0.001526, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[10C\r\r\n"] -[0.000203, "o", "\u001b[2;3m hint set — Set the transient hint lane to a message (persists until cleared)\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[0K\u001b[1m\u001b[33mcommands \u001b[0m\u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34mset\u001b[22m\u001b[39m\u001b[m\u001b[0m \u001b[2m# Set the transient hint lane to a message (persists until cleared)\u001b[0m \u001b[0K\u001b[1A"] -[0.000426, "o", "\u001b[1A\u001b[1A\u001b[10C\u001b[150D\u001b[10C\u001b[?25h"] -[1.145172, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] -[0.001041, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] -[0.000019, "o", "\u001b[1m\u001b[32mhint \u001b[39m\u001b[22mset \u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:36.438\u001b[0m"] -[0.000135, "o", "\u001b[150D\u001b[11C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[11C"] -[0.001823, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[11C\r\r\n\u001b[2;3m hint set — Set the transient hint lane to a message (persists until cleared)\u001b[0m\u001b[0K\r\r\n\u001b[2mset MESSAGE...\u001b[0m\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[3A\u001b[1A\u001b[11C\u001b[150D\u001b[11C\u001b[?25h"] -[2.332968, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] -[0.000937, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[1m\u001b[32mhint \u001b[39m\u001b[22mset t\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:38.774\u001b[0m\u001b[150D\u001b[12C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[12C"] -[0.000833, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[12C\r\r\n\u001b[2;3m hint set — Set the transient hint lane to a message (persists until cleared)\u001b[0m\u001b[0K\r\r\n\u001b[2mset MESSAGE...\u001b[0m\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[3A\u001b[1A\u001b[12C\u001b[150D\u001b[12C\u001b[?25h"] -[0.088322, "o", "\u001b[?25l\u001b[150D> "] -[0.000663, "o", "\u001b[6n"] -[0.001058, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[1m\u001b[32mhint \u001b[39m\u001b[22mset te\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:38.865\u001b[0m\u001b[150D\u001b[13C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[13C"] -[0.001896, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[13C\r\r\n\u001b[2;3m hint set — Set the transient hint lane to a message (persists until cleared)\u001b[0m\u001b[0K\r\r\n\u001b[2mset MESSAGE...\u001b[0m\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m"] -[0.0001, "o", "\u001b[3A\u001b[1A\u001b[13C\u001b[150D\u001b[13C\u001b[?25h"] -[0.218064, "o", "\u001b[?25l\u001b[150D> "] -[0.000103, "o", "\u001b[6n"] -[0.000645, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] -[0.000185, "o", "\u001b[1m\u001b[32mhint \u001b[39m\u001b[22mset tes\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:39.086\u001b[0m\u001b[150D\u001b[14C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[14C"] -[0.000767, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[14C\r\r\n\u001b[2;3m hint set — Set the transient hint lane to a message (persists until cleared)\u001b[0m\u001b[0K\r\r\n\u001b[2mset MESSAGE...\u001b[0m\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[3A\u001b[1A\u001b[14C\u001b[150D\u001b[14C\u001b[?25h"] -[0.174302, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] -[0.001231, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[1m\u001b[32mhint \u001b[39m\u001b[22mset test\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:39.262\u001b[0m\u001b[150D\u001b[15C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[15C"] -[0.001111, "o", "\u001b[1B"] -[0.000907, "o", "\u001b[150D\u001b[0J\u001b[1A\u001b[15C\r\r\n\u001b[2;3m hint set — Set the transient hint lane to a message (persists until cleared)\u001b[0m\u001b[0K\r\r\n\u001b[2mset MESSAGE...\u001b[0m\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[3A\u001b[1A\u001b[15C\u001b[150D\u001b[15C\u001b[?25h"] -[0.459465, "o", "\u001b[5 q\u001b[15D\u001b[2C\u001b[6n"] -[0.001119, "o", "\u001b[150D\u001b[15C\u001b[0J \u001b[1;30m01:49:39.725\u001b[0m\u001b[150D\r\r\n\u001b[0 q\u001b[?2004l"] -[0.000195, "o", "\r\n"] -[0.009177, "o", "\r\n"] -[0.000253, "o", "\u001b[?2004h\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\r\n"] -[0.000425, "o", "> \u001b[5 q"] -[0.000029, "o", "\u001b[?25l\u001b[150D"] -[0.00012, "o", "> \u001b[6n"] -[0.000879, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:39.736\u001b[0m\u001b[150D\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C"] -[0.000105, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\r\r\n\u001b[2;3mtype a command — try 'notify', 'hint set ...', or 'scan '\u001b[0m\u001b[0K\r\r\n\u001b[36mtest\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m"] -[0.000063, "o", "\u001b[3A\u001b[1A\u001b[2C\u001b[150D\u001b[2C\u001b[?25h"] -[2.436322, "o", "\u001b[2D\u001b[2C\u001b[6n"] -[0.001215, "o", "\u001b[150D\u001b[2C\u001b[0J \u001b[1;30m01:49:42.173\u001b[0m\u001b[150D\r\r\n"] -[0.000039, "o", "\u001b[0 q\u001b[?2004l"] -[0.000445, "o", "\u001b[?2004h\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\r\n> \u001b[5 q"] -[0.000204, "o", "\u001b[?25l\u001b[150D"] -[0.000019, "o", "> \u001b[6n"] -[0.00087, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:42.175\u001b[0m\u001b[150D\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\u001b[1B"] -[0.000223, "o", "\u001b[150D\u001b[0J\u001b[1A\u001b[2C\r\r\n\u001b[2;3mtype a command — try 'notify', 'hint set ...', or 'scan '\u001b[0m\u001b[0K\r\r\n\u001b[36mtest\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[3A\u001b[1A\u001b[2C\u001b[150D\u001b[2C\u001b[?25h"] -[0.387877, "o", "\u001b[2D\u001b[2C\u001b[6n"] -[0.00094, "o", "\u001b[150D\u001b[2C\u001b[0J \u001b[1;30m01:49:42.564\u001b[0m\u001b[150D\r\r\n\u001b[0 q\u001b[?2004l"] -[0.001222, "o", "\u001b[?2004h\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\r\n> \u001b[5 q\u001b[?25l\u001b[150D> \u001b[6n"] -[0.001149, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:42.566\u001b[0m\u001b[150D\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\r\r\n\u001b[2;3mtype a command — try 'notify', 'hint set ...', or 'scan '\u001b[0m\u001b[0K\r\r\n\u001b[36mtest\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m"] -[0.000129, "o", "\u001b[3A\u001b[1A\u001b[2C\u001b[150D\u001b[2C\u001b[?25h"] -[0.309419, "o", "\u001b[2D\u001b[2C\u001b[6n"] -[0.000786, "o", "\u001b[150D\u001b[2C\u001b[0J \u001b[1;30m01:49:42.877\u001b[0m\u001b[150D\r\r\n\u001b[0 q\u001b[?2004l"] -[0.000658, "o", "\u001b[?2004h\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\r\n> \u001b[5 q\u001b[?25l\u001b[150D> \u001b[6n"] -[0.000883, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] -[0.000205, "o", "\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:42.879\u001b[0m\u001b[150D"] -[0.000034, "o", "\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C"] -[0.000174, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\r\r\n\u001b[2;3mtype a command — try 'notify', 'hint set ...', or 'scan '\u001b[0m\u001b[0K\r\r\n\u001b[36mtest\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m"] -[0.000241, "o", "\u001b[3A\u001b[1A\u001b[2C\u001b[150D\u001b[2C\u001b[?25h"] -[0.243154, "o", "\u001b[2D\u001b[2C\u001b[6n"] -[0.00116, "o", "\u001b[150D\u001b[2C\u001b[0J \u001b[1;30m01:49:43.123\u001b[0m\u001b[150D\r\r\n"] -[0.000113, "o", "\u001b[0 q\u001b[?2004l"] -[0.000796, "o", "\u001b[?2004h\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\r\n> \u001b[5 q"] -[0.000071, "o", "\u001b[?25l"] -[0.000177, "o", "\u001b[150D"] -[0.000145, "o", "> \u001b[6n"] -[0.000679, "o", "\u001b[150D"] -[0.000142, "o", "\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] -[0.000334, "o", "\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:43.126\u001b[0m"] -[0.000123, "o", "\u001b[150D\u001b[2C"] -[0.000126, "o", "\u001b[1B"] -[0.000063, "o", "\u001b[150D"] -[0.000147, "o", "\u001b[0J"] -[0.000349, "o", "\u001b[1A\u001b[2C"] -[0.000129, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C"] -[0.000545, "o", "\r\r\n\u001b[2;3mtype a command — try 'notify', 'hint set ...', or 'scan '\u001b[0m\u001b[0K\r\r\n\u001b[36mtest\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[3A\u001b[1A\u001b[2C\u001b[150D\u001b[2C\u001b[?25h"] -[2.347618, "o", "\u001b[?25l\u001b[150D> "] -[0.000257, "o", "\u001b[6n"] -[0.001382, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2Ch\u001b[0m\u001b[2m\u001b[38;05;242mint set test\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:45.476\u001b[0m\u001b[150D\u001b[15C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[15C"] -[0.001233, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[15C\r\r\n\u001b[36mtest\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[0K\u001b[1m\u001b[33mcore commands \u001b[0m\u001b[0K\r\r\n"] -[0.00009, "o", "\u001b[m\u001b[1m\u001b[34mh\u001b[22m\u001b[39m\u001b[mello\u001b[0m \u001b[2m# Say hello with customizable message\u001b[0m \u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34mh\u001b[22m\u001b[39m\u001b[melp\u001b[0m \u001b[2m# Help about any command\u001b[0m \u001b[0K\r\r\n\u001b[1m\u001b[33mreadline commands \u001b[0m\u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34mh\u001b[22m\u001b[39m\u001b[mint\u001b[0m \u001b[2m# Set or clear a sticky transient hint immediately (non-async)\u001b[0m \u001b[0K\u001b[4A\u001b[1A\u001b[1A\u001b[15C\u001b[150D\u001b[3C\u001b[?25h"] -[0.162658, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] -[0.000815, "o", "\u001b[150D"] -[0.00018, "o", "\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] -[0.000589, "o", "hi\u001b[0m\u001b[2m\u001b[38;05;242mnt set test\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:45.642\u001b[0m\u001b[150D\u001b[15C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[15C"] -[0.001696, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[15C\r\r\n\u001b[36mtest\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[0K\u001b[1m\u001b[33mreadline commands \u001b[0m\u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34mhi\u001b[22m\u001b[39m\u001b[mnt\u001b[0m \u001b[2m# Set or clear a sticky transient hint immediately (non-async)\u001b[0m \u001b[0K\u001b[1A\u001b[1A\u001b[1A\u001b[15C\u001b[150D\u001b[4C\u001b[?25h"] -[0.192963, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] -[0.001209, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2Chin\u001b[0m\u001b[2m\u001b[38;05;242mt set test\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:45.838\u001b[0m\u001b[150D\u001b[15C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[15C"] -[0.002552, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[15C\r\r\n\u001b[36mtest\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[0K\u001b[1m\u001b[33mreadline commands \u001b[0m\u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34mhin\u001b[22m\u001b[39m\u001b[mt\u001b[0m \u001b[2m# Set or clear a sticky transient hint immediately (non-async)\u001b[0m \u001b[0K\u001b[1A\u001b[1A"] -[0.000242, "o", "\u001b[1A\u001b[15C\u001b[150D"] -[0.000136, "o", "\u001b[5C"] -[0.000004, "o", "\u001b[?25h"] -[0.161097, "o", "\u001b[?25l\u001b[150D> "] -[0.000071, "o", "\u001b[6n"] -[0.000843, "o", "\u001b[150D"] -[0.000681, "o", "\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[1m\u001b[32mhint\u001b[39m\u001b[22m\u001b[0m\u001b[2m\u001b[38;05;242m set test\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:46.003\u001b[0m\u001b[150D\u001b[15C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[15C"] -[0.004536, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[15C\r\r\n\u001b[2;3m hint — Set or clear a sticky transient hint immediately (non-async)\u001b[0m\u001b[0K\r\r\n\u001b[36mtest\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[0K\u001b[1m\u001b[33mreadline commands \u001b[0m\u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34mhint\u001b[22m\u001b[39m\u001b[m\u001b[0m \u001b[2m# Set or clear a sticky transient hint immediately (non-async)\u001b[0m \u001b[0K\u001b[1A\u001b[3A\u001b[1A\u001b[15C\u001b[150D\u001b[6C\u001b[?25h"] -[0.205828, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] -[0.001139, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[1m\u001b[32mhint \u001b[39m\u001b[22m\u001b[0m\u001b[2m\u001b[38;05;242mset test\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:46.215\u001b[0m\u001b[150D\u001b[15C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[15C"] -[0.001466, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[15C\r\r\n\u001b[2;3m hint — Set or clear a sticky transient hint immediately (non-async)\u001b[0m\u001b[0K\r\r\n\u001b[36mtest\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[0K\u001b[1m\u001b[33mcommands \u001b[0m\u001b[0K\r\r\n\u001b[mclear\u001b[0m \u001b[2m# Clear the transient hint lane\u001b[0m \u001b[0K\r\r\n\u001b[mset\u001b[0m \u001b[2m# Set the transient hint lane to a message (persists until cleared)\u001b[0m \u001b[0K\u001b[2A\u001b[3A"] -[0.000061, "o", "\u001b[1A\u001b[15C\u001b[150D\u001b[7C\u001b[?25h"] -[0.942846, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] -[0.001183, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] -[0.000161, "o", "\u001b[1m\u001b[32mhint \u001b[39m\u001b[22ms\u001b[0m\u001b[2m\u001b[38;05;242met test\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:47.161\u001b[0m\u001b[150D\u001b[15C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[15C"] -[0.001706, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[15C\r\r\n\u001b[2;3m hint — Set or clear a sticky transient hint immediately (non-async)\u001b[0m\u001b[0K\r\r\n\u001b[36mtest\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[0K\u001b[1m\u001b[33mcommands \u001b[0m\u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34ms\u001b[22m\u001b[39m\u001b[met\u001b[0m \u001b[2m# Set the transient hint lane to a message (persists until cleared)\u001b[0m \u001b[0K\u001b[1A\u001b[3A\u001b[1A\u001b[15C\u001b[150D\u001b[8C\u001b[?25h"] -[0.208063, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] -[0.00088, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[1m\u001b[32mhint \u001b[39m\u001b[22mse\u001b[0m\u001b[2m\u001b[38;05;242mt test\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:47.371\u001b[0m\u001b[150D\u001b[15C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[15C"] -[0.001678, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[15C\r\r\n\u001b[2;3m hint — Set or clear a sticky transient hint immediately (non-async)\u001b[0m\u001b[0K\r\r\n\u001b[36mtest\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m"] -[0.000041, "o", "\u001b[0K\u001b[1m\u001b[33mcommands \u001b[0m\u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34mse\u001b[22m\u001b[39m\u001b[mt\u001b[0m \u001b[2m# Set the transient hint lane to a message (persists until cleared)\u001b[0m \u001b[0K\u001b[1A\u001b[3A\u001b[1A\u001b[15C\u001b[150D\u001b[9C\u001b[?25h"] -[0.599676, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] -[0.000934, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[1m\u001b[32mhint \u001b[39m\u001b[22m\u001b[0m\u001b[2m\u001b[38;05;242mset test\u001b[0m\u001b[0K\u001b[49m"] -[0.000068, "o", " \u001b[1;30m01:49:47.974\u001b[0m\u001b[150D\u001b[15C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[15C"] -[0.002124, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[15C\r\r\n"] -[0.000064, "o", "\u001b[2;3m hint — Set or clear a sticky transient hint immediately (non-async)\u001b[0m\u001b[0K\r\r\n\u001b[36mtest\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m"] -[0.000188, "o", "\u001b[0K\u001b[1m\u001b[33mcommands \u001b[0m\u001b[0K\r\r\n\u001b[mclear\u001b[0m \u001b[2m# Clear the transient hint lane\u001b[0m \u001b[0K\r\r\n\u001b[mset\u001b[0m \u001b[2m# Set the transient hint lane to a message (persists until cleared)\u001b[0m \u001b[0K\u001b[2A\u001b[3A\u001b[1A\u001b[15C\u001b[150D\u001b[7C\u001b[?25h"] -[0.430185, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] -[0.001401, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[1m\u001b[32mhint \u001b[39m\u001b[22mc\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:48.408\u001b[0m\u001b[150D\u001b[8C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[8C"] -[0.001734, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[8C\r\r\n\u001b[2;3m hint — Set or clear a sticky transient hint immediately (non-async)\u001b[0m\u001b[0K\r\r\n\u001b[36mtest\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[0K\u001b[1m\u001b[33mcommands \u001b[0m\u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34mc\u001b[22m\u001b[39m\u001b[mlear\u001b[0m \u001b[2m# Clear the transient hint lane\u001b[0m \u001b[0K\u001b[1A\u001b[3A\u001b[1A\u001b[8C\u001b[150D\u001b[8C\u001b[?25h"] -[0.131817, "o", "\u001b[?25l\u001b[150D> "] -[0.000127, "o", "\u001b[6n"] -[0.000762, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] -[0.000199, "o", "\u001b[1m\u001b[32mhint \u001b[39m\u001b[22mcl\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:48.542\u001b[0m\u001b[150D\u001b[9C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[9C"] -[0.001749, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[9C\r\r\n\u001b[2;3m hint — Set or clear a sticky transient hint immediately (non-async)\u001b[0m\u001b[0K\r\r\n\u001b[36mtest\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m"] -[0.000145, "o", "\u001b[0K\u001b[1m\u001b[33mcommands \u001b[0m\u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34mcl\u001b[22m\u001b[39m\u001b[mear\u001b[0m \u001b[2m# Clear the transient hint lane\u001b[0m \u001b[0K\u001b[1A\u001b[3A\u001b[1A\u001b[9C\u001b[150D\u001b[9C\u001b[?25h"] -[0.12261, "o", "\u001b[?25l\u001b[150D> "] -[0.00025, "o", "\u001b[6n"] -[0.000787, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[1m\u001b[32mhint \u001b[39m\u001b[22mcle\u001b[0m\u001b[0K\u001b[49m"] -[0.000187, "o", " \u001b[1;30m01:49:48.668\u001b[0m\u001b[150D\u001b[10C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[10C"] -[0.001258, "o", "\u001b[1B"] -[0.000108, "o", "\u001b[150D\u001b[0J\u001b[1A\u001b[10C\r\r\n"] -[0.000455, "o", "\u001b[2;3m hint — Set or clear a sticky transient hint immediately (non-async)\u001b[0m\u001b[0K\r\r\n\u001b[36mtest\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m"] -[0.00022, "o", "\u001b[0K\u001b[1m\u001b[33mcommands \u001b[0m\u001b[0K\r\r\n"] -[0.000458, "o", "\u001b[m\u001b[1m\u001b[34mcle\u001b[22m\u001b[39m\u001b[mar\u001b[0m \u001b[2m# Clear the transient hint lane\u001b[0m \u001b[0K\u001b[1A\u001b[3A\u001b[1A\u001b[10C\u001b[150D\u001b[10C\u001b[?25h"] -[0.178518, "o", "\u001b[?25l\u001b[150D> "] -[0.000246, "o", "\u001b[6n"] -[0.001105, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[1m\u001b[32mhint \u001b[39m\u001b[22mclea\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:48.850\u001b[0m\u001b[150D\u001b[11C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[11C"] -[0.001521, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[11C\r\r\n\u001b[2;3m hint — Set or clear a sticky transient hint immediately (non-async)\u001b[0m\u001b[0K\r\r\n\u001b[36mtest\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m"] -[0.000123, "o", "\u001b[0K\u001b[1m\u001b[33mcommands \u001b[0m\u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34mclea\u001b[22m\u001b[39m\u001b[mr\u001b[0m \u001b[2m# Clear the transient hint lane\u001b[0m \u001b[0K\u001b[1A\u001b[3A\u001b[1A\u001b[11C\u001b[150D\u001b[11C\u001b[?25h"] -[0.126152, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] -[0.000949, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] -[0.000485, "o", "\u001b[1m\u001b[32mhint \u001b[39m\u001b[22mclear\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:48.979\u001b[0m\u001b[150D\u001b[12C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[12C"] -[0.001279, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[12C\r\r\n\u001b[2;3m hint clear — Clear the transient hint lane\u001b[0m\u001b[0K\r\r\n\u001b[36mtest\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[0K\u001b[1m\u001b[33mcommands \u001b[0m\u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34mclear\u001b[22m\u001b[39m\u001b[m\u001b[0m \u001b[2m# Clear the transient hint lane\u001b[0m \u001b[0K\u001b[1A\u001b[3A"] -[0.00005, "o", "\u001b[1A\u001b[12C\u001b[150D\u001b[12C\u001b[?25h"] -[0.465806, "o", "\u001b[5 q\u001b[12D\u001b[2C\u001b[6n"] -[0.000901, "o", "\u001b[150D\u001b[12C\u001b[0J \u001b[1;30m01:49:49.448\u001b[0m\u001b[150D\r\r\n"] -[0.000259, "o", "\u001b[0 q\u001b[?2004l"] -[0.000087, "o", "\r\n"] -[0.003905, "o", "\r\n"] -[0.00071, "o", "\u001b[?2004h\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\r\n> \u001b[5 q\u001b[?25l\u001b[150D> "] -[0.000106, "o", "\u001b[6n"] -[0.000769, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] -[0.000239, "o", "\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:49.454\u001b[0m\u001b[150D"] -[0.000029, "o", "\u001b[2C\u001b[1B"] -[0.000127, "o", "\u001b[150D\u001b[0J\u001b[1A\u001b[2C"] -[0.000094, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\r\r\n\u001b[2;3mtype a command — try 'notify', 'hint set ...', or 'scan '\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m"] -[0.000027, "o", "\u001b[1A\u001b[1A\u001b[2C\u001b[150D\u001b[2C\u001b[?25h"] -[1.395294, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] -[0.001151, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] -[0.000038, "o", "a\u001b[0m\u001b[2m\u001b[38;05;242mnother test\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:50.851\u001b[0m\u001b[150D\u001b[14C"] -[0.000016, "o", "\u001b[1B"] -[0.000008, "o", "\u001b[150D\u001b[0J"] -[0.000459, "o", "\u001b[1A\u001b[14C"] -[0.003071, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[14C\u001b[150D\u001b[3C\u001b[?25h"] -[0.100847, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] -[0.000982, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2Can\u001b[0m\u001b[2m\u001b[38;05;242mother test\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:50.956\u001b[0m\u001b[150D\u001b[14C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[14C"] -[0.001623, "o", "\u001b[1B\u001b[150D"] -[0.000131, "o", "\u001b[0J\u001b[1A\u001b[14C\u001b[150D\u001b[4C\u001b[?25h"] -[0.173555, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] -[0.000807, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] -[0.000263, "o", "ano\u001b[0m\u001b[2m\u001b[38;05;242mther test\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:51.132\u001b[0m\u001b[150D\u001b[14C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[14C"] -[0.001608, "o", "\u001b[1B"] -[0.000037, "o", "\u001b[150D"] -[0.000111, "o", "\u001b[0J\u001b[1A\u001b[14C\u001b[150D\u001b[5C\u001b[?25h"] -[0.122813, "o", "\u001b[?25l\u001b[150D> "] -[0.000028, "o", "\u001b[6n"] -[0.001012, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] -[0.000102, "o", "anot\u001b[0m\u001b[2m\u001b[38;05;242mher test\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:51.258\u001b[0m\u001b[150D\u001b[14C\u001b[1B\u001b[150D\u001b[0J"] -[0.000037, "o", "\u001b[1A\u001b[14C"] -[0.001867, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[14C\u001b[150D\u001b[6C\u001b[?25h"] -[0.131601, "o", "\u001b[?25l\u001b[150D> "] -[0.000152, "o", "\u001b[6n"] -[0.00127, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2Canoth\u001b[0m\u001b[2m\u001b[38;05;242mer test\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:51.393\u001b[0m\u001b[150D\u001b[14C\u001b[1B\u001b[150D"] -[0.000026, "o", "\u001b[0J"] -[0.000127, "o", "\u001b[1A\u001b[14C"] -[0.001935, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[14C\u001b[150D\u001b[7C\u001b[?25h"] -[0.124085, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] -[0.001039, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2Canothe\u001b[0m\u001b[2m\u001b[38;05;242mr test\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:51.520\u001b[0m\u001b[150D\u001b[14C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[14C"] -[0.001216, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[14C\u001b[150D\u001b[8C\u001b[?25h"] -[0.081154, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] -[0.000851, "o", "\u001b[150D"] -[0.000012, "o", "\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] -[0.000353, "o", "another\u001b[0m\u001b[2m\u001b[38;05;242m test\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:51.604\u001b[0m\u001b[150D\u001b[14C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[14C"] -[0.001841, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[14C\u001b[150D\u001b[9C\u001b[?25h"] -[0.168706, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] -[0.000671, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2Canother \u001b[0m\u001b[2m\u001b[38;05;242mtest\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:51.775\u001b[0m\u001b[150D\u001b[14C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[14C"] -[0.000573, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[14C\u001b[150D\u001b[10C\u001b[?25h"] -[0.476079, "o", "\u001b[?25l\u001b[150D"] -[0.000022, "o", "> \u001b[6n"] -[0.00082, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] -[0.00005, "o", "\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:52.253\u001b[0m"] -[0.000004, "o", "\u001b[150D\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C"] -[0.000157, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\r\r\n\u001b[2;3mtype a command — try 'notify', 'hint set ...', or 'scan '\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m"] -[0.000092, "o", "\u001b[1A\u001b[1A\u001b[2C\u001b[150D\u001b[2C\u001b[?25h"] -[0.525942, "o", "\u001b[5 q"] -[0.001525, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] -[0.00112, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:52.781\u001b[0m\u001b[150D\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C"] -[0.000687, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\r\r\n\u001b[2;3mtype a command — try 'notify', 'hint set ...', or 'scan '\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[0K\u001b[1m\u001b[33mfilesystem commands \u001b[0m\u001b[0K\r\r\n\u001b[mbackup\u001b[0m \u001b[2m# Create a backup of a file or directory\u001b[0m \u001b[0K\r\r\n\u001b[mconvert\u001b[0m \u001b[2m# Convert a file\u001b[0m \u001b[0K\r\r\n\u001b[mdownload\u001b[0m \u001b[2m# Download a file from a URL\u001b[0m \u001b[0K\r\r\n\u001b[mencrypt\u001b[0m \u001b[2m# Encrypt a file\u001b[0m \u001b[0K\r\r\n\u001b[mls\u001b[0m \u001b[2m# List directory contents\u001b[0m \u001b[0K\r\r\n\u001b[mmkdir\u001b[0m \u001b[2m# Create directories\u001b[0m \u001b[0K\r\r\n\u001b[mrename\u001b[0m \u001b[2m# Rename a file\u001b[0m \u001b[0K\r\r\n\u001b[msearch\u001b[0m \u001b[2m# Search for a query\u001b[0m \u001b[0K\r\r\n\u001b[1m\u001b[33mcore commands \u001b[0m\u001b[0K\r\r\n\u001b[mclient\u001b[0m \u001b[2m# Switch to the client menu (also works with CtrlC)\u001b[0m \u001b[0K\r\r\n\u001b[mexit\u001b[0m \u001b[2m# Exit the console application\u001b[0m \u001b[0K\r\r\n\u001b[mgreet\u001b[0m \u001b[2m# Greet a person\u001b[0m \u001b[0K\r\r\n\u001b[mhello\u001b[0m \u001b[2m# Say hello with customizable message\u001b[0m \u001b[0K\r\r\n\u001b[mhelp\u001b[0m \u001b[2m# Help about any command\u001b[0m \u001b[0K\r\r\n\u001b[1m\u001b[33mdeployment commands \u001b[0m\u001b[0K\r\r\n\u001b[mdeploy\u001b[0m \u001b[2m# Deploy a file\u001b[0m \u001b[0K\r\r\n\u001b[1m\u001b[33mtools commands \u001b[0m\u001b[0K\r\r\n\u001b[mgit\u001b[0m \u001b[2m# Git command\u001b[0m \u001b[0K\r\r\n\u001b[mssh\u001b[0m \u001b[2m# SSH client\u001b[0m \u001b[0K\r\r\n\u001b[1m\u001b[33mreadline commands \u001b[0m\u001b[0K\r\r\n\u001b[mhint\u001b[0m \u001b[2m# Set or clear a sticky transient hint immediately (non-async)\u001b[0m \u001b[0K\r\r\n\u001b[mnotify\u001b[0m \u001b[2m# Async status updates shown in the hint lane (transient hint + wake)\u001b[0m \u001b[0K\r\r\n\u001b[mscan\u001b[0m \u001b[2m# Async completions — press Tab after 'scan ' and watch hosts appear live\u001b[0m \u001b[0K\r\r\n\u001b[1m\u001b[33mother commands \u001b[0m\u001b[0K\r\r\n\u001b[mreadline\u001b[0m \u001b[2m# Manipulate readline options, keymaps and bindings\u001b[0m \u001b[0K\u001b[25A\u001b[1A\u001b[1A\u001b[2C\u001b[150D\u001b[2C\u001b[?25h"] -[0.952817, "o", "\u001b[5 q\u001b[?25l\u001b[150D> \u001b[6n"] -[0.000766, "o", "\u001b[150D"] -[0.000198, "o", "\u001b[1A"] -[0.000164, "o", "\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n"] -[0.001603, "o", "\u001b[2Cm\u001b[0m\u001b[2m\u001b[38;05;242multiple-ambiguous 10.203.23.45 127.0.0.1\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:53.737\u001b[0m\u001b[150D\u001b[44C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[44C"] -[0.001045, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[44C\r\r\n"] -[0.000307, "o", "\u001b[0K\u001b[1m\u001b[33mfilesystem commands \u001b[0m\u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34mm\u001b[22m\u001b[39m\u001b[mkdir\u001b[0m \u001b[2m# Create directories\u001b[0m \u001b[0K\u001b[1A\u001b[1A\u001b[44C"] -[0.000313, "o", "\u001b[150D\u001b[3C\u001b[?25h"] -[0.195881, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] -[0.00126, "o", "\u001b[150D\u001b[1A"] -[0.000229, "o", "\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] -[0.000137, "o", "mk\u001b[0m\u001b[0K\u001b[49m"] -[0.000226, "o", " \u001b[1;30m01:49:53.937\u001b[0m"] -[0.000538, "o", "\u001b[150D\u001b[4C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[4C"] -[0.00386, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[4C\r\r\n\u001b[0K\u001b[1m\u001b[33mfilesystem commands \u001b[0m\u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34mmk\u001b[22m\u001b[39m\u001b[mdir\u001b[0m \u001b[2m# Create directories\u001b[0m \u001b[0K\u001b[1A\u001b[1A\u001b[4C\u001b[150D\u001b[4C\u001b[?25h"] -[0.110343, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] -[0.001108, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] -[0.000186, "o", "mkd\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:54.053\u001b[0m"] -[0.000057, "o", "\u001b[150D\u001b[5C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[5C"] -[0.002146, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[5C\r\r\n\u001b[0K\u001b[1m\u001b[33mfilesystem commands \u001b[0m\u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34mmkd\u001b[22m\u001b[39m\u001b[mir\u001b[0m \u001b[2m# Create directories\u001b[0m \u001b[0K\u001b[1A\u001b[1A\u001b[5C\u001b[150D\u001b[5C\u001b[?25h"] -[0.107865, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] -[0.001231, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2Cmkdi\u001b[0m\u001b[0K\u001b[49m"] -[0.00008, "o", " \u001b[1;30m01:49:54.164\u001b[0m\u001b[150D\u001b[6C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[6C"] -[0.001904, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[6C\r\r\n\u001b[0K\u001b[1m\u001b[33mfilesystem commands \u001b[0m\u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34mmkdi\u001b[22m\u001b[39m\u001b[mr\u001b[0m \u001b[2m# Create directories\u001b[0m \u001b[0K\u001b[1A\u001b[1A\u001b[6C"] -[0.000109, "o", "\u001b[150D\u001b[6C\u001b[?25h"] -[0.196706, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] -[0.000747, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] -[0.000019, "o", "\u001b[1m\u001b[32mmkdir\u001b[39m\u001b[22m\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:54.364\u001b[0m\u001b[150D\u001b[7C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[7C"] -[0.001616, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[7C\r\r\n\u001b[2;3m mkdir — Create directories\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[0K\u001b[1m\u001b[33mfilesystem commands \u001b[0m\u001b[0K\r\r\n\u001b[m\u001b[1m\u001b[34mmkdir\u001b[22m\u001b[39m\u001b[m\u001b[0m \u001b[2m# Create directories\u001b[0m \u001b[0K\u001b[1A\u001b[1A\u001b[1A\u001b[7C\u001b[150D\u001b[7C\u001b[?25h"] -[0.149241, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] -[0.000992, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] -[0.000126, "o", "\u001b[1m\u001b[32mmkdir \u001b[39m\u001b[22m\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:54.516\u001b[0m"] -[0.000231, "o", "\u001b[150D\u001b[8C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[8C"] -[0.002592, "o", "\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[8C\r\r\n\u001b[2;3m mkdir — Create directories\u001b[0m\u001b[0K\r\r\n\u001b[2mmkdir [flags] DIRECTORY...\u001b[0m\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m\u001b[0K\u001b[1m\u001b[33mfiles \u001b[0m\u001b[0K\r\r\n\u001b[mcursor.cast\u001b[0m \u001b[0m\u001b[1;32mexample\u001b[0m \u001b[0m\u001b[1;32mexample.exe\u001b[0m \u001b[0m\u001b[mfeature-commands.go\u001b[0m \u001b[0m\u001b[mhistory.go\u001b[0m \u001b[0m\u001b[minterrupt.go\u001b[0m \u001b[0m\u001b[mmain-commands.go\u001b[0m \u001b[0m\u001b[0K\r\r\n\u001b[mmain.go\u001b[0m \u001b[0m\u001b[mmenu.go\u001b[0m \u001b[0m\u001b[mprompt.omp.json\u001b[0m \u001b[0m\u001b[mREADME.md\u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m\u001b[0K\u001b[2A\u001b[3A\u001b[1A\u001b[8C\u001b[150D\u001b[8C\u001b[?25h"] -[3.288206, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] -[0.001043, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:57.808\u001b[0m"] -[0.000162, "o", "\u001b[150D\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\u001b[1B\u001b[150D\u001b[0J"] -[0.000057, "o", "\u001b[1A\u001b[2C"] -[0.000009, "o", "\r\r\n"] -[0.000026, "o", "\u001b[2;3mtype a command — try 'notify', 'hint set ...', or 'scan '\u001b[0m\u001b[0K\r"] -[0.000001, "o", "\r\n"] -[0.000007, "o", "\u001b[0K\u001b[0m"] -[0.000571, "o", "\u001b[1A\u001b[1A\u001b[2C\u001b[150D\u001b[2C\u001b[?25h"] -[0.184947, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] -[0.001192, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] -[0.000153, "o", "\u001b[0m\u001b[0K\u001b[49m \u001b[1;30m01:49:57.995\u001b[0m\u001b[150D\u001b[2C\u001b[1B"] -[0.000432, "o", "\u001b[150D\u001b[0J\u001b[1A\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\r\r\n\u001b[2;3mtype a command — try 'notify', 'hint set ...', or 'scan '\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m"] -[0.000163, "o", "\u001b[1A\u001b[1A\u001b[2C\u001b[150D\u001b[2C\u001b[?25h"] -[0.289402, "o", "\u001b[2D\u001b[2C\u001b[6n"] -[0.001393, "o", "\u001b[150D\u001b[2C\u001b[0J \u001b[1;30m01:49:58.286\u001b[0m\u001b[150D\r\r\n\u001b[0 q\u001b[?2004lConfirm exit (Y/y): "] -[0.371704, "o", "\u001b[?2004h\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\r\n> \u001b[5 q"] -[0.00012, "o", "\u001b[?25l\u001b[150D> \u001b[6n"] -[0.001395, "o", "\u001b[150D\u001b[1A\u001b[33mexample\u001b[0m [main] in \u001b[34mcode/github.com/reeflective/console/example\u001b[0m\u001b[0K\r\r\n\u001b[2C"] -[0.000056, "o", "\u001b[0m\u001b[0K\u001b[49m"] -[0.000158, "o", " \u001b[1;30m01:49:58.660\u001b[0m\u001b[150D\u001b[2C\u001b[1B\u001b[150D\u001b[0J\u001b[1A\u001b[2C\u001b[1B\u001b[150D"] -[0.000038, "o", "\u001b[0J\u001b[1A"] -[0.000133, "o", "\u001b[2C\r\r\n\u001b[2;3mtype a command — try 'notify', 'hint set ...', or 'scan '\u001b[0m\u001b[0K\r\r\n\u001b[0K\u001b[0m"] -[0.000772, "o", "\u001b[1A\u001b[1A\u001b[2C\u001b[150D\u001b[2C\u001b[?25h"] -[0.709851, "o", "\u001b[2D\u001b[2C\u001b[6n"] -[0.00118, "o", "\u001b[150D\u001b[2C\u001b[0J \u001b[1;30m01:49:59.372\u001b[0m\u001b[150D\r\r\n\u001b[0 q\u001b[?2004lConfirm exit (Y/y): "] -[0.290469, "o", "y"] -[0.321349, "o", "\r\n"] -[0.011871, "x", "0"] From adab75410631d16978e43e2f0455d5049e76fee0 Mon Sep 17 00:00:00 2001 From: maxlandon Date: Mon, 1 Jun 2026 03:08:20 +0200 Subject: [PATCH 08/12] test: cover highlight flags, templates, menu filtering, and highlight cache - internal/line: HighlightCommandFlags (flags colored, operands passed through). - internal/strutil: Template rendering, the trim func, and ranging. - console: ActiveFiltersFor and CheckIsAvailable, including parent-subtree filter inheritance and activation/deactivation via Hide/ShowCommands. - console: highlight-cache memoization correctness and its invalidation on command-tree regeneration. Co-Authored-By: Claude Opus 4.8 (1M context) --- filters_test.go | 84 +++++++++++++++++++++++++++++++ highlight_cache_test.go | 40 +++++++++++++++ internal/line/highlight_test.go | 31 ++++++++++++ internal/strutil/template_test.go | 52 +++++++++++++++++++ 4 files changed, 207 insertions(+) create mode 100644 filters_test.go create mode 100644 highlight_cache_test.go create mode 100644 internal/strutil/template_test.go diff --git a/filters_test.go b/filters_test.go new file mode 100644 index 0000000..2155bed --- /dev/null +++ b/filters_test.go @@ -0,0 +1,84 @@ +package console + +import ( + "reflect" + "testing" + + "github.com/spf13/cobra" +) + +// buildFilterTree returns a small command tree: +// +// root +// └── net (filter: "windows") +// └── scan (no annotations -> inherits from parent) +// +// plus a standalone, unannotated "free" command. +func buildFilterTree() (net, scan, free *cobra.Command) { + root := &cobra.Command{Use: "root"} + net = &cobra.Command{Use: "net", Annotations: map[string]string{CommandFilterKey: "windows"}} + scan = &cobra.Command{Use: "scan"} + free = &cobra.Command{Use: "free"} + + root.AddCommand(net, free) + net.AddCommand(scan) + + return net, scan, free +} + +func TestActiveFiltersFor(t *testing.T) { + c := New("test") + menu := c.ActiveMenu() + net, scan, free := buildFilterTree() + + // No filter active yet: nothing is filtered, even annotated commands. + if got := menu.ActiveFiltersFor(net); len(got) != 0 { + t.Fatalf("before HideCommands: ActiveFiltersFor(net) = %q, want none", got) + } + + // Activate the "windows" filter. + c.HideCommands("windows") + + if got := menu.ActiveFiltersFor(net); !reflect.DeepEqual(got, []string{"windows"}) { + t.Fatalf("ActiveFiltersFor(net) = %q, want [windows]", got) + } + + // A child with no annotations inherits its parent's active filters. + if got := menu.ActiveFiltersFor(scan); !reflect.DeepEqual(got, []string{"windows"}) { + t.Fatalf("ActiveFiltersFor(scan) = %q, want [windows] (inherited)", got) + } + + // An unrelated, unannotated command is never filtered. + if got := menu.ActiveFiltersFor(free); len(got) != 0 { + t.Fatalf("ActiveFiltersFor(free) = %q, want none", got) + } + + // Removing the filter restores availability. + c.ShowCommands("windows") + if got := menu.ActiveFiltersFor(net); len(got) != 0 { + t.Fatalf("after ShowCommands: ActiveFiltersFor(net) = %q, want none", got) + } +} + +func TestCheckIsAvailable(t *testing.T) { + c := New("test") + menu := c.ActiveMenu() + net, scan, free := buildFilterTree() + + // A nil command is always available. + if err := menu.CheckIsAvailable(nil); err != nil { + t.Fatalf("CheckIsAvailable(nil) = %v, want nil", err) + } + + c.HideCommands("windows") + + if err := menu.CheckIsAvailable(net); err == nil { + t.Fatal("CheckIsAvailable(net) = nil, want error (command is filtered)") + } + if err := menu.CheckIsAvailable(scan); err == nil { + t.Fatal("CheckIsAvailable(scan) = nil, want error (inherited filter)") + } + if err := menu.CheckIsAvailable(free); err != nil { + t.Fatalf("CheckIsAvailable(free) = %v, want nil (not filtered)", err) + } +} diff --git a/highlight_cache_test.go b/highlight_cache_test.go new file mode 100644 index 0000000..6e07393 --- /dev/null +++ b/highlight_cache_test.go @@ -0,0 +1,40 @@ +package console + +import ( + "testing" + + "github.com/spf13/cobra" +) + +func TestHighlightCacheInvalidation(t *testing.T) { + c := New("test") + menu := c.ActiveMenu() + menu.SetCommands(func() *cobra.Command { + root := &cobra.Command{Use: "root"} + root.AddCommand(&cobra.Command{Use: "net", Run: func(*cobra.Command, []string) {}}) + return root + }) + menu.resetPreRun() + + in := []rune("net") + first := c.highlightSyntax(in) + + cached := c.hlCache.Load() + if cached == nil || cached.input != "net" { + t.Fatalf("expected cache populated for %q, got %+v", "net", cached) + } + if cached.output != first { + t.Fatalf("cached output %q != returned %q", cached.output, first) + } + + // Same input is served from cache and yields the same result. + if second := c.highlightSyntax(in); second != first { + t.Fatalf("second highlight %q != first %q", second, first) + } + + // Regenerating the command tree invalidates the cache. + menu.resetPreRun() + if c.hlCache.Load() != nil { + t.Fatal("expected highlight cache cleared after resetPreRun") + } +} diff --git a/internal/line/highlight_test.go b/internal/line/highlight_test.go index ea8e286..5aac0e4 100644 --- a/internal/line/highlight_test.go +++ b/internal/line/highlight_test.go @@ -36,3 +36,34 @@ func TestHighlightCommandAlias(t *testing.T) { }) } } + +func TestHighlightCommandFlags(t *testing.T) { + args := []string{"--verbose", "target", "-x", "value"} + done, _ := HighlightCommandFlags(nil, args, BrightWhiteFG) + + if len(done) != len(args) { + t.Fatalf("HighlightCommandFlags returned %d words, want %d: %q", len(done), len(args), done) + } + + tests := []struct { + idx int + shouldHighlit bool + raw string + }{ + {0, true, "--verbose"}, + {1, false, "target"}, + {2, true, "-x"}, + {3, false, "value"}, + } + + for _, tc := range tests { + got := done[tc.idx] + colored := strings.Contains(got, BrightWhiteFG) + if colored != tc.shouldHighlit { + t.Errorf("word %q: highlighted=%v, want %v (got %q)", tc.raw, colored, tc.shouldHighlit, got) + } + if !strings.Contains(got, tc.raw) { + t.Errorf("word %d: %q does not contain original %q", tc.idx, got, tc.raw) + } + } +} diff --git a/internal/strutil/template_test.go b/internal/strutil/template_test.go new file mode 100644 index 0000000..3bfd5d9 --- /dev/null +++ b/internal/strutil/template_test.go @@ -0,0 +1,52 @@ +package strutil + +import ( + "strings" + "testing" +) + +func TestTemplate(t *testing.T) { + tests := []struct { + name string + text string + data any + want string + }{ + { + name: "simple field", + text: "Hello {{.Name}}", + data: map[string]any{"Name": "world"}, + want: "Hello world", + }, + { + name: "trim func", + text: "[{{trim .S}}]", + data: map[string]any{"S": " padded "}, + want: "[padded]", + }, + { + name: "range over slice", + text: "{{range .Items}}{{.}},{{end}}", + data: map[string]any{"Items": []string{"a", "b", "c"}}, + want: "a,b,c,", + }, + { + name: "no substitution", + text: "static text", + data: nil, + want: "static text", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var b strings.Builder + if err := Template(&b, tc.text, tc.data); err != nil { + t.Fatalf("Template(%q): unexpected error: %v", tc.text, err) + } + if got := b.String(); got != tc.want { + t.Fatalf("Template(%q) = %q, want %q", tc.text, got, tc.want) + } + }) + } +} From af456c2127994386c1924ae2daafc314f38656c1 Mon Sep 17 00:00:00 2001 From: maxlandon Date: Mon, 1 Jun 2026 03:17:14 +0200 Subject: [PATCH 09/12] feat: robust interrupts, per-menu newlines, configurable signals, cancel docs - Interrupt matching: handleInterrupt now matches the incoming error against registered handlers with errors.Is first (so sentinel and wrapped errors work), then falls back to message comparison for the historical errors.New(...) pattern. Resolves the long-standing TODO in interrupt.go. - Per-menu newline behavior: NewlineBefore/After/WhenEmpty and EmptyChars are console-wide defaults that a menu can now override via SetNewlineBefore, SetNewlineAfter, SetNewlineWhenEmpty and SetEmptyChars (nil/unset inherits the console value). The pre/post-run display and TransientPrintf resolve through the active menu. - Configurable trapped signals: new Console.Signals field selects which OS signals cancel a running command. It defaults to SIGINT/SIGTERM/SIGQUIT (shared defaultTrapSignals), and monitorSignals registers exactly that set. - Cancellation model: StartContext now documents how a trapped signal cancels the command's cmd.Context(), and that a long-running command must observe that cancellation itself (cobra cannot preempt it). Co-Authored-By: Claude Opus 4.8 (1M context) --- console.go | 22 +++++++++++++-- interrupt.go | 19 +++++++------ menu.go | 80 ++++++++++++++++++++++++++++++++++++++++++++++++++++ run.go | 60 +++++++++++++++++++++++---------------- 4 files changed, 147 insertions(+), 34 deletions(-) diff --git a/console.go b/console.go index 7e91b7f..a3197b1 100644 --- a/console.go +++ b/console.go @@ -2,6 +2,7 @@ package console import ( "fmt" + "os" "strings" "sync" "sync/atomic" @@ -44,6 +45,8 @@ type Console struct { // Execution // Leave an empty line before executing the command. + // This is the console-wide default; a menu may override it with + // Menu.SetNewlineBefore. NewlineBefore bool // Leave an empty line after executing the command. @@ -51,19 +54,31 @@ type Console struct { // with TransientPrintf(), Printf() calls, you should leave this to false, // and add a leading newline to your prompt instead: the readline shell will // know how to handle it in all situations. + // This is the console-wide default; a menu may override it with + // Menu.SetNewlineAfter. NewlineAfter bool // Leave empty lines with NewlineBefore and NewlineAfter, even if the provided input was empty. // Empty characters are defined as any number of spaces and tabs. The 'empty' character set // can be changed by modifying Console.EmptyChars // This field is false by default. + // This is the console-wide default; a menu may override it with + // Menu.SetNewlineWhenEmpty. NewlineWhenEmpty bool // Characters that are used to determine whether an input line was empty. If a line is not entirely // made up by any of these characters, then it is not considered empty. The default characters // are ' ' and '\t'. + // This is the console-wide default; a menu may override it with + // Menu.SetEmptyChars. EmptyChars []rune + // Signals is the set of OS signals the console traps while a command is + // running. When one is received, the running command's context is + // cancelled (see StartContext for the cancellation model). If empty, the + // console defaults to SIGINT, SIGTERM and SIGQUIT. + Signals []os.Signal + // PreReadlineHooks - All the functions in this list will be executed, // in their respective orders, before the console starts reading // any user input (ie, before redrawing the prompt). @@ -125,6 +140,7 @@ func New(app string) *Console { // Defaults console.EmptyChars = []rune{' ', '\t'} + console.Signals = append([]os.Signal(nil), defaultTrapSignals...) return console } @@ -247,14 +263,16 @@ func (c *Console) TransientPrintf(msg string, args ...any) (n int, err error) { return fmt.Printf(msg, args...) } + newlineAfter := c.activeMenu().newlineAfter() + // If the last message we printed asynchronously // immediately precedes this new message, move up // another row, so we don't waste too much space. - if c.printed && c.NewlineAfter { + if c.printed && newlineAfter { fmt.Print("\x1b[1A") } - if c.NewlineAfter { + if newlineAfter { msg += "\n" } diff --git a/interrupt.go b/interrupt.go index 150ddf9..c0f952c 100644 --- a/interrupt.go +++ b/interrupt.go @@ -1,11 +1,18 @@ package console +import "errors" + // AddInterrupt registers a handler to run when the console receives // a given interrupt error from the underlying readline shell. // // On most systems, the following errors will be returned with keypresses: // - Linux/MacOS/Windows : Ctrl-C will return os.Interrupt. // +// The incoming error is matched against the registered one with errors.Is +// first (so wrapped errors and sentinel values work as expected), falling +// back to comparing their messages for errors that are merely value-equal +// (e.g. two distinct errors.New with the same text). +// // Many will want to use this to switch menus. Note that these interrupt errors only // work when the console is NOT currently executing a command, only when reading input. func (m *Menu) AddInterrupt(err error, handler func(c *Console)) { @@ -32,13 +39,9 @@ func (m *Menu) handleInterrupt(err error) { m.console.isExecuting.Store(true) defer m.console.isExecuting.Store(false) - // TODO: this is not a very, very safe way of comparing - // errors. I'm not sure what to right now with this, but - // from my (unreliable) expectations and usage, I see and - // use things like errors.New(os.Interrupt.String()), so - // the string itself is likely to change in the future. - // - // But if people use their own third-party errors... nothing is guaranteed. + // Match with errors.Is first so sentinel and wrapped errors behave + // correctly, then fall back to comparing messages for errors that are + // only value-equal (the historically supported errors.New(...) pattern). // // Snapshot the matching handlers under the lock, then run them once // released: a handler is free to mutate the menu (e.g. SwitchMenu) @@ -46,7 +49,7 @@ func (m *Menu) handleInterrupt(err error) { m.mutex.RLock() matched := make([]func(c *Console), 0, len(m.interruptHandlers)) for herr, handler := range m.interruptHandlers { - if err.Error() == herr.Error() { + if errors.Is(err, herr) || err.Error() == herr.Error() { matched = append(matched, handler) } } diff --git a/menu.go b/menu.go index a31fba5..5ee29f8 100644 --- a/menu.go +++ b/menu.go @@ -56,6 +56,13 @@ type Menu struct { historyNames []string histories map[string]readline.History + // Per-menu overrides of the console newline behavior. When a *bool is nil + // (or emptyChars is nil), the corresponding Console default is used. + nlBefore *bool + nlAfter *bool + nlWhenEmpty *bool + emptyChars []rune + // Concurrency management mutex *sync.RWMutex } @@ -98,6 +105,79 @@ func (m *Menu) Prompt() *Prompt { return m.prompt } +// SetNewlineBefore overrides Console.NewlineBefore for this menu only. +func (m *Menu) SetNewlineBefore(v bool) { + m.mutex.Lock() + m.nlBefore = &v + m.mutex.Unlock() +} + +// SetNewlineAfter overrides Console.NewlineAfter for this menu only. +func (m *Menu) SetNewlineAfter(v bool) { + m.mutex.Lock() + m.nlAfter = &v + m.mutex.Unlock() +} + +// SetNewlineWhenEmpty overrides Console.NewlineWhenEmpty for this menu only. +func (m *Menu) SetNewlineWhenEmpty(v bool) { + m.mutex.Lock() + m.nlWhenEmpty = &v + m.mutex.Unlock() +} + +// SetEmptyChars overrides Console.EmptyChars for this menu only. Passing no +// arguments clears the override, restoring the console default. +func (m *Menu) SetEmptyChars(chars ...rune) { + m.mutex.Lock() + m.emptyChars = chars + m.mutex.Unlock() +} + +func (m *Menu) newlineBefore() bool { + m.mutex.RLock() + defer m.mutex.RUnlock() + + if m.nlBefore != nil { + return *m.nlBefore + } + + return m.console.NewlineBefore +} + +func (m *Menu) newlineAfter() bool { + m.mutex.RLock() + defer m.mutex.RUnlock() + + if m.nlAfter != nil { + return *m.nlAfter + } + + return m.console.NewlineAfter +} + +func (m *Menu) newlineWhenEmpty() bool { + m.mutex.RLock() + defer m.mutex.RUnlock() + + if m.nlWhenEmpty != nil { + return *m.nlWhenEmpty + } + + return m.console.NewlineWhenEmpty +} + +func (m *Menu) emptyCharSet() []rune { + m.mutex.RLock() + defer m.mutex.RUnlock() + + if m.emptyChars != nil { + return m.emptyChars + } + + return m.console.EmptyChars +} + // AddHistorySource adds a source of history commands that will // be accessible to the shell when the menu is active. func (m *Menu) AddHistorySource(name string, source readline.History) { diff --git a/run.go b/run.go index 7d6e874..8cd2653 100644 --- a/run.go +++ b/run.go @@ -21,7 +21,20 @@ func (c *Console) Start() error { return c.StartContext(context.Background()) } -// StartContext is like console.Start(). with a user-provided context. +// StartContext is like console.Start(), with a user-provided context. +// +// Cancellation model: each command runs with a context derived from ctx, +// accessible from within the command via cmd.Context(). When the console +// traps one of its Signals (SIGINT/SIGTERM/SIGQUIT by default) while a command +// is running, that command's context is cancelled and any registered interrupt +// handler for the menu is invoked. Cancelling ctx itself does the same on the +// next command boundary. +// +// Because cobra cannot preempt a running command, a long-running command is +// only actually interrupted if it observes cancellation itself: select on +// cmd.Context().Done() (or pass cmd.Context() to context-aware callees) and +// return promptly. A command that ignores its context keeps running in its +// goroutine until it finishes, even though the prompt has already been freed. func (c *Console) StartContext(ctx context.Context) error { c.loadActiveHistories() @@ -249,43 +262,42 @@ func (c *Console) runLineHooks(args []string) ([]string, error) { } func (c *Console) displayPreRun(input string) { - if c.NewlineBefore { - if !c.NewlineWhenEmpty { - if !line.IsEmpty(input, c.EmptyChars...) { - fmt.Println() - } - } else { - fmt.Println() - } + menu := c.activeMenu() + + if menu.newlineBefore() && (menu.newlineWhenEmpty() || !line.IsEmpty(input, menu.emptyCharSet()...)) { + fmt.Println() } } func (c *Console) displayPostRun(lastLine string) { - if c.NewlineAfter { - if !c.NewlineWhenEmpty { - if !line.IsEmpty(lastLine, c.EmptyChars...) { - fmt.Println() - } - } else { - fmt.Println() - } + menu := c.activeMenu() + + if menu.newlineAfter() && (menu.newlineWhenEmpty() || !line.IsEmpty(lastLine, menu.emptyCharSet()...)) { + fmt.Println() } c.printed = false } +// defaultTrapSignals are the OS signals the console traps while a command is +// running when Console.Signals has not been customized. +var defaultTrapSignals = []os.Signal{ + syscall.SIGINT, + syscall.SIGTERM, + syscall.SIGQUIT, +} + // monitorSignals - Monitor the signals that can be sent to the process // while a command is running. We want to be able to cancel the command. func (c *Console) monitorSignals() chan os.Signal { sigchan := make(chan os.Signal, 1) - signal.Notify( - sigchan, - syscall.SIGINT, - syscall.SIGTERM, - syscall.SIGQUIT, - // syscall.SIGKILL, - ) + signals := c.Signals + if len(signals) == 0 { + signals = defaultTrapSignals + } + + signal.Notify(sigchan, signals...) return sigchan } From d7b56139587273a8309c24f340792dd0d74b6a45 Mon Sep 17 00:00:00 2001 From: maxlandon Date: Mon, 1 Jun 2026 03:17:17 +0200 Subject: [PATCH 10/12] test: cover interrupt matching, per-menu newlines, and signal config - handleInterrupt: errors.Is (wrapped), message fallback, and no-match. - Menu newline/empty-char overrides: inheritance, override precedence, and clearing back to inheritance. - Console.Signals: default set is populated, and monitorSignals honors a custom set (SIGUSR1 raise, unix-only build tag). Co-Authored-By: Claude Opus 4.8 (1M context) --- features_test.go | 124 +++++++++++++++++++++++++++++++++++++++++++ signals_unix_test.go | 35 ++++++++++++ 2 files changed, 159 insertions(+) create mode 100644 features_test.go create mode 100644 signals_unix_test.go diff --git a/features_test.go b/features_test.go new file mode 100644 index 0000000..2eb8a61 --- /dev/null +++ b/features_test.go @@ -0,0 +1,124 @@ +package console + +import ( + "errors" + "fmt" + "io" + "testing" +) + +func TestHandleInterruptMatching(t *testing.T) { + c := New("test") + m := c.ActiveMenu() + + var fired []string + sentinel := errors.New("boom") + + m.AddInterrupt(sentinel, func(*Console) { fired = append(fired, "sentinel") }) + m.AddInterrupt(io.EOF, func(*Console) { fired = append(fired, "eof") }) + + // errors.Is match: a wrapped io.EOF should reach the io.EOF handler. + fired = nil + m.handleInterrupt(fmt.Errorf("read failed: %w", io.EOF)) + if !reflect_equal(fired, []string{"eof"}) { + t.Fatalf("wrapped io.EOF fired %v, want [eof]", fired) + } + + // String fallback: a distinct error value with the same message as the + // registered sentinel should still match (the historical pattern). + fired = nil + m.handleInterrupt(errors.New("boom")) + if !reflect_equal(fired, []string{"sentinel"}) { + t.Fatalf("same-message error fired %v, want [sentinel]", fired) + } + + // No match: nothing fires. + fired = nil + m.handleInterrupt(errors.New("unrelated")) + if len(fired) != 0 { + t.Fatalf("unrelated error fired %v, want none", fired) + } +} + +func TestMenuNewlineOverrides(t *testing.T) { + c := New("test") + c.NewlineAfter = true + c.NewlineBefore = false + c.NewlineWhenEmpty = false + m := c.ActiveMenu() + + // With no override, the menu inherits the console defaults. + if !m.newlineAfter() { + t.Fatal("newlineAfter: expected inherited true") + } + if m.newlineBefore() { + t.Fatal("newlineBefore: expected inherited false") + } + if m.newlineWhenEmpty() { + t.Fatal("newlineWhenEmpty: expected inherited false") + } + + // Overrides take precedence over the console default. + m.SetNewlineAfter(false) + m.SetNewlineBefore(true) + m.SetNewlineWhenEmpty(true) + + if m.newlineAfter() { + t.Fatal("newlineAfter: expected override false") + } + if !m.newlineBefore() { + t.Fatal("newlineBefore: expected override true") + } + if !m.newlineWhenEmpty() { + t.Fatal("newlineWhenEmpty: expected override true") + } + + // Changing the console default no longer affects an overridden menu. + c.NewlineAfter = true + if m.newlineAfter() { + t.Fatal("newlineAfter: override should shadow console default") + } +} + +func TestMenuEmptyCharsOverride(t *testing.T) { + c := New("test") + m := c.ActiveMenu() + + // Inherits the console default set. + if string(m.emptyCharSet()) != string(c.EmptyChars) { + t.Fatalf("emptyCharSet inherited = %q, want %q", string(m.emptyCharSet()), string(c.EmptyChars)) + } + + // Override. + m.SetEmptyChars('x', 'y') + if string(m.emptyCharSet()) != "xy" { + t.Fatalf("emptyCharSet override = %q, want %q", string(m.emptyCharSet()), "xy") + } + + // No arguments clears the override, restoring inheritance. + m.SetEmptyChars() + if string(m.emptyCharSet()) != string(c.EmptyChars) { + t.Fatalf("emptyCharSet after clear = %q, want %q", string(m.emptyCharSet()), string(c.EmptyChars)) + } +} + +func TestConsoleDefaultSignals(t *testing.T) { + c := New("test") + if len(c.Signals) != len(defaultTrapSignals) { + t.Fatalf("default Signals = %v, want %v", c.Signals, defaultTrapSignals) + } +} + +// reflect_equal is a tiny string-slice comparison helper to avoid importing +// reflect for a single use. +func reflect_equal(a, b []string) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} diff --git a/signals_unix_test.go b/signals_unix_test.go new file mode 100644 index 0000000..5796c7b --- /dev/null +++ b/signals_unix_test.go @@ -0,0 +1,35 @@ +//go:build unix + +package console + +import ( + "os" + "os/signal" + "syscall" + "testing" + "time" +) + +// TestMonitorSignalsCustom verifies that monitorSignals honors a customized +// Console.Signals set. SIGUSR1 is used because it is not part of the default +// trapped set and is not sent by the test harness. +func TestMonitorSignalsCustom(t *testing.T) { + c := New("test") + c.Signals = []os.Signal{syscall.SIGUSR1} + + ch := c.monitorSignals() + defer signal.Stop(ch) + + if err := syscall.Kill(os.Getpid(), syscall.SIGUSR1); err != nil { + t.Fatalf("failed to raise SIGUSR1: %v", err) + } + + select { + case got := <-ch: + if got != syscall.SIGUSR1 { + t.Fatalf("received %v, want SIGUSR1", got) + } + case <-time.After(2 * time.Second): + t.Fatal("timed out waiting for the custom signal") + } +} From 1aeb5d84305d20fef0e00c98364568e57ea7de60 Mon Sep 17 00:00:00 2001 From: maxlandon Date: Mon, 1 Jun 2026 03:20:09 +0200 Subject: [PATCH 11/12] test: cover the pre/post-run newline display matrix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Capture os.Stdout and assert the displayPreRun/displayPostRun newline output across the enabled × when-empty × (empty/non-empty input) matrix, plus a case proving a per-menu newline override is honored by the display path. Co-Authored-By: Claude Opus 4.8 (1M context) --- newline_display_test.go | 108 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 newline_display_test.go diff --git a/newline_display_test.go b/newline_display_test.go new file mode 100644 index 0000000..10c9070 --- /dev/null +++ b/newline_display_test.go @@ -0,0 +1,108 @@ +package console + +import ( + "io" + "os" + "testing" +) + +// captureStdout redirects os.Stdout for the duration of fn and returns what was +// written. The display functions print via fmt.Println, which targets +// os.Stdout directly. +func captureStdout(t *testing.T, fn func()) string { + t.Helper() + + orig := os.Stdout + r, w, err := os.Pipe() + if err != nil { + t.Fatalf("os.Pipe: %v", err) + } + + os.Stdout = w + fn() + os.Stdout = orig + + if err := w.Close(); err != nil { + t.Fatalf("close writer: %v", err) + } + + out, err := io.ReadAll(r) + if err != nil { + t.Fatalf("read: %v", err) + } + _ = r.Close() + + return string(out) +} + +func TestDisplayNewlineMatrix(t *testing.T) { + // A newline is printed iff: enabled && (whenEmpty || input is non-empty). + cases := []struct { + name string + enabled bool + whenEmpty bool + input string + wantNewline bool + }{ + {"disabled/empty", false, false, "", false}, + {"disabled/nonempty", false, false, "cmd", false}, + {"disabled/whenEmpty/nonempty", false, true, "cmd", false}, + + {"enabled/nonempty", true, false, "cmd", true}, + {"enabled/empty", true, false, "", false}, + {"enabled/spaces-are-empty", true, false, " \t ", false}, + + {"enabled/whenEmpty/empty", true, true, "", true}, + {"enabled/whenEmpty/nonempty", true, true, "cmd", true}, + {"enabled/whenEmpty/spaces", true, true, " \t ", true}, + } + + for _, tc := range cases { + want := "" + if tc.wantNewline { + want = "\n" + } + + t.Run("pre/"+tc.name, func(t *testing.T) { + c := New("test") + c.NewlineBefore = tc.enabled + c.NewlineWhenEmpty = tc.whenEmpty + + got := captureStdout(t, func() { c.displayPreRun(tc.input) }) + if got != want { + t.Fatalf("displayPreRun(%q) printed %q, want %q", tc.input, got, want) + } + }) + + t.Run("post/"+tc.name, func(t *testing.T) { + c := New("test") + c.NewlineAfter = tc.enabled + c.NewlineWhenEmpty = tc.whenEmpty + + got := captureStdout(t, func() { c.displayPostRun(tc.input) }) + if got != want { + t.Fatalf("displayPostRun(%q) printed %q, want %q", tc.input, got, want) + } + }) + } +} + +// TestDisplayNewlineMenuOverride checks that a per-menu newline override is +// honored by the display path even when the console default differs. +func TestDisplayNewlineMenuOverride(t *testing.T) { + c := New("test") + c.NewlineAfter = false // console default: off + c.ActiveMenu().SetNewlineAfter(true) + + if got := captureStdout(t, func() { c.displayPostRun("cmd") }); got != "\n" { + t.Fatalf("menu override on: displayPostRun printed %q, want %q", got, "\n") + } + + // And the inverse: console on, menu override off. + c.NewlineAfter = true + c.ActiveMenu().SetNewlineAfter(false) + + if got := captureStdout(t, func() { c.displayPostRun("cmd") }); got != "" { + t.Fatalf("menu override off: displayPostRun printed %q, want %q", got, "") + } +} From e3c58371a83cb1fa90dd69c44d9a6b9555f7f68f Mon Sep 17 00:00:00 2001 From: maxlandon Date: Mon, 1 Jun 2026 03:23:02 +0200 Subject: [PATCH 12/12] ci: bump Go to 1.25 to match the module's go directive The module now requires go 1.25.0 (readline v1.2.0 and mvdan.cc/sh v3.13.1), so the CI's pinned Go 1.21 would fail to build. Bump both the unix and windows jobs to Go 1.25. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/go.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 1521716..685bf27 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -23,7 +23,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v3 with: - go-version: '1.21' + go-version: '1.25' - name: Build run: go build -v ./... @@ -42,7 +42,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v3 with: - go-version: '1.21' + go-version: '1.25' - name: Build run: go build -v ./...