editprompt 0.5.2 β†’ 0.7.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.
Files changed (3) hide show
  1. package/README.md +249 -99
  2. package/dist/index.js +475 -24
  3. package/package.json +2 -1
package/README.md CHANGED
@@ -1,16 +1,21 @@
1
+ <p align="center">
2
+ <a href="https://www.npmjs.com/package/editprompt"><img src="https://img.shields.io/npm/v/editprompt?color=CB0200" alt="link to npm.js" /></a>
3
+ </p>
4
+
1
5
  # πŸ“ editprompt
2
6
 
3
7
  A CLI tool that lets you write prompts for CLI tools using your favorite text editor. Works seamlessly with Claude Code, Codex CLI, Gemini CLI, and any other CLI process.
4
8
 
5
- https://github.com/user-attachments/assets/01bcda7c-7771-4b33-bf5c-629812d45cc4
9
+ ![send without closing editor](https://github.com/user-attachments/assets/b0e486af-78d7-4b70-8c82-64d330c22ba1)
6
10
 
7
11
 
8
12
  ## πŸ† Why editprompt?
9
13
 
10
14
  - **🎯 Your Editor, Your Way**: Write prompts in your favorite editor with full syntax highlighting, plugins, and customizations
11
15
  - **🚫 No Accidental Sends**: Never accidentally hit Enter and send an incomplete prompt again
12
- - πŸ”„ **Reusable Prompts**: Save and iterate on prompts with `--always-copy`
13
- - πŸ“ **Multi-line Commands**: Complex SQL queries, JSON payloads
16
+ - **πŸ”„ Iterate Efficiently**: Keep your editor open and send multiple prompts without reopening
17
+ - **πŸ’¬ Quote and Reply**: Collect multiple text selections and reply to specific parts of AI responses
18
+ - **πŸ“ Multi-line Commands**: Complex SQL queries, JSON payloads, and structured prompts
14
19
 
15
20
 
16
21
  ## ✨ Features
@@ -18,8 +23,9 @@ https://github.com/user-attachments/assets/01bcda7c-7771-4b33-bf5c-629812d45cc4
18
23
  - πŸ–ŠοΈ **Editor Integration**: Use your preferred text editor to write prompts
19
24
  - πŸ–₯️ **Multiplexer Support**: Send prompts directly to tmux or WezTerm sessions
20
25
  - πŸ–₯️ **Universal Terminal Support**: Works with any terminal via clipboard - no multiplexer required
26
+ - πŸ“€ **Send Without Closing**: Iterate on prompts without closing your editor
27
+ - πŸ“‹ **Quote Buffering**: Collect text selections and send them as quoted replies
21
28
  - πŸ“‹ **Clipboard Fallback**: Automatically copies to clipboard if sending fails
22
- - πŸ“‹ **Always Copy Option**: Copy to clipboard even after successful tmux delivery (`--always-copy`)
23
29
 
24
30
 
25
31
  ## πŸ“¦ Installation
@@ -34,67 +40,61 @@ npx editprompt
34
40
 
35
41
  ## πŸš€ Usage
36
42
 
37
- - Run `editprompt` to open a temporary Markdown file in your editor
38
- - Write your prompt comfortably with full editor features
39
- - Save and close - it automatically:
40
- - Sends to tmux/wezterm panes if detected
41
- - Falls back to clipboard otherwise (works with **any terminal**)
43
+ editprompt supports three main workflows to fit different use cases:
42
44
 
43
- ```sh
44
- editprompt
45
- ```
45
+ ### Workflow 1: Basic - Write and Send
46
46
 
47
- **Advanced usage:** You can also send content **without closing the editor** for faster iteration. See `Send Without Closing Editor` for details.
47
+ ![wrihte and send prompt by editprompt](https://github.com/user-attachments/assets/6587b0c4-8132-4d5c-be68-3aa32a8d4df2)
48
48
 
49
+ The simplest way to use editprompt:
49
50
 
50
- ### πŸ–₯️ Tmux Integration
51
+ 1. Run `editprompt` to open your editor
52
+ 2. Write your prompt
53
+ 3. Save and close the editor
54
+ 4. Content is automatically sent to the target pane or clipboard
51
55
 
52
- **Split window version:**
53
- ```tmux
54
- bind -n M-q run-shell 'tmux split-window -v -l 20 \
55
- -c "#{pane_current_path}" \
56
- "editprompt --editor nvim --always-copy --target-pane #{pane_id}"'
57
- ```
56
+ Perfect for one-off prompts when you need more space than a terminal input line.
58
57
 
59
- **Popup version:**
60
- ```tmux
61
- bind -n M-q run-shell 'tmux display-popup -E \
62
- -d "#{pane_current_path}" \
63
- -w 80% -h 65% \
64
- "editprompt --editor nvim --always-copy --target-pane #{pane_id}"'
65
- ```
58
+ ### Workflow 2: Interactive - Iterate with Editor Open
66
59
 
60
+ ![send without closing editor](https://github.com/user-attachments/assets/b0e486af-78d7-4b70-8c82-64d330c22ba1)
67
61
 
68
- ### πŸ–ΌοΈ WezTerm Integration
69
- ```lua
70
- {
71
- key = "q",
72
- mods = "OPT",
73
- action = wezterm.action_callback(function(window, pane)
74
- local target_pane_id = tostring(pane:pane_id())
75
- window:perform_action(
76
- act.SplitPane({
77
- direction = "Down",
78
- size = { Cells = 10 },
79
- }),
80
- pane
81
- )
82
- wezterm.time.call_after(1, function()
83
- window:perform_action(
84
- act.SendString(
85
- string.format(
86
- "editprompt --editor nvim --always-copy --mux wezterm --target-pane %s\n",
87
- target_pane_id
88
- )
89
- ),
90
- window:active_pane()
91
- )
92
- end)
93
- end),
94
- },
62
+ For iterating on prompts without constantly reopening the editor:
63
+
64
+ 1. Set up a keybinding to open editprompt with `--resume` mode
65
+ 2. Editor pane stays open between sends
66
+ 3. Write, send, refine, send again - all without closing the editor
67
+ 4. Use the same keybinding to toggle between your work pane and editor pane
68
+
69
+ Ideal for trial-and-error workflows with AI assistants.
70
+
71
+ ### Workflow 3: Quote - Collect and Reply
72
+
73
+ ![quote and capture with editprompt](https://github.com/user-attachments/assets/33af0702-5c80-4ccf-80d9-0ae42052e6fa)
74
+
75
+ ```markdown
76
+ > Some AI agents include leading spaces in their output,which can make the copied text look a bit awkward.
77
+
78
+ <!-- Write your reply here -->
79
+
80
+ > Using editprompt’s quote mode or capture mode makes it easy to reply while quoting the AI agent’s output.
81
+
82
+ <!-- Write your reply here -->
95
83
  ```
96
84
 
97
- ### πŸ’‘ Basic Usage
85
+ For replying to specific parts of AI responses:
86
+
87
+ 1. Select text in your terminal (tmux copy mode or WezTerm selection) and trigger quote mode
88
+ 2. Repeat to collect multiple selections
89
+ 3. Run `editprompt --capture` to retrieve all collected quotes
90
+ 4. Edit and send your reply with context
91
+
92
+ Perfect for addressing multiple points in long AI responses.
93
+
94
+
95
+ ## βš™οΈ Setup & Configuration
96
+
97
+ ### Basic Setup
98
98
 
99
99
  ```bash
100
100
  # Use with your default editor (from $EDITOR)
@@ -111,12 +111,67 @@ editprompt --always-copy
111
111
  editprompt --help
112
112
  ```
113
113
 
114
+ ### Tmux Integration
115
+
116
+ ```tmux
117
+ bind -n M-q run-shell '\
118
+ editprompt --resume --target-pane #{pane_id} || \
119
+ tmux split-window -v -l 10 -c "#{pane_current_path}" \
120
+ "editprompt --editor nvim --always-copy --target-pane #{pane_id}"'
121
+ ```
122
+
114
123
 
115
- ## πŸ“€ Send Without Closing Editor
124
+ ### WezTerm Integration
125
+
126
+ ```lua
127
+ {
128
+ key = "q",
129
+ mods = "OPT",
130
+ action = wezterm.action_callback(function(window, pane)
131
+ local target_pane_id = tostring(pane:pane_id())
132
+
133
+ -- Try to resume existing editor pane
134
+ local success, stdout, stderr = wezterm.run_child_process({
135
+ "/bin/zsh",
136
+ "-lc",
137
+ string.format(
138
+ "editprompt --resume --mux wezterm --target-pane %s",
139
+ target_pane_id
140
+ ),
141
+ })
142
+
143
+ -- If resume failed, create new editor pane
144
+ if not success then
145
+ window:perform_action(
146
+ act.SplitPane({
147
+ direction = "Down",
148
+ size = { Cells = 10 },
149
+ command = {
150
+ args = {
151
+ "/bin/zsh",
152
+ "-lc",
153
+ string.format(
154
+ "editprompt --editor nvim --always-copy --mux wezterm --target-pane %s",
155
+ target_pane_id
156
+ ),
157
+ },
158
+ },
159
+ }),
160
+ pane
161
+ )
162
+ end
163
+ end),
164
+ },
165
+ ```
166
+
167
+ **Note:** The `-lc` flag ensures your shell loads the full login environment, making `editprompt` available in your PATH.
168
+
169
+
170
+ ### Editor Integration (Send Without Closing)
116
171
 
117
172
  While editprompt is running, you can send content to the target pane or clipboard without closing the editor. This allows you to iterate quickly on your prompts.
118
173
 
119
- ### Basic Usage
174
+ #### Command Line Usage
120
175
 
121
176
  ```bash
122
177
  # Run this command from within your editor session
@@ -125,7 +180,7 @@ editprompt -- "your content here"
125
180
 
126
181
  This sends the content to the target pane (or clipboard) while keeping your editor open, so you can continue editing and send multiple times.
127
182
 
128
- ### Neovim Integration Example
183
+ #### Neovim Integration Example
129
184
 
130
185
  You can set up a convenient keybinding to send your buffer content:
131
186
 
@@ -159,33 +214,155 @@ if vim.env.EDITPROMPT then
159
214
  end
160
215
  ```
161
216
 
162
- With this configuration:
163
- 1. Open editprompt using the tmux/wezterm keybinding mentioned above
217
+ **Usage:**
218
+ 1. Open editprompt using the tmux/wezterm keybinding
164
219
  2. Write your prompt in the editor
165
220
  3. Press `<Space>x` to send the content to the target pane
166
221
  4. The buffer is automatically cleared on success
167
222
  5. Continue editing to send more content
168
223
 
224
+ ### Quote Workflow Setup
225
+
226
+ #### Collecting Quotes in tmux Copy Mode
169
227
 
170
- ## βš™οΈ Configuration
228
+ Add this keybinding to your `.tmux.conf` to collect selected text as quotes:
171
229
 
172
- ### πŸ“ Editor Selection
230
+ ```tmux
231
+ bind-key -T copy-mode-vi C-e { send-keys -X pipe "editprompt --quote --target-pane #{pane_id}" }
232
+ ```
233
+
234
+ **Usage:**
235
+ 1. Enter tmux copy mode (`prefix + [`)
236
+ 2. Select text using vi-mode keybindings
237
+ 3. Press `Ctrl-e` to add the selection as a quote
238
+ 4. Repeat to collect multiple quotes
239
+ 5. All quotes are stored in a pane variable associated with the target pane
240
+
241
+ #### Collecting Quotes in WezTerm
242
+
243
+ Add this event handler and keybinding to your `wezterm.lua` to collect selected text as quotes:
244
+
245
+ ```lua
246
+ local wezterm = require("wezterm")
247
+
248
+ wezterm.on("editprompt-quote", function(window, pane)
249
+ local text = window:get_selection_text_for_pane(pane)
250
+ local target_pane_id = tostring(pane:pane_id())
251
+
252
+ wezterm.run_child_process({
253
+ "/bin/zsh",
254
+ "-lc",
255
+ string.format(
256
+ "editprompt --quote --mux wezterm --target-pane %s -- %s",
257
+ target_pane_id,
258
+ wezterm.shell_quote_arg(text)
259
+ ),
260
+ })
261
+ end)
262
+
263
+ return {
264
+ keys = {
265
+ {
266
+ key = "e",
267
+ mods = "CTRL",
268
+ action = wezterm.action.EmitEvent("editprompt-quote"),
269
+ },
270
+ },
271
+ }
272
+ ```
273
+
274
+ **Usage:**
275
+ 1. Select text in WezTerm (by dragging with mouse or using copy mode)
276
+ 2. Press `Ctrl-e` to add the selection as a quote
277
+ 3. Repeat to collect multiple quotes
278
+ 4. All quotes are stored in a configuration file associated with the target pane
279
+
280
+ #### Capturing Collected Quotes
281
+
282
+ Run this command from within your editor pane to retrieve all collected quotes:
283
+
284
+ ```bash
285
+ editprompt --capture
286
+ ```
287
+
288
+ This copies all collected quotes to the clipboard and clears the buffer, ready for your reply.
289
+
290
+ **Complete workflow:**
291
+ 1. AI responds with multiple points
292
+ 2. Select each point in copy mode and press `Ctrl-e`
293
+ 3. Open your editor pane and run `editprompt --capture`
294
+ 4. Edit the quoted text with your responses
295
+ 5. Send to AI
296
+
297
+ **How quote buffering works:**
298
+ - **tmux**: Quotes are stored in pane variables, automatically cleaned up when the pane closes
299
+ - **WezTerm**: Quotes are stored in a configuration file associated with the pane
300
+ - Text is intelligently processed: removes common indentation, handles line breaks smartly
301
+ - Each quote is prefixed with `> ` in markdown quote format
302
+ - Multiple quotes are separated with blank lines
303
+
304
+ #### Neovim Integration Example
305
+
306
+ You can set up a convenient keybinding to capture your quote content:
307
+ ```lua
308
+ vim.keymap.set("n", "<Space>X", function()
309
+ vim.cmd("update")
310
+
311
+ vim.system({ "editprompt", "--capture" }, { text = true }, function(obj)
312
+ vim.schedule(function()
313
+ if obj.code == 0 then
314
+ vim.cmd("silent write")
315
+ -- Split stdout by lines
316
+ local output_lines = vim.split(obj.stdout, "\n")
317
+
318
+ local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false)
319
+ local is_empty = #lines == 1 and lines[1] == ""
320
+
321
+ if is_empty then
322
+ -- If empty, overwrite from the beginning
323
+ vim.api.nvim_buf_set_lines(0, 0, -1, false, output_lines)
324
+ vim.cmd("normal 2j")
325
+ else
326
+ -- If not empty, append to the end
327
+ table.insert(output_lines, 1, "")
328
+ local line_count = vim.api.nvim_buf_line_count(0)
329
+ vim.api.nvim_buf_set_lines(
330
+ 0,
331
+ line_count,
332
+ line_count,
333
+ false,
334
+ output_lines
335
+ )
336
+ vim.cmd("normal 4j")
337
+ end
338
+
339
+ vim.cmd("silent write")
340
+ else
341
+ vim.notify(
342
+ "editprompt failed: " .. (obj.stderr or "unknown error"),
343
+ vim.log.levels.ERROR
344
+ )
345
+ end
346
+ end)
347
+ end)
348
+ end, { silent = true, desc = "Capture from editprompt quote mode" })
349
+ ```
350
+
351
+ ### Environment Variables
352
+
353
+ #### Editor Selection
173
354
 
174
355
  editprompt respects the following editor priority:
175
356
 
176
357
  1. `--editor/-e` command line option
177
- 2. `$EDITOR` environment variable
358
+ 2. `$EDITOR` environment variable
178
359
  3. Default: `vim`
179
360
 
180
- ### 🌍 Environment Variables
181
-
182
- - `EDITOR`: Your preferred text editor
183
-
184
- ### πŸ”§ Editor Integration with EDITPROMPT
361
+ #### EDITPROMPT Environment Variable
185
362
 
186
363
  editprompt automatically sets `EDITPROMPT=1` when launching your editor. This allows you to detect when your editor is launched by editprompt and enable specific configurations or plugins.
187
364
 
188
- #### πŸ” Example: Neovim Configuration
365
+ **Example: Neovim Configuration**
189
366
 
190
367
  ```lua
191
368
  -- In your Neovim config (e.g., init.lua)
@@ -196,7 +373,7 @@ if vim.env.EDITPROMPT then
196
373
  end
197
374
  ```
198
375
 
199
- #### πŸ› οΈ Setting Custom Environment Variables
376
+ #### Custom Environment Variables
200
377
 
201
378
  You can also pass custom environment variables to your editor:
202
379
 
@@ -211,33 +388,6 @@ editprompt --env THEME=dark --env FOO=fooooo
211
388
  editprompt --env NVIM_CONFIG=minimal
212
389
  ```
213
390
 
214
- ---
215
-
216
- ## πŸ”§ Development
217
-
218
- ```bash
219
- # Clone the repository
220
- git clone https://github.com/eetann/editprompt.git
221
- cd editprompt
222
-
223
- # Install dependencies
224
- bun install
225
-
226
- # Build
227
- bun run build
228
-
229
- # Run tests
230
- bun test
231
-
232
- # Development mode
233
- bun run dev
234
- ```
235
-
236
- ## πŸ” Technical Details
237
-
238
- ### πŸ”„ Fallback Strategy
239
-
240
- editprompt implements a robust fallback strategy:
391
+ #### Target Pane Environment Variable
241
392
 
242
- 1. **Tmux Integration**: Direct input to tmux panes (when available)
243
- 2. **Clipboard**: Copy content to clipboard with user notification
393
+ When using the send-without-closing feature or quote capture, editprompt sets `EDITPROMPT_TARGET_PANE` to the target pane ID. This is automatically used by `editprompt --` and `editprompt --capture` commands.
package/dist/index.js CHANGED
@@ -1,14 +1,234 @@
1
1
  #!/usr/bin/env node
2
2
  import { cli } from "gunshi";
3
3
  import { exec, spawn } from "node:child_process";
4
+ import { promisify } from "node:util";
5
+ import Conf from "conf";
4
6
  import { mkdir, readFile, writeFile } from "node:fs/promises";
5
7
  import { tmpdir } from "node:os";
6
8
  import { join } from "node:path";
7
- import { promisify } from "node:util";
8
9
  import clipboardy from "clipboardy";
9
10
 
10
11
  //#region package.json
11
- var version = "0.5.2";
12
+ var version = "0.7.0";
13
+
14
+ //#endregion
15
+ //#region src/modules/tmux.ts
16
+ const execAsync$2 = promisify(exec);
17
+ async function getCurrentPaneId() {
18
+ const { stdout } = await execAsync$2("tmux display-message -p \"#{pane_id}\"");
19
+ return stdout.trim();
20
+ }
21
+ async function saveEditorPaneId(targetPaneId, editorPaneId) {
22
+ await execAsync$2(`tmux set-option -pt '${targetPaneId}' @editprompt_editor_pane '${editorPaneId}'`);
23
+ }
24
+ async function clearEditorPaneId(targetPaneId) {
25
+ await execAsync$2(`tmux set-option -pt '${targetPaneId}' @editprompt_editor_pane ""`);
26
+ }
27
+ async function getEditorPaneId(targetPaneId) {
28
+ try {
29
+ const { stdout } = await execAsync$2(`tmux show -pt '${targetPaneId}' -v @editprompt_editor_pane`);
30
+ return stdout.trim();
31
+ } catch {
32
+ return "";
33
+ }
34
+ }
35
+ async function checkPaneExists(paneId) {
36
+ try {
37
+ const { stdout } = await execAsync$2("tmux list-panes -a -F \"#{pane_id}\"");
38
+ return stdout.split("\n").map((id) => id.trim()).includes(paneId);
39
+ } catch {
40
+ return false;
41
+ }
42
+ }
43
+ async function focusPane(paneId) {
44
+ await execAsync$2(`tmux select-pane -t '${paneId}'`);
45
+ }
46
+ async function markAsEditorPane(editorPaneId, targetPaneId) {
47
+ await execAsync$2(`tmux set-option -pt '${editorPaneId}' @editprompt_is_editor 1`);
48
+ await execAsync$2(`tmux set-option -pt '${editorPaneId}' @editprompt_target_pane '${targetPaneId}'`);
49
+ }
50
+ async function getTargetPaneId(editorPaneId) {
51
+ try {
52
+ const { stdout } = await execAsync$2(`tmux show -pt '${editorPaneId}' -v @editprompt_target_pane`);
53
+ return stdout.trim();
54
+ } catch {
55
+ return "";
56
+ }
57
+ }
58
+ async function isEditorPane(paneId) {
59
+ try {
60
+ const { stdout } = await execAsync$2(`tmux show -pt '${paneId}' -v @editprompt_is_editor`);
61
+ return stdout.trim() === "1";
62
+ } catch {
63
+ return false;
64
+ }
65
+ }
66
+ async function getQuoteVariableContent(paneId) {
67
+ try {
68
+ const { stdout } = await execAsync$2(`tmux show -pt '${paneId}' -v @editprompt_quote`);
69
+ return stdout;
70
+ } catch {
71
+ return "";
72
+ }
73
+ }
74
+ async function appendToQuoteVariable(paneId, content) {
75
+ let newContent = "";
76
+ const existingContent = await getQuoteVariableContent(paneId);
77
+ if (existingContent.trim() !== "") newContent = `${existingContent}\n${content}`;
78
+ else newContent = content;
79
+ await execAsync$2(`tmux set-option -pt '${paneId}' @editprompt_quote '${newContent.replace(/'/g, "'\\''")}' `);
80
+ }
81
+ async function clearQuoteVariable(targetPaneId) {
82
+ await execAsync$2(`tmux set-option -pt '${targetPaneId}' @editprompt_quote ""`);
83
+ }
84
+
85
+ //#endregion
86
+ //#region src/modules/wezterm.ts
87
+ const execAsync$1 = promisify(exec);
88
+ const projectName = process.env.NODE_ENV === "test" ? "editprompt-test" : "editprompt";
89
+ const conf = new Conf({ projectName });
90
+ async function getCurrentPaneId$1() {
91
+ try {
92
+ const { stdout } = await execAsync$1("wezterm cli list --format json");
93
+ const activePane = JSON.parse(stdout).find((pane) => pane.is_active === true);
94
+ return String(activePane?.pane_id);
95
+ } catch (error) {
96
+ console.log(error);
97
+ return "";
98
+ }
99
+ }
100
+ async function checkPaneExists$1(paneId) {
101
+ try {
102
+ const { stdout } = await execAsync$1("wezterm cli list --format json");
103
+ console.log(stdout);
104
+ return JSON.parse(stdout).some((pane) => String(pane.pane_id) === paneId);
105
+ } catch (error) {
106
+ console.log(error);
107
+ return false;
108
+ }
109
+ }
110
+ async function getEditorPaneId$1(targetPaneId) {
111
+ try {
112
+ const data = conf.get(`wezterm.targetPane.pane_${targetPaneId}`);
113
+ if (typeof data === "object" && data !== null && "editorPaneId" in data) return String(data.editorPaneId);
114
+ return "";
115
+ } catch (error) {
116
+ console.log(error);
117
+ return "";
118
+ }
119
+ }
120
+ async function clearEditorPaneId$1(targetPaneId) {
121
+ try {
122
+ const editorPaneId = await getEditorPaneId$1(targetPaneId);
123
+ conf.delete(`wezterm.targetPane.pane_${targetPaneId}`);
124
+ if (editorPaneId) conf.delete(`wezterm.editorPane.pane_${editorPaneId}`);
125
+ } catch (error) {
126
+ console.log(error);
127
+ }
128
+ }
129
+ async function focusPane$1(paneId) {
130
+ await execAsync$1(`wezterm cli activate-pane --pane-id '${paneId}'`);
131
+ }
132
+ async function markAsEditorPane$1(editorPaneId, targetPaneId) {
133
+ try {
134
+ conf.set(`wezterm.targetPane.pane_${targetPaneId}`, { editorPaneId });
135
+ conf.set(`wezterm.editorPane.pane_${editorPaneId}`, { targetPaneId });
136
+ } catch (error) {
137
+ console.log(error);
138
+ }
139
+ }
140
+ async function getTargetPaneId$1(editorPaneId) {
141
+ try {
142
+ const data = conf.get(`wezterm.editorPane.pane_${editorPaneId}`);
143
+ if (typeof data === "object" && data !== null && "targetPaneId" in data) return String(data.targetPaneId);
144
+ return "";
145
+ } catch (error) {
146
+ console.log(error);
147
+ return "";
148
+ }
149
+ }
150
+ function isEditorPaneFromConf(paneId) {
151
+ try {
152
+ return conf.has(`wezterm.editorPane.pane_${paneId}`);
153
+ } catch (error) {
154
+ console.log(error);
155
+ return false;
156
+ }
157
+ }
158
+ async function appendToQuoteText(paneId, content) {
159
+ try {
160
+ const data = conf.get(`wezterm.targetPane.pane_${paneId}`);
161
+ let newData;
162
+ if (typeof data === "object" && data !== null) {
163
+ const existingQuoteText = "quote_text" in data ? String(data.quote_text) : "";
164
+ const newQuoteText = existingQuoteText.trim() !== "" ? `${existingQuoteText}\n\n${content}` : content;
165
+ newData = {
166
+ ...data,
167
+ quote_text: newQuoteText
168
+ };
169
+ } else newData = { quote_text: content };
170
+ conf.set(`wezterm.targetPane.pane_${paneId}`, newData);
171
+ } catch (error) {
172
+ console.log(error);
173
+ }
174
+ }
175
+ async function getQuoteText(paneId) {
176
+ try {
177
+ const data = conf.get(`wezterm.targetPane.pane_${paneId}`);
178
+ if (typeof data === "object" && data !== null && "quote_text" in data) return String(data.quote_text);
179
+ return "";
180
+ } catch (error) {
181
+ console.log(error);
182
+ return "";
183
+ }
184
+ }
185
+ async function clearQuoteText(paneId) {
186
+ try {
187
+ const key = `wezterm.targetPane.pane_${paneId}.quote_text`;
188
+ if (conf.has(key)) conf.delete(key);
189
+ } catch (error) {
190
+ console.log(error);
191
+ }
192
+ }
193
+
194
+ //#endregion
195
+ //#region src/utils/sendConfig.ts
196
+ const VALID_MUX_TYPES = ["tmux", "wezterm"];
197
+ function readSendConfig() {
198
+ const targetPane = process.env.EDITPROMPT_TARGET_PANE;
199
+ const muxValue = process.env.EDITPROMPT_MUX || "tmux";
200
+ if (!VALID_MUX_TYPES.includes(muxValue)) throw new Error(`Invalid EDITPROMPT_MUX value: ${muxValue}. Must be one of: ${VALID_MUX_TYPES.join(", ")}`);
201
+ return {
202
+ targetPane,
203
+ mux: muxValue,
204
+ alwaysCopy: process.env.EDITPROMPT_ALWAYS_COPY === "1"
205
+ };
206
+ }
207
+
208
+ //#endregion
209
+ //#region src/modes/capture.ts
210
+ async function runCaptureMode() {
211
+ try {
212
+ const config = readSendConfig();
213
+ if (!config.targetPane) {
214
+ console.error("Error: EDITPROMPT_TARGET_PANE environment variable is required in capture mode");
215
+ process.exit(1);
216
+ }
217
+ let quoteContent;
218
+ if (config.mux === "tmux") {
219
+ quoteContent = await getQuoteVariableContent(config.targetPane);
220
+ await clearQuoteVariable(config.targetPane);
221
+ } else {
222
+ quoteContent = await getQuoteText(config.targetPane);
223
+ await clearQuoteText(config.targetPane);
224
+ }
225
+ process.stdout.write(quoteContent.replace(/\n{3,}$/, "\n\n"));
226
+ process.exit(0);
227
+ } catch (error) {
228
+ console.error(`Error: ${error instanceof Error ? error.message : "Unknown error"}`);
229
+ process.exit(1);
230
+ }
231
+ }
12
232
 
13
233
  //#endregion
14
234
  //#region src/config/constants.ts
@@ -172,19 +392,158 @@ async function handleContentDelivery(content, mux, targetPane, alwaysCopy) {
172
392
  //#endregion
173
393
  //#region src/modes/openEditor.ts
174
394
  async function runOpenEditorMode(options) {
175
- const sendConfig = {
176
- targetPane: options.targetPane,
177
- mux: options.mux,
178
- alwaysCopy: options.alwaysCopy
179
- };
180
- console.log("Opening editor...");
181
- const content = await openEditorAndGetContent(options.editor, options.env, sendConfig);
182
- if (!content) {
183
- console.log("No content entered. Exiting.");
184
- return;
395
+ if (options.targetPane && options.mux === "tmux") try {
396
+ const currentPaneId = await getCurrentPaneId();
397
+ await saveEditorPaneId(options.targetPane, currentPaneId);
398
+ await markAsEditorPane(currentPaneId, options.targetPane);
399
+ } catch {}
400
+ else if (options.targetPane && options.mux === "wezterm") try {
401
+ const currentPaneId = await getCurrentPaneId$1();
402
+ await markAsEditorPane$1(currentPaneId, options.targetPane);
403
+ } catch {}
404
+ try {
405
+ const sendConfig = {
406
+ targetPane: options.targetPane,
407
+ mux: options.mux,
408
+ alwaysCopy: options.alwaysCopy
409
+ };
410
+ console.log("Opening editor...");
411
+ const content = await openEditorAndGetContent(options.editor, options.env, sendConfig);
412
+ if (!content) {
413
+ console.log("No content entered. Exiting.");
414
+ return;
415
+ }
416
+ try {
417
+ await handleContentDelivery(content, options.mux, options.targetPane, options.alwaysCopy);
418
+ } catch (error) {
419
+ console.error(`Error: ${error instanceof Error ? error.message : "Unknown error"}`);
420
+ process.exit(1);
421
+ }
422
+ } finally {
423
+ if (options.targetPane && options.mux === "tmux") try {
424
+ await clearEditorPaneId(options.targetPane);
425
+ } catch {}
426
+ else if (options.targetPane && options.mux === "wezterm") try {
427
+ await clearEditorPaneId$1(options.targetPane);
428
+ } catch {}
185
429
  }
430
+ }
431
+
432
+ //#endregion
433
+ //#region src/utils/quoteProcessor.ts
434
+ /**
435
+ * Calculate the minimum leading whitespace count across all non-empty lines
436
+ */
437
+ function getMinLeadingWhitespace(lines) {
438
+ let min = 99;
439
+ for (const line of lines) {
440
+ if (line.length === 0) continue;
441
+ const match = line.match(/^[ \t]*/);
442
+ const count = match ? match[0].length : 0;
443
+ if (count < min) min = count;
444
+ }
445
+ return min === 99 ? 0 : min;
446
+ }
447
+ /**
448
+ * Check if we need a space separator between two lines
449
+ */
450
+ function needsSpaceSeparator(prevLine, currentLine) {
451
+ if (prevLine.length === 0 || currentLine.length === 0) return false;
452
+ const lastChar = prevLine[prevLine.length - 1] ?? "";
453
+ const firstChar = currentLine[0] ?? "";
454
+ const isLastCharAlpha = /[a-zA-Z]/.test(lastChar);
455
+ const isFirstCharAlpha = /[a-zA-Z]/.test(firstChar);
456
+ return isLastCharAlpha && isFirstCharAlpha;
457
+ }
458
+ /**
459
+ * Determine if two lines should be merged
460
+ */
461
+ function shouldMergeLines(prevLine, currentLine) {
462
+ if (/^[-*+]\s/.test(currentLine)) return false;
463
+ const hasColon = (line) => line.includes(":") || line.includes(":");
464
+ if (hasColon(prevLine) && hasColon(currentLine)) return false;
465
+ return true;
466
+ }
467
+ /**
468
+ * Remove common leading whitespace and merge lines
469
+ */
470
+ function removeWhitespaceAndMergeLines(lines) {
471
+ const minWhitespace = getMinLeadingWhitespace(lines);
472
+ const trimmedLines = lines.map((line) => {
473
+ if (line.length === 0) return line;
474
+ return line.slice(minWhitespace);
475
+ });
476
+ const result = [];
477
+ let currentLine = "";
478
+ for (let i = 0; i < trimmedLines.length; i++) {
479
+ const line = trimmedLines[i] ?? "";
480
+ if (i === 0) {
481
+ currentLine = line;
482
+ continue;
483
+ }
484
+ if (line.length === 0) {
485
+ result.push(currentLine);
486
+ result.push("");
487
+ currentLine = "";
488
+ continue;
489
+ }
490
+ if (currentLine.length === 0) {
491
+ currentLine = line;
492
+ continue;
493
+ }
494
+ const prevLine = trimmedLines[i - 1] ?? "";
495
+ if (shouldMergeLines(prevLine, line)) {
496
+ const separator = needsSpaceSeparator(prevLine, line) ? " " : "";
497
+ currentLine += separator + line;
498
+ } else {
499
+ result.push(currentLine);
500
+ currentLine = line;
501
+ }
502
+ }
503
+ if (currentLine !== "") result.push(currentLine);
504
+ return result;
505
+ }
506
+ /**
507
+ * Processes text for quote buffering by:
508
+ * 1. Detecting if 2nd+ lines have no leading whitespace (Pattern A) or all lines have common leading whitespace (Pattern B)
509
+ * 2. Pattern A: Remove only leading whitespace, preserve all line breaks
510
+ * 3. Pattern B: Remove common leading whitespace and merge lines (with exceptions)
511
+ * 4. Adding quote prefix ("> ") to each line
512
+ * 5. Adding two newlines at the end
513
+ */
514
+ function processQuoteText(text) {
515
+ const lines = text.replace(/^\n+|\n+$/g, "").split("\n");
516
+ const hasNoLeadingWhitespaceInLaterLines = lines.slice(1).some((line) => line.length > 0 && !line.startsWith(" ") && !line.startsWith(" "));
517
+ let processedLines;
518
+ if (hasNoLeadingWhitespaceInLaterLines) processedLines = lines.map((line) => line.trimStart());
519
+ else processedLines = removeWhitespaceAndMergeLines(lines);
520
+ return `${processedLines.map((line) => `> ${line}`).join("\n")}\n\n`;
521
+ }
522
+
523
+ //#endregion
524
+ //#region src/modes/quote.ts
525
+ async function readStdin() {
526
+ return new Promise((resolve, reject) => {
527
+ const chunks = [];
528
+ process.stdin.on("data", (chunk) => {
529
+ chunks.push(chunk);
530
+ });
531
+ process.stdin.on("end", () => {
532
+ resolve(Buffer.concat(chunks).toString("utf8"));
533
+ });
534
+ process.stdin.on("error", (error) => {
535
+ reject(error);
536
+ });
537
+ });
538
+ }
539
+ async function runQuoteMode(mux, targetPaneId, rawContent) {
186
540
  try {
187
- await handleContentDelivery(content, options.mux, options.targetPane, options.alwaysCopy);
541
+ let selection;
542
+ if (rawContent !== void 0) selection = rawContent;
543
+ else selection = await readStdin();
544
+ const processedText = processQuoteText(selection);
545
+ if (mux === "tmux") await appendToQuoteVariable(targetPaneId, processedText);
546
+ else if (mux === "wezterm") await appendToQuoteText(targetPaneId, processedText);
188
547
  } catch (error) {
189
548
  console.error(`Error: ${error instanceof Error ? error.message : "Unknown error"}`);
190
549
  process.exit(1);
@@ -192,17 +551,60 @@ async function runOpenEditorMode(options) {
192
551
  }
193
552
 
194
553
  //#endregion
195
- //#region src/utils/sendConfig.ts
196
- const VALID_MUX_TYPES = ["tmux", "wezterm"];
197
- function readSendConfig() {
198
- const targetPane = process.env.EDITPROMPT_TARGET_PANE;
199
- const muxValue = process.env.EDITPROMPT_MUX || "tmux";
200
- if (!VALID_MUX_TYPES.includes(muxValue)) throw new Error(`Invalid EDITPROMPT_MUX value: ${muxValue}. Must be one of: ${VALID_MUX_TYPES.join(", ")}`);
201
- return {
202
- targetPane,
203
- mux: muxValue,
204
- alwaysCopy: process.env.EDITPROMPT_ALWAYS_COPY === "1"
205
- };
554
+ //#region src/modes/resume.ts
555
+ async function runResumeMode(targetPane, mux) {
556
+ if (mux === "wezterm") {
557
+ const currentPaneId$1 = await getCurrentPaneId$1();
558
+ if (isEditorPaneFromConf(currentPaneId$1)) {
559
+ console.log("isEditor");
560
+ const originalTargetPaneId = await getTargetPaneId$1(currentPaneId$1);
561
+ if (!originalTargetPaneId) {
562
+ console.log("Not found originalTargetPaneId");
563
+ process.exit(1);
564
+ }
565
+ if (!await checkPaneExists$1(originalTargetPaneId)) {
566
+ console.log("Not exist originalTargetPaneId");
567
+ process.exit(1);
568
+ }
569
+ await focusPane$1(originalTargetPaneId);
570
+ process.exit(0);
571
+ }
572
+ console.log("not isEditor");
573
+ const editorPaneId$1 = await getEditorPaneId$1(targetPane);
574
+ console.log(`wezterm editorPaneId: ${editorPaneId$1}`);
575
+ if (editorPaneId$1 === "") {
576
+ console.log("Not found editorPaneId");
577
+ process.exit(1);
578
+ }
579
+ if (!await checkPaneExists$1(editorPaneId$1)) {
580
+ console.log("Not exist editorPaneId");
581
+ await clearEditorPaneId$1(targetPane);
582
+ process.exit(1);
583
+ }
584
+ try {
585
+ await focusPane$1(editorPaneId$1);
586
+ process.exit(0);
587
+ } catch (error) {
588
+ console.log(`Can't focus editorPaneId: ${editorPaneId$1}\nerror: ${error}`);
589
+ process.exit(1);
590
+ }
591
+ }
592
+ const currentPaneId = await getCurrentPaneId();
593
+ if (await isEditorPane(currentPaneId)) {
594
+ const originalTargetPaneId = await getTargetPaneId(currentPaneId);
595
+ if (!originalTargetPaneId) process.exit(1);
596
+ if (!await checkPaneExists(originalTargetPaneId)) process.exit(1);
597
+ await focusPane(originalTargetPaneId);
598
+ process.exit(0);
599
+ }
600
+ const editorPaneId = await getEditorPaneId(targetPane);
601
+ if (editorPaneId === "") process.exit(1);
602
+ if (!await checkPaneExists(editorPaneId)) {
603
+ await clearEditorPaneId(targetPane);
604
+ process.exit(1);
605
+ }
606
+ await focusPane(editorPaneId);
607
+ process.exit(0);
206
608
  }
207
609
 
208
610
  //#endregion
@@ -247,6 +649,10 @@ await cli(process.argv.slice(2), {
247
649
  name: "editprompt",
248
650
  description: "A CLI tool that lets you write prompts for Claude Code using your favorite text editor",
249
651
  args: {
652
+ resume: {
653
+ description: "Resume existing editor pane instead of creating new one",
654
+ type: "boolean"
655
+ },
250
656
  editor: {
251
657
  short: "e",
252
658
  description: "Editor to use (overrides $EDITOR)",
@@ -276,10 +682,55 @@ await cli(process.argv.slice(2), {
276
682
  "always-copy": {
277
683
  description: "Always copy content to clipboard, even if tmux pane is available",
278
684
  type: "boolean"
685
+ },
686
+ quote: {
687
+ description: "Quote buffering mode - add quoted text to pane variable",
688
+ type: "boolean"
689
+ },
690
+ capture: {
691
+ description: "Capture mode - copy pane variable to clipboard and clear",
692
+ type: "boolean"
279
693
  }
280
694
  },
281
695
  async run(ctx) {
282
696
  try {
697
+ if (ctx.values.resume) {
698
+ if (!ctx.values["target-pane"]) {
699
+ console.error("Error: --target-pane is required when using --resume");
700
+ process.exit(1);
701
+ }
702
+ const mux$1 = ctx.values.mux || "tmux";
703
+ if (!isMuxType(mux$1)) {
704
+ console.error(`Error: Invalid mux type '${mux$1}'. Supported values: tmux, wezterm`);
705
+ process.exit(1);
706
+ }
707
+ await runResumeMode(ctx.values["target-pane"], mux$1);
708
+ return;
709
+ }
710
+ if (ctx.values.quote) {
711
+ if (!ctx.values["target-pane"]) {
712
+ console.error("Error: --target-pane is required when using --quote");
713
+ process.exit(1);
714
+ }
715
+ const muxValue$1 = ctx.values.mux || "tmux";
716
+ if (!isMuxType(muxValue$1)) {
717
+ console.error(`Error: Invalid mux type '${muxValue$1}'. Supported values: tmux, wezterm`);
718
+ process.exit(1);
719
+ }
720
+ if (muxValue$1 === "wezterm") {
721
+ const rawContent$1 = extractRawContent(ctx.rest, ctx.positionals);
722
+ if (rawContent$1 === void 0) {
723
+ console.error("Error: Text content is required for quote mode with wezterm. Use: editprompt --quote --mux wezterm --target-pane <id> -- \"<text>\"");
724
+ process.exit(1);
725
+ }
726
+ await runQuoteMode(muxValue$1, ctx.values["target-pane"], rawContent$1);
727
+ } else await runQuoteMode(muxValue$1, ctx.values["target-pane"]);
728
+ return;
729
+ }
730
+ if (ctx.values.capture) {
731
+ await runCaptureMode();
732
+ return;
733
+ }
283
734
  const rawContent = extractRawContent(ctx.rest, ctx.positionals);
284
735
  if (rawContent !== void 0) {
285
736
  await runSendOnlyMode(rawContent);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "editprompt",
3
- "version": "0.5.2",
3
+ "version": "0.7.0",
4
4
  "author": "eetann",
5
5
  "description": "A CLI tool that lets you write prompts for CLI tools using your favorite text editor",
6
6
  "license": "MIT",
@@ -56,6 +56,7 @@
56
56
  },
57
57
  "dependencies": {
58
58
  "clipboardy": "^4.0.0",
59
+ "conf": "^15.0.2",
59
60
  "gunshi": "^0.26.3"
60
61
  },
61
62
  "peerDependencies": {