diff --git a/tools.py b/tools.py index 1d6d5da..cd87b88 100644 --- a/tools.py +++ b/tools.py @@ -5,11 +5,36 @@ import logging import asyncio import io import datetime +import time from typing import Any, Callable, Dict, List, Union from PIL import Image, ImageDraw, ImageFont logger = logging.getLogger("MattCP") +def format_relative_time(timestamp: float) -> str: + """Converts a timestamp to a human-readable relative format.""" + now = time.time() + diff = now - timestamp + seconds = int(diff) + if seconds < 60: + return "Just now" + minutes = seconds // 60 + if minutes < 60: + return f"{minutes}m ago" + hours = minutes // 60 + if hours < 24: + return f"{hours}h ago" + days = hours // 24 + if days == 1: + return "Yesterday" + if days < 7: + return f"{days}d ago" + weeks = days // 7 + if weeks < 4: + return f"{weeks}w ago" + dt = datetime.datetime.fromtimestamp(timestamp) + return dt.strftime('%Y-%m-%d') + class Tool: def __init__(self, name: str, description: str, schema: Dict[str, Any], handler: Callable): self.name = name @@ -28,11 +53,9 @@ async def read_image_handler(args: Dict[str, Any]): path = args.get("path") if not path: return {"text": "Error: Missing path argument"} - mime_type, _ = mimetypes.guess_type(path) if not mime_type or not mime_type.startswith("image/"): return {"text": f"Error: File {path} is not a supported image format."} - try: with open(path, "rb") as f: data = base64.b64encode(f.read()).decode("utf-8") @@ -44,7 +67,6 @@ async def read_png_metadata_handler(args: Dict[str, Any]): path = args.get("path") if not path: return {"text": "Error: Missing path argument"} - try: with Image.open(path) as img: if img.format != 'PNG': @@ -69,7 +91,6 @@ async def list_thumbnails_handler(args: Dict[str, Any]): return {"text": f"Error: {dir_path} is not a directory."} exts = ('.png', '.jpg', '.jpeg', '.webp', '.bmp') - file_info_list = [] for f in os.listdir(dir_path): if f.lower().endswith(exts): @@ -153,13 +174,11 @@ async def list_thumbnails_handler(args: Dict[str, Any]): off_y = (thumb_size - img.height) // 2 canvas.paste(img, (x + off_x, y + off_y)) - mod_time = datetime.datetime.fromtimestamp(info["mtime"]).strftime('%Y-%m-%d %H:%M') + mod_time_rel = format_relative_time(info["mtime"]) size_kb = info["size"] / 1024 - file_details.append(f"{i+1}. {filename} | {width}x{height} | {size_kb:.1f}KB | {mod_time}") + file_details.append(f"{i+1}. {filename} | {width}x{height} | {size_kb:.1f}KB | {mod_time_rel}") text = str(i + 1) - text_w = len(text) * 10 - draw.rectangle([x, y + thumb_size + 2, x + text_w + 4, y + thumb_size + 24], fill=(60, 60, 60)) draw.text((x + 2, y + thumb_size + 2), text, fill=(255, 255, 255), font=font) except Exception as e: @@ -183,6 +202,9 @@ async def list_thumbnails_handler(args: Dict[str, Any]): async def list_directory_details_handler(args: Dict[str, Any]): path = args.get("path") + page = int(args.get("page", 1)) + page_size = int(args.get("page_size", 64)) + if not path: return {"text": "Error: Missing path argument"} if not os.path.isdir(path): @@ -193,50 +215,57 @@ async def list_directory_details_handler(args: Dict[str, Any]): except Exception as e: return {"text": f"Error reading directory: {e}"} - dirs = [] - files = [] - + all_items = [] for entry in entries: full_path = os.path.join(path, entry) try: stats = os.stat(full_path) - mtime = datetime.datetime.fromtimestamp(stats.st_mtime).strftime('%Y-%m-%d %H:%M') + mtime_rel = format_relative_time(stats.st_mtime) if os.path.isdir(full_path): - # Count items in dir (non-recursive) try: count = len(os.listdir(full_path)) except: count = 0 - dirs.append({ + all_items.append({ + "type": "dir", "name": entry, "info": f"{count} items", - "mtime": mtime + "mtime": mtime_rel }) else: size_kb = stats.st_size / 1024 - files.append({ + all_items.append({ + "type": "file", "name": entry, "info": f"{size_kb:.1f}KB", - "mtime": mtime + "mtime": mtime_rel }) except Exception as e: logger.error(f"Could not stat {entry}: {e}") - # Sort alphabetically - dirs.sort(key=lambda x: x["name"].lower()) - files.sort(key=lambda x: x["name"].lower()) + # Sort: Directories first, then files, then alphabetical + all_items.sort(key=lambda x: (x["type"] == "file", x["name"].lower())) + + total_items = len(all_items) + start_idx = (page - 1) * page_size + end_idx = start_idx + page_size + paged_items = all_items[start_idx:end_idx] + + if not paged_items: + return {"text": f"No items found on page {page}."} output = [] - for d in dirs: - output.append(f"[DIR] {d['name']} | {d['info']} | {d['mtime']}") - for f in files: - output.append(f" {f['name']} | {f['info']} | {f['mtime']}") + for item in paged_items: + if item["type"] == "dir": + output.append(f"[DIR] {item['name']} | {item['info']} | {item['mtime']}") + else: + output.append(f" {item['name']} | {item['info']} | {item['mtime']}") - if not output: - return {"text": f"Directory {path} is empty."} - - return {"text": f"Listing of {path}:\n\n" + "\n".join(output)} + total_pages = (total_items + page_size - 1) // page_size + header = f"Listing of {path}\nPage {page} of {total_pages} ({total_items} total items). Showing {start_idx+1}-{min(end_idx, total_items)}." + + return {"text": f"{header}\n\n" + "\n".join(output)} TOOL_REGISTRY = [ Tool( @@ -276,11 +305,13 @@ TOOL_REGISTRY = [ ), Tool( name="list_directory", - description="Lists the contents of a directory in a Nemo-like format. Directories first, then files, with sizes/counts and mtime.", + description="Lists the contents of a directory in a Nemo-like format. Directories first, then files. Supports pagination.", schema={ "type": "object", "properties": { "path": {"type": "string", "description": "Path to the directory to list"}, + "page": {"type": "integer", "description": "The page number to retrieve (1-indexed)", "default": 1}, + "page_size": {"type": "integer", "description": "Number of items per page", "default": 64}, }, "required": ["path"], },