From 410dc3ad498a3a43e3935a98960149881d9a08f1 Mon Sep 17 00:00:00 2001 From: Blue Falcon Date: Mon, 27 Oct 2025 05:55:11 -0700 Subject: [PATCH 01/13] move lock into clipboard --- app.js | 17 +---------------- clipboard.js | 14 +++++++++++++- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/app.js b/app.js index 43859a3..3a27e88 100644 --- a/app.js +++ b/app.js @@ -2,7 +2,6 @@ import fs from 'fs'; import readline from 'readline'; -import properLockFile from 'proper-lockfile'; import ansi from 'ansi-escapes'; import boring from 'boring'; import { inspect } from 'util'; @@ -42,8 +41,7 @@ fs.mkdirSync(stateFolder, { recursive: true }); const logFile = fs.createWriteStream(`${stateFolder}/log.txt`, 'utf8'); const clipboard = clipboardFactory({ - stateFolder, - lock + stateFolder }); const hintStack = [ @@ -171,19 +169,6 @@ function log(...args) { } } -function lock(filename) { - return properLockFile(filename, { - retries: { - retries: 30, - factor: 1, - minTimeout: 1000, - maxTimeout: 1000 - }, - // Avoid chicken and egg problem when the file does not exist yet - realpath: false - }); -} - function loadFile() { if (!fs.existsSync(filename)) { return false; diff --git a/clipboard.js b/clipboard.js index 296dca9..9d30a79 100644 --- a/clipboard.js +++ b/clipboard.js @@ -1,10 +1,22 @@ "use strict"; import fs from 'fs'; +import properLockFile from 'proper-lockfile'; export default ({ stateFolder, - lock + lock = function lock(filename) { + return properLockFile(filename, { + retries: { + retries: 30, + factor: 1, + minTimeout: 1000, + maxTimeout: 1000 + }, + // Avoid chicken and egg problem when the file does not exist yet + realpath: false + }); + } }) => { const clipboardFile = `${stateFolder}/clipboard.json`; const clipboardLockFile = `${clipboardFile}.lock`; From b27d2e8d3b84cc22588289677a12c8097303c3bb Mon Sep 17 00:00:00 2001 From: Blue Falcon Date: Mon, 27 Oct 2025 05:55:42 -0700 Subject: [PATCH 02/13] update package-lock --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index eede83f..05c4490 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@boutell/tome", - "version": "0.4.1", + "version": "0.13.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@boutell/tome", - "version": "0.4.1", + "version": "0.13.1", "license": "GPLv3", "dependencies": { "ansi-escapes": "^6.2.0", From 13910e81c7736deb821a466034ab72fe731ddee1 Mon Sep 17 00:00:00 2001 From: Blue Falcon Date: Mon, 27 Oct 2025 05:56:52 -0700 Subject: [PATCH 03/13] allow more key combos --- app.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/app.js b/app.js index 3a27e88..090d055 100644 --- a/app.js +++ b/app.js @@ -93,12 +93,13 @@ stdin.on('keypress', (c, k) => { // ignore occasional undefined keys on mac return; } - if (k.shift) { - k.name = `shift-${k.name}`; - } - if (k.ctrl) { - k.name = `control-${k.name}`; - } + let prefix = ''; + if (k.ctrl) prefix += 'control-'; + if (k.meta) prefix += 'meta-'; + if (k.shift) prefix += 'shift-'; + + k.name = prefix + k.name; + if ((k.sequence.charCodeAt(0) === 27) && (k.sequence.charCodeAt(1) === 27)) { // readline isn't quite smart enough on its own to do the right thing if // ESC is followed quickly by an arrow key, but gives us enough information From 8e03c4ec17de243420727fde80c644cb8ae022c5 Mon Sep 17 00:00:00 2001 From: Blue Falcon Date: Mon, 27 Oct 2025 05:59:51 -0700 Subject: [PATCH 04/13] allow stdout/stderr redirection --- app.js | 44 +++++++++++++++++++++++++++++--------------- screen.js | 4 ++-- 2 files changed, 31 insertions(+), 17 deletions(-) diff --git a/app.js b/app.js index 090d055..860859d 100644 --- a/app.js +++ b/app.js @@ -14,8 +14,19 @@ import loadLanguages from './load-languages.js'; import Screen from './screen.js'; +const tabSpaces = 2; + +const argv = boring(); + +const filename = argv._[0]; + const stdin = process.stdin; -const stdout = process.stdout; +let stdout; +if(argv.stdout === true) { + stdout = process.stderr; +} else { + stdout = process.stdout; +} stdin.setRawMode(true); readline.emitKeypressEvents(stdin); const screen = new Screen({ @@ -23,13 +34,7 @@ const screen = new Screen({ log }); -const tabSpaces = 2; - -const argv = boring(); - -const filename = argv._[0]; - -if (!filename && !argv['debug-keycodes']) { +if (!argv.stdout && !filename && !argv['debug-keycodes']) { usage(); } @@ -188,6 +193,9 @@ function newFile() { } function saveFile() { + if(argv.stdout) + console.log(getText(editor.chars)); + else fs.writeFileSync(filename, getText(editor.chars)); } @@ -196,6 +204,12 @@ function getText(chars) { } async function closeEditor() { + if(argv.stdout){ + stdout.write(ansi.clearScreen); + saveFile(); + process.exit(0); + } + const text = getText(editor.chars); if (text !== originalText) { if (await confirm('Save before exiting? [Y/n]', true)) { @@ -235,7 +249,7 @@ function usage() { function resize() { screen.resize(); - editor.resize(process.stdout.columns, process.stdout.rows - 3); + editor.resize(stdout.columns, stdout.rows - 3); } function guessLanguage(filename) { @@ -251,12 +265,12 @@ function status(prompt) { const hints = hintStack[hintStack.length - 1]; const width = Math.max(...hints.map(s => s.length)) + 2; let col = 0; - let row = process.stdout.rows - 2; + let row = stdout.rows - 2; for (const hint of hints) { - if (col + width >= process.stdout.columns) { + if (col + width >= stdout.columns) { fillRest(); row++; - if (row >= process.stdout.rows) { + if (row >= stdout.rows) { break; } } @@ -265,7 +279,7 @@ function status(prompt) { } col += width; } - while (row < process.stdout.rows) { + while (row < stdout.rows) { for (let sx = col; (sx < screen.width); sx++) { screen.set(sx, row, ' '); } @@ -274,9 +288,9 @@ function status(prompt) { } const left = `${editor.row + 1} ${editor.col + 1} ${shortFilename()}`; const right = (prompt !== false) ? prompt : ''; - const s = left + ' '.repeat(process.stdout.columns - 1 - right.length - left.length) + right; + const s = left + ' '.repeat(stdout.columns - 1 - right.length - left.length) + right; for (let i = 0; (i < s.length); i++) { - screen.set(i, process.stdout.rows - 3, s.charAt(i)); + screen.set(i, stdout.rows - 3, s.charAt(i)); } function fillRest() { while (col < screen.width) { diff --git a/screen.js b/screen.js index 73623c0..7feeb2f 100644 --- a/screen.js +++ b/screen.js @@ -35,8 +35,8 @@ export default class Screen { } allocate() { const data = []; - this.width = process.stdout.columns; - this.height = process.stdout.rows; + this.width = this.stdout.columns; + this.height = this.stdout.rows; for (let row = 0; (row < this.height); row++) { const cells = []; for (let col = 0; (col < this.width); col++) { From ffcad47586e389bbd26c1800e43ed67a2b54e7d3 Mon Sep 17 00:00:00 2001 From: Blue Falcon Date: Mon, 27 Oct 2025 06:01:52 -0700 Subject: [PATCH 05/13] fix close and save on std redirection --- app.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app.js b/app.js index 860859d..d489298 100644 --- a/app.js +++ b/app.js @@ -193,9 +193,8 @@ function newFile() { } function saveFile() { - if(argv.stdout) - console.log(getText(editor.chars)); - else + if(argv.stdout && filename == null) + return; fs.writeFileSync(filename, getText(editor.chars)); } @@ -206,7 +205,7 @@ function getText(chars) { async function closeEditor() { if(argv.stdout){ stdout.write(ansi.clearScreen); - saveFile(); + console.log(getText(editor.chars)); process.exit(0); } From 34b87f464018031ba29a914ee23a132aef476159 Mon Sep 17 00:00:00 2001 From: Blue Falcon Date: Mon, 27 Oct 2025 06:04:00 -0700 Subject: [PATCH 06/13] update executable --- bin/tome.sh | 14 ++++++++++++++ package.json | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 bin/tome.sh diff --git a/bin/tome.sh b/bin/tome.sh new file mode 100644 index 0000000..53a4ea7 --- /dev/null +++ b/bin/tome.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +# ~/.local/bin/tome (make sure this directory precedes the real one in PATH) + +scriptroot=$(dirname "$(realpath "$0")") + +# Resolve path to the real tome executable (avoid recursion) +real_tome="$scriptroot/tome.js" + +# Detect presence of --stdout among arguments +if [[ " $* " == *" --stdout "* ]]; then + exec "$real_tome" "$@" 2>/dev/tty +else + exec "$real_tome" "$@" +fi diff --git a/package.json b/package.json index dbdbdfe..a866ca3 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "url": "https://github.com/boutell/tome/issues" }, "bin": { - "tome": "./bin/tome.js" + "tome": "./bin/tome.sh" }, "homepage": "https://github.com/boutell/tome#readme", "dependencies": { From cc91884605fb8e0dd5fc7565bcea99bd84cb688c Mon Sep 17 00:00:00 2001 From: Blue Falcon Date: Mon, 27 Oct 2025 06:46:31 -0700 Subject: [PATCH 07/13] allow piped content --- app.js | 23 +++++++++++++++++++++-- bin/tome.sh | 18 ++++++++++++++---- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/app.js b/app.js index d489298..2672051 100644 --- a/app.js +++ b/app.js @@ -19,6 +19,7 @@ const tabSpaces = 2; const argv = boring(); const filename = argv._[0]; +argv.stdout = argv.stdout || !!argv._[1]; const stdin = process.stdin; let stdout; @@ -150,6 +151,9 @@ while (true) { } function shortFilename(prompt) { + if (!filename) { + return ''; + } return filename.split('/').pop().substring(0, stdout.columns - (prompt || '').length - 5); } @@ -176,11 +180,23 @@ function log(...args) { } function loadFile() { + let content; + if(argv.stdout) { + if(!!argv._[1]) { + content = argv._[1].split('\n').map(line => [...line]); + } + if(!filename && !content) { + return false; + } + } + if(!content){ if (!fs.existsSync(filename)) { return false; + } else { + // Emoji-safe split by character (split('') is not safe) + content = fs.readFileSync(filename, 'utf8').split('\n').map(line => [...line]); + } } - // Emoji-safe split by character (split('') is not safe) - const content = fs.readFileSync(filename, 'utf8').split('\n').map(line => [...line]); if (!content.length) { content.push([]); } @@ -252,6 +268,9 @@ function resize() { } function guessLanguage(filename) { + if(!filename) { + return languages.default; + } const matches = filename.match(/\.([^\.]+)$/); if (matches) { const found = Object.values(languages).find(language => language.extensions.includes(matches[1])); diff --git a/bin/tome.sh b/bin/tome.sh index 53a4ea7..0c32e2d 100644 --- a/bin/tome.sh +++ b/bin/tome.sh @@ -6,9 +6,19 @@ scriptroot=$(dirname "$(realpath "$0")") # Resolve path to the real tome executable (avoid recursion) real_tome="$scriptroot/tome.js" -# Detect presence of --stdout among arguments -if [[ " $* " == *" --stdout "* ]]; then - exec "$real_tome" "$@" 2>/dev/tty +# Detect presence of --stdout among arguments and handle stdin accordingly +if [ -t 0 ]; then + if [[ " $* " == *" --stdout "* ]]; then + exec "$real_tome" "$1" 2>/dev/tty + else + exec "$real_tome" "$1" + fi else - exec "$real_tome" "$@" + input_data=$(cat) + if [[ " $* " == *" --stdout "* ]]; then + exec "$real_tome" "$1" "$input_data" 2>/dev/tty < /dev/tty + else + exec "$real_tome" "$1" "$input_data" < /dev/tty + fi fi + From c0088b82da5e21107faefb22db7f873442271aa7 Mon Sep 17 00:00:00 2001 From: Blue Falcon Date: Mon, 27 Oct 2025 06:48:54 -0700 Subject: [PATCH 08/13] remove irrelevant comment --- bin/tome.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/bin/tome.sh b/bin/tome.sh index 0c32e2d..108c739 100644 --- a/bin/tome.sh +++ b/bin/tome.sh @@ -1,5 +1,4 @@ #!/usr/bin/env bash -# ~/.local/bin/tome (make sure this directory precedes the real one in PATH) scriptroot=$(dirname "$(realpath "$0")") From ea0f7e4d4e37ab7f2f21b547189f5f3d6d714061 Mon Sep 17 00:00:00 2001 From: Blue Falcon Date: Mon, 27 Oct 2025 07:07:04 -0700 Subject: [PATCH 09/13] cleanup window title when piping --- app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.js b/app.js index 2672051..dbcadad 100644 --- a/app.js +++ b/app.js @@ -151,7 +151,7 @@ while (true) { } function shortFilename(prompt) { - if (!filename) { + if (!filename || (argv.stdout && !fs.existsSync(filename))) { return ''; } return filename.split('/').pop().substring(0, stdout.columns - (prompt || '').length - 5); From 0a10b7fb488c9d0d5361557051d60f744c115ad2 Mon Sep 17 00:00:00 2001 From: Blue Falcon Date: Mon, 27 Oct 2025 07:07:42 -0700 Subject: [PATCH 10/13] prefer file content over piped --- app.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app.js b/app.js index dbcadad..70d8250 100644 --- a/app.js +++ b/app.js @@ -189,13 +189,11 @@ function loadFile() { return false; } } - if(!content){ - if (!fs.existsSync(filename)) { + if (!filename || !fs.existsSync(filename)) { return false; } else { // Emoji-safe split by character (split('') is not safe) content = fs.readFileSync(filename, 'utf8').split('\n').map(line => [...line]); - } } if (!content.length) { content.push([]); From be50d06e1d6965c689375e8a9cd6b6d7da290a30 Mon Sep 17 00:00:00 2001 From: Blue Falcon Date: Mon, 27 Oct 2025 07:08:54 -0700 Subject: [PATCH 11/13] switch to sh for compat --- bin/tome.sh | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/bin/tome.sh b/bin/tome.sh index 108c739..a1efd1d 100644 --- a/bin/tome.sh +++ b/bin/tome.sh @@ -1,21 +1,31 @@ -#!/usr/bin/env bash +#!/usr/bin/env sh scriptroot=$(dirname "$(realpath "$0")") - -# Resolve path to the real tome executable (avoid recursion) real_tome="$scriptroot/tome.js" +case " $* " in + *" --stdout "*) has_stdout=1 ;; + *) has_stdout=0 ;; +esac + +args= +for arg in "$@"; do + [ "$arg" = "--stdout" ] || args="$args \"$arg\"" +done + +eval "set -- $args" + # Detect presence of --stdout among arguments and handle stdin accordingly if [ -t 0 ]; then - if [[ " $* " == *" --stdout "* ]]; then - exec "$real_tome" "$1" 2>/dev/tty + if [ "$has_stdout" -eq 1 ]; then + exec "$real_tome" "$1" --stdout 2>/dev/tty else exec "$real_tome" "$1" fi else input_data=$(cat) - if [[ " $* " == *" --stdout "* ]]; then - exec "$real_tome" "$1" "$input_data" 2>/dev/tty < /dev/tty + if [ "$has_stdout" -eq 1 ]; then + exec "$real_tome" "$1" "$input_data" --stdout 2>/dev/tty < /dev/tty else exec "$real_tome" "$1" "$input_data" < /dev/tty fi From f6f98b8f9b7c90635f3f130e39e8bc6a16231f2e Mon Sep 17 00:00:00 2001 From: Blue Falcon Date: Tue, 28 Oct 2025 18:04:52 -0700 Subject: [PATCH 12/13] fix save file bug --- app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.js b/app.js index 70d8250..5008b44 100644 --- a/app.js +++ b/app.js @@ -207,7 +207,7 @@ function newFile() { } function saveFile() { - if(argv.stdout && filename == null) + if(argv.stdout && (filename == null || !fs.existsSync(filename))) return; fs.writeFileSync(filename, getText(editor.chars)); } From f3168edd248d37394310d25ee647527a84a73da4 Mon Sep 17 00:00:00 2001 From: Blue Falcon Date: Tue, 28 Oct 2025 18:05:58 -0700 Subject: [PATCH 13/13] handle stdout and stderr TTY absences --- app.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app.js b/app.js index 5008b44..587cc69 100644 --- a/app.js +++ b/app.js @@ -19,7 +19,7 @@ const tabSpaces = 2; const argv = boring(); const filename = argv._[0]; -argv.stdout = argv.stdout || !!argv._[1]; +argv.stdout = argv.stdout || !!argv._[1] || !process.stdout.isTTY; const stdin = process.stdin; let stdout; @@ -35,7 +35,10 @@ const screen = new Screen({ log }); -if (!argv.stdout && !filename && !argv['debug-keycodes']) { +if ((!argv.stdout && !filename && !argv['debug-keycodes']) || (argv.stdout && !process.stderr.isTTY)) { + if(argv.stdout && !process.stderr.isTTY) { + process.stderr.write('Error: Unable to display editor UI over stderr. When piping or using --stdout, stderr must be a TTY.\n\n'); + } usage(); }