mirror of
https://github.com/ksyasuda/dotfiles.git
synced 2026-03-20 06:11:27 -07:00
update
This commit is contained in:
17
projects/scripts/favorite-wallpaper.sh
Executable file
17
projects/scripts/favorite-wallpaper.sh
Executable file
@@ -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
|
||||||
22
projects/scripts/hyprland-pin.sh
Executable file
22
projects/scripts/hyprland-pin.sh
Executable file
@@ -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
|
||||||
214
projects/scripts/popup-ai-chat.py
Executable file
214
projects/scripts/popup-ai-chat.py
Executable file
@@ -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())
|
||||||
181
projects/scripts/popup-ai-translator.sh
Executable file
181
projects/scripts/popup-ai-translator.sh
Executable file
@@ -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 <<EOF
|
||||||
|
{
|
||||||
|
"model": "$MODEL",
|
||||||
|
"messages": [
|
||||||
|
{
|
||||||
|
"role": "system",
|
||||||
|
"content": "$escaped_system"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": "$escaped_input"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"temperature": 0.7
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
|
||||||
|
# Make API request
|
||||||
|
response=$(curl -s -X POST "$API_URL" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Authorization: Bearer $OPENROUTER_API_KEY" \
|
||||||
|
-H "HTTP-Referer: https://github.com/sudacode/scripts" \
|
||||||
|
-H "X-Title: Japanese Learning Assistant" \
|
||||||
|
-d "$json_payload")
|
||||||
|
|
||||||
|
# Close loading notification
|
||||||
|
pkill -f "notify-send.*Processing.*Analyzing" 2>/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
|
||||||
7
projects/scripts/screenshot-active-window.sh
Executable file
7
projects/scripts/screenshot-active-window.sh
Executable file
@@ -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"
|
||||||
Reference in New Issue
Block a user