editprompt 0.6.0 → 0.8.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 +255 -199
- package/dist/index.js +432 -139
- 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,21 +111,91 @@ editprompt --always-copy
|
|
|
150
111
|
editprompt --help
|
|
151
112
|
```
|
|
152
113
|
|
|
114
|
+
### Tmux Integration
|
|
153
115
|
|
|
154
|
-
|
|
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.
|
|
168
|
+
|
|
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
|
|
162
178
|
editprompt -- "your content here"
|
|
179
|
+
# Sends content to target pane and moves focus there
|
|
180
|
+
|
|
181
|
+
editprompt --auto-send -- "your content here"
|
|
182
|
+
# Sends content, automatically submits it (presses Enter), and returns focus to editor pane
|
|
183
|
+
# Perfect for iterating on prompts without leaving your editor
|
|
184
|
+
|
|
185
|
+
editprompt --auto-send --send-key "C-m" -- "your content here"
|
|
186
|
+
# Customize the key to send after content (tmux format example)
|
|
187
|
+
# WezTerm example: --send-key "\r" (default for WezTerm is \r, tmux default is Enter)
|
|
163
188
|
```
|
|
164
189
|
|
|
165
190
|
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
191
|
|
|
167
|
-
|
|
192
|
+
**Options:**
|
|
193
|
+
- `--auto-send`: Automatically sends the content and returns focus to your editor pane (requires multiplexer)
|
|
194
|
+
- `--send-key <key>`: Customize the key to send after content (requires `--auto-send`)
|
|
195
|
+
- tmux format: `Enter` (default), `C-a`, etc.
|
|
196
|
+
- WezTerm format: `\r` (default), `\x01`, etc.
|
|
197
|
+
|
|
198
|
+
#### Neovim Integration Example
|
|
168
199
|
|
|
169
200
|
You can set up a convenient keybinding to send your buffer content:
|
|
170
201
|
|
|
@@ -198,155 +229,180 @@ if vim.env.EDITPROMPT then
|
|
|
198
229
|
end
|
|
199
230
|
```
|
|
200
231
|
|
|
201
|
-
|
|
202
|
-
1. Open editprompt using the tmux/wezterm keybinding
|
|
232
|
+
**Usage:**
|
|
233
|
+
1. Open editprompt using the tmux/wezterm keybinding
|
|
203
234
|
2. Write your prompt in the editor
|
|
204
235
|
3. Press `<Space>x` to send the content to the target pane
|
|
205
236
|
4. The buffer is automatically cleared on success
|
|
206
237
|
5. Continue editing to send more content
|
|
207
238
|
|
|
239
|
+
### Quote Workflow Setup
|
|
208
240
|
|
|
209
|
-
|
|
241
|
+
#### Collecting Quotes in tmux Copy Mode
|
|
210
242
|
|
|
211
|
-
|
|
243
|
+
Add this keybinding to your `.tmux.conf` to collect selected text as quotes:
|
|
212
244
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
2. `$EDITOR` environment variable
|
|
217
|
-
3. Default: `vim`
|
|
218
|
-
|
|
219
|
-
### 🌍 Environment Variables
|
|
220
|
-
|
|
221
|
-
- `EDITOR`: Your preferred text editor
|
|
245
|
+
```tmux
|
|
246
|
+
bind-key -T copy-mode-vi C-e { send-keys -X pipe "editprompt --quote --target-pane #{pane_id}" }
|
|
247
|
+
```
|
|
222
248
|
|
|
223
|
-
|
|
249
|
+
**Usage:**
|
|
250
|
+
1. Enter tmux copy mode (`prefix + [`)
|
|
251
|
+
2. Select text using vi-mode keybindings
|
|
252
|
+
3. Press `Ctrl-e` to add the selection as a quote
|
|
253
|
+
4. Repeat to collect multiple quotes
|
|
254
|
+
5. All quotes are stored in a pane variable associated with the target pane
|
|
224
255
|
|
|
225
|
-
|
|
256
|
+
#### Collecting Quotes in WezTerm
|
|
226
257
|
|
|
227
|
-
|
|
258
|
+
Add this event handler and keybinding to your `wezterm.lua` to collect selected text as quotes:
|
|
228
259
|
|
|
229
260
|
```lua
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
261
|
+
local wezterm = require("wezterm")
|
|
262
|
+
|
|
263
|
+
wezterm.on("editprompt-quote", function(window, pane)
|
|
264
|
+
local text = window:get_selection_text_for_pane(pane)
|
|
265
|
+
local target_pane_id = tostring(pane:pane_id())
|
|
266
|
+
|
|
267
|
+
wezterm.run_child_process({
|
|
268
|
+
"/bin/zsh",
|
|
269
|
+
"-lc",
|
|
270
|
+
string.format(
|
|
271
|
+
"editprompt --quote --mux wezterm --target-pane %s -- %s",
|
|
272
|
+
target_pane_id,
|
|
273
|
+
wezterm.shell_quote_arg(text)
|
|
274
|
+
),
|
|
275
|
+
})
|
|
276
|
+
end)
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
keys = {
|
|
280
|
+
{
|
|
281
|
+
key = "e",
|
|
282
|
+
mods = "CTRL",
|
|
283
|
+
action = wezterm.action.EmitEvent("editprompt-quote"),
|
|
284
|
+
},
|
|
285
|
+
},
|
|
286
|
+
}
|
|
236
287
|
```
|
|
237
288
|
|
|
238
|
-
|
|
289
|
+
**Usage:**
|
|
290
|
+
1. Select text in WezTerm (by dragging with mouse or using copy mode)
|
|
291
|
+
2. Press `Ctrl-e` to add the selection as a quote
|
|
292
|
+
3. Repeat to collect multiple quotes
|
|
293
|
+
4. All quotes are stored in a configuration file associated with the target pane
|
|
239
294
|
|
|
240
|
-
|
|
295
|
+
#### Capturing Collected Quotes
|
|
241
296
|
|
|
242
|
-
|
|
243
|
-
# Single environment variable
|
|
244
|
-
editprompt --env THEME=dark
|
|
245
|
-
|
|
246
|
-
# Multiple environment variables
|
|
247
|
-
editprompt --env THEME=dark --env FOO=fooooo
|
|
297
|
+
Run this command from within your editor pane to retrieve all collected quotes:
|
|
248
298
|
|
|
249
|
-
|
|
250
|
-
editprompt --
|
|
299
|
+
```bash
|
|
300
|
+
editprompt --capture
|
|
251
301
|
```
|
|
252
302
|
|
|
253
|
-
|
|
303
|
+
This copies all collected quotes to the clipboard and clears the buffer, ready for your reply.
|
|
254
304
|
|
|
255
|
-
|
|
305
|
+
**Complete workflow:**
|
|
306
|
+
1. AI responds with multiple points
|
|
307
|
+
2. Select each point in copy mode and press `Ctrl-e`
|
|
308
|
+
3. Open your editor pane and run `editprompt --capture`
|
|
309
|
+
4. Edit the quoted text with your responses
|
|
310
|
+
5. Send to AI
|
|
256
311
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
312
|
+
**How quote buffering works:**
|
|
313
|
+
- **tmux**: Quotes are stored in pane variables, automatically cleaned up when the pane closes
|
|
314
|
+
- **WezTerm**: Quotes are stored in a configuration file associated with the pane
|
|
315
|
+
- Text is intelligently processed: removes common indentation, handles line breaks smartly
|
|
316
|
+
- Each quote is prefixed with `> ` in markdown quote format
|
|
317
|
+
- Multiple quotes are separated with blank lines
|
|
261
318
|
|
|
262
|
-
|
|
263
|
-
bun install
|
|
319
|
+
#### Neovim Integration Example
|
|
264
320
|
|
|
265
|
-
|
|
266
|
-
|
|
321
|
+
You can set up a convenient keybinding to capture your quote content:
|
|
322
|
+
```lua
|
|
323
|
+
vim.keymap.set("n", "<Space>X", function()
|
|
324
|
+
vim.cmd("update")
|
|
267
325
|
|
|
268
|
-
|
|
269
|
-
|
|
326
|
+
vim.system({ "editprompt", "--capture" }, { text = true }, function(obj)
|
|
327
|
+
vim.schedule(function()
|
|
328
|
+
if obj.code == 0 then
|
|
329
|
+
vim.cmd("silent write")
|
|
330
|
+
-- Split stdout by lines
|
|
331
|
+
local output_lines = vim.split(obj.stdout, "\n")
|
|
332
|
+
|
|
333
|
+
local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false)
|
|
334
|
+
local is_empty = #lines == 1 and lines[1] == ""
|
|
335
|
+
|
|
336
|
+
if is_empty then
|
|
337
|
+
-- If empty, overwrite from the beginning
|
|
338
|
+
vim.api.nvim_buf_set_lines(0, 0, -1, false, output_lines)
|
|
339
|
+
vim.cmd("normal 2j")
|
|
340
|
+
else
|
|
341
|
+
-- If not empty, append to the end
|
|
342
|
+
table.insert(output_lines, 1, "")
|
|
343
|
+
local line_count = vim.api.nvim_buf_line_count(0)
|
|
344
|
+
vim.api.nvim_buf_set_lines(
|
|
345
|
+
0,
|
|
346
|
+
line_count,
|
|
347
|
+
line_count,
|
|
348
|
+
false,
|
|
349
|
+
output_lines
|
|
350
|
+
)
|
|
351
|
+
vim.cmd("normal 4j")
|
|
352
|
+
end
|
|
270
353
|
|
|
271
|
-
|
|
272
|
-
|
|
354
|
+
vim.cmd("silent write")
|
|
355
|
+
else
|
|
356
|
+
vim.notify(
|
|
357
|
+
"editprompt failed: " .. (obj.stderr or "unknown error"),
|
|
358
|
+
vim.log.levels.ERROR
|
|
359
|
+
)
|
|
360
|
+
end
|
|
361
|
+
end)
|
|
362
|
+
end)
|
|
363
|
+
end, { silent = true, desc = "Capture from editprompt quote mode" })
|
|
273
364
|
```
|
|
274
365
|
|
|
275
|
-
###
|
|
366
|
+
### Environment Variables
|
|
276
367
|
|
|
277
|
-
|
|
368
|
+
#### Editor Selection
|
|
278
369
|
|
|
279
|
-
|
|
370
|
+
editprompt respects the following editor priority:
|
|
280
371
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
```
|
|
372
|
+
1. `--editor/-e` command line option
|
|
373
|
+
2. `$EDITOR` environment variable
|
|
374
|
+
3. Default: `vim`
|
|
285
375
|
|
|
286
|
-
####
|
|
376
|
+
#### EDITPROMPT Environment Variable
|
|
287
377
|
|
|
288
|
-
|
|
289
|
-
-- In your wezterm.lua
|
|
290
|
-
local editprompt_cmd = "node " .. os.getenv("HOME") .. "/path/to/editprompt/dist/index.js"
|
|
378
|
+
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
379
|
|
|
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
|
-
},
|
|
329
|
-
```
|
|
380
|
+
**Example: Neovim Configuration**
|
|
330
381
|
|
|
331
|
-
|
|
382
|
+
```lua
|
|
383
|
+
-- In your Neovim config (e.g., init.lua)
|
|
384
|
+
if vim.env.EDITPROMPT then
|
|
385
|
+
vim.opt.wrap = true
|
|
386
|
+
-- Load a specific colorscheme
|
|
387
|
+
vim.cmd('colorscheme blue')
|
|
388
|
+
end
|
|
389
|
+
```
|
|
332
390
|
|
|
333
|
-
|
|
334
|
-
# In your .tmux.conf
|
|
335
|
-
set-option -g @editprompt-cmd "node ~/path/to/editprompt/dist/index.js"
|
|
391
|
+
#### Custom Environment Variables
|
|
336
392
|
|
|
337
|
-
|
|
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
|
-
```
|
|
393
|
+
You can also pass custom environment variables to your editor:
|
|
342
394
|
|
|
343
|
-
|
|
395
|
+
```bash
|
|
396
|
+
# Single environment variable
|
|
397
|
+
editprompt --env THEME=dark
|
|
344
398
|
|
|
345
|
-
|
|
399
|
+
# Multiple environment variables
|
|
400
|
+
editprompt --env THEME=dark --env FOO=fooooo
|
|
346
401
|
|
|
347
|
-
|
|
402
|
+
# Useful for editor-specific configurations
|
|
403
|
+
editprompt --env NVIM_CONFIG=minimal
|
|
404
|
+
```
|
|
348
405
|
|
|
349
|
-
|
|
406
|
+
#### Target Pane Environment Variable
|
|
350
407
|
|
|
351
|
-
|
|
352
|
-
2. **Clipboard**: Copy content to clipboard with user notification
|
|
408
|
+
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.8.0";
|
|
112
13
|
|
|
113
14
|
//#endregion
|
|
114
15
|
//#region src/modules/tmux.ts
|
|
@@ -162,11 +63,38 @@ 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
|
+
}
|
|
84
|
+
async function sendKeyToTmuxPane(paneId, key) {
|
|
85
|
+
await execAsync$2(`tmux send-keys -t '${paneId}' ${key}`);
|
|
86
|
+
}
|
|
87
|
+
async function sendContentToTmuxPaneNoFocus(paneId, content) {
|
|
88
|
+
await execAsync$2(`tmux if-shell -t '${paneId}' '[ "#{pane_in_mode}" = "1" ]' "copy-mode -q -t '${paneId}'"`);
|
|
89
|
+
await execAsync$2(`tmux send-keys -t '${paneId}' -- '${content.replace(/'/g, "'\\''")}'`);
|
|
90
|
+
console.log(`Content sent to tmux pane: ${paneId}`);
|
|
91
|
+
}
|
|
165
92
|
|
|
166
93
|
//#endregion
|
|
167
94
|
//#region src/modules/wezterm.ts
|
|
168
95
|
const execAsync$1 = promisify(exec);
|
|
169
|
-
const
|
|
96
|
+
const projectName = process.env.NODE_ENV === "test" ? "editprompt-test" : "editprompt";
|
|
97
|
+
const conf = new Conf({ projectName });
|
|
170
98
|
async function getCurrentPaneId$1() {
|
|
171
99
|
try {
|
|
172
100
|
const { stdout } = await execAsync$1("wezterm cli list --format json");
|
|
@@ -235,6 +163,186 @@ function isEditorPaneFromConf(paneId) {
|
|
|
235
163
|
return false;
|
|
236
164
|
}
|
|
237
165
|
}
|
|
166
|
+
async function appendToQuoteText(paneId, content) {
|
|
167
|
+
try {
|
|
168
|
+
const data = conf.get(`wezterm.targetPane.pane_${paneId}`);
|
|
169
|
+
let newData;
|
|
170
|
+
if (typeof data === "object" && data !== null) {
|
|
171
|
+
const existingQuoteText = "quote_text" in data ? String(data.quote_text) : "";
|
|
172
|
+
const newQuoteText = existingQuoteText.trim() !== "" ? `${existingQuoteText}\n\n${content}` : content;
|
|
173
|
+
newData = {
|
|
174
|
+
...data,
|
|
175
|
+
quote_text: newQuoteText
|
|
176
|
+
};
|
|
177
|
+
} else newData = { quote_text: content };
|
|
178
|
+
conf.set(`wezterm.targetPane.pane_${paneId}`, newData);
|
|
179
|
+
} catch (error) {
|
|
180
|
+
console.log(error);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
async function getQuoteText(paneId) {
|
|
184
|
+
try {
|
|
185
|
+
const data = conf.get(`wezterm.targetPane.pane_${paneId}`);
|
|
186
|
+
if (typeof data === "object" && data !== null && "quote_text" in data) return String(data.quote_text);
|
|
187
|
+
return "";
|
|
188
|
+
} catch (error) {
|
|
189
|
+
console.log(error);
|
|
190
|
+
return "";
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
async function clearQuoteText(paneId) {
|
|
194
|
+
try {
|
|
195
|
+
const key = `wezterm.targetPane.pane_${paneId}.quote_text`;
|
|
196
|
+
if (conf.has(key)) conf.delete(key);
|
|
197
|
+
} catch (error) {
|
|
198
|
+
console.log(error);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
async function sendKeyToWeztermPane(paneId, key) {
|
|
202
|
+
await execAsync$1(`wezterm cli send-text --no-paste --pane-id '${paneId}' $'${key}'`);
|
|
203
|
+
}
|
|
204
|
+
async function sendContentToWeztermPaneNoFocus(paneId, content) {
|
|
205
|
+
await execAsync$1(`wezterm cli send-text --no-paste --pane-id '${paneId}' -- '${content.replace(/'/g, "'\\''")}'`);
|
|
206
|
+
console.log(`Content sent to wezterm pane: ${paneId}`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
//#endregion
|
|
210
|
+
//#region src/utils/sendConfig.ts
|
|
211
|
+
const VALID_MUX_TYPES = ["tmux", "wezterm"];
|
|
212
|
+
function readSendConfig() {
|
|
213
|
+
const targetPane = process.env.EDITPROMPT_TARGET_PANE;
|
|
214
|
+
const muxValue = process.env.EDITPROMPT_MUX || "tmux";
|
|
215
|
+
if (!VALID_MUX_TYPES.includes(muxValue)) throw new Error(`Invalid EDITPROMPT_MUX value: ${muxValue}. Must be one of: ${VALID_MUX_TYPES.join(", ")}`);
|
|
216
|
+
return {
|
|
217
|
+
targetPane,
|
|
218
|
+
mux: muxValue,
|
|
219
|
+
alwaysCopy: process.env.EDITPROMPT_ALWAYS_COPY === "1"
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
//#endregion
|
|
224
|
+
//#region src/modes/capture.ts
|
|
225
|
+
async function runCaptureMode() {
|
|
226
|
+
try {
|
|
227
|
+
const config = readSendConfig();
|
|
228
|
+
if (!config.targetPane) {
|
|
229
|
+
console.error("Error: EDITPROMPT_TARGET_PANE environment variable is required in capture mode");
|
|
230
|
+
process.exit(1);
|
|
231
|
+
}
|
|
232
|
+
let quoteContent;
|
|
233
|
+
if (config.mux === "tmux") {
|
|
234
|
+
quoteContent = await getQuoteVariableContent(config.targetPane);
|
|
235
|
+
await clearQuoteVariable(config.targetPane);
|
|
236
|
+
} else {
|
|
237
|
+
quoteContent = await getQuoteText(config.targetPane);
|
|
238
|
+
await clearQuoteText(config.targetPane);
|
|
239
|
+
}
|
|
240
|
+
process.stdout.write(quoteContent.replace(/\n{3,}$/, "\n\n"));
|
|
241
|
+
process.exit(0);
|
|
242
|
+
} catch (error) {
|
|
243
|
+
console.error(`Error: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
244
|
+
process.exit(1);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
//#endregion
|
|
249
|
+
//#region src/config/constants.ts
|
|
250
|
+
const TEMP_FILE_PREFIX = ".editprompt-";
|
|
251
|
+
const TEMP_FILE_EXTENSION = ".md";
|
|
252
|
+
const DEFAULT_EDITOR = "vim";
|
|
253
|
+
|
|
254
|
+
//#endregion
|
|
255
|
+
//#region src/utils/contentProcessor.ts
|
|
256
|
+
function processContent(content) {
|
|
257
|
+
let processed = content.replace(/\n$/, "");
|
|
258
|
+
if (/@[^\n]*$/.test(processed)) processed += " ";
|
|
259
|
+
return processed;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
//#endregion
|
|
263
|
+
//#region src/utils/envParser.ts
|
|
264
|
+
/**
|
|
265
|
+
* Parses environment variable strings into an object.
|
|
266
|
+
* @param envStrings - An array of strings in the format ["KEY=VALUE", "FOO=bar"].
|
|
267
|
+
* @returns An object of environment variable key-value pairs.
|
|
268
|
+
*/
|
|
269
|
+
function parseEnvVars(envStrings) {
|
|
270
|
+
if (!envStrings || envStrings.length === 0) return {};
|
|
271
|
+
const result = {};
|
|
272
|
+
for (const envString of envStrings) {
|
|
273
|
+
const [key, ...valueParts] = envString.split("=");
|
|
274
|
+
if (!key || valueParts.length === 0) throw new Error(`Invalid environment variable format: ${envString}`);
|
|
275
|
+
result[key] = valueParts.join("=");
|
|
276
|
+
}
|
|
277
|
+
return result;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
//#endregion
|
|
281
|
+
//#region src/utils/tempFile.ts
|
|
282
|
+
function getFormattedDateTime() {
|
|
283
|
+
const now = /* @__PURE__ */ new Date();
|
|
284
|
+
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")}`;
|
|
285
|
+
}
|
|
286
|
+
async function createTempFile() {
|
|
287
|
+
const tempDir = join(tmpdir(), "editprompt-prompts");
|
|
288
|
+
await mkdir(tempDir, { recursive: true });
|
|
289
|
+
const filePath = join(tempDir, `${TEMP_FILE_PREFIX}${getFormattedDateTime()}${TEMP_FILE_EXTENSION}`);
|
|
290
|
+
await writeFile(filePath, "", "utf-8");
|
|
291
|
+
return filePath;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
//#endregion
|
|
295
|
+
//#region src/modules/editor.ts
|
|
296
|
+
function getEditor(editorOption) {
|
|
297
|
+
return editorOption || process.env.EDITOR || DEFAULT_EDITOR;
|
|
298
|
+
}
|
|
299
|
+
async function launchEditor(editor, filePath, envVars, sendConfig) {
|
|
300
|
+
return new Promise((resolve, reject) => {
|
|
301
|
+
const configEnv = {};
|
|
302
|
+
if (sendConfig) {
|
|
303
|
+
if (sendConfig.targetPane) configEnv.EDITPROMPT_TARGET_PANE = sendConfig.targetPane;
|
|
304
|
+
configEnv.EDITPROMPT_MUX = sendConfig.mux;
|
|
305
|
+
configEnv.EDITPROMPT_ALWAYS_COPY = sendConfig.alwaysCopy ? "1" : "0";
|
|
306
|
+
}
|
|
307
|
+
const processEnv = {
|
|
308
|
+
...process.env,
|
|
309
|
+
EDITPROMPT: "1",
|
|
310
|
+
...configEnv,
|
|
311
|
+
...envVars
|
|
312
|
+
};
|
|
313
|
+
const editorProcess = spawn(editor, [filePath], {
|
|
314
|
+
stdio: "inherit",
|
|
315
|
+
shell: true,
|
|
316
|
+
env: processEnv
|
|
317
|
+
});
|
|
318
|
+
editorProcess.on("error", (error) => {
|
|
319
|
+
reject(/* @__PURE__ */ new Error(`Failed to launch editor: ${error.message}`));
|
|
320
|
+
});
|
|
321
|
+
editorProcess.on("exit", (code) => {
|
|
322
|
+
if (code === 0) resolve();
|
|
323
|
+
else reject(/* @__PURE__ */ new Error(`Editor exited with code: ${code}`));
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
async function readFileContent(filePath) {
|
|
328
|
+
try {
|
|
329
|
+
return processContent(await readFile(filePath, "utf-8"));
|
|
330
|
+
} catch (error) {
|
|
331
|
+
throw new Error(`Failed to read file: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
async function openEditorAndGetContent(editorOption, envVars, sendConfig) {
|
|
335
|
+
const tempFilePath = await createTempFile();
|
|
336
|
+
const editor = getEditor(editorOption);
|
|
337
|
+
const parsedEnvVars = parseEnvVars(envVars);
|
|
338
|
+
try {
|
|
339
|
+
await launchEditor(editor, tempFilePath, parsedEnvVars, sendConfig);
|
|
340
|
+
return await readFileContent(tempFilePath);
|
|
341
|
+
} catch (error) {
|
|
342
|
+
if (error instanceof Error) throw error;
|
|
343
|
+
throw new Error("An unknown error occurred");
|
|
344
|
+
}
|
|
345
|
+
}
|
|
238
346
|
|
|
239
347
|
//#endregion
|
|
240
348
|
//#region src/modules/process.ts
|
|
@@ -270,6 +378,50 @@ async function sendContentToPane(content, mux, targetPaneId, alwaysCopy) {
|
|
|
270
378
|
}
|
|
271
379
|
}
|
|
272
380
|
|
|
381
|
+
//#endregion
|
|
382
|
+
//#region src/modes/sendOnly.ts
|
|
383
|
+
async function sendContentToPaneWithAutoSend(content, mux, targetPaneId, sendKey) {
|
|
384
|
+
if (mux === "wezterm") {
|
|
385
|
+
await sendContentToWeztermPaneNoFocus(targetPaneId, content);
|
|
386
|
+
await sendKeyToWeztermPane(targetPaneId, sendKey);
|
|
387
|
+
} else {
|
|
388
|
+
await sendContentToTmuxPaneNoFocus(targetPaneId, content);
|
|
389
|
+
await sendKeyToTmuxPane(targetPaneId, sendKey);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
async function runSendOnlyMode(rawContent, autoSend, sendKey) {
|
|
393
|
+
const content = processContent(rawContent);
|
|
394
|
+
if (!content) {
|
|
395
|
+
console.log("No content to send. Exiting.");
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
const config = readSendConfig();
|
|
399
|
+
if (!config.targetPane) {
|
|
400
|
+
console.error("Error: EDITPROMPT_TARGET_PANE environment variable is required in send-only mode");
|
|
401
|
+
process.exit(1);
|
|
402
|
+
}
|
|
403
|
+
if (autoSend) {
|
|
404
|
+
if (!config.mux) {
|
|
405
|
+
console.error("Error: --auto-send requires a multiplexer (tmux or wezterm)");
|
|
406
|
+
process.exit(1);
|
|
407
|
+
}
|
|
408
|
+
try {
|
|
409
|
+
const key = sendKey || (config.mux === "wezterm" ? "\\r" : "Enter");
|
|
410
|
+
await handleAutoSendDelivery(content, config.mux, config.targetPane, key);
|
|
411
|
+
} catch (error) {
|
|
412
|
+
console.error(`Error: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
413
|
+
process.exit(1);
|
|
414
|
+
}
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
try {
|
|
418
|
+
await handleContentDelivery(content, config.mux, config.targetPane, config.alwaysCopy);
|
|
419
|
+
} catch (error) {
|
|
420
|
+
console.error(`Error: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
421
|
+
process.exit(1);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
273
425
|
//#endregion
|
|
274
426
|
//#region src/modes/common.ts
|
|
275
427
|
function outputContent(content) {
|
|
@@ -295,6 +447,17 @@ async function handleContentDelivery(content, mux, targetPane, alwaysCopy) {
|
|
|
295
447
|
}
|
|
296
448
|
outputContent(content);
|
|
297
449
|
}
|
|
450
|
+
async function handleAutoSendDelivery(content, mux, targetPane, sendKey) {
|
|
451
|
+
if (!content || !targetPane) throw new Error("Content and target pane are required");
|
|
452
|
+
try {
|
|
453
|
+
await sendContentToPaneWithAutoSend(content, mux, targetPane, sendKey);
|
|
454
|
+
console.log("Content sent and submitted successfully!");
|
|
455
|
+
} catch (error) {
|
|
456
|
+
console.error(`Failed to send content: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
457
|
+
throw error;
|
|
458
|
+
}
|
|
459
|
+
outputContent(content);
|
|
460
|
+
}
|
|
298
461
|
|
|
299
462
|
//#endregion
|
|
300
463
|
//#region src/modes/openEditor.ts
|
|
@@ -336,6 +499,127 @@ async function runOpenEditorMode(options) {
|
|
|
336
499
|
}
|
|
337
500
|
}
|
|
338
501
|
|
|
502
|
+
//#endregion
|
|
503
|
+
//#region src/utils/quoteProcessor.ts
|
|
504
|
+
/**
|
|
505
|
+
* Calculate the minimum leading whitespace count across all non-empty lines
|
|
506
|
+
*/
|
|
507
|
+
function getMinLeadingWhitespace(lines) {
|
|
508
|
+
let min = 99;
|
|
509
|
+
for (const line of lines) {
|
|
510
|
+
if (line.length === 0) continue;
|
|
511
|
+
const match = line.match(/^[ \t]*/);
|
|
512
|
+
const count = match ? match[0].length : 0;
|
|
513
|
+
if (count < min) min = count;
|
|
514
|
+
}
|
|
515
|
+
return min === 99 ? 0 : min;
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Check if we need a space separator between two lines
|
|
519
|
+
*/
|
|
520
|
+
function needsSpaceSeparator(prevLine, currentLine) {
|
|
521
|
+
if (prevLine.length === 0 || currentLine.length === 0) return false;
|
|
522
|
+
const lastChar = prevLine[prevLine.length - 1] ?? "";
|
|
523
|
+
const firstChar = currentLine[0] ?? "";
|
|
524
|
+
const isLastCharAlpha = /[a-zA-Z]/.test(lastChar);
|
|
525
|
+
const isFirstCharAlpha = /[a-zA-Z]/.test(firstChar);
|
|
526
|
+
return isLastCharAlpha && isFirstCharAlpha;
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Determine if two lines should be merged
|
|
530
|
+
*/
|
|
531
|
+
function shouldMergeLines(prevLine, currentLine) {
|
|
532
|
+
if (/^[-*+]\s/.test(currentLine)) return false;
|
|
533
|
+
const hasColon = (line) => line.includes(":") || line.includes(":");
|
|
534
|
+
if (hasColon(prevLine) && hasColon(currentLine)) return false;
|
|
535
|
+
return true;
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Remove common leading whitespace and merge lines
|
|
539
|
+
*/
|
|
540
|
+
function removeWhitespaceAndMergeLines(lines) {
|
|
541
|
+
const minWhitespace = getMinLeadingWhitespace(lines);
|
|
542
|
+
const trimmedLines = lines.map((line) => {
|
|
543
|
+
if (line.length === 0) return line;
|
|
544
|
+
return line.slice(minWhitespace);
|
|
545
|
+
});
|
|
546
|
+
const result = [];
|
|
547
|
+
let currentLine = "";
|
|
548
|
+
for (let i = 0; i < trimmedLines.length; i++) {
|
|
549
|
+
const line = trimmedLines[i] ?? "";
|
|
550
|
+
if (i === 0) {
|
|
551
|
+
currentLine = line;
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
if (line.length === 0) {
|
|
555
|
+
result.push(currentLine);
|
|
556
|
+
result.push("");
|
|
557
|
+
currentLine = "";
|
|
558
|
+
continue;
|
|
559
|
+
}
|
|
560
|
+
if (currentLine.length === 0) {
|
|
561
|
+
currentLine = line;
|
|
562
|
+
continue;
|
|
563
|
+
}
|
|
564
|
+
const prevLine = trimmedLines[i - 1] ?? "";
|
|
565
|
+
if (shouldMergeLines(prevLine, line)) {
|
|
566
|
+
const separator = needsSpaceSeparator(prevLine, line) ? " " : "";
|
|
567
|
+
currentLine += separator + line;
|
|
568
|
+
} else {
|
|
569
|
+
result.push(currentLine);
|
|
570
|
+
currentLine = line;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
if (currentLine !== "") result.push(currentLine);
|
|
574
|
+
return result;
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Processes text for quote buffering by:
|
|
578
|
+
* 1. Detecting if 2nd+ lines have no leading whitespace (Pattern A) or all lines have common leading whitespace (Pattern B)
|
|
579
|
+
* 2. Pattern A: Remove only leading whitespace, preserve all line breaks
|
|
580
|
+
* 3. Pattern B: Remove common leading whitespace and merge lines (with exceptions)
|
|
581
|
+
* 4. Adding quote prefix ("> ") to each line
|
|
582
|
+
* 5. Adding two newlines at the end
|
|
583
|
+
*/
|
|
584
|
+
function processQuoteText(text) {
|
|
585
|
+
const lines = text.replace(/^\n+|\n+$/g, "").split("\n");
|
|
586
|
+
const hasNoLeadingWhitespaceInLaterLines = lines.slice(1).some((line) => line.length > 0 && !line.startsWith(" ") && !line.startsWith(" "));
|
|
587
|
+
let processedLines;
|
|
588
|
+
if (hasNoLeadingWhitespaceInLaterLines) processedLines = lines.map((line) => line.trimStart());
|
|
589
|
+
else processedLines = removeWhitespaceAndMergeLines(lines);
|
|
590
|
+
return `${processedLines.map((line) => `> ${line}`).join("\n")}\n\n`;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
//#endregion
|
|
594
|
+
//#region src/modes/quote.ts
|
|
595
|
+
async function readStdin() {
|
|
596
|
+
return new Promise((resolve, reject) => {
|
|
597
|
+
const chunks = [];
|
|
598
|
+
process.stdin.on("data", (chunk) => {
|
|
599
|
+
chunks.push(chunk);
|
|
600
|
+
});
|
|
601
|
+
process.stdin.on("end", () => {
|
|
602
|
+
resolve(Buffer.concat(chunks).toString("utf8"));
|
|
603
|
+
});
|
|
604
|
+
process.stdin.on("error", (error) => {
|
|
605
|
+
reject(error);
|
|
606
|
+
});
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
async function runQuoteMode(mux, targetPaneId, rawContent) {
|
|
610
|
+
try {
|
|
611
|
+
let selection;
|
|
612
|
+
if (rawContent !== void 0) selection = rawContent;
|
|
613
|
+
else selection = await readStdin();
|
|
614
|
+
const processedText = processQuoteText(selection);
|
|
615
|
+
if (mux === "tmux") await appendToQuoteVariable(targetPaneId, processedText);
|
|
616
|
+
else if (mux === "wezterm") await appendToQuoteText(targetPaneId, processedText);
|
|
617
|
+
} catch (error) {
|
|
618
|
+
console.error(`Error: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
619
|
+
process.exit(1);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
339
623
|
//#endregion
|
|
340
624
|
//#region src/modes/resume.ts
|
|
341
625
|
async function runResumeMode(targetPane, mux) {
|
|
@@ -393,41 +677,6 @@ async function runResumeMode(targetPane, mux) {
|
|
|
393
677
|
process.exit(0);
|
|
394
678
|
}
|
|
395
679
|
|
|
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
|
-
//#endregion
|
|
411
|
-
//#region src/modes/sendOnly.ts
|
|
412
|
-
async function runSendOnlyMode(rawContent) {
|
|
413
|
-
const content = processContent(rawContent);
|
|
414
|
-
if (!content) {
|
|
415
|
-
console.log("No content to send. Exiting.");
|
|
416
|
-
return;
|
|
417
|
-
}
|
|
418
|
-
const config = readSendConfig();
|
|
419
|
-
if (!config.targetPane) {
|
|
420
|
-
console.error("Error: EDITPROMPT_TARGET_PANE environment variable is required in send-only mode");
|
|
421
|
-
process.exit(1);
|
|
422
|
-
}
|
|
423
|
-
try {
|
|
424
|
-
await handleContentDelivery(content, config.mux, config.targetPane, config.alwaysCopy);
|
|
425
|
-
} catch (error) {
|
|
426
|
-
console.error(`Error: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
427
|
-
process.exit(1);
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
|
|
431
680
|
//#endregion
|
|
432
681
|
//#region src/utils/argumentParser.ts
|
|
433
682
|
/**
|
|
@@ -482,10 +731,30 @@ await cli(process.argv.slice(2), {
|
|
|
482
731
|
"always-copy": {
|
|
483
732
|
description: "Always copy content to clipboard, even if tmux pane is available",
|
|
484
733
|
type: "boolean"
|
|
734
|
+
},
|
|
735
|
+
quote: {
|
|
736
|
+
description: "Quote buffering mode - add quoted text to pane variable",
|
|
737
|
+
type: "boolean"
|
|
738
|
+
},
|
|
739
|
+
capture: {
|
|
740
|
+
description: "Capture mode - copy pane variable to clipboard and clear",
|
|
741
|
+
type: "boolean"
|
|
742
|
+
},
|
|
743
|
+
"auto-send": {
|
|
744
|
+
description: "Automatically send content and return focus to editor pane",
|
|
745
|
+
type: "boolean"
|
|
746
|
+
},
|
|
747
|
+
"send-key": {
|
|
748
|
+
description: "Key to send after content (default: Enter, requires --auto-send)",
|
|
749
|
+
type: "string"
|
|
485
750
|
}
|
|
486
751
|
},
|
|
487
752
|
async run(ctx) {
|
|
488
753
|
try {
|
|
754
|
+
if (ctx.values["send-key"] && !ctx.values["auto-send"]) {
|
|
755
|
+
console.error("Error: --send-key requires --auto-send option");
|
|
756
|
+
process.exit(1);
|
|
757
|
+
}
|
|
489
758
|
if (ctx.values.resume) {
|
|
490
759
|
if (!ctx.values["target-pane"]) {
|
|
491
760
|
console.error("Error: --target-pane is required when using --resume");
|
|
@@ -499,9 +768,33 @@ await cli(process.argv.slice(2), {
|
|
|
499
768
|
await runResumeMode(ctx.values["target-pane"], mux$1);
|
|
500
769
|
return;
|
|
501
770
|
}
|
|
771
|
+
if (ctx.values.quote) {
|
|
772
|
+
if (!ctx.values["target-pane"]) {
|
|
773
|
+
console.error("Error: --target-pane is required when using --quote");
|
|
774
|
+
process.exit(1);
|
|
775
|
+
}
|
|
776
|
+
const muxValue$1 = ctx.values.mux || "tmux";
|
|
777
|
+
if (!isMuxType(muxValue$1)) {
|
|
778
|
+
console.error(`Error: Invalid mux type '${muxValue$1}'. Supported values: tmux, wezterm`);
|
|
779
|
+
process.exit(1);
|
|
780
|
+
}
|
|
781
|
+
if (muxValue$1 === "wezterm") {
|
|
782
|
+
const rawContent$1 = extractRawContent(ctx.rest, ctx.positionals);
|
|
783
|
+
if (rawContent$1 === void 0) {
|
|
784
|
+
console.error("Error: Text content is required for quote mode with wezterm. Use: editprompt --quote --mux wezterm --target-pane <id> -- \"<text>\"");
|
|
785
|
+
process.exit(1);
|
|
786
|
+
}
|
|
787
|
+
await runQuoteMode(muxValue$1, ctx.values["target-pane"], rawContent$1);
|
|
788
|
+
} else await runQuoteMode(muxValue$1, ctx.values["target-pane"]);
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
if (ctx.values.capture) {
|
|
792
|
+
await runCaptureMode();
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
502
795
|
const rawContent = extractRawContent(ctx.rest, ctx.positionals);
|
|
503
796
|
if (rawContent !== void 0) {
|
|
504
|
-
await runSendOnlyMode(rawContent);
|
|
797
|
+
await runSendOnlyMode(rawContent, ctx.values["auto-send"], ctx.values["send-key"]);
|
|
505
798
|
return;
|
|
506
799
|
}
|
|
507
800
|
const muxValue = ctx.values.mux || "tmux";
|