picosh 0.2.9 → 0.3.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/README.md +87 -0
- package/package.json +8 -4
- package/scripts/postinstall.js +35 -40
- package/scripts/preuninstall.js +14 -17
- package/wezterm/clipboard_image.ps1 +32 -0
- package/wezterm/wezterm.lua +64 -0
- package/bin/cli.js +0 -6
- package/index.js +0 -164
- package/target/ignore_this.js +0 -6
- package/target/renderer/bundle.js +0 -6
package/README.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# picosh
|
|
2
|
+
|
|
3
|
+
WezTerm config package for AI-assisted terminal workflows on Windows.
|
|
4
|
+
|
|
5
|
+
Two features:
|
|
6
|
+
- **Image paste** — Ctrl+V with an image in clipboard saves it to a temp file and inserts the path
|
|
7
|
+
- **Tab glow** — the active tab pulses blue when Claude Code is waiting for your input
|
|
8
|
+
|
|
9
|
+
## install
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npm install -g picosh
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The postinstall script copies the WezTerm config files to `~/.config/wezterm/`.
|
|
16
|
+
If WezTerm isn't installed, it installs it first via winget.
|
|
17
|
+
|
|
18
|
+
### manual setup
|
|
19
|
+
|
|
20
|
+
Copy these two files to your WezTerm config directory (`~/.config/wezterm/`):
|
|
21
|
+
|
|
22
|
+
- [`wezterm/wezterm.lua`](wezterm/wezterm.lua)
|
|
23
|
+
- [`wezterm/clipboard_image.ps1`](wezterm/clipboard_image.ps1)
|
|
24
|
+
|
|
25
|
+
## features
|
|
26
|
+
|
|
27
|
+
### image paste
|
|
28
|
+
|
|
29
|
+
Press **Ctrl+V** when an image is in the clipboard. Instead of pasting nothing (or broken text), picosh:
|
|
30
|
+
|
|
31
|
+
1. Saves the image to `%TEMP%\picosh\clip_latest.png`
|
|
32
|
+
2. Inserts that file path into the terminal at the cursor
|
|
33
|
+
|
|
34
|
+
Works with:
|
|
35
|
+
- Screenshots (bitmap from clipboard)
|
|
36
|
+
- Images copied from browsers (downloads from `<img src>` URL in HTML clipboard)
|
|
37
|
+
|
|
38
|
+
If the clipboard contains plain text, normal paste behavior is preserved.
|
|
39
|
+
|
|
40
|
+
**Why the PowerShell script?** Windows clipboard requires an [STA thread](https://learn.microsoft.com/en-us/windows/win32/com/single-threaded-apartments) (`-STA` flag). WezTerm's Lua cannot access the clipboard directly, so it shells out to a PowerShell script with `-STA`.
|
|
41
|
+
|
|
42
|
+
### tab glow
|
|
43
|
+
|
|
44
|
+
When Claude Code is waiting for your input (showing `? for shortcuts`), the tab title animates with a blue sine-wave pulse.
|
|
45
|
+
|
|
46
|
+
Detection: every 150 ms, WezTerm reads the last 5 lines of terminal output via `pane:get_lines_as_text(5)` and checks for the string `? for shortcuts`. When found, `format-tab-title` renders the tab in animated blue (`#4a__ff` where `__` oscillates).
|
|
47
|
+
|
|
48
|
+
## how it works
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
wezterm.lua
|
|
52
|
+
├── update-status (150ms interval)
|
|
53
|
+
│ └── get_lines_as_text(5) → match "? for shortcuts"
|
|
54
|
+
│ └── waiting_panes[pane_id] = true/false
|
|
55
|
+
├── format-tab-title
|
|
56
|
+
│ └── if waiting_panes[pane_id]: animate tab color (sine wave)
|
|
57
|
+
└── keys["Ctrl+V"]
|
|
58
|
+
└── run_child_process(powershell -STA -File clipboard_image.ps1)
|
|
59
|
+
├── image found → send path to pane
|
|
60
|
+
└── no image → PasteFrom Clipboard (normal paste)
|
|
61
|
+
|
|
62
|
+
clipboard_image.ps1 (must run in STA thread)
|
|
63
|
+
├── Clipboard::GetImage() → save PNG → print path
|
|
64
|
+
└── Clipboard::GetText(Html) → extract img src URL → download → print path
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## files
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
picosh/
|
|
71
|
+
├── wezterm/
|
|
72
|
+
│ ├── wezterm.lua ← main WezTerm config
|
|
73
|
+
│ └── clipboard_image.ps1 ← clipboard → file (requires STA)
|
|
74
|
+
└── scripts/
|
|
75
|
+
├── postinstall.js ← npm postinstall: install WezTerm + copy configs
|
|
76
|
+
└── preuninstall.js ← npm preuninstall: remove copied configs
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## requirements
|
|
80
|
+
|
|
81
|
+
- Windows 10/11
|
|
82
|
+
- [WezTerm](https://wezfurlong.org/wezterm/) (installed automatically by postinstall)
|
|
83
|
+
- PowerShell 5+ (built into Windows)
|
|
84
|
+
|
|
85
|
+
## background
|
|
86
|
+
|
|
87
|
+
Originally a Hyper terminal fork. Switched to WezTerm because Hyper had issues with PSReadLine (predictive completion and command history not working on Windows). WezTerm's Lua API makes both features clean and self-contained without needing to fork the terminal itself.
|
package/package.json
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "picosh",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
5
|
-
"main": "index.js",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "WezTerm config: clipboard image paste + Claude Code tab glow indicator",
|
|
6
5
|
"license": "MIT",
|
|
7
|
-
"keywords": ["
|
|
6
|
+
"keywords": ["wezterm", "claude", "ai", "terminal", "clipboard", "windows"],
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/tenhou-Ravenclaw/picosh"
|
|
10
|
+
},
|
|
11
|
+
"files": ["wezterm/", "scripts/", "LICENSE", "README.md"],
|
|
8
12
|
"scripts": {
|
|
9
13
|
"postinstall": "node scripts/postinstall.js",
|
|
10
14
|
"preuninstall": "node scripts/preuninstall.js"
|
package/scripts/postinstall.js
CHANGED
|
@@ -5,60 +5,55 @@ const path = require('path');
|
|
|
5
5
|
const os = require('os');
|
|
6
6
|
const fs = require('fs');
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
];
|
|
8
|
+
if (os.platform() !== 'win32') {
|
|
9
|
+
console.error('[picosh] Only Windows is supported.');
|
|
10
|
+
process.exit(0);
|
|
11
|
+
}
|
|
13
12
|
|
|
14
|
-
const
|
|
15
|
-
path.join(os.homedir(), 'AppData', '
|
|
16
|
-
|
|
13
|
+
const WEZTERM_EXE_PATHS = [
|
|
14
|
+
path.join(os.homedir(), 'AppData', 'Local', 'Programs', 'WezTerm', 'wezterm.exe'),
|
|
15
|
+
'C:\\Program Files\\WezTerm\\wezterm.exe',
|
|
17
16
|
];
|
|
18
17
|
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
const WEZTERM_CONFIG_DIR = path.join(os.homedir(), '.config', 'wezterm');
|
|
19
|
+
|
|
20
|
+
const SRC_DIR = path.join(__dirname, '..', 'wezterm');
|
|
21
|
+
|
|
22
|
+
function weztermInstalled() {
|
|
23
|
+
return WEZTERM_EXE_PATHS.some((p) => fs.existsSync(p));
|
|
21
24
|
}
|
|
22
25
|
|
|
23
|
-
function
|
|
24
|
-
|
|
25
|
-
console.error('[picosh] Installing Hyper...');
|
|
26
|
+
function installWezterm() {
|
|
27
|
+
console.error('[picosh] Installing WezTerm via winget...');
|
|
26
28
|
try {
|
|
27
|
-
|
|
28
|
-
execSync('winget install vercel.Hyper --silent', {stdio: 'inherit'});
|
|
29
|
-
} else if (platform === 'darwin') {
|
|
30
|
-
execSync('brew install --cask hyper', {stdio: 'inherit'});
|
|
31
|
-
} else {
|
|
32
|
-
console.error('[picosh] Please install Hyper manually: https://hyper.is');
|
|
33
|
-
return false;
|
|
34
|
-
}
|
|
29
|
+
execSync('winget install wez.wezterm --silent', {stdio: 'inherit'});
|
|
35
30
|
return true;
|
|
36
31
|
} catch (e) {
|
|
37
|
-
console.error('[picosh] Auto-install failed. Please install
|
|
32
|
+
console.error('[picosh] Auto-install failed. Please install WezTerm manually: https://wezfurlong.org/wezterm/');
|
|
38
33
|
return false;
|
|
39
34
|
}
|
|
40
35
|
}
|
|
41
36
|
|
|
42
|
-
|
|
37
|
+
function copyConfigs() {
|
|
38
|
+
fs.mkdirSync(WEZTERM_CONFIG_DIR, {recursive: true});
|
|
43
39
|
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
40
|
+
const files = fs.readdirSync(SRC_DIR);
|
|
41
|
+
for (const file of files) {
|
|
42
|
+
const src = path.join(SRC_DIR, file);
|
|
43
|
+
const dest = path.join(WEZTERM_CONFIG_DIR, file);
|
|
44
|
+
if (fs.existsSync(dest)) {
|
|
45
|
+
fs.copyFileSync(dest, dest + '.bak');
|
|
46
|
+
console.error(`[picosh] Backed up existing ${file} → ${file}.bak`);
|
|
47
|
+
}
|
|
48
|
+
fs.copyFileSync(src, dest);
|
|
49
|
+
console.error(`[picosh] Copied ${file} → ${dest}`);
|
|
50
|
+
}
|
|
48
51
|
}
|
|
49
52
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (!
|
|
53
|
-
console.error('[picosh] Hyper installed!');
|
|
54
|
-
console.error('[picosh] Launch Hyper once to initialize, then run: hyper i picosh');
|
|
55
|
-
process.exit(0);
|
|
53
|
+
if (!weztermInstalled()) {
|
|
54
|
+
const ok = installWezterm();
|
|
55
|
+
if (!ok) process.exit(0);
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
execSync(`"${cli}" i picosh`, {stdio: 'inherit'});
|
|
61
|
-
console.error('[picosh] Done! Launch Hyper to start using picosh.');
|
|
62
|
-
} catch (e) {
|
|
63
|
-
console.error('[picosh] Could not auto-register. Run manually: hyper i picosh');
|
|
64
|
-
}
|
|
58
|
+
copyConfigs();
|
|
59
|
+
console.error('[picosh] Done! Launch WezTerm to start using picosh.');
|
package/scripts/preuninstall.js
CHANGED
|
@@ -1,25 +1,22 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const {execSync} = require('child_process');
|
|
4
3
|
const path = require('path');
|
|
5
4
|
const os = require('os');
|
|
6
5
|
const fs = require('fs');
|
|
7
6
|
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
'/Applications/Hyper.app/Contents/Resources/bin/hyper',
|
|
11
|
-
'/usr/local/bin/hyper',
|
|
12
|
-
];
|
|
7
|
+
const WEZTERM_CONFIG_DIR = path.join(os.homedir(), '.config', 'wezterm');
|
|
8
|
+
const SRC_DIR = path.join(__dirname, '..', 'wezterm');
|
|
13
9
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
|
|
10
|
+
const files = fs.readdirSync(SRC_DIR);
|
|
11
|
+
for (const file of files) {
|
|
12
|
+
const dest = path.join(WEZTERM_CONFIG_DIR, file);
|
|
13
|
+
const bak = dest + '.bak';
|
|
14
|
+
try {
|
|
15
|
+
fs.unlinkSync(dest);
|
|
16
|
+
console.error(`[picosh] Removed ${dest}`);
|
|
17
|
+
if (fs.existsSync(bak)) {
|
|
18
|
+
fs.renameSync(bak, dest);
|
|
19
|
+
console.error(`[picosh] Restored ${file}.bak → ${file}`);
|
|
20
|
+
}
|
|
21
|
+
} catch (_) {}
|
|
25
22
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
2
|
+
Add-Type -AssemblyName System.Drawing
|
|
3
|
+
|
|
4
|
+
$saveDir = Join-Path $env:TEMP 'picosh'
|
|
5
|
+
$savePath = Join-Path $saveDir 'clip_latest.png'
|
|
6
|
+
|
|
7
|
+
New-Item -ItemType Directory -Force $saveDir | Out-Null
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
$img = [System.Windows.Forms.Clipboard]::GetImage()
|
|
11
|
+
if ($img -ne $null) {
|
|
12
|
+
$img.Save($savePath, [System.Drawing.Imaging.ImageFormat]::Png)
|
|
13
|
+
Write-Output $savePath
|
|
14
|
+
exit 0
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
# HTMLクリップボードから画像URL
|
|
18
|
+
$html = [System.Windows.Forms.Clipboard]::GetText([System.Windows.Forms.TextDataFormat]::Html)
|
|
19
|
+
if ($html) {
|
|
20
|
+
$match = [regex]::Match($html, 'src=[\"'']([^\"'']+)[\"'']')
|
|
21
|
+
if ($match.Success) {
|
|
22
|
+
$url = $match.Groups[1].Value
|
|
23
|
+
if ($url -match '^https?://') {
|
|
24
|
+
Invoke-WebRequest -Uri $url -OutFile $savePath -UseBasicParsing
|
|
25
|
+
Write-Output $savePath
|
|
26
|
+
exit 0
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
} catch {}
|
|
31
|
+
|
|
32
|
+
exit 1
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
local wezterm = require 'wezterm'
|
|
2
|
+
local config = wezterm.config_builder()
|
|
3
|
+
local act = wezterm.action
|
|
4
|
+
|
|
5
|
+
-- ─── 基本設定 ─────────────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
config.default_prog = { 'pwsh.exe' }
|
|
8
|
+
config.window_decorations = 'RESIZE'
|
|
9
|
+
config.hide_tab_bar_if_only_one_tab = false
|
|
10
|
+
config.use_fancy_tab_bar = false
|
|
11
|
+
config.status_update_interval = 150
|
|
12
|
+
|
|
13
|
+
-- ─── AI waiting indicator ─────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
local waiting_panes = {}
|
|
16
|
+
local tick = 0
|
|
17
|
+
|
|
18
|
+
wezterm.on('update-status', function(window, pane)
|
|
19
|
+
tick = tick + 1
|
|
20
|
+
local text = pane:get_lines_as_text(5)
|
|
21
|
+
waiting_panes[pane:pane_id()] = text:match('%? for shortcuts') ~= nil
|
|
22
|
+
window:set_right_status('')
|
|
23
|
+
end)
|
|
24
|
+
|
|
25
|
+
wezterm.on('format-tab-title', function(tab, tabs, panes, cfg, hover, max_width)
|
|
26
|
+
local pane_id = tab.active_pane.pane_id
|
|
27
|
+
local is_waiting = waiting_panes[pane_id]
|
|
28
|
+
local title = ' ' .. tab.active_pane.title .. ' '
|
|
29
|
+
|
|
30
|
+
if is_waiting then
|
|
31
|
+
local phase = (tick * 0.35) % (2 * math.pi)
|
|
32
|
+
local v = math.floor(158 + 80 * math.sin(phase))
|
|
33
|
+
return {
|
|
34
|
+
{ Background = { Color = string.format('#%02x%02x%02x', 74, v, 255) } },
|
|
35
|
+
{ Foreground = { Color = '#ffffff' } },
|
|
36
|
+
{ Text = title },
|
|
37
|
+
}
|
|
38
|
+
end
|
|
39
|
+
end)
|
|
40
|
+
|
|
41
|
+
-- ─── 画像ペースト ────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
local ps1 = wezterm.config_dir .. '\\clipboard_image.ps1'
|
|
44
|
+
|
|
45
|
+
config.keys = {
|
|
46
|
+
{
|
|
47
|
+
key = 'v',
|
|
48
|
+
mods = 'CTRL',
|
|
49
|
+
action = wezterm.action_callback(function(window, pane)
|
|
50
|
+
local ok, stdout, stderr = wezterm.run_child_process({
|
|
51
|
+
'powershell.exe', '-NoProfile', '-NonInteractive', '-STA', '-File', ps1,
|
|
52
|
+
})
|
|
53
|
+
local path = stdout and stdout:match('[^\r\n]+')
|
|
54
|
+
|
|
55
|
+
if path and path ~= '' then
|
|
56
|
+
pane:send_text(path)
|
|
57
|
+
else
|
|
58
|
+
window:perform_action(act.PasteFrom 'Clipboard', pane)
|
|
59
|
+
end
|
|
60
|
+
end),
|
|
61
|
+
},
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return config
|
package/bin/cli.js
DELETED
package/index.js
DELETED
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const path = require('path');
|
|
4
|
-
const fs = require('fs');
|
|
5
|
-
const os = require('os');
|
|
6
|
-
const https = require('https');
|
|
7
|
-
const http = require('http');
|
|
8
|
-
|
|
9
|
-
// ─── image paste ────────────────────────────────────────────────────────────
|
|
10
|
-
|
|
11
|
-
const SAVE_DIR = path.join(os.tmpdir(), 'picosh');
|
|
12
|
-
const SAVE_PATH = path.join(SAVE_DIR, 'clip_latest.png');
|
|
13
|
-
|
|
14
|
-
function hasImage(clipboard) {
|
|
15
|
-
if (!clipboard.readImage().isEmpty()) return {type: 'bitmap'};
|
|
16
|
-
const html = clipboard.readHTML();
|
|
17
|
-
if (html) {
|
|
18
|
-
const match = html.match(/<img[^>]+src=["']([^"']+)["']/i);
|
|
19
|
-
if (match) return {type: 'url', src: match[1]};
|
|
20
|
-
}
|
|
21
|
-
return null;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function saveBitmap(clipboard) {
|
|
25
|
-
fs.mkdirSync(SAVE_DIR, {recursive: true});
|
|
26
|
-
fs.writeFileSync(SAVE_PATH, clipboard.readImage().toPNG());
|
|
27
|
-
return Promise.resolve(SAVE_PATH);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function downloadImage(url) {
|
|
31
|
-
return new Promise((resolve) => {
|
|
32
|
-
fs.mkdirSync(SAVE_DIR, {recursive: true});
|
|
33
|
-
const file = fs.createWriteStream(SAVE_PATH);
|
|
34
|
-
const client = url.startsWith('https') ? https : http;
|
|
35
|
-
client.get(url, (res) => {
|
|
36
|
-
res.pipe(file);
|
|
37
|
-
file.on('finish', () => file.close(() => resolve(SAVE_PATH)));
|
|
38
|
-
}).on('error', () => {
|
|
39
|
-
fs.unlink(SAVE_PATH, () => {});
|
|
40
|
-
resolve(null);
|
|
41
|
-
});
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// ─── AI waiting indicator ───────────────────────────────────────────────────
|
|
46
|
-
|
|
47
|
-
const ANSI_RE = /\x1b\[[0-9;]*[A-Za-z]|\x1b\][^\x07]*\x07|\x07/g;
|
|
48
|
-
// Claude Codeの待機プロンプト: "❯ ? for shortcuts"
|
|
49
|
-
const PROMPT_RE = /❯\s*\?/;
|
|
50
|
-
const timers = {};
|
|
51
|
-
const waitingState = {};
|
|
52
|
-
let activeUid = null;
|
|
53
|
-
let promptActive = false;
|
|
54
|
-
|
|
55
|
-
function setWaiting(uid, waiting) {
|
|
56
|
-
if (!uid) return;
|
|
57
|
-
waitingState[uid] = waiting;
|
|
58
|
-
console.log('[picosh] setWaiting uid:', uid.slice(0, 8), 'waiting:', waiting);
|
|
59
|
-
if (typeof window !== 'undefined') {
|
|
60
|
-
window.dispatchEvent(new CustomEvent('picosh-ai-waiting', {detail: {uid, waiting}}));
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
exports.middleware = () => (next) => (action) => {
|
|
65
|
-
if (action.type && action.type.startsWith('SESSION_')) {
|
|
66
|
-
console.log('[picosh] action:', action.type, 'uid:', action.uid ? action.uid.slice(0, 8) : 'none', 'activeUid:', activeUid ? activeUid.slice(0, 8) : 'none');
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
if (action.type === 'SESSION_ADD' || action.type === 'SESSION_SET_ACTIVE') {
|
|
70
|
-
if (action.uid) activeUid = action.uid;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
if (action.type === 'SESSION_ADD_DATA') {
|
|
74
|
-
const uid = action.uid || activeUid;
|
|
75
|
-
const {data} = action;
|
|
76
|
-
const clean = data.replace(ANSI_RE, '');
|
|
77
|
-
console.log('[picosh] ADD_DATA uid:', uid ? uid.slice(0, 8) : 'NONE', JSON.stringify(clean.slice(-40)));
|
|
78
|
-
|
|
79
|
-
clearTimeout(timers[uid]);
|
|
80
|
-
timers[uid] = setTimeout(() => {
|
|
81
|
-
if (PROMPT_RE.test(clean)) {
|
|
82
|
-
promptActive = true;
|
|
83
|
-
setWaiting(uid, true);
|
|
84
|
-
} else if (promptActive) {
|
|
85
|
-
// スピナー等の実質的なデータが来たら waiting 解除
|
|
86
|
-
if (clean.trim().length > 0) {
|
|
87
|
-
promptActive = false;
|
|
88
|
-
setWaiting(uid, false);
|
|
89
|
-
}
|
|
90
|
-
} else {
|
|
91
|
-
setWaiting(uid, false);
|
|
92
|
-
}
|
|
93
|
-
}, 300);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
return next(action);
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
exports.decorateTab = (Tab, {React}) => {
|
|
100
|
-
return function PicoshTab(props) {
|
|
101
|
-
const [waiting, setWaitingState] = React.useState(!!waitingState[props.uid]);
|
|
102
|
-
console.log('[picosh] decorateTab render uid:', props.uid && props.uid.slice(0, 8), 'waiting:', waiting);
|
|
103
|
-
|
|
104
|
-
React.useEffect(() => {
|
|
105
|
-
function onWaiting(e) {
|
|
106
|
-
console.log('[picosh] tab event received', e.detail.uid.slice(0, 8), 'this:', props.uid && props.uid.slice(0, 8));
|
|
107
|
-
if (e.detail.uid === props.uid) setWaitingState(e.detail.waiting);
|
|
108
|
-
}
|
|
109
|
-
window.addEventListener('picosh-ai-waiting', onWaiting);
|
|
110
|
-
return () => window.removeEventListener('picosh-ai-waiting', onWaiting);
|
|
111
|
-
}, [props.uid]);
|
|
112
|
-
|
|
113
|
-
return React.createElement(
|
|
114
|
-
'div',
|
|
115
|
-
{style: {position: 'relative', display: 'contents'}},
|
|
116
|
-
waiting && React.createElement('style', {key: 's'}, `
|
|
117
|
-
@keyframes picosh-glow {
|
|
118
|
-
0%, 100% { box-shadow: 0 0 6px 2px #4a9eff; }
|
|
119
|
-
50% { box-shadow: 0 0 12px 4px #4a9eff; }
|
|
120
|
-
}
|
|
121
|
-
`),
|
|
122
|
-
React.createElement(Tab, Object.assign({}, props, {
|
|
123
|
-
style: Object.assign({}, props.style, waiting ? {
|
|
124
|
-
animation: 'picosh-glow 1.5s ease-in-out infinite',
|
|
125
|
-
borderRadius: '4px',
|
|
126
|
-
} : {}),
|
|
127
|
-
})),
|
|
128
|
-
);
|
|
129
|
-
};
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
// ─── image paste (term) ──────────────────────────────────────────────────────
|
|
133
|
-
|
|
134
|
-
exports.decorateTerm = (Term, {React}) => {
|
|
135
|
-
return class extends React.Component {
|
|
136
|
-
componentDidMount() {
|
|
137
|
-
this._onKeyDown = (e) => {
|
|
138
|
-
if (!((e.ctrlKey || e.metaKey) && e.key === 'v')) return;
|
|
139
|
-
try {
|
|
140
|
-
const {clipboard} = require('electron');
|
|
141
|
-
const found = hasImage(clipboard);
|
|
142
|
-
if (!found) return;
|
|
143
|
-
|
|
144
|
-
e.preventDefault();
|
|
145
|
-
e.stopPropagation();
|
|
146
|
-
|
|
147
|
-
const save = found.type === 'bitmap' ? saveBitmap(clipboard) : downloadImage(found.src);
|
|
148
|
-
save.then((filepath) => {
|
|
149
|
-
if (filepath && this.props.onData) this.props.onData(filepath);
|
|
150
|
-
});
|
|
151
|
-
} catch (_) {}
|
|
152
|
-
};
|
|
153
|
-
document.addEventListener('keydown', this._onKeyDown, true);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
componentWillUnmount() {
|
|
157
|
-
document.removeEventListener('keydown', this._onKeyDown, true);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
render() {
|
|
161
|
-
return React.createElement(Term, this.props);
|
|
162
|
-
}
|
|
163
|
-
};
|
|
164
|
-
};
|
package/target/ignore_this.js
DELETED