diff --git a/.config/nvim/lazy-lock.json b/.config/nvim/lazy-lock.json
index 4686a7c..ec559f8 100644
--- a/.config/nvim/lazy-lock.json
+++ b/.config/nvim/lazy-lock.json
@@ -12,7 +12,6 @@
"friendly-snippets": { "branch": "main", "commit": "6cd7280adead7f586db6fccbd15d2cac7e2188b9" },
"gitsigns.nvim": { "branch": "main", "commit": "25050e4ed39e628282831d4cbecb1850454ce915" },
"grug-far.nvim": { "branch": "main", "commit": "c995bbacf8229dc096ec1c3d60f8531059c86c1b" },
- "hybrid-theme": { "branch": "main", "commit": "b131110fbe63481d7d263c71982206a70a04d236" },
"indent-blankline.nvim": { "branch": "master", "commit": "d28a3f70721c79e3c5f6693057ae929f3d9c0a03" },
"just-runner.nvim": { "branch": "main", "commit": "f29d405aa828900df242600720a2b0e57261489f" },
"lazy.nvim": { "branch": "main", "commit": "85c7ff3711b730b4030d03144f6db6375044ae82" },
diff --git a/.config/nvim/lua/plugins/colorscheme.lua b/.config/nvim/lua/plugins/colorscheme.lua
index 8394a72..f81ac04 100644
--- a/.config/nvim/lua/plugins/colorscheme.lua
+++ b/.config/nvim/lua/plugins/colorscheme.lua
@@ -13,39 +13,39 @@
-- }
-- Local development:
--- return {
--- dir = "/home/chodak/src/git/nvim-hybrid-theme",
--- name = "hybrid-theme",
--- lazy = false,
--- priority = 1000,
--- config = function()
--- require("hybrid-theme").setup({
--- theme = "dark",
--- transparent = false,
--- background_variant = "flat",
--- italics = {
--- comments = true,
--- keywords = false,
--- functions = false,
--- strings = false,
--- variables = false,
--- },
--- })
--- require("hybrid-theme").colorscheme()
--- end,
--- }
-
return {
- {
- "chodak166/nvim-hybrid-theme",
- name = "hybrid-theme",
- lazy = false,
- priority = 1000,
- config = function()
- require("hybrid-theme").setup({
- background_variant = "semi_flat",
- })
- require("hybrid-theme").colorscheme()
- end,
- },
+ dir = "/home/chodak/src/git/nvim-hybrid-theme",
+ name = "hybrid-theme",
+ lazy = false,
+ priority = 1000,
+ config = function()
+ require("hybrid-theme").setup({
+ theme = "dark",
+ transparent = false,
+ background_variant = "flat",
+ italics = {
+ comments = true,
+ keywords = false,
+ functions = false,
+ strings = false,
+ variables = false,
+ },
+ })
+ require("hybrid-theme").colorscheme()
+ end,
}
+
+-- return {
+-- {
+-- "chodak166/nvim-hybrid-theme",
+-- name = "hybrid-theme",
+-- lazy = false,
+-- priority = 1000,
+-- config = function()
+-- require("hybrid-theme").setup({
+-- background_variant = "semi_flat",
+-- })
+-- require("hybrid-theme").colorscheme()
+-- end,
+-- },
+-- }
diff --git a/.config/waybar/config.jsonc b/.config/waybar/config.jsonc
new file mode 100644
index 0000000..5d95ab3
--- /dev/null
+++ b/.config/waybar/config.jsonc
@@ -0,0 +1,135 @@
+// -*- mode: json -*-
+
+{
+ "layer": "top",
+ "position": "top",
+
+ "modules-left": [
+ "sway/workspaces",
+ "custom/right-arrow-dark"
+ ],
+ "modules-center": [
+ "custom/left-arrow-dark",
+ "sway/window",
+ "custom/right-arrow-dark"
+ ],
+ "modules-right": [
+ "custom/left-arrow-dark",
+ "pulseaudio",
+ "custom/left-arrow-light",
+ "custom/left-arrow-dark",
+ "custom/network",
+ "custom/left-arrow-light",
+ "custom/left-arrow-dark",
+ "custom/disk",
+ "custom/left-arrow-light",
+ "custom/left-arrow-dark",
+ "memory",
+ "custom/left-arrow-light",
+ "custom/left-arrow-dark",
+ "custom/top",
+ "custom/left-arrow-light",
+ "custom/left-arrow-dark",
+ "battery",
+ "custom/left-arrow-light",
+ "custom/left-arrow-dark",
+ "clock#0",
+ "custom/left-arrow-light",
+ "custom/left-arrow-dark",
+ "tray"
+ ],
+
+ "custom/left-arrow-dark": {
+ "format": "",
+ "tooltip": false
+ },
+ "custom/left-arrow-light": {
+ "format": "",
+ "tooltip": false
+ },
+ "custom/right-arrow-dark": {
+ "format": "",
+ "tooltip": false
+ },
+
+ "sway/workspaces": {
+ "disable-scroll": true,
+ "format": "{name}"
+ },
+
+ "sway/window": {
+ "max-length": 70,
+ "format": "{title}"
+ },
+
+ "custom/network": {
+ "exec": "/home/chodak/.config/waybar/network.py",
+ "interval": 10,
+ "return-type": "json",
+ "tooltip": true
+ },
+
+ "clock#2": {
+ "format": "{:%H:%M}",
+ "tooltip": false
+ },
+ "clock#3": {
+ "format": "{:%d/%m}",
+ "tooltip": false
+ },
+ "clock#0": {
+ "format": "{:%H:%M %d/%m/%y}",
+ "tooltip": false
+ },
+
+ "pulseaudio": {
+ "format": "{icon} {volume:2}%",
+ "format-bluetooth": "{icon} {volume}%",
+ "format-muted": "MUTE",
+ "format-icons": {
+ "headphones": "",
+ "default": [
+ "",
+ ""
+ ]
+ },
+ "scroll-step": 5,
+ "on-click": "pamixer -t",
+ "on-click-right": "pavucontrol"
+ },
+ "memory": {
+ "interval": 5,
+ "format": " {}%"
+ },
+ "custom/top": {
+ "exec": "~/.config/waybar/top.sh",
+ "interval": 5,
+ "return-type": "json",
+ "tooltip": true
+ },
+ "battery": {
+ "states": {
+ "good": 95,
+ "warning": 30,
+ "critical": 15
+ },
+ "format": "{icon} {capacity}%",
+ "format-icons": [
+ "",
+ "",
+ "",
+ "",
+ ""
+ ]
+ },
+ "custom/disk": {
+ "exec": "/home/chodak/.config/waybar/disk.py",
+ "interval": 30,
+ "return-type": "json",
+ "tooltip": true,
+ "escape": false
+ },
+ "tray": {
+ "icon-size": 24
+ }
+}
diff --git a/.config/waybar/disk.py b/.config/waybar/disk.py
new file mode 100755
index 0000000..dab4761
--- /dev/null
+++ b/.config/waybar/disk.py
@@ -0,0 +1,157 @@
+#!/usr/bin/env python3
+"""Waybar disk module: shows disk usage across all mounted partitions."""
+
+import json
+import os
+
+# Filesystem types to ignore (virtual / non-disk)
+IGNORE_FSTYPES = {
+ "tmpfs", "devtmpfs", "proc", "sysfs", "cgroup", "cgroup2",
+ "pstore", "bpf", "debugfs", "tracefs", "securityfs", "configfs",
+ "hugetlbfs", "devpts", "ramfs", "fuse.gvfsd-fuse", "fusectl",
+ "mqueue", "rpc_pipefs", "overlay", "squashfs",
+}
+
+# Pango color thresholds
+COLOR_LOW = "#a6e3a1" # green — < 70%
+COLOR_MID = "#f9e2af" # yellow — 70–89%
+COLOR_HIGH = "#f38ba8" # red — ≥ 90%
+
+
+def _pct_color(pct: float) -> str:
+ """Return a Pango color string for the given usage percentage."""
+ if pct >= 90:
+ return COLOR_HIGH
+ if pct >= 70:
+ return COLOR_MID
+ return COLOR_LOW
+
+
+def _span(color: str, text: str) -> str:
+ """Wrap text in a Pango with the given foreground color."""
+ return f"{text}"
+
+
+def get_partitions():
+ """Return list of (device, mountpoint, fstype) for real partitions."""
+ partitions = []
+ seen_devices = set()
+
+ try:
+ with open("/proc/mounts") as f:
+ for line in f:
+ parts = line.split()
+ if len(parts) < 3:
+ continue
+ device, mountpoint, fstype = parts[:3]
+
+ if fstype in IGNORE_FSTYPES:
+ continue
+ if not device.startswith("/dev/"):
+ continue
+ if device in seen_devices:
+ continue
+ seen_devices.add(device)
+
+ partitions.append((device, mountpoint, fstype))
+ except OSError:
+ pass
+
+ return partitions
+
+
+def get_usage(mountpoint):
+ """Return (total_bytes, used_bytes, pct) for a mountpoint, or None."""
+ try:
+ st = os.statvfs(mountpoint)
+ total = st.f_frsize * st.f_blocks
+ free = st.f_frsize * st.f_bfree
+ used = total - free
+ pct = (used / total) * 100 if total > 0 else 0.0
+ return total, used, pct
+ except OSError:
+ return None
+
+
+def fmt_size(bytes_val):
+ """Format bytes as human-readable."""
+ if bytes_val >= 1 << 40:
+ return f"{bytes_val / (1 << 40):.1f}T"
+ if bytes_val >= 1 << 30:
+ return f"{bytes_val / (1 << 30):.1f}G"
+ if bytes_val >= 1 << 20:
+ return f"{bytes_val / (1 << 20):.1f}M"
+ if bytes_val >= 1 << 10:
+ return f"{bytes_val / (1 << 10):.1f}K"
+ return f"{bytes_val}B"
+
+
+def main():
+ partitions = get_partitions()
+
+ if not partitions:
+ print(json.dumps(
+ {"text": " —", "tooltip": "No disk partitions found"},
+ ensure_ascii=False))
+ return
+
+ tooltip_lines = []
+ root_pct = None
+ bar_parts = []
+
+ for device, mountpoint, fstype in partitions:
+ usage = get_usage(mountpoint)
+ if usage is None:
+ continue
+ total, used, pct = usage
+
+ total_str = fmt_size(total)
+ used_str = fmt_size(used)
+ color = _pct_color(pct)
+
+ # Icons per mountpoint
+ if mountpoint == "/":
+ icon = ""
+ elif mountpoint == "/home":
+ icon = ""
+ elif mountpoint == "/boot":
+ icon = ""
+ elif "/media/" in mountpoint or "/run/media/" in mountpoint:
+ icon = ""
+ else:
+ icon = ""
+
+ # Tooltip line with colored percentage
+ pct_span = _span(color, f"{pct:5.1f}%")
+ warn = " ⚠" if pct >= 90 else ""
+ tooltip_lines.append(
+ f"{icon} {mountpoint:20s} {used_str:>6s} / {total_str:>6s} ({pct_span}){warn}"
+ )
+
+ # Track root for bar display
+ if mountpoint == "/":
+ root_pct = pct
+
+ # Build bar text using Pango markup
+ if root_pct is not None:
+ bar_icon = "⚠" if root_pct >= 90 else ""
+ bar_color = _pct_color(root_pct)
+ bar_text = (
+ f"{bar_icon}"
+ f" {_span(bar_color, f'{root_pct:.0f}%')}"
+ )
+ elif tooltip_lines:
+ bar_text = f" —"
+ else:
+ bar_text = " —"
+
+ tooltip = "\n".join(tooltip_lines) if tooltip_lines else "No partitions"
+
+ print(json.dumps(
+ {"text": bar_text, "tooltip": tooltip},
+ ensure_ascii=False))
+
+
+if __name__ == "__main__":
+ main()
+
diff --git a/.config/waybar/network.py b/.config/waybar/network.py
new file mode 100755
index 0000000..dc67b1a
--- /dev/null
+++ b/.config/waybar/network.py
@@ -0,0 +1,140 @@
+#!/usr/bin/env python3
+"""Waybar network module: shows interface, IP, and up/down transfer rate."""
+
+import json
+import os
+import re
+import subprocess
+import time
+
+STATE_FILE_TMPL = "/tmp/waybar-network-stats-{}"
+ICON = "" # nerd font network icon
+NO_NET_ICON = ""
+
+
+def run(cmd: list[str]) -> str:
+ """Run a command and return stdout, or empty string on failure."""
+ try:
+ return subprocess.check_output(
+ cmd, stderr=subprocess.DEVNULL, text=True
+ ).strip()
+ except subprocess.CalledProcessError:
+ return ""
+
+
+def get_default_interface() -> str | None:
+ """Return the default route interface name."""
+ output = run(["ip", "route"])
+ for line in output.splitlines():
+ if line.startswith("default"):
+ parts = line.split()
+ # "default via X.X.X.X dev INTERFACE ..."
+ if len(parts) >= 5:
+ return parts[4]
+ return None
+
+
+def get_ip(iface: str) -> str | None:
+ """Return the IPv4 address of the given interface."""
+ output = run(["ip", "-4", "addr", "show", iface])
+ m = re.search(r"inet\s+(\d+\.\d+\.\d+\.\d+)", output)
+ return m.group(1) if m else None
+
+
+def iface_type(iface: str) -> str:
+ """Classify interface as WiFi, Ethernet, or generic."""
+ if re.match(r"^(wlan|wlp)", iface):
+ return "WiFi"
+ if re.match(r"^(eth|enp|eno|ens)", iface):
+ return "Ethernet"
+ return iface
+
+
+def format_rate(bps: float) -> str:
+ """Format bytes-per-second as human-readable with one decimal."""
+ bps = max(bps, 0)
+ if bps >= 1_073_741_824:
+ return f"{bps / 1_073_741_824:.1f}G"
+ if bps >= 1_048_576:
+ return f"{bps / 1_048_576:.1f}M"
+ if bps >= 1024:
+ return f"{bps / 1024:.1f}K"
+ return f"{bps:.1f}B"
+
+
+def main() -> None:
+ iface = get_default_interface()
+ if iface is None:
+ print(json.dumps(
+ {"text": f"{NO_NET_ICON} No net",
+ "tooltip": "No network connection"},
+ ensure_ascii=False))
+ return
+
+ ip = get_ip(iface)
+ if ip is None:
+ print(json.dumps(
+ {"text": f"{NO_NET_ICON} No IP",
+ "tooltip": f"Interface {iface} has no IP"},
+ ensure_ascii=False))
+ return
+
+ itype = iface_type(iface)
+
+ # Read current byte counters
+ now_ts = time.time_ns()
+ rx_path = f"/sys/class/net/{iface}/statistics/rx_bytes"
+ tx_path = f"/sys/class/net/{iface}/statistics/tx_bytes"
+
+ try:
+ with open(rx_path) as f:
+ now_rx = int(f.read().strip())
+ with open(tx_path) as f:
+ now_tx = int(f.read().strip())
+ except (OSError, ValueError):
+ now_rx = now_tx = 0
+
+ # Transfer rate calculation
+ rate_down = ""
+ rate_up = ""
+ state_file = STATE_FILE_TMPL.format(iface)
+
+ if os.path.exists(state_file):
+ try:
+ with open(state_file) as f:
+ prev_rx, prev_tx, prev_ts = map(int, f.read().split())
+ delta_ns = now_ts - prev_ts
+ if delta_ns > 0:
+ delta_rx = now_rx - prev_rx
+ delta_tx = now_tx - prev_tx
+ rx_bps = delta_rx * 1e9 / delta_ns
+ tx_bps = delta_tx * 1e9 / delta_ns
+ rate_down = format_rate(rx_bps)
+ rate_up = format_rate(tx_bps)
+ except (OSError, ValueError):
+ pass
+
+ # Save current state for next run
+ with open(state_file, "w") as f:
+ f.write(f"{now_rx} {now_tx} {now_ts}")
+
+ # Build output
+ if rate_down and rate_up:
+ rate_str = f" ↓{rate_down} ↑{rate_up}"
+ tooltip_rate = f"↓ {rate_down}/s ↑ {rate_up}/s"
+ else:
+ rate_str = ""
+ tooltip_rate = "↓ ?/s ↑ ?/s"
+
+ text = (
+ f"{ICON}"
+ f" {iface} {ip}{rate_str}"
+ )
+ tooltip = f"{itype}: {iface} — {ip} | {tooltip_rate}"
+
+ print(json.dumps({"text": text, "tooltip": tooltip}, ensure_ascii=False))
+
+
+if __name__ == "__main__":
+ main()
+
diff --git a/.config/waybar/network.sh b/.config/waybar/network.sh
new file mode 100755
index 0000000..4f6dd70
--- /dev/null
+++ b/.config/waybar/network.sh
@@ -0,0 +1,97 @@
+#!/usr/bin/env bash
+
+# Get the default interface used for routing
+INTERFACE=$(ip route 2>/dev/null | grep '^default' | awk '{print $5}' | head -1)
+
+if [ -z "$INTERFACE" ]; then
+ echo '{"text": " No net", "tooltip": "No network connection"}'
+ exit 0
+fi
+
+# Get IPv4 address of the interface
+IP=$(ip -4 addr show "$INTERFACE" 2>/dev/null | grep -oP '(?<=inet\s)\d+(\.\d+){3}' | head -1)
+
+if [ -z "$IP" ]; then
+ echo '{"text": " No IP", "tooltip": "Interface '"$INTERFACE"' has no IP"}'
+ exit 0
+fi
+
+# Determine interface type and set appropriate icon
+if [[ "$INTERFACE" =~ ^(wlan|wlp) ]]; then
+ ICON="" # generic network icon (nerd font)
+ TYPE="WiFi"
+elif [[ "$INTERFACE" =~ ^(eth|enp|eno|ens) ]]; then
+ ICON="" # generic network icon (nerd font)
+ TYPE="Ethernet"
+else
+ ICON="" # generic network icon (nerd font)
+ TYPE="$INTERFACE"
+fi
+
+# --- Transfer rate calculation ---
+
+# Path to sysfs statistics
+RX_FILE="/sys/class/net/$INTERFACE/statistics/rx_bytes"
+TX_FILE="/sys/class/net/$INTERFACE/statistics/tx_bytes"
+STATE_FILE="/tmp/waybar-network-stats-$INTERFACE"
+
+# Read current byte counters
+NOW_RX=$(cat "$RX_FILE" 2>/dev/null)
+NOW_TX=$(cat "$TX_FILE" 2>/dev/null)
+NOW_TS=$(date +%s%N) # nanoseconds for precision
+
+# Helper: format bytes/sec to human-readable
+format_rate() {
+ local bytes_per_sec="$1"
+ if [ "$bytes_per_sec" -lt 0 ]; then
+ bytes_per_sec=0
+ fi
+ if [ "$bytes_per_sec" -ge 1073741824 ]; then
+ awk -v n="$bytes_per_sec" 'BEGIN { printf "%.1fG", n / 1073741824 }'
+ elif [ "$bytes_per_sec" -ge 1048576 ]; then
+ awk -v n="$bytes_per_sec" 'BEGIN { printf "%.1fM", n / 1048576 }'
+ elif [ "$bytes_per_sec" -ge 1024 ]; then
+ awk -v n="$bytes_per_sec" 'BEGIN { printf "%.1fK", n / 1024 }'
+ else
+ awk -v n="$bytes_per_sec" 'BEGIN { printf "%.1fB", n }'
+ fi
+}
+
+RATE_DOWN=""
+RATE_UP=""
+
+if [ -f "$STATE_FILE" ]; then
+ # Read previous values: rx tx timestamp_ns
+ read -r PREV_RX PREV_TX PREV_TS < "$STATE_FILE"
+
+ if [ -n "$PREV_RX" ] && [ -n "$PREV_TX" ] && [ -n "$PREV_TS" ]; then
+ # Calculate time delta in seconds (nanosecond difference)
+ DELTA_NS=$((NOW_TS - PREV_TS))
+ if [ "$DELTA_NS" -gt 0 ]; then
+ DELTA_RX=$((NOW_RX - PREV_RX))
+ DELTA_TX=$((NOW_TX - PREV_TX))
+
+ # bytes per second = delta_bytes / (delta_ns / 1e9)
+ RX_BPS=$(awk -v rx="$DELTA_RX" -v ns="$DELTA_NS" 'BEGIN { printf "%.0f", rx * 1000000000 / ns }')
+ TX_BPS=$(awk -v tx="$DELTA_TX" -v ns="$DELTA_NS" 'BEGIN { printf "%.0f", tx * 1000000000 / ns }')
+
+ if [ -n "$RX_BPS" ] && [ -n "$TX_BPS" ]; then
+ RATE_DOWN=$(format_rate "$RX_BPS")
+ RATE_UP=$(format_rate "$TX_BPS")
+ fi
+ fi
+ fi
+fi
+
+# Save current state for next run
+echo "$NOW_RX $NOW_TX $NOW_TS" > "$STATE_FILE"
+
+# --- Build output ---
+
+if [ -n "$RATE_DOWN" ] && [ -n "$RATE_UP" ]; then
+ RATE_STR=" ↓${RATE_DOWN} ↑${RATE_UP}"
+else
+ RATE_STR=""
+fi
+
+echo "{\"text\": \"$ICON $INTERFACE $IP${RATE_STR}\", \"tooltip\": \"$TYPE: $INTERFACE — $IP | ↓ ${RATE_DOWN:-?}/s ↑ ${RATE_UP:-?}/s\"}"
diff --git a/.config/waybar/style.css b/.config/waybar/style.css
new file mode 100644
index 0000000..a9d2135
--- /dev/null
+++ b/.config/waybar/style.css
@@ -0,0 +1,77 @@
+* {
+ font-size: 13px;
+ font-family: monospace;
+}
+
+window#waybar {
+ background: #282828;
+ color: #fbf1c7;
+}
+
+#custom-right-arrow-dark,
+#custom-left-arrow-dark {
+ color: #1d2021;
+}
+#custom-right-arrow-light,
+#custom-left-arrow-light {
+ color: #282828;
+ background: #1d2021;
+}
+
+#workspaces,
+#window,
+#clock.0,
+#clock.2,
+#clock.3,
+#pulseaudio,
+#memory,
+#custom-top,
+#battery,
+#custom-disk,
+#custom-network,
+#tray {
+ background: #1d2021;
+}
+
+#workspaces button {
+ padding: 0 2px;
+ color: #a89984;
+}
+#workspaces button.focused {
+ color: #fabd2f;
+ font-weight: bold;
+ /* border-bottom: 2px solid #fabd2f; */
+}
+#workspaces button:hover {
+ box-shadow: inherit;
+ text-shadow: inherit;
+}
+#workspaces button:hover {
+ background: #1d2021;
+ border: #1d2021;
+ padding: 0 3px;
+}
+
+/* Right-side widgets: lighter sandy yellow */
+#window,
+#clock,
+#pulseaudio,
+#memory,
+#custom-top,
+#battery,
+#custom-disk,
+#custom-network,
+#tray {
+ color: #f5d67b;
+}
+
+#window,
+#clock,
+#pulseaudio,
+#memory,
+#custom-top,
+#battery,
+#custom-disk,
+#custom-network {
+ padding: 0 10px;
+}
diff --git a/.config/waybar/top.sh b/.config/waybar/top.sh
new file mode 100755
index 0000000..ae05501
--- /dev/null
+++ b/.config/waybar/top.sh
@@ -0,0 +1,30 @@
+#!/usr/bin/env bash
+
+# Get the top CPU-consuming process, excluding kernel threads and this script
+read -r NAME CPU < <(
+ ps -eo comm,pcpu --sort=-pcpu --no-headers 2>/dev/null |
+ awk '{
+ # Skip kernel threads (names in brackets)
+ if ($1 ~ /^\[/) next
+ # Skip this script and ps itself
+ if ($1 == "top.sh" || $1 == "ps") next
+ # Output the first matching line and exit
+ print $1, $2
+ exit
+ }'
+)
+
+if [ -z "$NAME" ]; then
+ echo '{"text": " --", "tooltip": "No process data"}'
+ exit 0
+fi
+
+# Round CPU to integer
+CPU=$(printf "%.0f" "$CPU")
+
+# Truncate long process names (keep bar readable)
+if [ ${#NAME} -gt 10 ]; then
+ NAME="${NAME:0:9}…"
+fi
+
+echo "{\"text\": \" $NAME ${CPU}%\", \"tooltip\": \"Top CPU: $NAME — ${CPU}%\"}"
diff --git a/.config/waybar/watch.sh b/.config/waybar/watch.sh
new file mode 100755
index 0000000..4474036
--- /dev/null
+++ b/.config/waybar/watch.sh
@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+
+while inotifywait -e close_write ~/.config/waybar; do
+ killall -SIGUSR2 waybar
+done