30 Commits

Author SHA1 Message Date
alex f30bf8a3bd feat(fish): preserve directory after Yazi exits
Add the standard cwd-file wrapper so Yazi navigation updates the interactive shell directory.
2026-08-13 15:50:46 +02:00
alex 11d8e9542a fix(noctalia): migrate portable widget settings
Repair the shared plugin declaration and use the current sysmon visualization schema for CPU, disk, and GPU telemetry.
2026-08-13 15:50:46 +02:00
alex 751432fbb1 feat(hypr): launch workspace applications on startup
Start Obsidian on workspace 8, Thunderbird on workspace 9, and Spotify in the scratchpad.
2026-08-13 15:50:46 +02:00
alex fd50b7deb9 feat(noctalia): add knowledge lookup panel
Provide a portable, cache-ignored Obsidian vault lookup plugin and bind it to Super+N.
2026-08-13 15:50:46 +02:00
alex ba75f3c7f7 feat(hypr): use Kitty as the default terminal 2026-08-13 15:50:46 +02:00
alex 03fbcb735a feat(ranger): use Kitty image previews 2026-08-13 15:50:46 +02:00
alex 04a89118bc fix(noctalia): suspend laptops only on battery
Require UPower to confirm battery operation before the idle timeout locks and suspends the laptop.
2026-08-13 15:45:58 +02:00
alex f9007e3225 fix(pi-status): migrate renamed bridge paths
Resolve the bridge and client from the npm installation while keeping UI launches behind the Wayland compatibility wrapper.
2026-08-13 15:45:43 +02:00
alex b14290fffe extensions from laptop 2026-08-03 21:57:14 +02:00
alex b93b27e43a feat(hypr): add non-locking idle blanker
Blank all displays after five minutes without using DPMS or locking the session.
2026-08-03 17:54:53 +02:00
alex a91c8cf8f2 feat(pi-status): integrate desktop status bridge
Expose the persistent Pi session controls through the desktop launcher, Noctalia, and user service.
2026-08-03 17:54:53 +02:00
alex b220b21509 extensions from laptop 2026-08-03 17:47:58 +02:00
alex a52cc3a427 extensions from laptop 2026-08-03 16:58:38 +02:00
alex 67004150dc feat(noctalia): add portable status widgets and idle monitoring 2026-08-03 16:57:19 +02:00
alex ba9c351589 extensions from laptop 2026-07-28 22:02:38 +02:00
alex a63724d4f1 Merge branch 'main' of ssh://git.commumedia.org:2222/alex/dotfiles 2026-07-28 21:13:22 +02:00
alex 00947d2410 extensions from laptop 2026-07-28 21:13:13 +02:00
alex b5ce26f244 docs: refresh README 2026-07-27 15:24:01 +02:00
alex 793a16b2fc modularization of noctalia config 2026-07-21 20:52:52 +02:00
alex 1e2f869bf3 laptop fixes 2026-07-21 20:29:28 +02:00
alex 7e7c62ec42 hyprland workspace back and forth fix 2026-07-21 15:37:02 +02:00
alex e6d69e1e45 Merge branch 'main' of gitlab.com:blankinator/dotfiles 2026-07-21 15:36:45 +02:00
alex 514e284b16 tmux clipboard fix 2026-07-21 15:36:22 +02:00
alex efa96077a5 Merge branch 'main' of gitlab.com:blankinator/dotfiles 2026-07-21 15:30:14 +02:00
alex 734906d50e laptop changes 2026-07-21 15:30:05 +02:00
alex 47ffa48678 fix(polybar): harden public ip lookup 2026-07-21 15:17:07 +02:00
alex e7c33b6411 chore(pi): update codex model defaults 2026-07-21 15:17:06 +02:00
alex 26cb98338a fix(shell): prevent repeated pyenv initialization 2026-07-21 15:17:06 +02:00
alex 89e9b93222 fix(waybar): repair mute and update widgets 2026-07-21 15:17:06 +02:00
alex f04bde4ec5 feat(desktop): align terminal workflow with ember oled 2026-07-21 15:17:05 +02:00
45 changed files with 3095 additions and 855 deletions
@@ -0,0 +1,3 @@
# Used by the Noctalia Pi Status Bridge adapter and the Hyprland launcher.
PI_STATUS_BRIDGE_CLIENT=/home/alex/.npm-global/bin/pi-status-bridge-client
PI_STATUS_UI_BINARY=/home/alex/.local/bin/pi-status-ui
+13
View File
@@ -29,3 +29,16 @@ end
# bun # bun
set --export BUN_INSTALL "$HOME/.bun" set --export BUN_INSTALL "$HOME/.bun"
set --export PATH $BUN_INSTALL/bin $PATH set --export PATH $BUN_INSTALL/bin $PATH
# opencode
fish_add_path /home/alex/.opencode/bin
# Yazi wrapper that keeps the shell in its selected directory.
function y
set tmp (mktemp -t "yazi-cwd.XXXXXX")
command yazi $argv --cwd-file="$tmp"
if read -z cwd <"$tmp"; and [ "$cwd" != "$PWD" ]; and test -d "$cwd"
builtin cd -- "$cwd"
end
command rm -f -- "$tmp"
end
+6
View File
@@ -0,0 +1,6 @@
# Non-locking Wayland screen blanker. It covers every monitor in black and
# dismisses itself on any key press or mouse click; it never changes DPMS.
listener {
timeout = 300
on-timeout = /home/alex/.local/bin/wayshell-blanker
}
+41 -12
View File
@@ -10,16 +10,21 @@ for _, monitor in ipairs(machine.monitors or {}) do
end end
local main_mod = "SUPER" local main_mod = "SUPER"
local terminal = "alacritty" local terminal = "kitty"
hl.config({ local input = {
input = {
kb_layout = "us", kb_layout = "us",
kb_variant = "intl", kb_variant = "intl",
numlock_by_default = true, numlock_by_default = true,
}, }
for key, value in pairs(machine.input or {}) do
input[key] = value
end
hl.config({
input = input,
general = { general = {
gaps_in = 8, gaps_in = 4,
-- Keep a thin black perimeter so the bottom border remains on-screen. -- Keep a thin black perimeter so the bottom border remains on-screen.
gaps_out = 2, gaps_out = 2,
border_size = 2, border_size = 2,
@@ -35,16 +40,36 @@ hl.config({
animations = { animations = {
enabled = false, enabled = false,
}, },
binds = {
-- Selecting the current workspace again returns to the last workspace.
workspace_back_and_forth = true,
},
})
-- Three-finger horizontal swipes change workspaces; touchscreens remain excluded
-- by Hyprland's default workspace_swipe_touch = false behavior.
hl.gesture({
fingers = 3,
direction = "horizontal",
action = "workspace",
})
-- Dynamically hide the border when a workspace has exactly one tiled app.
hl.workspace_rule({
workspace = "w[t1]",
no_border = true,
}) })
-- Application and session bindings. -- Application and session bindings.
local noctalia_ipc = "noctalia msg " local noctalia_ipc = "noctalia msg "
local pi_status_ui_toggle = 'ui="${PI_STATUS_UI_BINARY:-$HOME/.local/bin/pi-status-ui}"; if test -x "$ui"; then setsid -f env TMPDIR=/tmp "$ui" --toggle >/tmp/pi-status-ui.log 2>&1; fi'
-- Tap and release Super on its own to open Noctalia's control-center home. -- Tap and release Super on its own to open Noctalia's control-center home.
hl.bind(main_mod .. " + SUPER_L", hl.dsp.exec_cmd(noctalia_ipc .. "panel-toggle control-center"), { release = true }) hl.bind(main_mod .. " + SUPER_L", hl.dsp.exec_cmd(noctalia_ipc .. "panel-toggle control-center"), { release = true })
hl.bind(main_mod .. " + RETURN", hl.dsp.exec_cmd(terminal)) hl.bind(main_mod .. " + RETURN", hl.dsp.exec_cmd(terminal))
hl.bind(main_mod .. " + CTRL + RETURN", hl.dsp.exec_cmd(terminal .. " -e zellij")) hl.bind(main_mod .. " + CTRL + RETURN", hl.dsp.exec_cmd(terminal .. " -e zellij"))
hl.bind(main_mod .. " + SHIFT + RETURN", hl.dsp.exec_cmd("rofi -show combi")) hl.bind(main_mod .. " + SHIFT + RETURN", hl.dsp.exec_cmd("rofi -show combi"))
hl.bind(main_mod .. " + SPACE", hl.dsp.exec_cmd(noctalia_ipc .. "panel-toggle launcher")) hl.bind(main_mod .. " + SPACE", hl.dsp.exec_cmd(pi_status_ui_toggle))
hl.bind(main_mod .. " + N", hl.dsp.exec_cmd(noctalia_ipc .. "panel-toggle alex/knowledge-lookup:panel"))
hl.bind(main_mod .. " + S", hl.dsp.exec_cmd(noctalia_ipc .. "panel-toggle control-center")) hl.bind(main_mod .. " + S", hl.dsp.exec_cmd(noctalia_ipc .. "panel-toggle control-center"))
hl.bind(main_mod .. " + SHIFT + F", hl.dsp.exec_cmd(noctalia_ipc .. "bar-toggle main")) hl.bind(main_mod .. " + SHIFT + F", hl.dsp.exec_cmd(noctalia_ipc .. "bar-toggle main"))
hl.bind(main_mod .. " + SHIFT + R", hl.dsp.exec_cmd("hyprctl reload")) hl.bind(main_mod .. " + SHIFT + R", hl.dsp.exec_cmd("hyprctl reload"))
@@ -52,7 +77,7 @@ hl.bind(main_mod .. " + CTRL + L", hl.dsp.exec_cmd("command -v hyprlock >/dev/nu
hl.bind(main_mod .. " + CTRL + SHIFT + ALT + S", hl.dsp.exec_cmd("systemctl suspend")) hl.bind(main_mod .. " + CTRL + SHIFT + ALT + S", hl.dsp.exec_cmd("systemctl suspend"))
hl.bind(main_mod .. " + CTRL + SHIFT + ALT + H", hl.dsp.exec_cmd("systemctl hibernate")) hl.bind(main_mod .. " + CTRL + SHIFT + ALT + H", hl.dsp.exec_cmd("systemctl hibernate"))
hl.bind(main_mod .. " + SHIFT + C", hl.dsp.window.close()) hl.bind(main_mod .. " + SHIFT + C", hl.dsp.window.close())
hl.bind(main_mod .. " + CTRL + SHIFT + F", hl.dsp.exec_cmd("env LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 flameshot gui")) hl.bind(main_mod .. " + CTRL + SHIFT + F", hl.dsp.exec_cmd("/home/alex/.local/bin/screenshot-select"))
-- Audio, media, brightness, and calculator keys. -- Audio, media, brightness, and calculator keys.
hl.bind("XF86AudioLowerVolume", hl.dsp.exec_cmd("wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-"), { locked = true }) hl.bind("XF86AudioLowerVolume", hl.dsp.exec_cmd("wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-"), { locked = true })
@@ -64,10 +89,10 @@ hl.bind("XF86AudioNext", hl.dsp.exec_cmd("playerctl next"), { locked = true })
hl.bind("XF86AudioPrev", hl.dsp.exec_cmd("playerctl previous"), { locked = true }) hl.bind("XF86AudioPrev", hl.dsp.exec_cmd("playerctl previous"), { locked = true })
hl.bind(main_mod .. " + XF86AudioRaiseVolume", hl.dsp.exec_cmd("playerctl next"), { locked = true }) hl.bind(main_mod .. " + XF86AudioRaiseVolume", hl.dsp.exec_cmd("playerctl next"), { locked = true })
hl.bind(main_mod .. " + XF86AudioLowerVolume", hl.dsp.exec_cmd("playerctl previous"), { locked = true }) hl.bind(main_mod .. " + XF86AudioLowerVolume", hl.dsp.exec_cmd("playerctl previous"), { locked = true })
hl.bind("XF86MonBrightnessUp", hl.dsp.exec_cmd('light -A 5 && notify-send "Brightness set to $(light)"'), { locked = true }) hl.bind("XF86MonBrightnessUp", hl.dsp.exec_cmd('light -A 5'), { locked = true })
hl.bind("XF86MonBrightnessDown", hl.dsp.exec_cmd('light -U 5 && notify-send "Brightness set to $(light)"'), { locked = true }) hl.bind("XF86MonBrightnessDown", hl.dsp.exec_cmd('light -U 5'), { locked = true })
hl.bind(main_mod .. " + XF86MonBrightnessDown", hl.dsp.exec_cmd('light -S 0.2 && notify-send "Brightness set to min"'), { locked = true }) hl.bind(main_mod .. " + XF86MonBrightnessDown", hl.dsp.exec_cmd('light -S 0.0'), { locked = true })
hl.bind(main_mod .. " + XF86MonBrightnessUp", hl.dsp.exec_cmd('light -S 100 && notify-send "Brightness set to max"'), { locked = true }) hl.bind(main_mod .. " + XF86MonBrightnessUp", hl.dsp.exec_cmd('light -S 100'), { locked = true })
hl.bind("XF86Calculator", hl.dsp.exec_cmd("qalculate-qt"), { locked = true }) hl.bind("XF86Calculator", hl.dsp.exec_cmd("qalculate-qt"), { locked = true })
-- Retain i3's hjkl and arrow-key directional workflow. -- Retain i3's hjkl and arrow-key directional workflow.
@@ -88,7 +113,9 @@ for workspace = 1, 9 do
hl.bind(main_mod .. " + SHIFT + " .. key, hl.dsp.window.move({ workspace = workspace })) hl.bind(main_mod .. " + SHIFT + " .. key, hl.dsp.window.move({ workspace = workspace }))
hl.bind(main_mod .. " + CTRL + " .. key, hl.dsp.window.move({ workspace = workspace, follow = true })) hl.bind(main_mod .. " + CTRL + " .. key, hl.dsp.window.move({ workspace = workspace, follow = true }))
end end
-- Hyprland has no direct i3-style back_and_forth dispatcher; use numbered workspaces. -- Match i3's back-and-forth workflow: Super+Tab switches to the last workspace;
-- selecting the current numbered workspace also returns to it.
hl.bind(main_mod .. " + TAB", hl.dsp.focus({ workspace = "previous" }))
hl.bind(main_mod .. " + CTRL + RIGHT", hl.dsp.focus({ workspace = "+1" })) hl.bind(main_mod .. " + CTRL + RIGHT", hl.dsp.focus({ workspace = "+1" }))
hl.bind(main_mod .. " + CTRL + LEFT", hl.dsp.focus({ workspace = "-1" })) hl.bind(main_mod .. " + CTRL + LEFT", hl.dsp.focus({ workspace = "-1" }))
@@ -112,7 +139,9 @@ local function float_rule(name, match, border_size)
}) })
end end
hl.window_rule({ name = "obsidian-workspace", match = { class = "^obsidian$" }, workspace = "8" })
hl.window_rule({ name = "thunderbird-workspace", match = { class = "^Thunderbird$" }, workspace = "9" }) hl.window_rule({ name = "thunderbird-workspace", match = { class = "^Thunderbird$" }, workspace = "9" })
float_rule("pi-status-ui", { class = "^(pi-status-ui|Pi-status-ui)$" }, 1)
float_rule("alsamixer", { title = "^alsamixer$" }, 1) float_rule("alsamixer", { title = "^alsamixer$" }, 1)
float_rule("qalculate", { title = "^Qalculate!$" }) float_rule("qalculate", { title = "^Qalculate!$" })
float_rule("psensor", { class = "^Psensor$" }) float_rule("psensor", { class = "^Psensor$" })
+45
View File
@@ -0,0 +1,45 @@
# Minimal Hyprlock screen using the current desktop as a blurred backdrop.
$font = Sans
general {
hide_cursor = true
}
background {
monitor =
path = screenshot
blur_passes = 3
}
input-field {
monitor =
size = 20%, 5%
outline_thickness = 2
inner_color = rgba(0, 0, 0, 0.25)
outer_color = rgba(eeeeeecc)
check_color = rgba(8ec07ccc)
fail_color = rgba(fb4934cc)
font_color = rgb(eeeeee)
fade_on_empty = false
rounding = 12
font_family = $font
placeholder_text = Input password...
fail_text = $PAMFAIL
position = 0, -20
halign = center
valign = center
}
label {
monitor =
text = $TIME
font_size = 72
font_family = $font
position = 0, 120
halign = center
valign = center
}
@@ -10,8 +10,10 @@ return {
}, },
startup = { startup = {
"noctalia", "noctalia",
"hypridle",
-- Electron's tray icon registers only when the SNI watcher already exists. -- Electron's tray icon registers only when the SNI watcher already exists.
"sh -c 'sleep 2; exec mullvad-vpn'", "sh -c 'sleep 2; exec mullvad-vpn'",
"obsidian",
"thunderbird", "thunderbird",
"nextcloud", "nextcloud",
"spotify", "spotify",
@@ -1,11 +1,30 @@
-- Laptop profile: leave monitor selection to Hyprland's hotplug support. -- Laptop profile: leave monitor selection to Hyprland's hotplug support.
-- Noctalia replaces the i3-era Redshift, Polybar, Blueman Applet, and Picom stack. -- Noctalia replaces the i3-era Redshift, Polybar, Blueman Applet, and Picom stack.
return { return {
input = {
touchpad = {
-- Standard laptop behavior: one/two/three-finger taps map to
-- left/right/middle click, and a second tap held down starts a drag.
tap_to_click = true,
tap_button_map = "lrm",
tap_and_drag = true,
drag_lock = false,
clickfinger_behavior = true,
disable_while_typing = true,
-- Preserve natural two-finger scrolling at the default sensitivity.
natural_scroll = true,
scroll_factor = 1.0,
},
},
monitors = {}, monitors = {},
startup = { startup = {
"noctalia", "noctalia",
-- Start after Noctalia has claimed the StatusNotifier watcher. -- Start after Noctalia has claimed the StatusNotifier watcher.
"sh -c 'sleep 2; exec mullvad-vpn'", "sh -c 'sleep 2; exec mullvad-vpn'",
"obsidian",
"thunderbird",
"nextcloud", "nextcloud",
"spotify",
}, },
} }
+72
View File
@@ -5,8 +5,80 @@ offline_mode = true
external_ip_enabled = false external_ip_enabled = false
telemetry_enabled = false telemetry_enabled = false
# Noctalia owns idle handling so ScreenSaver/Wayland idle inhibitors (such as
# browser video playback) prevent these actions. Power displays down after five
# minutes, lock after ten minutes, then suspend-then-hibernate after thirty.
[idle]
behavior_order = ["lock", "screen-off", "suspend-then-hibernate"]
pre_action_fade_seconds = 2.0
[idle.behavior.lock]
action = "lock"
enabled = true
timeout = 600
[idle.behavior.screen-off]
action = "screen_off"
enabled = true
timeout = 300
[idle.behavior.suspend-then-hibernate]
action = "command"
command = "loginctl lock-session && systemctl suspend-then-hibernate"
enabled = true
timeout = 1800
[theme] [theme]
mode = "dark" mode = "dark"
source = "custom" source = "custom"
custom_palette = "Ember OLED" custom_palette = "Ember OLED"
pure_black_dark = true pure_black_dark = true
# Noctalia merges files alphabetically. Keep reusable modules here; machine-specific
# layout and hardware settings live in 90-machine.toml, while 99-extend.toml is local.
[plugins]
enabled = ["alex/system-status", "alex/knowledge-lookup"]
[widget.cpu]
type = "alex/system-status:cpu"
[widget.ram]
type = "alex/system-status:memory"
[widget.network-rate]
type = "alex/system-status:network-rate"
[widget.idle-inhibitors]
type = "alex/system-status:inhibitors"
[widget.knowledge-lookup]
type = "alex/knowledge-lookup:bar"
[widget.temp]
type = "sysmon"
stat = "cpu_temp"
visualization = "none"
show_value = true
[widget.disk]
type = "sysmon"
stat = "disk_used_pct"
visualization = "none"
show_value = true
path = "/"
[desktop_widgets]
enabled = false
[wallpaper]
enabled = true
fill_color = "#000000"
[wallpaper.default]
path = "color:#000000"
[nightlight]
enabled = true
force = false
temperature_day = 6500
temperature_night = 2500
@@ -1,20 +1,29 @@
# Desktop-specific shell layout and hardware telemetry. # Desktop-specific shell layout and hardware telemetry.
# Pi Status Bridge is installed locally from ~/projects/pi-status-bridge.
[plugins]
enabled = ["alex/system-status", "alex/pi-status-bridge", "alex/knowledge-lookup"]
[widget.pi-status]
type = "alex/pi-status-bridge:bridge"
[bar.main] [bar.main]
position = "bottom" position = "bottom"
layer = "overlay" layer = "top"
scale = 0.95
capsule_radius = 5 capsule_radius = 5
center = ["group:g3", "notifications"] center = ["group:g3", "notifications"]
end = ["bluetooth", "volume", "control-center", "group:g1", "session"] end = ["group:g2", "group:g6", "group:g1", "session"]
font_weight = 400 font_weight = 400
margin_ends = 0 margin_ends = 0
padding = 0 padding = 0
panel_overlap = 3 panel_overlap = 3
radius = 0 radius = 0
shadow = false shadow = false
smart_auto_hide = true # Keep the desktop status bar visible and reserve its screen edge.
# The bar may overlay the thin perimeter while visible; hidden bars release the space. auto_hide = false
reserve_space = false smart_auto_hide = false
start = ["tray", "group:g4", "group:g2", "privacy"] reserve_space = true
start = ["tray", "group:g4", "privacy"]
thickness = 39 thickness = 39
widget_spacing = 10 widget_spacing = 10
@@ -22,7 +31,7 @@ widget_spacing = 10
enabled = true enabled = true
fill = "surface_variant" fill = "surface_variant"
id = "g1" id = "g1"
members = ["caffeine", "date", "clock"] members = ["idle-inhibitors", "caffeine", "date", "clock"]
opacity = 1.0 opacity = 1.0
padding = 6.0 padding = 6.0
@@ -30,7 +39,7 @@ padding = 6.0
enabled = true enabled = true
fill = "surface_variant" fill = "surface_variant"
id = "g2" id = "g2"
members = ["network_rx", "network_tx", "network"] members = ["network", "bluetooth"]
opacity = 1.0 opacity = 1.0
padding = 6.0 padding = 6.0
@@ -47,64 +56,36 @@ radius = 5.0
enabled = true enabled = true
fill = "surface_variant" fill = "surface_variant"
id = "g4" id = "g4"
members = ["cpu", "temp", "ram", "gpu", "gpu_temp", "disk", "battery"] members = ["pi-status", "cpu", "temp", "ram", "gpu", "gpu_temp", "disk"]
opacity = 1.0 opacity = 1.0
padding = 6.0 padding = 6.0
[widget.cpu] [[bar.main.capsule_group]]
type = "alex/system-status:cpu" enabled = true
fill = "surface_variant"
[widget.ram] id = "g6"
type = "alex/system-status:memory" members = ["input_volume", "volume"]
opacity = 1.0
[widget.temp] padding = 6.0
type = "sysmon" radius = 5.0
stat = "cpu_temp"
display = "text"
[widget.gpu] [widget.gpu]
type = "sysmon" type = "sysmon"
stat = "gpu_usage" stat = "gpu_usage"
display = "graph" visualization = "graph"
show_value = true
[widget.gpu_temp] [widget.gpu_temp]
type = "sysmon" type = "sysmon"
stat = "gpu_temp" stat = "gpu_temp"
display = "text" visualization = "none"
show_value = true
[widget.disk]
type = "sysmon"
stat = "disk_pct"
display = "text"
path = "/"
[battery.device."/org/freedesktop/UPower/devices/battery_hidpp_battery_0"]
warning_threshold = 20
[control_center] [control_center]
sidebar = "full" sidebar = "full"
sidebar_section = "full" sidebar_section = "full"
width = 880 width = 880
[desktop_widgets]
enabled = false
[wallpaper]
enabled = true
fill_color = "#000000"
[wallpaper.default]
path = "color:#000000"
[system.monitor] [system.monitor]
# GPU polling is disabled by Noctalia by default; enable it for the RTX 3090 widgets. # GPU polling is disabled by Noctalia by default; enable it for the RTX 3090 widgets.
gpu_poll_seconds = 2.0 gpu_poll_seconds = 2.0
[plugins]
enabled = ["alex/system-status"]
[nightlight]
enabled = true
force = false
temperature_day = 6500
temperature_night = 2500
@@ -1,10 +1,11 @@
# Laptop-specific Noctalia layout. Shared privacy/theme defaults live in 10-base.toml. # Laptop-specific Noctalia layout. Shared privacy/theme defaults live in 10-base.toml.
[bar.main] [bar.main]
position = "top" position = "top"
layer = "overlay" layer = "top"
scale = 0.95
capsule_radius = 5 capsule_radius = 5
center = ["group:g3", "notifications"] center = ["group:g3", "notifications"]
end = ["bluetooth", "volume", "control-center", "group:g1", "session"] end = ["group:g2", "group:g6", "group:g5", "group:g1", "session"]
font_weight = 400 font_weight = 400
margin_ends = 0 margin_ends = 0
padding = 0 padding = 0
@@ -14,7 +15,8 @@ shadow = false
auto_hide = false auto_hide = false
smart_auto_hide = false smart_auto_hide = false
reserve_space = true reserve_space = true
start = ["tray", "group:g4", "group:g2", "privacy"] start = ["tray", "group:g4"]
# Accommodate the battery-rate widget's wattage + ETA lines without clipping.
thickness = 39 thickness = 39
widget_spacing = 10 widget_spacing = 10
@@ -22,7 +24,7 @@ widget_spacing = 10
enabled = true enabled = true
fill = "surface_variant" fill = "surface_variant"
id = "g1" id = "g1"
members = ["caffeine", "date", "clock"] members = ["idle-inhibitors", "caffeine", "date", "clock"]
opacity = 1.0 opacity = 1.0
padding = 6.0 padding = 6.0
@@ -30,7 +32,7 @@ padding = 6.0
enabled = true enabled = true
fill = "surface_variant" fill = "surface_variant"
id = "g2" id = "g2"
members = ["network_rx", "network_tx", "network"] members = ["network", "bluetooth"]
opacity = 1.0 opacity = 1.0
padding = 6.0 padding = 6.0
@@ -47,47 +49,40 @@ radius = 5.0
enabled = true enabled = true
fill = "surface_variant" fill = "surface_variant"
id = "g4" id = "g4"
members = ["cpu", "temp", "ram", "disk", "battery"] members = ["cpu", "temp", "ram", "gpu-mode", "disk"]
opacity = 1.0 opacity = 1.0
padding = 6.0 padding = 6.0
[widget.cpu] [[bar.main.capsule_group]]
type = "alex/system-status:cpu" enabled = true
fill = "surface_variant"
id = "g5"
members = ["battery-rate", "battery", "power_profile"]
opacity = 1.0
padding = 6.0
radius = 5.0
[widget.ram] [[bar.main.capsule_group]]
type = "alex/system-status:memory" enabled = true
fill = "surface_variant"
id = "g6"
members = ["input_volume", "volume"]
opacity = 1.0
padding = 6.0
radius = 5.0
[widget.temp] [widget.battery-rate]
type = "sysmon" type = "alex/system-status:battery-rate"
stat = "cpu_temp"
display = "text"
[widget.disk] [widget.gpu-mode]
type = "sysmon" type = "alex/system-status:gpu-mode"
stat = "disk_pct"
display = "text" # Only suspend-then-hibernate after 30 minutes when UPower confirms battery power.
path = "/" # The command fails closed on AC or when UPower is unavailable.
[idle.behavior.suspend-then-hibernate]
command = "busctl get-property org.freedesktop.UPower /org/freedesktop/UPower org.freedesktop.UPower OnBattery 2>/dev/null | grep -qx 'b true' && loginctl lock-session && systemctl suspend-then-hibernate"
[control_center] [control_center]
sidebar = "full" sidebar = "full"
sidebar_section = "full" sidebar_section = "full"
width = 720 width = 720
[desktop_widgets]
enabled = false
[wallpaper]
enabled = true
fill_color = "#000000"
[wallpaper.default]
path = "color:#000000"
[plugins]
enabled = ["alex/system-status"]
[nightlight]
enabled = true
force = false
temperature_day = 6500
temperature_night = 2500
+1 -1
View File
@@ -12,7 +12,7 @@
[module/public_ip] [module/public_ip]
type = custom/text type = custom/text
content= content=
click-left = curl -s ifconfig.co | while read OUTPUT; do notify-send "$OUTPUT"; done & curl -s ifconfig.co | xargs -I % sh -c "curl -s ipinfo.io/%/city" | while read OUTPUT; do notify-send "$OUTPUT"; done click-left = ip="$(curl -fsS --connect-timeout 3 --max-time 5 https://ifconfig.co/ip)" || exit; case "$ip" in ''|*[!0-9A-Fa-f.:]*) exit 1 ;; esac; notify-send "$ip"; city="$(curl -fsS --connect-timeout 3 --max-time 5 "https://ipinfo.io/${ip}/city")" || exit; notify-send "$city"
[module/mic_status] [module/mic_status]
type = custom/script type = custom/script
+2 -2
View File
@@ -70,7 +70,7 @@ set vcs_backend_svn disabled
# Truncate the long commit messages to this length when shown in the statusbar. # Truncate the long commit messages to this length when shown in the statusbar.
set vcs_msg_length 50 set vcs_msg_length 50
# Use one of the supported image preview protocols # Render image previews through Kitty's native graphics protocol.
set preview_images true set preview_images true
# Set the preview image method. Supported methods: # Set the preview image method. Supported methods:
@@ -114,7 +114,7 @@ set preview_images true
# Preview images in full color with the external command "ueberzug". # Preview images in full color with the external command "ueberzug".
# Images are shown by using a child window. # Images are shown by using a child window.
# Only for users who run X11 in GNU/Linux. # Only for users who run X11 in GNU/Linux.
set preview_images_method ueberzug set preview_images_method kitty
# Delay in seconds before displaying an image with the w3m method. # Delay in seconds before displaying an image with the w3m method.
# Increase it in case of experiencing display corruption. # Increase it in case of experiencing display corruption.
@@ -0,0 +1 @@
../idle-inhibitor-monitor.service
@@ -0,0 +1,12 @@
[Unit]
Description=Track session ScreenSaver idle inhibitors for Noctalia
After=graphical-session.target
PartOf=graphical-session.target
[Service]
ExecStart=%h/.local/bin/idle-inhibitor-monitor
Restart=on-failure
RestartSec=2
[Install]
WantedBy=default.target
@@ -0,0 +1,13 @@
[Unit]
Description=Pi Status Bridge home agent
After=graphical-session.target
[Service]
Type=simple
Environment=PATH=/home/alex/.npm-global/bin:/usr/local/bin:/usr/bin:/bin
ExecStart=/usr/bin/env pi-status-bridge --session-root %h/.local/state/pi-status-bridge/sessions
Restart=on-failure
RestartSec=3
[Install]
WantedBy=default.target
+4 -4
View File
@@ -33,10 +33,10 @@
"interval": 1, "interval": 1,
"tooltip-format": "\n<big>{:%Y %B}</big>\n<tt><small>{calendar}</small></tt>", "tooltip-format": "\n<big>{:%Y %B}</big>\n<tt><small>{calendar}</small></tt>",
"calendar-weeks-pos": "right", "calendar-weeks-pos": "right",
"today-format": "<span color='#7645AD'><b><u>{}</u></b></span>", "today-format": "<span color='#ff8800'><b><u>{}</u></b></span>",
"format-calendar": "<span color='#aeaeae'><b>{}</b></span>", "format-calendar": "<span color='#f4c7a1'><b>{}</b></span>",
"format-calendar-weeks": "<span color='#aeaeae'><b>W{:%V}</b></span>", "format-calendar-weeks": "<span color='#f4c7a1'><b>W{:%V}</b></span>",
"format-calendar-weekdays": "<span color='#aeaeae'><b>{}</b></span>" "format-calendar-weekdays": "<span color='#f4c7a1'><b>{}</b></span>"
}, },
"disk": { "disk": {
+3 -10
View File
@@ -1,11 +1,4 @@
#!/bin/bash #!/usr/bin/env bash
# Check the current state of the speaker
state=$(amixer -D pulse sget Master | awk '/\[on\]/{print "unmute"; exit} /\[off\]/{print "mute"; exit}')
# Toggle the state of the speaker
#
if [ "$state" = "[on] "]; then
amixer
# amixer -D pulse sset Master "$state" > /dev/null
# Toggle the default PulseAudio sink's mute state.
amixer -D pulse sset Master toggle >/dev/null
+1 -1
View File
@@ -21,4 +21,4 @@ if [ "$updates" -gt 0 ]; then
echo " ($(format $updates_arch)/$(format $updates_aur))" echo " ($(format $updates_arch)/$(format $updates_aur))"
else else
echo echo
fia fi
+30 -29
View File
@@ -1,22 +1,14 @@
/* Noctalia Ember OLED: near-black surfaces, warm amber focus. */
* { * {
font-family: "JetBrainsMono Nerd Font,JetBrainsMono NF"; font-family: "JetBrainsMono Nerd Font,JetBrainsMono NF";
color: white; color: #fff1e6;
}
window#waybar {
background: black;
}
.modules-right {
background: #000000;
}
.modules-center {
background: #000000;
} }
window#waybar,
.modules-right,
.modules-center,
.modules-left { .modules-left {
background: #000000; background: #050403;
} }
#battery, #battery,
@@ -41,40 +33,49 @@ window#waybar {
min-width: 1ex; min-width: 1ex;
padding-right: 5px; padding-right: 5px;
} }
#tray menu * { #tray menu * {
color: black; color: #050403;
}
#cpu {
/* "100% " fits in this box; fixed width prevents sibling modules shifting. */
min-width: 7ch;
max-width: 7ch;
text-align: right;
} }
#temperature.critical, #temperature.critical,
#pulseaudio { #pulseaudio {
min-width: 5ex; min-width: 5ex;
} }
#pulseaudio.muted {
color: #FF0000; #pulseaudio.muted,
}
#pulseaudio.mic.muted {
color: #ffffff;
}
#pulseaudio.mic:not(.source-muted) { #pulseaudio.mic:not(.source-muted) {
color: #fc0202; color: #ff6b4a;
} }
#pulseaudio.mic.muted {
color: #6e4a31;
}
#clock { #clock {
color: #f9780e; color: #ff8800;
} }
#battery.charging { #battery.charging {
color: #ffffff; color: #1a0d00;
background-color: #26A65B; background-color: #c98a45;
} }
#battery.warning:not(.charging) { #battery.warning:not(.charging) {
background-color: #ffbe61; background-color: #ffb05c;
color: black; color: #2b1600;
} }
#battery.critical:not(.charging) { #battery.critical:not(.charging) {
background-color: #f53c3c; background-color: #ff6b4a;
color: #ffffff; color: #2b0700;
animation-name: blink; animation-name: blink;
animation-duration: 0.5s; animation-duration: 0.5s;
animation-timing-function: linear; animation-timing-function: linear;
+189
View File
@@ -0,0 +1,189 @@
#!/usr/bin/env python3
"""Track session ScreenSaver inhibitors and expose combined idle blockers as TSV.
logind exposes idle inhibitors through systemd-inhibit. Browser video wake locks
use org.freedesktop.ScreenSaver instead, which has no list API, so this monitor
observes Inhibit/UnInhibit calls and keeps their active state in a small JSON
file for the Noctalia widget and panel.
"""
from __future__ import annotations
import argparse
import json
import os
import re
import signal
import subprocess
import sys
from pathlib import Path
from tempfile import NamedTemporaryFile
STATE_FILE = Path.home() / ".local/state/noctalia/idle-inhibitors.json"
SCREEN_SAVER = "org.freedesktop.ScreenSaver"
def write_state(inhibitors: dict[tuple[str, int], dict[str, str]]) -> None:
STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
payload = {"inhibitors": list(inhibitors.values())}
with NamedTemporaryFile("w", dir=STATE_FILE.parent, delete=False) as file:
json.dump(payload, file, separators=(",", ":"))
file.write("\n")
temporary_name = file.name
os.replace(temporary_name, STATE_FILE)
def read_screensaver_inhibitors() -> list[dict[str, str]]:
try:
payload = json.loads(STATE_FILE.read_text())
except (FileNotFoundError, json.JSONDecodeError):
return []
inhibitors = payload.get("inhibitors", [])
return [item for item in inhibitors if isinstance(item, dict)]
def logind_inhibitors() -> list[dict[str, str]]:
result = subprocess.run(
["systemd-inhibit", "--list", "--no-pager"],
check=False,
capture_output=True,
text=True,
)
if result.returncode != 0:
return []
inhibitors = []
for line in result.stdout.splitlines()[1:]:
fields = line.split(maxsplit=6)
if len(fields) < 7 or "idle" not in fields[5].split(":"):
continue
reason_and_mode = fields[6].rsplit(maxsplit=1)
inhibitors.append(
{
"source": "logind",
"who": fields[0],
"user": fields[2],
"pid": fields[3],
"comm": fields[4],
"mode": reason_and_mode[-1],
"why": reason_and_mode[0] if len(reason_and_mode) == 2 else "No reason supplied",
}
)
return inhibitors
def combined_inhibitors() -> list[dict[str, str]]:
return logind_inhibitors() + read_screensaver_inhibitors()
def print_inhibitors() -> None:
for inhibitor in combined_inhibitors():
values = [
inhibitor.get("source", "screensaver"),
inhibitor.get("who", ""),
inhibitor.get("user", ""),
inhibitor.get("pid", ""),
inhibitor.get("comm", ""),
inhibitor.get("mode", ""),
inhibitor.get("why", ""),
]
print("\t".join(value.replace("\t", " ").replace("\n", " ") for value in values))
def monitor() -> None:
active: dict[tuple[str, int], dict[str, str]] = {}
pending: dict[tuple[str, int], dict[str, str]] = {}
write_state(active)
process = subprocess.Popen(
["stdbuf", "-oL", "busctl", "--user", "--json=short", "monitor"],
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
text=True,
bufsize=1,
)
def stop(_signal: int, _frame: object) -> None:
process.terminate()
write_state({})
raise SystemExit(0)
signal.signal(signal.SIGTERM, stop)
signal.signal(signal.SIGINT, stop)
assert process.stdout is not None
for line in process.stdout:
try:
message = json.loads(line)
except json.JSONDecodeError:
continue
message_type = message.get("type")
sender = message.get("sender", "")
payload = message.get("payload", {}).get("data", [])
if (
message_type == "method_call"
and message.get("interface") == SCREEN_SAVER
and message.get("member") == "Inhibit"
and len(payload) >= 2
):
pending[(sender, int(message.get("cookie", 0)))] = {
"source": "screensaver",
"who": str(payload[0]),
"user": "session",
"pid": "",
"comm": str(payload[0]),
"mode": "block",
"why": str(payload[1]),
}
elif (
message_type == "method_return"
and payload
and isinstance(payload[0], int)
):
request = (message.get("destination", ""), int(message.get("reply_cookie", 0)))
inhibitor = pending.pop(request, None)
if inhibitor is not None:
active[(request[0], int(payload[0]))] = inhibitor
write_state(active)
elif (
message_type == "method_call"
and message.get("interface") == SCREEN_SAVER
and message.get("member") == "UnInhibit"
and payload
and isinstance(payload[0], int)
):
cookie = int(payload[0])
active.pop((sender, cookie), None)
# Some clients reconnect before releasing a cookie; the cookie is
# globally unique for Noctalia, so clean up that fallback as well.
for key in [key for key in active if key[1] == cookie]:
active.pop(key, None)
write_state(active)
elif (
message_type == "signal"
and message.get("interface") == "org.freedesktop.DBus"
and message.get("member") == "NameOwnerChanged"
and len(payload) == 3
and payload[2] == ""
):
vanished = str(payload[0])
for key in [key for key in active if key[0] == vanished]:
active.pop(key, None)
for key in [key for key in pending if key[0] == vanished]:
pending.pop(key, None)
write_state(active)
parser = argparse.ArgumentParser()
parser.add_argument("--count", action="store_true", help="print combined inhibitor count")
parser.add_argument("--list", action="store_true", help="print combined inhibitors as TSV")
arguments = parser.parse_args()
if arguments.count:
print(len(combined_inhibitors()))
elif arguments.list:
print_inhibitors()
else:
monitor()
+4
View File
@@ -0,0 +1,4 @@
#!/bin/sh
# The opaque release window runs natively on Wayland; disable WebKit DMABUF,
# which is unreliable with this NVIDIA desktop's compositor path.
exec env WEBKIT_DISABLE_DMABUF_RENDERER=1 "$HOME/projects/pi-status-bridge/ui/src-tauri/target/release/pi-status-ui" "$@"
@@ -0,0 +1,4 @@
__pycache__/
.ruff_cache/
index-cache.json
index-cache-*.json
@@ -0,0 +1,75 @@
#!/usr/bin/env python3
"""Safely create one direct child in the configured knowledge vault."""
from __future__ import annotations
import json
import sys
from pathlib import Path
def fail(message: str) -> int:
print(message, file=sys.stderr)
return 1
def child_name(raw: str) -> str | None:
name = raw.strip()
if not name or name in {".", ".."} or "/" in name or "\\" in name or "\0" in name:
return None
if name.startswith("."):
return None
return name
def main() -> int:
if len(sys.argv) != 5:
return fail("usage: create-entry.py VAULT KIND CURRENT_DIRECTORY NAME")
raw_vault, kind, current_directory, raw_name = sys.argv[1:]
vault = Path(raw_vault).expanduser().resolve()
if not vault.is_dir():
return fail(f"vault does not exist: {vault}")
if kind not in {"note", "directory"}:
return fail("kind must be note or directory")
current = (vault / current_directory).resolve()
if current != vault and vault not in current.parents:
return fail("current directory escaped the vault")
if not current.is_dir():
return fail("current directory no longer exists")
name = child_name(raw_name)
if name is None:
return fail("use a non-hidden direct name without slashes")
if kind == "note" and not name.lower().endswith(".md"):
name += ".md"
target = (current / name).resolve()
if target.parent != current:
return fail("target escaped the current directory")
try:
if kind == "directory":
target.mkdir()
else:
title = target.stem
with target.open("x", encoding="utf-8") as handle:
handle.write(f"# {title}\n")
except FileExistsError:
return fail(f"already exists: {name}")
except OSError as error:
return fail(f"cannot create {name}: {error}")
relative = target.relative_to(vault).as_posix()
json.dump(
{
"kind": kind,
"relative": relative,
"name": name,
"absolute": str(target),
},
sys.stdout,
)
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,139 @@
#!/usr/bin/env python3
"""Focus the most recently focused Obsidian window under Hyprland."""
from __future__ import annotations
import json
import subprocess
import sys
import time
from pathlib import Path
from urllib.parse import quote, urlencode
OBSIDIAN_CLASS = "md.obsidian.Obsidian"
VAULT_ROOT: Path
OBSIDIAN_VAULT: str
def hyprctl(*arguments: str) -> subprocess.CompletedProcess[str]:
return subprocess.run(
["hyprctl", *arguments],
check=False,
capture_output=True,
text=True,
)
def focus_expression(field: str, value: str) -> str:
escaped = value.replace("\\", "\\\\").replace('"', '\\"')
return f'hl.dispatch(hl.dsp.focus({{ {field} = "{escaped}" }}))'
def note_uri() -> str | None:
if len(sys.argv) != 4:
print("usage: focus-obsidian.py VAULT VAULT_ID NOTE_PATH", file=sys.stderr)
return None
note = Path(sys.argv[3]).resolve()
try:
relative = note.relative_to(VAULT_ROOT).as_posix()
except ValueError as error:
print(f"note escaped the knowledge vault: {error}", file=sys.stderr)
return None
return "obsidian://open?" + urlencode(
{"vault": OBSIDIAN_VAULT, "file": relative}, quote_via=quote
)
def dispatch_uri(uri: str) -> bool:
try:
subprocess.Popen(
["obsidian", uri],
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
start_new_session=True,
)
except OSError as error:
print(f"cannot open Obsidian: {error}", file=sys.stderr)
return False
return True
def main() -> int:
global VAULT_ROOT, OBSIDIAN_VAULT
if len(sys.argv) != 4:
print("usage: focus-obsidian.py VAULT VAULT_ID NOTE_PATH", file=sys.stderr)
return 2
VAULT_ROOT = Path(sys.argv[1]).expanduser().resolve()
OBSIDIAN_VAULT = sys.argv[2]
if not VAULT_ROOT.is_dir():
print(f"vault does not exist: {VAULT_ROOT}", file=sys.stderr)
return 1
if not OBSIDIAN_VAULT:
print("vault is not registered in Obsidian", file=sys.stderr)
return 1
uri = note_uri()
if uri is None or not dispatch_uri(uri):
return 1
target: dict[str, object] | None = None
for _ in range(150):
result = hyprctl("clients", "-j")
if result.returncode == 0:
try:
clients = json.loads(result.stdout)
except json.JSONDecodeError as error:
print(f"cannot parse hyprctl clients: {error}", file=sys.stderr)
clients = []
matches = [
client for client in clients if client.get("class") == OBSIDIAN_CLASS
]
if matches:
target = min(
matches, key=lambda client: client.get("focusHistoryID", 1_000_000)
)
break
time.sleep(0.1)
if target is None:
return 1
workspace = target.get("workspace")
address = target.get("address")
if not isinstance(workspace, dict) or not isinstance(address, str):
return 1
workspace_id = workspace.get("id")
if not isinstance(workspace_id, int):
return 1
workspace_expression = focus_expression("workspace", str(workspace_id))
selector = f"address:{address}"
window_expression = focus_expression("window", selector)
focused_samples = 0
for attempt in range(20):
if attempt == 0 or attempt % 5 == 0:
hyprctl("eval", workspace_expression)
hyprctl("eval", window_expression)
active = hyprctl("activewindow", "-j")
if active.returncode == 0:
try:
active_window = json.loads(active.stdout)
except json.JSONDecodeError as error:
print(f"cannot parse active Hyprland window: {error}", file=sys.stderr)
active_window = {}
focused_samples = (
focused_samples + 1 if active_window.get("address") == address else 0
)
if focused_samples >= 3:
# Re-send after focus: Electron occasionally consumes the URI
# before its renderer is ready, leaving the prior note visible.
if not dispatch_uri(uri):
return 1
time.sleep(0.35)
hyprctl("eval", window_expression)
return 0
time.sleep(0.1)
return 1
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,250 @@
#!/usr/bin/env python3
"""Build bounded JSON cache chunks for a Markdown vault."""
from __future__ import annotations
import json
import os
import re
import sys
import tempfile
import unicodedata
from pathlib import Path
from typing import Any
SKIP_DIRS = {
".git",
".obsidian",
".trash",
".stfolder",
"node_modules",
"dist",
"build",
}
HEADING = re.compile(r"^#\s+(.+?)\s*$", re.MULTILINE)
WORDS = re.compile(r"[^\W_]+", re.UNICODE)
CHUNK_BYTES = 96 * 1024
RECORD_BYTES = 48 * 1024
def visible_directory(name: str) -> bool:
return name not in SKIP_DIRS and not name.startswith(".")
def note_title(path: Path, content: str) -> str:
match = HEADING.search(content)
return match.group(1).strip() if match else path.stem
def folded(value: str) -> str:
"""Case-fold and strip accents so ASCII queries match Unicode notes."""
normalized = unicodedata.normalize("NFKD", value.casefold())
return "".join(
character for character in normalized if not unicodedata.combining(character)
)
def token_buckets(content: str) -> dict[str, dict[str, list[str]]]:
buckets: dict[str, dict[str, list[str]]] = {}
for token in sorted(set(WORDS.findall(content))):
if not token:
continue
by_length = buckets.setdefault(token[0], {})
by_length.setdefault(str(len(token)), []).append(token)
return buckets
def encoded(record: dict[str, Any]) -> str:
return json.dumps(record, ensure_ascii=False, separators=(",", ":"))
def fold_records(characters: set[str]) -> list[dict[str, Any]]:
mapping: dict[str, str] = {}
for character in characters:
variants = {character, character.lower(), character.upper(), character.title()}
for variant in variants:
for variant_character in variant:
mapping[variant_character] = folded(variant_character)
records: list[dict[str, Any]] = []
current: dict[str, str] = {}
for source, target in sorted(mapping.items()):
candidate = {**current, source: target}
record = {"kind": "fold", "map": candidate}
if current and len(encoded(record).encode("utf-8")) > RECORD_BYTES:
records.append({"kind": "fold", "map": current})
current = {source: target}
else:
current = candidate
if current:
records.append({"kind": "fold", "map": current})
return records
def directory_record(relative: str) -> dict[str, Any]:
record = {"kind": "directory", "relative": relative}
if len(encoded(record).encode("utf-8")) > RECORD_BYTES:
raise ValueError(f"directory metadata exceeded bounded size: {relative}")
return record
def note_record(title: str, relative: str) -> dict[str, Any]:
display_title = title[:256] + ("…" if len(title) > 256 else "")
record = {
"kind": "note",
"id": relative,
"title": display_title,
"relative": relative,
"titleRaw": display_title.casefold(),
"pathRaw": relative.casefold(),
"titleSearch": folded(display_title),
"pathSearch": folded(relative),
}
if len(encoded(record).encode("utf-8")) > RECORD_BYTES:
raise ValueError(f"note metadata exceeded bounded size: {relative}")
return record
def content_record(relative: str, fragment: str) -> dict[str, Any]:
content_search = folded(fragment)
return {
"kind": "content",
"id": relative,
"fragment": fragment,
"lines": fragment.split("\n"),
"contentRaw": fragment.casefold(),
"contentSearch": content_search,
"contentTokens": token_buckets(content_search),
}
def content_records(relative: str, content: str) -> list[dict[str, Any]]:
"""Split one note into bounded records without changing its original text."""
pending = [content]
records: list[dict[str, Any]] = []
while pending:
fragment = pending.pop()
record = content_record(relative, fragment)
if len(encoded(record).encode("utf-8")) <= RECORD_BYTES:
records.append(record)
continue
midpoint = max(len(fragment) // 2, 1)
if midpoint >= len(fragment):
raise ValueError(f"cannot split oversized content in {relative}")
pending.extend((fragment[midpoint:], fragment[:midpoint]))
return records
def index_records(root: Path) -> list[dict[str, Any]]:
notes: list[tuple[str, str, str]] = []
indexed_directories: set[str] = set()
characters: set[str] = set()
for current, directories, filenames in os.walk(root):
directories[:] = sorted(
name
for name in directories
if visible_directory(name) and not Path(current, name).is_symlink()
)
for directory in directories:
relative_directory = Path(current, directory).relative_to(root).as_posix()
indexed_directories.add(relative_directory)
characters.update(relative_directory)
for filename in sorted(filenames):
if not filename.lower().endswith(".md") or filename.startswith("."):
continue
path = Path(current, filename)
if path.is_symlink():
continue
try:
content = path.read_text(encoding="utf-8", errors="replace")
except OSError:
continue
relative = path.relative_to(root).as_posix()
title = note_title(path, content)
characters.update(title)
characters.update(relative)
characters.update(content)
notes.append((title, relative, content))
notes.sort(key=lambda note: (note[0].casefold(), note[1].casefold()))
records = fold_records(characters)
for relative in sorted(indexed_directories, key=str.casefold):
records.append(directory_record(relative))
for title, relative, content in notes:
records.append(note_record(title, relative))
records.extend(content_records(relative, content))
return records
def write_chunks(cache: Path, records: list[dict[str, Any]]) -> list[str]:
encoded_records = [encoded(record) for record in records]
chunks: list[list[str]] = []
current: list[str] = []
current_bytes = 2
for record in encoded_records:
record_bytes = len(record.encode("utf-8"))
if record_bytes > RECORD_BYTES:
raise ValueError("index record exceeded its bounded size")
separator_bytes = 1 if current else 0
if current and current_bytes + separator_bytes + record_bytes > CHUNK_BYTES:
chunks.append(current)
current = []
current_bytes = 2
separator_bytes = 0
current.append(record)
current_bytes += separator_bytes + record_bytes
if current or not chunks:
chunks.append(current)
names: list[str] = []
for index, chunk in enumerate(chunks):
chunk_name = f"index-cache-{index:03d}.json"
path = (cache.parent / chunk_name).resolve()
if path.parent != cache.parent:
raise ValueError("cache chunk escaped the plugin directory")
with tempfile.NamedTemporaryFile(
mode="w", encoding="utf-8", dir=cache.parent, delete=False
) as handle:
handle.write("[" + ",".join(chunk) + "]")
temporary = Path(handle.name)
os.replace(temporary, path)
names.append(chunk_name)
active = set(names)
for stale in cache.parent.glob(f"{cache.stem}-*.json"):
if stale.name not in active:
stale.unlink(missing_ok=True)
cache.unlink(missing_ok=True)
return names
def main() -> int:
if len(sys.argv) != 3:
print("usage: index.py VAULT CACHE", file=sys.stderr)
return 2
root = Path(sys.argv[1]).expanduser().resolve()
requested_cache = Path(sys.argv[2]).expanduser().resolve()
plugin_dir = Path(__file__).resolve().parent
if (
requested_cache.parent != plugin_dir
or requested_cache.name != "index-cache.json"
):
print("cache must be index-cache.json in the plugin directory", file=sys.stderr)
return 2
if not root.is_dir():
print(f"vault does not exist: {root}", file=sys.stderr)
return 1
records = index_records(root)
chunk_names = write_chunks(plugin_dir / "index-cache.json", records)
note_count = sum(record["kind"] == "note" for record in records)
json.dump(
{"chunks": chunk_names, "notes": note_count},
sys.stdout,
separators=(",", ":"),
)
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,97 @@
#!/usr/bin/env python3
"""Safely move or delete entries inside the knowledge vault."""
from __future__ import annotations
import json
import shutil
import sys
from pathlib import Path
VAULT: Path
def fail(message: str) -> int:
print(message, file=sys.stderr)
return 1
def vault_entry(relative: str) -> Path | None:
if not relative or relative.startswith("/") or "\0" in relative:
return None
candidate = VAULT / relative
parent = candidate.parent.resolve()
if parent != VAULT and VAULT not in parent.parents:
return None
return parent / candidate.name
def move_entry(source_relative: str, destination_relative: str) -> int:
source = vault_entry(source_relative)
destination_directory = (VAULT / destination_relative).resolve()
if source is None or not (source.exists() or source.is_symlink()):
return fail("selected entry no longer exists")
if destination_directory != VAULT and VAULT not in destination_directory.parents:
return fail("destination escaped the vault")
if not destination_directory.is_dir():
return fail("destination directory no longer exists")
if source.is_dir() and (
destination_directory == source or source in destination_directory.parents
):
return fail("cannot move a directory into itself")
destination = destination_directory / source.name
if destination.exists() or destination.is_symlink():
return fail(f"already exists: {destination.name}")
try:
source.rename(destination)
except OSError as error:
return fail(f"cannot move {source.name}: {error}")
json.dump(
{
"action": "move",
"relative": destination.relative_to(VAULT).as_posix(),
"kind": "directory" if destination.is_dir() else "note",
},
sys.stdout,
)
return 0
def delete_entry(source_relative: str) -> int:
source = vault_entry(source_relative)
if source is None or not (source.exists() or source.is_symlink()):
return fail("selected entry no longer exists")
try:
if source.is_symlink():
source.unlink()
elif source.is_dir():
shutil.rmtree(source)
else:
source.unlink()
except OSError as error:
return fail(f"cannot delete {source.name}: {error}")
json.dump({"action": "delete", "relative": source_relative}, sys.stdout)
return 0
def main() -> int:
global VAULT
if len(sys.argv) < 4:
return fail(
"usage: manage-entry.py VAULT ACTION SOURCE [DESTINATION_DIRECTORY]"
)
VAULT = Path(sys.argv[1]).expanduser().resolve()
if not VAULT.is_dir():
return fail(f"vault does not exist: {VAULT}")
action, source = sys.argv[2:4]
if action == "move" and len(sys.argv) == 5:
return move_entry(source, sys.argv[4])
if action == "delete" and len(sys.argv) == 4:
return delete_entry(source)
return fail("invalid file operation")
if __name__ == "__main__":
raise SystemExit(main())
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,52 @@
id = "alex/knowledge-lookup"
name = "Knowledge Lookup"
version = "1.0.0"
plugin_api = 24
author = "alex"
license = "MIT"
dependencies = []
tags = ["productivity", "knowledge"]
icon = "notes"
description = "Search and preview a local Obsidian knowledge vault from the bar."
[[setting]]
key = "vault_path"
type = "folder"
label_key = "Knowledge vault directory"
description_key = "Directory containing the Markdown vault to browse"
default = ""
[[widget]]
id = "bar"
entry = "widget.luau"
[[panel]]
id = "panel"
entry = "panel.luau"
width = 1100
height = 720
placement = "floating"
position = "center"
keyboard_focus = "exclusive"
dismiss_on_outside_click = true
capture_keys = [
"j",
"k",
"h",
"l",
"ctrl+U0063",
"ctrl+U0061",
"ctrl+U006e",
"g",
"shift+U0047",
"q",
"o",
"r",
"x",
"p",
"d",
"y",
"n",
"slash",
"enter",
]
@@ -0,0 +1,59 @@
#!/usr/bin/env python3
"""Validate a vault path and resolve its registered Obsidian vault ID."""
from __future__ import annotations
import json
import sys
from pathlib import Path
OBSIDIAN_CONFIG = Path.home() / ".config" / "obsidian" / "obsidian.json"
def fail(message: str) -> int:
json.dump({"ok": False, "error": message}, sys.stdout)
return 1
def main() -> int:
if len(sys.argv) != 2:
return fail("No vault directory is configured")
raw_path = sys.argv[1].strip()
if not raw_path:
return fail("No vault directory is configured")
vault = Path(raw_path).expanduser().resolve()
if not vault.is_dir():
return fail(f"Vault directory was not found: {vault}")
vault_id: str | None = None
config_error: str | None = None
try:
config = json.loads(OBSIDIAN_CONFIG.read_text(encoding="utf-8"))
for registered_id, metadata in config.get("vaults", {}).items():
registered_path = Path(str(metadata.get("path", ""))).expanduser().resolve()
if registered_path == vault:
vault_id = str(registered_id)
break
except FileNotFoundError:
config_error = "Obsidian has not registered any vaults yet"
except (OSError, json.JSONDecodeError, TypeError) as error:
config_error = f"Cannot read Obsidian vault registration: {error}"
response = {
"ok": True,
"path": str(vault),
"name": vault.name,
"vaultId": vault_id,
"obsidianError": config_error
or (
None
if vault_id
else "This directory is not registered as an Obsidian vault"
),
}
json.dump(response, sys.stdout)
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,17 @@
--!strict
local PANEL_ID = "alex/knowledge-lookup:panel"
function update()
barWidget.setGlyph("book-2")
barWidget.setText("KB")
barWidget.setTooltip("Knowledge lookup · Super+N")
end
function onClick()
noctalia.togglePanel(PANEL_ID)
end
function onRightClick()
noctalia.runAsync({ "obsidian" })
end
+1
View File
@@ -0,0 +1 @@
../../../../projects/pi-status-bridge/noctalia-v5-plugin/pi-status-bridge
@@ -0,0 +1,205 @@
-- Show instantaneous battery power above an ETA based on a time-weighted,
-- five-minute rolling average. Energy values are normalized to µWh so that
-- both energy_* and charge_* battery drivers can produce an estimate.
local command = [[
bat=/sys/class/power_supply/BAT0
number() {
if [ -r "$1" ]; then
value=$(cat "$1" 2>/dev/null)
case "$value" in
''|*[!0-9]*) ;;
*) printf '%s' "$value"; return 0 ;;
esac
fi
return 1
}
status=$(cat "$bat/status" 2>/dev/null) || exit 1
power=$(number "$bat/power_now")
voltage=$(number "$bat/voltage_now")
if [ -z "$power" ]; then
current=$(number "$bat/current_now")
if [ -n "$current" ] && [ -n "$voltage" ]; then
power=$(awk -v current="$current" -v voltage="$voltage" \
'BEGIN { printf "%.0f", current * voltage / 1000000 }')
fi
fi
energy_now=$(number "$bat/energy_now")
energy_full=$(number "$bat/energy_full")
if [ -z "$energy_now" ] || [ -z "$energy_full" ]; then
charge_now=$(number "$bat/charge_now")
charge_full=$(number "$bat/charge_full")
if [ -n "$charge_now" ] && [ -n "$charge_full" ] && [ -n "$voltage" ]; then
energy_now=$(awk -v charge="$charge_now" -v voltage="$voltage" \
'BEGIN { printf "%.0f", charge * voltage / 1000000 }')
energy_full=$(awk -v charge="$charge_full" -v voltage="$voltage" \
'BEGIN { printf "%.0f", charge * voltage / 1000000 }')
fi
fi
printf '%s\t%s\t%s\t%s\n' "$status" "$power" "$energy_now" "$energy_full"
]]
local sample_window_seconds = 5 * 60
local power_samples = {}
local sampled_state = nil
local request_in_flight = false
local function renderLines(top, bottom)
barWidget.render(ui.column({ gap = 0, align = "center" }, {
ui.label({ text = top, fontSize = 10 }),
ui.label({ text = bottom, fontSize = 9 }),
}))
end
local function resetSamples(state)
if sampled_state ~= state then
power_samples = {}
sampled_state = state
end
end
local function addPowerSample(state, power, now)
resetSamples(state)
local latest = power_samples[#power_samples]
if latest ~= nil and latest.time == now then
latest.power = power
else
table.insert(power_samples, { time = now, power = power })
end
local cutoff = now - sample_window_seconds
while #power_samples > 1 and power_samples[2].time <= cutoff do
table.remove(power_samples, 1)
end
end
local function rollingAverage(now)
if #power_samples == 0 then
return nil, 0
end
if #power_samples == 1 then
return power_samples[1].power, 0
end
local cutoff = now - sample_window_seconds
local integral = 0
local duration = 0
for index = 2, #power_samples do
local earlier = power_samples[index - 1]
local later = power_samples[index]
local interval = later.time - earlier.time
local start_time = math.max(earlier.time, cutoff)
local end_time = math.min(later.time, now)
if interval > 0 and end_time > start_time then
local start_fraction = (start_time - earlier.time) / interval
local end_fraction = (end_time - earlier.time) / interval
local start_power = earlier.power + (later.power - earlier.power) * start_fraction
local end_power = earlier.power + (later.power - earlier.power) * end_fraction
local elapsed = end_time - start_time
integral = integral + (start_power + end_power) * elapsed / 2
duration = duration + elapsed
end
end
if duration == 0 then
return power_samples[#power_samples].power, 0
end
return integral / duration, duration
end
local function formatDuration(hours)
local minutes = math.max(1, math.floor(hours * 60 + 0.5))
local whole_hours = math.floor(minutes / 60)
local remaining_minutes = minutes % 60
if whole_hours > 0 then
return string.format("%dh %02dm", whole_hours, remaining_minutes)
end
return string.format("%dm", remaining_minutes)
end
local function formatSampleDuration(seconds)
local minutes = math.floor(seconds / 60)
return string.format("%dm %02ds", minutes, seconds % 60)
end
function update()
noctalia.setUpdateInterval(5000)
if request_in_flight then
return
end
request_in_flight = true
noctalia.runAsync(command, function(result)
request_in_flight = false
if result.exitCode ~= 0 then
renderLines("—", "Unavailable")
barWidget.setTooltip("Battery rate and remaining-time estimate unavailable")
return
end
local state, power, energy_now, energy_full = string.match(
result.stdout,
"^([^\t]+)\t([^\t]*)\t([^\t]*)\t([^\r\n]*)"
)
power = tonumber(power)
energy_now = tonumber(energy_now)
energy_full = tonumber(energy_full)
if state ~= "Discharging" and state ~= "Charging" then
resetSamples(state)
renderLines("—", state)
barWidget.setTooltip("Battery: " .. state)
return
end
if power == nil or power <= 0 then
renderLines("—", "Calculating…")
barWidget.setTooltip("Battery: " .. state .. " · waiting for a power reading")
return
end
local now = os.time()
addPowerSample(state, power, now)
local average_power, sample_duration = rollingAverage(now)
local label = "Calculating…"
if average_power ~= nil and average_power > 0 and energy_now ~= nil and energy_full ~= nil then
local energy_remaining = energy_now
if state == "Charging" then
energy_remaining = energy_full - energy_now
end
if energy_remaining >= 0 then
local suffix = state == "Charging" and " to full" or " left"
label = formatDuration(energy_remaining / average_power) .. suffix
end
end
local direction = state == "Charging" and "↑" or "↓"
renderLines(string.format("%s %.1f W", direction, power / 1000000), label)
barWidget.setTooltip(
string.format(
"Battery: %s · %.1f W now · %.1f W rolling average (%s sampled of 5m)",
state,
power / 1000000,
average_power / 1000000,
formatSampleDuration(sample_duration)
)
)
end)
end
@@ -11,6 +11,26 @@ local command = [[awk '
local previous = {} local previous = {}
local request_in_flight = false local request_in_flight = false
local function colorForUsage(percent)
if percent >= 85 then
return "#ff453a" -- red
end
if percent >= 70 then
return "#ff8800" -- orange
end
if percent >= 50 then
return "#ffd60a" -- yellow
end
return "#ffffff" -- white
end
local function renderStatus(text, color)
barWidget.render(ui.row({ gap = 4, align = "center" }, {
ui.glyph({ name = "cpu", size = 14, color = color }),
ui.label({ text = text, color = color }),
}))
end
function update() function update()
noctalia.setUpdateInterval(2000) noctalia.setUpdateInterval(2000)
@@ -23,7 +43,8 @@ function update()
request_in_flight = false request_in_flight = false
if result.exitCode ~= 0 then if result.exitCode ~= 0 then
barWidget.setText("CPU —") renderStatus("CPU —", "#fff1e6")
barWidget.setTooltip("Unable to read CPU utilization")
return return
end end
@@ -47,12 +68,19 @@ function update()
previous[name] = { total = total, idle = idle } previous[name] = { total = total, idle = idle }
end end
barWidget.setGlyph("cpu")
if sampled > 0 then if sampled > 0 then
barWidget.setText(string.format("%.0f%%", utilization)) -- Keep the aggregate display, but base its heat color on average
barWidget.setTooltip("Aggregate CPU utilization (100% per logical CPU)") -- utilization per logical CPU so multicore systems do not stay red.
local per_cpu_utilization = utilization / sampled
renderStatus(string.format("%.0f%%", utilization), colorForUsage(per_cpu_utilization))
barWidget.setTooltip(
string.format(
"Aggregate CPU utilization (100%% per logical CPU) · %.0f%% average per CPU",
per_cpu_utilization
)
)
else else
barWidget.setText("—") renderStatus("—", "#fff1e6")
barWidget.setTooltip("Sampling CPU utilization…") barWidget.setTooltip("Sampling CPU utilization…")
end end
end) end)
@@ -0,0 +1,31 @@
-- Query EnvyControl once when this widget starts; GPU mode changes require a restart.
local command = "envycontrol --query"
local labels = {
integrated = "iGPU",
dedicated = "dGPU",
nvidia = "dGPU",
hybrid = "hybrid",
}
barWidget.setGlyph("device-desktop")
barWidget.setText("GPU…")
barWidget.setTooltip("Reading EnvyControl GPU mode…")
noctalia.runAsync(command, function(result)
if result.exitCode ~= 0 then
barWidget.setText("GPU?")
barWidget.setTooltip("Unable to read EnvyControl GPU mode")
return
end
local mode = string.lower(string.match(result.stdout, "([%a_-]+)") or "")
local label = labels[mode]
if label == nil then
barWidget.setText("GPU?")
barWidget.setTooltip("Unknown EnvyControl GPU mode: " .. mode)
return
end
barWidget.setText(label)
barWidget.setTooltip("EnvyControl GPU mode: " .. mode)
end)
@@ -0,0 +1,85 @@
-- Click-through details for both logind idle inhibitors and session
-- ScreenSaver requests such as browser video wake locks.
local command = (noctalia.getenv("HOME") or "") .. "/.local/bin/idle-inhibitor-monitor --list"
local inhibitors = {}
local status = "Loading idle inhibitors…"
local request_in_flight = false
local function renderPanel()
local children = {
ui.column({ gap = 2 }, {
ui.label({ text = "Idle inhibitors", fontSize = 18, fontWeight = "bold" }),
ui.label({ text = status, color = "on_surface_variant" }),
}),
ui.button({ text = "Refresh", onClick = "onRefresh" }),
}
if #inhibitors == 0 then
table.insert(children, ui.label({ text = "No process currently blocks automatic idle." }))
else
for _, inhibitor in ipairs(inhibitors) do
local title = inhibitor.comm ~= "" and inhibitor.comm or inhibitor.who
table.insert(children, ui.column({ gap = 2 }, {
ui.label({ text = string.format("%s · %s", inhibitor.source, title), fontWeight = "bold" }),
ui.label({ text = inhibitor.why ~= "" and inhibitor.why or "No reason supplied", maxLines = 2 }),
ui.label({
text = string.format("%s · %s · PID %s", inhibitor.mode, inhibitor.user, inhibitor.pid),
color = "on_surface_variant",
fontSize = 11,
}),
}))
end
end
panel.render(ui.scroll({ padding = 20, gap = 12 }, children))
end
local function refreshInhibitors()
if request_in_flight then return end
request_in_flight = true
status = "Loading idle inhibitors…"
renderPanel()
noctalia.runAsync(command, function(result)
request_in_flight = false
inhibitors = {}
if result.exitCode ~= 0 then
status = "Unable to inspect logind inhibitors."
renderPanel()
return
end
for line in string.gmatch(result.stdout, "[^\r\n]+") do
local source, who, user, pid, comm, mode, why = string.match(
line,
"^([^\t]*)\t([^\t]*)\t([^\t]*)\t([^\t]*)\t([^\t]*)\t([^\t]*)\t(.*)$"
)
if source ~= nil then
table.insert(inhibitors, {
source = source,
who = who,
user = user,
pid = pid,
comm = comm,
mode = mode,
why = why,
})
end
end
status = #inhibitors == 0
and "Automatic idle is eligible."
or string.format("%d active idle inhibitor%s.", #inhibitors, #inhibitors == 1 and "" or "s")
renderPanel()
end)
end
function onOpen()
refreshInhibitors()
end
function onRefresh()
refreshInhibitors()
end
@@ -0,0 +1,39 @@
-- Show whether any process blocks automatic idle. The monitor combines logind
-- idle inhibitors with session ScreenSaver requests such as browser video wake locks.
local command = (noctalia.getenv("HOME") or "") .. "/.local/bin/idle-inhibitor-monitor --count"
local request_in_flight = false
function update()
noctalia.setUpdateInterval(5000)
if request_in_flight then return end
request_in_flight = true
noctalia.runAsync(command, function(result)
request_in_flight = false
local count = tonumber(result.stdout)
if result.exitCode ~= 0 or count == nil then
barWidget.setGlyph("circle-help")
barWidget.setText("Idle ?")
barWidget.setTooltip("Unable to inspect idle inhibitors")
return
end
if count > 0 then
barWidget.setGlyph("moon-off")
barWidget.setText(string.format("Idle %d", count))
barWidget.setTooltip(
string.format("%d idle inhibitor%s active · click for details", count, count == 1 and "" or "s")
)
else
barWidget.setGlyph("moon")
barWidget.setText("Idle")
barWidget.setTooltip("No active idle inhibitors · click for details")
end
end)
end
function onClick()
noctalia.runAsync("noctalia msg panel-toggle alex/system-status:inhibitors-panel")
end
@@ -8,16 +8,37 @@ $1 == "SwapFree:" { swap_free = $2 }
END { END {
if (!total || !available) exit 1 if (!total || !available) exit 1
used = total - available used = total - available
memory_used = used * 100 / total
if (swap_total > 0) { if (swap_total > 0) {
swap_used = (swap_total - swap_free) * 100 / swap_total swap_used = (swap_total - swap_free) * 100 / swap_total
printf "RAM %.1f/%.1f GiB S %.0f%%\n", used / 1048576, total / 1048576, swap_used printf "%.1f/%.1f GiB S %.0f%%\t%.0f\t%.0f\n", used / 1048576, total / 1048576, swap_used, memory_used, swap_used
} else { } else {
printf "RAM %.1f/%.1f GiB S —\n", used / 1048576, total / 1048576 printf "%.1f/%.1f GiB S —\t%.0f\t0\n", used / 1048576, total / 1048576, memory_used
} }
}' /proc/meminfo]] }' /proc/meminfo]]
local request_in_flight = false local request_in_flight = false
local function colorForUsage(percent)
if percent >= 85 then
return "#ff453a" -- red
end
if percent >= 70 then
return "#ff8800" -- orange
end
if percent >= 50 then
return "#ffd60a" -- yellow
end
return "#ffffff" -- white
end
local function renderStatus(text, color)
barWidget.render(ui.row({ gap = 4, align = "center" }, {
ui.glyph({ name = "memory", size = 14, color = color }),
ui.label({ text = text, color = color }),
}))
end
function update() function update()
noctalia.setUpdateInterval(5000) noctalia.setUpdateInterval(5000)
@@ -30,14 +51,32 @@ function update()
request_in_flight = false request_in_flight = false
if result.exitCode ~= 0 then if result.exitCode ~= 0 then
barWidget.setText("RAM —") renderStatus("RAM —", "#fff1e6")
barWidget.setTooltip("Unable to read /proc/meminfo") barWidget.setTooltip("Unable to read /proc/meminfo")
return return
end end
local text = string.gsub(result.stdout, "%s+$", "") local text, memory_percent, swap_percent = string.match(
barWidget.setGlyph("memory") result.stdout,
barWidget.setText(text) "^([^\t]+)\t([%d.]+)\t([%d.]+)"
barWidget.setTooltip("RAM used / total · S = swap used") )
memory_percent = tonumber(memory_percent)
swap_percent = tonumber(swap_percent)
if text == nil or memory_percent == nil or swap_percent == nil then
renderStatus("RAM —", "#fff1e6")
barWidget.setTooltip("Unable to parse memory utilization")
return
end
local highest_usage = math.max(memory_percent, swap_percent)
renderStatus(text, colorForUsage(highest_usage))
barWidget.setTooltip(
string.format(
"RAM used / total · %.0f%% RAM · %.0f%% swap",
memory_percent,
swap_percent
)
)
end) end)
end end
@@ -0,0 +1,119 @@
-- Compact, two-line network throughput widget. It samples the interface used
-- by the default route so virtual and physical interfaces are not double-counted.
local command = [[
iface=$(ip route show default 2>/dev/null | awk '$1 == "default" { for (i = 1; i <= NF; i++) if ($i == "dev") { print $(i + 1); exit } }')
if [ -z "$iface" ]; then
for path in /sys/class/net/*; do
candidate=${path##*/}
[ "$candidate" = "lo" ] && continue
[ "$(cat "$path/carrier" 2>/dev/null)" = "1" ] || continue
iface=$candidate
break
done
fi
[ -n "$iface" ] || exit 1
awk -v iface="$iface" '$1 ~ ("^" iface ":") { sub(":", "", $1); print iface "\t" $2 "\t" $10; exit }' /proc/net/dev
]]
local previous = nil
local request_in_flight = false
-- 82px accommodates the longest fixed field ("1023 KiB/s") at 10px while
-- removing the excess side padding from the original 112px capsule.
local label_width = 82
local function formatRate(bytes_per_second)
local units = { "B/s", "KiB/s", "MiB/s", "GiB/s" }
local value = bytes_per_second
local unit_index = 1
while value >= 1024 and unit_index < #units do
value = value / 1024
unit_index = unit_index + 1
end
if unit_index == 1 then
return string.format("%d %s", math.floor(value + 0.5), units[unit_index])
end
if value >= 100 then
return string.format("%.0f %s", value, units[unit_index])
end
return string.format("%.1f %s", value, units[unit_index])
end
local function renderLines(download, upload)
-- The explicit minimum width and fixed ten-character value field keep the
-- capsule width stable while inheriting the bar font used by other modules.
barWidget.render(ui.column({ gap = 0, align = "center", minWidth = label_width }, {
ui.label({
text = string.format("↓ %10s", download),
maxLines = 1,
fontSize = 10,
}),
ui.label({
text = string.format("↑ %10s", upload),
maxLines = 1,
fontSize = 10,
}),
}))
end
function update()
noctalia.setUpdateInterval(2000)
if request_in_flight then
return
end
request_in_flight = true
noctalia.runAsync(command, function(result)
request_in_flight = false
if result.exitCode ~= 0 then
previous = nil
renderLines("—", "—")
barWidget.setTooltip("Network throughput unavailable: no active interface")
return
end
local iface, received, sent = string.match(
result.stdout,
"^([^\t]+)\t(%d+)\t(%d+)"
)
received = tonumber(received)
sent = tonumber(sent)
if iface == nil or received == nil or sent == nil then
previous = nil
renderLines("—", "—")
barWidget.setTooltip("Network throughput unavailable")
return
end
local now = os.time()
if previous == nil or previous.iface ~= iface or now <= previous.time then
previous = { iface = iface, received = received, sent = sent, time = now }
renderLines("Sampling…", "Sampling…")
barWidget.setTooltip("Sampling network throughput on " .. iface)
return
end
local elapsed = now - previous.time
local download = math.max(0, (received - previous.received) / elapsed)
local upload = math.max(0, (sent - previous.sent) / elapsed)
previous = { iface = iface, received = received, sent = sent, time = now }
renderLines(formatRate(download), formatRate(upload))
barWidget.setTooltip(
string.format(
"%s · Download %s · Upload %s",
iface,
formatRate(download),
formatRate(upload)
)
)
end)
end
@@ -3,7 +3,7 @@ name = "System Status"
version = "1.0.0" version = "1.0.0"
plugin_api = 3 plugin_api = 3
author = "alex" author = "alex"
description = "Low-overhead memory and swap indicator for the bar." description = "Low-overhead system and network status indicators for the bar."
[[widget]] [[widget]]
id = "cpu" id = "cpu"
@@ -12,3 +12,23 @@ entry = "cpu.luau"
[[widget]] [[widget]]
id = "memory" id = "memory"
entry = "memory.luau" entry = "memory.luau"
[[widget]]
id = "battery-rate"
entry = "battery-rate.luau"
[[widget]]
id = "network-rate"
entry = "network-rate.luau"
[[widget]]
id = "gpu-mode"
entry = "gpu-mode.luau"
[[widget]]
id = "inhibitors"
entry = "inhibitors.luau"
[[panel]]
id = "inhibitors-panel"
entry = "inhibitors-panel.luau"
+13
View File
@@ -12,6 +12,19 @@
"contextWindow": 1050000 "contextWindow": 1050000
} }
} }
},
"openai-codex": {
"modelOverrides": {
"gpt-5.6-sol": {
"contextWindow": 1050000
},
"gpt-5.6-terra": {
"contextWindow": 1050000
},
"gpt-5.6-luna": {
"contextWindow": 1050000
}
}
} }
} }
} }
+3 -3
View File
@@ -2,7 +2,7 @@
"quietStartup": true, "quietStartup": true,
"lastChangelogVersion": "0.80.10", "lastChangelogVersion": "0.80.10",
"defaultProvider": "openai-codex", "defaultProvider": "openai-codex",
"defaultModel": "gpt-5.6-sol", "defaultModel": "gpt-5.6-terra",
"defaultThinkingLevel": "high", "defaultThinkingLevel": "high",
"packages": [ "packages": [
"npm:pi-subagents", "npm:pi-subagents",
@@ -18,11 +18,11 @@
"npm:bigpowers", "npm:bigpowers",
"npm:@gotgenes/pi-permission-system", "npm:@gotgenes/pi-permission-system",
"npm:pi-intercom", "npm:pi-intercom",
"npm:gentle-engram", "npm:gentle-engram@0.1.8",
"npm:pi-lens", "npm:pi-lens",
"npm:@juicesharp/rpiv-todo", "npm:@juicesharp/rpiv-todo",
"npm:@majorgilles/pi-grill-me", "npm:@majorgilles/pi-grill-me",
"git:git.commumedia.org/alex/pi-map" "npm:pi-mcp-adapter"
], ],
"powerline": "full", "powerline": "full",
"hideThinkingBlock": false, "hideThinkingBlock": false,
+107 -173
View File
@@ -1,186 +1,120 @@
# -- general ------------------------------------------------------------------- # ~/.tmux.conf — Vim-centric tmux configuration (tmux 3.7+)
# Reload with: prefix + r
set -g extended-keys on # --- Terminal and responsiveness ---------------------------------------------
set -g default-terminal "tmux-256color"
set -g default-terminal "screen-256color" # colors! set -as terminal-features ",xterm-256color:RGB,screen-256color:RGB,tmux-256color:RGB"
setw -g xterm-keys on set -sg escape-time 10
set -s escape-time 10 # faster command sequences set -sg repeat-time 600
set -sg repeat-time 600 # increase repeat timeout
set -s focus-events on set -s focus-events on
set -g allow-passthrough on
set -g history-limit 100000
set -g set-clipboard on
set -g prefix2 C-a # GNU-Screen compatible prefix # --- Prefix and defaults ------------------------------------------------------
bind C-a send-prefix -2 unbind C-b
set -g prefix C-a
bind C-a send-prefix
set -q -g status-utf8 on # expect UTF-8 (tmux < 2.2) setw -g mode-keys vi
set -q -g utf8 on set -g status-keys vi
set -g mouse on
set -g base-index 1
setw -g pane-base-index 1
set -g renumber-windows on
set -g detach-on-destroy off
set -g display-panes-time 800
set -g display-time 1500
set -g history-limit 5000 # boost history # --- Reload and editing -------------------------------------------------------
bind r source-file ~/.tmux.conf \; display-message "tmux config reloaded"
bind e new-window -n tmux.conf "exec \${EDITOR:-nvim} ~/.tmux.conf"
# edit configuration # --- Vim-style panes ----------------------------------------------------------
bind e new-window -n "~/.tmux.conf.local" "sh -c '\${EDITOR:-vim} ~/.tmux.conf.local && tmux source ~/.tmux.conf && tmux display \"~/.tmux.conf sourced\"'" # Keep the familiar split keys and always inherit the active pane's directory.
bind m split-window -v -c "#{pane_current_path}"
bind n split-window -h -c "#{pane_current_path}"
bind c new-window -c "#{pane_current_path}"
# Re-tile the current window using Ctrl-a, Ctrl-h / Ctrl-a, Ctrl-k.
bind C-h select-layout even-vertical
bind C-k select-layout even-horizontal
# reload configuration bind -r h select-pane -L
bind r source-file ~/.tmux.conf \; display '~/.tmux.conf sourced' bind -r j select-pane -D
bind -r k select-pane -U
bind -r l select-pane -R
bind -r H resize-pane -L 5
bind -r J resize-pane -D 5
bind -r K resize-pane -U 5
bind -r L resize-pane -R 5
bind -r \{ swap-pane -U
bind -r \} swap-pane -D
bind z resize-pane -Z
bind w confirm-before -p "kill pane #P? (y/n)" kill-pane
# run the powerline daemon # --- Windows, sessions, and buffers ------------------------------------------
run-shell "powerline-daemon -q" bind -r f next-window
# source powerline tmux bindings bind -r b previous-window
source "/usr/lib/python3.14/site-packages/powerline/bindings/tmux/powerline.conf" bind Tab last-window
bind d choose-tree -Zw
bind s choose-tree -Zs
bind g command-prompt -p "switch to session:" "switch-client -t '%%'"
bind [ choose-buffer
bind P paste-buffer # tmux buffer fallback; prefix + ] pastes the system clipboard
bind C-l send-keys C-l \; clear-history
bind -T copy-mode-vi y send-keys -X copy-pipe-and-cancel 'xclip -in -selection clipboard' # --- Copy mode ----------------------------------------------------------------
bind Enter copy-mode
bind -T copy-mode-vi v send -X begin-selection
bind -T copy-mode-vi C-v send -X rectangle-toggle
bind -T copy-mode-vi y send -X copy-selection-and-cancel
bind -T copy-mode-vi Escape send -X cancel
bind -T copy-mode-vi q send -X cancel
bind -T copy-mode-vi H send -X start-of-line
bind -T copy-mode-vi L send -X end-of-line
bind -T copy-mode-vi C-u send -X halfpage-up
bind -T copy-mode-vi C-d send -X halfpage-down
# Prevent random characters from appearing during yank # Keep tmux and the desktop clipboard in sync by default. `set-clipboard on`
set -s set-clipboard off # handles OSC 52-capable terminals; these bindings cover local Wayland/X11.
# Copy-mode y writes to the system clipboard. Prefix + ] reads it back and
# pastes it; prefix + P remains available for the most recent tmux buffer.
bind -T copy-mode-vi y send -X copy-pipe-and-cancel 'if command -v wl-copy >/dev/null 2>&1; then wl-copy; elif command -v xclip >/dev/null 2>&1; then xclip -in -selection clipboard; fi'
bind ] run-shell 'if command -v wl-paste >/dev/null 2>&1; then wl-paste --no-newline | tmux load-buffer - && tmux paste-buffer; elif command -v xclip >/dev/null 2>&1; then xclip -out -selection clipboard | tmux load-buffer - && tmux paste-buffer; fi'
# -- display ------------------------------------------------------------------- # --- Status line --------------------------------------------------------------
set -g status on
set -g status-position bottom
set -g status-interval 5
set -g status-justify left
set -g status-left-length 40
set -g status-right-length 100
# Noctalia "Ember OLED" palette: deep near-black with restrained amber focus.
set -g status-style "bg=#050403,fg=#fff1e6"
set -g message-style "bg=#ff8800,fg=#1a0d00"
set -g message-command-style "bg=#ff8800,fg=#1a0d00"
set -g pane-border-style "fg=#6e4a31"
set -g pane-active-border-style "fg=#ff8800"
set -g mode-style "bg=#ffb05c,fg=#2b1600"
# The left segment switches to a large amber cue while tmux waits for the
# key following C-a, so prefix mode is visible rather than implicit.
# Keep each style attribute in its own sequence: conditional-format commas then
# cannot be mistaken for part of a #[fg=…,bg=…] style specification.
set -g status-left "#{?client_prefix,#[bg=#ffb05c]#[fg=#2b1600]#[bold] C-a COMMAND ,#[bg=#ff8800]#[fg=#1a0d00]#[bold] #S }#[bg=#050403]#[fg=#ff8800]"
set -g status-right "#[fg=#f4c7a1]#(whoami)@#H #[fg=#ff8800]| #[fg=#fff1e6]%Y-%m-%d %H:%M "
setw -g window-status-format " #[fg=#f4c7a1]#I:#W "
setw -g window-status-current-format "#[bg=#1a110c,fg=#fff1e6,bold] #I:#W "
set -g base-index 1 # start windows numbering at 1 # --- Optional TPM plugins -----------------------------------------------------
setw -g pane-base-index 1 # make pane numbering consistent with windows # Install TPM once: git clone https://github.com/tmux-plugins/tpm ~/.tmux/plugins/tpm
# Then reload and press prefix + I to install these plugins.
set -g @plugin 'tmux-plugins/tpm'
set -g @plugin 'tmux-plugins/tmux-sensible'
set -g @plugin 'tmux-plugins/tmux-resurrect'
set -g @plugin 'tmux-plugins/tmux-continuum'
set -g @resurrect-capture-pane-contents 'on'
set -g @continuum-restore 'on'
if-shell '[ -f ~/.tmux/plugins/tpm/tpm ]' 'run ~/.tmux/plugins/tpm/tpm'
#setw -g automatic-rename on # rename window to reflect current program # Keep machine-specific bindings and experiments out of this tracked base file.
set -g renumber-windows on # renumber windows when a window is closed if-shell '[ -f ~/.tmux.conf.local ]' 'source-file ~/.tmux.conf.local'
#set -g set-titles on # set terminal title
set -g display-panes-time 800 # slightly longer pane indicators display time
set -g display-time 1000 # slightly longer status messages display time
set-option -g status on
set-option -g status-interval 2
set-option -g status-justify "centre"
set-option -g status-left-length 60
set-option -g status-right-length 90
# clear both screen and history
bind -n C-l send-keys C-l \; run 'sleep 0.1' \; clear-history
# activity
set -g monitor-activity on
set -g visual-activity off
# set border color for panes
set -g pane-active-border-style bg=colour130,fg=colour250
# -- navigation ----------------------------------------------------------------
# create session
bind C-c new-session
# find session
bind C-f command-prompt -p find-session 'switch-client -t %%'
# split current window horizontally
bind s split-window -v
# split current window vertically
bind v split-window -h
# close pane
bind w kill-pane
# pane navigation
bind -r h select-pane -L # move left
bind -r j select-pane -D # move down
bind -r k select-pane -U # move up
bind -r l select-pane -R # move right
bind > swap-pane -D # swap current pane with the next one
bind < swap-pane -U # swap current pane with the previous one
# maximize current pane
bind + run 'cut -c3- ~/.tmux.conf | sh -s _maximize_pane "#{session_name}" #D'
# pane resizing
bind -r H resize-pane -L 2
bind -r J resize-pane -D 2
bind -r K resize-pane -U 2
bind -r L resize-pane -R 2
# window navigation
unbind n
unbind p
bin -r C-n new-window
bind -r C-h previous-window # select previous window
bind -r C-l next-window # select next window
bind Tab last-window # move to last active window
# toggle mouse
bind m run "cut -c3- ~/.tmux.conf | sh -s _toggle_mouse"
# -- urlview -------------------------------------------------------------------
bind U run "cut -c3- ~/.tmux.conf | sh -s _urlview #{pane_id}"
# -- facebook pathpicker -------------------------------------------------------
bind F run "cut -c3- ~/.tmux.conf | sh -s _fpp #{pane_id}"
# -- list choice (tmux < 2.4) --------------------------------------------------
# vi-choice is gone in tmux >= 2.4
run -b 'tmux bind -t vi-choice h tree-collapse 2> /dev/null || true'
run -b 'tmux bind -t vi-choice l tree-expand 2> /dev/null || true'
run -b 'tmux bind -t vi-choice K start-of-list 2> /dev/null || true'
run -b 'tmux bind -t vi-choice J end-of-list 2> /dev/null || true'
run -b 'tmux bind -t vi-choice H tree-collapse-all 2> /dev/null || true'
run -b 'tmux bind -t vi-choice L tree-expand-all 2> /dev/null || true'
run -b 'tmux bind -t vi-choice Escape cancel 2> /dev/null || true'
# -- edit mode (tmux < 2.4) ----------------------------------------------------
# vi-edit is gone in tmux >= 2.4
run -b 'tmux bind -ct vi-edit H start-of-line 2> /dev/null || true'
run -b 'tmux bind -ct vi-edit L end-of-line 2> /dev/null || true'
run -b 'tmux bind -ct vi-edit q cancel 2> /dev/null || true'
run -b 'tmux bind -ct vi-edit Escape cancel 2> /dev/null || true'
# -- copy mode -----------------------------------------------------------------
bind Enter copy-mode # enter copy mode
run -b 'tmux bind -t vi-copy v begin-selection 2> /dev/null || true'
run -b 'tmux bind -T copy-mode-vi v send -X begin-selection 2> /dev/null || true'
run -b 'tmux bind -t vi-copy C-v rectangle-toggle 2> /dev/null || true'
run -b 'tmux bind -T copy-mode-vi C-v send -X rectangle-toggle 2> /dev/null || true'
run -b 'tmux bind -t vi-copy y copy-selection 2> /dev/null || true'
run -b 'tmux bind -T copy-mode-vi y send -X copy-selection-and-cancel 2> /dev/null || true'
run -b 'tmux bind -t vi-copy Escape cancel 2> /dev/null || true'
run -b 'tmux bind -T copy-mode-vi Escape send -X cancel 2> /dev/null || true'
run -b 'tmux bind -t vi-copy H start-of-line 2> /dev/null || true'
run -b 'tmux bind -T copy-mode-vi H send -X start-of-line 2> /dev/null || true'
run -b 'tmux bind -t vi-copy L end-of-line 2> /dev/null || true'
run -b 'tmux bind -T copy-mode-vi L send -X end-of-line 2> /dev/null || true'
# copy to macOS clipboard
if -b 'command -v pbcopy > /dev/null 2>&1' 'bind y run -b "tmux save-buffer - | pbcopy"'
if -b 'command -v reattach-to-user-namespace > /dev/null 2>&1' 'bind y run -b "tmux save-buffer - | reattach-to-user-namespace pbcopy"'
# copy to X11 clipboard
if -b 'command -v xsel > /dev/null 2>&1' 'bind y run -b "tmux save-buffer - | xsel -i -b"'
if -b '! command -v xsel > /dev/null 2>&1 && command -v xclip > /dev/null 2>&1' 'bind y run -b "tmux save-buffer - | xclip -i -selection clipboard >/dev/null 2>&1"'
# copy to Windows clipboard
if -b 'command -v clip.exe > /dev/null 2>&1' 'bind y run -b "tmux save-buffer - | clip.exe"'
if -b '[ -c /dev/clipboard ]' 'bind y run -b "tmux save-buffer - > /dev/clipboard"'
# -- buffers -------------------------------------------------------------------
bind b list-buffers # list paste buffers
bind p paste-buffer # paste from the top paste buffer
bind P choose-buffer # choose which buffer to paste from
# -- user defined overrides ----------------------------------------------------
if '[ -f ~/.tmux.conf.local ]' 'source ~/.tmux.conf.local'
# -- 8< ------------------------------------------------------------------------
run 'cut -c3- ~/.tmux.conf | sh -s _apply_configuration'
run -b '[ -z "#{window_active}" ] && [ -z "#{version}" ] && tmux set display-time 3000 \; display "This configuration will soon require tmux >= 2.4" \; set -u display-time || true'
+5 -66
View File
@@ -1,68 +1,7 @@
# -- navigation ---------------------------------------------------------------- # ~/.tmux.conf.local
# Personal or machine-specific overrides for ~/.tmux.conf.
# This file is deliberately small; its previous version was saved beside it.
# if you're running tmux within iTerm2 # Examples:
# - and tmux is 1.9 or 1.9a
# - and iTerm2 is configured to let option key act as +Esc
# - and iTerm2 is configured to send [1;9A -> [1;9D for option + arrow keys
# then uncomment the following line to make Meta + arrow keys mapping work
#set -ga terminal-overrides "*:kUP3=\e[1;9A,*:kDN3=\e[1;9B,*:kRIT3=\e[1;9C,*:kLFT3=\e[1;9D"
# -- windows & pane creation ---------------------------------------------------
# new window retains current path, possible values are:
# - true
# - false (default)
tmux_conf_new_window_retain_current_path=false
# new pane retains current path, possible values are:
# - true (default)
# - false
tmux_conf_new_pane_retain_current_path=true
# new pane tries to reconnect ssh sessions (experimental), possible values are:
# - true
# - false (default)
tmux_conf_new_pane_reconnect_ssh=false
# prompt for session name when creating a new session, possible values are:
# - true
# - false (default)
tmux_conf_new_session_prompt=false
# -- display -------------------------------------------------------------------
# -- clipboard -----------------------------------------------------------------
# in copy mode, copying selection also copies to the OS clipboard
# - true
# - false (default)
# on macOS, this requires installing reattach-to-user-namespace, see README.md
# on Linux, this requires xsel or xclip
tmux_conf_copy_to_os_clipboard=true
# -- user customizations -------------------------------------------------------
# this is the place to override or undo settings
# increase history size
#set -g history-limit 10000
# start with mouse mode enabled
set -g mouse on
# force Vi mode
# really you should export VISUAL or EDITOR environment variable, see manual
#set -g status-keys vi
#set -g mode-keys vi
# replace C-b by C-a instead of using both prefixes
set -gu prefix2
unbind C-a
unbind C-b
set -g prefix C-a
bind C-a send-prefix
# move status line to top
# set -g status-position top # set -g status-position top
# bind -n M-Left select-pane -L
+7 -429
View File
@@ -156,437 +156,15 @@ alias sudo='sudo '
export EDITOR=nvim export EDITOR=nvim
export VISUAL=nvim export VISUAL=nvim
# add the custom functions from the .custom_functions file # Load the known shell helpers; do not source foreign-shell files.
custom_func_dir=$HOME/.scripts custom_func_dir=$HOME/.scripts
for f in $custom_func_dir/*; do source $f; done for f in "$custom_func_dir"/*.sh(N) "$custom_func_dir"/get_ip(N); do
source "$f"
done
# add pyenv to the path # add pyenv to the path
echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.zshrc if (( $+commands[pyenv] )); then
echo '[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.zshrc export PYENV_ROOT="${PYENV_ROOT:-$HOME/.pyenv}"
echo 'eval "$(pyenv init - zsh)"' >> ~/.zshrc
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)"
export PYENV_ROOT="$HOME/.pyenv"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH" [[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init - zsh)" eval "$(pyenv init - zsh)"
fi
+58
View File
@@ -0,0 +1,58 @@
# Dotfiles
Personal, Arch-oriented user dotfiles and support scripts for shells, window management, applications, and package lists. This repository is a collection of machine-specific configuration, not a general-purpose installer.
## Status and scope
There is no authoritative root installation, linking, backup, overwrite, or rollback script. Do not infer one: review and apply only the individual files appropriate to a machine. Some filenames use `##class.arch_*` suffixes to distinguish machine classes. The repository also includes a nested README for the custom/vendored `st` terminal under `.config/st/`; it documents that component, not installation of this repository.
Several scripts assume paths below `~/.setup`, Arch tooling, and root access. Some repository scripts contain machine-specific connection details; do not publish, copy, or document such values. Use local, private configuration for hosts, addresses, accounts, and credentials.
## Prerequisites
- An Arch-based system for the supplied `pacman`/`yay` package workflows.
- The package manager or helper required by the script being considered (`pacman` and/or `yay`).
- Desktop/session tools matching the chosen configuration (for example X11/XMonad-related files where applicable).
- Root access only for the system-changing scripts described below.
Package lists in `.setup/packages/` are inputs for a user-managed installation; no repository command defines how to install an entire list. Inspect package names and select lists for the intended machine before installing anything.
## Safe review and user-level configuration
Start by reviewing the intended files and any machine-class variants. Typical areas are:
- `.zshrc`, `.bashrc`, `.terminal_aliases`, `.tmux.conf`, and `.p10k.zsh` — shell and terminal setup.
- `.xmonad/`, `.Xresources##class.*`, `.xprofile##class.*`, and `.config/` — desktop/window-manager and application configuration.
- `.scripts/` — optional shell, network, virtual-environment, and update helpers.
- `.setup/packages/` — package lists grouped by purpose.
Back up existing configuration before manually placing or linking a file. No test suite is provided; validate a change in the relevant shell/session or application and keep a rollback copy of the previous configuration.
## Package updates
`.scripts/updating.sh` defines, but does not invoke, two functions. It expects `~/.setup/packages/exclude_from_updating.list`:
```sh
source .scripts/updating.sh
update_system
# or
update_pacman_packages
```
> **Warning: system-wide, potentially disruptive operation.** `update_system` runs `yay -Syu` and `update_pacman_packages` runs `sudo pacman -Syu`, both non-interactively and with the exclusion list. Review the exclusion list and pending package changes first. These functions update the running system; they are not tests.
## System-changing scripts
The following scripts mutate files outside this repository and must be inspected before use:
- `.setup/config/setup_touchpad.sh` copies an X11 configuration into `/etc/X11/xorg.conf.d/` when absent.
- `.setup/config/exclude_from_update.sh` edits `/etc/pacman.conf` with `sudo` based on `~/.setup/packages/exclude_from_updating.list`.
- `.setup/config/dinit_add_logs_to_all_services.sh` edits files in `/etc/dinit.d` and writes log specifications under `/var/log/dinit`; its `-o` option removes existing logfile lines before adding them.
> **Warning: destructive/system-wide operations.** These scripts can alter system package policy, service definitions, or input configuration. They have no repository-provided dry run or rollback. Run only on a machine you control after backups and after checking paths, permissions, and the script contents.
`.setup/config/set_default_programs.sh` sets the default HTTP/HTTPS handler through `xdg-mime`; review the desktop entry before using it.
## Custom `st` terminal
The nested `.config/st/README.md` documents its own `make` and `sudo make install` workflow. Those commands build/install only that terminal component, may modify the system, and are not a bootstrap procedure for all dotfiles.