janito 3.12.0__py3-none-any.whl → 3.12.2__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.
@@ -1,80 +1,82 @@
1
- from .adapter import LocalToolsAdapter
2
-
3
- from .ask_user import AskUserTool
4
- from .copy_file import CopyFileTool
5
- from .create_directory import CreateDirectoryTool
6
- from .create_file import CreateFileTool
7
- from .fetch_url import FetchUrlTool
8
- from .find_files import FindFilesTool
9
- from .view_file import ViewFileTool
10
- from .read_files import ReadFilesTool
11
- from .move_file import MoveFileTool
12
- from .open_url import OpenUrlTool
13
- from .open_html_in_browser import OpenHtmlInBrowserTool
14
- from .python_code_run import PythonCodeRunTool
15
- from .python_command_run import PythonCommandRunTool
16
- from .python_file_run import PythonFileRunTool
17
- from .remove_directory import RemoveDirectoryTool
18
- from .remove_file import RemoveFileTool
19
- from .replace_text_in_file import ReplaceTextInFileTool
20
- from .run_bash_command import RunBashCommandTool
21
- from .run_powershell_command import RunPowershellCommandTool
22
- from .get_file_outline.core import GetFileOutlineTool
23
- from .get_file_outline.search_outline import SearchOutlineTool
24
- from .search_text.core import SearchTextTool
25
- from .validate_file_syntax.core import ValidateFileSyntaxTool
26
- from .read_chart import ReadChartTool
27
- from .show_image import ShowImageTool
28
- from .show_image_grid import ShowImageGridTool
29
-
30
- from janito.tools.tool_base import ToolPermissions
31
- import os
32
- from janito.tools.permissions import get_global_allowed_permissions
33
- from janito.platform_discovery import PlatformDiscovery
34
-
35
- # Singleton tools adapter with all standard tools registered
36
- local_tools_adapter = LocalToolsAdapter(workdir=os.getcwd())
37
-
38
-
39
- def get_local_tools_adapter(workdir=None):
40
- return LocalToolsAdapter(workdir=workdir or os.getcwd())
41
-
42
-
43
- # Register tools
44
- pd = PlatformDiscovery()
45
- is_powershell = pd.detect_shell().startswith("PowerShell")
46
-
47
- for tool_class in [
48
- AskUserTool,
49
- CopyFileTool,
50
- CreateDirectoryTool,
51
- CreateFileTool,
52
- FetchUrlTool,
53
- FindFilesTool,
54
- ViewFileTool,
55
- ReadFilesTool,
56
- MoveFileTool,
57
- OpenUrlTool,
58
- OpenHtmlInBrowserTool,
59
- PythonCodeRunTool,
60
- PythonCommandRunTool,
61
- PythonFileRunTool,
62
- RemoveDirectoryTool,
63
- RemoveFileTool,
64
- ReplaceTextInFileTool,
65
- RunBashCommandTool,
66
- RunPowershellCommandTool,
67
- GetFileOutlineTool,
68
- SearchOutlineTool,
69
- SearchTextTool,
70
- ValidateFileSyntaxTool,
71
- ReadChartTool,
72
- ShowImageTool,
73
- ShowImageGridTool,
74
- ]:
75
- # Skip bash tools when running in PowerShell
76
- if is_powershell and tool_class.__name__ in ["RunBashCommandTool"]:
77
- continue
78
- local_tools_adapter.register_tool(tool_class)
79
-
80
- # DEBUG: Print registered tools at startup
1
+ from .adapter import LocalToolsAdapter
2
+
3
+ from .ask_user import AskUserTool
4
+ from .copy_file import CopyFileTool
5
+ from .create_directory import CreateDirectoryTool
6
+ from .create_file import CreateFileTool
7
+ from .fetch_url import FetchUrlTool
8
+ from .find_files import FindFilesTool
9
+ from .view_file import ViewFileTool
10
+ from .read_files import ReadFilesTool
11
+ from .move_file import MoveFileTool
12
+ from .open_url import OpenUrlTool
13
+ from .open_html_in_browser import OpenHtmlInBrowserTool
14
+ from .python_code_run import PythonCodeRunTool
15
+ from .python_command_run import PythonCommandRunTool
16
+ from .python_file_run import PythonFileRunTool
17
+ from .remove_directory import RemoveDirectoryTool
18
+ from .remove_file import RemoveFileTool
19
+ from .replace_text_in_file import ReplaceTextInFileTool
20
+ from .run_bash_command import RunBashCommandTool
21
+ from .run_powershell_command import RunPowershellCommandTool
22
+ from .get_file_outline.core import GetFileOutlineTool
23
+ from .get_file_outline.search_outline import SearchOutlineTool
24
+ from .search_text.core import SearchTextTool
25
+ from .validate_file_syntax.core import ValidateFileSyntaxTool
26
+ from .read_chart import ReadChartTool
27
+ from .show_image import ShowImageTool
28
+ from .show_image_grid import ShowImageGridTool
29
+ from .markdown_view import MarkdownViewTool
30
+
31
+ from janito.tools.tool_base import ToolPermissions
32
+ import os
33
+ from janito.tools.permissions import get_global_allowed_permissions
34
+ from janito.platform_discovery import PlatformDiscovery
35
+
36
+ # Singleton tools adapter with all standard tools registered
37
+ local_tools_adapter = LocalToolsAdapter(workdir=os.getcwd())
38
+
39
+
40
+ def get_local_tools_adapter(workdir=None):
41
+ return LocalToolsAdapter(workdir=workdir or os.getcwd())
42
+
43
+
44
+ # Register tools
45
+ pd = PlatformDiscovery()
46
+ is_powershell = pd.detect_shell().startswith("PowerShell")
47
+
48
+ for tool_class in [
49
+ AskUserTool,
50
+ CopyFileTool,
51
+ CreateDirectoryTool,
52
+ CreateFileTool,
53
+ FetchUrlTool,
54
+ FindFilesTool,
55
+ ViewFileTool,
56
+ ReadFilesTool,
57
+ MoveFileTool,
58
+ OpenUrlTool,
59
+ OpenHtmlInBrowserTool,
60
+ PythonCodeRunTool,
61
+ PythonCommandRunTool,
62
+ PythonFileRunTool,
63
+ RemoveDirectoryTool,
64
+ RemoveFileTool,
65
+ ReplaceTextInFileTool,
66
+ RunBashCommandTool,
67
+ RunPowershellCommandTool,
68
+ GetFileOutlineTool,
69
+ SearchOutlineTool,
70
+ SearchTextTool,
71
+ ValidateFileSyntaxTool,
72
+ ReadChartTool,
73
+ ShowImageTool,
74
+ ShowImageGridTool,
75
+ MarkdownViewTool,
76
+ ]:
77
+ # Skip bash tools when running in PowerShell
78
+ if is_powershell and tool_class.__name__ in ["RunBashCommandTool"]:
79
+ continue
80
+ local_tools_adapter.register_tool(tool_class)
81
+
82
+ # DEBUG: Print registered tools at startup
@@ -0,0 +1,94 @@
1
+ from janito.tools.tool_base import ToolBase, ToolPermissions
2
+ from janito.report_events import ReportAction
3
+ from janito.plugins.tools.local.adapter import register_local_tool
4
+ from janito.tools.tool_utils import display_path
5
+ from janito.i18n import tr
6
+ from janito.tools.loop_protection_decorator import protect_against_loops
7
+
8
+
9
+ @register_local_tool
10
+ class MarkdownViewTool(ToolBase):
11
+ """
12
+ Display markdown content in the terminal using rich markdown rendering.
13
+
14
+ Args:
15
+ path (str): Path to the markdown file to display.
16
+ width (int, optional): Display width. Defaults to 80.
17
+ theme (str, optional): Markdown theme. Defaults to "github".
18
+
19
+ Returns:
20
+ str: Status message indicating the result of the markdown display.
21
+ """
22
+
23
+ permissions = ToolPermissions(read=True)
24
+ tool_name = "markdown_view"
25
+
26
+ @protect_against_loops(max_calls=5, time_window=10.0, key_field="path")
27
+ def run(self, path: str, width: int = 80, theme: str = "github") -> str:
28
+ import os
29
+ from janito.tools.path_utils import expand_path
30
+
31
+ path = expand_path(path)
32
+ disp_path = display_path(path)
33
+
34
+ self.report_action(
35
+ tr("📖 View markdown '{disp_path}'", disp_path=disp_path),
36
+ ReportAction.READ,
37
+ )
38
+
39
+ try:
40
+ if not os.path.exists(path):
41
+ return f"❌ Error: File not found at '{path}'"
42
+
43
+ if not path.lower().endswith(('.md', '.markdown')):
44
+ return f"⚠️ Warning: File '{path}' does not appear to be a markdown file"
45
+
46
+ # Read the markdown file
47
+ with open(path, 'r', encoding='utf-8', errors='replace') as f:
48
+ markdown_content = f.read()
49
+
50
+ if not markdown_content.strip():
51
+ return f"⚠️ Warning: Markdown file '{path}' is empty"
52
+
53
+ # Import rich components for markdown rendering
54
+ try:
55
+ from rich.console import Console
56
+ from rich.markdown import Markdown
57
+ from rich.panel import Panel
58
+ from rich.text import Text
59
+ except ImportError:
60
+ return "❌ Error: rich library not available for markdown rendering"
61
+
62
+ # Create console with specified width
63
+ console = Console(width=width)
64
+
65
+ # Create markdown object
66
+ markdown = Markdown(markdown_content)
67
+
68
+ # Display the markdown with a header
69
+ console.print(f"\n[bold cyan]📄 Markdown: {disp_path}[/bold cyan]")
70
+ console.print("=" * min(len(disp_path) + 15, width))
71
+ console.print()
72
+
73
+ # Render the markdown content
74
+ console.print(markdown)
75
+ console.print()
76
+
77
+ self.report_success(
78
+ tr(" ✅ Markdown displayed: {disp_path}", disp_path=disp_path)
79
+ )
80
+
81
+ return f"✅ Markdown displayed: {disp_path}"
82
+
83
+ except FileNotFoundError:
84
+ self.report_warning(tr("❗ not found"))
85
+ return f"❌ Error: File not found at '{path}'"
86
+ except PermissionError:
87
+ self.report_error(tr(" ❌ Permission denied: {path}", path=disp_path))
88
+ return f"❌ Error: Permission denied reading '{path}'"
89
+ except UnicodeDecodeError as e:
90
+ self.report_error(tr(" ❌ Encoding error: {error}", error=e))
91
+ return f"❌ Error: Unable to decode file '{path}' - {e}"
92
+ except Exception as e:
93
+ self.report_error(tr(" ❌ Error: {error}", error=e))
94
+ return f"❌ Error displaying markdown: {e}"
@@ -1,74 +1,119 @@
1
- from janito.tools.tool_base import ToolBase, ToolPermissions
2
- from janito.report_events import ReportAction
3
- from janito.plugins.tools.local.adapter import register_local_tool
4
- from janito.i18n import tr
5
- from janito.tools.loop_protection_decorator import protect_against_loops
6
-
7
-
8
- @register_local_tool
9
- class ShowImageTool(ToolBase):
10
- """Display an image inline in the terminal using the rich library.
11
-
12
- Args:
13
- path (str): Path to the image file.
14
- width (int, optional): Target width in terminal cells. If unset, auto-fit.
15
- height (int, optional): Target height in terminal rows. If unset, auto-fit.
16
- preserve_aspect (bool, optional): Preserve aspect ratio. Default: True.
17
-
18
- Returns:
19
- str: Status message indicating display result or error details.
20
- """
21
-
22
- permissions = ToolPermissions(read=True)
23
- tool_name = "show_image"
24
-
25
- @protect_against_loops(max_calls=5, time_window=10.0, key_field="path")
26
- def run(
27
- self,
28
- path: str,
29
- width: int | None = None,
30
- height: int | None = None,
31
- preserve_aspect: bool = True,
32
- ) -> str:
33
- from janito.tools.tool_utils import display_path
34
- from janito.tools.path_utils import expand_path
35
- import os
36
-
37
- try:
38
- from rich.console import Console
39
- from PIL import Image as PILImage
40
- except Exception as e:
41
- msg = tr("⚠️ Missing dependency: PIL/Pillow ({error})", error=e)
42
- self.report_error(msg)
43
- return msg
44
-
45
- path = expand_path(path)
46
- disp_path = display_path(path)
47
- self.report_action(tr("🖼️ Show image '{disp_path}'", disp_path=disp_path), ReportAction.READ)
48
-
49
- if not os.path.exists(path):
50
- msg = tr("❗ not found")
51
- self.report_warning(msg)
52
- return tr("Error: file not found: {path}", path=disp_path)
53
-
54
- try:
55
- console = Console()
56
- from rich.console import Console
57
- from rich.text import Text
58
- console = Console()
59
- img = PILImage.open(path)
60
- console.print(Text(f"Image: {disp_path} ({img.width}x{img.height})", style="bold green"))
61
- console.print(img)
62
- self.report_success(tr("✅ Displayed"))
63
- details = []
64
- if width:
65
- details.append(f"width={width}")
66
- if height:
67
- details.append(f"height={height}")
68
- if not preserve_aspect:
69
- details.append("preserve_aspect=False")
70
- info = ("; ".join(details)) if details else "auto-fit"
71
- return tr("Image displayed: {disp_path} ({info})", disp_path=disp_path, info=info)
72
- except Exception as e:
73
- self.report_error(tr(" Error: {error}", error=e))
74
- return tr("Error displaying image: {error}", error=e)
1
+ from janito.tools.tool_base import ToolBase, ToolPermissions
2
+ from janito.report_events import ReportAction
3
+ from janito.plugins.tools.local.adapter import register_local_tool
4
+ from janito.i18n import tr
5
+ from janito.tools.loop_protection_decorator import protect_against_loops
6
+
7
+
8
+ @register_local_tool
9
+ class ShowImageTool(ToolBase):
10
+ """Display an image inline in the terminal using the rich library.
11
+
12
+ Args:
13
+ path (str): Path to the image file.
14
+ width (int, optional): Target width in terminal cells. If unset, auto-fit.
15
+ height (int, optional): Target height in terminal rows. If unset, auto-fit.
16
+ preserve_aspect (bool, optional): Preserve aspect ratio. Default: True.
17
+
18
+ Returns:
19
+ str: Status message indicating display result or error details.
20
+ """
21
+
22
+ permissions = ToolPermissions(read=True)
23
+ tool_name = "show_image"
24
+
25
+ @protect_against_loops(max_calls=5, time_window=10.0, key_field="path")
26
+ def run(
27
+ self,
28
+ path: str,
29
+ width: int | None = None,
30
+ height: int | None = None,
31
+ preserve_aspect: bool = True,
32
+ ) -> str:
33
+ from janito.tools.tool_utils import display_path
34
+ from janito.tools.path_utils import expand_path
35
+ import os
36
+
37
+ try:
38
+ from rich.console import Console
39
+ from PIL import Image as PILImage
40
+ except Exception as e:
41
+ msg = tr("⚠️ Missing dependency: PIL/Pillow ({error})", error=e)
42
+ self.report_error(msg)
43
+ return msg
44
+
45
+ path = expand_path(path)
46
+ disp_path = display_path(path)
47
+ self.report_action(tr("🖼️ Show image '{disp_path}'", disp_path=disp_path), ReportAction.READ)
48
+
49
+ if not os.path.exists(path):
50
+ msg = tr("❗ not found")
51
+ self.report_warning(msg)
52
+ return tr("Error: file not found: {path}", path=disp_path)
53
+
54
+ try:
55
+ console = Console()
56
+ from rich.panel import Panel
57
+ from rich.text import Text
58
+ import numpy as np
59
+
60
+ img = PILImage.open(path)
61
+
62
+ # Create ASCII art representation
63
+ def image_to_ascii(image, width=40, height=20):
64
+ try:
65
+ # Convert to grayscale and resize
66
+ img_gray = image.convert('L')
67
+ img_resized = img_gray.resize((width, height))
68
+
69
+ # Convert to numpy array
70
+ pixels = np.array(img_resized)
71
+
72
+ # ASCII characters from dark to light
73
+ ascii_chars = "@%#*+=-:. "
74
+
75
+ # Normalize pixels to ASCII range
76
+ ascii_art = ""
77
+ for row in pixels:
78
+ for pixel in row:
79
+ # Map pixel value (0-255) to ASCII index
80
+ ascii_index = int((pixel / 255) * (len(ascii_chars) - 1))
81
+ ascii_art += ascii_chars[ascii_index]
82
+ ascii_art += "\n"
83
+
84
+ return ascii_art.strip()
85
+ except Exception:
86
+ return None
87
+
88
+ # Calculate appropriate size for terminal display
89
+ display_width = width or min(60, img.width // 4)
90
+ display_height = height or min(30, img.height // 4)
91
+
92
+ ascii_art = image_to_ascii(img, display_width, display_height)
93
+
94
+ if ascii_art:
95
+ # Create a panel with both info and ASCII art
96
+ img_info = Text(f"🖼️ {disp_path}\nSize: {img.width}×{img.height}\nMode: {img.mode}\n", style="bold green")
97
+ ascii_text = Text(ascii_art, style="dim")
98
+ combined = Text.assemble(img_info, ascii_text)
99
+ panel = Panel(combined, title="Image Preview", border_style="blue")
100
+ else:
101
+ # Fallback to just info if ASCII art fails
102
+ img_info = Text(f"🖼️ {disp_path}\nSize: {img.width}×{img.height}\nMode: {img.mode}", style="bold green")
103
+ panel = Panel(img_info, title="Image Info", border_style="blue")
104
+
105
+ console.print(panel)
106
+
107
+ self.report_success(tr("✅ Displayed"))
108
+ details = []
109
+ if width:
110
+ details.append(f"width={width}")
111
+ if height:
112
+ details.append(f"height={height}")
113
+ if not preserve_aspect:
114
+ details.append("preserve_aspect=False")
115
+ info = ("; ".join(details)) if details else "auto-fit"
116
+ return tr("Image displayed: {disp_path} ({info})", disp_path=disp_path, info=info)
117
+ except Exception as e:
118
+ self.report_error(tr(" ❌ Error: {error}", error=e))
119
+ return tr("Error displaying image: {error}", error=e)
@@ -1,76 +1,134 @@
1
- from janito.tools.tool_base import ToolBase, ToolPermissions
2
- from janito.report_events import ReportAction
3
- from janito.plugins.tools.local.adapter import register_local_tool
4
- from janito.i18n import tr
5
- from janito.tools.loop_protection_decorator import protect_against_loops
6
- from typing import Sequence
7
-
8
-
9
- @register_local_tool
10
- class ShowImageGridTool(ToolBase):
11
- """Display multiple images in a grid inline in the terminal using rich.
12
-
13
- Args:
14
- paths (list[str]): List of image file paths.
15
- columns (int, optional): Number of columns in the grid. Default: 2.
16
- width (int, optional): Max width for each image cell. Default: None (auto).
17
- height (int, optional): Max height for each image cell. Default: None (auto).
18
- preserve_aspect (bool, optional): Preserve aspect ratio. Default: True.
19
-
20
- Returns:
21
- str: Status string summarizing the grid display.
22
- """
23
-
24
- permissions = ToolPermissions(read=True)
25
- tool_name = "show_image_grid"
26
-
27
- @protect_against_loops(max_calls=5, time_window=10.0, key_field="paths")
28
- def run(
29
- self,
30
- paths: Sequence[str],
31
- columns: int = 2,
32
- width: int | None = None,
33
- height: int | None = None,
34
- preserve_aspect: bool = True,
35
- ) -> str:
36
- from janito.tools.path_utils import expand_path
37
- from janito.tools.tool_utils import display_path
38
- import os
39
-
40
- try:
41
- from rich.console import Console
42
- from rich.columns import Columns
43
- from PIL import Image as PILImage
44
- from rich.panel import Panel
45
- except Exception as e:
46
- msg = tr("⚠️ Missing dependency: PIL/Pillow ({error})", error=e)
47
- self.report_error(msg)
48
- return msg
49
-
50
- if not paths:
51
- return tr("No images provided")
52
-
53
- self.report_action(tr("🖼️ Show image grid ({n} images)", n=len(paths)), ReportAction.READ)
54
-
55
- console = Console()
56
- images = []
57
- shown = 0
58
- for p in paths:
59
- fp = expand_path(p)
60
- if not os.path.exists(fp):
61
- self.report_warning(tr("❗ not found: {p}", p=display_path(fp)))
62
- continue
63
- try:
64
- img = PILImage.open(fp)
65
- title = f"{display_path(fp)} ({img.width}x{img.height})"
66
- images.append(Panel.fit(title, title=display_path(fp), border_style="dim"))
67
- shown += 1
68
- except Exception as e:
69
- self.report_warning(tr("⚠️ Skipped {p}: {e}", p=display_path(fp), e=e))
70
-
71
- if not images:
72
- return tr("No images could be displayed")
73
-
74
- console.print(Columns(images, equal=True, expand=True, columns=columns))
75
- self.report_success(tr("✅ Displayed {n} images", n=shown))
76
- return tr("Displayed {shown}/{total} images in a {cols}x? grid", shown=shown, total=len(paths), cols=columns)
1
+ from janito.tools.tool_base import ToolBase, ToolPermissions
2
+ from janito.report_events import ReportAction
3
+ from janito.plugins.tools.local.adapter import register_local_tool
4
+ from janito.i18n import tr
5
+ from janito.tools.loop_protection_decorator import protect_against_loops
6
+ from typing import Sequence
7
+
8
+
9
+ @register_local_tool
10
+ class ShowImageGridTool(ToolBase):
11
+ """Display multiple images in a grid inline in the terminal using rich.
12
+
13
+ Args:
14
+ paths (list[str]): List of image file paths.
15
+ columns (int, optional): Number of columns in the grid. Default: 2.
16
+ width (int, optional): Max width for each image cell. Default: None (auto).
17
+ height (int, optional): Max height for each image cell. Default: None (auto).
18
+ preserve_aspect (bool, optional): Preserve aspect ratio. Default: True.
19
+
20
+ Returns:
21
+ str: Status string summarizing the grid display.
22
+ """
23
+
24
+ permissions = ToolPermissions(read=True)
25
+ tool_name = "show_image_grid"
26
+
27
+ @protect_against_loops(max_calls=5, time_window=10.0, key_field="paths")
28
+ def run(
29
+ self,
30
+ paths: Sequence[str],
31
+ columns: int = 2,
32
+ width: int | None = None,
33
+ height: int | None = None,
34
+ preserve_aspect: bool = True,
35
+ ) -> str:
36
+ from janito.tools.path_utils import expand_path
37
+ from janito.tools.tool_utils import display_path
38
+ import os
39
+
40
+ try:
41
+ from rich.console import Console
42
+ from rich.columns import Columns
43
+ from PIL import Image as PILImage
44
+ from rich.panel import Panel
45
+ except Exception as e:
46
+ msg = tr("⚠️ Missing dependency: PIL/Pillow ({error})", error=e)
47
+ self.report_error(msg)
48
+ return msg
49
+
50
+ if not paths:
51
+ return tr("No images provided")
52
+
53
+ self.report_action(tr("🖼️ Show image grid ({n} images)", n=len(paths)), ReportAction.READ)
54
+
55
+ console = Console()
56
+ images = []
57
+ shown = 0
58
+
59
+ # Import numpy for ASCII art conversion
60
+ import numpy as np
61
+
62
+ # Create ASCII art representation function
63
+ def image_to_ascii(image, target_width=20, target_height=10):
64
+ try:
65
+ # Convert to grayscale and resize
66
+ img_gray = image.convert('L')
67
+ img_resized = img_gray.resize((target_width, target_height))
68
+
69
+ # Convert to numpy array
70
+ pixels = np.array(img_resized)
71
+
72
+ # ASCII characters from dark to light
73
+ ascii_chars = "@%#*+=-:. "
74
+
75
+ # Normalize pixels to ASCII range
76
+ ascii_art = ""
77
+ for row in pixels:
78
+ for pixel in row:
79
+ # Map pixel value (0-255) to ASCII index
80
+ ascii_index = int((pixel / 255) * (len(ascii_chars) - 1))
81
+ ascii_art += ascii_chars[ascii_index]
82
+ ascii_art += "\n"
83
+
84
+ return ascii_art.strip()
85
+ except Exception:
86
+ return None
87
+
88
+ for p in paths:
89
+ fp = expand_path(p)
90
+ if not os.path.exists(fp):
91
+ self.report_warning(tr("❗ not found: {p}", p=display_path(fp)))
92
+ continue
93
+ try:
94
+ img = PILImage.open(fp)
95
+
96
+ # Create ASCII art preview
97
+ ascii_art = image_to_ascii(img, 20, 10)
98
+
99
+ if ascii_art:
100
+ from rich.text import Text
101
+ title_text = Text(f"{display_path(fp)}\n{img.width}×{img.height}", style="bold")
102
+ ascii_text = Text(ascii_art, style="dim")
103
+ combined_text = Text.assemble(title_text, "\n", ascii_text)
104
+ panel = Panel(combined_text, title="Image", border_style="dim")
105
+ else:
106
+ # Fallback to just info if ASCII art fails
107
+ title = f"{display_path(fp)} ({img.width}x{img.height})"
108
+ panel = Panel.fit(title, title=display_path(fp), border_style="dim")
109
+
110
+ images.append(panel)
111
+ shown += 1
112
+ except Exception as e:
113
+ self.report_warning(tr("⚠️ Skipped {p}: {e}", p=display_path(fp), e=e))
114
+
115
+ if not images:
116
+ return tr("No images could be displayed")
117
+
118
+ # Use manual column layout since Columns doesn't support columns parameter
119
+ if columns > 1:
120
+ # Group images into rows
121
+ rows = []
122
+ for i in range(0, len(images), columns):
123
+ row_images = images[i:i+columns]
124
+ rows.append(Columns(row_images, equal=True, expand=True))
125
+
126
+ # Print all rows
127
+ for row in rows:
128
+ console.print(row)
129
+ else:
130
+ # Single column - print each image panel separately
131
+ for image_panel in images:
132
+ console.print(image_panel)
133
+ self.report_success(tr("✅ Displayed {n} images", n=shown))
134
+ return tr("Displayed {shown}/{total} images in a {cols}x? grid", shown=shown, total=len(paths), cols=columns)