editprompt 0.6.0 → 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 +240 -199
  2. package/dist/index.js +349 -117
  3. package/package.json +1 -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,106 +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 (with pane resume):**
53
- ```tmux
54
- bind -n M-q run-shell '\
55
- editprompt --resume --target-pane #{pane_id} || \
56
- tmux split-window -v -l 10 -c "#{pane_current_path}" \
57
- "editprompt --editor nvim --always-copy --target-pane #{pane_id}"'
58
- ```
56
+ Perfect for one-off prompts when you need more space than a terminal input line.
59
57
 
60
- **How it works:**
61
- - **First time**: Creates a new editor pane if one doesn't exist
62
- - **Subsequent times**: Focuses the existing editor pane instead of creating a new one
63
- - **Bidirectional**: Pressing the same keybinding from within the editor pane returns you to the original target pane
58
+ ### Workflow 2: Interactive - Iterate with Editor Open
64
59
 
65
- This allows you to toggle between your target pane and editor pane using the same keybinding (`M-q`).
60
+ ![send without closing editor](https://github.com/user-attachments/assets/b0e486af-78d7-4b70-8c82-64d330c22ba1)
66
61
 
67
- **Benefits:**
68
- - Prevents pane proliferation and keeps your window management simple
69
- - Switch between your work pane and editor pane while preserving your editing content
62
+ For iterating on prompts without constantly reopening the editor:
70
63
 
71
- **Popup version:**
72
- ```tmux
73
- bind -n M-q run-shell 'tmux display-popup -E \
74
- -d "#{pane_current_path}" \
75
- -w 80% -h 65% \
76
- "editprompt --editor nvim --always-copy --target-pane #{pane_id}"'
77
- ```
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
78
68
 
69
+ Ideal for trial-and-error workflows with AI assistants.
79
70
 
80
- ### 🖼️ WezTerm Integration
71
+ ### Workflow 3: Quote - Collect and Reply
81
72
 
82
- ```lua
83
- {
84
- key = "q",
85
- mods = "OPT",
86
- action = wezterm.action_callback(function(window, pane)
87
- local target_pane_id = tostring(pane:pane_id())
88
-
89
- -- Try to resume existing editor pane
90
- local success, stdout, stderr = wezterm.run_child_process({
91
- "/bin/zsh",
92
- "-lc",
93
- string.format(
94
- "editprompt --resume --mux wezterm --target-pane %s",
95
- target_pane_id
96
- ),
97
- })
98
-
99
- -- If resume failed, create new editor pane
100
- if not success then
101
- window:perform_action(
102
- act.SplitPane({
103
- direction = "Down",
104
- size = { Cells = 10 },
105
- command = {
106
- args = {
107
- "/bin/zsh",
108
- "-lc",
109
- string.format(
110
- "editprompt --editor nvim --always-copy --mux wezterm --target-pane %s",
111
- target_pane_id
112
- ),
113
- },
114
- },
115
- }),
116
- pane
117
- )
118
- end
119
- end),
120
- },
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 -->
121
83
  ```
122
84
 
123
- **How it works:**
124
- - **First time**: Creates a new editor pane if one doesn't exist
125
- - **Subsequent times**: Focuses the existing editor pane instead of creating a new one
126
- - **Bidirectional**: Pressing the same keybinding from within the editor pane returns you to the original target pane
85
+ For replying to specific parts of AI responses:
127
86
 
128
- This allows you to toggle between your target pane and editor pane using the same keybinding (`OPT-q`).
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
129
91
 
130
- **Benefits:**
131
- - Prevents pane proliferation and keeps your window management simple
132
- - Switch between your work pane and editor pane while preserving your editing content
92
+ Perfect for addressing multiple points in long AI responses.
133
93
 
134
- **Note:** The `-lc` flag ensures your shell loads the full login environment, making `editprompt` available in your PATH.
135
94
 
136
- ### 💡 Basic Usage
95
+ ## ⚙️ Setup & Configuration
96
+
97
+ ### Basic Setup
137
98
 
138
99
  ```bash
139
100
  # Use with your default editor (from $EDITOR)
@@ -150,12 +111,67 @@ editprompt --always-copy
150
111
  editprompt --help
151
112
  ```
152
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
+
123
+
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.
153
168
 
154
- ## 📤 Send Without Closing Editor
169
+
170
+ ### Editor Integration (Send Without Closing)
155
171
 
156
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.
157
173
 
158
- ### Basic Usage
174
+ #### Command Line Usage
159
175
 
160
176
  ```bash
161
177
  # Run this command from within your editor session
@@ -164,7 +180,7 @@ editprompt -- "your content here"
164
180
 
165
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.
166
182
 
167
- ### Neovim Integration Example
183
+ #### Neovim Integration Example
168
184
 
169
185
  You can set up a convenient keybinding to send your buffer content:
170
186
 
@@ -198,155 +214,180 @@ if vim.env.EDITPROMPT then
198
214
  end
199
215
  ```
200
216
 
201
- With this configuration:
202
- 1. Open editprompt using the tmux/wezterm keybinding mentioned above
217
+ **Usage:**
218
+ 1. Open editprompt using the tmux/wezterm keybinding
203
219
  2. Write your prompt in the editor
204
220
  3. Press `<Space>x` to send the content to the target pane
205
221
  4. The buffer is automatically cleared on success
206
222
  5. Continue editing to send more content
207
223
 
224
+ ### Quote Workflow Setup
208
225
 
209
- ## ⚙️ Configuration
210
-
211
- ### 📝 Editor Selection
212
-
213
- editprompt respects the following editor priority:
214
-
215
- 1. `--editor/-e` command line option
216
- 2. `$EDITOR` environment variable
217
- 3. Default: `vim`
226
+ #### Collecting Quotes in tmux Copy Mode
218
227
 
219
- ### 🌍 Environment Variables
228
+ Add this keybinding to your `.tmux.conf` to collect selected text as quotes:
220
229
 
221
- - `EDITOR`: Your preferred text editor
230
+ ```tmux
231
+ bind-key -T copy-mode-vi C-e { send-keys -X pipe "editprompt --quote --target-pane #{pane_id}" }
232
+ ```
222
233
 
223
- ### 🔧 Editor Integration with EDITPROMPT
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
224
240
 
225
- 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.
241
+ #### Collecting Quotes in WezTerm
226
242
 
227
- #### 🔍 Example: Neovim Configuration
243
+ Add this event handler and keybinding to your `wezterm.lua` to collect selected text as quotes:
228
244
 
229
245
  ```lua
230
- -- In your Neovim config (e.g., init.lua)
231
- if vim.env.EDITPROMPT then
232
- vim.opt.wrap = true
233
- -- Load a specific colorscheme
234
- vim.cmd('colorscheme blue')
235
- end
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
+ }
236
272
  ```
237
273
 
238
- #### 🛠️ Setting Custom Environment Variables
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
239
279
 
240
- You can also pass custom environment variables to your editor:
280
+ #### Capturing Collected Quotes
241
281
 
242
- ```bash
243
- # Single environment variable
244
- editprompt --env THEME=dark
245
-
246
- # Multiple environment variables
247
- editprompt --env THEME=dark --env FOO=fooooo
282
+ Run this command from within your editor pane to retrieve all collected quotes:
248
283
 
249
- # Useful for editor-specific configurations
250
- editprompt --env NVIM_CONFIG=minimal
284
+ ```bash
285
+ editprompt --capture
251
286
  ```
252
287
 
253
- ---
288
+ This copies all collected quotes to the clipboard and clears the buffer, ready for your reply.
254
289
 
255
- ## 🔧 Development
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
256
296
 
257
- ```bash
258
- # Clone the repository
259
- git clone https://github.com/eetann/editprompt.git
260
- cd editprompt
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
261
303
 
262
- # Install dependencies
263
- bun install
304
+ #### Neovim Integration Example
264
305
 
265
- # Build
266
- bun run build
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")
267
310
 
268
- # Run tests
269
- bun test
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")
270
317
 
271
- # Development mode
272
- bun run dev
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" })
273
349
  ```
274
350
 
275
- ### 💻 Testing During Development
351
+ ### Environment Variables
276
352
 
277
- When developing, you can test the built `dist/index.js` directly:
353
+ #### Editor Selection
278
354
 
279
- #### Neovim Configuration
355
+ editprompt respects the following editor priority:
280
356
 
281
- ```diff
282
- - { "editprompt", "--", content },
283
- + { "node", vim.fn.expand("~/path/to/editprompt/dist/index.js"), "--", content },
284
- ```
357
+ 1. `--editor/-e` command line option
358
+ 2. `$EDITOR` environment variable
359
+ 3. Default: `vim`
285
360
 
286
- #### WezTerm Configuration
361
+ #### EDITPROMPT Environment Variable
287
362
 
288
- ```lua
289
- -- In your wezterm.lua
290
- local editprompt_cmd = "node " .. os.getenv("HOME") .. "/path/to/editprompt/dist/index.js"
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.
291
364
 
292
- {
293
- key = "e",
294
- mods = "OPT",
295
- action = wezterm.action_callback(function(window, pane)
296
- local target_pane_id = tostring(pane:pane_id())
297
-
298
- local success, stdout, stderr = wezterm.run_child_process({
299
- "/bin/zsh",
300
- "-lc",
301
- string.format(
302
- "%s --resume --mux wezterm --target-pane %s",
303
- editprompt_cmd,
304
- target_pane_id
305
- ),
306
- })
307
-
308
- if not success then
309
- window:perform_action(
310
- act.SplitPane({
311
- -- ...
312
- command = {
313
- args = {
314
- "/bin/zsh",
315
- "-lc",
316
- string.format(
317
- "%s --editor nvim --always-copy --mux wezterm --target-pane %s",
318
- editprompt_cmd,
319
- target_pane_id
320
- ),
321
- },
322
- },
323
- }),
324
- pane
325
- )
326
- end
327
- end),
328
- },
365
+ **Example: Neovim Configuration**
366
+
367
+ ```lua
368
+ -- In your Neovim config (e.g., init.lua)
369
+ if vim.env.EDITPROMPT then
370
+ vim.opt.wrap = true
371
+ -- Load a specific colorscheme
372
+ vim.cmd('colorscheme blue')
373
+ end
329
374
  ```
330
375
 
331
- #### tmux Configuration
376
+ #### Custom Environment Variables
332
377
 
333
- ```tmux
334
- # In your .tmux.conf
335
- set-option -g @editprompt-cmd "node ~/path/to/editprompt/dist/index.js"
336
-
337
- bind-key -n M-q run-shell '\
338
- #{@editprompt-cmd} --resume --target-pane #{pane_id} || \
339
- tmux split-window -v -l 10 -c "#{pane_current_path}" \
340
- "#{@editprompt-cmd} --editor nvim --always-copy --target-pane #{pane_id}"'
341
- ```
378
+ You can also pass custom environment variables to your editor:
342
379
 
343
- This allows you to make changes, run `bun run build`, and test immediately without reinstalling globally.
380
+ ```bash
381
+ # Single environment variable
382
+ editprompt --env THEME=dark
344
383
 
345
- ## 🔍 Technical Details
384
+ # Multiple environment variables
385
+ editprompt --env THEME=dark --env FOO=fooooo
346
386
 
347
- ### 🔄 Fallback Strategy
387
+ # Useful for editor-specific configurations
388
+ editprompt --env NVIM_CONFIG=minimal
389
+ ```
348
390
 
349
- editprompt implements a robust fallback strategy:
391
+ #### Target Pane Environment Variable
350
392
 
351
- 1. **Tmux Integration**: Direct input to tmux panes (when available)
352
- 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,114 +1,15 @@
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
- import Conf from "conf";
9
9
  import clipboardy from "clipboardy";
10
10
 
11
11
  //#region package.json
12
- var version = "0.6.0";
13
-
14
- //#endregion
15
- //#region src/config/constants.ts
16
- const TEMP_FILE_PREFIX = ".editprompt-";
17
- const TEMP_FILE_EXTENSION = ".md";
18
- const DEFAULT_EDITOR = "vim";
19
-
20
- //#endregion
21
- //#region src/utils/contentProcessor.ts
22
- function processContent(content) {
23
- let processed = content.replace(/\n$/, "");
24
- if (/@[^\n]*$/.test(processed)) processed += " ";
25
- return processed;
26
- }
27
-
28
- //#endregion
29
- //#region src/utils/envParser.ts
30
- /**
31
- * Parses environment variable strings into an object.
32
- * @param envStrings - An array of strings in the format ["KEY=VALUE", "FOO=bar"].
33
- * @returns An object of environment variable key-value pairs.
34
- */
35
- function parseEnvVars(envStrings) {
36
- if (!envStrings || envStrings.length === 0) return {};
37
- const result = {};
38
- for (const envString of envStrings) {
39
- const [key, ...valueParts] = envString.split("=");
40
- if (!key || valueParts.length === 0) throw new Error(`Invalid environment variable format: ${envString}`);
41
- result[key] = valueParts.join("=");
42
- }
43
- return result;
44
- }
45
-
46
- //#endregion
47
- //#region src/utils/tempFile.ts
48
- function getFormattedDateTime() {
49
- const now = /* @__PURE__ */ new Date();
50
- return `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, "0")}${String(now.getDate()).padStart(2, "0")}${String(now.getHours()).padStart(2, "0")}${String(now.getMinutes()).padStart(2, "0")}${String(now.getSeconds()).padStart(2, "0")}`;
51
- }
52
- async function createTempFile() {
53
- const tempDir = join(tmpdir(), "editprompt-prompts");
54
- await mkdir(tempDir, { recursive: true });
55
- const filePath = join(tempDir, `${TEMP_FILE_PREFIX}${getFormattedDateTime()}${TEMP_FILE_EXTENSION}`);
56
- await writeFile(filePath, "", "utf-8");
57
- return filePath;
58
- }
59
-
60
- //#endregion
61
- //#region src/modules/editor.ts
62
- function getEditor(editorOption) {
63
- return editorOption || process.env.EDITOR || DEFAULT_EDITOR;
64
- }
65
- async function launchEditor(editor, filePath, envVars, sendConfig) {
66
- return new Promise((resolve, reject) => {
67
- const configEnv = {};
68
- if (sendConfig) {
69
- if (sendConfig.targetPane) configEnv.EDITPROMPT_TARGET_PANE = sendConfig.targetPane;
70
- configEnv.EDITPROMPT_MUX = sendConfig.mux;
71
- configEnv.EDITPROMPT_ALWAYS_COPY = sendConfig.alwaysCopy ? "1" : "0";
72
- }
73
- const processEnv = {
74
- ...process.env,
75
- EDITPROMPT: "1",
76
- ...configEnv,
77
- ...envVars
78
- };
79
- const editorProcess = spawn(editor, [filePath], {
80
- stdio: "inherit",
81
- shell: true,
82
- env: processEnv
83
- });
84
- editorProcess.on("error", (error) => {
85
- reject(/* @__PURE__ */ new Error(`Failed to launch editor: ${error.message}`));
86
- });
87
- editorProcess.on("exit", (code) => {
88
- if (code === 0) resolve();
89
- else reject(/* @__PURE__ */ new Error(`Editor exited with code: ${code}`));
90
- });
91
- });
92
- }
93
- async function readFileContent(filePath) {
94
- try {
95
- return processContent(await readFile(filePath, "utf-8"));
96
- } catch (error) {
97
- throw new Error(`Failed to read file: ${error instanceof Error ? error.message : "Unknown error"}`);
98
- }
99
- }
100
- async function openEditorAndGetContent(editorOption, envVars, sendConfig) {
101
- const tempFilePath = await createTempFile();
102
- const editor = getEditor(editorOption);
103
- const parsedEnvVars = parseEnvVars(envVars);
104
- try {
105
- await launchEditor(editor, tempFilePath, parsedEnvVars, sendConfig);
106
- return await readFileContent(tempFilePath);
107
- } catch (error) {
108
- if (error instanceof Error) throw error;
109
- throw new Error("An unknown error occurred");
110
- }
111
- }
12
+ var version = "0.7.0";
112
13
 
113
14
  //#endregion
114
15
  //#region src/modules/tmux.ts
@@ -162,11 +63,30 @@ async function isEditorPane(paneId) {
162
63
  return false;
163
64
  }
164
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
+ }
165
84
 
166
85
  //#endregion
167
86
  //#region src/modules/wezterm.ts
168
87
  const execAsync$1 = promisify(exec);
169
- const conf = new Conf({ projectName: "editprompt" });
88
+ const projectName = process.env.NODE_ENV === "test" ? "editprompt-test" : "editprompt";
89
+ const conf = new Conf({ projectName });
170
90
  async function getCurrentPaneId$1() {
171
91
  try {
172
92
  const { stdout } = await execAsync$1("wezterm cli list --format json");
@@ -235,6 +155,179 @@ function isEditorPaneFromConf(paneId) {
235
155
  return false;
236
156
  }
237
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
+ }
232
+
233
+ //#endregion
234
+ //#region src/config/constants.ts
235
+ const TEMP_FILE_PREFIX = ".editprompt-";
236
+ const TEMP_FILE_EXTENSION = ".md";
237
+ const DEFAULT_EDITOR = "vim";
238
+
239
+ //#endregion
240
+ //#region src/utils/contentProcessor.ts
241
+ function processContent(content) {
242
+ let processed = content.replace(/\n$/, "");
243
+ if (/@[^\n]*$/.test(processed)) processed += " ";
244
+ return processed;
245
+ }
246
+
247
+ //#endregion
248
+ //#region src/utils/envParser.ts
249
+ /**
250
+ * Parses environment variable strings into an object.
251
+ * @param envStrings - An array of strings in the format ["KEY=VALUE", "FOO=bar"].
252
+ * @returns An object of environment variable key-value pairs.
253
+ */
254
+ function parseEnvVars(envStrings) {
255
+ if (!envStrings || envStrings.length === 0) return {};
256
+ const result = {};
257
+ for (const envString of envStrings) {
258
+ const [key, ...valueParts] = envString.split("=");
259
+ if (!key || valueParts.length === 0) throw new Error(`Invalid environment variable format: ${envString}`);
260
+ result[key] = valueParts.join("=");
261
+ }
262
+ return result;
263
+ }
264
+
265
+ //#endregion
266
+ //#region src/utils/tempFile.ts
267
+ function getFormattedDateTime() {
268
+ const now = /* @__PURE__ */ new Date();
269
+ return `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, "0")}${String(now.getDate()).padStart(2, "0")}${String(now.getHours()).padStart(2, "0")}${String(now.getMinutes()).padStart(2, "0")}${String(now.getSeconds()).padStart(2, "0")}`;
270
+ }
271
+ async function createTempFile() {
272
+ const tempDir = join(tmpdir(), "editprompt-prompts");
273
+ await mkdir(tempDir, { recursive: true });
274
+ const filePath = join(tempDir, `${TEMP_FILE_PREFIX}${getFormattedDateTime()}${TEMP_FILE_EXTENSION}`);
275
+ await writeFile(filePath, "", "utf-8");
276
+ return filePath;
277
+ }
278
+
279
+ //#endregion
280
+ //#region src/modules/editor.ts
281
+ function getEditor(editorOption) {
282
+ return editorOption || process.env.EDITOR || DEFAULT_EDITOR;
283
+ }
284
+ async function launchEditor(editor, filePath, envVars, sendConfig) {
285
+ return new Promise((resolve, reject) => {
286
+ const configEnv = {};
287
+ if (sendConfig) {
288
+ if (sendConfig.targetPane) configEnv.EDITPROMPT_TARGET_PANE = sendConfig.targetPane;
289
+ configEnv.EDITPROMPT_MUX = sendConfig.mux;
290
+ configEnv.EDITPROMPT_ALWAYS_COPY = sendConfig.alwaysCopy ? "1" : "0";
291
+ }
292
+ const processEnv = {
293
+ ...process.env,
294
+ EDITPROMPT: "1",
295
+ ...configEnv,
296
+ ...envVars
297
+ };
298
+ const editorProcess = spawn(editor, [filePath], {
299
+ stdio: "inherit",
300
+ shell: true,
301
+ env: processEnv
302
+ });
303
+ editorProcess.on("error", (error) => {
304
+ reject(/* @__PURE__ */ new Error(`Failed to launch editor: ${error.message}`));
305
+ });
306
+ editorProcess.on("exit", (code) => {
307
+ if (code === 0) resolve();
308
+ else reject(/* @__PURE__ */ new Error(`Editor exited with code: ${code}`));
309
+ });
310
+ });
311
+ }
312
+ async function readFileContent(filePath) {
313
+ try {
314
+ return processContent(await readFile(filePath, "utf-8"));
315
+ } catch (error) {
316
+ throw new Error(`Failed to read file: ${error instanceof Error ? error.message : "Unknown error"}`);
317
+ }
318
+ }
319
+ async function openEditorAndGetContent(editorOption, envVars, sendConfig) {
320
+ const tempFilePath = await createTempFile();
321
+ const editor = getEditor(editorOption);
322
+ const parsedEnvVars = parseEnvVars(envVars);
323
+ try {
324
+ await launchEditor(editor, tempFilePath, parsedEnvVars, sendConfig);
325
+ return await readFileContent(tempFilePath);
326
+ } catch (error) {
327
+ if (error instanceof Error) throw error;
328
+ throw new Error("An unknown error occurred");
329
+ }
330
+ }
238
331
 
239
332
  //#endregion
240
333
  //#region src/modules/process.ts
@@ -336,6 +429,127 @@ async function runOpenEditorMode(options) {
336
429
  }
337
430
  }
338
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) {
540
+ try {
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);
547
+ } catch (error) {
548
+ console.error(`Error: ${error instanceof Error ? error.message : "Unknown error"}`);
549
+ process.exit(1);
550
+ }
551
+ }
552
+
339
553
  //#endregion
340
554
  //#region src/modes/resume.ts
341
555
  async function runResumeMode(targetPane, mux) {
@@ -393,20 +607,6 @@ async function runResumeMode(targetPane, mux) {
393
607
  process.exit(0);
394
608
  }
395
609
 
396
- //#endregion
397
- //#region src/utils/sendConfig.ts
398
- const VALID_MUX_TYPES = ["tmux", "wezterm"];
399
- function readSendConfig() {
400
- const targetPane = process.env.EDITPROMPT_TARGET_PANE;
401
- const muxValue = process.env.EDITPROMPT_MUX || "tmux";
402
- if (!VALID_MUX_TYPES.includes(muxValue)) throw new Error(`Invalid EDITPROMPT_MUX value: ${muxValue}. Must be one of: ${VALID_MUX_TYPES.join(", ")}`);
403
- return {
404
- targetPane,
405
- mux: muxValue,
406
- alwaysCopy: process.env.EDITPROMPT_ALWAYS_COPY === "1"
407
- };
408
- }
409
-
410
610
  //#endregion
411
611
  //#region src/modes/sendOnly.ts
412
612
  async function runSendOnlyMode(rawContent) {
@@ -482,6 +682,14 @@ await cli(process.argv.slice(2), {
482
682
  "always-copy": {
483
683
  description: "Always copy content to clipboard, even if tmux pane is available",
484
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"
485
693
  }
486
694
  },
487
695
  async run(ctx) {
@@ -499,6 +707,30 @@ await cli(process.argv.slice(2), {
499
707
  await runResumeMode(ctx.values["target-pane"], mux$1);
500
708
  return;
501
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
+ }
502
734
  const rawContent = extractRawContent(ctx.rest, ctx.positionals);
503
735
  if (rawContent !== void 0) {
504
736
  await runSendOnlyMode(rawContent);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "editprompt",
3
- "version": "0.6.0",
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",