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 ## Colorls
alias ls='eza -M --group-directories-first --icons --color=always --group --git' alias ls='eza -M --group-directories-first --icons --color=always --group --git'
alias ll='ls -l' alias ll='ls -lh'
alias la='ls -la' alias la='ls -lah'
alias vimf='vim $(fzf --height=45% --layout=reverse --preview="bat --style=numbers --color=always --line-range :500 {}")' 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 = +calt
font-feature = +liga font-feature = +liga
font-feature = +dlig font-feature = +dlig
theme = catppuccin-macchiato theme = Catppuccin Macchiato
cursor-style = block cursor-style = block
window-padding-x = 10 window-padding-x = 10
window-padding-y = 10 window-padding-y = 10
@@ -14,7 +14,12 @@ window-width = 180
confirm-close-surface = false confirm-close-surface = false
copy-on-select = clipboard copy-on-select = clipboard
app-notifications = no-clipboard-copy 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+enter=unbind
keybind = all:ctrl+shift+j=next_tab keybind = all:ctrl+shift+j=next_tab
keybind = all:ctrl+shift+k=last_tab keybind = all:ctrl+shift+k=last_tab
keybind = all:ctrl+grave_accent=toggle_quick_terminal 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 keybind = all:ctrl+grave_accent=toggle_quick_terminal
shell-integration = zsh shell-integration = zsh
keybind = shift+enter=text:\x1b\r 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/ # See https://wiki.hyprland.org/Configuring/Keywords/
# Set programs that you use # Set programs that you use
$terminal = uwsm app -- ghostty $terminal = uwsm app -- ghostty +new-window
$fileManager = uwsm app -- thunar $fileManager = uwsm app -- thunar
$menu = rofi -show drun -run-command "uwsm app -- {cmd}" $menu = rofi -show drun -run-command "uwsm app -- {cmd}"
# $notification_daemon = dunst # $notification_daemon = dunst

View File

@@ -93,8 +93,12 @@ bindl = , XF86AudioPrev, exec, mpc prev
# rofi # rofi
bind = $mainMod SHIFT, v, exec, uwsm app -sb -- rofi-rbw 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, 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, w, exec, "$HOME/.config/rofi/scripts/rofi-wallpaper.sh"
bind = $mainMod SHIFT, d, exec, $HOME/.config/rofi/scripts/rofi-docs.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 # ncmcppp
bind = $mainMod, n, exec, uwsm app -sb -- ghostty --command=/usr/bin/ncmpcpp bind = $mainMod, n, exec, uwsm app -sb -- ghostty --command=/usr/bin/ncmpcpp
@@ -139,3 +143,7 @@ bind = $mainMod, code:112, submap, reset
submap = reset submap = reset
bind = SUPER, l, exec, hyprlock 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 # 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 # https://tatsumoto.neocities.org/blog/setting-up-anki.html#import-an-example-mining-deck
model_name=Lapis model_name=Lapis
# model_name=Kiku
# Field names as they appear in the selected note type. # Field names as they appear in the selected note type.
# If you set `audio_field` or `image_field` empty, # If you set `audio_field` or `image_field` empty,

View File

@@ -131,7 +131,7 @@ return {
}, },
-- }}} -- }}}
}, },
strategies = { interactions = {
chat = { chat = {
adapter = "copilot", adapter = "copilot",
-- adapter = "openrouter", -- adapter = "openrouter",
@@ -182,11 +182,21 @@ return {
end, end,
completion_provider = "cmp", completion_provider = "cmp",
}, },
fold_reasoning = true,
show_reasoning = true,
}, },
inline = { inline = {
adapter = "copilot", adapter = "copilot",
-- adapter = "openrouter", -- adapter = "openrouter",
}, },
cmd = {
adapter = "copilot",
},
background = {
adapter = {
name = "copilot",
},
},
}, },
display = { display = {
action_palette = { action_palette = {
@@ -232,7 +242,6 @@ return {
-- Options for inline diff provider -- Options for inline diff provider
inline = { inline = {
layout = "buffer", -- float|buffer - Where to display the diff layout = "buffer", -- float|buffer - Where to display the diff
diff_signs = { diff_signs = {
signs = { signs = {
text = "", -- Sign text for normal changes 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() init = function()
require("utils.codecompanion.fidget-spinner"):init() require("utils.codecompanion.fidget-spinner"):init()

View File

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

View File

@@ -43,10 +43,10 @@ end
function M:create_progress_handle(request) function M:create_progress_handle(request)
return progress.handle.create({ return progress.handle.create({
title = " Requesting assistance (" .. request.data.strategy .. ")", title = " Requesting assistance (" .. request.data.adapter.model .. ")",
message = "In progress...", message = "In progress...",
lsp_client = { lsp_client = {
name = M:llm_role_title(request.data.adapter), name = M:llm_role_title(request.data.adapter.name),
}, },
}) })
end 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", "Pihole2 - https://pihole2.suda.codes/admin",
"Proxmox - https://thebox.unicorn-ilish.ts.net", "Proxmox - https://thebox.unicorn-ilish.ts.net",
"qBittorrent - https://qbittorrent.suda.codes", "qBittorrent - https://qbittorrent.suda.codes",
"qui - https://qui.suda.codes", "qui - http://pve-main:7476",
"Plausible - https://plausible.sudacode.com", "Plausible - https://plausible.sudacode.com",
"Paperless - https://paperless.suda.codes", "Paperless - https://paperless.suda.codes",
"Prometheus - http://prometheus:9090", "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 #!/usr/bin/env bash
SCRIPT_DIR="$(cd -- "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/rofi-menu-helpers.sh"
BROWSER=/usr/bin/zen-browser BROWSER=/usr/bin/zen-browser
OPTIONS=( DOC_GROUPS=(
"Arch Linux (btw)" "Arch Linux (btw)|ARCH"
"Hyprland" "Hyprland|HYPRLAND"
) )
ARCH=( ARCH=(
"Archlinux Wiki|https://wiki.archlinux.org/title/Main_page" "Archlinux Wiki|https://wiki.archlinux.org/title/Main_page"
@@ -13,56 +16,37 @@ HYPRLAND=(
"Hyprland Window Rules|https://wiki.hypr.land/Configuring/Window-Rules/" "Hyprland Window Rules|https://wiki.hypr.land/Configuring/Window-Rules/"
) )
get_url() { select_group() {
urls=("$@") rofi_select_label_value "Select Documentation Group" DOC_GROUPS
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"
} }
get_docs_list() { select_url() {
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")" local urls_array="$1"
case "$selection" in rofi_select_label_value "Select Documentation" "$urls_array" "Back"
"Arch Linux (btw)") }
urls=("${ARCH[@]}")
main() {
while true; do
group_key="$(select_group)" || exit 0
case "$group_key" in
ARCH)
urls_ref=ARCH
;; ;;
"Hyprland") HYPRLAND)
urls=("${HYPRLAND[@]}") urls_ref=HYPRLAND
;; ;;
*) *)
exit 0 exit 0
;; ;;
esac esac
printf "%s\n" "${urls[@]}" selection="$(select_url "$urls_ref")" || exit 0
} if [[ "$selection" == "Back" ]]; then
continue
main() {
mapfile -t urls < <(get_docs_list)
url="$(get_url "${urls[@]}")"
if [ -z "$url" ]; then
printf "No URL selected.\n"
exit 0
elif [ "$url" == "Back" ]; then
main
exit 0
fi fi
$BROWSER "$url" &>/dev/null & $BROWSER "$selection" &>/dev/null &
exit 0
done
} }
main 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" 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%;}') 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 '/')" 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" 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" echo "$PTH" >"$HOME/.wallpaper"

View File

@@ -15,6 +15,7 @@ export QT_QPA_PLATFORMTHEME=qt5ct
export GTK_THEME=Dracula export GTK_THEME=Dracula
export XDG_CONFIG_HOME=$HOME/.config export XDG_CONFIG_HOME=$HOME/.config
export COMPOSE_BAKE=true export COMPOSE_BAKE=true
export ANKI_WAYLAND=1
# nvidia # nvidia
export NVD_BACKEND=direct 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" FPATH="$HOME/.docker/completions:$FPATH"
[[ "$TERM_PROGRAM" == "vscode" ]] && . "$(code --locate-shell-integration-path zsh)" [[ "$TERM_PROGRAM" == "vscode" ]] && . "$(code --locate-shell-integration-path zsh)"
eval "$(fnm env --use-on-cd --shell zsh)"
bindkey -v bindkey -v
# zstyle ':completion:*' matcher-list 'm:{a-zA-Z}={A-Za-z}' # zstyle ':completion:*' matcher-list 'm:{a-zA-Z}={A-Za-z}'

View File

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

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 # Toggle desktop audio recording and attach the result to the newest Anki note
# shoutout to https://gist.github.com/Cephian/f849e326e3522be9a4386b60b85f2f23 for the original script, # (as tagged by Yomichan). Run once to start recording, run again to stop.
# https://github.com/xythh/ added the ankiConnect functionality # Dependencies: jq, curl, ffmpeg/ffprobe, pulseaudio (parec+pactl), bc, notify-send
# toggle record computer audio (run once to start, run again to stop)
# dependencies: ffmpeg, pulseaudio, curl
# where recording gets saved, gets deleted after being imported to anki set -euo pipefail
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"
#if there is no newest note, you either have a complete empty anki or ankiconnect isn't running ANKI_CONNECT_PORT="${ANKI_CONNECT_PORT:-8765}"
if [ "$newestNoteId" = "" ]; then AUDIO_FIELD_NAME="${AUDIO_FIELD_NAME:-SentenceAudio}"
notify-send "anki connect not found" 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}"
require_cmd() {
command -v "$1" >/dev/null 2>&1 || {
echo "Missing dependency: $1" >&2
exit 1 exit 1
fi
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'"
]
}]
} }
} }
}'
# opens changed note, comment if you don't want it. notify() {
curl -s localhost:$ankiConnectPort -X POST -d '{ # Best-effort notification; keep script running if notify-send is missing.
"action": "guiBrowse", if command -v notify-send >/dev/null 2>&1; then
"version": 6, notify-send -t 1000 "$@"
"params": { fi
"query": "nid:'"$newestNoteId"'" }
}
}' get_active_sink() {
notify-send -t 1000 "Audio recording copied" pactl list sinks short 2>/dev/null | awk '$6=="RUNNING"{print $1; exit 0}'
rm "$out_file" }
fi
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 "$@"

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

@@ -1,75 +1,202 @@
#!/bin/sh #!/usr/bin/env bash
# Version 1.2 # Capture a region with slurp+grim. If AnkiConnect is available, attach the
# click and drag to screenshot dragged portion # JPEG to the newest note; otherwise copy a PNG to the clipboard.
# 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"
# This gets your notes marked as new and returns the newest one. set -euo pipefail
newestNoteId=$(curl -s localhost:$ankiConnectPort -X POST -d '{"action": "findNotes", "version": 6, "params": { "query": "is:new"}}' | jq '.result[-1]')
# you can remove these two lines if you don't have software which ANKI_CONNECT_PORT="${ANKI_CONNECT_PORT:-8765}"
# makes your mouse disappear when you use the keyboard (e.g. xbanish, unclutter) PICTURE_FIELD="${PICTURE_FIELD:-Picture}"
# https://github.com/ImageMagick/ImageMagick/issues/1745#issuecomment-777747494 QUALITY="${QUALITY:-90}"
xdotool mousemove_relative 1 1 CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/screenshot-anki"
xdotool mousemove_relative -- -1 -1 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. parse_opts() {
if [ "$newestNoteId" != "" ]; then while getopts "cd:" opt; do
if ! import -quality $quality "$tmp_file.jpg"; then case "$opt" in
# 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. c)
notify-send "Error screenshoting " "most likely unable to find selection" 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
}
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
}
}
wiggle_mouse() {
# Avoid disappearing cursor on some compositors
xdotool mousemove_relative 1 1
xdotool mousemove_relative -- -1 -1
}
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 exit 1
fi fi
if [[ "$fmt" == "jpeg" ]]; then
grim -g "$geometry" -t jpeg -q "$quality" "$output"
else
grim -g "$geometry" -t png "$output"
fi
}
curl -s localhost:$ankiConnectPort -X POST -d '{ capture_current_window() {
"action": "updateNoteFields", local fmt="$1" quality="$2" output="$3" geometry
"version": 6,
"params": {
"note": {
"id": '"$newestNoteId"',
"fields": {
"'$pictureField'": ""
},
"picture": [{
"path": "'"$tmp_file"'.jpg",
"filename": "paste-'"$time"'.jpg",
"fields": [
"'$pictureField'"
]
}]
}
}
}'
#remove if you don't want anki to show you the card you just edited if [[ "$fmt" == "jpeg" ]]; then
curl -s localhost:$ankiConnectPort -X POST -d '{ grim -w "$(hyprctl activewindow -j | jq -r '.address')" -t jpeg -q "$quality" "$output"
"action": "guiBrowse", else
"version": 6, grim -w "$(hyprctl activewindow -j | jq -r '.address')" -t png "$output"
"params": { fi
"query": "nid:'"$newestNoteId"'" }
}
}'
#you can comment this if you do not use notifcations. choose_capture_mode() {
notify-send "Screenshot Taken" "Added to note" local selection
rm "$tmp_file.jpg" selection=$(printf "%s\n%s\n" "Region (slurp)" "Current window (Hyprland)" |
else rofi -dmenu -i \
if ! import -quality $quality "$tmp_file.png"; then -p "Capture mode" \
notify-send "Error screenshoting " "most likely unable to find selection" -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 exit 1
fi 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" get_newest_note_id() {
#you can comment this if you do not use notifcations. local response query="is:new"
notify-send "Screenshot Taken" "Copied to clipboard" if [[ -n "$DECK_NAME" ]]; then
fi 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

@@ -39,7 +39,7 @@ notify() {
check_deps() { check_deps() {
for cmd in "${REQUIREMENTS[@]}"; do for cmd in "${REQUIREMENTS[@]}"; do
if ! command -v "$cmd" &> /dev/null; then if ! command -v "$cmd" &>/dev/null; then
echo "Error: $cmd is not installed. Please install it first." echo "Error: $cmd is not installed. Please install it first."
exit 1 exit 1
fi fi
@@ -54,7 +54,7 @@ main() {
-theme-str 'listview {columns: 2; lines: 3;} window {width: 45%;}' \ -theme-str 'listview {columns: 2; lines: 3;} window {width: 45%;}' \
-window-title "$SCRIPT_NAME" \ -window-title "$SCRIPT_NAME" \
-format 'i' \ -format 'i' \
<<< "$(printf "%s\n" "${CHOICES[@]%% - *}")")" <<<"$(printf "%s\n" "${CHOICES[@]%% - *}")")"
if [[ -z "$CHOICE" ]]; then if [[ -z "$CHOICE" ]]; then
notify "No option selected." "" notify "No option selected." ""
@@ -86,7 +86,7 @@ main() {
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) FILE=$(zenity --file-selection --title="Save Screenshot" --filename="$DEFAULT_FILENAME" --save 2>/dev/null)
case "$?" in case "$?" in
0) 0)
if mv "$TMP_SCREENSHOT" "$FILE"; then if mv "$TMP_SCREENSHOT" "$FILE"; then

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"