tabby-clipsnag 0.0.0-stage → 1.0.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 +21 -0
- package/README.md +123 -2
- package/README_ru.md +129 -0
- package/dist/capture-buffer.d.ts +32 -0
- package/dist/capture.middleware.d.ts +22 -0
- package/dist/clipsnag.service.d.ts +56 -0
- package/dist/config.d.ts +39 -0
- package/dist/decorator.d.ts +43 -0
- package/dist/hotkeys.d.ts +6 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +1 -0
- package/dist/settings.d.ts +39 -0
- package/package.json +65 -6
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alexey Belov
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,124 @@
|
|
|
1
|
-
#
|
|
1
|
+
# tabby-clipsnag
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
English | [Русский](README_ru.md)
|
|
4
|
+
|
|
5
|
+
A [Tabby](https://tabby.sh) terminal plugin. Press a hotkey, let a noisy command run,
|
|
6
|
+
press it again — the plugin strips the ANSI control garbage and drops clean text on your
|
|
7
|
+
clipboard.
|
|
8
|
+
|
|
9
|
+
Think of it as "select all the output of the last command", but driven by the actual byte
|
|
10
|
+
stream (colors, cursor moves, TUI redraws and all) instead of the terminal's scrollback,
|
|
11
|
+
and it works the same whether the output scrolled off screen or not.
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
- **One toggle hotkey** (default `Ctrl-Shift-X`, `⌘-Shift-X` on macOS) to start and stop.
|
|
16
|
+
- **Passive capture** — installs a `SessionMiddleware` that observes bytes and forwards
|
|
17
|
+
them unchanged; your terminal renders exactly as before while recording.
|
|
18
|
+
- **Correct ANSI stripping** — the whole capture is decoded and stripped once on stop, so
|
|
19
|
+
colour codes, cursor movement, OSC titles, and hyperlinks never leak into the clipboard.
|
|
20
|
+
- **Bounded memory** — capture stops growing at a configurable cap (default **5 MB**),
|
|
21
|
+
keeping the earliest bytes and reporting truncation in the notification.
|
|
22
|
+
- **Auto-stop on idle** — optionally stop by itself after N seconds of silence (default 3 s).
|
|
23
|
+
- **Settings UI** — every option is editable under **Settings → Clipsnag**; nothing to hand-edit.
|
|
24
|
+
- **Leak-safe** — everything is torn down with the tab; closing a recorded tab cancels the
|
|
25
|
+
recording cleanly with no dangling subscriptions.
|
|
26
|
+
|
|
27
|
+
## Configuration
|
|
28
|
+
|
|
29
|
+
Everything is editable in **Settings → Clipsnag** (the hotkey lives under
|
|
30
|
+
**Settings → Hotkeys → Toggle terminal output snagging**). The same values are stored under
|
|
31
|
+
the `clipsnag` key in `~/.config/tabby/config.yaml` (`%APPDATA%\tabby\config.yaml` on Windows)
|
|
32
|
+
and can be hand-edited there, though the settings UI is the recommended way:
|
|
33
|
+
|
|
34
|
+
```yaml
|
|
35
|
+
hotkeys:
|
|
36
|
+
clipsnag-toggle: Ctrl-Shift-X
|
|
37
|
+
|
|
38
|
+
clipsnag:
|
|
39
|
+
maxCaptureBytes: 5242880 # 5 MB hard cap; older bytes kept, rest discarded
|
|
40
|
+
autoStopAfterIdleMs: 3000 # stop after 3s of silence; 0 disables auto-stop
|
|
41
|
+
copyToClipboardOnStop: true # write the cleaned text to the OS clipboard
|
|
42
|
+
stopOnTabSwitch: false # stop + copy as soon as you switch away from the tab
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## How it works
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
hotkey ──▶ ClipsnagService.toggle()
|
|
49
|
+
│
|
|
50
|
+
├─ start: resolve active terminal tab
|
|
51
|
+
│ └─ TerminalDecorator.startRecording(tab, sink)
|
|
52
|
+
│ └─ CaptureMiddleware pushed into session.middleware
|
|
53
|
+
│ └─ feedFromSession() copies bytes into CaptureBuffer
|
|
54
|
+
│
|
|
55
|
+
└─ stop: CaptureBuffer.finish()
|
|
56
|
+
├─ Buffer.concat + utf8 decode
|
|
57
|
+
├─ stripAnsi(...)
|
|
58
|
+
└─ PlatformService.setClipboard({ text })
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
- `TerminalDecorator` supplies the per-tab lifecycle hook and guarantees cleanup with
|
|
62
|
+
`subscribeUntilDestroyed`.
|
|
63
|
+
- `SessionMiddleware` is the only supported way to see the raw session byte stream.
|
|
64
|
+
- The service owns the single active recording and subscribes to the toggle hotkey once.
|
|
65
|
+
|
|
66
|
+
Because bytes are accumulated (not decoded per chunk), multi-byte UTF-8 characters split
|
|
67
|
+
across PTY chunks survive intact, and ANSI sequences that straddle chunk boundaries are
|
|
68
|
+
stripped correctly.
|
|
69
|
+
|
|
70
|
+
## Building
|
|
71
|
+
|
|
72
|
+
Requires Node 18+.
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
npm install
|
|
76
|
+
npm run build # -> dist/index.js (+ .d.ts)
|
|
77
|
+
npm run watch # incremental rebuild while developing
|
|
78
|
+
npm run typecheck # tsc --noEmit
|
|
79
|
+
npm run lint # eslint
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Installing into Tabby for testing
|
|
83
|
+
|
|
84
|
+
Pick whichever is convenient:
|
|
85
|
+
|
|
86
|
+
### Option A — point Tabby at the folder (fastest for dev)
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
# Linux / macOS
|
|
90
|
+
tabby --plugin-dir=/absolute/path/to/tabby-clipsnag
|
|
91
|
+
|
|
92
|
+
# Windows (PowerShell)
|
|
93
|
+
tabby --plugin-dir=C:\Users\you\github\alexoffspace\tabby-clipsnag
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
`--plugin-dir` must point at the project root (the folder containing `package.json` and
|
|
97
|
+
`dist/`). Run `npm run build` first.
|
|
98
|
+
|
|
99
|
+
### Option B — drop it into the plugins directory
|
|
100
|
+
|
|
101
|
+
Copy `package.json` and the whole `dist/` folder into the per-user plugin directory:
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
Linux: ~/.config/tabby/plugins/node_modules/tabby-clipsnag/
|
|
105
|
+
macOS: ~/Library/Application Support/tabby/plugins/node_modules/tabby-clipsnag/
|
|
106
|
+
Windows: %APPDATA%\tabby\plugins\node_modules\tabby-clipsnag\
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Then restart Tabby.
|
|
110
|
+
|
|
111
|
+
### Verifying it loaded
|
|
112
|
+
|
|
113
|
+
Open **Settings → Plugins** — `tabby-clipsnag` should be listed. Then:
|
|
114
|
+
|
|
115
|
+
1. Focus a terminal tab and run `ls --color=always`.
|
|
116
|
+
2. Press `Ctrl-Shift-X` — a toast says *recording …*.
|
|
117
|
+
3. Press `Ctrl-Shift-X` again — a second toast reports the size; paste anywhere.
|
|
118
|
+
|
|
119
|
+
The first press arms capture, the second stops it. With `autoStopAfterIdleMs > 0` you can
|
|
120
|
+
also just wait: recording ends automatically once output goes quiet.
|
|
121
|
+
|
|
122
|
+
## License
|
|
123
|
+
|
|
124
|
+
MIT © Alexey Belov — see [LICENSE](LICENSE).
|
package/README_ru.md
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# tabby-clipsnag
|
|
2
|
+
|
|
3
|
+
[English](README.md) | Русский
|
|
4
|
+
|
|
5
|
+
Плагин для терминала [Tabby](https://tabby.sh). Нажмите горячую клавишу, дайте
|
|
6
|
+
«шумной» команде отработать, нажмите её снова — плагин вырежет ANSI-мусор управления и
|
|
7
|
+
положит в буфер обмена чистый текст.
|
|
8
|
+
|
|
9
|
+
Это как «выделить весь вывод последней команды», но работающее по реальному потоку
|
|
10
|
+
байтов (цвета, перемещение курсора, перерисовки TUI и всё остальное), а не по скроллбеку
|
|
11
|
+
терминала. Работает одинаково независимо от того, уехал вывод за пределы экрана или нет.
|
|
12
|
+
|
|
13
|
+
## Возможности
|
|
14
|
+
|
|
15
|
+
- **Одна горячая клавиша-переключатель** (по умолчанию `Ctrl-Shift-X`, `⌘-Shift-X` на
|
|
16
|
+
macOS) для запуска и остановки.
|
|
17
|
+
- **Пассивный захват** — устанавливает `SessionMiddleware`, который наблюдает байты и
|
|
18
|
+
передаёт их дальше без изменений; при записи терминал отображается точно как раньше.
|
|
19
|
+
- **Корректное удаление ANSI** — весь захват декодируется и очищается один раз при
|
|
20
|
+
остановке, поэтому цветовые коды, движение курсора, OSC-заголовки и гиперссылки никогда
|
|
21
|
+
не попадут в буфер обмена.
|
|
22
|
+
- **Ограниченная память** — захват перестаёт расти при достижимом лимите (по умолчанию
|
|
23
|
+
**5 МБ**), сохраняются самые ранние байты, а факт усечения сообщается в уведомлении.
|
|
24
|
+
- **Авто-остановка по простою** — опционально останавливается сам после N секунд тишины
|
|
25
|
+
(по умолчанию 3 с).
|
|
26
|
+
- **UI настроек** — все параметры редактируются в **Settings → Clipsnag**; править вручную
|
|
27
|
+
ничего не нужно.
|
|
28
|
+
- **Безопасно относительно утечек** — всё уничтожается вместе с вкладкой; закрытие
|
|
29
|
+
записываемой вкладки чисто отменяет запись без «висячих» подписок.
|
|
30
|
+
|
|
31
|
+
## Настройка
|
|
32
|
+
|
|
33
|
+
Всё редактируется в **Settings → Clipsnag** (а горячая клавиша — в
|
|
34
|
+
**Settings → Hotkeys → Toggle terminal output snagging**). Те же значения хранятся под
|
|
35
|
+
ключом `clipsnag` в `~/.config/tabby/config.yaml` (`%APPDATA%\tabby\config.yaml` на Windows)
|
|
36
|
+
и могут быть отредактированы вручную там же, хотя UI настроек — рекомендуемый способ:
|
|
37
|
+
|
|
38
|
+
```yaml
|
|
39
|
+
hotkeys:
|
|
40
|
+
clipsnag-toggle: Ctrl-Shift-X
|
|
41
|
+
|
|
42
|
+
clipsnag:
|
|
43
|
+
maxCaptureBytes: 5242880 # жёсткий лимит 5 МБ; старые байты сохраняются, остальное отбрасывается
|
|
44
|
+
autoStopAfterIdleMs: 3000 # остановка после 3с тишины; 0 отключает авто-остановку
|
|
45
|
+
copyToClipboardOnStop: true # записывать очищенный текст в буфер обмена ОС
|
|
46
|
+
stopOnTabSwitch: false # остановка и копирование при переключении на другую вкладку
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Как это работает
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
hotkey ──▶ ClipsnagService.toggle()
|
|
53
|
+
│
|
|
54
|
+
├─ start: resolve active terminal tab
|
|
55
|
+
│ └─ TerminalDecorator.startRecording(tab, sink)
|
|
56
|
+
│ └─ CaptureMiddleware pushed into session.middleware
|
|
57
|
+
│ └─ feedFromSession() copies bytes into CaptureBuffer
|
|
58
|
+
│
|
|
59
|
+
└─ stop: CaptureBuffer.finish()
|
|
60
|
+
├─ Buffer.concat + utf8 decode
|
|
61
|
+
├─ stripAnsi(...)
|
|
62
|
+
└─ PlatformService.setClipboard({ text })
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
- `TerminalDecorator` предоставляет хук жизненного цикла на вкладку и гарантирует очистку
|
|
66
|
+
через `subscribeUntilDestroyed`.
|
|
67
|
+
- `SessionMiddleware` — единственный поддерживаемый способ увидеть сырой поток байтов
|
|
68
|
+
сессии.
|
|
69
|
+
- Сервис владеет единственной активной записью и подписывается на горячую клавишу один раз.
|
|
70
|
+
|
|
71
|
+
Так как байты накапливаются (а не декодируются по чанкам), многобайтовые символы UTF-8,
|
|
72
|
+
разделённые между чанками PTY, остаются целыми, а ANSI-последовательности, пересекающие
|
|
73
|
+
границы чанков, удаляются корректно.
|
|
74
|
+
|
|
75
|
+
## Сборка
|
|
76
|
+
|
|
77
|
+
Требуется Node 18+.
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
npm install
|
|
81
|
+
npm run build # -> dist/index.js (+ .d.ts)
|
|
82
|
+
npm run watch # инкрементальная пересборка во время разработки
|
|
83
|
+
npm run typecheck # tsc --noEmit
|
|
84
|
+
npm run lint # eslint
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Установка в Tabby для тестирования
|
|
88
|
+
|
|
89
|
+
Выберите удобный вариант:
|
|
90
|
+
|
|
91
|
+
### Вариант A — указать Tabby на папку (быстрее всего для разработки)
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
# Linux / macOS
|
|
95
|
+
tabby --plugin-dir=/absolute/path/to/tabby-clipsnag
|
|
96
|
+
|
|
97
|
+
# Windows (PowerShell)
|
|
98
|
+
tabby --plugin-dir=C:\Users\you\github\alexoffspace\tabby-clipsnag
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`--plugin-dir` должен указывать на корень проекта (папку с `package.json` и `dist/`).
|
|
102
|
+
Сначала запустите `npm run build`.
|
|
103
|
+
|
|
104
|
+
### Вариант B — положить в каталог плагинов
|
|
105
|
+
|
|
106
|
+
Скопируйте `package.json` и всю папку `dist/` в пользовательский каталог плагинов:
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
Linux: ~/.config/tabby/plugins/node_modules/tabby-clipsnag/
|
|
110
|
+
macOS: ~/Library/Application Support/tabby/plugins/node_modules/tabby-clipsnag/
|
|
111
|
+
Windows: %APPDATA%\tabby\plugins\node_modules\tabby-clipsnag\
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Затем перезапустите Tabby.
|
|
115
|
+
|
|
116
|
+
### Проверка загрузки
|
|
117
|
+
|
|
118
|
+
Откройте **Settings → Plugins** — там должен быть указан `tabby-clipsnag`. Затем:
|
|
119
|
+
|
|
120
|
+
1. Сфокусируйте вкладку терминала и запустите `ls --color=always`.
|
|
121
|
+
2. Нажмите `Ctrl-Shift-X` — появится уведомление *recording …*.
|
|
122
|
+
3. Нажмите `Ctrl-Shift-X` снова — второе уведомление сообщит размер; вставьте куда угодно.
|
|
123
|
+
|
|
124
|
+
Первое нажатие взводит захват, второе — останавливает его. При `autoStopAfterIdleMs > 0`
|
|
125
|
+
можно также просто подождать: запись завершится автоматически, когда вывод затихнет.
|
|
126
|
+
|
|
127
|
+
## Лицензия
|
|
128
|
+
|
|
129
|
+
MIT © Алексей Белов — см. [LICENSE](LICENSE).
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
/// <reference types="node" />
|
|
3
|
+
/**
|
|
4
|
+
* One in-flight recording for a single terminal tab.
|
|
5
|
+
*
|
|
6
|
+
* Bytes are accumulated raw (never decoded per chunk) so that multi-byte UTF-8
|
|
7
|
+
* characters split across chunk boundaries survive. Decoding and ANSI stripping
|
|
8
|
+
* both happen exactly once, in `finish()`.
|
|
9
|
+
*/
|
|
10
|
+
export declare class CaptureBuffer {
|
|
11
|
+
private readonly maxBytes;
|
|
12
|
+
private readonly chunks;
|
|
13
|
+
private capturedBytes;
|
|
14
|
+
private truncated;
|
|
15
|
+
constructor(maxBytes: number);
|
|
16
|
+
/** Bytes retained so far (always <= maxBytes). */
|
|
17
|
+
get byteLength(): number;
|
|
18
|
+
/** True once the cap was hit and later chunks were dropped. */
|
|
19
|
+
get isTruncated(): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Append a chunk. Chunks past the cap are dropped and the buffer is marked
|
|
22
|
+
* truncated; we never exceed `maxBytes` of retained memory.
|
|
23
|
+
*/
|
|
24
|
+
append(data: Buffer): void;
|
|
25
|
+
/**
|
|
26
|
+
* Decode, strip ANSI control sequences, and normalise line endings.
|
|
27
|
+
* Callable once per recording; it does not clear the buffer.
|
|
28
|
+
*/
|
|
29
|
+
finish(): string;
|
|
30
|
+
/** Drop all retained data (used when a recording is abandoned). */
|
|
31
|
+
reset(): void;
|
|
32
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
/// <reference types="node" />
|
|
3
|
+
import { Buffer } from 'buffer';
|
|
4
|
+
import { SessionMiddleware } from 'tabby-terminal';
|
|
5
|
+
/**
|
|
6
|
+
* Passive tap on the session output stream.
|
|
7
|
+
*
|
|
8
|
+
* `feedFromSession` receives bytes travelling FROM the session TO the terminal
|
|
9
|
+
* (i.e. the program's stdout/stderr as rendered). We hand a copy to the sink and
|
|
10
|
+
* then ALWAYS forward the original bytes — capturing must never alter the stream.
|
|
11
|
+
*
|
|
12
|
+
* The middleware is a no-op when `sink` is `null`, so it can safely stay in the
|
|
13
|
+
* stack across record/stop cycles without re-wiring subscriptions.
|
|
14
|
+
*/
|
|
15
|
+
export declare class CaptureMiddleware extends SessionMiddleware {
|
|
16
|
+
private sink;
|
|
17
|
+
/** Begin forwarding copies of session output to `sink`. */
|
|
18
|
+
start(sink: (data: Buffer) => void): void;
|
|
19
|
+
/** Stop forwarding (the terminal keeps receiving data normally). */
|
|
20
|
+
stop(): void;
|
|
21
|
+
feedFromSession(data: Buffer): void;
|
|
22
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { NgZone } from '@angular/core';
|
|
2
|
+
import { AppService, ConfigService, HotkeysService, NotificationsService, PlatformService } from 'tabby-core';
|
|
3
|
+
import { ClipsnagDecorator } from './decorator';
|
|
4
|
+
/**
|
|
5
|
+
* Coordinates snag recording across tabs.
|
|
6
|
+
*
|
|
7
|
+
* At most one recording is active at a time. The service:
|
|
8
|
+
* - listens for the toggle hotkey,
|
|
9
|
+
* - starts/stops recording on the active terminal tab,
|
|
10
|
+
* - auto-stops after a configurable period of output silence,
|
|
11
|
+
* - strips ANSI and writes cleaned text to the OS clipboard,
|
|
12
|
+
* - reports state changes through Tabby notifications.
|
|
13
|
+
*
|
|
14
|
+
* Leak safety: every per-recording RxJS subscription is collected into a single
|
|
15
|
+
* `Subscription` torn down in `stopRecording()`/`abortRecording()`; the toggle
|
|
16
|
+
* hotkey subscription lives for the app lifetime, which is correct for a
|
|
17
|
+
* singleton service. If the recorded tab is destroyed or its session ends,
|
|
18
|
+
* recording is finalized without touching the dead tab.
|
|
19
|
+
*/
|
|
20
|
+
export declare class ClipsnagService {
|
|
21
|
+
private readonly config;
|
|
22
|
+
private readonly hotkeys;
|
|
23
|
+
private readonly notifications;
|
|
24
|
+
private readonly platform;
|
|
25
|
+
private readonly app;
|
|
26
|
+
private readonly zone;
|
|
27
|
+
private readonly decorator;
|
|
28
|
+
private active;
|
|
29
|
+
constructor(config: ConfigService, hotkeys: HotkeysService, notifications: NotificationsService, platform: PlatformService, app: AppService, zone: NgZone, decorator: ClipsnagDecorator);
|
|
30
|
+
/** App-lifetime subscription to the toggle hotkey. */
|
|
31
|
+
private hotkeySubscription;
|
|
32
|
+
/** Wire up the toggle hotkey. Called once from the plugin module constructor. */
|
|
33
|
+
init(): void;
|
|
34
|
+
/** True while a capture is in progress. */
|
|
35
|
+
get isRecording(): boolean;
|
|
36
|
+
/** Release app-lifetime resources. Called on plugin/app teardown. */
|
|
37
|
+
dispose(): void;
|
|
38
|
+
/** Toggle recording on the currently active terminal tab. */
|
|
39
|
+
toggle(): Promise<void>;
|
|
40
|
+
/** Begin capturing the active terminal tab's output. */
|
|
41
|
+
startRecording(): void;
|
|
42
|
+
/**
|
|
43
|
+
* Stop recording, strip ANSI, and copy to the clipboard.
|
|
44
|
+
* @param automatic true when triggered by the idle timer rather than the hotkey
|
|
45
|
+
*/
|
|
46
|
+
stopRecording(automatic?: boolean): Promise<void>;
|
|
47
|
+
/** Finalize a recording because its terminal went away; never touches the tab. */
|
|
48
|
+
private abortRecording;
|
|
49
|
+
/**
|
|
50
|
+
* The active tab may be a plain terminal or a `SplitTabComponent` wrapping
|
|
51
|
+
* several. In the split case we record the focused pane.
|
|
52
|
+
*/
|
|
53
|
+
private resolveActiveTerminal;
|
|
54
|
+
/** Read + validate plugin settings, falling back to defaults per-field. */
|
|
55
|
+
private settings;
|
|
56
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { ConfigProvider } from 'tabby-core';
|
|
2
|
+
/**
|
|
3
|
+
* Settings shape exposed under `config.store.clipsnag`.
|
|
4
|
+
* Declared explicitly (rather than left as `any`) so the service reads typed values.
|
|
5
|
+
*/
|
|
6
|
+
export interface ClipsnagConfig {
|
|
7
|
+
/** Hard cap on captured bytes before truncation kicks in. */
|
|
8
|
+
maxCaptureBytes: number;
|
|
9
|
+
/**
|
|
10
|
+
* Automatically stop recording after this many milliseconds of silence.
|
|
11
|
+
* `0` disables auto-stop (recording then ends only on a second hotkey press).
|
|
12
|
+
*/
|
|
13
|
+
autoStopAfterIdleMs: number;
|
|
14
|
+
/** Copy the cleaned capture to the system clipboard when recording stops. */
|
|
15
|
+
copyToClipboardOnStop: boolean;
|
|
16
|
+
/** Stop and copy the capture as soon as the user switches away from the tab. */
|
|
17
|
+
stopOnTabSwitch: boolean;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Single source of truth for default settings. Used both by the config provider
|
|
21
|
+
* (to seed `config.store`) and by the service (as a per-field validation fallback
|
|
22
|
+
* against malformed user config).
|
|
23
|
+
*/
|
|
24
|
+
export declare const DEFAULT_CLIPSNAG_CONFIG: ClipsnagConfig;
|
|
25
|
+
export declare class ClipsnagConfigProvider extends ConfigProvider {
|
|
26
|
+
defaults: {
|
|
27
|
+
hotkeys: {
|
|
28
|
+
'clipsnag-toggle': string[];
|
|
29
|
+
};
|
|
30
|
+
clipsnag: ClipsnagConfig;
|
|
31
|
+
};
|
|
32
|
+
platformDefaults: {
|
|
33
|
+
macOS: {
|
|
34
|
+
hotkeys: {
|
|
35
|
+
'clipsnag-toggle': string[];
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
/// <reference types="node" />
|
|
3
|
+
import { TerminalDecorator, BaseTerminalTabComponent, SessionMiddlewareStack } from 'tabby-terminal';
|
|
4
|
+
import { CaptureMiddleware } from './capture.middleware';
|
|
5
|
+
/**
|
|
6
|
+
* Per-tab recorder handle.
|
|
7
|
+
*
|
|
8
|
+
* Owns the CaptureMiddleware instance that is installed into the tab's current
|
|
9
|
+
* session stack. `dispose()` is idempotent so it is safe to call from both the
|
|
10
|
+
* "user stopped recording" and "tab/session vanished" paths.
|
|
11
|
+
*/
|
|
12
|
+
export declare class TabRecorder {
|
|
13
|
+
private readonly middleware;
|
|
14
|
+
private readonly stack;
|
|
15
|
+
private disposed;
|
|
16
|
+
constructor(middleware: CaptureMiddleware, stack: SessionMiddlewareStack, onData: (data: Buffer) => void);
|
|
17
|
+
/**
|
|
18
|
+
* Remove the tap from the session stack. We never call `close()` on the
|
|
19
|
+
* middleware: it holds Subjects and calling close() would complete them,
|
|
20
|
+
* making the instance unusable if it were ever reused.
|
|
21
|
+
*/
|
|
22
|
+
dispose(): void;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Attaches/detaches per-tab recording support.
|
|
26
|
+
*
|
|
27
|
+
* The decorator does two things:
|
|
28
|
+
* 1. Tracks the tab's *current* session so `startRecording` can find a stack to
|
|
29
|
+
* install into, updating it whenever the session is replaced (reconnectable
|
|
30
|
+
* sessions emit `sessionChanged$` more than once).
|
|
31
|
+
* 2. Guarantees cleanup with the tab's lifecycle via `subscribeUntilDestroyed`,
|
|
32
|
+
* so no map entry or subscription outlives the tab.
|
|
33
|
+
*/
|
|
34
|
+
export declare class ClipsnagDecorator extends TerminalDecorator {
|
|
35
|
+
private readonly sessionByTab;
|
|
36
|
+
attach(terminal: BaseTerminalTabComponent<never>): void;
|
|
37
|
+
detach(terminal: BaseTerminalTabComponent<never>): void;
|
|
38
|
+
/**
|
|
39
|
+
* Start forwarding this tab's output into `onData`.
|
|
40
|
+
* Returns `null` when the tab has no live session to tap.
|
|
41
|
+
*/
|
|
42
|
+
startRecording(terminal: BaseTerminalTabComponent<never>, onData: (data: Buffer) => void): TabRecorder | null;
|
|
43
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { HotkeyProvider, HotkeyDescription } from 'tabby-core';
|
|
2
|
+
/** Single toggle hotkey: first press arms capture, the next press stops and copies. */
|
|
3
|
+
export declare const CLIPSNAG_TOGGLE_HOTKEY = "clipsnag-toggle";
|
|
4
|
+
export declare class ClipsnagHotkeyProvider extends HotkeyProvider {
|
|
5
|
+
provide(): Promise<HotkeyDescription[]>;
|
|
6
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { ClipsnagService } from './clipsnag.service';
|
|
2
|
+
export { ClipsnagService } from './clipsnag.service';
|
|
3
|
+
export { ClipsnagDecorator } from './decorator';
|
|
4
|
+
export { ClipsnagConfigProvider, ClipsnagConfig, DEFAULT_CLIPSNAG_CONFIG } from './config';
|
|
5
|
+
export { ClipsnagHotkeyProvider, CLIPSNAG_TOGGLE_HOTKEY } from './hotkeys';
|
|
6
|
+
export { ClipsnagSettingsTabComponent, ClipsnagSettingsTabProvider } from './settings';
|
|
7
|
+
/**
|
|
8
|
+
* Plugin module.
|
|
9
|
+
*
|
|
10
|
+
* Registration points:
|
|
11
|
+
* - `HotkeyProvider` (multi) declares the toggle hotkey id so it appears in
|
|
12
|
+
* Settings → Hotkeys.
|
|
13
|
+
* - `ConfigProvider` (multi) seeds `config.store.hotkeys['clipsnag-toggle']`
|
|
14
|
+
* and `config.store.clipsnag`.
|
|
15
|
+
* - `TerminalDecorator` (multi) attaches the per-tab recording machinery.
|
|
16
|
+
* - `SettingsTabProvider` (multi) adds the "Clipsnag" section to Settings.
|
|
17
|
+
*
|
|
18
|
+
* The settings component must be declared here (not just provided) because Tabby
|
|
19
|
+
* instantiates it through `ComponentFactoryResolver`, which resolves directives
|
|
20
|
+
* and pipes from the declaring module's scope. `TabbyCoreModule` is imported so
|
|
21
|
+
* the component can use core building blocks such as `<toggle>`.
|
|
22
|
+
*
|
|
23
|
+
* The module constructor runs once at app start; that is where we subscribe the
|
|
24
|
+
* service to the hotkey stream. `ClipsnagDecorator` is injected by identity so the
|
|
25
|
+
* service talks to the exact instance Tabby registered.
|
|
26
|
+
*/
|
|
27
|
+
export default class ClipsnagModule {
|
|
28
|
+
private readonly service;
|
|
29
|
+
constructor(service: ClipsnagService);
|
|
30
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
(()=>{"use strict";const t={n:e=>{const s=e&&e.__esModule?()=>e.default:()=>e;return t.d(s,{a:s}),s},d:(e,s)=>{for(var i in s)t.o(s,i)&&!t.o(e,i)&&Object.defineProperty(e,i,{enumerable:!0,get:s[i]})},o:(t,e)=>Object.prototype.hasOwnProperty.call(t,e),r:t=>{Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})}};let e={};t.r(e),t.d(e,{CLIPSNAG_TOGGLE_HOTKEY:()=>d,ClipsnagConfigProvider:()=>l,ClipsnagDecorator:()=>f,ClipsnagHotkeyProvider:()=>p,ClipsnagService:()=>$,ClipsnagSettingsTabComponent:()=>O,ClipsnagSettingsTabProvider:()=>x,DEFAULT_CLIPSNAG_CONFIG:()=>u,default:()=>j});const s=require("@angular/core"),i=require("@angular/common"),n=require("@angular/forms"),o=require("tabby-core");var r=t.n(o);const a=require("tabby-terminal"),c=require("tabby-settings"),u={maxCaptureBytes:5242880,autoStopAfterIdleMs:3e3,copyToClipboardOnStop:!0,stopOnTabSwitch:!1};class l extends o.ConfigProvider{constructor(){super(...arguments),this.defaults={hotkeys:{"clipsnag-toggle":["Ctrl-Shift-X"]},clipsnag:u},this.platformDefaults={[o.Platform.macOS]:{hotkeys:{"clipsnag-toggle":["⌘-Shift-X"]}}}}}const d="clipsnag-toggle";class p extends o.HotkeyProvider{async provide(){return[{id:d,name:"Toggle terminal output snagging"}]}}class h extends a.SessionMiddleware{constructor(){super(...arguments),this.sink=null}start(t){this.sink=t}stop(){this.sink=null}feedFromSession(t){const e=this.sink;e&&e(t),super.feedFromSession(t)}}class g{constructor(t,e,s){this.middleware=t,this.stack=e,this.disposed=!1,t.start(s),e.push(t)}dispose(){this.disposed||(this.disposed=!0,this.middleware.stop(),this.stack.remove(this.middleware))}}class f extends a.TerminalDecorator{constructor(){super(...arguments),this.sessionByTab=new Map}attach(t){t.session&&this.sessionByTab.set(t,t.session),t.subscribeUntilDestroyed(t.sessionChanged$,e=>{e?this.sessionByTab.set(t,e):this.sessionByTab.delete(t)}),t.subscribeUntilDestroyed(t.destroyed$,()=>{this.sessionByTab.delete(t)})}detach(t){super.detach(t),this.sessionByTab.delete(t)}startRecording(t,e){const s=this.sessionByTab.get(t)??t.session;return s?new g(new h,s.middleware,e):null}}const b=require("rxjs"),y=require("rxjs/operators"),m=String.raw`(?:\u001B\u005C|\u009C)`,v=String.raw`\u0018\u001A\u001B\u0080-\u009F`,S=String.raw`(?:\u001B\]|\u009D)[^\u0007${v}]*(?:\u0007|${m})`,C=String.raw`(?:\u001B[PX^_k]|[\u0090\u0098\u009E\u009F])[^${v}]*${m}`,T=`${String.raw`\u001B\](?:P[\dA-Fa-f]{7}|R)(?:\u0007|${m})?`}|${S}|${C}|${String.raw`(?:\u001B\[|\u009B)[\u0030-\u003F]*[\u0020-\u002F]*[\u0040-\u007E]`}|${String.raw`\u001B(?:[\u0020-\u002F]+[\u0030-\u007E]|[\dA-PR-TZcf-oq-uy=><~|}])`}`,w=function({onlyFirst:t=!1}={}){return new RegExp(T,t?void 0:"g")}();class B{constructor(t){this.maxBytes=t,this.chunks=[],this.capturedBytes=0,this.truncated=!1}get byteLength(){return this.capturedBytes}get isTruncated(){return this.truncated}append(t){if(0===t.length)return;const e=this.maxBytes-this.capturedBytes;if(e<=0)this.truncated=!0;else{if(t.length>e)return this.chunks.push(t.subarray(0,e)),this.capturedBytes+=e,void(this.truncated=!0);this.chunks.push(t),this.capturedBytes+=t.length}}finish(){return function(t){if("string"!=typeof t)throw new TypeError(`Expected a \`string\`, got \`${typeof t}\``);return t.includes("")||t.includes("")?t.replace(w,""):t}(Buffer.concat(this.chunks,this.capturedBytes).toString("utf8")).replace(/\r\n/g,"\n").replace(/\r/g,"\n")}reset(){this.chunks.length=0,this.capturedBytes=0,this.truncated=!1}}let $=class{constructor(t,e,s,i,n,o,r){this.config=t,this.hotkeys=e,this.notifications=s,this.platform=i,this.app=n,this.zone=o,this.decorator=r,this.active=null,this.hotkeySubscription=null}init(){this.hotkeySubscription||(this.hotkeySubscription=this.hotkeys.hotkey$.subscribe(t=>{t===d&&this.zone.run(()=>{this.toggle()})}))}get isRecording(){return null!==this.active}dispose(){this.abortRecording("plugin shut down"),this.hotkeySubscription?.unsubscribe(),this.hotkeySubscription=null}async toggle(){this.active?await this.stopRecording():this.startRecording()}startRecording(){this.active&&this.abortRecording("a new recording started");const t=this.resolveActiveTerminal();if(!t)return void this.notifications.notice("Clipsnag: no active terminal to record");const e=this.settings(),s=e.maxCaptureBytes,i=new B(s),n=new b.Subject,r=new b.Subscription,a=e.autoStopAfterIdleMs;let c=null;const u=()=>{c?.unsubscribe();const t=(0,b.timer)(a).pipe((0,y.takeUntil)(n)).subscribe(()=>{this.zone.run(()=>{this.stopRecording(!0)})});c=t,r.add(t)},l=this.decorator.startRecording(t,t=>{i.append(t),null!==c&&u()});l?(a>0&&u(),r.add(t.destroyed$.subscribe(()=>{this.zone.run(()=>{this.abortRecording("terminal was closed")})})),r.add(t.sessionChanged$.subscribe(t=>{t||this.zone.run(()=>{this.abortRecording("terminal session ended")})})),e.stopOnTabSwitch&&r.add(this.app.activeTabChange$.subscribe(e=>{(e instanceof o.SplitTabComponent?e.getFocusedTab():e)!==t&&this.zone.run(()=>{this.stopRecording(!0)})})),this.active={terminal:t,buffer:i,recorder:l,idleStop$:n,subscriptions:r},this.notifications.info(`Clipsnag: recording "${t.title}" — press the hotkey again to stop`,`Buffer limit: ${M(s)}`)):this.notifications.notice("Clipsnag: this terminal has no live session yet")}async stopRecording(t=!1){const e=this.active;if(!e)return;this.active=null,e.idleStop$.next(),e.recorder.dispose(),e.subscriptions.unsubscribe();const s=e.buffer.finish(),i=e.buffer.isTruncated,n=e.buffer.byteLength;if(e.buffer.reset(),e.idleStop$.complete(),0===s.length)return void this.notifications.notice("Clipsnag: nothing captured");const o=this.settings();o.copyToClipboardOnStop&&await this.platform.setClipboard({text:s});const r=t?" (idle timeout)":"",a=i?` · truncated at ${M(o.maxCaptureBytes)}`:"",c=o.copyToClipboardOnStop?"copied to clipboard":"captured (clipboard write disabled)";this.notifications.info(`Clipsnag: ${c}${r}`,`${M(n)} of output · ${s.length} characters${a}`)}abortRecording(t){const e=this.active;e&&(this.active=null,e.idleStop$.next(),e.recorder.dispose(),e.subscriptions.unsubscribe(),e.buffer.reset(),e.idleStop$.complete(),this.notifications.notice(`Clipsnag: recording cancelled — ${t}`))}resolveActiveTerminal(){const t=this.app.activeTab;if(!t)return null;if(t instanceof a.BaseTerminalTabComponent)return t;if(t instanceof o.SplitTabComponent){const e=t.getFocusedTab();if(e instanceof a.BaseTerminalTabComponent)return e}return null}settings(){const t=this.config.store.clipsnag,e=Number(t?.maxCaptureBytes),s=Number(t?.autoStopAfterIdleMs);return{maxCaptureBytes:Number.isFinite(e)&&e>0?e:u.maxCaptureBytes,autoStopAfterIdleMs:Number.isFinite(s)&&s>=0?s:u.autoStopAfterIdleMs,copyToClipboardOnStop:t?.copyToClipboardOnStop??u.copyToClipboardOnStop,stopOnTabSwitch:t?.stopOnTabSwitch??u.stopOnTabSwitch}}};function M(t){return t<1024?`${t} B`:t<1048576?`${(t/1024).toFixed(1)} KB`:`${(t/1048576).toFixed(1)} MB`}$=function(t,e,s,i){var n,o=arguments.length,r=o<3?e:null===i?i=Object.getOwnPropertyDescriptor(e,s):i;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)r=Reflect.decorate(t,e,s,i);else for(var a=t.length-1;a>=0;a--)(n=t[a])&&(r=(o<3?n(r):o>3?n(e,s,r):n(e,s))||r);return o>3&&r&&Object.defineProperty(e,s,r),r}([(0,s.Injectable)({providedIn:"root"}),function(t,e){if("object"==typeof Reflect&&"function"==typeof Reflect.metadata)return Reflect.metadata(t,e)}("design:paramtypes",[o.ConfigService,o.HotkeysService,o.NotificationsService,o.PlatformService,o.AppService,s.NgZone,f])],$);const R=1048576;let O=class{constructor(t){this.config=t,this.ensureSettings()}get maxCaptureMiB(){return k((this.settings().maxCaptureBytes??u.maxCaptureBytes)/R,2)}get autoStopSeconds(){return k((this.settings().autoStopAfterIdleMs??u.autoStopAfterIdleMs)/1e3,2)}setMaxCaptureMiB(t){const e=Number(t),s=Number.isFinite(e)&&e>0?Math.round(e*R):u.maxCaptureBytes;this.settings().maxCaptureBytes=s,this.save()}setAutoStopSeconds(t){const e=Number(t),s=Number.isFinite(e)&&e>=0?Math.round(1e3*e):u.autoStopAfterIdleMs;this.settings().autoStopAfterIdleMs=s,this.save()}setCopyToClipboard(t){this.settings().copyToClipboardOnStop=t,this.save()}setStopOnTabSwitch(t){this.settings().stopOnTabSwitch=t,this.save()}ensureSettings(){const t=this.config.store;t.clipsnag&&"object"==typeof t.clipsnag||(t.clipsnag={...u})}settings(){return this.ensureSettings(),this.config.store.clipsnag}save(){this.config.save()}};O=function(t,e,s,i){var n,o=arguments.length,r=o<3?e:null===i?i=Object.getOwnPropertyDescriptor(e,s):i;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)r=Reflect.decorate(t,e,s,i);else for(var a=t.length-1;a>=0;a--)(n=t[a])&&(r=(o<3?n(r):o>3?n(e,s,r):n(e,s))||r);return o>3&&r&&Object.defineProperty(e,s,r),r}([(0,s.Component)({selector:"clipsnag-settings-tab",encapsulation:s.ViewEncapsulation.None,template:'\n <h3 class="mb-3">Clipsnag</h3>\n\n <div class="content-box">\n <div class="form-line">\n <div class="header">\n <div class="title">Capture buffer limit (MiB)</div>\n <div class="description">Maximum amount of output kept per recording; older bytes are dropped past this size</div>\n </div>\n <input\n class="form-control"\n type="number"\n min="0.1"\n step="0.1"\n [ngModel]="maxCaptureMiB"\n (ngModelChange)="setMaxCaptureMiB($event)"\n >\n </div>\n\n <div class="form-line">\n <div class="header">\n <div class="title">Stop after idle (seconds)</div>\n <div class="description">Recording stops automatically after this much silence; set to 0 to disable</div>\n </div>\n <input\n class="form-control"\n type="number"\n min="0"\n step="1"\n [ngModel]="autoStopSeconds"\n (ngModelChange)="setAutoStopSeconds($event)"\n >\n </div>\n\n <div class="form-line">\n <div class="header">\n <div class="title">Copy to clipboard on stop</div>\n <div class="description">Write the ANSI-stripped capture to the system clipboard when recording ends</div>\n </div>\n <toggle [ngModel]="settings().copyToClipboardOnStop" (ngModelChange)="setCopyToClipboard($event)"></toggle>\n </div>\n\n <div class="form-line">\n <div class="header">\n <div class="title">Stop on tab switch</div>\n <div class="description">Finish and copy the capture as soon as you switch away from the recorded tab</div>\n </div>\n <toggle [ngModel]="settings().stopOnTabSwitch" (ngModelChange)="setStopOnTabSwitch($event)"></toggle>\n </div>\n </div>\n '}),function(t,e){if("object"==typeof Reflect&&"function"==typeof Reflect.metadata)return Reflect.metadata(t,e)}("design:paramtypes",[o.ConfigService])],O);class x extends c.SettingsTabProvider{constructor(){super(...arguments),this.id="clipsnag",this.icon="clipboard",this.title="Clipsnag",this.weight=10}getComponentType(){return O}}function k(t,e){const s=10**e;return Math.round(t*s)/s}let P=class{constructor(t){this.service=t,this.service.init()}};P=function(t,e,s,i){var n,o=arguments.length,r=o<3?e:null===i?i=Object.getOwnPropertyDescriptor(e,s):i;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)r=Reflect.decorate(t,e,s,i);else for(var a=t.length-1;a>=0;a--)(n=t[a])&&(r=(o<3?n(r):o>3?n(e,s,r):n(e,s))||r);return o>3&&r&&Object.defineProperty(e,s,r),r}([(0,s.NgModule)({imports:[i.CommonModule,n.FormsModule,r()],declarations:[O],providers:[{provide:o.HotkeyProvider,useClass:p,multi:!0},{provide:o.ConfigProvider,useClass:l,multi:!0},{provide:a.TerminalDecorator,useClass:f,multi:!0},{provide:c.SettingsTabProvider,useClass:x,multi:!0},$,f]}),function(t,e){if("object"==typeof Reflect&&"function"==typeof Reflect.metadata)return Reflect.metadata(t,e)}("design:paramtypes",[$])],P);const j=P;module.exports=e})();
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { ConfigService } from 'tabby-core';
|
|
2
|
+
import { SettingsTabProvider } from 'tabby-settings';
|
|
3
|
+
import { ClipsnagConfig } from './config';
|
|
4
|
+
/**
|
|
5
|
+
* Settings UI for the Clipsnag plugin.
|
|
6
|
+
*
|
|
7
|
+
* Binds two-way to `config.store.clipsnag`; every change persists immediately
|
|
8
|
+
* through `ConfigService.save()`, mirroring the behaviour of Tabby's own tabs.
|
|
9
|
+
*/
|
|
10
|
+
export declare class ClipsnagSettingsTabComponent {
|
|
11
|
+
readonly config: ConfigService;
|
|
12
|
+
constructor(config: ConfigService);
|
|
13
|
+
/** MiB as shown in the number input. */
|
|
14
|
+
get maxCaptureMiB(): number;
|
|
15
|
+
/** Seconds as shown in the number input. */
|
|
16
|
+
get autoStopSeconds(): number;
|
|
17
|
+
setMaxCaptureMiB(value: number | string): void;
|
|
18
|
+
setAutoStopSeconds(value: number | string): void;
|
|
19
|
+
setCopyToClipboard(value: boolean): void;
|
|
20
|
+
setStopOnTabSwitch(value: boolean): void;
|
|
21
|
+
/**
|
|
22
|
+
* `config.store.clipsnag` is seeded by the config provider, but the settings
|
|
23
|
+
* component is also reachable before the store is merged (e.g. during a
|
|
24
|
+
* config-file reload). Materialize the section so nested bindings never read
|
|
25
|
+
* through `undefined`.
|
|
26
|
+
*/
|
|
27
|
+
private ensureSettings;
|
|
28
|
+
/** Config view used by the template; never yields `undefined` fields. */
|
|
29
|
+
settings(): Partial<ClipsnagConfig>;
|
|
30
|
+
private save;
|
|
31
|
+
}
|
|
32
|
+
/** Registers the Clipsnag section in Tabby's settings window. */
|
|
33
|
+
export declare class ClipsnagSettingsTabProvider extends SettingsTabProvider {
|
|
34
|
+
id: string;
|
|
35
|
+
icon: string;
|
|
36
|
+
title: string;
|
|
37
|
+
weight: number;
|
|
38
|
+
getComponentType(): unknown;
|
|
39
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,65 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "tabby-clipsnag",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "tabby-clipsnag",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Tabby plugin: hotkey-driven terminal output capture that strips ANSI and copies clean text to the clipboard",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"tabby-plugin",
|
|
7
|
+
"tabby",
|
|
8
|
+
"terminus",
|
|
9
|
+
"clipboard",
|
|
10
|
+
"ansi",
|
|
11
|
+
"terminal"
|
|
12
|
+
],
|
|
13
|
+
"main": "dist/index.js",
|
|
14
|
+
"types": "dist/index.d.ts",
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"README.md",
|
|
18
|
+
"README_ru.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"author": "alexoffspace",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "https://github.com/alexoffspace/tabby-clipsnag.git"
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "webpack --mode production",
|
|
29
|
+
"watch": "webpack --mode development --watch",
|
|
30
|
+
"typecheck": "tsc --noEmit",
|
|
31
|
+
"lint": "eslint \"src/**/*.ts\"",
|
|
32
|
+
"lint:fix": "eslint \"src/**/*.ts\" --fix",
|
|
33
|
+
"prepublishOnly": "npm run build"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"strip-ansi": "^7.1.0"
|
|
37
|
+
},
|
|
38
|
+
"peerDependencies": {
|
|
39
|
+
"@angular/common": "^15",
|
|
40
|
+
"@angular/core": "^15",
|
|
41
|
+
"@angular/forms": "^15",
|
|
42
|
+
"@angular/platform-browser": "^15",
|
|
43
|
+
"rxjs": "^7",
|
|
44
|
+
"tabby-core": "*",
|
|
45
|
+
"tabby-terminal": "*"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@angular/common": "^15.2.10",
|
|
49
|
+
"@angular/compiler": "^15.2.10",
|
|
50
|
+
"@angular/compiler-cli": "^15.2.10",
|
|
51
|
+
"@angular/core": "^15.2.10",
|
|
52
|
+
"@angular/platform-browser": "^15.2.10",
|
|
53
|
+
"@types/node": "^20.14.9",
|
|
54
|
+
"@typescript-eslint/eslint-plugin": "^7.16.0",
|
|
55
|
+
"@typescript-eslint/parser": "^7.16.0",
|
|
56
|
+
"eslint": "^8.57.0",
|
|
57
|
+
"ts-loader": "^9.5.1",
|
|
58
|
+
"typescript": "~4.9.5",
|
|
59
|
+
"webpack": "^5.92.1",
|
|
60
|
+
"webpack-cli": "^5.1.4"
|
|
61
|
+
},
|
|
62
|
+
"engines": {
|
|
63
|
+
"node": ">=18"
|
|
64
|
+
}
|
|
65
|
+
}
|