#!/usr/bin/env python3 """Quick memo - text input or clipboard capture to jax-bot. Hotkeys: F12 - Text input mode (wofi dialog) SHIFT+F12 - Clipboard mode (text/URL/image) Usage: quick-memo --input # Text input via wofi quick-memo --clipboard # Capture clipboard content """ import argparse import subprocess import sys # httpx is optional - fall back to urllib if not available try: import httpx HAS_HTTPX = True except ImportError: import urllib.request import urllib.error import json as json_module HAS_HTTPX = False # Configuration - chezmoi template with default fallback {{- $jaxUrl := "http://127.0.0.1:8080" -}} {{- if hasKey . "jaxBotUrl" -}} {{- $jaxUrl = .jaxBotUrl -}} {{- end }} JAX_URL = "{{ $jaxUrl }}" NOTIFY_ID = "91192" def notify(title: str, msg: str, critical: bool = False): """Send desktop notification.""" cmd = ["notify-send", title, msg, "-r", NOTIFY_ID, "-t", "3000"] if critical: cmd.extend(["-u", "critical"]) subprocess.run(cmd, capture_output=True) def check_jax() -> bool: """Check if jax-bot is running.""" try: if HAS_HTTPX: httpx.get(f"{JAX_URL}/", timeout=2) else: urllib.request.urlopen(f"{JAX_URL}/", timeout=2) return True except Exception: return False def get_text_input() -> str | None: """Open yad text area dialog.""" result = subprocess.run( [ "yad", "--form", "--field", ":TXT", "--title", "Quick Memo", "--width", "500", "--height", "300", "--center", "--on-top", "--skip-taskbar", "--undecorated", "--borders", "20", ], capture_output=True, text=True, ) if result.returncode != 0: return None # User cancelled # yad returns field value followed by | text = result.stdout.strip() if text.endswith("|"): text = text[:-1] return text.strip() def get_clipboard_type() -> str: """Detect clipboard content type.""" result = subprocess.run( ["wl-paste", "--list-types"], capture_output=True, text=True, ) types = result.stdout.lower() if "image/png" in types: return "image/png" elif "image/jpeg" in types: return "image/jpeg" elif "text/plain" in types: return "text" return "unknown" def get_clipboard_text() -> str: """Get text from clipboard.""" result = subprocess.run(["wl-paste"], capture_output=True, text=True) return result.stdout.strip() def get_clipboard_image(mime: str) -> bytes: """Get image bytes from clipboard.""" result = subprocess.run( ["wl-paste", "--type", mime], capture_output=True, ) return result.stdout def send_memo_httpx( content: str | None = None, image_data: bytes | None = None, image_type: str | None = None, ) -> dict: """Send memo via httpx.""" try: if image_data: ext = "png" if "png" in (image_type or "") else "jpg" response = httpx.post( f"{JAX_URL}/api/memo", files={"file": (f"clipboard.{ext}", image_data, image_type)}, data={"auto_tag": "true"}, timeout=30, ) else: is_url = (content or "").startswith(("http://", "https://")) response = httpx.post( f"{JAX_URL}/api/memo", data={ "content": content, "auto_tag": "true", "summarize_url": "true" if is_url else "false", }, timeout=30, ) return response.json() except Exception as e: return {"success": False, "error": str(e)} def send_memo_urllib( content: str | None = None, image_data: bytes | None = None, image_type: str | None = None, ) -> dict: """Send memo via urllib (fallback when httpx unavailable).""" import io import uuid try: boundary = uuid.uuid4().hex if image_data: ext = "png" if "png" in (image_type or "") else "jpg" filename = f"clipboard.{ext}" body = io.BytesIO() body.write(f"--{boundary}\r\n".encode()) body.write(f'Content-Disposition: form-data; name="file"; filename="{filename}"\r\n'.encode()) body.write(f"Content-Type: {image_type}\r\n\r\n".encode()) body.write(image_data) body.write(f"\r\n--{boundary}\r\n".encode()) body.write(b'Content-Disposition: form-data; name="auto_tag"\r\n\r\ntrue') body.write(f"\r\n--{boundary}--\r\n".encode()) req = urllib.request.Request( f"{JAX_URL}/api/memo", data=body.getvalue(), headers={"Content-Type": f"multipart/form-data; boundary={boundary}"}, ) else: is_url = (content or "").startswith(("http://", "https://")) data = urllib.parse.urlencode({ "content": content or "", "auto_tag": "true", "summarize_url": "true" if is_url else "false", }).encode() req = urllib.request.Request(f"{JAX_URL}/api/memo", data=data) with urllib.request.urlopen(req, timeout=30) as response: return json_module.loads(response.read().decode()) except Exception as e: return {"success": False, "error": str(e)} def send_memo( content: str | None = None, image_data: bytes | None = None, image_type: str | None = None, ) -> dict: """Send memo to jax-bot API.""" if HAS_HTTPX: return send_memo_httpx(content, image_data, image_type) return send_memo_urllib(content, image_data, image_type) def mode_input(): """F12: Text input mode via wofi dialog.""" text = get_text_input() if not text: return # User cancelled, no notification notify("Quick Memo", "Processing...") result = send_memo(content=text) if result.get("success"): tags = " ".join(f"#{t}" for t in result.get("tags", [])) preview = text[:60] + ("..." if len(text) > 60 else "") notify("Quick Memo", f"Saved!\n{preview}\n{tags}") else: notify("Quick Memo", f"Error: {result.get('error')}", critical=True) def mode_clipboard(): """SHIFT+F12: Clipboard capture mode.""" clip_type = get_clipboard_type() if clip_type == "unknown": notify("Quick Memo", "Clipboard empty", critical=True) return notify("Quick Memo", "Processing...") if clip_type.startswith("image/"): image_data = get_clipboard_image(clip_type) if not image_data: notify("Quick Memo", "Failed to read image", critical=True) return result = send_memo(image_data=image_data, image_type=clip_type) else: text = get_clipboard_text() if not text: notify("Quick Memo", "Clipboard empty", critical=True) return result = send_memo(content=text) if result.get("success"): tags = " ".join(f"#{t}" for t in result.get("tags", [])) preview = result.get("content", "")[:60] if len(result.get("content", "")) > 60: preview += "..." notify("Quick Memo", f"Saved!\n{preview}\n{tags}") else: notify("Quick Memo", f"Error: {result.get('error')}", critical=True) def main(): parser = argparse.ArgumentParser( description="Quick memo to jax-bot", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=__doc__, ) parser.add_argument( "--input", "-i", action="store_true", help="Text input mode (wofi dialog)", ) parser.add_argument( "--clipboard", "-c", action="store_true", help="Clipboard capture mode", ) args = parser.parse_args() if not args.input and not args.clipboard: parser.print_help() sys.exit(1) if not check_jax(): notify("Quick Memo", "jax-bot not running", critical=True) sys.exit(1) if args.input: mode_input() elif args.clipboard: mode_clipboard() if __name__ == "__main__": main()