Merge branch 'master' of github.com:ksyasuda/dotfiles

This commit is contained in:
2026-01-16 18:17:30 -08:00
29 changed files with 1463 additions and 354 deletions

View File

@@ -14,8 +14,8 @@ alias aniwrapper='aniwrapper -D 144'
## Colorls
alias ls='eza -M --group-directories-first --icons --color=always --group --git'
alias ll='ls -l'
alias la='ls -la'
alias ll='ls -lh'
alias la='ls -lah'
alias vimf='vim $(fzf --height=45% --layout=reverse --preview="bat --style=numbers --color=always --line-range :500 {}")'

View File

@@ -4,7 +4,7 @@ font-size = 12
font-feature = +calt
font-feature = +liga
font-feature = +dlig
theme = catppuccin-macchiato
theme = Catppuccin Macchiato
cursor-style = block
window-padding-x = 10
window-padding-y = 10
@@ -14,7 +14,12 @@ window-width = 180
confirm-close-surface = false
copy-on-select = clipboard
app-notifications = no-clipboard-copy
shell-integration = zsh
shell-integration-features = title,sudo,ssh-env,ssh-terminfo
desktop-notifications = true
term=ghostty
keybind = all:ctrl+enter=unbind
keybind = all:ctrl+shift+j=next_tab
keybind = all:ctrl+shift+k=last_tab
keybind = all:ctrl+grave_accent=toggle_quick_terminal
keybind = shift+enter=text:\x1b\r

View File

@@ -18,3 +18,6 @@ keybind = all:ctrl+enter=unbind
keybind = all:ctrl+grave_accent=toggle_quick_terminal
shell-integration = zsh
keybind = shift+enter=text:\x1b\r
shell-integration-features = title,sudo,ssh-env,ssh-terminfo
desktop-notifications = true
term=ghostty

View File

@@ -54,7 +54,7 @@ xwayland {
# See https://wiki.hyprland.org/Configuring/Keywords/
# Set programs that you use
$terminal = uwsm app -- ghostty
$terminal = uwsm app -- ghostty +new-window
$fileManager = uwsm app -- thunar
$menu = rofi -show drun -run-command "uwsm app -- {cmd}"
# $notification_daemon = dunst

View File

@@ -93,8 +93,12 @@ bindl = , XF86AudioPrev, exec, mpc prev
# rofi
bind = $mainMod SHIFT, v, exec, uwsm app -sb -- rofi-rbw
bind = $mainMod, w, exec, rofi -show window -theme $HOME/.config/rofi/launchers/type-2/style-2.rasi -dpi 96 -theme-str 'window {width: 35%;}'
bind = $mainMod SHIFT, w, exec, $HOME/.config/rofi/scripts/rofi-wallpaper.sh
bind = $mainMod SHIFT, d, exec, $HOME/.config/rofi/scripts/rofi-docs.sh
bind = $mainMod SHIFT, w, exec, "$HOME/.config/rofi/scripts/rofi-wallpaper.sh"
bind = $mainMod SHIFT, d, exec, "$HOME/.config/rofi/scripts/rofi-docs.sh"
bind = SUPER, j, exec, "$HOME/.config/rofi/scripts/rofi-jellyfin-dir.sh"
bind = SUPER, t, exec, "$HOME/.config/rofi/scripts/rofi-launch-texthooker-steam.sh"
bind = $mainMod SHIFT, t, exec, "$HOME/projects/scripts/popup-ai-translator.py"
bind = SUPER SHIFT, g, exec, "$HOME/.config/rofi/scripts/rofi-vn-helper.sh"
# ncmcppp
bind = $mainMod, n, exec, uwsm app -sb -- ghostty --command=/usr/bin/ncmpcpp
@@ -139,3 +143,7 @@ bind = $mainMod, code:112, submap, reset
submap = reset
bind = SUPER, l, exec, hyprlock
# ANKI
bind = $mainMod, a, exec, ~/.config/rofi/scripts/rofi-anki-script.sh
bind = $mainMod SHIFT, a, exec, ~/projects/scripts/screenshot-anki.sh -cdMinecraft

155
.config/mimeapps.list Normal file
View File

@@ -0,0 +1,155 @@
[Added Associations]
application/epub+zip=calibre-ebook-viewer.desktop;calibre-ebook-edit.desktop;opencomic.desktop;
application/json=notepadqq.desktop;
application/octet-stream=nvim.desktop;vim.desktop;emacsclient.desktop;
application/pdf=okularApplication_pdf.desktop;google-chrome.desktop;microsoft-edge-beta.desktop;org.inkscape.Inkscape.desktop;chromium.desktop;
application/rss+xml=fluent-reader.desktop;
application/sql=notepadqq.desktop;nvim.desktop;gvim.desktop;
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet=libreoffice-calc.desktop;ms-office-online.desktop;
application/x-desktop=nvim.desktop;
application/x-extension-htm=google-chrome.desktop;
application/x-extension-html=google-chrome.desktop;
application/x-extension-shtml=google-chrome.desktop;
application/x-extension-xht=google-chrome.desktop;
application/x-extension-xhtml=google-chrome.desktop;
application/x-ms-dos-executable=wine.desktop;
application/x-ms-shortcut=wine.desktop;
application/x-yaml=notepadqq.desktop;nvim.desktop;
application/xhtml+xml=google-chrome.desktop;microsoft-edge-beta.desktop;qutebrowser.desktop;
application/zip=org.gnome.FileRoller.desktop;
audio/aac=mpv.desktop;
audio/mp4=mpv.desktop;
audio/mpeg=mpv.desktop;
audio/mpegurl=mpv.desktop;
audio/ogg=mpv.desktop;
audio/vnd.rn-realaudio=mpv.desktop;
audio/vorbis=mpv.desktop;
audio/x-flac=mpv.desktop;
audio/x-mp3=mpv.desktop;
audio/x-mpegurl=mpv.desktop;
audio/x-ms-wma=mpv.desktop;
audio/x-musepack=mpv.desktop;
audio/x-oggflac=mpv.desktop;
audio/x-pn-realaudio=mpv.desktop;
audio/x-scpls=mpv.desktop;
audio/x-vorbis=mpv.desktop;
audio/x-vorbis+ogg=mpv.desktop;
audio/x-wav=mpv.desktop;
image/avif=okularApplication_kimgio.desktop;
image/bmp=okularApplication_kimgio.desktop;
image/gif=org.gnome.gThumb.desktop;google-chrome.desktop;gimp.desktop;org.kde.gwenview.desktop;okularApplication_kimgio.desktop;
image/heif=okularApplication_kimgio.desktop;
image/jpeg=okularApplication_kimgio.desktop;
image/png=okularApplication_kimgio.desktop;org.gnome.gThumb.desktop;feh.desktop;gimp.desktop;org.kde.gwenview.desktop;
image/webp=okularApplication_kimgio.desktop;
inode/directory=thunar.desktop;
text/csv=libreoffice-calc.desktop;
text/html=google-chrome.desktop;
text/javascript=notepadqq.desktop;
text/plain=notepadqq.desktop;nvim.desktop;vim.desktop;okularApplication_txt.desktop;xed.desktop;
text/vnd.trolltech.linguist=mpv.desktop;
text/x-log=notepadqq.desktop;
video/mp4=mpv.desktop;vlc.desktop;io.github.celluloid_player.Celluloid.desktop;
video/webm=mpv.desktop;vlc.desktop;io.github.celluloid_player.Celluloid.desktop;
video/x-matroska=mpv.desktop;vlc.desktop;
x-scheme-handler/betterdiscord=discord.desktop;
x-scheme-handler/bitwarden=bitwarden.desktop;Bitwarden.desktop;
x-scheme-handler/chrome=google-chrome.desktop;
x-scheme-handler/exodus=Exodus.desktop;
x-scheme-handler/geo=google-maps-geo-handler.desktop;
x-scheme-handler/http=zen.desktop;firefox.desktop;microsoft-edge-beta.desktop;google-chrome.desktop;
x-scheme-handler/https=zen.desktop;firefox.desktop;microsoft-edge-beta.desktop;google-chrome.desktop;
x-scheme-handler/mailspring=Mailspring.desktop;
x-scheme-handler/mailto=org.mozilla.Thunderbird.desktop;Mailspring.desktop;userapp-Thunderbird-6JYZ12.desktop;
x-scheme-handler/mid=userapp-Thunderbird-6JYZ12.desktop;
x-scheme-handler/postman=Postman.desktop;
x-scheme-handler/ror2mm=r2modman.desktop;
x-scheme-handler/ssh=kitty-open.desktop;Termius.desktop;
x-scheme-handler/termius=Termius.desktop;
x-scheme-handler/tg=org.telegram.desktop.desktop;org.telegram.desktop._f79d601e26a782fd149b3ffb098aae9f.desktop;userapp-Kotatogram Desktop-IP6312.desktop;
x-scheme-handler/tonsite=org.telegram.desktop.desktop;
x-scheme-handler/tradingview=tradingview.desktop;TradingView.desktop;
application/x-wine-extension-ini=nvim.desktop;
[Default Applications]
application/x-extension-htm=google-chrome.desktop
application/x-extension-html=google-chrome.desktop
application/x-extension-shtml=google-chrome.desktop
application/x-extension-xht=google-chrome.desktop
application/x-extension-xhtml=google-chrome.desktop
audio/aac=mpv.desktop;
audio/mp4=mpv.desktop;
audio/mpeg=mpv.desktop;
audio/mpegurl=mpv.desktop;
audio/ogg=mpv.desktop;
audio/vnd.rn-realaudio=mpv.desktop;
audio/vorbis=mpv.desktop;
audio/x-flac=mpv.desktop;
audio/x-mp3=mpv.desktop;
audio/x-mpegurl=mpv.desktop;
audio/x-ms-wma=mpv.desktop;
audio/x-musepack=mpv.desktop;
audio/x-oggflac=mpv.desktop;
audio/x-pn-realaudio=mpv.desktop;
audio/x-scpls=mpv.desktop;
audio/x-vorbis=mpv.desktop;
audio/x-vorbis+ogg=mpv.desktop;
audio/x-wav=mpv.desktop;
image/avif=okularApplication_kimgio.desktop;
image/bmp=okularApplication_kimgio.desktop;
image/heif=okularApplication_kimgio.desktop;
image/jpeg=okularApplication_kimgio.desktop;
image/png=okularApplication_kimgio.desktop;
image/webp=okularApplication_kimgio.desktop;
inode/directory=Thunar.desktop
message/rfc822=userapp-Thunderbird-6JYZ12.desktop
text/plain=nvim.desktop;
x-scheme-handler/betterdiscord=discord.desktop
x-scheme-handler/bitwarden=bitwarden.desktop
x-scheme-handler/chrome=chromium.desktop
x-scheme-handler/discord-455712169795780630=discord-455712169795780630.desktop
x-scheme-handler/discord-712465656758665259=discord-712465656758665259.desktop
x-scheme-handler/eclipse+command=_usr_lib_dbeaver_.desktop
x-scheme-handler/exodus=Exodus.desktop
x-scheme-handler/geo=google-maps-geo-handler.desktop;
x-scheme-handler/http=zen.desktop;
x-scheme-handler/https=zen.desktop;
x-scheme-handler/mailspring=Mailspring.desktop
x-scheme-handler/mailto=Mailspring.desktop
x-scheme-handler/mid=userapp-Thunderbird-6JYZ12.desktop
x-scheme-handler/msteams=teams.desktop
x-scheme-handler/postman=Postman.desktop
x-scheme-handler/ror2mm=r2modman.desktop
x-scheme-handler/ssh=kitty-open.desktop
x-scheme-handler/termius=Termius.desktop
x-scheme-handler/tg=org.telegram.desktop.desktop
x-scheme-handler/tonsite=org.telegram.desktop.desktop
x-scheme-handler/tradingview=tradingview.desktop
x-scheme-handler/webcal=google-chrome.desktop
video/webm=mpv.desktop
video/x-matroska=mpv.desktop
video/x-ms-wmv=mpv.desktop
video/quicktime=mpv.desktop
video/x-flv=mpv.desktop
video/dv=mpv.desktop
video/vnd.avi=mpv.desktop
video/x-ogm+ogg=mpv.desktop
video/ogg=mpv.desktop
video/vnd.rn-realvideo=mpv.desktop
video/mp4=mpv.desktop
video/mp2t=mpv.desktop
video/x-flic=mpv.desktop
video/3gpp2=mpv.desktop
video/x-theora+ogg=mpv.desktop
video/mpeg=mpv.desktop
video/vnd.mpegurl=mpv.desktop
video/3gpp=mpv.desktop
application/json=zen.desktop
application/xhtml+xml=zen.desktop
application/x-xpinstall=zen.desktop
application/xml=zen.desktop
application/pdf=zen.desktop
text/html=zen.desktop
text/vnd.trolltech.linguist=zen.desktop
x-scheme-handler/nxm=modorganizer2-nxm-handler.desktop
x-scheme-handler/discord-1361252452329848892=discord-1361252452329848892.desktop

View File

@@ -14,6 +14,7 @@ deck_name=Minecraft
# If you don't have a model for Japanese, get it from
# https://tatsumoto.neocities.org/blog/setting-up-anki.html#import-an-example-mining-deck
model_name=Lapis
# model_name=Kiku
# Field names as they appear in the selected note type.
# If you set `audio_field` or `image_field` empty,

View File

@@ -131,7 +131,7 @@ return {
},
-- }}}
},
strategies = {
interactions = {
chat = {
adapter = "copilot",
-- adapter = "openrouter",
@@ -182,11 +182,21 @@ return {
end,
completion_provider = "cmp",
},
fold_reasoning = true,
show_reasoning = true,
},
inline = {
adapter = "copilot",
-- adapter = "openrouter",
},
cmd = {
adapter = "copilot",
},
background = {
adapter = {
name = "copilot",
},
},
},
display = {
action_palette = {
@@ -232,7 +242,6 @@ return {
-- Options for inline diff provider
inline = {
layout = "buffer", -- float|buffer - Where to display the diff
diff_signs = {
signs = {
text = "", -- Sign text for normal changes
@@ -307,6 +316,31 @@ return {
},
},
},
rules = {
default = {
description = "Collection of common files for all projects",
files = {
".clinerules",
".cursorrules",
".goosehints",
".rules",
".windsurfrules",
".github/copilot-instructions.md",
"AGENT.md",
"AGENTS.md",
{ path = "CLAUDE.md", parser = "claude" },
{ path = "CLAUDE.local.md", parser = "claude" },
{ path = "~/.claude/CLAUDE.md", parser = "claude" },
},
is_preset = true,
},
opts = {
chat = {
enabled = true,
default_rules = "default", -- The rule groups to load
},
},
},
},
init = function()
require("utils.codecompanion.fidget-spinner"):init()

View File

@@ -50,7 +50,7 @@ end
function M:create_progress_handle(request)
local title = " Requesting assistance"
.. " ("
.. request.data.strategy
.. request.data.interaction
.. ") from "
.. request.data.adapter.formatted_name
.. " using "

View File

@@ -43,10 +43,10 @@ end
function M:create_progress_handle(request)
return progress.handle.create({
title = " Requesting assistance (" .. request.data.strategy .. ")",
title = " Requesting assistance (" .. request.data.adapter.model .. ")",
message = "In progress...",
lsp_client = {
name = M:llm_role_title(request.data.adapter),
name = M:llm_role_title(request.data.adapter.name),
},
})
end

View File

@@ -0,0 +1,44 @@
{
"$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json",
"google_auth": true,
"agents": {
"Sisyphus": {
"model": "opencode/glm-4.7-free"
},
"librarian": {
"model": "opencode/glm-4.7-free"
},
"explore": {
"model": "google/antigravity-gemini-3-flash"
},
"frontend-ui-ux-engineer": {
"model": "google/antigravity-gemini-3-pro-high"
},
"document-writer": {
"model": "google/antigravity-gemini-3-flash"
},
"multimodal-looker": {
"model": "google/antigravity-gemini-3-flash"
}
},
"lsp": {
"typescript-language-server": {
"command": ["typescript-language-server", "--stdio"],
"extensions": [".ts", ".tsx"],
"priority": 10
},
"pylsp": {
"disabled": true
},
"pyright": {
"command": ["basedpyright-languageserver", "--stdio"],
"extensions": [".py"],
"priority": 10
},
"bash-language-server": {
"command": ["bash-language-server", "start"],
"extensions": [".sh", ".bash"],
"priority": 10
}
}
}

View File

@@ -0,0 +1,24 @@
{
"sound": false,
"notification": true,
"timeout": 5,
"showProjectName": true,
"events": {
"permission": { "sound": false, "notification": true },
"complete": { "sound": false, "notification": true },
"error": { "sound": false, "notification": true },
"question": { "sound": false, "notification": true }
},
"messages": {
"permission": "Session needs permission",
"complete": "Session has finished",
"error": "Session encountered an error",
"question": "Session has a question"
},
"sounds": {
"permission": "/path/to/custom/sound.wav",
"complete": "/path/to/custom/sound.wav",
"error": "/path/to/custom/sound.wav",
"question": "/path/to/custom/sound.wav"
}
}

View File

@@ -1,49 +0,0 @@
{
"$schema": "https://opencode.ai/config.json",
"theme": "catppuccin",
"model": "github-copilot/gpt-5.1",
"provider": {
"openai": {
"models": {
"gpt-5": {
"options": {
"reasoningEffort": "high",
"textVerbosity": "low",
"reasoningSummary": "auto",
"include": ["reasoning.encrypted_content"],
},
},
},
},
},
"agent": {
"build": {
"mode": "primary",
"model": "github-copilot/gpt-5.1",
"tools": {
"write": true,
"edit": true,
"bash": true
}
},
"plan": {
"mode": "primary",
"model": "github-copilot/gpt-5.1-codex",
"tools": {
"write": false,
"edit": false,
"bash": false
}
},
"code-reviewer": {
"description": "Reviews code for best practices and potential issues",
"mode": "subagent",
"model": "github-copilot/gpt-5.1",
"prompt": "You are a code reviewer. Focus on security, performance, and maintainability.",
"tools": {
"write": false,
"edit": false
}
}
}
}

View File

@@ -0,0 +1,225 @@
{
"$schema": "https://opencode.ai/config.json",
"plugin": [
"opencode-openai-codex-auth",
"opencode-antigravity-auth@beta",
"@mohak34/opencode-notifier@latest",
"oh-my-opencode"
],
"provider": {
"openai": {
"name": "OpenAI",
"options": {
"reasoningEffort": "medium",
"reasoningSummary": "auto",
"textVerbosity": "medium",
"include": [
"reasoning.encrypted_content"
],
"store": false
},
"models": {
"gpt-5.2": {
"name": "GPT 5.2 (OAuth)",
"limit": {
"context": 272000,
"output": 128000
},
"modalities": {
"input": [
"text",
"image"
],
"output": [
"text"
]
},
"variants": {
"none": {
"reasoningEffort": "none",
"reasoningSummary": "auto",
"textVerbosity": "medium"
},
"low": {
"reasoningEffort": "low",
"reasoningSummary": "auto",
"textVerbosity": "medium"
},
"medium": {
"reasoningEffort": "medium",
"reasoningSummary": "auto",
"textVerbosity": "medium"
},
"high": {
"reasoningEffort": "high",
"reasoningSummary": "detailed",
"textVerbosity": "medium"
},
"xhigh": {
"reasoningEffort": "xhigh",
"reasoningSummary": "detailed",
"textVerbosity": "medium"
}
}
},
"gpt-5.2-codex": {
"name": "GPT 5.2 Codex (OAuth)",
"limit": {
"context": 272000,
"output": 128000
},
"modalities": {
"input": [
"text",
"image"
],
"output": [
"text"
]
},
"variants": {
"low": {
"reasoningEffort": "low",
"reasoningSummary": "auto",
"textVerbosity": "medium"
},
"medium": {
"reasoningEffort": "medium",
"reasoningSummary": "auto",
"textVerbosity": "medium"
},
"high": {
"reasoningEffort": "high",
"reasoningSummary": "detailed",
"textVerbosity": "medium"
},
"xhigh": {
"reasoningEffort": "xhigh",
"reasoningSummary": "detailed",
"textVerbosity": "medium"
}
}
},
"gpt-5.1-codex-max": {
"name": "GPT 5.1 Codex Max (OAuth)",
"limit": {
"context": 272000,
"output": 128000
},
"modalities": {
"input": [
"text",
"image"
],
"output": [
"text"
]
},
"variants": {
"low": {
"reasoningEffort": "low",
"reasoningSummary": "detailed",
"textVerbosity": "medium"
},
"medium": {
"reasoningEffort": "medium",
"reasoningSummary": "detailed",
"textVerbosity": "medium"
},
"high": {
"reasoningEffort": "high",
"reasoningSummary": "detailed",
"textVerbosity": "medium"
},
"xhigh": {
"reasoningEffort": "xhigh",
"reasoningSummary": "detailed",
"textVerbosity": "medium"
}
}
}
}
},
"google": {
"name": "Google",
"models": {
"antigravity-gemini-3-pro-high": {
"name": "Gemini 3 Pro High (Antigravity)",
"thinking": true,
"attachment": true,
"limit": {
"context": 1048576,
"output": 65535
},
"modalities": {
"input": [
"text",
"image",
"pdf"
],
"output": [
"text"
]
}
},
"antigravity-gemini-3-pro-low": {
"name": "Gemini 3 Pro Low (Antigravity)",
"thinking": true,
"attachment": true,
"limit": {
"context": 1048576,
"output": 65535
},
"modalities": {
"input": [
"text",
"image",
"pdf"
],
"output": [
"text"
]
}
},
"antigravity-gemini-3-flash": {
"name": "Gemini 3 Flash (Antigravity)",
"attachment": true,
"limit": {
"context": 1048576,
"output": 65536
},
"modalities": {
"input": [
"text",
"image",
"pdf"
],
"output": [
"text"
]
}
}
}
}
},
"theme": "catppuccin-macchiato",
"share": "manual",
"formatter": {
"prettier": {
"disabled": false
},
"ruff": {
"disabled": false
}
},
"instructions": [
"AGENTS.md",
"CONTRIBUTING.md",
"docs/guidelines.md",
".cursor/rules/*.md"
],
"permission": {
"edit": "ask",
"bash": "ask"
}
}

View File

@@ -35,7 +35,7 @@
"Pihole2 - https://pihole2.suda.codes/admin",
"Proxmox - https://thebox.unicorn-ilish.ts.net",
"qBittorrent - https://qbittorrent.suda.codes",
"qui - https://qui.suda.codes",
"qui - http://pve-main:7476",
"Plausible - https://plausible.sudacode.com",
"Paperless - https://paperless.suda.codes",
"Prometheus - http://prometheus:9090",

View File

@@ -0,0 +1,23 @@
#!/usr/bin/env bash
CHOICES=(
"1. Screenshot (Kiku)"
"2. Screenshot (Luna)"
"3. Record Audio"
)
CHOICE=$(printf "%s\n" "${CHOICES[@]}" | rofi -dmenu -i -theme "$HOME/.config/rofi/launchers/type-2/style-2.rasi" -theme-str 'window {width: 25%;} listview {columns: 1; lines: 5;}' -p "Select an option")
case "$CHOICE" in
"1. Screenshot (Kiku)")
PICTURE_FIELD=Picture "$HOME/projects/scripts/screenshot-anki.sh"
;;
"2. Screenshot (Luna)")
PICTURE_FIELD=screenshot "$HOME/projects/scripts/screenshot-anki.sh"
;;
"3. Record Audio")
"$HOME/projects/scripts/record-audio.sh"
;;
*)
exit 1
;;
esac

View File

@@ -1,9 +1,12 @@
#!/usr/bin/env bash
SCRIPT_DIR="$(cd -- "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/rofi-menu-helpers.sh"
BROWSER=/usr/bin/zen-browser
OPTIONS=(
"Arch Linux (btw)"
"Hyprland"
DOC_GROUPS=(
"Arch Linux (btw)|ARCH"
"Hyprland|HYPRLAND"
)
ARCH=(
"Archlinux Wiki|https://wiki.archlinux.org/title/Main_page"
@@ -13,56 +16,37 @@ HYPRLAND=(
"Hyprland Window Rules|https://wiki.hypr.land/Configuring/Window-Rules/"
)
get_url() {
urls=("$@")
display_urls=()
declare -A url_map
for url in "${urls[@]}"; do
display_urls+=("${url%%|*}")
label="${url%%|*}"
url_map["$label"]="${url##*|}"
done
display_urls+=("Back")
url_map["Back"]="Back"
selection="$(printf "%s\n" "${display_urls[@]}" | rofi -theme-str 'window {width: 25%;} listview {columns: 1; lines: 10;}' -theme ~/.config/rofi/launchers/type-2/style-2.rasi -dmenu -l 5 -i -p "Select Documentation")"
url="${url_map[$selection]}"
if [ -z "$url" ]; then
exit 0
fi
printf "%s\n" "$url"
select_group() {
rofi_select_label_value "Select Documentation Group" DOC_GROUPS
}
get_docs_list() {
selection="$(printf "%s\n" "${OPTIONS[@]}" | rofi -theme-str 'window {width: 25%;} listview {columns: 1; lines: 10;}' -theme ~/.config/rofi/launchers/type-2/style-2.rasi -dmenu -l 5 -i -p "Select Documentation Group")"
case "$selection" in
"Arch Linux (btw)")
urls=("${ARCH[@]}")
;;
"Hyprland")
urls=("${HYPRLAND[@]}")
;;
*)
exit 0
;;
esac
printf "%s\n" "${urls[@]}"
select_url() {
local urls_array="$1"
rofi_select_label_value "Select Documentation" "$urls_array" "Back"
}
main() {
mapfile -t urls < <(get_docs_list)
url="$(get_url "${urls[@]}")"
if [ -z "$url" ]; then
printf "No URL selected.\n"
while true; do
group_key="$(select_group)" || exit 0
case "$group_key" in
ARCH)
urls_ref=ARCH
;;
HYPRLAND)
urls_ref=HYPRLAND
;;
*)
exit 0
;;
esac
selection="$(select_url "$urls_ref")" || exit 0
if [[ "$selection" == "Back" ]]; then
continue
fi
$BROWSER "$selection" &>/dev/null &
exit 0
elif [ "$url" == "Back" ]; then
main
exit 0
fi
$BROWSER "$url" &>/dev/null &
done
}
main

View File

@@ -0,0 +1,89 @@
#!/usr/bin/env bash
set -Eeuo pipefail
BASE_DIR="/truenas/jellyfin"
. "$HOME/.config/rofi/scripts/rofi-menu-helpers.sh"
ACTION="xdg-open"
# Theme for icon display
ICON_THEME="$HOME/.config/rofi/launchers/type-3/style-4.rasi"
ICON_THEME_STR='configuration {show-icons: true; icon-size: 128; dpi: 96;} window {width: 50%; height: 60%;} listview {columns: 3; lines: 5;}'
# Map display names to actual directory names
declare -A DIR_MAP=(
["Anime"]="anime"
["Movies"]="movies"
["Manga"]="manga"
["TV"]="tv"
["YouTube"]="youtube"
["Books"]="books"
["Podcasts"]="podcasts"
["Audiobooks"]="audiobooks"
)
DIRS=(
"Anime"
"Movies"
"Manga"
"TV"
"YouTube"
"Books"
"Podcasts"
"Audiobooks"
)
# Select top-level category
CHOICE=$(rofi_select_list "Select a category" DIRS) || exit 1
# Get the actual directory name
ACTUAL_DIR="${DIR_MAP[$CHOICE]}"
TARGET_DIR="$BASE_DIR/$ACTUAL_DIR"
if [[ ! -d "$TARGET_DIR" ]]; then
notify-send -u critical "Jellyfin Browser" "Directory not found: $TARGET_DIR"
exit 1
fi
# Build rofi entries with folder.jpg icons
build_icon_menu() {
local dir="$1"
local entries=""
while IFS= read -r -d '' subdir; do
local name
name="$(basename "$subdir")"
local icon="$subdir/folder.jpg"
# Check for folder.jpg, fallback to folder.png, then no icon
if [[ -f "$icon" ]]; then
entries+="${name}\0icon\x1f${icon}\n"
elif [[ -f "$subdir/folder.png" ]]; then
entries+="${name}\0icon\x1f${subdir}/folder.png\n"
else
entries+="${name}\n"
fi
done < <(find "$dir" -mindepth 1 -maxdepth 1 -type d -print0 | sort -z)
printf "%b" "$entries"
}
# Show subdirectories with icons
SELECTION=$(build_icon_menu "$TARGET_DIR" | rofi -dmenu -i -no-custom \
-theme "$ICON_THEME" \
-theme-str "$ICON_THEME_STR" \
-p "Select from $CHOICE") || exit 1
# Full path to selected item
SELECTED_PATH="$TARGET_DIR/$SELECTION"
if [[ -d "$SELECTED_PATH" ]]; then
# Open in file manager or do something with it
# You can customize this action as needed
$ACTION "$SELECTED_PATH" &>/dev/null &
else
notify-send -u critical "Jellyfin Browser" "Path not found: $SELECTED_PATH"
exit 1
fi

View File

@@ -0,0 +1,12 @@
#!/usr/bin/env bash
PROGRAM="$HOME/S/lutris/wineprefix/drive_c/users/steamuser/luna-translator/LunaTranslator.exe"
SELECTION="$(protontricks -l | tail -n +2 | rofi -dmenu -theme ~/.config/rofi/launchers/type-2/style-2.rasi -theme-str 'listview {lines: 12; columns: 1;}' -i -p "Select game" | awk '{print $NF}' | tr -d '()')"
if [[ -z "$SELECTION" ]]; then
printf "%s\n" "No game selected"
exit 1
fi
printf "%s\n" "Launching $PROGRAM for game ID: $SELECTION"
protontricks-launch --appid "$SELECTION" "$PROGRAM" &>/dev/null &

View File

@@ -0,0 +1,68 @@
#!/usr/bin/env bash
# Lightweight helpers to build rofi menus with label/value pairs.
# Intended to be sourced from other scripts.
# Allow callers to override theme/args without touching code.
: "${ROFI_THEME:=$HOME/.config/rofi/launchers/type-2/style-2.rasi}"
: "${ROFI_THEME_STR:="window {width: 25%;} listview {columns: 1; lines: 10;}"}"
: "${ROFI_DMENU_ARGS:=-i -l 5}"
# rofi_menu prompt option...
# Prints the selected option to stdout and propagates the rofi exit code
# (1 when the user cancels).
rofi_menu() {
local prompt="$1"
shift
local -a options=("$@")
local selection
selection="$(printf "%s\n" "${options[@]}" | rofi -dmenu $ROFI_DMENU_ARGS \
${ROFI_THEME:+-theme "$ROFI_THEME"} \
${ROFI_THEME_STR:+-theme-str "$ROFI_THEME_STR"} \
-p "$prompt")"
local status=$?
[[ $status -ne 0 ]] && return "$status"
printf "%s\n" "$selection"
}
# rofi_select_label_value prompt array_name [back_label]
# array_name should contain entries shaped as "Label|Value".
# Prints the mapped value (or the back label when chosen). Returns 1 on cancel.
rofi_select_label_value() {
local prompt="$1"
local array_name="$2"
local back_label="${3:-}"
# Access caller's array by name
local -n kv_source="$array_name"
local -A kv_map=()
local -a display=()
for entry in "${kv_source[@]}"; do
local label="${entry%%|*}"
local value="${entry#*|}"
kv_map["$label"]="$value"
display+=("$label")
done
if [[ -n "$back_label" ]]; then
kv_map["$back_label"]="$back_label"
display+=("$back_label")
fi
local selection
selection="$(rofi_menu "$prompt" "${display[@]}")" || return "$?"
[[ -z "$selection" ]] && return 1
printf "%s\n" "${kv_map[$selection]}"
}
# rofi_select_list prompt array_name
# Convenience wrapper for plain lists (no label/value mapping).
rofi_select_list() {
local prompt="$1"
local array_name="$2"
local -n list_source="$array_name"
rofi_menu "$prompt" "${list_source[@]}"
}

View File

@@ -6,8 +6,6 @@ THEME="$HOME/.config/rofi/launchers/type-3/style-4.rasi"
DIR="$HOME/Pictures/wallpapers/favorites"
SELECTED_WALL=$(cd "$DIR" && for a in *.jpg *.png; do echo -en "$a\0icon\x1f$a\n"; done | rofi -dmenu -i -no-custom -theme "$THEME" -p "Select a wallpaper" -theme-str 'configuration {icon-size: 128; dpi: 96;} window {width: 45%; height: 45%;}')
PTH="$(printf "%s" "$DIR/$SELECTED_WALL" | tr -s '/')"
hyprctl hyprpaper wallpaper "DP-1, $PTH"
notify-send -a "rofi-wallpaper" "Wallpaper set to" -i "$PTH" "$PTH"
hyprctl hyprpaper preload "$PTH"
hyprctl hyprpaper wallpaper "DP-1,$PTH"
hyprctl hyprpaper unload "$(cat "$HOME/.wallpaper")"
echo "$PTH" >"$HOME/.wallpaper"

View File

@@ -15,6 +15,7 @@ export QT_QPA_PLATFORMTHEME=qt5ct
export GTK_THEME=Dracula
export XDG_CONFIG_HOME=$HOME/.config
export COMPOSE_BAKE=true
export ANKI_WAYLAND=1
# nvidia
export NVD_BACKEND=direct

View File

@@ -28,6 +28,7 @@ setopt HIST_IGNORE_SPACE # dont record commands that start with a space
FPATH="$HOME/.docker/completions:$FPATH"
[[ "$TERM_PROGRAM" == "vscode" ]] && . "$(code --locate-shell-integration-path zsh)"
eval "$(fnm env --use-on-cd --shell zsh)"
bindkey -v
# zstyle ':completion:*' matcher-list 'm:{a-zA-Z}={A-Za-z}'

View File

@@ -1,15 +1,9 @@
#!/bin/bash
set -Eeuo pipefail
# RES="$(slurp | grim -g - - | gazou | sed '1d;$d')"
# # Truncate RES for display if it's longer than 100 characters
# DISPLAY_RES="${RES:0:100}"
# if [ ${#RES} -gt 100 ]; then
# DISPLAY_RES="${DISPLAY_RES}..."
# fi
# notify-send "GAZOU" "Text: $DISPLAY_RES"
# echo "$RES" | wl-copy
slurp | grim -g - /tmp/ocr.png || exit 1
transformers_ocr recognize --image-path /tmp/ocr.png || exit 1
notify-send "tramsformers_ocr" "Text: $DISPLAY_RES"
if ! pgrep -af owocr; then
notify-send "ocr.sh" "Starting owocr daemon..."
owocr -e meikiocr -r clipboard -w clipboard -l ja -n &>/dev/null &
fi
slurp | grim -g - - | wl-copy
notify-send "ocr.sh" "Text: $DISPLAY_RES"

View File

@@ -0,0 +1,300 @@
#!/usr/bin/env python3
"""
Japanese Learning Assistant using OpenRouter API
Uses Google Gemini Flash 2.0 for AJATT-aligned Japanese analysis
"""
import os
import subprocess
import sys
import requests
# Configuration
OPENROUTER_API_KEY = os.environ.get("OPENROUTER_API_KEY", "")
MODEL = os.environ.get("OPENROUTER_MODEL", "google/gemini-2.0-flash-001")
API_URL = "https://openrouter.ai/api/v1/chat/completions"
# Try to load API key from file if not in environment
if not OPENROUTER_API_KEY:
key_file = os.path.expanduser("~/.openrouterapikey")
if os.path.isfile(key_file):
with open(key_file, "r") as f:
OPENROUTER_API_KEY = f.read().strip()
TRANSLATION_PROMPT = """You are my Japanese-learning assistant. Help me acquire Japanese through deep, AJATT-aligned analysis.
For every input, output exactly using clean plain text formatting:
═══════════════════════════════════════
1. JAPANESE INPUT
═══════════════════════════════════════
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. BREAKDOWN
═══════════════════════════════════════
For each token, provide a breakdown of the vocabulary, grammar, and nuance.
▸ 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)
• Use the section dividers shown above (═══) for major sections
• Use ▸ for sub-items and • for bullet points
• Put Japanese terms in 「brackets」
• 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."""
GRAMMAR_PROMPT = """
You are a Japanese grammar analysis assistant.
The user will provide Japanese sentences. Your task is to explain the **grammar and structure in English**, prioritizing how the sentence is constructed rather than translating word-for-word.
Rules:
* Always show the original Japanese first.
* Provide a short, natural English gloss only if helpful.
* Explain grammar patterns, verb forms, particles, and omissions.
* Emphasize nuance, implication, and speaker intent.
* Avoid unnecessary vocabulary definitions unless they affect the grammar.
* Assume the user is actively studying Japanese and wants deep understanding.
Your explanations should help the user internalize patterns, not memorize translations.
"""
def show_error(message: str) -> None:
"""Display an error dialog using zenity."""
subprocess.run(
["zenity", "--error", "--text", message, "--title", "Error"],
stderr=subprocess.DEVNULL,
)
def get_clipboard() -> str:
"""Get clipboard contents using wl-paste or xclip."""
# Try wl-paste first (Wayland)
result = subprocess.run(
["wl-paste", "--no-newline"],
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
text=True,
)
if result.returncode == 0:
return result.stdout.strip()
# Fall back to xclip (X11)
result = subprocess.run(
["xclip", "-selection", "clipboard", "-o"],
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
text=True,
)
if result.returncode == 0:
return result.stdout.strip()
return ""
def get_input() -> str | None:
"""Get input from user via zenity entry dialog, pre-populated with clipboard."""
clipboard = get_clipboard()
cmd = [
"zenity",
"--entry",
"--title",
"Japanese Assistant",
"--text",
"Enter Japanese text to analyze (press Enter to send):",
"--width",
"600",
]
if clipboard:
cmd.extend(["--entry-text", clipboard])
result = subprocess.run(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
text=True,
)
if result.returncode != 0:
return None
return result.stdout.strip()
def get_mode() -> str | None:
"""Ask user to choose between translation and grammar assistant."""
result = subprocess.run(
[
"zenity",
"--list",
"--title",
"Japanese Assistant",
"--text",
"Choose analysis mode:",
"--column=Mode",
"Translation (full analysis)",
"Grammar (structure focus)",
],
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
text=True,
)
if result.returncode != 0:
return None
return result.stdout.strip()
def show_notification(message: str, body: str) -> subprocess.Popen:
"""Show a notification using notify-send."""
return subprocess.Popen(
["notify-send", "-t", "0", "-a", "Japanese Assistant", message, body],
stderr=subprocess.DEVNULL,
)
def close_notification() -> None:
"""Close the processing notification."""
subprocess.run(
["pkill", "-f", "notify-send.*Processing.*Analyzing"],
stderr=subprocess.DEVNULL,
)
def display_result(content: str) -> None:
"""Display the result in a zenity text-info dialog."""
subprocess.run(
[
"zenity",
"--text-info",
"--title",
"Japanese Analysis",
"--width",
"900",
"--height",
"700",
"--font",
"monospace 11",
],
input=content,
text=True,
stderr=subprocess.DEVNULL,
)
def make_api_request(user_input: str, system_prompt: str) -> dict:
"""Make the API request to OpenRouter."""
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {OPENROUTER_API_KEY}",
"HTTP-Referer": "https://github.com/sudacode/scripts",
"X-Title": "Japanese Learning Assistant",
}
payload = {
"model": MODEL,
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_input},
],
"temperature": 0.7,
}
response = requests.post(API_URL, headers=headers, json=payload, timeout=60)
return response.json()
def main() -> int:
# Check for API key
if not OPENROUTER_API_KEY:
show_error("OPENROUTER_API_KEY environment variable is not set.")
return 1
# Ask user for mode
mode = get_mode()
if not mode:
return 0
# Select appropriate prompt
if "Grammar" in mode:
system_prompt = GRAMMAR_PROMPT
else:
system_prompt = TRANSLATION_PROMPT
# Get input from user
user_input = get_input()
if not user_input:
return 0
# Show loading notification
show_notification("Processing...", f"Analyzing: {user_input[:50]}...")
try:
# Make API request
response = make_api_request(user_input, system_prompt)
# Close loading notification
close_notification()
# Check for errors in response
if "error" in response:
error_msg = response["error"].get("message", "Unknown error")
show_error(error_msg)
return 1
# Extract content from response
try:
content = response["choices"][0]["message"]["content"]
except (KeyError, IndexError):
show_error("Failed to parse API response")
return 1
if not content:
show_error("Empty response from API")
return 1
# Display result
display_result(content)
except requests.RequestException as e:
close_notification()
show_error(f"API request failed: {e}")
return 1
except Exception as e:
close_notification()
show_error(f"Error: {e}")
return 1
return 0
if __name__ == "__main__":
sys.exit(main())

226
projects/scripts/record-audio.sh Normal file → Executable file
View File

@@ -1,93 +1,145 @@
#!/bin/sh
#!/usr/bin/env bash
# Version 1.2
# shoutout to https://gist.github.com/Cephian/f849e326e3522be9a4386b60b85f2f23 for the original script,
# https://github.com/xythh/ added the ankiConnect functionality
# toggle record computer audio (run once to start, run again to stop)
# dependencies: ffmpeg, pulseaudio, curl
# Toggle desktop audio recording and attach the result to the newest Anki note
# (as tagged by Yomichan). Run once to start recording, run again to stop.
# Dependencies: jq, curl, ffmpeg/ffprobe, pulseaudio (parec+pactl), bc, notify-send
# where recording gets saved, gets deleted after being imported to anki
DIRECTORY="$HOME/.cache/"
FORMAT="mp3" # ogg or mp3
# cut file since it glitches a bit at the end sometimes
CUT_DURATION="0.1"
#port used by ankiconnect
ankiConnectPort="8765"
# gets the newest created card, so make sure to create the card first with yomichan
newestNoteId=$(curl -s localhost:$ankiConnectPort -X POST -d '{"action": "findNotes", "version": 6, "params": { "query": "is:new"}}' | jq '.result[-1]')
#Audio field name
audioFieldName="SentenceAudio"
set -euo pipefail
#if there is no newest note, you either have a complete empty anki or ankiconnect isn't running
if [ "$newestNoteId" = "" ]; then
notify-send "anki connect not found"
exit 1
fi
ANKI_CONNECT_PORT="${ANKI_CONNECT_PORT:-8765}"
AUDIO_FIELD_NAME="${AUDIO_FIELD_NAME:-SentenceAudio}"
FORMAT="${FORMAT:-mp3}" # mp3 or ogg
CUT_DURATION="${CUT_DURATION:-0.1}"
CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/record-audio"
RECORD_TIMEOUT="${RECORD_TIMEOUT:-60}"
ANKI_URL="http://localhost:${ANKI_CONNECT_PORT}"
if pgrep -f "parec"; then
pkill -f "parec"
else
time=$(date +%s)
name="$DIRECTORY/$time"
wav_file="$name.wav"
out_file="$name.$FORMAT"
if ! [ -d "$DIRECTORY" ]; then
mkdir "$DIRECTORY"
fi
notify-send -t 1000 "Audio recording started"
#timeout 1m arecord -t wav -f cd "$wav_file"
# just grabs last running source... may not always work if your pulseaudio setup is complicated
if ! timeout 1m parec -d"$(pactl list sinks | grep -B1 'State: RUNNING' | sed -nE 's/Sink #(.*)/\1/p' | tail -n 1)" --file-format=wav "$wav_file"; then
notify-send "Error recording " "most likely no audio playing"
rm "$wav_file"
exit 1
fi
input_duration=$(ffprobe -v error -select_streams a:0 -show_entries stream=duration -of default=noprint_wrappers=1:nokey=1 "$wav_file")
output_duration=$(echo "$input_duration"-"$CUT_DURATION" | bc)
# encode file and delete OG
if [ $FORMAT = "ogg" ]; then
ffmpeg -i "$wav_file" -vn -codec:a libvorbis -b:a 64k -t "$output_duration" "$out_file"
elif [ $FORMAT = "mp3" ]; then
ffmpeg -i "$wav_file" -vn -codec:a libmp3lame -qscale:a 1 -t "$output_duration" "$out_file"
else
notify-send "Record Error" "Unknown format $FORMAT"
fi
rm "$wav_file"
# Update newest note with recorded audio
curl -s localhost:$ankiConnectPort -X POST -d '{
"action": "updateNoteFields",
"version": 6,
"params": {
"note": {
"id": '"$newestNoteId"',
"fields": {
"'$audioFieldName'": ""
},
"audio": [{
"path": "'"$out_file"'",
"filename": "'"$time"'.'$FORMAT'",
"fields": [
"'$audioFieldName'"
]
}]
require_cmd() {
command -v "$1" >/dev/null 2>&1 || {
echo "Missing dependency: $1" >&2
exit 1
}
}
}'
# opens changed note, comment if you don't want it.
curl -s localhost:$ankiConnectPort -X POST -d '{
"action": "guiBrowse",
"version": 6,
"params": {
"query": "nid:'"$newestNoteId"'"
}
}'
notify-send -t 1000 "Audio recording copied"
rm "$out_file"
fi
notify() {
# Best-effort notification; keep script running if notify-send is missing.
if command -v notify-send >/dev/null 2>&1; then
notify-send -t 1000 "$@"
fi
}
get_active_sink() {
pactl list sinks short 2>/dev/null | awk '$6=="RUNNING"{print $1; exit 0}'
}
get_newest_note_id() {
local response
response=$(curl -sS "$ANKI_URL" -X POST -H 'Content-Type: application/json' \
-d '{"action":"findNotes","version":6,"params":{"query":"is:new"}}')
jq -r '.result[-1] // empty' <<<"$response"
}
update_anki_note() {
local note_id="$1" audio_path="$2" filename="$3"
local payload
payload=$(jq -n --argjson noteId "$note_id" --arg field "$AUDIO_FIELD_NAME" \
--arg path "$audio_path" --arg filename "$filename" '
{action:"updateNoteFields",version:6,
params:{note:{id:$noteId,fields:{($field):""},
audio:[{path:$path,filename:$filename,fields:[$field]}]}}}')
curl -sS "$ANKI_URL" -X POST -H 'Content-Type: application/json' -d "$payload" >/dev/null
}
open_note_in_browser() {
local note_id="$1"
local payload
payload=$(jq -n --argjson noteId "$note_id" '
{action:"guiBrowse",version:6,params:{query:("nid:" + ($noteId|tostring))}}')
curl -sS "$ANKI_URL" -X POST -H 'Content-Type: application/json' -d "$payload" >/dev/null
}
record_audio() {
local note_id="$1"
local sink
sink=$(get_active_sink) || true
if [[ -z "$sink" ]]; then
notify "Record Error" "No running PulseAudio sink found"
exit 1
fi
mkdir -p "$CACHE_DIR"
local timestamp wav_file out_file
timestamp=$(date +%s)
wav_file="$CACHE_DIR/$timestamp.wav"
out_file="$CACHE_DIR/$timestamp.$FORMAT"
notify "Audio recording started"
if ! timeout "$RECORD_TIMEOUT" parec -d"$sink" --file-format=wav "$wav_file"; then
notify "Record Error" "No audio captured (timeout or sink issue)"
rm -f "$wav_file"
exit 1
fi
local input_duration output_duration
input_duration=$(ffprobe -v error -select_streams a:0 \
-show_entries stream=duration -of default=noprint_wrappers=1:nokey=1 "$wav_file")
output_duration=$(echo "$input_duration - $CUT_DURATION" | bc -l)
# Guard against negative durations
if [[ $(echo "$output_duration < 0" | bc -l) -eq 1 ]]; then
output_duration="0"
fi
case "$FORMAT" in
ogg)
ffmpeg -nostdin -y -i "$wav_file" -vn -codec:a libvorbis -b:a 64k \
-t "$output_duration" "$out_file"
;;
mp3)
ffmpeg -nostdin -y -i "$wav_file" -vn -codec:a libmp3lame -qscale:a 1 \
-t "$output_duration" "$out_file"
;;
*)
notify "Record Error" "Unknown format: $FORMAT"
rm -f "$wav_file"
exit 1
;;
esac
rm -f "$wav_file"
update_anki_note "$note_id" "$out_file" "$timestamp.$FORMAT"
open_note_in_browser "$note_id"
notify "Audio recording copied"
rm -f "$out_file"
}
main() {
for cmd in curl jq ffmpeg ffprobe parec pactl bc; do
require_cmd "$cmd"
done
if pgrep -x parec >/dev/null 2>&1; then
pkill -x parec
notify "Audio recording stopped"
exit 0
fi
local newest_note
newest_note=$(get_newest_note_id)
if [[ -z "$newest_note" ]]; then
notify "Anki Connect" "No new notes found or AnkiConnect unavailable"
exit 1
fi
record_audio "$newest_note"
}
main "$@"

261
projects/scripts/screenshot-anki.sh Normal file → Executable file
View File

@@ -1,75 +1,202 @@
#!/bin/sh
#!/usr/bin/env bash
# Version 1.2
# click and drag to screenshot dragged portion
# click on specific window to screenshot window area
# dependencies: imagemagick, xclip,curl maybe xdotool (see comment below)
# shoutout to https://gist.github.com/Cephian/f849e326e3522be9a4386b60b85f2f23 for the original script,
# https://github.com/xythh/ added the ankiConnect functionality
# if anki is running the image is added to your latest note as a jpg, if anki is not running it's added to your clipboard as a png
time=$(date +%s)
tmp_file="$HOME/.cache/$time"
ankiConnectPort="8765"
pictureField="Picture"
quality="90"
# Capture a region with slurp+grim. If AnkiConnect is available, attach the
# JPEG to the newest note; otherwise copy a PNG to the clipboard.
# This gets your notes marked as new and returns the newest one.
newestNoteId=$(curl -s localhost:$ankiConnectPort -X POST -d '{"action": "findNotes", "version": 6, "params": { "query": "is:new"}}' | jq '.result[-1]')
set -euo pipefail
# you can remove these two lines if you don't have software which
# makes your mouse disappear when you use the keyboard (e.g. xbanish, unclutter)
# https://github.com/ImageMagick/ImageMagick/issues/1745#issuecomment-777747494
xdotool mousemove_relative 1 1
xdotool mousemove_relative -- -1 -1
ANKI_CONNECT_PORT="${ANKI_CONNECT_PORT:-8765}"
PICTURE_FIELD="${PICTURE_FIELD:-Picture}"
QUALITY="${QUALITY:-90}"
CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/screenshot-anki"
ANKI_URL="http://localhost:${ANKI_CONNECT_PORT}"
HOSTNAME_SHORT="$(hostname -s 2>/dev/null || hostname)"
REQUIREMENTS=(slurp grim wl-copy xdotool curl jq rofi)
ROFI_THEME_STR='listview {columns: 2; lines: 3;} window {width: 45%;}'
ROFI_THEME="$HOME/.config/rofi/launchers/type-2/style-2.rasi"
CAPTURE_MODE=""
DECK_NAME=""
AUTO_MODE=false
# if anki connect is running it will return your latest note id, and the following code will run, if anki connect is not running nothing is return.
if [ "$newestNoteId" != "" ]; then
if ! import -quality $quality "$tmp_file.jpg"; then
# most likley reason this returns a error, is for fullscreen applications that take full control which does not allowing imagemagick to select the area, use windowed fullscreen or if running wine use a virtual desktop to avoid this.
notify-send "Error screenshoting " "most likely unable to find selection"
exit 1
fi
parse_opts() {
while getopts "cd:" opt; do
case "$opt" in
c)
CAPTURE_MODE="window"
AUTO_MODE=true
;;
d)
DECK_NAME="$OPTARG"
;;
*)
echo "Usage: $0 [-c] [-n DECK_NAME]" >&2
echo " -c: Capture current window" >&2
echo " -n: Specify note name (e.g., Kiku)" >&2
exit 1
;;
esac
done
}
curl -s localhost:$ankiConnectPort -X POST -d '{
"action": "updateNoteFields",
"version": 6,
"params": {
"note": {
"id": '"$newestNoteId"',
"fields": {
"'$pictureField'": ""
},
"picture": [{
"path": "'"$tmp_file"'.jpg",
"filename": "paste-'"$time"'.jpg",
"fields": [
"'$pictureField'"
]
}]
}
notify() {
if command -v notify-send >/dev/null 2>&1; then
notify-send "$@"
fi
}
require_cmd() {
command -v "$1" >/dev/null 2>&1 || {
notify "Missing dependency" "$1 is required"
exit 1
}
}'
}
#remove if you don't want anki to show you the card you just edited
curl -s localhost:$ankiConnectPort -X POST -d '{
"action": "guiBrowse",
"version": 6,
"params": {
"query": "nid:'"$newestNoteId"'"
}
}'
wiggle_mouse() {
# Avoid disappearing cursor on some compositors
xdotool mousemove_relative 1 1
xdotool mousemove_relative -- -1 -1
}
#you can comment this if you do not use notifcations.
notify-send "Screenshot Taken" "Added to note"
rm "$tmp_file.jpg"
else
if ! import -quality $quality "$tmp_file.png"; then
notify-send "Error screenshoting " "most likely unable to find selection"
exit 1
fi
# we use pngs when copying to clipboard because they have greater support when pasting.
xclip -selection clipboard -target image/png -i "$tmp_file.png"
rm "$tmp_file.png"
#you can comment this if you do not use notifcations.
notify-send "Screenshot Taken" "Copied to clipboard"
fi
drain_enter_key() {
# Release lingering Enter press from launching via rofi so it
# doesn't reach the next focused window (e.g., a game).
xdotool keyup Return 2>/dev/null || true
xdotool keyup KP_Enter 2>/dev/null || true
}
capture_region() {
local fmt="$1" quality="$2" output="$3"
local geometry
geometry=$(slurp)
if [[ -z "$geometry" ]]; then
notify "Screenshot cancelled" "No region selected"
exit 1
fi
if [[ "$fmt" == "jpeg" ]]; then
grim -g "$geometry" -t jpeg -q "$quality" "$output"
else
grim -g "$geometry" -t png "$output"
fi
}
capture_current_window() {
local fmt="$1" quality="$2" output="$3" geometry
if [[ "$fmt" == "jpeg" ]]; then
grim -w "$(hyprctl activewindow -j | jq -r '.address')" -t jpeg -q "$quality" "$output"
else
grim -w "$(hyprctl activewindow -j | jq -r '.address')" -t png "$output"
fi
}
choose_capture_mode() {
local selection
selection=$(printf "%s\n%s\n" "Region (slurp)" "Current window (Hyprland)" |
rofi -dmenu -i \
-p "Capture mode" \
-mesg "Select capture target" \
-no-custom \
-no-lazy-grab \
-location 0 -yoffset 30 -xoffset 30 \
-theme "$ROFI_THEME" \
-theme-str "$ROFI_THEME_STR" \
-window-title "screenshot-anki")
if [[ -z "$selection" ]]; then
notify "Screenshot cancelled" "No capture mode selected"
exit 0
fi
if [[ "$selection" == "Current window (Hyprland)" ]]; then
CAPTURE_MODE="window"
else
CAPTURE_MODE="region"
fi
}
copy_to_clipboard() {
local file="$1"
if ! wl-copy <"$file"; then
notify "Error copying screenshot" "wl-copy failed"
exit 1
fi
}
get_newest_note_id() {
local response query="is:new"
if [[ -n "$DECK_NAME" ]]; then
query="is:new deck:$DECK_NAME"
fi
response=$(curl -sS "$ANKI_URL" -X POST -H 'Content-Type: application/json' \
-d "{\"action\":\"findNotes\",\"version\":6,\"params\":{\"query\":\"$query\"}}")
jq -r '.result[-1] // empty' <<<"$response"
}
update_note_with_image() {
local note_id="$1" image_path="$2" filename="$3"
local payload
payload=$(jq -n --argjson noteId "$note_id" --arg field "$PICTURE_FIELD" \
--arg path "$image_path" --arg filename "$filename" '
{action:"updateNoteFields",version:6,
params:{note:{id:$noteId,fields:{($field):""},
picture:[{path:$path,filename:$filename,fields:[$field]}]}}}')
curl -sS "$ANKI_URL" -X POST -H 'Content-Type: application/json' -d "$payload" >/dev/null
}
open_note_in_browser() {
local note_id="$1"
local payload
payload=$(jq -n --argjson noteId "$note_id" '
{action:"guiBrowse",version:6,params:{query:("nid:" + ($noteId|tostring))}}')
curl -sS "$ANKI_URL" -X POST -H 'Content-Type: application/json' -d "$payload" >/dev/null
}
main() {
parse_opts "$@"
local requirements=("${REQUIREMENTS[@]}")
for cmd in "${requirements[@]}"; do
require_cmd "$cmd"
done
mkdir -p "$CACHE_DIR"
local timestamp base newest_note image_path
timestamp=$(date +%s)
base="$CACHE_DIR/$timestamp"
drain_enter_key
# Only show interactive menu if not in auto mode
if [[ "$AUTO_MODE" == false ]]; then
choose_capture_mode
fi
if [[ "$CAPTURE_MODE" == "window" ]]; then
require_cmd hyprctl
fi
wiggle_mouse
newest_note=$(get_newest_note_id)
local capture_fn="capture_region"
if [[ "$CAPTURE_MODE" == "window" ]]; then
capture_fn="capture_current_window"
fi
if [[ -n "$newest_note" ]]; then
image_path="$base.jpg"
"$capture_fn" "jpeg" "$QUALITY" "$image_path"
update_note_with_image "$newest_note" "$image_path" "paste-$timestamp.jpg"
open_note_in_browser "$newest_note"
notify -i "$image_path" "Screenshot Taken" "Added to Anki note"
rm -f "$image_path"
else
image_path="$base.png"
"$capture_fn" "png" "" "$image_path"
copy_to_clipboard "$image_path"
notify -i "$image_path" "Screenshot Taken" "Copied to clipboard"
rm -f "$image_path"
fi
}
main "$@"

View File

@@ -10,99 +10,99 @@ HYPRLAND_REGEX='.at[0],(.at[1]) .size[0]x(.size[1])'
REQUIREMENTS=(grim slurp rofi zenity wl-copy)
USE_NOTIFICATIONS=1
CHOICES=(
"1. Select a region and save - slurp | grim -g - \"$TMP_SCREENSHOT\""
"2. Select a region and copy to clipboard - slurp | grim -g - - | wl-copy"
"3. Whole screen - grim \"$TMP_SCREENSHOT\""
"4. Current window - hyprctl -j activewindow | jq -r \"${HYPRLAND_REGEX}\" | grim -g - \"$TMP_SCREENSHOT\""
"5. Edit - slurp | grim -g - - | swappy -f -"
"6. Quit - exit 0"
"1. Select a region and save - slurp | grim -g - \"$TMP_SCREENSHOT\""
"2. Select a region and copy to clipboard - slurp | grim -g - - | wl-copy"
"3. Whole screen - grim \"$TMP_SCREENSHOT\""
"4. Current window - hyprctl -j activewindow | jq -r \"${HYPRLAND_REGEX}\" | grim -g - \"$TMP_SCREENSHOT\""
"5. Edit - slurp | grim -g - - | swappy -f -"
"6. Quit - exit 0"
)
notify() {
local body="$1"
local title="$2"
if [[ -z "$body" ]]; then
echo "notify: No message provided"
return 1
fi
if [[ -z "$title" ]]; then
title="$SCRIPT_NAME"
fi
local body="$1"
local title="$2"
if [[ -z "$body" ]]; then
echo "notify: No message provided"
return 1
fi
if [[ -z "$title" ]]; then
title="$SCRIPT_NAME"
fi
if ((USE_NOTIFICATIONS)); then
notify-send "$title" "$body"
else
printf "%s\n%s\n" "$title" "$body"
fi
return 0
if ((USE_NOTIFICATIONS)); then
notify-send "$title" "$body"
else
printf "%s\n%s\n" "$title" "$body"
fi
return 0
}
check_deps() {
for cmd in "${REQUIREMENTS[@]}"; do
if ! command -v "$cmd" &> /dev/null; then
echo "Error: $cmd is not installed. Please install it first."
exit 1
fi
done
for cmd in "${REQUIREMENTS[@]}"; do
if ! command -v "$cmd" &>/dev/null; then
echo "Error: $cmd is not installed. Please install it first."
exit 1
fi
done
}
main() {
CHOICE="$(rofi -dmenu -i -p "Enter option or select from the list" \
-mesg "Select a Screenshot Option" \
-a 0 -no-custom -location 0 \
-yoffset 30 -xoffset 30 \
-theme-str 'listview {columns: 2; lines: 3;} window {width: 45%;}' \
-window-title "$SCRIPT_NAME" \
-format 'i' \
<<< "$(printf "%s\n" "${CHOICES[@]%% - *}")")"
CHOICE="$(rofi -dmenu -i -p "Enter option or select from the list" \
-mesg "Select a Screenshot Option" \
-a 0 -no-custom -location 0 \
-yoffset 30 -xoffset 30 \
-theme-str 'listview {columns: 2; lines: 3;} window {width: 45%;}' \
-window-title "$SCRIPT_NAME" \
-format 'i' \
<<<"$(printf "%s\n" "${CHOICES[@]%% - *}")")"
if [[ -z "$CHOICE" ]]; then
notify "No option selected." ""
exit 0
fi
if [[ -z "$CHOICE" ]]; then
notify "No option selected." ""
exit 0
fi
sleep 0.2 # give time for the rofi window to close
CMD="${CHOICES[$CHOICE]#* -}"
if [[ -z "$CMD" ]]; then
notify "No option selected." ""
exit 0
fi
sleep 0.2 # give time for the rofi window to close
CMD="${CHOICES[$CHOICE]#* -}"
if [[ -z "$CMD" ]]; then
notify "No option selected." ""
exit 0
fi
# For option 2 (copy to clipboard), handle differently
if [[ "$CHOICE" == "1" ]]; then
if eval "$CMD"; then
notify "Screenshot copied to clipboard"
exit 0
else
notify "An error occurred while taking the screenshot."
exit 1
fi
fi
# For option 2 (copy to clipboard), handle differently
if [[ "$CHOICE" == "1" ]]; then
if eval "$CMD"; then
notify "Screenshot copied to clipboard"
exit 0
else
notify "An error occurred while taking the screenshot."
exit 1
fi
fi
if ! eval "$CMD"; then
notify "An error occurred while taking the screenshot."
exit 1
fi
if ! eval "$CMD"; then
notify "An error occurred while taking the screenshot."
exit 1
fi
notify "screenshot.sh" "Screenshot saved temporarily.\nChoose where to save it permanently"
notify "screenshot.sh" "Screenshot saved temporarily.\nChoose where to save it permanently"
FILE=$(zenity --file-selection --title="Save Screenshot" --filename="$DEFAULT_FILENAME" --save 2> /dev/null)
case "$?" in
0)
if mv "$TMP_SCREENSHOT" "$FILE"; then
notify "Screenshot saved to $FILE"
else
notify "Failed to save screenshot to $FILE"
fi
;;
1)
rm -f "$TMP_SCREENSHOT"
notify "Screenshot discarded"
;;
-1)
notify "An unexpected error has occurred."
;;
esac
FILE=$(zenity --file-selection --title="Save Screenshot" --filename="$DEFAULT_FILENAME" --save 2>/dev/null)
case "$?" in
0)
if mv "$TMP_SCREENSHOT" "$FILE"; then
notify "Screenshot saved to $FILE"
else
notify "Failed to save screenshot to $FILE"
fi
;;
1)
rm -f "$TMP_SCREENSHOT"
notify "Screenshot discarded"
;;
-1)
notify "An unexpected error has occurred."
;;
esac
}
check_deps

10
projects/scripts/songinfo.sh Executable file
View File

@@ -0,0 +1,10 @@
#!/bin/sh
music_dir="/jellyfin/music"
previewdir="$XDG_CONFIG_HOME/ncmpcpp/previews"
filename="$(mpc --format "$music_dir"/%file% current)"
previewname="$previewdir/$(mpc --format %album% current | base64).png"
[ -e "$previewname" ] || ffmpeg -y -i "$filename" -an -vf scale=128:128 "$previewname" > /dev/null 2>&1
notify-send -a "ncmpcpp" "Now Playing" "$(mpc --format '%title% \n%artist% - %album%' current)" -i "$previewname"