11#!/usr/bin/env python3
2- import curses
32import functools
43import os
54import re
5+ import sys
6+ import termios
7+ import tty
68import unicodedata
79from itertools import islice
810from typing import List , Optional
911
12+ # ANSI escape sequences
13+ ESC = '\033 '
14+ CLEAR = f'{ ESC } [2J'
15+ CLEAR_LINE = f'{ ESC } [2K'
16+ HIDE_CURSOR = f'{ ESC } [?25l'
17+ SHOW_CURSOR = f'{ ESC } [?25h'
18+ RESET = f'{ ESC } [0m'
19+ RED_FG = f'{ ESC } [31m'
20+ GREEN_FG = f'{ ESC } [32m'
21+ DIM = f'{ ESC } [2m'
22+
1023# Configuration from environment
1124KEYS = os .environ .get ('TMUX_EASYMOTION_KEYS' , 'asdfghjkl;' )
1225HINT_COLOR_1 = int (os .environ .get ('TMUX_EASYMOTION_COLOR1' , '1' )) # RED
@@ -129,14 +142,41 @@ def cleanup_window():
129142 if current_window != previous_window :
130143 pyshell ('tmux kill-window' )
131144
145+ def get_terminal_size ():
146+ """Get terminal size from tmux"""
147+ output = pyshell ('tmux display-message -p "#{client_width},#{client_height}"' )
148+ width , height = map (int , output .strip ().split (',' ))
149+ return width , height - 1 # Subtract 1 from height
150+
151+ def init_terminal ():
152+ """Initialize terminal settings"""
153+ sys .stdout .write (HIDE_CURSOR )
154+ sys .stdout .flush ()
155+
156+ def restore_terminal ():
157+ """Restore terminal settings"""
158+ sys .stdout .write (SHOW_CURSOR )
159+ sys .stdout .write (RESET )
160+ sys .stdout .flush ()
161+
162+ def getch ():
163+ """Get a single character from terminal"""
164+ fd = sys .stdin .fileno ()
165+ old_settings = termios .tcgetattr (fd )
166+ try :
167+ tty .setraw (fd )
168+ ch = sys .stdin .read (1 )
169+ finally :
170+ termios .tcsetattr (fd , termios .TCSADRAIN , old_settings )
171+ return ch
172+
132173
133174def tmux_capture_pane (pane ):
134175 if pane .scroll_position > 0 :
135176 # When scrolled up, use negative numbers to capture from history
136177 # -scroll_pos is where we are in history
137- # -(scroll_pos - curses.LINES + 1) captures one screen worth from there
138- end_pos = - (pane .scroll_position - curses .LINES + 1
139- ) # Calculate separately to avoid string formatting issues
178+ # -(scroll_pos - pane.height + 1) captures one screen worth from there
179+ end_pos = - (pane .scroll_position - pane .height + 1 )
140180 cmd = f'tmux capture-pane -p -S -{ pane .scroll_position } -E { end_pos } -t { pane .pane_id } '
141181 else :
142182 # If not scrolled, just capture current view (default behavior)
@@ -174,14 +214,7 @@ def generate_hints(keys: str, needed_count: Optional[int] = None) -> List[str]:
174214GREEN = 2
175215
176216
177- def init_curses ():
178- """Initialize curses settings and colors"""
179- curses .curs_set (False )
180- curses .start_color ()
181- curses .use_default_colors ()
182- curses .init_pair (RED , curses .COLOR_RED , - 1 )
183- curses .init_pair (GREEN , curses .COLOR_GREEN , - 1 )
184-
217+ # Remove the init_curses function as it's no longer needed
185218
186219def init_panes ():
187220 """Initialize pane information with cached calculations"""
@@ -203,34 +236,25 @@ def init_panes():
203236 return panes , max_x , padding_cache
204237
205238
206- def draw_pane_content (stdscr , pane , padding_cache ):
239+ def draw_pane_content (pane , padding_cache ):
207240 """Draw the content of a single pane"""
208241 for y , line in enumerate (pane .lines [:pane .height ]):
209242 visual_width = get_string_width (line )
210243 if visual_width < pane .width :
211244 line = line + padding_cache [pane .width - visual_width ]
212- try :
213- stdscr .addstr (pane .start_y + y , pane .start_x , line [:pane .width ])
214- except curses .error :
215- pass
245+ sys .stdout .write (f'{ ESC } [{ pane .start_y + y } ;{ pane .start_x } H{ line [:pane .width ]} ' )
216246
217247
218- def draw_vertical_borders (stdscr , pane , max_x ):
248+ def draw_vertical_borders (pane , max_x ):
219249 """Draw vertical borders for a pane"""
220250 if pane .start_x + pane .width < max_x : # Only if not rightmost pane
221- try :
222- for y in range (pane .start_y , pane .start_y + pane .height ):
223- stdscr .addstr (y , pane .start_x + pane .width , VERTICAL_BORDER , curses .A_DIM )
224- except curses .error :
225- pass
251+ for y in range (pane .start_y , pane .start_y + pane .height ):
252+ sys .stdout .write (f'{ ESC } [{ y } ;{ pane .start_x + pane .width } H{ DIM } { VERTICAL_BORDER } { RESET } ' )
226253
227254
228- def draw_horizontal_border (stdscr , pane , y_pos ):
255+ def draw_horizontal_border (pane , y_pos ):
229256 """Draw horizontal border for a pane"""
230- try :
231- stdscr .addstr (y_pos , pane .start_x , HORIZONTAL_BORDER * pane .width , curses .A_DIM )
232- except curses .error :
233- pass
257+ sys .stdout .write (f'{ ESC } [{ y_pos } ;{ pane .start_x } H{ DIM } { HORIZONTAL_BORDER * pane .width } { RESET } ' )
234258
235259
236260def group_panes_by_end_y (panes ):
@@ -242,28 +266,32 @@ def group_panes_by_end_y(panes):
242266 return rows
243267
244268
245- def draw_all_panes (stdscr , panes , max_x , padding_cache ):
269+ def draw_all_panes (panes , max_x , padding_cache , terminal_height ):
246270 """Draw all panes and their borders"""
247- # Pre-calculate row groups
248- rows = group_panes_by_end_y (panes )
249- for pane in panes :
250- # Draw content and borders in single pass
251- draw_pane_content (stdscr , pane , padding_cache )
252- # Vertical borders
271+ sorted_panes = sorted (panes , key = lambda p : p .start_y + p .height )
272+
273+ for pane in sorted_panes :
274+ # 限制繪製的行數
275+ visible_height = min (pane .height , terminal_height - pane .start_y )
276+
277+ # 繪製內容
278+ for y , line in enumerate (pane .lines [:visible_height ]):
279+ visual_width = get_string_width (line )
280+ if visual_width < pane .width :
281+ line = line + padding_cache [pane .width - visual_width ]
282+ sys .stdout .write (f'{ ESC } [{ pane .start_y + y + 1 } ;{ pane .start_x + 1 } H{ line [:pane .width ]} ' )
283+
284+ # 繪製垂直邊框
253285 if pane .start_x + pane .width < max_x :
254- try :
255- for y in range (pane .start_y , pane .start_y + pane .height ):
256- stdscr .addstr (y , pane .start_x + pane .width , VERTICAL_BORDER , curses .A_DIM )
257- except curses .error :
258- pass
259- # Horizontal borders
260- end_y = pane .start_y + pane .height
261- if end_y in rows :
262- try :
263- stdscr .addstr (end_y , pane .start_x , HORIZONTAL_BORDER * pane .width , curses .A_DIM )
264- except curses .error :
265- pass
266- stdscr .refresh ()
286+ for y in range (pane .start_y , pane .start_y + visible_height ):
287+ sys .stdout .write (f'{ ESC } [{ y + 1 } ;{ pane .start_x + pane .width } H{ DIM } { VERTICAL_BORDER } { RESET } ' )
288+
289+ # 只為非最底部的 pane 繪製水平邊框
290+ end_y = pane .start_y + visible_height
291+ if end_y < terminal_height and pane != sorted_panes [- 1 ]:
292+ sys .stdout .write (f'{ ESC } [{ end_y + 1 } ;{ pane .start_x + 1 } H{ DIM } { HORIZONTAL_BORDER * pane .width } { RESET } ' )
293+
294+ sys .stdout .flush ()
267295
268296
269297def find_matches (panes , search_ch , hints ):
@@ -284,83 +312,84 @@ def find_matches(panes, search_ch, hints):
284312 return hint_positions
285313
286314
287- def draw_all_hints (stdscr , panes ):
315+ def draw_all_hints (panes , terminal_height ):
288316 """Draw all hints across all panes"""
289317 for pane in panes :
290318 for line_num , col , char , hint in pane .positions :
291319 y = pane .start_y + line_num
292320 x = pane .start_x + col
293- if (y < pane .start_y + pane .height and
321+ if (y < min ( pane .start_y + pane .height , terminal_height ) and
294322 x < pane .start_x + pane .width and
295323 x + get_char_width (char ) + 1 < pane .start_x + pane .width ):
296- try :
297- stdscr .addstr (y , x , hint [0 ], curses .color_pair (RED ))
298- char_width = get_char_width (char )
299- stdscr .addstr (y , x + char_width , hint [1 ], curses .color_pair (GREEN ))
300- except curses .error :
301- pass
302-
303-
304- def main (stdscr ):
305- init_curses ()
306- panes , max_x , padding_cache = init_panes ()
307- hints = generate_hints (KEYS )
308-
309- # Draw initial pane contents
310- draw_all_panes (stdscr , panes , max_x , padding_cache )
311-
312- # Get search character and find matches
313- search_ch = stdscr .getkey ()
314- hint_positions = find_matches (panes , search_ch , hints )
315-
316- # Draw hints for all matches
317- draw_all_panes (stdscr , panes , max_x , padding_cache )
318- draw_all_hints (stdscr , panes )
319- stdscr .refresh ()
320-
321- # Handle first character selection
322- ch1 = stdscr .getkey ()
323- if ch1 not in KEYS :
324- cleanup_window ()
325- exit (0 )
324+ sys .stdout .write (f'{ ESC } [{ y + 1 } ;{ x + 1 } H{ RED_FG } { hint [0 ]} { RESET } ' )
325+ char_width = get_char_width (char )
326+ sys .stdout .write (f'{ ESC } [{ y + 1 } ;{ x + char_width + 1 } H{ GREEN_FG } { hint [1 ]} { RESET } ' )
327+ sys .stdout .flush ()
326328
327- # Redraw panes and show filtered hints
328- draw_all_panes (stdscr , panes , max_x , padding_cache )
329- for pane in panes :
330- for line_num , col , char , hint in pane .positions :
331- if not hint .startswith (ch1 ):
332- continue
333- y = pane .start_y + line_num
334- x = pane .start_x + col
335- char_width = get_char_width (char )
336- if (y < pane .start_y + pane .height and
337- x < pane .start_x + pane .width and
338- x + char_width + 1 < pane .start_x + pane .width ):
339- try :
340- stdscr .addstr (y , x + char_width , hint [1 ], curses .color_pair (GREEN ))
341- except curses .error :
342- pass
343- stdscr .refresh ()
344-
345- # Handle second character selection
346- ch2 = stdscr .getkey ()
347- if ch2 not in KEYS :
348- cleanup_window ()
349- exit (0 )
350-
351- # Move cursor to selected position - now using lookup
352- target_hint = ch1 + ch2
353- if target_hint in hint_positions :
354- pane , line_num , col = hint_positions [target_hint ]
355- true_col = get_true_position (pane .lines [line_num ], col ) # Use lines directly
356- tmux_move_cursor (pane , line_num , true_col )
357-
358- cleanup_window ()
359329
330+ def main ():
331+ try :
332+ init_terminal ()
333+ terminal_width , terminal_height = get_terminal_size ()
334+ panes , max_x , padding_cache = init_panes ()
335+ hints = generate_hints (KEYS )
336+
337+ def clear_and_draw ():
338+ sys .stdout .write (CLEAR )
339+ draw_all_panes (panes , max_x , padding_cache , terminal_height )
340+ sys .stdout .write (f'{ ESC } [{ terminal_height } ;1H' )
341+ sys .stdout .flush ()
342+
343+ clear_and_draw ()
344+
345+ # Get search character and find matches
346+ search_ch = getch ()
347+ hint_positions = find_matches (panes , search_ch , hints )
348+
349+ # Draw hints for all matches
350+ clear_and_draw ()
351+ draw_all_hints (panes , terminal_height )
352+ sys .stdout .flush ()
353+
354+ # Handle first character selection
355+ ch1 = getch ()
356+ if ch1 not in KEYS :
357+ return
358+
359+ # Redraw panes and show filtered hints
360+ clear_and_draw ()
361+ for pane in panes :
362+ for line_num , col , char , hint in pane .positions :
363+ if not hint .startswith (ch1 ):
364+ continue
365+ y = pane .start_y + line_num
366+ x = pane .start_x + col
367+ char_width = get_char_width (char )
368+ if (y < min (pane .start_y + pane .height , terminal_height ) and
369+ x < pane .start_x + pane .width and
370+ x + char_width + 1 < pane .start_x + pane .width ):
371+ sys .stdout .write (f'{ ESC } [{ y + 1 } ;{ x + char_width + 1 } H{ GREEN_FG } { hint [1 ]} { RESET } ' )
372+ sys .stdout .flush ()
373+
374+ # Handle second character selection
375+ ch2 = getch ()
376+ if ch2 not in KEYS :
377+ return
378+
379+ # Move cursor to selected position
380+ target_hint = ch1 + ch2
381+ if target_hint in hint_positions :
382+ pane , line_num , col = hint_positions [target_hint ]
383+ true_col = get_true_position (pane .lines [line_num ], col )
384+ tmux_move_cursor (pane , line_num , true_col )
385+
386+ finally :
387+ restore_terminal ()
360388
361389if __name__ == '__main__' :
362390 try :
363- curses . wrapper ( main )
391+ main ( )
364392 except KeyboardInterrupt :
393+ restore_terminal ()
394+ finally :
365395 cleanup_window ()
366- exit (0 )
0 commit comments