diff --git a/.bash_aliases b/.bash_aliases index d2b9022..302e6b8 100644 --- a/.bash_aliases +++ b/.bash_aliases @@ -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 {}")' diff --git a/.config/ghostty/config##Default b/.config/ghostty/config##Default index 453bfee..a45f478 100644 --- a/.config/ghostty/config##Default +++ b/.config/ghostty/config##Default @@ -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 diff --git a/.config/ghostty/config##os.Darwin b/.config/ghostty/config##os.Darwin index 3d8c138..87118f2 100644 --- a/.config/ghostty/config##os.Darwin +++ b/.config/ghostty/config##os.Darwin @@ -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 diff --git a/.config/hypr/hyprland.conf b/.config/hypr/hyprland.conf index cd8fc87..3896428 100644 --- a/.config/hypr/hyprland.conf +++ b/.config/hypr/hyprland.conf @@ -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 diff --git a/.config/hypr/keybindings.conf b/.config/hypr/keybindings.conf index 3f71e56..ed6e6e7 100644 --- a/.config/hypr/keybindings.conf +++ b/.config/hypr/keybindings.conf @@ -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 diff --git a/.config/mimeapps.list b/.config/mimeapps.list new file mode 100644 index 0000000..b2b9eb9 --- /dev/null +++ b/.config/mimeapps.list @@ -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 diff --git a/.config/mpv/script-opts/subs2srs.conf b/.config/mpv/script-opts/subs2srs.conf index db01b5a..ce529e4 100644 --- a/.config/mpv/script-opts/subs2srs.conf +++ b/.config/mpv/script-opts/subs2srs.conf @@ -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, diff --git a/.config/nvim/lua/plugins/codecompanion.lua b/.config/nvim/lua/plugins/codecompanion.lua index 3c555ca..a040394 100644 --- a/.config/nvim/lua/plugins/codecompanion.lua +++ b/.config/nvim/lua/plugins/codecompanion.lua @@ -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() diff --git a/.config/nvim/lua/utils/codecompanion/fidget-spinner-notify.lua b/.config/nvim/lua/utils/codecompanion/fidget-spinner-notify.lua index d6aaa50..8cfbb29 100644 --- a/.config/nvim/lua/utils/codecompanion/fidget-spinner-notify.lua +++ b/.config/nvim/lua/utils/codecompanion/fidget-spinner-notify.lua @@ -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 " diff --git a/.config/nvim/lua/utils/codecompanion/fidget-spinner.lua b/.config/nvim/lua/utils/codecompanion/fidget-spinner.lua index 59c3c5f..bc2bb90 100644 --- a/.config/nvim/lua/utils/codecompanion/fidget-spinner.lua +++ b/.config/nvim/lua/utils/codecompanion/fidget-spinner.lua @@ -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 diff --git a/.config/opencode/oh-my-opencode.jsonc b/.config/opencode/oh-my-opencode.jsonc new file mode 100644 index 0000000..c11d884 --- /dev/null +++ b/.config/opencode/oh-my-opencode.jsonc @@ -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 + } + } +} diff --git a/.config/opencode/opencode-notifier.json b/.config/opencode/opencode-notifier.json new file mode 100644 index 0000000..3917473 --- /dev/null +++ b/.config/opencode/opencode-notifier.json @@ -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" + } +} diff --git a/.config/opencode/opencode.json b/.config/opencode/opencode.json deleted file mode 100644 index 432ff89..0000000 --- a/.config/opencode/opencode.json +++ /dev/null @@ -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 - } - } - } -} diff --git a/.config/opencode/opencode.jsonc b/.config/opencode/opencode.jsonc new file mode 100644 index 0000000..21ab9f7 --- /dev/null +++ b/.config/opencode/opencode.jsonc @@ -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" + } +} diff --git a/.config/rofi-open/config.json b/.config/rofi-open/config.json index ab3d960..a1a0fef 100644 --- a/.config/rofi-open/config.json +++ b/.config/rofi-open/config.json @@ -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", diff --git a/.config/rofi/scripts/rofi-anki-script.sh b/.config/rofi/scripts/rofi-anki-script.sh new file mode 100755 index 0000000..b387446 --- /dev/null +++ b/.config/rofi/scripts/rofi-anki-script.sh @@ -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 diff --git a/.config/rofi/scripts/rofi-docs.sh b/.config/rofi/scripts/rofi-docs.sh index b307bd7..589c01c 100755 --- a/.config/rofi/scripts/rofi-docs.sh +++ b/.config/rofi/scripts/rofi-docs.sh @@ -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 diff --git a/.config/rofi/scripts/rofi-jellyfin-dir.sh b/.config/rofi/scripts/rofi-jellyfin-dir.sh new file mode 100755 index 0000000..c58d932 --- /dev/null +++ b/.config/rofi/scripts/rofi-jellyfin-dir.sh @@ -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 diff --git a/.config/rofi/scripts/rofi-launch-texthooker-steam.sh b/.config/rofi/scripts/rofi-launch-texthooker-steam.sh new file mode 100755 index 0000000..497069e --- /dev/null +++ b/.config/rofi/scripts/rofi-launch-texthooker-steam.sh @@ -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 & diff --git a/.config/rofi/scripts/rofi-menu-helpers.sh b/.config/rofi/scripts/rofi-menu-helpers.sh new file mode 100644 index 0000000..80c6f5b --- /dev/null +++ b/.config/rofi/scripts/rofi-menu-helpers.sh @@ -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[@]}" +} + diff --git a/.config/rofi/scripts/rofi-wallpaper.sh b/.config/rofi/scripts/rofi-wallpaper.sh index 0993f05..0e81e44 100755 --- a/.config/rofi/scripts/rofi-wallpaper.sh +++ b/.config/rofi/scripts/rofi-wallpaper.sh @@ -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" diff --git a/.config/uwsm/env b/.config/uwsm/env index a868364..0446a31 100644 --- a/.config/uwsm/env +++ b/.config/uwsm/env @@ -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 diff --git a/.zsh/.zshrc##default b/.zsh/.zshrc##default index 28cc42e..3410acb 100644 --- a/.zsh/.zshrc##default +++ b/.zsh/.zshrc##default @@ -28,6 +28,7 @@ setopt HIST_IGNORE_SPACE # don’t 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}' diff --git a/projects/scripts/ocr.sh b/projects/scripts/ocr.sh index 8f1e97c..2ae9426 100755 --- a/projects/scripts/ocr.sh +++ b/projects/scripts/ocr.sh @@ -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" diff --git a/projects/scripts/popup-ai-translator.py b/projects/scripts/popup-ai-translator.py new file mode 100755 index 0000000..2870323 --- /dev/null +++ b/projects/scripts/popup-ai-translator.py @@ -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()) diff --git a/projects/scripts/record-audio.sh b/projects/scripts/record-audio.sh old mode 100644 new mode 100755 index 30947fc..66898d8 --- a/projects/scripts/record-audio.sh +++ b/projects/scripts/record-audio.sh @@ -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 "$@" diff --git a/projects/scripts/screenshot-anki.sh b/projects/scripts/screenshot-anki.sh old mode 100644 new mode 100755 index 464bbc0..72bd318 --- a/projects/scripts/screenshot-anki.sh +++ b/projects/scripts/screenshot-anki.sh @@ -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 "$@" diff --git a/projects/scripts/screenshot.sh b/projects/scripts/screenshot.sh index 939684c..7220740 100755 --- a/projects/scripts/screenshot.sh +++ b/projects/scripts/screenshot.sh @@ -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 diff --git a/projects/scripts/songinfo.sh b/projects/scripts/songinfo.sh new file mode 100755 index 0000000..7239765 --- /dev/null +++ b/projects/scripts/songinfo.sh @@ -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"