Files
neoromantique-dotfiles/home/dot_local/bin/executable_quick-memo.tmpl
David Aizenberg 11e5fb4e54 sync
2025-12-14 03:24:35 +01:00

286 lines
8.2 KiB
Python

#!/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()