janito 1.9.0__py3-none-any.whl → 1.11.0__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 (106) hide show
  1. janito/__init__.py +1 -1
  2. janito/agent/api_exceptions.py +4 -0
  3. janito/agent/config.py +1 -1
  4. janito/agent/config_defaults.py +2 -26
  5. janito/agent/conversation.py +163 -122
  6. janito/agent/conversation_api.py +246 -168
  7. janito/agent/conversation_ui.py +1 -1
  8. janito/agent/{conversation_history.py → llm_conversation_history.py} +30 -1
  9. janito/agent/openai_client.py +38 -23
  10. janito/agent/openai_schema_generator.py +162 -129
  11. janito/agent/platform_discovery.py +134 -77
  12. janito/agent/profile_manager.py +5 -5
  13. janito/agent/rich_message_handler.py +80 -31
  14. janito/agent/templates/profiles/system_prompt_template_base.txt.j2 +20 -4
  15. janito/agent/test_openai_schema_generator.py +93 -0
  16. janito/agent/tool_base.py +7 -2
  17. janito/agent/tool_executor.py +54 -49
  18. janito/agent/tool_registry.py +5 -2
  19. janito/agent/tool_use_tracker.py +26 -5
  20. janito/agent/tools/__init__.py +8 -3
  21. janito/agent/tools/create_directory.py +3 -1
  22. janito/agent/tools/create_file.py +7 -1
  23. janito/agent/tools/fetch_url.py +40 -3
  24. janito/agent/tools/find_files.py +29 -14
  25. janito/agent/tools/get_file_outline/core.py +7 -8
  26. janito/agent/tools/get_file_outline/python_outline.py +139 -95
  27. janito/agent/tools/get_file_outline/search_outline.py +3 -1
  28. janito/agent/tools/get_lines.py +98 -64
  29. janito/agent/tools/move_file.py +59 -31
  30. janito/agent/tools/open_url.py +31 -0
  31. janito/agent/tools/present_choices.py +3 -1
  32. janito/agent/tools/python_command_runner.py +149 -0
  33. janito/agent/tools/python_file_runner.py +147 -0
  34. janito/agent/tools/python_stdin_runner.py +153 -0
  35. janito/agent/tools/remove_directory.py +3 -1
  36. janito/agent/tools/remove_file.py +5 -1
  37. janito/agent/tools/replace_file.py +12 -2
  38. janito/agent/tools/replace_text_in_file.py +195 -149
  39. janito/agent/tools/run_bash_command.py +30 -69
  40. janito/agent/tools/run_powershell_command.py +138 -105
  41. janito/agent/tools/search_text/__init__.py +1 -0
  42. janito/agent/tools/search_text/core.py +176 -0
  43. janito/agent/tools/search_text/match_lines.py +58 -0
  44. janito/agent/tools/search_text/pattern_utils.py +65 -0
  45. janito/agent/tools/search_text/traverse_directory.py +127 -0
  46. janito/agent/tools/validate_file_syntax/core.py +43 -30
  47. janito/agent/tools/validate_file_syntax/html_validator.py +21 -5
  48. janito/agent/tools/validate_file_syntax/markdown_validator.py +77 -34
  49. janito/agent/tools_utils/action_type.py +7 -0
  50. janito/agent/tools_utils/dir_walk_utils.py +3 -2
  51. janito/agent/tools_utils/formatting.py +47 -21
  52. janito/agent/tools_utils/gitignore_utils.py +89 -40
  53. janito/agent/tools_utils/test_gitignore_utils.py +46 -0
  54. janito/agent/tools_utils/utils.py +7 -1
  55. janito/cli/_print_config.py +63 -61
  56. janito/cli/arg_parser.py +13 -12
  57. janito/cli/cli_main.py +137 -147
  58. janito/cli/config_commands.py +112 -109
  59. janito/cli/main.py +152 -174
  60. janito/cli/one_shot.py +40 -26
  61. janito/i18n/__init__.py +1 -1
  62. janito/rich_utils.py +46 -8
  63. janito/shell/commands/__init__.py +2 -4
  64. janito/shell/commands/conversation_restart.py +3 -1
  65. janito/shell/commands/edit.py +3 -0
  66. janito/shell/commands/history_view.py +3 -3
  67. janito/shell/commands/lang.py +3 -0
  68. janito/shell/commands/livelogs.py +5 -3
  69. janito/shell/commands/prompt.py +6 -0
  70. janito/shell/commands/session.py +3 -0
  71. janito/shell/commands/session_control.py +3 -0
  72. janito/shell/commands/termweb_log.py +8 -0
  73. janito/shell/commands/tools.py +3 -0
  74. janito/shell/commands/track.py +36 -0
  75. janito/shell/commands/utility.py +13 -18
  76. janito/shell/commands/verbose.py +3 -4
  77. janito/shell/input_history.py +62 -0
  78. janito/shell/main.py +160 -181
  79. janito/shell/session/config.py +83 -75
  80. janito/shell/session/manager.py +0 -21
  81. janito/shell/ui/interactive.py +97 -75
  82. janito/termweb/static/editor.css +32 -33
  83. janito/termweb/static/editor.css.bak +140 -22
  84. janito/termweb/static/editor.html +12 -7
  85. janito/termweb/static/editor.html.bak +16 -11
  86. janito/termweb/static/editor.js +94 -40
  87. janito/termweb/static/editor.js.bak +97 -65
  88. janito/termweb/static/index.html +1 -2
  89. janito/termweb/static/index.html.bak +1 -1
  90. janito/termweb/static/termweb.css +1 -22
  91. janito/termweb/static/termweb.css.bak +6 -4
  92. janito/termweb/static/termweb.js +0 -6
  93. janito/termweb/static/termweb.js.bak +1 -2
  94. janito/tests/test_rich_utils.py +44 -0
  95. janito/web/app.py +0 -75
  96. {janito-1.9.0.dist-info → janito-1.11.0.dist-info}/METADATA +61 -42
  97. janito-1.11.0.dist-info/RECORD +163 -0
  98. {janito-1.9.0.dist-info → janito-1.11.0.dist-info}/WHEEL +1 -1
  99. janito/agent/providers.py +0 -77
  100. janito/agent/tools/run_python_command.py +0 -161
  101. janito/agent/tools/search_text.py +0 -204
  102. janito/shell/commands/sum.py +0 -49
  103. janito-1.9.0.dist-info/RECORD +0 -151
  104. {janito-1.9.0.dist-info → janito-1.11.0.dist-info}/entry_points.txt +0 -0
  105. {janito-1.9.0.dist-info → janito-1.11.0.dist-info}/licenses/LICENSE +0 -0
  106. {janito-1.9.0.dist-info → janito-1.11.0.dist-info}/top_level.txt +0 -0
@@ -1,4 +1,5 @@
1
1
  from janito.agent.tool_base import ToolBase
2
+ from janito.agent.tools_utils.action_type import ActionType
2
3
  from janito.agent.tool_registry import register_tool
3
4
  from janito.agent.tools_utils.utils import pluralize, display_path
4
5
  from janito.agent.tools_utils.dir_walk_utils import walk_dir_with_gitignore
@@ -24,6 +25,26 @@ class FindFilesTool(ToolBase):
24
25
  If max_results is reached, appends a note to the output.
25
26
  """
26
27
 
28
+ def _match_directories(self, root, dirs, pat):
29
+ dir_output = set()
30
+ dir_pat = pat.rstrip("/\\")
31
+ for d in dirs:
32
+ if fnmatch.fnmatch(d, dir_pat):
33
+ dir_output.add(os.path.join(root, d) + os.sep)
34
+ return dir_output
35
+
36
+ def _match_files(self, root, files, pat):
37
+ file_output = set()
38
+ for filename in fnmatch.filter(files, pat):
39
+ file_output.add(os.path.join(root, filename))
40
+ return file_output
41
+
42
+ def _match_dirs_without_slash(self, root, dirs, pat):
43
+ dir_output = set()
44
+ for d in fnmatch.filter(dirs, pat):
45
+ dir_output.add(os.path.join(root, d))
46
+ return dir_output
47
+
27
48
  def run(self, paths: str, pattern: str, max_depth: int = None) -> str:
28
49
  if not pattern:
29
50
  self.report_warning(tr("ℹ️ Empty file pattern provided."))
@@ -38,31 +59,26 @@ class FindFilesTool(ToolBase):
38
59
  else ""
39
60
  )
40
61
  self.report_info(
62
+ ActionType.READ,
41
63
  tr(
42
- "🔍 Searching for files '{pattern}' in '{disp_path}'{depth_msg} ...",
64
+ "🔍 Search for files '{pattern}' in '{disp_path}'{depth_msg} ...",
43
65
  pattern=pattern,
44
66
  disp_path=disp_path,
45
67
  depth_msg=depth_msg,
46
- )
68
+ ),
47
69
  )
48
70
  dir_output = set()
49
71
  for root, dirs, files in walk_dir_with_gitignore(
50
72
  directory, max_depth=max_depth
51
73
  ):
52
74
  for pat in patterns:
53
- # Directory matching: pattern ends with '/' or '\'
54
75
  if pat.endswith("/") or pat.endswith("\\"):
55
- dir_pat = pat.rstrip("/\\")
56
- for d in dirs:
57
- if fnmatch.fnmatch(d, dir_pat):
58
- dir_output.add(os.path.join(root, d) + os.sep)
76
+ dir_output.update(self._match_directories(root, dirs, pat))
59
77
  else:
60
- # Match files
61
- for filename in fnmatch.filter(files, pat):
62
- dir_output.add(os.path.join(root, filename))
63
- # Also match directories (without trailing slash)
64
- for d in fnmatch.filter(dirs, pat):
65
- dir_output.add(os.path.join(root, d))
78
+ dir_output.update(self._match_files(root, files, pat))
79
+ dir_output.update(
80
+ self._match_dirs_without_slash(root, dirs, pat)
81
+ )
66
82
  self.report_success(
67
83
  tr(
68
84
  " ✅ {count} {file_word}",
@@ -70,7 +86,6 @@ class FindFilesTool(ToolBase):
70
86
  file_word=pluralize("file", len(dir_output)),
71
87
  )
72
88
  )
73
- # If searching in '.', strip leading './' from results
74
89
  if directory.strip() == ".":
75
90
  dir_output = {
76
91
  p[2:] if (p.startswith("./") or p.startswith(".\\")) else p
@@ -1,12 +1,10 @@
1
1
  from janito.agent.tool_registry import register_tool
2
2
  from .python_outline import parse_python_outline
3
3
  from .markdown_outline import parse_markdown_outline
4
- from janito.agent.tools_utils.formatting import (
5
- format_outline_table,
6
- format_markdown_outline_table,
7
- )
4
+ from janito.agent.tools_utils.formatting import OutlineFormatter
8
5
  import os
9
6
  from janito.agent.tool_base import ToolBase
7
+ from janito.agent.tools_utils.action_type import ActionType
10
8
  from janito.agent.tools_utils.utils import display_path, pluralize
11
9
  from janito.i18n import tr
12
10
 
@@ -23,10 +21,11 @@ class GetFileOutlineTool(ToolBase):
23
21
  def run(self, file_path: str) -> str:
24
22
  try:
25
23
  self.report_info(
24
+ ActionType.READ,
26
25
  tr(
27
- "📄 Outlining file '{disp_path}' ...",
26
+ "📄 Outline file '{disp_path}' ...",
28
27
  disp_path=display_path(file_path),
29
- )
28
+ ),
30
29
  )
31
30
  ext = os.path.splitext(file_path)[1].lower()
32
31
  with open(file_path, "r", encoding="utf-8", errors="replace") as f:
@@ -34,7 +33,7 @@ class GetFileOutlineTool(ToolBase):
34
33
  if ext == ".py":
35
34
  outline_items = parse_python_outline(lines)
36
35
  outline_type = "python"
37
- table = format_outline_table(outline_items)
36
+ table = OutlineFormatter.format_outline_table(outline_items)
38
37
  self.report_success(
39
38
  tr(
40
39
  "✅ Outlined {count} {item_word}",
@@ -53,7 +52,7 @@ class GetFileOutlineTool(ToolBase):
53
52
  elif ext == ".md":
54
53
  outline_items = parse_markdown_outline(lines)
55
54
  outline_type = "markdown"
56
- table = format_markdown_outline_table(outline_items)
55
+ table = OutlineFormatter.format_markdown_outline_table(outline_items)
57
56
  self.report_success(
58
57
  tr(
59
58
  "✅ Outlined {count} {item_word}",
@@ -2,114 +2,158 @@ import re
2
2
  from typing import List
3
3
 
4
4
 
5
- def parse_python_outline(lines: List[str]):
6
- class_pat = re.compile(r"^(\s*)class\s+(\w+)")
7
- func_pat = re.compile(r"^(\s*)def\s+(\w+)")
8
- assign_pat = re.compile(r"^(\s*)([A-Za-z_][A-Za-z0-9_]*)\s*=.*")
9
- main_pat = re.compile(r"^\s*if\s+__name__\s*==\s*[\'\"]__main__[\'\"]\s*:")
5
+ def handle_assignment(idx, assign_match, outline):
6
+ var_name = assign_match.group(2)
7
+ var_type = "const" if var_name.isupper() else "var"
8
+ outline.append(
9
+ {
10
+ "type": var_type,
11
+ "name": var_name,
12
+ "start": idx + 1,
13
+ "end": idx + 1,
14
+ "parent": "",
15
+ "docstring": "",
16
+ }
17
+ )
18
+
19
+
20
+ def handle_main(idx, outline):
21
+ outline.append(
22
+ {
23
+ "type": "main",
24
+ "name": "__main__",
25
+ "start": idx + 1,
26
+ "end": idx + 1,
27
+ "parent": "",
28
+ "docstring": "",
29
+ }
30
+ )
31
+
32
+
33
+ def close_stack_objects(idx, indent, stack, obj_ranges):
34
+ while stack and indent < stack[-1][2]:
35
+ popped = stack.pop()
36
+ obj_ranges.append((popped[0], popped[1], popped[3], idx, popped[4], popped[2]))
37
+
38
+
39
+ def close_last_top_obj(idx, last_top_obj, stack, obj_ranges):
40
+ if last_top_obj and last_top_obj in stack:
41
+ stack.remove(last_top_obj)
42
+ obj_ranges.append(
43
+ (
44
+ last_top_obj[0],
45
+ last_top_obj[1],
46
+ last_top_obj[3],
47
+ idx,
48
+ last_top_obj[4],
49
+ last_top_obj[2],
50
+ )
51
+ )
52
+ return None
53
+ return last_top_obj
54
+
55
+
56
+ def handle_class(idx, class_match, indent, stack, last_top_obj):
57
+ name = class_match.group(2)
58
+ parent = stack[-1][1] if stack and stack[-1][0] == "class" else ""
59
+ obj = ("class", name, indent, idx + 1, parent)
60
+ stack.append(obj)
61
+ if indent == 0:
62
+ last_top_obj = obj
63
+ return last_top_obj
64
+
65
+
66
+ def handle_function(idx, func_match, indent, stack, last_top_obj):
67
+ name = func_match.group(2)
68
+ parent = ""
69
+ for s in reversed(stack):
70
+ if s[0] == "class" and indent > s[2]:
71
+ parent = s[1]
72
+ break
73
+ obj = ("function", name, indent, idx + 1, parent)
74
+ stack.append(obj)
75
+ if indent == 0:
76
+ last_top_obj = obj
77
+ return last_top_obj
78
+
79
+
80
+ def process_line(idx, line, regexes, stack, obj_ranges, outline, last_top_obj):
81
+ class_pat, func_pat, assign_pat, main_pat = regexes
82
+ class_match = class_pat.match(line)
83
+ func_match = func_pat.match(line)
84
+ assign_match = assign_pat.match(line)
85
+ indent = len(line) - len(line.lstrip())
86
+ # If a new top-level class or function starts, close the previous one
87
+ if (class_match or func_match) and indent == 0 and last_top_obj:
88
+ last_top_obj = close_last_top_obj(idx, last_top_obj, stack, obj_ranges)
89
+ if class_match:
90
+ last_top_obj = handle_class(idx, class_match, indent, stack, last_top_obj)
91
+ elif func_match:
92
+ last_top_obj = handle_function(idx, func_match, indent, stack, last_top_obj)
93
+ elif assign_match and indent == 0:
94
+ handle_assignment(idx, assign_match, outline)
95
+ main_match = main_pat.match(line)
96
+ if main_match:
97
+ handle_main(idx, outline)
98
+ close_stack_objects(idx, indent, stack, obj_ranges)
99
+ return last_top_obj
100
+
101
+
102
+ def build_outline_entry(obj, lines, outline):
103
+ obj_type, name, start, end, parent, indent = obj
104
+ # Determine if this is a method
105
+ if obj_type == "function" and parent:
106
+ outline_type = "method"
107
+ elif obj_type == "function":
108
+ outline_type = "function"
109
+ else:
110
+ outline_type = obj_type
111
+ docstring = extract_docstring(lines, start, end)
112
+ outline.append(
113
+ {
114
+ "type": outline_type,
115
+ "name": name,
116
+ "start": start,
117
+ "end": end,
118
+ "parent": parent,
119
+ "docstring": docstring,
120
+ }
121
+ )
122
+
123
+
124
+ def process_lines(lines, regexes):
10
125
  outline = []
11
- stack = [] # (type, name, indent, start, parent)
12
- obj_ranges = [] # (type, name, start, end, parent, indent)
126
+ stack = []
127
+ obj_ranges = []
13
128
  last_top_obj = None
14
129
  for idx, line in enumerate(lines):
15
- class_match = class_pat.match(line)
16
- func_match = func_pat.match(line)
17
- assign_match = assign_pat.match(line)
18
- indent = len(line) - len(line.lstrip())
19
- # If a new top-level class or function starts, close the previous one
20
- if (class_match or func_match) and indent == 0 and last_top_obj:
21
- # Only close if still open
22
- if last_top_obj in stack:
23
- stack.remove(last_top_obj)
24
- obj_ranges.append(
25
- (
26
- last_top_obj[0],
27
- last_top_obj[1],
28
- last_top_obj[3],
29
- idx,
30
- last_top_obj[4],
31
- last_top_obj[2],
32
- )
33
- )
34
- last_top_obj = None
35
- if class_match:
36
- name = class_match.group(2)
37
- parent = stack[-1][1] if stack and stack[-1][0] == "class" else ""
38
- obj = ("class", name, indent, idx + 1, parent)
39
- stack.append(obj)
40
- if indent == 0:
41
- last_top_obj = obj
42
- elif func_match:
43
- name = func_match.group(2)
44
- parent = ""
45
- for s in reversed(stack):
46
- if s[0] == "class" and indent > s[2]:
47
- parent = s[1]
48
- break
49
- obj = ("function", name, indent, idx + 1, parent)
50
- stack.append(obj)
51
- if indent == 0:
52
- last_top_obj = obj
53
- elif assign_match and indent == 0:
54
- var_name = assign_match.group(2)
55
- var_type = "const" if var_name.isupper() else "var"
56
- outline.append(
57
- {
58
- "type": var_type,
59
- "name": var_name,
60
- "start": idx + 1,
61
- "end": idx + 1,
62
- "parent": "",
63
- "docstring": "",
64
- }
65
- )
66
- main_match = main_pat.match(line)
67
- if main_match:
68
- outline.append(
69
- {
70
- "type": "main",
71
- "name": "__main__",
72
- "start": idx + 1,
73
- "end": idx + 1,
74
- "parent": "",
75
- "docstring": "",
76
- }
77
- )
78
- while stack and indent < stack[-1][2]:
79
- popped = stack.pop()
80
- obj_ranges.append(
81
- (popped[0], popped[1], popped[3], idx, popped[4], popped[2])
82
- )
130
+ last_top_obj = process_line(
131
+ idx, line, regexes, stack, obj_ranges, outline, last_top_obj
132
+ )
83
133
  # Close any remaining open objects
84
134
  for popped in stack:
85
135
  obj_ranges.append(
86
136
  (popped[0], popped[1], popped[3], len(lines), popped[4], popped[2])
87
137
  )
138
+ return outline, obj_ranges
139
+
88
140
 
89
- # Now, extract docstrings for classes, functions, and methods
141
+ def build_outline(obj_ranges, lines, outline):
90
142
  for obj in obj_ranges:
91
- obj_type, name, start, end, parent, indent = obj
92
- # Determine if this is a method
93
- if obj_type == "function" and parent:
94
- outline_type = "method"
95
- elif obj_type == "function":
96
- outline_type = "function"
97
- else:
98
- outline_type = obj_type
99
- docstring = extract_docstring(lines, start, end)
100
- outline.append(
101
- {
102
- "type": outline_type,
103
- "name": name,
104
- "start": start,
105
- "end": end,
106
- "parent": parent,
107
- "docstring": docstring,
108
- }
109
- )
143
+ build_outline_entry(obj, lines, outline)
110
144
  return outline
111
145
 
112
146
 
147
+ def parse_python_outline(lines: List[str]):
148
+ class_pat = re.compile(r"^(\s*)class\s+(\w+)")
149
+ func_pat = re.compile(r"^(\s*)def\s+(\w+)")
150
+ assign_pat = re.compile(r"^(\s*)([A-Za-z_][A-Za-z0-9_]*)\s*=.*")
151
+ main_pat = re.compile(r"^\s*if\s+__name__\s*==\s*[\'\"]__main__[\'\"]\s*:")
152
+ regexes = (class_pat, func_pat, assign_pat, main_pat)
153
+ outline, obj_ranges = process_lines(lines, regexes)
154
+ return build_outline(obj_ranges, lines, outline)
155
+
156
+
113
157
  def extract_docstring(lines, start_idx, end_idx):
114
158
  """Extracts a docstring from lines[start_idx:end_idx] if present."""
115
159
  for i in range(start_idx, min(end_idx, len(lines))):
@@ -1,4 +1,5 @@
1
1
  from janito.agent.tool_base import ToolBase
2
+ from janito.agent.tools_utils.action_type import ActionType
2
3
  from janito.agent.tool_registry import register_tool
3
4
 
4
5
 
@@ -13,10 +14,11 @@ class SearchOutlineTool(ToolBase):
13
14
  from janito.i18n import tr
14
15
 
15
16
  self.report_info(
17
+ ActionType.READ,
16
18
  tr(
17
19
  "🔍 Searching for outline in '{disp_path}'",
18
20
  disp_path=display_path(file_path),
19
- )
21
+ ),
20
22
  )
21
23
  # ... rest of implementation ...
22
24
  # Example warnings and successes:
@@ -1,4 +1,5 @@
1
1
  from janito.agent.tool_base import ToolBase
2
+ from janito.agent.tools_utils.action_type import ActionType
2
3
  from janito.agent.tool_registry import register_tool
3
4
  from janito.agent.tools_utils.utils import pluralize
4
5
  from janito.i18n import tr
@@ -21,95 +22,128 @@ class GetLinesTool(ToolBase):
21
22
  - "---\nFile: /path/to/file.py | Lines: 1-10 (of 100)\n---\n<lines...>"
22
23
  - "---\nFile: /path/to/file.py | All lines (total: 100 (all))\n---\n<all lines...>"
23
24
  - "Error reading file: <error message>"
24
- - " not found"
25
+ - "\u2757 not found"
25
26
  """
26
27
 
27
28
  def run(self, file_path: str, from_line: int = None, to_line: int = None) -> str:
28
29
  from janito.agent.tools_utils.utils import display_path
29
30
 
30
31
  disp_path = display_path(file_path)
32
+ self._report_read_info(disp_path, from_line, to_line)
33
+ try:
34
+ lines = self._read_file_lines(file_path)
35
+ selected, selected_len, total_lines = self._select_lines(
36
+ lines, from_line, to_line
37
+ )
38
+ self._report_success(selected_len, from_line, to_line, total_lines)
39
+ header = self._format_header(
40
+ disp_path, from_line, to_line, selected_len, total_lines
41
+ )
42
+ return header + "".join(selected)
43
+ except Exception as e:
44
+ return self._handle_read_error(e)
45
+
46
+ def _report_read_info(self, disp_path, from_line, to_line):
47
+ """Report the info message for reading lines."""
31
48
  if from_line and to_line:
32
49
  self.report_info(
50
+ ActionType.READ,
33
51
  tr(
34
- "📖 Reading file '{disp_path}' {from_line}-{to_line}",
52
+ "📖 Read file '{disp_path}' {from_line}-{to_line}",
35
53
  disp_path=disp_path,
36
54
  from_line=from_line,
37
55
  to_line=to_line,
38
- )
56
+ ),
39
57
  )
40
58
  else:
41
- self.report_info(tr("📖 Reading file '{disp_path}'", disp_path=disp_path))
42
- try:
43
- with open(file_path, "r", encoding="utf-8", errors="replace") as f:
44
- lines = f.readlines()
45
- selected = lines[
46
- (from_line - 1 if from_line else 0) : (to_line if to_line else None)
47
- ]
48
- selected_len = len(selected)
49
- total_lines = len(lines)
50
- at_end = False
51
- if from_line and to_line:
52
- requested = to_line - from_line + 1
53
- if to_line >= total_lines or selected_len < requested:
54
- at_end = True
55
- if at_end:
56
- self.report_success(
57
- tr(
58
- " {selected_len} {line_word} (end)",
59
- selected_len=selected_len,
60
- line_word=pluralize("line", selected_len),
61
- )
62
- )
63
- elif to_line < total_lines:
64
- self.report_success(
65
- tr(
66
- " ✅ {selected_len} {line_word} ({remaining} to end)",
67
- selected_len=selected_len,
68
- line_word=pluralize("line", selected_len),
69
- remaining=total_lines - to_line,
70
- )
71
- )
72
- else:
59
+ self.report_info(
60
+ ActionType.READ,
61
+ tr("📖 Read file '{disp_path}'", disp_path=disp_path),
62
+ )
63
+
64
+ def _read_file_lines(self, file_path):
65
+ """Read all lines from the file."""
66
+ with open(file_path, "r", encoding="utf-8", errors="replace") as f:
67
+ return f.readlines()
68
+
69
+ def _select_lines(self, lines, from_line, to_line):
70
+ """Select the requested lines and return them with their count and total lines."""
71
+ selected = lines[
72
+ (from_line - 1 if from_line else 0) : (to_line if to_line else None)
73
+ ]
74
+ selected_len = len(selected)
75
+ total_lines = len(lines)
76
+ return selected, selected_len, total_lines
77
+
78
+ def _report_success(self, selected_len, from_line, to_line, total_lines):
79
+ """Report the success message after reading lines."""
80
+ if from_line and to_line:
81
+ requested = to_line - from_line + 1
82
+ at_end = to_line >= total_lines or selected_len < requested
83
+ if at_end:
73
84
  self.report_success(
74
85
  tr(
75
- " {selected_len} {line_word} (all)",
86
+ " \u2705 {selected_len} {line_word} (end)",
76
87
  selected_len=selected_len,
77
88
  line_word=pluralize("line", selected_len),
78
89
  )
79
90
  )
80
- if from_line and to_line:
81
- if to_line >= total_lines or selected_len < (to_line - from_line + 1):
82
- header = tr(
83
- "---\n{disp_path} {from_line}-{to_line} (end)\n---\n",
84
- disp_path=disp_path,
85
- from_line=from_line,
86
- to_line=to_line,
87
- )
88
- else:
89
- header = tr(
90
- "---\n{disp_path} {from_line}-{to_line} (of {total_lines})\n---\n",
91
- disp_path=disp_path,
92
- from_line=from_line,
93
- to_line=to_line,
94
- total_lines=total_lines,
91
+ elif to_line < total_lines:
92
+ self.report_success(
93
+ tr(
94
+ " \u2705 {selected_len} {line_word} ({remaining} to end)",
95
+ selected_len=selected_len,
96
+ line_word=pluralize("line", selected_len),
97
+ remaining=total_lines - to_line,
95
98
  )
96
- elif from_line:
97
- header = tr(
98
- "---\n{disp_path} {from_line}-END (of {total_lines})\n---\n",
99
+ )
100
+ else:
101
+ self.report_success(
102
+ tr(
103
+ " \u2705 {selected_len} {line_word} (all)",
104
+ selected_len=selected_len,
105
+ line_word=pluralize("line", selected_len),
106
+ )
107
+ )
108
+
109
+ def _format_header(self, disp_path, from_line, to_line, selected_len, total_lines):
110
+ """Format the header for the output."""
111
+ if from_line and to_line:
112
+ requested = to_line - from_line + 1
113
+ at_end = selected_len < requested or to_line >= total_lines
114
+ if at_end:
115
+ return tr(
116
+ "---\n{disp_path} {from_line}-{to_line} (end)\n---\n",
99
117
  disp_path=disp_path,
100
118
  from_line=from_line,
101
- total_lines=total_lines,
119
+ to_line=to_line,
102
120
  )
103
121
  else:
104
- header = tr(
105
- "---\n{disp_path} All lines (total: {total_lines} (all))\n---\n",
122
+ return tr(
123
+ "---\n{disp_path} {from_line}-{to_line} (of {total_lines})\n---\n",
106
124
  disp_path=disp_path,
125
+ from_line=from_line,
126
+ to_line=to_line,
107
127
  total_lines=total_lines,
108
128
  )
109
- return header + "".join(selected)
110
- except Exception as e:
111
- if isinstance(e, FileNotFoundError):
112
- self.report_error(tr("❗ not found"))
113
- return tr("❗ not found")
114
- self.report_error(tr(" ❌ Error: {error}", error=e))
115
- return tr("Error reading file: {error}", error=e)
129
+ elif from_line:
130
+ return tr(
131
+ "---\n{disp_path} {from_line}-END (of {total_lines})\n---\n",
132
+ disp_path=disp_path,
133
+ from_line=from_line,
134
+ total_lines=total_lines,
135
+ )
136
+ else:
137
+ return tr(
138
+ "---\n{disp_path} All lines (total: {total_lines} (all))\n---\n",
139
+ disp_path=disp_path,
140
+ total_lines=total_lines,
141
+ )
142
+
143
+ def _handle_read_error(self, e):
144
+ """Handle file read errors and report appropriately."""
145
+ if isinstance(e, FileNotFoundError):
146
+ self.report_error(tr("\u2757 not found"))
147
+ return tr("\u2757 not found")
148
+ self.report_error(tr(" \u274c Error: {error}", error=e))
149
+ return tr("Error reading file: {error}", error=e)