|
2 | 2 | import curses |
3 | 3 | import re |
4 | 4 | import os |
5 | | -import itertools |
6 | 5 | import unicodedata |
7 | 6 |
|
8 | 7 | KEYS='asdfghjkl;' |
@@ -39,6 +38,25 @@ def pyshell(cmd): |
39 | 38 | return result |
40 | 39 | return os.popen(cmd).read() |
41 | 40 |
|
| 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 | + |
42 | 60 | def tmux_pane_id(): |
43 | 61 | # Get the ID of the pane that launched this script |
44 | 62 | source_pane = os.environ.get('TMUX_PANE') |
@@ -112,94 +130,102 @@ def generate_hints(keys): |
112 | 130 | GREEN = 2 |
113 | 131 |
|
114 | 132 | 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) |
118 | 138 |
|
119 | | - # invisible cursor |
120 | 139 | curses.curs_set(False) |
121 | | - |
122 | | - # get screen width |
123 | | - _, width = stdscr.getmaxyx() |
124 | | - |
125 | | - # init default colors |
126 | 140 | curses.start_color() |
127 | 141 | curses.use_default_colors() |
128 | 142 | curses.init_pair(RED, curses.COLOR_RED, -1) |
129 | 143 | curses.init_pair(GREEN, curses.COLOR_GREEN, -1) |
130 | 144 |
|
131 | | - # keys = 'abcd', hints = a, b, c, d, aa, ab, ac .... dd |
132 | 145 | hints = generate_hints(KEYS) |
133 | 146 | hints_dict = {hint: i for i, hint in enumerate(hints)} |
134 | 147 |
|
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 |
144 | 156 | stdscr.refresh() |
| 157 | + |
145 | 158 | search_ch = stdscr.getkey() |
146 | 159 |
|
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)) |
165 | 182 | stdscr.refresh() |
166 | 183 |
|
| 184 | + # Handle hint selection |
167 | 185 | ch1 = stdscr.getkey() |
168 | | - if ch1 not in KEYS: |
| 186 | + if ch1 not in KEYS: |
| 187 | + cleanup_window() |
169 | 188 | exit(0) |
170 | 189 |
|
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)) |
185 | 207 | stdscr.refresh() |
186 | 208 |
|
187 | 209 | ch2 = stdscr.getkey() |
188 | 210 | if ch2 not in KEYS: |
189 | 211 | cleanup_window() |
190 | 212 | 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 | + |
203 | 229 | cleanup_window() |
204 | 230 |
|
205 | 231 | if __name__ == '__main__': |
|
0 commit comments