Compare commits

..

5 Commits

Author SHA1 Message Date
7fe60df077 update fonts 2026-03-17 23:06:36 -07:00
748994320a update waybar 2026-03-17 22:58:42 -07:00
15d31864db update 2026-03-17 22:02:18 -07:00
ac41c90066 update 2026-03-17 22:00:05 -07:00
b20e439015 update hyprland config 2026-03-17 21:59:49 -07:00
24 changed files with 954 additions and 48 deletions

View File

@@ -10,7 +10,14 @@
"Read(~/.zshrc)",
"Bash(git * main)",
"Bash(ls *)",
"Bash(pnpm build *)"
"Bash(pnpm build *)",
"Bash(subminer stats:*)",
"Bash(identify /home/sudacode/projects/japanese/SubMiner/docs-site/public/screenshots/stats-trends.png)",
"mcp__plugin_playwright_playwright__browser_resize",
"mcp__plugin_playwright_playwright__browser_click",
"mcp__plugin_playwright_playwright__browser_take_screenshot",
"mcp__plugin_playwright_playwright__browser_close",
"Bash(bun run:*)"
],
"deny": [
"Bash(curl *)",

View File

@@ -123,6 +123,15 @@ trust_level = "trusted"
[projects."/home/sudacode/.config/git"]
trust_level = "trusted"
[projects."/home/sudacode/.config/rofi"]
trust_level = "trusted"
[projects."/home/sudacode/.config/swaync"]
trust_level = "trusted"
[projects."/home/sudacode/.config/ranger"]
trust_level = "trusted"
[notice.model_migrations]
"gpt-5.3-codex" = "gpt-5.4"

View File

@@ -93,12 +93,15 @@
"overwriteImage": true,
"mediaInsertMode": "append",
"highlightWord": true,
"notificationType": "system",
"notificationType": "both",
"showNotificationOnUpdate": true,
"autoUpdateNewCards": true,
},
"knownWords": {
"decks": ["Minecraft", "Kaishi 1.5k"],
"decks": {
"Minecraft": ["Expression", "Reading"],
"Kaishi 1.5k": ["Word", "Word Reading"]
},
"highlightEnabled": true,
"refreshMinutes": 60,
"matchMode": "headword",

View File

@@ -2,7 +2,7 @@
#* Name of a btop++/bpytop/bashtop formatted ".theme" file, "Default" and "TTY" for builtin themes.
#* Themes should be placed in "../share/btop/themes" relative to binary or "$HOME/.config/btop/themes"
color_theme = "/Users/sudacode/.config/btop/themes/catppuccin_macchiato.theme"
color_theme = "/home/sudacode/.config/btop/themes/catppuccin_macchiato.theme"
#* If the theme set background should be shown, set to False if you want terminal background transparency.
theme_background = true
@@ -40,6 +40,9 @@ graph_symbol = "braille"
# Graph symbol to use for graphs in cpu box, "default", "braille", "block" or "tty".
graph_symbol_cpu = "default"
# Graph symbol to use for graphs in gpu box, "default", "braille", "block" or "tty".
graph_symbol_gpu = "default"
# Graph symbol to use for graphs in cpu box, "default", "braille", "block" or "tty".
graph_symbol_mem = "default"
@@ -103,6 +106,9 @@ cpu_graph_upper = "Auto"
#* Select from a list of detected attributes from the options menu.
cpu_graph_lower = "Auto"
#* If gpu info should be shown in the cpu box. Available values = "Auto", "On" and "Off".
show_gpu_info = "Auto"
#* Toggles if the lower CPU graph should be inverted.
cpu_invert_lower = true
@@ -142,6 +148,9 @@ base_10_sizes = false
#* Show CPU frequency.
show_cpu_freq = true
#* How to calculate CPU frequency, available values: "first", "range", "lowest", "highest" and "average".
freq_mode = "first"
#* Draw a clock at top of screen, formatting according to strftime, empty string to disable.
#* Special formatting: /host = hostname | /user = username | /uptime = system uptime
clock_format = "%X"
@@ -231,3 +240,33 @@ log_level = "WARNING"
#* Automatically save current settings to config file on exit.
save_config_on_exit = true
#* Measure PCIe throughput on NVIDIA cards, may impact performance on certain cards.
nvml_measure_pcie_speeds = true
#* Measure PCIe throughput on AMD cards, may impact performance on certain cards.
rsmi_measure_pcie_speeds = true
#* Horizontally mirror the GPU graph.
gpu_mirror_graph = true
#* Set which GPU vendors to show. Available values are "nvidia amd intel"
shown_gpus = "nvidia amd intel"
#* Custom gpu0 model name, empty string to disable.
custom_gpu_name0 = ""
#* Custom gpu1 model name, empty string to disable.
custom_gpu_name1 = ""
#* Custom gpu2 model name, empty string to disable.
custom_gpu_name2 = ""
#* Custom gpu3 model name, empty string to disable.
custom_gpu_name3 = ""
#* Custom gpu4 model name, empty string to disable.
custom_gpu_name4 = ""
#* Custom gpu5 model name, empty string to disable.
custom_gpu_name5 = ""

View File

@@ -25,17 +25,17 @@ monitorv2 {
position = 0x0
scale = 1
vrr = 2
cm = srgb
# cm = srgb
# Optional HDR settings
# cm = hdr
# bitdepth = 10
# sdr_min_luminance = 0.005
# sdr_max_luminance = 200
# min_luminance = 0
# max_luminance = 1000
# max_avg_luminance = 200
# sdrbrightness = 1.2
# sdrsaturation = 0.98
cm = hdr
bitdepth = 10
sdr_min_luminance = 0.005
sdr_max_luminance = 200
min_luminance = 0
max_luminance = 1000
max_avg_luminance = 200
sdrbrightness = 1.2
sdrsaturation = 0.98
}
source = ~/.config/hypr/keybindings.conf
@@ -252,7 +252,7 @@ render {
}
misc {
font_family = JetBrainsMono Nerd Font
font_family = Manrope ExtraLight Medium, JetBrainsMono Nerd Font, M PLUS 1
}
# {{{ WORKSPACES - HANDLED IN WAYBAR CONFIG

106
.config/hypr/hyprlock.conf Normal file
View File

@@ -0,0 +1,106 @@
# sample hyprlock.conf
# for more configuration options, refer https://wiki.hyprland.org/Hypr-Ecosystem/hyprlock
#
# rendered text in all widgets supports pango markup (e.g. <b> or <i> tags)
# ref. https://wiki.hyprland.org/Hypr-Ecosystem/hyprlock/#general-remarks
#
# shortcuts to clear password buffer: ESC, Ctrl+U, Ctrl+Backspace
#
# you can get started by copying this config to ~/.config/hypr/hyprlock.conf
#
$font = Manrope ExtraLight
general {
hide_cursor = false
}
# uncomment to enable fingerprint authentication
auth {
fingerprint {
enabled = true
ready_message = Scan fingerprint to unlock
present_message = Scanning...
retry_delay = 250 # in milliseconds
}
}
animations {
enabled = true
bezier = linear, 1, 1, 0, 0
animation = fadeIn, 1, 5, linear
animation = fadeOut, 1, 5, linear
animation = inputFieldDots, 1, 2, linear
}
background {
monitor =
path = screenshot
blur_passes = 3
}
input-field {
monitor =
size = 20%, 5%
outline_thickness = 3
inner_color = rgba(0, 0, 0, 0.0) # no fill
outer_color = rgba(33ccffee) rgba(00ff99ee) 45deg
check_color = rgba(00ff99ee) rgba(ff6633ee) 120deg
fail_color = rgba(ff6633ee) rgba(ff0066ee) 40deg
font_color = rgb(143, 143, 143)
fade_on_empty = false
rounding = 15
font_family = $font
placeholder_text = Input password...
fail_text = $PAMFAIL
# uncomment to use a letter instead of a dot to indicate the typed password
# dots_text_format = *
# dots_size = 0.4
dots_spacing = 0.3
# uncomment to use an input indicator that does not show the password length (similar to swaylock's input indicator)
# hide_input = true
position = 0, -20
halign = center
valign = center
}
# TIME
label {
monitor =
text = $TIME # ref. https://wiki.hyprland.org/Hypr-Ecosystem/hyprlock/#variable-substitution
font_size = 90
font_family = $font
position = -30, 0
halign = right
valign = top
}
# DATE
label {
monitor =
text = cmd[update:60000] date +"%A, %d %B %Y" # update every 60 seconds
font_size = 25
font_family = $font
position = -30, -150
halign = right
valign = top
}
label {
monitor =
text = $LAYOUT[en,ru]
font_size = 24
onclick = hyprctl switchxkblayout all next
position = 250, -20
halign = center
valign = center
}

View File

@@ -0,0 +1,10 @@
theme = {
color_schema = "/usr/share/themes/Colloid-Purple-Dark-Catppuccin"
icon_theme = "Colloid-Purple-Catppuccin-Dark"
style = "kvantum-dark"
font_fixed = "JetBrainsMono Nerd Font"
font_fixed_size = 12
font = "Manrope ExtraLight"
font_size = 12
}

View File

@@ -112,7 +112,7 @@ bind = SUPER, m, exec, ~/.local/bin/mpv-add.sh
bind = SUPER SHIFT, s, exec, slurp | grim -g - - | wl-copy
bind = ,code:107, exec, ~/.local/bin/screenshot
bind = SHIFT ,code:107, exec, grim -g "$(hyprctl activewindow -j | jq -r '.at[0],.at[1],.size[0],.size[1]' | tr '\n' ' ' | awk '{print $1","$2" "$3"x"$4}')" - | wl-copy
bind = SHIFT ,code:107, exec, ~/.local/bin/screenshot-active-window.sh
bind = SUPER,code:107, exec, flameshot screen
bind = SUPER, o, exec, ~/.local/bin/ocr.sh
bind = $mainMod, o, exec, ~/.local/bin/rofi-open tab

View File

@@ -0,0 +1,87 @@
# See https://wiki.hyprland.org/Configuring/Workspace-Rules/ for workspace rules
# {{{ Floating windows
windowrule = float on, match:class discord
windowrule = float on, match:class mpv
windowrule = float on, match:class anki
windowrule = float on, match:class steam
windowrule = float on, match:class python, match:title Import
windowrule = float on, match:class zenity, match:title Japanese Analysis
windowrule = float on, match:class zenity, match:title Japanese Assistant
windowrule = min_size 1600 600, match:class anki
windowrule = max_size 2222 1234, match:class anki
windowrule = no_focus on, no_follow_mouse 1, float on, no_shadow on, no_anim on, match:class ueberzugpp.*
windowrule = size 1920 1080, match:class anki
# windowrule = min_size 1600 600, match:class anki
# windowrule = max_size 2222 1234, match:class anki
# }}}
# {{{ Workspace assignments
windowrule = workspace 3 silent, match:class Cursor
windowrule = workspace 5 silent, match:class GameSentenceMiner
windowrule = workspace 5 silent, match:class com.obsproject.Studio
windowrule = workspace 6 silent, match:class gamescope
windowrule = workspace 8 silent, match:class anki
windowrule = workspace 9 silent, match:class steam
windowrule = workspace 10 silent, match:class discord
# }}}
# {{{ Center floating windows
windowrule = match:float 1, match:class discord, center on
windowrule = match:float 1, match:class anki, center on
# }}}
# {{{ Opacity rules
# windowrule = opacity 0.88, match:class .* fullscreen:0
windowrule = opacity 1.0 override, match:class mpv
windowrule = opacity 1.0 override, match:class ^(remote-viewer)$
windowrule = opacity 1.0 override, match:class com.obsproject.Studio
windowrule = opacity 1.0 override, match:title (.*)(- YouTube(.*))
windowrule = opacity 1.0 override, match:class zen, match:title (.*)YouTube TV(.*)
windowrule = opacity 1.0 override, match:class anki
windowrule = opacity 1.0 override, match:title (.*)asbplayer
windowrule = opacity 1.0 override, match:class dolphin-emu
windowrule = opacity 1.0 override, match:class steam_app_default
windowrule = opacity 1.0 override, match:class steam_app.*
windowrule = opacity 1.0 override, match:class Rustdesk
# }}}
# {{{ Misc
windowrule = tile on, match:title (.*)asbplayer
# windowrule = size 2118 1182, match:class anki
# Ignore maximize requests from apps. You'll probably like this.
windowrule = suppress_event maximize, match:class .*
# Fix some dragging issues with XWayland
windowrule = no_focus on,match:class ^$,match:title ^$,match:xwayland 1,match:float 1,match:fullscreen 0,match:pin 0
# }}}
# {{{ Screen sharing workaround: https://wiki.hyprland.org/Useful-Utilities/Screen-Sharing/#xwayland
windowrule = opacity 0.0 override, match:class ^(xwaylandvideobridge)$
windowrule = no_anim on, match:class ^(xwaylandvideobridge)$
windowrule = no_initial_focus on, match:class ^(xwaylandvideobridge)$
windowrule = max_size 1 1, match:class ^(xwaylandvideobridge)$
windowrule = no_blur on, match:class ^(xwaylandvideobridge)$
windowrule = no_focus on, match:class ^(xwaylandvideobridge)$
# }}}
# {{{ GSM Overlay and LunaTranslator tweaks
windowrule = float on, match:class gsm_overlay
windowrule = border_size 0, match:class gsm_overlay
windowrule = xray off, match:class gsm_overlay
windowrule = no_shadow on, match:class gsm_overlay
windowrule = no_blur on, match:class gsm_overlay
windowrule = opacity 1.0 override, match:title LunaTranslator
windowrule = border_size 0, match:title LunaTranslator
windowrule = stay_focused on, match:class gsm_overlay
# windowrule = fullscreen_state 2, match:class gsm_overlay
windowrule = float on, match:class SubMiner
windowrule = border_size 0, match:class SubMiner
windowrule = xray off override, match:class SubMiner
windowrule = no_shadow on, match:class SubMiner
windowrule = no_blur on, match:class SubMiner
windowrule = allows_input offf, match:class SubMiner
windowrule = border_size 0, match:class steam_app_1277940
# {{{ GSM Overlay and LunaTranslator tweaks

View File

@@ -130,13 +130,23 @@ scale=ewa_lanczos
target-peak=800
[hdr]
target-colorspace-hint=yes
profile-desc=HDR content
profile-cond=get("video-params/gamma", "") == "pq" or get("video-params/gamma", "") == "hlg"
profile-restore=copy
tone-mapping-param=0.5
tone-mapping-max-boost=2.0
allow-delayed-peak-detect=yes
# For SDR content on HDR display (or vice versa)
icc-profile-auto=yes
[sdr]
profile-desc=SDR content
profile-cond=get("video-params/gamma", "") ~= "pq" and get("video-params/gamma", "") ~= "hlg"
profile-restore=copy
# put SDR-only tweaks here
deband=yes
target-colorspace-hint=auto
[svp]
interpolation=no
input-ipc-server=/tmp/mpvsocket

View File

@@ -157,14 +157,25 @@ scale=ewa_lanczos
target-peak=800
# hdr-tone-mapping=bt.2390
# HDR profile
[hdr]
target-colorspace-hint=yes
profile-desc=HDR content
profile-cond=get("video-params/gamma", "") == "pq" or get("video-params/gamma", "") == "hlg"
profile-restore=copy
tone-mapping-param=0.5
tone-mapping-max-boost=2.0
allow-delayed-peak-detect=yes
# For SDR content on HDR display (or vice versa)
icc-profile-auto=yes
[sdr]
profile-desc=SDR content
profile-cond=get("video-params/gamma", "") ~= "pq" and get("video-params/gamma", "") ~= "hlg"
profile-restore=copy
# put SDR-only tweaks here
deband=yes
target-colorspace-hint=auto
# SVP compatibility profile
[svp]
interpolation=no

View File

@@ -493,6 +493,17 @@ slider {
padding-bottom: 1rem;
}
.image {
padding-right: 0.5rem;
.notification-default-action .notification-content .image,
.notification .notification-content .image {
-gtk-icon-size: 64px;
min-width: 64px;
min-height: 64px;
margin-right: 8px;
}
.notification-default-action .notification-content .app-icon,
.notification .notification-content .app-icon {
-gtk-icon-size: 24px;
min-width: 24px;
min-height: 24px;
}

View File

@@ -16,12 +16,12 @@
"custom/firefox-scroll",
"hyprland/submap",
],
// "modules-center": ["hyprland/window"],
"modules-center": ["custom/notification"],
"modules-center": ["hyprland/window"],
// "modules-center": ["custom/notification"],
"modules-right": [
// "hyprland/scratchpad",
// "idle_inhibitor",
// "custom/notification",
"custom/notification",
"custom/updates",
"custom/kernel",
// "disk#ssd",
@@ -170,8 +170,8 @@
"pulseaudio": {
"scroll-step": 2,
"format": "{volume}% {icon} {format_source}",
"format-bluetooth": "{volume}% {icon} {format_source}",
"format-bluetooth-muted": "󰝟 {icon} {format_source}",
"format-bluetooth": "{volume}% {icon}  {format_source}",
"format-bluetooth-muted": "󰝟 {icon}  {format_source}",
"format-muted": "󰝟 {format_source}",
"format-source": "{volume}% ",
"format-source-muted": "",
@@ -192,8 +192,8 @@
"on-click-right": "qpwgraph",
"scroll-step": 2,
"format": "{volume}% {icon} {format_source}",
"format-bluetooth": "{volume}% {icon} {format_source}",
"format-bluetooth-muted": "󰝟 {icon} {format_source}",
"format-bluetooth": "{volume}% {icon}  {format_source}",
"format-bluetooth-muted": "󰝟 {icon}  {format_source}",
"format-muted": "󰝟 {format_source}",
"format-source": "{volume}% ",
"format-source-muted": "",
@@ -241,8 +241,8 @@
"hyprland/window": {
"max-length": 88,
"rewrite": {
"(.*) - YouTube(.*)": "",
"(.*) - mpv(.*)": "",
// "(.*) - YouTube(.*)": "$1",
// "(.*) - mpv(.*)": "$1",
},
},
"custom/updates": {
@@ -273,7 +273,7 @@
"format": "{}",
"max-length": 35,
"on-click": "mpc toggle",
"on-click-right": "ghostty --initial-command=ncmpcpp",
"on-click-right": "ghostty --initial-command=rmpc",
"on-scroll-down": "mpc volume -5",
"on-scroll-up": "mpc volume +5",
"hide-empty-text": true,

View File

@@ -2,8 +2,9 @@
* {
border: none;
font-family: "JetBrainsMono Nerd Font", "Font Awesome", "Noto Sans CJK JP";
font-size: 13px;
font-family: "Manrope ExtraLight Medium","JetBrainsMono Nerd Font", "Font Awesome", "M PLUS 1 Medium";
font-size: 15px;
font-weight: 600;
min-height: 0;
border-radius: 0.69em;
}
@@ -19,9 +20,12 @@ window#waybar.hidden {
}
#window {
background-color: inherit;
padding: 0 10px;
background-color: transparent;
color: @subtext1;
padding: 0 16px;
margin: 5px 1px;
font-style: italic;
font-weight: bold;
}
tooltip {
@@ -126,7 +130,7 @@ button:hover {
}
.modules-right {
padding-left: 1px;
padding-left: 5px;
}
.modules-left {
padding-right: 4px;
@@ -192,6 +196,7 @@ label:focus {
background-color: @maroon;
color: @mantle;
min-width: 40px;
padding: 0 12px;
}
#pulseaudio {
@@ -322,9 +327,10 @@ label:focus {
}
#custom-notification {
font-family: "JetBrainsMono Nerd Font";
margin: 0px 0.88em;
font-family: inherit;
padding: 0 10px;
margin: 5px 1px;
min-width: 1em;
background-color: transparent;
color: @maroon;
background-color: @maroon;
color: @mantle;
}

View File

@@ -1 +0,0 @@
../../../projects/scripts/waybar/wttr.sh

167
.config/waybar/scripts/wttr.sh Executable file
View File

@@ -0,0 +1,167 @@
#!/usr/bin/env bash
set -euo pipefail
location_input="${1:-Los_Angeles}"
location_query="${location_input//_/ }"
cache_dir="${XDG_CACHE_HOME:-$HOME/.cache}/waybar"
cache_key="${location_input//[^[:alnum:]_.-]/_}"
geo_cache_file="$cache_dir/weather-geo-${cache_key}.json"
weather_cache_file="$cache_dir/weather-${cache_key}.json"
json_escape() {
jq -Rsa .
}
emit_json() {
local text="$1"
local tooltip="$2"
printf '{"text":%s,"tooltip":%s}\n' \
"$(printf '%s' "$text" | json_escape)" \
"$(printf '%s' "$tooltip" | json_escape)"
}
emit_cached_or_error() {
if [[ -f "$weather_cache_file" ]]; then
cat "$weather_cache_file"
return
fi
emit_json "weather unavailable" "weather service unavailable"
}
url_encode() {
jq -rn --arg value "$1" '$value | @uri'
}
weather_icon() {
local code="$1"
local is_day="$2"
case "$code" in
0) [[ "$is_day" == "1" ]] && printf '☀' || printf '☾' ;;
1 | 2) [[ "$is_day" == "1" ]] && printf '⛅' || printf '☁' ;;
3) printf '☁' ;;
45 | 48) printf '🌫' ;;
51 | 53 | 55 | 56 | 57) printf '🌦' ;;
61 | 63 | 65 | 66 | 67 | 80 | 81 | 82) printf '🌧' ;;
71 | 73 | 75 | 77 | 85 | 86) printf '🌨' ;;
95 | 96 | 99) printf '⛈' ;;
*) printf '☁' ;;
esac
}
weather_description() {
case "$1" in
0) printf 'Clear sky' ;;
1) printf 'Mainly clear' ;;
2) printf 'Partly cloudy' ;;
3) printf 'Overcast' ;;
45 | 48) printf 'Fog' ;;
51) printf 'Light drizzle' ;;
53) printf 'Drizzle' ;;
55) printf 'Dense drizzle' ;;
56 | 57) printf 'Freezing drizzle' ;;
61) printf 'Slight rain' ;;
63) printf 'Rain' ;;
65) printf 'Heavy rain' ;;
66 | 67) printf 'Freezing rain' ;;
71) printf 'Slight snow' ;;
73) printf 'Snow' ;;
75) printf 'Heavy snow' ;;
77) printf 'Snow grains' ;;
80) printf 'Rain showers' ;;
81) printf 'Rain showers' ;;
82) printf 'Heavy rain showers' ;;
85 | 86) printf 'Snow showers' ;;
95) printf 'Thunderstorm' ;;
96 | 99) printf 'Thunderstorm with hail' ;;
*) printf 'Weather unavailable' ;;
esac
}
load_geocode() {
if [[ -f "$geo_cache_file" ]]; then
cat "$geo_cache_file"
return
fi
local encoded_query response
encoded_query="$(url_encode "$location_query")"
response="$(
curl \
--silent \
--show-error \
--fail \
--max-time 10 \
"https://geocoding-api.open-meteo.com/v1/search?name=${encoded_query}&count=1&language=en&format=json" 2>/dev/null || true
)"
if jq -e '.results[0] | .name and .latitude and .longitude and .timezone' >/dev/null <<<"$response"; then
jq -c '.results[0] | {name, admin1, country, latitude, longitude, timezone}' <<<"$response" | tee "$geo_cache_file"
return
fi
return 1
}
fetch_weather() {
local latitude="$1"
local longitude="$2"
local timezone="$3"
curl \
--silent \
--show-error \
--fail \
--max-time 10 \
"https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}&current=temperature_2m,relative_humidity_2m,apparent_temperature,weather_code,wind_speed_10m,is_day&timezone=$(url_encode "$timezone")" 2>/dev/null || true
}
format_weather() {
local place_json="$1"
local weather_json="$2"
local name region place code is_day icon description temperature feels_like humidity wind text tooltip
name="$(jq -r '.name' <<<"$place_json")"
region="$(jq -r '.admin1 // empty' <<<"$place_json")"
place="$name"
if [[ -n "$region" ]]; then
place+=", $region"
fi
code="$(jq -r '.current.weather_code' <<<"$weather_json")"
is_day="$(jq -r '.current.is_day' <<<"$weather_json")"
icon="$(weather_icon "$code" "$is_day")"
description="$(weather_description "$code")"
temperature="$(jq -r '.current.temperature_2m | round | "\(.)"' <<<"$weather_json")"
feels_like="$(jq -r '.current.apparent_temperature | round | "\(.)"' <<<"$weather_json")"
humidity="$(jq -r '.current.relative_humidity_2m | round | "\(.)"' <<<"$weather_json")"
wind="$(jq -r '.current.wind_speed_10m | round | "\(.)"' <<<"$weather_json")"
text="${icon} ${temperature}°C"
tooltip="${place}: ${description}. Feels like ${feels_like}°C, humidity ${humidity}%, wind ${wind} km/h"
emit_json "$text" "$tooltip"
}
mkdir -p "$cache_dir"
if ! place_json="$(load_geocode)"; then
emit_cached_or_error
exit 0
fi
latitude="$(jq -r '.latitude' <<<"$place_json")"
longitude="$(jq -r '.longitude' <<<"$place_json")"
timezone="$(jq -r '.timezone' <<<"$place_json")"
weather_json="$(fetch_weather "$latitude" "$longitude" "$timezone")"
if jq -e '.current | .temperature_2m and .relative_humidity_2m and .apparent_temperature and .weather_code and .wind_speed_10m and .is_day' >/dev/null <<<"$weather_json"; then
format_weather "$place_json" "$weather_json" | tee "$weather_cache_file"
exit 0
fi
emit_cached_or_error

View File

@@ -0,0 +1,17 @@
#!/bin/bash
HOME=/home/$USER
CURRENT="$(cat ~/.wallpaper)"
CURRENT="${CURRENT/\/\///}"
OUTPUT_DIR="/truenas/sudacode/pictures/wallpapers/"
cp "$CURRENT" "$HOME/Pictures/wallpapers/favorites/"
if cp "$CURRENT" "$OUTPUT_DIR"; then
notify-send "favorite-wallpaper" "Wallpaper saved to $OUTPUT_DIR"
else
notify-send "favorite-wallpaper" "Failed to saved wallpaper to $OUTPUT_DIR"
fi
# ft: sh

View File

@@ -0,0 +1,22 @@
#!/usr/bin/env bash
window_info=$(hyprctl activewindow -j)
read -r is_pinned window_class window_title <<< "$(echo "$window_info" | jq -r '[.pinned, .class, .title] | @tsv')"
hyprctl dispatch pin active
read -r window_x window_y window_w window_h <<< "$(echo "$window_info" | jq -r '[.at[0], .at[1], .size[0], .size[1]] | @tsv')"
screenshot=$(mktemp --suffix=.png)
grim -g "${window_x},${window_y} ${window_w}x${window_h}" "$screenshot"
if [ "$is_pinned" = "true" ]; then
status="Unpinned"
else
status="Pinned"
fi
notify-send -u low -i "$screenshot" "$status: $window_class" "$window_title"
rm -f "$screenshot"
# vim: set ft=sh

214
projects/scripts/popup-ai-chat.py Executable file
View File

@@ -0,0 +1,214 @@
#!/usr/bin/env python3
"""
Popup AI chat assistant using rofi for input and OpenRouter for responses.
"""
import os
import shutil
import subprocess
import sys
from typing import Optional
import requests
API_URL = "https://openrouter.ai/api/v1/chat/completions"
MODEL = os.environ.get("OPENROUTER_MODEL", "openai/gpt-oss-120b:free")
APP_NAME = "Popup AI Chat"
SYSTEM_PROMPT = (
"You are a helpful AI assistant. Give direct, accurate answers. "
"Use concise formatting unless the user asks for depth."
)
def load_api_key() -> str:
"""Load OpenRouter API key from env or fallback file."""
api_key = os.environ.get("OPENROUTER_API_KEY", "").strip()
if api_key:
return api_key
key_file = os.path.expanduser("~/.openrouterapikey")
if os.path.isfile(key_file):
with open(key_file, "r", encoding="utf-8") as handle:
return handle.read().strip()
return ""
def show_error(message: str) -> None:
"""Display an error message via zenity."""
subprocess.run(
["zenity", "--error", "--title", "Error", "--text", message],
stderr=subprocess.DEVNULL,
)
def check_dependencies() -> bool:
"""Validate required desktop tools are available."""
missing = [cmd for cmd in ("rofi", "zenity") if shutil.which(cmd) is None]
if not missing:
return True
message = f"Missing required command(s): {', '.join(missing)}"
if shutil.which("zenity") is not None:
show_error(message)
else:
print(f"Error: {message}", file=sys.stderr)
return False
def get_rofi_input() -> Optional[str]:
"""Ask for user input through rofi."""
result = subprocess.run(
["rofi", "-dmenu", "-i", "-p", "Ask AI"],
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
text=True,
)
if result.returncode != 0:
return None
prompt = result.stdout.strip()
return prompt or None
def show_notification(body: str) -> None:
"""Show processing notification when notify-send exists."""
if shutil.which("notify-send") is None:
return
subprocess.Popen(
["notify-send", "-t", "0", "-a", APP_NAME, "Processing...", body],
stderr=subprocess.DEVNULL,
)
def close_notification() -> None:
"""Close the processing notification if one was sent."""
if shutil.which("pkill") is None:
return
subprocess.run(
["pkill", "-f", "notify-send.*Processing..."],
stderr=subprocess.DEVNULL,
)
def make_api_request(api_key: str, messages: list[dict[str, str]]) -> dict:
"""Send chat request to OpenRouter and return JSON payload."""
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {api_key}",
"HTTP-Referer": "https://github.com/sudacode/scripts",
"X-Title": APP_NAME,
}
payload = {
"model": MODEL,
"messages": messages,
"temperature": 0.7,
}
response = requests.post(API_URL, headers=headers, json=payload, timeout=90)
return response.json()
def display_result(content: str) -> None:
"""Display model output in a text window."""
subprocess.run(
[
"zenity",
"--text-info",
"--title",
"AI Response",
"--width",
"900",
"--height",
"700",
"--font",
"monospace 11",
],
input=content,
text=True,
stderr=subprocess.DEVNULL,
)
def ask_follow_up() -> bool:
"""Ask if the user wants to continue the conversation."""
result = subprocess.run(
[
"zenity",
"--question",
"--title",
APP_NAME,
"--text",
"Ask a follow-up question?",
"--ok-label",
"Ask Follow-up",
"--cancel-label",
"Close",
],
stderr=subprocess.DEVNULL,
)
return result.returncode == 0
def extract_content(response: dict) -> str:
"""Extract assistant response from OpenRouter payload."""
if "error" in response:
message = response["error"].get("message", "Unknown API error")
raise ValueError(message)
try:
content = response["choices"][0]["message"]["content"]
except (KeyError, IndexError, TypeError) as exc:
raise ValueError("Failed to parse API response") from exc
if not content:
raise ValueError("Empty response from API")
return content
def main() -> int:
if not check_dependencies():
return 1
api_key = load_api_key()
if not api_key:
show_error("OPENROUTER_API_KEY environment variable is not set.")
return 1
history: list[dict[str, str]] = [{"role": "system", "content": SYSTEM_PROMPT}]
while True:
user_input = get_rofi_input()
if not user_input:
return 0
request_messages = history + [{"role": "user", "content": user_input}]
show_notification(f"Thinking: {user_input[:60]}...")
try:
response = make_api_request(api_key, request_messages)
content = extract_content(response)
except requests.RequestException as exc:
show_error(f"API request failed: {exc}")
return 1
except ValueError as exc:
show_error(str(exc))
return 1
except Exception as exc: # pragma: no cover
show_error(f"Unexpected error: {exc}")
return 1
finally:
close_notification()
history.append({"role": "user", "content": user_input})
history.append({"role": "assistant", "content": content})
display_result(content)
if not ask_follow_up():
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -0,0 +1,181 @@
#!/bin/bash
# Japanese Learning Assistant using OpenRouter API
# Uses Google Gemini Flash 2.0 for AJATT-aligned Japanese analysis
# Configuration
OPENROUTER_API_KEY="${OPENROUTER_API_KEY:-}"
MODEL="${OPENROUTER_MODEL:-google/gemini-2.0-flash-001}"
API_URL="https://openrouter.ai/api/v1/chat/completions"
if [[ -z $OPENROUTER_API_KEY && -f "$HOME/.openrouterapikey" ]]; then
OPENROUTER_API_KEY="$(<"$HOME/.openrouterapikey")"
fi
# System prompt for Japanese learning
SYSTEM_PROMPT='You are my Japanese-learning assistant. Help me acquire Japanese through deep, AJATT-aligned analysis.
For every input, output exactly:
1. Japanese Input (Verbatim)
Repeat the original text exactly. Correct only critical OCR/punctuation errors.
2. Natural English Translation
Accurate and natural. Preserve tone, formality, and nuance. Avoid literalism.
3. Word-by-Word Breakdown
For each unit:
- Vocabulary: Part of speech + concise definition
- Grammar: Particles, conjugations, constructions (contextual usage)
- Nuance: Implied meaning, connotation, emotional tone, differences from similar expressions
Core Principles:
- Preserve native phrasing—never oversimplify
- Highlight subtle grammar, register shifts, and pragmatic implications
- Encourage pattern recognition; provide contrastive examples (e.g., ~のに vs ~けど)
- Focus on real Japanese usage
Rules:
- English explanations only (no romaji)
- Clean, structured formatting; calm, precise tone
- No filler text
Optional Additions (only when valuable):
- Synonyms, formality/register notes, cultural insights, common mistakes, extra native examples
Goal: Deep comprehension, natural grammar internalization, nuanced vocabulary, progress toward Japanese-only understanding.'
# Check for API key
if [[ -z "$OPENROUTER_API_KEY" ]]; then
zenity --error --text="OPENROUTER_API_KEY environment variable is not set." --title="Error" 2>/dev/null
exit 1
fi
# Get input from zenity
input=$(zenity --entry \
--title="Japanese Assistant" \
--text="Enter Japanese text to analyze:" \
--width=500 \
2>/dev/null)
# Exit if no input
if [[ -z "$input" ]]; then
exit 0
fi
# Show loading notification
notify-send -t 0 -a "Japanese Assistant" "Processing..." "Analyzing: ${input:0:50}..." &
notif_pid=$!
# Escape special characters for JSON
escape_json() {
local str="$1"
str="${str//\\/\\\\}"
str="${str//\"/\\\"}"
str="${str//$'\n'/\\n}"
str="${str//$'\r'/\\r}"
str="${str//$'\t'/\\t}"
printf '%s' "$str"
}
escaped_input=$(escape_json "$input")
escaped_system=$(escape_json "$SYSTEM_PROMPT")
# Build JSON payload
json_payload=$(
cat <<EOF
{
"model": "$MODEL",
"messages": [
{
"role": "system",
"content": "$escaped_system"
},
{
"role": "user",
"content": "$escaped_input"
}
],
"temperature": 0.7
}
EOF
)
# Make API request
response=$(curl -s -X POST "$API_URL" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $OPENROUTER_API_KEY" \
-H "HTTP-Referer: https://github.com/sudacode/scripts" \
-H "X-Title: Japanese Learning Assistant" \
-d "$json_payload")
# Close loading notification
pkill -f "notify-send.*Processing.*Analyzing" 2>/dev/null
# Check for errors
if [[ -z "$response" ]]; then
zenity --error --text="No response from API" --title="Error" 2>/dev/null
exit 1
fi
# Parse response and extract content using Python (handles Unicode properly)
result=$(echo "$response" | python3 -c "
import json
import sys
try:
data = json.load(sys.stdin)
if 'error' in data:
err = data['error'].get('message', 'Unknown error')
print(f'ERROR:{err}', end='')
sys.exit(1)
content = data.get('choices', [{}])[0].get('message', {}).get('content', '')
if not content:
print('ERROR:Failed to parse API response', end='')
sys.exit(1)
# Decode any unicode escape sequences in the content
try:
content = content.encode('utf-8').decode('unicode_escape').encode('latin-1').decode('utf-8')
except:
pass # Keep original if decoding fails
print(content, end='')
except json.JSONDecodeError as e:
print(f'ERROR:Invalid JSON response: {e}', end='')
sys.exit(1)
except Exception as e:
print(f'ERROR:{e}', end='')
sys.exit(1)
")
# Check for errors from Python parsing
if [[ "$result" == ERROR:* ]]; then
error_msg="${result#ERROR:}"
zenity --error --text="$error_msg" --title="Error" 2>/dev/null
exit 1
fi
content="$result"
if [[ -z "$content" ]]; then
zenity --error --text="Empty response from API" --title="Error" 2>/dev/null
exit 1
fi
# Display result in zenity
zenity --text-info \
--title="Japanese Analysis" \
--width=800 \
--height=600 \
--font="monospace" \
<<<"$content" 2>/dev/null

View File

@@ -0,0 +1,7 @@
#!/usr/bin/env bash
tmpfile=$(mktemp /tmp/screenshot-XXXXXX.png)
grim -g "$(hyprctl activewindow -j | jq -r '.at[0],.at[1],.size[0],.size[1]' | tr '\n' ' ' | awk '{print $1","$2" "$3"x"$4}')" "$tmpfile"
wl-copy < "$tmpfile"
notify-send -i "$tmpfile" "Screenshot of active window copied to clipboard"
rm -f "$tmpfile"

View File

@@ -1,7 +1,7 @@
#!/bin/sh
#!/usr/bin/env bash
music_dir="/jellyfin/music"
previewdir="$XDG_CONFIG_HOME/ncmpcpp/previews"
music_dir="/truenas/jellyfin/music"
previewdir="${XDG_CONFIG_HOME:-$HOME/.config}/ncmpcpp/previews"
filename="$(mpc --format "$music_dir"/%file% current)"
previewname="$previewdir/$(mpc --format %album% current | base64).png"

View File

@@ -1,4 +1,4 @@
#!/bin/sh
#!/usr/bin/env bash
if pgrep -af "waybar -c /home/sudacode/.config/waybar/catppuccin-macchiato/config-battery.jsonc -s /home/sudacode/.config/waybar/catppuccin-macchiato/style.css" ||
pgrep -af "waybar -c /home/sudacode/.config/waybar/catppuccin-macchiato/config.jsonc -s /home/sudacode/.config/waybar/catppuccin-macchiato/style.css"; then
@@ -9,7 +9,7 @@ fi
BASE_DIR="$HOME/.config/waybar/catppuccin-macchiato"
NODE_NAME="$(hyprctl systeminfo | grep -i "node name" | sed 's/Node name: //')"
if [[ "$NODE_NAME" = "sc-arch" ]]; then
if [[ "$NODE_NAME" = "sc-arch" || "$NODE_NAME" = "cachypc" ]]; then
CONFIG="$BASE_DIR/config.jsonc"
else
CONFIG="$BASE_DIR/config-laptop.jsonc"