diff --git a/flake.nix b/flake.nix
index 972706b..976f3fc 100644
--- a/flake.nix
+++ b/flake.nix
@@ -81,6 +81,7 @@
./home/sscoble.nix
./home/pi.nix
./home/wgu.nix
+ ./home/macos.nix
{
home.homeDirectory = "/Users/sscoble";
nixpkgs.config.allowUnfree = true;
diff --git a/home/bin/lid-handler.sh b/home/bin/lid-handler.sh
new file mode 100755
index 0000000..05318ad
--- /dev/null
+++ b/home/bin/lid-handler.sh
@@ -0,0 +1,56 @@
+#!/usr/bin/env bash
+# ~/.config/hypr/scripts/lid-handler.sh
+# Handles laptop lid close/open events for Hyprland.
+# Called by Hyprland bindl (runs in user session context).
+# Usage: lid-handler.sh close|open
+
+set -euo pipefail
+
+EVENT="${1:-}" # "close" or "open"
+
+# Determine lid state if not provided
+if [ -z "$EVENT" ]; then
+ LID_STATE=$(cat /proc/acpi/button/lid/LID0/state 2>/dev/null || cat /proc/acpi/button/lid/LID/state 2>/dev/null || echo "open")
+ if echo "$LID_STATE" | grep -qi "closed"; then
+ EVENT="close"
+ else
+ EVENT="open"
+ fi
+fi
+
+# Detect monitors via hyprctl
+INTERNAL=$(hyprctl monitors -j 2>/dev/null | jq -r '.[] | select(.name | startswith("eDP")) | .name' | head -1 || echo "")
+EXTERNAL=$(hyprctl monitors -j 2>/dev/null | jq -r '.[] | select(.name | startswith("eDP") | not) | .name' | head -1 || echo "")
+
+# Fallback: try known names from hyprland.conf
+[ -z "$INTERNAL" ] && INTERNAL="eDP-1"
+[ -z "$EXTERNAL" ] && EXTERNAL="DP-3"
+
+case "$EVENT" in
+ close)
+ if [ -n "$EXTERNAL" ]; then
+ # Move all workspaces to the external display first
+ WORKSPACES=$(hyprctl workspaces -j 2>/dev/null | jq -r '.[].id' || echo "")
+ for ws in $WORKSPACES; do
+ hyprctl dispatch moveworkspacetomonitor "$ws" "$EXTERNAL" 2>/dev/null || true
+ done
+ # Set external as primary (scale factor 1)
+ hyprctl keyword monitor "$EXTERNAL",preferred,auto,1 2>/dev/null || true
+ # Disable internal display
+ if [ -n "$INTERNAL" ]; then
+ hyprctl keyword monitor "$INTERNAL,disable" 2>/dev/null || true
+ fi
+ notify-send -t 3000 "Lid Closed" "Switched to $EXTERNAL as primary display"
+ else
+ notify-send -t 3000 "Lid Closed" "No external display detected — locking"
+ hyprlock 2>/dev/null || true
+ fi
+ ;;
+ open)
+ # Re-enable internal display alongside external
+ if [ -n "$INTERNAL" ]; then
+ hyprctl keyword monitor "$INTERNAL,preferred,auto,1" 2>/dev/null || true
+ notify-send -t 3000 "Lid Opened" "Internal display $INTERNAL re-enabled"
+ fi
+ ;;
+esac
diff --git a/home/desktop.nix b/home/desktop.nix
index 822382e..f2e5857 100644
--- a/home/desktop.nix
+++ b/home/desktop.nix
@@ -5,6 +5,8 @@
home.packages = with pkgs; [
# pi + qmd (from overlay)
qmd
+ # Browser
+ chromium
# Terminal
ghostty
zellij
@@ -19,6 +21,8 @@
awww
btop
fuzzel
+ # Screen recording / streaming
+ obs-studio
# Lock + idle
hyprlock
hypridle
@@ -40,9 +44,13 @@
bibata-cursors
papirus-icon-theme
adw-gtk3
+ beos-r5-icons
+ haiku-cursor
# Network / containers / system
networkmanagerapplet
- firefox
+ gh
+ winetricks
+ cabextract # required by winetricks for .cab extraction
tailscale
docker
dive
@@ -53,24 +61,58 @@
nmap
tcpdump
speedtest-cli
+ protonmail-bridge-gui
];
+ # === Proton Mail Bridge user service (starts at login) ===
+ systemd.user.services.protonmail-bridge = {
+ Unit = {
+ Description = "Proton Mail Bridge";
+ After = [ "network-online.target" "graphical-session.target" ];
+ Wants = [ "network-online.target" ];
+ };
+
+ Service = {
+ Type = "simple";
+ ExecStart = "${pkgs.protonmail-bridge}/bin/protonmail-bridge --non-interactive";
+ Restart = "on-failure";
+ RestartSec = "5";
+ };
+
+ Install = {
+ WantedBy = [ "graphical-session.target" ];
+ };
+ };
+
# === GTK / QT theming ===
gtk = {
enable = true;
theme.name = "adw-gtk3-dark";
- iconTheme.name = "Papirus-Dark";
+ iconTheme.name = "BeOS-r5";
cursorTheme = {
- name = "Bibata-Modern-Classic";
+ name = "Haiku_Cursor";
size = 24;
};
+ gtk3.extraConfig = {
+ gtk-application-prefer-dark-theme = 1;
+ };
+ gtk4.extraConfig = {
+ gtk-application-prefer-dark-theme = 1;
+ };
};
qt = {
enable = true;
- platformTheme.name = "gtk";
+ platformTheme.name = "gtk3";
style.name = "adwaita-dark";
};
+ # === libadwaita dark mode ===
+ dconf.settings = {
+ "org/gnome/desktop/interface" = {
+ color-scheme = "prefer-dark";
+ };
+ };
+
# === Hyprland WM config ===
home.file.".config/hypr/hyprland.conf" = {
force = true;
@@ -165,7 +207,7 @@
layer = "top"; position = "top"; height = 32; spacing = 4; margin = "6 8 0 8";
modules-left = [ "hyprland/workspaces" ];
modules-center = [ "clock" ];
- modules-right = [ "tray" "pulseaudio" "backlight" "battery" "network" "cpu" "temperature" "memory" ];
+ modules-right = [ "tray" "bluetooth" "pulseaudio" "backlight" "battery" "network" "cpu" "temperature" "memory" ];
"hyprland/workspaces" = {
format = "{icon}";
@@ -175,7 +217,7 @@
};
persistent_workspaces = { "1" = []; "2" = []; "3" = []; "4" = []; "5" = []; };
};
- clock = { format = "{:%a %b %d %H:%M}"; tooltip-format = "{:%Y %B}\n{calendar}"; };
+ clock = { format = "{:%a %b %d %I:%M %p}"; tooltip-format = "{:%Y %B}\n{calendar}"; };
cpu = { format = " {usage}%"; };
temperature = { format = " {temperatureC}°C"; interval = 10; thermal-zone = 5; };
memory = { format = " {}%"; };
@@ -193,6 +235,15 @@
format = "{icon} {volume}%"; format-bluetooth = "{icon} {volume}%"; format-muted = "";
format-icons.default = [ "" "" "" ]; on-click = "pavucontrol";
};
+ bluetooth = {
+ format = "";
+ format-connected = " {device_alias}";
+ format-disabled = "";
+ format-no-controller = "";
+ format-off = "";
+ tooltip-format = "{device_alias} — {device_address}";
+ on-click = "blueman-manager";
+ };
tray.spacing = 10;
};
};
@@ -204,7 +255,7 @@
#workspaces button { padding: 0 6px; color: #6c7086; background: transparent; border-radius: 4px; }
#workspaces button.active { color: #cdd6f4; background: #45475a; }
#workspaces button:hover { color: #cdd6f4; background: #313244; }
- #clock, #cpu, #memory, #backlight, #battery, #network, #pulseaudio, #tray { padding: 0 10px; color: #cdd6f4; }
+ #clock, #cpu, #memory, #backlight, #battery, #network, #pulseaudio, #bluetooth, #tray { padding: 0 10px; color: #cdd6f4; }
#cpu.warning, #memory.warning, #battery.warning { color: #f9e2af; }
#cpu.critical, #memory.critical, #battery.critical { color: #f38ba8; }
tooltip { background: rgba(30,30,46,0.95); border: 1px solid #45475a; border-radius: 6px; }
@@ -213,7 +264,7 @@
# === Rofi launcher ===
home.file.".config/rofi/config.rasi" = {
force = true;
- source = ./rofi-config.rasi;
+ source = ./spotlight-dark.rasi;
};
# === Dunst notifications ===
home.file.".config/dunst/dunstrc".text = ''
@@ -230,8 +281,110 @@
[urgency_normal] background = "#1e1e2e"; foreground = "#cdd6f4"; timeout = 5
[urgency_critical] background = "#f38ba8"; foreground = "#1e1e2e"; timeout = 0
'';
+ # === Lid handler ===
+ home.file.".config/hypr/scripts/lid-handler.sh" = {
+ force = true;
+ executable = true;
+ source = ./bin/lid-handler.sh;
+ };
+
# === Wallpaper rotation ===
home.file.".local/bin/wallpaper-rotate" = {
+ force = true;
source = ./wallpaper-rotate.sh;
};
+
+ # === BeOS cursor theme installer ===
+ home.file.".local/bin/install-beos-cursor" = {
+ force = true;
+ executable = true;
+ text = ''
+ #!/usr/bin/env bash
+ set -euo pipefail
+
+ CURSOR_NAME="BeOS"
+ ICONS_DIR="$HOME/.local/share/icons"
+ CURSOR_DIR="$ICONS_DIR/$CURSOR_NAME"
+
+ echo "==> BeOS Cursor Installer"
+ echo ""
+
+ if [ -d "$CURSOR_DIR" ] && [ -f "$CURSOR_DIR/index.theme" ]; then
+ echo " ✅ BeOS cursor already installed at $CURSOR_DIR"
+ echo ""
+ echo "To switch to it:"
+ echo " gsettings set org.gnome.desktop.interface cursor-theme BeOS"
+ echo " # Or update ~/.config/gtk-3.0/settings.ini:"
+ echo " # gtk-cursor-theme-name=BeOS"
+ exit 0
+ fi
+
+ echo "To install BeOS cursors:"
+ echo " 1. Open https://www.gnome-look.org/p/999293/ in your browser"
+ echo " 2. Download the BeOS-Cursors.tar.gz"
+ echo " 3. Run: install-beos-cursor /path/to/BeOS-Cursors.tar.gz"
+ echo ""
+
+ if [ $# -eq 0 ]; then
+ echo "No file provided. See instructions above."
+ exit 1
+ fi
+
+ ARCHIVE="$1"
+ if [ ! -f "$ARCHIVE" ]; then
+ echo " ❌ File not found: $ARCHIVE"
+ exit 1
+ fi
+
+ echo "==> Installing BeOS cursor from $ARCHIVE..."
+ mkdir -p "$ICONS_DIR"
+ tar xzf "$ARCHIVE" -C "$ICONS_DIR" 2>/dev/null || tar xzf "$ARCHIVE" -C "$ICONS_DIR" --strip-components=1 2>/dev/null || true
+
+ if [ -f "$CURSOR_DIR/index.theme" ]; then
+ echo " ✅ Installed to $CURSOR_DIR"
+ echo ""
+ echo "Now switch to BeOS cursor:"
+ echo " gsettings set org.gnome.desktop.interface cursor-theme BeOS"
+ else
+ # Try to find what was extracted
+ echo " Checking extracted directories..."
+ find "$ICONS_DIR" -name "index.theme" -exec dirname {} \\; 2>/dev/null | head -5
+ echo ""
+ echo "The extracted cursor directories are listed above."
+ fi
+ '';
+ };
+
+ # === Logos Bible Software (Wine) ===
+
+ # Helper script: first run installs Logos, subsequent runs launch it
+ home.file.".local/bin/logos" = {
+ force = true;
+ executable = true;
+ text = ''
+ #!/usr/bin/env bash
+ # Launch Logos Bible Software as a Chromium web app
+ exec chromium --app="https://app.logos.com" --name="Logos" "$@"
+ '';
+ };
+
+ # Logos web app icon
+ home.file.".local/share/icons/hicolor/scalable/apps/logos.svg".source = ./logos-icon.svg;
+
+ # Desktop entry for Logos web app
+ home.file.".local/share/applications/logos.desktop" = {
+ force = true;
+ text = ''
+ [Desktop Entry]
+ Type=Application
+ Name=Logos Bible Software
+ Comment=Logos Bible Software (Web App)
+ Exec=logos
+ Icon=logos
+ Terminal=false
+ Categories=Office;Education;Network;
+ StartupWMClass=logos
+ MimeType=x-scheme-handler/logos;
+ '';
+ };
}
diff --git a/home/hyprland.conf b/home/hyprland.conf
index 4c79240..365c562 100644
--- a/home/hyprland.conf
+++ b/home/hyprland.conf
@@ -4,7 +4,7 @@ monitor=,preferred,auto,1
monitor=DP-3,preferred,auto,1.5
env = XCURSOR_SIZE,24
-env = XCURSOR_THEME,Bibata-Modern-Classic
+env = XCURSOR_THEME,Haiku_Cursor
env = GTK_THEME,adw-gtk3-dark
env = QT_QPA_PLATFORMTHEME,gtk2
@@ -14,11 +14,12 @@ exec-once = wl-paste --type text --watch cliphist store
exec-once = wl-paste --type image --watch cliphist store
exec-once = hypridle
exec-once = ~/.local/bin/wallpaper-rotate
+exec-once = blueman-applet
-# Auto-launch apps into their workspaces
-exec-once = [workspace 1 silent] ghostty -e zellij
-exec-once = [workspace 3 silent] firefox
-exec-once = [workspace 4 silent] discord
+# Auto-launch apps (window rules place them in correct workspaces)
+exec-once = ghostty -e zellij
+exec-once = firefox
+exec-once = discord
input {
kb_layout = us
@@ -85,9 +86,11 @@ bind = $mainMod SHIFT, Return, exec, ghostty
bind = $mainMod, B, exec, firefox
bind = $mainMod, D, exec, fuzzel
bind = $mainMod, Space, exec, fuzzel
+bind = $mainMod SHIFT, Space, exec, rofi -show drun
bind = $mainMod, V, exec, cliphist list | rofi -dmenu | cliphist decode | wl-copy
bind = $mainMod, N, exec, thunar
bind = $mainMod, L, exec, hyprlock
+bind = $mainMod, X, exec, blueman-manager
bind = $mainMod, Q, killactive
bind = $mainMod, F, fullscreen
bind = $mainMod, G, togglefloating
@@ -137,6 +140,10 @@ bindel = , XF86MonBrightnessDown, exec, brightnessctl set 5%-
bind = $mainMod SHIFT, Q, exec, hyprctl dispatch exit
bind = $mainMod SHIFT, R, exec, hyprctl reload
+# Lid switch: close → external display only, open → restore internal
+bindl = , switch:on:Lid Switch, exec, ~/.config/hypr/scripts/lid-handler.sh close
+bindl = , switch:off:Lid Switch, exec, ~/.config/hypr/scripts/lid-handler.sh open
+
windowrule = match:class ^(pavucontrol)$, float on
windowrule = match:class ^(firefox)$, match:title PIP, float on
diff --git a/home/logos-icon.svg b/home/logos-icon.svg
new file mode 100644
index 0000000..220fe8e
--- /dev/null
+++ b/home/logos-icon.svg
@@ -0,0 +1,4 @@
+
diff --git a/home/macos.nix b/home/macos.nix
new file mode 100644
index 0000000..b900388
--- /dev/null
+++ b/home/macos.nix
@@ -0,0 +1,270 @@
+{ config, pkgs, lib, ... }:
+
+{
+ # === macOS-specific configuration ===
+
+ home.packages = with pkgs; [
+ # macOS window manager
+ aerospace
+
+ # Keyboard remapping (GUI app — download separately or install via brew)
+ # brew install --cask karabiner-elements
+ ];
+
+ # =============================================================================
+ # KEYBOARD GOAL: Consistent finger positions across keyboards
+ #
+ # ThinkPad T490 (Linux): Ctrl | Super(Win) | Alt | Space
+ # Keychron V10M (macOS): Ctrl | Alt(⌥) | Cmd | Space ← remapped
+ # After remap (macOS): Ctrl | Super(Cmd) | Alt | Space
+ #
+ # Muscle memory:
+ # Middle key → Super/Cmd = WM actions (workspaces, focus, move)
+ # Next to space → Alt/Option = app shortcuts
+ #
+ # Remap: Physical Alt(⌥) → Cmd(⌘), Physical Cmd(⌘) → Alt(⌥)
+ # Applied via Karabiner-Elements, targeted to Keychron only.
+ #
+ # To find your Keychron's vendor/product IDs:
+ # 1. Open Karabiner-Elements → EventViewer tab
+ # 2. Press a key on the Keychron
+ # 3. Note the "VendorID" and "ProductID" values
+ # 4. Update them in the JSON below
+ #
+ # =============================================================================
+
+ home.file.".config/karabiner/karabiner.json" = {
+ force = true;
+ text = builtins.toJSON {
+ global = {
+ check_for_updates_on_startup = false;
+ show_in_menu_bar = false;
+ show_profile_name_in_menu_bar = false;
+ };
+ profiles = [
+ {
+ name = "Default";
+ selected = true;
+ simple_modifications = [];
+ fn_function_keys = [];
+ virtual_hid_keyboard = {
+ keyboard_type = "ansi";
+ caps_lock_delay_microseconds = 0;
+ };
+ devices = [];
+ complex_modifications = {
+ parameters = {
+ "basic.simultaneous_threshold_milliseconds" = 50;
+ "basic.to_if_held_down_threshold_milliseconds" = 500;
+ "basic.to_delayed_action_delay_milliseconds" = 500;
+ "basic.to_if_alone_timeout_milliseconds" = 1000;
+ };
+ rules = [
+ {
+ description = "Keychron V10M: swap Option ↔ Command (match ThinkPad bottom row)";
+ manipulators = [
+ {
+ # Physical Alt(⌥) → sends Cmd(⌘) = Super/WM key
+ type = "basic";
+ from = {
+ key_code = "left_alt";
+ modifiers = { optional = [ "any" ]; };
+ };
+ to = [ { key_code = "left_gui"; } ];
+ conditions = [
+ {
+ type = "device_if";
+ identifiers = [
+ {
+ # ⚠ UPDATE THESE with your Keychron's IDs
+ vendor_id = 0;
+ product_id = 0;
+ description = "Keychron V10M (Alice 75)";
+ }
+ ];
+ }
+ ];
+ }
+ {
+ # Physical Cmd(⌘) → sends Alt(⌥) = Alt key
+ type = "basic";
+ from = {
+ key_code = "left_gui";
+ modifiers = { optional = [ "any" ]; };
+ };
+ to = [ { key_code = "left_alt"; } ];
+ conditions = [
+ {
+ type = "device_if";
+ identifiers = [
+ {
+ vendor_id = 0;
+ product_id = 0;
+ }
+ ];
+ }
+ ];
+ }
+ ];
+ }
+ ];
+ };
+ }
+ ];
+ };
+ };
+
+ # =============================================================================
+ # AEROSPACE — macOS Tiling Window Manager
+ #
+ # Keybindings use `cmd` (Super after remap) to match Hyprland's SUPER:
+ # Middle key + number → switch workspace
+ # Middle key + h/j/k/l → focus direction
+ # Middle key + tab → focus next window (cycle through all workspaces)
+ # Middle key + shift → move window
+ #
+ # macOS-native Cmd+Tab (now Alt+Tab after remap) still cycles apps.
+ # =============================================================================
+
+ home.file.".config/aerospace/aerospace.toml" = {
+ force = true;
+ text = ''
+ # =============================================================================
+ # Aerospace — macOS Tiling Window Manager
+ #
+ # Keybinding conventions (after Keychron remap matches ThinkPad):
+ # cmd (middle key) = SUPER — WM actions
+ # alt (next to space) = ALT — app shortcuts
+ # =============================================================================
+
+ # --- General ---
+ [general]
+ border = true
+ gaps = {
+ inner = { px = 6, vertical = 8, horizontal = 8 },
+ outer = { px = 4, px-monitor-workspace = 4, bottom = 6 },
+ }
+
+ # =============================================================================
+ # WORKSPACE SWITCHING — cmd = middle key
+ # =============================================================================
+ [mode.main.binding]
+ # Switch to workspace
+ cmd-1 = "workspace 1"
+ cmd-2 = "workspace 2"
+ cmd-3 = "workspace 3"
+ cmd-4 = "workspace 4"
+ cmd-5 = "workspace 5"
+ cmd-6 = "workspace 6"
+ cmd-7 = "workspace 7"
+ cmd-8 = "workspace 8"
+ cmd-9 = "workspace 9"
+
+ # Move window to workspace
+ cmd-shift-1 = "move-node-to-workspace 1"
+ cmd-shift-2 = "move-node-to-workspace 2"
+ cmd-shift-3 = "move-node-to-workspace 3"
+ cmd-shift-4 = "move-node-to-workspace 4"
+ cmd-shift-5 = "move-node-to-workspace 5"
+ cmd-shift-6 = "move-node-to-workspace 6"
+ cmd-shift-7 = "move-node-to-workspace 7"
+ cmd-shift-8 = "move-node-to-workspace 8"
+ cmd-shift-9 = "move-node-to-workspace 9"
+
+ # =============================================================================
+ # FOCUS NAVIGATION — Vim-style with cmd (middle key)
+ # =============================================================================
+ cmd-h = "focus left"
+ cmd-j = "focus down"
+ cmd-k = "focus up"
+ cmd-l = "focus right"
+
+ # Tab through windows across all workspaces
+ cmd-tab = "focus next"
+
+ # =============================================================================
+ # WINDOW MANAGEMENT
+ # =============================================================================
+ cmd-f = "fullscreen"
+ cmd-w = "close"
+ cmd-t = "floating toggle"
+ cmd-d = "layout tiles horizontal vertical"
+ cmd-shift-d = "layout tiles vertical horizontal"
+
+ # =============================================================================
+ # LAYOUT MANAGEMENT
+ # =============================================================================
+ cmd-r = "mode resize"
+
+ cmd-shift-h = "move left"
+ cmd-shift-j = "move down"
+ cmd-shift-k = "move up"
+ cmd-shift-l = "move right"
+
+ # =============================================================================
+ # RESIZE MODE
+ # =============================================================================
+ [mode.resize.binding]
+ cmd-h = "resize -50 0"
+ cmd-j = "resize 0 50"
+ cmd-k = "resize 0 -50"
+ cmd-l = "resize 50 0"
+ escape = "mode main"
+ enter = "mode main"
+
+ # =============================================================================
+ # MISC
+ # =============================================================================
+ cmd-shift-period = "reload-config"
+ cmd-backslash = "layout v_accordion"
+ cmd-shift-slash = "layout h_accordion"
+
+ # =============================================================================
+ # APP CYCLING
+ #
+ # macOS natively handles app cycling via Cmd+Tab.
+ # After Karabiner remap, the physical key next to spacebar now sends Alt,
+ # so what was Cmd+Tab is now available as a distinct chord.
+ # We use cmd+tab (middle key + tab) for Aerospace's "focus next" above,
+ # and the macOS native cycle is still accessible via alt+tab (next-to-space + tab).
+ # =============================================================================
+ '';
+ };
+
+ # =============================================================================
+ # HELPER: Find Keychron keyboard IDs for Karabiner
+ # =============================================================================
+ home.file.".local/bin/find-keychron-id" = {
+ force = true;
+ executable = true;
+ text = ''
+ #!/usr/bin/env bash
+ # Find your Keychron's vendor/product IDs for Karabiner-Elements config.
+ #
+ # Usage: ./find-keychron-id
+ #
+ # If Karabiner EventViewer is running, check its log for recent key events.
+ LOG="$HOME/.local/share/karabiner/eventviewer.log"
+ if [ -f "$LOG" ]; then
+ echo "Checking Karabiner EventViewer log..."
+ grep -i "keychron\|vendor_id\|product_id" "$LOG" 2>/dev/null | tail -10
+ fi
+
+ # Alternative: use ioreg to list HID devices
+ echo ""
+ echo "All HID keyboard devices (look for Keychron):"
+ ioreg -p IOUSB -l -w 0 | grep -A 10 -i "keyboard\|keychron" 2>/dev/null || \\
+ ioreg -r -c IOUSBDevice -l -w 0 | grep -A 15 -i "keychron\|keyboard" | grep -E "vendor|product|idProduct|idVendor|USB Product Name" | head -20
+
+ echo ""
+ echo "Or press a key on your Keychron and run:"
+ echo " hidutil list 2>/dev/null | grep -i keychron"
+ echo ""
+ echo "Then update vendor_id and product_id (as DECIMAL numbers) in:"
+ echo " ~/.config/karabiner/karabiner.json"
+ echo ""
+ echo " e.g. if vendor_id=0x3434, product_id=0x0408"
+ echo " then in JSON: vendor_id=13364, product_id=1032"
+ '';
+ };
+}
diff --git a/home/spotlight-dark.rasi b/home/spotlight-dark.rasi
new file mode 100644
index 0000000..9f5ca38
--- /dev/null
+++ b/home/spotlight-dark.rasi
@@ -0,0 +1,150 @@
+/* Spotlight-Dark — macOS-inspired Rofi theme */
+
+configuration {
+ modi: "drun,run,window";
+ show-icons: true;
+ icon-theme: "Papirus-Dark";
+ drun-display-format: "{name}";
+ font: "JetBrainsMono Nerd Font 14";
+ matching: "fuzzy";
+ terminal: "ghostty";
+ fuzzy-match: true;
+ scrollbar: false;
+ sidebar-mode: false;
+}
+
+* {
+ bg-col: rgba(30, 30, 46, 0.93);
+ bg-alt: rgba(49, 50, 68, 0.95);
+ fg-col: rgba(205, 214, 244, 1.0);
+ fg-dim: rgba(205, 214, 244, 0.5);
+ sel-bg: rgba(137, 180, 250, 0.18);
+ urgent-col: rgba(243, 139, 168, 1.0);
+ border-col: rgba(137, 180, 250, 0.0);
+}
+
+window {
+ width: 520px;
+ location: center;
+ anchor: center;
+ fullscreen: false;
+ y-offset: -120px;
+
+ border: 0px;
+ border-radius: 14px;
+ background-color: @bg-col;
+ text-color: @fg-col;
+ padding: 0;
+}
+
+mainbox {
+ padding: 0;
+ children: [ inputbar, listview ];
+ spacing: 0;
+}
+
+inputbar {
+ padding: 14px 18px;
+ background-color: @bg-alt;
+ border-radius: 14px 14px 0 0;
+ margin: 0;
+ children: [ entry, case-indicator ];
+ spacing: 8;
+}
+
+entry {
+ background: transparent;
+ text-color: @fg-col;
+ caret-color: @fg-col;
+ placeholder-color: @fg-dim;
+ placeholder: "Search apps, commands...";
+ expand: true;
+ horizontal-align: 0;
+ vertical-align: 0.5;
+ font: "JetBrainsMono Nerd Font 14";
+}
+
+case-indicator {
+ background: transparent;
+ text-color: @fg-dim;
+ font: "JetBrainsMono Nerd Font 10";
+ spacing: 0;
+}
+
+listview {
+ background: transparent;
+ padding: 6px 0;
+ lines: 8;
+ columns: 1;
+ spacing: 0;
+ cycle: true;
+ dynamic: true;
+ scrollbar: false;
+ layout: vertical;
+}
+
+element {
+ padding: 10px 18px;
+ border-radius: 0;
+ text-color: @fg-col;
+ background-color: transparent;
+}
+
+element normal.normal {
+ background-color: transparent;
+ text-color: @fg-col;
+}
+
+element selected.normal {
+ background-color: @sel-bg;
+ text-color: @fg-col;
+}
+
+element urgent.normal {
+ text-color: @urgent-col;
+}
+
+element-icon {
+ size: 22px;
+ padding: 0 12px 0 0;
+ horizontal-align: 0;
+ vertical-align: 0.5;
+}
+
+element-text {
+ padding: 0;
+ vertical-align: 0.5;
+ horizontal-align: 0;
+ text-color: inherit;
+ font: "JetBrainsMono Nerd Font 12";
+}
+
+element-text selected {
+ text-color: @fg-col;
+}
+
+message {
+ margin: 0;
+ padding: 10px 18px;
+ border-radius: 0 0 14px 14px;
+ background-color: @bg-alt;
+ text-color: @fg-dim;
+}
+
+textbox {
+ padding: 4px 8px;
+ text-color: @fg-dim;
+}
+
+button {
+ padding: 4px;
+ border-radius: 4px;
+ text-color: @fg-col;
+}
+
+scrollbar {
+ width: 4px;
+ border: 0;
+ handle-width: 4px;
+ handle-color: @border-col;
+}
diff --git a/home/sscoble.nix b/home/sscoble.nix
index 9911ca6..76cbb8b 100644
--- a/home/sscoble.nix
+++ b/home/sscoble.nix
@@ -45,20 +45,22 @@
# === Git Config ===
programs.git = {
enable = true;
- userName = "Scott Scoble";
- userEmail = "sscoble@scobleclaw.dev";
- extraConfig = {
+ settings = {
+ user = {
+ name = "Scott Scoble";
+ email = "sscoble@scobleclaw.dev";
+ };
init.defaultBranch = "main";
pull.rebase = true;
push.autoSetupRemote = true;
core.editor = "nvim";
- };
- aliases = {
- lol = "log --graph --oneline --decorate --all";
- co = "checkout";
- br = "branch";
- st = "status";
- ci = "commit";
+ alias = {
+ lol = "log --graph --oneline --decorate --all";
+ co = "checkout";
+ br = "branch";
+ st = "status";
+ ci = "commit";
+ };
};
};
@@ -156,7 +158,7 @@
l = "ls -CF";
nix-clean = "sudo nix-collect-garbage -d && nix-collect-garbage -d";
};
- initExtra = ''
+ initContent = ''
# Nix flake shortcuts (adapts to platform)
if command -v nixos-rebuild &>/dev/null; then
alias nr="sudo nixos-rebuild switch --flake git+https://os.scoble.me/forgejo/sscoble/laptop-config.git#laptop"
diff --git a/home/wgu.nix b/home/wgu.nix
index acc1cb0..c802ccf 100644
--- a/home/wgu.nix
+++ b/home/wgu.nix
@@ -8,7 +8,6 @@
# Browsers
brave
- firefox
# Note-taking
obsidian
diff --git a/modules/hyprland.nix b/modules/hyprland.nix
index 917b6c8..4aa38d7 100644
--- a/modules/hyprland.nix
+++ b/modules/hyprland.nix
@@ -10,6 +10,49 @@
# Session variables
environment.sessionVariables.NIXOS_OZONE_WL = "1";
+ # === Lid close behavior ===
+ # Don't suspend when lid closes — external display stays active
+ services.logind = {
+ lidSwitch = "ignore";
+ lidSwitchExternalPower = "ignore";
+ lidSwitchDocked = "ignore";
+ };
+
+ # Fallback ACPI handler for lid events outside Hyprland session
+ # Primary handling is done via Hyprland bindl in hyprland.conf
+ services.acpid = {
+ enable = true;
+ handlers.lid = {
+ event = "button/lid.*";
+ action = ''
+ ${pkgs.bash}/bin/bash -c '
+ LID_STATE=$(cat /proc/acpi/button/lid/LID0/state 2>/dev/null || cat /proc/acpi/button/lid/LID/state 2>/dev/null || echo "open")
+ if echo "$LID_STATE" | grep -qi "closed"; then
+ EVENT="close"
+ else
+ EVENT="open"
+ fi
+ for user_dir in /run/user/*/hypr/; do
+ [ -d "$user_dir" ] || continue
+ SIG=$(ls "$user_dir" 2>/dev/null | head -1)
+ [ -z "$SIG" ] && continue
+ UID=$(basename $(dirname "$user_dir"))
+ USER=$(id -nu $UID 2>/dev/null || echo "sscoble")
+ HOME_DIR=$(getent passwd "$USER" | cut -d: -f6)
+ USER_SCRIPT="$HOME_DIR/.config/hypr/scripts/lid-handler.sh"
+ if [ -x "$USER_SCRIPT" ]; then
+ sudo -u "$USER" \
+ XDG_RUNTIME_DIR="/run/user/$UID" \
+ HYPRLAND_INSTANCE_SIGNATURE="$SIG" \
+ "$USER_SCRIPT" "$EVENT"
+ fi
+ break
+ done
+ '
+ '';
+ };
+ };
+
# Security — required for hyprlock
security.pam.services.hyprlock = {};
@@ -82,6 +125,15 @@
hardware.bluetooth.enable = true;
hardware.bluetooth.powerOnBoot = true;
+ # Bluetooth manager (GUI pairing, tray applet)
+ # Note: using manual setup instead of services.blueman.enable to avoid
+ # a systemd unit bug (duplicate ExecStart in blueman-applet.service).
+ # The applet is launched via exec-once in hyprland.conf instead.
+ environment.systemPackages = with pkgs; [ blueman ];
+ services.dbus.packages = with pkgs; [ blueman ];
+ systemd.user.services.blueman-applet = { enable = false; };
+ systemd.user.services.blueman-manager = { enable = false; };
+
# NetworkManager applet
programs.nm-applet.enable = true;
}
diff --git a/modules/laptop.nix b/modules/laptop.nix
index 6d9ad7d..42a8f0c 100644
--- a/modules/laptop.nix
+++ b/modules/laptop.nix
@@ -43,8 +43,15 @@
wineWow64Packages.stable
];
- # DNS fallback (Tailscale MagicDNS sometimes blocks public resolvers)
- networking.nameservers = [ "1.1.1.1" "8.8.8.8" ];
+ # === WiFi / NetworkManager (imperative — not idempotent) ===
+ # We move between SSIDs regularly, so NO declarative wifi networks here.
+ # Use nm-applet (tray) or nmcli (CLI) to connect to any SSID at runtime.
+ # Connections persist across reboots via /etc/NetworkManager/system-connections/
+ networking.networkmanager.enable = true;
+
+ # DNS handed out by NM to each connection; Tailscale MagicDNS can interfere
+ # with public resolvers, so we inject Cloudflare + Google as fallbacks.
+ networking.networkmanager.insertNameservers = [ "1.1.1.1" "8.8.8.8" ];
# Passwordless sudo for sscoble
security.sudo.extraRules = [
diff --git a/modules/printing.nix b/modules/printing.nix
index 5531626..9dc4d6b 100644
--- a/modules/printing.nix
+++ b/modules/printing.nix
@@ -20,7 +20,7 @@
# Enable network printer discovery via mDNS/Bonjour
services.avahi = {
enable = true;
- nssmdns = true;
+ nssmdns4 = true;
publish = {
enable = true;
addresses = true;
diff --git a/modules/t490.nix b/modules/t490.nix
index 15d20a0..6d50f29 100644
--- a/modules/t490.nix
+++ b/modules/t490.nix
@@ -63,7 +63,51 @@
# === powertop ===
powerManagement.powertop.enable = true;
- # Skip thermald — conflicts with throttled (enabled by nixos-hardware)
+ # === throttled — undervolt + raise trip temp for better sustained perf ===
+ services.throttled = {
+ extraConfig = ''
+ [GENERAL]
+ Enabled: True
+ Sysfs_Power_Path: /sys/class/power_supply/AC*/online
+ Autoreload: True
+
+ [BATTERY]
+ Update_Rate_s: 30
+ PL1_Tdp_W: 29
+ PL1_Duration_s: 28
+ PL2_Tdp_W: 29
+ PL2_Duration_S: 0.002
+ Trip_Temp_C: 85
+ cTDP: 0
+ Disable_BDPROCHOT: False
+
+ [AC]
+ Update_Rate_s: 5
+ PL1_Tdp_W: 44
+ PL1_Duration_s: 28
+ PL2_Tdp_W: 44
+ PL2_Duration_S: 0.002
+ Trip_Temp_C: 97
+ cTDP: 0
+ Disable_BDPROCHOT: False
+
+ [UNDERVOLT.BATTERY]
+ CORE: -70
+ GPU: -50
+ CACHE: -70
+ UNCORE: -40
+ ANALOGIO: -40
+
+ [UNDERVOLT.AC]
+ CORE: -80
+ GPU: -50
+ CACHE: -80
+ UNCORE: -50
+ ANALOGIO: -50
+ '';
+ };
+
+ # Skip thermald — conflicts with throttled
services.thermald.enable = false;
# === zram swap (faster + saves NVMe writes) ===
@@ -135,11 +179,14 @@
# === Tailscale (consistent cluster access from any network) ===
services.tailscale.enable = true;
+ # === Proton Mail Bridge (local IMAP/SMTP for Proton Mail) ===
+ # The systemd user service is defined in home/desktop.nix
+
# === NVMe tools ===
- environment.systemPackages = with pkgs; [ nvme-cli ];
+ environment.systemPackages = with pkgs; [ nvme-cli protonmail-bridge ];
# === Touchpad & TrackPoint ===
- services.xserver.libinput = {
+ services.libinput = {
enable = true;
touchpad = {
tapping = true;
diff --git a/pi-overlay.nix b/pi-overlay.nix
index 29e8f78..e908b38 100644
--- a/pi-overlay.nix
+++ b/pi-overlay.nix
@@ -2,4 +2,7 @@ final: prev: {
qmd = final.writeShellScriptBin "qmd" ''
exec ${final.bun}/bin/bunx --bun @tobilu/qmd "$@"
'';
+
+ beos-r5-icons = final.callPackage ./pkgs/beos-r5-icons { };
+ haiku-cursor = final.callPackage ./pkgs/haiku-cursor { };
}
diff --git a/pkgs/beos-r5-icons/default.nix b/pkgs/beos-r5-icons/default.nix
new file mode 100644
index 0000000..7da88b7
--- /dev/null
+++ b/pkgs/beos-r5-icons/default.nix
@@ -0,0 +1,34 @@
+{ lib, stdenvNoCC, fetchFromGitHub }:
+
+stdenvNoCC.mkDerivation {
+ pname = "beos-r5-icons";
+ version = "2021-03-24";
+
+ src = fetchFromGitHub {
+ owner = "jcherven";
+ repo = "BeOS-r5-Icons";
+ rev = "1c1067eabaeb0196c12606ac19b2aed4496bc15e";
+ hash = "sha256-6qnOWMdwgK9tKifHXSF6s1KjHkdIG/jnM9Te43cVOjo=";
+ };
+
+ installPhase = ''
+ mkdir -p $out/share/icons/BeOS-r5
+ cp -pr * $out/share/icons/BeOS-r5/
+ # Remove broken symlinks (icon theme has some dangling links)
+ find $out/share/icons/BeOS-r5 -type l -not -exec test -e {} \; -delete
+ rm -f $out/share/icons/BeOS-r5/screenshot.png
+ rm -f $out/share/icons/BeOS-r5/.icon-theme.cache
+ '';
+
+ meta = with lib; {
+ description = "Classic BeOS r5 icons for Linux desktops";
+ longDescription = ''
+ A large collection of BeOS & period-correct icons.
+ Includes icons from BeOS-r5, Zeta OS, Haiku OS, KDEclassic, and more.
+ '';
+ homepage = "https://github.com/jcherven/BeOS-r5-Icons";
+ license = licenses.gpl2Plus;
+ platforms = platforms.linux;
+ maintainers = [ ];
+ };
+}
diff --git a/pkgs/haiku-cursor/default.nix b/pkgs/haiku-cursor/default.nix
new file mode 100644
index 0000000..45f9b2e
--- /dev/null
+++ b/pkgs/haiku-cursor/default.nix
@@ -0,0 +1,28 @@
+{ lib, stdenvNoCC, fetchurl }:
+
+stdenvNoCC.mkDerivation {
+ pname = "haiku-cursor";
+ version = "linux-02";
+
+ src = fetchurl {
+ url = "https://github.com/leath-dub/Haiku_Cursor/releases/download/linux-02/Haiku_Cursor.tgz";
+ hash = "sha256-AXgo+jqrCjEyDpWL6GnpUAbvx3Oje+7zQvnIC8j1uz0=";
+ };
+
+ installPhase = ''
+ mkdir -p $out/share/icons/Haiku_Cursor
+ cp -pr * $out/share/icons/Haiku_Cursor/
+ '';
+
+ meta = with lib; {
+ description = "Haiku/BeOS-style XCursor theme";
+ longDescription = ''
+ A port of the Haiku cursor set as an XCursor theme.
+ These cursors are faithful to the BeOS/Haiku look.
+ '';
+ homepage = "https://github.com/leath-dub/Haiku_Cursor";
+ license = licenses.mit;
+ platforms = platforms.linux;
+ maintainers = [ ];
+ };
+}