From 5075adbeb335c27450ffbf647808a23ed5c3c0c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Jane=C5=BEi=C4=8D?= Date: Thu, 12 Mar 2026 17:41:50 +0100 Subject: [PATCH 1/7] chore: update .gitignore --- .gitignore | 40 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 82eed0b..ace5a14 100644 --- a/.gitignore +++ b/.gitignore @@ -63,13 +63,51 @@ swayidle/* waybar/* !waybar/config.jsonc !waybar/style.css -!waybar/cider.sh +!waybar/scripts +waybar/scripts/* +!waybar/scripts/ssh-session.sh +!waybar/scripts/vpn-status.sh +!waybar/scripts/notification-status.sh +!waybar/scripts/mic-status.sh + +# eww +!eww +eww/* +!eww/eww.yuck +!eww/eww.scss +!eww/scripts +eww/scripts/* +!eww/scripts/popup.sh +!eww/scripts/system.sh +!eww/scripts/battery.sh +!eww/scripts/volume.sh +!eww/scripts/bluetooth.sh +!eww/scripts/network.sh +!eww/scripts/vpn.sh +!eww/scripts/keyboard.sh +!eww/scripts/media.sh + +# sway scripts +!sway/scripts +sway/scripts/* +!sway/scripts/power-menu.sh # bin !bin bin/* !bin/ssh-menu +# swaync +!swaync +swaync/* +!swaync/config.json +!swaync/style.css + +# fuzzel +!fuzzel +fuzzel/* +!fuzzel/fuzzel.ini + # flameshot !flameshot flameshot/* From adeb93bd248dc36f1a4958cbd742f4c5193b79dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Jane=C5=BEi=C4=8D?= Date: Thu, 12 Mar 2026 17:42:17 +0100 Subject: [PATCH 2/7] feat(fuzzel): add config --- fuzzel/fuzzel.ini | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 fuzzel/fuzzel.ini diff --git a/fuzzel/fuzzel.ini b/fuzzel/fuzzel.ini new file mode 100644 index 0000000..b22e827 --- /dev/null +++ b/fuzzel/fuzzel.ini @@ -0,0 +1,22 @@ +[main] +font=JetBrainsMono Nerd Font:size=14 +prompt=" " +icons=no +terminal=ghostty -e +width=40 +lines=12 +layer=overlay +exit-on-keyboard-focus-loss=yes + +[colors] +background=252423ff +text=d4be98ff +match=a9b665ff +selection=45403dff +selection-text=d4be98ff +selection-match=a9b665ff +border=45403dff + +[border] +width=2 +radius=8 From c6eaad94ef82af7cc99308e897c6d54a2037b6c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Jane=C5=BEi=C4=8D?= Date: Thu, 12 Mar 2026 17:42:32 +0100 Subject: [PATCH 3/7] feat(eww): add config --- eww/eww.scss | 239 ++++++++++++++++++++++ eww/eww.yuck | 421 +++++++++++++++++++++++++++++++++++++++ eww/scripts/battery.sh | 44 ++++ eww/scripts/bluetooth.sh | 43 ++++ eww/scripts/keyboard.sh | 35 ++++ eww/scripts/media.sh | 46 +++++ eww/scripts/network.sh | 80 ++++++++ eww/scripts/popup.sh | 29 +++ eww/scripts/system.sh | 40 ++++ eww/scripts/volume.sh | 62 ++++++ eww/scripts/vpn.sh | 89 +++++++++ 11 files changed, 1128 insertions(+) create mode 100644 eww/eww.scss create mode 100644 eww/eww.yuck create mode 100755 eww/scripts/battery.sh create mode 100755 eww/scripts/bluetooth.sh create mode 100755 eww/scripts/keyboard.sh create mode 100755 eww/scripts/media.sh create mode 100755 eww/scripts/network.sh create mode 100755 eww/scripts/popup.sh create mode 100755 eww/scripts/system.sh create mode 100755 eww/scripts/volume.sh create mode 100755 eww/scripts/vpn.sh diff --git a/eww/eww.scss b/eww/eww.scss new file mode 100644 index 0000000..6ec71b5 --- /dev/null +++ b/eww/eww.scss @@ -0,0 +1,239 @@ +// gruvbox-material-soft-dark palette +$bg: #32302f; +$bg_dim: #252423; +$bg_sel: #45403d; +$fg: #d4be98; +$red: #ea6962; +$green: #a9b665; +$yellow: #d8a657; +$blue: #7daea3; +$magenta: #d3869b; +$cyan: #89b482; +$gray: #928374; + +// base +* { + all: unset; + font-family: "JetBrainsMono Nerd Font"; + font-size: 13px; +} + +// transparent backdrop for click-away dismiss +.backdrop { + background-color: transparent; +} + +// popup container +.popup { + background-color: $bg_dim; + border: 1px solid $bg_sel; + border-radius: 12px; + padding: 16px; +} + +// section titles +.title { + color: $fg; + font-size: 13px; + font-weight: bold; + margin-bottom: 4px; +} + +.subtitle { + color: $gray; + font-size: 12px; + margin-bottom: 2px; +} + +// labels and values +.label { + color: $gray; +} + +.value { + color: $fg; +} + +.value.dim { + color: $gray; + font-size: 12px; + margin-left: 8px; +} + +// detail lines (smaller, dimmer) +.details { + margin-top: 4px; + padding-top: 8px; + border-top: 1px solid $bg_sel; +} + +.detail { + color: $gray; + font-size: 12px; +} + +// rows +.row { + padding: 2px 0; +} + +// progress bars +progressbar { + border-radius: 3px; + + trough { + min-height: 4px; + border-radius: 3px; + background-color: $bg_sel; + + progress { + min-height: 4px; + border-radius: 3px; + background-color: $green; + } + } +} + +progressbar.warning trough progress { + background-color: $yellow; +} + +progressbar.critical trough progress { + background-color: $red; +} + +// sliders +scale { + margin: 0 4px; + + trough { + min-height: 4px; + border-radius: 3px; + background-color: $bg_sel; + + highlight { + min-height: 4px; + border-radius: 3px; + background-color: $blue; + } + + slider { + min-width: 14px; + min-height: 14px; + border-radius: 50%; + background-color: $fg; + margin: -5px 0; + } + } +} + +// buttons +.toggle-btn { + color: $gray; + padding: 2px 8px; + border-radius: 6px; + background-color: $bg_sel; + min-width: 30px; + + &.active { + color: $bg_dim; + background-color: $green; + } + + &:hover { + background-color: lighten($bg_sel, 10%); + } +} + +// device list buttons +.device-btn { + color: $fg; + padding: 6px 10px; + border-radius: 6px; + background-color: transparent; + + &.active { + background-color: $bg_sel; + color: $fg; + + label { + font-weight: bold; + } + } + + &:hover { + background-color: $bg; + } +} + +// device rows (non-clickable list items) +.device-row { + padding: 4px 10px; +} + +// close/disconnect button +.close-btn { + color: $gray; + padding: 2px 6px; + border-radius: 4px; + + &:hover { + color: $red; + background-color: $bg; + } +} + +// profile selector buttons +.profile-btn { + color: $gray; + padding: 6px 12px; + border-radius: 6px; + background-color: $bg; + + &.active { + color: $bg_dim; + background-color: $cyan; + font-weight: bold; + } + + &:hover { + background-color: $bg_sel; + } +} + +// separator line +.separator { + min-height: 1px; + background-color: $bg_sel; + margin: 4px 0; +} + +// status indicators +.indicator { + min-width: 14px; + + &.online { + color: $green; + } + + &.offline { + color: $gray; + } +} + +// empty state +.empty { + color: $gray; + font-size: 12px; + padding: 8px 0; +} + +// active player label +.playing { + color: $green; +} + +// slider row layout +.slider-row { + padding: 4px 0; +} diff --git a/eww/eww.yuck b/eww/eww.yuck new file mode 100644 index 0000000..3002585 --- /dev/null +++ b/eww/eww.yuck @@ -0,0 +1,421 @@ +; sway shell popups + +; ============================================================================= +; variables +; ============================================================================= + +(defpoll sys :interval "5s" + :initial '{"cpu":0,"ram_percent":0,"ram_used":"0Gi","ram_total":"0Gi","temp":0,"disk_percent":0,"disk_used":"0","disk_total":"0","swap":"0/0","load":"0 0 0","uptime":""}' + "~/.config/eww/scripts/system.sh") + +(defpoll bat :interval "30s" + :initial '{"capacity":0,"status":"Unknown","power":"0","time":"","cycles":0,"profile":"unknown"}' + "~/.config/eww/scripts/battery.sh") + +(defpoll vol :interval "5s" + :initial '{"volume":0,"muted":false,"mic_volume":0,"mic_muted":false,"brightness":0,"sinks":[],"sources":[],"sink_count":0,"source_count":0}' + "~/.config/eww/scripts/volume.sh") + +(defpoll bt :interval "15s" + :initial '{"powered":false,"count":0,"devices":[]}' + "~/.config/eww/scripts/bluetooth.sh") + +(defpoll net :interval "10s" + :initial '{"type":"none","iface":"none","ip":"none","gateway":"none","ssid":"","signal":0,"conn_name":"","count":0,"networks":[],"unknown_count":0,"unknown":[]}' + "~/.config/eww/scripts/network.sh") + +(defpoll vpn_data :interval "15s" + :initial '{"tailscale":{"running":false,"ip":"","hostname":"","login":"","exit_nodes":[],"exit_count":0,"peers":[],"peer_count":0},"wireguard":{"active":false,"iface":""}}' + "~/.config/eww/scripts/vpn.sh") + +(defpoll kbd :interval "10s" + :initial '{"current":"unknown","layouts":[],"layout_count":0,"keyboards":[],"kb_count":0}' + "~/.config/eww/scripts/keyboard.sh") + +(defpoll media :interval "5s" + :initial '{"count":0,"players":[]}' + "~/.config/eww/scripts/media.sh") + +; ============================================================================= +; media popup +; ============================================================================= + +(defwidget media-widget [] + (box :class "popup" :orientation "v" :space-evenly false :spacing 8 + (label :class "title" :text "media" :halign "start") + (box :visible {media.count == 0} + (label :class "empty" :text "no active players")) + (box :orientation "v" :space-evenly false :spacing 4 + (for player in {media.players} + (box :class "device-row" :orientation "v" :space-evenly false :spacing 2 + (box :orientation "h" :space-evenly false :spacing 8 + (label :class "label ${player.status == 'Playing' ? 'playing' : ''}" + :text {player.display} :halign "start" :hexpand true) + (button :class "toggle-btn ${player.status == 'Playing' ? 'active' : ''}" + :onclick "~/.config/eww/scripts/media.sh play-pause '${player.name}'" + {player.status == "Playing" ? "pause" : "play"}) + (button :class "close-btn" + :onclick "~/.config/eww/scripts/media.sh prev '${player.name}'" + "prev") + (button :class "close-btn" + :onclick "~/.config/eww/scripts/media.sh next '${player.name}'" + "next")) + (label :class "value" :text "${player.artist}${player.artist != '' ? ' - ' : ''}${player.title}" + :halign "start" :limit-width 45)))))) + +; ============================================================================= +; system popup +; ============================================================================= + +(defwidget system-widget [] + (box :class "popup" :orientation "v" :space-evenly false :spacing 8 + (label :class "title" :text "system" :halign "start") + + ; cpu + (box :class "row" :orientation "v" :space-evenly false :spacing 2 + (box :orientation "h" + (label :class "label" :text "cpu" :halign "start" :hexpand true) + (label :class "value" :text "${sys.cpu}%")) + (progress :class "bar" :value {sys.cpu})) + + ; ram + (box :class "row" :orientation "v" :space-evenly false :spacing 2 + (box :orientation "h" + (label :class "label" :text "ram" :halign "start" :hexpand true) + (label :class "value" :text "${sys.ram_used}/${sys.ram_total}")) + (progress :class "bar" :value {sys.ram_percent})) + + ; temp + (box :class "row" :orientation "h" + (label :class "label" :text "temp" :halign "start" :hexpand true) + (label :class "value" :text "${sys.temp}°")) + + ; disk + (box :class "row" :orientation "v" :space-evenly false :spacing 2 + (box :orientation "h" + (label :class "label" :text "disk" :halign "start" :hexpand true) + (label :class "value" :text "${sys.disk_used}/${sys.disk_total}")) + (progress :class "bar" :value {sys.disk_percent})) + + ; details + (box :class "details" :orientation "v" :space-evenly false :spacing 2 + (label :class "detail" :text "swap ${sys.swap}" :halign "start") + (label :class "detail" :text "load ${sys.load}" :halign "start") + (label :class "detail" :text "up ${sys.uptime}" :halign "start")))) + +; ============================================================================= +; battery popup +; ============================================================================= + +(defwidget battery-widget [] + (box :class "popup" :orientation "v" :space-evenly false :spacing 8 + (label :class "title" :text "battery" :halign "start") + + ; capacity bar + (box :class "row" :orientation "v" :space-evenly false :spacing 2 + (box :orientation "h" + (label :class "label" :text {bat.status} :halign "start" :hexpand true) + (label :class "value" :text "${bat.capacity}%")) + (progress :class "bar ${bat.capacity < 15 ? 'critical' : bat.capacity < 30 ? 'warning' : ''}" + :value {bat.capacity})) + + ; power + time + (box :class "row" :orientation "h" + (label :class "label" :text "power" :halign "start" :hexpand true) + (label :class "value" :text "${bat.power}W")) + (box :class "row" :orientation "h" :visible {bat.time != ""} + (label :class "label" :text "remaining" :halign "start" :hexpand true) + (label :class "value" :text {bat.time})) + (box :class "row" :orientation "h" + (label :class "label" :text "cycles" :halign "start" :hexpand true) + (label :class "value" :text {bat.cycles})) + + ; power profile selector + (box :class "separator") + (label :class "title" :text "profile" :halign "start") + (box :class "profiles" :orientation "h" :space-evenly true :spacing 4 + (button :class "profile-btn ${bat.profile == 'power-saver' ? 'active' : ''}" + :onclick "~/.config/eww/scripts/battery.sh set-profile power-saver" + "saver") + (button :class "profile-btn ${bat.profile == 'balanced' ? 'active' : ''}" + :onclick "~/.config/eww/scripts/battery.sh set-profile balanced" + "balanced") + (button :class "profile-btn ${bat.profile == 'performance' ? 'active' : ''}" + :onclick "~/.config/eww/scripts/battery.sh set-profile performance" + "perform")))) + +; ============================================================================= +; volume popup +; ============================================================================= + +(defwidget volume-widget [] + (box :class "popup" :orientation "v" :space-evenly false :spacing 8 + ; output section + (box :orientation "v" :space-evenly false :spacing 4 + (label :class "title" :text "output" :halign "start") + (box :class "row slider-row" :orientation "h" :space-evenly false :spacing 8 + (button :class "toggle-btn ${vol.muted ? 'active' : ''}" + :onclick "~/.config/eww/scripts/volume.sh toggle-mute" + {vol.muted ? "muted" : "vol"}) + (scale :class "slider" :min 0 :max 100 :value {vol.volume} + :onchange "pamixer --set-volume {}" :hexpand true) + (label :class "value" :text "${vol.volume}%")) + (for sink in {vol.sinks} + (button :class "device-btn ${sink.active ? 'active' : ''}" + :onclick "~/.config/eww/scripts/volume.sh set-sink '${sink.sink_name}'" + (label :text {sink.name} :halign "start")))) + + ; input section + (box :class "separator") + (box :orientation "v" :space-evenly false :spacing 4 + (label :class "title" :text "input" :halign "start") + (box :class "row slider-row" :orientation "h" :space-evenly false :spacing 8 + (button :class "toggle-btn ${vol.mic_muted ? 'active' : ''}" + :onclick "~/.config/eww/scripts/volume.sh toggle-mic" + {vol.mic_muted ? "muted" : "mic"}) + (scale :class "slider" :min 0 :max 100 :value {vol.mic_volume} + :onchange "pamixer --default-source --set-volume {}" :hexpand true) + (label :class "value" :text "${vol.mic_volume}%")) + (for source in {vol.sources} + (button :class "device-btn ${source.active ? 'active' : ''}" + :onclick "~/.config/eww/scripts/volume.sh set-source '${source.source_name}'" + (label :text {source.name} :halign "start")))) + + ; brightness section + (box :class "separator") + (label :class "title" :text "brightness" :halign "start") + (box :class "row slider-row" :orientation "h" :space-evenly false :spacing 8 + (label :class "label" :text "bl") + (scale :class "slider" :min 0 :max 100 :value {vol.brightness} + :onchange "brightnessctl set {}%" :hexpand true) + (label :class "value" :text "${vol.brightness}%")))) + +; ============================================================================= +; bluetooth popup +; ============================================================================= + +(defwidget bluetooth-widget [] + (box :class "popup" :orientation "v" :space-evenly false :spacing 8 + (box :class "row" :orientation "h" + (label :class "title" :text "bluetooth" :halign "start" :hexpand true) + (button :class "toggle-btn ${bt.powered ? 'active' : ''}" + :onclick "~/.config/eww/scripts/bluetooth.sh toggle-power" + {bt.powered ? "on" : "off"})) + + (box :visible {bt.powered} :orientation "v" :space-evenly false :spacing 4 + (for device in {bt.devices} + (box :class "device-row" :orientation "h" :space-evenly false :spacing 8 + (label :class "label" :text {device.name} :halign "start" :hexpand true) + (label :class "value dim" :visible {device.battery >= 0} + :text "${device.battery}%") + (button :class "close-btn" + :onclick "~/.config/eww/scripts/bluetooth.sh disconnect '${device.mac}'" + "x"))) + (label :visible {bt.count == 0} :class "empty" :text "no devices connected")))) + +; ============================================================================= +; network popup +; ============================================================================= + +(defwidget network-widget [] + (box :class "popup" :orientation "v" :space-evenly false :spacing 8 + (label :class "title" :text "network" :halign "start") + + ; connection info + (box :class "row" :orientation "h" + (label :class "label" :text "interface" :halign "start" :hexpand true) + (label :class "value" :text {net.iface})) + (box :class "row" :orientation "h" + (label :class "label" :text "ip" :halign "start" :hexpand true) + (label :class "value" :text {net.ip})) + (box :class "row" :orientation "h" + (label :class "label" :text "gateway" :halign "start" :hexpand true) + (label :class "value" :text {net.gateway})) + (box :class "row" :orientation "h" :visible {net.type == "wifi"} + (label :class "label" :text "ssid" :halign "start" :hexpand true) + (label :class "value" :text "${net.ssid} ${net.signal}%")) + + ; saved wifi networks nearby + (box :visible {net.count > 0} :orientation "v" :space-evenly false :spacing 4 + (box :class "separator") + (label :class "title" :text "saved" :halign "start") + (box :orientation "v" :space-evenly false :spacing 0 + (for network in {net.networks} + (button :class "device-btn ${network.active ? 'active' : ''}" + :onclick {network.active ? "~/.config/eww/scripts/network.sh disconnect '${network.ssid}'" : "~/.config/eww/scripts/network.sh connect '${network.ssid}'"} + (box :orientation "h" :space-evenly false + (label :class "label" :text {network.ssid} :halign "start" :hexpand true) + (label :class "value dim" :text "${network.signal}%")))))) + + ; unknown wifi networks nearby + (box :visible {net.unknown_count > 0} :orientation "v" :space-evenly false :spacing 4 + (box :class "separator") + (label :class "title" :text "nearby" :halign "start") + (box :orientation "v" :space-evenly false :spacing 0 + (for network in {net.unknown} + (button :class "device-btn" + :onclick "~/.config/eww/scripts/network.sh connect-new '${network.ssid}'" + (box :orientation "h" :space-evenly false + (label :class "label" :text {network.ssid} :halign "start" :hexpand true) + (label :class "value dim" :text "${network.signal}%")))))))) + +; ============================================================================= +; vpn popup +; ============================================================================= + +(defwidget vpn-widget [] + (box :class "popup" :orientation "v" :space-evenly false :spacing 8 + ; tailscale + (box :class "row" :orientation "h" + (label :class "title" :text "tailscale" :halign "start" :hexpand true) + (button :class "toggle-btn ${vpn_data.tailscale.running ? 'active' : ''}" + :onclick "~/.config/eww/scripts/vpn.sh ${vpn_data.tailscale.running ? 'ts-down' : 'ts-up'}" + {vpn_data.tailscale.running ? "on" : "off"})) + + (box :visible {vpn_data.tailscale.running} :orientation "v" :space-evenly false :spacing 4 + (box :class "row" :orientation "h" + (label :class "label" :text "ip" :halign "start" :hexpand true) + (label :class "value" :text {vpn_data.tailscale.ip})) + (box :class "row" :orientation "h" + (label :class "label" :text "host" :halign "start" :hexpand true) + (label :class "value" :text {vpn_data.tailscale.hostname})) + (box :class "row" :orientation "h" + (label :class "label" :text "network" :halign "start" :hexpand true) + (label :class "value" :text {vpn_data.tailscale.login})) + + ; exit nodes + (box :visible {vpn_data.tailscale.exit_count > 0} :orientation "v" :space-evenly false :spacing 4 + (box :class "separator") + (label :class "subtitle" :text "exit node" :halign "start") + (box :orientation "v" :space-evenly false :spacing 0 + (for node in {vpn_data.tailscale.exit_nodes} + (button :class "device-btn ${node.active ? 'active' : ''}" + :onclick "~/.config/eww/scripts/vpn.sh ts-exit '${node.active ? '' : node.ip}'" + (label :text {node.name} :halign "start"))))) + + ; peers + (box :orientation "v" :space-evenly false :spacing 4 + (box :class "separator") + (label :class "subtitle" :text "devices" :halign "start") + (for peer in {vpn_data.tailscale.peers} + (box :class "device-row" :orientation "h" :space-evenly false :spacing 8 + (label :class "indicator ${peer.online ? 'online' : 'offline'}" + :text {peer.online ? "●" : "○"}) + (label :class "label" :text {peer.name} :halign "start" :hexpand true) + (label :class "value dim" :text {peer.ip}))))) + + ; wireguard + (box :class "separator") + (box :class "row" :orientation "h" + (label :class "title" :text "wireguard" :halign "start" :hexpand true) + (button :class "toggle-btn ${vpn_data.wireguard.active ? 'active' : ''}" + :onclick "~/.config/eww/scripts/vpn.sh ${vpn_data.wireguard.active ? 'wg-down' : 'wg-up'}" + {vpn_data.wireguard.active ? "on" : "off"})))) + +; ============================================================================= +; keyboard popup +; ============================================================================= + +(defwidget keyboard-widget [] + (box :class "popup" :orientation "v" :space-evenly false :spacing 8 + (label :class "title" :text "keyboard" :halign "start") + + ; layout selector + (label :class "subtitle" :text "layout" :halign "start") + (for layout in {kbd.layouts} + (button :class "device-btn ${layout == kbd.current ? 'active' : ''}" + :onclick "~/.config/eww/scripts/keyboard.sh switch" + (label :text {layout} :halign "start"))) + + ; keyboard devices + (box :visible {kbd.kb_count > 1} :orientation "v" :space-evenly false :spacing 4 + (box :class "separator") + (label :class "subtitle" :text "devices" :halign "start") + (for kb in {kbd.keyboards} + (box :class "device-row" :orientation "h" :space-evenly false :spacing 8 + (label :class "label" :text {kb.name} :halign "start" :hexpand true) + (label :class "value dim" :text {kb.layout})))))) + +; ============================================================================= +; backdrop (click-away to close) +; ============================================================================= + +(defwidget backdrop-widget [] + (eventbox :onclick "~/.config/eww/scripts/popup.sh close-all" + (box :class "backdrop" :hexpand true :vexpand true))) + +(defwindow backdrop + :monitor 0 + :geometry (geometry :x "0px" :y "0px" :width "100%" :height "100%" :anchor "top left") + :stacking "overlay" + :exclusive false + :focusable true + (backdrop-widget)) + +; ============================================================================= +; windows +; ============================================================================= + +(defwindow system-popup + :monitor 0 + :geometry (geometry :x "380px" :y "3px" :width "300px" :anchor "top right") + :stacking "overlay" + :exclusive false + :focusable false + (system-widget)) + +(defwindow battery-popup + :monitor 0 + :geometry (geometry :x "260px" :y "3px" :width "300px" :anchor "top right") + :stacking "overlay" + :exclusive false + :focusable false + (battery-widget)) + +(defwindow volume-popup + :monitor 0 + :geometry (geometry :x "140px" :y "3px" :width "320px" :anchor "top right") + :stacking "overlay" + :exclusive false + :focusable false + (volume-widget)) + +(defwindow bluetooth-popup + :monitor 0 + :geometry (geometry :x "500px" :y "3px" :width "280px" :anchor "top right") + :stacking "overlay" + :exclusive false + :focusable false + (bluetooth-widget)) + +(defwindow network-popup + :monitor 0 + :geometry (geometry :x "560px" :y "3px" :width "320px" :anchor "top right") + :stacking "overlay" + :exclusive false + :focusable false + (network-widget)) + +(defwindow vpn-popup + :monitor 0 + :geometry (geometry :x "480px" :y "3px" :width "320px" :anchor "top right") + :stacking "overlay" + :exclusive false + :focusable false + (vpn-widget)) + +(defwindow keyboard-popup + :monitor 0 + :geometry (geometry :x "50px" :y "3px" :width "280px" :anchor "top right") + :stacking "overlay" + :exclusive false + :focusable false + (keyboard-widget)) + +(defwindow media-popup + :monitor 0 + :geometry (geometry :x "700px" :y "3px" :width "350px" :anchor "top right") + :stacking "overlay" + :exclusive false + :focusable false + (media-widget)) diff --git a/eww/scripts/battery.sh b/eww/scripts/battery.sh new file mode 100755 index 0000000..e7889c3 --- /dev/null +++ b/eww/scripts/battery.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash + +# battery info as JSON for eww battery-popup + +action="${1:-status}" + +if [[ "$action" == "set-profile" ]]; then + powerprofilesctl set "$2" + ( data=$(~/.config/eww/scripts/battery.sh); eww update bat="$data" ) & + exit 0 +fi + +bat_path=$(echo /sys/class/power_supply/BAT* 2>/dev/null | awk '{print $1}') + +if [[ ! -d "$bat_path" ]]; then + jq -nc '{capacity:0,status:"No battery",power:"0",time:"",cycles:0,profile:"unknown"}' + exit 0 +fi + +capacity=$(cat "$bat_path/capacity" 2>/dev/null || echo 0) +status=$(cat "$bat_path/status" 2>/dev/null || echo "Unknown") + +power_uw=$(cat "$bat_path/power_now" 2>/dev/null || echo 0) +power=$(awk -v p="$power_uw" 'BEGIN{printf "%.1f", p/1000000}') + +cycles=$(cat "$bat_path/cycle_count" 2>/dev/null || echo 0) +[[ "$cycles" =~ ^[0-9]+$ ]] || cycles=0 + +bat_upower=$(upower -e 2>/dev/null | grep BAT | head -1) +time_str="" +if [[ -n "$bat_upower" ]]; then + time_str=$(upower -i "$bat_upower" 2>/dev/null | awk '/time to/{print $4, $5}') +fi + +profile=$(powerprofilesctl get 2>/dev/null || echo "unknown") + +jq -nc \ + --argjson capacity "$capacity" \ + --arg status "$status" \ + --arg power "$power" \ + --arg time "$time_str" \ + --argjson cycles "${cycles:-0}" \ + --arg profile "$profile" \ + '{$capacity,$status,$power,$time,$cycles,$profile}' diff --git a/eww/scripts/bluetooth.sh b/eww/scripts/bluetooth.sh new file mode 100755 index 0000000..4b5c7bf --- /dev/null +++ b/eww/scripts/bluetooth.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash + +# bluetooth device info as JSON for eww bluetooth-popup + +action="${1:-status}" + +case "$action" in + status) + powered=$(bluetoothctl show 2>/dev/null | grep -q "Powered: yes" && echo true || echo false) + + devices="[]" + count=0 + if [[ "$powered" == "true" ]]; then + # get connected devices in one pass + devices=$(bluetoothctl devices Connected 2>/dev/null | while read -r _ mac name; do + info=$(bluetoothctl info "$mac" 2>/dev/null) + battery=$(awk '/Battery Percentage:/{gsub(/[()]/,""); print $4}' <<< "$info") + jq -nc --arg name "$name" --arg mac "$mac" --argjson battery "${battery:--1}" \ + '{$name,$mac,$battery}' + done | jq -sc '.') + [[ -z "$devices" || "$devices" == "null" ]] && devices="[]" + count=$(jq 'length' <<< "$devices" 2>/dev/null || echo 0) + fi + + jq -nc \ + --argjson powered "$powered" \ + --argjson count "$count" \ + --argjson devices "$devices" \ + '{$powered,$count,$devices}' + ;; + toggle-power) + if bluetoothctl show 2>/dev/null | grep -q "Powered: yes"; then + bluetoothctl power off + else + bluetoothctl power on + fi + ( sleep 0.5; data=$(~/.config/eww/scripts/bluetooth.sh); eww update bt="$data" ) & + ;; + disconnect) + bluetoothctl disconnect "$2" + ( sleep 0.5; data=$(~/.config/eww/scripts/bluetooth.sh); eww update bt="$data" ) & + ;; +esac diff --git a/eww/scripts/keyboard.sh b/eww/scripts/keyboard.sh new file mode 100755 index 0000000..9fd75de --- /dev/null +++ b/eww/scripts/keyboard.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash + +# keyboard layout info as JSON for eww keyboard-popup + +action="${1:-status}" + +case "$action" in + status) + inputs=$(swaymsg -t get_inputs 2>/dev/null) + + current=$(jq -r '[.[] | select(.type == "keyboard")] | .[0].xkb_active_layout_name // "unknown"' <<< "$inputs") + layouts=$(jq -c '[.[] | select(.type == "keyboard")] | .[0].xkb_layout_names // []' <<< "$inputs") + layout_count=$(jq 'length' <<< "$layouts" 2>/dev/null || echo 0) + + keyboards=$(jq -c '[.[] | select(.type == "keyboard") | + {id: .identifier, name: .name, layout: .xkb_active_layout_name}] | unique_by(.name)' <<< "$inputs") + kb_count=$(jq 'length' <<< "$keyboards" 2>/dev/null || echo 0) + + jq -nc \ + --arg current "${current:-unknown}" \ + --argjson layouts "${layouts:-[]}" \ + --argjson layout_count "${layout_count:-0}" \ + --argjson keyboards "${keyboards:-[]}" \ + --argjson kb_count "${kb_count:-0}" \ + '{$current,$layouts,$layout_count,$keyboards,$kb_count}' + ;; + switch) + swaymsg input type:keyboard xkb_switch_layout next 2>/dev/null + ( sleep 0.3; data=$(~/.config/eww/scripts/keyboard.sh); eww update kbd="$data" ) & + ;; + set-layout) + swaymsg input type:keyboard xkb_switch_layout "$2" 2>/dev/null + ( sleep 0.3; data=$(~/.config/eww/scripts/keyboard.sh); eww update kbd="$data" ) & + ;; +esac diff --git a/eww/scripts/media.sh b/eww/scripts/media.sh new file mode 100755 index 0000000..39c178a --- /dev/null +++ b/eww/scripts/media.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash + +# media player info as JSON for eww media-popup + +action="${1:-status}" + +case "$action" in + status) + players=$(playerctl -l 2>/dev/null | head -10) + if [[ -z "$players" ]]; then + jq -nc '{count:0,players:[]}' + exit 0 + fi + + result="[]" + while IFS= read -r name; do + status=$(playerctl -p "$name" status 2>/dev/null || echo "Stopped") + artist=$(playerctl -p "$name" metadata artist 2>/dev/null || echo "") + title=$(playerctl -p "$name" metadata title 2>/dev/null || echo "") + album=$(playerctl -p "$name" metadata album 2>/dev/null || echo "") + + # clean up player name for display + display=${name%%.*} + + result=$(jq -c --arg name "$name" --arg display "$display" \ + --arg status "$status" --arg artist "$artist" \ + --arg title "$title" --arg album "$album" \ + '. + [{name:$name, display:$display, status:$status, artist:$artist, title:$title, album:$album}]' <<< "$result") + done <<< "$players" + + count=$(jq 'length' <<< "$result") + jq -nc --argjson count "$count" --argjson players "$result" '{$count,$players}' + ;; + play-pause) + playerctl -p "$2" play-pause 2>/dev/null + ( sleep 0.3; data=$(~/.config/eww/scripts/media.sh); eww update media="$data" ) & + ;; + next) + playerctl -p "$2" next 2>/dev/null + ( sleep 0.5; data=$(~/.config/eww/scripts/media.sh); eww update media="$data" ) & + ;; + prev) + playerctl -p "$2" previous 2>/dev/null + ( sleep 0.5; data=$(~/.config/eww/scripts/media.sh); eww update media="$data" ) & + ;; +esac diff --git a/eww/scripts/network.sh b/eww/scripts/network.sh new file mode 100755 index 0000000..c8aa3dd --- /dev/null +++ b/eww/scripts/network.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash + +# network info as JSON for eww network-popup + +action="${1:-status}" + +case "$action" in + status) + active=$(nmcli -t -f DEVICE,TYPE,STATE,CONNECTION device status 2>/dev/null \ + | grep ':connected:' | head -1) + iface=$(echo "$active" | cut -d: -f1) + conn_type=$(echo "$active" | cut -d: -f2) + conn_name=$(echo "$active" | cut -d: -f4-) + + ip=$(ip -4 -o addr show "$iface" 2>/dev/null | awk '{print $4}' | cut -d/ -f1) + gateway=$(ip route show default dev "$iface" 2>/dev/null | awk '{print $3}') + + networks="[]" + net_count=0 + + # always check active wifi regardless of primary connection type + ssid=$(nmcli -t -f active,ssid dev wifi 2>/dev/null | grep '^yes' | cut -d: -f2-) + signal=$(nmcli -t -f active,signal dev wifi 2>/dev/null | grep '^yes' | cut -d: -f2-) + signal=${signal:-0} + + # scan nearby wifi + saved=$(nmcli -t -f NAME connection show 2>/dev/null | sort -u) + # replace last 3 colons with tabs to handle SSIDs containing colons + all_wifi=$(nmcli -t -f SSID,SIGNAL,SECURITY,IN-USE dev wifi list --rescan no 2>/dev/null \ + | sed 's/:\([^:]*\):\([^:]*\):\([^:]*\)$/\t\1\t\2\t\3/' \ + | awk -F'\t' 'NF>=3 && $1!=""' \ + | sort -t$'\t' -k4,4r -k2,2rn \ + | awk -F'\t' '!seen[$1]++') + + # known networks nearby + networks=$(echo "$all_wifi" \ + | while IFS=$'\t' read -r s sig sec use; do + echo "$saved" | grep -qxF "$s" && printf '%s\t%s\n' "$s" "$sig" + done \ + | head -10 \ + | jq -Rnc --arg active "$ssid" '[inputs | split("\t") | + {ssid:.[0], signal:(.[1]|tonumber), active:(.[0] == $active)}]') + net_count=$(jq 'length' <<< "$networks" 2>/dev/null || echo 0) + + # unknown networks nearby + unknown=$(echo "$all_wifi" \ + | while IFS=$'\t' read -r s sig sec use; do + echo "$saved" | grep -qxF "$s" || printf '%s\t%s\t%s\n' "$s" "$sig" "$sec" + done \ + | head -5 \ + | jq -Rnc '[inputs | split("\t") | + {ssid:.[0], signal:(.[1]|tonumber), security:.[2]}]') + unknown_count=$(jq 'length' <<< "$unknown" 2>/dev/null || echo 0) + + jq -nc \ + --arg type "${conn_type:-none}" \ + --arg iface "${iface:-none}" \ + --arg ip "${ip:-none}" \ + --arg gateway "${gateway:-none}" \ + --arg ssid "$ssid" \ + --argjson signal "${signal:-0}" \ + --arg conn_name "$conn_name" \ + --argjson count "${net_count:-0}" \ + --argjson networks "${networks:-[]}" \ + --argjson unknown_count "${unknown_count:-0}" \ + --argjson unknown "${unknown:-[]}" \ + '{$type,$iface,$ip,$gateway,$ssid,$signal,$conn_name,$count,$networks,$unknown_count,$unknown}' + ;; + connect) + ( nmcli dev wifi connect "$2" 2>/dev/null; sleep 1; data=$(~/.config/eww/scripts/network.sh); eww update net="$data" ) & + ;; + disconnect) + ( nmcli connection down "$2" 2>/dev/null; sleep 1; data=$(~/.config/eww/scripts/network.sh); eww update net="$data" ) & + ;; + connect-new) + ssid="$2" + pass=$(zenity --entry --hide-text --title="WiFi" --text="Password for $ssid" 2>/dev/null) + ( [[ -n "$pass" ]] && nmcli dev wifi connect "$ssid" password "$pass" 2>/dev/null; sleep 1; data=$(~/.config/eww/scripts/network.sh); eww update net="$data" ) & + ;; +esac diff --git a/eww/scripts/popup.sh b/eww/scripts/popup.sh new file mode 100755 index 0000000..3f2271e --- /dev/null +++ b/eww/scripts/popup.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash + +# toggle an eww popup +# opens on the currently focused monitor + +POPUPS=(system-popup battery-popup network-popup vpn-popup volume-popup bluetooth-popup keyboard-popup media-popup) + +target="$1" + +if [[ -z "$target" ]]; then + echo "usage: popup.sh " >&2 + exit 1 +fi + +if [[ "$target" == "close-all" ]]; then + eww close "${POPUPS[@]}" 2>/dev/null + exit 0 +fi + +# check if target is already open +if eww active-windows 2>/dev/null | grep -q "$target"; then + eww close "$target" 2>/dev/null +else + # close others, open popup + screen=$(swaymsg -t get_outputs 2>/dev/null \ + | jq '[.[] | .focused] | index(true) // 0') + eww close "${POPUPS[@]}" 2>/dev/null + eww open --screen "${screen:-0}" "$target" +fi diff --git a/eww/scripts/system.sh b/eww/scripts/system.sh new file mode 100755 index 0000000..8fcd009 --- /dev/null +++ b/eww/scripts/system.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash + +# system metrics as JSON for eww system-popup + +# delta-based cpu: sample /proc/stat twice 0.5s apart +read -r _ u1 n1 s1 i1 _ < /proc/stat +sleep 0.5 +read -r _ u2 n2 s2 i2 _ < /proc/stat +cpu=$(( (u2+n2+s2 - u1-n1-s1) * 100 / (u2+n2+s2+i2 - u1-n1-s1-i1) )) + +ram_info=$(free -b | awk '/^Mem:/{printf "%.0f %.1f %.1f", $3/$2*100, $3/1073741824, $2/1073741824}') +ram_percent=$(awk '{print $1}' <<< "$ram_info") +ram_used=$(awk '{printf "%.1f", $2}' <<< "$ram_info") +ram_total=$(awk '{printf "%.1f", $3}' <<< "$ram_info") + +temp=$(cat /sys/class/thermal/thermal_zone*/temp 2>/dev/null | sort -rn | head -1) +temp=$(( ${temp:-0} / 1000 )) + +disk_info=$(df -h / | awk 'NR==2{gsub(/%/,""); printf "%s %s %s", $5, $3, $2}') +disk_percent=$(awk '{print $1}' <<< "$disk_info") +disk_used=$(awk '{print $2}' <<< "$disk_info") +disk_total=$(awk '{print $3}' <<< "$disk_info") + +swap=$(free -h | awk '/^Swap:/{printf "%s/%s", $3, $2}') +load=$(awk '{print $1, $2, $3}' /proc/loadavg) +uptime_str=$(uptime -p 2>/dev/null | sed 's/up //' || echo "n/a") + +jq -nc \ + --argjson cpu "${cpu:-0}" \ + --argjson ram_percent "${ram_percent:-0}" \ + --arg ram_used "${ram_used:-0}Gi" \ + --arg ram_total "${ram_total:-0}Gi" \ + --argjson temp "${temp:-0}" \ + --argjson disk_percent "${disk_percent:-0}" \ + --arg disk_used "$disk_used" \ + --arg disk_total "$disk_total" \ + --arg swap "$swap" \ + --arg load "$load" \ + --arg uptime "$uptime_str" \ + '{$cpu,$ram_percent,$ram_used,$ram_total,$temp,$disk_percent,$disk_used,$disk_total,$swap,$load,$uptime}' diff --git a/eww/scripts/volume.sh b/eww/scripts/volume.sh new file mode 100755 index 0000000..d966962 --- /dev/null +++ b/eww/scripts/volume.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash + +# audio + brightness info for eww volume-popup + +action="${1:-status}" + +case "$action" in + status) + default_sink=$(pactl get-default-sink 2>/dev/null) + default_source=$(pactl get-default-source 2>/dev/null) + + # get all sink+source info in one pactl call, extract volume/mute + device lists + all_data=$(pactl --format=json list sinks 2>/dev/null) + sinks=$(jq -c --arg d "$default_sink" \ + '[.[] | {name: .description, sink_name: .name, active: (.name == $d)}]' <<< "$all_data" 2>/dev/null || echo '[]') + # extract default sink volume+mute + volume=$(jq --arg d "$default_sink" \ + '[.[] | select(.name == $d)][0] | .volume | to_entries[0].value.value_percent | rtrimstr("%") | tonumber' <<< "$all_data" 2>/dev/null || echo 0) + muted=$(jq --arg d "$default_sink" \ + '[.[] | select(.name == $d)][0].mute' <<< "$all_data" 2>/dev/null || echo false) + + all_sources=$(pactl --format=json list sources 2>/dev/null) + sources=$(jq -c --arg d "$default_source" \ + '[.[] | select(.name | test("monitor$") | not) | {name: .description, source_name: .name, active: (.name == $d)}]' <<< "$all_sources" 2>/dev/null || echo '[]') + mic_volume=$(jq --arg d "$default_source" \ + '[.[] | select(.name == $d)][0] | .volume | to_entries[0].value.value_percent | rtrimstr("%") | tonumber' <<< "$all_sources" 2>/dev/null || echo 0) + mic_muted=$(jq --arg d "$default_source" \ + '[.[] | select(.name == $d)][0].mute' <<< "$all_sources" 2>/dev/null || echo false) + + brightness=$(brightnessctl -m 2>/dev/null | cut -d, -f5 | tr -d '%') + sink_count=$(jq 'length' <<< "$sinks" 2>/dev/null || echo 0) + source_count=$(jq 'length' <<< "$sources" 2>/dev/null || echo 0) + + jq -nc \ + --argjson volume "${volume:-0}" \ + --argjson muted "${muted:-false}" \ + --argjson mic_volume "${mic_volume:-0}" \ + --argjson mic_muted "${mic_muted:-false}" \ + --argjson brightness "${brightness:-0}" \ + --argjson sinks "${sinks:-[]}" \ + --argjson sources "${sources:-[]}" \ + --argjson sink_count "${sink_count:-0}" \ + --argjson source_count "${source_count:-0}" \ + '{$volume,$muted,$mic_volume,$mic_muted,$brightness,$sinks,$sources,$sink_count,$source_count}' + ;; + set-sink) + pactl set-default-sink "$2" + ( data=$(~/.config/eww/scripts/volume.sh); eww update vol="$data" ) & + ;; + set-source) + pactl set-default-source "$2" + ( data=$(~/.config/eww/scripts/volume.sh); eww update vol="$data" ) & + ;; + toggle-mute) + pamixer -t + ( data=$(~/.config/eww/scripts/volume.sh); eww update vol="$data" ) & + ;; + toggle-mic) + pamixer --default-source -t + ( data=$(~/.config/eww/scripts/volume.sh); eww update vol="$data" ) & + ;; +esac diff --git a/eww/scripts/vpn.sh b/eww/scripts/vpn.sh new file mode 100755 index 0000000..4e8ed2d --- /dev/null +++ b/eww/scripts/vpn.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env bash + +# vpn status as JSON for eww vpn-popup + +action="${1:-status}" + +case "$action" in + status) + ts_running=false + ts_ip="" + ts_hostname="" + ts_login="" + ts_exit_nodes="[]" + ts_exit_count=0 + ts_peers="[]" + ts_peer_count=0 + + if command -v tailscale &>/dev/null; then + ts_json=$(tailscale status --json 2>/dev/null) + state=$(jq -r '.BackendState // empty' <<< "$ts_json") + + if [[ "$state" == "Running" ]]; then + ts_running=true + ts_ip=$(jq -r '.TailscaleIPs[0] // empty' <<< "$ts_json") + ts_hostname=$(jq -r '.Self.HostName // empty' <<< "$ts_json") + ts_login=$(jq -r '.CurrentTailnet.Name // empty' <<< "$ts_json") + + ts_exit_nodes=$(jq -c '[.Peer | to_entries[]? | + select(.value.ExitNodeOption) | + {id: .key, name: .value.HostName, ip: (.value.TailscaleIPs[0] // ""), active: (.value.ExitNode // false)} + ] // []' <<< "$ts_json" 2>/dev/null || echo '[]') + ts_exit_count=$(jq 'length' <<< "$ts_exit_nodes" 2>/dev/null || echo 0) + + ts_peers=$(jq -c '[.Peer | to_entries[]? | + {name: .value.HostName, ip: (.value.TailscaleIPs[0] // ""), online: .value.Online} + ] // []' <<< "$ts_json" 2>/dev/null || echo '[]') + ts_peer_count=$(jq 'length' <<< "$ts_peers" 2>/dev/null || echo 0) + fi + fi + + wg_active=false + wg_iface="" + if command -v wg &>/dev/null; then + wg_iface=$(wg show interfaces 2>/dev/null | head -1) + [[ -n "$wg_iface" ]] && wg_active=true + fi + + jq -nc \ + --argjson ts_running "$ts_running" \ + --arg ts_ip "$ts_ip" \ + --arg ts_hostname "$ts_hostname" \ + --arg ts_login "$ts_login" \ + --argjson ts_exit_nodes "$ts_exit_nodes" \ + --argjson ts_exit_count "$ts_exit_count" \ + --argjson ts_peers "$ts_peers" \ + --argjson ts_peer_count "$ts_peer_count" \ + --argjson wg_active "$wg_active" \ + --arg wg_iface "$wg_iface" \ + '{tailscale:{running:$ts_running,ip:$ts_ip,hostname:$ts_hostname,login:$ts_login, + exit_nodes:$ts_exit_nodes,exit_count:$ts_exit_count, + peers:$ts_peers,peer_count:$ts_peer_count}, + wireguard:{active:$wg_active,iface:$wg_iface}}' + ;; + ts-up) + tailscale up 2>/dev/null + ( sleep 1; data=$(~/.config/eww/scripts/vpn.sh); eww update vpn_data="$data" ) & + ;; + ts-down) + tailscale down 2>/dev/null + ( sleep 1; data=$(~/.config/eww/scripts/vpn.sh); eww update vpn_data="$data" ) & + ;; + ts-exit) + if [[ -n "$2" ]]; then + tailscale set --exit-node="$2" 2>/dev/null + else + tailscale set --exit-node="" 2>/dev/null + fi + ( sleep 0.5; data=$(~/.config/eww/scripts/vpn.sh); eww update vpn_data="$data" ) & + ;; + wg-up) + sudo wg-quick up "${2:-wg0}" 2>/dev/null + ( sleep 1; data=$(~/.config/eww/scripts/vpn.sh); eww update vpn_data="$data" ) & + ;; + wg-down) + iface="${2:-$(wg show interfaces 2>/dev/null | head -1)}" + sudo wg-quick down "${iface:-wg0}" 2>/dev/null + ( sleep 1; data=$(~/.config/eww/scripts/vpn.sh); eww update vpn_data="$data" ) & + ;; +esac From f82afb461171157ec55a8e49e5da663a36ff0e21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Jane=C5=BEi=C4=8D?= Date: Thu, 12 Mar 2026 17:42:47 +0100 Subject: [PATCH 4/7] feat(swaync): add config --- swaync/config.json | 50 ++++++++++++++ swaync/style.css | 163 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 213 insertions(+) create mode 100644 swaync/config.json create mode 100644 swaync/style.css diff --git a/swaync/config.json b/swaync/config.json new file mode 100644 index 0000000..b7c80c4 --- /dev/null +++ b/swaync/config.json @@ -0,0 +1,50 @@ +{ + "positionX": "right", + "positionY": "top", + "layer": "overlay", + "control-center-layer": "overlay", + "cssPriority": "application", + + "control-center-width": 400, + "control-center-height": 600, + "control-center-margin-top": 3, + "control-center-margin-right": 3, + "control-center-margin-bottom": 3, + + "notification-window-width": 400, + "notification-icon-size": 48, + "notification-body-image-height": 100, + "notification-body-image-width": 200, + + "timeout": 10, + "timeout-low": 5, + "timeout-critical": 0, + "transition-time": 200, + + "notification-grouping": true, + "image-visibility": "when-available", + "relative-timestamps": true, + "keyboard-shortcuts": true, + "hide-on-clear": true, + "hide-on-action": true, + "fit-to-screen": true, + + "widgets": [ + "title", + "dnd", + "notifications" + ], + "widget-config": { + "title": { + "text": "notifications", + "clear-all-button": true, + "button-text": "clear" + }, + "dnd": { + "text": "do not disturb" + }, + "notifications": { + "vexpand": true + } + } +} diff --git a/swaync/style.css b/swaync/style.css new file mode 100644 index 0000000..42c14e5 --- /dev/null +++ b/swaync/style.css @@ -0,0 +1,163 @@ +/* gruvbox-material-soft-dark */ +@define-color bg #32302f; +@define-color bg_dim #252423; +@define-color bg_sel #45403d; +@define-color fg #d4be98; +@define-color red #ea6962; +@define-color green #a9b665; +@define-color yellow #d8a657; +@define-color blue #7daea3; +@define-color magenta #d3869b; +@define-color cyan #89b482; +@define-color gray #928374; + +* { + font-family: "JetBrainsMono Nerd Font"; + font-size: 14px; + font-weight: bold; +} + +/* floating notifications */ +.floating-notifications { + background: transparent; +} + +.notification { + background-color: @bg; + border: 2px solid @bg_sel; + border-radius: 8px; + margin: 4px; + padding: 8px; +} + +.notification.critical { + border-color: @red; +} + +.notification .summary { + color: @fg; + font-size: 14px; +} + +.notification .body { + color: @gray; + font-size: 13px; +} + +.notification .time { + color: @gray; + font-size: 12px; +} + +.notification .image { + margin-right: 8px; + border-radius: 4px; +} + +.close-button { + background-color: @bg_sel; + color: @fg; + border-radius: 50%; + min-width: 24px; + min-height: 24px; + padding: 0; +} + +.close-button:hover { + background-color: @red; + color: @bg_dim; +} + +/* action buttons */ +.notification .text-button { + background-color: @bg_sel; + color: @fg; + border-radius: 4px; + padding: 4px 8px; + margin: 2px; +} + +.notification .text-button:hover { + background-color: @blue; + color: @bg_dim; +} + +/* control center panel */ +.control-center { + background-color: @bg_dim; + border: 2px solid @bg_sel; + border-radius: 8px; + padding: 8px; +} + +.control-center .notification-row { + background-color: transparent; + margin: 2px 0; +} + +.control-center .notification-row .notification { + background-color: @bg; + border: 1px solid @bg_sel; +} + +.control-center .notification-row:hover .notification { + border-color: @gray; +} + +/* title widget */ +.widget-title { + color: @fg; + padding: 4px 8px; +} + +.widget-title button { + background-color: @bg_sel; + color: @fg; + border-radius: 4px; + padding: 4px 12px; +} + +.widget-title button:hover { + background-color: @red; + color: @bg_dim; +} + +/* dnd toggle */ +.widget-dnd { + color: @fg; + padding: 4px 8px; +} + +.widget-dnd > switch { + background-color: @bg_sel; + border-radius: 12px; +} + +.widget-dnd > switch:checked { + background-color: @yellow; +} + +.widget-dnd > switch slider { + background-color: @fg; + border-radius: 50%; + min-width: 20px; + min-height: 20px; +} + +/* empty state */ +.widget-notifications > label { + color: @gray; + padding: 16px; +} + +/* notification group */ +.notification-group { + background-color: @bg; + border-radius: 8px; + margin: 4px; +} + +.notification-group .notification-group-headers { + padding: 4px 8px; + color: @gray; +} From d7cb42a64c554793e42f271ccad8de1cf216cad8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Jane=C5=BEi=C4=8D?= Date: Thu, 12 Mar 2026 17:43:03 +0100 Subject: [PATCH 5/7] feat(waybar): add config --- waybar/config.jsonc | 179 ++++++++++++++++++++ waybar/scripts/mic-status.sh | 12 ++ waybar/scripts/notification-status.sh | 14 ++ waybar/scripts/ssh-session.sh | 21 +++ waybar/scripts/vpn-status.sh | 40 +++++ waybar/style.css | 232 ++++++++++++++++++++++++++ 6 files changed, 498 insertions(+) create mode 100644 waybar/config.jsonc create mode 100755 waybar/scripts/mic-status.sh create mode 100755 waybar/scripts/notification-status.sh create mode 100755 waybar/scripts/ssh-session.sh create mode 100755 waybar/scripts/vpn-status.sh create mode 100644 waybar/style.css diff --git a/waybar/config.jsonc b/waybar/config.jsonc new file mode 100644 index 0000000..8ebb038 --- /dev/null +++ b/waybar/config.jsonc @@ -0,0 +1,179 @@ +[ + { + "layer": "top", + "position": "top", + "height": 30, + "spacing": 1, + "margin": 0, + "modules-left": [ + "sway/workspaces", + "sway/mode" + ], + "modules-center": [ + "custom/ssh", + "custom/notification", + "systemd-failed-units" + ], + "modules-right": [ + "mpris", + "custom/vpn", + "cpu", + "memory", + "battery", + "wireplumber", + "custom/mic", + "backlight", + "sway/language", + "bluetooth", + "network", + "clock" + ], + + "sway/workspaces": { + "disable-scroll": true, + "format": "{name}" + }, + + "sway/mode": { + "format": "{}" + }, + + "sway/language": { + "format": "{short}", + "on-click": "swaymsg input type:keyboard xkb_switch_layout next", + "on-click-right": "~/.config/eww/scripts/popup.sh keyboard-popup" + }, + + "custom/ssh": { + "format": "{}", + "return-type": "json", + "interval": 15, + "exec": "~/.config/waybar/scripts/ssh-session.sh", + "on-click": "~/.config/waybar/scripts/ssh-session.sh disconnect" + }, + + "custom/notification": { + "format": "{}", + "return-type": "json", + "exec": "~/.config/waybar/scripts/notification-status.sh", + "on-click": "sleep 0.1 && swaync-client -t -sw", + "on-click-right": "sleep 0.1 && swaync-client -d -sw", + "escape": true + }, + + "systemd-failed-units": { + "hide-on-ok": true, + "format": "!{nr_failed}", + "format-ok": "", + "system": true, + "user": true, + "on-click": "ghostty -e sh -c 'systemctl --failed; systemctl --user --failed; read'" + }, + + // right: media + "mpris": { + "format": "{artist} - {title}", + "format-paused": "{artist} - {title} [paused]", + "format-stopped": "", + "max-length": 35, + "tooltip-format": "{player}: {artist} - {title} ({album})", + "on-click": "playerctl play-pause", + "on-click-right": "~/.config/eww/scripts/popup.sh media-popup", + "on-scroll-up": "playerctl next", + "on-scroll-down": "playerctl previous" + }, + + // right: connectivity + "network": { + "interval": 5, + "format-ethernet": "󰈀 {ipaddr}", + "format-wifi": "󰖩 {ipaddr}", + "format-linked": "󰈀 (no ip)", + "format-disconnected": "󰖪", + "tooltip-format": "{ifname} {ipaddr}/{cidr}\n{gwaddr}\n{bandwidthUpBits}up {bandwidthDownBits}down", + "on-click-right": "~/.config/eww/scripts/popup.sh network-popup" + }, + + "bluetooth": { + "format": "󰂯", + "format-connected": "󰂯 {num_connections}", + "format-connected-battery": "󰂯 {num_connections}", + "tooltip-format-connected": "{device_enumerate}", + "on-click-right": "~/.config/eww/scripts/popup.sh bluetooth-popup" + }, + + "custom/vpn": { + "format": "{}", + "return-type": "json", + "interval": 10, + "exec": "~/.config/waybar/scripts/vpn-status.sh", + "on-click-right": "~/.config/eww/scripts/popup.sh vpn-popup" + }, + + + "cpu": { + "format": "󰻠 {usage}%", + "tooltip": false, + "on-click-right": "~/.config/eww/scripts/popup.sh system-popup" + }, + + "memory": { + "interval": 10, + "format": "󰍛 {percentage}%", + "tooltip-format": "total: {total:0.2f}GiB\nused: {used:0.2f}GiB\navailable: {avail:0.2f}GiB\nswap: {swapUsed:0.2f}/{swapTotal:0.2f}GiB", + "on-click-right": "~/.config/eww/scripts/popup.sh system-popup" + }, + + // right: battery + "battery": { + "states": { + "warning": 30, + "critical": 15 + }, + "format": "󰁹 {capacity}%", + "format-charging": "󰂄 {capacity}%", + "format-plugged": "󰚥 {capacity}%", + "format-full": "󰁹 full", + "format-alt": "{time}", + "tooltip-format": "{timeTo}\n{power}W", + "on-click": "p=$(powerprofilesctl get); case $p in power-saver) n=balanced;; balanced) n=performance;; *) n=power-saver;; esac; powerprofilesctl set $n", + "on-click-right": "~/.config/eww/scripts/popup.sh battery-popup" + }, + + // right: audio + "wireplumber": { + "format": "󰕾 {volume}%", + "format-muted": "󰖁 muted", + "on-click": "pamixer -t", + "on-click-right": "~/.config/eww/scripts/popup.sh volume-popup", + "on-scroll-up": "pamixer -i 5", + "on-scroll-down": "pamixer -d 5", + "tooltip-format": "{node_name}: {volume}%" + }, + + "custom/mic": { + "format": "{}", + "return-type": "json", + "interval": 2, + "exec": "~/.config/waybar/scripts/mic-status.sh", + "on-click": "pamixer --default-source -t", + "on-click-right": "~/.config/eww/scripts/popup.sh volume-popup", + "on-scroll-up": "pamixer --default-source -i 5", + "on-scroll-down": "pamixer --default-source -d 5" + }, + + "backlight": { + "format": "󰃟 {percent}%", + "tooltip": false, + "on-click-right": "~/.config/eww/scripts/popup.sh volume-popup" + }, + + // right: clock + "clock": { + "interval": 1, + "format": "{:%d/%m %H:%M:%S}", + "tooltip-format": "{calendar}" + } + + } +] diff --git a/waybar/scripts/mic-status.sh b/waybar/scripts/mic-status.sh new file mode 100755 index 0000000..df3060c --- /dev/null +++ b/waybar/scripts/mic-status.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +# mic volume for waybar custom module + +vol=$(pamixer --default-source --get-volume 2>/dev/null || echo 0) +muted=$(pamixer --default-source --get-mute 2>/dev/null || echo false) + +if [[ "$muted" == "true" ]]; then + printf '{"text": "󰍭", "class": "muted", "tooltip": "mic muted"}\n' +else + printf '{"text": "󰍬 %d%%", "class": "", "tooltip": "mic %d%%"}\n' "$vol" "$vol" +fi diff --git a/waybar/scripts/notification-status.sh b/waybar/scripts/notification-status.sh new file mode 100755 index 0000000..65e9a33 --- /dev/null +++ b/waybar/scripts/notification-status.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +# wrap swaync subscription, hide when no notifications + +swaync-client -swb 2>/dev/null | while read -r line; do + count=$(echo "$line" | jq -r '.text // "0"') + class=$(echo "$line" | jq -r '.class // "none"') + + if [[ "$count" == "0" ]]; then + echo '{"text": "", "class": "none"}' + else + jq -nc --arg text "󰂚 $count" --arg class "$class" '{$text,$class}' + fi +done diff --git a/waybar/scripts/ssh-session.sh b/waybar/scripts/ssh-session.sh new file mode 100755 index 0000000..e6fc792 --- /dev/null +++ b/waybar/scripts/ssh-session.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash + +# detect remote ssh sessions and optionally disconnect them +# outputs waybar JSON; empty when no remote sessions + +if [[ "$1" == "disconnect" ]]; then + pkill -HUP -f 'sshd-session:.*@' 2>/dev/null + exit 0 +fi + +count=$(pgrep -cf 'sshd-session:.*@' 2>/dev/null || echo 0) + +if [[ "$count" -gt 0 ]]; then + # get remote session details + sessions=$(who 2>/dev/null | awk '$NF ~ /\([0-9]/ {gsub(/[()]/, "", $NF); print $1 "@" $NF}') + tooltip=${sessions:-"$count remote sessions"} + # replace newlines with \n for valid JSON + tooltip=${tooltip//$'\n'/\\n} + tooltip=${tooltip//\"/\\\"} + printf '{"text": "●", "class": "active", "tooltip": "%s"}\n' "$tooltip" +fi diff --git a/waybar/scripts/vpn-status.sh b/waybar/scripts/vpn-status.sh new file mode 100755 index 0000000..7d1c009 --- /dev/null +++ b/waybar/scripts/vpn-status.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash + +# check wireguard and tailscale status +# outputs waybar JSON; empty when inactive + +parts=() +tooltip_parts=() + +if command -v wg &>/dev/null; then + ifaces=$(wg show interfaces 2>/dev/null) + if [[ -n "$ifaces" ]]; then + parts+=("󰖂") + tooltip_parts+=("wireguard: $ifaces") + fi +fi + +if command -v tailscale &>/dev/null; then + ts_json=$(tailscale status --json 2>/dev/null) + state=$(jq -r '.BackendState // empty' <<<"$ts_json") + if [[ "$state" == "Running" ]]; then + ts_ip=$(jq -r '.TailscaleIPs[0] // empty' <<<"$ts_json") + ts_name=$(jq -r '.Self.HostName // empty' <<<"$ts_json") + ts_exit=$(jq -r '.ExitNodeStatus.ID // empty' <<<"$ts_json") + parts+=("󰒒") + tip="tailscale: ${ts_name} ${ts_ip}" + if [[ -n "$ts_exit" ]]; then + tip="$tip (exit node)" + fi + tooltip_parts+=("$tip") + fi +fi + +if [[ ${#parts[@]} -gt 0 ]]; then + text="${parts[*]}" + tip=$(printf '%s\\n' "${tooltip_parts[@]}") + # strip trailing \n + tip=${tip%\\n} + jq -nc --arg text "$text" --arg tooltip "$tip" --arg class "active" \ + '{$text,$class,$tooltip}' +fi diff --git a/waybar/style.css b/waybar/style.css new file mode 100644 index 0000000..333668c --- /dev/null +++ b/waybar/style.css @@ -0,0 +1,232 @@ +/* gruvbox-material-soft-dark palette + * mapped from ghostty/themes/gruvbox-material-soft-dark + * ansi 0=#252423 1=#ea6962 2=#a9b665 3=#d8a657 + * 4=#7daea3 5=#d3869b 6=#89b482 7=#d4be98 + * bg=#32302f fg=#d4be98 selection=#45403d */ +@define-color bg #32302f; +@define-color bg_dim #252423; +@define-color bg_sel #45403d; +@define-color fg #d4be98; +@define-color red #ea6962; +@define-color green #a9b665; +@define-color yellow #d8a657; +@define-color blue #7daea3; +@define-color magenta #d3869b; +@define-color cyan #89b482; +@define-color gray #928374; + +* { + font-family: "JetBrainsMono Nerd Font"; + font-size: 14px; + font-weight: bold; +} + +/* bar: dark background, modules float inside */ +window#waybar { + background-color: @bg_dim; + color: @fg; +} + +window#waybar.hidden { + opacity: 0.2; +} + +button { + box-shadow: none; + border: none; + border-radius: 0; +} + +button:hover { + background: inherit; + box-shadow: none; +} + +/* workspaces */ +#workspaces { + background-color: @bg; + border-radius: 8px; + margin: 3px 2px; + padding: 0; +} + +#workspaces button { + padding: 0 8px; + background-color: transparent; + color: @gray; + margin: 0; + border-radius: 8px; +} + +#workspaces button:hover { + background-color: @bg_sel; + color: @fg; + box-shadow: none; +} + +#workspaces button.focused { + background-color: @bg_sel; + color: @fg; + box-shadow: none; +} + +#workspaces button.urgent { + background-color: @red; + color: @bg_dim; +} + +/* floating pill style for all modules */ +#mode, +#clock, +#battery, +#cpu, +#memory, +#backlight, +#network, +#wireplumber, +#mpris, +#bluetooth, +#language, +#custom-mic, +#custom-vpn, +#custom-ssh, +#custom-notification, +#systemd-failed-units { + background-color: @bg; + border-radius: 8px; + color: @fg; + padding: 0 10px; + margin: 3px 2px; +} + +/* center: alert zone */ +#custom-ssh.active { + color: @cyan; + font-size: 20px; +} + +#custom-notification.notification { + color: @yellow; +} + +#custom-notification.dnd-notification { + color: @red; +} + +#custom-notification.dnd-none { + color: @gray; +} + +#systemd-failed-units { + color: @red; +} + +/* right: media (hidden by default, visible only when playing/paused) */ +#mpris { + padding: 0; + margin: 0; + min-width: 0; + background-color: transparent; + color: @green; +} + +#mpris.playing { + padding: 0 10px; + margin: 3px 2px; + background-color: @bg; +} + +#mpris.paused { + padding: 0 10px; + margin: 3px 2px; + background-color: @bg; + color: @gray; +} + +/* right: connectivity */ +#network.disconnected { + color: @red; +} + +#bluetooth { + color: @blue; +} + +#custom-vpn { + color: @cyan; +} + +/* right: system */ +#cpu { + margin-right: 0; + border-radius: 8px 0 0 8px; +} + +#memory { + margin-left: 0; + border-radius: 0 8px 8px 0; +} + +/* right: battery */ +#battery.charging, +#battery.plugged { + color: @green; +} + +#battery.warning:not(.charging) { + color: @yellow; +} + +@keyframes blink { + to { + background-color: @red; + color: @bg_dim; + } +} + +#battery.critical:not(.charging) { + color: @red; + animation-name: blink; + animation-duration: 0.5s; + animation-timing-function: steps(12); + animation-iteration-count: infinite; + animation-direction: alternate; +} + +/* right: audio */ +#wireplumber.muted { + color: @red; +} + +#custom-mic.muted { + color: @red; +} + +/* right: audio group (volume + mic + brightness) */ +#wireplumber { + margin-right: 0; + border-radius: 8px 0 0 8px; +} + +#custom-mic { + margin-left: 0; + margin-right: 0; + border-radius: 0; +} + +#backlight { + margin-left: 0; + border-radius: 0 8px 8px 0; +} + +/* right: clock */ +#clock { + color: @fg; +} + +/* mode highlight */ +#mode { + color: @bg_dim; + background-color: @yellow; + padding: 0 10px; +} From 8cf31772a5aa0484d36f618dcbb50025126b5c93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Jane=C5=BEi=C4=8D?= Date: Thu, 12 Mar 2026 17:43:17 +0100 Subject: [PATCH 6/7] feat(swaylock): update image and colors --- swaylock/config | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/swaylock/config b/swaylock/config index 8803a5f..8a4799c 100644 --- a/swaylock/config +++ b/swaylock/config @@ -1,7 +1,7 @@ ignore-empty-password show-failed-attempts -image=~/.assets/lockscreen.png +image=~/.assets/wallpaper.png clock datestr=%A, %d/%m/%y @@ -12,23 +12,24 @@ indicator-idle-visible indicator-y-position=300 indicator-x-position=1200 -inside-color=000000 -text-color=ffffff -ring-color=ffffff +# gruvbox-material-soft-dark +inside-color=#32302f +text-color=#d4be98 +ring-color=#928374 -inside-ver-color=000000 -text-ver-color=000000 -ring-ver-color=ffffff +inside-ver-color=#32302f +text-ver-color=#a9b665 +ring-ver-color=#a9b665 -inside-clear-color=000000 -text-clear-color=ffffff -ring-clear-color=ffffff +inside-clear-color=#32302f +text-clear-color=#d8a657 +ring-clear-color=#d8a657 -inside-wrong-color=000000 -text-wrong-color=ffffff -ring-wrong-color=ffffff +inside-wrong-color=#32302f +text-wrong-color=#ea6962 +ring-wrong-color=#ea6962 -key-hl-color=000000 -bs-hl-color=000000 -caps-lock-key-hl-color=000000 -caps-lock-bs-hl-color=000000 +key-hl-color=#7daea3 +bs-hl-color=#ea6962 +caps-lock-key-hl-color=#d8a657 +caps-lock-bs-hl-color=#ea6962 From 4e765c57ff8e10fcad7e978f2fb02ee4cd024e12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Jane=C5=BEi=C4=8D?= Date: Thu, 12 Mar 2026 17:44:05 +0100 Subject: [PATCH 7/7] feat(sway): update for new components --- sway/config.d/10-variables.conf | 2 +- sway/config.d/50-keybind.conf | 14 ++++++++++++-- sway/config.d/80-autostart.conf | 5 ++++- sway/scripts/power-menu.sh | 13 +++++++++++++ 4 files changed, 30 insertions(+), 4 deletions(-) create mode 100755 sway/scripts/power-menu.sh diff --git a/sway/config.d/10-variables.conf b/sway/config.d/10-variables.conf index 4ffdd91..8e5f6f2 100644 --- a/sway/config.d/10-variables.conf +++ b/sway/config.d/10-variables.conf @@ -8,7 +8,7 @@ set $right l # programs set $term ghostty -set $menu wofi --show drun -iIG -w 2 +set $menu fuzzel set $browser google-chrome-stable set $lockscreen swaylock diff --git a/sway/config.d/50-keybind.conf b/sway/config.d/50-keybind.conf index 3574de8..23ff348 100644 --- a/sway/config.d/50-keybind.conf +++ b/sway/config.d/50-keybind.conf @@ -1,6 +1,6 @@ # sway bindsym $mod+Shift+c reload -bindsym $mod+Shift+e exec swaynag -t warning -m 'You pressed the exit shortcut. Do you really want to exit sway? This will end your Wayland session.' -B 'Yes, exit sway' 'swaymsg exit' +bindsym $mod+Shift+e exec ~/.config/sway/scripts/power-menu.sh floating_modifier $mod normal # programs @@ -10,6 +10,16 @@ bindsym $mod+u exec $lockscreen bindsym $mod+Shift+Ctrl+Alt+space exec 1password --quick-access bindsym Print exec flameshot gui +# notifications +bindsym $mod+n exec swaync-client -t -sw +bindsym $mod+Shift+n exec swaync-client -C -sw + +# clipboard +bindsym $mod+c exec cliphist list | fuzzel -d | cliphist decode | wl-copy + +# eww popups +bindsym $mod+Escape exec ~/.config/eww/scripts/popup.sh close-all + # window bindsym $mod+Shift+q kill @@ -80,7 +90,7 @@ bindsym $mod+Shift+minus move scratchpad bindsym $mod+minus scratchpad show # xf86-volume -bindsym --locked XF86AudioMute exec pamixer -t && pamixer --get-volume > $wobs +bindsym --locked XF86AudioMute exec pamixer -t && ( pamixer --get-mute && echo 0 > $wobs || pamixer --get-volume > $wobs ) bindsym --locked XF86AudioLowerVolume exec pamixer -d 5 && pamixer --get-volume > $wobs bindsym --locked XF86AudioRaiseVolume exec pamixer -i 5 && pamixer --get-volume > $wobs bindsym --locked F16 exec pamixer --default-source -t diff --git a/sway/config.d/80-autostart.conf b/sway/config.d/80-autostart.conf index 73cf2d8..739e2a9 100644 --- a/sway/config.d/80-autostart.conf +++ b/sway/config.d/80-autostart.conf @@ -1,7 +1,10 @@ # daemon -exec swayidle -w +exec eww daemon +exec swayidle -w exec wlsunset -l 46.1 -L 14.5 exec pkill -x wob; rm -f $wobs && mkfifo $wobs && tail -f $wobs | wob +exec swaync +exec wl-paste --watch cliphist store exec protonmail-bridge -n # traditional "start when os starts" programs diff --git a/sway/scripts/power-menu.sh b/sway/scripts/power-menu.sh new file mode 100755 index 0000000..cf4649d --- /dev/null +++ b/sway/scripts/power-menu.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +# power menu via fuzzel + +choice=$(printf "lock\nlogout\nsuspend\nreboot\nshutdown" | fuzzel -d -p "power: ") + +case "$choice" in + lock) swaylock ;; + logout) swaymsg exit ;; + suspend) systemctl suspend ;; + reboot) systemctl reboot ;; + shutdown) systemctl poweroff ;; +esac