diff --git a/home/hyprland/default.nix b/home/hyprland/default.nix index 870b67c..3fe5a1e 100644 --- a/home/hyprland/default.nix +++ b/home/hyprland/default.nix @@ -11,6 +11,8 @@ in { imports = [ ./kitty.nix # Terminal + ../waybar + ../rofi ]; diff --git a/home/waybar/config/config.jsonc b/home/waybar/config/config.jsonc new file mode 100644 index 0000000..3614ba4 --- /dev/null +++ b/home/waybar/config/config.jsonc @@ -0,0 +1,607 @@ +// --// waybar config generated by wbarconfgen.sh //-- // + +[ +/*{ + "layer": "top", + "position": "left", + "mod": "dock", + "width": 20, + "margin-top": 200, + "margin-bottom": 200, + "exclusive": true, + "passthrough": false, + "gtk-layer-shell": true, + "reload_style_on_change": true, + "output": "HDMI-A-2", + + "modules-left": [], + "modules-center": ["wlr/taskbar"], + "modules-right": [], + + "wlr/taskbar": { + "all-outputs": true, + "format": "{icon}", + "icon-size": 20, + "active-first": true + } +},*/ + +{ +// sourced from header module // + + "layer": "top", + "position": "top", + "mod": "dock", + "margin-left": 10, + "margin-right": 10, + "margin-top": 7, + "margin-bottom": 0, + "exclusive": true, + "passthrough": false, + "gtk-layer-shell": true, + "reload_style_on_change": true, + "output": "DP-1", + +// positions generated based on config.ctl // + + "modules-left": ["custom/smallspacer","hyprland/workspaces","custom/spacer","mpris"], + "modules-center": ["custom/padd","custom/l_end","custom/r_end","hyprland/window","custom/padd"], + "modules-right": ["custom/padd","custom/l_end","group/expand","network","group/expand-3","group/expand-2","group/expand-4","memory","cpu","clock","custom/notification","custom/padd"], + + +// sourced from modules based on config.ctl // + +"custom/led": { + "format": "󰍿 ", + "format-alt": "󰍿 ", + "on-click": "~/mouse.sh", + "tooltip": false, +}, + +"upower": { + "icon-size": 20, + "format": "", + "format-alt": "{}[{time}]", + "tooltip": true, + "tooltip-spacing": 20, + "on-click-right": "pkill waybar & hyprctl dispatch exec waybar" +}, + +"upower#headset": { +"format": " {percentage}", +"native-path": "/org/freedesktop/UPower/devices/headset_dev_A6_98_9A_0D_D3_49", +"show-icon": false, +"tooltip": false, +}, + +"group/expand-4": { + "orientation": "horizontal", + "drawer": { + "transition-duration": 600, + "children-class": "not-power", + "transition-to-left": true, + "click-to-reveal": true + }, + "modules": ["upower","upower/headset"] + }, + +"custom/smallspacer":{ +"format": " ", +}, +"memory": { + "interval": 1, + "rotate": 270, + "format": "{icon}", + "format-icons": ["󰝦","󰪞","󰪟","󰪠","󰪡","󰪢","󰪣","󰪤","󰪥"], + "max-length": 10 +}, +"cpu": { + "interval": 1, + "format": "{icon}", + "rotate": 270, + "format-icons": ["󰝦","󰪞","󰪟","󰪠","󰪡","󰪢","󰪣","󰪤","󰪥"], +}, + + +"mpris": { + "format": "{player_icon} {dynamic}", + "format-paused": "{status_icon} {dynamic}", + "max-length": 100, + "player-icons": { + "default": "⏸", + "mpv": "🎵" + }, + "status-icons": { + "paused": "▶" + }, + // "ignored-players": ["firefox"] +}, +"tray": { + "icon-size": 16, + "rotate": 0, + "spacing": 3 + }, + + "group/expand": { + "orientation": "horizontal", + "drawer": { + "transition-duration": 600, + "children-class": "not-power", + "transition-to-left": true, + // "click-to-reveal": true + }, + "modules": ["custom/menu","custom/spacer","tray"] + }, + + "custom/menu":{ + "format": "󰅃", + "rotate": 90, + }, + + +"custom/notification": { + "tooltip": false, + "format": "{icon}", + "format-icons": { + "notification": "󰅸", + "none": "󰂜", + "dnd-notification": "󰅸", + "dnd-none": "󱏨", + "inhibited-notification": "󰅸", + "inhibited-none": "󰂜", + "dnd-inhibited-notification": "󰅸", + "dnd-inhibited-none": "󱏨" + }, + "return-type": "json", + "exec-if": "which swaync-client", + "exec": "swaync-client -swb", + "on-click-right": "swaync-client -d -sw", + "on-click": "swaync-client -t -sw", + "escape": true + }, + + "hyprland/window": { +//"format": "{}" // <--- these is the default value +"format": "{class}", + "max-length": 120, + "icon": false, + "icon-size": 13, +}, + + "custom/power": { + "format": "@{}", + "rotate": 0, + "on-click": "ags -t ControlPanel", + "on-click-right": "pkill ags", + "tooltip": true + }, + + "custom/spacer":{ + "format": "|" + }, + + + + + +"hyprland/workspaces": { + "format": "{icon}", + "format-icons": { + "default": "", + "active": "", + //"default": "○", + //"default": "●" + }, +}, + +"wlr/workspaces": { + "persistent-workspaces": { + "3": [], // Always show a workspace with name '3', on all outputs if it does not exists + "4": ["eDP-1"], // Always show a workspace with name '4', on output 'eDP-1' if it does not exists + "5": ["eDP-1", "DP-2"] // Always show a workspace with name '5', on outputs 'eDP-1' and 'DP-2' if it does not exists + } +}, + + +"cava": { + "cava_config": "~/.config/cava/config", + "framerate": 60, + "autosens": 1, + "bars": 14, + "lower_cutoff_freq": 50, + "higher_cutoff_freq": 10000, + "method": "pulse", + "source": "auto", + "stereo": true, + "reverse": false, + "bar_delimiter": 0, + "monstercat": false, + "waves": false, + "noise_reduction": 0.77, + "input_delay": 2, + "format-icons" : ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█" ], + "actions": { + "on-click-right": "mode" + } + }, + + "custom/script": { + "on-click": "~/.config/waybar/volume.sh toggle", + "format": "", + + }, + + "custom/cliphist": { + "format": "{}", + "rotate": 0, + "exec": "echo ; echo 󰅇 clipboard history", + "on-click": "sleep 0.1 && cliphist.sh c", + "on-click-right": "sleep 0.1 && cliphist.sh d", + "on-click-middle": "sleep 0.1 && cliphist.sh w", + "interval" : 86400, // once every day + "tooltip": true + }, + + "custom/wbar": { + "format": "𐌏{}", //   // + "rotate": 0, + "exec": "echo ; echo show app menu", + "on-click": "wofi --show drun", + "on-click-right": "wbarconfgen.sh p", + "on-click-middle": "sleep 0.1 && quickapps.sh kitty firefox spotify code dolphin", + "interval" : 86400, + "tooltip": true + }, + + "custom/theme": { + "format": "{}", + "rotate": 0, + "exec": "echo ; echo 󰟡 pick color", + "on-click": "hyprpicker", + "on-click-right": "themeswitch.sh -p", + "on-click-middle": "sleep 0.1 && themeselect.sh", + "interval" : 86400, // once every day + "tooltip": true + }, + + "custom/wallchange": { + "format": "{}", + "rotate": 0, + "exec": "echo ; echo 󰆊 switch wallpaper", + "on-click": "swww img --transition-type grow --transition-pos 0.071,0.988 --transition-step 255 --transition-fps 60 /home/anik/Downloads/skyway.jpg", + "on-click-right": "swww img --transition-type grow --transition-pos 0.071,0.988 --transition-step 255 --transition-fps 60 /home/anik/Downloads/cloud.png", + "on-click-middle": "swww img --transition-type grow --transition-pos 0.071,0.988 --transition-step 255 --transition-fps 60 /home/anik/Downloads/gradient.jpg", + "on-scroll-up": "swww img --transition-type grow --transition-pos 0.071,0.988 --transition-step 255 --transition-fps 60 /home/anik/Downloads/torvalds.png", + "on-scroll-down": "swww img --transition-type grow --transition-pos 0.071,0.988 --transition-step 255 --transition-fps 60 /home/anik/Downloads/night.png", + "interval" : 86400, // once every day + "tooltip": true + }, + "custom/mouse": + { + "format": "", + "format-alt": "", + + "on-click": "m8mouse -dpi 1 -led 2 -speed 4", + "on-click-right": "m8mouse -dpi 1 -led 4 -speed 4", + "on-click-middle": "m8mouse -dpi 1 -led 7 -speed 4", + "tooltip": true + }, + + "wlr/taskbar": { + "format": "{icon}", + "rotate": 0, + "icon-size": 18, + "icon-theme": "Tela-circle-dracula", + "spacing": 0, + "tooltip-format": "{title}", + "on-click": "activate", + "on-click-middle": "close", + "ignore-list": [ + "Alacritty" + ], + "app_ids-mapping": { + "firefoxdeveloperedition": "firefox-developer-edition" + } + }, + + "custom/spotify": { + "exec": "mediaplayer.py --player spotify", + "format": " {}", + "rotate": 0, + "return-type": "json", + "on-click": "playerctl play-pause --player spotify", + "on-click-right": "playerctl next --player spotify", + "on-click-middle": "playerctl previous --player spotify", + "on-scroll-up": "volumecontrol.sh -p spotify i", + "on-scroll-down": "volumecontrol.sh -p spotify d", + "max-length": 25, + "escape": true, + "tooltip": true + }, + + "idle_inhibitor": { + "format": "{icon}", + "rotate": 0, + "format-icons": { + "activated": "󰥔", + "deactivated": "" + } + }, + + "clock": { + "format": "{:%I:%M %p}", + "rotate": 0, + "on-click": "/usr/local/bin/ags -t ActivityCenter", + "tooltip-format": "{calendar}", + "calendar": { + "mode": "month", + "mode-mon-col": 3, + "on-scroll": 1, + "on-click-right": "mode", + "format": { + "months": "{}", + "weekdays": "{}", + "today": "{}" + } + }, + "actions": { + "on-click-right": "mode", + "on-click-forward": "tz_up", + "on-click-backward": "tz_down", + "on-scroll-up": "shift_up", + "on-scroll-down": "shift_down" + } + }, + + + "battery": { + "states": { + "good": 95, + "warning": 30, + "critical": 20 + }, + "format": "{icon}", + "rotate": 0, + "format-charging": "󱐋", + "format-plugged": "󰂄", + // "format-alt": "<{time} | {capacity}%", + "format-icons": ["󰝦","󰪞","󰪟","󰪠","󰪡","󰪢","󰪣","󰪤","󰪥"], + // "format-icons": ["","","","","","","",""], + //"format-icons": ["󰂎", "󰁺", "󰁻", "󰁼", "󰁽", "󰁾", "󰁿", "󰂀", "󰂁", "󰂂", "󰁹"], + "on-click-right": "pkill waybar & hyprctl dispatch exec waybar", + // "format-icons": [], + + }, + + "backlight": { + "device": "intel_backlight", + "rotate": 0, + "format": "{icon}", + "format-icons": ["󰃞", "󰃝", "󰃟", "󰃠"], + "scroll-step": 1, + "min-length": 2 + }, + + "group/expand-2": { + "orientation": "horizontal", + "drawer": { + "transition-duration": 600, + "children-class": "not-power", + "transition-to-left": true, + "click-to-reveal": true + }, + "modules": ["backlight","backlight/slider","custom/smallspacer","custom/led"] + }, + + "group/expand-3": { + "orientation": "horizontal", + "drawer": { + "transition-duration": 600, + "children-class": "not-power", + "transition-to-left": true, + "click-to-reveal": true + }, + "modules": ["pulseaudio","pulseaudio/slider"] + }, + + "network": { + "tooltip": true, + "format-wifi": "{icon} ", + "format-icons": ["󰤟", "󰤢", "󰤥"], + // "format-wifi": "", + "rotate": 0, + "format-ethernet": "󰈀 ", + "tooltip-format": "Network: {essid}\nSignal strength: {signaldBm}dBm ({signalStrength}%)\nFrequency: {frequency}MHz\nInterface: {ifname}\nIP: {ipaddr}/{cidr}\nGateway: {gwaddr}\nNetmask: {netmask}", + "format-linked": "󰈀 {ifname} (No IP)", + "format-disconnected": " ", + "tooltip-format-disconnected": "Disconnected", + "on-click": "/usr/local/bin/ags -t ControlPanel", + "interval": 2, + }, + + "pulseaudio": { + "format": "{icon}", + "rotate": 0, + "format-muted": "婢", + "tooltip-format": "{icon} {desc} // {volume}%", + "scroll-step": 5, + "format-icons": { + "headphone": "", + "hands-free": "", + "headset": "", + "phone": "", + "portable": "", + "car": "", + "default": ["", "", ""] + } + }, + + "pulseaudio#microphone": { + "format": "{format_source}", + "rotate": 0, + "format-source": "", + "format-source-muted": "", + "on-click": "pavucontrol -t 4", + "on-click-middle": "volumecontrol.sh -i m", + "on-scroll-up": "volumecontrol.sh -i i", + "on-scroll-down": "volumecontrol.sh -i d", + "tooltip-format": "{format_source} {source_desc} // {source_volume}%", + "scroll-step": 5 + }, + + "custom/notifications": { + "tooltip": false, + "format": "{icon} {}", + "rotate": 0, + "format-icons": { + "email-notification": "", + "chat-notification": "󱋊", + "warning-notification": "󱨪", + "error-notification": "󱨪", + "network-notification": "󱂇", + "battery-notification": "󰁺", + "update-notification": "󰚰", + "music-notification": "󰝚", + "volume-notification": "󰕿", + "notification": "", + "none": "" + }, + "return-type": "json", + "exec-if": "which dunstctl", + "exec": "notifications.py", + "on-click": "sleep 0.1 && dunstctl history-pop", + "on-click-middle": "dunstctl history-clear", + "on-click-right": "dunstctl close-all", + "interval": 1, + "tooltip": true, + "escape": true + }, + + "custom/keybindhint": { + "format": " ", + "rotate": 0, + "on-click": "keybinds_hint.sh" + }, + +"custom/expand": { + "on-click":"~/.config/hypr/scripts/expand_toolbar", + "format":"{}", + "exec":"~/.config/hypr/scripts/tools/expand arrow-icon" + }, + +// modules for padding // + + "custom/l_end": { + "format": " ", + "interval" : "once", + "tooltip": false + }, + + "custom/r_end": { + "format": " ", + "interval" : "once", + "tooltip": false + }, + + "custom/sl_end": { + "format": " ", + "interval" : "once", + "tooltip": false + }, + + "custom/sr_end": { + "format": " ", + "interval" : "once", + "tooltip": false + }, + + "custom/rl_end": { + "format": " ", + "interval" : "once", + "tooltip": false + }, + + "custom/rr_end": { + "format": " ", + "interval" : "once", + "tooltip": false + }, + + "custom/padd": { + "format": " ", + "interval" : "once", + "tooltip": false + }, + + +"backlight/slider": { + "min": 5, + "max": 100, + "rotate": 0, + "device": "intel_backlight", + "scroll-step": 1, + }, + + "pulseaudio/slider": { + "min": 5, + "max": 100, + "rotate": 0, + "device": "pulseaudio", + "scroll-step": 1, + }, +}, +{ +// sourced from header module // + + "layer": "top", + "position": "top", + "mod": "dock", + "margin-left": 10, + "margin-right": 10, + "margin-top": 7, + "margin-bottom": 0, + "exclusive": true, + "passthrough": false, + "gtk-layer-shell": true, + "reload_style_on_change": true, + "output": "HDMI-A-2", + +// positions generated based on config.ctl // + + "modules-left": ["hyprland/workspaces"], + "modules-center": ["custom/weather"], + "modules-right": ["network"], + + +// sourced from modules based on config.ctl // + "hyprland/workspaces": { + "format": "{icon}", + "format-icons": { + "default": "", + "active": "", + //"default": "○", + //"default": "●" + } + }, + + "custom/weather": { + "exec": "python3 ~/.config/waybar/weather.py waybar", + "restart-interval": 900, + "return-type": "json" + }, + + "network": { + "tooltip": true, + "format-wifi": "{icon} ", + "format-icons": ["󰤟", "󰤢", "󰤥"], + // "format-wifi": "", + "rotate": 0, + "format-ethernet": "{bandwidthTotalBits} 󰈀 {bandwidthUpBits} 󰶣 {bandwidthDownBits} 󰶡 ", + "tooltip-format": "Network: {essid}\nSignal strength: {signaldBm}dBm ({signalStrength}%)\nFrequency: {frequency}MHz\nInterface: {ifname}\nIP: {ipaddr}/{cidr}\nGateway: {gwaddr}\nNetmask: {netmask}", + "format-linked": "󰈀 {ifname} (No IP)", + "format-disconnected": " ", + "tooltip-format-disconnected": "Disconnected", + "on-click": "/usr/local/bin/ags -t ControlPanel", + "interval": 1, + }, +}] diff --git a/home/waybar/config/style.css b/home/waybar/config/style.css new file mode 100644 index 0000000..dc23957 --- /dev/null +++ b/home/waybar/config/style.css @@ -0,0 +1,397 @@ +* { + font-family: "JetBrains Mono Nerd Font"; + /*font-family: "JetBrainsMono Nerd Font";*/ + font-weight: bold; + font-size: 15px; +} + +#custom-notification { + font-family: "JetBrains Mono Nerd Font"; + font-size: 17px; + color: #A1BDCE; + margin: 2px 0px 0px 0px; +} + +window#waybar { + background: #092047; + /* border-radius: 15px; */ + /* border: 2px solid #124323; */ +/* border: 0px solid #A1BDCE; */ + border: 3px solid rgba(172, 97, 185, 1); + border-radius: 10px; +} + +tooltip { + background: #171717; + color: #A1BDCE; + font-size: 13px; + border-radius: 7px; + border: 2px solid #101a24; + + + } +#workspaces{ +background: rgba(23, 23, 23, 0.0); + color: #888789; + box-shadow: none; + text-shadow: none; + border-radius: 9px; + transition: 0.2s ease; + padding-left: 4px; + padding-right: 4px; + padding-top: 1px; +} + + +#workspaces button { +background: rgba(23, 23, 23, 0.0); + color: #A1BDCE; + box-shadow: none; + text-shadow: none; + border-radius: 9px; + transition: 0.2s ease; + padding-left: 4px; + padding-right: 4px; + /* animation: ws_normal 20s ease-in-out 1; */ +} + + + +#workspaces button.active { + + + /* background-image: url("/home/anik/Documents/bar1.png");*/ + color: #FF2A6D; + transition: all 0.3s ease; + padding-left: 4px; + padding-right: 4px; + /* transition: all 0.4s cubic-bezier(.55,0.68,.48,1.682); */ +} + +#workspaces button:hover { + background: none; + color: #65DC98; + animation: ws_hover 20s ease-in-out 1; + transition: all 0.5s cubic-bezier(.55,-0.68,.48,1.682); +} + +#taskbar button { + box-shadow: none; + text-shadow: none; + font-size: 4px; + padding: 0px; + border-radius: 9px; + margin-bottom: 3px; + margin-left: 0px; + padding-left: 3px; + padding-right: 3px; + margin-right: 0px; + color: @wb-color; + animation: tb_normal 20s ease-in-out 1; +} + +#taskbar button.active { + background: @wb-act-bg; + color: @wb-act-color; + margin-left: 3px; + padding-left: 12px; + padding-right: 12px; + margin-right: 3px; + animation: tb_active 20s ease-in-out 1; + transition: all 0.4s cubic-bezier(.55,-0.68,.48,1.682); + min-height: 9px; +} + +#taskbar button:hover { + background: @wb-hvr-bg; + color: @wb-hvr-color; + animation: tb_hover 20s ease-in-out 1; + transition: all 0.3s cubic-bezier(.55,-0.68,.48,1.682); +} + +#tray menu * { + min-height: 16px; + font-weight: bold; + font-size: 13px; + color: #9488e3; +} + +#tray menu separator { + min-height: 10px +} + + +#custom-spacer{ +opacity: 0.0; +} +#custom-smallspacer{ +opacity: 0.0; +} + + +#custom-mouse{ +font-size: 14px; +margin-bottom: 6px; +background: #161320; +} + + +#custom-power{ + font-size: 15px; + color: #FFFFFF; + background: rgba(22, 19, 32, 0.9); + margin: 6px 0px 6px 0px; + padding-left: 4px; + padding-right: 4px; + } + +#backlight{ + color: #2096C0; + background: rgba(23, 23, 23, 0.0); + font-weight: normal; + font-size: 19px; + margin: 1px 0px 0px 0px; + padding-left: 0px; + padding-right: 2px; + +} +#bluetooth, +#custom-cliphist{ + color: #E6E7E7; + background: #161320; + opacity: 1; + margin: 4px 0px 4px 0px; + padding-left: 4px; + padding-right: 4px; + +} +#battery{ + font-weight: normal; + font-size: 22px; + color: #a6d189; + background: rgba(23, 23, 23, 0.0); + opacity: 1; + margin: 0px 0px 0px 0px; + padding-left: 0px; + padding-right: 0px; + +} + +#idle_inhibitor{ +color: #24966e; +background: @bar-bg; + opacity: 1; + margin: 4px 0px 4px 0px; + padding-left: 4px; + padding-right: 4px; + +} +#clock{ + color: #FDD870; + font-size: 15px; + font-weight: 900; + font-family: "JetBrains Mono Nerd Font"; + background: rgba(23, 23, 23, 0.0); + opacity: 1; + margin: 3px 0px 0px 0px; + padding-left: 10px; + padding-right: 10px; + border: none; + +} +#pulseaudio{ +font-weight: normal; +font-size: 18px; +color: #0A9CF5; + background: rgba(22, 19, 32, 0.0); + opacity: 1; + margin: 0px 0px 0px 0px; + padding-left: 3px; + padding-right: 3px; +} +#cpu{ +font-weight: normal; +font-size: 22px; +color: #FF184C; +} +#custom-led{ +background: #427287; +color: #FFFFFF; +margin-top: 7px; +margin-bottom: 7px; +padding-left: 6px; +border-radius: 7px; +margin-right: 6px; +} +#custom-gpuinfo, +#custom-keybindhint, +#language, +#memory{ +font-weight: normal; +font-size: 22px; +color: #1AFE49; +} +#mpris{ +color: white; +animation: repeat; + animation-name: blink; + animation-duration: 3s; + animation-timing-function: linear; + animation-iteration-count: infinite; + animation-direction: alternate; +} + +@keyframes blink { + to { + color: #4a4a4a; + + } +} +#network{ +color: #FF6E27; +font-weight: normal; +font-size: 19px; +padding-right: 0px; +padding-left: 4px +} +#custom-notifications, +#custom-spotify, +#taskbar, +#custom-theme, +#custom-menu{ +color: #E8EDF0; +background: rgba(23, 23, 23, 0.0); +margin: 0px 0px 0px 0px; + padding-left: 1px; + padding-right: 1px; + opacity: 0.1 +} +#tray, +#custom-updates, +#custom-wallchange, +#custom-wbar, +#window{ +color: #A1BDCE; +font-family: "Martian Mono"; +} +#custom-l_end, +#custom-r_end, +#custom-sl_end, +#custom-sr_end, +#custom-rl_end, +#cava, +#upower#headset, +#upower{ +color: #a6d189; +} +#mpris{ +font-size: 15px; +font-weight: bold +} +#custom-rr_end { +font-weight: normal; + color: #E8EDF0; + background: rgba(23, 23, 23, 0.0); + opacity: 1; + margin: 0px 0px 0px 0px; + padding-left: 4px; + padding-right: 4px; + ; + +} + +#backlight-slider slider, +#pulseaudio-slider slider { + background: #A1BDCE; + background-color: transparent; + box-shadow: none; + margin-right: 7px; +} + +#backlight-slider trough, +#pulseaudio-slider trough { + margin-top: -3px; + min-width: 90px; + min-height: 10px; + margin-bottom: -4px; + border-radius: 8px; + background: #343434; +} + +#backlight-slider highlight, +#pulseaudio-slider highlight { + border-radius: 8px; + background-color: #2096C0; +} + +#battery.charging, #battery.plugged { + color: #E8EDF0; + +} + + +#battery.critical:not(.charging) { + color: red; +} + + +#taskbar { + padding: 1px; +} + +#custom-r_end { + border-radius: 0px 7px 7px 0px; + margin-right: 1px; + padding-right: 3px; +} + +#custom-l_end { + border-radius: 7px 0px 0px 7px; + margin-left: 1px; + padding-left: 3px; +} + +#custom-sr_end { + border-radius: 0px; + margin-right: 1px; + padding-right: 3px; +} + +#custom-sl_end { + border-radius: 0px; + margin-left: 1px; + padding-left: 3px; +} + +#custom-rr_end { + border-radius: 0px 7px 7px 0px; + margin-right: 1px; + padding-right: 3px; +} + +#custom-rl_end { + border-radius: 7px 0px 0px 7px; + margin-left: 1px; + padding-left: 3px; +} + + +/* Style for launchers */ + +#custom-expand { + min-width: 25px; + color: #A1BDCE; + font-size: 16px; +} + +#group-minimized { + border-left: solid; + border-left-width: 0.5; +} + +#custom-quote { + padding-top:0px; + color: #999999; + font-family: "JetBrains Mono Nerd Font"; + font-size: 13px; +} diff --git a/home/waybar/config/weather.py b/home/waybar/config/weather.py new file mode 100755 index 0000000..5dcf4b2 --- /dev/null +++ b/home/waybar/config/weather.py @@ -0,0 +1,528 @@ +#!/usr/bin/env python3 + +import datetime +import json +import requests +import statistics +import sys + + + +# TODO +# - snowfall data +# - weather warnings + + + +### CONSTANTS ### + +# api key - get it at https://openweathermap.org/ +API_KEY = "bfca28ca110b5595380d9aad164d5ee5" + +# latitude and longitude of the city you want to query +# can be obtained through `./weather.py geocoding ` +LATITUDE = 52.264149 +LONGITUDE = 10.526420 + +# waybar colors +GRAY = '#b7c1de' +DARK = '#01012b' +GREEN = '#65dc98' +YELLOW = '#ffe69d' +ORANGE = '#ff6e27' +RED = '#ff124f' +PURPLE = '#7a04eb' +BLUE = '#8386f5' +MOIST = '#e96d5e' + + +### UTILITIES ### + +def print_error(msg: str): + ''' + print an error message with appropriate prefix. + + @param str: the message to print + ''' + print('\x1b[90m[\x1b[31merr\x1b[90m]\x1b[0m', msg) + + +def print_help(): + ''' + print help message, to be used for the `--help` flag and as response to + incorrect usage + ''' + print('usage: \x1b[33m./weather.py [options]\x1b[0m') + print() + print('available subcommands:') + print(' - \x1b[32mgeocoding\x1b[0m : search for city to get its coordinates') + print(' - \x1b[32mcurrent\x1b[0m : print current weather information') + print(' - \x1b[32mforecast[-daily]\x1b[0m : print forecast for the next ~5 days') + print(' - \x1b[32mforecast-detail\x1b[0m : print detailed forecast in 3h intervals') + print(' - \x1b[32mwaybar\x1b[0m : get output for usage with waybar') + + +def make_request(call: str) -> list | dict: + ''' + make a request to an openweathermap api and returns the response as either + a list or a dictionary. the api key is added automatically. quits on error. + + @param call: the api path, e.g. `data/2.5/weather?...` + + @return response of the api request as a list or dict + ''' + try: + req = requests.get(f'https://api.openweathermap.org/{call}&appid={API_KEY}') + return req.json() + except: + print_error(f'failed to make request to `/{call}`') + quit(1) + + +def get_wind_direction(deg: int) -> str: + ''' + turn a wind direction specified by meteorological degrees into a + human-readable form + + @param deg: degrees. expected to be in range [0..360] + + @return human-readable form (e.g. 'NE' for `deg == 45`) + ''' + if deg < 22.5: return 'N' + if deg < 67.5: return 'NE' + if deg < 112.5: return 'E' + if deg < 157.5: return 'SE' + if deg < 202.5: return 'S' + if deg < 247.5: return 'SW' + if deg < 292.5: return 'W' + if deg < 337.5: return 'NW' + else: return 'N' + + +def print_entry(label: str, content: str, indent: int = 0, label_width: int = 8): + ''' + print an 'entry' that consists of a label (printed in gray) and some + content. the labels are automatically filled with whitespace to align + multiple lines properly. + + @param label: the label of the line + @param content: content, printed after the label + @param indent: number of spaces to indent with + @param label_width: width of label + ''' + label_with_whitespace = label.ljust(label_width) + print(f'{" " * indent}\x1b[90m{label_with_whitespace}\x1b[0m {content}') + + +def get_weekday(date: str) -> str: + ''' + get weekday from date + + @param date: date as ISO-8601-formatted string (YYYY-mm-dd) + + @return weekday as lowercase string (e.g. 'monday') + ''' + return datetime.datetime.strptime(date, '%Y-%m-%d').strftime('%A').lower() + + +def colorize(text: str, color: str) -> str: + ''' + wrap `text` with pango markup to colorize it for usage with waybar. + + @param text: the text to colorize + @param color: the color as string ('#rrggbb') + ''' + return f'{text}' + + +def waybar_entry(label: str, content: str, indent: int = 2, label_width: int = 9): + ''' + create an 'entry' for a waybar tooltip that consists of a label (printed in + gray) and some content. the labels are automatically filled with whitespace + to align multiple lines properly. + + @param label: the label of the line + @param content: content, printed after the label + @param indent: number of spaces to indent with + @param label_width: width of label + + @return the entry for use within waybar + ''' + + label_with_whitespace = label.ljust(label_width) + return f'{" " * indent}{colorize(label_with_whitespace, GRAY)} {content}\n' + + + +### GEOCODING ### + +def geocoding(search: str): + ''' + call openweathermap's geocoding api to find the coordinates of cities. can + be used to find the values required for the `LATITUDE` and `LONGITUDE` + constants within this script. the results are printed. + + @param: search term in the format 'city[,state][,country]' + ''' + + res = make_request(f'geo/1.0/direct?q={search}&limit=5') + num_results = len(res) + + if (num_results == 0): + print_error('no results found') + else: + print(f'found {num_results} result{"" if num_results == 1 else "s"}:') + for entry in res: + # obtain data + name = entry['name'] + state = entry['state'] if 'state' in entry.keys() else None + country = entry['country'] + latitude = entry['lat'] + longitude = entry['lon'] + + # print data + print(f' - \x1b[32m{name}\x1b[90m', + f'({f"{state}, " if state else ""}{country})\x1b[0m:', + f'latitude = \x1b[35m{latitude}\x1b[0m,', + f'longitude = \x1b[35m{longitude}\x1b[0m') + + +### CURRENT WEATHER ### + +def get_current_weather_data() -> dict: + ''' + get current weather data from openweathermap api. when the `rain` is not + set (due to there not being any rain), it will be added with a rain amount + of 0 mm over the last hour. + + @return api response + ''' + + res = make_request(f'data/2.5/weather?lat={LATITUDE}&lon={LONGITUDE}&units=metric') + + if 'rain' not in res.keys(): + res['rain'] = { '1h': 0 } + + return res + + +def current_weather(): + ''' + print current weather information + ''' + + data = get_current_weather_data() + + # collect relevant data + weather = data['weather'][0]['description'].lower() + temperature = round(data['main']['temp'], 1) + temperature_felt = round(data['main']['feels_like'], 1) + humidity = data['main']['humidity'] + wind_speed = round(data['wind']['speed'], 1) + wind_direction = get_wind_direction(data['wind']['deg']) + rainfall = data['rain']['1h'] + + # print data + print_entry('weather', f'\x1b[32m{weather}\x1b[0m') + print_entry('temp', f'\x1b[33m{temperature} °C\x1b[90m, feels like \x1b[33m{temperature_felt} °C\x1b[0m') + print_entry('humidity', f'\x1b[31m{humidity} % RH\x1b[0m') + print_entry('wind', f'\x1b[35m{wind_speed} m/s\x1b[90m ({wind_direction})\x1b[0m') + print_entry('rain', f'\x1b[34m{rainfall} mm\x1b[0m') + + + +### FORECAST ### + +def get_forecast_data() -> dict: + ''' + get forecast data for the next ~5 days from the openweathermap api, with + data points separated by 3 hours. the data is grouped by date (the api does + not group the data by default and instead sends it as one sequence). + + @return api response grouped by date + ''' + + res = make_request(f'data/2.5/forecast?lat={LATITUDE}&lon={LONGITUDE}&units=metric') + + days = dict() + for i in res['list']: + day = i['dt_txt'].split(' ')[0] + if day not in days.keys(): + days[day] = list() + if 'rain' not in i.keys(): + i['rain'] = { '3h': 0 } + days[day].append(i) + + return days + + +def get_daily_forecast_data() -> dict[dict]: + ''' + obtain forecast data for the next ~5 days, where values are grouped by day. + since the api only provides the data in 3h intervals, the properties of + different data points are combined in order to provide appropriate data for + each day. + + @return the processed data as a dict with key = date and value = data as + another dict + ''' + + res = get_forecast_data() + + output = dict() + + for day in sorted(res): + data = res[day] + number_of_data_points = len(data) + + # collect relevant from data for each day + temperatures = [i['main']['temp'] for i in data] + weather_descriptions = [i['weather'][0]['description'].lower() for i in data] + humidity = [i['main']['humidity'] for i in data] + rainfall = [i['rain']['3h'] for i in data] + precipitation_prob = [i['pop'] for i in data] + wind_speeds = [i['wind']['speed'] for i in data] + weekday = get_weekday(day) + + # min and max temperature for the day + min_temperature = round(min(temperatures)) + max_temperature = round(max(temperatures)) + + # obtain the average weather by finding the weather description with the highest number of occurances in `weather_descriptions` + weather_count = {i: weather_descriptions.count(i) for i in set(weather_descriptions)} + weather_count_max = max(weather_count.values()) + weather_average = tuple(filter(lambda x: weather_count[x] == weather_count_max, weather_count.keys()))[0] + + # humidiy + humidity_average = round(statistics.mean(humidity)) + + # total rainfall and probability of precipitation. also, estimate the total rainfall if not all data points for a day are available + rainfall_total = round(sum(rainfall), 1) + rainfall_total_estimated = round(rainfall_total / number_of_data_points * 8, 1) + max_precipitation_prob = round(max(precipitation_prob) * 100) + + # average wind + wind_average = round(statistics.mean(wind_speeds), 1) + + output[day] = { + 'number_of_data_points': number_of_data_points, + 'min_temperature': min_temperature, + 'max_temperature': max_temperature, + 'weather_average': weather_average, + 'humidity_average': humidity_average, + 'rainfall_total': rainfall_total, + 'rainfall_total_estimated': rainfall_total_estimated, + 'max_precipitation_prob': max_precipitation_prob, + 'wind_average': wind_average, + 'weekday': weekday, + } + + return output + + +def daily_forecast(): + ''' + print forecast data for the next ~5 days + ''' + + daily_data = get_daily_forecast_data() + + for day in sorted(daily_data): + data = daily_data[day] + + # only display data point if at least half of the data points are available + if data['number_of_data_points'] < 4: + continue + + # print data + print(f'\x1b[1m{day}\x1b[0m ({data["weekday"]}):') + print_entry('weather', f'\x1b[32m{data["weather_average"]}\x1b[0m', indent = 2) + print_entry('temp', f'\x1b[33m{data["max_temperature"]} °C\x1b[0m / \x1b[33m{data["min_temperature"]} °C\x1b[0m', indent = 2) + print_entry('humidity', f'\x1b[31m{data["humidity_average"]} % RH\x1b[0m', indent = 2) + print_entry('wind', f'\x1b[35m{data["wind_average"]} m/s\x1b[0m', indent = 2) + print_entry('rain', f'\x1b[34m{data["rainfall_total_estimated"]} mm\x1b[0m' + + f'\x1b[90m{f""" ({data["max_precipitation_prob"]}%)""" if data["max_precipitation_prob"] > 0 else ""}' + + f'{f" (estimated)" if data["rainfall_total"] != data["rainfall_total_estimated"] else ""}\x1b[0m\n', indent = 2) + + + +### DETAILED FORECAST ### + +def detailed_forecast(): + ''' + print forecast data for the next ~5 days, where values are printed for + every 3h interval provided by the api. some data (e.g. humidity or wind + speeds) are omitted. + ''' + + res = get_forecast_data() + + for day in sorted(res): + weekday = get_weekday(day) + print(f'\x1b[1m{day}\x1b[0m ({weekday})') + + for entry in res[day]: + # collect data + weather = entry['weather'][0]['description'].lower() + temperature = round(entry['main']['temp'], 1) + rainfall = entry['rain']['3h'] + precipitation_prob = round(entry['pop'] * 100) + time = f"{int(entry['dt_txt'].split(' ')[1].split(':')[0]):2}h" + + # print data + output = '' + output += f'\x1b[33m{temperature:4} °C\x1b[90m, ' + output += f'\x1b[32m{weather}\x1b[0m' + if rainfall > 0: + output += f'\x1b[90m: \x1b[34m{rainfall} mm \x1b[90m({precipitation_prob}%)\x1b[0m' + print_entry(time, output, indent = 2, label_width = 3) + + print() + + + +### WAYBAR ### + +def waybar_widget(data: dict) -> str: + ''' + get the widget component of the waybar output. contains the current weather + group and temperature. + + @param current weather data + + @return widget component + ''' + + weather = data['weather'][0]['main'].lower() + temperature = round(data['main']['temp']) + + return f'{colorize(weather, MOIST)} {temperature}°' + + +def waybar_current(data: dict) -> str: + ''' + get the current weather overview for the tooltip of the waybar output. + + @param current weather data + + @return formatted current weather overview + ''' + + # retrieve relevant data + weather = data['weather'][0]['description'].lower() + temperature = round(data['main']['temp'], 1) + temperature_felt = round(data['main']['feels_like'], 1) + humidity = data['main']['humidity'] + wind_speed = round(data['wind']['speed'], 1) + wind_direction = get_wind_direction(data['wind']['deg']) + rainfall = data['rain']['1h'] + + # generate output + output = '' + output += waybar_entry('weather', colorize(weather, YELLOW)) + output += waybar_entry('temp', f'{colorize(f"{temperature} °C", ORANGE)}{colorize(", feels like ", GRAY)}{colorize(f"{temperature_felt} °C", ORANGE)}') + output += waybar_entry('humidity', colorize(f'{humidity} % RH', RED)) + output += waybar_entry('wind', f'{colorize(f"{wind_speed} m/s", PURPLE)} {colorize(f"({wind_direction})", GRAY)}') + output += waybar_entry('rain', colorize(f"{rainfall} mm", BLUE)) + return output + + +def waybar_forecast(data: dict) -> str: + ''' + get the daily forecast for the tooltip of the waybar output. + + @param forecast weather data + + @return formatted daily forecast + ''' + + output = '' + + daily_data = get_daily_forecast_data() + + for day in sorted(daily_data): + data = daily_data[day] + + line_content = colorize(f'{data["max_temperature"]:2}°', ORANGE) + \ + colorize(' / ', GRAY) + \ + colorize(f'{data["min_temperature"]:2}°', ORANGE) + \ + colorize(', ', GRAY) + \ + colorize(data['weather_average'], YELLOW) + + if data['rainfall_total_estimated'] > 0: + line_content += colorize(': ', GRAY) + \ + colorize(f'{data["rainfall_total_estimated"]} mm ', BLUE) + \ + colorize(f'({data["max_precipitation_prob"]}%)', GRAY) + + output += waybar_entry(data['weekday'], line_content) + + return output.rstrip() + + +def waybar(): + ''' + get current and forecast weather data and output it formatted in a way that + allows it to be included as a widget in waybar. only shows weather category + and temperature in the widget, but reveals detailed weather information and + a ~5 day forecast in the tooltip. + ''' + + current_data = get_current_weather_data() + forecast_data = get_forecast_data() + + widget = waybar_widget(current_data) + current = waybar_current(current_data) + forecast = waybar_forecast(forecast_data) + tooltip = colorize('current weather', GREEN) + '\n' + current + '\n' + \ + colorize('forecast', GREEN) + '\n' + forecast + + print(json.dumps({ + 'text': widget, + 'tooltip': tooltip, + })) + + + +### MAIN ### + +def main(): + ''' + main function + ''' + + # no parameters or `--help` + if len(sys.argv) == 1 or sys.argv[1] in ('help', '-h', '--help'): + print_help() + + # >= 1 parameter provided + else: + # geocoding + if sys.argv[1] == 'geocoding': + if len(sys.argv) == 3: + geocoding(sys.argv[2]) + else: + print_error('expected argument ``') + print_help() + # constants not set + elif API_KEY is None or LATITUDE is None or LONGITUDE is None: + print_error('please modify the constants within the script before use.') + # current weather + elif sys.argv[1] == 'current': + current_weather() + # daily forecast + elif sys.argv[1] == 'forecast' or sys.argv[1] == 'forecast-daily': + daily_forecast() + # detailed forecast + elif sys.argv[1] == 'forecast-detail': + detailed_forecast() + # waybar + elif sys.argv[1] == 'waybar': + waybar() + # unknown command + else: + print_error(f'unknown command `{sys.argv[1]}`') + print_help() + +if __name__ == '__main__': + main() + diff --git a/home/waybar/default.nix b/home/waybar/default.nix new file mode 100644 index 0000000..341031e --- /dev/null +++ b/home/waybar/default.nix @@ -0,0 +1,16 @@ +{ + pkgs, + config, + ... +}: { + programs.waybar = { + enable = true; + package = pkgs.waybar; + }; + + home.file.".config/waybar" = { + source = ./configs; + # copy the scripts directory recursively + recursive = true; + }; +} diff --git a/users/phil/home.nix b/users/phil/home.nix index e9a9c00..194bce7 100644 --- a/users/phil/home.nix +++ b/users/phil/home.nix @@ -10,7 +10,6 @@ ../../home/hyprland ../../home/programs - ../../home/rofi ]; programs.git = {