33 Commits

Author SHA1 Message Date
alex f99cfeae79 feat: track Decman configuration 2026-08-13 23:03:55 +02:00
alex 34acd648c9 refactor: hand active dotfiles to Decman 2026-08-13 22:57:25 +02:00
alex 9647feebfa feat(knowledge-lookup): restore panel navigation
Persist the active vault folder and selected entry across panel opens, and render the bar widget as an icon only.
2026-08-13 16:32:42 +02:00
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
131 changed files with 9704 additions and 933 deletions
@@ -1,11 +0,0 @@
-- Laptop profile: leave monitor selection to Hyprland's hotplug support.
-- Noctalia replaces the i3-era Redshift, Polybar, Blueman Applet, and Picom stack.
return {
monitors = {},
startup = {
"noctalia",
-- Start after Noctalia has claimed the StatusNotifier watcher.
"sh -c 'sleep 2; exec mullvad-vpn'",
"nextcloud",
},
}
-12
View File
@@ -1,12 +0,0 @@
# Shared Noctalia defaults. Keep GUI experiments in state; promote durable choices here.
[shell]
setup_wizard_enabled = false
offline_mode = true
external_ip_enabled = false
telemetry_enabled = false
[theme]
mode = "dark"
source = "custom"
custom_palette = "Ember OLED"
pure_black_dark = true
@@ -1,93 +0,0 @@
# Laptop-specific Noctalia layout. Shared privacy/theme defaults live in 10-base.toml.
[bar.main]
position = "top"
layer = "overlay"
capsule_radius = 5
center = ["group:g3", "notifications"]
end = ["bluetooth", "volume", "control-center", "group:g1", "session"]
font_weight = 400
margin_ends = 0
padding = 0
panel_overlap = 3
radius = 0
shadow = false
auto_hide = false
smart_auto_hide = false
reserve_space = true
start = ["tray", "group:g4", "group:g2", "privacy"]
thickness = 39
widget_spacing = 10
[[bar.main.capsule_group]]
enabled = true
fill = "surface_variant"
id = "g1"
members = ["caffeine", "date", "clock"]
opacity = 1.0
padding = 6.0
[[bar.main.capsule_group]]
enabled = true
fill = "surface_variant"
id = "g2"
members = ["network_rx", "network_tx", "network"]
opacity = 1.0
padding = 6.0
[[bar.main.capsule_group]]
enabled = true
fill = "surface_variant"
id = "g3"
members = ["launcher", "media", "control-center", "clipboard"]
opacity = 1.0
padding = 6.0
radius = 5.0
[[bar.main.capsule_group]]
enabled = true
fill = "surface_variant"
id = "g4"
members = ["cpu", "temp", "ram", "disk", "battery"]
opacity = 1.0
padding = 6.0
[widget.cpu]
type = "alex/system-status:cpu"
[widget.ram]
type = "alex/system-status:memory"
[widget.temp]
type = "sysmon"
stat = "cpu_temp"
display = "text"
[widget.disk]
type = "sysmon"
stat = "disk_pct"
display = "text"
path = "/"
[control_center]
sidebar = "full"
sidebar_section = "full"
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]
type = custom/text
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]
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.
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 the preview image method. Supported methods:
@@ -114,7 +114,7 @@ set preview_images true
# Preview images in full color with the external command "ueberzug".
# Images are shown by using a child window.
# 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.
# Increase it in case of experiencing display corruption.
+4 -4
View File
@@ -33,10 +33,10 @@
"interval": 1,
"tooltip-format": "\n<big>{:%Y %B}</big>\n<tt><small>{calendar}</small></tt>",
"calendar-weeks-pos": "right",
"today-format": "<span color='#7645AD'><b><u>{}</u></b></span>",
"format-calendar": "<span color='#aeaeae'><b>{}</b></span>",
"format-calendar-weeks": "<span color='#aeaeae'><b>W{:%V}</b></span>",
"format-calendar-weekdays": "<span color='#aeaeae'><b>{}</b></span>"
"today-format": "<span color='#ff8800'><b><u>{}</u></b></span>",
"format-calendar": "<span color='#f4c7a1'><b>{}</b></span>",
"format-calendar-weeks": "<span color='#f4c7a1'><b>W{:%V}</b></span>",
"format-calendar-weekdays": "<span color='#f4c7a1'><b>{}</b></span>"
},
"disk": {
+3 -10
View File
@@ -1,11 +1,4 @@
#!/bin/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
#!/usr/bin/env bash
# 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))"
else
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";
color: white;
}
window#waybar {
background: black;
}
.modules-right {
background: #000000;
}
.modules-center {
background: #000000;
color: #fff1e6;
}
window#waybar,
.modules-right,
.modules-center,
.modules-left {
background: #000000;
background: #050403;
}
#battery,
@@ -41,40 +33,49 @@ window#waybar {
min-width: 1ex;
padding-right: 5px;
}
#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,
#pulseaudio {
min-width: 5ex;
}
#pulseaudio.muted {
color: #FF0000;
}
#pulseaudio.mic.muted {
color: #ffffff;
}
#pulseaudio.muted,
#pulseaudio.mic:not(.source-muted) {
color: #fc0202;
color: #ff6b4a;
}
#pulseaudio.mic.muted {
color: #6e4a31;
}
#clock {
color: #f9780e;
color: #ff8800;
}
#battery.charging {
color: #ffffff;
background-color: #26A65B;
color: #1a0d00;
background-color: #c98a45;
}
#battery.warning:not(.charging) {
background-color: #ffbe61;
color: black;
background-color: #ffb05c;
color: #2b1600;
}
#battery.critical:not(.charging) {
background-color: #f53c3c;
color: #ffffff;
background-color: #ff6b4a;
color: #2b0700;
animation-name: blink;
animation-duration: 0.5s;
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("")
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 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()
noctalia.setUpdateInterval(2000)
@@ -23,7 +43,8 @@ function update()
request_in_flight = false
if result.exitCode ~= 0 then
barWidget.setText("CPU —")
renderStatus("CPU —", "#fff1e6")
barWidget.setTooltip("Unable to read CPU utilization")
return
end
@@ -47,12 +68,19 @@ function update()
previous[name] = { total = total, idle = idle }
end
barWidget.setGlyph("cpu")
if sampled > 0 then
barWidget.setText(string.format("%.0f%%", utilization))
barWidget.setTooltip("Aggregate CPU utilization (100% per logical CPU)")
-- Keep the aggregate display, but base its heat color on average
-- 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
barWidget.setText("—")
renderStatus("—", "#fff1e6")
barWidget.setTooltip("Sampling CPU utilization…")
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 {
if (!total || !available) exit 1
used = total - available
memory_used = used * 100 / total
if (swap_total > 0) {
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 {
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]]
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()
noctalia.setUpdateInterval(5000)
@@ -30,14 +51,32 @@ function update()
request_in_flight = false
if result.exitCode ~= 0 then
barWidget.setText("RAM —")
renderStatus("RAM —", "#fff1e6")
barWidget.setTooltip("Unable to read /proc/meminfo")
return
end
local text = string.gsub(result.stdout, "%s+$", "")
barWidget.setGlyph("memory")
barWidget.setText(text)
barWidget.setTooltip("RAM used / total · S = swap used")
local text, memory_percent, swap_percent = string.match(
result.stdout,
"^([^\t]+)\t([%d.]+)\t([%d.]+)"
)
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
@@ -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"
plugin_api = 3
author = "alex"
description = "Low-overhead memory and swap indicator for the bar."
description = "Low-overhead system and network status indicators for the bar."
[[widget]]
id = "cpu"
@@ -12,3 +12,23 @@ entry = "cpu.luau"
[[widget]]
id = "memory"
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
}
}
},
"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,
"lastChangelogVersion": "0.80.10",
"defaultProvider": "openai-codex",
"defaultModel": "gpt-5.6-sol",
"defaultModel": "gpt-5.6-terra",
"defaultThinkingLevel": "high",
"packages": [
"npm:pi-subagents",
@@ -18,11 +18,11 @@
"npm:bigpowers",
"npm:@gotgenes/pi-permission-system",
"npm:pi-intercom",
"npm:gentle-engram",
"npm:gentle-engram@0.1.8",
"npm:pi-lens",
"npm:@juicesharp/rpiv-todo",
"npm:@majorgilles/pi-grill-me",
"git:git.commumedia.org/alex/pi-map"
"npm:pi-mcp-adapter"
],
"powerline": "full",
"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
set -g default-terminal "screen-256color" # colors!
setw -g xterm-keys on
set -s escape-time 10 # faster command sequences
set -sg repeat-time 600 # increase repeat timeout
# --- Terminal and responsiveness ---------------------------------------------
set -g default-terminal "tmux-256color"
set -as terminal-features ",xterm-256color:RGB,screen-256color:RGB,tmux-256color:RGB"
set -sg escape-time 10
set -sg repeat-time 600
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
bind C-a send-prefix -2
# --- Prefix and defaults ------------------------------------------------------
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)
set -q -g utf8 on
setw -g mode-keys vi
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
bind e new-window -n "~/.tmux.conf.local" "sh -c '\${EDITOR:-vim} ~/.tmux.conf.local && tmux source ~/.tmux.conf && tmux display \"~/.tmux.conf sourced\"'"
# --- Vim-style panes ----------------------------------------------------------
# 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 source-file ~/.tmux.conf \; display '~/.tmux.conf sourced'
bind -r h select-pane -L
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
run-shell "powerline-daemon -q"
# source powerline tmux bindings
source "/usr/lib/python3.14/site-packages/powerline/bindings/tmux/powerline.conf"
# --- Windows, sessions, and buffers ------------------------------------------
bind -r f next-window
bind -r b previous-window
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
set -s set-clipboard off
# Keep tmux and the desktop clipboard in sync by default. `set-clipboard on`
# 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
setw -g pane-base-index 1 # make pane numbering consistent with windows
# --- Optional TPM plugins -----------------------------------------------------
# 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
set -g renumber-windows on # renumber windows when a window is closed
#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'
# Keep machine-specific bindings and experiments out of this tracked base file.
if-shell '[ -f ~/.tmux.conf.local ]' 'source-file ~/.tmux.conf.local'
+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
# - 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
# Examples:
# 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 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
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
echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.zshrc
echo '[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.zshrc
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"
if (( $+commands[pyenv] )); then
export PYENV_ROOT="${PYENV_ROOT:-$HOME/.pyenv}"
[[ -d $PYENV_ROOT/bin ]] && export PATH="$PYENV_ROOT/bin:$PATH"
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.
+3
View File
@@ -0,0 +1,3 @@
__pycache__/
*.py[cod]
.ruff_cache/
+79
View File
@@ -0,0 +1,79 @@
# system-config
A small, declarative Arch configuration managed by Decman.
## Design
- **Concerns live in `packages/`**: each concern has a native (`*-repo.txt`)
and AUR (`*-aur.txt`) list.
- **Hosts live in `hosts.py`**: a host composes concerns. `laptop()` contains
every explicit package. `desktop()` begins with shared concerns only.
- **System files live in `config/system/`**: shared files go in `common/`;
hardware, sleep/logind, and boot policy go in the relevant host directory.
- **State lives in `manifests/`**: units follow the same concern and host
scopes as packages.
- **Python glue lives in `modules/`**: package and system-state adapters only.
No yadm migration, user-home dotfiles, or application state is managed here.
Those can be added later as small, explicit modules.
## System policy
- `config/system/common/pacman/pacman.conf` enables only the official core,
extra, and multilib repositories. No debug repository is enabled.
- `config/system/common/makepkg/makepkg.conf` sets `OPTIONS` with `!debug`, so
newly built packages do not produce debug packages.
- `config/system/common/locale/` owns locale generation and system locale.
- `config/system/common/keyboard/` owns console and X11 keyboard policy.
- `config/system/laptop/sleep.conf.d/` owns sleep policy; the logind drop-in
remains in `config/system/laptop/systemd/`.
- `config/system/laptop/power/tlp.conf` owns laptop power policy. TLP reads
`/etc/tlp.conf` after drop-ins, so a whole-file policy is required here.
- `config/system/laptop/boot/mkinitcpio.conf` owns encrypted-root and resume
initramfs hooks; its module rebuilds the initramfs after a change.
- `config/system/laptop/refind/` owns the laptop ESP rEFInd configuration and
encrypted-root kernel options. It is intentionally laptop-only.
- `config/system/laptop/iwlwifi.conf` retains the Wi-Fi 6 stability workaround;
NetworkManager owns Wi-Fi power saving for battery use.
- `config/user/{common,laptop,desktop}/` owns selected active user dotfiles.
Host-specific Hyprland and Noctalia variants are selected by host composition.
- EnvyControl's generated NVIDIA blacklist and udev files are intentionally
unmanaged so GPU modes can be switched outside of Decman.
Changing the debug policy does not uninstall debug packages already installed;
remove those explicitly in a separate reviewed package change.
## Package concerns
| Scope | Concerns |
| --- | --- |
| Shared | core, wayland, fonts, development, productivity, media, research, network, services |
| Laptop | hardware, legacy; hardware units |
| Desktop | shared concerns now; add `packages/desktop/<concern>-{repo,aur}.txt` when hardware/software differs |
Every package must have a named purpose. Add a new concern when an existing one
does not explain a package. System and user units use the same naming:
`manifests/common/<concern>-{system,user}.txt` or
`manifests/laptop/<concern>-system.txt`.
## Commands
```bash
cd ~/system-config
./scripts/verify.py
sudo decman --source source.py --dry-run --no-hooks
sudo decman --source source.py --no-hooks
```
`verify.py` proves that every currently explicit package appears exactly once
in the active laptop profile. The ignored dependency lists prevent Decman's
orphan cleanup from removing dependencies during adoption.
After intentional package changes, regenerate and review concern lists:
```bash
./scripts/split_packages.py
./scripts/refresh_orphan_guards.py
./scripts/verify.py
git diff
```
@@ -0,0 +1,6 @@
# Written by systemd-localed(8) or systemd-firstboot(1), read by systemd-localed
# and systemd-vconsole-setup(8). Use localectl(1) to update this file.
KEYMAP=us
XKBLAYOUT=us
XKBMODEL=variant
XKBVARIANT=altgr-intl
@@ -0,0 +1,9 @@
# Written by systemd-localed(8), read by systemd-localed and Xorg. It's
# probably wise not to edit this file manually. Use localectl(1) to
# update this file.
Section "InputClass"
Identifier "system-keyboard"
MatchIsKeyboard "on"
Option "XkbLayout" "us"
Option "XkbVariant" "intl"
EndSection
@@ -0,0 +1,20 @@
[ids]
*
[main]
capslock = layer(capslayer)
' = G-'
[capslayer]
j = down
h = left
k = up
l = right
a = G-q
o = G-p
u = G-y
s = G-s
e = G-5
@@ -0,0 +1,3 @@
# This is the fallback locale configuration provided by systemd.
LANG="en_US.UTF-8"
@@ -0,0 +1 @@
en_US.UTF-8 UTF-8
@@ -0,0 +1,168 @@
#!/hint/bash
# shellcheck disable=2034
#
# /etc/makepkg.conf
#
#########################################################################
# SOURCE ACQUISITION
#########################################################################
#
#-- The download utilities that makepkg should use to acquire sources
# Format: 'protocol::agent'
DLAGENTS=('file::/usr/bin/curl -qgC - -o %o %u'
'ftp::/usr/bin/curl -qgfC - --ftp-pasv --retry 3 --retry-delay 3 -o %o %u'
'http::/usr/bin/curl -qgb "" -fLC - --retry 3 --retry-delay 3 -o %o %u'
'https::/usr/bin/curl -qgb "" -fLC - --retry 3 --retry-delay 3 -o %o %u'
'rsync::/usr/bin/rsync --no-motd -z %u %o'
'scp::/usr/bin/scp -C %u %o')
# Other common tools:
# /usr/bin/snarf
# /usr/bin/lftpget -c
# /usr/bin/wget
#-- The package required by makepkg to download VCS sources
# Format: 'protocol::package'
VCSCLIENTS=('bzr::breezy'
'fossil::fossil'
'git::git'
'hg::mercurial'
'svn::subversion')
#########################################################################
# ARCHITECTURE, COMPILE FLAGS
#########################################################################
#
CARCH="x86_64"
CHOST="x86_64-pc-linux-gnu"
#NPROC=2
#-- Compiler and Linker Flags
#CPPFLAGS=""
CFLAGS="-march=x86-64 -mtune=generic -O2 -pipe -fno-plt -fexceptions \
-Wp,-D_FORTIFY_SOURCE=3 -Wformat -Werror=format-security \
-fstack-clash-protection -fcf-protection \
-fno-omit-frame-pointer -mno-omit-leaf-frame-pointer"
CXXFLAGS="$CFLAGS -Wp,-D_GLIBCXX_ASSERTIONS"
LDFLAGS="-Wl,-O1 -Wl,--sort-common -Wl,--as-needed -Wl,-z,relro -Wl,-z,now \
-Wl,-z,pack-relative-relocs"
LTOFLAGS="-flto=auto"
#-- Make Flags: change this for DistCC/SMP systems
#MAKEFLAGS="-j2"
#-- Debugging flags
DEBUG_CFLAGS="-g"
DEBUG_CXXFLAGS="$DEBUG_CFLAGS"
#########################################################################
# BUILD ENVIRONMENT
#########################################################################
#
# Makepkg defaults: BUILDENV=(!distcc !color !ccache check !sign)
# A negated environment option will do the opposite of the comments below.
#
#-- distcc: Use the Distributed C/C++/ObjC compiler
#-- color: Colorize output messages
#-- ccache: Use ccache to cache compilation
#-- check: Run the check() function if present in the PKGBUILD
#-- sign: Generate PGP signature file
#
BUILDENV=(!distcc color !ccache check !sign)
#
#-- If using DistCC, your MAKEFLAGS will also need modification. In addition,
#-- specify a space-delimited list of hosts running in the DistCC cluster.
#DISTCC_HOSTS=""
#
#-- Specify a directory for package building.
#BUILDDIR=/tmp/makepkg
#########################################################################
# GLOBAL PACKAGE OPTIONS
# These are default values for the options=() settings
#########################################################################
#
# Makepkg defaults:
# OPTIONS=(!strip docs libtool staticlibs emptydirs !zipman !purge !debug !lto !autodeps)
# A negated option will do the opposite of the comments below.
#
#-- strip: Strip symbols from binaries/libraries
#-- docs: Save doc directories specified by DOC_DIRS
#-- libtool: Leave libtool (.la) files in packages
#-- staticlibs: Leave static library (.a) files in packages
#-- emptydirs: Leave empty directories in packages
#-- zipman: Compress manual (man and info) pages in MAN_DIRS with gzip
#-- purge: Remove files specified by PURGE_TARGETS
#-- debug: Add debugging flags as specified in DEBUG_* variables
#-- lto: Add compile flags for building with link time optimization
#-- autodeps: Automatically add depends/provides
#
OPTIONS=(strip docs !libtool !staticlibs emptydirs zipman purge !debug lto)
#-- File integrity checks to use. Valid: md5, sha1, sha224, sha256, sha384, sha512, b2
INTEGRITY_CHECK=(sha256)
#-- Options to be used when stripping binaries. See `man strip' for details.
STRIP_BINARIES="--strip-all"
#-- Options to be used when stripping shared libraries. See `man strip' for details.
STRIP_SHARED="--strip-unneeded"
#-- Options to be used when stripping static libraries. See `man strip' for details.
STRIP_STATIC="--strip-debug"
#-- Manual (man and info) directories to compress (if zipman is specified)
MAN_DIRS=(usr{,/local}{,/share}/{man,info})
#-- Doc directories to remove (if !docs is specified)
DOC_DIRS=(usr/{,local/}{,share/}{doc,gtk-doc})
#-- Files to be removed from all packages (if purge is specified)
PURGE_TARGETS=(usr/{,share}/info/dir .packlist *.pod)
#-- Directory to store source code in for debug packages
DBGSRCDIR="/usr/src/debug"
#-- Prefix and directories for library autodeps
LIB_DIRS=('lib:usr/lib' 'lib32:usr/lib32')
#########################################################################
# PACKAGE OUTPUT
#########################################################################
#
# Default: put built package and cached source in build directory
#
#-- Destination: specify a fixed directory where all packages will be placed
#PKGDEST=/home/packages
#-- Source cache: specify a fixed directory where source files will be cached
#SRCDEST=/home/sources
#-- Source packages: specify a fixed directory where all src packages will be placed
#SRCPKGDEST=/home/srcpackages
#-- Log files: specify a fixed directory where all log files will be placed
#LOGDEST=/home/makepkglogs
#-- Packager: name/email of the person or organization building packages
#PACKAGER="John Doe <john@doe.com>"
#-- Specify a key to use for package signing
#GPGKEY=""
#########################################################################
# COMPRESSION DEFAULTS
#########################################################################
#
COMPRESSGZ=(gzip -c -f -n)
COMPRESSBZ2=(bzip2 -c -f)
COMPRESSXZ=(xz -c -z -)
COMPRESSZST=(zstd -c -T0 -)
COMPRESSLRZ=(lrzip -q)
COMPRESSLZO=(lzop -q)
COMPRESSZ=(compress -c -f)
COMPRESSLZ4=(lz4 -q)
COMPRESSLZ=(lzip -c -f)
#########################################################################
# EXTENSION DEFAULTS
#########################################################################
#
PKGEXT='.pkg.tar.zst'
SRCEXT='.src.tar.gz'
#########################################################################
# OTHER
#########################################################################
#
#-- Command used to run pacman as root, instead of trying sudo and su
#PACMAN_AUTH=()
# vim: set ft=sh ts=2 sw=2 et:
@@ -0,0 +1,99 @@
#
# /etc/pacman.conf
#
# See the pacman.conf(5) manpage for option and repository directives
#
# GENERAL OPTIONS
#
[options]
# The following paths are commented out with their default values listed.
# If you wish to use different paths, uncomment and update the paths.
#RootDir = /
#DBPath = /var/lib/pacman/
#CacheDir = /var/cache/pacman/pkg/
#LogFile = /var/log/pacman.log
#GPGDir = /etc/pacman.d/gnupg/
#HookDir = /etc/pacman.d/hooks/
HoldPkg = pacman glibc
#XferCommand = /usr/bin/curl -L -C - -f -o %o %u
#XferCommand = /usr/bin/wget --passive-ftp -c -O %o %u
#CleanMethod = KeepInstalled
Architecture = auto
# Pacman won't upgrade packages listed in IgnorePkg and members of IgnoreGroup
#IgnorePkg =
#IgnoreGroup =
#NoUpgrade =
#NoExtract =
# Misc options
#UseSyslog
#Color
#NoProgressBar
CheckSpace
#VerbosePkgLists
#ParallelDownloads = 5
DownloadUser = alpm
#DisableSandbox
# By default, pacman accepts packages signed by keys that its local keyring
# trusts (see pacman-key and its man page), as well as unsigned packages.
SigLevel = Required DatabaseOptional
LocalFileSigLevel = Optional
#RemoteFileSigLevel = Required
# NOTE: You must run `pacman-key --init` before first using pacman; the local
# keyring can then be populated with the keys of all official Arch Linux
# packagers with `pacman-key --populate archlinux`.
#
# REPOSITORIES
# - can be defined here or included from another file
# - pacman will search repositories in the order defined here
# - local/custom mirrors can be added here or in separate files
# - repositories listed first will take precedence when packages
# have identical names, regardless of version number
# - URLs will have $repo replaced by the name of the current repo
# - URLs will have $arch replaced by the name of the architecture
#
# Repository entries are of the format:
# [repo-name]
# Server = ServerName
# Include = IncludePath
#
# The header [repo-name] is crucial - it must be present and
# uncommented to enable the repo.
#
# The testing repositories are disabled by default. To enable, uncomment the
# repo name header and Include lines. You can add preferred servers immediately
# after the header, and they will be used before the default mirrors.
#[core-testing]
#Include = /etc/pacman.d/mirrorlist
[core]
Include = /etc/pacman.d/mirrorlist
#[extra-testing]
#Include = /etc/pacman.d/mirrorlist
[extra]
Include = /etc/pacman.d/mirrorlist
# If you want to run 32 bit applications on your x86_64 system,
# enable the multilib repositories as required here.
#[multilib-testing]
#Include = /etc/pacman.d/mirrorlist
[multilib]
Include = /etc/pacman.d/mirrorlist
# An example of a custom package repository. See the pacman manpage for
# tips on creating your own repositories.
#[custom]
#SigLevel = Optional TrustAll
#Server = file:///home/custompkgs
@@ -0,0 +1,81 @@
# vim:set ft=sh
# MODULES
# The following modules are loaded before any boot hooks are
# run. Advanced users may wish to specify all system modules
# in this array. For instance:
# MODULES=(usbhid xhci_hcd)
MODULES=()
# BINARIES
# This setting includes any additional binaries a given user may
# wish into the CPIO image. This is run last, so it may be used to
# override the actual binaries included by a given hook
# BINARIES are dependency parsed, so you may safely ignore libraries
BINARIES=()
# FILES
# This setting is similar to BINARIES above, however, files are added
# as-is and are not parsed in any way. This is useful for config files.
FILES=()
# HOOKS
# This is the most important setting in this file. The HOOKS control the
# modules and scripts added to the image, and what happens at boot time.
# Order is important, and it is recommended that you do not change the
# order in which HOOKS are added. Run 'mkinitcpio -H <hook name>' for
# help on a given hook.
# 'base' is _required_ unless you know precisely what you are doing.
# 'udev' is _required_ in order to automatically load modules
# 'filesystems' is _required_ unless you specify your fs modules in MODULES
# Examples:
## This setup specifies all modules in the MODULES setting above.
## No RAID, lvm2, or encrypted root is needed.
# HOOKS=(base)
#
## This setup will autodetect all modules for your system and should
## work as a sane default
# HOOKS=(base udev autodetect modconf block filesystems fsck)
#
## This setup will generate a 'full' image which supports most systems.
## No autodetection is done.
# HOOKS=(base udev modconf block filesystems fsck)
#
## This setup assembles a mdadm array with an encrypted root file system.
## Note: See 'mkinitcpio -H mdadm_udev' for more information on RAID devices.
# HOOKS=(base udev modconf keyboard keymap consolefont block mdadm_udev encrypt filesystems fsck)
#
## This setup loads an lvm2 volume group.
# HOOKS=(base udev modconf block lvm2 filesystems fsck)
#
## This will create a systemd based initramfs which loads an encrypted root filesystem.
# HOOKS=(base systemd autodetect modconf kms keyboard sd-vconsole sd-encrypt block filesystems fsck)
#
## NOTE: If you have /usr on a separate partition, you MUST include the
# usr and fsck hooks.
HOOKS=(base udev autodetect microcode modconf kms keyboard keymap consolefont block encrypt filesystems resume fsck)
# COMPRESSION
# Use this to compress the initramfs image. By default, zstd compression
# is used for Linux ≥ 5.9 and gzip compression is used for Linux < 5.9.
# Use 'cat' to create an uncompressed image.
#COMPRESSION="zstd"
#COMPRESSION="gzip"
#COMPRESSION="bzip2"
#COMPRESSION="lzma"
#COMPRESSION="xz"
#COMPRESSION="lzop"
#COMPRESSION="lz4"
# COMPRESSION_OPTIONS
# Additional options for the compressor
#COMPRESSION_OPTIONS=()
# MODULES_DECOMPRESS
# Decompress loadable kernel modules and their firmware during initramfs
# creation. Switch (yes/no).
# Enable to allow further decreasing image size when using high compression
# (e.g. xz -9e or zstd --long --ultra -22) at the expense of increased RAM usage
# at early boot.
# Note that any compressed files will be placed in the uncompressed early CPIO
# to avoid double compression.
#MODULES_DECOMPRESS="no"
@@ -0,0 +1,3 @@
# Keep Wi-Fi 6 disabled for this laptop's known stability workaround.
# NetworkManager owns association-level Wi-Fi power saving.
options iwlwifi disable_11ax=1
@@ -0,0 +1,11 @@
DEVICES_TO_DISABLE_ON_STARTUP="bluetooth"
DEVICES_TO_DISABLE_ON_BAT_NOT_IN_USE="bluetooth"
CPU_ENERGY_PERF_POLICY_ON_AC=balance
CPU_ENERGY_PERF_POLICY_ON_BAT=power
CPU_BOOST_ON_AC=1
CPU_BOOST_ON_BAT=0
CPU_MAX_PERF_ON_AC=100
CPU_MAX_PERF_ON_BAT=70
@@ -0,0 +1,732 @@
#
# refind.conf
# Configuration file for the rEFInd boot menu
#
# Timeout in seconds for the main menu screen. Setting the timeout to 0
# disables automatic booting (i.e., no timeout). Setting it to -1 causes
# an immediate boot to the default OS *UNLESS* a keypress is in the buffer
# when rEFInd launches, in which case that keypress is interpreted as a
# shortcut key. If no matching shortcut is found, rEFInd displays its
# menu with no timeout.
#
timeout 20
# Set the logging level. When set to 0, rEFInd does not log its actions.
# When set to 1 or above, rEFInd creates a file called refind.log in
# its home directory on the ESP and records information about what it's
# doing. Higher values record more information, up to a maximum of 4.
# This token should be left at the default of 0 except when debugging
# problems.
# Default value is 0
#
#log_level 1
# Normally, when the timeout period has passed, rEFInd boots the
# default_selection. If the following option is uncommented, though,
# rEFInd will instead attempt to shut down the computer.
# CAUTION: MANY COMPUTERS WILL INSTEAD HANG OR REBOOT! Macs and more
# recent UEFI-based PCs are most likely to work with this feature.
# Default value is true
#
#shutdown_after_timeout
# Whether to store rEFInd's rEFInd-specific variables in NVRAM (1, true,
# or on) or in files in the "vars" subdirectory of rEFInd's directory on
# disk (0, false, or off). Using NVRAM works well with most computers;
# however, it increases wear on the motherboard's NVRAM, and if the EFI
# is buggy or the NVRAM is old and worn out, it may not work at all.
# Storing variables on disk is a viable alternative in such cases, or
# if you want to minimize wear and tear on the NVRAM; however, it won't
# work if rEFInd is stored on a filesystem that's read-only to the EFI
# (such as an HFS+ volume), and it increases the risk of filesystem
# damage. Note that this option affects ONLY rEFInd's own variables,
# such as the PreviousBoot, HiddenTags, HiddenTools, and HiddenLegacy
# variables. It does NOT affect Secure Boot or other non-rEFInd
# variables.
# Default is true
#
use_nvram false
# Screen saver timeout; the screen blanks after the specified number of
# seconds with no keyboard input. The screen returns after most keypresses
# (unfortunately, not including modifier keys such as Shift, Control, Alt,
# or Option). Setting a value of "-1" causes rEFInd to start up with its
# screen saver active. The default is 0, which disables the screen saver.
#
#screensaver 300
# Hide user interface elements for personal preference or to increase
# security:
# banner - the rEFInd title banner (built-in or loaded via "banner")
# label - boot option text label in the menu
# singleuser - remove the submenu options to boot macOS in single-user
# or verbose modes; affects ONLY macOS
# safemode - remove the submenu option to boot macOS in "safe mode"
# hwtest - the submenu option to run Apple's hardware test
# arrows - scroll arrows on the OS selection tag line
# hints - brief command summary in the menu
# editor - the options editor (+, F2, or Insert on boot options menu)
# badges - device-type badges for boot options
# all - all of the above
# Default is none of these (all elements active)
#
#hideui singleuser
#hideui all
# Set the name of a subdirectory in which icons are stored. Icons must
# have the same names they have in the standard directory. The directory
# name is specified relative to the main rEFInd binary's directory. If
# an icon can't be found in the specified directory, an attempt is made
# to load it from the default directory; thus, you can replace just some
# icons in your own directory and rely on the default for others.
# Icon files may be in any supported format -- ICNS (*.icns), BMP (*.bmp),
# PNG (*.png), or JPEG (*.jpg or *.jpeg); however, rEFInd's BMP and JPEG
# implementations do not support transparency, which is highly desirable
# in icons.
# Default is "icons".
#
#icons_dir myicons
#icons_dir icons/snowy
# Use a custom title banner instead of the rEFInd icon and name. The file
# path is relative to the directory where refind.efi is located. The color
# in the top left corner of the image is used as the background color
# for the menu screens. Currently uncompressed BMP images with color
# depths of 24, 8, 4 or 1 bits are supported, as well as PNG and JPEG
# images. (ICNS images can also be used, but ICNS has limitations that
# make it a poor choice for this purpose.) PNG and JPEG support is
# limited by the underlying libraries; some files, like progressive JPEGs,
# will not work.
#
#banner hostname.bmp
#banner mybanner.jpg
#banner icons/snowy/banner-snowy.png
# Specify how to handle banners that aren't exactly the same as the screen
# size:
# noscale - Crop if too big, show with border if too small
# fillscreen - Fill the screen
# Default is noscale
#
#banner_scale fillscreen
# Icon sizes. All icons are square, so just one value is specified. The
# big icons are used for OS selectors in the first row and the small
# icons are used for tools on the second row. Drive-type badges are 1/4
# the size of the big icons. Legal values are 32 and above. If the icon
# files do not hold icons of the proper size, the icons are scaled to
# the specified size. The default values are 48 and 128 for small and
# big icons, respectively.
#
#small_icon_size 96
#big_icon_size 256
# Custom images for the selection background. There is a big one (144 x 144)
# for the OS icons, and a small one (64 x 64) for the function icons in the
# second row. If only a small image is given, that one is also used for
# the big icons by stretching it in the middle. If only a big one is given,
# the built-in default will be used for the small icons. If an image other
# than the optimal size is specified, it will be scaled in a way that may
# be ugly.
#
# Like the banner option above, these options take a filename of an
# uncompressed BMP, PNG, JPEG, or ICNS image file with a color depth of
# 24, 8, 4, or 1 bits. The PNG or ICNS format is required if you need
# transparency support (to let you "see through" to a full-screen banner).
#
#selection_big selection-big.bmp
#selection_small selection-small.bmp
# Set the font to be used for all textual displays in graphics mode.
# For best results, the font must be a PNG file with alpha channel
# transparency. It must contain ASCII characters 32-126 (space through
# tilde), inclusive, plus a glyph to be displayed in place of characters
# outside of this range, for a total of 96 glyphs. Only monospaced fonts
# are supported. Fonts may be of any size, although large fonts can
# produce display irregularities.
# The default is rEFInd's built-in font, Luxi Mono Regular 12 point.
#
#font myfont.png
# Use text mode only. When enabled, this option forces rEFInd into text mode.
# Passing this option a "0" value causes graphics mode to be used. Pasing
# it no value or any non-0 value causes text mode to be used.
# Default is to use graphics mode.
#
#textonly
# Set the EFI text mode to be used for textual displays. This option
# takes a single digit that refers to a mode number. Mode 0 is normally
# 80x25, 1 is sometimes 80x50, and higher numbers are system-specific
# modes. Mode 1024 is a special code that tells rEFInd to not set the
# text mode; it uses whatever was in use when the program was launched.
# If you specify an invalid mode, rEFInd pauses during boot to inform
# you of valid modes.
# CAUTION: On VirtualBox, and perhaps on some real computers, specifying
# a text mode and uncommenting the "textonly" option while NOT specifying
# a resolution can result in an unusable display in the booted OS.
# Default is 1024 (no change)
#
#textmode 2
# Set the screen's video resolution. Pass this option one of the following:
# * two integer values, corresponding to the X and Y resolutions
# * one integer value, corresponding to a GOP (UEFI) video mode
# * the string "max", which sets the maximum available resolution
# Note that not all resolutions are supported. On UEFI systems, passing
# an incorrect value results in a message being shown on the screen to
# that effect, along with a list of supported modes. On EFI 1.x systems
# (e.g., Macintoshes), setting an incorrect mode silently fails. On both
# types of systems, setting an incorrect resolution results in the default
# resolution being used. A resolution of 1024x768 usually works, but higher
# values often don't.
# Default is "0 0" (use the system default resolution, usually 800x600).
#
#resolution 1024 768
#resolution 1440 900
#resolution 3
#resolution max
# Enable touch screen support. If active, this feature enables use of
# touch screen controls (as on tablets). Note, however, that not all
# tablets' EFIs provide the necessary underlying support, so this
# feature may not work for you. If it does work, you should be able
# to launch an OS or tool by touching it. In a submenu, touching
# anywhere launches the currently-selection item; there is, at present,
# no way to select a specific submenu item. This feature is mutually
# exclusive with the enable_mouse feature. If both are uncommented,
# the one read most recently takes precedence.
#
#enable_touch
# Enable mouse support. If active, this feature enables use of the
# computer's mouse. Note, however, that not all computers' EFIs
# provide the necessary underlying support, so this feature may not
# work for you. If it does work, you should be able to launch an
# OS or tool by clicking it with the mouse pointer. This feature
# is mutually exclusive with the enable_touch feature. If both
# are uncommented, the one read most recently takes precedence.
#
#enable_mouse
# Size of the mouse pointer, in pixels, per side.
# Default is 16
#
#mouse_size 16
# Speed of mouse tracking. Higher numbers equate to faster
# mouse movement. This option requires that enable_mouse be
# uncommented.
# Legal values are between 1 and 32. Default is 4.
#
#mouse_speed 4
# Launch specified OSes in graphics mode. By default, rEFInd switches
# to text mode and displays basic pre-launch information when launching
# all OSes except macOS. Using graphics mode can produce a more seamless
# transition, but displays no information, which can make matters
# difficult if you must debug a problem. Also, on at least one known
# computer, using graphics mode prevents a crash when using the Linux
# kernel's EFI stub loader. You can specify an empty list to boot all
# OSes in text mode.
# Valid options:
# osx - macOS
# linux - A Linux kernel with EFI stub loader
# elilo - The ELILO boot loader
# grub - The GRUB (Legacy or 2) boot loader
# windows - Microsoft Windows
# Default value: osx
#
#use_graphics_for osx,linux
# Which non-bootloader tools to show on the tools line, and in what
# order to display them:
# shell - the EFI shell (requires external program; see rEFInd
# documentation for details)
# memtest - the memtest86 program, in EFI/tools, EFI/memtest86,
# EFI/memtest, EFI/tools/memtest86, or EFI/tools/memtest
# gptsync - the (dangerous) gptsync.efi utility (requires external
# program; see rEFInd documentation for details)
# gdisk - the gdisk partitioning program
# apple_recovery - boots the Apple Recovery HD partition, if present
# windows_recovery - boots an OEM Windows recovery tool, if present
# (see also the windows_recovery_files option)
# mok_tool - makes available the Machine Owner Key (MOK) maintenance
# tool, MokManager.efi, used on Secure Boot systems
# csr_rotate - adjusts Apple System Integrity Protection (SIP)
# policy. Requires "csr_values" to be set.
# install - an option to install rEFInd from the current location
# to another ESP
# bootorder - adjust the EFI's (NOT rEFInd's) boot order
# about - an "about this program" option
# hidden_tags - manage hidden tags
# exit - a tag to exit from rEFInd
# shutdown - shuts down the computer (a bug causes this to reboot
# many UEFI systems)
# reboot - a tag to reboot the computer
# firmware - a tag to reboot the computer into the firmware's
# user interface (ignored on older computers)
# fwupdate - a tag to update the firmware; launches the fwupx64.efi
# (or similar) program
# netboot - launch the ipxe.efi tool for network (PXE) booting
# Default is shell,memtest,gdisk,apple_recovery,windows_recovery,mok_tool,about,hidden_tags,shutdown,reboot,firmware,fwupdate
# To completely disable scanning for all tools, provide a showtools line
# with no options.
#
#showtools shell, bootorder, gdisk, memtest, mok_tool, apple_recovery, windows_recovery, about, hidden_tags, reboot, exit, firmware, fwupdate
# Tool binaries to be excluded from the tools line, even if the
# general class is specified in showtools. This enables trimming an
# overabundance of tools, as when you see multiple mok_tool entries
# after installing multiple Linux distributions.
# Just as with dont_scan_files, you can specify a filename alone, a
# full pathname, or a volume identifier (filesystem label, partition
# name, or partition GUID) and a full pathname.
# Default is an empty list (nothing is excluded)
#
#dont_scan_tools ESP2:/EFI/ubuntu/mmx64.efi,gptsync_x64.efi
# Boot loaders that can launch a Windows restore or emergency system.
# These tend to be OEM-specific.
# Default is LRS_ESP:/EFI/Microsoft/Boot/LrsBootmgr.efi
#
#windows_recovery_files LRS_ESP:/EFI/Microsoft/Boot/LrsBootmgr.efi
# Directories in which to search for EFI drivers. These drivers can
# provide filesystem support, give access to hard disks on plug-in
# controllers, etc. In most cases none are needed, but if you add
# EFI drivers and you want rEFInd to automatically load them, you
# should specify one or more paths here. rEFInd always scans the
# "drivers" and "drivers_{arch}" subdirectories of its own installation
# directory (where "{arch}" is your architecture code); this option
# specifies ADDITIONAL directories to scan.
# Default is to scan no additional directories for EFI drivers
#
#scan_driver_dirs EFI/tools/drivers,drivers
# Which types of boot loaders to search, and in what order to display them:
# internal - internal EFI disk-based boot loaders
# external - external EFI disk-based boot loaders
# optical - EFI optical discs (CD, DVD, etc.)
# netboot - EFI network (PXE) boot options
# hdbios - BIOS disk-based boot loaders
# biosexternal - BIOS external boot loaders (USB, eSATA, etc.)
# cd - BIOS optical-disc boot loaders
# manual - use stanzas later in this configuration file
# firmware - boot EFI programs set in the firmware's NVRAM
# Note that the legacy BIOS options require firmware support, which is
# not present on all computers.
# The netboot option is experimental and relies on the ipxe.efi and
# ipxe_discover.efi program files.
# On UEFI PCs, default is internal,external,optical,manual
# On Macs, default is internal,hdbios,external,biosexternal,optical,cd,manual
#
#scanfor internal,external,optical,manual,firmware
# By default, rEFInd relies on the UEFI firmware to detect BIOS-mode boot
# devices. This sometimes doesn't detect all the available devices, though.
# For these cases, uefi_deep_legacy_scan results in a forced scan and
# modification of NVRAM variables on each boot. Adding "0", "off", or
# "false" resets to the default value. This token has no effect on Macs or
# when no BIOS-mode options are set via scanfor.
# Default is unset (or "uefi_deep_legacy_scan false")
#
#uefi_deep_legacy_scan
# Delay for the specified number of seconds before scanning disks.
# This can help some users who find that some of their disks
# (usually external or optical discs) aren't detected initially,
# but are detected after pressing Esc.
# The default is 0.
#
#scan_delay 5
# When scanning volumes for EFI boot loaders, rEFInd always looks for
# macOS's and Microsoft Windows' boot loaders in their normal locations,
# and scans the root directory and every subdirectory of the /EFI directory
# for additional boot loaders, but it doesn't recurse into these directories.
# The also_scan_dirs token adds more directories to the scan list.
# Directories are specified relative to the volume's root directory. This
# option applies to ALL the volumes that rEFInd scans UNLESS you include
# a volume name and colon before the directory name, as in "myvol:/somedir"
# to scan the somedir directory only on the filesystem named myvol. If a
# specified directory doesn't exist, it's ignored (no error condition
# results). The "+" symbol denotes appending to the list of scanned
# directories rather than overwriting that list.
# The default is to scan the "boot" and "@/boot" directories in addition
# to various hard-coded directories.
#
#also_scan_dirs boot,ESP2:EFI/linux/kernels
#also_scan_dirs boot,@/boot
#also_scan_dirs +,@/kernels
# Partitions (or whole disks, for legacy-mode boots) to omit from scans.
# For EFI-mode scans, you normally specify a volume by its label, which you
# can obtain in an EFI shell by typing "vol", from Linux by typing
# "blkid /dev/{devicename}", or by examining the disk's label in various
# OSes' file browsers. It's also possible to identify a partition by its
# unique GUID (aka its "PARTUUID" in Linux parlance). (Note that this is
# NOT the partition TYPE CODE GUID.) This identifier can be obtained via
# "blkid" in Linux or "diskutil info {partition-id}" in macOS.
# For legacy-mode scans, you can specify any subset of the boot loader
# description shown when you highlight the option in rEFInd.
# The default is "LRS_ESP".
#
#dont_scan_volumes "Recovery HD"
# Directories that should NOT be scanned for boot loaders. By default,
# rEFInd doesn't scan its own directory, the EFI/tools directory, the
# EFI/memtest directory, the EFI/memtest86 directory, or the
# com.apple.recovery.boot directory. Using the dont_scan_dirs option
# enables you to "blacklist" other directories; but be sure to use "+"
# as the first element if you want to continue blacklisting existing
# directories. You might use this token to keep EFI/boot/bootx64.efi out
# of the menu if that's a duplicate of another boot loader or to exclude
# a directory that holds drivers or non-bootloader utilities provided by
# a hardware manufacturer. If a directory is listed both here and in
# also_scan_dirs, dont_scan_dirs takes precedence. Note that this
# blacklist applies to ALL the filesystems that rEFInd scans, not just
# the ESP, unless you precede the directory name by a filesystem name or
# partition unique GUID, as in "myvol:EFI/somedir" to exclude EFI/somedir
# from the scan on the myvol volume but not on other volumes.
#
#dont_scan_dirs ESP:/EFI/boot,EFI/Dell,EFI/memtest86
# Files that should NOT be included as EFI boot loaders (on the
# first line of the display). If you're using a boot loader that
# relies on support programs or drivers that are installed alongside
# the main binary or if you want to "blacklist" certain loaders by
# name rather than location, use this option. Note that this will
# NOT prevent certain binaries from showing up in the second-row
# set of tools. Most notably, various Secure Boot and recovery
# tools are present in this list, but may appear as second-row
# items.
# The file may be specified as a bare name (e.g., "notme.efi"), as
# a complete pathname (e.g., "/EFI/somedir/notme.efi"), or as a
# complete pathname with volume (e.g., "SOMEDISK:/EFI/somedir/notme.efi"
# or 2C17D5ED-850D-4F76-BA31-47A561740082:/EFI/somedir/notme.efi").
# OS tags hidden via the Delete or '-' key in the rEFInd menu are
# added to this list, but stored in NVRAM.
# The default is shim.efi,shim-fedora.efi,shimx64.efi,PreLoader.efi,
# TextMode.efi,ebounce.efi,GraphicsConsole.efi,MokManager.efi,HashTool.efi,
# HashTool-signed.efi,bootmgr.efi,fb{arch}.efi
# (where "{arch}" is the architecture code, like "x64").
# If you want to keep these defaults but add to them, be sure to
# specify "+" as the first item in the new list; if you don't, then
# items from the default list are likely to appear.
#
#dont_scan_files shim.efi,MokManager.efi
# EFI NVRAM Boot#### variables that should NOT be presented as loaders
# when "firmware" is an option to "scanfor". The comma-separated list
# presented here contains strings that are matched against the
# description field -- if a value here is a case-insensitive substring
# of the boot option description, then it will be excluded from the
# boot list. To specify a string that includes a space, enclose it
# in quotes. Specifying "shell" will counteract the automatic
# inclusion of built-in EFI shells.
#
#dont_scan_firmware HARDDISK,shell,"Removable Device"
# Scan for Linux kernels that lack a ".efi" filename extension. This is
# useful for better integration with Linux distributions that provide
# kernels with EFI stub loaders but that don't give those kernels filenames
# that end in ".efi", particularly if the kernels are stored on a
# filesystem that the EFI can read. When set to "1", "true", or "on", this
# option causes all files in scanned directories with names that begin with
# "vmlinuz", "bzImage", or "kernel" to be included as loaders, even if they
# lack ".efi" extensions. Passing this option a "0", "false", or "off" value
# causes kernels without ".efi" extensions to NOT be scanned.
# Default is "true" -- to scan for kernels without ".efi" extensions.
#
#scan_all_linux_kernels false
# Support loaders that have been compressed with gzip.
# On x86 and x86-64 platforms, Linux kernels are self-decompressing.
# On ARM64, Linux kernel files are typically compressed with gzip,
# including the EFI stub loader. This makes them unloadable in rEFInd
# unless rEFInd itself uncompresses them. This option enables rEFInd
# to do this. This feature is unnecessary on x86 and x86-64 systems.
# Default is "false" on x86 and x86-64; "true" on ARM64.
#
#support_gzipped_loaders true
# Combine all Linux kernels in a given directory into a single entry.
# When so set, the kernel with the most recent time stamp will be launched
# by default, and its filename will appear in the entry's description.
# To launch other kernels, the user must press F2 or Insert; alternate
# kernels then appear as options on the sub-menu.
# Default is "true" -- kernels are "folded" into a single menu entry.
#
#fold_linux_kernels false
# Comma-delimited list of strings to treat as if they were numbers for the
# purpose of kernel version number detection. These strings are matched on a
# first-found basis; that is, if you want to treat both "linux-lts" and
# "linux" as version strings, they MUST be specified as "linux-lts,linux",
# since if you specify it the other way, both vmlinuz-linux and
# vmlinuz-linux-lts will return with "linux" as the "version string," which
# is not what you'd want. Also, if the kernel or initrd file includes both a
# specified string and digits, the "version string" includes both. For
# instance, "vmlinuz-linux-4.8" would yield a version string of "linux-4.8".
# This option is intended for Arch and other distributions that don't include
# version numbers in their kernel filenames, but may provide other uniquely
# identifying strings for multiple kernels. If this feature causes problems
# (say, if your kernel filename includes "linux" but the initrd filename
# doesn't), be sure this is set to an empty string
# (extra_kernel_version_strings "") or comment out the option to disable it.
# Default is no extra version strings
#
#extra_kernel_version_strings linux-lts,linux
# Write to systemd EFI variables (currently only LoaderDevicePartUUID) when
# launching Linux via an EFI stub loader, ELILO, or GRUB. This variable,
# when present, causes systemd to mount the ESP at /boot or /efi *IF* either
# directory is empty and nothing else is mounted there.
# Default is "false"
#
#write_systemd_vars true
# Symlinked loaders will be processed when this setting is set to true.
# These are ignored by default as they may result in undesirable outcomes.
# This token may, however, be useful on Linux setups that provide symbolic
# links in scanned locations that point to kernels in unscanned locations,
# such as some openSUSE installations.
#
#follow_symlinks true
# Set the maximum number of tags that can be displayed on the screen at
# any time. If more loaders are discovered than this value, rEFInd shows
# a subset in a scrolling list. If this value is set too high for the
# screen to handle, it's reduced to the value that the screen can manage.
# If this value is set to 0 (the default), it's adjusted to the number
# that the screen can handle.
#
#max_tags 0
# Set the default menu selection. The available arguments match the
# keyboard accelerators available within rEFInd. You may select the
# default loader using:
# - A digit between 1 and 9, in which case the Nth loader in the menu
# will be the default.
# - A "+" symbol at the start of the string, which refers to the most
# recently booted loader.
# - Any substring that corresponds to a portion of the loader's title
# (usually the OS's name, boot loader's path, or a volume or
# filesystem title).
# You may also specify multiple selectors by separating them with commas
# and enclosing the list in quotes. (The "+" option is only meaningful in
# this context.)
# If you follow the selector(s) with two times, in 24-hour format, the
# default will apply only between those times. The times are in the
# motherboard's time standard, whether that's UTC or local time, so if
# you use UTC, you'll need to adjust this from local time manually.
# Times may span midnight as in "23:30 00:30", which applies to 11:30 PM
# to 12:30 AM. You may specify multiple default_selection lines, in which
# case the last one to match takes precedence. Thus, you can set a main
# option without a time followed by one or more that include times to
# set different defaults for different times of day.
# The default behavior is to boot the previously-booted OS.
#
#default_selection 1
#default_selection Microsoft
#default_selection "+,bzImage,vmlinuz"
#default_selection Maintenance 23:30 2:00
#default_selection "Maintenance,macOS" 1:00 2:30
# Enable VMX bit and lock the CPU MSR if unlocked.
# On some Intel Apple computers, the firmware does not lock the MSR 0x3A.
# The symptom on Windows is Hyper-V not working even if the CPU
# meets the minimum requirements (HW assisted virtualization and SLAT)
# DO NOT SET THIS EXCEPT ON INTEL CPUs THAT SUPPORT VMX! See
# http://www.thomas-krenn.com/en/wiki/Activating_the_Intel_VT_Virtualization_Feature
# for more on this subject.
# The default is false: Don't try to enable and lock the MSR.
#
#enable_and_lock_vmx false
# Tell a Mac's EFI that macOS is about to be launched, even when it's not.
# This option causes some Macs to initialize their hardware differently than
# when a third-party OS is launched normally. In some cases (particularly on
# Macs with multiple video cards), using this option can cause hardware to
# work that would not otherwise work. On the other hand, using this option
# when it is not necessary can cause hardware (such as keyboards and mice) to
# become inaccessible. Therefore, you should not enable this option if your
# non-Apple OSes work correctly; enable it only if you have problems with
# some hardware devices. When needed, a value of "10.9" usually works, but
# you can experiment with other values. This feature has no effect on
# non-Apple computers.
# The default is inactive (no macOS spoofing is done).
#
#spoof_osx_version 10.9
# Set the CSR values for Apple's System Integrity Protection (SIP) feature.
# Values are two-byte (four-character) hexadecimal numbers. These values
# define which specific security features are enabled. Below are the codes
# for what the values mean. Add them up (in hexadecimal!) to set new values.
# Apple's "csrutil enable" and "csrutil disable" commands set values of 10
# and 877, respectively. (Prior to OS 11, 77 was used rather than 877; 877
# is required for OS 11, and should work for OS X 10.x, too.)
# CSR_ALLOW_UNTRUSTED_KEXTS 0x0001
# CSR_ALLOW_UNRESTRICTED_FS 0x0002
# CSR_ALLOW_TASK_FOR_PID 0x0004
# CSR_ALLOW_KERNEL_DEBUGGER 0x0008
# CSR_ALLOW_APPLE_INTERNAL 0x0010
# CSR_ALLOW_UNRESTRICTED_DTRACE 0x0020
# CSR_ALLOW_UNRESTRICTED_NVRAM 0x0040
# CSR_ALLOW_DEVICE_CONFIGURATION 0x0080
# CSR_ALLOW_ANY_RECOVERY_OS 0x0100
# CSR_ALLOW_UNAPPROVED_KEXTS 0x0200
# CSR_ALLOW_EXECUTABLE_POLICY_OVERRIDE 0x0400
# CSR_ALLOW_UNAUTHENTICATED_ROOT 0x0800
#csr_values 10,877
# Include a secondary configuration file within this one. This secondary
# file is loaded as if its options appeared at the point of the "include"
# token itself, so if you want to override a setting in the main file,
# the secondary file must be referenced AFTER the setting you want to
# override. Note that the secondary file may NOT load a tertiary file.
#
#include manual.conf
# Sample manual configuration stanzas. Each begins with the "menuentry"
# keyword followed by a name that's to appear in the menu (use quotes
# if you want the name to contain a space) and an open curly brace
# ("{"). Each entry ends with a close curly brace ("}"). Common
# keywords within each stanza include:
#
# volume - identifies the filesystem from which subsequent files
# are loaded. You can specify the volume by filesystem
# label, by partition label, or by partition GUID number
# (but NOT yet by filesystem UUID number).
# loader - identifies the boot loader file
# initrd - Specifies an initial RAM disk file
# icon - specifies a custom boot loader icon
# ostype - OS type code to determine boot options available by
# pressing Insert. Valid values are "MacOS", "Linux",
# "Windows", and "XOM". Case-sensitive.
# graphics - set to "on" to enable graphics-mode boot (useful
# mainly for MacOS) or "off" for text-mode boot.
# Default is auto-detected from loader filename.
# options - sets options to be passed to the boot loader; use
# quotes if more than one option should be passed or
# if any options use characters that might be changed
# by rEFInd parsing procedures (=, /, #, or tab).
# disabled - use alone or set to "yes" to disable this entry.
#
# Note that you can use either DOS/Windows/EFI-style backslashes (\)
# or Unix-style forward slashes (/) as directory separators. Either
# way, all file references are on the ESP from which rEFInd was
# launched.
# Use of quotes around parameters causes them to be interpreted as
# one keyword, and for parsing of special characters (spaces, =, /,
# and #) to be disabled. This is useful mainly with the "options"
# keyword. Use of quotes around parameters that specify filenames is
# permissible, but you must then use backslashes instead of slashes,
# except when you must pass a forward slash to the loader, as when
# passing a root= option to a Linux kernel.
# Below are several sample boot stanzas. All are disabled by default.
# Find one similar to what you need, copy it, remove the "disabled" line,
# and adjust the entries to suit your needs.
# A sample entry for a Linux 3.13 kernel with EFI boot stub support
# on a partition with a GUID of 904404F8-B481-440C-A1E3-11A5A954E601.
# This entry includes Linux-specific boot options and specification
# of an initial RAM disk. Note uses of Linux-style forward slashes.
# Also note that a leading slash is optional in file specifications.
menuentry Linux {
icon EFI/refind/icons/os_linux.png
volume 904404F8-B481-440C-A1E3-11A5A954E601
loader bzImage-3.3.0-rc7
initrd initrd-3.3.0.img
options "ro root=UUID=5f96cafa-e0a7-4057-b18f-fa709db5b837"
disabled
}
# Below is a more complex Linux example, specifically for Arch Linux.
# This example MUST be modified for your specific installation; if nothing
# else, the PARTUUID code must be changed for your disk. Because Arch Linux
# does not include version numbers in its kernel and initrd filenames, you
# may need to use manual boot stanzas when using fallback initrds or
# multiple kernels with Arch. This example is modified from one in the Arch
# wiki page on rEFInd (https://wiki.archlinux.org/index.php/rEFInd).
menuentry "Arch Linux" {
icon /EFI/refind/icons/os_arch.png
volume "Arch Linux"
loader /boot/vmlinuz-linux
initrd /boot/initramfs-linux.img
options "root=PARTUUID=5028fa50-0079-4c40-b240-abfaf28693ea rw add_efi_memmap"
submenuentry "Boot using fallback initramfs" {
initrd /boot/initramfs-linux-fallback.img
}
submenuentry "Boot to terminal" {
add_options "systemd.unit=multi-user.target"
}
disabled
}
# A sample entry for loading Ubuntu using its standard name for
# its GRUB 2 boot loader. Note uses of Linux-style forward slashes
menuentry Ubuntu {
loader /EFI/ubuntu/grubx64.efi
icon /EFI/refind/icons/os_linux.png
disabled
}
# A minimal ELILO entry, which probably offers nothing that
# auto-detection can't accomplish.
menuentry "ELILO" {
loader \EFI\elilo\elilo.efi
disabled
}
# Like the ELILO entry, this one offers nothing that auto-detection
# can't do; but you might use it if you want to disable auto-detection
# but still boot Windows....
menuentry "Windows 7" {
loader \EFI\Microsoft\Boot\bootmgfw.efi
disabled
}
# EFI shells are programs just like boot loaders, and can be
# launched in the same way. You can pass a shell the name of a
# script that it's to run on the "options" line. The script
# could initialize hardware and then launch an OS, or it could
# do something entirely different.
menuentry "Windows via shell script" {
icon \EFI\refind\icons\os_win.png
loader \EFI\tools\shell.efi
options "fs0:\EFI\tools\launch_windows.nsh"
disabled
}
# MacOS is normally detected and run automatically; however,
# if you want to do something unusual, a manual boot stanza may
# be the way to do it. This one does nothing very unusual, but
# it may serve as a starting point. Note that you'll almost
# certainly need to change the "volume" line for this example
# to work.
menuentry "My macOS" {
icon \EFI\refind\icons\os_mac.png
volume "macOS boot"
loader \System\Library\CoreServices\boot.efi
disabled
}
# The firmware_bootnum token takes a HEXADECIMAL value as an option
# and sets that value using the EFI's BootNext variable and then
# reboots the computer. This then causes a one-time boot of the
# computer using this EFI boot option. It can be used for various
# purposes, but one that's likely to interest some rEFInd users is
# that some Macs with HiDPI displays produce lower-resolution
# desktops when booted through rEFInd than when booted via Apple's
# own boot manager. Booting using the firmware_bootnum option
# produces the better resolution. Note that no loader option is
# used in this type of configuration.
menuentry "macOS via BootNext" {
icon /EFI/refind/icons/os_mac.png
firmware_bootnum 80
disabled
}
@@ -0,0 +1,3 @@
"Boot with standard options" "rw root=/dev/mapper/cryptroot cryptdevice=UUID=c452045f-1bad-48c0-9816-f53c67a416dc:cryptroot resume=UUID=11c0ac93-cd2a-451d-9a6e-cf7f855fb26a quiet"
"Boot to single-user mode" "rw root=/dev/mapper/cryptroot cryptdevice=UUID=c452045f-1bad-48c0-9816-f53c67a416dc:cryptroot resume=UUID=11c0ac93-cd2a-451d-9a6e-cf7f855fb26a quiet single"
"Boot with minimal options" "rw root=/dev/mapper/cryptroot cryptdevice=UUID=c452045f-1bad-48c0-9816-f53c67a416dc:cryptroot resume=UUID=11c0ac93-cd2a-451d-9a6e-cf7f855fb26a quiet"
@@ -0,0 +1,6 @@
# Laptop suspend and hibernation policy.
[Sleep]
AllowSuspend=yes
AllowHibernation=yes
AllowSuspendThenHibernate=yes
HibernateDelaySec=15min
@@ -0,0 +1,2 @@
[Login]
IdleAction=ignore
@@ -0,0 +1,9 @@
Section "InputClass"
Identifier "touchpad"
Driver "libinput"
MatchIsTouchpad "on"
Option "Tapping" "on"
Option "TappingButtonMap" "lrm"
Option "NaturalScrolling" "true"
Option "ClickMethod" "clickfinger"
EndSection
@@ -0,0 +1,4 @@
# File to be place under /etc/NetworkManager/conf.d
[connection]
# Values are 0 (use default), 1 (ignore/don't touch), 2 (disable) or 3 (enable).
wifi.powersave = 3
@@ -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
@@ -29,3 +29,16 @@ end
# bun
set --export BUN_INSTALL "$HOME/.bun"
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
@@ -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
}
@@ -10,16 +10,21 @@ for _, monitor in ipairs(machine.monitors or {}) do
end
local main_mod = "SUPER"
local terminal = "alacritty"
local terminal = "kitty"
hl.config({
input = {
local input = {
kb_layout = "us",
kb_variant = "intl",
numlock_by_default = true,
},
}
for key, value in pairs(machine.input or {}) do
input[key] = value
end
hl.config({
input = input,
general = {
gaps_in = 8,
gaps_in = 4,
-- Keep a thin black perimeter so the bottom border remains on-screen.
gaps_out = 2,
border_size = 2,
@@ -35,24 +40,45 @@ hl.config({
animations = {
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.
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.
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 .. " + 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 .. " + 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 .. " + 0", hl.dsp.exec_cmd(noctalia_ipc .. "panel-toggle session"))
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 .. " + CTRL + L", hl.dsp.exec_cmd("command -v hyprlock >/dev/null && hyprlock || loginctl lock-session"))
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 .. " + 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.
hl.bind("XF86AudioLowerVolume", hl.dsp.exec_cmd("wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-"), { locked = true })
@@ -64,10 +90,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(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("XF86MonBrightnessUp", hl.dsp.exec_cmd('light -A 5 && notify-send "Brightness set to $(light)"'), { locked = true })
hl.bind("XF86MonBrightnessDown", hl.dsp.exec_cmd('light -U 5 && notify-send "Brightness set to $(light)"'), { 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 .. " + XF86MonBrightnessUp", hl.dsp.exec_cmd('light -S 100 && notify-send "Brightness set to max"'), { locked = true })
hl.bind("XF86MonBrightnessUp", hl.dsp.exec_cmd('light -A 5'), { 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.0'), { 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 })
-- Retain i3's hjkl and arrow-key directional workflow.
@@ -88,7 +114,9 @@ for workspace = 1, 9 do
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 }))
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 + LEFT", hl.dsp.focus({ workspace = "-1" }))
@@ -112,7 +140,9 @@ local function float_rule(name, match, border_size)
})
end
hl.window_rule({ name = "obsidian-workspace", match = { class = "^obsidian$" }, workspace = "8" })
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("qalculate", { title = "^Qalculate!$" })
float_rule("psensor", { class = "^Psensor$" })
@@ -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
}
@@ -0,0 +1,35 @@
# Match the active Alacritty look while retaining Kitty's native graphics protocol.
font_family MesloLGS NF
font_size 15.0
# Palette: active ~/.config/alacritty/alacritty.toml plus Noctalia cursor/selection.
foreground #FCFCFA
background #2D2A2E
cursor #fff1e6
cursor_text_color #000000
selection_foreground #fff1e6
selection_background #3b220f
color0 #403E41
color1 #FF6188
color2 #A9DC76
color3 #FFD866
color4 #FC9867
color5 #AB9DF2
color6 #78DCE8
color7 #FCFCFA
color8 #727072
color9 #FF6188
color10 #A9DC76
color11 #FFD866
color12 #FC9867
color13 #AB9DF2
color14 #78DCE8
color15 #FCFCFA
# Preserve Alacritty-like minimal window behavior.
window_padding_width 0
copy_on_select no
# Keep the shell integration and graphics protocol available to Ranger.
shell_integration enabled
@@ -0,0 +1,84 @@
# Shared Noctalia defaults. Keep GUI experiments in state; promote durable choices here.
[shell]
setup_wizard_enabled = false
offline_mode = true
external_ip_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]
mode = "dark"
source = "custom"
custom_palette = "Ember OLED"
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
@@ -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
@@ -0,0 +1,10 @@
[Unit]
Description=OpenSSH key agent
[Service]
Type=simple
Environment=SSH_AUTH_SOCK=%t/ssh-agent.socket
ExecStart=/usr/bin/ssh-agent -D -a $SSH_AUTH_SOCK
[Install]
WantedBy=default.target
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 yazi-rs
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Catppuccin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,36 @@
<div align="center">
<img src="https://github.com/sxyazi/yazi/blob/main/assets/logo.png?raw=true" alt="Yazi logo" width="20%">
</div>
<h3 align="center">
Catppuccin Macchiato Flavor for <a href="https://github.com/sxyazi/yazi">Yazi</a>
</h3>
## 👀 Preview
<img src="preview.png" width="600" />
## 🎨 Installation
```sh
ya pkg add yazi-rs/flavors:catppuccin-macchiato
```
## ⚙️ Usage
Set the content of your `theme.toml` to enable it as your _dark_ flavor:
```toml
[flavor]
dark = "catppuccin-macchiato"
```
Make sure your `theme.toml` doesn't contain anything other than `[flavor]`, unless you want to override certain styles of this flavor.
See the [Yazi flavor documentation](https://yazi-rs.github.io/docs/flavors/overview) for more details.
## 📜 License
The flavor is MIT-licensed, and the included tmTheme is also MIT-licensed.
Check the [LICENSE](LICENSE) and [LICENSE-tmtheme](LICENSE-tmtheme) file for more details.
@@ -0,0 +1,215 @@
# vim:fileencoding=utf-8:foldmethod=marker
# : Manager {{{
[mgr]
cwd = { fg = "#8bd5ca" }
# Find
find_keyword = { fg = "#eed49f", bold = true, italic = true, underline = true }
find_position = { fg = "#f5bde6", bg = "reset", bold = true, italic = true }
# Marker
marker_copied = { fg = "#a6da95", bg = "#a6da95" }
marker_cut = { fg = "#ed8796", bg = "#ed8796" }
marker_marked = { fg = "#8bd5ca", bg = "#8bd5ca" }
marker_selected = { fg = "#eed49f", bg = "#eed49f" }
# Count
count_copied = { fg = "#24273a", bg = "#a6da95" }
count_cut = { fg = "#24273a", bg = "#ed8796" }
count_selected = { fg = "#24273a", bg = "#eed49f" }
# Border
border_symbol = "│"
border_style = { fg = "#8087a2" }
# : }}}
# : Tabs {{{
[tabs]
active = { fg = "#24273a", bg = "#8aadf4", bold = true }
inactive = { fg = "#8aadf4", bg = "#363a4f" }
# : }}}
# : Mode {{{
[mode]
normal_main = { fg = "#24273a", bg = "#8aadf4", bold = true }
normal_alt = { fg = "#8aadf4", bg = "#363a4f" }
# Select mode
select_main = { fg = "#24273a", bg = "#8bd5ca", bold = true }
select_alt = { fg = "#8bd5ca", bg = "#363a4f" }
# Unset mode
unset_main = { fg = "#24273a", bg = "#f0c6c6", bold = true }
unset_alt = { fg = "#f0c6c6", bg = "#363a4f" }
# : }}}
# : Status bar {{{
[status]
# Permissions
perm_sep = { fg = "#8087a2" }
perm_type = { fg = "#8aadf4" }
perm_read = { fg = "#eed49f" }
perm_write = { fg = "#ed8796" }
perm_exec = { fg = "#a6da95" }
# Progress
progress_label = { fg = "#ffffff", bold = true }
progress_normal = { fg = "#a6da95", bg = "#494d64" }
progress_error = { fg = "#eed49f", bg = "#ed8796" }
# : }}}
# : Pick {{{
[pick]
border = { fg = "#8aadf4" }
active = { fg = "#f5bde6", bold = true }
inactive = {}
# : }}}
# : Input {{{
[input]
border = { fg = "#8aadf4" }
title = {}
value = {}
selected = { reversed = true }
# : }}}
# : Completion {{{
[cmp]
border = { fg = "#8aadf4" }
# : }}}
# : Tasks {{{
[tasks]
border = { fg = "#8aadf4" }
title = {}
hovered = { fg = "#f5bde6", bold = true }
# : }}}
# : Which {{{
[which]
border = { fg = "#8aadf4" }
cand = { fg = "#8bd5ca" }
rest = { fg = "#939ab7" }
desc = { fg = "#f5bde6" }
separator = "  "
separator_style = { fg = "#5b6078" }
# : }}}
# : Help {{{
[help]
on = { fg = "#8bd5ca" }
run = { fg = "#f5bde6" }
hovered = { reversed = true, bold = true }
footer = { fg = "#363a4f", bg = "#cad3f5" }
# : }}}
# : Spotter {{{
[spot]
border = { fg = "#8aadf4" }
title = { fg = "#8aadf4" }
tbl_col = { fg = "#8bd5ca" }
tbl_cell = { fg = "#f5bde6", bg = "#494d64" }
# : }}}
# : Notification {{{
[notify]
title_info = { fg = "#a6da95" }
title_warn = { fg = "#eed49f" }
title_error = { fg = "#ed8796" }
# : }}}
# : File-specific styles {{{
[filetype]
rules = [
# Image
{ mime = "**/image/*", fg = "#8bd5ca" },
# Media
{ mime = "**/{audio,video}/*", fg = "#eed49f" },
# Archive
{ mime = "**/application/{zip,rar,7z*,tar,gzip,xz,zstd,bzip*,lzma,compress,archive,cpio,arj,xar,ms-cab*}", fg = "#f5bde6" },
# Document
{ mime = "**/application/{pdf,doc,rtf}", fg = "#a6da95" },
# Virtual file system
{ mime = "vfs/{absent,stale}", fg = "#939ab7" },
# Fallback
{ url = "*", fg = "#cad3f5" },
{ url = "*/", fg = "#8aadf4" },
]
# : }}}
[icon]
dirs = [
{ name = ".config", text = "", fg = "#f5bde6" },
{ name = ".git", text = "", fg = "#8bd5ca" },
{ name = ".github", text = "", fg = "#8aadf4" },
{ name = ".npm", text = "", fg = "#8aadf4" },
{ name = "Desktop", text = "", fg = "#8bd5ca" },
{ name = "Development", text = "", fg = "#8bd5ca" },
{ name = "Documents", text = "", fg = "#8bd5ca" },
{ name = "Downloads", text = "", fg = "#8bd5ca" },
{ name = "Library", text = "", fg = "#8bd5ca" },
{ name = "Movies", text = "", fg = "#8bd5ca" },
{ name = "Music", text = "", fg = "#8bd5ca" },
{ name = "Pictures", text = "", fg = "#8bd5ca" },
{ name = "Public", text = "", fg = "#8bd5ca" },
{ name = "Videos", text = "", fg = "#8bd5ca" },
]
conds = [
# Special files
{ if = "orphan", text = "", fg = "#cad3f5" },
{ if = "link", text = "", fg = "#8087a2" },
{ if = "block", text = "", fg = "#f0c6c6" },
{ if = "char", text = "", fg = "#f0c6c6" },
{ if = "fifo", text = "", fg = "#f0c6c6" },
{ if = "sock", text = "", fg = "#f0c6c6" },
{ if = "sticky", text = "", fg = "#f0c6c6" },
{ if = "dummy", text = "", fg = "#ed8796" },
# Fallback
{ if = "dir & hovered", text = "", fg = "#8aadf4" },
{ if = "dir", text = "", fg = "#8aadf4" },
{ if = "exec", text = "", fg = "#a6da95" },
{ if = "!dir", text = "", fg = "#cad3f5" },
]
Binary file not shown.

After

Width:  |  Height:  |  Size: 674 KiB

@@ -0,0 +1,20 @@
require("git"):setup {
order = 1500,
}
require("githead"):setup {
branch_prefix = "on ",
branch_symbol = "",
branch_borders = "()",
branch_color = "#ffb05c",
remote_branch_color = "#f4c7a1",
tag_color = "#ffcc99",
commit_color = "#f4c7a1",
behind_remote_color = "#ff6b4a",
ahead_remote_color = "#ff8800",
stashes_color = "#c98a45",
state_color = "#ff6b4a",
staged_color = "#ff8800",
unstaged_color = "#ffb05c",
untracked_color = "#f4c7a1",
}
@@ -0,0 +1,16 @@
#:schema https://yazi-rs.github.io/schemas/keymap.json
[[mgr.prepend_keymap]]
on = "l"
run = "plugin smart-enter"
desc = "Enter the child directory, or open the file"
[[mgr.prepend_keymap]]
on = ["g", "d"]
run = "cd ~/downloads"
desc = "Cd to Downloads"
[[mgr.prepend_keymap]]
on = ["g", "D"]
run = "cd ~/cloud/documents"
desc = "Cd to Documents"
@@ -0,0 +1,19 @@
[[plugin.deps]]
use = "yazi-rs/plugins:smart-enter"
rev = "0be29a9"
hash = "187cc58ba7ac3befd49c342129e6f1b6"
[[plugin.deps]]
use = "yazi-rs/plugins:git"
rev = "3f2b882"
hash = "336548d9f67926e12d5584f60b790f1"
[[plugin.deps]]
use = "llanosrocas/githead"
rev = "317d09f"
hash = "41dc37a724b2afd21da4f1ea6c1182d8"
[[flavor.deps]]
use = "yazi-rs/flavors:catppuccin-macchiato"
rev = "be0b21d"
hash = "5b1bf96d1cba26376087c37de993c517"
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 yazi-rs
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,84 @@
# git.yazi
Show the status of Git file changes as linemode in the file list.
https://github.com/user-attachments/assets/34976be9-a871-4ffe-9d5a-c4cdd0bf4576
## Installation
```sh
ya pkg add yazi-rs/plugins:git
```
## Setup
Add the following to your `~/.config/yazi/init.lua`:
```lua
require("git"):setup {
-- Order of status signs showing in the linemode
order = 1500,
}
```
And register it as fetchers in your `~/.config/yazi/yazi.toml`:
```toml
[[plugin.prepend_fetchers]]
url = "*"
run = "git"
group = "git"
[[plugin.prepend_fetchers]]
url = "*/"
run = "git"
group = "git"
```
## Advanced
You can customize the [Style](https://yazi-rs.github.io/docs/configuration/theme#types.style) of the status sign with:
- `[git].unknown` - status cannot/not yet determined
- `[git].modified` - modified file
- `[git].added` - added file
- `[git].untracked` - untracked file
- `[git].ignored` - ignored file
- `[git].deleted` - deleted file
- `[git].updated` - updated file
- `[git].clean` - clean file
For example:
```toml
# theme.toml / flavor.toml
[git]
modified = { fg = "blue" }
deleted = { fg = "red", bold = true }
```
You can also customize the text of the status sign with:
- `[git].unknown_sign` - status cannot/not yet determined
- `[git].modified_sign` - modified file
- `[git].added_sign` - added file
- `[git].untracked_sign` - untracked file
- `[git].ignored_sign` - ignored file
- `[git].deleted_sign` - deleted file
- `[git].updated_sign` - updated file
- `[git].clean_sign` - clean file
For example:
```toml
# theme.toml / flavor.toml
[git]
unknown_sign = " "
modified_sign = "M"
deleted_sign = "D"
clean_sign = "✔"
```
## License
This plugin is MIT-licensed. For more information check the [LICENSE](LICENSE) file.
@@ -0,0 +1,279 @@
--- @since 26.5.6
local WINDOWS = ya.target_family() == "windows"
-- The code of supported git status,
-- also used to determine which status to show for directories when they contain different statuses
-- see `bubble_up`
---@enum CODES
local CODES = {
unknown = 100, -- status cannot/not yet determined
excluded = 99, -- ignored directory
ignored = 6, -- ignored file
untracked = 5,
modified = 4,
added = 3,
deleted = 2,
updated = 1,
clean = 0,
}
local PATTERNS = {
{ "!$", CODES.ignored },
{ "?$", CODES.untracked },
{ "[MT]", CODES.modified },
{ "[AC]", CODES.added },
{ "D", CODES.deleted },
{ "U", CODES.updated },
{ "[AD][AD]", CODES.updated },
}
local function theme()
local t = th.git or {}
return {
[CODES.unknown] = t.unknown or ui.Style(),
[CODES.ignored] = t.ignored or ui.Style():fg("darkgray"),
[CODES.untracked] = t.untracked or ui.Style():fg("magenta"),
[CODES.modified] = t.modified or ui.Style():fg("yellow"),
[CODES.added] = t.added or ui.Style():fg("green"),
[CODES.deleted] = t.deleted or ui.Style():fg("red"),
[CODES.updated] = t.updated or ui.Style():fg("yellow"),
[CODES.clean] = t.clean or ui.Style(),
}, {
[CODES.unknown] = t.unknown_sign or "",
[CODES.ignored] = t.ignored_sign or "",
[CODES.untracked] = t.untracked_sign or "? ",
[CODES.modified] = t.modified_sign or "",
[CODES.added] = t.added_sign or "",
[CODES.deleted] = t.deleted_sign or "",
[CODES.updated] = t.updated_sign or "",
[CODES.clean] = t.clean_sign or "",
}
end
---@param line string
---@return CODES, string
local function match(line)
local signs = line:sub(1, 2)
for _, p in ipairs(PATTERNS) do
local path, pattern, code = nil, p[1], p[2]
if signs:find(pattern) then
path = line:sub(4, 4) == '"' and line:sub(5, -2) or line:sub(4)
path = WINDOWS and path:gsub("/", "\\") or path
end
if not path then
elseif path:find("[/\\]$") then
-- Mark the ignored directory as `excluded`, so we can process it further within `propagate_down`
return code == CODES.ignored and CODES.excluded or code, path:sub(1, -2)
else
return code, path
end
---@diagnostic disable-next-line: missing-return
end
end
---@param cwd Url
---@return string?
local function root(cwd)
local is_worktree = function(url)
local file, head = io.open(tostring(url)), nil
if file then
head = file:read(8)
file:close()
end
return head == "gitdir: "
end
repeat
local next = cwd:join(".git")
local cha = fs.cha(next)
if cha and (cha.is_dir or is_worktree(next)) then
return tostring(cwd)
end
cwd = cwd.parent
until not cwd
end
---@param changed Changes
---@return Changes
local function bubble_up(changed)
local new, empty = {}, Url("")
for path, code in pairs(changed) do
if code ~= CODES.ignored then
local url = Url(path).parent
while url and url ~= empty do
local s = tostring(url)
new[s] = (new[s] or CODES.clean) > code and new[s] or code
url = url.parent
end
end
end
return new
end
---@param excluded string[]
---@param cwd Url
---@param repo Url
---@return Changes
local function propagate_down(excluded, cwd, repo)
local new, rel = {}, cwd:strip_prefix(repo)
for _, path in ipairs(excluded) do
if rel:starts_with(path) then
-- If `cwd` is a subdirectory of an excluded directory, also mark it as `excluded`
new[tostring(cwd)] = CODES.excluded
elseif cwd == repo:join(path).parent then
-- If `path` is a direct subdirectory of `cwd`, mark it as `ignored`
new[path] = CODES.ignored
else
-- Skipping, we only care about `cwd` itself and its direct subdirectories for maximum performance
end
end
return new
end
---@param cwd string
---@param repo string
---@param changed Changes
local add = ya.sync(function(st, cwd, repo, changed)
---@cast st State
st.dirs[cwd] = repo
st.repos[repo] = st.repos[repo] or {}
for path, code in pairs(changed) do
if code == CODES.clean then
st.repos[repo][path] = nil
elseif code == CODES.excluded then
-- Mark the directory with a special value `excluded` so that it can be distinguished during UI rendering
st.dirs[path] = CODES.excluded
else
st.repos[repo][path] = code
end
end
ui.render()
end)
---@param cwd string
local remove = ya.sync(function(st, cwd)
---@cast st State
local repo = st.dirs[cwd]
if not repo then
return
end
ui.render()
st.dirs[cwd] = nil
if not st.repos[repo] then
return
end
for _, r in pairs(st.dirs) do
if r == repo then
return
end
end
st.repos[repo] = nil
end)
---@param st State
---@param opts Options
local function setup(st, opts)
st.dirs = {}
st.repos = {}
opts = opts or {}
opts.order = opts.order or 1500
local styles, signs = theme()
ps.sub("theme", function()
styles, signs = theme()
end)
Linemode:children_add(function(self)
if not self._file.in_current then
return ""
end
local url = self._file.url
local repo = st.dirs[tostring(url.base or url.parent)]
local code = CODES.unknown
if repo then
code = repo == CODES.excluded and CODES.ignored or st.repos[repo][tostring(url):sub(#repo + 2)] or CODES.clean
end
if signs[code] == "" then
return ""
elseif self._file.is_hovered then
return ui.Line { " ", signs[code] }
else
return ui.Line { " ", ui.Span(signs[code]):style(styles[code]) }
end
end, opts.order)
end
---@type UnstableFetcher
local function fetch(_, job)
local cwd = job.files[1].url.base or job.files[1].url.parent
local repo = root(cwd)
if not repo then
remove(tostring(cwd))
return true
end
local paths = {}
for _, file in ipairs(job.files) do
paths[#paths + 1] = tostring(file.url)
end
-- stylua: ignore
local output, err = Command("git")
:cwd(tostring(cwd))
:arg({ "--no-optional-locks", "-c", "core.quotePath=", "status", "--porcelain", "-unormal", "--no-renames", "--ignored=matching" })
:arg(paths)
:output()
if not output then
return true, Err("Cannot spawn `git` command, error: %s", err)
end
local changed, excluded = {}, {}
for line in output.stdout:gmatch("[^\r\n]+") do
local code, path = match(line)
if code == CODES.excluded then
excluded[#excluded + 1] = path
else
changed[path] = code
end
end
if job.files[1].cha.is_dir then
ya.dict_merge(changed, bubble_up(changed))
end
ya.dict_merge(changed, propagate_down(excluded, cwd, Url(repo)))
-- Reset the status of any files that don't appear in the output of `git status` to `clean`,
-- so that cleaning up outdated statuses from `st.repos`
for _, path in ipairs(paths) do
local s = path:sub(#repo + 2)
changed[s] = changed[s] or CODES.clean
end
add(tostring(cwd), repo, changed)
return false
end
-- TODO: remove
local function fetch_compact(self, job)
if ya.throttle then
fetch(self, job)
return ya.co(function()
for _, file in ipairs(job.files) do
coroutine.yield(file, { retry = true })
end
end)
else
return fetch(self, job)
end
end
return { setup = setup, fetch = fetch_compact }
@@ -0,0 +1,12 @@
---@class State
---@field dirs table<string, string|CODES> Mapping between a directory and its corresponding repository
---@field repos table<string, Changes> Mapping between a repository and the status of each of its files
---@class Options
---@field order number The order in which the status is displayed
---@field renamed boolean Whether to include renamed files in the status (or treat them as modified)
-- TODO: move this to `types.yazi` once it's get stable
---@alias UnstableFetcher fun(self: unknown, job: { files: File[] }): boolean, Error?
---@alias Changes table<string, CODES>
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 llanosrocas
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,422 @@
# githead.yazi
Git status header for yazi inspired by [powerlevel10k](https://github.com/romkatv/powerlevel10k?tab=readme-ov-file#what-do-different-symbols-in-git-status-mean).
![preview-default](https://github.com/llanosrocas/githead.yazi/blob/main/.github/images/preview-default.png)
![preview-fullscreen](https://github.com/llanosrocas/githead.yazi/blob/main/.github/images/preview-fullscreen.png)
All supported features are listed [here](#features). More presets are available [here](#presets).
## Requirements
- yazi version >= [26.1.22](https://github.com/sxyazi/yazi/releases/tag/v26.1.22).
- Font with symbol support. For example [Nerd Fonts](https://www.nerdfonts.com/).
## Compatibility
To keep the plugin up to date, there are two branches: `main` and `nightly`.
The `main` branch follows major yazi releases, while `nightly` is linked to specific yazi commits or changes.
This setup allows shipping stable versions on time, while giving early access to "cutting-edge" changes. See matrix below.
<details close>
<summary>Compatibility matrix</summary>
| githead | yazi |
| :-------------------------------------------------------------------------------------------------------: | ----------------------------------------------------------------------------------------- |
| [v2.0.2](https://github.com/llanosrocas/githead.yazi/releases/tag/v2.0.2) | [v26.1.22](https://github.com/sxyazi/yazi/releases/tag/v26.1.22) |
| [v2.0.2](https://github.com/llanosrocas/githead.yazi/releases/tag/v2.0.2) | [v25.12.29](https://github.com/sxyazi/yazi/releases/tag/v25.12.29) |
| [v2.0.2-nightly.292d37c](https://github.com/llanosrocas/githead.yazi/releases/tag/v2.0.2-nightly.292d37c) | [2f66561](https://github.com/sxyazi/yazi/commit/2f66561a8251f8788b2b0fd366af90555ecafc86) |
| [v2.0.2-nightly.292d37c](https://github.com/llanosrocas/githead.yazi/releases/tag/v2.0.2-nightly.292d37c) | [6cfa92f](https://github.com/sxyazi/yazi/commit/6cfa92f11205d212155579b5b76d4cbabe723829) |
| [v2.0.2-nightly.292d37c](https://github.com/llanosrocas/githead.yazi/releases/tag/v2.0.2-nightly.292d37c) | [292d37c](https://github.com/sxyazi/yazi/commit/292d37ccde5fb09dc7318247caf552213d16ec36) |
| [v2.0.1](https://github.com/llanosrocas/githead.yazi/releases/tag/v2.0.1) | [917e1f5](https://github.com/sxyazi/yazi/commit/917e1f54a10445f2e25147c4b81a3c77d8233632) |
| [v2.0.0](https://github.com/llanosrocas/githead.yazi/releases/tag/v2.0.0) | [693dff2](https://github.com/sxyazi/yazi/commit/693dff25e3165e357cc9d0b94ca3f2b176741a36) |
| [v1.7.0](https://github.com/llanosrocas/githead.yazi/releases/tag/v1.7.0) | [693dff2](https://github.com/sxyazi/yazi/tree/693dff25e3165e357cc9d0b94ca3f2b176741a36) |
| [v1.6.0](https://github.com/llanosrocas/githead.yazi/releases/tag/v1.6.0) | [v25.5.31](https://github.com/sxyazi/yazi/releases/tag/v25.5.31) |
</details>
## Installation
1. Using yazi package manager
```sh
ya pkg add llanosrocas/githead
```
_Or manually copy `main.lua` to the `~/.config/yazi/plugins/githead.yazi/main.lua`_
2. Add this line to your `~/.config/yazi/init.lua`:
```lua
require("githead"):setup()
```
## Configuration
This is default config, if you want to see copy-paste presets go to [this section](#presets).
```lua
require("githead"):setup({
order = {
"__spacer__",
"branch",
"remote",
"__spacer__",
"tag",
"__spacer__",
"commit",
"__spacer__",
"behind_ahead_remote",
"__spacer__",
"stashes",
"__spacer__",
"state",
"__spacer__",
"staged",
"__spacer__",
"unstaged",
"__spacer__",
"untracked",
},
show_numbers = true, -- shows staged, unstaged, untracked, stashes count
show_branch = true,
branch_prefix = "",
branch_color = "blue",
branch_symbol = "",
branch_borders = "",
show_remote_branch = true, -- only shown if different from local branch
always_show_remote_branch = false, -- always show remote branch even if it the same as local branch
always_show_remote_repo = false, -- Adds `origin/` if `always_show_remote_branch` is enabled
remote_branch_prefix = ":",
remote_branch_color = "bright magenta",
show_tag = true, -- only shown if branch is not available
always_show_tag = false,
tag_color = "magenta",
tag_symbol = "#",
show_commit = true, -- only shown if branch AND tag are not available
always_show_commit = false,
commit_color = "bright magenta",
commit_symbol = "@",
show_behind_ahead_remote = true,
behind_remote_color = "bright magenta",
behind_remote_symbol = "⇣",
ahead_remote_color = "bright magenta",
ahead_remote_symbol = "⇡",
show_stashes = true,
stashes_color = "bright magenta",
stashes_symbol = "$",
show_state = true,
show_state_prefix = true,
state_color = "red",
state_symbol = "~",
show_staged = true,
staged_color = "bright yellow",
staged_symbol = "+",
show_unstaged = true,
unstaged_color = "bright yellow",
unstaged_symbol = "!",
show_untracked = true,
untracked_color = "blue",
untracked_symbol = "?",
})
```
```
/cwd on ( feature):origin/main #v1.0.0 #1234567 ⇣2⇡3 $1 rebase 1/2 ~2 +4 !1 ?5
| | | | ││ | | | | | | | | | | | |
| | | | ││ | | | | | | | | | | | └─── untracked_symbol
| | | | ││ | | | | | | | | | | └────── unstaged_symbol
| | | | ││ | | | | | | | | | └───────── staged_symbol
| | | | ││ | | | | | | | | └──────────── state_symbol
| | | | ││ | | | | | | | └─────────────────────── state_prefix
| | | | ││ | | | | | | └────────────────────────── stashes_symbol
| | | | ││ | | | | | └───────────────────────────── ahead_symbol
| | | | ││ | | | | └─────────────────────────────── behind_symbol
| | | | ││ | | | └──────────────────────────────────────── commit_symbol
| | | | ││ | | └──────────────────────────────────────────────── tag_symbol
| | | | ││ | └───────────────────────────────────────────────────── remote_branch
| | | | ││ └─────────────────────────────────────────────────────────── remote_repo
| | | | |└───────────────────────────────────────────────────────────── remote_branch_prefix
| | | | └────────────────────────────────────────────────────────────── branch_borders
| | | └──────────────────────────────────────────────────────────────────── branch
| | └─────────────────────────────────────────────────────────────────────── branch_symbol
| └─────────────────────────────────────────────────────────────────────────── branch_prefix
└──────────────────────────────────────────────────────────────────────────────── cwd
```
## Presets
- v1 default (My config)
![preview-fav](https://github.com/llanosrocas/githead.yazi/blob/main/.github/images/preview-fav.png)
<details>
<summary>Config</summary>
```lua
require("githead"):setup({
branch_prefix = "on",
branch_symbol = " ",
branch_borders = "()",
})
```
</details>
- [spaceship-prompt](https://github.com/spaceship-prompt/spaceship-prompt)
![preview-spaceship](https://github.com/llanosrocas/githead.yazi/blob/main/.github/images/preview-spaceship.png)
<details>
<summary>Config</summary>
```lua
require("githead"):setup({
order = {
"__spacer__",
"branch",
"remote",
"__spacer__",
"tag",
"__spacer__",
"commit",
"__spacer__",
"behind_ahead_remote",
"stashes",
"state",
"staged",
"unstaged",
"untracked",
},
show_numbers = false,
branch_symbol = " ",
branch_prefix = "on",
})
```
</details>
- [robertgzr/porcelain](https://github.com/robertgzr/porcelain)
![preview-porcelain](https://github.com/llanosrocas/githead.yazi/blob/main/.github/images/preview-porcelain.png)
<details>
<summary>Config</summary>
```lua
require("githead"):setup({
order = {
"__spacer__",
"branch",
"commit",
"__spacer__",
"behind_ahead_remote",
"__spacer__",
"untracked",
"state",
"unstaged",
"__spacer__",
"staged",
},
show_numbers = false,
show_branch = true,
branch_prefix = "",
branch_color = "#288BD2",
always_show_commit = true,
commit_color = "#859A00",
show_behind_ahead_remote = true,
behind_remote_symbol = "↓",
ahead_remote_symbol = "↑",
behind_remote_color = "#DC322E",
ahead_remote_color = "#4DB6AC",
show_state = true,
show_state_prefix = false,
state_symbol = "!!",
state_color = "#B58901",
staged_symbol = "✔",
staged_color = "green",
unstaged_symbol = "Δ",
unstaged_color = "#288BD2",
untracked_symbol = "?",
untracked_color = "#415F65",
})
```
</details>
- Minimal (No color)
![preview-minimal-no-color](https://github.com/llanosrocas/githead.yazi/blob/main/.github/images/preview-minimal-no-color.png)
<details>
<summary>Config</summary>
```lua
require("githead"):setup({
branch_color = "",
remote_branch_prefix = "@",
remote_branch_color = "",
tag_color = "",
commit_color = "",
ahead_remote_symbol = "+",
ahead_remote_color = "",
behind_remote_symbol = "-",
behind_remote_color = "",
stashes_symbol = "*",
stashes_color = "",
show_state_prefix = false,
state_symbol = "~",
state_color = "",
staged_symbol = "S",
staged_color = "",
unstaged_symbol = "U",
unstaged_color = "",
untracked_symbol = "N",
untracked_color = "",
})
```
</details>
- Vibrant (Full)
![preview-vibrant-full](https://github.com/llanosrocas/githead.yazi/blob/main/.github/images/preview-vibrant-full.png)
<details>
<summary>Config</summary>
```lua
require("githead"):setup({
order = {
"__spacer__",
"stashes",
"__spacer__",
"state",
"__spacer__",
"staged",
"__spacer__",
"unstaged",
"__spacer__",
"untracked",
"__spacer__",
"branch",
"remote_branch",
"__spacer__",
"tag",
"__spacer__",
"commit",
"__spacer__",
"behind_ahead_remote",
"__spacer__",
},
branch_borders = "{}",
branch_prefix = "|",
branch_color = "#7aa2f7",
remote_branch_color = "#9ece6a",
always_show_remote_branch = true,
always_show_remote_repo = true,
tag_symbol = "󰓼",
always_show_tag = true,
tag_color = "#bb9af7",
commit_symbol = "",
always_show_commit = true,
commit_color = "#e0af68",
staged_color = "#73daca",
staged_symbol = "●",
unstaged_color = "#e0af68",
unstaged_symbol = "✗",
untracked_color = "#f7768e",
untracked_symbol = "?",
state_color = "#f5c359",
state_symbol = "󱐋",
stashes_color = "#565f89",
stashes_symbol = "⚑",
})
```
</details>
## Features
- [x] Current branch
- [x] Remote and remote branch
- [x] Latest tag
- [x] Latest commit
- [x] Behind/Ahead of the remote
- [x] Stashes
- [x] States
- [x] merge
- [x] cherry
- [x] rebase (+ done counter)
- [x] revert
- [x] bisect
- [x] Staged
- [x] Unstaged
- [x] Untracked
### Under the hood
The goal is to use minimum amount of shell commands.
- Branch, stashes, staged files, unstaged files, untracked files.
```shell
git status --ignore-submodules=dirty --branch --show-stash --ahead-behind
```
- Remote branch and repo:
```shell
git rev-parse --abbrev-ref --symbolic-full-name @{upstream}
```
- Latest tag and commit:
```shell
git log --format="commit %h%d" -n 1
```
## Credits
- [yazi source code](https://github.com/sxyazi/yazi)
- [powerlevel10k](https://github.com/romkatv/powerlevel10k)
- [twio142](https://github.com/twio142/githead.yazi)
@@ -0,0 +1,598 @@
---@diagnostic disable: undefined-global
local save = ya.sync(function(this, cwd, output)
if cwd:match("%.git[/\\]") or cwd:match("%.git$") then
this.output = nil
return
end
if cx.active.current.cwd == Url(cwd) then
this.output = output
ui.render()
end
end)
return {
setup = function(this, options)
options = options or {}
local config = {
order = options.order or {
"__spacer__",
"branch",
"remote_branch",
"__spacer__",
"tag",
"__spacer__",
"commit",
"__spacer__",
"behind_ahead_remote",
"__spacer__",
"stashes",
"__spacer__",
"state",
"__spacer__",
"staged",
"__spacer__",
"unstaged",
"__spacer__",
"untracked",
},
show_numbers = options.show_numbers == nil and true
or options.show_numbers,
show_branch = options.show_branch == nil and true or options.show_branch,
branch_prefix = options.branch_prefix or "",
branch_color = options.branch_color or "blue",
branch_symbol = options.branch_symbol or "",
branch_borders = options.branch_borders or "",
show_remote_branch = options.show_remote_branch == nil and true
or options.show_remote_branch,
always_show_remote_branch = options.always_show_remote_branch == nil
and false
or options.always_show_remote_branch,
always_show_remote_repo = options.always_show_remote_repo == nil
and false
or options.always_show_remote_repo,
remote_branch_prefix = options.remote_branch_prefix or ":",
remote_branch_color = options.remote_branch_color or "bright magenta",
show_tag = options.show_tag == nil and true or options.show_tag,
always_show_tag = options.always_show_tag == nil and false
or options.always_show_tag,
tag_color = options.tag_color or "magenta",
tag_symbol = options.tag_symbol == nil and "#" or options.tag_symbol,
show_commit = options.show_commit == nil and true or options.show_commit,
always_show_commit = options.always_show_commit == nil and false
or options.always_show_commit,
commit_color = options.commit_color or "bright magenta",
commit_symbol = options.commit_symbol == nil and "@"
or options.commit_symbol,
show_behind_ahead_remote = options.show_behind_ahead_remote == nil
and true
or options.show_behind_ahead_remote,
behind_remote_color = options.behind_remote_color or "bright magenta",
behind_remote_symbol = options.behind_remote_symbol or "⇣",
ahead_remote_color = options.ahead_remote_color or "bright magenta",
ahead_remote_symbol = options.ahead_remote_symbol or "⇡",
show_stashes = options.show_stashes == nil and true
or options.show_stashes,
stashes_color = options.stashes_color or "bright magenta",
stashes_symbol = options.stashes_symbol or "$",
show_state = options.show_state == nil and true or options.show_state,
show_state_prefix = options.show_state_prefix == nil and true
or options.show_state_prefix,
state_color = options.state_color or "red",
state_symbol = options.state_symbol or "~",
show_staged = options.show_staged == nil and true or options.show_staged,
staged_color = options.staged_color or "bright yellow",
staged_symbol = options.staged_symbol or "+",
show_unstaged = options.show_unstaged == nil and true
or options.show_unstaged,
unstaged_color = options.unstaged_color or "bright yellow",
unstaged_symbol = options.unstaged_symbol or "!",
show_untracked = options.show_untracked == nil and true
or options.show_untracked,
untracked_color = options.untracked_color or "bright blue",
untracked_symbol = options.untracked_symbol or "?",
}
--- @param data GitStatusData
function Header:render_branch(data)
local branch = data.branch
if not branch then
return nil
end
local left_border = config.branch_borders:sub(1, 1)
local right_border = config.branch_borders:sub(2, 2)
local branch_string = ""
if config.branch_symbol == "" then
branch_string = left_border .. branch .. right_border
else
branch_string = left_border
.. config.branch_symbol
.. branch
.. right_border
end
local branch_prefix = config.branch_prefix == "" and ""
or config.branch_prefix .. " "
return ui.Line({
ui.Span(branch_prefix),
ui.Span(branch_string):fg(config.branch_color),
})
end
--- @param data GitStatusData
function Header:render_remote_branch(data)
local branch = data.branch
local remote_branch = data.remote_branch
local remote_repo = data.remote_repo
if not remote_branch then
return nil
end
local show_remote = config.always_show_remote_branch
or branch ~= remote_branch
if not show_remote then
return nil
end
local remote_branch_label = config.always_show_remote_repo
and (remote_repo .. "/" .. remote_branch)
or remote_branch
return ui.Line({
ui.Span(config.remote_branch_prefix),
ui.Span(remote_branch_label):fg(config.remote_branch_color),
})
end
--- @param data GitStatusData
function Header:render_tag(data)
local branch = data.branch
local tag = data.tag
if not tag then
return nil
end
if not branch or config.always_show_tag then
return ui.Line({
ui.Span(config.tag_symbol),
ui.Span(tag):fg(config.tag_color),
})
end
end
--- @param data GitStatusData
function Header:render_commit(data)
local branch = data.branch
local tag = data.tag
local commit = data.commit
if not commit then
return nil
end
if (not branch and not tag) or config.always_show_commit then
return ui.Line({
ui.Span(config.commit_symbol),
ui.Span(commit):fg(config.commit_color),
})
end
end
--- @param data GitStatusData
function Header:render_behind_ahead_remote(data)
local behind = data.behind_remote
local ahead = data.ahead_remote
local behind_label = behind
and behind > 0
and ui.Span(
config.behind_remote_symbol .. (config.show_numbers and behind or "")
):fg(config.behind_remote_color)
local ahead_label = ahead
and ahead > 0
and ui.Span(
config.ahead_remote_symbol .. (config.show_numbers and ahead or "")
):fg(config.ahead_remote_color)
if ahead_label and behind_label then
return ui.Line({
behind_label,
ahead_label,
})
elseif ahead_label then
return ahead_label
elseif behind_label then
return behind_label
else
return nil
end
end
--- @param data GitStatusData
function Header:render_stashes(data)
local stashes_count = data.stashes
if not stashes_count then
return nil
end
local stashes_label = config.stashes_symbol
if config.show_numbers then
stashes_label = stashes_label .. stashes_count
end
return ui.Span(stashes_label):fg(config.stashes_color)
end
--- @param data GitStatusData
function Header:render_state(data)
local state_prefix = data.state_prefix
local unmerged_count = data.unmerged_count
local rebase_done = data.rebase_done
local rebase_total = data.rebase_total
if state_prefix then
local state_label = ""
if config.show_state_prefix then
state_label = state_prefix
if
state_prefix == "rebase-i"
and config.show_numbers
and rebase_done
and rebase_total
then
state_label = state_label
.. " "
.. rebase_done
.. "/"
.. rebase_total
end
end
local unmerged_label = ""
local symbol = ""
if unmerged_count and unmerged_count > 0 then
symbol = " " .. config.state_symbol
if config.show_numbers then
unmerged_label = tostring(unmerged_count)
end
end
return ui.Span(state_label .. symbol .. unmerged_label)
:fg(config.state_color)
end
return nil
end
--- @param data GitStatusData
function Header:render_staged(data)
local staged = data.staged
if staged then
local staged_label = config.staged_symbol
if config.show_numbers then
local filtered_staged = staged:gsub("^[%s]*%b()[%s]*", "")
local staged_count = 0
for line in filtered_staged:gmatch("[^\r\n]+") do
if line:match("%S") then
staged_count = staged_count + 1
end
end
staged_label = staged_label .. staged_count
end
return ui.Span(staged_label):fg(config.staged_color)
end
return nil
end
--- @param data GitStatusData
function Header:render_unstaged(data)
local unstaged = data.unstaged
if unstaged then
local unstaged_label = config.unstaged_symbol
if config.show_numbers then
local filtered_unstaged =
unstaged:gsub("^[%s]*%b()[\r\n]*", ""):gsub("^[%s]*%b()[\r\n]*", "")
local unstaged_count = 0
for line in filtered_unstaged:gmatch("[^\r\n]+") do
if line:match("%S") then
unstaged_count = unstaged_count + 1
end
end
unstaged_label = unstaged_label .. unstaged_count
end
return ui.Span(unstaged_label):fg(config.unstaged_color)
end
return nil
end
--- @param data GitStatusData
function Header:render_untracked(data)
local untracked = data.untracked
if untracked then
local untracked_label = config.untracked_symbol
if config.show_numbers then
local filtered_untracked = untracked:gsub("^[%s]*%b()[\r\n]*", "")
local untracked_count = 0
for line in filtered_untracked:gmatch("[^\r\n]+") do
if line:match("%S") then
untracked_count = untracked_count + 1
end
end
untracked_label = untracked_label .. untracked_count
end
return ui.Span(untracked_label):fg(config.untracked_color)
end
return nil
end
function Header:githead()
--- @type GitStatusData
local data = this.output
if not data then
return ui.Line({})
end
local head = {}
for _, key in ipairs(config.order) do
if key == "__spacer__" then
if head[#head] ~= " " then
table.insert(head, " ")
end
else
local fn = self["render_" .. key]
local is_shown = config["show_" .. key]
if fn and is_shown then
local value = fn(self, data)
if value then
table.insert(head, value)
end
end
end
end
return ui.Line(head)
end
Header:children_add(Header.githead, 2000, Header.LEFT)
local callback = function()
local cwd = cx.active.current.cwd
ya.emit("plugin", {
this._id,
ya.quote(tostring(cwd), true),
})
end
ps.sub("cd", callback)
ps.sub("rename", callback)
ps.sub("bulk", callback)
ps.sub("move", callback)
ps.sub("trash", callback)
ps.sub("delete", callback)
ps.sub("tab", callback)
end,
entry = function(_, job)
local args = job.args or job
---@class GitStatusData
---@field ahead_remote? integer
---@field behind_remote? integer
---@field branch? string
---@field commit? string
---@field rebase_done? integer
---@field remote_branch? string
---@field remote_repo? string
---@field staged? string
---@field stashes? integer
---@field state_prefix? "bisect"|"cherry"|"merge"|"rebase-i"|"revert"
---@field tag? string
---@field unmerged_count? integer
---@field unstaged? string
---@field untracked? string
local data = {}
--- @param status string
local get_behind_ahead_remote = function(status)
local diverged_ahead, diverged_behind =
status:match("have (%d+) and (%d+) different")
if diverged_ahead and diverged_behind then
data.behind_remote = tonumber(diverged_behind)
data.ahead_remote = tonumber(diverged_ahead)
else
local behind_remote = status:match("behind %S+ by (%d+) commits?")
local ahead_remote = status:match("ahead of %S+ by (%d+) commits?")
data.behind_remote = tonumber(behind_remote)
data.ahead_remote = tonumber(ahead_remote)
end
end
--- @param status string
local get_branch = function(status)
data.branch = status:match("On branch (%S+)")
end
--- @param status string
local get_stashes = function(status)
data.stashes = tonumber(status:match("Your stash currently has (%d+)"))
end
--- @param status string
local get_state = function(status)
local unmerged = status:match("Unmerged paths:%s*(.-)%s*\n\n")
if unmerged then
local filtered_unmerged =
unmerged:gsub("^[%s]*%b()[%s]*", ""):gsub("^[%s]*%b()[%s]*", "")
local count = 0
for line in filtered_unmerged:gmatch("[^\r\n]+") do
if line:match("%S") then
count = count + 1
end
end
data.unmerged_count = count
end
if status:find("git merge") then
data.state_prefix = "merge"
elseif status:find("git cherry%-pick") then
data.state_prefix = "cherry"
elseif status:find("git rebase") then
data.state_prefix = "rebase-i"
local commands_done = status:match("%((%d+) commands? done%)")
if commands_done then
data.rebase_done = commands_done
if not data.unmerged_count then
data.rebase_total = commands_done
else
data.rebase_total = data.unmerged_count
end
end
elseif status:find("git revert") then
data.state_prefix = "revert"
elseif status:find("git bisect") then
data.state_prefix = "bisect"
end
end
--- @param status string
local get_staged = function(status)
data.staged = status:match("Changes to be committed:%s*(.-)%s*\n\n")
end
--- @param status string
local get_unstaged = function(status)
data.unstaged =
status:match("Changes not staged for commit:%s*(.-)%s*\n\n")
end
--- @param status string
local get_untracked = function(status)
data.untracked = status:match("Untracked files:%s*(.-)%s*\n\n")
end
local get_status = function()
local cmd = Command("git")
:arg({
"status",
"--ignore-submodules=dirty",
"--branch",
"--show-stash",
"--ahead-behind",
})
:cwd(args[1])
:env("LANGUAGE", "en_US.UTF-8")
:stdout(Command.PIPED)
local cmd_output = cmd:output()
if cmd_output then
local status = cmd_output.stdout
get_branch(status)
get_behind_ahead_remote(status)
get_stashes(status)
get_state(status)
get_staged(status)
get_unstaged(status)
get_untracked(status)
end
end
local get_remote_branch = function()
local cmd = Command("git")
:arg({
"rev-parse",
"--abbrev-ref",
"--symbolic-full-name",
"@{upstream}",
})
:cwd(args[1])
:env("LANGUAGE", "en_US.UTF-8")
:stdout(Command.PIPED)
local cmd_output = cmd:output()
if cmd_output then
local remote_branch = cmd_output.stdout
data.remote_branch =
remote_branch:gsub("[\r\n]", ""):match("^[^/]+/(.+)")
data.remote_repo = remote_branch:gsub("[\r\n]", ""):match("^([^/]+)/")
end
end
--- @param log string
local get_tag = function(log)
data.tag = log:match("tag: ([^, )]+)")
end
--- @param log string
local get_commit = function(log)
data.commit = log:match("^commit%s+([a-f0-9]+)")
end
local get_git_log = function()
local cmd = Command("git")
:arg({ "log", "--format=commit %h%d", "-n", "1" })
:cwd(args[1])
:env("LANGUAGE", "en_US.UTF-8")
:stdout(Command.PIPED)
local cmd_output = cmd:output()
if cmd_output then
local log = cmd_output.stdout
get_tag(log)
get_commit(log)
end
end
get_status()
get_remote_branch()
get_git_log()
save(args[1], data)
end,
}
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2023 yazi-rs
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,40 @@
# smart-enter.yazi
[`Open`][open] files or [`enter`][enter] directories all in one key!
## Installation
```sh
ya pkg add yazi-rs/plugins:smart-enter
```
## Usage
Bind your <kbd>l</kbd> key to the plugin, in your `~/.config/yazi/keymap.toml`:
```toml
[[mgr.prepend_keymap]]
on = "l"
run = "plugin smart-enter"
desc = "Enter the child directory, or open the file"
```
## Advanced
By default, `--hovered` is passed to the [`open`][open] action, make the behavior consistent with [`enter`][enter] avoiding accidental triggers,
which means both will only target the currently hovered file.
If you still want `open` to target multiple selected files, add this to your `~/.config/yazi/init.lua`:
```lua
require("smart-enter"):setup {
open_multi = true,
}
```
## License
This plugin is MIT-licensed. For more information check the [LICENSE](LICENSE) file.
[open]: https://yazi-rs.github.io/docs/configuration/keymap/#mgr.open
[enter]: https://yazi-rs.github.io/docs/configuration/keymap/#mgr.enter
@@ -0,0 +1,11 @@
--- @since 25.5.31
--- @sync entry
local function setup(self, opts) self.open_multi = opts.open_multi end
local function entry(self)
local h = cx.active.current.hovered
ya.emit(h and h.cha.is_dir and "enter" or "open", { hovered = not self.open_multi })
end
return { entry = entry, setup = setup }
@@ -0,0 +1,125 @@
# Ember OLED: aligned with Noctalia's "Ember OLED" palette and tmux status line.
[flavor]
dark = "catppuccin-macchiato"
[mgr]
cwd = { fg = "#ffb05c", bold = true }
find_keyword = { fg = "#ffcc99", bold = true, underline = true }
find_position = { fg = "#ff8800", bold = true }
marker_copied = { fg = "#c98a45", bg = "#c98a45" }
marker_cut = { fg = "#ff6b4a", bg = "#ff6b4a" }
marker_marked = { fg = "#ff8800", bg = "#ff8800" }
marker_selected = { fg = "#ffcc99", bg = "#ffcc99" }
count_copied = { fg = "#050403", bg = "#c98a45" }
count_cut = { fg = "#050403", bg = "#ff6b4a" }
count_selected = { fg = "#050403", bg = "#ffcc99" }
border_symbol = "│"
border_style = { fg = "#6e4a31" }
[tabs]
active = { fg = "#fff1e6", bg = "#1a110c", bold = true }
inactive = { fg = "#f4c7a1", bg = "#050403" }
[mode]
normal_main = { fg = "#050403", bg = "#ff8800", bold = true }
normal_alt = { fg = "#ffb05c", bg = "#1a110c" }
select_main = { fg = "#050403", bg = "#ffb05c", bold = true }
select_alt = { fg = "#ffb05c", bg = "#1a110c" }
unset_main = { fg = "#050403", bg = "#ff6b4a", bold = true }
unset_alt = { fg = "#ff6b4a", bg = "#1a110c" }
[status]
perm_sep = { fg = "#6e4a31" }
perm_type = { fg = "#ffb05c" }
perm_read = { fg = "#fff1e6" }
perm_write = { fg = "#ff6b4a" }
perm_exec = { fg = "#c98a45" }
progress_label = { fg = "#fff1e6", bold = true }
progress_normal = { fg = "#ff8800", bg = "#1a110c" }
progress_error = { fg = "#fff1e6", bg = "#ff6b4a" }
[pick]
border = { fg = "#ff8800" }
active = { fg = "#ffb05c", bold = true }
[input]
border = { fg = "#ff8800" }
selected = { fg = "#050403", bg = "#ffb05c" }
[cmp]
border = { fg = "#ff8800" }
[tasks]
border = { fg = "#ff8800" }
title = { fg = "#fff1e6" }
hovered = { fg = "#ffb05c", bold = true }
[which]
border = { fg = "#ff8800" }
cand = { fg = "#ffb05c" }
rest = { fg = "#f4c7a1" }
desc = { fg = "#fff1e6" }
separator = "  "
separator_style = { fg = "#6e4a31" }
[help]
on = { fg = "#ffb05c" }
run = { fg = "#fff1e6" }
hovered = { fg = "#050403", bg = "#ffb05c", bold = true }
footer = { fg = "#fff1e6", bg = "#1a110c" }
[spot]
border = { fg = "#ff8800" }
title = { fg = "#ffb05c" }
tbl_col = { fg = "#ffb05c" }
tbl_cell = { fg = "#fff1e6", bg = "#1a110c" }
[notify]
title_info = { fg = "#c98a45" }
title_warn = { fg = "#ffb05c" }
title_error = { fg = "#ff6b4a" }
[filetype]
rules = [
{ mime = "**/image/*", fg = "#ffcc99" },
{ mime = "**/{audio,video}/*", fg = "#c98a45" },
{ mime = "**/application/{zip,rar,7z*,tar,gzip,xz,zstd,bzip*,lzma,compress,archive,cpio,arj,xar,ms-cab*}", fg = "#ffb05c" },
{ mime = "**/application/{pdf,doc,rtf}", fg = "#f4c7a1" },
{ mime = "vfs/{absent,stale}", fg = "#6e4a31" },
{ url = "*", fg = "#fff1e6" },
{ url = "*/", fg = "#ffb05c" },
]
[icon]
dirs = [
{ name = ".config", text = "", fg = "#ffb05c" },
{ name = ".git", text = "", fg = "#ff8800" },
{ name = ".github", text = "", fg = "#ffb05c" },
{ name = "Desktop", text = "", fg = "#ffb05c" },
{ name = "Documents", text = "", fg = "#ffb05c" },
{ name = "Downloads", text = "", fg = "#ffb05c" },
{ name = "Pictures", text = "", fg = "#ffcc99" },
{ name = "Videos", text = "", fg = "#c98a45" },
]
conds = [
{ if = "orphan", text = "", fg = "#ff6b4a" },
{ if = "link", text = "", fg = "#6e4a31" },
{ if = "dir & hovered", text = "", fg = "#ffcc99" },
{ if = "dir", text = "", fg = "#ffb05c" },
{ if = "exec", text = "", fg = "#c98a45" },
{ if = "!dir", text = "", fg = "#fff1e6" },
]
[git]
modified = { fg = "#ffb05c" }
added = { fg = "#c98a45" }
untracked = { fg = "#f4c7a1" }
deleted = { fg = "#ff6b4a", bold = true }
updated = { fg = "#ff8800" }
ignored = { fg = "#6e4a31" }
modified_sign = "M "
added_sign = "A "
untracked_sign = "? "
deleted_sign = "D "
updated_sign = "U "
ignored_sign = "I "
@@ -0,0 +1,9 @@
[[plugin.prepend_fetchers]]
url = "*"
run = "git"
group = "git"
[[plugin.prepend_fetchers]]
url = "*/"
run = "git"
group = "git"
@@ -10,8 +10,10 @@ return {
},
startup = {
"noctalia",
"hypridle",
-- Electron's tray icon registers only when the SNI watcher already exists.
"sh -c 'sleep 2; exec mullvad-vpn'",
"obsidian",
"thunderbird",
"nextcloud",
"spotify",
@@ -1,20 +1,29 @@
# 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]
position = "bottom"
layer = "overlay"
layer = "top"
scale = 0.95
capsule_radius = 5
center = ["group:g3", "notifications"]
end = ["bluetooth", "volume", "control-center", "group:g1", "session"]
end = ["group:g2", "group:g6", "group:g1", "session"]
font_weight = 400
margin_ends = 0
padding = 0
panel_overlap = 3
radius = 0
shadow = false
smart_auto_hide = true
# The bar may overlay the thin perimeter while visible; hidden bars release the space.
reserve_space = false
start = ["tray", "group:g4", "group:g2", "privacy"]
# Keep the desktop status bar visible and reserve its screen edge.
auto_hide = false
smart_auto_hide = false
reserve_space = true
start = ["tray", "group:g4", "privacy"]
thickness = 39
widget_spacing = 10
@@ -22,7 +31,7 @@ widget_spacing = 10
enabled = true
fill = "surface_variant"
id = "g1"
members = ["caffeine", "date", "clock"]
members = ["idle-inhibitors", "caffeine", "date", "clock"]
opacity = 1.0
padding = 6.0
@@ -30,7 +39,7 @@ padding = 6.0
enabled = true
fill = "surface_variant"
id = "g2"
members = ["network_rx", "network_tx", "network"]
members = ["network", "bluetooth"]
opacity = 1.0
padding = 6.0
@@ -47,64 +56,36 @@ radius = 5.0
enabled = true
fill = "surface_variant"
id = "g4"
members = ["cpu", "temp", "ram", "gpu", "gpu_temp", "disk", "battery"]
members = ["pi-status", "cpu", "temp", "ram", "gpu", "gpu_temp", "disk"]
opacity = 1.0
padding = 6.0
[widget.cpu]
type = "alex/system-status:cpu"
[widget.ram]
type = "alex/system-status:memory"
[widget.temp]
type = "sysmon"
stat = "cpu_temp"
display = "text"
[[bar.main.capsule_group]]
enabled = true
fill = "surface_variant"
id = "g6"
members = ["input_volume", "volume"]
opacity = 1.0
padding = 6.0
radius = 5.0
[widget.gpu]
type = "sysmon"
stat = "gpu_usage"
display = "graph"
visualization = "graph"
show_value = true
[widget.gpu_temp]
type = "sysmon"
stat = "gpu_temp"
display = "text"
[widget.disk]
type = "sysmon"
stat = "disk_pct"
display = "text"
path = "/"
[battery.device."/org/freedesktop/UPower/devices/battery_hidpp_battery_0"]
warning_threshold = 20
visualization = "none"
show_value = true
[control_center]
sidebar = "full"
sidebar_section = "full"
width = 880
[desktop_widgets]
enabled = false
[wallpaper]
enabled = true
fill_color = "#000000"
[wallpaper.default]
path = "color:#000000"
[system.monitor]
# GPU polling is disabled by Noctalia by default; enable it for the RTX 3090 widgets.
gpu_poll_seconds = 2.0
[plugins]
enabled = ["alex/system-status"]
[nightlight]
enabled = true
force = false
temperature_day = 6500
temperature_night = 2500
@@ -0,0 +1,30 @@
-- Laptop profile: leave monitor selection to Hyprland's hotplug support.
-- Noctalia replaces the i3-era Redshift, Polybar, Blueman Applet, and Picom stack.
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 = {},
startup = {
"noctalia",
-- Start after Noctalia has claimed the StatusNotifier watcher.
"sh -c 'sleep 2; exec mullvad-vpn'",
"obsidian",
"thunderbird",
"nextcloud",
"spotify",
},
}
@@ -0,0 +1,88 @@
# Laptop-specific Noctalia layout. Shared privacy/theme defaults live in 10-base.toml.
[bar.main]
position = "top"
layer = "top"
scale = 0.95
capsule_radius = 5
center = ["group:g3", "notifications"]
end = ["group:g2", "group:g6", "group:g5", "group:g1", "session"]
font_weight = 400
margin_ends = 0
padding = 0
panel_overlap = 3
radius = 0
shadow = false
auto_hide = false
smart_auto_hide = false
reserve_space = true
start = ["tray", "group:g4"]
# Accommodate the battery-rate widget's wattage + ETA lines without clipping.
thickness = 39
widget_spacing = 10
[[bar.main.capsule_group]]
enabled = true
fill = "surface_variant"
id = "g1"
members = ["idle-inhibitors", "caffeine", "date", "clock"]
opacity = 1.0
padding = 6.0
[[bar.main.capsule_group]]
enabled = true
fill = "surface_variant"
id = "g2"
members = ["network", "bluetooth"]
opacity = 1.0
padding = 6.0
[[bar.main.capsule_group]]
enabled = true
fill = "surface_variant"
id = "g3"
members = ["launcher", "media", "control-center", "clipboard"]
opacity = 1.0
padding = 6.0
radius = 5.0
[[bar.main.capsule_group]]
enabled = true
fill = "surface_variant"
id = "g4"
members = ["cpu", "temp", "ram", "gpu-mode", "disk"]
opacity = 1.0
padding = 6.0
[[bar.main.capsule_group]]
enabled = true
fill = "surface_variant"
id = "g5"
members = ["battery-rate", "battery", "power_profile"]
opacity = 1.0
padding = 6.0
radius = 5.0
[[bar.main.capsule_group]]
enabled = true
fill = "surface_variant"
id = "g6"
members = ["input_volume", "volume"]
opacity = 1.0
padding = 6.0
radius = 5.0
[widget.battery-rate]
type = "alex/system-status:battery-rate"
[widget.gpu-mode]
type = "alex/system-status:gpu-mode"
# Only suspend-then-hibernate after 30 minutes when UPower confirms battery power.
# 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]
sidebar = "full"
sidebar_section = "full"
width = 720
@@ -0,0 +1,59 @@
# Local, untracked Noctalia overrides.
# This file is loaded after the shared base (10-base.toml) and machine profile
# (90-machine.toml). Keep host-only experiments and machine-local adjustments here.
#
# Example:
# [bar.main]
# thickness = 32
[plugins]
enabled = [
"alex/system-status",
"alex/pi-status-bridge",
"alex/knowledge-lookup",
]
[widget.pi-status]
type = "alex/pi-status-bridge:bridge"
[widget.knowledge-lookup]
type = "alex/knowledge-lookup:bar"
[bar.main]
center = ["pi-status", "knowledge-lookup", "group:g3", "notifications"]
end = ["group:g2", "group:g6", "group:g5", "group:g1", "session"]
# Migrated from Noctalia's GUI-managed state so these host preferences survive
# resetting the state override file.
[location]
address = "Leipzig, Germany"
[lockscreen_widgets]
enabled = false
schema_version = 2
widget_order = ["lockscreen-login-box@eDP-1"]
[lockscreen_widgets.grid]
cell_size = 16
major_interval = 4
visible = true
[lockscreen_widgets.widget."lockscreen-login-box@eDP-1"]
box_height = 70.0
box_width = 400.0
cx = 960.0
cy = 1081.0
output = "eDP-1"
rotation = 0.0
type = "login_box"
[lockscreen_widgets.widget."lockscreen-login-box@eDP-1".settings]
background_color = "surface_variant"
background_opacity = 0.88
background_radius = 12.0
center_password_text = false
input_opacity = 1.0
input_radius = 6.0
show_caps_lock = true
show_keyboard_layout = true
show_login_button = true
+32
View File
@@ -0,0 +1,32 @@
"""Machine profiles compose concerns; concerns never depend on a host."""
from modules.host_system import desktop_system, laptop_system
from modules.packages import PackageConcern
COMMON = (
"common/core",
"common/wayland",
"common/fonts",
"common/development",
"common/productivity",
"common/media",
"common/research",
"common/network",
"common/services",
)
LAPTOP = ("laptop/hardware",)
DESKTOP = ()
def _concerns(names: tuple[str, ...]) -> list[PackageConcern]:
return [PackageConcern(name) for name in names]
def laptop() -> list:
"""Laptop: shared concerns, laptop hardware, and laptop system configuration."""
return [*_concerns((*COMMON, *LAPTOP)), *laptop_system()]
def desktop() -> list:
"""Future desktop: shared concerns plus desktop-only concerns when declared."""
return [*_concerns((*COMMON, *DESKTOP)), *desktop_system()]
@@ -0,0 +1,7 @@
getty@.service
remote-fs.target
systemd-resolved-monitor.socket
systemd-resolved-varlink.socket
systemd-resolved.service
systemd-timesyncd.service
systemd-userdbd.socket
@@ -0,0 +1,6 @@
gnome-keyring-daemon.socket
p11-kit-server.socket
pipewire-pulse.socket
pipewire.socket
wireplumber.service
xdg-user-dirs.service
@@ -0,0 +1 @@
docker.service
@@ -0,0 +1,3 @@
idle-inhibitor-monitor.service
pi-status-bridge.service
ssh-agent.service
@@ -0,0 +1,6 @@
avahi-daemon.service
avahi-daemon.socket
avahi-dnsconfd.service
cups.path
cups.service
cups.socket
@@ -0,0 +1,3 @@
NetworkManager-dispatcher.service
NetworkManager-wait-online.service
NetworkManager.service
@@ -0,0 +1,2 @@
mullvad-daemon.service
mullvad-early-boot-blocking.service
@@ -0,0 +1,6 @@
bluetooth.service
keyd.service
lm_sensors.service
thermald.service
tlp-pd.service
tlp.service
+10
View File
@@ -0,0 +1,10 @@
# Modules
Only two adapters belong here:
- `packages.py` turns package concern lists into Decman package declarations.
- `host_system.py` deploys reviewed system policy, user configuration, and enabled units for a host.
Keep policy in `packages/`, `config/`, and `manifests/`; keep this directory
thin and reusable. User configuration belongs under `config/user/{common,laptop,desktop}/`
and is installed with owner `alex`.

Some files were not shown because too many files have changed in this diff Show More