agentcrew-ai 0.8.13__py3-none-any.whl → 0.9.1__py3-none-any.whl

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 (55) hide show
  1. AgentCrew/__init__.py +1 -1
  2. AgentCrew/app.py +46 -634
  3. AgentCrew/main_docker.py +1 -30
  4. AgentCrew/modules/a2a/common/client/card_resolver.py +27 -8
  5. AgentCrew/modules/a2a/server.py +5 -0
  6. AgentCrew/modules/a2a/task_manager.py +1 -0
  7. AgentCrew/modules/agents/local_agent.py +2 -2
  8. AgentCrew/modules/chat/message/command_processor.py +33 -8
  9. AgentCrew/modules/chat/message/conversation.py +18 -1
  10. AgentCrew/modules/chat/message/handler.py +5 -1
  11. AgentCrew/modules/code_analysis/service.py +50 -7
  12. AgentCrew/modules/code_analysis/tool.py +9 -8
  13. AgentCrew/modules/console/completers.py +5 -1
  14. AgentCrew/modules/console/console_ui.py +23 -11
  15. AgentCrew/modules/console/conversation_browser/__init__.py +9 -0
  16. AgentCrew/modules/console/conversation_browser/browser.py +84 -0
  17. AgentCrew/modules/console/conversation_browser/browser_input_handler.py +279 -0
  18. AgentCrew/modules/console/{conversation_browser.py → conversation_browser/browser_ui.py} +249 -163
  19. AgentCrew/modules/console/conversation_handler.py +34 -1
  20. AgentCrew/modules/console/display_handlers.py +127 -7
  21. AgentCrew/modules/console/visual_mode/__init__.py +5 -0
  22. AgentCrew/modules/console/visual_mode/viewer.py +41 -0
  23. AgentCrew/modules/console/visual_mode/viewer_input_handler.py +315 -0
  24. AgentCrew/modules/console/visual_mode/viewer_ui.py +608 -0
  25. AgentCrew/modules/gui/components/command_handler.py +137 -29
  26. AgentCrew/modules/gui/components/menu_components.py +8 -7
  27. AgentCrew/modules/gui/themes/README.md +30 -14
  28. AgentCrew/modules/gui/themes/__init__.py +2 -1
  29. AgentCrew/modules/gui/themes/atom_light.yaml +1287 -0
  30. AgentCrew/modules/gui/themes/catppuccin.yaml +1276 -0
  31. AgentCrew/modules/gui/themes/dracula.yaml +1262 -0
  32. AgentCrew/modules/gui/themes/nord.yaml +1267 -0
  33. AgentCrew/modules/gui/themes/saigontech.yaml +1268 -0
  34. AgentCrew/modules/gui/themes/style_provider.py +78 -264
  35. AgentCrew/modules/gui/themes/theme_loader.py +379 -0
  36. AgentCrew/modules/gui/themes/unicorn.yaml +1276 -0
  37. AgentCrew/modules/gui/widgets/configs/global_settings.py +4 -4
  38. AgentCrew/modules/gui/widgets/history_sidebar.py +6 -1
  39. AgentCrew/modules/llm/constants.py +28 -9
  40. AgentCrew/modules/mcpclient/service.py +0 -1
  41. AgentCrew/modules/memory/base_service.py +13 -0
  42. AgentCrew/modules/memory/chroma_service.py +50 -0
  43. AgentCrew/setup.py +470 -0
  44. {agentcrew_ai-0.8.13.dist-info → agentcrew_ai-0.9.1.dist-info}/METADATA +1 -1
  45. {agentcrew_ai-0.8.13.dist-info → agentcrew_ai-0.9.1.dist-info}/RECORD +49 -40
  46. {agentcrew_ai-0.8.13.dist-info → agentcrew_ai-0.9.1.dist-info}/WHEEL +1 -1
  47. AgentCrew/modules/gui/themes/atom_light.py +0 -1365
  48. AgentCrew/modules/gui/themes/catppuccin.py +0 -1404
  49. AgentCrew/modules/gui/themes/dracula.py +0 -1372
  50. AgentCrew/modules/gui/themes/nord.py +0 -1365
  51. AgentCrew/modules/gui/themes/saigontech.py +0 -1359
  52. AgentCrew/modules/gui/themes/unicorn.py +0 -1372
  53. {agentcrew_ai-0.8.13.dist-info → agentcrew_ai-0.9.1.dist-info}/entry_points.txt +0 -0
  54. {agentcrew_ai-0.8.13.dist-info → agentcrew_ai-0.9.1.dist-info}/licenses/LICENSE +0 -0
  55. {agentcrew_ai-0.8.13.dist-info → agentcrew_ai-0.9.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,608 @@
1
+ """Visual mode UI for displaying raw message content."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from typing import TYPE_CHECKING, List, Dict, Any, Optional, Tuple
7
+ from rich.console import Console
8
+ from rich.panel import Panel
9
+ from rich.text import Text
10
+ from rich.live import Live
11
+ from rich.box import HORIZONTALS, SIMPLE
12
+ from rich.table import Table
13
+ from rich.layout import Layout
14
+
15
+ from ..constants import (
16
+ RICH_STYLE_BLUE,
17
+ RICH_STYLE_GREEN,
18
+ RICH_STYLE_GRAY,
19
+ RICH_STYLE_YELLOW_BOLD,
20
+ RICH_STYLE_GREEN_BOLD,
21
+ RICH_STYLE_BLUE_BOLD,
22
+ )
23
+
24
+ if TYPE_CHECKING:
25
+ pass
26
+
27
+
28
+ class VisualModeUI:
29
+ """UI component for visual mode viewer."""
30
+
31
+ def __init__(self, console: Console):
32
+ self.console = console
33
+ self._messages: List[Dict[str, Any]] = []
34
+ self._lines: List[Tuple[str, str, int]] = []
35
+ self._cursor_line = 0
36
+ self._cursor_col = 0
37
+ self._scroll_offset = 0
38
+ self._horizontal_scroll = 0
39
+ self._selection_start: Optional[Tuple[int, int]] = None
40
+ self._selection_end: Optional[Tuple[int, int]] = None
41
+ self._visual_mode = False
42
+ self._live: Optional[Live] = None
43
+ self._layout: Optional[Layout] = None
44
+ self._search_mode = False
45
+ self._search_query = ""
46
+ self._search_matches: List[Tuple[int, int]] = []
47
+ self._current_match_idx = -1
48
+
49
+ @property
50
+ def total_lines(self) -> int:
51
+ return len(self._lines)
52
+
53
+ @property
54
+ def viewport_height(self) -> int:
55
+ return max(5, self.console.size.height - 10)
56
+
57
+ @property
58
+ def viewport_width(self) -> int:
59
+ return max(20, self.console.size.width - 10)
60
+
61
+ @property
62
+ def current_line_length(self) -> int:
63
+ if 0 <= self._cursor_line < len(self._lines):
64
+ return len(self._lines[self._cursor_line][0])
65
+ return 0
66
+
67
+ def set_messages(self, messages: List[Dict[str, Any]]):
68
+ self._messages = messages
69
+ self._build_lines()
70
+ self._cursor_line = max(0, self.total_lines - 1)
71
+ self._cursor_col = 0
72
+ self._scroll_offset = max(0, self.total_lines - self.viewport_height)
73
+ self._horizontal_scroll = 0
74
+ self._selection_start = None
75
+ self._selection_end = None
76
+ self._visual_mode = False
77
+ self._search_mode = False
78
+ self._search_query = ""
79
+ self._search_matches = []
80
+ self._current_match_idx = -1
81
+
82
+ def _extract_content(self, message: Dict[str, Any]) -> str:
83
+ content = message.get("content", "")
84
+ if isinstance(content, str):
85
+ return content
86
+ elif isinstance(content, list):
87
+ result = []
88
+ for item in content:
89
+ if isinstance(item, dict):
90
+ if item.get("type") == "text":
91
+ result.append(item.get("text", ""))
92
+ elif isinstance(item, str):
93
+ result.append(item)
94
+ return "\n".join(result)
95
+ return str(content)
96
+
97
+ def _build_lines(self):
98
+ self._lines = []
99
+ for msg_idx, msg in enumerate(self._messages):
100
+ role = msg.get("role", "unknown")
101
+ if role == "tool":
102
+ continue
103
+ agent = msg.get("agent", "")
104
+ content = self._extract_content(msg)
105
+ if not content.strip():
106
+ continue
107
+
108
+ header = f"--- {role.upper()}"
109
+ if agent:
110
+ header += f" ({agent})"
111
+ header += " ---"
112
+ self._lines.append((header, role, msg_idx))
113
+
114
+ for line in content.split("\n"):
115
+ self._lines.append((line, role, msg_idx))
116
+
117
+ self._lines.append(("", role, msg_idx))
118
+
119
+ def start_search_mode(self):
120
+ self._search_mode = True
121
+ self._search_query = ""
122
+ self._search_matches = []
123
+ self._current_match_idx = -1
124
+
125
+ def exit_search_mode(self, clear_results: bool = False):
126
+ self._search_mode = False
127
+ if clear_results:
128
+ self._search_query = ""
129
+ self._search_matches = []
130
+ self._current_match_idx = -1
131
+
132
+ def append_search_char(self, char: str):
133
+ self._search_query += char
134
+ self._perform_search()
135
+
136
+ def backspace_search(self):
137
+ if self._search_query:
138
+ self._search_query = self._search_query[:-1]
139
+ self._perform_search()
140
+
141
+ def _perform_search(self):
142
+ self._search_matches = []
143
+ self._current_match_idx = -1
144
+ if not self._search_query:
145
+ return
146
+
147
+ pattern = re.escape(self._search_query.lower())
148
+ for line_idx, (line_text, _, _) in enumerate(self._lines):
149
+ for match in re.finditer(pattern, line_text.lower()):
150
+ self._search_matches.append((line_idx, match.start()))
151
+
152
+ if self._search_matches:
153
+ self._current_match_idx = 0
154
+ self._jump_to_match(0)
155
+
156
+ def next_search_match(self):
157
+ if not self._search_matches:
158
+ return
159
+ self._current_match_idx = (self._current_match_idx + 1) % len(
160
+ self._search_matches
161
+ )
162
+ self._jump_to_match(self._current_match_idx)
163
+
164
+ def prev_search_match(self):
165
+ if not self._search_matches:
166
+ return
167
+ self._current_match_idx = (self._current_match_idx - 1) % len(
168
+ self._search_matches
169
+ )
170
+ self._jump_to_match(self._current_match_idx)
171
+
172
+ def _jump_to_match(self, match_idx: int):
173
+ if 0 <= match_idx < len(self._search_matches):
174
+ line_idx, col_idx = self._search_matches[match_idx]
175
+ self._cursor_line = line_idx
176
+ self._cursor_col = col_idx
177
+ self._adjust_scroll()
178
+ self._adjust_horizontal_scroll()
179
+
180
+ def toggle_visual_mode(self):
181
+ if self._visual_mode:
182
+ self._visual_mode = False
183
+ self._selection_start = None
184
+ self._selection_end = None
185
+ else:
186
+ self._visual_mode = True
187
+ self._selection_start = (self._cursor_line, self._cursor_col)
188
+ self._selection_end = (self._cursor_line, self._cursor_col)
189
+
190
+ def update_selection(self):
191
+ if self._visual_mode and self._selection_start is not None:
192
+ self._selection_end = (self._cursor_line, self._cursor_col)
193
+
194
+ def get_selected_text(self) -> str:
195
+ if self._selection_start is None or self._selection_end is None:
196
+ if 0 <= self._cursor_line < len(self._lines):
197
+ return self._lines[self._cursor_line][0]
198
+ return ""
199
+
200
+ start_line, start_col = self._selection_start
201
+ end_line, end_col = self._selection_end
202
+
203
+ if (start_line, start_col) > (end_line, end_col):
204
+ start_line, start_col, end_line, end_col = (
205
+ end_line,
206
+ end_col,
207
+ start_line,
208
+ start_col,
209
+ )
210
+
211
+ if start_line == end_line:
212
+ line_text = self._lines[start_line][0]
213
+ return line_text[start_col : end_col + 1]
214
+
215
+ result = []
216
+ for i in range(start_line, end_line + 1):
217
+ line_text = self._lines[i][0]
218
+ if i == start_line:
219
+ result.append(line_text[start_col:])
220
+ elif i == end_line:
221
+ result.append(line_text[: end_col + 1])
222
+ else:
223
+ result.append(line_text)
224
+ return "\n".join(result)
225
+
226
+ def move_cursor(self, direction: str) -> bool:
227
+ old_line = self._cursor_line
228
+ old_col = self._cursor_col
229
+
230
+ if direction == "up":
231
+ self._cursor_line = max(0, self._cursor_line - 1)
232
+ self._cursor_col = min(self._cursor_col, self.current_line_length)
233
+ elif direction == "down":
234
+ self._cursor_line = min(self.total_lines - 1, self._cursor_line + 1)
235
+ self._cursor_col = min(self._cursor_col, self.current_line_length)
236
+ elif direction == "left":
237
+ if self._cursor_col > 0:
238
+ self._cursor_col -= 1
239
+ elif self._cursor_line > 0:
240
+ self._cursor_line -= 1
241
+ self._cursor_col = self.current_line_length
242
+ elif direction == "right":
243
+ if self._cursor_col < self.current_line_length:
244
+ self._cursor_col += 1
245
+ elif self._cursor_line < self.total_lines - 1:
246
+ self._cursor_line += 1
247
+ self._cursor_col = 0
248
+ elif direction == "line_start":
249
+ self._cursor_col = 0
250
+ elif direction == "line_end":
251
+ self._cursor_col = max(0, self.current_line_length - 1)
252
+ elif direction == "word_forward":
253
+ self._move_word_forward()
254
+ elif direction == "word_backward":
255
+ self._move_word_backward()
256
+ elif direction == "top":
257
+ self._cursor_line = 0
258
+ self._cursor_col = 0
259
+ elif direction == "bottom":
260
+ self._cursor_line = max(0, self.total_lines - 1)
261
+ self._cursor_col = 0
262
+ elif direction == "page_up":
263
+ self._cursor_line = max(0, self._cursor_line - self.viewport_height)
264
+ self._cursor_col = min(self._cursor_col, self.current_line_length)
265
+ elif direction == "page_down":
266
+ self._cursor_line = min(
267
+ self.total_lines - 1, self._cursor_line + self.viewport_height
268
+ )
269
+ self._cursor_col = min(self._cursor_col, self.current_line_length)
270
+ elif direction == "half_up":
271
+ self._cursor_line = max(0, self._cursor_line - self.viewport_height // 2)
272
+ self._cursor_col = min(self._cursor_col, self.current_line_length)
273
+ elif direction == "half_down":
274
+ self._cursor_line = min(
275
+ self.total_lines - 1, self._cursor_line + self.viewport_height // 2
276
+ )
277
+ self._cursor_col = min(self._cursor_col, self.current_line_length)
278
+
279
+ self._adjust_scroll()
280
+ self._adjust_horizontal_scroll()
281
+ self.update_selection()
282
+ return old_line != self._cursor_line or old_col != self._cursor_col
283
+
284
+ def _move_word_forward(self):
285
+ if self._cursor_line >= len(self._lines):
286
+ return
287
+ line = self._lines[self._cursor_line][0]
288
+ col = self._cursor_col
289
+
290
+ while col < len(line) and line[col].isalnum():
291
+ col += 1
292
+ while col < len(line) and not line[col].isalnum():
293
+ col += 1
294
+
295
+ if col >= len(line) and self._cursor_line < self.total_lines - 1:
296
+ self._cursor_line += 1
297
+ self._cursor_col = 0
298
+ else:
299
+ self._cursor_col = min(col, max(0, len(line) - 1))
300
+
301
+ def _move_word_backward(self):
302
+ if self._cursor_line >= len(self._lines):
303
+ return
304
+ line = self._lines[self._cursor_line][0]
305
+ col = self._cursor_col
306
+
307
+ if col == 0 and self._cursor_line > 0:
308
+ self._cursor_line -= 1
309
+ line = self._lines[self._cursor_line][0]
310
+ col = len(line)
311
+
312
+ while col > 0 and not line[col - 1].isalnum():
313
+ col -= 1
314
+ while col > 0 and line[col - 1].isalnum():
315
+ col -= 1
316
+
317
+ self._cursor_col = col
318
+
319
+ def _adjust_scroll(self):
320
+ if self._cursor_line < self._scroll_offset:
321
+ self._scroll_offset = self._cursor_line
322
+ elif self._cursor_line >= self._scroll_offset + self.viewport_height:
323
+ self._scroll_offset = self._cursor_line - self.viewport_height + 1
324
+
325
+ def _adjust_horizontal_scroll(self):
326
+ visible_width = self.viewport_width - 6
327
+ if self._cursor_col < self._horizontal_scroll:
328
+ self._horizontal_scroll = self._cursor_col
329
+ elif self._cursor_col >= self._horizontal_scroll + visible_width:
330
+ self._horizontal_scroll = self._cursor_col - visible_width + 1
331
+
332
+ def _create_header(self) -> Panel:
333
+ header_table = Table.grid(expand=True)
334
+ header_table.add_column(justify="left", ratio=1)
335
+ header_table.add_column(justify="center", ratio=2)
336
+ header_table.add_column(justify="right", ratio=1)
337
+
338
+ mode_text = Text()
339
+ if self._search_mode:
340
+ mode_text.append("-- SEARCH --", style="bold cyan")
341
+ elif self._visual_mode:
342
+ mode_text.append("-- VISUAL --", style="bold yellow")
343
+ else:
344
+ mode_text.append("-- NORMAL --", style="bold green")
345
+
346
+ position = (
347
+ f"L{self._cursor_line + 1}:C{self._cursor_col + 1} [{self.total_lines}]"
348
+ )
349
+ title = "Visual Mode - Raw Content Viewer"
350
+
351
+ header_table.add_row(
352
+ Text(title, style=RICH_STYLE_YELLOW_BOLD),
353
+ mode_text,
354
+ Text(position, style=RICH_STYLE_GRAY),
355
+ )
356
+
357
+ return Panel(header_table, box=SIMPLE, style=RICH_STYLE_BLUE)
358
+
359
+ def _is_position_selected(self, line: int, col: int) -> bool:
360
+ if self._selection_start is None or self._selection_end is None:
361
+ return False
362
+
363
+ start_line, start_col = self._selection_start
364
+ end_line, end_col = self._selection_end
365
+
366
+ if (start_line, start_col) > (end_line, end_col):
367
+ start_line, start_col, end_line, end_col = (
368
+ end_line,
369
+ end_col,
370
+ start_line,
371
+ start_col,
372
+ )
373
+
374
+ if line < start_line or line > end_line:
375
+ return False
376
+ if line == start_line and line == end_line:
377
+ return start_col <= col <= end_col
378
+ if line == start_line:
379
+ return col >= start_col
380
+ if line == end_line:
381
+ return col <= end_col
382
+ return True
383
+
384
+ def _is_search_match(self, line: int, col: int) -> bool:
385
+ if not self._search_query or not self._search_matches:
386
+ return False
387
+ query_len = len(self._search_query)
388
+ for match_line, match_col in self._search_matches:
389
+ if line == match_line and match_col <= col < match_col + query_len:
390
+ return True
391
+ return False
392
+
393
+ def _create_content_panel(self) -> Panel:
394
+ content = Text()
395
+ end_line = min(self._scroll_offset + self.viewport_height, self.total_lines)
396
+ visible_width = self.viewport_width - 6
397
+
398
+ search_set = set()
399
+ if self._search_query and self._search_matches:
400
+ query_len = len(self._search_query)
401
+ for match_line, match_col in self._search_matches:
402
+ for c in range(match_col, match_col + query_len):
403
+ search_set.add((match_line, c))
404
+
405
+ sel_start = None
406
+ sel_end = None
407
+ if self._selection_start is not None and self._selection_end is not None:
408
+ s_line, s_col = self._selection_start
409
+ e_line, e_col = self._selection_end
410
+ if (s_line, s_col) > (e_line, e_col):
411
+ s_line, s_col, e_line, e_col = e_line, e_col, s_line, s_col
412
+ sel_start = (s_line, s_col)
413
+ sel_end = (e_line, e_col)
414
+
415
+ for i in range(self._scroll_offset, end_line):
416
+ line_text, role, _ = self._lines[i]
417
+ is_header = line_text.startswith("---") and line_text.endswith("---")
418
+
419
+ if is_header:
420
+ if role == "user":
421
+ base_style = RICH_STYLE_BLUE_BOLD
422
+ elif role == "assistant":
423
+ base_style = RICH_STYLE_GREEN_BOLD
424
+ else:
425
+ base_style = RICH_STYLE_YELLOW_BOLD
426
+ else:
427
+ base_style = "white"
428
+
429
+ line_num = f"{i + 1:4d} "
430
+ content.append(line_num, style=RICH_STYLE_GRAY)
431
+
432
+ visible_text = line_text[
433
+ self._horizontal_scroll : self._horizontal_scroll + visible_width
434
+ ]
435
+
436
+ if not visible_text and i == self._cursor_line and self._cursor_col == 0:
437
+ content.append(" ", style="reverse")
438
+ else:
439
+ segment_start = 0
440
+ current_style = None
441
+
442
+ for col_idx, char in enumerate(visible_text):
443
+ actual_col = self._horizontal_scroll + col_idx
444
+ is_cursor = (
445
+ i == self._cursor_line and actual_col == self._cursor_col
446
+ )
447
+ is_selected = False
448
+ if sel_start and sel_end:
449
+ if sel_start[0] == sel_end[0] == i:
450
+ is_selected = sel_start[1] <= actual_col <= sel_end[1]
451
+ elif sel_start[0] == i:
452
+ is_selected = actual_col >= sel_start[1]
453
+ elif sel_end[0] == i:
454
+ is_selected = actual_col <= sel_end[1]
455
+ elif sel_start[0] < i < sel_end[0]:
456
+ is_selected = True
457
+ is_match = (i, actual_col) in search_set
458
+
459
+ if is_cursor:
460
+ char_style = "reverse"
461
+ elif is_selected:
462
+ char_style = "on blue"
463
+ elif is_match:
464
+ char_style = "on yellow black"
465
+ else:
466
+ char_style = base_style
467
+
468
+ if char_style != current_style:
469
+ if current_style is not None and segment_start < col_idx:
470
+ content.append(
471
+ visible_text[segment_start:col_idx], style=current_style
472
+ )
473
+ segment_start = col_idx
474
+ current_style = char_style
475
+
476
+ if current_style is not None and segment_start < len(visible_text):
477
+ content.append(visible_text[segment_start:], style=current_style)
478
+
479
+ if i == self._cursor_line and self._cursor_col >= len(line_text):
480
+ if self._cursor_col == self._horizontal_scroll + len(visible_text):
481
+ content.append(" ", style="reverse")
482
+
483
+ content.append("\n")
484
+
485
+ scroll_info = ""
486
+ if self._scroll_offset > 0:
487
+ scroll_info += f"↑ {self._scroll_offset} more "
488
+ remaining = self.total_lines - end_line
489
+ if remaining > 0:
490
+ scroll_info += f"↓ {remaining} more"
491
+ if self._horizontal_scroll > 0:
492
+ scroll_info += f" ← {self._horizontal_scroll}"
493
+
494
+ return Panel(
495
+ content,
496
+ box=HORIZONTALS,
497
+ subtitle=Text(scroll_info, style=RICH_STYLE_GRAY) if scroll_info else None,
498
+ border_style=RICH_STYLE_GREEN,
499
+ height=self.viewport_height + 2,
500
+ )
501
+
502
+ def _create_search_bar(self) -> Panel:
503
+ search_text = Text()
504
+ search_text.append("/", style="bold cyan")
505
+ search_text.append(self._search_query, style="white")
506
+ search_text.append("█", style="blink")
507
+
508
+ if self._search_matches:
509
+ match_info = f" [{self._current_match_idx + 1}/{len(self._search_matches)}]"
510
+ search_text.append(match_info, style=RICH_STYLE_GRAY)
511
+ elif self._search_query:
512
+ search_text.append(" [no matches]", style="red")
513
+
514
+ return Panel(search_text, box=SIMPLE, border_style="cyan")
515
+
516
+ def _create_help_panel(self) -> Panel:
517
+ help_table = Table.grid(expand=True)
518
+ help_table.add_column(justify="left", ratio=1)
519
+ help_table.add_column(justify="left", ratio=1)
520
+ help_table.add_column(justify="left", ratio=1)
521
+
522
+ if self._search_mode:
523
+ nav_text = Text()
524
+ nav_text.append("Enter", style="bold")
525
+ nav_text.append(": confirm ")
526
+ nav_text.append("Esc", style="bold")
527
+ nav_text.append(": cancel")
528
+
529
+ action_text = Text()
530
+ action_text.append("n/N", style="bold")
531
+ action_text.append(": next/prev match")
532
+
533
+ exit_text = Text()
534
+ exit_text.append("Backspace", style="bold")
535
+ exit_text.append(": delete char")
536
+ else:
537
+ nav_text = Text()
538
+ nav_text.append("h/j/k/l", style="bold")
539
+ nav_text.append(": move ")
540
+ nav_text.append("w/b", style="bold")
541
+ nav_text.append(": word ")
542
+ nav_text.append("0/$", style="bold")
543
+ nav_text.append(": line start/end")
544
+
545
+ action_text = Text()
546
+ action_text.append("v", style="bold")
547
+ action_text.append(": visual ")
548
+ action_text.append("y", style="bold")
549
+ action_text.append(": yank ")
550
+ action_text.append("/", style="bold")
551
+ action_text.append(": search")
552
+
553
+ exit_text = Text()
554
+ exit_text.append("gg/G", style="bold")
555
+ exit_text.append(": top/bottom ")
556
+ exit_text.append("q/Esc", style="bold")
557
+ exit_text.append(": quit")
558
+
559
+ help_table.add_row(nav_text, action_text, exit_text)
560
+
561
+ return Panel(help_table, box=SIMPLE, border_style=RICH_STYLE_GRAY)
562
+
563
+ def _create_layout(self) -> Layout:
564
+ layout = Layout()
565
+ layout.split_column(
566
+ Layout(name="header", size=3),
567
+ Layout(name="content"),
568
+ Layout(name="search", size=3),
569
+ Layout(name="help", size=3),
570
+ )
571
+ return layout
572
+
573
+ def _update_layout(self):
574
+ if self._layout:
575
+ self._layout["header"].update(self._create_header())
576
+ self._layout["content"].update(self._create_content_panel())
577
+ if self._search_mode:
578
+ self._layout["search"].update(self._create_search_bar())
579
+ self._layout["search"].visible = True
580
+ else:
581
+ self._layout["search"].update("")
582
+ self._layout["search"].visible = False
583
+ self._layout["help"].update(self._create_help_panel())
584
+
585
+ def render(self):
586
+ if self._live and self._layout:
587
+ self._update_layout()
588
+ self._live.refresh()
589
+
590
+ def start_live(self):
591
+ self.console.clear()
592
+ self._layout = self._create_layout()
593
+ self._update_layout()
594
+
595
+ self._live = Live(
596
+ self._layout,
597
+ console=self.console,
598
+ auto_refresh=False,
599
+ screen=True,
600
+ )
601
+ self._live.start()
602
+ self._live.refresh()
603
+
604
+ def stop_live(self):
605
+ if self._live:
606
+ self._live.stop()
607
+ self._live = None
608
+ self._layout = None