novac 2.1.1 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +0 -0
- package/demo.nv +0 -0
- package/demo_builtins.nv +0 -0
- package/demo_http.nv +0 -0
- package/examples/bf.nv +69 -0
- package/examples/math.nv +21 -0
- package/kits/birdAPI/kitdef.js +954 -0
- package/kits/kitRNG/kitdef.js +740 -0
- package/kits/kitSSH/kitdef.js +1272 -0
- package/kits/kitadb/kitdef.js +606 -0
- package/kits/kitai/kitdef.js +2185 -0
- package/kits/kitcanvas/kitdef.js +914 -0
- package/kits/kitclippy/kitdef.js +925 -0
- package/kits/kitgps/kitdef.js +1862 -0
- package/kits/kitlibproc/kitdef.js +3 -2
- package/kits/kitmorse/kitdef.js +229 -0
- package/kits/kitmpatch/kitdef.js +906 -0
- package/kits/kitnet/kitdef.js +1401 -0
- package/kits/kitproto/kitdef.js +613 -0
- package/kits/kitqr/kitdef.js +637 -0
- package/kits/kitrequire/kitdef.js +1599 -0
- package/kits/libtea/kitdef.js +2691 -0
- package/kits/libterm/kitdef.js +2 -0
- package/novac/LICENSE +21 -0
- package/novac/README.md +1823 -0
- package/novac/bin/novac +950 -0
- package/novac/bin/nvc +522 -0
- package/novac/bin/nvml +542 -0
- package/novac/demo.nv +245 -0
- package/novac/demo_builtins.nv +209 -0
- package/novac/demo_http.nv +62 -0
- package/novac/examples/bf.nv +69 -0
- package/novac/examples/math.nv +21 -0
- package/novac/kits/kitai/kitdef.js +2185 -0
- package/novac/kits/kitansi/kitdef.js +1402 -0
- package/novac/kits/kitformat/kitdef.js +1485 -0
- package/novac/kits/kitgps/kitdef.js +1862 -0
- package/novac/kits/kitlibfs/kitdef.js +231 -0
- package/{examples/example-project/nova_modules → novac/kits}/kitlibproc/kitdef.js +3 -2
- package/novac/kits/kitmatrix/ex.js +19 -0
- package/novac/kits/kitmatrix/kitdef.js +960 -0
- package/novac/kits/kitmpatch/kitdef.js +906 -0
- package/novac/kits/kitnovacweb/README.md +1572 -0
- package/novac/kits/kitnovacweb/demo.nv +12 -0
- package/novac/kits/kitnovacweb/demo.nvml +71 -0
- package/novac/kits/kitnovacweb/index.nova +12 -0
- package/novac/kits/kitnovacweb/kitdef.js +692 -0
- package/novac/kits/kitnovacweb/nova.kit.json +8 -0
- package/novac/kits/kitnovacweb/nvml/executor.js +739 -0
- package/novac/kits/kitnovacweb/nvml/index.js +67 -0
- package/novac/kits/kitnovacweb/nvml/lexer.js +263 -0
- package/novac/kits/kitnovacweb/nvml/parser.js +508 -0
- package/novac/kits/kitnovacweb/nvml/renderer.js +924 -0
- package/novac/kits/kitparse/kitdef.js +1688 -0
- package/novac/kits/kitregex++/kitdef.js +1353 -0
- package/novac/kits/kitrequire/kitdef.js +1599 -0
- package/novac/kits/kitx11/kitdef.js +1 -0
- package/novac/kits/kitx11/kitx11.js +2472 -0
- package/novac/kits/kitx11/kitx11_conn.js +948 -0
- package/novac/kits/kitx11/kitx11_worker.js +121 -0
- package/novac/kits/libterm/ex.js +285 -0
- package/novac/kits/libterm/kitdef.js +1927 -0
- package/novac/node_modules/chalk/license +9 -0
- package/novac/node_modules/chalk/package.json +83 -0
- package/novac/node_modules/chalk/readme.md +297 -0
- package/novac/node_modules/chalk/source/index.d.ts +325 -0
- package/novac/node_modules/chalk/source/index.js +225 -0
- package/novac/node_modules/chalk/source/utilities.js +33 -0
- package/novac/node_modules/chalk/source/vendor/ansi-styles/index.d.ts +236 -0
- package/novac/node_modules/chalk/source/vendor/ansi-styles/index.js +223 -0
- package/novac/node_modules/chalk/source/vendor/supports-color/browser.d.ts +1 -0
- package/novac/node_modules/chalk/source/vendor/supports-color/browser.js +34 -0
- package/novac/node_modules/chalk/source/vendor/supports-color/index.d.ts +55 -0
- package/novac/node_modules/chalk/source/vendor/supports-color/index.js +190 -0
- package/novac/node_modules/commander/LICENSE +22 -0
- package/novac/node_modules/commander/Readme.md +1176 -0
- package/novac/node_modules/commander/esm.mjs +16 -0
- package/novac/node_modules/commander/index.js +24 -0
- package/novac/node_modules/commander/lib/argument.js +150 -0
- package/novac/node_modules/commander/lib/command.js +2777 -0
- package/novac/node_modules/commander/lib/error.js +39 -0
- package/novac/node_modules/commander/lib/help.js +747 -0
- package/novac/node_modules/commander/lib/option.js +380 -0
- package/novac/node_modules/commander/lib/suggestSimilar.js +101 -0
- package/novac/node_modules/commander/package-support.json +19 -0
- package/novac/node_modules/commander/package.json +82 -0
- package/novac/node_modules/commander/typings/esm.d.mts +3 -0
- package/novac/node_modules/commander/typings/index.d.ts +1113 -0
- package/novac/node_modules/node-addon-api/LICENSE.md +9 -0
- package/novac/node_modules/node-addon-api/README.md +95 -0
- package/novac/node_modules/node-addon-api/common.gypi +21 -0
- package/novac/node_modules/node-addon-api/except.gypi +25 -0
- package/novac/node_modules/node-addon-api/index.js +14 -0
- package/novac/node_modules/node-addon-api/napi-inl.deprecated.h +186 -0
- package/novac/node_modules/node-addon-api/napi-inl.h +7165 -0
- package/novac/node_modules/node-addon-api/napi.h +3364 -0
- package/novac/node_modules/node-addon-api/node_addon_api.gyp +42 -0
- package/novac/node_modules/node-addon-api/node_api.gyp +9 -0
- package/novac/node_modules/node-addon-api/noexcept.gypi +26 -0
- package/novac/node_modules/node-addon-api/package-support.json +21 -0
- package/novac/node_modules/node-addon-api/package.json +480 -0
- package/novac/node_modules/node-addon-api/tools/README.md +73 -0
- package/novac/node_modules/node-addon-api/tools/check-napi.js +99 -0
- package/novac/node_modules/node-addon-api/tools/clang-format.js +71 -0
- package/novac/node_modules/node-addon-api/tools/conversion.js +301 -0
- package/novac/node_modules/serialize-javascript/LICENSE +27 -0
- package/novac/node_modules/serialize-javascript/README.md +149 -0
- package/novac/node_modules/serialize-javascript/index.js +297 -0
- package/novac/node_modules/serialize-javascript/package.json +33 -0
- package/novac/package.json +27 -0
- package/novac/scripts/update-bin.js +24 -0
- package/novac/src/core/bstd.js +1035 -0
- package/novac/src/core/config.js +155 -0
- package/novac/src/core/describe.js +187 -0
- package/novac/src/core/emitter.js +499 -0
- package/novac/src/core/error.js +86 -0
- package/novac/src/core/executor.js +5606 -0
- package/novac/src/core/formatter.js +686 -0
- package/novac/src/core/lexer.js +1026 -0
- package/novac/src/core/nova_builtins.js +717 -0
- package/novac/src/core/nova_thread_worker.js +166 -0
- package/novac/src/core/parser.js +2181 -0
- package/novac/src/core/types.js +112 -0
- package/novac/src/index.js +28 -0
- package/novac/src/runtime/stdlib.js +244 -0
- package/package.json +3 -2
- package/scripts/update-bin.js +0 -0
- package/src/core/bstd.js +835 -361
- package/src/core/executor.js +427 -246
- package/src/core/lexer.js +19 -2
- package/src/core/parser.js +13 -0
- package/src/index.js +0 -0
- package/examples/example-project/README.md +0 -3
- package/examples/example-project/src/main.nova +0 -3
- package/src/core/environment.js +0 -0
- /package/{kits → novac/kits}/libtea/tf.js +0 -0
- /package/{examples/example-project/bin/example-project.nv → novac/node_modules/node-addon-api/nothing.c} +0 -0
|
@@ -0,0 +1,925 @@
|
|
|
1
|
+
// kitdef.js — kitClipboard
|
|
2
|
+
// Cross-platform clipboard management: read, write, file copy, format detection,
|
|
3
|
+
// history, monitoring, paste detection, and more.
|
|
4
|
+
//
|
|
5
|
+
// Platform support: Windows, macOS, Linux (X11/Wayland), Android (Termux), iOS
|
|
6
|
+
// Backends: pbcopy/pbpaste (macOS), xclip/xsel/wl-clipboard (Linux),
|
|
7
|
+
// clip/powershell (Windows), Termux API (Android)
|
|
8
|
+
//
|
|
9
|
+
// const { kitClipboard } = require('./kitdef').kitdef;
|
|
10
|
+
|
|
11
|
+
const { execSync, execFileSync, spawn } = require("child_process");
|
|
12
|
+
const fs = require("fs");
|
|
13
|
+
const path = require("path");
|
|
14
|
+
const os = require("os");
|
|
15
|
+
|
|
16
|
+
// ── Platform detection ───────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
function _platform() {
|
|
19
|
+
const p = process.platform;
|
|
20
|
+
if (p === "darwin") return "macos";
|
|
21
|
+
if (p === "win32") return "windows";
|
|
22
|
+
if (p === "android" || process.env.TERMUX_VERSION) return "android";
|
|
23
|
+
// Linux: detect Wayland vs X11
|
|
24
|
+
if (process.env.WAYLAND_DISPLAY) return "wayland";
|
|
25
|
+
if (process.env.DISPLAY) return "x11";
|
|
26
|
+
return "linux"; // fallback
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function _hasCmd(cmd) {
|
|
30
|
+
try { execSync(`command -v ${cmd}`, { stdio: "pipe" }); return true; }
|
|
31
|
+
catch { return false; }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function _linuxBackend() {
|
|
35
|
+
if (_platform() === "wayland" && _hasCmd("wl-copy")) return "wayland";
|
|
36
|
+
if (_hasCmd("xclip")) return "xclip";
|
|
37
|
+
if (_hasCmd("xsel")) return "xsel";
|
|
38
|
+
if (_hasCmd("wl-copy")) return "wayland";
|
|
39
|
+
throw new Error("No clipboard backend found. Install xclip, xsel, or wl-clipboard.");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ── Bracketed Paste Mode (terminal escape sequence) ──────────────────────────
|
|
43
|
+
//
|
|
44
|
+
// When a terminal supports bracketed paste mode (\x1b[?2004h), pasted text is
|
|
45
|
+
// wrapped between \x1b[200~ ... \x1b[201~, making it unambiguously distinct
|
|
46
|
+
// from keyboard input. We enable it on stdin when it's a TTY, listen for the
|
|
47
|
+
// markers, and set a flag that detectSource() can read.
|
|
48
|
+
//
|
|
49
|
+
// References:
|
|
50
|
+
// https://cirw.in/blog/bracketed-paste
|
|
51
|
+
// https://invisible-island.net/xterm/ctlseqs/ctlseqs.html (DEC mode 2004)
|
|
52
|
+
|
|
53
|
+
const PASTE_START = "\x1b[200~";
|
|
54
|
+
const PASTE_END = "\x1b[201~";
|
|
55
|
+
const BP_ENABLE = "\x1b[?2004h";
|
|
56
|
+
const BP_DISABLE = "\x1b[?2004l";
|
|
57
|
+
|
|
58
|
+
let _bracketedPasteSupported = false;
|
|
59
|
+
let _lastInputSource = "unknown"; // "paste" | "typed" | "unknown"
|
|
60
|
+
let _bpBuffer = ""; // accumulates raw stdin bytes
|
|
61
|
+
|
|
62
|
+
function _enableBracketedPaste() {
|
|
63
|
+
if (!process.stdin.isTTY) return;
|
|
64
|
+
try {
|
|
65
|
+
process.stdout.write(BP_ENABLE);
|
|
66
|
+
_bracketedPasteSupported = true;
|
|
67
|
+
|
|
68
|
+
if (process.stdin.setRawMode) process.stdin.setRawMode(true);
|
|
69
|
+
process.stdin.resume();
|
|
70
|
+
process.stdin.setEncoding("utf8");
|
|
71
|
+
|
|
72
|
+
process.stdin.on("data", (chunk) => {
|
|
73
|
+
_bpBuffer += chunk;
|
|
74
|
+
|
|
75
|
+
// Consume all complete bracketed-paste sequences
|
|
76
|
+
let start;
|
|
77
|
+
while ((start = _bpBuffer.indexOf(PASTE_START)) !== -1) {
|
|
78
|
+
const end = _bpBuffer.indexOf(PASTE_END, start + PASTE_START.length);
|
|
79
|
+
if (end === -1) break; // incomplete sequence — wait for more data
|
|
80
|
+
// Everything between the markers is pasted text
|
|
81
|
+
_lastInputSource = "paste";
|
|
82
|
+
_bpBuffer = _bpBuffer.slice(0, start) + _bpBuffer.slice(end + PASTE_END.length);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Anything left in the buffer (outside markers) is typed
|
|
86
|
+
if (_bpBuffer.length > 0 && !_bpBuffer.includes(PASTE_START)) {
|
|
87
|
+
_lastInputSource = "typed";
|
|
88
|
+
_bpBuffer = "";
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Restore terminal on exit
|
|
93
|
+
const _restore = () => {
|
|
94
|
+
try { process.stdout.write(BP_DISABLE); } catch {}
|
|
95
|
+
};
|
|
96
|
+
process.once("exit", _restore);
|
|
97
|
+
process.once("SIGINT", () => { _restore(); process.exit(130); });
|
|
98
|
+
process.once("SIGTERM", () => { _restore(); process.exit(143); });
|
|
99
|
+
} catch {
|
|
100
|
+
// Non-TTY or terminal doesn't support bracketed paste — fall through to
|
|
101
|
+
// the content-fingerprint fallback in detectSource().
|
|
102
|
+
_bracketedPasteSupported = false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Attempt to enable bracketed paste immediately.
|
|
107
|
+
_enableBracketedPaste();
|
|
108
|
+
|
|
109
|
+
// ── Internal state ───────────────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
const _history = []; // { text, timestamp, source }
|
|
112
|
+
const _MAX_HIST = 100;
|
|
113
|
+
let _lastWrite = null; // timestamp of last kitClipboard.write() call
|
|
114
|
+
let _lastWriteContent = null; // exact string written — used by content-fingerprint fallback
|
|
115
|
+
let _watchTimer = null;
|
|
116
|
+
let _watchLast = null;
|
|
117
|
+
|
|
118
|
+
function _pushHistory(text, source = "external") {
|
|
119
|
+
_history.unshift({ text, timestamp: Date.now(), source });
|
|
120
|
+
if (_history.length > _MAX_HIST) _history.pop();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ── Low-level read/write ─────────────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
function _read() {
|
|
126
|
+
const plat = _platform();
|
|
127
|
+
switch (plat) {
|
|
128
|
+
case "macos":
|
|
129
|
+
return execSync("pbpaste", { encoding: "utf8" });
|
|
130
|
+
|
|
131
|
+
case "windows":
|
|
132
|
+
// PowerShell gives us Unicode-safe clipboard access
|
|
133
|
+
return execSync(
|
|
134
|
+
`powershell -NoProfile -NonInteractive -Command "Get-Clipboard"`,
|
|
135
|
+
{ encoding: "utf8" }
|
|
136
|
+
).replace(/\r\n/g, "\n");
|
|
137
|
+
|
|
138
|
+
case "android":
|
|
139
|
+
return execSync("termux-clipboard-get", { encoding: "utf8" });
|
|
140
|
+
|
|
141
|
+
case "wayland": {
|
|
142
|
+
const backend = _linuxBackend();
|
|
143
|
+
if (backend === "wayland") return execSync("wl-paste --no-newline", { encoding: "utf8" });
|
|
144
|
+
// fallthrough to x11 backends
|
|
145
|
+
}
|
|
146
|
+
// eslint-disable-next-line no-fallthrough
|
|
147
|
+
case "x11":
|
|
148
|
+
case "linux": {
|
|
149
|
+
const backend = _linuxBackend();
|
|
150
|
+
if (backend === "xclip") return execSync("xclip -selection clipboard -o", { encoding: "utf8" });
|
|
151
|
+
if (backend === "xsel") return execSync("xsel --clipboard --output", { encoding: "utf8" });
|
|
152
|
+
if (backend === "wayland") return execSync("wl-paste --no-newline", { encoding: "utf8" });
|
|
153
|
+
throw new Error("No clipboard backend available.");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
default:
|
|
157
|
+
throw new Error(`Unsupported platform: ${plat}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function _write(text) {
|
|
162
|
+
const plat = _platform();
|
|
163
|
+
switch (plat) {
|
|
164
|
+
case "macos":
|
|
165
|
+
execSync("pbcopy", { input: text, stdio: ["pipe", "ignore", "ignore"] });
|
|
166
|
+
break;
|
|
167
|
+
|
|
168
|
+
case "windows":
|
|
169
|
+
// Use clip.exe for simple text; PowerShell for Unicode safety
|
|
170
|
+
execSync(
|
|
171
|
+
`powershell -NoProfile -NonInteractive -Command "Set-Clipboard -Value @'\n${text.replace(/'/g, "''")}\n'@"`,
|
|
172
|
+
{ stdio: "pipe" }
|
|
173
|
+
);
|
|
174
|
+
break;
|
|
175
|
+
|
|
176
|
+
case "android":
|
|
177
|
+
execSync(`termux-clipboard-set`, { input: text, stdio: ["pipe", "ignore", "ignore"] });
|
|
178
|
+
break;
|
|
179
|
+
|
|
180
|
+
case "wayland":
|
|
181
|
+
case "x11":
|
|
182
|
+
case "linux": {
|
|
183
|
+
const backend = _linuxBackend();
|
|
184
|
+
if (backend === "xclip")
|
|
185
|
+
execSync("xclip -selection clipboard", { input: text, stdio: ["pipe", "ignore", "ignore"] });
|
|
186
|
+
else if (backend === "xsel")
|
|
187
|
+
execSync("xsel --clipboard --input", { input: text, stdio: ["pipe", "ignore", "ignore"] });
|
|
188
|
+
else if (backend === "wayland")
|
|
189
|
+
execSync("wl-copy", { input: text, stdio: ["pipe", "ignore", "ignore"] });
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
default:
|
|
194
|
+
throw new Error(`Unsupported platform: ${plat}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ── Export ───────────────────────────────────────────────────────────────────
|
|
199
|
+
|
|
200
|
+
module.exports = {
|
|
201
|
+
kitdef: {
|
|
202
|
+
|
|
203
|
+
name: "kitClipboard",
|
|
204
|
+
version: "1.0.0",
|
|
205
|
+
description: "Cross-platform clipboard management: read, write, file copy, history, monitoring, format detection, and paste detection.",
|
|
206
|
+
|
|
207
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
208
|
+
// READ
|
|
209
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Read the current clipboard contents as a string.
|
|
213
|
+
* @returns {string}
|
|
214
|
+
*
|
|
215
|
+
* @example
|
|
216
|
+
* const text = kitClipboard.read();
|
|
217
|
+
*/
|
|
218
|
+
read() {
|
|
219
|
+
return _read();
|
|
220
|
+
},
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Read clipboard and return trimmed text.
|
|
224
|
+
* @returns {string}
|
|
225
|
+
*/
|
|
226
|
+
readTrimmed() {
|
|
227
|
+
return _read().trim();
|
|
228
|
+
},
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Read clipboard and parse as JSON.
|
|
232
|
+
* Throws if content is not valid JSON.
|
|
233
|
+
* @returns {any}
|
|
234
|
+
*/
|
|
235
|
+
readJSON() {
|
|
236
|
+
const text = _read().trim();
|
|
237
|
+
try { return JSON.parse(text); }
|
|
238
|
+
catch { throw new Error("Clipboard content is not valid JSON."); }
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Read clipboard and split into lines.
|
|
243
|
+
* @param {{ keepEmpty?: boolean }} [options]
|
|
244
|
+
* @returns {string[]}
|
|
245
|
+
*/
|
|
246
|
+
readLines(options = {}) {
|
|
247
|
+
const lines = _read().split("\n");
|
|
248
|
+
return options.keepEmpty ? lines : lines.filter(l => l.trim().length > 0);
|
|
249
|
+
},
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Read clipboard and return as a Buffer.
|
|
253
|
+
* @returns {Buffer}
|
|
254
|
+
*/
|
|
255
|
+
readBuffer() {
|
|
256
|
+
return Buffer.from(_read(), "utf8");
|
|
257
|
+
},
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Read clipboard and return as a base64 string.
|
|
261
|
+
* @returns {string}
|
|
262
|
+
*/
|
|
263
|
+
readBase64() {
|
|
264
|
+
return Buffer.from(_read(), "utf8").toString("base64");
|
|
265
|
+
},
|
|
266
|
+
|
|
267
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
268
|
+
// WRITE
|
|
269
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Write text to the clipboard.
|
|
273
|
+
* @param {string} text
|
|
274
|
+
* @returns {{ written: string, length: number }}
|
|
275
|
+
*
|
|
276
|
+
* @example
|
|
277
|
+
* kitClipboard.write("Hello, world!");
|
|
278
|
+
*/
|
|
279
|
+
write(text) {
|
|
280
|
+
const str = String(text);
|
|
281
|
+
_lastWrite = Date.now();
|
|
282
|
+
_lastWriteContent = str; // fingerprint fallback: remember exact content
|
|
283
|
+
_lastInputSource = "unknown"; // clear TTY source — clipboard is now ours
|
|
284
|
+
_write(str);
|
|
285
|
+
_pushHistory(str, "write");
|
|
286
|
+
return { written: str, length: str.length };
|
|
287
|
+
},
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Write JSON to the clipboard (pretty-printed).
|
|
291
|
+
* @param {any} value
|
|
292
|
+
* @param {number} [indent=2]
|
|
293
|
+
* @returns {{ written: string }}
|
|
294
|
+
*/
|
|
295
|
+
writeJSON(value, indent = 2) {
|
|
296
|
+
return this.write(JSON.stringify(value, null, indent));
|
|
297
|
+
},
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Append text to existing clipboard content.
|
|
301
|
+
* @param {string} text
|
|
302
|
+
* @param {string} [separator="\n"]
|
|
303
|
+
* @returns {{ written: string }}
|
|
304
|
+
*/
|
|
305
|
+
append(text, separator = "\n") {
|
|
306
|
+
const current = _read();
|
|
307
|
+
return this.write(current + separator + text);
|
|
308
|
+
},
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Prepend text to existing clipboard content.
|
|
312
|
+
* @param {string} text
|
|
313
|
+
* @param {string} [separator="\n"]
|
|
314
|
+
* @returns {{ written: string }}
|
|
315
|
+
*/
|
|
316
|
+
prepend(text, separator = "\n") {
|
|
317
|
+
const current = _read();
|
|
318
|
+
return this.write(text + separator + current);
|
|
319
|
+
},
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Clear the clipboard.
|
|
323
|
+
* @returns {{ status: string }}
|
|
324
|
+
*/
|
|
325
|
+
clear() {
|
|
326
|
+
_write("");
|
|
327
|
+
_lastWrite = Date.now();
|
|
328
|
+
return { status: "CLEARED" };
|
|
329
|
+
},
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Write from a Buffer or base64 string.
|
|
333
|
+
* @param {Buffer|string} data
|
|
334
|
+
* @returns {{ written: number }}
|
|
335
|
+
*/
|
|
336
|
+
writeBuffer(data) {
|
|
337
|
+
const str = Buffer.isBuffer(data)
|
|
338
|
+
? data.toString("utf8")
|
|
339
|
+
: Buffer.from(data, "base64").toString("utf8");
|
|
340
|
+
_write(str);
|
|
341
|
+
_lastWrite = Date.now();
|
|
342
|
+
_pushHistory(str, "writeBuffer");
|
|
343
|
+
return { written: str.length };
|
|
344
|
+
},
|
|
345
|
+
|
|
346
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
347
|
+
// FILE OPERATIONS
|
|
348
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Copy the contents of a file to the clipboard.
|
|
352
|
+
* @param {string} filePath
|
|
353
|
+
* @param {BufferEncoding} [encoding="utf8"]
|
|
354
|
+
* @returns {{ file: string, length: number }}
|
|
355
|
+
*
|
|
356
|
+
* @example
|
|
357
|
+
* kitClipboard.copyFile("./README.md");
|
|
358
|
+
*/
|
|
359
|
+
copyFile(filePath, encoding = "utf8") {
|
|
360
|
+
const resolved = filePath.replace(/^~/, os.homedir());
|
|
361
|
+
const content = fs.readFileSync(resolved, encoding);
|
|
362
|
+
this.write(content);
|
|
363
|
+
return { file: resolved, length: content.length };
|
|
364
|
+
},
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Save the current clipboard content to a file.
|
|
368
|
+
* @param {string} filePath
|
|
369
|
+
* @param {BufferEncoding} [encoding="utf8"]
|
|
370
|
+
* @returns {{ file: string, length: number }}
|
|
371
|
+
*
|
|
372
|
+
* @example
|
|
373
|
+
* kitClipboard.saveToFile("./output.txt");
|
|
374
|
+
*/
|
|
375
|
+
saveToFile(filePath, encoding = "utf8") {
|
|
376
|
+
const resolved = filePath.replace(/^~/, os.homedir());
|
|
377
|
+
const content = _read();
|
|
378
|
+
fs.writeFileSync(resolved, content, encoding);
|
|
379
|
+
return { file: resolved, length: content.length };
|
|
380
|
+
},
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Append the current clipboard content to a file.
|
|
384
|
+
* @param {string} filePath
|
|
385
|
+
* @returns {{ file: string, appended: number }}
|
|
386
|
+
*/
|
|
387
|
+
appendToFile(filePath) {
|
|
388
|
+
const resolved = filePath.replace(/^~/, os.homedir());
|
|
389
|
+
const content = _read();
|
|
390
|
+
fs.appendFileSync(resolved, content, "utf8");
|
|
391
|
+
return { file: resolved, appended: content.length };
|
|
392
|
+
},
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Copy multiple files' contents to clipboard, joined by a separator.
|
|
396
|
+
* @param {string[]} filePaths
|
|
397
|
+
* @param {string} [separator="\n---\n"]
|
|
398
|
+
* @returns {{ files: string[], totalLength: number }}
|
|
399
|
+
*/
|
|
400
|
+
copyFiles(filePaths, separator = "\n---\n") {
|
|
401
|
+
const contents = filePaths.map(f => {
|
|
402
|
+
const resolved = f.replace(/^~/, os.homedir());
|
|
403
|
+
return `// ${path.basename(resolved)}\n` + fs.readFileSync(resolved, "utf8");
|
|
404
|
+
});
|
|
405
|
+
const joined = contents.join(separator);
|
|
406
|
+
this.write(joined);
|
|
407
|
+
return { files: filePaths, totalLength: joined.length };
|
|
408
|
+
},
|
|
409
|
+
|
|
410
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
411
|
+
// PASTE DETECTION
|
|
412
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Detect whether the current clipboard content was written programmatically
|
|
416
|
+
* (via kitClipboard.write()) or pasted/typed by a human.
|
|
417
|
+
*
|
|
418
|
+
* Three-tier detection strategy (most → least reliable):
|
|
419
|
+
*
|
|
420
|
+
* TIER 1 — Terminal bracketed paste mode (exact, ~100%)
|
|
421
|
+
* If the process is running in a TTY that supports DEC mode 2004
|
|
422
|
+
* (\x1b[?2004h), pasted text arrives wrapped between \x1b[200~ and
|
|
423
|
+
* \x1b[201~. The stdin listener set up by _enableBracketedPaste()
|
|
424
|
+
* records whether the last input event was a paste or a keypress and
|
|
425
|
+
* stores it in _lastInputSource. When this information is available
|
|
426
|
+
* we trust it unconditionally — it comes straight from the terminal.
|
|
427
|
+
*
|
|
428
|
+
* TIER 2 — Content fingerprint (exact, ~100%)
|
|
429
|
+
* Every kitClipboard.write() call stores the exact string it wrote in
|
|
430
|
+
* _lastWriteContent. We read the clipboard right now and compare. If
|
|
431
|
+
* the live content === _lastWriteContent we know it is still our write.
|
|
432
|
+
* If it differs, something external changed it (human paste, another
|
|
433
|
+
* program). This is purely observational — no timing, no races.
|
|
434
|
+
* It fails only in the astronomically unlikely event that a human
|
|
435
|
+
* happens to paste the exact same string we last wrote (still detected
|
|
436
|
+
* as "programmatic" — an acceptable false-negative).
|
|
437
|
+
*
|
|
438
|
+
* TIER 3 — Timing heuristic (fallback, ~95%)
|
|
439
|
+
* If we have never written anything (_lastWrite === null) we return
|
|
440
|
+
* "unknown". Otherwise we use the age of the last write: if the last
|
|
441
|
+
* write happened very recently (< 200 ms) we call it "programmatic";
|
|
442
|
+
* otherwise "typed". This is the weakest tier and is only reached
|
|
443
|
+
* when _lastWriteContent is unavailable (e.g. after a process restart
|
|
444
|
+
* that re-uses an existing clipboard value).
|
|
445
|
+
*
|
|
446
|
+
* @returns {{
|
|
447
|
+
* source: 'programmatic'|'typed'|'unknown',
|
|
448
|
+
* method: 'bracketed-paste'|'content-fingerprint'|'timing-heuristic'|'no-data',
|
|
449
|
+
* lastWriteMs: number|null,
|
|
450
|
+
* ageMs: number|null,
|
|
451
|
+
* }}
|
|
452
|
+
*/
|
|
453
|
+
detectSource() {
|
|
454
|
+
// ── Tier 1: bracketed paste ──────────────────────────────────────────
|
|
455
|
+
if (_bracketedPasteSupported && _lastInputSource !== "unknown") {
|
|
456
|
+
return {
|
|
457
|
+
source: _lastInputSource === "paste" ? "typed" : "programmatic",
|
|
458
|
+
method: "bracketed-paste",
|
|
459
|
+
lastWriteMs: _lastWrite,
|
|
460
|
+
ageMs: _lastWrite !== null ? Date.now() - _lastWrite : null,
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// ── Tier 2: content fingerprint ─────────────────────────────────────
|
|
465
|
+
// Read the live clipboard and compare to what we last wrote.
|
|
466
|
+
if (_lastWriteContent !== null) {
|
|
467
|
+
let liveContent;
|
|
468
|
+
try { liveContent = _read(); } catch { liveContent = null; }
|
|
469
|
+
|
|
470
|
+
if (liveContent !== null) {
|
|
471
|
+
const isOurs = liveContent === _lastWriteContent;
|
|
472
|
+
return {
|
|
473
|
+
source: isOurs ? "programmatic" : "typed",
|
|
474
|
+
method: "content-fingerprint",
|
|
475
|
+
lastWriteMs: _lastWrite,
|
|
476
|
+
ageMs: _lastWrite !== null ? Date.now() - _lastWrite : null,
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// ── Tier 3: timing heuristic (last resort) ───────────────────────────
|
|
482
|
+
if (_lastWrite === null) {
|
|
483
|
+
return { source: "unknown", method: "no-data", lastWriteMs: null, ageMs: null };
|
|
484
|
+
}
|
|
485
|
+
const age = Date.now() - _lastWrite;
|
|
486
|
+
return {
|
|
487
|
+
source: age < 200 ? "programmatic" : "typed",
|
|
488
|
+
method: "timing-heuristic",
|
|
489
|
+
lastWriteMs: _lastWrite,
|
|
490
|
+
ageMs: age,
|
|
491
|
+
};
|
|
492
|
+
},
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Returns true if the clipboard was most recently written by a human
|
|
496
|
+
* (pasted or typed), not by kitClipboard.write().
|
|
497
|
+
* @returns {boolean}
|
|
498
|
+
*/
|
|
499
|
+
wasPasted() {
|
|
500
|
+
return this.detectSource().source !== "programmatic";
|
|
501
|
+
},
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Returns true if the clipboard was most recently written programmatically.
|
|
505
|
+
* @returns {boolean}
|
|
506
|
+
*/
|
|
507
|
+
wasProgrammatic() {
|
|
508
|
+
return this.detectSource().source === "programmatic";
|
|
509
|
+
},
|
|
510
|
+
|
|
511
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
512
|
+
// FORMAT DETECTION
|
|
513
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Detect the format/type of the current clipboard content.
|
|
517
|
+
* @returns {{ format: string, confidence: 'high'|'medium'|'low', details: object }}
|
|
518
|
+
*
|
|
519
|
+
* Possible formats: "json", "url", "email", "html", "csv", "code",
|
|
520
|
+
* "markdown", "base64", "hex", "uuid", "number", "date", "empty", "text"
|
|
521
|
+
*/
|
|
522
|
+
detectFormat() {
|
|
523
|
+
const text = _read().trim();
|
|
524
|
+
|
|
525
|
+
if (!text) return { format: "empty", confidence: "high", details: {} };
|
|
526
|
+
|
|
527
|
+
// JSON
|
|
528
|
+
try {
|
|
529
|
+
const parsed = JSON.parse(text);
|
|
530
|
+
return { format: "json", confidence: "high", details: { type: typeof parsed, isArray: Array.isArray(parsed) } };
|
|
531
|
+
} catch {}
|
|
532
|
+
|
|
533
|
+
// UUID
|
|
534
|
+
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(text)) {
|
|
535
|
+
return { format: "uuid", confidence: "high", details: { version: parseInt(text[14]) || null } };
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// URL
|
|
539
|
+
try {
|
|
540
|
+
const url = new URL(text);
|
|
541
|
+
return { format: "url", confidence: "high", details: { protocol: url.protocol, host: url.host } };
|
|
542
|
+
} catch {}
|
|
543
|
+
|
|
544
|
+
// Email
|
|
545
|
+
if (/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(text)) {
|
|
546
|
+
return { format: "email", confidence: "high", details: {} };
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Hex color
|
|
550
|
+
if (/^#([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test(text)) {
|
|
551
|
+
return { format: "hex-color", confidence: "high", details: { value: text } };
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Pure hex string
|
|
555
|
+
if (/^[0-9a-f]+$/i.test(text) && text.length % 2 === 0 && text.length >= 8) {
|
|
556
|
+
return { format: "hex", confidence: "medium", details: { bytes: text.length / 2 } };
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Base64
|
|
560
|
+
if (/^[A-Za-z0-9+/]+=*$/.test(text) && text.length % 4 === 0 && text.length >= 16) {
|
|
561
|
+
return { format: "base64", confidence: "medium", details: { decodedLength: Math.floor(text.length * 0.75) } };
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Number
|
|
565
|
+
if (!isNaN(Number(text)) && text.length > 0) {
|
|
566
|
+
return { format: "number", confidence: "high", details: { value: Number(text) } };
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Date
|
|
570
|
+
const d = new Date(text);
|
|
571
|
+
if (!isNaN(d.getTime()) && text.length > 5) {
|
|
572
|
+
return { format: "date", confidence: "medium", details: { iso: d.toISOString() } };
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// HTML
|
|
576
|
+
if (/<[a-z][\s\S]*>/i.test(text)) {
|
|
577
|
+
return { format: "html", confidence: "medium", details: { tags: (text.match(/<[a-z][a-z0-9]*/gi) || []).slice(0, 5) } };
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// CSV (has commas and newlines or consistent comma count per line)
|
|
581
|
+
const lines = text.split("\n").filter(Boolean);
|
|
582
|
+
if (lines.length > 1) {
|
|
583
|
+
const commasPerLine = lines.map(l => (l.match(/,/g) || []).length);
|
|
584
|
+
const consistent = commasPerLine.every(c => c === commasPerLine[0] && c > 0);
|
|
585
|
+
if (consistent) return { format: "csv", confidence: "medium", details: { rows: lines.length, cols: commasPerLine[0] + 1 } };
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// Markdown (has #, **, __, -, ```)
|
|
589
|
+
if (/^#{1,6} |^\*\*|^__|^- |\`\`\`/m.test(text)) {
|
|
590
|
+
return { format: "markdown", confidence: "medium", details: {} };
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Code heuristic (has braces, semicolons, keywords)
|
|
594
|
+
const codeScore = [
|
|
595
|
+
/[{};]/.test(text),
|
|
596
|
+
/\b(function|const|let|var|if|for|while|return|import|export|class|def|fn|pub)\b/.test(text),
|
|
597
|
+
/\/\/|\/\*|#/.test(text),
|
|
598
|
+
].filter(Boolean).length;
|
|
599
|
+
if (codeScore >= 2) {
|
|
600
|
+
return { format: "code", confidence: "medium", details: { codeScore } };
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
return { format: "text", confidence: "high", details: { length: text.length, lines: lines.length } };
|
|
604
|
+
},
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Check if the clipboard contains a specific format.
|
|
608
|
+
* @param {'json'|'url'|'email'|'html'|'csv'|'code'|'markdown'|'base64'|'hex'|'uuid'|'number'|'date'|'empty'|'text'} format
|
|
609
|
+
* @returns {boolean}
|
|
610
|
+
*/
|
|
611
|
+
isFormat(format) {
|
|
612
|
+
return this.detectFormat().format === format;
|
|
613
|
+
},
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Check if the clipboard is empty.
|
|
617
|
+
* @returns {boolean}
|
|
618
|
+
*/
|
|
619
|
+
isEmpty() {
|
|
620
|
+
return _read().trim().length === 0;
|
|
621
|
+
},
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* Get the byte size of the current clipboard content.
|
|
625
|
+
* @returns {number}
|
|
626
|
+
*/
|
|
627
|
+
size() {
|
|
628
|
+
return Buffer.byteLength(_read(), "utf8");
|
|
629
|
+
},
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Get the character count of the current clipboard content.
|
|
633
|
+
* @returns {number}
|
|
634
|
+
*/
|
|
635
|
+
length() {
|
|
636
|
+
return _read().length;
|
|
637
|
+
},
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Get the line count of the current clipboard content.
|
|
641
|
+
* @returns {number}
|
|
642
|
+
*/
|
|
643
|
+
lineCount() {
|
|
644
|
+
return _read().split("\n").length;
|
|
645
|
+
},
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* Get the word count of the current clipboard content.
|
|
649
|
+
* @returns {number}
|
|
650
|
+
*/
|
|
651
|
+
wordCount() {
|
|
652
|
+
return _read().trim().split(/\s+/).filter(Boolean).length;
|
|
653
|
+
},
|
|
654
|
+
|
|
655
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
656
|
+
// TRANSFORM
|
|
657
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* Transform the clipboard content with a function and write it back.
|
|
661
|
+
* @param {function} fn - (text: string) => string
|
|
662
|
+
* @returns {{ before: string, after: string }}
|
|
663
|
+
*
|
|
664
|
+
* @example
|
|
665
|
+
* kitClipboard.transform(text => text.toUpperCase());
|
|
666
|
+
*/
|
|
667
|
+
transform(fn) {
|
|
668
|
+
const before = _read();
|
|
669
|
+
const after = fn(before);
|
|
670
|
+
this.write(after);
|
|
671
|
+
return { before, after };
|
|
672
|
+
},
|
|
673
|
+
|
|
674
|
+
/** Uppercase the clipboard content. */
|
|
675
|
+
toUpperCase() { return this.transform(t => t.toUpperCase()); },
|
|
676
|
+
|
|
677
|
+
/** Lowercase the clipboard content. */
|
|
678
|
+
toLowerCase() { return this.transform(t => t.toLowerCase()); },
|
|
679
|
+
|
|
680
|
+
/** Trim whitespace from clipboard content. */
|
|
681
|
+
trim() { return this.transform(t => t.trim()); },
|
|
682
|
+
|
|
683
|
+
/** Reverse the clipboard content. */
|
|
684
|
+
reverse() { return this.transform(t => t.split("").reverse().join("")); },
|
|
685
|
+
|
|
686
|
+
/** Sort lines in the clipboard alphabetically. */
|
|
687
|
+
sortLines() { return this.transform(t => t.split("\n").sort().join("\n")); },
|
|
688
|
+
|
|
689
|
+
/** Deduplicate lines in the clipboard. */
|
|
690
|
+
dedupeLines() { return this.transform(t => [...new Set(t.split("\n"))].join("\n")); },
|
|
691
|
+
|
|
692
|
+
/** URL-encode the clipboard content. */
|
|
693
|
+
urlEncode() { return this.transform(t => encodeURIComponent(t)); },
|
|
694
|
+
|
|
695
|
+
/** URL-decode the clipboard content. */
|
|
696
|
+
urlDecode() { return this.transform(t => decodeURIComponent(t)); },
|
|
697
|
+
|
|
698
|
+
/** Base64-encode the clipboard content. */
|
|
699
|
+
base64Encode() { return this.transform(t => Buffer.from(t).toString("base64")); },
|
|
700
|
+
|
|
701
|
+
/** Base64-decode the clipboard content. */
|
|
702
|
+
base64Decode() { return this.transform(t => Buffer.from(t, "base64").toString("utf8")); },
|
|
703
|
+
|
|
704
|
+
/** Strip all HTML tags from the clipboard content. */
|
|
705
|
+
stripHTML() { return this.transform(t => t.replace(/<[^>]+>/g, "")); },
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* Find and replace in clipboard content.
|
|
709
|
+
* @param {string|RegExp} search
|
|
710
|
+
* @param {string} replace
|
|
711
|
+
* @returns {{ before, after }}
|
|
712
|
+
*/
|
|
713
|
+
replace(search, replace) {
|
|
714
|
+
return this.transform(t => t.replace(search, replace));
|
|
715
|
+
},
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* Find and replace all occurrences in clipboard content.
|
|
719
|
+
* @param {string|RegExp} search
|
|
720
|
+
* @param {string} replace
|
|
721
|
+
*/
|
|
722
|
+
replaceAll(search, replace) {
|
|
723
|
+
return this.transform(t => t.replaceAll(search, replace));
|
|
724
|
+
},
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* Wrap clipboard content in a string on both sides.
|
|
728
|
+
* @param {string} prefix
|
|
729
|
+
* @param {string} [suffix] - Defaults to prefix.
|
|
730
|
+
*/
|
|
731
|
+
wrap(prefix, suffix) {
|
|
732
|
+
return this.transform(t => `${prefix}${t}${suffix ?? prefix}`);
|
|
733
|
+
},
|
|
734
|
+
|
|
735
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
736
|
+
// HISTORY
|
|
737
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* Get the clipboard history (most recent first).
|
|
741
|
+
* History is tracked for writes made through kitClipboard.write() only.
|
|
742
|
+
* @param {number} [limit] - Max entries to return.
|
|
743
|
+
* @returns {Array<{ text, timestamp, source }>}
|
|
744
|
+
*/
|
|
745
|
+
history(limit) {
|
|
746
|
+
return limit ? _history.slice(0, limit) : _history.slice();
|
|
747
|
+
},
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* Get a specific history entry by index (0 = most recent).
|
|
751
|
+
* @param {number} index
|
|
752
|
+
* @returns {{ text, timestamp, source } | undefined}
|
|
753
|
+
*/
|
|
754
|
+
historyAt(index) {
|
|
755
|
+
return _history[index];
|
|
756
|
+
},
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* Restore a previous clipboard entry by index.
|
|
760
|
+
* @param {number} index
|
|
761
|
+
* @returns {{ restored: string }}
|
|
762
|
+
*/
|
|
763
|
+
restore(index) {
|
|
764
|
+
const entry = _history[index];
|
|
765
|
+
if (!entry) throw new Error(`No history entry at index ${index}.`);
|
|
766
|
+
this.write(entry.text);
|
|
767
|
+
return { restored: entry.text };
|
|
768
|
+
},
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* Clear the clipboard history.
|
|
772
|
+
* @returns {{ cleared: number }}
|
|
773
|
+
*/
|
|
774
|
+
clearHistory() {
|
|
775
|
+
const count = _history.length;
|
|
776
|
+
_history.length = 0;
|
|
777
|
+
return { cleared: count };
|
|
778
|
+
},
|
|
779
|
+
|
|
780
|
+
/**
|
|
781
|
+
* Search history for entries matching a string or regex.
|
|
782
|
+
* @param {string|RegExp} query
|
|
783
|
+
* @returns {Array<{ index, text, timestamp, source }>}
|
|
784
|
+
*/
|
|
785
|
+
searchHistory(query) {
|
|
786
|
+
return _history
|
|
787
|
+
.map((entry, index) => ({ index, ...entry }))
|
|
788
|
+
.filter(e => typeof query === "string"
|
|
789
|
+
? e.text.includes(query)
|
|
790
|
+
: query.test(e.text));
|
|
791
|
+
},
|
|
792
|
+
|
|
793
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
794
|
+
// MONITORING / WATCHING
|
|
795
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
796
|
+
|
|
797
|
+
/**
|
|
798
|
+
* Watch the clipboard for changes and call a callback when it changes.
|
|
799
|
+
* @param {function} onChange - Called with { text, previous, timestamp, format }
|
|
800
|
+
* @param {number} [intervalMs=500] - Polling interval.
|
|
801
|
+
* @returns {{ stop: function }}
|
|
802
|
+
*
|
|
803
|
+
* @example
|
|
804
|
+
* const watcher = kitClipboard.watch(({ text }) => console.log("Clipboard:", text));
|
|
805
|
+
* // later:
|
|
806
|
+
* watcher.stop();
|
|
807
|
+
*/
|
|
808
|
+
watch(onChange, intervalMs = 500) {
|
|
809
|
+
if (_watchTimer) clearInterval(_watchTimer);
|
|
810
|
+
_watchLast = _read();
|
|
811
|
+
|
|
812
|
+
_watchTimer = setInterval(() => {
|
|
813
|
+
try {
|
|
814
|
+
const current = _read();
|
|
815
|
+
if (current !== _watchLast) {
|
|
816
|
+
const previous = _watchLast;
|
|
817
|
+
_watchLast = current;
|
|
818
|
+
_pushHistory(current, "external");
|
|
819
|
+
onChange({
|
|
820
|
+
text: current,
|
|
821
|
+
previous,
|
|
822
|
+
timestamp: Date.now(),
|
|
823
|
+
format: this.detectFormat().format,
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
} catch { /* clipboard may be temporarily unavailable */ }
|
|
827
|
+
}, intervalMs);
|
|
828
|
+
|
|
829
|
+
return { stop: () => { clearInterval(_watchTimer); _watchTimer = null; } };
|
|
830
|
+
},
|
|
831
|
+
|
|
832
|
+
/**
|
|
833
|
+
* Wait for the clipboard to change and resolve with the new value.
|
|
834
|
+
* @param {number} [timeoutMs=30000]
|
|
835
|
+
* @returns {Promise<{ text, previous, format }>}
|
|
836
|
+
*/
|
|
837
|
+
waitForChange(timeoutMs = 30000) {
|
|
838
|
+
return new Promise((resolve, reject) => {
|
|
839
|
+
const initial = _read();
|
|
840
|
+
const start = Date.now();
|
|
841
|
+
|
|
842
|
+
const timer = setInterval(() => {
|
|
843
|
+
if (Date.now() - start > timeoutMs) {
|
|
844
|
+
clearInterval(timer);
|
|
845
|
+
reject(new Error("Clipboard watch timed out."));
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
try {
|
|
849
|
+
const current = _read();
|
|
850
|
+
if (current !== initial) {
|
|
851
|
+
clearInterval(timer);
|
|
852
|
+
resolve({ text: current, previous: initial, format: this.detectFormat().format });
|
|
853
|
+
}
|
|
854
|
+
} catch {}
|
|
855
|
+
}, 200);
|
|
856
|
+
});
|
|
857
|
+
},
|
|
858
|
+
|
|
859
|
+
/**
|
|
860
|
+
* Stop any active clipboard watcher.
|
|
861
|
+
* @returns {{ stopped: boolean }}
|
|
862
|
+
*/
|
|
863
|
+
stopWatch() {
|
|
864
|
+
if (_watchTimer) { clearInterval(_watchTimer); _watchTimer = null; return { stopped: true }; }
|
|
865
|
+
return { stopped: false };
|
|
866
|
+
},
|
|
867
|
+
|
|
868
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
869
|
+
// PLATFORM & DIAGNOSTICS
|
|
870
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
871
|
+
|
|
872
|
+
/**
|
|
873
|
+
* Get information about the current platform and clipboard backend.
|
|
874
|
+
* @returns {object}
|
|
875
|
+
*/
|
|
876
|
+
platformInfo() {
|
|
877
|
+
const plat = _platform();
|
|
878
|
+
let backend = "native";
|
|
879
|
+
if (["x11", "wayland", "linux"].includes(plat)) {
|
|
880
|
+
try { backend = _linuxBackend(); } catch { backend = "unavailable"; }
|
|
881
|
+
}
|
|
882
|
+
return {
|
|
883
|
+
platform: plat,
|
|
884
|
+
backend,
|
|
885
|
+
nodeVersion: process.version,
|
|
886
|
+
supported: ["macos", "windows", "android", "x11", "wayland", "linux"].includes(plat),
|
|
887
|
+
};
|
|
888
|
+
},
|
|
889
|
+
|
|
890
|
+
/**
|
|
891
|
+
* Check if the clipboard is accessible (backend available).
|
|
892
|
+
* @returns {boolean}
|
|
893
|
+
*/
|
|
894
|
+
isAvailable() {
|
|
895
|
+
try { _read(); return true; }
|
|
896
|
+
catch { return false; }
|
|
897
|
+
},
|
|
898
|
+
|
|
899
|
+
/**
|
|
900
|
+
* Run a full diagnostic on the clipboard system.
|
|
901
|
+
* @returns {object}
|
|
902
|
+
*/
|
|
903
|
+
diagnose() {
|
|
904
|
+
const info = this.platformInfo();
|
|
905
|
+
let canRead = false;
|
|
906
|
+
let canWrite = false;
|
|
907
|
+
let readValue = null;
|
|
908
|
+
|
|
909
|
+
try { readValue = _read(); canRead = true; } catch {}
|
|
910
|
+
try { _write("kitClipboard-diagnostic-test"); canWrite = true; } catch {}
|
|
911
|
+
// Restore
|
|
912
|
+
if (readValue !== null) try { _write(readValue); } catch {}
|
|
913
|
+
|
|
914
|
+
return {
|
|
915
|
+
...info,
|
|
916
|
+
canRead,
|
|
917
|
+
canWrite,
|
|
918
|
+
historyEntries: _history.length,
|
|
919
|
+
watcherActive: _watchTimer !== null,
|
|
920
|
+
lastWriteMs: _lastWrite,
|
|
921
|
+
};
|
|
922
|
+
},
|
|
923
|
+
|
|
924
|
+
}
|
|
925
|
+
};
|