editprompt 0.5.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +129 -20
  2. package/dist/index.js +234 -15
  3. package/package.json +2 -1
package/README.md CHANGED
@@ -49,13 +49,25 @@ editprompt
49
49
 
50
50
  ### 🖥️ Tmux Integration
51
51
 
52
- **Split window version:**
52
+ **Split window version (with pane resume):**
53
53
  ```tmux
54
- bind -n M-q run-shell 'tmux split-window -v -l 20 \
55
- -c "#{pane_current_path}" \
56
- "editprompt --editor nvim --always-copy --target-pane #{pane_id}"'
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}"'
57
58
  ```
58
59
 
60
+ **How it works:**
61
+ - **First time**: Creates a new editor pane if one doesn't exist
62
+ - **Subsequent times**: Focuses the existing editor pane instead of creating a new one
63
+ - **Bidirectional**: Pressing the same keybinding from within the editor pane returns you to the original target pane
64
+
65
+ This allows you to toggle between your target pane and editor pane using the same keybinding (`M-q`).
66
+
67
+ **Benefits:**
68
+ - Prevents pane proliferation and keeps your window management simple
69
+ - Switch between your work pane and editor pane while preserving your editing content
70
+
59
71
  **Popup version:**
60
72
  ```tmux
61
73
  bind -n M-q run-shell 'tmux display-popup -E \
@@ -66,34 +78,61 @@ bind -n M-q run-shell 'tmux display-popup -E \
66
78
 
67
79
 
68
80
  ### 🖼️ WezTerm Integration
81
+
69
82
  ```lua
70
83
  {
71
84
  key = "q",
72
85
  mods = "OPT",
73
86
  action = wezterm.action_callback(function(window, pane)
74
87
  local target_pane_id = tostring(pane:pane_id())
75
- window:perform_action(
76
- act.SplitPane({
77
- direction = "Down",
78
- size = { Cells = 10 },
79
- }),
80
- pane
81
- )
82
- wezterm.time.call_after(1, function()
88
+
89
+ -- Try to resume existing editor pane
90
+ local success, stdout, stderr = wezterm.run_child_process({
91
+ "/bin/zsh",
92
+ "-lc",
93
+ string.format(
94
+ "editprompt --resume --mux wezterm --target-pane %s",
95
+ target_pane_id
96
+ ),
97
+ })
98
+
99
+ -- If resume failed, create new editor pane
100
+ if not success then
83
101
  window:perform_action(
84
- act.SendString(
85
- string.format(
86
- "editprompt --editor nvim --always-copy --mux wezterm --target-pane %s\n",
87
- target_pane_id
88
- )
89
- ),
90
- window:active_pane()
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
91
117
  )
92
- end)
118
+ end
93
119
  end),
94
120
  },
95
121
  ```
96
122
 
123
+ **How it works:**
124
+ - **First time**: Creates a new editor pane if one doesn't exist
125
+ - **Subsequent times**: Focuses the existing editor pane instead of creating a new one
126
+ - **Bidirectional**: Pressing the same keybinding from within the editor pane returns you to the original target pane
127
+
128
+ This allows you to toggle between your target pane and editor pane using the same keybinding (`OPT-q`).
129
+
130
+ **Benefits:**
131
+ - Prevents pane proliferation and keeps your window management simple
132
+ - Switch between your work pane and editor pane while preserving your editing content
133
+
134
+ **Note:** The `-lc` flag ensures your shell loads the full login environment, making `editprompt` available in your PATH.
135
+
97
136
  ### 💡 Basic Usage
98
137
 
99
138
  ```bash
@@ -233,6 +272,76 @@ bun test
233
272
  bun run dev
234
273
  ```
235
274
 
275
+ ### 💻 Testing During Development
276
+
277
+ When developing, you can test the built `dist/index.js` directly:
278
+
279
+ #### Neovim Configuration
280
+
281
+ ```diff
282
+ - { "editprompt", "--", content },
283
+ + { "node", vim.fn.expand("~/path/to/editprompt/dist/index.js"), "--", content },
284
+ ```
285
+
286
+ #### WezTerm Configuration
287
+
288
+ ```lua
289
+ -- In your wezterm.lua
290
+ local editprompt_cmd = "node " .. os.getenv("HOME") .. "/path/to/editprompt/dist/index.js"
291
+
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
+ ```
330
+
331
+ #### tmux Configuration
332
+
333
+ ```tmux
334
+ # In your .tmux.conf
335
+ set-option -g @editprompt-cmd "node ~/path/to/editprompt/dist/index.js"
336
+
337
+ bind-key -n M-q run-shell '\
338
+ #{@editprompt-cmd} --resume --target-pane #{pane_id} || \
339
+ tmux split-window -v -l 10 -c "#{pane_current_path}" \
340
+ "#{@editprompt-cmd} --editor nvim --always-copy --target-pane #{pane_id}"'
341
+ ```
342
+
343
+ This allows you to make changes, run `bun run build`, and test immediately without reinstalling globally.
344
+
236
345
  ## 🔍 Technical Details
237
346
 
238
347
  ### 🔄 Fallback Strategy
package/dist/index.js CHANGED
@@ -5,10 +5,11 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
5
5
  import { tmpdir } from "node:os";
6
6
  import { join } from "node:path";
7
7
  import { promisify } from "node:util";
8
+ import Conf from "conf";
8
9
  import clipboardy from "clipboardy";
9
10
 
10
11
  //#region package.json
11
- var version = "0.5.2";
12
+ var version = "0.6.0";
12
13
 
13
14
  //#endregion
14
15
  //#region src/config/constants.ts
@@ -109,6 +110,132 @@ async function openEditorAndGetContent(editorOption, envVars, sendConfig) {
109
110
  }
110
111
  }
111
112
 
113
+ //#endregion
114
+ //#region src/modules/tmux.ts
115
+ const execAsync$2 = promisify(exec);
116
+ async function getCurrentPaneId() {
117
+ const { stdout } = await execAsync$2("tmux display-message -p \"#{pane_id}\"");
118
+ return stdout.trim();
119
+ }
120
+ async function saveEditorPaneId(targetPaneId, editorPaneId) {
121
+ await execAsync$2(`tmux set-option -pt '${targetPaneId}' @editprompt_editor_pane '${editorPaneId}'`);
122
+ }
123
+ async function clearEditorPaneId(targetPaneId) {
124
+ await execAsync$2(`tmux set-option -pt '${targetPaneId}' @editprompt_editor_pane ""`);
125
+ }
126
+ async function getEditorPaneId(targetPaneId) {
127
+ try {
128
+ const { stdout } = await execAsync$2(`tmux show -pt '${targetPaneId}' -v @editprompt_editor_pane`);
129
+ return stdout.trim();
130
+ } catch {
131
+ return "";
132
+ }
133
+ }
134
+ async function checkPaneExists(paneId) {
135
+ try {
136
+ const { stdout } = await execAsync$2("tmux list-panes -a -F \"#{pane_id}\"");
137
+ return stdout.split("\n").map((id) => id.trim()).includes(paneId);
138
+ } catch {
139
+ return false;
140
+ }
141
+ }
142
+ async function focusPane(paneId) {
143
+ await execAsync$2(`tmux select-pane -t '${paneId}'`);
144
+ }
145
+ async function markAsEditorPane(editorPaneId, targetPaneId) {
146
+ await execAsync$2(`tmux set-option -pt '${editorPaneId}' @editprompt_is_editor 1`);
147
+ await execAsync$2(`tmux set-option -pt '${editorPaneId}' @editprompt_target_pane '${targetPaneId}'`);
148
+ }
149
+ async function getTargetPaneId(editorPaneId) {
150
+ try {
151
+ const { stdout } = await execAsync$2(`tmux show -pt '${editorPaneId}' -v @editprompt_target_pane`);
152
+ return stdout.trim();
153
+ } catch {
154
+ return "";
155
+ }
156
+ }
157
+ async function isEditorPane(paneId) {
158
+ try {
159
+ const { stdout } = await execAsync$2(`tmux show -pt '${paneId}' -v @editprompt_is_editor`);
160
+ return stdout.trim() === "1";
161
+ } catch {
162
+ return false;
163
+ }
164
+ }
165
+
166
+ //#endregion
167
+ //#region src/modules/wezterm.ts
168
+ const execAsync$1 = promisify(exec);
169
+ const conf = new Conf({ projectName: "editprompt" });
170
+ async function getCurrentPaneId$1() {
171
+ try {
172
+ const { stdout } = await execAsync$1("wezterm cli list --format json");
173
+ const activePane = JSON.parse(stdout).find((pane) => pane.is_active === true);
174
+ return String(activePane?.pane_id);
175
+ } catch (error) {
176
+ console.log(error);
177
+ return "";
178
+ }
179
+ }
180
+ async function checkPaneExists$1(paneId) {
181
+ try {
182
+ const { stdout } = await execAsync$1("wezterm cli list --format json");
183
+ console.log(stdout);
184
+ return JSON.parse(stdout).some((pane) => String(pane.pane_id) === paneId);
185
+ } catch (error) {
186
+ console.log(error);
187
+ return false;
188
+ }
189
+ }
190
+ async function getEditorPaneId$1(targetPaneId) {
191
+ try {
192
+ const data = conf.get(`wezterm.targetPane.pane_${targetPaneId}`);
193
+ if (typeof data === "object" && data !== null && "editorPaneId" in data) return String(data.editorPaneId);
194
+ return "";
195
+ } catch (error) {
196
+ console.log(error);
197
+ return "";
198
+ }
199
+ }
200
+ async function clearEditorPaneId$1(targetPaneId) {
201
+ try {
202
+ const editorPaneId = await getEditorPaneId$1(targetPaneId);
203
+ conf.delete(`wezterm.targetPane.pane_${targetPaneId}`);
204
+ if (editorPaneId) conf.delete(`wezterm.editorPane.pane_${editorPaneId}`);
205
+ } catch (error) {
206
+ console.log(error);
207
+ }
208
+ }
209
+ async function focusPane$1(paneId) {
210
+ await execAsync$1(`wezterm cli activate-pane --pane-id '${paneId}'`);
211
+ }
212
+ async function markAsEditorPane$1(editorPaneId, targetPaneId) {
213
+ try {
214
+ conf.set(`wezterm.targetPane.pane_${targetPaneId}`, { editorPaneId });
215
+ conf.set(`wezterm.editorPane.pane_${editorPaneId}`, { targetPaneId });
216
+ } catch (error) {
217
+ console.log(error);
218
+ }
219
+ }
220
+ async function getTargetPaneId$1(editorPaneId) {
221
+ try {
222
+ const data = conf.get(`wezterm.editorPane.pane_${editorPaneId}`);
223
+ if (typeof data === "object" && data !== null && "targetPaneId" in data) return String(data.targetPaneId);
224
+ return "";
225
+ } catch (error) {
226
+ console.log(error);
227
+ return "";
228
+ }
229
+ }
230
+ function isEditorPaneFromConf(paneId) {
231
+ try {
232
+ return conf.has(`wezterm.editorPane.pane_${paneId}`);
233
+ } catch (error) {
234
+ console.log(error);
235
+ return false;
236
+ }
237
+ }
238
+
112
239
  //#endregion
113
240
  //#region src/modules/process.ts
114
241
  const execAsync = promisify(exec);
@@ -172,23 +299,98 @@ async function handleContentDelivery(content, mux, targetPane, alwaysCopy) {
172
299
  //#endregion
173
300
  //#region src/modes/openEditor.ts
174
301
  async function runOpenEditorMode(options) {
175
- const sendConfig = {
176
- targetPane: options.targetPane,
177
- mux: options.mux,
178
- alwaysCopy: options.alwaysCopy
179
- };
180
- console.log("Opening editor...");
181
- const content = await openEditorAndGetContent(options.editor, options.env, sendConfig);
182
- if (!content) {
183
- console.log("No content entered. Exiting.");
184
- return;
185
- }
302
+ if (options.targetPane && options.mux === "tmux") try {
303
+ const currentPaneId = await getCurrentPaneId();
304
+ await saveEditorPaneId(options.targetPane, currentPaneId);
305
+ await markAsEditorPane(currentPaneId, options.targetPane);
306
+ } catch {}
307
+ else if (options.targetPane && options.mux === "wezterm") try {
308
+ const currentPaneId = await getCurrentPaneId$1();
309
+ await markAsEditorPane$1(currentPaneId, options.targetPane);
310
+ } catch {}
186
311
  try {
187
- await handleContentDelivery(content, options.mux, options.targetPane, options.alwaysCopy);
188
- } catch (error) {
189
- console.error(`Error: ${error instanceof Error ? error.message : "Unknown error"}`);
312
+ const sendConfig = {
313
+ targetPane: options.targetPane,
314
+ mux: options.mux,
315
+ alwaysCopy: options.alwaysCopy
316
+ };
317
+ console.log("Opening editor...");
318
+ const content = await openEditorAndGetContent(options.editor, options.env, sendConfig);
319
+ if (!content) {
320
+ console.log("No content entered. Exiting.");
321
+ return;
322
+ }
323
+ try {
324
+ await handleContentDelivery(content, options.mux, options.targetPane, options.alwaysCopy);
325
+ } catch (error) {
326
+ console.error(`Error: ${error instanceof Error ? error.message : "Unknown error"}`);
327
+ process.exit(1);
328
+ }
329
+ } finally {
330
+ if (options.targetPane && options.mux === "tmux") try {
331
+ await clearEditorPaneId(options.targetPane);
332
+ } catch {}
333
+ else if (options.targetPane && options.mux === "wezterm") try {
334
+ await clearEditorPaneId$1(options.targetPane);
335
+ } catch {}
336
+ }
337
+ }
338
+
339
+ //#endregion
340
+ //#region src/modes/resume.ts
341
+ async function runResumeMode(targetPane, mux) {
342
+ if (mux === "wezterm") {
343
+ const currentPaneId$1 = await getCurrentPaneId$1();
344
+ if (isEditorPaneFromConf(currentPaneId$1)) {
345
+ console.log("isEditor");
346
+ const originalTargetPaneId = await getTargetPaneId$1(currentPaneId$1);
347
+ if (!originalTargetPaneId) {
348
+ console.log("Not found originalTargetPaneId");
349
+ process.exit(1);
350
+ }
351
+ if (!await checkPaneExists$1(originalTargetPaneId)) {
352
+ console.log("Not exist originalTargetPaneId");
353
+ process.exit(1);
354
+ }
355
+ await focusPane$1(originalTargetPaneId);
356
+ process.exit(0);
357
+ }
358
+ console.log("not isEditor");
359
+ const editorPaneId$1 = await getEditorPaneId$1(targetPane);
360
+ console.log(`wezterm editorPaneId: ${editorPaneId$1}`);
361
+ if (editorPaneId$1 === "") {
362
+ console.log("Not found editorPaneId");
363
+ process.exit(1);
364
+ }
365
+ if (!await checkPaneExists$1(editorPaneId$1)) {
366
+ console.log("Not exist editorPaneId");
367
+ await clearEditorPaneId$1(targetPane);
368
+ process.exit(1);
369
+ }
370
+ try {
371
+ await focusPane$1(editorPaneId$1);
372
+ process.exit(0);
373
+ } catch (error) {
374
+ console.log(`Can't focus editorPaneId: ${editorPaneId$1}\nerror: ${error}`);
375
+ process.exit(1);
376
+ }
377
+ }
378
+ const currentPaneId = await getCurrentPaneId();
379
+ if (await isEditorPane(currentPaneId)) {
380
+ const originalTargetPaneId = await getTargetPaneId(currentPaneId);
381
+ if (!originalTargetPaneId) process.exit(1);
382
+ if (!await checkPaneExists(originalTargetPaneId)) process.exit(1);
383
+ await focusPane(originalTargetPaneId);
384
+ process.exit(0);
385
+ }
386
+ const editorPaneId = await getEditorPaneId(targetPane);
387
+ if (editorPaneId === "") process.exit(1);
388
+ if (!await checkPaneExists(editorPaneId)) {
389
+ await clearEditorPaneId(targetPane);
190
390
  process.exit(1);
191
391
  }
392
+ await focusPane(editorPaneId);
393
+ process.exit(0);
192
394
  }
193
395
 
194
396
  //#endregion
@@ -247,6 +449,10 @@ await cli(process.argv.slice(2), {
247
449
  name: "editprompt",
248
450
  description: "A CLI tool that lets you write prompts for Claude Code using your favorite text editor",
249
451
  args: {
452
+ resume: {
453
+ description: "Resume existing editor pane instead of creating new one",
454
+ type: "boolean"
455
+ },
250
456
  editor: {
251
457
  short: "e",
252
458
  description: "Editor to use (overrides $EDITOR)",
@@ -280,6 +486,19 @@ await cli(process.argv.slice(2), {
280
486
  },
281
487
  async run(ctx) {
282
488
  try {
489
+ if (ctx.values.resume) {
490
+ if (!ctx.values["target-pane"]) {
491
+ console.error("Error: --target-pane is required when using --resume");
492
+ process.exit(1);
493
+ }
494
+ const mux$1 = ctx.values.mux || "tmux";
495
+ if (!isMuxType(mux$1)) {
496
+ console.error(`Error: Invalid mux type '${mux$1}'. Supported values: tmux, wezterm`);
497
+ process.exit(1);
498
+ }
499
+ await runResumeMode(ctx.values["target-pane"], mux$1);
500
+ return;
501
+ }
283
502
  const rawContent = extractRawContent(ctx.rest, ctx.positionals);
284
503
  if (rawContent !== void 0) {
285
504
  await runSendOnlyMode(rawContent);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "editprompt",
3
- "version": "0.5.2",
3
+ "version": "0.6.0",
4
4
  "author": "eetann",
5
5
  "description": "A CLI tool that lets you write prompts for CLI tools using your favorite text editor",
6
6
  "license": "MIT",
@@ -56,6 +56,7 @@
56
56
  },
57
57
  "dependencies": {
58
58
  "clipboardy": "^4.0.0",
59
+ "conf": "^15.0.2",
59
60
  "gunshi": "^0.26.3"
60
61
  },
61
62
  "peerDependencies": {