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.
- package/README.md +240 -199
- package/dist/index.js +349 -117
- 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/
|
|
9
|
+

|
|
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
|
-
-
|
|
13
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
44
|
-
editprompt
|
|
45
|
-
```
|
|
45
|
+
### Workflow 1: Basic - Write and Send
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+

|
|
48
48
|
|
|
49
|
+
The simplest way to use editprompt:
|
|
49
50
|
|
|
50
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
60
|
+

|
|
66
61
|
|
|
67
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
###
|
|
71
|
+
### Workflow 3: Quote - Collect and Reply
|
|
81
72
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
+

|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
202
|
-
1. Open editprompt using the tmux/wezterm keybinding
|
|
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
|
-
|
|
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
|
-
|
|
228
|
+
Add this keybinding to your `.tmux.conf` to collect selected text as quotes:
|
|
220
229
|
|
|
221
|
-
|
|
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
|
-
|
|
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
|
-
|
|
241
|
+
#### Collecting Quotes in WezTerm
|
|
226
242
|
|
|
227
|
-
|
|
243
|
+
Add this event handler and keybinding to your `wezterm.lua` to collect selected text as quotes:
|
|
228
244
|
|
|
229
245
|
```lua
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
|
|
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
|
-
|
|
280
|
+
#### Capturing Collected Quotes
|
|
241
281
|
|
|
242
|
-
|
|
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
|
-
|
|
250
|
-
editprompt --
|
|
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
|
-
|
|
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
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
-
|
|
263
|
-
bun install
|
|
304
|
+
#### Neovim Integration Example
|
|
264
305
|
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
269
|
-
|
|
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
|
-
|
|
272
|
-
|
|
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
|
-
###
|
|
351
|
+
### Environment Variables
|
|
276
352
|
|
|
277
|
-
|
|
353
|
+
#### Editor Selection
|
|
278
354
|
|
|
279
|
-
|
|
355
|
+
editprompt respects the following editor priority:
|
|
280
356
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
```
|
|
357
|
+
1. `--editor/-e` command line option
|
|
358
|
+
2. `$EDITOR` environment variable
|
|
359
|
+
3. Default: `vim`
|
|
285
360
|
|
|
286
|
-
####
|
|
361
|
+
#### EDITPROMPT Environment Variable
|
|
287
362
|
|
|
288
|
-
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
-
####
|
|
376
|
+
#### Custom Environment Variables
|
|
332
377
|
|
|
333
|
-
|
|
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
|
-
|
|
380
|
+
```bash
|
|
381
|
+
# Single environment variable
|
|
382
|
+
editprompt --env THEME=dark
|
|
344
383
|
|
|
345
|
-
|
|
384
|
+
# Multiple environment variables
|
|
385
|
+
editprompt --env THEME=dark --env FOO=fooooo
|
|
346
386
|
|
|
347
|
-
|
|
387
|
+
# Useful for editor-specific configurations
|
|
388
|
+
editprompt --env NVIM_CONFIG=minimal
|
|
389
|
+
```
|
|
348
390
|
|
|
349
|
-
|
|
391
|
+
#### Target Pane Environment Variable
|
|
350
392
|
|
|
351
|
-
|
|
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.
|
|
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
|
|
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);
|