tmux-fuzzy-motion 0.0.5 → 0.0.7

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 (4) hide show
  1. package/README.ja.md +43 -20
  2. package/README.md +48 -20
  3. package/dist/cli.js +323 -31
  4. package/package.json +1 -1
package/README.ja.md CHANGED
@@ -2,14 +2,16 @@
2
2
 
3
3
  [English README](./README.md)
4
4
 
5
- `tmux-fuzzy-motion` は、`tmux copy-mode` 内で素早くカーソル移動するための
6
- CLI です。現在の viewport からジャンプ対象を抽出し、fuzzy search で絞り込み、
5
+ `tmux-fuzzy-motion` は、tmux pane 内で素早くカーソル移動するための CLI です。
6
+ 現在の viewport からジャンプ対象を抽出し、fuzzy search で絞り込み、
7
7
  大文字の hint で移動できます。英字のローマ字 query に対しては Migemo による
8
8
  日本語マッチも行います。
9
9
 
10
10
  ## 特徴
11
11
 
12
12
  - `tmux copy-mode` 内で動作
13
+ - `start` は copy mode の外から起動しても自動で copy mode に入る
14
+ - `start --scope all` は current window の visible pane 全体を対象にできる
13
15
  - 現在の viewport から URL、path、filename、symbol、一般的な単語を抽出
14
16
  - `fzf` による fuzzy match
15
17
  - `jsmigemo` による英字 query の Migemo マッチ
@@ -56,13 +58,32 @@ bind-key -T copy-mode-vi s run-shell 'tmux-fuzzy-motion start #{pane_id} #{clien
56
58
  bind-key -T copy-mode s run-shell 'tmux-fuzzy-motion start #{pane_id} #{client_tty}'
57
59
  ```
58
60
 
59
- `run-shell` を経由せずに tmux から直接 popup を開きたい場合は、次の設定も使えます。
61
+ copy mode の外からも起動したい場合は、root table にも bind を追加します。
62
+
63
+ ```tmux
64
+ bind-key s run-shell 'tmux-fuzzy-motion start #{pane_id} #{client_tty}'
65
+ ```
66
+
67
+ current window の visible pane 全体から選びたい場合は、`--scope all` を付けた bind を追加します。
68
+
69
+ ```tmux
70
+ bind-key S run-shell 'tmux-fuzzy-motion start --scope all #{pane_id} #{client_tty}'
71
+ ```
72
+
73
+ `start` サブコマンドを経由せずに tmux から直接 popup を開きたい場合は、次の設定も使えます。
60
74
 
61
75
  ```tmux
62
76
  bind-key -T copy-mode-vi s run-shell -C "display-popup -E -B -x '##{popup_pane_left}' -y '##{popup_pane_top}' -w '#{pane_width}' -h '#{pane_height}' 'tmux-fuzzy-motion popup-live #{pane_id}'"
63
77
  bind-key -T copy-mode s run-shell -C "display-popup -E -B -x '##{popup_pane_left}' -y '##{popup_pane_top}' -w '#{pane_width}' -h '#{pane_height}' 'tmux-fuzzy-motion popup-live #{pane_id}'"
64
78
  ```
65
79
 
80
+ > [!NOTE]
81
+ > 後述の手順の`2.`で`'tmux-fuzzy-motion start %25 /dev/ttys000' returned 127`のようなエラーが表示される場合は以下のようにrun-shell環境のPATHに`tmux-fuzzy-motion`を含める必要があります。
82
+ >
83
+ > ```tmux
84
+ > set-environment -g PATH "/path/to/node/bin:$PATH"
85
+ > ```
86
+
66
87
  設定変更後は tmux を reload します。
67
88
 
68
89
  ```bash
@@ -71,13 +92,15 @@ tmux source-file ~/.tmux.conf
71
92
 
72
93
  ## 使い方
73
94
 
74
- 1. `copy-mode` に入る
75
- 2. `s` を押す
76
- 3. 小文字や記号で query を入力する
77
- 4. fuzzy match で候補を絞り込む
78
- 5. 英字 query の場合は Migemo による日本語候補も対象になる
79
- 6. 大文字 hint を押して即座に移動する
80
- 7. `Esc` または `Ctrl-[` でキャンセルする
95
+ 1. `tmux-fuzzy-motion start` を bind したキーを押す
96
+ 2. `--scope current`(default)は current pane のみを対象にし、pane がまだ `copy-mode` でなければ先に `copy-mode` に入る
97
+ 3. `--scope all` は current window の visible pane 全体を popup に合成して対象にする
98
+ 4. 小文字や記号で query を入力する
99
+ 5. fuzzy match で候補を絞り込む
100
+ 6. 英字 query の場合は Migemo による日本語候補も対象になる
101
+ 7. 大文字 hint を押して即座に移動する
102
+ 8. `--scope all` で選択した場合は、該当 pane を active にして必要なら `copy-mode` に入ってから移動する
103
+ 9. `Esc` または `Ctrl-[` でキャンセルする
81
104
 
82
105
  ## 入力キー
83
106
 
@@ -91,7 +114,7 @@ tmux source-file ~/.tmux.conf
91
114
  ## コマンド
92
115
 
93
116
  ```text
94
- tmux-fuzzy-motion start <pane-id> <client-tty>
117
+ tmux-fuzzy-motion start [--scope current|all] <pane-id> <client-tty>
95
118
  tmux-fuzzy-motion popup-live <pane-id>
96
119
  tmux-fuzzy-motion doctor
97
120
  ```
@@ -99,6 +122,11 @@ tmux-fuzzy-motion doctor
99
122
  `popup` と `daemon` は内部サブコマンドです。`popup-live` は `display-popup`
100
123
  から直接起動する設定向けです。
101
124
 
125
+ `--scope`:
126
+
127
+ - `current`: current pane のみを対象にする。default
128
+ - `all`: current window の visible pane 全体を対象にする
129
+
102
130
  ## Doctor
103
131
 
104
132
  ローカル環境の確認には `doctor` を使います。
@@ -141,17 +169,12 @@ pnpm run dev
141
169
  pnpm check
142
170
  ```
143
171
 
144
- warm な daemon を前提に popup 起動時間を測る場合:
145
-
146
- ```bash
147
- pnpm bench:startup dist/cli.js
148
- ```
149
-
150
172
  ## 制約
151
173
 
152
- - 対象は現在の viewport のみ
153
- - `copy-mode` 専用
174
+ - 対象は各 pane の現在の viewport のみ
175
+ - `--scope all` の対象は current window の visible pane のみ
176
+ - zoom 中の `--scope all` は見えている pane のみを対象にする
154
177
  - query 入力は ASCII 寄り
155
178
  - combining character の完全な扱いは未保証
156
179
  - `display-popup` が必要なため、tmux 3.2 以上が必須
157
- - query は pane 内の最下行右端に描画する
180
+ - query は popup の最下行右端に描画する
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [日本語版はこちら](./README.ja.md)
4
4
 
5
- `tmux-fuzzy-motion` is a CLI for quick cursor jumps inside `tmux copy-mode`.
5
+ `tmux-fuzzy-motion` is a CLI for quick cursor jumps in tmux panes.
6
6
  It scans the current viewport, extracts jump targets, filters them with fuzzy
7
7
  search, and lets you jump with uppercase hints. Roman queries can also match
8
8
  Japanese text through Migemo.
@@ -10,6 +10,8 @@ Japanese text through Migemo.
10
10
  ## Features
11
11
 
12
12
  - Works inside `tmux copy-mode`
13
+ - `start` can also be launched outside copy-mode and enters copy-mode automatically
14
+ - `start --scope all` can target every visible pane in the current window
13
15
  - Extracts URLs, paths, filenames, symbols, and general words from the current viewport
14
16
  - Supports fuzzy matching with `fzf`
15
17
  - Supports Migemo matching for alphabetic queries via `jsmigemo`
@@ -56,14 +58,35 @@ bind-key -T copy-mode-vi s run-shell 'tmux-fuzzy-motion start #{pane_id} #{clien
56
58
  bind-key -T copy-mode s run-shell 'tmux-fuzzy-motion start #{pane_id} #{client_tty}'
57
59
  ```
58
60
 
59
- If you want tmux to open the popup directly and avoid the extra `run-shell`
60
- hop, use this instead:
61
+ If you also want to launch it outside copy-mode, add a binding in the root
62
+ table as well:
63
+
64
+ ```tmux
65
+ bind-key s run-shell 'tmux-fuzzy-motion start #{pane_id} #{client_tty}'
66
+ ```
67
+
68
+ If you want to search across every visible pane in the current window, add a
69
+ binding with `--scope all`.
70
+
71
+ ```tmux
72
+ bind-key S run-shell 'tmux-fuzzy-motion start --scope all #{pane_id} #{client_tty}'
73
+ ```
74
+
75
+ If you want tmux to open the popup directly without going through the `start`
76
+ subcommand, use this instead:
61
77
 
62
78
  ```tmux
63
79
  bind-key -T copy-mode-vi s run-shell -C "display-popup -E -B -x '##{popup_pane_left}' -y '##{popup_pane_top}' -w '#{pane_width}' -h '#{pane_height}' 'tmux-fuzzy-motion popup-live #{pane_id}'"
64
80
  bind-key -T copy-mode s run-shell -C "display-popup -E -B -x '##{popup_pane_left}' -y '##{popup_pane_top}' -w '#{pane_width}' -h '#{pane_height}' 'tmux-fuzzy-motion popup-live #{pane_id}'"
65
81
  ```
66
82
 
83
+ > [!NOTE]
84
+ > If you see an error like `'tmux-fuzzy-motion start %25 /dev/ttys000' returned 127` at step 2 below, you need to add `tmux-fuzzy-motion` to the PATH in the run-shell environment:
85
+ >
86
+ > ```tmux
87
+ > set-environment -g PATH "/path/to/node/bin:$PATH"
88
+ > ```
89
+
67
90
  Reload tmux after editing the config:
68
91
 
69
92
  ```bash
@@ -72,13 +95,18 @@ tmux source-file ~/.tmux.conf
72
95
 
73
96
  ## Usage
74
97
 
75
- 1. Enter `copy-mode`.
76
- 2. Press `s`.
77
- 3. Type a query in lowercase or symbols.
78
- 4. Narrow the candidates with fuzzy matching.
79
- 5. For alphabetic queries, Migemo also expands roman input to Japanese matches.
80
- 6. Press an uppercase hint to jump immediately.
81
- 7. Press `Esc` or `Ctrl-[` to cancel.
98
+ 1. Press the key bound to `tmux-fuzzy-motion start`.
99
+ 2. `--scope current` (the default) targets only the current pane and enters
100
+ copy-mode first if needed.
101
+ 3. `--scope all` targets every visible pane in the current window by composing
102
+ them into a single popup.
103
+ 4. Type a query in lowercase or symbols.
104
+ 5. Narrow the candidates with fuzzy matching.
105
+ 6. For alphabetic queries, Migemo also expands roman input to Japanese matches.
106
+ 7. Press an uppercase hint to jump immediately.
107
+ 8. In `--scope all`, the selected pane becomes active and enters copy-mode if
108
+ needed before the cursor moves.
109
+ 9. Press `Esc` or `Ctrl-[` to cancel.
82
110
 
83
111
  ## Input Keys
84
112
 
@@ -92,7 +120,7 @@ tmux source-file ~/.tmux.conf
92
120
  ## Commands
93
121
 
94
122
  ```text
95
- tmux-fuzzy-motion start <pane-id> <client-tty>
123
+ tmux-fuzzy-motion start [--scope current|all] <pane-id> <client-tty>
96
124
  tmux-fuzzy-motion popup-live <pane-id>
97
125
  tmux-fuzzy-motion doctor
98
126
  ```
@@ -100,6 +128,11 @@ tmux-fuzzy-motion doctor
100
128
  `popup` and `daemon` are internal subcommands. `popup-live` is intended for
101
129
  direct `display-popup` bindings.
102
130
 
131
+ `--scope`:
132
+
133
+ - `current`: target only the current pane. This is the default.
134
+ - `all`: target every visible pane in the current window.
135
+
103
136
  ## Doctor
104
137
 
105
138
  Use `doctor` to verify the local environment:
@@ -142,17 +175,12 @@ Run the full local check:
142
175
  pnpm check
143
176
  ```
144
177
 
145
- Measure popup startup against a warm daemon:
146
-
147
- ```bash
148
- pnpm bench:startup dist/cli.js
149
- ```
150
-
151
178
  ## Limitations
152
179
 
153
- - Targets are limited to the current viewport
154
- - Designed for `copy-mode` only
180
+ - Targets are limited to the current viewport of each pane
181
+ - `--scope all` targets only visible panes in the current window
182
+ - Zoomed windows with `--scope all` only target the pane that is visible
155
183
  - Query input is ASCII-oriented
156
184
  - Exact behavior for combining characters is not fully guaranteed
157
185
  - Requires `display-popup`, so tmux 3.2 or later is mandatory
158
- - The query is drawn on the bottom row inside the pane, aligned to the right edge
186
+ - The query is drawn on the popup's bottom row, aligned to the right edge
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from "node:module";
3
3
  import { execFile, spawn } from "node:child_process";
4
- import { access, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
4
+ import { access, appendFile, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
5
5
  import { dirname, join, resolve } from "node:path";
6
6
  import { createConnection, createServer } from "node:net";
7
7
  import { tmpdir } from "node:os";
@@ -52,6 +52,17 @@ const focusClientPane = async (tmux, paneId, clientTty) => {
52
52
  throw new Error("tmux-fuzzy-motion: client not found", { cause: error });
53
53
  }
54
54
  };
55
+ const enterCopyMode = async (tmux, paneId) => {
56
+ try {
57
+ await tmux.run([
58
+ "copy-mode",
59
+ "-t",
60
+ paneId
61
+ ]);
62
+ } catch (error) {
63
+ throw new Error("tmux-fuzzy-motion: failed to enter copy-mode", { cause: error });
64
+ }
65
+ };
55
66
  const getPaneStartContext = async (tmux, paneId) => {
56
67
  let output = "";
57
68
  try {
@@ -67,10 +78,9 @@ const getPaneStartContext = async (tmux, paneId) => {
67
78
  }
68
79
  const [resolvedPaneId, paneInMode, width, height, currentPath] = output.split(" ");
69
80
  if (!resolvedPaneId || !paneInMode || !currentPath || [width, height].map((value) => Number(value)).some((value) => !Number.isFinite(value))) throw new Error("tmux-fuzzy-motion: failed to resolve pane context");
70
- if (paneInMode !== "1") throw new Error("tmux-fuzzy-motion: pane is not in copy-mode");
71
81
  return {
72
82
  paneId: resolvedPaneId,
73
- inCopyMode: true,
83
+ inCopyMode: paneInMode === "1",
74
84
  width: Number(width),
75
85
  height: Number(height),
76
86
  currentPath
@@ -88,9 +98,9 @@ const displayPopup = async (tmux, options) => {
88
98
  "-d",
89
99
  options.currentPath,
90
100
  "-x",
91
- "#{popup_pane_left}",
101
+ String(options.x ?? "#{popup_pane_left}"),
92
102
  "-y",
93
- "#{popup_pane_top}",
103
+ String(options.y ?? "#{popup_pane_top}"),
94
104
  "-w",
95
105
  String(options.width),
96
106
  "-h",
@@ -98,6 +108,64 @@ const displayPopup = async (tmux, options) => {
98
108
  ...options.command
99
109
  ]);
100
110
  };
111
+ const listWindowPanes = async (tmux, paneId) => {
112
+ let output = "";
113
+ try {
114
+ output = (await tmux.capture([
115
+ "list-panes",
116
+ "-t",
117
+ paneId,
118
+ "-F",
119
+ "#{pane_id} #{pane_in_mode} #{pane_width} #{pane_height} #{pane_current_path} #{pane_left} #{pane_top} #{?pane_active,1,0} #{window_zoomed_flag}"
120
+ ])).trim();
121
+ } catch (error) {
122
+ throw new Error("tmux-fuzzy-motion: pane not found", { cause: error });
123
+ }
124
+ const panes = output.split("\n").filter((line) => line.length > 0).map((line) => {
125
+ const [resolvedPaneId, paneInMode, width, height, currentPath, left, top, active, zoomed] = line.split(" ");
126
+ const numeric = [
127
+ width,
128
+ height,
129
+ left,
130
+ top
131
+ ].map((value) => Number(value));
132
+ if (!resolvedPaneId || !paneInMode || !currentPath || [active, zoomed].some((value) => value === void 0) || numeric.some((value) => !Number.isFinite(value))) throw new Error("tmux-fuzzy-motion: failed to resolve window panes");
133
+ return {
134
+ paneId: resolvedPaneId,
135
+ inCopyMode: paneInMode === "1",
136
+ width: Number(width),
137
+ height: Number(height),
138
+ currentPath,
139
+ left: Number(left),
140
+ top: Number(top),
141
+ active: active === "1",
142
+ zoomed: zoomed === "1"
143
+ };
144
+ });
145
+ if (panes.length === 0) throw new Error("tmux-fuzzy-motion: pane not found");
146
+ const zoomed = panes.some((pane) => pane.zoomed);
147
+ return panes.filter((pane) => !zoomed || pane.active).map((pane) => {
148
+ const { zoomed: paneZoomed, ...rest } = pane;
149
+ return rest;
150
+ });
151
+ };
152
+ const getPaneBorderLines = async (tmux, paneId) => {
153
+ let output = "";
154
+ try {
155
+ output = (await tmux.capture([
156
+ "show-options",
157
+ "-A",
158
+ "-wv",
159
+ "-t",
160
+ paneId,
161
+ "pane-border-lines"
162
+ ])).trim();
163
+ } catch (error) {
164
+ throw new Error("tmux-fuzzy-motion: failed to resolve pane border lines", { cause: error });
165
+ }
166
+ if (output === "single" || output === "double" || output === "heavy" || output === "simple" || output === "number" || output === "spaces") return output;
167
+ throw new Error("tmux-fuzzy-motion: failed to resolve pane border lines");
168
+ };
101
169
  const getTmuxVersion = async () => {
102
170
  return (await runProcess("tmux", ["-V"])).stdout.trim();
103
171
  };
@@ -4789,7 +4857,7 @@ const extractCandidates = (lines) => {
4789
4857
  //#region src/core/hint.ts
4790
4858
  const HINT_CHARS$1 = "ASDFGHJKLQWERTYUIOPZXCVBNM";
4791
4859
  const MAX_TARGETS = 200;
4792
- const createTargetKey = (target) => `${target.line}:${target.col}:${target.text}`;
4860
+ const createTargetKey = (target) => `${target.paneId ?? ""}:${target.line}:${target.col}:${target.text}`;
4793
4861
  const generateHints = (characters, maxHintLength) => {
4794
4862
  const single = [...characters];
4795
4863
  if (maxHintLength === 1) return single;
@@ -5602,6 +5670,7 @@ var Fzf = class {
5602
5670
  //#endregion
5603
5671
  //#region src/core/matcher.ts
5604
5672
  const candidateKey = (candidate) => [
5673
+ candidate.paneId ?? "",
5605
5674
  candidate.kind,
5606
5675
  candidate.text,
5607
5676
  String(candidate.line),
@@ -5773,14 +5842,16 @@ const measureCellWidth = (cells, start) => {
5773
5842
  const createOverlayRenderer = (lines) => {
5774
5843
  const baseCellsByLine = lines.map((line) => createStyledDisplayCells(line));
5775
5844
  const baseLines = baseCellsByLine.map((cells) => cells.join(""));
5845
+ const targetLine = (target) => (target.screenLine ?? target.line) - 1;
5846
+ const targetCol = (target) => target.screenCol ?? target.col;
5776
5847
  return (targets) => {
5777
5848
  const rendered = [...baseLines];
5778
5849
  const mutableCells = /* @__PURE__ */ new Map();
5779
5850
  const occupiedByLine = /* @__PURE__ */ new Map();
5780
5851
  const enterTarget = targets[0];
5781
- const sorted = [...targets].sort((left, right) => left.line - right.line || left.col + left.primary - (right.col + right.primary));
5852
+ const sorted = [...targets].sort((left, right) => targetLine(left) - targetLine(right) || targetCol(left) + left.primary - (targetCol(right) + right.primary));
5782
5853
  for (const target of sorted) {
5783
- const lineIndex = target.line - 1;
5854
+ const lineIndex = targetLine(target);
5784
5855
  const baseCells = baseCellsByLine[lineIndex];
5785
5856
  if (!baseCells) continue;
5786
5857
  let cells = mutableCells.get(lineIndex);
@@ -5793,14 +5864,14 @@ const createOverlayRenderer = (lines) => {
5793
5864
  lineOccupied = Array.from({ length: baseCells.length }, () => false);
5794
5865
  occupiedByLine.set(lineIndex, lineOccupied);
5795
5866
  }
5796
- const matchCol = target.col + target.primary;
5867
+ const matchCol = targetCol(target) + target.primary;
5797
5868
  const hintCol = findOverlayStart(cells, matchCol);
5798
5869
  const baseWidth = measureCellWidth(cells, hintCol);
5799
5870
  const paddedHint = hintCol < matchCol ? target.hint.padEnd(baseWidth, " ") : target.hint;
5800
5871
  const hintWidth = displayWidth(paddedHint);
5801
5872
  const shouldHighlightPrimary = hintCol < matchCol;
5802
- const highlightCols = target.positions.filter((position) => position !== target.primary || shouldHighlightPrimary).map((position) => target.col + position);
5803
- const isEnterTarget = enterTarget?.line === target.line && enterTarget.col === target.col && enterTarget.text === target.text;
5873
+ const highlightCols = target.positions.filter((position) => position !== target.primary || shouldHighlightPrimary).map((position) => targetCol(target) + position);
5874
+ const isEnterTarget = enterTarget !== void 0 && enterTarget.paneId === target.paneId && targetLine(enterTarget) === lineIndex && targetCol(enterTarget) === targetCol(target) && enterTarget.text === target.text;
5804
5875
  const hintStyle = isEnterTarget ? PRIMARY_HINT_STYLE : SECONDARY_HINT_STYLE;
5805
5876
  const highlightStyle = isEnterTarget ? PRIMARY_HIGHLIGHT_STYLE : SECONDARY_HIGHLIGHT_STYLE;
5806
5877
  const overlapsHint = Array.from({ length: hintWidth }, (_, offset) => lineOccupied[hintCol + offset]).some(Boolean);
@@ -6006,8 +6077,13 @@ const computeMatches = (query, previousHints, matcher) => assignHints(matcher(qu
6006
6077
  maxHintLength: 1,
6007
6078
  maxTargets: 26
6008
6079
  });
6009
- const createPreparedMatcher = async (lines, migemoPromise) => {
6010
- const candidates = extractCandidates(lines);
6080
+ const createPreparedMatcher = async (state, migemoPromise) => {
6081
+ const candidates = state.scope === "all" ? state.panes.flatMap((pane) => extractCandidates(pane.plainLines).map((candidate) => ({
6082
+ ...candidate,
6083
+ paneId: pane.paneId,
6084
+ screenLine: pane.top + candidate.line,
6085
+ screenCol: pane.left + candidate.col
6086
+ }))) : extractCandidates(state.plainLines);
6011
6087
  const migemo = await migemoPromise;
6012
6088
  return {
6013
6089
  candidateCount: candidates.length,
@@ -6193,8 +6269,10 @@ const runPopupLive = async (args) => {
6193
6269
  const socketPath = createDaemonSocketPath();
6194
6270
  try {
6195
6271
  const pane = await getPaneStartContext(tmux, paneId);
6272
+ if (!pane.inCopyMode) throw new Error("tmux-fuzzy-motion: pane is not in copy-mode");
6196
6273
  const capture = fitCaptureToHeight(await capturePane(tmux, paneId), pane.height);
6197
6274
  const state = {
6275
+ scope: "current",
6198
6276
  paneId,
6199
6277
  clientTty: "",
6200
6278
  displayLines: capture.displayLines,
@@ -6279,7 +6357,7 @@ const runDaemon = async (args) => {
6279
6357
  }
6280
6358
  if (request.type === "prepare") {
6281
6359
  activeSocket = socket;
6282
- const prepared = await createPreparedMatcher(JSON.parse(await readFile(request.stateFile, "utf8")).plainLines, migemoPromise);
6360
+ const prepared = await createPreparedMatcher(JSON.parse(await readFile(request.stateFile, "utf8")), migemoPromise);
6283
6361
  activeMatcher = prepared.matcher;
6284
6362
  await writeMessage({
6285
6363
  type: "prepared",
@@ -6337,6 +6415,21 @@ const runDaemon = async (args) => {
6337
6415
  };
6338
6416
  //#endregion
6339
6417
  //#region src/commands/start.ts
6418
+ const writeDebugLog = async (payload) => {
6419
+ const debugLogPath = process.env.TMUX_FUZZY_MOTION_DEBUG_LOG;
6420
+ if (!debugLogPath) return;
6421
+ const line = JSON.stringify({
6422
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
6423
+ ...payload
6424
+ });
6425
+ try {
6426
+ await appendFile(debugLogPath, `${line}\n`, "utf8");
6427
+ } catch {}
6428
+ };
6429
+ const buildPopupRelativePosition = (axis, targetOrigin) => {
6430
+ if (axis === "x") return `#{e|+|:#{popup_pane_left},#{e|-|:${targetOrigin},#{pane_left}}}`;
6431
+ return `#{e|+|:#{popup_height},${`#{e|+|:#{e|-|:${targetOrigin},#{window_offset_y}},#{?#{==:#{status-position},top},#{e|-|:#{client_height},#{window_height}},0}}`}}`;
6432
+ };
6340
6433
  const buildPopupCommand = (stateFile, resultFile, socketPath) => [
6341
6434
  process.execPath,
6342
6435
  resolveCliEntrypoint(),
@@ -6355,8 +6448,194 @@ const readResult = async (resultFile) => {
6355
6448
  throw new Error("tmux-fuzzy-motion: popup did not produce result", { cause: error });
6356
6449
  }
6357
6450
  };
6451
+ const parseStartArgs = (args) => {
6452
+ let scope = "current";
6453
+ const positional = [];
6454
+ for (let index = 0; index < args.length; index += 1) {
6455
+ const value = args[index];
6456
+ if (value === "--scope") {
6457
+ const nextScope = args[index + 1];
6458
+ if (nextScope === "current" || nextScope === "all") scope = nextScope;
6459
+ index += 1;
6460
+ continue;
6461
+ }
6462
+ positional.push(value ?? "");
6463
+ }
6464
+ return {
6465
+ scope,
6466
+ paneId: positional[0] ?? "",
6467
+ clientTty: positional[1] ?? ""
6468
+ };
6469
+ };
6470
+ const createBlankRow = (width) => Array.from({ length: width }, () => " ");
6471
+ const BORDER_SETS = {
6472
+ single: {
6473
+ vertical: "│",
6474
+ horizontal: "─",
6475
+ intersection: "┼"
6476
+ },
6477
+ double: {
6478
+ vertical: "║",
6479
+ horizontal: "═",
6480
+ intersection: "╬"
6481
+ },
6482
+ heavy: {
6483
+ vertical: "┃",
6484
+ horizontal: "━",
6485
+ intersection: "╋"
6486
+ },
6487
+ simple: {
6488
+ vertical: "|",
6489
+ horizontal: "-",
6490
+ intersection: "+"
6491
+ },
6492
+ spaces: {
6493
+ vertical: " ",
6494
+ horizontal: " ",
6495
+ intersection: " "
6496
+ }
6497
+ };
6498
+ const createOccupancyGrid = (panes, width, height) => {
6499
+ const occupied = Array.from({ length: height }, () => Array.from({ length: width }, () => false));
6500
+ for (const pane of panes) for (let row = pane.top; row < pane.top + pane.height; row += 1) {
6501
+ const line = occupied[row];
6502
+ if (!line) continue;
6503
+ for (let column = pane.left; column < pane.left + pane.width; column += 1) if (column >= 0 && column < line.length) line[column] = true;
6504
+ }
6505
+ return occupied;
6506
+ };
6507
+ const resolveBorderSet = (borderLines) => borderLines === "number" ? BORDER_SETS.simple : BORDER_SETS[borderLines];
6508
+ const drawPaneBorders = (rows, occupied, borderLines) => {
6509
+ const borderSet = resolveBorderSet(borderLines);
6510
+ for (let rowIndex = 0; rowIndex < rows.length; rowIndex += 1) {
6511
+ const row = rows[rowIndex];
6512
+ const occupiedRow = occupied[rowIndex];
6513
+ if (!row || !occupiedRow) continue;
6514
+ for (let columnIndex = 0; columnIndex < row.length; columnIndex += 1) {
6515
+ if (occupiedRow[columnIndex]) continue;
6516
+ const left = occupiedRow[columnIndex - 1] ?? false;
6517
+ const right = occupiedRow[columnIndex + 1] ?? false;
6518
+ const top = occupied[rowIndex - 1]?.[columnIndex] ?? false;
6519
+ const bottom = occupied[rowIndex + 1]?.[columnIndex] ?? false;
6520
+ const hasVertical = left || right;
6521
+ const hasHorizontal = top || bottom;
6522
+ if (hasVertical && hasHorizontal) row[columnIndex] = borderSet.intersection;
6523
+ else if (hasVertical) row[columnIndex] = borderSet.vertical;
6524
+ else if (hasHorizontal) row[columnIndex] = borderSet.horizontal;
6525
+ }
6526
+ }
6527
+ };
6528
+ const composeDisplayLines = (panes, width, height, borderLines) => {
6529
+ const rows = Array.from({ length: height }, () => createBlankRow(width));
6530
+ const occupied = createOccupancyGrid(panes, width, height);
6531
+ for (const pane of panes) pane.displayLines.forEach((line, lineIndex) => {
6532
+ const row = rows[pane.top + lineIndex];
6533
+ if (!row) return;
6534
+ createStyledDisplayCells(line).forEach((cell, cellIndex) => {
6535
+ const column = pane.left + cellIndex;
6536
+ if (column < 0 || column >= row.length) return;
6537
+ row[column] = cell;
6538
+ });
6539
+ });
6540
+ drawPaneBorders(rows, occupied, borderLines);
6541
+ return rows.map((row) => row.join(""));
6542
+ };
6543
+ const buildCurrentState = async (tmux, pane, paneId, clientTty) => {
6544
+ if (!pane.inCopyMode) await enterCopyMode(tmux, paneId);
6545
+ const capture = fitCaptureToHeight(await capturePane(tmux, paneId), pane.height);
6546
+ return {
6547
+ currentPath: pane.currentPath,
6548
+ state: {
6549
+ scope: "current",
6550
+ paneId,
6551
+ clientTty,
6552
+ displayLines: capture.displayLines,
6553
+ plainLines: capture.lines,
6554
+ width: pane.width,
6555
+ height: pane.height
6556
+ }
6557
+ };
6558
+ };
6559
+ const buildAllPaneState = async (tmux, pane, paneId, clientTty) => {
6560
+ const panes = await listWindowPanes(tmux, paneId);
6561
+ const borderLines = await getPaneBorderLines(tmux, paneId);
6562
+ const bounds = panes.reduce((accumulator, item) => ({
6563
+ left: Math.min(accumulator.left, item.left),
6564
+ top: Math.min(accumulator.top, item.top),
6565
+ right: Math.max(accumulator.right, item.left + item.width),
6566
+ bottom: Math.max(accumulator.bottom, item.top + item.height)
6567
+ }), {
6568
+ left: Number.POSITIVE_INFINITY,
6569
+ top: Number.POSITIVE_INFINITY,
6570
+ right: Number.NEGATIVE_INFINITY,
6571
+ bottom: Number.NEGATIVE_INFINITY
6572
+ });
6573
+ const snapshots = [];
6574
+ for (const item of panes) {
6575
+ const capture = fitCaptureToHeight(await capturePane(tmux, item.paneId), item.height);
6576
+ snapshots.push({
6577
+ paneId: item.paneId,
6578
+ inCopyMode: item.inCopyMode,
6579
+ width: item.width,
6580
+ height: item.height,
6581
+ left: item.left - bounds.left,
6582
+ top: item.top - bounds.top,
6583
+ plainLines: capture.lines,
6584
+ displayLines: capture.displayLines
6585
+ });
6586
+ }
6587
+ const width = Math.max(0, bounds.right - bounds.left);
6588
+ const height = Math.max(0, bounds.bottom - bounds.top);
6589
+ const x = buildPopupRelativePosition("x", bounds.left);
6590
+ const y = buildPopupRelativePosition("y", bounds.top);
6591
+ await writeDebugLog({
6592
+ event: "start.build-all-pane-state",
6593
+ paneId,
6594
+ clientTty,
6595
+ targetPane: {
6596
+ paneId: pane.paneId,
6597
+ width: pane.width,
6598
+ height: pane.height,
6599
+ inCopyMode: pane.inCopyMode,
6600
+ currentPath: pane.currentPath
6601
+ },
6602
+ bounds,
6603
+ size: {
6604
+ width,
6605
+ height
6606
+ },
6607
+ popupPosition: {
6608
+ x,
6609
+ y
6610
+ },
6611
+ panes: panes.map((item) => ({
6612
+ paneId: item.paneId,
6613
+ left: item.left,
6614
+ top: item.top,
6615
+ width: item.width,
6616
+ height: item.height,
6617
+ active: item.active,
6618
+ inCopyMode: item.inCopyMode,
6619
+ currentPath: item.currentPath
6620
+ }))
6621
+ });
6622
+ return {
6623
+ currentPath: pane.currentPath,
6624
+ x,
6625
+ y,
6626
+ state: {
6627
+ scope: "all",
6628
+ paneId,
6629
+ clientTty,
6630
+ displayLines: composeDisplayLines(snapshots, width, height, borderLines),
6631
+ panes: snapshots,
6632
+ width,
6633
+ height
6634
+ }
6635
+ };
6636
+ };
6358
6637
  const runStart = async (args) => {
6359
- const [paneId, clientTty] = args;
6638
+ const { scope, paneId, clientTty } = parseStartArgs(args);
6360
6639
  if (!process.env.TMUX) {
6361
6640
  console.error("tmux-fuzzy-motion: must be run inside tmux");
6362
6641
  return 2;
@@ -6377,33 +6656,46 @@ const runStart = async (args) => {
6377
6656
  try {
6378
6657
  const pane = await getPaneStartContext(tmux, paneId);
6379
6658
  await focusClientPane(tmux, paneId, clientTty);
6380
- const capture = fitCaptureToHeight(await capturePane(tmux, paneId), pane.height);
6381
- const state = {
6382
- paneId,
6383
- clientTty,
6384
- displayLines: capture.displayLines,
6385
- plainLines: capture.lines,
6386
- width: pane.width,
6387
- height: pane.height
6388
- };
6659
+ const popupState = scope === "all" ? await buildAllPaneState(tmux, pane, paneId, clientTty) : await buildCurrentState(tmux, pane, paneId, clientTty);
6660
+ const state = popupState.state;
6389
6661
  await writeFile(stateFile, JSON.stringify(state), "utf8");
6390
6662
  await ensureDaemon(socketPath);
6391
- await displayPopup(tmux, {
6663
+ const popupOptions = {
6392
6664
  command: buildPopupCommand(stateFile, resultFile, socketPath),
6393
- currentPath: pane.currentPath,
6394
- height: pane.height,
6665
+ currentPath: popupState.currentPath,
6666
+ height: state.height,
6395
6667
  targetClient: clientTty,
6396
6668
  targetPane: paneId,
6397
- width: pane.width
6669
+ width: state.width
6670
+ };
6671
+ if (popupState.x !== void 0) popupOptions.x = popupState.x;
6672
+ if (popupState.y !== void 0) popupOptions.y = popupState.y;
6673
+ await writeDebugLog({
6674
+ event: "start.display-popup",
6675
+ scope,
6676
+ paneId,
6677
+ clientTty,
6678
+ popupOptions: {
6679
+ targetPane: popupOptions.targetPane,
6680
+ targetClient: popupOptions.targetClient,
6681
+ currentPath: popupOptions.currentPath,
6682
+ width: popupOptions.width,
6683
+ height: popupOptions.height,
6684
+ x: popupOptions.x ?? "#{popup_pane_left}",
6685
+ y: popupOptions.y ?? "#{popup_pane_top}"
6686
+ }
6398
6687
  });
6688
+ await displayPopup(tmux, popupOptions);
6399
6689
  const result = await readResult(resultFile);
6400
6690
  if (result.status === "selected") {
6691
+ const targetPaneId = result.target.paneId ?? paneId;
6401
6692
  await tmux.runQuiet([
6402
6693
  "select-pane",
6403
6694
  "-t",
6404
- paneId
6695
+ targetPaneId
6405
6696
  ]);
6406
- await moveCopyCursor(tmux, paneId, result.target);
6697
+ if (state.scope === "all" && !state.panes.some((targetPane) => targetPane.paneId === targetPaneId && targetPane.inCopyMode)) await enterCopyMode(tmux, targetPaneId);
6698
+ await moveCopyCursor(tmux, targetPaneId, result.target);
6407
6699
  }
6408
6700
  return 0;
6409
6701
  } catch (error) {
@@ -6422,7 +6714,7 @@ const runStart = async (args) => {
6422
6714
  const usage = `tmux-fuzzy-motion
6423
6715
 
6424
6716
  Usage:
6425
- tmux-fuzzy-motion start <pane-id> <client-tty>
6717
+ tmux-fuzzy-motion start [--scope current|all] <pane-id> <client-tty>
6426
6718
  tmux-fuzzy-motion popup --state-file <path> --result-file <path> --socket <path>
6427
6719
  tmux-fuzzy-motion popup-live <pane-id>
6428
6720
  tmux-fuzzy-motion daemon --socket <path>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tmux-fuzzy-motion",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
4
4
  "description": "Fuzzy hint motion for tmux copy-mode",
5
5
  "type": "module",
6
6
  "files": [