diff --git a/projects/scripts/favorite-wallpaper.sh b/projects/scripts/favorite-wallpaper.sh new file mode 100755 index 0000000..ffaca9e --- /dev/null +++ b/projects/scripts/favorite-wallpaper.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +HOME=/home/$USER + +CURRENT="$(cat ~/.wallpaper)" +CURRENT="${CURRENT/\/\///}" +OUTPUT_DIR="/truenas/sudacode/pictures/wallpapers/" + +cp "$CURRENT" "$HOME/Pictures/wallpapers/favorites/" + +if cp "$CURRENT" "$OUTPUT_DIR"; then + notify-send "favorite-wallpaper" "Wallpaper saved to $OUTPUT_DIR" +else + notify-send "favorite-wallpaper" "Failed to saved wallpaper to $OUTPUT_DIR" +fi + +# ft: sh diff --git a/projects/scripts/hyprland-pin.sh b/projects/scripts/hyprland-pin.sh new file mode 100755 index 0000000..729aafa --- /dev/null +++ b/projects/scripts/hyprland-pin.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +window_info=$(hyprctl activewindow -j) +read -r is_pinned window_class window_title <<< "$(echo "$window_info" | jq -r '[.pinned, .class, .title] | @tsv')" + +hyprctl dispatch pin active + +read -r window_x window_y window_w window_h <<< "$(echo "$window_info" | jq -r '[.at[0], .at[1], .size[0], .size[1]] | @tsv')" + +screenshot=$(mktemp --suffix=.png) +grim -g "${window_x},${window_y} ${window_w}x${window_h}" "$screenshot" + +if [ "$is_pinned" = "true" ]; then + status="Unpinned" +else + status="Pinned" +fi + +notify-send -u low -i "$screenshot" "$status: $window_class" "$window_title" +rm -f "$screenshot" + +# vim: set ft=sh diff --git a/projects/scripts/popup-ai-chat.py b/projects/scripts/popup-ai-chat.py new file mode 100755 index 0000000..f1a3e99 --- /dev/null +++ b/projects/scripts/popup-ai-chat.py @@ -0,0 +1,214 @@ +#!/usr/bin/env python3 +""" +Popup AI chat assistant using rofi for input and OpenRouter for responses. +""" + +import os +import shutil +import subprocess +import sys +from typing import Optional + +import requests + +API_URL = "https://openrouter.ai/api/v1/chat/completions" +MODEL = os.environ.get("OPENROUTER_MODEL", "openai/gpt-oss-120b:free") +APP_NAME = "Popup AI Chat" +SYSTEM_PROMPT = ( + "You are a helpful AI assistant. Give direct, accurate answers. " + "Use concise formatting unless the user asks for depth." +) + + +def load_api_key() -> str: + """Load OpenRouter API key from env or fallback file.""" + api_key = os.environ.get("OPENROUTER_API_KEY", "").strip() + if api_key: + return api_key + + key_file = os.path.expanduser("~/.openrouterapikey") + if os.path.isfile(key_file): + with open(key_file, "r", encoding="utf-8") as handle: + return handle.read().strip() + + return "" + + +def show_error(message: str) -> None: + """Display an error message via zenity.""" + subprocess.run( + ["zenity", "--error", "--title", "Error", "--text", message], + stderr=subprocess.DEVNULL, + ) + + +def check_dependencies() -> bool: + """Validate required desktop tools are available.""" + missing = [cmd for cmd in ("rofi", "zenity") if shutil.which(cmd) is None] + if not missing: + return True + + message = f"Missing required command(s): {', '.join(missing)}" + if shutil.which("zenity") is not None: + show_error(message) + else: + print(f"Error: {message}", file=sys.stderr) + return False + + +def get_rofi_input() -> Optional[str]: + """Ask for user input through rofi.""" + result = subprocess.run( + ["rofi", "-dmenu", "-i", "-p", "Ask AI"], + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + text=True, + ) + if result.returncode != 0: + return None + + prompt = result.stdout.strip() + return prompt or None + + +def show_notification(body: str) -> None: + """Show processing notification when notify-send exists.""" + if shutil.which("notify-send") is None: + return + + subprocess.Popen( + ["notify-send", "-t", "0", "-a", APP_NAME, "Processing...", body], + stderr=subprocess.DEVNULL, + ) + + +def close_notification() -> None: + """Close the processing notification if one was sent.""" + if shutil.which("pkill") is None: + return + + subprocess.run( + ["pkill", "-f", "notify-send.*Processing..."], + stderr=subprocess.DEVNULL, + ) + + +def make_api_request(api_key: str, messages: list[dict[str, str]]) -> dict: + """Send chat request to OpenRouter and return JSON payload.""" + headers = { + "Content-Type": "application/json", + "Authorization": f"Bearer {api_key}", + "HTTP-Referer": "https://github.com/sudacode/scripts", + "X-Title": APP_NAME, + } + payload = { + "model": MODEL, + "messages": messages, + "temperature": 0.7, + } + response = requests.post(API_URL, headers=headers, json=payload, timeout=90) + return response.json() + + +def display_result(content: str) -> None: + """Display model output in a text window.""" + subprocess.run( + [ + "zenity", + "--text-info", + "--title", + "AI Response", + "--width", + "900", + "--height", + "700", + "--font", + "monospace 11", + ], + input=content, + text=True, + stderr=subprocess.DEVNULL, + ) + + +def ask_follow_up() -> bool: + """Ask if the user wants to continue the conversation.""" + result = subprocess.run( + [ + "zenity", + "--question", + "--title", + APP_NAME, + "--text", + "Ask a follow-up question?", + "--ok-label", + "Ask Follow-up", + "--cancel-label", + "Close", + ], + stderr=subprocess.DEVNULL, + ) + return result.returncode == 0 + + +def extract_content(response: dict) -> str: + """Extract assistant response from OpenRouter payload.""" + if "error" in response: + message = response["error"].get("message", "Unknown API error") + raise ValueError(message) + + try: + content = response["choices"][0]["message"]["content"] + except (KeyError, IndexError, TypeError) as exc: + raise ValueError("Failed to parse API response") from exc + + if not content: + raise ValueError("Empty response from API") + + return content + + +def main() -> int: + if not check_dependencies(): + return 1 + + api_key = load_api_key() + if not api_key: + show_error("OPENROUTER_API_KEY environment variable is not set.") + return 1 + + history: list[dict[str, str]] = [{"role": "system", "content": SYSTEM_PROMPT}] + + while True: + user_input = get_rofi_input() + if not user_input: + return 0 + + request_messages = history + [{"role": "user", "content": user_input}] + show_notification(f"Thinking: {user_input[:60]}...") + + try: + response = make_api_request(api_key, request_messages) + content = extract_content(response) + except requests.RequestException as exc: + show_error(f"API request failed: {exc}") + return 1 + except ValueError as exc: + show_error(str(exc)) + return 1 + except Exception as exc: # pragma: no cover + show_error(f"Unexpected error: {exc}") + return 1 + finally: + close_notification() + + history.append({"role": "user", "content": user_input}) + history.append({"role": "assistant", "content": content}) + + display_result(content) + if not ask_follow_up(): + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/projects/scripts/popup-ai-translator.sh b/projects/scripts/popup-ai-translator.sh new file mode 100755 index 0000000..91a634b --- /dev/null +++ b/projects/scripts/popup-ai-translator.sh @@ -0,0 +1,181 @@ +#!/bin/bash + +# Japanese Learning Assistant using OpenRouter API +# Uses Google Gemini Flash 2.0 for AJATT-aligned Japanese analysis + +# Configuration +OPENROUTER_API_KEY="${OPENROUTER_API_KEY:-}" +MODEL="${OPENROUTER_MODEL:-google/gemini-2.0-flash-001}" +API_URL="https://openrouter.ai/api/v1/chat/completions" + +if [[ -z $OPENROUTER_API_KEY && -f "$HOME/.openrouterapikey" ]]; then + OPENROUTER_API_KEY="$(<"$HOME/.openrouterapikey")" +fi + +# System prompt for Japanese learning +SYSTEM_PROMPT='You are my Japanese-learning assistant. Help me acquire Japanese through deep, AJATT-aligned analysis. + +For every input, output exactly: + +1. Japanese Input (Verbatim) + +Repeat the original text exactly. Correct only critical OCR/punctuation errors. + +2. Natural English Translation + +Accurate and natural. Preserve tone, formality, and nuance. Avoid literalism. + +3. Word-by-Word Breakdown + +For each unit: + +- Vocabulary: Part of speech + concise definition +- Grammar: Particles, conjugations, constructions (contextual usage) +- Nuance: Implied meaning, connotation, emotional tone, differences from similar expressions + +Core Principles: + +- Preserve native phrasing—never oversimplify +- Highlight subtle grammar, register shifts, and pragmatic implications +- Encourage pattern recognition; provide contrastive examples (e.g., ~のに vs ~けど) +- Focus on real Japanese usage + +Rules: + +- English explanations only (no romaji) +- Clean, structured formatting; calm, precise tone +- No filler text + +Optional Additions (only when valuable): + +- Synonyms, formality/register notes, cultural insights, common mistakes, extra native examples + +Goal: Deep comprehension, natural grammar internalization, nuanced vocabulary, progress toward Japanese-only understanding.' + +# Check for API key +if [[ -z "$OPENROUTER_API_KEY" ]]; then + zenity --error --text="OPENROUTER_API_KEY environment variable is not set." --title="Error" 2>/dev/null + exit 1 +fi + +# Get input from zenity +input=$(zenity --entry \ + --title="Japanese Assistant" \ + --text="Enter Japanese text to analyze:" \ + --width=500 \ + 2>/dev/null) + +# Exit if no input +if [[ -z "$input" ]]; then + exit 0 +fi + +# Show loading notification +notify-send -t 0 -a "Japanese Assistant" "Processing..." "Analyzing: ${input:0:50}..." & +notif_pid=$! + +# Escape special characters for JSON +escape_json() { + local str="$1" + str="${str//\\/\\\\}" + str="${str//\"/\\\"}" + str="${str//$'\n'/\\n}" + str="${str//$'\r'/\\r}" + str="${str//$'\t'/\\t}" + printf '%s' "$str" +} + +escaped_input=$(escape_json "$input") +escaped_system=$(escape_json "$SYSTEM_PROMPT") + +# Build JSON payload +json_payload=$( + cat </dev/null + +# Check for errors +if [[ -z "$response" ]]; then + zenity --error --text="No response from API" --title="Error" 2>/dev/null + exit 1 +fi + +# Parse response and extract content using Python (handles Unicode properly) +result=$(echo "$response" | python3 -c " +import json +import sys + +try: + data = json.load(sys.stdin) + + if 'error' in data: + err = data['error'].get('message', 'Unknown error') + print(f'ERROR:{err}', end='') + sys.exit(1) + + content = data.get('choices', [{}])[0].get('message', {}).get('content', '') + if not content: + print('ERROR:Failed to parse API response', end='') + sys.exit(1) + + # Decode any unicode escape sequences in the content + try: + content = content.encode('utf-8').decode('unicode_escape').encode('latin-1').decode('utf-8') + except: + pass # Keep original if decoding fails + + print(content, end='') +except json.JSONDecodeError as e: + print(f'ERROR:Invalid JSON response: {e}', end='') + sys.exit(1) +except Exception as e: + print(f'ERROR:{e}', end='') + sys.exit(1) +") + +# Check for errors from Python parsing +if [[ "$result" == ERROR:* ]]; then + error_msg="${result#ERROR:}" + zenity --error --text="$error_msg" --title="Error" 2>/dev/null + exit 1 +fi + +content="$result" + +if [[ -z "$content" ]]; then + zenity --error --text="Empty response from API" --title="Error" 2>/dev/null + exit 1 +fi + +# Display result in zenity +zenity --text-info \ + --title="Japanese Analysis" \ + --width=800 \ + --height=600 \ + --font="monospace" \ + <<<"$content" 2>/dev/null diff --git a/projects/scripts/screenshot-active-window.sh b/projects/scripts/screenshot-active-window.sh new file mode 100755 index 0000000..4a73141 --- /dev/null +++ b/projects/scripts/screenshot-active-window.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +tmpfile=$(mktemp /tmp/screenshot-XXXXXX.png) +grim -g "$(hyprctl activewindow -j | jq -r '.at[0],.at[1],.size[0],.size[1]' | tr '\n' ' ' | awk '{print $1","$2" "$3"x"$4}')" "$tmpfile" +wl-copy < "$tmpfile" +notify-send -i "$tmpfile" "Screenshot of active window copied to clipboard" +rm -f "$tmpfile"