tmux-fuzzy-motion 0.0.8 → 0.0.10

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 (2) hide show
  1. package/dist/cli.js +116 -66
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -40,19 +40,6 @@ const createTmuxClient = () => ({
40
40
  return (await runProcess("tmux", args)).stdout;
41
41
  }
42
42
  });
43
- const focusClientPane = async (tmux, paneId, clientTty) => {
44
- try {
45
- await tmux.run([
46
- "switch-client",
47
- "-c",
48
- clientTty,
49
- "-t",
50
- paneId
51
- ]);
52
- } catch (error) {
53
- throw new Error("tmux-fuzzy-motion: client not found", { cause: error });
54
- }
55
- };
56
43
  const enterCopyMode = async (tmux, paneId) => {
57
44
  try {
58
45
  await tmux.run([
@@ -117,20 +104,26 @@ const listWindowPanes = async (tmux, paneId) => {
117
104
  "-t",
118
105
  paneId,
119
106
  "-F",
120
- "#{pane_id} #{pane_in_mode} #{pane_width} #{pane_height} #{pane_current_path} #{pane_left} #{pane_top} #{?pane_active,1,0} #{window_zoomed_flag}"
107
+ "#{pane_id} #{pane_in_mode} #{pane_width} #{pane_height} #{pane_current_path} #{pane_left} #{pane_top} #{?pane_active,1,0} #{window_zoomed_flag} #{pane-border-lines}"
121
108
  ])).trim();
122
109
  } catch (error) {
123
110
  throw new Error("tmux-fuzzy-motion: pane not found", { cause: error });
124
111
  }
125
112
  const panes = output.split("\n").filter((line) => line.length > 0).map((line) => {
126
- const [resolvedPaneId, paneInMode, width, height, currentPath, left, top, active, zoomed] = line.split(" ");
113
+ const [resolvedPaneId, paneInMode, width, height, currentPath, left, top, active, zoomed, borderLines] = line.split(" ");
127
114
  const numeric = [
128
115
  width,
129
116
  height,
130
117
  left,
131
118
  top
132
119
  ].map((value) => Number(value));
133
- 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");
120
+ if (!resolvedPaneId || !paneInMode || !currentPath || [
121
+ active,
122
+ zoomed,
123
+ borderLines
124
+ ].some((value) => value === void 0) || numeric.some((value) => !Number.isFinite(value))) throw new Error("tmux-fuzzy-motion: failed to resolve window panes");
125
+ if (borderLines !== "single" && borderLines !== "double" && borderLines !== "heavy" && borderLines !== "simple" && borderLines !== "number" && borderLines !== "spaces") throw new Error("tmux-fuzzy-motion: failed to resolve window panes");
126
+ const resolvedBorderLines = borderLines;
134
127
  return {
135
128
  paneId: resolvedPaneId,
136
129
  inCopyMode: paneInMode === "1",
@@ -140,6 +133,7 @@ const listWindowPanes = async (tmux, paneId) => {
140
133
  left: Number(left),
141
134
  top: Number(top),
142
135
  active: active === "1",
136
+ borderLines: resolvedBorderLines,
143
137
  zoomed: zoomed === "1"
144
138
  };
145
139
  });
@@ -150,23 +144,6 @@ const listWindowPanes = async (tmux, paneId) => {
150
144
  return rest;
151
145
  });
152
146
  };
153
- const getPaneBorderLines = async (tmux, paneId) => {
154
- let output = "";
155
- try {
156
- output = (await tmux.capture([
157
- "show-options",
158
- "-A",
159
- "-wv",
160
- "-t",
161
- paneId,
162
- "pane-border-lines"
163
- ])).trim();
164
- } catch (error) {
165
- throw new Error("tmux-fuzzy-motion: failed to resolve pane border lines", { cause: error });
166
- }
167
- if (output === "single" || output === "double" || output === "heavy" || output === "simple" || output === "number" || output === "spaces") return output;
168
- throw new Error("tmux-fuzzy-motion: failed to resolve pane border lines");
169
- };
170
147
  const getTmuxVersion = async () => {
171
148
  return (await runProcess("tmux", ["-V"])).stdout.trim();
172
149
  };
@@ -4727,7 +4704,7 @@ const capturePane = async (tmux, paneId) => {
4727
4704
  };
4728
4705
  //#endregion
4729
4706
  //#region src/core/width.ts
4730
- const ANSI_PATTERN = /\u001B\[[0-9;]*m/gu;
4707
+ const ANSI_PATTERN$1 = /\u001B\[[0-9;]*m/gu;
4731
4708
  const RESET$2 = "\x1B[0m";
4732
4709
  const displayWidth = (value) => stringWidth(value);
4733
4710
  const codeUnitIndexToColumn = (value, index) => stringWidth(value.slice(0, index));
@@ -4765,11 +4742,52 @@ const codeUnitRangeToColumns = (value, start, end) => {
4765
4742
  }
4766
4743
  return columns;
4767
4744
  };
4745
+ const createCompactStyledDisplayCells = (value) => {
4746
+ const cells = [];
4747
+ let activeStyle = "";
4748
+ let pendingStyle = "";
4749
+ let styleOpen = false;
4750
+ let lastVisibleCellIndex = -1;
4751
+ let lastIndex = 0;
4752
+ const appendChunk = (chunk) => {
4753
+ for (const char of chunk) {
4754
+ const width = Math.max(1, stringWidth(char));
4755
+ const prefix = pendingStyle.length > 0 ? pendingStyle : !styleOpen && activeStyle.length > 0 ? activeStyle : "";
4756
+ cells.push(`${prefix}${char}`);
4757
+ if (prefix.length > 0) {
4758
+ pendingStyle = "";
4759
+ styleOpen = true;
4760
+ }
4761
+ lastVisibleCellIndex = cells.length - 1;
4762
+ for (let offset = 1; offset < width; offset += 1) cells.push("");
4763
+ }
4764
+ };
4765
+ const closeStyle = () => {
4766
+ if (styleOpen && lastVisibleCellIndex >= 0) cells[lastVisibleCellIndex] = `${cells[lastVisibleCellIndex]}${RESET$2}`;
4767
+ activeStyle = "";
4768
+ pendingStyle = "";
4769
+ styleOpen = false;
4770
+ };
4771
+ for (const match of value.matchAll(ANSI_PATTERN$1)) {
4772
+ const start = match.index ?? 0;
4773
+ appendChunk(value.slice(lastIndex, start));
4774
+ const sequence = match[0];
4775
+ if (sequence === RESET$2) closeStyle();
4776
+ else {
4777
+ activeStyle = `${activeStyle}${sequence}`;
4778
+ pendingStyle = `${pendingStyle}${sequence}`;
4779
+ }
4780
+ lastIndex = start + sequence.length;
4781
+ }
4782
+ appendChunk(value.slice(lastIndex));
4783
+ if (styleOpen) closeStyle();
4784
+ return cells;
4785
+ };
4768
4786
  const createStyledDisplayCells = (value) => {
4769
4787
  const cells = [];
4770
4788
  let activeStyle = "";
4771
4789
  let lastIndex = 0;
4772
- for (const match of value.matchAll(ANSI_PATTERN)) {
4790
+ for (const match of value.matchAll(ANSI_PATTERN$1)) {
4773
4791
  const start = match.index ?? 0;
4774
4792
  const chunk = value.slice(lastIndex, start);
4775
4793
  for (const char of chunk) {
@@ -4816,9 +4834,19 @@ const PATTERNS = [
4816
4834
  }
4817
4835
  ];
4818
4836
  const isContainedByHigherPriority = (current, others) => others.some((candidate) => candidate.line === current.line && candidate.priority < current.priority && candidate.startIndex <= current.startIndex && candidate.endIndex >= current.endIndex);
4837
+ const dedupeLineCandidates = (candidates) => {
4838
+ const byStartIndex = /* @__PURE__ */ new Map();
4839
+ for (const candidate of candidates) {
4840
+ const current = byStartIndex.get(candidate.startIndex);
4841
+ if (!current || candidate.priority < current.priority) byStartIndex.set(candidate.startIndex, candidate);
4842
+ }
4843
+ const deduped = [...byStartIndex.values()];
4844
+ return deduped.filter((candidate) => !isContainedByHigherPriority(candidate, deduped));
4845
+ };
4819
4846
  const extractCandidates = (lines) => {
4820
4847
  const collected = [];
4821
4848
  lines.forEach((lineText, lineIndex) => {
4849
+ const lineCandidates = [];
4822
4850
  PATTERNS.forEach(({ kind, pattern }, priority) => {
4823
4851
  const regex = new RegExp(pattern.source, pattern.flags);
4824
4852
  for (const match of lineText.matchAll(regex)) {
@@ -4829,7 +4857,7 @@ const extractCandidates = (lines) => {
4829
4857
  if (width === 1 && kind !== "word") continue;
4830
4858
  const col = codeUnitIndexToColumn(lineText, startIndex);
4831
4859
  const charCol = codeUnitIndexToCharacterIndex(lineText, startIndex);
4832
- collected.push({
4860
+ lineCandidates.push({
4833
4861
  kind,
4834
4862
  text,
4835
4863
  line: lineIndex + 1,
@@ -4842,10 +4870,9 @@ const extractCandidates = (lines) => {
4842
4870
  });
4843
4871
  }
4844
4872
  });
4873
+ collected.push(...dedupeLineCandidates(lineCandidates));
4845
4874
  });
4846
- return collected.filter((candidate, _, source) => {
4847
- return source.find((other) => other.line === candidate.line && other.startIndex === candidate.startIndex && other.priority <= candidate.priority) === candidate;
4848
- }).filter((candidate, _, source) => !isContainedByHigherPriority(candidate, source)).sort((left, right) => left.line - right.line || left.col - right.col || left.priority - right.priority).map((candidate) => ({
4875
+ return collected.sort((left, right) => left.line - right.line || left.col - right.col || left.priority - right.priority).map((candidate) => ({
4849
4876
  kind: candidate.kind,
4850
4877
  text: candidate.text,
4851
4878
  line: candidate.line,
@@ -5851,10 +5878,19 @@ const measureCellWidth = (cells, start) => {
5851
5878
  return width;
5852
5879
  };
5853
5880
  const createOverlayRenderer = (lines) => {
5854
- const baseCellsByLine = lines.map((line) => createStyledDisplayCells(line));
5855
- const baseLines = baseCellsByLine.map((cells) => cells.join(""));
5881
+ const baseLines = [...lines];
5882
+ const baseCellsByLine = /* @__PURE__ */ new Map();
5856
5883
  const targetLine = (target) => (target.screenLine ?? target.line) - 1;
5857
5884
  const targetCol = (target) => target.screenCol ?? target.col;
5885
+ const baseCellsForLine = (lineIndex) => {
5886
+ const line = lines[lineIndex];
5887
+ if (line === void 0) return;
5888
+ const existing = baseCellsByLine.get(lineIndex);
5889
+ if (existing) return existing;
5890
+ const cells = createStyledDisplayCells(line);
5891
+ baseCellsByLine.set(lineIndex, cells);
5892
+ return cells;
5893
+ };
5858
5894
  return (targets) => {
5859
5895
  const rendered = [...baseLines];
5860
5896
  const mutableCells = /* @__PURE__ */ new Map();
@@ -5863,7 +5899,7 @@ const createOverlayRenderer = (lines) => {
5863
5899
  const sorted = [...targets].sort((left, right) => targetLine(left) - targetLine(right) || targetCol(left) + left.primary - (targetCol(right) + right.primary));
5864
5900
  for (const target of sorted) {
5865
5901
  const lineIndex = targetLine(target);
5866
- const baseCells = baseCellsByLine[lineIndex];
5902
+ const baseCells = baseCellsForLine(lineIndex);
5867
5903
  if (!baseCells) continue;
5868
5904
  let cells = mutableCells.get(lineIndex);
5869
5905
  if (!cells) {
@@ -5961,6 +5997,7 @@ const withRawMode = async (input, output, callback) => {
5961
5997
  //#region src/commands/input.ts
5962
5998
  const QUERY_STYLE = "\x1B[48;5;236;38;5;252m";
5963
5999
  const RESET = "\x1B[0m";
6000
+ const ANSI_PATTERN = /\u001B\[[0-9;]*m/u;
5964
6001
  const HINT_CHARS = /* @__PURE__ */ new Set("ASDFGHJKLQWERTYUIOPZXCVBNM");
5965
6002
  const WORD_CHAR_PATTERN = /[a-z0-9_-]/u;
5966
6003
  const parsePopupArgs = (args) => {
@@ -6036,9 +6073,14 @@ const fitBodyToHeight = (lines, height) => {
6036
6073
  return next;
6037
6074
  };
6038
6075
  const renderQueryOnBottomLine = (line, width, query) => {
6076
+ if (query.length === 0) {
6077
+ const cells = createCompactStyledDisplayCells(line);
6078
+ while (cells.length < width) cells.push(" ");
6079
+ const rendered = cells.slice(0, width).join("");
6080
+ return ANSI_PATTERN.test(rendered) && !rendered.endsWith(RESET) ? `${rendered}${RESET}` : rendered;
6081
+ }
6039
6082
  const cells = createStyledDisplayCells(line);
6040
6083
  while (cells.length < width) cells.push(" ");
6041
- if (query.length === 0) return cells.slice(0, width).join("");
6042
6084
  const queryWidth = Math.min(width, stringWidth(query));
6043
6085
  const start = Math.max(0, width - queryWidth);
6044
6086
  const content = Array.from(query);
@@ -6055,15 +6097,20 @@ const renderQueryOnBottomLine = (line, width, query) => {
6055
6097
  return cells.slice(0, width).join("");
6056
6098
  };
6057
6099
  const createFrame = (state, query, matches, renderOverlay) => {
6058
- const body = fitBodyToHeight(query.length > 0 && matches.length > 0 ? renderOverlay(matches) : state.displayLines, state.height);
6100
+ const body = fitBodyToHeight(query.length > 0 && matches.length > 0 && renderOverlay ? renderOverlay(matches) : state.displayLines, state.height);
6059
6101
  const lastLineIndex = Math.max(0, body.length - 1);
6060
6102
  body[lastLineIndex] = renderQueryOnBottomLine(body[lastLineIndex] ?? "", state.width, query);
6061
6103
  return { body };
6062
6104
  };
6105
+ const createInitialFrameOutput = (frame) => {
6106
+ const full = frame.body.map((line) => line.trimEnd()).join("\n");
6107
+ const sparse = frame.body.map((line, index) => [index, line.trimEnd()]).filter(([, line]) => line.length > 0).map(([index, line]) => `\u001B[${index + 1};1H${line}`).join("");
6108
+ return sparse.length < full.length ? sparse : full;
6109
+ };
6063
6110
  const writeFullFrame = (output, frame) => {
6064
6111
  output.write("\x1B[?7l");
6065
6112
  clearScreen(output);
6066
- output.write(frame.body.join("\n"));
6113
+ output.write(createInitialFrameOutput(frame));
6067
6114
  output.write("\x1B[H\x1B[?7h");
6068
6115
  };
6069
6116
  const renderFrame = (output, frame, previousFrame) => {
@@ -6173,7 +6220,11 @@ const expectResponse = (response, type) => {
6173
6220
  return response;
6174
6221
  };
6175
6222
  const runPopupJob = async (state, options) => {
6176
- const overlayRenderer = createOverlayRenderer(state.displayLines);
6223
+ let overlayRenderer = null;
6224
+ const getOverlayRenderer = () => {
6225
+ overlayRenderer ??= createOverlayRenderer(state.displayLines);
6226
+ return overlayRenderer;
6227
+ };
6177
6228
  let query = "";
6178
6229
  let previousHints = /* @__PURE__ */ new Map();
6179
6230
  let matches = [];
@@ -6181,7 +6232,7 @@ const runPopupJob = async (state, options) => {
6181
6232
  const output = process.stdout;
6182
6233
  await withRawMode(input, output, async () => {
6183
6234
  const reader = createByteReader(input);
6184
- let previousFrame = renderFrame(output, createFrame(state, query, matches, overlayRenderer));
6235
+ let previousFrame = renderFrame(output, createFrame(state, query, matches));
6185
6236
  try {
6186
6237
  while (true) {
6187
6238
  const value = await reader.nextByte();
@@ -6224,7 +6275,7 @@ const runPopupJob = async (state, options) => {
6224
6275
  }
6225
6276
  matches = query.length === 0 ? [] : await options.onMatch(query, previousHints);
6226
6277
  previousHints = new Map(matches.map((target) => [createTargetKey(target), target.hint]));
6227
- previousFrame = renderFrame(output, createFrame(state, query, matches, overlayRenderer), previousFrame);
6278
+ previousFrame = renderFrame(output, createFrame(state, query, matches, matches.length > 0 ? getOverlayRenderer() : void 0), previousFrame);
6228
6279
  }
6229
6280
  } finally {
6230
6281
  reader.close();
@@ -6542,14 +6593,14 @@ const composeDisplayLines = (panes, width, height, borderLines) => {
6542
6593
  for (const pane of panes) pane.displayLines.forEach((line, lineIndex) => {
6543
6594
  const row = rows[pane.top + lineIndex];
6544
6595
  if (!row) return;
6545
- createStyledDisplayCells(line).forEach((cell, cellIndex) => {
6596
+ createCompactStyledDisplayCells(line).forEach((cell, cellIndex) => {
6546
6597
  const column = pane.left + cellIndex;
6547
6598
  if (column < 0 || column >= row.length) return;
6548
6599
  row[column] = cell;
6549
6600
  });
6550
6601
  });
6551
6602
  drawPaneBorders(rows, occupied, borderLines);
6552
- return rows.map((row) => row.join(""));
6603
+ return rows.map((row) => row.join("").trimEnd());
6553
6604
  };
6554
6605
  const buildCurrentState = async (tmux, pane, paneId, clientTty) => {
6555
6606
  if (!pane.inCopyMode) await enterCopyMode(tmux, paneId);
@@ -6567,9 +6618,11 @@ const buildCurrentState = async (tmux, pane, paneId, clientTty) => {
6567
6618
  }
6568
6619
  };
6569
6620
  };
6570
- const buildAllPaneState = async (tmux, pane, paneId, clientTty) => {
6621
+ const buildAllPaneState = async (tmux, paneId, clientTty) => {
6571
6622
  const panes = await listWindowPanes(tmux, paneId);
6572
- const borderLines = await getPaneBorderLines(tmux, paneId);
6623
+ const targetPane = panes.find((item) => item.paneId === paneId) ?? panes.find((item) => item.active) ?? panes[0];
6624
+ if (!targetPane) throw new Error("tmux-fuzzy-motion: pane not found");
6625
+ const borderLines = targetPane.borderLines;
6573
6626
  const bounds = panes.reduce((accumulator, item) => ({
6574
6627
  left: Math.min(accumulator.left, item.left),
6575
6628
  top: Math.min(accumulator.top, item.top),
@@ -6581,10 +6634,9 @@ const buildAllPaneState = async (tmux, pane, paneId, clientTty) => {
6581
6634
  right: Number.NEGATIVE_INFINITY,
6582
6635
  bottom: Number.NEGATIVE_INFINITY
6583
6636
  });
6584
- const snapshots = [];
6585
- for (const item of panes) {
6637
+ const snapshots = await Promise.all(panes.map(async (item) => {
6586
6638
  const capture = fitCaptureToHeight(await capturePane(tmux, item.paneId), item.height);
6587
- snapshots.push({
6639
+ return {
6588
6640
  paneId: item.paneId,
6589
6641
  inCopyMode: item.inCopyMode,
6590
6642
  width: item.width,
@@ -6593,8 +6645,8 @@ const buildAllPaneState = async (tmux, pane, paneId, clientTty) => {
6593
6645
  top: item.top - bounds.top,
6594
6646
  plainLines: capture.lines,
6595
6647
  displayLines: capture.displayLines
6596
- });
6597
- }
6648
+ };
6649
+ }));
6598
6650
  const width = Math.max(0, bounds.right - bounds.left);
6599
6651
  const height = Math.max(0, bounds.bottom - bounds.top);
6600
6652
  const x = buildPopupRelativePosition("x", bounds.left);
@@ -6604,11 +6656,11 @@ const buildAllPaneState = async (tmux, pane, paneId, clientTty) => {
6604
6656
  paneId,
6605
6657
  clientTty,
6606
6658
  targetPane: {
6607
- paneId: pane.paneId,
6608
- width: pane.width,
6609
- height: pane.height,
6610
- inCopyMode: pane.inCopyMode,
6611
- currentPath: pane.currentPath
6659
+ paneId: targetPane.paneId,
6660
+ width: targetPane.width,
6661
+ height: targetPane.height,
6662
+ inCopyMode: targetPane.inCopyMode,
6663
+ currentPath: targetPane.currentPath
6612
6664
  },
6613
6665
  bounds,
6614
6666
  size: {
@@ -6631,7 +6683,7 @@ const buildAllPaneState = async (tmux, pane, paneId, clientTty) => {
6631
6683
  }))
6632
6684
  });
6633
6685
  return {
6634
- currentPath: pane.currentPath,
6686
+ currentPath: targetPane.currentPath,
6635
6687
  x,
6636
6688
  y,
6637
6689
  state: {
@@ -6665,9 +6717,7 @@ const runStart = async (args) => {
6665
6717
  const resultFile = join(tempDir, "result.json");
6666
6718
  const socketPath = createDaemonSocketPath();
6667
6719
  try {
6668
- const pane = await getPaneStartContext(tmux, paneId);
6669
- await focusClientPane(tmux, paneId, clientTty);
6670
- const popupState = scope === "all" ? await buildAllPaneState(tmux, pane, paneId, clientTty) : await buildCurrentState(tmux, pane, paneId, clientTty);
6720
+ const popupState = scope === "all" ? await buildAllPaneState(tmux, paneId, clientTty) : await buildCurrentState(tmux, await getPaneStartContext(tmux, paneId), paneId, clientTty);
6671
6721
  const state = popupState.state;
6672
6722
  await writeFile(stateFile, JSON.stringify(state), "utf8");
6673
6723
  await ensureDaemon(socketPath);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tmux-fuzzy-motion",
3
- "version": "0.0.8",
3
+ "version": "0.0.10",
4
4
  "description": "Fuzzy hint motion for tmux copy-mode",
5
5
  "type": "module",
6
6
  "files": [