commit 98cf6301848a21d4e1133fdcabb49e6484a99467 Author: teraflops Date: Wed May 28 18:27:10 2025 +0200 waybar diff --git a/.config/waybar/config.jsonc b/.config/waybar/config.jsonc new file mode 100644 index 0000000..b7421ee --- /dev/null +++ b/.config/waybar/config.jsonc @@ -0,0 +1,404 @@ +{ + "layer": "top", + "position": "top", + "margin-top": 1, + "margin-left": 1, + "margin-right": 1, + "margin-bottom": 1, + "height": 54, + "spacing": 0, + "modules-left": [ + "custom/launcher", + "hyprland/workspaces", + "cpu", +// "temperature", + "custom/temps", + "power-profiles-daemon", + "memory", +// "custom/updates", + "custom/fans", + "custom/weather" +// "custom/mpris-buttons" + ], + "modules-center": [ + "custom/calendar_clock" + ], + "modules-right": [ + //"mpd", + //"custom/rate", + // "image#test", +// "custom/dmp_nowplaying", + "custom/eww_nowplaying", + "custom/lyrics", + "mpris", + "custom/roon", + "tray", + "custom/wf-recorder", + //"image#gpu", + "bluetooth", + "network", + "wireplumber", + "custom/pwrate", + "battery", + "custom/powermenu" + //"hyprland/language" + //"pulseaudio/slider", + ], + "custom/roon": { + "exec": "cat /tmp/waybar_roon_info.json", + "format": "{text}", + "tooltip": true, + "return-type": "json", + "signal": 3 + }, + "custom/temps": { + "exec": "~/.config/waybar/scripts/temps.sh", + "interval": 10, + "return-type": "json", + "tooltip": true, + "on-click": "~/.config/waybar/scripts/temps.sh click" +}, + + "custom/dmp_nowplaying": { + "exec": "~/.config/waybar/scripts/dmp_nowplaying.sh", + "return-type": "json", + "interval": 5, + "tooltip": true +}, +"custom/eww_nowplaying": { + "exec": "echo '🎶 '", + "interval": 3600, // No es dinámico, solo muestra el ícono + "tooltip": false, + "on-click": "~/.config/eww/scripts/toggle_nowplaying.sh" +}, + + "custom/fans": { + "exec": "~/.config/waybar/scripts/fan_speed.sh", + "format": "{text}", + "return-type": "json", + "on-click": "~/.config/waybar/scripts/fan_speed.sh next", + "on-click-right": "~/.config/waybar/scripts/toggle_fans.sh", + "interval": 5, + "tooltip": true +}, + +"custom/updates": { + "format": "{text}", + "exec": "/bin/bash /home/teraflops/.config/waybar/scripts/updates.sh", + "interval": 300, + "return-type": "json", + "tooltip": true, +}, + + + "hyprland/language": { + "format-es": "es", + "format-en": "us", + "keyboard-name": "asus-keyboard", + "keyboard-name": "melgeek-mojo68-c1-keyboard", + "keyboard-name": "mwstudio-mmkzoo65-keyboard", + "on-click": "hyprctl switchxkblayout mwstudio-mmkzoo65-keyboard next" + }, + "custom/mpris-buttons": { + "exec": "~/.config/waybar/scripts/mpris_buttons.sh", + // "interval": 1, + "return-type": "json", + "on-click": "playerctl play-pause", + "on-click-right": "playerctl next", + "on-click-middle": "playerctl previous", + "signal": 24 +}, + + "pulseaudio/slider": { + "min": 0, + "max": 100, + "orientation": "vertical" + }, + "custom/network_speed": { + "exec": "/home/teraflops/.local/bin/network_speed_client.py", + "interval": 1, + "return-type": "json", + "format": "{}", + "tooltip": true, + }, + "image#gpu": { + "exec": "/tmp/gpu.sh", + "size": 18, + // "interval": 1, + // "signal": 3, + "tooltip": true, + "on-click": "python /home/teraflops/.local/bin/comandos.py", + }, + "custom/lyrics": { + "format": "{text}", + "exec": "/home/teraflops/.config/waybar/scripts/lyrics_module.sh", + "return-type": "json", + "escape": true, + "interval": 10, + "on-empty": "hide", + //"on-click": "termite -t playlist -e ncmpcpp", + "on-click": "/home/teraflops/.config/waybar/scripts/show_cover.py", + //"on-scroll-down": "/home/teraflops/.config/waybar/scripts/lyrics_scroll.sh", + "signal": 9 + }, +"mpris": { + "format": "{title}", + "format-paused": "⏸ {title}", + "tooltip-format": "{title}\n{artist} ", + //"title-len": 20, + "artist-len": 0, + "album-len": 0, + "dynamic-len": 20, + "ellipsis": "…", + "expand": false, + "max-length": 60, + // "min-length": 25, + "tooltip": true, + //"ignored-players": ["firefox"], +}, + + + "hyprland/workspaces": { + "on-click": "activate", + "format": "{icon}", + "format-icons": { + "default": "", + "1": "", + "2": "", + "3": "", + "4": "", + "active": "󱓻", + "urgent": "󱓻", + }, + "persistent-workspaces": { + "*": 4, + }, + }, + "custom/inetspeedup": { + "format": "{}", + "exec": "/home/teraflops/.config/waybar/scripts/inetup.py", + "on-click": "/home/teraflops/.config/waybar/scripts/inetup.py toggle", + "interval": 10, + "return-type": "json", + "signal": 20, + }, + "custom/inetspeeddown": { + "format": "{}", + "exec": "/home/teraflops/.config/waybar/scripts/inetdown.py", + "on-click": "/home/teraflops/.config/waybar/scripts/inetdown.py toggle", + "interval": 10, + "return-type": "json", + "signal": 21, + }, + "custom/borg": { + //pkill -RTMIN+12 waybar + "format": "BACKUP RUNNING", + "exec": "echo '{\"class\": \"running\"}'", + "exec-if": "pgrep -x pika-backup", + "return-type": "json", + "signal": 12, + }, + "custom/wf-recorder": { + //pkill -RTMIN+11 waybar + "format": "• ", + "exec": "echo '{\"class\": \"recording\"}'", + "exec-if": "pgrep -x wf-recorder", + "return-type": "json", + "signal": 11, + }, + "custom/tailscale": { + "format": "VPN", + "exec": "echo '{\"class\": \"vpn\"}'", + "exec-if": "pgrep -x tailscaled", + "return-type": "json", + "signal": 22, + }, + "custom/pwrate": { + "exec": "/home/teraflops/.local/bin/pwrate3.sh", + "return-type": "json", + "format": "{}", + "signal": 23 + }, + "custom/rate": { + //pkill -RTMIN+15 waybar + //"exec": "/home/teraflops/.local/bin/show_rate.sh", + "exec": "python /home/teraflops/.local/bin/now_playing_tooltip.py", + //"exec": "echo '{\"text\":\"🎵\", \"tooltip\":\"Testing Tooltip\"}'", // Simple test module + "tooltip": true, + "return-type": "json", + "escape": false, + "on-click": "python /home/teraflops/.local/bin/now_playing_tooltip.py toggle", + "on-click-right": "pkill -Af now_playing_tooltip.py", + "signal": 15, + "class": "rate-module", + }, + "backlight": { + "device": "amdgpu_bl1", + "format": "{icon}", + "format-icons": [ + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + ], + }, + "custom/weather": { + "exec": "python ~/.config/waybar/scripts/weather.py", + "restart-interval": 300, + "return-type": "json", + // "on-click": "xdg-open https://weather.com/en-IN/weather/today/l/$(location_id)" + // "format-alt": "{alt}", + }, + "bluetooth": { + "format": "", + "format-connected": "", + "format-connected-battery": "󰂴 ", + "tooltip-format": "{controller_alias}\t{controller_address}\n\n{num_connections} connected", + "tooltip-format-connected": "{controller_alias}\t{controller_address}\n\n{num_connections} connected\n\n{device_enumerate}", + "tooltip-format-enumerate-connected": "{device_alias}\t{device_address}", + "tooltip-format-enumerate-connected-battery": "{device_alias}\t{device_address}\t{device_battery_percentage}%", + "on-click": "/home/teraflops/.config/waybar/scripts/bt_menu.py", + }, + "hyprland/window": { + "max-length": 60, + "format": "{}", + "separate-outputs": true, + }, + "temperature": { + "hwmon-path": [ + "/sys/class/thermal/thermal_zone0/temp" + ], + "critical-threshold": 80, + "format-critical": "{temperatureC}°C", + "format": "{temperatureC}°C", + }, + "memory": { + "interval": 3, + "format": "  {}% ", + "max-length": 10, + }, + "cpu": { + "interval": 1, + "format": "{icon} {usage:>2}%", + "format-icons": [ + "􏘭 ", + "􏘬 ", + "􏘩 ", + "􏘪 ", + "􏘫 " + ], + }, + "power-profiles-daemon": { + "format": "{icon}", + "tooltip-format": "Power profile: {profile}\nDriver: {driver}", + "tooltip": true, + "format-icons": { + "default": "BAL", + "performance": "PERF", + "balanced": "BAL", + "power-saver": "PSAV", + }, + }, + "custom/powermenu": { + "format": "⏻ ", + "tooltip": false, + "on-click": "/home/teraflops/.config/waybar/scripts/wlogout.sh", + }, + "tray": { + "icon-size": 18, + "spacing": 10, + }, + "custom/calendar_clock": { + "exec": "python3 /home/teraflops/.local/bin/list_events_v5_json.py", + "interval": 120, + "tooltip": true, + "return-type": "json", + }, + "network": { + "format-wifi": "{icon}", + "format-disconnected": "󰤭 ", + "format-icons": [ + "󰤯 ", + "󰤟 ", + "󰤢 ", + "󰤥 ", + "󰤨 " + ], + "tooltip-format-wifi": "{essid}", + "tooltip-format-disconnected": "Disconnected", + "nospacing": 1, + "on-click": "/home/teraflops/.config/waybar/scripts/wifi_menu.py", + }, + //"battery": { + // "bat": "BAT1", // Usa el identificador correcto de tu batería + //"interval": 60, + //"format": "{icon}", + //"tooltip": false, + //"format-icons": ["", "", "", "", ""], + //"menu": "on-click", + //"menu-file": "/home/teraflops/.config/waybar/menu/battery_menu.xml", + //"menu-actions": { + // "view_details": "notify send 1", + //"check_health": "notify send 2", + //"activate_saving_mode": "notify send 3", + //}, + "battery": { + "interval": 60, + "states": { + "warning": 30, + "critical": 15, + }, + "format": "{icon}", + "format-charging": " ", + "format-plugged": "{icon}", + "format-icons": [ + " ", + " ", + " ", + " ", + " " + ], + "max-length": 18, + "menu": "on-click", + "menu-file": "/home/teraflops/.config/waybar/menu/battery_menu.xml", + "menu-actions": { + "view_details": "/home/teraflops/.config/waybar/scripts/view_battery_details_yad.sh", + "check_health": "/home/teraflops/.config/waybar/scripts/check_battery_health.sh", + }, + }, + "wireplumber": { + "format": "{icon}", + "nospacing": 1, + "tooltip-format": "Volume : {volume}%", + "format-muted": "󰝟 ", + "format-icons": { + "headphone": " ", + "default": [ + "󰕿 ", + "󰖀 ", + "󰕾 " + ], + }, + "on-click": "/home/teraflops/.config/waybar/scripts/volume_menu.py", + "on-click-right": "pavucontrol", + "on-update": "/usr/bin/pkill -RTMIN+23 waybar", + // "on-update": "/home/teraflops/.local/bin/show_rate.sh", + "scroll-step": 3, + }, + "custom/launcher": { + "format": "  ", + "tooltip": false, + "on-click": "/home/teraflops/.config/rofi/launchers/misc/launcher.sh &", + }, +} diff --git a/.config/waybar/config.jsonc_bicolor b/.config/waybar/config.jsonc_bicolor new file mode 100644 index 0000000..10e6503 --- /dev/null +++ b/.config/waybar/config.jsonc_bicolor @@ -0,0 +1,386 @@ +{ + "layer": "top", + "position": "top", + "margin-top": 1, + "margin-left": 1, + "margin-right": 1, + "margin-bottom": 1, + "height": 54, + "spacing": 0, + "modules-left": [ + "custom/launcher", + "hyprland/workspaces", + "cpu", + "temperature", + "power-profiles-daemon", + "memory", + "custom/fans", + "custom/weather" +// "custom/updates" +// "custom/mpris-buttons" + ], + "modules-center": [ + "custom/calendar_clock" + ], + "modules-right": [ + //"mpd", + //"custom/rate", + // "image#test", + "custom/lyrics", + "mpris", + "custom/roon", + "tray", + "custom/wf-recorder", + //"image#gpu", + "bluetooth", + "network", + "wireplumber", + "custom/pwrate", + "battery", + "custom/powermenu" + //"hyprland/language" + //"pulseaudio/slider", + ], + "custom/roon": { + "exec": "cat /tmp/waybar_roon_info.json", + "format": "{text}", + "tooltip": true, + "return-type": "json", + "signal": 3 + }, +"custom/fans": { + "exec": "~/.config/waybar/scripts/fan_speed.sh", + "format": "{text}", + "markup": "pango", + "return-type": "json", + "on-click": "~/.config/waybar/scripts/fan_speed.sh next", + "on-click-right": "~/.config/waybar/scripts/toggle_fans.sh", + "interval": 5, + "tooltip": true +}, + + + "custom/updates": { + "format": "📦 {}", + "exec": "~/.config/waybar/scripts/updates.sh", + "interval": 18, + "return-type": "json", + "tooltip": true, + "tooltip-format": "{full_text}" +}, + + "hyprland/language": { + "format-es": "es", + "format-en": "us", + "keyboard-name": "asus-keyboard", + "keyboard-name": "melgeek-mojo68-c1-keyboard", + "keyboard-name": "mwstudio-mmkzoo65-keyboard", + "on-click": "hyprctl switchxkblayout mwstudio-mmkzoo65-keyboard next" + }, + "custom/mpris-buttons": { + "exec": "~/.config/waybar/scripts/mpris_buttons.sh", + // "interval": 1, + "return-type": "json", + "on-click": "playerctl play-pause", + "on-click-right": "playerctl next", + "on-click-middle": "playerctl previous", + "signal": 24 +}, + + "pulseaudio/slider": { + "min": 0, + "max": 100, + "orientation": "vertical" + }, + "custom/network_speed": { + "exec": "/home/teraflops/.local/bin/network_speed_client.py", + "interval": 1, + "return-type": "json", + "format": "{}", + "tooltip": true, + }, + "image#gpu": { + "exec": "/tmp/gpu.sh", + "size": 18, + // "interval": 1, + // "signal": 3, + "tooltip": true, + "on-click": "python /home/teraflops/.local/bin/comandos.py", + }, + "custom/lyrics": { + "format": "{text}", + "exec": "/home/teraflops/.config/waybar/scripts/lyrics_module.sh", + "return-type": "json", + "escape": true, + "interval": 10, + "on-empty": "hide", + //"on-click": "termite -t playlist -e ncmpcpp", + "on-click": "/home/teraflops/.config/waybar/scripts/show_cover.py", + //"on-scroll-down": "/home/teraflops/.config/waybar/scripts/lyrics_scroll.sh", + "signal": 9 + }, +"mpris": { + "format": "{title}", + "format-paused": "⏸ {title}", + "tooltip-format": "{title}\n{artist} ", + //"title-len": 20, + "artist-len": 0, + "album-len": 0, + "dynamic-len": 20, + "ellipsis": "…", + "expand": false, + "max-length": 60, + // "min-length": 25, + "tooltip": true, + //"ignored-players": ["firefox"], +}, + + + "hyprland/workspaces": { + "on-click": "activate", + "format": "{icon}", + "format-icons": { + "default": "", + "1": "", + "2": "", + "3": "", + "4": "", + "active": "󱓻", + "urgent": "󱓻", + }, + "persistent-workspaces": { + "*": 4, + }, + }, + "custom/inetspeedup": { + "format": "{}", + "exec": "/home/teraflops/.config/waybar/scripts/inetup.py", + "on-click": "/home/teraflops/.config/waybar/scripts/inetup.py toggle", + "interval": 10, + "return-type": "json", + "signal": 20, + }, + "custom/inetspeeddown": { + "format": "{}", + "exec": "/home/teraflops/.config/waybar/scripts/inetdown.py", + "on-click": "/home/teraflops/.config/waybar/scripts/inetdown.py toggle", + "interval": 10, + "return-type": "json", + "signal": 21, + }, + "custom/borg": { + //pkill -RTMIN+12 waybar + "format": "BACKUP RUNNING", + "exec": "echo '{\"class\": \"running\"}'", + "exec-if": "pgrep -x pika-backup", + "return-type": "json", + "signal": 12, + }, + "custom/wf-recorder": { + //pkill -RTMIN+11 waybar + "format": "• ", + "exec": "echo '{\"class\": \"recording\"}'", + "exec-if": "pgrep -x wf-recorder", + "return-type": "json", + "signal": 11, + }, + "custom/tailscale": { + "format": "VPN", + "exec": "echo '{\"class\": \"vpn\"}'", + "exec-if": "pgrep -x tailscaled", + "return-type": "json", + "signal": 22, + }, + "custom/pwrate": { + "exec": "/home/teraflops/.local/bin/pwrate3.sh", + "return-type": "json", + "format": "{}", + "signal": 23 + }, + "custom/rate": { + //pkill -RTMIN+15 waybar + //"exec": "/home/teraflops/.local/bin/show_rate.sh", + "exec": "python /home/teraflops/.local/bin/now_playing_tooltip.py", + //"exec": "echo '{\"text\":\"🎵\", \"tooltip\":\"Testing Tooltip\"}'", // Simple test module + "tooltip": true, + "return-type": "json", + "escape": false, + "on-click": "python /home/teraflops/.local/bin/now_playing_tooltip.py toggle", + "on-click-right": "pkill -Af now_playing_tooltip.py", + "signal": 15, + "class": "rate-module", + }, + "backlight": { + "device": "amdgpu_bl1", + "format": "{icon}", + "format-icons": [ + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + ], + }, + "custom/weather": { + "exec": "python ~/.config/waybar/scripts/weather.py", + "restart-interval": 300, + "return-type": "json", + // "on-click": "xdg-open https://weather.com/en-IN/weather/today/l/$(location_id)" + // "format-alt": "{alt}", + }, + "bluetooth": { + "format": "", + "format-connected": "", + "format-connected-battery": "󰂴 ", + "tooltip-format": "{controller_alias}\t{controller_address}\n\n{num_connections} connected", + "tooltip-format-connected": "{controller_alias}\t{controller_address}\n\n{num_connections} connected\n\n{device_enumerate}", + "tooltip-format-enumerate-connected": "{device_alias}\t{device_address}", + "tooltip-format-enumerate-connected-battery": "{device_alias}\t{device_address}\t{device_battery_percentage}%", + "on-click": "/home/teraflops/.config/waybar/scripts/bt_menu.py", + }, + "hyprland/window": { + "max-length": 60, + "format": "{}", + "separate-outputs": true, + }, + "temperature": { + "hwmon-path": [ + "/sys/class/thermal/thermal_zone0/temp" + ], + "critical-threshold": 80, + "format": "cpu: {temperatureC}°C", + "format-critical": "cpu: {temperatureC}°C" + }, +"memory": { + "interval": 3, + "format": " {}%", + "markup": "pango", + "max-length": 10 +}, + +"cpu": { + "interval": 1, + "format": "cpu: {usage}%", + "format-icons": [ + "􏘭 ", + "􏘬 ", + "􏘩 ", + "􏘪 ", + "􏘫 " + ], + "markup": "pango" +}, + + "power-profiles-daemon": { + "format": "{icon}", + "tooltip-format": "Power profile: {profile}\nDriver: {driver}", + "tooltip": true, + "format-icons": { + "default": "BAL", + "performance": "PERF", + "balanced": "BAL", + "power-saver": "PSAV", + }, + }, + "custom/powermenu": { + "format": "⏻ ", + "tooltip": false, + "on-click": "/home/teraflops/.config/waybar/scripts/wlogout.sh", + }, + "tray": { + "icon-size": 18, + "spacing": 10, + }, + "custom/calendar_clock": { + "exec": "python3 /home/teraflops/.local/bin/list_events_v5_json.py", + "interval": 120, + "tooltip": true, + "return-type": "json", + }, + "network": { + "format-wifi": "{icon}", + "format-disconnected": "󰤭 ", + "format-icons": [ + "󰤯 ", + "󰤟 ", + "󰤢 ", + "󰤥 ", + "󰤨 " + ], + "tooltip-format-wifi": "{essid}", + "tooltip-format-disconnected": "Disconnected", + "nospacing": 1, + "on-click": "/home/teraflops/.config/waybar/scripts/wifi_menu.py", + }, + //"battery": { + // "bat": "BAT1", // Usa el identificador correcto de tu batería + //"interval": 60, + //"format": "{icon}", + //"tooltip": false, + //"format-icons": ["", "", "", "", ""], + //"menu": "on-click", + //"menu-file": "/home/teraflops/.config/waybar/menu/battery_menu.xml", + //"menu-actions": { + // "view_details": "notify send 1", + //"check_health": "notify send 2", + //"activate_saving_mode": "notify send 3", + //}, + "battery": { + "interval": 60, + "states": { + "warning": 30, + "critical": 15, + }, + "format": "{icon}", + "format-charging": " ", + "format-plugged": "{icon}", + "format-icons": [ + " ", + " ", + " ", + " ", + " " + ], + "max-length": 18, + "menu": "on-click", + "menu-file": "/home/teraflops/.config/waybar/menu/battery_menu.xml", + "menu-actions": { + "view_details": "/home/teraflops/.config/waybar/scripts/view_battery_details_yad.sh", + "check_health": "/home/teraflops/.config/waybar/scripts/check_battery_health_yad.sh", + }, + }, + "wireplumber": { + "format": "{icon}", + "nospacing": 1, + "tooltip-format": "Volume : {volume}%", + "format-muted": "󰝟 ", + "format-icons": { + "headphone": " ", + "default": [ + "󰕿 ", + "󰖀 ", + "󰕾 " + ], + }, + "on-click": "/home/teraflops/.config/waybar/scripts/volume_menu.py", + "on-click-right": "pavucontrol", + "on-update": "/usr/bin/pkill -RTMIN+23 waybar && /usr/bin/pkill -RTMIN+24 waybar ", + // "on-update": "/home/teraflops/.local/bin/show_rate.sh", + "scroll-step": 3, + }, + "custom/launcher": { + "format": "  ", + "tooltip": false, + "on-click": "/home/teraflops/.config/rofi/launchers/misc/launcher.sh &", + }, +} diff --git a/.config/waybar/menu/battery_menu.xml b/.config/waybar/menu/battery_menu.xml new file mode 100644 index 0000000..868fb1a --- /dev/null +++ b/.config/waybar/menu/battery_menu.xml @@ -0,0 +1,16 @@ + + + + + + Ver Detalles de la Batería + + + + + Verificar Salud de la Batería + + + + + diff --git a/.config/waybar/menu/unique.xml b/.config/waybar/menu/unique.xml new file mode 100644 index 0000000..525ec26 --- /dev/null +++ b/.config/waybar/menu/unique.xml @@ -0,0 +1,52 @@ + + + + + + + + Ver Detalles de la Batería + + + + + Verificar Salud de la Batería + + + + + Activar Modo de Ahorro de Batería + + + + + + + + + + Mute/Unmute + + + + + Crear Sink + + + + + Establecer Sink Predeterminado + + + + + Abrir Pavucontrol + + + + + + + + + diff --git a/.config/waybar/menu/wireplumber_menu.xml b/.config/waybar/menu/wireplumber_menu.xml new file mode 100644 index 0000000..f52f520 --- /dev/null +++ b/.config/waybar/menu/wireplumber_menu.xml @@ -0,0 +1,26 @@ + + + + + + Ver Detalles de la Batería + + + + + Verificar Salud de la Batería + + + + + Crear Sink + + + + + Abrir Pavucontrol + + + + + diff --git a/.config/waybar/scripts/__pycache__/lastfm_cover_downloader.cpython-312.pyc b/.config/waybar/scripts/__pycache__/lastfm_cover_downloader.cpython-312.pyc new file mode 100644 index 0000000..f15836c Binary files /dev/null and b/.config/waybar/scripts/__pycache__/lastfm_cover_downloader.cpython-312.pyc differ diff --git a/.config/waybar/scripts/__pycache__/lastfm_cover_downloader.cpython-313.pyc b/.config/waybar/scripts/__pycache__/lastfm_cover_downloader.cpython-313.pyc new file mode 100644 index 0000000..9b63e79 Binary files /dev/null and b/.config/waybar/scripts/__pycache__/lastfm_cover_downloader.cpython-313.pyc differ diff --git a/.config/waybar/scripts/__pycache__/tiling.d.er b/.config/waybar/scripts/__pycache__/tiling.d.er new file mode 100644 index 0000000..99c651b --- /dev/null +++ b/.config/waybar/scripts/__pycache__/tiling.d.er @@ -0,0 +1,23 @@ +##[pylyzer] failed /home/teraflops/.config/waybar/scripts/tiling.py 1731075378 1588 +.___v_desugar_1 = pyimport "" +. = pyimport "" +.Gtk: Never +.subprocess = pyimport "" + + +.gi = pyimport "__init__" +.__init__ = pyimport "__init__" + + +.layouts: Dict!({{"vertical"}: {"hyprctl dispatch layout vert"}, {"horizontal"}: {"hyprctl dispatch layout horiz"}, {"floating"}: {"hyprctl dispatch layout float"}, {"tabbed"}: {"hyprctl dispatch layout tabbed"}}) +.LayoutSelector: ClassType +.LayoutSelector <: Never +.LayoutSelector.__call__: () -> tiling.LayoutSelector +.LayoutSelector.create_button: (self: tiling.LayoutSelector, label: Obj, image_path: Obj, command: Obj, grid: Obj, row: Obj, col: Obj) -> Never +.LayoutSelector.on_button_clicked: (self: tiling.LayoutSelector, widget: Obj, command: Obj) -> Never + +tiling = pyimport "tiling" +.win: .LayoutSelector + + + diff --git a/.config/waybar/scripts/activate_saving_mode.sh b/.config/waybar/scripts/activate_saving_mode.sh new file mode 100755 index 0000000..9309625 --- /dev/null +++ b/.config/waybar/scripts/activate_saving_mode.sh @@ -0,0 +1,5 @@ +#!/bin/bash +# Reducir el brillo al 50% (asegúrate de tener instalada xbacklight) +xbacklight -set 50 +zenity --info --text="Modo de Ahorro Activado:\nBrillo reducido al 50%." --title="Modo de Ahorro Activado" + diff --git a/.config/waybar/scripts/apply_grid_layout.sh b/.config/waybar/scripts/apply_grid_layout.sh new file mode 100755 index 0000000..a9e2b9e --- /dev/null +++ b/.config/waybar/scripts/apply_grid_layout.sh @@ -0,0 +1,37 @@ +#!/bin/bash + +# Obtiene las ventanas abiertas en el espacio de trabajo actual +windows=$(hyprctl clients -j | jq -r '.[] | .address') + +# Cuenta las ventanas abiertas +window_count=$(echo "$windows" | wc -l) + +# Define las posiciones y tamaños de cada ventana según la cuadrícula +case $window_count in + 1) + hyprctl dispatch movewindow 50% 50% resizewindowpixel 100% 100% ;; + 2) + hyprctl dispatch focuswindow $(echo "$windows" | sed -n 1p) + hyprctl dispatch movewindow 0 0 resizewindowpixel 50% 100% + hyprctl dispatch focuswindow $(echo "$windows" | sed -n 2p) + hyprctl dispatch movewindow 50% 0 resizewindowpixel 50% 100% ;; + 3) + # Configura tres ventanas, dos arriba y una en la mitad inferior + hyprctl dispatch focuswindow $(echo "$windows" | sed -n 1p) + hyprctl dispatch movewindow 0 0 resizewindowpixel 50% 50% + hyprctl dispatch focuswindow $(echo "$windows" | sed -n 2p) + hyprctl dispatch movewindow 50% 0 resizewindowpixel 50% 50% + hyprctl dispatch focuswindow $(echo "$windows" | sed -n 3p) + hyprctl dispatch movewindow 0 50% resizewindowpixel 100% 50% ;; + 4) + # Configura cuatro ventanas en una cuadrícula de 2x2 + hyprctl dispatch focuswindow $(echo "$windows" | sed -n 1p) + hyprctl dispatch movewindow 0 0 resizewindowpixel 50% 50% + hyprctl dispatch focuswindow $(echo "$windows" | sed -n 2p) + hyprctl dispatch movewindow 50% 0 resizewindowpixel 50% 50% + hyprctl dispatch focuswindow $(echo "$windows" | sed -n 3p) + hyprctl dispatch movewindow 0 50% resizewindowpixel 50% 50% + hyprctl dispatch focuswindow $(echo "$windows" | sed -n 4p) + hyprctl dispatch movewindow 50% 50% resizewindowpixel 50% 50% ;; +esac + diff --git a/.config/waybar/scripts/battery_menu.py b/.config/waybar/scripts/battery_menu.py new file mode 100755 index 0000000..92c66bd --- /dev/null +++ b/.config/waybar/scripts/battery_menu.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 + +import sys +import subprocess + + +def view_details(): + try: + details = subprocess.check_output( + ["upower", "-i", "/org/freedesktop/UPower/devices/battery_BAT1"], + text=True + ) + # Extraer información relevante si es necesario + subprocess.run(["notify-send", "Detalles de la Batería", details]) + except subprocess.CalledProcessError as e: + subprocess.run( + ["notify-send", "Error", "No se pudieron obtener los detalles de la batería."]) + + +def check_health(): + try: + health = subprocess.check_output( + ["upower", "-i", "/org/freedesktop/UPower/devices/battery_BAT1"], + text=True + ) + # Extraer información específica sobre la salud + energy_full = "" + for line in health.splitlines(): + if "energy-full:" in line: + energy_full = line.split(":", 1)[1].strip() + break + if energy_full: + subprocess.run(["notify-send", "Salud de la Batería", + f"La capacidad máxima actual es {energy_full}."]) + else: + subprocess.run(["notify-send", "Salud de la Batería", + "No se pudo determinar la capacidad máxima."]) + except subprocess.CalledProcessError as e: + subprocess.run( + ["notify-send", "Error", "No se pudo verificar la salud de la batería."]) + + +def save_mode(): + try: + # Reducir el brillo al 50% (asegúrate de tener xbacklight instalado) + subprocess.run(["xbacklight", "-set", "50"]) + subprocess.run( + ["notify-send", "Modo de Ahorro Activado", "Brillo reducido al 50%."]) + except Exception as e: + subprocess.run( + ["notify-send", "Error", "No se pudo activar el modo de ahorro."]) + + +def main(): + if len(sys.argv) < 2: + subprocess.run( + ["notify-send", "Error", "No se proporcionó ninguna acción."]) + sys.exit(1) + + action = sys.argv[1] + + if action == "view_details": + view_details() + elif action == "check_health": + check_health() + elif action == "save_mode": + save_mode() + else: + subprocess.run( + ["notify-send", "Error", f"Acción desconocida: {action}"]) + sys.exit(1) + + +if __name__ == "__main__": + main() + diff --git a/.config/waybar/scripts/bt_menu.py b/.config/waybar/scripts/bt_menu.py new file mode 100755 index 0000000..24ac509 --- /dev/null +++ b/.config/waybar/scripts/bt_menu.py @@ -0,0 +1,304 @@ +#!/usr/bin/env python3 +from gi.repository import Gtk, GLib, Gio +import threading +from pydbus import SystemBus +import subprocess + + +class BluetoothMenu(Gtk.Application): + def __init__(self): + super().__init__(application_id="org.example.BluetoothMenu") + self.bus = SystemBus() + self.adapter = self.bus.get("org.bluez", "/org/bluez/hci0") + self.object_manager = self.bus.get("org.bluez", "/") + self.window = None + self.device_window = None + self.scanning = False + self.discovered_devices = {} + + def do_activate(self): + if not self.window: + self.window = Gtk.ApplicationWindow(application=self) + self.window.set_default_size(200, 150) + self.window.set_title("Bluetooth Menu") + self.vbox = Gtk.Box( + orientation=Gtk.Orientation.VERTICAL, spacing=6) + + if hasattr(self.window, "set_child"): + self.window.set_child(self.vbox) + else: + self.window.add(self.vbox) + + self.build_menu() + self.update_bluetooth_buttons() + + self.window.present() + + def build_menu(self): + self.item_scan = Gtk.Button(label='Escanear Dispositivos') + self.item_scan.connect('clicked', self.scan_and_show_devices) + self.add_to_box(self.vbox, self.item_scan) + + self.item_on = Gtk.Button(label='Encender Bluetooth') + self.item_on.connect('clicked', self.turn_on_bluetooth) + self.add_to_box(self.vbox, self.item_on) + + self.item_off = Gtk.Button(label='Apagar Bluetooth') + self.item_off.connect('clicked', self.turn_off_bluetooth) + self.add_to_box(self.vbox, self.item_off) + + self.item_send_file = Gtk.Button(label='Enviar Archivo') + self.item_send_file.connect('clicked', self.send_file_dialog) + self.add_to_box(self.vbox, self.item_send_file) + + item_quit = Gtk.Button(label='Cerrar') + item_quit.connect('clicked', self.exit_app) + self.add_to_box(self.vbox, item_quit) + + def add_to_box(self, box, widget): + if hasattr(box, "append"): + box.append(widget) + else: + box.pack_start(widget, True, True, 0) + + def send_file_dialog(self, _): + dialog = Gtk.FileChooserDialog( + title="Selecciona un archivo para enviar", + transient_for=self.window, + action=Gtk.FileChooserAction.OPEN, + modal=True + ) + dialog.add_buttons( + "Cancelar", Gtk.ResponseType.CANCEL, + "Abrir", Gtk.ResponseType.OK + ) + + dialog.connect("response", self.on_file_chosen) + dialog.show() + + def on_file_chosen(self, dialog, response): + if response == Gtk.ResponseType.OK: + file = dialog.get_file() # Obtenemos el archivo directamente + if file: + file_path = file.get_path() # Obtenemos la ruta del archivo + + # Oculta la ventana principal antes de abrir la ventana de selección de dispositivos + self.window.hide() + + # Llama a la función para seleccionar el dispositivo y enviar el archivo + self.select_device_for_file_transfer(file_path) + + dialog.destroy() + + def select_device_for_file_transfer(self, file_path): + if self.device_window: + self.device_window.destroy() + + self.device_window = Gtk.Window(title="Seleccionar dispositivo") + self.device_window.set_default_size(300, 400) + vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6) + self.device_window.set_child(vbox) + + devices = self.get_discovered_devices() + for name, address in devices: + button = Gtk.Button(label=f"{name} ({address})") + button.connect("clicked", self.send_file_to_device, + file_path, address) + vbox.append(button) + + # Añade un botón "Cerrar" al final de la ventana de selección de dispositivos + close_button = Gtk.Button(label="Cerrar") + close_button.connect("clicked", lambda _: self.device_window.destroy()) + vbox.append(close_button) + + # Muestra la ventana de selección de dispositivos + self.device_window.show() + + # Restaura la ventana principal al cerrar la ventana de selección de dispositivos + self.device_window.connect("destroy", lambda w: self.window.show()) + + def send_file_to_device(self, button, file_path, address): + try: + # Ejecuta bluetooth-sendto con la dirección del dispositivo y el archivo + result = subprocess.run( + ['bluetooth-sendto', '--device=' + address, file_path], + capture_output=True, + text=True + ) + + # Verifica si se produjo un error en la ejecución + if result.returncode == 0: + self.show_notification(f"Archivo enviado a {address}") + else: + self.show_notification( + f"Error al enviar archivo: {result.stderr}") + + except Exception as e: + self.show_notification(f"Error al ejecutar bluetooth-sendto: {e}") + + def update_bluetooth_buttons(self): + powered = self.adapter.Powered + self.item_on.set_sensitive(not powered) + self.item_off.set_sensitive(powered) + + def scan_and_show_devices(self, _): + try: + # Inicia el escaneo solo si no está en proceso + if not self.scanning: + self.adapter.StartDiscovery() + self.scanning = True + self.show_notification( + "Escaneo de dispositivos Bluetooth iniciado") + + # Muestra la ventana de dispositivos en tiempo real + self.show_device_list_window() + except Exception as e: + self.show_notification(f"Error iniciando el escaneo: {e}") + + def show_device_list_window(self): + if self.device_window: + self.device_window.destroy() + + self.device_window = Gtk.Window(title="Dispositivos Disponibles") + self.device_window.set_default_size(400, 500) + self.device_window.set_resizable(True) + self.device_window.set_transient_for(self.window) + + scrolled_window = Gtk.ScrolledWindow() + scrolled_window.set_policy( + Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + scrolled_window.set_min_content_height(400) + + self.device_list_vbox = Gtk.Box( + orientation=Gtk.Orientation.VERTICAL, spacing=10) + if hasattr(scrolled_window, "set_child"): + scrolled_window.set_child(self.device_list_vbox) + else: + scrolled_window.add(self.device_list_vbox) + + vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10) + self.add_to_box(vbox, scrolled_window) + + refresh_button = Gtk.Button(label="Refrescar") + refresh_button.connect("clicked", self.refresh_device_list) + self.add_to_box(vbox, refresh_button) + + close_button = Gtk.Button(label="Cerrar") + close_button.connect("clicked", self.stop_scan_and_close) + self.add_to_box(vbox, close_button) + + if hasattr(self.device_window, "set_child"): + self.device_window.set_child(vbox) + else: + self.device_window.add(vbox) + + self.device_window.present() + + # Asegura que el temporizador se ejecute para actualizar la lista en tiempo real + if not hasattr(self, 'update_devices_timer'): + self.update_devices_timer = GLib.timeout_add_seconds( + 2, self.update_device_list) + + def get_discovered_devices(self): + devices = [] + managed_objects = self.object_manager.GetManagedObjects() + for path, interfaces in managed_objects.items(): + if "org.bluez.Device1" in interfaces: + device_properties = interfaces["org.bluez.Device1"] + if device_properties.get("Connected") or device_properties.get("Paired"): + # Omitir dispositivos emparejados o ya conectados + continue + name = device_properties.get("Name", "Desconocido") + address = device_properties.get("Address") + devices.append((name, address)) + return devices + + def refresh_device_list(self, _): + self.discovered_devices.clear() + if self.device_list_vbox and self.device_window: + self.clear_box_children(self.device_list_vbox) + self.show_notification("Lista de dispositivos actualizada") + + def clear_box_children(self, box): + if hasattr(box, 'get_children'): + for child in box.get_children(): + box.remove(child) + else: + child = box.get_first_child() + while child: + next_child = child.get_next_sibling() + box.remove(child) + child = next_child + + def update_device_list(self): + devices = self.get_discovered_devices() + for name, address in devices: + if address not in self.discovered_devices: + self.discovered_devices[address] = name + if self.device_window and self.device_list_vbox: + button = Gtk.Button(label=f"{name} ({address})") + button.connect("clicked", self.pair_trust_connect, address) + self.add_to_box(self.device_list_vbox, button) + return True + + def stop_scan_and_close(self, _): + if self.device_window: + self.device_window.destroy() + self.device_window = None + self.discovered_devices.clear() + + def get_discovered_devices(self): + devices = [] + managed_objects = self.object_manager.GetManagedObjects() + for path, interfaces in managed_objects.items(): + if "org.bluez.Device1" in interfaces: + device_properties = interfaces["org.bluez.Device1"] + name = device_properties.get("Name", "Desconocido") + address = device_properties.get("Address") + devices.append((name, address)) + return devices + + def pair_trust_connect(self, button, address): + threading.Thread(target=self.pair_trust_connect_thread, + args=(address,)).start() + + def pair_trust_connect_thread(self, address): + device_path = f"/org/bluez/hci0/dev_{address.replace(':', '_')}" + device = self.bus.get("org.bluez", device_path) + try: + device.Pair() + device.Trusted = True + device.Connect() + self.show_notification(f"Conectado a {address}") + except Exception as e: + self.show_notification(f"Error al conectar con {address}: {e}") + + def turn_on_bluetooth(self, _): + self.adapter.Powered = True + self.show_notification("Bluetooth Encendido") + self.update_bluetooth_buttons() + + def turn_off_bluetooth(self, _): + self.adapter.Powered = False + self.show_notification("Bluetooth Apagado") + self.update_bluetooth_buttons() + + def exit_app(self, _): + if self.scanning: + self.adapter.StopDiscovery() + self.scanning = False + if hasattr(self, 'update_devices_timer'): + GLib.source_remove(self.update_devices_timer) + self.quit() + + def show_notification(self, message): + subprocess.run(['notify-send', message]) + + +def main(): + app = BluetoothMenu() + app.run() + + +if __name__ == "__main__": + main() diff --git a/.config/waybar/scripts/check_battery_health.sh b/.config/waybar/scripts/check_battery_health.sh new file mode 100755 index 0000000..ebeb3fe --- /dev/null +++ b/.config/waybar/scripts/check_battery_health.sh @@ -0,0 +1,4 @@ +#!/bin/bash +HEALTH=$(upower -i /org/freedesktop/UPower/devices/battery_BAT1 | grep "energy-full") +zenity --info --text="Salud de la Batería:\nLa capacidad máxima actual es $HEALTH." --title="Salud de la Batería" + diff --git a/.config/waybar/scripts/config.json b/.config/waybar/scripts/config.json new file mode 100644 index 0000000..99fa098 --- /dev/null +++ b/.config/waybar/scripts/config.json @@ -0,0 +1,9 @@ +{ + "roonstate": { + "tokens": { + "4000ff7f-2284-4810-9e5d-a0a7b1f7bc1a": "a979118e-66c9-48a0-8eae-c6899d5dca81", + "0754ce41-74ac-44bb-a9d5-e47d8f377f46": "77528fe8-8f9e-422b-84e7-794bfe5d8739" + }, + "paired_core_id": "0754ce41-74ac-44bb-a9d5-e47d8f377f46" + } +} \ No newline at end of file diff --git a/.config/waybar/scripts/discovery.py b/.config/waybar/scripts/discovery.py new file mode 100644 index 0000000..710c8be --- /dev/null +++ b/.config/waybar/scripts/discovery.py @@ -0,0 +1,51 @@ +from roonapi import RoonApi +import time +import subprocess + +appinfo = { + "extension_id": "python_roon_waybar", + "display_name": "Waybar Roon Extension", + "display_version": "1.0.0", + "publisher": "gregd", + "email": "mygreat@emailaddress.com", +} + +# Lee el core_id y el token desde los archivos +try: + core_id = open("/home/teraflops/apikeys/roon_core_id").read().strip() + token = open("/home/teraflops/apikeys/roon_core_token").read().strip() +except FileNotFoundError: + print("Error: core_id o token no encontrados. Debes autorizar usando discovery.py primero.") + exit() + +# Conéctate a Roon usando el core_id y el token guardados +roonapi = RoonApi(appinfo, token, "192.168.1.37", 9330, True) + + +def my_state_callback(event, changed_ids): + """Llamado cuando algo cambia en Roon.""" + for zone_id in changed_ids: + zone = roonapi.zones[zone_id] + if zone['state'] == 'playing': + track_name = zone['now_playing']['three_line']['line1'] + artist_name = zone['now_playing']['three_line']['line2'] + print(f"Canción en reproducción: {track_name} - {artist_name}") + + # Actualiza Waybar usando un archivo de texto o variable + with open("/tmp/waybar_roon_track", "w") as f: + f.write(f"{track_name} - {artist_name}") + + # Envía notificación de la canción + subprocess.run([ + "notify-send", "Roon Now Playing", + f"{track_name} - {artist_name}" + ]) + + +# Registra el callback para recibir actualizaciones +roonapi.register_state_callback(my_state_callback) + +# Mantén el script corriendo para escuchar cambios +while True: + time.sleep(10) + diff --git a/.config/waybar/scripts/dmp_nowplaying.sh b/.config/waybar/scripts/dmp_nowplaying.sh new file mode 100755 index 0000000..2cddc53 --- /dev/null +++ b/.config/waybar/scripts/dmp_nowplaying.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +IP="192.168.1.140" +ENDPOINT="http://$IP:9529/ZidooMusicControl/v2/getState" + +data=$(curl -s --max-time 2 "$ENDPOINT") + +if [[ -n "$data" ]]; then + artist=$(echo "$data" | jq -r '.playingMusic.artist') + title=$(echo "$data" | jq -r '.playingMusic.title') + status=$(echo "$data" | jq -r '.everSoloPlayInfo.playStatus') + + # Detalles extra para el tooltip + bitrate=$(echo "$data" | jq -r '.playingMusic.bitrate') + samplerate=$(echo "$data" | jq -r '.playingMusic.sampleRate') + quality=$(echo "$data" | jq -r '.playingMusic.audioQuality') + + # Escapar caracteres especiales + artist="${artist//&/&}" + title="${title//&/&}" + bitrate="${bitrate//&/&}" + samplerate="${samplerate//&/&}" + quality="${quality//&/&}" + + tooltip="${artist} - ${title}\n${quality} | ${samplerate} | ${bitrate}" + + if [[ "$status" == "1" && "$artist" != "null" && "$title" != "null" ]]; then + echo "{\"text\": \"🎵 $artist - $title\", \"tooltip\": \"$tooltip\"}" + else + echo "{\"text\": \"⏸️ Pausado o sin música\", \"tooltip\": \"No se está reproduciendo música.\"}" + fi +else + echo "{\"text\": \"⚠️ DMP-A6 desconectado\", \"tooltip\": \"No se pudo conectar al Eversolo.\"}" +fi + diff --git a/.config/waybar/scripts/down.txt b/.config/waybar/scripts/down.txt new file mode 100644 index 0000000..527da67 --- /dev/null +++ b/.config/waybar/scripts/down.txt @@ -0,0 +1 @@ +⬇ 548.60 Mbps diff --git a/.config/waybar/scripts/fan_speed.sh b/.config/waybar/scripts/fan_speed.sh new file mode 100755 index 0000000..296780c --- /dev/null +++ b/.config/waybar/scripts/fan_speed.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash + +STATE_FILE="/tmp/fancycle_state" +HWMON_PATH="/sys/class/hwmon/hwmon3" # Ajusta según tu caso + +# Si no existe el archivo de estado, inícialo en 1 (fan1) +if [[ ! -f "$STATE_FILE" ]]; then + echo 1 > "$STATE_FILE" +fi + +# Si el script es invocado con el argumento "next", incrementa el estado +if [[ "$1" == "next" ]]; then + current_state=$(cat "$STATE_FILE") + # Suponiendo que tienes 3 ventiladores y quieres ciclar 1->2->3->1->... + next_state=$(( (current_state % 3) + 1 )) + echo "$next_state" > "$STATE_FILE" +fi + +# Lee el estado actual +state=$(cat "$STATE_FILE") + +# Lee la info real de tus ventiladores +# Lee la info real de tus ventiladores +fan1_label=$(cat "$HWMON_PATH/fan1_label" 2>/dev/null | sed 's/_fan//I' | tr '[:lower:]' '[:upper:]') +fan1_speed=$(cat "$HWMON_PATH/fan1_input" 2>/dev/null) + +fan2_label=$(cat "$HWMON_PATH/fan2_label" 2>/dev/null | sed 's/_fan//I' | tr '[:lower:]' '[:upper:]') +fan2_speed=$(cat "$HWMON_PATH/fan2_input" 2>/dev/null) + +fan3_label=$(cat "$HWMON_PATH/fan3_label" 2>/dev/null | sed 's/_fan//I' | tr '[:lower:]' '[:upper:]') +fan3_speed=$(cat "$HWMON_PATH/fan3_input" 2>/dev/null) + + +# Prepara el tooltip con TODOS los ventiladores +tooltip="${fan1_label}: ${fan1_speed} RPM +${fan2_label}: ${fan2_speed} RPM +${fan3_label}: ${fan3_speed} RPM" + +# Según el "estado", decide cuál ventilador se muestra en "text" +case "$state" in + 1) text="${fan1_label}: ${fan1_speed} RPM" ;; + 2) text="${fan2_label}: ${fan2_speed} RPM" ;; + 3) text="${fan3_label}: ${fan3_speed} RPM" ;; +esac + +# Usa jq para generar JSON +jq -c -n \ + --arg text "$text" \ + --arg tooltip "$tooltip" \ + '{ "text": $text, "tooltip": $tooltip }' + diff --git a/.config/waybar/scripts/fan_speed.sh.v1 b/.config/waybar/scripts/fan_speed.sh.v1 new file mode 100755 index 0000000..b326bbf --- /dev/null +++ b/.config/waybar/scripts/fan_speed.sh.v1 @@ -0,0 +1,49 @@ +#!/usr/bin/env bash + +STATE_FILE="/tmp/fancycle_state" +HWMON_PATH="/sys/class/hwmon/hwmon3" # Ajusta según tu caso + +# Si no existe el archivo de estado, inícialo en 1 (fan1) +if [[ ! -f "$STATE_FILE" ]]; then + echo 1 > "$STATE_FILE" +fi + +# Si el script es invocado con el argumento "next", incrementa el estado +if [[ "$1" == "next" ]]; then + current_state=$(cat "$STATE_FILE") + # Suponiendo que tienes 3 ventiladores y quieres ciclar 1->2->3->1->... + next_state=$(( (current_state % 3) + 1 )) + echo "$next_state" > "$STATE_FILE" +fi + +# Lee el estado actual +state=$(cat "$STATE_FILE") + +# Lee la info real de tus ventiladores +fan1_label=$(cat "$HWMON_PATH/fan1_label" 2>/dev/null) +fan1_speed=$(cat "$HWMON_PATH/fan1_input" 2>/dev/null) + +fan2_label=$(cat "$HWMON_PATH/fan2_label" 2>/dev/null) +fan2_speed=$(cat "$HWMON_PATH/fan2_input" 2>/dev/null) + +fan3_label=$(cat "$HWMON_PATH/fan3_label" 2>/dev/null) +fan3_speed=$(cat "$HWMON_PATH/fan3_input" 2>/dev/null) + +# Prepara el tooltip con TODOS los ventiladores +tooltip="${fan1_label}: ${fan1_speed} RPM +${fan2_label}: ${fan2_speed} RPM +${fan3_label}: ${fan3_speed} RPM" + +# Según el "estado", decide cuál ventilador se muestra en "text" +case "$state" in + 1) text="${fan1_label}: ${fan1_speed} RPM" ;; + 2) text="${fan2_label}: ${fan2_speed} RPM" ;; + 3) text="${fan3_label}: ${fan3_speed} RPM" ;; +esac + +# Usa jq para generar JSON +jq -c -n \ + --arg text "$text" \ + --arg tooltip "$tooltip" \ + '{ "text": $text, "tooltip": $tooltip }' + diff --git a/.config/waybar/scripts/fan_speed.sh_bicolor b/.config/waybar/scripts/fan_speed.sh_bicolor new file mode 100755 index 0000000..8053df5 --- /dev/null +++ b/.config/waybar/scripts/fan_speed.sh_bicolor @@ -0,0 +1,51 @@ +#!/usr/bin/env bash + +STATE_FILE="/tmp/fancycle_state" +HWMON_PATH="/sys/class/hwmon/hwmon3" # Ajusta según tu caso + +# Si no existe el archivo de estado, inícialo en 1 (fan1) +if [[ ! -f "$STATE_FILE" ]]; then + echo 1 > "$STATE_FILE" +fi + +# Si el script es invocado con el argumento "next", incrementa el estado +if [[ "$1" == "next" ]]; then + current_state=$(cat "$STATE_FILE") + next_state=$(( (current_state % 3) + 1 )) + echo "$next_state" > "$STATE_FILE" +fi + +state=$(cat "$STATE_FILE") + +# Lee info de los ventiladores +fan1_label=$(cat "$HWMON_PATH/fan1_label" 2>/dev/null | sed 's/_fan//I') +fan1_speed=$(cat "$HWMON_PATH/fan1_input" 2>/dev/null) + +fan2_label=$(cat "$HWMON_PATH/fan2_label" 2>/dev/null | sed 's/_fan//I') +fan2_speed=$(cat "$HWMON_PATH/fan2_input" 2>/dev/null) + +fan3_label=$(cat "$HWMON_PATH/fan3_label" 2>/dev/null | sed 's/_fan//I') +fan3_speed=$(cat "$HWMON_PATH/fan3_input" 2>/dev/null) + +tooltip="${fan1_label}: ${fan1_speed} RPM +${fan2_label}: ${fan2_speed} RPM +${fan3_label}: ${fan3_speed} RPM" + +# Determinar texto con color según estado +case "$state" in + 1) + text="${fan1_label}: ${fan1_speed} RPM" + ;; + 2) + text="${fan2_label}: ${fan2_speed} RPM" + ;; + 3) + text="${fan3_label}: ${fan3_speed} RPM" + ;; +esac + +jq -c -n \ + --arg text "$text" \ + --arg tooltip "$tooltip" \ + '{ "text": $text, "tooltip": $tooltip }' + diff --git a/.config/waybar/scripts/get_current_track.py b/.config/waybar/scripts/get_current_track.py new file mode 100755 index 0000000..cf72fcb --- /dev/null +++ b/.config/waybar/scripts/get_current_track.py @@ -0,0 +1,49 @@ +import time +from roonapi import RoonApi + +appinfo = { + "extension_id": "python_roon_waybar", + "display_name": "Waybar Roon Extension", + "display_version": "1.0.0", + "publisher": "gregd", + "email": "mygreat@emailaddress.com", +} + +core_id = open("/home/teraflops/apikeys/roon_core_id").read() +token = open("/home/teraflops/apikeys/roon_core_token").read() + +roonapi = RoonApi(appinfo, token, "192.168.1.37", "9330", True) + + +def send_album_cover_notification(zone): + """Envía una notificación con la carátula del álbum.""" + album_cover_url = zone['now_playing']['image_key'] + response = requests.get( + f"http://{roonapi.host}:{roonapi.port}/image/{album_cover_url}") + + if response.status_code == 200: + cover_path = "/tmp/roon_album_cover.jpg" + with open(cover_path, "wb") as f: + f.write(response.content) + + # Envía la notificación usando notify-send + subprocess.run([ + "notify-send", "-i", cover_path, "Now Playing", + f"{zone['now_playing']['three_line']['line1'] + } - {zone['now_playing']['three_line']['line2']}" + ]) + + +def get_current_track(): + """Obtiene la canción actual que está sonando en Roon.""" + zones = roonapi.zones + for zone in zones.values(): + if zone['state'] == 'playing': + track_name = zone['now_playing']['three_line']['line1'] + artist_name = zone['now_playing']['three_line']['line2'] + return f"{track_name} - {artist_name}" + return "No track playing" + + +if __name__ == "__main__": + print(get_current_track()) diff --git a/.config/waybar/scripts/get_lyrics.py b/.config/waybar/scripts/get_lyrics.py new file mode 100755 index 0000000..554ccf1 --- /dev/null +++ b/.config/waybar/scripts/get_lyrics.py @@ -0,0 +1,70 @@ +import sys +import requests +import json +import os + +with open(os.path.expanduser("~/apikeys/musixmatch"), "r") as file: + musixmatch_token = file.read().strip() + +def get_lyrics(artist, song_title): + search_url = "https://api.musixmatch.com/ws/1.1/track.search" + params = { + "q_track_artist": f"{artist} {song_title}", + "apikey": musixmatch_token, + "s_track_rating": "desc", + "f_has_lyrics": "1" + } + + response = requests.get(search_url, params=params) + if response.status_code != 200: + return f"Error al buscar la canción: {response.status_code}" + + data = response.json() + tracks = data.get("message", {}).get("body", {}).get("track_list", []) + if not tracks: + return "No results found for this track." + + track_id = None + for t in tracks: + t_title = t["track"]["track_name"].lower() + t_artist = t["track"]["artist_name"].lower() + if song_title.lower() in t_title and artist.lower() in t_artist: + track_id = t["track"]["track_id"] + print("\n Filtered search result:") + print("Track found:", t["track"]["track_name"]) + print("Artist foung:", t["track"]["artist_name"]) + print("Track ID:", track_id) + print("---") + break + + if not track_id: + track_id = tracks[0]["track"]["track_id"] + print("\n generic search result:") + print("Title found:", tracks[0]["track"]["track_name"]) + print("Artist found:", tracks[0]["track"]["artist_name"]) + print("Track ID:", track_id) + print("---") + + lyrics_url = "https://api.musixmatch.com/ws/1.1/track.lyrics.get" + lyrics_params = { + "track_id": track_id, + "apikey": musixmatch_token + } + + lyrics_response = requests.get(lyrics_url, params=lyrics_params) + if lyrics_response.status_code != 200: + return f"Error obtaining lyrics: {lyrics_response.status_code}" + + lyrics_data = lyrics_response.json() + lyrics = lyrics_data.get("message", {}).get("body", {}).get("lyrics", {}).get("lyrics_body", "") + return lyrics or "no lyrics available." + +if len(sys.argv) != 3: + print("Usage: python get_lyrics.py ") + sys.exit(1) + +artist = sys.argv[1] +song_title = sys.argv[2] +lyrics = get_lyrics(artist, song_title) +print(lyrics) + diff --git a/.config/waybar/scripts/get_lyrics.sh b/.config/waybar/scripts/get_lyrics.sh new file mode 100755 index 0000000..e615339 --- /dev/null +++ b/.config/waybar/scripts/get_lyrics.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +# Rutas y archivos temporales +CURRENT_TRACK_FILE="/tmp/current_track.txt" +LYRICS_FILE="/tmp/lyrics.txt" +LOG_FILE="/tmp/lyrics_error.log" + +# Obtener la información actual del track usando `playerctl` +TRACK_INFO=$(playerctl metadata --format "{{artist}} - {{title}}" 2>>"$LOG_FILE") + +# Validar si hay información del track +if [ -z "$TRACK_INFO" ]; then + echo "No track info available" > "$LYRICS_FILE" + exit 1 +fi + +# Extraer artista y título +ARTIST=$(echo "$TRACK_INFO" | cut -d '-' -f 1 | xargs) +TITLE=$(echo "$TRACK_INFO" | cut -d '-' -f 2 | xargs) + +# Usar Python para obtener las letras +python3 ~/.config/waybar/scripts/get_lyrics.py "$ARTIST" "$TITLE" > "$LYRICS_FILE" + +# Verificar si el archivo de letras está vacío +if [ ! -s "$LYRICS_FILE" ]; then + echo "Letras no encontradas para $ARTIST - $TITLE" > "$LYRICS_FILE" +fi + diff --git a/.config/waybar/scripts/get_power_profile.sh b/.config/waybar/scripts/get_power_profile.sh new file mode 100755 index 0000000..f121228 --- /dev/null +++ b/.config/waybar/scripts/get_power_profile.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +profile = $(asusctl profile -p |awk '{print $4}') + diff --git a/.config/waybar/scripts/gpu.sh b/.config/waybar/scripts/gpu.sh new file mode 100755 index 0000000..acb374e --- /dev/null +++ b/.config/waybar/scripts/gpu.sh @@ -0,0 +1,2 @@ +#!/bin/sh +echo "/home/teraflops/Icons/nvidia.png" diff --git a/.config/waybar/scripts/inetdown.py b/.config/waybar/scripts/inetdown.py new file mode 100755 index 0000000..4bcc7d6 --- /dev/null +++ b/.config/waybar/scripts/inetdown.py @@ -0,0 +1,64 @@ +#!/usr/bin/python3 + +import json +import os +import sys + +def read_value_from_file(file_path): + try: + with open(file_path, 'r') as file: + value = file.read().strip() + return value + except Exception as e: + return f"Error: {e}" + +def read_click_state(state_file): + try: + if os.path.exists(state_file): + with open(state_file, 'r') as file: + state = file.read().strip() + return state == "clicked" + else: + return False + except Exception as e: + return False + +def toggle_click_state(state_file): + try: + if read_click_state(state_file): + with open(state_file, 'w') as file: + file.write("unclicked") + else: + with open(state_file, 'w') as file: + file.write("clicked") + except Exception as e: + pass + +def generate_waybar_json(value, clicked): + if clicked: + data = { + "text": value, + #"tooltip": f"Download Speed: {value}", + #"class": "custom" + } + else: + data = { + "text": "DL", + "tooltip": f"Dpwnload Speed: {value}", + "class": "custom" + } + return json.dumps(data) + +if __name__ == "__main__": + file_path = "/home/teraflops/.config/waybar/scripts/down.txt" + state_file = "/tmp/waybar_tooltip_state" + value = read_value_from_file(file_path) + + # Check for the toggle argument + if len(sys.argv) > 1 and sys.argv[1] == "toggle": + toggle_click_state(state_file) + + clicked = read_click_state(state_file) + waybar_json = generate_waybar_json(value, clicked) + print(waybar_json) + diff --git a/.config/waybar/scripts/inetup.py b/.config/waybar/scripts/inetup.py new file mode 100755 index 0000000..43a24f8 --- /dev/null +++ b/.config/waybar/scripts/inetup.py @@ -0,0 +1,64 @@ +#!/usr/bin/python3 + +import json +import os +import sys + +def read_value_from_file(file_path): + try: + with open(file_path, 'r') as file: + value = file.read().strip() + return value + except Exception as e: + return f"Error: {e}" + +def read_click_state(state_file): + try: + if os.path.exists(state_file): + with open(state_file, 'r') as file: + state = file.read().strip() + return state == "clicked" + else: + return False + except Exception as e: + return False + +def toggle_click_state(state_file): + try: + if read_click_state(state_file): + with open(state_file, 'w') as file: + file.write("unclicked") + else: + with open(state_file, 'w') as file: + file.write("clicked") + except Exception as e: + pass + +def generate_waybar_json(value, clicked): + if clicked: + data = { + "text": value, + #"tooltip": f"Upload Speed: {value}", + #"class": "custom" + } + else: + data = { + "text": "UP", + "tooltip": f"Upload Speed: {value}", + "class": "custom" + } + return json.dumps(data) + +if __name__ == "__main__": + file_path = "/home/teraflops/.config/waybar/scripts/up.txt" + state_file = "/tmp/waybar_tooltip_state" + value = read_value_from_file(file_path) + + # Check for the toggle argument + if len(sys.argv) > 1 and sys.argv[1] == "toggle": + toggle_click_state(state_file) + + clicked = read_click_state(state_file) + waybar_json = generate_waybar_json(value, clicked) + print(waybar_json) + diff --git a/.config/waybar/scripts/lastfm_cover_downloader.py b/.config/waybar/scripts/lastfm_cover_downloader.py new file mode 100755 index 0000000..957992a --- /dev/null +++ b/.config/waybar/scripts/lastfm_cover_downloader.py @@ -0,0 +1,154 @@ +# lastfm_cover_downloader.py + +import os +import requests +import urllib.parse +from pathlib import Path +import logging + +# Configuración básica de logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +# Obtener la ruta de la clave de API de Last.fm desde una variable de entorno +API_KEY_FILE = os.environ.get( + 'LASTFM_API_KEY_PATH', os.path.expandvars('$HOME/apikeys/lastfm')) + + +def get_lastfm_api_key(): + """Leer la clave de API de Last.fm desde un archivo.""" + try: + with open(API_KEY_FILE, 'r') as file: + api_key = file.readline().strip() + logger.info(f"Successfully read Last.fm API key from { + API_KEY_FILE}") + return api_key + except FileNotFoundError: + logger.error(f"API key file '{API_KEY_FILE}' not found.") + raise + except Exception as e: + logger.error(f"Error reading API key: {e}") + raise + + +# Obtener la clave de API de Last.fm +LASTFM_API_KEY = get_lastfm_api_key() +if not LASTFM_API_KEY: + raise RuntimeError("Last.fm API key could not be loaded.") + + +def get_largest_cover_image(data): + """Seleccionar la imagen de portada más grande disponible en la respuesta de Last.fm.""" + if 'album' in data and 'image' in data['album']: + # Definir el orden de prioridad de tamaños + size_priority = ["mega", "extralarge", "large", "medium", "small"] + for size in size_priority: + for img in data['album']['image']: + if img["size"] == size and img["#text"]: + logger.info(f"Using {size} cover: {img['#text']}") + return img["#text"] + return None + + +def download_image(cover_url, output_path): + """Descargar la imagen de portada desde una URL y guardarla en la ruta especificada.""" + try: + logger.info(f"Downloading image from {cover_url} to {output_path}") + response = requests.get(cover_url, stream=True) + if response.status_code == 200: + with open(output_path, 'wb') as f: + for chunk in response.iter_content(1024): + f.write(chunk) + logger.info(f"Image downloaded to {output_path}") + return output_path + else: + logger.error(f"Failed to download image. HTTP Status: { + response.status_code}") + raise Exception(f"HTTP Status {response.status_code}") + except Exception as e: + logger.error(f"Error downloading image: {e}") + raise + + +def download_cover_from_lastfm(artist, album, album_dir): + """Descargar la portada del álbum desde Last.fm y guardarla en el directorio del álbum.""" + try: + logger.info(f"Fetching cover for '{album}' by { + artist} from Last.fm...") + # URL de la API de Last.fm + url = f"http://ws.audioscrobbler.com/2.0/?method=album.getinfo&api_key={LASTFM_API_KEY}&artist={ + urllib.parse.quote(artist)}&album={urllib.parse.quote(album)}&format=json" + response = requests.get(url) + if response.status_code == 200: + data = response.json() + cover_url = get_largest_cover_image(data) + if cover_url: + # Asegurar que el directorio existe + album_dir.mkdir(parents=True, exist_ok=True) + local_cover_path = album_dir / "cover.jpg" # Guardar como 'cover.jpg' + download_image(cover_url, local_cover_path) + logger.info(f"Downloaded cover to {local_cover_path}") + return local_cover_path + logger.warning(f"No cover found for '{album}' by {artist} on Last.fm.") + raise Exception(f"No cover found for '{album}' by {artist}.") + except Exception as e: + logger.error(f"Error fetching cover from Last.fm: {e}") + raise + + +def cover_exists(album_dir): + """Verificar si 'cover.jpg' o 'Cover.jpg' existe en el directorio del álbum.""" + cover_path_lower = album_dir / "cover.jpg" + cover_path_upper = album_dir / "Cover.jpg" + exists = cover_path_lower.exists() or cover_path_upper.exists() + logger.info(f"Checking cover existence in '{album_dir}': { + 'Found' if exists else 'Not found'}") + return exists + + +def get_local_album_dir(xesam_url, music_base_path=os.environ.get('MUSIC_LIBRARY_PATH', os.path.expandvars('$HOME/media/all'))): + """Extraer el directorio del álbum desde la ruta `xesam:url`.""" + if not xesam_url: + logger.warning( + "xesam:url is empty or missing. Skipping cover detection.") + return None + + try: + logger.info(f"Raw xesam:url value: {xesam_url}") + + # Convertir la URL xesam en una ruta de archivo local + local_path = urllib.parse.unquote(xesam_url).replace("file://", "") + logger.info(f"Converted local path: {local_path}") + + # Eliminar '/trackXXXX' si es una ruta de archivo CUE + if "/track" in local_path and ".cue" in local_path: + local_path = local_path.split("/track")[0] + logger.info(f"Stripped CUE path: {local_path}") + + # Si la ruta no es absoluta, agregar el directorio base de música + track_path = Path(local_path) + if not track_path.is_absolute(): + track_path = Path(music_base_path) / track_path + logger.info(f"Updated to absolute path: {track_path}") + + # Verificar si la ruta resultante existe + if not track_path.exists(): + logger.warning(f"The path '{track_path}' does not exist.") + return None + + # Manejar archivos CUE: Subir al directorio del álbum real + if track_path.suffix == ".cue": + return track_path.parent + + # Manejo estándar de rutas + if track_path.is_file(): + return track_path.parent + else: + logger.warning(f"Path is not a valid file: {track_path}") + except Exception as e: + logger.error(f"Error extracting local album directory: {e}") + return None + diff --git a/.config/waybar/scripts/live.py b/.config/waybar/scripts/live.py new file mode 100755 index 0000000..8507ebb --- /dev/null +++ b/.config/waybar/scripts/live.py @@ -0,0 +1,32 @@ +import asyncio +from msgraph.core import GraphClient +from azure.identity import DeviceCodeCredential + +# Reemplaza con tu CLIENT_ID +credential = DeviceCodeCredential( + client_id="b8d7f3bf-e9a7-4352-b379-833e99a79733" +) +scopes = ['Mail.Read'] +graph_client = GraphClient(credential=credential, scopes=scopes) + + +async def get_mail(): + response = await graph_client.get( + '/me/messages', + params={ + '$select': 'sender,subject', + '$filter': 'isRead eq false' + } + ) + if response.status_code == 200: + mail = response.json() + for message in mail.get('value', []): + sender = message['sender']['emailAddress']['address'] + subject = message['subject'] + print(f"De: {sender}, Asunto: {subject}") + else: + print(f"Error: {response.status_code}") + print(response.json()) + +asyncio.run(get_mail()) + diff --git a/.config/waybar/scripts/lyrics_module.sh b/.config/waybar/scripts/lyrics_module.sh new file mode 100755 index 0000000..cb3a304 --- /dev/null +++ b/.config/waybar/scripts/lyrics_module.sh @@ -0,0 +1,95 @@ +#!/bin/bash + +#set -x # Activa el modo de depuración + +# Paths y archivos temporales +CURRENT_TRACK_FILE="/tmp/current_track.txt" +LYRICS_FILE="/tmp/lyrics.txt" +LOG_FILE="/tmp/lyrics_error.log" +STATE_FILE="/tmp/lyrics_state.txt" + +# Función para limpiar el título de la canción +clean_title() { + local title="$1" + # Eliminar prefijos como "B3 ", "A1 ", etc. + title=$(echo "$title" | sed -E 's/^[A-Za-z0-9]{1,3}[[:space:]]+//') + # Eliminar caracteres especiales como "$", "#", etc. + title=$(echo "$title" | tr -d '$#@!%^&*()[]{}<>?/\\|`~') + # Eliminar espacios adicionales al inicio y al final + title=$(echo "$title" | sed 's/^ *//;s/ *$//') + echo "$title" +} + +# Obtener información de la pista actual +TRACK_INFO=$(playerctl metadata --format "{{artist}} - {{title}}" 2>>"$LOG_FILE") +STATUS=$(playerctl status 2>>"$LOG_FILE") +echo "STATUS: '$STATUS'" >> "$LOG_FILE" + +# Si no hay información de pista o el estado no es 'Playing', salimos +if [ -z "$TRACK_INFO" ] || [ "$STATUS" != "Playing" ]; then + jq -c -n '{"text":"","tooltip":""}' + exit 0 +fi + +echo "TRACK_INFO: '$TRACK_INFO'" >> "$LOG_FILE" + +if [ -z "$TRACK_INFO" ]; then + echo "No track info available" > "$LYRICS_FILE" + exit 1 +fi + +# Extraer artista y título +ARTIST=$(echo "$TRACK_INFO" | cut -d '-' -f 1 | sed 's/^ *//;s/ *$//') +TITLE=$(echo "$TRACK_INFO" | cut -d '-' -f 2- | sed 's/^ *//;s/ *$//') + +echo "ARTIST: '$ARTIST'" >> "$LOG_FILE" +echo "TITLE Original: '$TITLE'" >> "$LOG_FILE" + +# Limpiar el título de la canción +CLEAN_TITLE=$(clean_title "$TITLE") +echo "TITLE Limpio: '$CLEAN_TITLE'" >> "$LOG_FILE" + +# Verificar si la pista ha cambiado +if [ "$TRACK_INFO" != "$(cat "$CURRENT_TRACK_FILE" 2>/dev/null)" ]; then + echo "$TRACK_INFO" > "$CURRENT_TRACK_FILE" + + # Obtener letras usando el script Python + lyrics=$(python3 ~/.config/waybar/scripts/get_lyrics.py "$ARTIST" "$CLEAN_TITLE" 2>>"$LOG_FILE") + + echo "lyrics: '$lyrics'" >> "$LOG_FILE" + + if [[ "$lyrics" == "Letras no encontradas." ]]; then + lyrics="Letras no encontradas para $ARTIST - $CLEAN_TITLE" + fi + + echo "$lyrics" > "$LYRICS_FILE" +else + lyrics=$(cat "$LYRICS_FILE") +fi + +# Leer el estado +if [ -f "$STATE_FILE" ]; then + STATE=$(cat "$STATE_FILE") +else + STATE="full" +fi + +# Ajustar las letras según el estado +if [ "$STATE" = "second_half" ]; then + total_lines=$(echo "$lyrics" | wc -l) + half_line=$(( (total_lines + 1) / 2 )) + lyrics=$(echo "$lyrics" | tail -n +$half_line) +fi + +# Verificar si el player activo es Firefox +PLAYER_NAME=$(playerctl -a metadata --format '{{playerName}}' 2>>"$LOG_FILE" | head -n 1) +echo "PLAYER_NAME: '$PLAYER_NAME'" >> "$LOG_FILE" + +if [[ "$PLAYER_NAME" == "firefox" ]]; then + jq -c -n '{"text":"","tooltip":""}' + exit 0 +fi + +# Generar JSON para Waybar +jq -c -n --arg text "🎵" --arg tooltip "$lyrics" '{"text":$text,"tooltip":$tooltip}' + diff --git a/.config/waybar/scripts/lyrics_scroll.sh b/.config/waybar/scripts/lyrics_scroll.sh new file mode 100755 index 0000000..d0c9bde --- /dev/null +++ b/.config/waybar/scripts/lyrics_scroll.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +STATE_FILE="/tmp/lyrics_state.txt" + +if [ ! -f "$STATE_FILE" ]; then + echo "second_half" > "$STATE_FILE" +else + STATE=$(cat "$STATE_FILE") + if [ "$STATE" = "second_half" ]; then + echo "full" > "$STATE_FILE" + else + echo "second_half" > "$STATE_FILE" + fi +fi + +# Send signal to Waybar to update the module immediately +pkill -SIGRTMIN+9 waybar + diff --git a/.config/waybar/scripts/mpd_playlist.sh b/.config/waybar/scripts/mpd_playlist.sh new file mode 100755 index 0000000..9a383ad --- /dev/null +++ b/.config/waybar/scripts/mpd_playlist.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +# Obtener las primeras 5 canciones de la lista de reproducción actual de MPD +playlist=$(mpc playlist | head -n 5) + +# Verificar si la lista de reproducción no está vacía +if [[ -z "$playlist" ]]; then + playlist="No hay canciones en la lista de reproducción." +else + total=$(mpc playlist | wc -l) + if [[ "$total" -gt 5 ]]; then + playlist="$playlist\n...y más" + fi +fi + +# Reemplazar saltos de línea por '\n' para que el tooltip los entienda +playlist=$(echo "$playlist" | sed ':a;N;$!ba;s/\n/\\n/g') + +# Generar JSON compacto y seguro +jq -c -n --arg text "🎶" --arg tooltip "$playlist" '{"text":$text,"tooltip":$tooltip}' + + diff --git a/.config/waybar/scripts/mpris_buttons.sh b/.config/waybar/scripts/mpris_buttons.sh new file mode 100755 index 0000000..76ac70b --- /dev/null +++ b/.config/waybar/scripts/mpris_buttons.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +# Comprobar si hay algún reproductor activo +status=$(playerctl status 2>/dev/null) +if [[ $? -ne 0 ]]; then + echo "" # No mostrar nada si no hay reproductor + exit 0 +fi + +# Obtener metadatos +artist=$(playerctl metadata artist 2>/dev/null) +title=$(playerctl metadata title 2>/dev/null) +playback_status=$(playerctl status) + +# Iconos según el estado +if [[ "$playback_status" == "Playing" ]]; then + play_icon=" " +else + play_icon="" +fi + +# Botones +echo "{\"text\": \" $play_icon  \", \"tooltip\": \"$artist - $title\"}" + diff --git a/.config/waybar/scripts/roon_debug.js b/.config/waybar/scripts/roon_debug.js new file mode 100644 index 0000000..fea2b17 --- /dev/null +++ b/.config/waybar/scripts/roon_debug.js @@ -0,0 +1,119 @@ +const { RoonExtension } = require('roon-kit'); +const fs = require('fs'); +const { exec } = require('child_process'); + +const lyricsScriptPath = "/home/teraflops/.config/waybar/scripts/get_lyrics.py"; + +function log(msg) { + const timestamp = new Date().toISOString(); + const fullMsg = `[${timestamp}] ${msg}`; + console.log(fullMsg); + fs.appendFileSync('/tmp/roon_debug.log', fullMsg + '\n'); +} + +const extension = new RoonExtension({ + description: { + extension_id: 'roon-kit-now-playing', + display_name: "Roon Kit Now Playing", + display_version: "0.2.0", + publisher: 'roon-kit', + email: 'stevenic@microsoft.com', + website: 'https://github.com/Stevenic/roon-kit' + }, + RoonApiBrowse: 'not_required', + RoonApiImage: 'required', + RoonApiTransport: 'required', + subscribe_outputs: false, + subscribe_zones: true, + log_level: 'none' +}); + +function cleanTitle(title) { + return title.replace(/^[A-Za-z0-9]{1,3}\s+/, '').replace(/[$#@!%^&*(){}\[\]<>?/\\|`~]/g, '').trim(); +} + +function getLyrics(artist, title, callback) { + const cmd = `python3 ${lyricsScriptPath} "${artist}" "${title}"`; + log(`🎤 Ejecutando: ${cmd}`); + exec(cmd, (error, stdout, stderr) => { + if (error) { + log(`❌ Error en getLyrics: ${stderr}`); + callback("Letras no disponibles."); + } else { + const lyrics = stdout.trim(); + log(`✅ Letras obtenidas (${lyrics.length} chars)`); + callback(lyrics || "Letras no encontradas."); + } + }); +} + +extension.on("subscribe_zones", async (core, response, body) => { + log("🔄 Evento subscribe_zones recibido"); + log(JSON.stringify(body, null, 2)); + + const changedZones = body.zones_changed ?? []; + const addedZones = body.zones_added ?? []; + const removedZones = body.zones_removed ?? []; + + if (removedZones.length > 0) { + log("🧹 Zona eliminada, limpiando /tmp/waybar_roon_info.json"); + fs.writeFileSync('/tmp/waybar_roon_info.json', JSON.stringify({ text: '', tooltip: '' })); + exec("/usr/bin/pkill -RTMIN+3 waybar"); + } + + for (const zone of [...addedZones, ...changedZones]) { + log(`🎧 Zona: ${zone.display_name}, estado: ${zone.state}`); + if (zone.state === 'playing') { + const track = cleanTitle(zone.now_playing?.one_line?.line1 || ''); + const artist = zone.now_playing?.one_line?.line2 || ''; + const album = zone.now_playing?.three_line?.line3 || ''; + + log(`🎵 Reproduciendo: ${track} - ${artist} (${album})`); + + getLyrics(artist, track, async (lyrics) => { + const displayText = `${track} - ${artist}`; + const tooltip = `🎵 ${track}\n👤 ${artist}\n💿 ${album}\n\n${lyrics}`; + + const data = { + text: displayText, + tooltip: tooltip + }; + + log(`✍️ Escribiendo JSON para Waybar: ${JSON.stringify(data)}`); + fs.writeFileSync('/tmp/waybar_roon_info.json', JSON.stringify(data)); + exec("/usr/bin/pkill -RTMIN+3 waybar"); + + const image_key = zone.now_playing?.image_key; + if (image_key) { + try { + const imageData = await core.services.RoonApiImage.get_image(image_key, { width: 300, height: 300 }); + const coverPath = '/tmp/roon_album_cover.jpg'; + fs.writeFileSync(coverPath, imageData.image); + exec(`notify-send -i ${coverPath} "Now Playing" "${displayText}"`); + log("✅ Notificación enviada con carátula"); + } catch (error) { + log(`⚠️ Error obteniendo imagen: ${error}`); + exec(`notify-send "Now Playing" "${displayText}"`); + } + } else { + exec(`notify-send "Now Playing" "${displayText}"`); + } + }); + } + } +}); + +extension.start_discovery(); +extension.set_status('Esperando conexión al Core de Roon...'); +log("🔍 Buscando Core..."); + +(async () => { + const core = await extension.get_core(); + if (core) { + log("✅ Core emparejado correctamente"); + extension.set_status('Emparejado con el Core de Roon'); + } else { + log("❌ No se emparejó con ningún Core"); + } +})(); + diff --git a/.config/waybar/scripts/roon_track.sh b/.config/waybar/scripts/roon_track.sh new file mode 100755 index 0000000..7d9dbc9 --- /dev/null +++ b/.config/waybar/scripts/roon_track.sh @@ -0,0 +1,12 @@ +#!/bin/bash +# Script para Waybar que muestra la canción actual de Roon + +# Variables de configuración +PYTHON_SCRIPT="/home/teraflops/.config/waybar/scripts/get_current_track.py" + +# Ejecuta el script de Python y captura la salida +TRACK_INFO=$(python3 "$PYTHON_SCRIPT") + +# Imprime el nombre de la canción y el artista +echo "$TRACK_INFO" + diff --git a/.config/waybar/scripts/roon_track_writer.js b/.config/waybar/scripts/roon_track_writer.js new file mode 100644 index 0000000..6f7fb89 --- /dev/null +++ b/.config/waybar/scripts/roon_track_writer.js @@ -0,0 +1,41 @@ +const { RoonExtension } = require('roon-kit'); +const fs = require('fs'); + +const outputTrackFile = "/tmp/roon_track.txt"; + +const extension = new RoonExtension({ + description: { + extension_id: 'roon-track-writer', + display_name: "Roon Track Writer", + display_version: "0.1.0", + publisher: 'teraflops', + email: 'teraflops@example.com' + }, + RoonApiTransport: 'required', + subscribe_zones: true +}); + + +extension.on("subscribe_zones", async (core, response, body) => { + const changedZones = body.zones_changed ?? []; + const addedZones = body.zones_added ?? []; + + for (const zone of [...changedZones, ...addedZones]) { + if (zone.state === 'playing') { + const artist = zone.now_playing?.one_line?.line2 || ''; + const track = zone.now_playing?.one_line?.line1 || ''; + const line = `${artist} - ${track}`; + fs.writeFileSync(outputTrackFile, line); + console.log(`🎵 Track escrito: ${line}`); + } + } +}); + +extension.start_discovery(); +extension.set_status("Esperando conexión con Roon..."); + +(async () => { + const core = await extension.get_core(); + extension.set_status("Emparejado con el Core de Roon"); +})(); + diff --git a/.config/waybar/scripts/show_cover.py b/.config/waybar/scripts/show_cover.py new file mode 100755 index 0000000..757e21a --- /dev/null +++ b/.config/waybar/scripts/show_cover.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python3 + +import threading +from pathlib import Path +import asyncio +from lastfm_cover_downloader import download_cover_from_lastfm, cover_exists, get_local_album_dir +from dbus_next.aio import MessageBus +from dbus_next import Variant +from gi.repository import Gtk, GdkPixbuf, GLib +import os +import gi +gi.require_version("Gtk", "4.0") + +os.environ["GDK_BACKEND"] = "x11" # O "wayland" si usas Wayland + + +class CoverPanel: + def __init__(self, music_base_path): + self.music_base_path = Path(music_base_path) + self.window = Gtk.Window(title="Album Cover") + self.window.set_default_size(300, 300) + self.window.set_resizable(False) + self.window.set_decorated(False) + self.window.set_opacity(0.8) + + # Crear el widget Gtk.Picture para mostrar y escalar la imagen + self.picture = Gtk.Picture() + self.window.set_child(self.picture) + + self.window.present() + print("CoverPanel window initialized successfully.") + + def update_cover(self, cover_path): + print(f"Attempting to update cover with path: {cover_path}") + if cover_path and cover_path.exists(): + print(f"Updating cover with path: {cover_path.resolve()}") + try: + # Configurar Gtk.Picture para mostrar la imagen y escalarla automáticamente + pixbuf = GdkPixbuf.Pixbuf.new_from_file(str(cover_path)) + GLib.idle_add(self.picture.set_paintable, + Gtk.Picture.new_for_pixbuf(pixbuf).get_paintable()) + print(f"Cover panel updated with: {cover_path.resolve()}") + except Exception as e: + print(f"Error loading image: {e}") + else: + print(f"Cover path does not exist or is invalid: {cover_path}") + + def load_initial_cover(self): + """Busca y carga la última carátula disponible en la biblioteca.""" + cover_path = self.find_latest_cover() + if cover_path: + self.update_cover(cover_path) + + def find_latest_cover(self): + """Busca la carátula más reciente en la biblioteca de música.""" + for album_dir in sorted(self.music_base_path.glob("*/"), reverse=True): + if cover_exists(album_dir): + return album_dir / "cover.jpg" + return None + + +class MprisCoverHandlerWithGTK: + def __init__(self, music_base_path): + print("Inicializando MprisCoverHandlerWithGTK.") + self.music_base_path = music_base_path + self.panel = CoverPanel(music_base_path) + self.bus = None + + async def load_current_song_cover(self): + """Intenta cargar la carátula de la canción actual usando MPRIS.""" + try: + self.bus = await MessageBus().connect() + introspectable = await self.bus.introspect("org.mpris.MediaPlayer2.mpd", "/org/mpris/MediaPlayer2") + obj = self.bus.get_proxy_object( + "org.mpris.MediaPlayer2.mpd", "/org/mpris/MediaPlayer2", introspectable) + properties = obj.get_interface("org.freedesktop.DBus.Properties") + metadata_variant = await properties.call_get("org.mpris.MediaPlayer2.Player", "Metadata") + metadata = metadata_variant.value # Obtener el valor real del Variant + + track_title = metadata.get('xesam:title', Variant('s', '')).value + artist = metadata.get('xesam:artist', Variant('as', [])).value + album = metadata.get('xesam:album', Variant('s', '')).value + xesam_url = metadata.get('xesam:url', Variant('s', '')).value + + print(f"Metadata recibida: Track Title: {track_title}, Artist: { + artist}, Album: {album}, URL: {xesam_url}") + + if not isinstance(artist, list): + artist = [str(artist)] + artist = [str(a) for a in artist] + + if artist and album != 'Unknown' and xesam_url: + album_dir = get_local_album_dir( + xesam_url, self.music_base_path) + print(f"Determined album directory: {album_dir}") + + if album_dir and cover_exists(album_dir): + cover_path = album_dir / "cover.jpg" + self.panel.update_cover(cover_path) + return + except Exception as e: + print(f"Error loading current song cover: {e}") + + # Si no se encuentra la carátula de la canción actual, cargar la última carátula encontrada + print("No current song cover found. Loading last known cover.") + self.panel.load_initial_cover() + + async def on_properties_changed(self, interface, changed, invalidated): + print("on_properties_changed: Detectando cambios de propiedades MPRIS.") + if 'Metadata' in changed: + metadata = changed['Metadata'].value + + # Extraer los valores reales de cada Variant + track_title = metadata.get('xesam:title', Variant('s', '')).value + artist = metadata.get('xesam:artist', Variant('as', [])).value + album = metadata.get('xesam:album', Variant('s', '')).value + xesam_url = metadata.get('xesam:url', Variant('s', '')).value + + print(f"Metadata recibida: Track Title: {track_title}, Artist: { + artist}, Album: {album}, URL: {xesam_url}") + + if not isinstance(artist, list): + artist = [str(artist)] + artist = [str(a) for a in artist] + + if not artist or album == 'Unknown' or not xesam_url: + print(f"Skipping track '{ + track_title}' due to missing metadata.") + return + + print(f"Now playing: {track_title} by { + ', '.join(artist)} from the album '{album}'") + album_dir = get_local_album_dir(xesam_url, self.music_base_path) + print(f"Determined album directory: {album_dir}") + + if not album_dir: + print(f"Could not determine album directory for '{ + track_title}'. xesam:url: {xesam_url}") + return + + if cover_exists(album_dir): + print(f"Cover already exists in '{ + album_dir}', loading from local.") + cover_path = album_dir / "cover.jpg" + self.panel.update_cover(cover_path) + else: + print(f"No cover found in '{ + album_dir}', attempting to download.") + try: + cover_path = download_cover_from_lastfm( + artist[0], album, album_dir) + if cover_path: + self.panel.update_cover(cover_path) + except Exception as e: + print(f"Error downloading cover: {e}") + + await asyncio.sleep(0.5) + os.system("/usr/bin/pkill -RTMIN+23 waybar") + + async def main(self): + print("Starting D-Bus listener with GTK panel...") + # Cargar la carátula de la canción actual al inicio + await self.load_current_song_cover() + + introspectable = await self.bus.introspect("org.mpris.MediaPlayer2.mpd", "/org/mpris/MediaPlayer2") + obj = self.bus.get_proxy_object( + "org.mpris.MediaPlayer2.mpd", "/org/mpris/MediaPlayer2", introspectable) + properties = obj.get_interface("org.freedesktop.DBus.Properties") + await asyncio.sleep(0.5) + os.system("/usr/bin/pkill -RTMIN+23 waybar") + + properties.on_properties_changed(self.on_properties_changed) + print("D-Bus listener with GTK panel started successfully.") + await asyncio.Future() + + +if __name__ == "__main__": + print("Iniciando script show_cover.py") + MUSIC_BASE_PATH = os.environ.get( + 'MUSIC_LIBRARY_PATH', os.path.expandvars('$HOME/media/all')) + print(f"MUSIC_BASE_PATH: {MUSIC_BASE_PATH}") + + fetcher = MprisCoverHandlerWithGTK(MUSIC_BASE_PATH) + + # Ejecutar el ciclo de asyncio en un hilo separado + def run_asyncio(): + asyncio.run(fetcher.main()) + + asyncio_thread = threading.Thread(target=run_asyncio) + asyncio_thread.start() + + # Ejecutar el ciclo de GTK en el hilo principal + loop = GLib.MainLoop() + loop.run() diff --git a/.config/waybar/scripts/temps.sh b/.config/waybar/scripts/temps.sh new file mode 100755 index 0000000..014613e --- /dev/null +++ b/.config/waybar/scripts/temps.sh @@ -0,0 +1,76 @@ +#!/bin/bash + +STATE_FILE="/tmp/waybar_temp_mode" +MODES=("cpu" "igpu" "dgpu") + +# Detecta si la dGPU está realmente activa +dgpu_is_active() { + runtime_status=$(cat /sys/bus/pci/devices/0000:01:00.0/power/runtime_status 2>/dev/null) + [[ "$runtime_status" == "active" ]] +} + +get_dgpu_temp() { + if dgpu_is_active; then + nvidia-smi --query-gpu=temperature.gpu --format=csv,noheader,nounits 2>/dev/null || echo "N/A" + else + echo "N/A" + fi +} + +get_cpu_temp() { + sensors | awk '/^Tctl:/ {print $2}' | sed 's/+//;s/°C//' +} + +get_igpu_temp() { + sensors | awk '/amdgpu/ {found=1} found && /edge:/ {print $2; exit}' | sed 's/+//;s/°C//' +} + +get_nvme_temp() { + sensors | awk '/nvme/ {found=1} found && /Composite:/ {print $2; exit}' | sed 's/+//;s/°C//' +} + +get_board_temp() { + sensors | awk '/temp1:/ && /°C/ {print $2; exit}' | sed 's/+//;s/°C//' +} + +# Leer estado actual +current_mode="cpu" +[[ -f "$STATE_FILE" ]] && current_mode=$(<"$STATE_FILE") + +# Cambiar modo si se hizo clic +if [[ "$1" == "click" ]]; then + for i in "${!MODES[@]}"; do + if [[ "${MODES[$i]}" == "$current_mode" ]]; then + next_index=$(( (i + 1) % ${#MODES[@]} )) + echo "${MODES[$next_index]}" > "$STATE_FILE" + exit 0 + fi + done +fi + +# Obtener temperaturas +cpu=$(get_cpu_temp) +igpu=$(get_igpu_temp) +dgpu=$(get_dgpu_temp) +nvme=$(get_nvme_temp) +board=$(get_board_temp) + +cpu=${cpu:-N/A} +igpu=${igpu:-N/A} +dgpu=${dgpu:-N/A} +nvme=${nvme:-N/A} +board=${board:-N/A} + +# Definir ícono y valor visible +case "$current_mode" in + cpu) icon="CPU"; temp="$cpu" ;; + igpu) icon="iGPU"; temp="$igpu" ;; + dgpu) icon="dGPU"; temp="$dgpu" ;; +esac + +# Tooltip con \n y escapado correcto +tooltip=$(printf "CPU: %s°C\niGPU: %s°C\ndGPU: %s°C\nNVMe: %s°C\nBoard: %s°C" "$cpu" "$igpu" "$dgpu" "$nvme" "$board") +tooltip_escaped=$(echo "$tooltip" | jq -Rs .) + +# Salida final +echo "{\"text\": \"$icon: ${temp}°C\", \"tooltip\": ${tooltip_escaped}}" diff --git a/.config/waybar/scripts/temps.sh_solodedicada b/.config/waybar/scripts/temps.sh_solodedicada new file mode 100755 index 0000000..bc1bd31 --- /dev/null +++ b/.config/waybar/scripts/temps.sh_solodedicada @@ -0,0 +1,95 @@ +#!/bin/bash + +STATE_FILE="/tmp/waybar_temp_mode" +MODES=("cpu" "igpu" "dgpu") + +# Detecta si la dGPU está realmente activa +dgpu_is_active() { + runtime_status=$(cat /sys/bus/pci/devices/0000:01:00.0/power/runtime_status 2>/dev/null) + [[ "$runtime_status" == "active" ]] +} + +get_dgpu_temp() { + gpu_mode_file="/tmp/gpu_mode" + timestamp_file="/tmp/gpu_mode_timestamp" + + echo_na() { + echo "N/A" + } + + [[ ! -f "$gpu_mode_file" || ! -f "$timestamp_file" ]] && echo_na && return + + mode=$(< "$gpu_mode_file") + last_change=$(< "$timestamp_file") + now=$(date +%s) + diff=$((now - last_change)) + + # Solo permitir nvidia-smi si: + # 1. El modo es nvidia + # 2. Han pasado más de 5s + # 3. La dGPU está realmente activa + if [[ "$mode" == "nvidia" && "$diff" -gt 5 && $(dgpu_is_active) ]]; then + nvidia-smi --query-gpu=temperature.gpu --format=csv,noheader,nounits 2>/dev/null + else + echo_na + fi +} + +get_cpu_temp() { + sensors | awk '/^Tctl:/ {print $2}' | sed 's/+//;s/°C//' +} + +get_igpu_temp() { + sensors | awk '/amdgpu/ {found=1} found && /edge:/ {print $2; exit}' | sed 's/+//;s/°C//' +} + +get_nvme_temp() { + sensors | awk '/nvme/ {found=1} found && /Composite:/ {print $2; exit}' | sed 's/+//;s/°C//' +} + +get_board_temp() { + sensors | awk '/temp1:/ && /°C/ {print $2; exit}' | sed 's/+//;s/°C//' +} + +# Leer estado actual +current_mode="cpu" +[[ -f "$STATE_FILE" ]] && current_mode=$(<"$STATE_FILE") + +# Cambiar modo si se hizo clic +if [[ "$1" == "click" ]]; then + for i in "${!MODES[@]}"; do + if [[ "${MODES[$i]}" == "$current_mode" ]]; then + next_index=$(( (i + 1) % ${#MODES[@]} )) + echo "${MODES[$next_index]}" > "$STATE_FILE" + exit 0 + fi + done +fi + +# Obtener temperaturas +cpu=$(get_cpu_temp) +igpu=$(get_igpu_temp) +dgpu=$(get_dgpu_temp) +nvme=$(get_nvme_temp) +board=$(get_board_temp) + +cpu=${cpu:-N/A} +igpu=${igpu:-N/A} +dgpu=${dgpu:-N/A} +nvme=${nvme:-N/A} +board=${board:-N/A} + +# Definir ícono y valor visible +case "$current_mode" in + cpu) icon="CPU"; temp="$cpu" ;; + igpu) icon="iGPU"; temp="$igpu" ;; + dgpu) icon="dGPU"; temp="$dgpu" ;; +esac + +# Tooltip con \n y escapado correcto +tooltip=$(printf "CPU: %s°C\niGPU: %s°C\ndGPU: %s°C\nNVMe: %s°C\nBoard: %s°C" "$cpu" "$igpu" "$dgpu" "$nvme" "$board") +tooltip_escaped=$(echo "$tooltip" | jq -Rs .) + +# Salida final +echo "{\"text\": \"$icon: ${temp}°C\", \"tooltip\": ${tooltip_escaped}}" + diff --git a/.config/waybar/scripts/tiling.py b/.config/waybar/scripts/tiling.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/.config/waybar/scripts/tiling.py @@ -0,0 +1 @@ + diff --git a/.config/waybar/scripts/toggle_dmp_widget.sh b/.config/waybar/scripts/toggle_dmp_widget.sh new file mode 100755 index 0000000..0eeff7f --- /dev/null +++ b/.config/waybar/scripts/toggle_dmp_widget.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +PIDFILE="/tmp/dmp_widget.pid" + +if [ -f "$PIDFILE" ] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then + kill "$(cat "$PIDFILE")" + rm -f "$PIDFILE" +else + yad --title="DMP-A6 Info" \ + --window-icon=audio-x-generic \ + --on-top --skip-taskbar \ + --undecorated \ + --geometry=500x300+40+40 \ + --no-buttons \ + --image="/tmp/dmp_cover.jpg" \ + --text="$(~/.config/eww/scripts/get_dmp_state.sh | jq -r '.title + "\n" + .artist + "\n" + .info')" \ + --text-align=center \ + --fontname="JetBrains Mono 11" \ + --borders=12 & + echo $! > "$PIDFILE" +fi + diff --git a/.config/waybar/scripts/toggle_fans.sh b/.config/waybar/scripts/toggle_fans.sh new file mode 100755 index 0000000..8eebdb9 --- /dev/null +++ b/.config/waybar/scripts/toggle_fans.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +# Comprobamos el estado actual leyendo la línea de CPU en "-g" +status=$(asusctl fan-curve -g 2>/dev/null | grep "^CPU: enabled:") + +# Si se encuentra "true" en esa línea, significa que las fan-curves están activas +# (asumimos que CPU/GPU/MID van sincronizados para el perfil Balanced). +if echo "$status" | grep -q "true"; then + echo "Fan-curve actualmente habilitado -> Lo deshabilitamos" + asusctl fan-curve -m Balanced -e false +else + echo "Fan-curve actualmente deshabilitado -> Lo habilitamos" + asusctl fan-curve -m Balanced -e true +fi + diff --git a/.config/waybar/scripts/up.txt b/.config/waybar/scripts/up.txt new file mode 100644 index 0000000..e8755d4 --- /dev/null +++ b/.config/waybar/scripts/up.txt @@ -0,0 +1 @@ +⬆ 525.94 Mbps diff --git a/.config/waybar/scripts/updates.sh b/.config/waybar/scripts/updates.sh new file mode 100755 index 0000000..e75c8a6 --- /dev/null +++ b/.config/waybar/scripts/updates.sh @@ -0,0 +1,70 @@ +#!/bin/bash + +SYNC_INTERVAL=1800 +SYNC_TIMESTAMP="/tmp/last_checkupdates_sync" +NO_ZERO_OUTPUT=false + +now=$(date +%s) +last=$(cat "$SYNC_TIMESTAMP" 2>/dev/null || echo 0) +if (( now - last >= SYNC_INTERVAL )); then + rm -rf "/tmp/checkup-db-$UID" + checkupdates > /dev/null 2>&1 + echo "$now" > "$SYNC_TIMESTAMP" +fi + +# Mapping: REPOS destino de cada paquete +declare -A update_repo_map +mapping=$(sudo pacman -Syuw --print-format '%r %n' --noconfirm | grep -v '^::' | grep -v '^descargando') +while read -r repo pkg; do + [[ -z "$repo" || -z "$pkg" ]] && continue + update_repo_map["$pkg"]="$repo" +done <<< "$mapping" + +updates=$(checkupdates --nosync 2>/dev/null) +aur_updates=$(paru -Qua 2>/dev/null) + +total_count=0 +tooltip_lines=() + +# OJO: este while debe ser así, ¡NUNCA uses tuberías aquí! +IFS=$'\n' +for line in $updates; do + [[ -z "$line" ]] && continue + pkg=$(awk '{print $1}' <<< "$line") + old=$(awk '{print $2}' <<< "$line") + new=$(awk '{print $4}' <<< "$line") + repo="${update_repo_map[$pkg]}" + [[ -z "$repo" ]] && repo="???" + formatted=$(printf "%-15s %-30s %s → %s" "$repo" "$pkg" "$old" "$new") + tooltip_lines+=("$formatted") + ((total_count++)) +done + +# AUR +for line in $aur_updates; do + [[ -z "$line" ]] && continue + pkg=$(awk '{print $1}' <<< "$line") + old=$(awk '{print $2}' <<< "$line") + new=$(awk '{print $4}' <<< "$line") + formatted=$(printf "%-15s %-30s %s → %s" "aur" "$pkg" "$old" "$new") + tooltip_lines+=("$formatted") + ((total_count++)) +done + +aur_rebuild=$(cat /tmp/aur_rebuild_count 2>/dev/null || echo 0) +if [[ "$aur_rebuild" -gt 0 ]]; then + tooltip_lines+=("") + tooltip_lines+=("️ $aur_rebuild paquetes AUR necesitan recompilación") +fi + +if [[ "$total_count" -eq 0 && "$aur_rebuild" -eq 0 ]]; then + if [[ "$NO_ZERO_OUTPUT" == true ]]; then + echo '{"text": "", "tooltip": "Todo actualizado", "class": "updated", "alt": "updated"}' + else + echo '{"text": "0", "tooltip": "Todo actualizado", "class": "updated", "alt": "updated"}' + fi +else + tooltip=$(printf "%s\n" "${tooltip_lines[@]}" | jq -R -s '.') + echo "{\"text\": \"$total_count\", \"tooltip\": $tooltip, \"class\": \"has-updates\", \"alt\": \"has-updates\"}" +fi + diff --git a/.config/waybar/scripts/view_battery_details.sh b/.config/waybar/scripts/view_battery_details.sh new file mode 100755 index 0000000..8716ba2 --- /dev/null +++ b/.config/waybar/scripts/view_battery_details.sh @@ -0,0 +1,5 @@ +#!/bin/bash +DETAILS=$(upower -i /org/freedesktop/UPower/devices/battery_BAT1 | grep -E "state|percentage|energy|energy-full|energy-rate|voltage") +echo "$DETAILS" | zenity --text-info --title="Detalles de la Batería" --width=400 --height=300 + + diff --git a/.config/waybar/scripts/view_battery_details_yad.sh b/.config/waybar/scripts/view_battery_details_yad.sh new file mode 100755 index 0000000..58a1485 --- /dev/null +++ b/.config/waybar/scripts/view_battery_details_yad.sh @@ -0,0 +1,4 @@ +#!/bin/bash +DETAILS=$(upower -i /org/freedesktop/UPower/devices/battery_BAT1 | grep -E "state|percentage|energy|energy-full|energy-rate|voltage") +echo "$DETAILS" | yad --text-info --title="Detalles de la Batería" --width=400 --height=300 + diff --git a/.config/waybar/scripts/volume_menu.py b/.config/waybar/scripts/volume_menu.py new file mode 100755 index 0000000..9712795 --- /dev/null +++ b/.config/waybar/scripts/volume_menu.py @@ -0,0 +1,63 @@ +#!/usr/bin/env python3 +from gi.repository import AppIndicator3 +from gi.repository import Gtk, GLib, GObject +import gi +import subprocess +import psutil +import sys +import os + +gi.require_version("Gtk", "3.0") + + +class VolumeSlider(Gtk.Window): + def __init__(self): + super().__init__(title="Control de Volumen") + self.set_default_size(50, 300) # Tamaño de la ventana ajustado + + # Crear el control deslizante vertical + self.slider = Gtk.Scale(orientation=Gtk.Orientation.VERTICAL) + self.slider.set_range(0, 100) # Rango de 0 a 100 para el volumen + self.slider.set_value(self.get_current_volume()) + # Invertir para que vaya de abajo hacia arriba + self.slider.set_inverted(True) + self.slider.connect("value-changed", self.on_volume_changed) + + # Ajustar el ancho del control deslizante + self.slider.set_size_request(30, -1) # Establece el ancho a 30 píxeles + + # Añadir el control deslizante a la ventana + self.add(self.slider) + + def get_current_volume(self): + """Obtiene el volumen actual usando pamixer""" + result = subprocess.run( + ["pamixer", "--get-volume"], capture_output=True, text=True) + # Default al 50% + return int(result.stdout.strip()) if result.returncode == 0 else 50 + + def on_volume_changed(self, slider): + """Ajusta el volumen según la posición del control deslizante""" + volume = int(slider.get_value()) + subprocess.run(["pamixer", "--set-volume", str(volume)]) + + +def is_another_instance_running(): + """Comprueba si hay otra instancia de este script en ejecución""" + current_pid = os.getpid() + for process in psutil.process_iter(attrs=['pid', 'cmdline']): + if process.info['pid'] != current_pid and 'volume_slider.py' in process.info['cmdline']: + return True + return False + + +if __name__ == "__main__": + if is_another_instance_running(): + print("Ya hay una instancia ejecutándose. Finalizando esta.") + sys.exit() + + # Ejecutar la aplicación + win = VolumeSlider() + win.connect("destroy", Gtk.main_quit) + win.show_all() + Gtk.main() diff --git a/.config/waybar/scripts/weather.py b/.config/waybar/scripts/weather.py new file mode 100755 index 0000000..d88058f --- /dev/null +++ b/.config/waybar/scripts/weather.py @@ -0,0 +1,229 @@ +#!/usr/bin/env python3 + +import os +import sys +import json +import gi +import requests +from datetime import datetime, timezone + +gi.require_version('Geoclue', '2.0') +from gi.repository import Geoclue + +# ─────────── ICONOS DEL CLIMA ─────────── # +weather_icons = { + "01d": "", "01n": "", "02d": "", "02n": "", + "03d": "", "03n": "", "04d": "", "04n": "", + "09d": "", "09n": "", "10d": "", "10n": "", + "11d": "", "11n": "", "13d": "", "13n": "", + "50d": "", "50n": "", "default": "" +} + +# ─────────── API KEYS ─────────── # +try: + with open('/home/teraflops/apikeys/openweathermaps', 'r') as f: + api_key = f.read().strip() + with open('/home/teraflops/apikeys/google_geoip', 'r') as f: + google_key = f.read().strip() +except Exception: + print(json.dumps({"text": "Error al leer las API keys"})) + sys.exit(1) + +# ─────────── OBTENER UBICACIÓN ─────────── # +def get_location_from_geoclue(): + try: + clue = Geoclue.Simple.new_sync("weather-script", Geoclue.AccuracyLevel.EXACT, None) + location = clue.get_location() + lat = location.get_property('latitude') + lon = location.get_property('longitude') + desc = location.get_property('description') + acc = location.get_property('accuracy') + print(f"🛰️ GeoClue: {lat}, {lon} — Precisión: {acc:.1f} m — Fuente: {desc}") + return lat, lon, desc + except Exception as e: + print("GeoClue (GI): error al obtener ubicación:", e) + return None + +def get_location_from_google(): + try: + url = f"https://www.googleapis.com/geolocation/v1/geolocate?key={google_key}" + response = requests.post(url, json={}) + response.raise_for_status() + data = response.json() + lat = data['location']['lat'] + lon = data['location']['lng'] + accuracy = data.get('accuracy', 0) + print(f"🛰️ GoogleGeo: {lat}, {lon} — Precisión: {accuracy:.0f} m") + return lat, lon, f"GoogleGeo ({accuracy:.0f} m)" + except Exception as e: + print("Google Geolocation API: fallo:", e) + return None + +def reverse_geocode(lat, lon): + try: + url = "https://nominatim.openstreetmap.org/reverse" + params = { + "lat": lat, + "lon": lon, + "format": "json", + "zoom": 10, + "addressdetails": 1, + } + headers = {"User-Agent": "weather-waybar-script"} + response = requests.get(url, params=params, headers=headers) + response.raise_for_status() + data = response.json() + address = data.get("address", {}) + ciudad = address.get("city") or address.get("town") or address.get("village") or "Desconocido" + pais = address.get("country", "Desconocido") + return ciudad, pais + except Exception as e: + print("Reverse geocode falló:", e) + return "Desconocido", "Desconocido" + +# ─────────── ALERTA LLUVIA SOLO EN TOOLTIP ─────────── # +def check_rain_alert(forecast_data, threshold=60, max_horas=6): + ahora = datetime.utcnow() + mensaje_alerta = "" + + for block in forecast_data['list']: + bloque_hora = datetime.strptime(block['dt_txt'], "%Y-%m-%d %H:%M:%S") + if bloque_hora <= ahora: + continue + + if (bloque_hora - ahora).total_seconds() > max_horas * 3600: + continue + + pop = block.get('pop', 0) * 100 + if pop >= threshold: + hora_local = bloque_hora.replace(tzinfo=timezone.utc).astimezone().strftime("%H:%M") + mensaje_alerta = f"⚠️ Posible lluvia a las {hora_local} ({int(pop)}%)" + break + + return mensaje_alerta + +# ─────────── PRONÓSTICO EXTENDIDO ─────────── # +def get_daily_forecast(forecast_data): + from collections import defaultdict + dias = defaultdict(list) + for entry in forecast_data['list']: + fecha = entry['dt_txt'].split(' ')[0] + dias[fecha].append(entry) + + resumen = [] + for i, (dia, bloques) in enumerate(dias.items()): + if i >= 3: break + temps = [b['main']['temp'] for b in bloques] + pops = [b.get('pop', 0) * 100 for b in bloques] + estado = bloques[0]['weather'][0]['description'].capitalize() + resumen.append(f"📅 {dia}: {estado}, {round(min(temps))}–{round(max(temps))}°C, ☔ {round(max(pops))}%") + return "\n".join(resumen) + +# ─────────── UBICACIÓN ─────────── # +usar_ubicacion_manual = False # Cambia a True si quieres forzar ubicación + +if usar_ubicacion_manual: + latitud, longitud = 40.4168, -3.7038 # Ejemplo: Madrid + region = "Manual" + print(f"🧪 Usando ubicación manual: {latitud}, {longitud}") +else: + loc = get_location_from_geoclue() or get_location_from_google() + if not loc: + print(json.dumps({"text": "Error al obtener la ubicación"})) + sys.exit(1) + latitud, longitud, region = loc + +ciudad, pais = reverse_geocode(latitud, longitud) + +# ─────────── API WEATHER ─────────── # +params = { + "lat": latitud, + "lon": longitud, + "appid": api_key, + "units": "metric", + "lang": "es", +} + +weather_url = "https://api.openweathermap.org/data/2.5/weather" +response = requests.get(weather_url, params=params) +if response.status_code != 200: + print(json.dumps({"text": "Error al obtener datos del clima"})) + sys.exit(1) +data = response.json() + +# ─────────── DATOS CLIMA ─────────── # +temp = f"{round(data['main']['temp'])}°C" +status = data['weather'][0]['description'].capitalize() +status_code = data['weather'][0]['icon'] +icon = weather_icons.get(status_code, weather_icons["default"]) +temp_feel = f"{round(data['main']['feels_like'])}°C" +temp_feel_text = f"Sensación de {temp_feel}" +temp_min = f"{round(data['main']['temp_min'])}°C" +temp_max = f"{round(data['main']['temp_max'])}°C" +temp_min_max = f" {temp_min}\t\t {temp_max}" +wind_speed = f"{round(data['wind']['speed'] * 3.6)} km/h" +wind_text = f" {wind_speed}" +humidity = f"{data['main']['humidity']}%" +humidity_text = f" {humidity}" +visibility = data.get('visibility', 0) +visibility_km = f"{visibility / 1000} km" +visibility_text = f" {visibility_km}" + +# ─────────── CALIDAD DEL AIRE ─────────── # +aqi_url = "https://api.openweathermap.org/data/2.5/air_pollution" +aqi_response = requests.get(aqi_url, params=params) +if aqi_response.status_code == 200: + aqi = aqi_response.json()['list'][0]['main']['aqi'] + aqi_levels = {1: "Bueno", 2: "Moderado", 3: "Perjudicial", 4: "Malo", 5: "Muy malo"} + air_quality_index = aqi_levels.get(aqi, "N/A") +else: + air_quality_index = "N/A" + +# ─────────── PRONÓSTICO ─────────── # +forecast_url = "https://api.openweathermap.org/data/2.5/forecast" +forecast_response = requests.get(forecast_url, params=params) +if forecast_response.status_code == 200: + forecast_data = forecast_response.json() + precipitation = forecast_data['list'][0].get('pop', 0) * 100 + prediction = f"\n\n  {precipitation:.0f}% probabilidad de lluvia" + alerta_lluvia = check_rain_alert(forecast_data) + pronostico_dias = get_daily_forecast(forecast_data) +else: + prediction = "" + alerta_lluvia = "" + pronostico_dias = "" + +# ─────────── TOOLTIP ─────────── # +time_of_day = "Día" if status_code.endswith('d') else "Noche" +ubicacion = f"{ciudad}, {region}, {pais} ({latitud:.2f}, {longitud:.2f})" + +tooltip_text = "" +if alerta_lluvia: + tooltip_text += f"{alerta_lluvia}\n\n" + +tooltip_text += str.format( + "{}\n{}\n{}\n{}\n\n{}\n{}\n{}{}\n\n{}", + f'{temp}', + f"{icon}", + f"{status} ({time_of_day})", + f"{temp_feel_text}", + f"{temp_min_max}", + f"{wind_text}\t{humidity_text}", + f"{visibility_text}\tICA {air_quality_index}", + f"{prediction}", + f"Ubicación: {ubicacion}" +) + +if pronostico_dias: + tooltip_text += f"\n\nPróximos días:\n{pronostico_dias}" + +# ─────────── OUTPUT PARA WAYBAR ─────────── # +out_data = { + "text": f"{icon} {temp}", + "alt": f"{status} ({time_of_day})", + "tooltip": tooltip_text, + "class": status_code, +} + +print(json.dumps(out_data)) + diff --git a/.config/waybar/scripts/wifi_menu.py b/.config/waybar/scripts/wifi_menu.py new file mode 100755 index 0000000..f8fa76d --- /dev/null +++ b/.config/waybar/scripts/wifi_menu.py @@ -0,0 +1,440 @@ +#!/usr/bin/env python3 +import dbus +import dbus.mainloop.glib +from gi.repository import AppIndicator3 +from gi.repository import Gtk, GLib, GObject +import gi +import subprocess +import threading +import time +import sys +# Especificar las versiones antes de importar +gi.require_version('Gtk', '3.0') + + +class NetworkMenu(Gtk.Window): + def __init__(self): + super().__init__(title="Menú de Red") # Título estático + self.set_default_size(300, 200) + self.set_border_width(10) + + # Crear una caja vertical para organizar los widgets + vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10) + self.add(vbox) + + # Etiqueta para mostrar el estado de la red + self.status_label = Gtk.Label(label="Estado de la red: Desconocido") + vbox.pack_start(self.status_label, False, False, 0) + + # Construir el menú + self.build_menu(vbox) + + # Inicializar D-Bus + dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) + self.bus = dbus.SystemBus() + + try: + self.network_manager = self.bus.get_object( + 'org.freedesktop.NetworkManager', '/org/freedesktop/NetworkManager') + self.network_manager_props = dbus.Interface( + self.network_manager, 'org.freedesktop.DBus.Properties') + except dbus.exceptions.DBusException as e: + print( + f"Error al conectar con NetworkManager a través de D-Bus: {e}", file=sys.stderr) + sys.exit(1) + + # Suscribirse a las señales de cambio de propiedades + self.bus.add_signal_receiver( + self.on_properties_changed, + dbus_interface='org.freedesktop.DBus.Properties', + signal_name='PropertiesChanged', + arg0='org.freedesktop.NetworkManager', + path='/org/freedesktop/NetworkManager', + ) + + # Obtener estado inicial + self.update_status_initial() + + def build_menu(self, vbox): + # Botón Conectar WiFi + self.item_connect = Gtk.Button(label='Conectar WiFi') + self.item_connect.connect('clicked', self.connect_wifi) + vbox.pack_start(self.item_connect, True, True, 0) + + # Botón Desconectar WiFi + self.item_disconnect = Gtk.Button(label='Desconectar WiFi') + self.item_disconnect.connect('clicked', self.disconnect_wifi) + vbox.pack_start(self.item_disconnect, True, True, 0) + + # Botón Ver Redes Disponibles + self.item_view_networks = Gtk.Button(label='Ver Redes Disponibles') + self.item_view_networks.connect('clicked', self.show_network_list) + vbox.pack_start(self.item_view_networks, True, True, 0) + + # Botón Olvidar Red + self.item_forget_network = Gtk.Button(label='Olvidar Red') + self.item_forget_network.connect('clicked', self.forget_network) + vbox.pack_start(self.item_forget_network, True, True, 0) + + # Botón Crear AP + self.item_create_ap = Gtk.Button(label='Crear AP') + self.item_create_ap.connect('clicked', self.create_ap) + vbox.pack_start(self.item_create_ap, True, True, 0) + + # Separador + separator = Gtk.Separator() + vbox.pack_start(separator, False, False, 10) + + # Botón Salir + close_button = Gtk.Button(label='Cerrar') + close_button.connect('clicked', self.quit) + vbox.pack_start(close_button, False, False, 0) + + def create_ap(self, _): + """Ejecuta el comando para abrir wihotspot-gui.""" + try: + subprocess.run(['/usr/bin/wihotspot-gui'], check=True) + except subprocess.CalledProcessError: + self.show_notification("Error al iniciar wihotspot-gui") + + def update_status_initial(self): + """Inicializa el estado de la conexión WiFi al abrir la aplicación.""" + try: + wifi_state = self.get_wifi_state() + ssid = self.get_connected_ssid() + self.set_status(wifi_state, ssid) + except dbus.exceptions.DBusException as e: + print(f"Error al obtener el estado inicial: {e}", file=sys.stderr) + + def forget_network(self, _): + """Función para olvidar una red guardada""" + # Obtener la lista de redes WiFi guardadas de manera confiable + saved_networks = subprocess.getoutput( + "nmcli -t -f NAME,TYPE connection show | grep ':802-11-wireless$' | cut -d: -f1").splitlines() + + if not saved_networks: + self.show_notification("No hay redes guardadas para olvidar.") + return + + # Crear una ventana para seleccionar la red a olvidar + window = Gtk.Window(title="Olvidar Red Guardada") + window.set_default_size(300, 200) + window.set_border_width(10) + + vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10) + + # Añadir botones para cada red guardada + for network in saved_networks: + button = Gtk.Button(label=network) + button.connect("clicked", self.delete_network_profile, network) + vbox.pack_start(button, True, True, 0) + + # Botón para cerrar la ventana + close_button = Gtk.Button(label="Cerrar") + close_button.connect("clicked", lambda x: window.destroy()) + vbox.pack_start(close_button, False, False, 0) + + window.add(vbox) + window.show_all() + + def delete_network_profile(self, button, network): + """Elimina el perfil de conexión guardado para la red especificada""" + try: + subprocess.run( + ['nmcli', 'connection', 'delete', network], check=True) + self.show_notification(f"Red '{network}' olvidada exitosamente.") + except subprocess.CalledProcessError: + self.show_notification(f"Error al olvidar la red '{network}'.") + + def connect_wifi(self, _): + """Función llamada al hacer clic en el botón 'Conectar WiFi'. + Muestra la lista de redes y permite al usuario seleccionar una.""" + self.show_network_list(None) + + def get_available_networks_list(self): + """Obtiene la lista de redes WiFi junto con la intensidad de la señal y la banda (2.4 GHz o 5 GHz), sin duplicados y sin entradas vacías""" + try: + # Obtener SSID, señal y frecuencia + output = subprocess.getoutput( + "nmcli -t -f SSID,SIGNAL,FREQ dev wifi list") + networks = {} + for line in output.splitlines(): + if line: + # Separar el SSID, señal y frecuencia en tres variables + parts = line.split(":") + + # Validación para evitar entradas vacías en SSID + ssid = parts[0].strip() + if not ssid: + continue # Saltamos esta entrada si el SSID está vacío + + signal_strength = int(parts[1].strip()) if len( + parts) > 1 and parts[1].isdigit() else 0 + frequency = int(parts[2].strip().split()[0]) if len( + parts) > 2 and parts[2].strip().split()[0].isdigit() else 0 + + # Determinar la banda en función de la frecuencia + band = "5 GHz" if frequency >= 5000 else "2.4 GHz" + + # Evitar duplicados: guardar solo la señal más alta para cada SSID + if ssid in networks: + if signal_strength > networks[ssid][0]: + networks[ssid] = (signal_strength, band) + else: + networks[ssid] = (signal_strength, band) + + # Convertir el diccionario a una lista de tuplas (SSID, señal, banda) + return [(ssid, data[0], data[1]) for ssid, data in networks.items()] + except Exception as e: + print(f"Error al obtener la lista de redes disponibles: { + e}", file=sys.stderr) + return [] + + def show_network_list(self, _): + # Llamar a la función para obtener la lista de redes con señal y banda + networks = self.get_available_networks_list() + + if not networks: + self.show_notification("No se encontraron redes disponibles.") + return + + # Crear una ventana de lista de redes + window = Gtk.Window(title="Redes Disponibles") + window.set_default_size(300, 200) + window.set_border_width(10) + + vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=10) + + # Añadir botones para cada red disponible con la intensidad de señal y la banda + for network, signal_strength, band in networks: + # Mostramos el nombre de la red junto con su intensidad de señal y la banda (2.4 GHz o 5 GHz) + button_label = f"{network} ({signal_strength}%, {band})" + button = Gtk.Button(label=button_label) + button.connect( + "clicked", self.prompt_password_and_connect, network) + vbox.pack_start(button, True, True, 0) + + # Botón para cerrar la ventana + close_button = Gtk.Button(label="Cerrar") + close_button.connect("clicked", lambda x: window.destroy()) + vbox.pack_start(close_button, False, False, 0) + + window.add(vbox) + window.show_all() + + def prompt_password_and_connect(self, button, network): + # Verificar si el perfil de la conexión ya existe + existing_profile = subprocess.getoutput( + f"nmcli -t -f NAME connection show | grep '^{network}$'") + + if existing_profile: + # Si el perfil ya existe, intentar conectarse sin solicitar la contraseña + self.show_notification(f"Conectando a {network}...") + # None para indicar que no se requiere contraseña + self.connect_to_network_with_nmcli(network, None) + else: + # Si no existe el perfil, solicitar la contraseña + dialog = Gtk.Dialog( + title=f"Conectar a {network}", + transient_for=self, + flags=0 + ) + dialog.add_buttons( + Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, + Gtk.STOCK_OK, Gtk.ResponseType.OK + ) + + # Crear un campo de entrada de texto para la contraseña + box = dialog.get_content_area() + label = Gtk.Label(label="Introduce la contraseña:") + box.add(label) + + password_entry = Gtk.Entry() + # Ocultar la contraseña al escribir + password_entry.set_visibility(False) + box.add(password_entry) + dialog.show_all() + + # Ejecutar el diálogo y obtener la respuesta del usuario + response = dialog.run() + if response == Gtk.ResponseType.OK: + password = password_entry.get_text() + dialog.destroy() + # Intentar conectar a la red con nmcli y configuración de seguridad + self.connect_to_network_with_nmcli(network, password) + else: + dialog.destroy() + print("Conexión cancelada por el usuario.") + + def connect_to_network_with_nmcli(self, ssid, password): + # Detectar la interfaz de red WiFi principal + try: + interface = subprocess.getoutput( + "nmcli device status | grep wifi | grep -v 'p2p' | awk '{print $1}'").strip() + if not interface: + self.show_notification( + "No se encontró una interfaz WiFi válida.") + return + except subprocess.CalledProcessError: + self.show_notification("Error al obtener la interfaz de red.") + return + + try: + # Verificar si ya existe un perfil de conexión con el SSID + existing_profile = subprocess.getoutput( + f"nmcli -t -f NAME connection show | grep '^{ssid}$'") + + if existing_profile: + # Si el perfil existe y se proporciona una contraseña, actualizarlo + if password: + subprocess.run([ + 'nmcli', 'connection', 'modify', ssid, + 'wifi-sec.key-mgmt', 'wpa-psk', 'wifi-sec.psk', password, + 'connection.interface-name', interface + ], check=True) + self.show_notification(f"Perfil actualizado: {ssid}") + else: + # Si el perfil no existe, crearlo + subprocess.run([ + 'nmcli', 'connection', 'add', 'type', 'wifi', + 'ifname', interface, 'con-name', ssid, 'ssid', ssid, + 'wifi-sec.key-mgmt', 'wpa-psk', 'wifi-sec.psk', password + ], check=True) + self.show_notification(f"Perfil creado: {ssid}") + + # Intentar conectar usando el perfil existente o actualizado + subprocess.run(['nmcli', 'connection', 'up', ssid], check=True) + self.show_notification(f"Conectado a {ssid}") + except subprocess.CalledProcessError: + self.show_notification(f"Error al intentar conectar a {ssid}") + + def disconnect_wifi(self, _): + """Función para desconectar la red WiFi actual.""" + try: + # Intentar desconectar la red activa + subprocess.run(['nmcli', 'networking', 'off'], check=True) + # Reactivar después de desconectar + subprocess.run(['nmcli', 'networking', 'on'], check=True) + self.show_notification("WiFi Desconectado.") + except subprocess.CalledProcessError: + self.show_notification("Error al desconectar WiFi.") + + def quit(self, _): + Gtk.main_quit() + + def get_available_networks_list(self): + """Obtiene la lista de redes WiFi junto con la intensidad de la señal y la banda (2.4 GHz o 5 GHz), sin duplicados y sin entradas vacías""" + try: + # Obtener SSID, señal y frecuencia + output = subprocess.getoutput( + "nmcli -t -f SSID,SIGNAL,FREQ dev wifi list") + networks = {} + for line in output.splitlines(): + if line: + # Separar el SSID, señal y frecuencia en tres variables + parts = line.split(":") + + # Validación para evitar entradas vacías en SSID + ssid = parts[0].strip() + if not ssid: + continue # Saltamos esta entrada si el SSID está vacío + + signal_strength = int(parts[1].strip()) if len( + parts) > 1 and parts[1].isdigit() else 0 + frequency = int(parts[2].strip().split()[0]) if len( + parts) > 2 and parts[2].strip().split()[0].isdigit() else 0 + + # Determinar la banda en función de la frecuencia + band = "5 GHz" if frequency >= 5000 else "2.4 GHz" + + # Evitar duplicados: guardar solo la señal más alta para cada SSID + if ssid in networks: + if signal_strength > networks[ssid][0]: + networks[ssid] = (signal_strength, band) + else: + networks[ssid] = (signal_strength, band) + + # Convertir el diccionario a una lista de tuplas (SSID, señal, banda) + return [(ssid, data[0], data[1]) for ssid, data in networks.items()] + except Exception as e: + print(f"Error al obtener la lista de redes disponibles: { + e}", file=sys.stderr) + return [] + + def show_notification(self, message): + """Muestra una notificación en el sistema""" + subprocess.run(['notify-send', message]) + + def on_properties_changed(self, interface, changed_properties, invalidated_properties): + if 'State' in changed_properties: + wifi_state = self.get_wifi_state() + ssid = self.get_connected_ssid() + self.set_status(wifi_state, ssid) + + def set_status(self, wifi_state, ssid): + if wifi_state == 'disabled': + status_text = "WiFi Apagado" + # Deshabilitar el botón de Desconectar WiFi y habilitar Conectar WiFi + self.item_disconnect.set_sensitive(False) + self.item_connect.set_sensitive(True) + elif wifi_state == 'enabled' and not ssid: + status_text = "WiFi Encendido (No Conectado)" + # Habilitar ambos botones + self.item_disconnect.set_sensitive(True) + self.item_connect.set_sensitive(True) + elif wifi_state == 'enabled' and ssid: + status_text = f"Conectado a: {ssid}" + # Deshabilitar el botón de Conectar WiFi y habilitar Desconectar WiFi + self.item_connect.set_sensitive(False) + self.item_disconnect.set_sensitive(True) + else: + status_text = "WiFi Estado Desconocido" + # Habilitar ambos botones por defecto + self.item_connect.set_sensitive(True) + self.item_disconnect.set_sensitive(True) + + print(f"Actualizando estado: '{status_text}'") + self.status_label.set_text(f"Estado de la red: {status_text}") + + def get_wifi_state(self): + try: + output = subprocess.getoutput("nmcli radio wifi") + print(f"Estado WiFi: {output}") + return output.lower() + except Exception as e: + print(f"Error al obtener el estado de WiFi: {e}", file=sys.stderr) + return "unknown" + + def get_connected_ssid(self): + try: + # Usar iwgetid para obtener el SSID conectado + ssid = subprocess.getoutput("iwgetid -r").strip() + if ssid: + print(f"SSID conectado obtenido con iwgetid: {ssid}") + return ssid + else: + # Fallback a nmcli si iwgetid no retorna SSID + output = subprocess.getoutput( + "nmcli -t -f ACTIVE,SSID dev wifi | grep '^yes' | cut -d':' -f2").strip() + if output: + print(f"SSID conectado obtenido con nmcli: {output}") + return output + else: + print(f"SSID conectado obtenido: None") + return None + except Exception as e: + print(f"Error al obtener SSID conectado: {e}", file=sys.stderr) + return None + + +def main(): + # Para evitar múltiples instancias, usar una lock con D-Bus o similar + # Aquí, simplemente se muestra la ventana + window = NetworkMenu() + window.connect("destroy", Gtk.main_quit) + window.show_all() + Gtk.main() + + +if __name__ == "__main__": + main() diff --git a/.config/waybar/scripts/wlogout.sh b/.config/waybar/scripts/wlogout.sh new file mode 100755 index 0000000..9e2c757 --- /dev/null +++ b/.config/waybar/scripts/wlogout.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +# wlogout (Power, Screen Lock, Suspend, etc) + +# Set variables for parameters +A_2160=680 +B_2160=750 +A_1440=500 +B_1440=550 +A_1080=300 +B_1080=380 +A_720=50 +B_720=50 + +# Check if wlogout is already running +if pgrep -x "wlogout" > /dev/null; then + pkill -x "wlogout" + exit 0 +fi + +# Detect monitor resolution and scaling factor +resolution=$(hyprctl -j monitors | jq -r '.[] | select(.focused==true) | .height / .scale' | awk -F'.' '{print $1}') +hypr_scale=$(hyprctl -j monitors | jq -r '.[] | select(.focused==true) | .scale') + +echo "Detected Resolution: $resolution" + +# Prints the screen and applies blur. Very slow, looking to make faster +#grim - | magick - -blur 0x8 /tmp/shot_blurred.png +#grim - | convert - -scale 2.5% -resize 4000% /tmp/shot_blurred.png + +# Set parameters based on screen resolution and scaling factor +if ((resolution == 1353)); then + wlogout --protocol layer-shell -b 6 -T 600 -B 560 & + echo "Setting parameters for custom 1353p resolution" + exit 0 +fi +if ((resolution >= 2160)); then + wlogout --protocol layer-shell -b 3 -T $(awk "BEGIN {printf \"%.0f\", $A_2160 * 2160 * $hypr_scale / $resolution}") -B $(awk "BEGIN {printf \"%.0f\", $B_2160 * 2160 * $hypr_scale / $resolution}") & + echo "Setting parameters for resolution >= 2160p" +elif ((resolution >= 1440)); then + wlogout --protocol layer-shell -b 6 -T $(awk "BEGIN {printf \"%.0f\", $A_1440 * 1440 * $hypr_scale / $resolution}") -B $(awk "BEGIN {printf \"%.0f\", $B_1440 * 1440 * $hypr_scale / $resolution}") & + echo "Setting parameters for resolution >= 1440p" +elif ((resolution >= 1080)); then + wlogout --protocol layer-shell -b 6 -T $(awk "BEGIN {printf \"%.0f\", $A_1080 * 1080 * $hypr_scale / $resolution}") -B $(awk "BEGIN {printf \"%.0f\", $B_1080 * 1080 * $hypr_scale / $resolution}") & + echo "Setting parameters for resolution >= 1080p" +elif ((resolution > 720)); then + wlogout --protocol layer-shell -b 3 -T $(awk "BEGIN {printf \"%.0f\", $A_720 * 720 * $hypr_scale / $resolution}") -B $(awk "BEGIN {printf \"%.0f\", $B_720 * 720 * $hypr_scale / $resolution}") & + echo "Setting parameters for resolution >= 720p" +else + wlogout & + echo "Setting default parameters for resolution <= 720p" +fi diff --git a/.config/waybar/style.css b/.config/waybar/style.css new file mode 100644 index 0000000..74e4e91 --- /dev/null +++ b/.config/waybar/style.css @@ -0,0 +1,572 @@ +* { + font-family: "Ubuntu Nerd Font", "Font Awesome 6 Pro"; + font-size: 15px; +} +window#waybar { + background-color: #242933; + color: #ffffff; + border-radius: 8px; + border-width: 3px; + border-color: #3b4252; + border-style: solid; +} + +window#waybar.hidden { + opacity: 0.1; +} + +#window { + color: #64727d; + margin-left: 50px; + margin-right: 50px; + margin-top: 10px; + margin-bottom: 10px; + border-radius: 8px 8px 8px 8px; +} + +#workspaces, +#custom-calendar_clock, +#custom-weather, +#cpu, +#memory, +#custom-updtes, +#custom-rate, +#custom-inetspeedup, +#custom-inetspeeddown, +#tray, +#custom-separator, +#mode, +#custom-lock, +#workspaces, +#idle_inhibitor, +#power-profiles-daemon, +#custom-fans, +#custom-powermenu, +#custom-launcher, +#custom-vpn, +#custom-borg, +#temperature, +#custom-temps, +#cpu { + color: #fa9f8e; + border-radius: 8px 8px 8px 8px; +} + +#mpris { + color: #fa9f8e; +} +#custom-roon { + color: #fa9f8e; +} +#custom-dmp_nowplaying { + color: #fa9f8e; +} +#custom-lyrics { + margin-left: 10px; + margin-right: 10px; +} +#image { + margin-left: 10px; + margin-right: 10px; +} + +#temperature { + color: #fa9f8e; + border-radius: 8px 8px 8px 8px; +} +#custom-temps { + color: #fa9f8e; + border-radius: 8px 8px 8px 8px; +} + +#custom-tailscale { + color: #81a1c1; + border-radius: 8px 8px 8px 8px; +} +#cutom-borg { + color: #fa9f8e; + border-radius: 8px 8px 8px 8px; +} +#power-profiles-daemon { + color: #81a1c1; + border-radius: 8px 8px 8px 8px; +} +#custom-fans { + color: #fa9f8e; + border-radius: 8px 8px 8px 8px; +} +#memory { + font-size: 15px; + color: #81a1c1; + border-radius: 8px 8px 8px 8px; +} +#custom-updates { + font-size: 15px; + color: #81a1c1; + border-radius: 8px 8px 8px 8px; +} + +#custom-inetspeeddown { + font-size: 15px; + color: #81a1c1; + border-radius: 8px 8px 8px 8px; +} +#custom-inetspeedup { + font-size: 15px; + color: #81a1c1; + border-radius: 8px 8px 8px 8px; +} + +@keyframes button_activate { + from { + opacity: 0.3; + } + to { + opacity: 1; + } +} + +#workspaces button { + border: none; + color: #d4d2a9; + padding: 3px; + background: transparent; + box-shadow: none; +} + +#workspaces button.active { + color: #81a1c1; + background: radial-gradient( + circle, + rgba(20, 48, 187, 0.699) 5%, + rgba(36, 39, 54, 0.5) 18%, + rgba(132, 129, 156, 0) 23%, + rgba(70, 76, 98, 0.201) 24%, + rgba(132, 129, 156, 0) 30% + ); + animation: button_activate 0.2s ease-in-out; +} + +#workspaces button.urgent { + color: #fa9f8e; +} + + +#workspaces button.persistent { + color: #85909e; +} + +#workspaces button:hover { + border: none; + background: radial-gradient( + circle, + rgba(25, 79, 255, 0.1) 20%, + rgba(132, 129, 156, 0) 30% + ); +} + +#workspaces button.active:hover, +#workspaces button.urgent:hover { + background: inherit; +} + +#custom-launcher { + margin-left: 10px; + margin-right: 10px; + + padding-right: 5px; + padding-left: 0px; + + font-size: 22px; + + color: #7a95c9; + + margin-top: 1px; + margin-bottom: 1px; + border-radius: 8px 8px 8px 8px; +} + +#workspaces, +#custom-calendar_clock, +#custom-weather, +#custom-network_speed, +#cpu, +#custom-song_name, +#custom-vpn, +#custom-borg, +#temperature, +#custom-temps, +#custom-fans, +#power-profiles-daemon, +#memory, +#custom-updates, +#custom-tailscale, +#custom-rate, +#custom-inetspeedup, +#custom-inetspeeddown, +#pulseaudio, +#custom-separator, +#tray #network { + background-color: #3b4252; + padding: 0em 2em; + + font-size: 20px; + + padding-left: 7.5px; + padding-right: 7.5px; + + padding-top: 2px; + padding-bottom: 2px; + + margin-top: 10px; + margin-bottom: 10px; + margin-right: 10px; + + font-size: 19px; +} +#custom-separator { + color: #1b5e20; + margin: 0 5px; +} +#workspaces { + padding-right: 10px; +} +#custom-pwrate { + color: #81a1c1; + padding-left: 10px; + margin-right: 15px; + font-size: 13px; + border-radius: 0px 8px 8px 0px; + margin-left: -15px; +} + +#wireplumber { + color: #81a1c1; + padding-left: 10px; + margin-right: 10px; + font-size: 23px; + border-radius: 0px 8px 8px 0px; + margin-left: -15px; +} + +#wireplumber.muted { + color: #fb958b; + padding-left: 10px; + font-size: 23px; + border-radius: 0px 8px 8px 0px; +} + +#backlight { + color: #81a1c1; + padding-left: 10px; + padding-right: 10px; + font-size: 24px; + border-radius: 8px 8px 8px 8px; +} +#custom-wf-recorder { + color: #ff0000; +} + +#network { + padding-left: 0.2em; + color: #5e81ac; + border-radius: 8px 8px 8px 8px; + padding-left: 12px; + padding-right: 12px; + font-size: 19px; +} + +#network.disconnected { + color: #fb958b; +} + +#battery { + color: #81a1c1; + font-size: 19px; +} + +#battery.critical, +#battery.warning, +#battery.full, +#battery.plugged { + color: #81a1c1; + font-size: 19px; +} + +#battery.charging { + font-size: 19px; +} + +#battery.full, +#battery.plugged { + font-size: 19px; +} + +@keyframes blink { + to { + background-color: rgba(30, 34, 42, 0.5); + color: #abb2bf; + } +} + +#battery.warning { + color: #ecd3a0; +} + +#battery.critical:not(.charging) { + color: #fb958b; +} + +#custom-lock { + color: #ecd3a0; + padding: 0 15px 0 15px; + margin-left: 7px; + margin-top: 7px; + margin-bottom: 7px; +} + +#custom-calendar_clock { + color: #8a909e; + font-size: 15px; + font-weight: bold; + margin-top: 10px; + margin-bottom: 10px; + border-radius: 8px 8px 8px 8px; +} + +#bluetooth { + color: #fb958b; + padding-left: 10px; + margin-right: -5px; + font-size: 18px; + border-radius: 0px 8px 8px 0px; + margin-left: -5px; +} + +#bluetooth.on { + color: #fb958b; + padding-left: 10px; + margin-right: -5px; + font-size: 18px; + border-radius: 0px 8px 8px 0px; + margin-left: -5px; +} + +#bluetooth.connected { + color: #81a1c1; + padding-left: 10px; + margin-right: -5px; + font-size: 18px; + border-radius: 0px 8px 8px 0px; + margin-left: -5px; +} + +#bluetooth.off { + color: #8a909e; + padding-left: 10px; + margin-right: -5px; + font-size: 18px; + border-radius: 0px 8px 8px 0px; + margin-left: -5px; +} + +#custom-powermenu { + color: #e78284; + margin-right: 12px; + margin-left: 12px; + border-radius: 8px; + padding: 0 6px 0 6.8px; + margin-top: 7px; + margin-bottom: 7px; +} + +tooltip { + font-family: Ubuntu Nerd Font; + border-radius: 8px; + padding: 15px; + background-color: #242933; +} + +tooltip label { + font-family: Ubuntu Nerd Font; + padding: 5px; + background-color: #242933; +} + +#now_playing > tooltip { + background-color: red; +} + +label:focus { + background-color: #242933; +} + +#tray { + margin-right: 10px; + margin-top: 10px; + margin-bottom: 10px; + font-size: 30px; + border-radius: 8px 8px 8px 8px; +} + +#tray > .passive { + -gtk-icon-effect: dim; +} + +#tray > .needs-attention { + -gtk-icon-effect: highlight; + background-color: #eb4d4b; +} + +#idle_inhibitor { + background-color: #242933; +} + +#idle_inhibitor.activated { + background-color: #ecf0f1; + color: #2d3436; +} + +#memory, +#custom-updates, +#custom-rate, +#custom-tailscale, +#custom-inetspeedup, +#custom-inetspeeddown, +#cpu { + font-size: 15px; +} + +#mpris { + font-size: 15px; +} +#custom-roon { + font-size: 15px; +} + +#custom-dmp_nowplaying { + font-size: 15px; +} + +#power-profiles-daemon { + font-size: 15px; +} +#custom-fans { + font-size: 15px; +} +#temperature { + font-size: 15px; +} +#custom-temps { + font-size: 15px; +} + +#custom-vpn { + font-size: 15px; +} +#custom-borg { + font-size: 15px; +} +#custom-song_name, +#custom-spotify, +#custom-next, +#custom-pre { + background-color: #3b4252; + font-size: 20px; + padding-right: 6.5px; + margin-top: 10px; + margin-bottom: 10px; + font-weight: bold; +} + +#custom-weather.severe { + color: #8a909e; + font-size: 15px; + font-weight: bold; + margin-top: 10px; + margin-right: 10px; + margin-bottom: 10px; + border-radius: 8px 8px 8px 8px; +} + +#custom-weather.sunnyDay { + color: #8a909e; + font-size: 15px; + font-weight: bold; + margin-top: 10px; + margin-right: 10px; + margin-bottom: 10px; + border-radius: 8px 8px 8px 8px; +} + +#custom-weather.clearNight { + color: #8a909e; + font-size: 15px; + font-weight: bold; + margin-top: 10px; + margin-right: 10px; + margin-bottom: 10px; + border-radius: 8px 8px 8px 8px; +} + +#custom-weather.cloudyFoggyDay, +#custom-weather.cloudyFoggyNight { + color: #8a909e; + font-size: 15px; + font-weight: bold; + margin-top: 10px; + margin-right: 10px; + margin-bottom: 10px; + border-radius: 8px 8px 8px 8px; +} + +#custom-weather.rainyDay, +#custom-weather.rainyNight { + color: #8a909e; + font-size: 15px; + font-weight: bold; + margin-top: 10px; + margin-right: 10px; + margin-bottom: 10px; + border-radius: 8px 8px 8px 8px; +} + +#custom-weather.showyIcyDay, +#custom-weather.snowyIcyNight { + color: #8a909e; + font-size: 15px; + font-weight: bold; + margin-top: 10px; + margin-right: 10px; + margin-bottom: 10px; + border-radius: 8px 8px 8px 8px; +} + +#custom-weather { + color: #8a909e; + font-size: 15px; + font-weight: bold; + margin-top: 10px; + margin-right: 10px; + margin-bottom: 10px; + border-radius: 8px 8px 8px 8px; +} +#custom-network_speed { + color: #8a909e; + font-size: 15px; + font-weight: bold; + margin-top: 10px; + margin-right: 10px; + margin-bottom: 10px; + border-radius: 8px 8px 8px 8px; +} +#waybar.info { + background-color: rgba(10, 10, 10, 0.0); + font-family: JetBrainsMonoNL Nerd Font Mono; + font-size: 14px; +} + +#submap { + background-color: rgba(10, 10, 10, 0.8); + color: rgba(152, 239, 106, 1); +} diff --git a/.config/waybar/style.css_bicolor b/.config/waybar/style.css_bicolor new file mode 100644 index 0000000..abe3cd8 --- /dev/null +++ b/.config/waybar/style.css_bicolor @@ -0,0 +1,549 @@ +* { + font-family: "Ubuntu Nerd Font", "Font Awesome 6 Pro"; + font-size: 15px; +} +window#waybar { + background-color: #242933; + color: #ffffff; + border-radius: 8px; + border-width: 3px; + border-color: #3b4252; + border-style: solid; +} + +window#waybar.hidden { + opacity: 0.1; +} + +#window { + color: #64727d; + margin-left: 50px; + margin-right: 50px; + margin-top: 10px; + margin-bottom: 10px; + border-radius: 8px 8px 8px 8px; +} + +#workspaces, +#custom-calendar_clock, +#custom-weather, +#cpu, +#memory, +#custom-rate, +#custom-inetspeedup, +#custom-inetspeeddown, +#tray, +#custom-separator, +#mode, +#custom-lock, +#workspaces, +#idle_inhibitor, +#power-profiles-daemon, +#custom-fans, +#custom-powermenu, +#custom-launcher, +#custom-vpn, +#custom-borg, + + +#cpu { + color: #fa9f8e; + border-radius: 8px 8px 8px 8px; +} +#mpris { + color: #fa9f8e; +} +#custom-roon { + color: #fa9f8e; +} +#custom-lyrics { + margin-left: 10px; + margin-right: 10px; +} +#image { + margin-left: 10px; + margin-right: 10px; +} + +#temperature { + color: #fa9f8e; + border-radius: 8px 8px 8px 8px; +} +#custom-tailscale { + color: #81a1c1; + border-radius: 8px 8px 8px 8px; +} +#cutom-borg { + color: #fa9f8e; + border-radius: 8px 8px 8px 8px; +} +#power-profiles-daemon { + color: #fa9f8e; + border-radius: 8px 8px 8px 8px; +} +#custom-fans { + color: #fa9f8e; + border-radius: 8px 8px 8px 8px; +} +#memory { + font-size: 15px; + color: #81a1c1; + border-radius: 8px 8px 8px 8px; +} +#custom-inetspeeddown { + font-size: 15px; + color: #81a1c1; + border-radius: 8px 8px 8px 8px; +} +#custom-inetspeedup { + font-size: 15px; + color: #81a1c1; + border-radius: 8px 8px 8px 8px; +} + +@keyframes button_activate { + from { + opacity: 0.3; + } + to { + opacity: 1; + } +} + +#workspaces button { + border: none; + color: #d4d2a9; + padding: 3px; + background: transparent; + box-shadow: none; +} + +#workspaces button.active { + color: #81a1c1; + background: radial-gradient( + circle, + rgba(20, 48, 187, 0.699) 5%, + rgba(36, 39, 54, 0.5) 18%, + rgba(132, 129, 156, 0) 23%, + rgba(70, 76, 98, 0.201) 24%, + rgba(132, 129, 156, 0) 30% + ); + animation: button_activate 0.2s ease-in-out; +} + +#workspaces button.urgent { + color: #fa9f8e; + background: radial-gradient( + circle, + rgba(25, 79, 255, 0.3) 20%, + rgba(132, 129, 156, 0) 30% + ); +} + +#workspaces button.persistent { + color: #85909e; +} + +#workspaces button:hover { + border: none; + background: radial-gradient( + circle, + rgba(25, 79, 255, 0.1) 20%, + rgba(132, 129, 156, 0) 30% + ); +} + +#workspaces button.active:hover, +#workspaces button.urgent:hover { + background: inherit; +} + +#custom-launcher { + margin-left: 10px; + margin-right: 10px; + + padding-right: 5px; + padding-left: 0px; + + font-size: 22px; + + color: #7a95c9; + + margin-top: 1px; + margin-bottom: 1px; + border-radius: 8px 8px 8px 8px; +} + +#workspaces, +#custom-calendar_clock, +#custom-weather, +#custom-network_speed, +#cpu, +#custom-song_name, +#custom-vpn, +#custom-borg, +#temperature, +#custom-fans, +#power-profiles-daemon, +#memory, +#custom-tailscale, +#custom-rate, +#custom-inetspeedup, +#custom-inetspeeddown, +#pulseaudio, +#custom-separator, +#tray #network { + background-color: #3b4252; + padding: 0em 2em; + + font-size: 20px; + + padding-left: 7.5px; + padding-right: 7.5px; + + padding-top: 2px; + padding-bottom: 2px; + + margin-top: 10px; + margin-bottom: 10px; + margin-right: 10px; + + font-size: 19px; +} +#custom-separator { + color: #1b5e20; + margin: 0 5px; +} +#workspaces { + padding-right: 10px; +} +#custom-pwrate { + color: #81a1c1; + padding-left: 10px; + margin-right: 15px; + font-size: 13px; + border-radius: 0px 8px 8px 0px; + margin-left: -15px; +} + +#wireplumber { + color: #81a1c1; + padding-left: 10px; + margin-right: 10px; + font-size: 23px; + border-radius: 0px 8px 8px 0px; + margin-left: -15px; +} + +#wireplumber.muted { + color: #fb958b; + padding-left: 10px; + font-size: 23px; + border-radius: 0px 8px 8px 0px; +} + +#backlight { + color: #81a1c1; + padding-left: 10px; + padding-right: 10px; + font-size: 24px; + border-radius: 8px 8px 8px 8px; +} +#custom-wf-recorder { + color: #ff0000; +} + +#network { + padding-left: 0.2em; + color: #5e81ac; + border-radius: 8px 8px 8px 8px; + padding-left: 12px; + padding-right: 12px; + font-size: 19px; +} + +#network.disconnected { + color: #fb958b; +} + +#battery { + color: #81a1c1; + font-size: 19px; +} + +#battery.critical, +#battery.warning, +#battery.full, +#battery.plugged { + color: #81a1c1; + font-size: 19px; +} + +#battery.charging { + font-size: 19px; +} + +#battery.full, +#battery.plugged { + font-size: 19px; +} + +@keyframes blink { + to { + background-color: rgba(30, 34, 42, 0.5); + color: #abb2bf; + } +} + +#battery.warning { + color: #ecd3a0; +} + +#battery.critical:not(.charging) { + color: #fb958b; +} + +#custom-lock { + color: #ecd3a0; + padding: 0 15px 0 15px; + margin-left: 7px; + margin-top: 7px; + margin-bottom: 7px; +} + +#custom-calendar_clock { + color: #8a909e; + font-size: 15px; + font-weight: bold; + margin-top: 10px; + margin-bottom: 10px; + border-radius: 8px 8px 8px 8px; +} + +#bluetooth { + color: #fb958b; + padding-left: 10px; + margin-right: -5px; + font-size: 18px; + border-radius: 0px 8px 8px 0px; + margin-left: -5px; +} + +#bluetooth.on { + color: #fb958b; + padding-left: 10px; + margin-right: -5px; + font-size: 18px; + border-radius: 0px 8px 8px 0px; + margin-left: -5px; +} + +#bluetooth.connected { + color: #81a1c1; + padding-left: 10px; + margin-right: -5px; + font-size: 18px; + border-radius: 0px 8px 8px 0px; + margin-left: -5px; +} + +#bluetooth.off { + color: #8a909e; + padding-left: 10px; + margin-right: -5px; + font-size: 18px; + border-radius: 0px 8px 8px 0px; + margin-left: -5px; +} + +#custom-powermenu { + color: #e78284; + margin-right: 12px; + margin-left: 12px; + border-radius: 8px; + padding: 0 6px 0 6.8px; + margin-top: 7px; + margin-bottom: 7px; +} + +tooltip { + font-family: Ubuntu Nerd Font; + border-radius: 8px; + padding: 15px; + background-color: #242933; +} + +tooltip label { + font-family: Ubuntu Nerd Font; + padding: 5px; + background-color: #242933; +} + +#now_playing > tooltip { + background-color: red; +} + +label:focus { + background-color: #242933; +} + +#tray { + margin-right: 10px; + margin-top: 10px; + margin-bottom: 10px; + font-size: 30px; + border-radius: 8px 8px 8px 8px; +} + +#tray > .passive { + -gtk-icon-effect: dim; +} + +#tray > .needs-attention { + -gtk-icon-effect: highlight; + background-color: #eb4d4b; +} + +#idle_inhibitor { + background-color: #242933; +} + +#idle_inhibitor.activated { + background-color: #ecf0f1; + color: #2d3436; +} + +#memory, +#custom-rate, +#custom-tailscale, +#custom-inetspeedup, +#custom-inetspeeddown, +#cpu { + font-size: 15px; +} + +#mpris { + font-size: 15px; +} +#custom-roon { + font-size: 15px; +} + +#power-profiles-daemon { + font-size: 15px; +} +#custom-fans { + font-size: 15px; +} +#temperature { + font-size: 15px; +} +#custom-vpn { + font-size: 15px; +} +#custom-borg { + font-size: 15px; +} +#custom-song_name, +#custom-spotify, +#custom-next, +#custom-pre { + background-color: #3b4252; + font-size: 20px; + padding-right: 6.5px; + margin-top: 10px; + margin-bottom: 10px; + font-weight: bold; +} + +#custom-weather.severe { + color: #8a909e; + font-size: 15px; + font-weight: bold; + margin-top: 10px; + margin-right: 10px; + margin-bottom: 10px; + border-radius: 8px 8px 8px 8px; +} + +#custom-weather.sunnyDay { + color: #8a909e; + font-size: 15px; + font-weight: bold; + margin-top: 10px; + margin-right: 10px; + margin-bottom: 10px; + border-radius: 8px 8px 8px 8px; +} + +#custom-weather.clearNight { + color: #8a909e; + font-size: 15px; + font-weight: bold; + margin-top: 10px; + margin-right: 10px; + margin-bottom: 10px; + border-radius: 8px 8px 8px 8px; +} + +#custom-weather.cloudyFoggyDay, +#custom-weather.cloudyFoggyNight { + color: #8a909e; + font-size: 15px; + font-weight: bold; + margin-top: 10px; + margin-right: 10px; + margin-bottom: 10px; + border-radius: 8px 8px 8px 8px; +} + +#custom-weather.rainyDay, +#custom-weather.rainyNight { + color: #8a909e; + font-size: 15px; + font-weight: bold; + margin-top: 10px; + margin-right: 10px; + margin-bottom: 10px; + border-radius: 8px 8px 8px 8px; +} + +#custom-weather.showyIcyDay, +#custom-weather.snowyIcyNight { + color: #8a909e; + font-size: 15px; + font-weight: bold; + margin-top: 10px; + margin-right: 10px; + margin-bottom: 10px; + border-radius: 8px 8px 8px 8px; +} + +#custom-weather { + color: #8a909e; + font-size: 15px; + font-weight: bold; + margin-top: 10px; + margin-right: 10px; + margin-bottom: 10px; + border-radius: 8px 8px 8px 8px; +} +#custom-network_speed { + color: #8a909e; + font-size: 15px; + font-weight: bold; + margin-top: 10px; + margin-right: 10px; + margin-bottom: 10px; + border-radius: 8px 8px 8px 8px; +} +#waybar.info { + background-color: rgba(10, 10, 10, 0.0); + font-family: JetBrainsMonoNL Nerd Font Mono; + font-size: 14px; +} + +#submap { + background-color: rgba(10, 10, 10, 0.8); + color: rgba(152, 239, 106, 1); +} diff --git a/.config/waybar/sway_config b/.config/waybar/sway_config new file mode 100644 index 0000000..061f7ea --- /dev/null +++ b/.config/waybar/sway_config @@ -0,0 +1,210 @@ +{ + "layer": "top", + "position": "top", + "margin-top": 10, + "margin-left": 10, + "margin-right": 10, + "height": 54, + "spacing": 0, + "modules-left": ["custom/launcher","sway/workspaces","cpu","temperature","power-profiles-daemon","memory","custom/vpn"], + "modules-center": ["sway/window"], + "modules-right": ["tray","custom/wf-recorder","bluetooth","network", "wireplumber","custom/weather","clock","battery","custom/powermenu"], + +// "sway/workspaces": { +// "on-click": "activate", +// "format": "{icon}", +// "format-icons": { +// "default": "", +// "1": "", +// "2": "", +// "3": "", +// "4": "", +// "active": "󱓻", +// "urgent": "󱓻", +// }, +// "persistent-workspaces": { +// "*": 4, +// } + // }, + + "sway/workspaces": { + "disable-scroll": true, + "all-outputs": true, + "format": "{icon}", + "format-icons": { + "1": "", + "2": "", + "3": "", + "4": "", + "urgent": "󱓻", + "focused": "󱓻", + "default": "" + }, + "persistent-workspaces": { + "1": [], + "2": [], + "3": [], + "4": [] + } + }, + + "custom/vpn": { + //pkill -RTMIN+10 waybar + "format": "VPN ", + "exec": "echo '{\"class\": \"connected\"}'", + "exec-if": "test -d /proc/sys/net/ipv4/conf/wg0", + "return-type": "json", + "signal": 10, + }, + + "custom/wf-recorder": { + //pkill -RTMIN+11 waybar + "format": "• ", + "exec": "echo '{\"class\": \"recording\"}'", + "exec-if": "pgrep -x wf-recorder >/dev/null", + "return-type": "json", + "signal": 11, + }, + + + "backlight": { + "device": "amdgpu_bl1", + "format": "{icon}", + "format-icons": [" "," "," "," "," "," "," "," "," "," "," "," "," "," "," "] + }, + + "custom/weather": { + "exec": "python ~/.config/waybar/scripts/weather.py", + "restart-interval": 300, + "return-type": "json", + // "on-click": "xdg-open https://weather.com/en-IN/weather/today/l/$(location_id)" + // "format-alt": "{alt}", + }, + + "bluetooth": { + "format": "", + "format-connected": "", + "format-connected-battery": "", + "tooltip-format": "{controller_alias}\t{controller_address}\n\n{num_connections} connected", + "tooltip-format-connected": "{controller_alias}\t{controller_address}\n\n{num_connections} connected\n\n{device_enumerate}", + "tooltip-format-enumerate-connected": "{device_alias}\t{device_address}", + "tooltip-format-enumerate-connected-battery": "{device_alias}\t{device_address}\t{device_battery_percentage}%", + "on-click": "/home/teraflops/.local/bin/bt.sh", + "on-click-right" :"bluetoothctl connect 88:C9:E8:EF:69:1E", + }, + + "hyprland/window": { + "max-length": 80, + "format": "{}", + "separate-outputs": true, + }, + + "temperature": { + "hwmon-path": ["/sys/class/thermal/thermal_zone0/temp"], + "critical-threshold": 80, + "format-critical": "{temperatureC}°C", + "format": "{temperatureC}°C" + }, + + "memory": { + "interval": 3, + "format": "  {}% ", + "max-length": 10 + }, + + "cpu": { + "interval": 1, + "format": "  {}% ", + "max-length": 10 + }, + + "power-profiles-daemon": { + "format": "{icon}", + "tooltip-format": "Power profile: {profile}\nDriver: {driver}", + "tooltip": true, + "format-icons": { + "default": "BAL", + "performance": "PERF", + "balanced": "BAL", + "power-saver": "PSAV" + } + }, + + "custom/powermenu": { + "format": "⏻ ", + "tooltip": false, + "on-click": "/home/teraflops/.config/waybar/scripts/wlogout.sh" + }, + + "tray": { + "spacing": 10 + }, + + "clock": { + "tooltip-format": "{calendar}", + "format-alt": " {:%a, %d %b %Y} ", + "format": " {:%H:%M} ", + "calendar": { + "weeks-pos" : "none", + "on-scroll" : 1, + "format": { + "months": "{}", + "days": "{}", + "weeks": "W{}", + "weekdays": "{}", + "today": "{}" + } + }, + "actions": { + "on-click-right": "none", + "on-click-forward": "tz_up", + "on-click-backward": "tz_down", + "on-scroll-up": "shift_up", + "on-scroll-down": "shift_down" + } + }, + + "network": { + "format-wifi": "{icon}", + "format-disconnected": "󰤭 ", + "format-icons": ["󰤯 ","󰤟 ","󰤢 ","󰤥 ","󰤨 "], + "tooltip-format-wifi": "{essid}", + "tooltip-format-disconnected": "Disconnected", + "nospacing": 1, + }, + + "battery": { + "interval": 60, + "states": { + "warning": 30, + "critical": 15 + }, + "format": "{icon}", + "format-charging": " ", + "format-plugged": "{icon}", + "format-icons": [" ", " ", " ", " ", " "], + "max-length": 25, + "on-click": "gnome-terminal -- battop", + }, + + "wireplumber": { + "format": "{icon}", + "nospacing": 1, + "tooltip-format": "Volume : {volume}%", + "format-muted": "󰝟 ", + "format-icons": { + "headphone": " ", + "default": ["󰕿 ", "󰖀 ", "󰕾 "] + }, + "on-click": "pamixer -t", + "on-click-right": "pavucontrol", + "scroll-step": 5, + }, + + "custom/launcher": { + "format": "  ", + "tooltip": false, + "on-click": "/home/teraflops/.config/rofi/launchers/misc/launcher.sh &" + }, +} +