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 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.2.9",
4
- "description": "Hyper plugin: paste clipboard images as file paths",
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": ["hyper", "hyper-plugin"],
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"
@@ -5,60 +5,55 @@ const path = require('path');
5
5
  const os = require('os');
6
6
  const fs = require('fs');
7
7
 
8
- const HYPER_CLI_PATHS = [
9
- path.join(os.homedir(), 'AppData', 'Local', 'Programs', 'Hyper', 'resources', 'bin', 'hyper.cmd'),
10
- '/Applications/Hyper.app/Contents/Resources/bin/hyper',
11
- '/usr/local/bin/hyper',
12
- ];
8
+ if (os.platform() !== 'win32') {
9
+ console.error('[picosh] Only Windows is supported.');
10
+ process.exit(0);
11
+ }
13
12
 
14
- const HYPER_CONFIG_PATHS = [
15
- path.join(os.homedir(), 'AppData', 'Roaming', 'Hyper', 'config.json'),
16
- path.join(os.homedir(), '.hyper.js'),
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
- function findHyperCli() {
20
- return HYPER_CLI_PATHS.find((p) => fs.existsSync(p));
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 installHyper() {
24
- const platform = os.platform();
25
- console.error('[picosh] Installing Hyper...');
26
+ function installWezterm() {
27
+ console.error('[picosh] Installing WezTerm via winget...');
26
28
  try {
27
- if (platform === 'win32') {
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 Hyper manually: https://hyper.is');
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
- let cli = findHyperCli();
37
+ function copyConfigs() {
38
+ fs.mkdirSync(WEZTERM_CONFIG_DIR, {recursive: true});
43
39
 
44
- if (!cli) {
45
- const installed = installHyper();
46
- if (!installed) process.exit(0);
47
- cli = findHyperCli();
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
- const configExists = HYPER_CONFIG_PATHS.some((p) => fs.existsSync(p));
51
-
52
- if (!cli || !configExists) {
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
- try {
59
- console.error('[picosh] Registering plugin with Hyper...');
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.');
@@ -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 HYPER_CLI_PATHS = [
9
- path.join(os.homedir(), 'AppData', 'Local', 'Programs', 'Hyper', 'resources', 'bin', 'hyper.cmd'),
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
- function findHyperCli() {
15
- return HYPER_CLI_PATHS.find((p) => fs.existsSync(p));
16
- }
17
-
18
- const cli = findHyperCli();
19
- if (!cli) process.exit(0);
20
-
21
- try {
22
- execSync(`"${cli}" u picosh`, {stdio: 'inherit'});
23
- } catch (e) {
24
- // ignore
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
@@ -1,6 +0,0 @@
1
- /******/ (() => { // webpackBootstrap
2
- /******/ "use strict";
3
- /******/
4
- /******/
5
- /******/ })()
6
- ;
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
- };
@@ -1,6 +0,0 @@
1
- /******/ (() => { // webpackBootstrap
2
- /******/ "use strict";
3
- /******/
4
- /******/
5
- /******/ })()
6
- ;
@@ -1,6 +0,0 @@
1
- /******/ (() => { // webpackBootstrap
2
- /******/ "use strict";
3
- /******/
4
- /******/
5
- /******/ })()
6
- ;