11#!/usr/bin/env python3
22import curses
3- import re
43import os
4+ import re
55import unicodedata
66
7- KEYS = 'asdfghjkl;'
7+ KEYS = 'asdfghjkl;'
88stdscr = None
99
10+
1011def get_char_width (char ):
1112 """Get visual width of a single character"""
1213 return 2 if unicodedata .east_asian_width (char ) in 'WF' else 1
1314
15+
1416def get_string_width (s ):
1517 """Calculate visual width of string, accounting for double-width characters"""
1618 width = 0
1719 for c in s :
1820 width += get_char_width (c )
1921 return width
2022
23+
2124def get_true_position (line , target_col ):
2225 """Calculate true position accounting for wide characters"""
2326 visual_pos = 0
@@ -28,6 +31,7 @@ def get_true_position(line, target_col):
2831 true_pos += 1
2932 return true_pos
3033
34+
3135def pyshell (cmd ):
3236 debug = os .environ .get ('TMUX_EASYMOTION_DEBUG' ) == 'true'
3337 if debug :
@@ -39,15 +43,20 @@ def pyshell(cmd):
3943 return result
4044 return os .popen (cmd ).read ()
4145
46+
4247def get_visible_panes ():
43- panes = pyshell ('tmux list-panes -F "#{pane_id},#{window_zoomed_flag},#{pane_active}" -t "{last}"' ).strip ().split ('\n ' )
48+ panes = pyshell (
49+ 'tmux list-panes -F "#{pane_id},#{window_zoomed_flag},#{pane_active}" -t "{last}"'
50+ ).strip ().split ('\n ' )
4451 panes = [v .split (',' ) for v in panes ]
4552 if panes [0 ][1 ] == "1" :
4653 return [v [0 ] for v in panes if v [2 ] == "1" ]
4754 else :
4855 return [v [0 ] for v in panes ]
4956
57+
5058class PaneInfo :
59+
5160 def __init__ (self , pane_id , start_y , height , start_x , width ):
5261 self .pane_id = pane_id
5362 self .start_y = start_y
@@ -59,54 +68,65 @@ def __init__(self, pane_id, start_y, height, start_x, width):
5968 self .copy_mode = False
6069 self .scroll_position = 0
6170
71+
6272def get_pane_info (pane_id ):
6373 """Get pane position and size information"""
6474 cmd = f'tmux display-message -p -t { pane_id } "#{{pane_top}} #{{pane_height}} #{{pane_left}} #{{pane_width}}"'
6575 top , height , left , width = map (int , pyshell (cmd ).strip ().split ())
6676 pane = PaneInfo (pane_id , top , height , left , width )
67- copy_mode = pyshell (f'tmux display-message -p -t { pane_id } "#{{pane_in_mode}}"' ).strip ()
77+ copy_mode = pyshell (
78+ f'tmux display-message -p -t { pane_id } "#{{pane_in_mode}}"' ).strip ()
6879 if copy_mode == "1" :
6980 pane .copy_mode = True
70- scroll_pos = pyshell (f'tmux display-message -p -t { pane_id } "#{{scroll_position}}"' ).strip ()
81+ scroll_pos = pyshell (
82+ f'tmux display-message -p -t { pane_id } "#{{scroll_position}}"' ).strip (
83+ )
7184 try :
72- pane .scroll_position = int (scroll_pos )
73- except :
74- pane .scroll_position = 0
85+ pane .scroll_position = int (scroll_pos )
86+ except ValueError :
87+ pane .scroll_position = 0
7588 return pane
7689
90+
7791def tmux_pane_id ():
7892 # Get the ID of the pane that launched this script
7993 source_pane = os .environ .get ('TMUX_PANE' )
8094 if not source_pane :
8195 return '%0'
8296
8397 # We're in a new window, get the pane from the previous window
84- previous_pane = pyshell ('tmux list-panes -F "#{pane_id}" -t "{last}"' ).strip ()
98+ previous_pane = pyshell (
99+ 'tmux list-panes -F "#{pane_id}" -t "{last}"' ).strip ()
85100 if re .match (r'%\d+' , previous_pane ):
86101 return previous_pane .split ('\n ' )[0 ]
87102
88103 # Fallback to current pane if can't get previous
89104 return pyshell ('tmux display-message -p "#{pane_id}"' ).strip ()
90105
106+
91107def cleanup_window ():
92108 """Close the current window if we opened in a new one"""
93109 current_window = pyshell ('tmux display-message -p "#{window_id}"' ).strip ()
94- previous_window = pyshell ('tmux display-message -p "#{window_id}" -t "{last}"' ).strip ()
110+ previous_window = pyshell (
111+ 'tmux display-message -p "#{window_id}" -t "{last}"' ).strip ()
95112 if current_window != previous_window :
96113 pyshell ('tmux kill-window' )
97114
115+
98116def tmux_capture_pane (pane ):
99117 if pane .scroll_position > 0 :
100118 # When scrolled up, use negative numbers to capture from history
101119 # -scroll_pos is where we are in history
102120 # -(scroll_pos - curses.LINES + 1) captures one screen worth from there
103- end_pos = - (pane .scroll_position - curses .LINES + 1 ) # Calculate separately to avoid string formatting issues
121+ end_pos = - (pane .scroll_position - curses .LINES + 1
122+ ) # Calculate separately to avoid string formatting issues
104123 cmd = f'tmux capture-pane -p -S -{ pane .scroll_position } -E { end_pos } -t { pane .pane_id } '
105124 else :
106125 # If not scrolled, just capture current view (default behavior)
107126 cmd = f'tmux capture-pane -p -t { pane .pane_id } '
108127 return pyshell (cmd )[:- 1 ]
109128
129+
110130def fill_pane_content_with_space (pane_content , width ):
111131 lines = pane_content .splitlines ()
112132 result = []
@@ -129,13 +149,16 @@ def tmux_move_cursor(pane, line_num, true_col):
129149 cmd += f' \\ ; send-keys -X -t { pane .pane_id } -N { true_col } cursor-right'
130150 pyshell (cmd )
131151
152+
132153def generate_hints (keys ):
133154 """Generate two-character hints from key set more efficiently"""
134155 return [k1 + k2 for k1 in keys for k2 in keys ]
135156
157+
136158RED = 1
137159GREEN = 2
138160
161+
139162def main (stdscr ):
140163 panes = []
141164 for pane_id in get_visible_panes ():
@@ -153,10 +176,13 @@ def main(stdscr):
153176
154177 # Draw all pane contents
155178 for pane in panes :
156- fixed_width_content = fill_pane_content_with_space (pane .content , pane .width )
157- for y , line in enumerate (fixed_width_content .splitlines ()[:pane .height ]):
179+ fixed_width_content = fill_pane_content_with_space (
180+ pane .content , pane .width )
181+ for y , line in enumerate (
182+ fixed_width_content .splitlines ()[:pane .height ]):
158183 try :
159- stdscr .addstr (pane .start_y + y , pane .start_x , line [:pane .width ])
184+ stdscr .addstr (pane .start_y + y , pane .start_x ,
185+ line [:pane .width ])
160186 except curses .error :
161187 pass
162188 stdscr .refresh ()
@@ -171,22 +197,25 @@ def main(stdscr):
171197 for match in re .finditer (search_ch , line .lower ()):
172198 if hint_index >= len (hints ):
173199 continue
174- visual_col = sum (get_char_width (c ) for c in line [:match .start ()])
175- pane .positions .append ((line_num , visual_col , line [match .start ()], hints [hint_index ]))
200+ visual_col = sum (
201+ get_char_width (c ) for c in line [:match .start ()])
202+ pane .positions .append ((line_num , visual_col ,
203+ line [match .start ()], hints [hint_index ]))
176204 hint_index += 1
177205
178206 # Draw hints
179207 for pane in panes :
180208 for line_num , col , char , hint in pane .positions :
181209 y = pane .start_y + line_num
182210 x = pane .start_x + col
183- if (y < pane .start_y + pane .height and
184- x < pane .start_x + pane .width and
185- x + get_char_width (char ) + 1 < pane .start_x + pane .width ):
211+ if (y < pane .start_y + pane .height
212+ and x < pane .start_x + pane .width and
213+ x + get_char_width (char ) + 1 < pane .start_x + pane .width ):
186214 try :
187215 stdscr .addstr (y , x , hint [0 ], curses .color_pair (RED ))
188216 char_width = get_char_width (char )
189- stdscr .addstr (y , x + char_width , hint [1 ], curses .color_pair (GREEN ))
217+ stdscr .addstr (y , x + char_width , hint [1 ],
218+ curses .color_pair (GREEN ))
190219 except curses .error :
191220 pass
192221 stdscr .refresh ()
@@ -199,10 +228,13 @@ def main(stdscr):
199228
200229 # Redraw and show second character hints
201230 for pane in panes :
202- fixed_width_content = fill_pane_content_with_space (pane .content , pane .width )
203- for y , line in enumerate (fixed_width_content .splitlines ()[:pane .height ]):
231+ fixed_width_content = fill_pane_content_with_space (
232+ pane .content , pane .width )
233+ for y , line in enumerate (
234+ fixed_width_content .splitlines ()[:pane .height ]):
204235 try :
205- stdscr .addstr (pane .start_y + y , pane .start_x , line [:pane .width ])
236+ stdscr .addstr (pane .start_y + y , pane .start_x ,
237+ line [:pane .width ])
206238 except curses .error :
207239 pass
208240 for line_num , col , char , hint in pane .positions :
@@ -211,11 +243,11 @@ def main(stdscr):
211243 y = pane .start_y + line_num
212244 x = pane .start_x + col
213245 char_width = get_char_width (char )
214- if (y < pane .start_y + pane .height and
215- x < pane .start_x + pane .width and
216- x + char_width + 1 < pane .start_x + pane .width ):
246+ if (y < pane .start_y + pane .height and x < pane .start_x + pane .width
247+ and x + char_width + 1 < pane .start_x + pane .width ):
217248 try :
218- stdscr .addstr (y , x + char_width , hint [1 ], curses .color_pair (GREEN ))
249+ stdscr .addstr (y , x + char_width , hint [1 ],
250+ curses .color_pair (GREEN ))
219251 except curses .error :
220252 pass
221253 stdscr .refresh ()
@@ -237,5 +269,6 @@ def main(stdscr):
237269
238270 cleanup_window ()
239271
272+
240273if __name__ == '__main__' :
241274 curses .wrapper (main )
0 commit comments