Rectangle, ls pagination

This commit is contained in:
moosecrap 2026-07-21 04:18:00 -07:00
parent 823da03858
commit f033d9ea8c

View File

@ -5,11 +5,36 @@ import logging
import asyncio import asyncio
import io import io
import datetime import datetime
import time
from typing import Any, Callable, Dict, List, Union from typing import Any, Callable, Dict, List, Union
from PIL import Image, ImageDraw, ImageFont from PIL import Image, ImageDraw, ImageFont
logger = logging.getLogger("MattCP") 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: class Tool:
def __init__(self, name: str, description: str, schema: Dict[str, Any], handler: Callable): def __init__(self, name: str, description: str, schema: Dict[str, Any], handler: Callable):
self.name = name self.name = name
@ -28,11 +53,9 @@ async def read_image_handler(args: Dict[str, Any]):
path = args.get("path") path = args.get("path")
if not path: if not path:
return {"text": "Error: Missing path argument"} return {"text": "Error: Missing path argument"}
mime_type, _ = mimetypes.guess_type(path) mime_type, _ = mimetypes.guess_type(path)
if not mime_type or not mime_type.startswith("image/"): if not mime_type or not mime_type.startswith("image/"):
return {"text": f"Error: File {path} is not a supported image format."} return {"text": f"Error: File {path} is not a supported image format."}
try: try:
with open(path, "rb") as f: with open(path, "rb") as f:
data = base64.b64encode(f.read()).decode("utf-8") 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") path = args.get("path")
if not path: if not path:
return {"text": "Error: Missing path argument"} return {"text": "Error: Missing path argument"}
try: try:
with Image.open(path) as img: with Image.open(path) as img:
if img.format != 'PNG': 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."} return {"text": f"Error: {dir_path} is not a directory."}
exts = ('.png', '.jpg', '.jpeg', '.webp', '.bmp') exts = ('.png', '.jpg', '.jpeg', '.webp', '.bmp')
file_info_list = [] file_info_list = []
for f in os.listdir(dir_path): for f in os.listdir(dir_path):
if f.lower().endswith(exts): 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 off_y = (thumb_size - img.height) // 2
canvas.paste(img, (x + off_x, y + off_y)) 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 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 = 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) draw.text((x + 2, y + thumb_size + 2), text, fill=(255, 255, 255), font=font)
except Exception as e: 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]): async def list_directory_details_handler(args: Dict[str, Any]):
path = args.get("path") path = args.get("path")
page = int(args.get("page", 1))
page_size = int(args.get("page_size", 64))
if not path: if not path:
return {"text": "Error: Missing path argument"} return {"text": "Error: Missing path argument"}
if not os.path.isdir(path): if not os.path.isdir(path):
@ -193,50 +215,57 @@ async def list_directory_details_handler(args: Dict[str, Any]):
except Exception as e: except Exception as e:
return {"text": f"Error reading directory: {e}"} return {"text": f"Error reading directory: {e}"}
dirs = [] all_items = []
files = []
for entry in entries: for entry in entries:
full_path = os.path.join(path, entry) full_path = os.path.join(path, entry)
try: try:
stats = os.stat(full_path) 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): if os.path.isdir(full_path):
# Count items in dir (non-recursive)
try: try:
count = len(os.listdir(full_path)) count = len(os.listdir(full_path))
except: except:
count = 0 count = 0
dirs.append({ all_items.append({
"type": "dir",
"name": entry, "name": entry,
"info": f"{count} items", "info": f"{count} items",
"mtime": mtime "mtime": mtime_rel
}) })
else: else:
size_kb = stats.st_size / 1024 size_kb = stats.st_size / 1024
files.append({ all_items.append({
"type": "file",
"name": entry, "name": entry,
"info": f"{size_kb:.1f}KB", "info": f"{size_kb:.1f}KB",
"mtime": mtime "mtime": mtime_rel
}) })
except Exception as e: except Exception as e:
logger.error(f"Could not stat {entry}: {e}") logger.error(f"Could not stat {entry}: {e}")
# Sort alphabetically # Sort: Directories first, then files, then alphabetical
dirs.sort(key=lambda x: x["name"].lower()) all_items.sort(key=lambda x: (x["type"] == "file", x["name"].lower()))
files.sort(key=lambda x: 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 = [] output = []
for d in dirs: for item in paged_items:
output.append(f"[DIR] {d['name']} | {d['info']} | {d['mtime']}") if item["type"] == "dir":
for f in files: output.append(f"[DIR] {item['name']} | {item['info']} | {item['mtime']}")
output.append(f" {f['name']} | {f['info']} | {f['mtime']}") else:
output.append(f" {item['name']} | {item['info']} | {item['mtime']}")
if not output: total_pages = (total_items + page_size - 1) // page_size
return {"text": f"Directory {path} is empty."} 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"Listing of {path}:\n\n" + "\n".join(output)} return {"text": f"{header}\n\n" + "\n".join(output)}
TOOL_REGISTRY = [ TOOL_REGISTRY = [
Tool( Tool(
@ -276,11 +305,13 @@ TOOL_REGISTRY = [
), ),
Tool( Tool(
name="list_directory", 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={ schema={
"type": "object", "type": "object",
"properties": { "properties": {
"path": {"type": "string", "description": "Path to the directory to list"}, "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"], "required": ["path"],
}, },