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.
- package/README.ja.md +43 -20
- package/README.md +48 -20
- package/dist/cli.js +323 -31
- 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`
|
|
6
|
-
|
|
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
|
-
|
|
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. `
|
|
75
|
-
2. `
|
|
76
|
-
3.
|
|
77
|
-
4.
|
|
78
|
-
5.
|
|
79
|
-
6.
|
|
80
|
-
7.
|
|
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
|
-
-
|
|
153
|
-
- `
|
|
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 は
|
|
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
|
|
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
|
|
60
|
-
|
|
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.
|
|
76
|
-
2.
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
-
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
5803
|
-
const isEnterTarget = enterTarget
|
|
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 (
|
|
6010
|
-
const candidates = extractCandidates(
|
|
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"))
|
|
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
|
|
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
|
|
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
|
-
|
|
6663
|
+
const popupOptions = {
|
|
6392
6664
|
command: buildPopupCommand(stateFile, resultFile, socketPath),
|
|
6393
|
-
currentPath:
|
|
6394
|
-
height:
|
|
6665
|
+
currentPath: popupState.currentPath,
|
|
6666
|
+
height: state.height,
|
|
6395
6667
|
targetClient: clientTty,
|
|
6396
6668
|
targetPane: paneId,
|
|
6397
|
-
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
|
-
|
|
6695
|
+
targetPaneId
|
|
6405
6696
|
]);
|
|
6406
|
-
|
|
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>
|