Skip to content

Commit 9bdd712

Browse files
committed
feat: Add support for multiple panes in easymotion.py
1 parent 463dbd7 commit 9bdd712

File tree

2 files changed

+92
-66
lines changed

2 files changed

+92
-66
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,4 @@ pytest test_easymotion.py -v --cache-clear
4242
~~- ex. `'哈哈'`.~~
4343
- ~~Scrolled up panes are not supported~~
4444
- ~~Broken when tmux window has split panes~~
45-
- Jump between panes is not supported
45+
- ~~Jump between panes is not supported~~

easymotion.py

Lines changed: 91 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import curses
33
import re
44
import os
5-
import itertools
65
import unicodedata
76

87
KEYS='asdfghjkl;'
@@ -39,6 +38,25 @@ def pyshell(cmd):
3938
return result
4039
return os.popen(cmd).read()
4140

41+
def get_visible_panes():
42+
return pyshell('tmux list-panes -F "#{pane_id}" -t "{last}"').strip().split('\n')
43+
44+
class PaneInfo:
45+
def __init__(self, pane_id, start_y, height, start_x, width):
46+
self.pane_id = pane_id
47+
self.start_y = start_y
48+
self.height = height
49+
self.start_x = start_x
50+
self.width = width
51+
self.content = None
52+
self.positions = []
53+
54+
def get_pane_info(pane_id):
55+
"""Get pane position and size information"""
56+
cmd = f'tmux display-message -p -t {pane_id} "#{{pane_top}} #{{pane_height}} #{{pane_left}} #{{pane_width}}"'
57+
top, height, left, width = map(int, pyshell(cmd).strip().split())
58+
return PaneInfo(pane_id, top, height, left, width)
59+
4260
def tmux_pane_id():
4361
# Get the ID of the pane that launched this script
4462
source_pane = os.environ.get('TMUX_PANE')
@@ -112,94 +130,102 @@ def generate_hints(keys):
112130
GREEN = 2
113131

114132
def main(stdscr):
115-
pane_id = tmux_pane_id()
116-
scroll_position = get_scroll_position(pane_id)
117-
captured_pane = tmux_capture_pane(pane_id)
133+
panes = []
134+
for pane_id in get_visible_panes():
135+
pane = get_pane_info(pane_id)
136+
pane.content = tmux_capture_pane(pane_id)
137+
panes.append(pane)
118138

119-
# invisible cursor
120139
curses.curs_set(False)
121-
122-
# get screen width
123-
_, width = stdscr.getmaxyx()
124-
125-
# init default colors
126140
curses.start_color()
127141
curses.use_default_colors()
128142
curses.init_pair(RED, curses.COLOR_RED, -1)
129143
curses.init_pair(GREEN, curses.COLOR_GREEN, -1)
130144

131-
# keys = 'abcd', hints = a, b, c, d, aa, ab, ac .... dd
132145
hints = generate_hints(KEYS)
133146
hints_dict = {hint: i for i, hint in enumerate(hints)}
134147

135-
# wrap newline to fixed width space
136-
fixed_width_pane = fill_pane_content_with_space(captured_pane, width)
137-
138-
# Split into lines and add each line separately
139-
for y, line in enumerate(fixed_width_pane.splitlines()):
140-
try:
141-
stdscr.addstr(y, 0, line)
142-
except curses.error:
143-
pass # Ignore errors from writing to bottom-right corner
148+
# Draw all pane contents
149+
for pane in panes:
150+
fixed_width_content = fill_pane_content_with_space(pane.content, pane.width)
151+
for y, line in enumerate(fixed_width_content.splitlines()[:pane.height]):
152+
try:
153+
stdscr.addstr(pane.start_y + y, pane.start_x, line[:pane.width])
154+
except curses.error:
155+
pass
144156
stdscr.refresh()
157+
145158
search_ch = stdscr.getkey()
146159

147-
# Track positions by line number and column
148-
positions = []
149-
lines = captured_pane.splitlines()
150-
for line_num, line in enumerate(lines):
151-
for match in re.finditer(search_ch, line.lower()):
152-
visual_col = sum(get_char_width(c) for c in line[:match.start()])
153-
positions.append((line_num, visual_col, line[match.start()]))
154-
155-
# render 1st hints
156-
for i, (line_num, col, char) in enumerate(positions):
157-
if i >= len(hints):
158-
break
159-
y = line_num
160-
x = col
161-
stdscr.addstr(y, x, hints[i][0], curses.color_pair(RED))
162-
char_width = get_char_width(char)
163-
if x + char_width < width:
164-
stdscr.addstr(y, x + char_width, hints[i][1], curses.color_pair(GREEN))
160+
# Find matches in all panes
161+
hint_index = 0
162+
for pane in panes:
163+
lines = pane.content.splitlines()
164+
for line_num, line in enumerate(lines):
165+
for match in re.finditer(search_ch, line.lower()):
166+
if hint_index >= len(hints):
167+
continue
168+
visual_col = sum(get_char_width(c) for c in line[:match.start()])
169+
pane.positions.append((line_num, visual_col, line[match.start()], hints[hint_index]))
170+
hint_index += 1
171+
172+
# Draw hints
173+
for pane in panes:
174+
for line_num, col, char, hint in pane.positions:
175+
y = pane.start_y + line_num
176+
x = pane.start_x + col
177+
if y < pane.start_y + pane.height and x < pane.start_x + pane.width:
178+
stdscr.addstr(y, x, hint[0], curses.color_pair(RED))
179+
char_width = get_char_width(char)
180+
if x + char_width < pane.start_x + pane.width:
181+
stdscr.addstr(y, x + char_width, hint[1], curses.color_pair(GREEN))
165182
stdscr.refresh()
166183

184+
# Handle hint selection
167185
ch1 = stdscr.getkey()
168-
if ch1 not in KEYS:
186+
if ch1 not in KEYS:
187+
cleanup_window()
169188
exit(0)
170189

171-
# render 2nd hints
172-
for y, line in enumerate(fixed_width_pane.splitlines()):
173-
try:
174-
stdscr.addstr(y, 0, line)
175-
except curses.error:
176-
pass
177-
for i, (line_num, col, char) in enumerate(positions):
178-
if not hints[i].startswith(ch1) or len(hints[i]) < 2:
179-
continue
180-
y = line_num
181-
x = col
182-
char_width = get_char_width(char)
183-
if x + char_width < width:
184-
stdscr.addstr(y, x + char_width, hints[i][1], curses.color_pair(GREEN))
190+
# Redraw and show second character hints
191+
for pane in panes:
192+
fixed_width_content = fill_pane_content_with_space(pane.content, pane.width)
193+
for y, line in enumerate(fixed_width_content.splitlines()[:pane.height]):
194+
try:
195+
stdscr.addstr(pane.start_y + y, pane.start_x, line[:pane.width])
196+
except curses.error:
197+
pass
198+
199+
for line_num, col, char, hint in pane.positions:
200+
if not hint.startswith(ch1):
201+
continue
202+
y = pane.start_y + line_num
203+
x = pane.start_x + col
204+
char_width = get_char_width(char)
205+
if x + char_width < pane.start_x + pane.width:
206+
stdscr.addstr(y, x + char_width, hint[1], curses.color_pair(GREEN))
185207
stdscr.refresh()
186208

187209
ch2 = stdscr.getkey()
188210
if ch2 not in KEYS:
189211
cleanup_window()
190212
exit(0)
191-
# Calculate final position based on line and column
192-
target_pos = positions[hints_dict[ch1+ch2]]
193-
line_offset = sum(len(line) + 1 for line in lines[:target_pos[0]])
194-
true_col = get_true_position(lines[target_pos[0]], target_pos[1])
195-
final_pos = line_offset + true_col # Convert visual position to true position
196-
197-
# Adjust for scroll position
198-
if scroll_position > 0:
199-
tmux_move_cursor(pane_id, final_pos)
200-
else:
201-
# If not scrolled, move to absolute position
202-
tmux_move_cursor(pane_id, final_pos)
213+
214+
# Find target pane and position
215+
target_hint = ch1 + ch2
216+
for pane in panes:
217+
for line_num, col, char, hint in pane.positions:
218+
if hint == target_hint:
219+
lines = pane.content.splitlines()
220+
line_offset = sum(len(line) + 1 for line in lines[:line_num])
221+
true_col = get_true_position(lines[line_num], col)
222+
final_pos = line_offset + true_col
223+
224+
scroll_position = get_scroll_position(pane.pane_id)
225+
tmux_move_cursor(pane.pane_id, final_pos)
226+
pyshell(f'tmux select-pane -t {pane.pane_id}')
227+
break
228+
203229
cleanup_window()
204230

205231
if __name__ == '__main__':

0 commit comments

Comments
 (0)