commit 90cff4f16a192bf4e7a236eed18e373ed96fd259 Author: GammaKinematics Date: Mon Mar 30 13:10:42 2026 +0700 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..17550b3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +# Nix build artifacts +result +result-* + +# Secrets (never commit these) +secrets/ +*.secret +*.key +*.pem + +# Editor backups +*.backup +*~ +*.swp + +# Local overrides +local.nix + +# Claude stuff +.claude/ + +# Quickshell generated theme files (generated by Nix) +Hyprland/Quickshell/bar/Theme.qml +Hyprland/Quickshell/control-center/Theme.qml + +# nixos-hardware fork (now on GitHub) +nixos-hardware-fork/ + +# Hyprland wiki +hyprland-wiki/ + +/st/ + +/dwm/ diff --git a/Hetzner/axiomania.nix b/Hetzner/axiomania.nix new file mode 100644 index 0000000..2fb126e --- /dev/null +++ b/Hetzner/axiomania.nix @@ -0,0 +1,409 @@ +{ pkgs, ... }: + +{ + imports = [ ./common.nix ]; + + networking.hostName = "axiomania"; + + # ============================================================================ + # Firewall + # ============================================================================ + networking.firewall = { + allowedTCPPorts = [ 22 80 443 2222 ]; + allowedUDPPorts = [ 51820 53 ]; # Wireguard, Adguard DNS + }; + + # ============================================================================ + # Forgejo + # ============================================================================ + services.forgejo = { + enable = true; + database.type = "sqlite3"; + settings = { + DEFAULT.APP_NAME = "Axiomania Git"; + + server = { + DOMAIN = "git.axiomania.org"; + ROOT_URL = "https://git.axiomania.org/"; + HTTP_ADDR = "127.0.0.1"; + HTTP_PORT = 3000; + SSH_DOMAIN = "git.axiomania.org"; + SSH_PORT = 2222; + START_SSH_SERVER = true; + }; + + service = { + DISABLE_REGISTRATION = true; + DEFAULT_KEEP_EMAIL_PRIVATE = true; + }; + + repository = { + DEFAULT_PRIVATE = "private"; + }; + + api = { + ENABLE_SWAGGER = false; + }; + + session = { + COOKIE_SECURE = true; + }; + + actions = { + ENABLED = true; + }; + }; + }; + + # ============================================================================ + # Forgejo Actions Runner + # ============================================================================ + # Requires a registration token from Forgejo. + # Generate one: ssh root@178.104.15.221, then: + # forgejo-runner register (follow prompts) + # Or via Forgejo web UI: Site Administration > Runners > Create Runner + # Then put the token in /var/lib/forgejo-runner/token + services.gitea-actions-runner.instances.default = { + enable = true; + name = "axiomania"; + url = "https://git.axiomania.org"; + tokenFile = "/var/lib/forgejo-runner/token"; + labels = [ "native:host" ]; + hostPackages = with pkgs; [ + bash + coreutils + curl + gawk + git + gnused + nodejs + nix + ]; + }; + + # ============================================================================ + # Vaultwarden (Bitwarden-compatible password manager) + # ============================================================================ + services.vaultwarden = { + enable = true; + config = { + DOMAIN = "https://vault.axiomania.org"; + SIGNUPS_ALLOWED = false; + ROCKET_ADDRESS = "127.0.0.1"; + ROCKET_PORT = 8222; + }; + }; + + # ============================================================================ + # Navidrome (music streaming) + # ============================================================================ + services.navidrome = { + enable = true; + settings = { + Address = "127.0.0.1"; + Port = 4533; + MusicFolder = "/var/lib/navidrome/music"; + }; + }; + + # ============================================================================ + # Syncthing (file sync) + # ============================================================================ + services.syncthing = { + enable = true; + openDefaultPorts = true; + dataDir = "/var/lib/syncthing"; + settings.gui.insecureSkipHostcheck = true; + }; + + # ============================================================================ + # Attic (Nix binary cache) + # ============================================================================ + # Requires a secret generated on the server: + # openssl genrsa -traditional 4096 | base64 -w0 > /var/lib/atticd/token-secret + # echo "ATTIC_SERVER_TOKEN_RS256_SECRET_BASE64=$(cat /var/lib/atticd/token-secret)" > /etc/atticd.env + services.atticd = { + enable = true; + environmentFile = "/etc/atticd.env"; + settings = { + listen = "[::]:8080"; + storage = { + type = "local"; + path = "/var/lib/atticd/storage"; + }; + garbage-collection = { + default-retention-period = "3 months"; + }; + chunking = { + nar-size-threshold = 65536; + min-size = 16384; + avg-size = 65536; + max-size = 262144; + }; + }; + }; + + # ============================================================================ + # Kavita (ebook library) + # ============================================================================ + # Generate token on the server: + # openssl rand -hex 128 > /var/lib/kavita/token-key + services.kavita = { + enable = true; + settings.Port = 5000; + tokenKeyFile = "/var/lib/kavita/token-key"; + }; + + # ============================================================================ + # Audiobookshelf + # ============================================================================ + services.audiobookshelf = { + enable = true; + port = 8234; + host = "127.0.0.1"; + }; + + # ============================================================================ + # Paperless-ngx (document management) + # ============================================================================ + services.paperless = { + enable = true; + port = 28981; + address = "127.0.0.1"; + domain = "docs.axiomania.org"; + }; + + # ============================================================================ + # Adguard Home (DNS ad blocker) + # ============================================================================ + # Use as DNS server via Wireguard: set DNS = 10.100.0.1 in client config + services.adguardhome = { + enable = true; + port = 3300; + host = "127.0.0.1"; + }; + + # ============================================================================ + # SearXNG (private search engine) + # ============================================================================ + # Generate secret on the server: + # echo "SEARX_SECRET=$(openssl rand -hex 32)" > /etc/searx/secrets.env + services.searx = { + enable = true; + redisCreateLocally = true; + settings = { + server = { + bind_address = "127.0.0.1"; + port = 8888; + secret_key = "$SEARX_SECRET"; + }; + }; + environmentFile = "/etc/searx/secrets.env"; + }; + + # ============================================================================ + # Homepage (services dashboard) + # ============================================================================ + services.homepage-dashboard = { + enable = true; + listenPort = 8082; + allowedHosts = "home.axiomania.org"; + }; + + # ============================================================================ + # Matrix Conduit (encrypted messaging) + # ============================================================================ + services.matrix-conduit = { + enable = true; + settings.global = { + server_name = "axiomania.org"; + port = 6167; + address = "127.0.0.1"; + allow_registration = false; + }; + }; + + # ============================================================================ + # Gokapi (file sharing) — disabled due to CVEs in stable + # ============================================================================ + # services.gokapi = { + # enable = true; + # environment = { + # GOKAPI_PORT = 53842; + # }; + # }; + + # ============================================================================ + # Wireguard VPN + # ============================================================================ + # Generate keys on the server: + # wg genkey | tee /etc/wireguard/private.key | wg pubkey > /etc/wireguard/public.key + networking.wireguard.interfaces.wg0 = { + ips = [ "10.100.0.1/24" ]; + listenPort = 51820; + privateKeyFile = "/etc/wireguard/private.key"; + peers = [ + { + # Minisforum V3 SE (local NixOS machine) + publicKey = "rEsHqzJVnv9AoOIEVTrdEF3x4G6rWxXqHLXTzAx88gc="; + allowedIPs = [ "10.100.0.2/32" ]; + } + { + # Pixel 5 + publicKey = "jP+rYdc8qKdNY4l3PDFmGiOA31SnJRgcmQIVu6AJ9EM="; + allowedIPs = [ "10.100.0.3/32" ]; + } + ]; + }; + + # NAT for Wireguard clients to access the internet + networking.nat = { + enable = true; + externalInterface = "ens3"; + internalInterfaces = [ "wg0" ]; + }; + + # ============================================================================ + # Nginx reverse proxy + ACME + # ============================================================================ + services.nginx = { + enable = true; + recommendedProxySettings = true; + recommendedTlsSettings = true; + recommendedOptimisation = true; + recommendedGzipSettings = true; + + # ── Public services ── + virtualHosts."git.axiomania.org" = { + enableACME = true; + forceSSL = true; + locations."/" = { + proxyPass = "http://127.0.0.1:3000"; + proxyWebsockets = true; + }; + }; + + virtualHosts."matrix.axiomania.org" = { + enableACME = true; + forceSSL = true; + locations."/" = { + proxyPass = "http://127.0.0.1:6167"; + proxyWebsockets = true; + }; + }; + + virtualHosts."cache.axiomania.org" = { + enableACME = true; + forceSSL = true; + locations."/" = { + proxyPass = "http://[::1]:8080"; + proxyWebsockets = true; + }; + }; + + # ── VPN-only services (WireGuard clients only) ── + virtualHosts."vault.axiomania.org" = { + enableACME = true; + forceSSL = true; + locations."/" = { + proxyPass = "http://127.0.0.1:8222"; + proxyWebsockets = true; + extraConfig = "allow 10.100.0.0/24; allow 127.0.0.1; deny all;"; + }; + }; + + virtualHosts."music.axiomania.org" = { + enableACME = true; + forceSSL = true; + locations."/" = { + proxyPass = "http://127.0.0.1:4533"; + proxyWebsockets = true; + extraConfig = "allow 10.100.0.0/24; allow 127.0.0.1; deny all;"; + }; + }; + + virtualHosts."sync.axiomania.org" = { + enableACME = true; + forceSSL = true; + locations."/" = { + proxyPass = "http://127.0.0.1:8384"; + proxyWebsockets = true; + extraConfig = "allow 10.100.0.0/24; allow 127.0.0.1; deny all;"; + }; + }; + + virtualHosts."books.axiomania.org" = { + enableACME = true; + forceSSL = true; + locations."/" = { + proxyPass = "http://127.0.0.1:5000"; + proxyWebsockets = true; + extraConfig = "allow 10.100.0.0/24; allow 127.0.0.1; deny all;"; + }; + }; + + virtualHosts."audio.axiomania.org" = { + enableACME = true; + forceSSL = true; + locations."/" = { + proxyPass = "http://127.0.0.1:8234"; + proxyWebsockets = true; + extraConfig = "allow 10.100.0.0/24; allow 127.0.0.1; deny all;"; + }; + }; + + virtualHosts."docs.axiomania.org" = { + enableACME = true; + forceSSL = true; + locations."/" = { + proxyPass = "http://127.0.0.1:28981"; + proxyWebsockets = true; + extraConfig = "allow 10.100.0.0/24; allow 127.0.0.1; deny all;"; + }; + }; + + virtualHosts."search.axiomania.org" = { + enableACME = true; + forceSSL = true; + locations."/" = { + proxyPass = "http://127.0.0.1:8888"; + proxyWebsockets = true; + extraConfig = "allow 10.100.0.0/24; allow 127.0.0.1; deny all;"; + }; + }; + + virtualHosts."home.axiomania.org" = { + enableACME = true; + forceSSL = true; + locations."/" = { + proxyPass = "http://127.0.0.1:8082"; + proxyWebsockets = true; + extraConfig = "allow 10.100.0.0/24; allow 127.0.0.1; deny all;"; + }; + }; + + # virtualHosts."share.axiomania.org" = { + # enableACME = true; + # forceSSL = true; + # locations."/" = { + # proxyPass = "http://127.0.0.1:53842"; + # proxyWebsockets = true; + # }; + # }; + }; + + security.acme = { + acceptTerms = true; + defaults.email = "gamma.kinematics@gmail.com"; + }; + + # ============================================================================ + # Packages + # ============================================================================ + environment.systemPackages = with pkgs; [ + git + vim + htop + wireguard-tools + ]; +} diff --git a/Hetzner/builder.nix b/Hetzner/builder.nix new file mode 100644 index 0000000..e7fbea2 --- /dev/null +++ b/Hetzner/builder.nix @@ -0,0 +1,26 @@ +{ pkgs, ... }: + +{ + imports = [ ./common.nix ]; + + networking.hostName = "builder"; + + nix.settings = { + max-jobs = "auto"; + substituters = [ + "https://cache.nixos.org" + "https://cache.axiomania.org/main" + ]; + trusted-public-keys = [ + "main:Uz5F0MbXItVx2XCmBbEAMmQ0T6+DZDgLaXWalh1k++o=" + ]; + }; + + environment.systemPackages = with pkgs; [ + git + tmux + attic-client + vim + htop + ]; +} diff --git a/Hetzner/common.nix b/Hetzner/common.nix new file mode 100644 index 0000000..cdd5e7f --- /dev/null +++ b/Hetzner/common.nix @@ -0,0 +1,34 @@ +{ modulesPath, ... }: + +{ + imports = [ + (modulesPath + "/profiles/qemu-guest.nix") + (modulesPath + "/installer/scan/not-detected.nix") + ./disk-config.nix + ]; + + system.stateVersion = "25.11"; + time.timeZone = "Europe/Berlin"; + + nix.settings.experimental-features = [ "nix-command" "flakes" ]; + + boot.loader.grub = { + enable = true; + efiSupport = true; + efiInstallAsRemovable = true; + }; + + services.qemuGuest.enable = true; + + users.users.root.openssh.authorizedKeys.keys = [ + "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIA7XcPUNMwpUqXMqhwTlW3YV2BUj0I6efk0LxqFcyYgA gamma.kinematics@gmail.com" + ]; + + services.openssh = { + enable = true; + settings = { + PasswordAuthentication = false; + PermitRootLogin = "prohibit-password"; + }; + }; +} diff --git a/Hetzner/disk-config.nix b/Hetzner/disk-config.nix new file mode 100644 index 0000000..37365c5 --- /dev/null +++ b/Hetzner/disk-config.nix @@ -0,0 +1,34 @@ +{ lib, ... }: +{ + disko.devices.disk.main = { + device = lib.mkDefault "/dev/sda"; + type = "disk"; + content = { + type = "gpt"; + partitions = { + boot = { + size = "1M"; + type = "EF02"; # BIOS boot partition + }; + esp = { + size = "512M"; + type = "EF00"; + content = { + type = "filesystem"; + format = "vfat"; + mountpoint = "/boot"; + mountOptions = [ "umask=0077" ]; + }; + }; + root = { + size = "100%"; + content = { + type = "filesystem"; + format = "ext4"; + mountpoint = "/"; + }; + }; + }; + }; + }; +} diff --git a/Hetzner/setup.nix b/Hetzner/setup.nix new file mode 100644 index 0000000..0f7a958 --- /dev/null +++ b/Hetzner/setup.nix @@ -0,0 +1,121 @@ +{ pkgs, ... }: + +let + sshKeyPath = "/home/lebowski/.ssh/id_ed25519"; + sshKeyName = "V3"; + serverName = "nix-builder"; + serverType = "ccx43"; + location = "nbg1"; + + sshCmd = "ssh -o StrictHostKeyChecking=no -o BatchMode=yes -i ${sshKeyPath}"; + + # Spin up a builder from the NixOS snapshot + builder-up = pkgs.writeShellApplication { + name = "builder-up"; + runtimeInputs = with pkgs; [ hcloud openssh ]; + text = '' + SERVER_NAME="${serverName}" + SSH_KEY_PATH="${sshKeyPath}" + + # ── Check if server already exists ── + if hcloud server describe "$SERVER_NAME" &>/dev/null; then + IP=$(hcloud server ip "$SERVER_NAME") + echo "Server '$SERVER_NAME' already running at $IP" + echo " builder-ssh" + exit 0 + fi + + # ── Find snapshot ── + SNAPSHOT_ID=$(hcloud image list --type snapshot --selector description="${snapshotDesc}" -o noheader -o columns=id 2>/dev/null | head -1) + if [ -z "$SNAPSHOT_ID" ]; then + # Fallback: search by description + SNAPSHOT_ID=$(hcloud image list --type snapshot -o noheader -o columns=id,description | grep "${snapshotDesc}" | awk '{print $1}' | head -1) + fi + + if [ -z "$SNAPSHOT_ID" ]; then + echo "ERROR: No snapshot '${snapshotDesc}' found." + echo " Create one with: builder-snapshot" + exit 1 + fi + + # ── Create server from snapshot ── + echo "==> Creating ${serverType} in ${location} from snapshot $SNAPSHOT_ID..." + hcloud server create \ + --name "$SERVER_NAME" \ + --type "${serverType}" \ + --image "$SNAPSHOT_ID" \ + --location "${location}" \ + --label role=nix-builder \ + --ssh-key "${sshKeyName}" + + IP=$(hcloud server ip "$SERVER_NAME") + + # ── Wait for SSH ── + echo "==> Waiting for SSH..." + for i in $(seq 1 30); do + if ${sshCmd} "root@''${IP}" true 2>/dev/null; then + break + fi + if [ "$i" -eq 30 ]; then + echo "ERROR: SSH did not become available after 150s" + exit 1 + fi + sleep 5 + done + + # ── Update known_hosts ── + ssh-keygen -R "$IP" 2>/dev/null || true + ssh-keyscan -H "$IP" >> ~/.ssh/known_hosts 2>/dev/null + + echo "" + echo "=== Builder ready ===" + echo " builder-ssh" + ''; + }; + + # SSH into the builder + builder-ssh = pkgs.writeShellApplication { + name = "builder-ssh"; + runtimeInputs = with pkgs; [ hcloud openssh ]; + text = '' + SERVER_NAME="${serverName}" + SSH_KEY_PATH="${sshKeyPath}" + + if ! hcloud server describe "$SERVER_NAME" &>/dev/null; then + echo "No server '$SERVER_NAME' found. Run builder-up first." + exit 1 + fi + + IP=$(hcloud server ip "$SERVER_NAME") + ssh -i "$SSH_KEY_PATH" -t "root@$IP" "tmux new-session -A -s build" + ''; + }; + + # Tear down the builder + builder-down = pkgs.writeShellApplication { + name = "builder-down"; + runtimeInputs = with pkgs; [ hcloud openssh ]; + text = '' + SERVER_NAME="${serverName}" + + if hcloud server describe "$SERVER_NAME" &>/dev/null; then + IP=$(hcloud server ip "$SERVER_NAME") + echo "==> Deleting '$SERVER_NAME' ($IP)..." + hcloud server delete "$SERVER_NAME" + ssh-keygen -R "$IP" 2>/dev/null || true + echo " Done." + else + echo "No server '$SERVER_NAME' found." + fi + ''; + }; + +in +{ + environment.systemPackages = [ + builder-up + builder-ssh + builder-down + pkgs.hcloud + ]; +} diff --git a/Hyprsuck/home.nix b/Hyprsuck/home.nix new file mode 100644 index 0000000..0148d86 --- /dev/null +++ b/Hyprsuck/home.nix @@ -0,0 +1,41 @@ +# Hyprsuck home-manager configuration +# Unstable extras and autostart +{ pkgs-unstable, ... }: + +{ + imports = [ + ./hyprland/hyprland.nix + ./waybar.nix + ]; + + # Screenshot annotation tool + programs.swappy = { + enable = true; + package = pkgs-unstable.swappy; + settings.Default = { + save_dir = "$HOME/Pictures/Screenshots"; + save_filename_format = "%F_%T.png"; + early_exit = true; + }; + }; + + # Clipboard history manager + services.cliphist = { + enable = true; + package = pkgs-unstable.cliphist; + allowImages = true; + }; + + # Terminal emulator (Wayland-native) + programs.foot = { + enable = true; + package = pkgs-unstable.foot; + }; + + # Auto-start Hyprland on tty1 + programs.bash.profileExtra = '' + if [ -z "$WAYLAND_DISPLAY" ] && [ "$XDG_VTNR" = 1 ]; then + exec Hyprland + fi + ''; +} diff --git a/Hyprsuck/hyprland/bindings.nix b/Hyprsuck/hyprland/bindings.nix new file mode 100644 index 0000000..4a53c93 --- /dev/null +++ b/Hyprsuck/hyprland/bindings.nix @@ -0,0 +1,152 @@ +# Hyprland keybindings configuration +{ ... }: + +{ + wayland.windowManager.hyprland.settings = { + # Variables + "$mod" = "SUPER"; + "$terminal" = "foot"; + "$browser" = "zen"; + + # Keybindings + bind = [ + # Rofi + "$mod, Space, exec, rofi -show system:rofi-system" + "$mod ALT, Space, exec, rofi-websearch" + + # Foot terminal - go to workspace 50, launch if not running + "$mod, A, exec, hyprctl dispatch workspace 50; hyprctl clients -j | grep -q foot || $terminal" + + # Zed - go to workspace 60, launch if not running + "$mod, Z, exec, hyprctl dispatch workspace 60; hyprctl clients -j | grep -q dev.zed.Zed || zeditor" + + # Zen Browser - workspace 70 (DP-3), launch if no browser on that workspace + "$mod, B, exec, hyprctl dispatch workspace 70; hyprctl clients -j | jq -e '.[] | select(.class == \"zen-twilight\" and .workspace.id == 70)' > /dev/null || hyprctl dispatch exec [workspace 70 silent] -- $browser" + # Zen Browser - workspace 71 (eDP-1), launch new window if no browser on that workspace + "$mod ALT, B, exec, hyprctl dispatch workspace 71; hyprctl clients -j | jq -e '.[] | select(.class == \"zen-twilight\" and .workspace.id == 71)' > /dev/null || hyprctl dispatch exec [workspace 71 silent] -- $browser --new-window" + + # Nautilus - go to workspace 80, launch if not running + "$mod, N, exec, hyprctl dispatch workspace 80; hyprctl clients -j | grep -q org.gnome.Nautilus || nautilus" + + # Haruna - go to workspace 90 + "$mod, M, workspace, 90" + + "$mod, Escape, exec, hyprlock" + + # Switch to KiCad workspaces (both monitors) + "$mod, K, workspace, 101" + "$mod, K, workspace, 102" + # Launch KiCad project selector + "$mod CTRL, K, exec, kicad-projects" + # Launch library editors + "$mod, L, exec, kicad-lib-launch" + # Show project manager + "$mod SHIFT, K, togglespecialworkspace, kicad-pm" + # Swap SCH/PCB positions + "$mod ALT, K, exec, kicad-swap" + # Cycle through project instances + "$mod, bracketright, exec, kicad-cycle f" + "$mod, bracketleft, exec, kicad-cycle b" + + # FreeCAD - go to workspace 110, launch if not running + "$mod, F, exec, hyprctl dispatch workspace 110; hyprctl clients -j | grep -q org.freecad.FreeCAD || env QT_QPA_PLATFORM=xcb FreeCAD --single-instance" + + # Window management + "$mod, Q, killactive" + "$mod, W, fullscreen" + "$mod, R, togglefloating" + "$mod, P, pseudo" + "$mod, E, togglesplit" + + # Focus movement + "$mod, left, movefocus, l" + "$mod, right, movefocus, r" + "$mod, up, movefocus, u" + "$mod, down, movefocus, d" + + # Workspace switching + "$mod, 1, workspace, 1" + "$mod, 2, workspace, 2" + "$mod, 3, workspace, 3" + "$mod, 4, workspace, 4" + "$mod, 5, workspace, 5" + "$mod, 6, workspace, 6" + "$mod, 7, workspace, 7" + "$mod, 8, workspace, 8" + "$mod, 9, workspace, 9" + "$mod, 0, workspace, 10" + + # Move window to workspace + "$mod SHIFT, 1, movetoworkspace, 1" + "$mod SHIFT, 2, movetoworkspace, 2" + "$mod SHIFT, 3, movetoworkspace, 3" + "$mod SHIFT, 4, movetoworkspace, 4" + "$mod SHIFT, 5, movetoworkspace, 5" + "$mod SHIFT, 6, movetoworkspace, 6" + "$mod SHIFT, 7, movetoworkspace, 7" + "$mod SHIFT, 8, movetoworkspace, 8" + "$mod SHIFT, 9, movetoworkspace, 9" + "$mod SHIFT, 0, movetoworkspace, 10" + + # Special workspace (scratchpad) + "$mod, S, togglespecialworkspace, magic" + "$mod SHIFT, S, movetoworkspace, special:magic" + + # Exit Hyprland + "$mod SHIFT, E, exit" + + # Scroll through workspaces on current monitor (vertical model) + "$mod, mouse_down, workspace, m-1" + "$mod, mouse_up, workspace, m+1" + + # Keyboard workspace navigation (up/down) + "$mod CTRL, up, workspace, m-1" + "$mod CTRL, down, workspace, m+1" + "$mod CTRL SHIFT, up, movetoworkspace, m-1" + "$mod CTRL SHIFT, down, movetoworkspace, m+1" + + # Clipboard history (SUPER+SHIFT+V) + "$mod SHIFT, V, exec, cliphist list | rofi -dmenu -p 'Clipboard' | cliphist decode | wl-copy" + + # Screenshot bindings (saves to ~/Pictures/Screenshots/) + ", Print, exec, mkdir -p ~/Pictures/Screenshots && grimblast --notify copysave area ~/Pictures/Screenshots/$(date +%F_%T).png" + "SHIFT, Print, exec, mkdir -p ~/Pictures/Screenshots && grimblast --notify copysave output ~/Pictures/Screenshots/$(date +%F_%T).png" + "CTRL, Print, exec, grimblast --notify save area - | swappy -f -" + + # System menu (rofi) + "$mod, X, exec, rofi -show system -modes 'system:rofi-system'" + + # On-screen keyboard toggle (for tablet mode) + # "$mod, K, exec, pkill wvkbd-mobintl || wvkbd-mobintl" + + # Refresh waybar (for dock/undock) + "$mod CTRL, Space, exec, pkill waybar; waybar-start" + ]; + + # Mouse bindings + bindm = [ + "$mod, mouse:272, movewindow" + "$mod, mouse:273, resizewindow" + ]; + + # Repeat bindings (hold key for continuous action) + binde = [ + "$mod ALT, left, resizeactive, -20 0" + "$mod ALT, right, resizeactive, 20 0" + "$mod ALT, up, resizeactive, 0 -20" + "$mod ALT, down, resizeactive, 0 20" + ]; + + # Media/hardware key bindings (bindl = works even when locked) + bindl = [ + ", XF86AudioMute, exec, wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle" + ", XF86AudioMicMute, exec, wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle" + ]; + + # Volume keys with repeat (binde = repeat when held) + bindel = [ + ", XF86AudioRaiseVolume, exec, wpctl set-volume -l 1.0 @DEFAULT_AUDIO_SINK@ 5%+" + ", XF86AudioLowerVolume, exec, wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-" + ]; + }; +} diff --git a/Hyprsuck/hyprland/hypridle.nix b/Hyprsuck/hyprland/hypridle.nix new file mode 100644 index 0000000..6a2cdd5 --- /dev/null +++ b/Hyprsuck/hyprland/hypridle.nix @@ -0,0 +1,41 @@ +# Hypridle - Idle daemon configuration +{ pkgs, ... }: + +{ + services.hypridle = { + enable = true; + package = pkgs.hypridle; + settings = { + general = { + lock_cmd = "pidof hyprlock || hyprlock"; + before_sleep_cmd = "loginctl lock-session"; + after_sleep_cmd = "hyprctl dispatch dpms on"; + }; + + listener = [ + # Dim screen after 2.5 minutes + { + timeout = 150; + on-timeout = "brightnessctl -s set 10"; + on-resume = "brightnessctl -r"; + } + # Lock after 5 minutes + { + timeout = 300; + on-timeout = "loginctl lock-session"; + } + # Screen off after 5.5 minutes + { + timeout = 330; + on-timeout = "hyprctl dispatch dpms off"; + on-resume = "hyprctl dispatch dpms on"; + } + # Suspend after 30 minutes + { + timeout = 1800; + on-timeout = "systemctl suspend"; + } + ]; + }; + }; +} diff --git a/Hyprsuck/hyprland/hyprland.nix b/Hyprsuck/hyprland/hyprland.nix new file mode 100644 index 0000000..d7112b8 --- /dev/null +++ b/Hyprsuck/hyprland/hyprland.nix @@ -0,0 +1,54 @@ +# Hyprland ecosystem configuration (stable packages) +# Core hyprland WM and hypr* tools only +{ pkgs, ... }: + +{ + imports = [ + ./bindings.nix + ./settings.nix + ./monitors.nix + ./rules.nix + ./hyprlock.nix + ./hypridle.nix + ./hyprpaper.nix + ]; + + # ============================================================================ + # Hyprland Window Manager (home-manager config) + # ============================================================================ + # Package/portal set at NixOS level in hyprsuck.nix + wayland.windowManager.hyprland = { + enable = true; + systemd.enable = true; + }; + + # ============================================================================ + # Hyprland ecosystem packages (stable) + # ============================================================================ + home.packages = with pkgs; [ + # Tablet mode + iio-hyprland + + # Monitor hotplug + hyprland-monitor-attached + ]; + + wayland.windowManager.hyprland.settings = { + exec-once = [ + "iio-hyprland" + "hyprland-monitor-attached" + ]; + }; + + # Polkit authentication agent + services.hyprpolkitagent = { + enable = true; + package = pkgs.hyprpolkitagent; + }; + + # Blue light filter + services.hyprsunset = { + enable = true; + package = pkgs.hyprsunset; + }; +} diff --git a/Hyprsuck/hyprland/hyprlock.nix b/Hyprsuck/hyprland/hyprlock.nix new file mode 100644 index 0000000..c749fec --- /dev/null +++ b/Hyprsuck/hyprland/hyprlock.nix @@ -0,0 +1,39 @@ +# Hyprlock - Screen locker configuration +{ pkgs, ... }: + +{ + # Disable Stylix's hyprlock theming (we use screenshot blur) + stylix.targets.hyprlock.enable = false; + + programs.hyprlock = { + enable = true; + package = pkgs.hyprlock; + settings = { + general = { + hide_cursor = true; + grace = 5; # seconds before lock takes effect + }; + + background = [ + { + path = "screenshot"; + blur_passes = 3; + blur_size = 8; + } + ]; + + input-field = [ + { + size = "200, 50"; + position = "0, -80"; + monitor = ""; + dots_center = true; + fade_on_empty = false; + outline_thickness = 2; + placeholder_text = "Password..."; + shadow_passes = 2; + } + ]; + }; + }; +} diff --git a/Hyprsuck/hyprland/hyprpaper.nix b/Hyprsuck/hyprland/hyprpaper.nix new file mode 100644 index 0000000..01b38cf --- /dev/null +++ b/Hyprsuck/hyprland/hyprpaper.nix @@ -0,0 +1,22 @@ +# Hyprpaper - Wallpaper manager configuration +{ config, pkgs, ... }: + +let + flakeDir = "${config.home.homeDirectory}/NixOS"; +in + +{ + services.hyprpaper = { + enable = true; + package = pkgs.hyprpaper; + settings = { + ipc = "on"; + splash = false; + + # Change the wallpaper filename here + preload = [ "${flakeDir}/Wallpapers/Shift.png" ]; + wallpaper = [ ", ${flakeDir}/Wallpapers/Shift.png" ]; + }; + }; + +} diff --git a/Hyprsuck/hyprland/monitors.nix b/Hyprsuck/hyprland/monitors.nix new file mode 100644 index 0000000..04b96b0 --- /dev/null +++ b/Hyprsuck/hyprland/monitors.nix @@ -0,0 +1,15 @@ +# Hyprland monitor configuration +{ ... }: + +{ + wayland.windowManager.hyprland.settings = { + # Monitor configuration + # eDP-1: Laptop display in portrait mode (rotated 270deg) + # DP-3: External monitor in landscape, positioned to the right + monitor = [ + "eDP-1, 1920x1200@60, 0x0, 1.25, transform, 3" + "DP-3, 1920x1080@100, 960x-250, 1" + ", preferred, auto, 1" # Fallback for any other monitor + ]; + }; +} diff --git a/Hyprsuck/hyprland/rules.nix b/Hyprsuck/hyprland/rules.nix new file mode 100644 index 0000000..9ecb8e5 --- /dev/null +++ b/Hyprsuck/hyprland/rules.nix @@ -0,0 +1,59 @@ +# Hyprland window rules and workspace configuration +{ ... }: + +{ + wayland.windowManager.hyprland.settings = { + # Window rules (old syntax for Hyprland 0.52.1) + windowrule = [ + "float, class:^(pavucontrol)$" + "float, class:^(org.keepassxc.KeePassXC)$" + "float, title:^(Picture-in-Picture)$" + "pin, title:^(Picture-in-Picture)$" + + # Foot terminal → workspace 50 (DP-3) + "workspace 50 silent, class:^(foot)$" + + # Zed → workspace 60 (DP-3) + "workspace 60 silent, class:^(dev.zed.Zed)$" + + # FreeCAD → workspace 110 (DP-3) + "workspace 110 silent, class:^(org.freecad.FreeCAD)$" + + # Nautilus → workspace 80 (eDP-1) + "workspace 80 silent, class:^(org.gnome.Nautilus)$" + + # Haruna → workspace 104, tabbed/grouped + "workspace 90 silent, class:^(org.kde.haruna)$" + "group set, class:^(org.kde.haruna)$" + + # Schematic/Symbol Editor → workspace 102 (eDP-1), maximized, grouped + "workspace 102 silent, class:^(KiCad|kicad)$, title:.*Schematic Editor.*" + "maximize, class:^(KiCad|kicad)$, title:.*Schematic Editor.*" + "group set, class:^(KiCad|kicad)$, title:.*Schematic Editor.*" + "workspace 102 silent, class:^(KiCad|kicad)$, title:.*Symbol Editor.*" + "maximize, class:^(KiCad|kicad)$, title:.*Symbol Editor.*" + "group set, class:^(KiCad|kicad)$, title:.*Symbol Editor.*" + + # PCB/Footprint Editor → workspace 101 (DP-3), maximized, grouped + "workspace 101 silent, class:^(KiCad|kicad)$, title:.*PCB Editor.*" + "maximize, class:^(KiCad|kicad)$, title:.*PCB Editor.*" + "group set, class:^(KiCad|kicad)$, title:.*PCB Editor.*" + "workspace 101 silent, class:^(KiCad|kicad)$, title:.*Footprint Editor.*" + "maximize, class:^(KiCad|kicad)$, title:.*Footprint Editor.*" + "group set, class:^(KiCad|kicad)$, title:.*Footprint Editor.*" + ]; + + # Workspace rules + workspace = [ + "50, monitor:eDP-1, defaultName:terminal" + "60, monitor:DP-3, defaultName:code" + "70, monitor:DP-3, defaultName:browser" + "71, monitor:eDP-1, defaultName:browser" + "80, monitor:eDP-1, defaultName:files" + "90, monitor:DP-3, defaultName:video" + "101, monitor:DP-3, defaultName:kicad_prim" + "102, monitor:eDP-1, defaultName:kicad_sec" + "110, monitor:DP-3, defaultName:freecad" + ]; + }; +} diff --git a/Hyprsuck/hyprland/settings.nix b/Hyprsuck/hyprland/settings.nix new file mode 100644 index 0000000..00bdabd --- /dev/null +++ b/Hyprsuck/hyprland/settings.nix @@ -0,0 +1,81 @@ +# Hyprland general settings +# Input, appearance, layout, gestures, misc +{ ... }: + +{ + wayland.windowManager.hyprland.settings = { + # Input configuration + input = { + kb_layout = "us"; + numlock_by_default = true; + + follow_mouse = 1; + sensitivity = 0; + + touchpad = { + natural_scroll = true; + }; + }; + + # General settings + general = { + gaps_in = 5; + gaps_out = 5; + border_size = 2; + layout = "dwindle"; + }; + + # Decoration + decoration = { + rounding = 10; + # blur = { + # enabled = true; + # size = 3; + # passes = 1; + # }; + # shadow = { + # enabled = true; + # range = 4; + # render_power = 3; + # }; + }; + + # Animations + animations = { + enabled = true; + # bezier = "myBezier, 0.05, 0.9, 0.1, 1.05"; + animation = [ + # "windows, 1, 7, myBezier" + # "windowsOut, 1, 7, default, popin 80%" + # "border, 1, 10, default" + # "fade, 1, 7, default" + "workspaces, 1, 4, default, slidevert" + ]; + }; + + # Layout settings + dwindle = { + pseudotile = true; + preserve_split = true; + }; + + # Gestures (Hyprland 0.51+ new syntax) + gestures = { + workspace_swipe_distance = 500; + workspace_swipe_invert = false; + workspace_swipe_create_new = false; + }; + + # New gesture bindings (replaces workspace_swipe) + # Vertical 3-finger swipe for workspace switching + gesture = [ + "3, vertical, workspace" + ]; + + # Misc + misc = { + force_default_wallpaper = 0; + disable_hyprland_logo = true; + }; + }; +} diff --git a/Hyprsuck/hyprsuck.nix b/Hyprsuck/hyprsuck.nix new file mode 100644 index 0000000..2cc3c69 --- /dev/null +++ b/Hyprsuck/hyprsuck.nix @@ -0,0 +1,23 @@ +# Hyprsuck configuration entry point +# Wayland compositor setup with Hyprland +{ pkgs, ... }: + +{ + # ============================================================================ + # Hyprland - NixOS level config + # ============================================================================ + programs.hyprland = { + enable = true; + package = pkgs.hyprland; + portalPackage = pkgs.xdg-desktop-portal-hyprland; + xwayland.enable = true; + }; + + environment.systemPackages = with pkgs; [ + # Screenshot tool for Hyprland + grimblast + + # Tablet mode + wvkbd + ]; +} diff --git a/Hyprsuck/waybar.nix b/Hyprsuck/waybar.nix new file mode 100644 index 0000000..86a1655 --- /dev/null +++ b/Hyprsuck/waybar.nix @@ -0,0 +1,219 @@ +# Waybar - Dynamic config based on connected monitors +# Docked (DP-3): external gets workspaces+clock, internal gets workspaces +# Undocked: internal gets workspaces+clock +{ pkgs-unstable, pkgs, ... }: + +let + workspaceIcons = { + "terminal" = ""; + "code" = ""; + "browser" = "󰖟"; + "files" = ""; + "video" = ""; + "kicad_prim" = ""; + "kicad_sec" = ""; + "freecad" = "󰻬"; + }; + + sharedStyle = '' + * { + font-family: "JetBrainsMono Nerd Font Mono", monospace; + } + + window#waybar { + background: transparent; + } + + .modules-left, + .modules-center, + .modules-right { + background: alpha(@theme_bg_color, 0.8); + border-radius: 8px; + padding: 2px; + margin: 0px; + } + + window#waybar.right .modules-center { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + margin-right: 0; + } + + window#waybar.left .modules-center { + border-top-left-radius: 0; + border-bottom-left-radius: 0; + margin-left: 0; + } + + #workspaces { + padding: 0px; + } + + .modules-center #workspaces button, + .modules-left #workspaces button, + .modules-right #workspaces button { + padding: 1px 2px; + min-width: 14px; + min-height: 12px; + background: transparent; + border: none; + border-bottom: none; + box-shadow: none; + text-shadow: none; + border-radius: 0; + } + + .modules-center #workspaces button label, + .modules-left #workspaces button label, + .modules-right #workspaces button label { + font-size: 13px; + } + + .modules-center #workspaces button.active, + .modules-left #workspaces button.active, + .modules-right #workspaces button.active { + color: @base0D; + border-bottom: none; + box-shadow: none; + } + + .modules-center #workspaces button.active label, + .modules-left #workspaces button.active label, + .modules-right #workspaces button.active label { + font-size: 15px; + } + + #clock { + padding: 0px; + font-weight: bold; + } + ''; + + # JSON config for docked mode (DP-3 primary) + dockedConfig = builtins.toJSON [ + # DP-3: workspaces on right + { + name = "dp3-workspaces"; + output = "DP-3"; + layer = "top"; + exclusive = false; + passthrough = true; + position = "right"; + margin-right = 0; + modules-center = [ "hyprland/workspaces" ]; + "hyprland/workspaces" = { + format = "{icon}"; + format-icons = workspaceIcons; + show-special = false; + persistent-workspaces = { }; + }; + } + + # DP-3: clock on left + { + name = "dp3-clock"; + output = "DP-3"; + layer = "top"; + exclusive = false; + passthrough = true; + position = "left"; + margin-left = 0; + modules-center = [ "clock" ]; + clock = { + format = "{:%H\n%M}"; + tooltip-format = "{:%A, %B %d, %Y}"; + }; + } + + # eDP-1: workspaces on left (secondary) + { + name = "edp1-workspaces"; + output = "eDP-1"; + layer = "top"; + exclusive = false; + passthrough = true; + position = "left"; + margin-left = 0; + modules-center = [ "hyprland/workspaces" ]; + "hyprland/workspaces" = { + format = "{icon}"; + format-icons = workspaceIcons; + show-special = false; + persistent-workspaces = { }; + }; + } + ]; + + # JSON config for undocked mode (eDP-1 only) + undockedConfig = builtins.toJSON [ + # eDP-1: workspaces on right + { + name = "edp1-workspaces"; + output = "eDP-1"; + layer = "top"; + exclusive = false; + passthrough = true; + position = "right"; + margin-right = 0; + modules-center = [ "hyprland/workspaces" ]; + "hyprland/workspaces" = { + format = "{icon}"; + format-icons = workspaceIcons; + show-special = false; + persistent-workspaces = { }; + }; + } + + # eDP-1: clock on left + { + name = "edp1-clock"; + output = "eDP-1"; + layer = "top"; + exclusive = false; + passthrough = true; + position = "left"; + margin-left = 0; + modules-center = [ "clock" ]; + clock = { + format = "{:%H\n%M}"; + tooltip-format = "{:%A, %B %d, %Y}"; + }; + } + ]; + + # Simple startup script - check for DP-3 and load appropriate config + startupScript = '' + sleep 1 + CONFIG_DIR="$HOME/.config/waybar" + + if hyprctl monitors -j | grep -q '"name": "DP-3"'; then + waybar -c "$CONFIG_DIR/config-docked.jsonc" & + else + waybar -c "$CONFIG_DIR/config-undocked.jsonc" & + fi + ''; + +in +{ + # Waybar - disable systemd, we manage startup ourselves + programs.waybar = { + enable = true; + package = pkgs-unstable.waybar; + systemd.enable = false; + style = sharedStyle; + }; + + # Write config files (style via programs.waybar.style to avoid conflict) + home.file.".config/waybar/config-docked.jsonc".text = dockedConfig; + home.file.".config/waybar/config-undocked.jsonc".text = undockedConfig; + + # Waybar startup script + home.packages = [ + (pkgs.writeShellScriptBin "waybar-start" startupScript) + ]; + + # Start waybar + wayland.windowManager.hyprland.settings.exec-once = [ + "waybar-start" + ]; +} diff --git a/KiCad/Scripts/kicad-cycle.sh b/KiCad/Scripts/kicad-cycle.sh new file mode 100644 index 0000000..d2ab94d --- /dev/null +++ b/KiCad/Scripts/kicad-cycle.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# Cycle through KiCad windows/projects +# Usage: kicad-cycle [f|b] (forward/backward, default: f) +# Supports both Hyprland (Wayland) and dwm (X11) + +DIRECTION="${1:-f}" + +# ============================================================================== +# Environment Detection +# ============================================================================== +if [[ -n "${HYPRLAND_INSTANCE_SIGNATURE:-}" ]]; then + WM="hyprland" +else + WM="dwm" +fi + +# ============================================================================== +# Main +# ============================================================================== +if [[ "$WM" == "hyprland" ]]; then + # Hyprland: Cycle through groups on both workspaces + hyprctl dispatch workspace 101 + hyprctl dispatch changegroupactive "$DIRECTION" + hyprctl dispatch workspace 102 + hyprctl dispatch changegroupactive "$DIRECTION" +else + # Ensure we're on kicad tag on both monitors first + kicad-show + + # Determine focus direction + FOCUS_CMD="focus-next" + [[ "$DIRECTION" == "b" ]] && FOCUS_CMD="focus-prev" + + if [[ $(autorandr --detected) != "mobile" ]]; then + # Multi-monitor: cycle on both monitors + echo "mon-prim" > /tmp/dwm.fifo + echo "$FOCUS_CMD" > /tmp/dwm.fifo + echo "mon-sec" > /tmp/dwm.fifo + echo "$FOCUS_CMD" > /tmp/dwm.fifo + else + # Single monitor: just cycle windows + echo "$FOCUS_CMD" > /tmp/dwm.fifo + fi +fi diff --git a/KiCad/Scripts/kicad-launch.sh b/KiCad/Scripts/kicad-launch.sh new file mode 100644 index 0000000..b11b448 --- /dev/null +++ b/KiCad/Scripts/kicad-launch.sh @@ -0,0 +1,103 @@ +#!/usr/bin/env bash +# Launch KiCad with managed window layout +# Usage: kicad-launch +# Supports both Hyprland (Wayland) and dwm (X11) + +set -euo pipefail + +# ============================================================================== +# Environment Detection +# ============================================================================== +if [[ -n "${HYPRLAND_INSTANCE_SIGNATURE:-}" ]]; then + WM="hyprland" +else + WM="dwm" +fi + +# ============================================================================== +# Main +# ============================================================================== +PROJECT="${1:-}" +if [[ -z "$PROJECT" ]]; then + echo "Usage: kicad-launch " + exit 1 +fi + +PROJECT_NAME=$(basename "$PROJECT" .kicad_pro) + +if [[ "$WM" == "hyprland" ]]; then + # Hyprland: Use special workspace for PM + hyprctl dispatch workspace 102 + hyprctl dispatch togglespecialworkspace kicad-pm + hyprctl dispatch exec "[workspace special:kicad-pm silent]" -- env GDK_BACKEND=x11 kicad "$PROJECT" + + # Wait for project manager window + PM_ADDR="" + for i in {1..50}; do + PM_ADDR=$(hyprctl clients -j | jq -r --arg name "$PROJECT_NAME" \ + '.[] | select(.class == "KiCad" and (.title | test($name))) | .address' 2>/dev/null | head -1) + [[ -n "$PM_ADDR" ]] && break + sleep 0.1 + done + + if [[ -z "$PM_ADDR" ]]; then + hyprctl dispatch togglespecialworkspace kicad-pm + echo "Failed to find KiCad project manager window" + exit 1 + fi + + # Focus PM and open PCB editor + hyprctl dispatch focuswindow "address:$PM_ADDR" + sleep 0.25 + xdotool key ctrl+p + sleep 0.25 + + # Hide special workspace + hyprctl dispatch togglespecialworkspace kicad-pm + + # Focus PCB editor and open schematic + sleep 2 + hyprctl dispatch workspace 101 + sleep 0.25 + xdotool key ctrl+e + sleep 0.25 + + # Switch to KiCad workspaces + hyprctl dispatch workspace 101 + hyprctl dispatch workspace 102 + +else + # dwm: Launch KiCad, window rules handle placement + # Tag 16 = kicad-pm, Tag 17 = kicad (sch/pcb), Tag 18 = kicad-aux (mobile) + + # Detect profile + PROFILE=$(autorandr --detected 2>/dev/null || echo "mobile") + + # Switch to PM tag and launch + [[ "$PROFILE" != "mobile" ]] && echo "mon-sec" > /tmp/dwm.fifo + echo "kicad-pm" > /tmp/dwm.fifo + kicad "$PROJECT" & + + # Wait for PM, open PCB editor + sleep 1 + sleep 0.25 + xdotool key ctrl+p + + # Wait for PCB Editor + sleep 2 + [[ "$PROFILE" != "mobile" ]] && echo "mon-prim" > /tmp/dwm.fifo + echo "kicad" > /tmp/dwm.fifo + + if [[ "$PROFILE" == "mobile" ]]; then + # Mobile: retag PCB to tag 18 (kicad-aux) + # PCB landed on tag 17, focus it, retag to 18, follow + xdotool search --name "PCB Editor" windowactivate --sync + sleep 0.25 + echo "tag 18" > /tmp/dwm.fifo + echo "kicad-aux" > /tmp/dwm.fifo + fi + + # Focus PCB Editor, open Schematic + sleep 0.25 + xdotool key ctrl+e +fi diff --git a/KiCad/Scripts/kicad-lib-launch.sh b/KiCad/Scripts/kicad-lib-launch.sh new file mode 100644 index 0000000..5eded78 --- /dev/null +++ b/KiCad/Scripts/kicad-lib-launch.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +# Open Symbol + Footprint editors from running schematic/PCB editors +# Supports both Hyprland (Wayland) and dwm (X11) + +set -euo pipefail + +# ============================================================================== +# Environment Detection +# ============================================================================== +if [[ -n "${HYPRLAND_INSTANCE_SIGNATURE:-}" ]]; then + WM="hyprland" +else + WM="dwm" + PROFILE=$(autorandr --detected 2>/dev/null || echo "mobile") +fi + +# ============================================================================== +# Main +# ============================================================================== +if [[ "$WM" == "hyprland" ]]; then + # Hyprland: Focus workspaces and send keys + hyprctl dispatch workspace 101 + sleep 0.25 + xdotool key ctrl+u # Open Footprint Editor from PCB + + sleep 2 + + hyprctl dispatch workspace 102 + sleep 0.25 + xdotool key ctrl+i # Open Symbol Editor from Schematic +else + # Focus PCB Editor, open Footprint Editor + [[ "$PROFILE" != "mobile" ]] && echo "mon-prim" > /tmp/dwm.fifo + echo "kicad" > /tmp/dwm.fifo + sleep 0.25 + xdotool key ctrl+u + + sleep 0.5 + + # Focus right monitor, Schematic Editor, open Symbol Editor + [[ "$PROFILE" != "mobile" ]] && echo "mon-sec" > /tmp/dwm.fifo + echo "kicad" > /tmp/dwm.fifo + sleep 0.25 + xdotool key ctrl+i +fi diff --git a/KiCad/Scripts/kicad-projects.sh b/KiCad/Scripts/kicad-projects.sh new file mode 100644 index 0000000..c48c92e --- /dev/null +++ b/KiCad/Scripts/kicad-projects.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +# Rofi KiCad project launcher +# Two-step: select folder, then select project +# Works with both Hyprland and dwm + +# Define project folders: "Display Name|/path/to/folder" +PROJECT_FOLDERS=( + "CNC|/data/3D-Printer/Electronics/PCBs" + "Keyboard|/data/Keyboard/PCB" + "eGPU|/data/eGPU" +) + +# Step 1: Select project folder +FOLDER_MENU="" +for entry in "${PROJECT_FOLDERS[@]}"; do + FOLDER_MENU+="${entry%%|*}\n" +done + +SELECTED_FOLDER=$(echo -e "$FOLDER_MENU" | rofi -dmenu -p "KiCad Folder" -i) +[ -z "$SELECTED_FOLDER" ] && exit 0 + +# Get the path for selected folder +FOLDER_PATH="" +for entry in "${PROJECT_FOLDERS[@]}"; do + name="${entry%%|*}" + path="${entry#*|}" + if [ "$name" = "$SELECTED_FOLDER" ]; then + FOLDER_PATH="$path" + break + fi +done +[ -z "$FOLDER_PATH" ] && exit 1 + +# Step 2: Find .kicad_pro files in selected folder +# Sort by path length (shallowest first) to handle submodule duplicates +mapfile -t PROJECTS < <(find "$FOLDER_PATH" -name "*.kicad_pro" -type f 2>/dev/null | awk '{print length, $0}' | sort -n | cut -d' ' -f2-) + +if [ ${#PROJECTS[@]} -eq 0 ]; then + rofi -e "No KiCad projects found in $FOLDER_PATH" + exit 1 +fi + +# Build menu with duplicate filtering (keep shallowest path for each project name) +declare -A SEEN_PROJECTS +MENU="" +for project in "${PROJECTS[@]}"; do + name=$(basename "$project" .kicad_pro) + # Skip if we've already seen this project name (deeper path = submodule) + if [ -z "${SEEN_PROJECTS[$name]}" ]; then + SEEN_PROJECTS[$name]="$project" + MENU+="$name\n" + fi +done + +# Show rofi menu +SELECTED=$(echo -e "$MENU" | rofi -dmenu -p "Project" -i) +[ -z "$SELECTED" ] && exit 0 + +# Launch selected project +PROJECT_PATH="${SEEN_PROJECTS[$SELECTED]}" +if [ -n "$PROJECT_PATH" ]; then + kicad-launch "$PROJECT_PATH" +fi diff --git a/KiCad/Scripts/kicad-show.sh b/KiCad/Scripts/kicad-show.sh new file mode 100644 index 0000000..552955e --- /dev/null +++ b/KiCad/Scripts/kicad-show.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# Show KiCad tag on both monitors +# Supports both Hyprland (Wayland) and dwm (X11) + +# ============================================================================== +# Environment Detection +# ============================================================================== +if [[ -n "${HYPRLAND_INSTANCE_SIGNATURE:-}" ]]; then + WM="hyprland" +else + WM="dwm" +fi + +# ============================================================================== +# Main +# ============================================================================== +if [[ "$WM" == "hyprland" ]]; then + hyprctl dispatch workspace 101 + hyprctl dispatch workspace 102 +else + if [[ $(autorandr --detected) != "mobile" ]]; then + # Multi-monitor: set kicad tag on both monitors + echo "mon-prim" > /tmp/dwm.fifo + echo "kicad" > /tmp/dwm.fifo + echo "mon-sec" > /tmp/dwm.fifo + echo "kicad" > /tmp/dwm.fifo + else + # Single monitor: just switch to kicad tag + echo "kicad" > /tmp/dwm.fifo + fi +fi diff --git a/KiCad/Scripts/kicad-swap.sh b/KiCad/Scripts/kicad-swap.sh new file mode 100644 index 0000000..dcd5fc2 --- /dev/null +++ b/KiCad/Scripts/kicad-swap.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +# Swap KiCad workspaces between monitors +# Supports both Hyprland (Wayland) and dwm (X11) + +# ============================================================================== +# Environment Detection +# ============================================================================== +if [[ -n "${HYPRLAND_INSTANCE_SIGNATURE:-}" ]]; then + WM="hyprland" +else + WM="dwm" +fi + +# ============================================================================== +# Main +# ============================================================================== +if [[ "$WM" == "hyprland" ]]; then + hyprctl dispatch swapactiveworkspaces DP-3 eDP-1 +else + # Ensure we're on kicad tag on both monitors first + kicad-show + echo "mon-swap" > /tmp/dwm.fifo +fi diff --git a/KiCad/kicad.nix b/KiCad/kicad.nix new file mode 100644 index 0000000..19240c7 --- /dev/null +++ b/KiCad/kicad.nix @@ -0,0 +1,20 @@ +# KiCad configuration and scripts +# Works with both Hyprland (Wayland) and dwm (X11) +{ pkgs-stable, ... }: + +let + scriptsDir = ./Scripts; +in +{ + home.packages = with pkgs-stable; [ + kicad + + # Custom scripts + (writeShellScriptBin "kicad-launch" (builtins.readFile "${scriptsDir}/kicad-launch.sh")) + (writeShellScriptBin "kicad-projects" (builtins.readFile "${scriptsDir}/kicad-projects.sh")) + (writeShellScriptBin "kicad-show" (builtins.readFile "${scriptsDir}/kicad-show.sh")) + (writeShellScriptBin "kicad-swap" (builtins.readFile "${scriptsDir}/kicad-swap.sh")) + (writeShellScriptBin "kicad-cycle" (builtins.readFile "${scriptsDir}/kicad-cycle.sh")) + (writeShellScriptBin "kicad-lib-launch" (builtins.readFile "${scriptsDir}/kicad-lib-launch.sh")) + ]; +} diff --git a/Rofi/Scripts/favorites.sh b/Rofi/Scripts/favorites.sh new file mode 100644 index 0000000..22b8d2f --- /dev/null +++ b/Rofi/Scripts/favorites.sh @@ -0,0 +1,86 @@ +#!/usr/bin/env bash +# Rofi Favorites - Reads Zen/Firefox bookmarks directly from SQLite +# Supports folder navigation with ROFI_DATA state tracking +# Works with both Hyprland and dwm (no WM-specific code) + +PLACES_DB="$HOME/.zen/default/places.sqlite" +TEMP_DB="/tmp/rofi-bookmarks.sqlite" +STATE="${ROFI_DATA:-3}" # 3 = Bookmarks Toolbar folder ID +SELECTION="$1" + +# Copy database to avoid lock issues (browser keeps it locked) +cp "$PLACES_DB" "$TEMP_DB" 2>/dev/null + +show_folder() { + local folder_id="$1" + echo -en "\0data\x1f${folder_id}\n" + echo -en "\0keep-selection\x1ftrue\n" + + # Show back option if not at root + [[ "$folder_id" != "3" ]] && echo "󰁍 .." + + # Query bookmarks in this folder + # type 1 = bookmark, type 2 = folder + sqlite3 -separator '|' "$TEMP_DB" " + SELECT + b.id, + b.type, + COALESCE(b.title, ''), + COALESCE(p.url, '') + FROM moz_bookmarks b + LEFT JOIN moz_places p ON b.fk = p.id + WHERE b.parent = $folder_id + AND b.title IS NOT NULL + AND b.title != '' + ORDER BY b.position; + " | while IFS='|' read -r id type title url; do + if [[ "$type" == "2" ]]; then + # Folder + echo -en "󰉋 ${title}\0info\x1ffolder:${id}\n" + elif [[ "$type" == "1" && -n "$url" ]]; then + # Bookmark + echo -en "󰈙 ${title}\0info\x1f${url}\n" + fi + done +} + +handle_selection() { + local info="$ROFI_INFO" + + # Back/parent + if [[ "$SELECTION" == "󰁍 .." ]]; then + parent=$(sqlite3 "$TEMP_DB" "SELECT parent FROM moz_bookmarks WHERE id = $STATE;") + [[ -z "$parent" || "$parent" == "0" || "$parent" == "1" ]] && parent=3 + show_folder "$parent" + return + fi + + # Folder - navigate into it + if [[ "$info" == folder:* ]]; then + local folder_id="${info#folder:}" + show_folder "$folder_id" + return + fi + + # URL - open it + if [[ -n "$info" && "$info" != folder:* ]]; then + coproc (xdg-open "$info" &) + exit 0 + fi + + show_folder "$STATE" +} + +# Check if database exists +if [[ ! -f "$PLACES_DB" ]]; then + echo "󰀨 No Zen browser profile found" + echo "󰈙 Expected: ~/.zen/default/places.sqlite" + exit 0 +fi + +# Main entry +if [[ -z "$SELECTION" ]]; then + show_folder "$STATE" +else + handle_selection +fi diff --git a/Rofi/Scripts/max30.sh b/Rofi/Scripts/max30.sh new file mode 100644 index 0000000..c6c12e8 --- /dev/null +++ b/Rofi/Scripts/max30.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# Rofi Max30 mode - list and open video files with custom names +# Supports both Hyprland (Wayland) and dwm (X11) + +# ============================================================================== +# Environment Detection +# ============================================================================== +if [[ -n "${HYPRLAND_INSTANCE_SIGNATURE:-}" ]]; then + WM="hyprland" +else + WM="dwm" +fi + +VIDEOS=( + "Max Out Cardio|/data-bis/Insanity MAX 30/Max Out Cardio.mkv" + "Max Out Power|/data-bis/Insanity MAX 30/Max Out Power.mkv" + "Max Out Sweat|/data-bis/Insanity MAX 30/Max Out Sweat.mkv" + "Max Out Strength|/data-bis/Insanity MAX 30/Max Out Strength.mkv" + "Friday Fight #2|/data-bis/Insanity MAX 30/Friday Fight Round 2.mkv" +) + +# If no argument, list video names +if [ -z "$1" ]; then + for entry in "${VIDEOS[@]}"; do + echo "${entry%%|*}" + done + exit 0 +fi + +# If argument provided, find and open the matching video +SELECTED="$1" +for entry in "${VIDEOS[@]}"; do + name="${entry%%|*}" + path="${entry#*|}" + if [ "$name" = "$SELECTED" ]; then + setsid xdg-open "$path" >/dev/null 2>&1 & + if [[ "$WM" == "hyprland" ]]; then + hyprctl dispatch workspace 90 >/dev/null 2>&1 + else + # Focus primary monitor first if not on mobile (single monitor) profile + [[ $(autorandr --detected) != "mobile" ]] && echo "mon-prim" > /tmp/dwm.fifo + echo "video" > /tmp/dwm.fifo + fi + exit 0 + fi +done diff --git a/Rofi/Scripts/system.sh b/Rofi/Scripts/system.sh new file mode 100644 index 0000000..c8fa3ef --- /dev/null +++ b/Rofi/Scripts/system.sh @@ -0,0 +1,317 @@ +#!/usr/bin/env bash +# Rofi System Menu - as rofi mode with inline submenus +# Uses ROFI_DATA for state tracking between calls +# Supports both Hyprland (Wayland) and dwm (X11) + +STATE="${ROFI_DATA:-main}" +SELECTION="$1" + +# ============================================================================== +# Environment Detection +# ============================================================================== +if [[ -n "${HYPRLAND_INSTANCE_SIGNATURE:-}" ]]; then + WM="hyprland" +else + WM="dwm" +fi + +# ============================================================================== +# Helper Functions +# ============================================================================== + +get_volume() { wpctl get-volume @DEFAULT_AUDIO_SINK@ 2>/dev/null | awk '{print int($2*100)}'; } +get_mic_volume() { wpctl get-volume @DEFAULT_AUDIO_SOURCE@ 2>/dev/null | awk '{print int($2*100)}'; } +is_muted() { wpctl get-volume @DEFAULT_AUDIO_SINK@ 2>/dev/null | grep -q MUTED && echo "yes" || echo "no"; } +is_mic_muted() { wpctl get-volume @DEFAULT_AUDIO_SOURCE@ 2>/dev/null | grep -q MUTED && echo "yes" || echo "no"; } +get_brightness_internal() { brightnessctl -m 2>/dev/null | cut -d',' -f4 | tr -d '%'; } +get_brightness_external() { + if [[ "$WM" == "hyprland" ]]; then + local val=$(busctl --user get-property rs.wl-gammarelay /outputs/DP_3 rs.wl.gammarelay Brightness 2>/dev/null | awk '{print $2}') + echo "${val:-1}" | awk '{print int($1*100)}' + else + # X11: Use xrandr gamma (approximate) + local gamma=$(xrandr --verbose | grep -A5 "DP-3" | grep "Brightness" | awk '{print $2}' 2>/dev/null) + echo "${gamma:-1}" | awk '{print int($1*100)}' + fi +} +get_power_profile() { powerprofilesctl get 2>/dev/null || echo "balanced"; } + +get_default_sink_id() { + wpctl inspect @DEFAULT_AUDIO_SINK@ 2>/dev/null | head -1 | awk '{print $2}' | tr -d ',' +} + +get_default_source_id() { + wpctl inspect @DEFAULT_AUDIO_SOURCE@ 2>/dev/null | head -1 | awk '{print $2}' | tr -d ',' +} + +get_sinks() { + local default_id=$(get_default_sink_id) + pw-cli list-objects Node 2>/dev/null | awk ' + /^[[:space:]]*id [0-9]+/ { id = $2; gsub(",", "", id) } + /node.description = / { gsub(/.*node.description = "|"$/, ""); desc = $0 } + /media.class = "Audio\/Sink"/ { print id "|" desc } + ' | while IFS='|' read -r id desc; do + if [[ "$id" == "$default_id" ]]; then + echo "✓ ${desc}|${id}" + else + echo "${desc}|${id}" + fi + done +} + +get_sources() { + local default_id=$(get_default_source_id) + pw-cli list-objects Node 2>/dev/null | awk ' + /^[[:space:]]*id [0-9]+/ { id = $2; gsub(",", "", id) } + /node.description = / { gsub(/.*node.description = "|"$/, ""); desc = $0 } + /media.class = "Audio\/Source"/ { print id "|" desc } + ' | while IFS='|' read -r id desc; do + if [[ "$id" == "$default_id" ]]; then + echo "✓ ${desc}|${id}" + else + echo "${desc}|${id}" + fi + done +} + +# ============================================================================== +# Menu Display Functions +# ============================================================================== + +show_main() { + echo -en "\0data\x1fmain\n" + echo -en "\0keep-selection\x1ftrue\n" + echo "󰕾 Sound" + echo "󰃟 Brightness" + echo "󰤨 WiFi" + echo "󰂯 Bluetooth" + echo "󱐋 Power Profile" + echo "󰐥 Power" +} + +show_sound() { + echo -en "\0data\x1fsound\n" + echo -en "\0keep-selection\x1ftrue\n" + local vol=$(get_volume) + local mic=$(get_mic_volume) + local muted=$(is_muted) + local mic_muted=$(is_mic_muted) + local vol_icon="󰕾"; [[ "$muted" == "yes" ]] && vol_icon="󰝟" + local mic_icon="󰍬"; [[ "$mic_muted" == "yes" ]] && mic_icon="󰍭" + + echo "$vol_icon Volume: ${vol}%" + echo "󰝝 Volume +5%" + echo "󰝞 Volume -5%" + echo "󰓃 Output Device" + echo "$mic_icon Mic: ${mic}%" + echo "󰍮 Mic +5%" + echo "󰍯 Mic -5%" + echo "󰍬 Input Device" + echo "󰁍 Back" +} + +show_output() { + echo -en "\0data\x1foutput\n" + echo -en "\0keep-selection\x1ftrue\n" + get_sinks | while IFS='|' read -r name id; do + echo -en "󰓃 ${name}\0info\x1f${id}\n" + done + echo "󰁍 Back" +} + +show_input() { + echo -en "\0data\x1finput\n" + echo -en "\0keep-selection\x1ftrue\n" + get_sources | while IFS='|' read -r name id; do + echo -en "󰍬 ${name}\0info\x1f${id}\n" + done + echo "󰁍 Back" +} + +show_brightness() { + echo -en "\0data\x1fbrightness\n" + echo -en "\0keep-selection\x1ftrue\n" + local internal=$(get_brightness_internal) + local external=$(get_brightness_external) + + echo "󰛩 Internal: ${internal}%" + echo "󰹐 Internal +5%" + echo "󰹏 Internal -5%" + echo "󰍹 External: ${external}%" + echo "󰹐 External +5%" + echo "󰹏 External -5%" + echo "󰁍 Back" +} + +show_power_profile() { + echo -en "\0data\x1fprofile\n" + echo -en "\0keep-selection\x1ftrue\n" + local current=$(get_power_profile) + local perf="" bal="" saver="" + [[ "$current" == "performance" ]] && perf="✓ " + [[ "$current" == "balanced" ]] && bal="✓ " + [[ "$current" == "power-saver" ]] && saver="✓ " + + echo "${perf}󱐌 Performance" + echo "${bal}󰗑 Balanced" + echo "${saver}󰌪 Power Saver" + echo "󰢻 Edit Tuning" + echo "󰁍 Back" +} + +show_power() { + echo -en "\0data\x1fpower\n" + echo -en "\0keep-selection\x1ftrue\n" + echo "󰐥 Shutdown" + echo "󰜉 Reboot" + echo "󰍃 Logout" + echo "󰌾 Lock" + echo "󰁍 Back" +} + +# ============================================================================== +# Action Handlers +# ============================================================================== + +handle_main() { + case "$SELECTION" in + *"Sound"*) show_sound ;; + *"Brightness"*) show_brightness ;; + *"WiFi"*) coproc (rofi-network-manager &); exit 0 ;; + *"Bluetooth"*) coproc (rofi-bluetooth &); exit 0 ;; + *"Power Profile"*) show_power_profile ;; + *"Power"*) show_power ;; + *) show_main ;; + esac +} + +handle_sound() { + case "$SELECTION" in + *"Volume:"*) wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle; show_sound ;; + *"Volume +5%"*) wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+ -l 1.0; show_sound ;; + *"Volume -5%"*) wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-; show_sound ;; + *"Output Device"*) show_output ;; + *"Mic:"*) wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle; show_sound ;; + *"Mic +5%"*) wpctl set-volume @DEFAULT_AUDIO_SOURCE@ 5%+ -l 1.0; show_sound ;; + *"Mic -5%"*) wpctl set-volume @DEFAULT_AUDIO_SOURCE@ 5%-; show_sound ;; + *"Input Device"*) show_input ;; + *"Back"*) show_main ;; + *) show_sound ;; + esac +} + +handle_output() { + case "$SELECTION" in + *"Back"*) show_sound ;; + *) + [[ -n "$ROFI_INFO" ]] && wpctl set-default "$ROFI_INFO" + show_output + ;; + esac +} + +handle_input() { + case "$SELECTION" in + *"Back"*) show_sound ;; + *) + [[ -n "$ROFI_INFO" ]] && wpctl set-default "$ROFI_INFO" + show_input + ;; + esac +} + +handle_brightness() { + case "$SELECTION" in + *"Internal +5%"*) brightnessctl -q set +5%; show_brightness ;; + *"Internal -5%"*) brightnessctl -q set 5%-; show_brightness ;; + *"External +5%"*) + if [[ "$WM" == "hyprland" ]]; then + current=$(get_brightness_external) + new=$((current + 5)); [[ $new -gt 100 ]] && new=100 + val=$(echo "scale=2; $new/100" | bc) + busctl --user set-property rs.wl-gammarelay /outputs/DP_3 rs.wl.gammarelay Brightness d "$val" 2>/dev/null + else + current=$(get_brightness_external) + new=$((current + 5)); [[ $new -gt 100 ]] && new=100 + val=$(echo "scale=2; $new/100" | bc) + xrandr --output DP-3 --brightness "$val" 2>/dev/null + fi + show_brightness ;; + *"External -5%"*) + if [[ "$WM" == "hyprland" ]]; then + current=$(get_brightness_external) + new=$((current - 5)); [[ $new -lt 5 ]] && new=5 + val=$(echo "scale=2; $new/100" | bc) + busctl --user set-property rs.wl-gammarelay /outputs/DP_3 rs.wl.gammarelay Brightness d "$val" 2>/dev/null + else + current=$(get_brightness_external) + new=$((current - 5)); [[ $new -lt 5 ]] && new=5 + val=$(echo "scale=2; $new/100" | bc) + xrandr --output DP-3 --brightness "$val" 2>/dev/null + fi + show_brightness ;; + *"Back"*) show_main ;; + *) show_brightness ;; + esac +} + +handle_power_profile() { + case "$SELECTION" in + # systemd path watcher auto-applies power-tuning on profile change + *"Performance"*) powerprofilesctl set performance; show_power_profile ;; + *"Balanced"*) powerprofilesctl set balanced; show_power_profile ;; + *"Power Saver"*) powerprofilesctl set power-saver; show_power_profile ;; + *"Edit Tuning"*) coproc (zeditor "$HOME/NixOS/ryzenadj.nix" &); exit 0 ;; + *"Back"*) show_main ;; + *) show_power_profile ;; + esac +} + +handle_power() { + case "$SELECTION" in + *"Shutdown"*) systemctl poweroff ;; + *"Reboot"*) systemctl reboot ;; + *"Logout"*) + if [[ "$WM" == "hyprland" ]]; then + hyprctl dispatch exit + else + pkill -x dwm + fi + ;; + *"Lock"*) + if [[ "$WM" == "hyprland" ]]; then + hyprlock & exit 0 + else + slock & exit 0 + fi + ;; + *"Back"*) show_main ;; + *) show_power ;; + esac +} + +# ============================================================================== +# Main Entry Point +# ============================================================================== + +if [[ -z "$SELECTION" ]]; then + case "$STATE" in + sound) show_sound ;; + output) show_output ;; + input) show_input ;; + brightness) show_brightness ;; + profile) show_power_profile ;; + power) show_power ;; + *) show_main ;; + esac +else + case "$STATE" in + sound) handle_sound ;; + output) handle_output ;; + input) handle_input ;; + brightness) handle_brightness ;; + profile) handle_power_profile ;; + power) handle_power ;; + *) handle_main ;; + esac +fi diff --git a/Rofi/Scripts/websearch.sh b/Rofi/Scripts/websearch.sh new file mode 100644 index 0000000..bf03511 --- /dev/null +++ b/Rofi/Scripts/websearch.sh @@ -0,0 +1,86 @@ +#!/usr/bin/env bash +# Rofi web search - with DuckDuckGo result preview +# Supports both Hyprland (Wayland) and dwm (X11) + +# ============================================================================== +# Environment Detection +# ============================================================================== +if [[ -n "${HYPRLAND_INSTANCE_SIGNATURE:-}" ]]; then + WM="hyprland" +else + WM="dwm" +fi + +switch_to_browser() { + if [[ "$WM" == "hyprland" ]]; then + hyprctl dispatch workspace 70 + else + # Focus primary monitor first if not on mobile (single monitor) profile + [[ $(autorandr --detected) != "mobile" ]] && echo "mon-prim" > /tmp/dwm.fifo + echo "browser" > /tmp/dwm.fifo + fi +} + +# Step 1: Query input (no list) +QUERY=$(echo "" | rofi -dmenu -p "Search : " -l 0) +[ -z "$QUERY" ] && exit 0 + +# Check if input looks like a URL - open directly +if echo "$QUERY" | grep -qE '\.(com|org|net|io|dev|co|me|gov|edu|app|xyz|info)(/|$)'; then + # Add https:// if no protocol specified + [[ "$QUERY" =~ ^https?:// ]] || QUERY="https://$QUERY" + switch_to_browser + xdg-open "$QUERY" + exit 0 +fi + +# Step 2: Engine selection (DuckDuckGo first) +ENGINE=$(printf " DuckDuckGo\n Google\n MyNixOS\n Nixpkgs\n NerdFonts" | rofi -dmenu -p "Engine : " -no-custom -l 5) +[ -z "$ENGINE" ] && exit 0 + +# Remove icon prefix +ENGINE="${ENGINE#* }" + +ENCODED=$(echo "$QUERY" | sed 's/ /%20/g; s/&/%26/g; s/?/%3F/g; s/=/%3D/g') + +case "$ENGINE" in + "DuckDuckGo") + # Fetch top 5 results using ddgr + RESULTS=$(ddgr --json -n 5 "$QUERY" 2>/dev/null | jq -r '.[] | "\(.title)\t\(.url)"') + + if [ -z "$RESULTS" ]; then + # Fallback to direct search if no results + switch_to_browser + xdg-open "https://duckduckgo.com/?q=$ENCODED" + exit 0 + fi + + # Show results in rofi (title only) + TITLES=$(echo "$RESULTS" | cut -f1) + SELECTED=$(echo "$TITLES" | rofi -dmenu -p "Results : " -l 5) + [ -z "$SELECTED" ] && exit 0 + + # Get URL for selected title + URL=$(echo "$RESULTS" | grep "^$SELECTED " | cut -f2) + if [ -n "$URL" ]; then + switch_to_browser + xdg-open "$URL" + fi + ;; + "Google") + switch_to_browser + xdg-open "https://www.google.com/search?q=$ENCODED" + ;; + "MyNixOS") + switch_to_browser + xdg-open "https://mynixos.com/search?q=$ENCODED" + ;; + "Nixpkgs") + switch_to_browser + xdg-open "https://search.nixos.org/packages?query=$ENCODED" + ;; + "NerdFonts") + switch_to_browser + xdg-open "https://www.nerdfonts.com/cheat-sheet?q=$ENCODED" + ;; +esac diff --git a/Rofi/rofi.nix b/Rofi/rofi.nix new file mode 100644 index 0000000..16bf66e --- /dev/null +++ b/Rofi/rofi.nix @@ -0,0 +1,162 @@ +# Rofi configuration and scripts +# Works with both Hyprland (Wayland) and dwm (X11) +{ + pkgs-stable, + config, + lib, + ... +}: + +let + scriptsDir = ./Scripts; + + # Detect if we're in Wayland or X11 based on session type + # This is evaluated at build time, so we need runtime detection in scripts + isWayland = config.wayland.windowManager.hyprland.enable or false; + + # Terminal varies by environment + terminal = if isWayland then "foot" else "st"; +in +{ + # Wayland-specific packages (only when using Hyprland) + home.packages = + with pkgs-stable; + [ + rofi-bluetooth + rofi-network-manager + ddgr + brightnessctl + bc + sqlite + + # Custom scripts (environment-aware) + (writeShellScriptBin "rofi-websearch" (builtins.readFile "${scriptsDir}/websearch.sh")) + (writeShellScriptBin "rofi-max30" (builtins.readFile "${scriptsDir}/max30.sh")) + (writeShellScriptBin "rofi-system" (builtins.readFile "${scriptsDir}/system.sh")) + (writeShellScriptBin "rofi-favorites" (builtins.readFile "${scriptsDir}/favorites.sh")) + ] + ++ ( + if isWayland then + [ + wl-gammarelay-rs + ] + else + [ + ] + ); + + # Start wl-gammarelay-rs on Hyprland + wayland.windowManager.hyprland.settings = lib.mkIf isWayland { + exec-once = [ "wl-gammarelay-rs" ]; + }; + + programs.rofi = { + enable = true; + package = pkgs-stable.rofi; + terminal = terminal; + + plugins = with pkgs-stable; [ + rofi-calc + rofi-emoji + ]; + + modes = [ + "system:rofi-system" + "favorites:rofi-favorites" + "drun" + "calc" + "emoji" + "max30:rofi-max30" + ]; + + extraConfig = { + show-icons = true; + display-system = ""; + display-favorites = ""; + display-drun = ""; + display-calc = ""; + display-emoji = ""; + display-max30 = ""; + drun-display-format = "{name}"; + scroll-method = 0; + disable-history = false; + sidebar-mode = false; + sort = true; + sorting-method = "fzf"; + matching = "fuzzy"; + + kb-primary-paste = "Control+V,Shift+Insert"; + kb-secondary-paste = "Control+v,Insert"; + kb-secondary-copy = "Control+c"; + kb-clear-line = "Control+w"; + kb-move-front = "Control+a"; + kb-move-end = "Control+e"; + kb-move-word-back = "Alt+b,Control+Left"; + kb-move-word-forward = "Alt+f,Control+Right"; + kb-move-char-back = "Control+b"; + kb-move-char-forward = "Control+f"; + kb-remove-word-back = "Control+Alt+h,Control+BackSpace"; + kb-remove-word-forward = "Control+Alt+d"; + kb-remove-char-forward = "Delete,Control+d"; + kb-remove-char-back = "BackSpace,Shift+BackSpace,Control+h"; + kb-remove-to-eol = ""; + kb-remove-to-sol = "Control+u"; + kb-accept-entry = "Return,KP_Enter"; + kb-accept-custom = "Control+Return"; + kb-accept-custom-alt = "Control+Shift+Return"; + kb-accept-alt = "Shift+Return"; + kb-delete-entry = "Shift+Delete"; + kb-mode-next = "Right"; + kb-mode-previous = "Left"; + kb-mode-complete = "Control+l"; + kb-row-left = "Control+Page_Up"; + kb-row-right = "Control+Page_Down"; + kb-row-up = "Up"; + kb-row-down = "Down"; + kb-row-tab = ""; + kb-element-next = "Tab"; + kb-element-prev = "ISO_Left_Tab"; + kb-page-prev = "Page_Up"; + kb-page-next = "Page_Down"; + kb-row-first = "Home,KP_Home"; + kb-row-last = "End,KP_End"; + kb-row-select = "Control+space"; + kb-screenshot = "Alt+S"; + kb-ellipsize = "Alt+period"; + kb-toggle-case-sensitivity = "grave,dead_grave"; + kb-toggle-sort = "Alt+grave"; + kb-cancel = "Escape"; + }; + + theme = + let + mkLiteral = value: { + _type = "literal"; + value = value; + }; + in + { + window = { + width = mkLiteral "300px"; + location = mkLiteral "center"; + # border-radius = mkLiteral "12px"; + border = mkLiteral "2px solid"; + border-color = mkLiteral "@border-color"; + }; + listview = { + lines = 8; + fixed-height = false; + }; + element = { + padding = mkLiteral "8px"; + # border-radius = mkLiteral "6px"; + }; + # "element selected" = { + # border-radius = mkLiteral "6px"; + # }; + inputbar = { + padding = mkLiteral "8px"; + }; + }; + }; +} diff --git a/Suckless/dwm/api.nix b/Suckless/dwm/api.nix new file mode 100644 index 0000000..5950a4b --- /dev/null +++ b/Suckless/dwm/api.nix @@ -0,0 +1,92 @@ +# dwm fifo API - control dwm via named pipe +# Usage: echo "command" > /tmp/dwm.fifo +{ lib }: + +{ + config = '' + /* dwmfifo - control dwm via named pipe */ + static const char *dwmfifo = "/tmp/dwm.fifo"; + static Command commands[] = { + /* ═══════════════════════════════════════════════════════════════════════ + * TAG NAVIGATION - Semantic names matching workflow + * ═══════════════════════════════════════════════════════════════════════ */ + { "view", view, .parse = parsetag }, /* Generic: view 5 */ + { "view-all", view, {.ui = ~0} }, /* All tags */ + { "view-prev", viewprev, {0} }, /* Previous tag */ + { "view-next", viewnext, {0} }, /* Next tag */ + + /* Semantic tag shortcuts (matching tags.nix) */ + { "terminal", view, {.ui = 1 << 9} }, /* Tag 10:  */ + { "files", view, {.ui = 1 << 10} }, /* Tag 11:  */ + { "video", view, {.ui = 1 << 11} }, /* Tag 12:  */ + { "browser", view, {.ui = 1 << 12} }, /* Tag 13: 󰖟 */ + { "code", view, {.ui = 1 << 13} }, /* Tag 14:  */ + { "freecad", view, {.ui = 1 << 14} }, /* Tag 15: 󰻬 */ + { "kicad-pm", view, {.ui = 1 << 15} }, /* Tag 16:  (project manager) */ + { "kicad", view, {.ui = 1 << 16} }, /* Tag 17:  (sch/pcb) */ + { "kicad-aux", view, {.ui = 1 << 17} }, /* Tag 18:  (pcb mobile) */ + + /* ═══════════════════════════════════════════════════════════════════════ + * MONITOR CONTROL - Primary (left) / Secondary (right) + * ═══════════════════════════════════════════════════════════════════════ */ + { "mon-prim", focusnthmon, {.i = 1} }, /* Focus primary monitor */ + { "mon-sec", focusnthmon, {.i = 0} }, /* Focus secondary monitor */ + { "mon-send-prim", tagnthmon, {.i = 1} }, /* Send window to primary */ + { "mon-send-sec", tagnthmon, {.i = 0} }, /* Send window to secondary */ + { "mon-swap", swapmon, {0} }, /* Swap monitor contents */ + + /* ═══════════════════════════════════════════════════════════════════════ + * WINDOW MANAGEMENT + * ═══════════════════════════════════════════════════════════════════════ */ + { "focus-next", focusstack, {.i = +1} }, /* Next window in stack */ + { "focus-prev", focusstack, {.i = -1} }, /* Previous window */ + { "focus", focusstack, .parse = parseplusminus },/* Generic: focus +2 */ + { "kill", killclient, {0} }, /* Close window */ + { "zoom", zoom, {0} }, /* Promote to master */ + { "float", togglefloating, {0} }, /* Toggle floating */ + + /* ═══════════════════════════════════════════════════════════════════════ + * LAYOUT + * ═══════════════════════════════════════════════════════════════════════ */ + { "layout-tile", setlayout, {.v = &layouts[0]} }, + { "layout-float", setlayout, {.v = &layouts[1]} }, + { "layout-mono", setlayout, {.v = &layouts[2]} }, + { "layout-toggle", setlayout, {0} }, /* Cycle layouts */ + + /* ═══════════════════════════════════════════════════════════════════════ + * MASTER AREA + * ═══════════════════════════════════════════════════════════════════════ */ + { "master-inc", incnmaster, {.i = +1} }, + { "master-dec", incnmaster, {.i = -1} }, + { "mfact", setmfact, .parse = parseplusminus },/* mfact +0.05 */ + + /* ═══════════════════════════════════════════════════════════════════════ + * TAG MANAGEMENT + * ═══════════════════════════════════════════════════════════════════════ */ + { "toggleview", toggleview, .parse = parsetag }, + { "tag", tag, .parse = parsetag }, + { "toggletag", toggletag, .parse = parsetag }, + { "tagmon", tagmon, .parse = parseplusminus }, + + /* ═══════════════════════════════════════════════════════════════════════ + * ADVANCED (window targeting) + * ═══════════════════════════════════════════════════════════════════════ */ + { "viewwin", viewwin, .parse = parsexid }, /* Focus by X window ID */ + { "viewname", viewname, .parse = parsestr }, /* Focus by name */ + + /* ═══════════════════════════════════════════════════════════════════════ + * SPAWNS + * ═══════════════════════════════════════════════════════════════════════ */ + { "spawn-term", spawn, {.v = termcmd} }, + { "spawn-browser", spawn, {.v = browsercmd} }, + { "spawn-rofi", spawn, {.v = roficmd} }, + { "spawn-files", spawn, {.v = filescmd} }, + { "spawn-editor", spawn, {.v = editorcmd} }, + + /* ═══════════════════════════════════════════════════════════════════════ + * SESSION + * ═══════════════════════════════════════════════════════════════════════ */ + { "quit", quit, {0} }, + }; + ''; +} diff --git a/Suckless/dwm/appearance.nix b/Suckless/dwm/appearance.nix new file mode 100644 index 0000000..dfcc393 --- /dev/null +++ b/Suckless/dwm/appearance.nix @@ -0,0 +1,39 @@ +# dwm appearance settings (colors, fonts, borders, gaps) +# Integrates with Stylix for consistent theming +{ config, lib }: + +let + colors = config.lib.stylix.colors; + fonts = config.stylix.fonts; +in +{ + config = '' + /* See LICENSE file for copyright and license details. */ + + /* appearance */ + static const unsigned int borderpx = 2; /* border pixel of windows */ + static const unsigned int gappx = 5; /* gaps between windows */ + static const unsigned int snap = 32; /* snap pixel */ + static const int showbar = 0; /* 0 means no bar (holdbar shows on key hold) */ + static const int topbar = 1; /* 0 means bottom bar */ + /* Display modes of the tab bar: never shown, always shown, shown only in */ + /* monocle mode in the presence of several windows. */ + /* Modes after showtab_nmodes are disabled. */ + enum showtab_modes { showtab_never, showtab_auto, showtab_nmodes, showtab_always}; + static const int showtab = showtab_auto; /* Default tab bar show mode */ + static const int toptab = False; /* False means bottom tab bar */ + + static const char *fonts[] = { "${fonts.monospace.name}:size=${toString fonts.sizes.terminal}" }; + static const char dmenufont[] = "${fonts.monospace.name}:size=${toString fonts.sizes.terminal}"; + static const char col_gray1[] = "#${colors.base00}"; + static const char col_gray2[] = "#${colors.base01}"; + static const char col_gray3[] = "#${colors.base04}"; + static const char col_gray4[] = "#${colors.base05}"; + static const char col_cyan[] = "#${colors.base0D}"; + static const char *colors[][3] = { + /* fg bg border */ + [SchemeNorm] = { col_gray3, col_gray1, col_gray2 }, + [SchemeSel] = { col_gray4, col_cyan, col_cyan }, + }; + ''; +} diff --git a/Suckless/dwm/dwm-lebowski.patch b/Suckless/dwm/dwm-lebowski.patch new file mode 100644 index 0000000..0b66be6 --- /dev/null +++ b/Suckless/dwm/dwm-lebowski.patch @@ -0,0 +1,1779 @@ +diff --git a/config.def.h b/config.def.h +index 9efa774..17ee062 100644 +--- a/config.def.h ++++ b/config.def.h +@@ -2,9 +2,17 @@ + + /* appearance */ + static const unsigned int borderpx = 1; /* border pixel of windows */ ++static const unsigned int gappx = 5; /* gaps between windows */ + static const unsigned int snap = 32; /* snap pixel */ + static const int showbar = 1; /* 0 means no bar */ + static const int topbar = 1; /* 0 means bottom bar */ ++/* Display modes of the tab bar: never shown, always shown, shown only in */ ++/* monocle mode in the presence of several windows. */ ++/* Modes after showtab_nmodes are disabled. */ ++enum showtab_modes { showtab_never, showtab_auto, showtab_nmodes, showtab_always}; ++static const int showtab = showtab_auto; /* Default tab bar show mode */ ++static const int toptab = False; /* False means bottom tab bar */ ++ + static const char *fonts[] = { "monospace:size=10" }; + static const char dmenufont[] = "monospace:size=10"; + static const char col_gray1[] = "#222222"; +@@ -44,13 +52,21 @@ static const Layout layouts[] = { + { "[M]", monocle }, + }; + ++/* default layout per tag */ ++/* 0 = tile, 1 = floating, 2 = monocle */ ++static int def_layouts[1 + LENGTH(tags)] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; ++ + /* key definitions */ + #define MODKEY Mod1Mask ++#define ALTMOD Mod4Mask + #define TAGKEYS(KEY,TAG) \ + { MODKEY, KEY, view, {.ui = 1 << TAG} }, \ + { MODKEY|ControlMask, KEY, toggleview, {.ui = 1 << TAG} }, \ + { MODKEY|ShiftMask, KEY, tag, {.ui = 1 << TAG} }, \ +- { MODKEY|ControlMask|ShiftMask, KEY, toggletag, {.ui = 1 << TAG} }, ++ { MODKEY|ControlMask|ShiftMask, KEY, toggletag, {.ui = 1 << TAG} }, \ ++ { ALTMOD, KEY, focusnthmon, {.i = TAG } }, \ ++ { ALTMOD|ShiftMask, KEY, tagnthmon, {.i = TAG } }, ++#define HOLDKEY 0 // replace 0 with the keysym to activate holdbar + + /* helper for spawning shell commands in the pre dwm-5.0 fashion */ + #define SHCMD(cmd) { .v = (const char*[]){ "/bin/sh", "-c", cmd, NULL } } +@@ -65,6 +81,7 @@ static const Key keys[] = { + { MODKEY, XK_p, spawn, {.v = dmenucmd } }, + { MODKEY|ShiftMask, XK_Return, spawn, {.v = termcmd } }, + { MODKEY, XK_b, togglebar, {0} }, ++ { MODKEY, XK_w, tabmode, {-1} }, + { MODKEY, XK_j, focusstack, {.i = +1 } }, + { MODKEY, XK_k, focusstack, {.i = -1 } }, + { MODKEY, XK_i, incnmaster, {.i = +1 } }, +@@ -79,12 +96,21 @@ static const Key keys[] = { + { MODKEY, XK_m, setlayout, {.v = &layouts[2]} }, + { MODKEY, XK_space, setlayout, {0} }, + { MODKEY|ShiftMask, XK_space, togglefloating, {0} }, ++ { MODKEY|ShiftMask, XK_f, togglefullscr, {0} }, + { MODKEY, XK_0, view, {.ui = ~0 } }, + { MODKEY|ShiftMask, XK_0, tag, {.ui = ~0 } }, + { MODKEY, XK_comma, focusmon, {.i = -1 } }, + { MODKEY, XK_period, focusmon, {.i = +1 } }, + { MODKEY|ShiftMask, XK_comma, tagmon, {.i = -1 } }, + { MODKEY|ShiftMask, XK_period, tagmon, {.i = +1 } }, ++ { MODKEY, XK_Right, viewnext, {0} }, ++ { MODKEY, XK_Left, viewprev, {0} }, ++ { MODKEY|ShiftMask, XK_Right, tagtonext, {0} }, ++ { MODKEY|ShiftMask, XK_Left, tagtoprev, {0} }, ++ { MODKEY|ShiftMask, XK_apostrophe, swapmon, {0} }, ++ { MODKEY, XK_minus, setgaps, {.i = -1 } }, ++ { MODKEY, XK_equal, setgaps, {.i = +1 } }, ++ { MODKEY|ShiftMask, XK_equal, setgaps, {.i = 0 } }, + TAGKEYS( XK_1, 0) + TAGKEYS( XK_2, 1) + TAGKEYS( XK_3, 2) +@@ -95,6 +121,7 @@ static const Key keys[] = { + TAGKEYS( XK_8, 7) + TAGKEYS( XK_9, 8) + { MODKEY|ShiftMask, XK_q, quit, {0} }, ++ { 0, HOLDKEY, holdbar, {0} }, + }; + + /* button definitions */ +@@ -112,5 +139,32 @@ static const Button buttons[] = { + { ClkTagBar, 0, Button3, toggleview, {0} }, + { ClkTagBar, MODKEY, Button1, tag, {0} }, + { ClkTagBar, MODKEY, Button3, toggletag, {0} }, ++ { ClkTabBar, 0, Button1, focuswin, {0} }, + }; + ++static const char *dwmfifo = "/tmp/dwm.fifo"; ++static Command commands[] = { ++ { "dmenu", spawn, {.v = dmenucmd} }, ++ { "term", spawn, {.v = termcmd} }, ++ { "quit", quit, {0} }, ++ { "togglebar", togglebar, {0} }, ++ { "focusstack", focusstack, .parse = parseplusminus }, ++ { "incnmaster", incnmaster, .parse = parseplusminus }, ++ { "setmfact", setmfact, .parse = parseplusminus }, ++ { "zoom", zoom, {0} }, ++ { "killclient", killclient, {0} }, ++ { "setlayout-tiled", setlayout, {.v = &layouts[0]} }, ++ { "setlayout-float", setlayout, {.v = &layouts[1]} }, ++ { "setlayout-mono", setlayout, {.v = &layouts[2]} }, ++ { "togglelayout", setlayout, {0} }, ++ { "togglefloating", togglefloating, {0} }, ++ { "viewwin", viewwin, .parse = parsexid }, ++ { "viewname", viewname, .parse = parsestr }, ++ { "viewall", view, {.ui = ~0} }, ++ { "focusmon", focusmon, .parse = parseplusminus }, ++ { "tagmon", tagmon, .parse = parseplusminus }, ++ { "view", view, .parse = parsetag }, ++ { "toggleview", toggleview, .parse = parsetag }, ++ { "tag", tag, .parse = parsetag }, ++ { "toggletag", toggletag, .parse = parsetag }, ++}; +diff --git a/config.mk b/config.mk +index b469a2b..c57985c 100644 +--- a/config.mk ++++ b/config.mk +@@ -23,7 +23,7 @@ FREETYPEINC = /usr/include/freetype2 + + # includes and libs + INCS = -I${X11INC} -I${FREETYPEINC} +-LIBS = -L${X11LIB} -lX11 ${XINERAMALIBS} ${FREETYPELIBS} ++LIBS = -L${X11LIB} -lX11 ${XINERAMALIBS} ${FREETYPELIBS} -lXcursor + + # flags + CPPFLAGS = -D_DEFAULT_SOURCE -D_BSD_SOURCE -D_XOPEN_SOURCE=700L -DVERSION=\"${VERSION}\" ${XINERAMAFLAGS} +diff --git a/drw.c b/drw.c +index c41e6af..80d2c2d 100644 +--- a/drw.c ++++ b/drw.c +@@ -4,6 +4,7 @@ + #include + #include + #include ++#include + + #include "drw.h" + #include "util.h" +@@ -425,14 +426,14 @@ drw_font_getexts(Fnt *font, const char *text, unsigned int len, unsigned int *w, + } + + Cur * +-drw_cur_create(Drw *drw, int shape) ++drw_cur_create(Drw *drw, const char *shape) + { + Cur *cur; + + if (!drw || !(cur = ecalloc(1, sizeof(Cur)))) + return NULL; + +- cur->cursor = XCreateFontCursor(drw->dpy, shape); ++ cur->cursor = XcursorLibraryLoadCursor(drw->dpy, shape); + + return cur; + } +diff --git a/drw.h b/drw.h +index 6471431..22295a3 100644 +--- a/drw.h ++++ b/drw.h +@@ -43,7 +43,7 @@ void drw_clr_create(Drw *drw, Clr *dest, const char *clrname); + Clr *drw_scm_create(Drw *drw, const char *clrnames[], size_t clrcount); + + /* Cursor abstraction */ +-Cur *drw_cur_create(Drw *drw, int shape); ++Cur *drw_cur_create(Drw *drw, const char *shape); + void drw_cur_free(Drw *drw, Cur *cursor); + + /* Drawing context manipulation */ +diff --git a/dwm.1 b/dwm.1 +index ddc8321..5903e70 100644 +--- a/dwm.1 ++++ b/dwm.1 +@@ -20,14 +20,22 @@ layout applied. + Windows are grouped by tags. Each window can be tagged with one or multiple + tags. Selecting certain tags displays all windows with these tags. + .P +-Each screen contains a small status bar which displays all available tags, the +-layout, the title of the focused window, and the text read from the root window +-name property, if the screen is focused. A floating window is indicated with an +-empty square and a maximised floating window is indicated with a filled square +-before the windows title. The selected tags are indicated with a different +-color. The tags of the focused window are indicated with a filled square in the +-top left corner. The tags which are applied to one or more windows are +-indicated with an empty square in the top left corner. ++Each screen contains two small status bars. ++.P ++One bar displays all available tags, the layout, the title of the focused ++window, and the text read from the root window name property, if the screen is ++focused. A floating window is indicated with an empty square and a maximised ++floating window is indicated with a filled square before the windows title. The ++selected tags are indicated with a different color. The tags of the focused ++window are indicated with a filled square in the top left corner. The tags ++which are applied to one or more windows are indicated with an empty square in ++the top left corner. ++.P ++Another bar contains a tab for each window of the current view and allows ++navigation between windows, especially in the monocle mode. The different ++display modes of this bar are described under the Mod1\-w Keybord command ++section. When a single tag is selected, this tag is indicated in the left corner ++of the tab bar. + .P + dwm draws a small border around windows to indicate the focus state. + .SH OPTIONS +@@ -44,7 +52,8 @@ command. + .TP + .B Button1 + click on a tag label to display all windows with that tag, click on the layout +-label toggles between tiled and floating layout. ++label toggles between tiled and floating layout, click on a window name in the ++tab bar brings focus to that window. + .TP + .B Button3 + click on a tag label adds/removes all windows with that tag to/from the view. +@@ -110,12 +119,21 @@ Increase master area size. + .B Mod1\-h + Decrease master area size. + .TP ++.B Mod1\-w ++Cycle over the tab bar display modes: never displayed, always displayed, ++displayed only in monocle mode when the view contains more than one window (auto ++mode). Some display modes can be disabled in the configuration, config.h. In ++the default configuration only "never" and "auto" display modes are enabled. ++.TP + .B Mod1\-Return + Zooms/cycles focused window to/from master area (tiled layouts only). + .TP + .B Mod1\-Shift\-c + Close focused window. + .TP ++.B Mod1\-Shift\-f ++Toggle fullscreen for focused window. ++.TP + .B Mod1\-Shift\-space + Toggle focused window between tiled and floating state. + .TP +diff --git a/dwm.c b/dwm.c +index 1443802..08d4517 100644 +--- a/dwm.c ++++ b/dwm.c +@@ -21,6 +21,7 @@ + * To understand everything else, start reading main(). + */ + #include ++#include + #include + #include + #include +@@ -28,10 +29,12 @@ + #include + #include + #include ++#include ++#include + #include + #include +-#include + #include ++#include + #include + #include + #include +@@ -40,6 +43,7 @@ + #include + #endif /* XINERAMA */ + #include ++#include + + #include "drw.h" + #include "util.h" +@@ -63,7 +67,7 @@ enum { NetSupported, NetWMName, NetWMState, NetWMCheck, + NetWMFullscreen, NetActiveWindow, NetWMWindowType, + NetWMWindowTypeDialog, NetClientList, NetLast }; /* EWMH atoms */ + enum { WMProtocols, WMDelete, WMState, WMTakeFocus, WMLast }; /* default atoms */ +-enum { ClkTagBar, ClkLtSymbol, ClkStatusText, ClkWinTitle, ++enum { ClkTagBar, ClkTabBar, ClkLtSymbol, ClkStatusText, ClkWinTitle, + ClkClientWin, ClkRootWin, ClkLast }; /* clicks */ + + typedef union { +@@ -110,25 +114,36 @@ typedef struct { + void (*arrange)(Monitor *); + } Layout; + ++#define MAXTABS 50 ++ ++typedef struct Pertag Pertag; + struct Monitor { + char ltsymbol[16]; + float mfact; + int nmaster; + int num; + int by; /* bar geometry */ ++ int ty; /* tab bar geometry */ + int mx, my, mw, mh; /* screen size */ + int wx, wy, ww, wh; /* window area */ ++ int gappx; /* gaps between windows */ + unsigned int seltags; + unsigned int sellt; + unsigned int tagset[2]; + int showbar; ++ int showtab; + int topbar; ++ int toptab; + Client *clients; + Client *sel; + Client *stack; + Monitor *next; + Window barwin; ++ Window tabwin; ++ int ntabs; ++ int tab_widths[MAXTABS]; + const Layout *lt[2]; ++ Pertag *pertag; + }; + + typedef struct { +@@ -140,9 +155,20 @@ typedef struct { + int monitor; + } Rule; + ++typedef struct { ++ int x, y, w, h; ++} DwmLogo; ++ ++typedef struct { ++ const char *name; ++ void (*func)(const Arg *arg); ++ const Arg arg; ++ int (*parse)(Arg *arg, const char *s, size_t len); ++} Command; ++ + /* function declarations */ + static void applyrules(Client *c); +-static int applysizehints(Client *c, int *x, int *y, int *w, int *h, int interact); ++static int applysizehints(Client *c, int *x, int *y, int *w, int *h, int *bw, int interact); + static void arrange(Monitor *m); + static void arrangemon(Monitor *m); + static void attach(Client *c); +@@ -160,14 +186,20 @@ static void destroynotify(XEvent *e); + static void detach(Client *c); + static void detachstack(Client *c); + static Monitor *dirtomon(int dir); ++static void dispatchcmd(void); ++static Monitor *numtomon(int num); + static void drawbar(Monitor *m); + static void drawbars(void); ++static void drawtab(Monitor *m); ++static void drawtabs(void); + static void enternotify(XEvent *e); + static void expose(XEvent *e); + static void focus(Client *c); + static void focusin(XEvent *e); + static void focusmon(const Arg *arg); ++static void focusnthmon(const Arg *arg); + static void focusstack(const Arg *arg); ++static void focuswin(const Arg* arg); + static Atom getatomprop(Client *c, Atom prop); + static int getrootptr(int *x, int *y); + static long getstate(Window w); +@@ -176,6 +208,7 @@ static void grabbuttons(Client *c, int focused); + static void grabkeys(void); + static void incnmaster(const Arg *arg); + static void keypress(XEvent *e); ++static void keyrelease(XEvent *e); + static void killclient(const Arg *arg); + static void manage(Window w, XWindowAttributes *wa); + static void mappingnotify(XEvent *e); +@@ -183,13 +216,15 @@ static void maprequest(XEvent *e); + static void monocle(Monitor *m); + static void motionnotify(XEvent *e); + static void movemouse(const Arg *arg); ++static unsigned int nexttag(void); + static Client *nexttiled(Client *c); + static void pop(Client *c); ++static unsigned int prevtag(void); + static void propertynotify(XEvent *e); + static void quit(const Arg *arg); + static Monitor *recttomon(int x, int y, int w, int h); +-static void resize(Client *c, int x, int y, int w, int h, int interact); +-static void resizeclient(Client *c, int x, int y, int w, int h); ++static void resize(Client *c, int x, int y, int w, int h, int bw, int interact); ++static void resizeclient(Client *c, int x, int y, int w, int h, int bw); + static void resizemouse(const Arg *arg); + static void restack(Monitor *m); + static void run(void); +@@ -199,23 +234,31 @@ static void sendmon(Client *c, Monitor *m); + static void setclientstate(Client *c, long state); + static void setfocus(Client *c); + static void setfullscreen(Client *c, int fullscreen); ++static void setgaps(const Arg *arg); + static void setlayout(const Arg *arg); + static void setmfact(const Arg *arg); + static void setup(void); + static void seturgent(Client *c, int urg); + static void showhide(Client *c); + static void spawn(const Arg *arg); ++static void tabmode(const Arg *arg); + static void tag(const Arg *arg); + static void tagmon(const Arg *arg); ++static void tagnthmon(const Arg *arg); ++static void tagtonext(const Arg *arg); ++static void tagtoprev(const Arg *arg); + static void tile(Monitor *m); + static void togglebar(const Arg *arg); ++static void holdbar(const Arg *arg); + static void togglefloating(const Arg *arg); ++static void togglefullscr(const Arg *arg); + static void toggletag(const Arg *arg); + static void toggleview(const Arg *arg); + static void unfocus(Client *c, int setfocus); + static void unmanage(Client *c, int destroyed); + static void unmapnotify(XEvent *e); + static void updatebarpos(Monitor *m); ++static void updateholdbarpos(Monitor *m); + static void updatebars(void); + static void updateclientlist(void); + static int updategeom(void); +@@ -226,12 +269,17 @@ static void updatetitle(Client *c); + static void updatewindowtype(Client *c); + static void updatewmhints(Client *c); + static void view(const Arg *arg); ++static void viewname(const Arg *arg); ++static void viewwin(const Arg *arg); ++static void viewnext(const Arg *arg); ++static void viewprev(const Arg *arg); + static Client *wintoclient(Window w); + static Monitor *wintomon(Window w); + static int xerror(Display *dpy, XErrorEvent *ee); + static int xerrordummy(Display *dpy, XErrorEvent *ee); + static int xerrorstart(Display *dpy, XErrorEvent *ee); + static void zoom(const Arg *arg); ++static void swapmon(const Arg *arg); + + /* variables */ + static const char broken[] = "broken"; +@@ -239,11 +287,14 @@ static char stext[256]; + static int screen; + static int sw, sh; /* X display screen geometry width, height */ + static int bh; /* bar height */ ++static int th = 0; /* tab bar geometry */ + static int lrpad; /* sum of left and right padding for text */ ++static int dwmlogowdth = 74; /* dwm logo width + left/right padding */ + static int (*xerrorxlib)(Display *, XErrorEvent *); + static unsigned int numlockmask = 0; + static void (*handler[LASTEvent]) (XEvent *) = { + [ButtonPress] = buttonpress, ++ [ButtonRelease] = keyrelease, + [ClientMessage] = clientmessage, + [ConfigureRequest] = configurerequest, + [ConfigureNotify] = configurenotify, +@@ -251,6 +302,7 @@ static void (*handler[LASTEvent]) (XEvent *) = { + [EnterNotify] = enternotify, + [Expose] = expose, + [FocusIn] = focusin, ++ [KeyRelease] = keyrelease, + [KeyPress] = keypress, + [MappingNotify] = mappingnotify, + [MapRequest] = maprequest, +@@ -266,14 +318,120 @@ static Display *dpy; + static Drw *drw; + static Monitor *mons, *selmon; + static Window root, wmcheckwin; ++static int fifofd; ++ ++static int parsetag(Arg *a, const char *s, size_t len); ++static int parseplusminus(Arg *a, const char *s, size_t len); ++static int parsexid(Arg *a, const char *s, size_t len); ++static int parsestr(Arg *a, const char *s, size_t len); + + /* configuration, allows nested code to access above variables */ + #include "config.h" + ++static int parsetag(Arg *a, const char *s, size_t len) ++{ ++ char *end; ++ unsigned int rv = strtoul(s, &end, 10); ++ if (end == s) ++ a->ui = 0; ++ else if (rv > LENGTH(tags)) ++ return 0; ++ else if (rv == 0) ++ a->ui = ~0U; ++ else ++ a->ui = 1U << (rv - 1); ++ ++ return 1; ++} ++ ++static int parseplusminus(Arg *a, const char *s, size_t len) ++{ ++ if (*s == '+') ++ a->i = +1; ++ else if (*s == '-') ++ a->i = -1; ++ else ++ return 0; ++ return 1; ++} ++ ++static int parsexid(Arg *a, const char *s, size_t len) ++{ ++ char *end; ++ unsigned long long sv = strtoull(s, &end, 0); ++ ++ if (end == s) ++ return 0; ++ ++ a->v = (void *)(intptr_t)sv; ++ return 1; ++} ++ ++static int parsestr(Arg *a, const char *s, size_t len) ++{ ++ while (*s == ' ' || *s == '\t') ++ s++; ++ a->v = s; ++ return 1; ++} ++ ++struct Pertag { ++ unsigned int curtag, prevtag; /* current and previous tag */ ++ int nmasters[LENGTH(tags) + 1]; /* number of windows in master area */ ++ float mfacts[LENGTH(tags) + 1]; /* mfacts per tag */ ++ unsigned int sellts[LENGTH(tags) + 1]; /* selected layouts */ ++ const Layout *ltidxs[LENGTH(tags) + 1][2]; /* matrix of tags and layouts indexes */ ++ /* int showbars[LENGTH(tags) + 1]; */ /* disabled for holdbar compat */ ++}; ++ + /* compile-time check if all tags fit into an unsigned int bit array. */ + struct NumTags { char limitexceeded[LENGTH(tags) > 31 ? -1 : 1]; }; + + /* function implementations */ ++void ++holdbar(const Arg *arg) ++{ ++ if (selmon->showbar) ++ return; ++ selmon->showbar = 2; ++ updateholdbarpos(selmon); ++ XMoveResizeWindow(dpy, selmon->barwin, selmon->wx, selmon->by, selmon->ww, bh); ++} ++ ++void ++keyrelease(XEvent *e) ++{ ++ if (XEventsQueued(dpy, QueuedAfterReading)) { ++ XEvent ne; ++ XPeekEvent(dpy, &ne); ++ ++ if (ne.type == KeyPress && ne.xkey.time == e->xkey.time && ++ ne.xkey.keycode == e->xkey.keycode) { ++ XNextEvent(dpy, &ne); ++ return; ++ } ++ } ++ if (e->xkey.keycode == XKeysymToKeycode(dpy, HOLDKEY) && selmon->showbar == 2) { ++ selmon->showbar = 0; ++ updateholdbarpos(selmon); ++ XMoveResizeWindow(dpy, selmon->barwin, selmon->wx, selmon->by, selmon->ww, bh); ++ arrange(selmon); ++ } ++} ++ ++void ++updateholdbarpos(Monitor *m) ++{ ++ m->wy = m->my; ++ m->wh = m->mh; ++ if (m->showbar) { ++ m->by = m->topbar ? m->wy : m->wy + m->wh - bh; ++ m->wy = m->topbar ? m->wy - bh + bh : m->wy; ++ } else { ++ m->by = -bh; ++ } ++} ++ + void + applyrules(Client *c) + { +@@ -311,7 +469,7 @@ applyrules(Client *c) + } + + int +-applysizehints(Client *c, int *x, int *y, int *w, int *h, int interact) ++applysizehints(Client *c, int *x, int *y, int *w, int *h, int *bw, int interact) + { + int baseismin; + Monitor *m = c->mon; +@@ -324,18 +482,18 @@ applysizehints(Client *c, int *x, int *y, int *w, int *h, int interact) + *x = sw - WIDTH(c); + if (*y > sh) + *y = sh - HEIGHT(c); +- if (*x + *w + 2 * c->bw < 0) ++ if (*x + *w + 2 * *bw < 0) + *x = 0; +- if (*y + *h + 2 * c->bw < 0) ++ if (*y + *h + 2 * *bw < 0) + *y = 0; + } else { + if (*x >= m->wx + m->ww) + *x = m->wx + m->ww - WIDTH(c); + if (*y >= m->wy + m->wh) + *y = m->wy + m->wh - HEIGHT(c); +- if (*x + *w + 2 * c->bw <= m->wx) ++ if (*x + *w + 2 * *bw <= m->wx) + *x = m->wx; +- if (*y + *h + 2 * c->bw <= m->wy) ++ if (*y + *h + 2 * *bw <= m->wy) + *y = m->wy; + } + if (*h < bh) +@@ -375,7 +533,7 @@ applysizehints(Client *c, int *x, int *y, int *w, int *h, int interact) + if (c->maxh) + *h = MIN(*h, c->maxh); + } +- return *x != c->x || *y != c->y || *w != c->w || *h != c->h; ++ return *x != c->x || *y != c->y || *w != c->w || *h != c->h || *bw != c->bw; + } + + void +@@ -393,11 +551,19 @@ arrange(Monitor *m) + } + + void +-arrangemon(Monitor *m) +-{ ++arrangemon(Monitor *m) { ++ Client *c; ++ ++ updatebarpos(m); ++ XMoveResizeWindow(dpy, m->tabwin, m->wx, m->ty, m->ww, th); + strncpy(m->ltsymbol, m->lt[m->sellt]->symbol, sizeof m->ltsymbol); + if (m->lt[m->sellt]->arrange) + m->lt[m->sellt]->arrange(m); ++ else ++ /* <>< floating layout; add borders */ ++ for (c = selmon->clients; c; c = c->next) ++ if (ISVISIBLE(c) && c->bw == 0) ++ resize(c, c->x, c->y, c->w - 2*borderpx, c->h - 2*borderpx, borderpx, 0); + } + + void +@@ -432,9 +598,16 @@ buttonpress(XEvent *e) + } + if (ev->window == selmon->barwin) { + i = x = 0; +- do ++ x = dwmlogowdth; /* dwm logo width */ ++ unsigned int occ = 0; ++ for(c = m->clients; c; c=c->next) ++ occ |= c->tags == TAGMASK ? 0 : c->tags; ++ do { ++ /* Do not reserve space for vacant tags */ ++ if (!(occ & 1 << i || m->tagset[m->seltags] & 1 << i)) ++ continue; + x += TEXTW(tags[i]); +- while (ev->x >= x && ++i < LENGTH(tags)); ++ } while (ev->x >= x && ++i < LENGTH(tags)); + if (i < LENGTH(tags)) { + click = ClkTagBar; + arg.ui = 1 << i; +@@ -444,7 +617,24 @@ buttonpress(XEvent *e) + click = ClkStatusText; + else + click = ClkWinTitle; +- } else if ((c = wintoclient(ev->window))) { ++ } ++ if(ev->window == selmon->tabwin) { ++ i = 0; x = 0; ++ for(c = selmon->clients; c; c = c->next){ ++ if(!ISVISIBLE(c)) continue; ++ x += selmon->tab_widths[i]; ++ if (ev->x > x) ++ ++i; ++ else ++ break; ++ if(i >= m->ntabs) break; ++ } ++ if(c) { ++ click = ClkTabBar; ++ arg.ui = i; ++ } ++ } ++ else if((c = wintoclient(ev->window))) { + focus(c); + restack(selmon); + XAllowEvents(dpy, ReplayPointer, CurrentTime); +@@ -452,8 +642,9 @@ buttonpress(XEvent *e) + } + for (i = 0; i < LENGTH(buttons); i++) + if (click == buttons[i].click && buttons[i].func && buttons[i].button == ev->button +- && CLEANMASK(buttons[i].mask) == CLEANMASK(ev->state)) +- buttons[i].func(click == ClkTagBar && buttons[i].arg.i == 0 ? &arg : &buttons[i].arg); ++ && CLEANMASK(buttons[i].mask) == CLEANMASK(ev->state)){ ++ buttons[i].func(((click == ClkTagBar || click == ClkTabBar) && buttons[i].arg.i == 0) ? &arg : &buttons[i].arg); ++ } + } + + void +@@ -493,6 +684,7 @@ cleanup(void) + XSync(dpy, False); + XSetInputFocus(dpy, PointerRoot, RevertToPointerRoot, CurrentTime); + XDeleteProperty(dpy, root, netatom[NetActiveWindow]); ++ close(fifofd); + } + + void +@@ -508,6 +700,8 @@ cleanupmon(Monitor *mon) + } + XUnmapWindow(dpy, mon->barwin); + XDestroyWindow(dpy, mon->barwin); ++ XUnmapWindow(dpy, mon->tabwin); ++ XDestroyWindow(dpy, mon->tabwin); + free(mon); + } + +@@ -516,6 +710,7 @@ clientmessage(XEvent *e) + { + XClientMessageEvent *cme = &e->xclient; + Client *c = wintoclient(cme->window); ++ unsigned int i; + + if (!c) + return; +@@ -525,8 +720,15 @@ clientmessage(XEvent *e) + setfullscreen(c, (cme->data.l[0] == 1 /* _NET_WM_STATE_ADD */ + || (cme->data.l[0] == 2 /* _NET_WM_STATE_TOGGLE */ && !c->isfullscreen))); + } else if (cme->message_type == netatom[NetActiveWindow]) { +- if (c != selmon->sel && !c->isurgent) +- seturgent(c, 1); ++ for (i = 0; i < LENGTH(tags) && !((1 << i) & c->tags); i++); ++ if (i < LENGTH(tags)) { ++ const Arg a = {.ui = 1 << i}; ++ selmon = c->mon; ++ view(&a); ++ focus(c); ++ XWarpPointer(dpy, None, c->win, 0, 0, 0, 0, c->w/2, c->h/2); ++ restack(selmon); ++ } + } + } + +@@ -568,7 +770,7 @@ configurenotify(XEvent *e) + for (m = mons; m; m = m->next) { + for (c = m->clients; c; c = c->next) + if (c->isfullscreen) +- resizeclient(c, m->mx, m->my, m->mw, m->mh); ++ resizeclient(c, m->mx, m->my, m->mw, m->mh, 0); + XMoveResizeWindow(dpy, m->barwin, m->wx, m->by, m->ww, bh); + } + focus(NULL); +@@ -633,16 +835,35 @@ Monitor * + createmon(void) + { + Monitor *m; ++ unsigned int i; + + m = ecalloc(1, sizeof(Monitor)); + m->tagset[0] = m->tagset[1] = 1; + m->mfact = mfact; + m->nmaster = nmaster; + m->showbar = showbar; ++ m->showtab = showtab; + m->topbar = topbar; ++ m->gappx = gappx; ++ m->toptab = toptab; ++ m->ntabs = 0; + m->lt[0] = &layouts[0]; + m->lt[1] = &layouts[1 % LENGTH(layouts)]; + strncpy(m->ltsymbol, layouts[0].symbol, sizeof m->ltsymbol); ++ m->pertag = ecalloc(1, sizeof(Pertag)); ++ m->pertag->curtag = m->pertag->prevtag = 1; ++ ++ for (i = 0; i <= LENGTH(tags); i++) { ++ m->pertag->nmasters[i] = m->nmaster; ++ m->pertag->mfacts[i] = m->mfact; ++ ++ m->pertag->ltidxs[i][0] = &layouts[def_layouts[i % LENGTH(def_layouts)] % LENGTH(layouts)]; ++ m->pertag->ltidxs[i][1] = m->lt[1]; ++ m->pertag->sellts[i] = m->sellt; ++ ++ /* m->pertag->showbars[i] = m->showbar; */ ++ } ++ + return m; + } + +@@ -694,10 +915,88 @@ dirtomon(int dir) + return m; + } + ++Monitor * ++numtomon(int num) ++{ ++ Monitor *m = NULL; ++ int i = 0; ++ ++ for(m = mons, i=0; m->next && i < num; m = m->next){ ++ i++; ++ } ++ return m; ++} ++ ++static const char * ++strnprefix(const char *haystack, size_t hlen, const char *needle) ++{ ++ while (*needle && hlen--) { ++ if (*haystack++ != *needle++) ++ return 0; ++ } ++ ++ if (*needle) ++ return NULL; ++ return haystack; ++} ++ ++void ++dispatchcmd(void) ++{ ++ static char buf[BUFSIZ]; ++ static char * const bend = 1[&buf]; ++ static char *bw = buf; ++ static int longline = 0; ++ ssize_t n; ++ char *nl; ++ char *pl = buf; ++ char *dend; ++ Command *i; ++ ++ n = read(fifofd, bw, bend - bw); ++ if (n == -1) ++ die("Failed to read() from DWM fifo %s:", dwmfifo); ++ dend = bw + n; ++ ++ if (longline) { ++ if (!(nl = memchr(bw, '\n', dend - bw))) ++ return; ++ bw = pl = nl + 1; ++ longline = 0; ++ } ++ ++ while ((nl = memchr(bw, '\n', dend - bw))) { ++ for (i = commands; i < 1[&commands]; i++) { ++ const char *arg; ++ ++ if (!(arg = strnprefix(pl, nl - pl, i->name))) ++ continue; ++ *nl = '\0'; ++ if (i->parse) { ++ Arg a; ++ if (i->parse(&a, arg, nl - arg)) ++ i->func(&a); ++ } else { ++ i->func(&i->arg); ++ } ++ ++ break; ++ } ++ bw = pl = nl + 1; ++ } ++ ++ memmove(buf, pl, dend - pl); ++ bw = dend - pl + buf; ++ ++ if (bw == bend) ++ bw = buf; ++} ++ + void + drawbar(Monitor *m) + { + int x, w, tw = 0; ++ int stroke = 4, letterHeight = bh - 4; + int boxs = drw->fonts->h / 9; + int boxw = drw->fonts->h / 6 + 2; + unsigned int i, occ = 0, urg = 0; +@@ -714,19 +1013,43 @@ drawbar(Monitor *m) + } + + for (c = m->clients; c; c = c->next) { +- occ |= c->tags; ++ occ |= c->tags == TAGMASK ? 0 : c->tags; + if (c->isurgent) + urg |= c->tags; + } +- x = 0; ++ ++ /* use colored scheme for visibility */ ++ drw_setscheme(drw, scheme[SchemeNorm]); ++ ++ /* draw dark background for logo */ ++ drw_rect(drw, 0, 0, dwmlogowdth, bh, 1, 1); ++ ++ /* draw dwm logo */ ++ const DwmLogo dwmLogo[] = { ++ { 12, 9, stroke, letterHeight / 2 }, /* d: left vertical */ ++ { 12, 15, 35, stroke }, /* d: bottom horizontal */ ++ { 25, 1, stroke, letterHeight }, /* d: right vertical */ ++ { 12, 7, 15, stroke }, /* d: top horizontal */ ++ { 34, 7, stroke, letterHeight / 2 }, /* w: center vertical */ ++ { 43, 7, stroke, letterHeight / 2 }, /* w: right vertical */ ++ { 43, 7, 22, stroke }, /* m: top horizontal */ ++ { 52, 11, stroke, letterHeight / 2 }, /* m: center vertical */ ++ { 61, 11, stroke, letterHeight / 2 } /* m: right vertical */ ++ }; ++ ++ for (int i = 0; i < LENGTH(dwmLogo); i++) { ++ drw_rect(drw, dwmLogo[i].x, dwmLogo[i].y, dwmLogo[i].w, dwmLogo[i].h, 1, 0); ++ } ++ ++ /* start drawing tags after logo */ ++ x = dwmlogowdth; + for (i = 0; i < LENGTH(tags); i++) { ++ /* Do not draw vacant tags */ ++ if(!(occ & 1 << i || m->tagset[m->seltags] & 1 << i)) ++ continue; + w = TEXTW(tags[i]); + drw_setscheme(drw, scheme[m->tagset[m->seltags] & 1 << i ? SchemeSel : SchemeNorm]); + drw_text(drw, x, 0, w, bh, lrpad / 2, tags[i], urg & 1 << i); +- if (occ & 1 << i) +- drw_rect(drw, x + boxs, boxs, boxw, boxw, +- m == selmon && selmon->sel && selmon->sel->tags & 1 << i, +- urg & 1 << i); + x += w; + } + w = TEXTW(m->ltsymbol); +@@ -735,7 +1058,7 @@ drawbar(Monitor *m) + + if ((w = m->ww - tw - x) > bh) { + if (m->sel) { +- drw_setscheme(drw, scheme[m == selmon ? SchemeSel : SchemeNorm]); ++ drw_setscheme(drw, scheme[SchemeNorm]); + drw_text(drw, x, 0, w, bh, lrpad / 2, m->sel->name, 0); + if (m->sel->isfloating) + drw_rect(drw, x + boxs, boxs, boxw, boxw, m->sel->isfixed, 0); +@@ -756,6 +1079,105 @@ drawbars(void) + drawbar(m); + } + ++void ++drawtabs(void) { ++ Monitor *m; ++ ++ for(m = mons; m; m = m->next) ++ drawtab(m); ++} ++ ++static int ++cmpint(const void *p1, const void *p2) { ++ /* The actual arguments to this function are "pointers to ++ pointers to char", but strcmp(3) arguments are "pointers ++ to char", hence the following cast plus dereference */ ++ return *((int*) p1) > * (int*) p2; ++} ++ ++ ++void ++drawtab(Monitor *m) { ++ Client *c; ++ int i; ++ int itag = -1; ++ char view_info[50]; ++ int view_info_w = 0; ++ int sorted_label_widths[MAXTABS]; ++ int tot_width; ++ int maxsize = bh; ++ int x = 0; ++ int w = 0; ++ ++ //view_info: indicate the tag which is displayed in the view ++ for(i = 0; i < LENGTH(tags); ++i){ ++ if((selmon->tagset[selmon->seltags] >> i) & 1) { ++ if(itag >=0){ //more than one tag selected ++ itag = -1; ++ break; ++ } ++ itag = i; ++ } ++ } ++ ++ if(0 <= itag && itag < LENGTH(tags)){ ++ snprintf(view_info, sizeof view_info, "[%s]", tags[itag]); ++ } else { ++ strncpy(view_info, "[...]", sizeof view_info); ++ } ++ view_info[sizeof(view_info) - 1 ] = 0; ++ view_info_w = TEXTW(view_info); ++ tot_width = view_info_w; ++ ++ /* Calculates number of labels and their width */ ++ m->ntabs = 0; ++ for(c = m->clients; c; c = c->next){ ++ if(!ISVISIBLE(c)) continue; ++ m->tab_widths[m->ntabs] = TEXTW(c->name); ++ tot_width += m->tab_widths[m->ntabs]; ++ ++m->ntabs; ++ if(m->ntabs >= MAXTABS) break; ++ } ++ ++ if(tot_width > m->ww){ //not enough space to display the labels, they need to be truncated ++ memcpy(sorted_label_widths, m->tab_widths, sizeof(int) * m->ntabs); ++ qsort(sorted_label_widths, m->ntabs, sizeof(int), cmpint); ++ tot_width = view_info_w; ++ for(i = 0; i < m->ntabs; ++i){ ++ if(tot_width + (m->ntabs - i) * sorted_label_widths[i] > m->ww) ++ break; ++ tot_width += sorted_label_widths[i]; ++ } ++ maxsize = (m->ww - tot_width) / (m->ntabs - i); ++ } else{ ++ maxsize = m->ww; ++ } ++ i = 0; ++ for(c = m->clients; c; c = c->next){ ++ if(!ISVISIBLE(c)) continue; ++ if(i >= m->ntabs) break; ++ if(m->tab_widths[i] > maxsize) m->tab_widths[i] = maxsize; ++ w = m->tab_widths[i]; ++ drw_setscheme(drw, scheme[(c == m->sel) ? SchemeSel : SchemeNorm]); ++ drw_text(drw, x, 0, w, th, 0, c->name, 0); ++ x += w; ++ ++i; ++ } ++ ++ drw_setscheme(drw, scheme[SchemeNorm]); ++ ++ /* cleans interspace between window names and current viewed tag label */ ++ w = m->ww - view_info_w - x; ++ drw_text(drw, x, 0, w, th, 0, "", 0); ++ ++ /* view info */ ++ x += w; ++ w = view_info_w; ++ drw_text(drw, x, 0, w, th, 0, view_info, 0); ++ ++ drw_map(drw, m->tabwin, 0, 0, m->ww, th); ++} ++ + void + enternotify(XEvent *e) + { +@@ -781,8 +1203,10 @@ expose(XEvent *e) + Monitor *m; + XExposeEvent *ev = &e->xexpose; + +- if (ev->count == 0 && (m = wintomon(ev->window))) ++ if(ev->count == 0 && (m = wintomon(ev->window))){ + drawbar(m); ++ drawtab(m); ++ } + } + + void +@@ -808,6 +1232,7 @@ focus(Client *c) + } + selmon->sel = c; + drawbars(); ++ drawtabs(); + } + + /* there are some broken focus acquiring clients needing extra handling */ +@@ -830,6 +1255,25 @@ focusmon(const Arg *arg) + if ((m = dirtomon(arg->i)) == selmon) + return; + unfocus(selmon->sel, 0); ++ XWarpPointer(dpy, None, m->barwin, 0, 0, 0, 0, m->mw / 2, m->mh / 2); ++ selmon = m; ++ focus(NULL); ++ if (selmon->sel) ++ XWarpPointer(dpy, None, selmon->sel->win, 0, 0, 0, 0, selmon->sel->w/2, selmon->sel->h/2); ++} ++ ++void ++focusnthmon(const Arg *arg) ++{ ++ Monitor *m; ++ ++ if (!mons->next) ++ return; ++ ++ if ((m = numtomon(arg->i)) == selmon) ++ return; ++ unfocus(selmon->sel, 0); ++ XWarpPointer(dpy, None, m->barwin, 0, 0, 0, 0, m->mw / 2, m->mh / 2); + selmon = m; + focus(NULL); + } +@@ -857,9 +1301,23 @@ focusstack(const Arg *arg) + if (c) { + focus(c); + restack(selmon); ++ XWarpPointer(dpy, None, c->win, 0, 0, 0, 0, c->w/2, c->h/2); + } + } + ++void ++focuswin(const Arg* arg){ ++ int iwin = arg->i; ++ Client* c = NULL; ++ for(c = selmon->clients; c && (iwin || !ISVISIBLE(c)) ; c = c->next){ ++ if(ISVISIBLE(c)) --iwin; ++ }; ++ if(c) { ++ focus(c); ++ restack(selmon); ++ } ++} ++ + Atom + getatomprop(Client *c, Atom prop) + { +@@ -979,7 +1437,7 @@ grabkeys(void) + void + incnmaster(const Arg *arg) + { +- selmon->nmaster = MAX(selmon->nmaster + arg->i, 0); ++ selmon->nmaster = selmon->pertag->nmasters[selmon->pertag->curtag] = MAX(selmon->nmaster + arg->i, 0); + arrange(selmon); + } + +@@ -1067,6 +1525,8 @@ manage(Window w, XWindowAttributes *wa) + updatewindowtype(c); + updatesizehints(c); + updatewmhints(c); ++ c->x = c->mon->mx + (c->mon->mw - WIDTH(c)) / 2; ++ c->y = c->mon->my + (c->mon->mh - HEIGHT(c)) / 2; + XSelectInput(dpy, w, EnterWindowMask|FocusChangeMask|PropertyChangeMask|StructureNotifyMask); + grabbuttons(c, 0); + if (!c->isfloating) +@@ -1084,6 +1544,8 @@ manage(Window w, XWindowAttributes *wa) + c->mon->sel = c; + arrange(c->mon); + XMapWindow(dpy, c->win); ++ if (c && c->mon == selmon) ++ XWarpPointer(dpy, None, c->win, 0, 0, 0, 0, c->w/2, c->h/2); + focus(NULL); + } + +@@ -1121,7 +1583,7 @@ monocle(Monitor *m) + if (n > 0) /* override layout symbol */ + snprintf(m->ltsymbol, sizeof m->ltsymbol, "[%d]", n); + for (c = nexttiled(m->clients); c; c = nexttiled(c->next)) +- resize(c, m->wx, m->wy, m->ww - 2 * c->bw, m->wh - 2 * c->bw, 0); ++ resize(c, m->wx, m->wy, m->ww, m->wh, 0, 0); + } + + void +@@ -1189,7 +1651,7 @@ movemouse(const Arg *arg) + && (abs(nx - c->x) > snap || abs(ny - c->y) > snap)) + togglefloating(NULL); + if (!selmon->lt[selmon->sellt]->arrange || c->isfloating) +- resize(c, nx, ny, c->w, c->h, 1); ++ resize(c, nx, ny, c->w, c->h, c->bw, 1); + break; + } + } while (ev.type != ButtonRelease); +@@ -1201,6 +1663,29 @@ movemouse(const Arg *arg) + } + } + ++unsigned int ++nexttag(void) ++{ ++ unsigned int seltag = selmon->tagset[selmon->seltags]; ++ unsigned int usedtags = 0; ++ Client *c = selmon->clients; ++ ++ if (!c) ++ return seltag; ++ ++ /* skip vacant tags */ ++ do { ++ usedtags |= c->tags; ++ c = c->next; ++ } while (c); ++ ++ do { ++ seltag = seltag == (1 << (LENGTH(tags) - 1)) ? 1 : seltag << 1; ++ } while (!(seltag & usedtags)); ++ ++ return seltag; ++} ++ + Client * + nexttiled(Client *c) + { +@@ -1217,6 +1702,28 @@ pop(Client *c) + arrange(c->mon); + } + ++unsigned int ++prevtag(void) ++{ ++ unsigned int seltag = selmon->tagset[selmon->seltags]; ++ unsigned int usedtags = 0; ++ Client *c = selmon->clients; ++ if (!c) ++ return seltag; ++ ++ /* skip vacant tags */ ++ do { ++ usedtags |= c->tags; ++ c = c->next; ++ } while (c); ++ ++ do { ++ seltag = seltag == 1 ? (1 << (LENGTH(tags) - 1)) : seltag >> 1; ++ } while (!(seltag & usedtags)); ++ ++ return seltag; ++} ++ + void + propertynotify(XEvent *e) + { +@@ -1242,12 +1749,14 @@ propertynotify(XEvent *e) + case XA_WM_HINTS: + updatewmhints(c); + drawbars(); ++ drawtabs(); + break; + } + if (ev->atom == XA_WM_NAME || ev->atom == netatom[NetWMName]) { + updatetitle(c); + if (c == c->mon->sel) + drawbar(c->mon); ++ drawtab(c->mon); + } + if (ev->atom == netatom[NetWMWindowType]) + updatewindowtype(c); +@@ -1275,14 +1784,14 @@ recttomon(int x, int y, int w, int h) + } + + void +-resize(Client *c, int x, int y, int w, int h, int interact) ++resize(Client *c, int x, int y, int w, int h, int bw, int interact) + { +- if (applysizehints(c, &x, &y, &w, &h, interact)) +- resizeclient(c, x, y, w, h); ++ if (applysizehints(c, &x, &y, &w, &h, &bw, interact)) ++ resizeclient(c, x, y, w, h, bw); + } + + void +-resizeclient(Client *c, int x, int y, int w, int h) ++resizeclient(Client *c, int x, int y, int w, int h, int bw) + { + XWindowChanges wc; + +@@ -1290,7 +1799,7 @@ resizeclient(Client *c, int x, int y, int w, int h) + c->oldy = c->y; c->y = wc.y = y; + c->oldw = c->w; c->w = wc.width = w; + c->oldh = c->h; c->h = wc.height = h; +- wc.border_width = c->bw; ++ c->oldbw = c->bw; c->bw = wc.border_width = bw; + XConfigureWindow(dpy, c->win, CWX|CWY|CWWidth|CWHeight|CWBorderWidth, &wc); + configure(c); + XSync(dpy, False); +@@ -1339,7 +1848,7 @@ resizemouse(const Arg *arg) + togglefloating(NULL); + } + if (!selmon->lt[selmon->sellt]->arrange || c->isfloating) +- resize(c, c->x, c->y, nw, nh, 1); ++ resize(c, c->x, c->y, nw, nh, c->bw, 1); + break; + } + } while (ev.type != ButtonRelease); +@@ -1361,6 +1870,7 @@ restack(Monitor *m) + XWindowChanges wc; + + drawbar(m); ++ drawtab(m); + if (!m->sel) + return; + if (m->sel->isfloating || !m->lt[m->sellt]->arrange) +@@ -1378,15 +1888,31 @@ restack(Monitor *m) + while (XCheckMaskEvent(dpy, EnterWindowMask, &ev)); + } + ++static Bool evpredicate() ++{ ++ return True; ++} ++ + void + run(void) + { + XEvent ev; ++ struct pollfd fds[2] = { ++ { .events = POLLIN }, ++ { .fd = fifofd, .events = POLLIN } ++ }; + /* main event loop */ + XSync(dpy, False); +- while (running && !XNextEvent(dpy, &ev)) +- if (handler[ev.type]) +- handler[ev.type](&ev); /* call handler */ ++ fds[0].fd = ConnectionNumber(dpy); ++ while (running) { ++ (void)poll(fds, 1[&fds] - fds, -1); ++ if (fds[1].revents & POLLIN) ++ dispatchcmd(); ++ if (fds[0].revents & POLLIN) ++ while (XCheckIfEvent(dpy, &ev, evpredicate, NULL)) ++ if (handler[ev.type]) ++ handler[ev.type](&ev); /* call handler */ ++ } + } + + void +@@ -1486,33 +2012,41 @@ setfullscreen(Client *c, int fullscreen) + PropModeReplace, (unsigned char*)&netatom[NetWMFullscreen], 1); + c->isfullscreen = 1; + c->oldstate = c->isfloating; +- c->oldbw = c->bw; +- c->bw = 0; + c->isfloating = 1; +- resizeclient(c, c->mon->mx, c->mon->my, c->mon->mw, c->mon->mh); ++ resizeclient(c, c->mon->mx, c->mon->my, c->mon->mw, c->mon->mh, 0); + XRaiseWindow(dpy, c->win); + } else if (!fullscreen && c->isfullscreen){ + XChangeProperty(dpy, c->win, netatom[NetWMState], XA_ATOM, 32, + PropModeReplace, (unsigned char*)0, 0); + c->isfullscreen = 0; + c->isfloating = c->oldstate; +- c->bw = c->oldbw; + c->x = c->oldx; + c->y = c->oldy; + c->w = c->oldw; + c->h = c->oldh; +- resizeclient(c, c->x, c->y, c->w, c->h); ++ c->bw = c->oldbw; ++ resizeclient(c, c->x, c->y, c->w, c->h, c->bw); + arrange(c->mon); + } + } + ++void ++setgaps(const Arg *arg) ++{ ++ if ((arg->i == 0) || (selmon->gappx + arg->i < 0)) ++ selmon->gappx = 0; ++ else ++ selmon->gappx += arg->i; ++ arrange(selmon); ++} ++ + void + setlayout(const Arg *arg) + { + if (!arg || !arg->v || arg->v != selmon->lt[selmon->sellt]) +- selmon->sellt ^= 1; ++ selmon->sellt = selmon->pertag->sellts[selmon->pertag->curtag] ^= 1; + if (arg && arg->v) +- selmon->lt[selmon->sellt] = (Layout *)arg->v; ++ selmon->lt[selmon->sellt] = selmon->pertag->ltidxs[selmon->pertag->curtag][selmon->sellt] = (Layout *)arg->v; + strncpy(selmon->ltsymbol, selmon->lt[selmon->sellt]->symbol, sizeof selmon->ltsymbol); + if (selmon->sel) + arrange(selmon); +@@ -1531,7 +2065,7 @@ setmfact(const Arg *arg) + f = arg->f < 1.0 ? arg->f + selmon->mfact : arg->f - 1.0; + if (f < 0.05 || f > 0.95) + return; +- selmon->mfact = f; ++ selmon->mfact = selmon->pertag->mfacts[selmon->pertag->curtag] = f; + arrange(selmon); + } + +@@ -1562,6 +2096,7 @@ setup(void) + die("no fonts could be loaded."); + lrpad = drw->fonts->h; + bh = drw->fonts->h + 2; ++ th = bh; + updategeom(); + /* init atoms */ + utf8string = XInternAtom(dpy, "UTF8_STRING", False); +@@ -1579,9 +2114,9 @@ setup(void) + netatom[NetWMWindowTypeDialog] = XInternAtom(dpy, "_NET_WM_WINDOW_TYPE_DIALOG", False); + netatom[NetClientList] = XInternAtom(dpy, "_NET_CLIENT_LIST", False); + /* init cursors */ +- cursor[CurNormal] = drw_cur_create(drw, XC_left_ptr); +- cursor[CurResize] = drw_cur_create(drw, XC_sizing); +- cursor[CurMove] = drw_cur_create(drw, XC_fleur); ++ cursor[CurNormal] = drw_cur_create(drw, "left_ptr"); ++ cursor[CurResize] = drw_cur_create(drw, "se-resize"); ++ cursor[CurMove] = drw_cur_create(drw, "fleur"); + /* init appearance */ + scheme = ecalloc(LENGTH(colors), sizeof(Clr *)); + for (i = 0; i < LENGTH(colors); i++) +@@ -1610,6 +2145,9 @@ setup(void) + XSelectInput(dpy, root, wa.event_mask); + grabkeys(); + focus(NULL); ++ fifofd = open(dwmfifo, O_RDWR | O_CLOEXEC | O_NONBLOCK); ++ if (fifofd < 0) ++ die("Failed to open() DWM fifo %s:", dwmfifo); + } + + void +@@ -1634,7 +2172,7 @@ showhide(Client *c) + /* show clients top down */ + XMoveWindow(dpy, c->win, c->x, c->y); + if ((!c->mon->lt[c->mon->sellt]->arrange || c->isfloating) && !c->isfullscreen) +- resize(c, c->x, c->y, c->w, c->h, 0); ++ resize(c, c->x, c->y, c->w, c->h, c->bw, 0); + showhide(c->snext); + } else { + /* hide clients bottom up */ +@@ -1683,43 +2221,96 @@ tagmon(const Arg *arg) + sendmon(selmon->sel, dirtomon(arg->i)); + } + ++void ++tagtonext(const Arg *arg) ++{ ++ unsigned int tmp; ++ ++ if (selmon->sel == NULL) ++ return; ++ ++ if ((tmp = nexttag()) == selmon->tagset[selmon->seltags]) ++ return; ++ ++ tag(&(const Arg){.ui = tmp }); ++ view(&(const Arg){.ui = tmp }); ++} ++ ++void ++tagtoprev(const Arg *arg) ++{ ++ unsigned int tmp; ++ ++ if (selmon->sel == NULL) ++ return; ++ ++ if ((tmp = prevtag()) == selmon->tagset[selmon->seltags]) ++ return; ++ ++ tag(&(const Arg){.ui = tmp }); ++ view(&(const Arg){.ui = tmp }); ++} ++ ++void ++tagnthmon(const Arg *arg) ++{ ++ if (!selmon->sel || !mons->next) ++ return; ++ sendmon(selmon->sel, numtomon(arg->i)); ++} ++ + void + tile(Monitor *m) + { +- unsigned int i, n, h, mw, my, ty; ++ unsigned int i, n, h, mw, my, ty, bw; + Client *c; + + for (n = 0, c = nexttiled(m->clients); c; c = nexttiled(c->next), n++); + if (n == 0) + return; + ++ if (n == 1) ++ bw = 0; ++ else ++ bw = borderpx; + if (n > m->nmaster) + mw = m->nmaster ? m->ww * m->mfact : 0; + else +- mw = m->ww; +- for (i = my = ty = 0, c = nexttiled(m->clients); c; c = nexttiled(c->next), i++) +- if (i < m->nmaster) { +- h = (m->wh - my) / (MIN(n, m->nmaster) - i); +- resize(c, m->wx, m->wy + my, mw - (2*c->bw), h - (2*c->bw), 0); +- if (my + HEIGHT(c) < m->wh) +- my += HEIGHT(c); ++ mw = m->ww - m->gappx; ++ for (i = 0, my = ty = m->gappx, c = nexttiled(m->clients); c; c = nexttiled(c->next), i++) ++ if (i < m->nmaster) { ++ h = (m->wh - my) / (MIN(n, m->nmaster) - i) - m->gappx; ++ resize(c, m->wx + m->gappx, m->wy + my, mw - 2*bw - m->gappx, h - 2*bw, bw, 0); ++ if (my + HEIGHT(c) + m->gappx < m->wh) ++ my += HEIGHT(c) + m->gappx; + } else { +- h = (m->wh - ty) / (n - i); +- resize(c, m->wx + mw, m->wy + ty, m->ww - mw - (2*c->bw), h - (2*c->bw), 0); +- if (ty + HEIGHT(c) < m->wh) +- ty += HEIGHT(c); ++ h = (m->wh - ty) / (n - i) - m->gappx; ++ resize(c, m->wx + mw + m->gappx, m->wy + ty, m->ww - mw - 2*bw - 2*m->gappx, h - 2*bw, bw, 0); ++ if (ty + HEIGHT(c) + m->gappx < m->wh) ++ ty += HEIGHT(c) + m->gappx; + } + } + + void + togglebar(const Arg *arg) + { +- selmon->showbar = !selmon->showbar; ++ selmon->showbar = /* selmon->pertag->showbars[selmon->pertag->curtag] = */ (selmon->showbar == 2 ? 1 : !selmon->showbar); + updatebarpos(selmon); + XMoveResizeWindow(dpy, selmon->barwin, selmon->wx, selmon->by, selmon->ww, bh); + arrange(selmon); + } + ++void ++tabmode(const Arg *arg) ++{ ++ if(arg && arg->i >= 0) ++ selmon->showtab = arg->ui % showtab_nmodes; ++ else ++ selmon->showtab = (selmon->showtab + 1 ) % showtab_nmodes; ++ arrange(selmon); ++} ++ ++ + void + togglefloating(const Arg *arg) + { +@@ -1730,10 +2321,19 @@ togglefloating(const Arg *arg) + selmon->sel->isfloating = !selmon->sel->isfloating || selmon->sel->isfixed; + if (selmon->sel->isfloating) + resize(selmon->sel, selmon->sel->x, selmon->sel->y, +- selmon->sel->w, selmon->sel->h, 0); ++ selmon->sel->w - 2 * (borderpx - selmon->sel->bw), ++ selmon->sel->h - 2 * (borderpx - selmon->sel->bw), ++ borderpx, 0); + arrange(selmon); + } + ++void ++togglefullscr(const Arg *arg) ++{ ++ if(selmon->sel) ++ setfullscreen(selmon->sel, !selmon->sel->isfullscreen); ++} ++ + void + toggletag(const Arg *arg) + { +@@ -1753,9 +2353,33 @@ void + toggleview(const Arg *arg) + { + unsigned int newtagset = selmon->tagset[selmon->seltags] ^ (arg->ui & TAGMASK); ++ int i; + + if (newtagset) { + selmon->tagset[selmon->seltags] = newtagset; ++ ++ if (newtagset == ~0) { ++ selmon->pertag->prevtag = selmon->pertag->curtag; ++ selmon->pertag->curtag = 0; ++ } ++ ++ /* test if the user did not select the same tag */ ++ if (!(newtagset & 1 << (selmon->pertag->curtag - 1))) { ++ selmon->pertag->prevtag = selmon->pertag->curtag; ++ for (i = 0; !(newtagset & 1 << i); i++) ; ++ selmon->pertag->curtag = i + 1; ++ } ++ ++ /* apply settings for this view */ ++ selmon->nmaster = selmon->pertag->nmasters[selmon->pertag->curtag]; ++ selmon->mfact = selmon->pertag->mfacts[selmon->pertag->curtag]; ++ selmon->sellt = selmon->pertag->sellts[selmon->pertag->curtag]; ++ selmon->lt[selmon->sellt] = selmon->pertag->ltidxs[selmon->pertag->curtag][selmon->sellt]; ++ selmon->lt[selmon->sellt^1] = selmon->pertag->ltidxs[selmon->pertag->curtag][selmon->sellt^1]; ++ ++ /* if (selmon->showbar != selmon->pertag->showbars[selmon->pertag->curtag]) */ ++ /* togglebar(NULL); */ ++ + focus(NULL); + arrange(selmon); + } +@@ -1798,6 +2422,9 @@ unmanage(Client *c, int destroyed) + focus(NULL); + updateclientlist(); + arrange(m); ++ if (m == selmon && m->sel) ++ XWarpPointer(dpy, None, m->sel->win, 0, 0, 0, 0, ++ m->sel->w/2, m->sel->h/2); + } + + void +@@ -1832,6 +2459,11 @@ updatebars(void) + CWOverrideRedirect|CWBackPixmap|CWEventMask, &wa); + XDefineCursor(dpy, m->barwin, cursor[CurNormal]->cursor); + XMapRaised(dpy, m->barwin); ++ m->tabwin = XCreateWindow(dpy, root, m->wx, m->ty, m->ww, th, 0, DefaultDepth(dpy, screen), ++ CopyFromParent, DefaultVisual(dpy, screen), ++ CWOverrideRedirect|CWBackPixmap|CWEventMask, &wa); ++ XDefineCursor(dpy, m->tabwin, cursor[CurNormal]->cursor); ++ XMapRaised(dpy, m->tabwin); + XSetClassHint(dpy, m->barwin, &ch); + } + } +@@ -1839,14 +2471,33 @@ updatebars(void) + void + updatebarpos(Monitor *m) + { ++ Client *c; ++ int nvis = 0; ++ + m->wy = m->my; + m->wh = m->mh; + if (m->showbar) { + m->wh -= bh; + m->by = m->topbar ? m->wy : m->wy + m->wh; +- m->wy = m->topbar ? m->wy + bh : m->wy; +- } else ++ if ( m->topbar ) ++ m->wy += bh; ++ } else { + m->by = -bh; ++ } ++ ++ for(c = m->clients; c; c = c->next) { ++ if(ISVISIBLE(c)) ++nvis; ++ } ++ ++ if(m->showtab == showtab_always ++ || ((m->showtab == showtab_auto) && (nvis > 1) && (m->lt[m->sellt]->arrange == monocle))) { ++ m->wh -= th; ++ m->ty = m->toptab ? m->wy : m->wy + m->wh; ++ if ( m->toptab ) ++ m->wy += th; ++ } else { ++ m->ty = -th; ++ } + } + + void +@@ -2052,15 +2703,96 @@ updatewmhints(Client *c) + void + view(const Arg *arg) + { ++ int i; ++ unsigned int tmptag; ++ + if ((arg->ui & TAGMASK) == selmon->tagset[selmon->seltags]) + return; + selmon->seltags ^= 1; /* toggle sel tagset */ +- if (arg->ui & TAGMASK) ++ if (arg->ui & TAGMASK) { + selmon->tagset[selmon->seltags] = arg->ui & TAGMASK; ++ selmon->pertag->prevtag = selmon->pertag->curtag; ++ ++ if (arg->ui == ~0) ++ selmon->pertag->curtag = 0; ++ else { ++ for (i = 0; !(arg->ui & 1 << i); i++) ; ++ selmon->pertag->curtag = i + 1; ++ } ++ } else { ++ tmptag = selmon->pertag->prevtag; ++ selmon->pertag->prevtag = selmon->pertag->curtag; ++ selmon->pertag->curtag = tmptag; ++ } ++ ++ selmon->nmaster = selmon->pertag->nmasters[selmon->pertag->curtag]; ++ selmon->mfact = selmon->pertag->mfacts[selmon->pertag->curtag]; ++ selmon->sellt = selmon->pertag->sellts[selmon->pertag->curtag]; ++ selmon->lt[selmon->sellt] = selmon->pertag->ltidxs[selmon->pertag->curtag][selmon->sellt]; ++ selmon->lt[selmon->sellt^1] = selmon->pertag->ltidxs[selmon->pertag->curtag][selmon->sellt^1]; ++ ++ /* if (selmon->showbar != selmon->pertag->showbars[selmon->pertag->curtag]) */ ++ /* togglebar(NULL); */ ++ + focus(NULL); + arrange(selmon); + } + ++void ++viewclient(Client *c) ++{ ++ if (!(c->tags & c->mon->tagset[c->mon->seltags])) ++ view(&(Arg){ .ui = c->tags }); ++ focus(c); ++} ++ ++void ++viewwin(const Arg *arg) ++{ ++ Client *c = wintoclient((Window)(intptr_t)arg->v); ++ ++ if (!c) ++ return; ++ ++ viewclient(c); ++} ++ ++Client * ++pattoclient(const char *pattern) ++{ ++ Client *c; ++ Monitor *m; ++ ++ for (m = mons; m; m = m->next) ++ for (c = m->clients; c; c = c->next) ++ if (!fnmatch(pattern, c->name, 0)) ++ return c; ++ return NULL; ++} ++ ++void ++viewname(const Arg *arg) ++{ ++ Client *c = pattoclient(arg->v); ++ ++ if (!c) ++ return; ++ ++ viewclient(c); ++} ++ ++void ++viewnext(const Arg *arg) ++{ ++ view(&(const Arg){.ui = nexttag()}); ++} ++ ++void ++viewprev(const Arg *arg) ++{ ++ view(&(const Arg){.ui = prevtag()}); ++} ++ + Client * + wintoclient(Window w) + { +@@ -2084,7 +2816,7 @@ wintomon(Window w) + if (w == root && getrootptr(&x, &y)) + return recttomon(x, y, 1, 1); + for (m = mons; m; m = m->next) +- if (w == m->barwin) ++ if (w == m->barwin || w == m->tabwin) + return m; + if ((c = wintoclient(w))) + return c->mon; +@@ -2139,6 +2871,38 @@ zoom(const Arg *arg) + pop(c); + } + ++void ++swapmon(const Arg *arg) ++{ ++ if (mons->next == NULL) ++ return; ++ ++ Monitor *m1 = mons; ++ Monitor *m2 = mons->next; ++ ++ unsigned int tmp = m1->tagset[m1->seltags]; ++ m1->tagset[m1->seltags] = m2->tagset[m2->seltags]; ++ m2->tagset[m2->seltags] = tmp; ++ ++ Client *c; ++ for (c = m1->clients; c; c = c->next) ++ c->mon = m2; ++ for (c = m2->clients; c; c = c->next) ++ c->mon = m1; ++ ++ Client *tmp_clients = m1->clients; ++ m1->clients = m2->clients; ++ m2->clients = tmp_clients; ++ ++ Client *tmp_stack = m1->stack; ++ m1->stack = m2->stack; ++ m2->stack = tmp_stack; ++ ++ focus(NULL); ++ arrange(m1); ++ arrange(m2); ++} ++ + int + main(int argc, char *argv[]) + { diff --git a/Suckless/dwm/dwm.nix b/Suckless/dwm/dwm.nix new file mode 100644 index 0000000..8dad3e7 --- /dev/null +++ b/Suckless/dwm/dwm.nix @@ -0,0 +1,100 @@ +# dwm window manager configuration +# +# ============================================================================= +# Patch Development Notes +# ============================================================================= +# +# Base: dwm 6.6 (matches nixpkgs) +# Clone: ~/NixOS/dwm +# +# Clean & Build Commands: +# cd ~/NixOS/dwm +# git reset --hard 6.6 +# rm -f config.h drw.o dwm dwm.o util.o +# nix-shell -p xorg.libX11 xorg.libXft xorg.libXinerama xorg.libXcursor pkg-config gnumake gcc +# make clean && make +# +# Generate patch after all patches applied: +# git diff 6.6 > ~/NixOS/Suckless/dwm/dwm-lebowski.patch +# +# ============================================================================= +# Patches (apply in order): +# ============================================================================= +# +# 1. pertag - Per-tag layouts (applied separately, then tab) +# 2. tab - Tabbed monocle bar +# 3. fullgaps - Simple gaps between windows +# 4. holdbar-modkey - Show bar while holding mod key +# 5. hide_vacant_tags - Only show occupied tags +# 6. bardwmlogo - DWM logo in bar +# 7. cursorwarp - Cursor follows focus +# 8. alwayscenter - Float windows spawn centered +# 9. swapmonitors - Swap tagsets between monitors +# 10. adjacenttag - Navigate tags with arrows (skipvacant) +# 11. accessnthmonitor - Focus/send to specific monitor by number +# 12. dwmfifo - Control dwm via named pipe +# 13. xcursor - Use system Xcursor theme (Stylix) +# 14. actualfullscreen +# +# Custom additions: +# - def_layouts array for per-tag default layouts +# +# ============================================================================= + +{ + config, + pkgs, + lib, + ... +}: + +let + appearance = import ./appearance.nix { inherit config lib; }; + tags = import ./tags.nix { inherit lib; }; + rules = import ./rules.nix { inherit lib; }; + layouts = import ./layouts.nix { inherit lib; }; + keybindings = import ./keybindings.nix { inherit lib; }; + api = import ./api.nix { inherit lib; }; + + configDefH = '' + ${appearance.config} + + ${tags.config} + + ${rules.config} + + ${layouts.config} + + ${keybindings.config} + + ${api.config} + ''; +in +{ + services.xserver.windowManager.dwm = { + enable = true; + package = + (pkgs.dwm.override { + conf = configDefH; + }).overrideAttrs + (old: { + patches = [ ./dwm-lebowski.patch ]; + buildInputs = old.buildInputs ++ [ pkgs.xorg.libXcursor ]; + }); + }; + + environment.systemPackages = [ + (pkgs.writeShellScriptBin "vieworspawn" '' + mon=$1; tag=$2; class=$3; shift 3 + # Focus monitor if multi-monitor setup (0=secondary, 1=primary) + if [[ $(${pkgs.autorandr}/bin/autorandr --detected) != "mobile" ]]; then + [[ "$mon" == "0" ]] && echo "mon-sec" > /tmp/dwm.fifo || echo "mon-prim" > /tmp/dwm.fifo + fi + echo "view $tag" > /tmp/dwm.fifo + sleep 0.02 + if ! ${pkgs.xdotool}/bin/xdotool search --class "$class" >/dev/null 2>&1; then + exec "$@" + fi + '') + ]; +} diff --git a/Suckless/dwm/keybindings.nix b/Suckless/dwm/keybindings.nix new file mode 100644 index 0000000..8302b16 --- /dev/null +++ b/Suckless/dwm/keybindings.nix @@ -0,0 +1,181 @@ +# dwm keybindings +{ lib }: + +{ + config = '' + /* key definitions */ + #define MODKEY Mod4Mask + #define TAGKEYS(KEY,TAG) \ + { MODKEY, KEY, view, {.ui = 1 << TAG} }, \ + { MODKEY|ControlMask, KEY, toggleview, {.ui = 1 << TAG} }, \ + { MODKEY|ShiftMask, KEY, tag, {.ui = 1 << TAG} }, \ + { MODKEY|ControlMask|ShiftMask, KEY, toggletag, {.ui = 1 << TAG} }, + #define HOLDKEY 0xffeb // 0 - disable; 0xffe9 - Mod1Mask; 0xffeb - Mod4Mask (Super) + + /* helper for spawning shell commands in the pre dwm-5.0 fashion */ + #define SHCMD(cmd) { .v = (const char*[]){ "/bin/sh", "-c", cmd, NULL } } + + /* commands */ + static char dmenumon[2] = "0"; /* component of dmenucmd, manipulated in spawn() */ + static const char *dmenucmd[] = { "dmenu_run", "-m", dmenumon, "-fn", dmenufont, "-nb", col_gray1, "-nf", col_gray3, "-sb", col_cyan, "-sf", col_gray4, NULL }; + static const char *roficmd[] = { "rofi", "-show", "system", NULL }; + static const char *rofiwebcmd[] = { "rofi-websearch", NULL }; + static const char *browsercmd[] = { "zen-twilight", NULL }; + static const char *lockcmd[] = { "slock", NULL }; + static const char *kicadshowcmd[] = { "kicad-show", NULL }; + static const char *kicadprojectscmd[] = { "kicad-projects", NULL }; + static const char *kicadlibcmd[] = { "kicad-lib-launch", NULL }; + static const char *kicadswapcmd[] = { "kicad-swap", NULL }; + static const char *kicadcyclefcmd[] = { "kicad-cycle", "f", NULL }; + static const char *kicadcyclebcmd[] = { "kicad-cycle", "b", NULL }; + static const char *screenshotcmd[] = { "flameshot", "gui", NULL }; + static const char *screenshotfullcmd[] = { "flameshot", "full", "--path", "/home/lebowski/Pictures/Screenshots", NULL }; + static const char *cliphistcmd[] = { "sh", "-c", "greenclip print | rofi -dmenu -p Clipboard | xclip -selection clipboard", NULL }; + static const char *volmutecmd[] = { "wpctl", "set-mute", "@DEFAULT_AUDIO_SINK@", "toggle", NULL }; + static const char *micmutecmd[] = { "wpctl", "set-mute", "@DEFAULT_AUDIO_SOURCE@", "toggle", NULL }; + static const char *volupcmd[] = { "sh", "-c", "wpctl set-volume -l 1.0 @DEFAULT_AUDIO_SINK@ 5%+", NULL }; + static const char *voldowncmd[] = { "wpctl", "set-volume", "@DEFAULT_AUDIO_SINK@", "5%-", NULL }; + + /* vieworspawn: monitor, tag (1-based), class, cmd... */ + static const char *termvos[] = { "vieworspawn", "0", "10", "st-256color", "st", NULL }; + static const char *filesvos[] = { "vieworspawn", "0", "11", "Thunar", "thunar", NULL }; + static const char *editorvos[] = { "vieworspawn", "1", "14", "dev.zed.Zed", "zeditor", NULL }; + static const char *freecadvos[] = { "vieworspawn", "1", "15", "FreeCAD", "FreeCAD", "--single-instance", NULL }; + static const char *bambuvos[] = { "vieworspawn", "1", "19", "BambuStudio", "bambu-studio", NULL }; + + /* fallback spawn commands */ + static const char *termcmd[] = { "st", NULL }; + static const char *filescmd[] = { "thunar", NULL }; + static const char *editorcmd[] = { "zeditor", NULL }; + static const char *freecadcmd[] = { "FreeCAD", "--single-instance", NULL }; + static const char *bambucmd[] = { "bambu-studio", NULL }; + + static const Key keys[] = { + /* modifier key function argument */ + + /* Rofi */ + { MODKEY, XK_space, spawn, {.v = roficmd } }, + { MODKEY|Mod1Mask, XK_space, spawn, {.v = rofiwebcmd } }, + + /* Terminal */ + { MODKEY, XK_a, spawn, {.v = termvos } }, + { MODKEY|ShiftMask, XK_a, spawn, {.v = termcmd } }, + + /* Zed (code) */ + { MODKEY, XK_z, spawn, {.v = editorvos } }, + { MODKEY|ShiftMask, XK_z, spawn, {.v = editorcmd } }, + + /* Zen Browser (web) */ + { MODKEY, XK_x, view, {.ui = 1 << 12 } }, + { MODKEY|ShiftMask, XK_x, spawn, {.v = browsercmd } }, + + /* Thunar (files) */ + { MODKEY, XK_n, spawn, {.v = filesvos } }, + { MODKEY|ShiftMask, XK_n, spawn, {.v = filescmd } }, + + /* Video */ + { MODKEY, XK_m, view, {.ui = 1 << 11 } }, + + /* Lock */ + { MODKEY, XK_Escape, spawn, {.v = lockcmd } }, + + /* KiCad */ + { MODKEY, XK_k, spawn, {.v = kicadshowcmd } }, + { MODKEY|ShiftMask, XK_k, spawn, {.v = kicadprojectscmd } }, + { MODKEY, XK_l, spawn, {.v = kicadlibcmd } }, + { MODKEY|ControlMask, XK_k, view, {.ui = 1 << 15 } }, + { MODKEY|Mod1Mask, XK_k, spawn, {.v = kicadswapcmd } }, + { MODKEY, XK_bracketright, spawn, {.v = kicadcyclefcmd } }, + { MODKEY, XK_bracketleft, spawn, {.v = kicadcyclebcmd } }, + + /* FreeCAD */ + { MODKEY, XK_f, spawn, {.v = freecadvos } }, + { MODKEY|ShiftMask, XK_f, spawn, {.v = freecadcmd } }, + + /* Bambu Studio */ + { MODKEY, XK_p, spawn, {.v = bambuvos } }, + { MODKEY|ShiftMask, XK_p, spawn, {.v = bambucmd } }, + + /* Window management */ + { MODKEY, XK_q, killclient, {0} }, + { MODKEY, XK_e, togglefloating, {0} }, + { MODKEY, XK_w, togglefullscr, {0} }, + { MODKEY, XK_t, tabmode, {-1} }, + + /* Focus movement - stack */ + { MODKEY, XK_j, focusstack, {.i = +1 } }, + { MODKEY|ShiftMask, XK_j, focusstack, {.i = -1 } }, + + /* Adjacent tags - Up/Down */ + { MODKEY, XK_Up, viewprev, {0} }, + { MODKEY, XK_Down, viewnext, {0} }, + { MODKEY|ShiftMask, XK_Up, tagtoprev, {0} }, + { MODKEY|ShiftMask, XK_Down, tagtonext, {0} }, + + /* Move to monitor */ + { MODKEY, XK_Left, focusnthmon, {.i = 1 } }, + { MODKEY, XK_Right, focusnthmon, {.i = 0 } }, + { MODKEY|ShiftMask, XK_Left, tagnthmon, {.i = 1 } }, + { MODKEY|ShiftMask, XK_Right, tagnthmon, {.i = 0 } }, + + /* Swap monitors */ + { MODKEY|ShiftMask, XK_apostrophe, swapmon, {0} }, + + /* Resize master */ + { MODKEY|Mod1Mask, XK_Left, setmfact, {.f = -0.05} }, + { MODKEY|Mod1Mask, XK_Right, setmfact, {.f = +0.05} }, + + /* Tags */ + TAGKEYS( XK_1, 0) + TAGKEYS( XK_2, 1) + TAGKEYS( XK_3, 2) + TAGKEYS( XK_4, 3) + TAGKEYS( XK_5, 4) + TAGKEYS( XK_6, 5) + TAGKEYS( XK_7, 6) + TAGKEYS( XK_8, 7) + TAGKEYS( XK_9, 8) + { MODKEY, XK_0, view, {.ui = ~0 } }, + { MODKEY|ShiftMask, XK_0, tag, {.ui = ~0 } }, + + /* Clipboard */ + { MODKEY|ShiftMask, XK_v, spawn, {.v = cliphistcmd } }, + + /* Screenshots */ + { 0, XK_Print, spawn, {.v = screenshotcmd } }, + { ShiftMask, XK_Print, spawn, {.v = screenshotfullcmd } }, + + /* Media keys */ + { 0, XF86XK_AudioMute, spawn, {.v = volmutecmd } }, + { 0, XF86XK_AudioMicMute, spawn, {.v = micmutecmd } }, + { 0, XF86XK_AudioRaiseVolume, spawn, {.v = volupcmd } }, + { 0, XF86XK_AudioLowerVolume, spawn, {.v = voldowncmd } }, + + /* Holdbar */ + { 0, HOLDKEY, holdbar, {0} }, + }; + + /* button definitions */ + /* click can be ClkTagBar, ClkLtSymbol, ClkStatusText, ClkWinTitle, ClkClientWin, or ClkRootWin */ + static const Button buttons[] = { + /* click event mask button function argument */ + { ClkLtSymbol, 0, Button1, setlayout, {0} }, + { ClkLtSymbol, 0, Button3, setlayout, {.v = &layouts[2]} }, + { ClkWinTitle, 0, Button2, zoom, {0} }, + { ClkStatusText, 0, Button2, spawn, {.v = termcmd } }, + { ClkClientWin, MODKEY, Button1, movemouse, {0} }, + { ClkClientWin, MODKEY, Button2, togglefloating, {0} }, + { ClkClientWin, MODKEY, Button3, resizemouse, {0} }, + { ClkTagBar, 0, Button1, view, {0} }, + { ClkTagBar, 0, Button3, toggleview, {0} }, + { ClkTagBar, MODKEY, Button1, tag, {0} }, + { ClkTagBar, MODKEY, Button3, toggletag, {0} }, + { ClkTabBar, 0, Button1, focuswin, {0} }, + /* Scroll through tags with Mod+scroll */ + { ClkRootWin, MODKEY, Button4, viewprev, {0} }, + { ClkRootWin, MODKEY, Button5, viewnext, {0} }, + { ClkClientWin, MODKEY, Button4, viewprev, {0} }, + { ClkClientWin, MODKEY, Button5, viewnext, {0} }, + }; + ''; +} diff --git a/Suckless/dwm/layouts.nix b/Suckless/dwm/layouts.nix new file mode 100644 index 0000000..ddc78fb --- /dev/null +++ b/Suckless/dwm/layouts.nix @@ -0,0 +1,49 @@ +# dwm layouts +{ lib }: + +let + # Layout indices: 0 = tile, 1 = floating, 2 = monocle + defLayouts = [ + 0 # index 0: all-tags view + 0 # tag 1 + 0 # tag 2 + 0 # tag 3 + 0 # tag 4 + 0 # tag 5 + 0 # tag 6 + 0 # tag 7 + 0 # tag 8 + 0 # tag 9 + 2 # tag 10: terminal + 2 # tag 11: files + 2 # tag 12: video + 2 # tag 13: web/browser + 2 # tag 14: code + 2 # tag 15: freecad + 2 # tag 16: kicad project manager + 2 # tag 17: kicad (sch/pcb editors) + 2 # tag 18: kicad-aux (pcb/footprint mobile) + 2 # tag 19: bambu studio + ]; + defLayoutsStr = lib.concatMapStringsSep ", " toString defLayouts; +in +{ + config = '' + /* layout(s) */ + static const float mfact = 0.55; /* factor of master area size [0.05..0.95] */ + static const int nmaster = 1; /* number of clients in master area */ + static const int resizehints = 0; /* 1 means respect size hints in tiled resizals */ + static const int lockfullscreen = 1; /* 1 will force focus on the fullscreen window */ + + static const Layout layouts[] = { + /* symbol arrange function */ + { "[]=", tile }, /* 0: first entry is default */ + { "><>", NULL }, /* 1: floating */ + { "[M]", monocle }, /* 2: monocle (tabbed) */ + }; + + /* default layout per tag */ + /* 0 = tile, 1 = floating, 2 = monocle */ + static int def_layouts[1 + LENGTH(tags)] = { ${defLayoutsStr} }; + ''; +} diff --git a/Suckless/dwm/rules.nix b/Suckless/dwm/rules.nix new file mode 100644 index 0000000..ca7e54b --- /dev/null +++ b/Suckless/dwm/rules.nix @@ -0,0 +1,41 @@ +# dwm window rules +# Tag mapping: 1<<9=term, 1<<10=files, 1<<11=video, 1<<12=web, 1<<13=code, +# 1<<14=freecad, 1<<15=kicad-pm, 1<<16=kicad, 1<<17=kicad-aux +{ lib }: + +{ + config = '' + static const Rule rules[] = { + /* xprop(1): + * WM_CLASS(STRING) = instance, class + * WM_NAME(STRING) = title + */ + /* class instance title tags mask isfloating monitor */ + { "Polkit-gnome-authentication-agent-1", NULL, NULL, 0, 1, -1 }, + { "KeePassXC", NULL, NULL, 0, 1, -1 }, + { NULL, NULL, "Picture-in-Picture", 0, 1, -1 }, + { "slint-viewer", NULL, NULL, 0, 1, -1 }, + { "LVGL Simulator", NULL, NULL, 0, 1, -1 }, + { NULL, NULL, "Axium Browser", 0, 1, -1 }, + { NULL, NULL, "Axium", 0, 1, -1 }, + { NULL, NULL, "Fex", 0, 1, -1 }, + { NULL, NULL, "@", 0, 1, -1 }, + { NULL, NULL, "OSM", 0, 1, -1 }, + { NULL, NULL, "YouNix", 0, 1, -1 }, + { "st-256color", NULL, NULL, 1 << 9, 0, 0 }, /* terminal */ + { "Thunar", NULL, NULL, 1 << 10, 0, 0 }, /* files */ + { "haruna", NULL, NULL, 1 << 11, 0, 1 }, /* video */ + { "zen-twilight", NULL, "Zen Twilight", 1 << 12, 0, 1 }, /* web */ + { "dev.zed.Zed", NULL, NULL, 1 << 13, 0, 1 }, /* code */ + { "FreeCAD", NULL, "Expression editor", 0, 1, -1 }, /* freecad formula popup */ + { "FreeCAD", NULL, "Insert length", 0, 1, -1 }, /* freecad dimension popup */ + { "FreeCAD", NULL, NULL, 1 << 14, 0, 1 }, /* freecad */ + { "KiCad", NULL, "KiCad 9", 1 << 15, 0, 0 }, /* kicad-pm: tag 16 */ + { "KiCad", NULL, "Schematic Editor", 1 << 16, 0, 0 }, /* kicad: tag 17, mon 0 */ + { "KiCad", NULL, "Symbol Editor", 1 << 16, 0, 0 }, /* kicad: tag 17, mon 0 */ + { "KiCad", NULL, "PCB Editor", 1 << 16, 0, 1 }, /* kicad: tag 17, mon 1 */ + { "KiCad", NULL, "Footprint Editor", 1 << 16, 0, 1 }, /* kicad: tag 17, mon 1 */ + { "BambuStudio", NULL, NULL, 1 << 18, 0, 1 }, /* bambu studio */ + }; + ''; +} diff --git a/Suckless/dwm/tags.nix b/Suckless/dwm/tags.nix new file mode 100644 index 0000000..95c01dd --- /dev/null +++ b/Suckless/dwm/tags.nix @@ -0,0 +1,33 @@ +# dwm tag names (Nerd Font icons for special workspaces) +{ lib }: + +let + tags = [ + "1" + "2" + "3" + "4" + "5" + "6" + "7" + "8" + "9" + "" # 10: terminal + "" # 11: files + "" # 12: video + "󰖟" # 13: web/browser + "" # 14: code + "󰻬" # 15: freecad + "" # 16: kicad project manager + "" # 17: kicad (sch/pcb editors) + "" # 18: kicad-aux (pcb/footprint mobile) + "󰹜" # 19: bambu studio + ]; + tagsStr = lib.concatMapStringsSep ", " (t: ''"${t}"'') tags; +in +{ + config = '' + /* tagging */ + static const char *tags[] = { ${tagsStr} }; + ''; +} diff --git a/Suckless/home.nix b/Suckless/home.nix new file mode 100644 index 0000000..9618fe6 --- /dev/null +++ b/Suckless/home.nix @@ -0,0 +1,144 @@ +# Suckless home-manager configuration +# X11-specific settings that integrate with home-manager +{ pkgs-stable, ... }: + +{ + # Monitor management + programs.autorandr = { + enable = true; + package = pkgs-stable.autorandr; + hooks.postswitch = { + "set-xft-dpi" = "echo 'Xft.dpi: 96' | xrdb -merge"; + "set-wallpaper" = '' + case "$AUTORANDR_CURRENT_PROFILE" in + docked) + xwallpaper --output eDP-1 --zoom ~/NixOS/Wallpapers/pearl.jpg --output DP-3 --zoom ~/NixOS/Wallpapers/siege.png + ;; + mobile) + xwallpaper --output eDP-1 --zoom ~/NixOS/Wallpapers/siege.png + ;; + esac + ''; + }; + profiles = { + "docked" = { + fingerprint = { + eDP-1 = "00ffffffffffff002c831207000000001d220104a51e1378025645935e5b9325185054000000010101010101010101010101010101010f3c80a070b0204018303c002ebd10000018000000000000000000000000000000000000000000000000000000000000000000000000000000fe004b443134304e3336333041303100df"; + DP-3 = "00ffffffffffff0061a906b00100000025220103803c2278afa545ad504da6260c5054a5cb0081809500a9c0b300d1c0010101010101023a801871382d40582c450055502100001e000000ff0035333738323030303434353132000000fd0030a561ba3c000a202020202020000000fc005032374642422d52410a20202001c0020319b349010311130414051f90e200ca67030c00100038448e4480a070382d40582c450055502100001e605980a0703814403024350055502100001c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000050"; + }; + config = { + DP-3 = { + enable = true; + primary = false; + mode = "1920x1080"; + rate = "75.00"; + dpi = 96; + position = "0x180"; + }; + eDP-1 = { + enable = true; + primary = true; + mode = "1920x1200"; + rate = "60.00"; + dpi = 96; + position = "1920x0"; + rotate = "left"; + scale = { + x = 0.75; + y = 0.75; + }; + }; + }; + }; + "mobile" = { + fingerprint = { + eDP-1 = "00ffffffffffff002c831207000000001d220104a51e1378025645935e5b9325185054000000010101010101010101010101010101010f3c80a070b0204018303c002ebd10000018000000000000000000000000000000000000000000000000000000000000000000000000000000fe004b443134304e3336333041303100df"; + }; + config = { + eDP-1 = { + enable = true; + primary = true; + mode = "1920x1200"; + rate = "60.00"; + dpi = 96; + position = "0x0"; + }; + }; + }; + }; + }; + services.autorandr = { + enable = true; + package = pkgs-stable.autorandr; + }; + + # Screenshots + services.flameshot = { + enable = true; + package = pkgs-stable.flameshot; + }; + + # Touchpad gestures (for mobile mode) + home.file.".config/libinput-gestures.conf".text = '' + # 3-finger up/down: tag navigation + gesture swipe up 3 sh -c 'echo "view-prev" > /tmp/dwm.fifo' + gesture swipe down 3 sh -c 'echo "view-next" > /tmp/dwm.fifo' + + # 3-finger left/right: window cycling + gesture swipe left 3 sh -c 'echo "focus-prev" > /tmp/dwm.fifo' + gesture swipe right 3 sh -c 'echo "focus-next" > /tmp/dwm.fifo' + + # 4-finger up/down: layout + gesture swipe up 4 sh -c 'echo "layout-mono" > /tmp/dwm.fifo' + gesture swipe down 4 sh -c 'echo "layout-tile" > /tmp/dwm.fifo' + + # Pinch: rofi system menu / view all + gesture pinch in rofi -show system + gesture pinch out sh -c 'echo "view-all" > /tmp/dwm.fifo' + ''; + + # X11 startup script + home.file.".xinitrc".text = '' + # D-Bus environment for GTK apps (fixes slow first launch) + dbus-update-activation-environment --systemd DBUS_SESSION_BUS_ADDRESS DISPLAY XAUTHORITY + + # Apply monitor profile + autorandr --change --default mobile + + # Disable DPMS and screen blanking + xset s off + xset -dpms + xset s noblank + + # Numlock on + numlockx + + # Status bar + slstatus & + + # Compositor + # picom & + + # Touchpad gestures + libinput-gestures-setup start & + + # Auto-rotate + auto-rotate & + + # KeePassXC (minimized, ready for browser extension) + keepassxc --minimized & + + # Create dwmfifo for IPC + mkfifo /tmp/dwm.fifo 2>/dev/null || true + + # Start dwm + exec dwm + ''; + + # Auto-start X11/dwm on tty1 + programs.bash.profileExtra = '' + if [ -z "$DISPLAY" ] && [ "$XDG_VTNR" = 1 ]; then + exec startx + fi + ''; +} diff --git a/Suckless/slstatus.nix b/Suckless/slstatus.nix new file mode 100644 index 0000000..5587ce9 --- /dev/null +++ b/Suckless/slstatus.nix @@ -0,0 +1,93 @@ +# slstatus - suckless status bar +{ pkgs, ... }: + +let + # Dynamic icon scripts + batteryScript = pkgs.writeShellScript "slstatus-battery" '' + perc=$(cat /sys/class/power_supply/BATT/capacity 2>/dev/null || echo "0") + status=$(cat /sys/class/power_supply/BATT/status 2>/dev/null || echo "Unknown") + + if [ "$status" = "Charging" ]; then icon="󰂄" + elif [ "$perc" -ge 90 ]; then icon="󰁹" + elif [ "$perc" -ge 80 ]; then icon="󰂂" + elif [ "$perc" -ge 70 ]; then icon="󰂁" + elif [ "$perc" -ge 60 ]; then icon="󰂀" + elif [ "$perc" -ge 50 ]; then icon="󰁿" + elif [ "$perc" -ge 40 ]; then icon="󰁾" + elif [ "$perc" -ge 30 ]; then icon="󰁽" + elif [ "$perc" -ge 20 ]; then icon="󰁼" + elif [ "$perc" -ge 10 ]; then icon="󰁻" + else icon="󰁺" + fi + + printf "%s %s%%" "$icon" "$perc" + ''; + + mouseScript = pkgs.writeShellScript "slstatus-mouse" '' + perc=$(cat /sys/class/power_supply/hidpp_battery_0/capacity 2>/dev/null || echo "") + [ -z "$perc" ] && exit 0 + printf "󰍽 %s%%" "$perc" + ''; + + volumeScript = pkgs.writeShellScript "slstatus-volume" '' + vol=$(${pkgs.wireplumber}/bin/wpctl get-volume @DEFAULT_AUDIO_SINK@ 2>/dev/null) + muted=$(echo "$vol" | grep -c MUTED) + perc=$(echo "$vol" | awk '{printf "%.0f", $2*100}') + + if [ "$muted" -eq 1 ]; then icon="󰝟" + elif [ "$perc" -ge 66 ]; then icon="󰕾" + elif [ "$perc" -ge 33 ]; then icon="󰖀" + else icon="󰕿" + fi + + printf "%s %s%%" "$icon" "$perc" + ''; + + wifiScript = pkgs.writeShellScript "slstatus-wifi" '' + essid=$(cat /sys/class/net/wlp2s0/wireless/../uevent 2>/dev/null | grep INTERFACE | cut -d= -f2) + essid=$(${pkgs.iw}/bin/iw dev wlp2s0 link 2>/dev/null | grep SSID | awk '{print $2}') + [ -z "$essid" ] && exit 0 + + perc=$(awk 'NR==3 {printf "%.0f", $3*100/70}' /proc/net/wireless 2>/dev/null || echo "0") + + if [ "$perc" -ge 75 ]; then icon="󰤥" + elif [ "$perc" -ge 50 ]; then icon="󰤢" + elif [ "$perc" -ge 25 ]; then icon="󰤟" + else icon="󰤯" + fi + + printf "%s %s %s%%" "$icon" "$essid" "$perc" + ''; + + config = '' + /* See LICENSE file for copyright and license details. */ + + /* interval between updates (in ms) */ + const unsigned int interval = 1000; + + /* text to show if no value can be retrieved */ + static const char unknown_str[] = ""; + + /* maximum output string length */ + #define MAXLEN 512 + + static const struct arg args[] = { + /* function format argument */ + { cpu_freq, " %s/", NULL }, + { cpu_perc, "%s%% | ", NULL }, + { ram_used, " %s/", NULL }, + { ram_perc, "%s%% | ", NULL }, + { temp, " %s°C | ", "/sys/class/thermal/thermal_zone0/temp" }, + { run_command, "%s | ", "${wifiScript}" }, + { run_command, "%s | ", "${volumeScript}" }, + { run_command, "%s | ", "${batteryScript}" }, + { run_command, "%s | ", "${mouseScript}" }, + { datetime, " %s ", "%d/%m %H:%M:%S" }, + }; + ''; +in +{ + environment.systemPackages = [ + (pkgs.slstatus.override { conf = config; }) + ]; +} diff --git a/Suckless/st/appearance.nix b/Suckless/st/appearance.nix new file mode 100644 index 0000000..115ffe9 --- /dev/null +++ b/Suckless/st/appearance.nix @@ -0,0 +1,46 @@ +# st appearance settings +{ config, lib }: + +let + colors = config.lib.stylix.colors; + fonts = config.stylix.fonts; +in +{ + font = "${fonts.monospace.name}:pixelsize=${ + toString (fonts.sizes.terminal + 6) + }:antialias=true:autohint=true"; + alpha = "0.9"; + + colorsSed = '' + sed -i '/static const char \*colorname\[\]/,/^};/c\ + static const char *colorname[] = {\ + /* 8 normal colors */\ + "#${colors.base00}",\ + "#${colors.base08}",\ + "#${colors.base0B}",\ + "#${colors.base0A}",\ + "#${colors.base0D}",\ + "#${colors.base0E}",\ + "#${colors.base0C}",\ + "#${colors.base05}",\ + \ + /* 8 bright colors */\ + "#${colors.base03}",\ + "#${colors.base08}",\ + "#${colors.base0B}",\ + "#${colors.base0A}",\ + "#${colors.base0D}",\ + "#${colors.base0E}",\ + "#${colors.base0C}",\ + "#${colors.base07}",\ + \ + [255] = 0,\ + \ + /* more colors can be added after 255 to use with DefaultXX */\ + "#${colors.base04}", /* 256: cursor */\ + "#${colors.base03}", /* 257: reverse cursor */\ + "#${colors.base05}", /* 258: foreground */\ + "#${colors.base00}", /* 259: background */\ + };' config.def.h + ''; +} diff --git a/Suckless/st/st.nix b/Suckless/st/st.nix new file mode 100644 index 0000000..9399866 --- /dev/null +++ b/Suckless/st/st.nix @@ -0,0 +1,69 @@ +# st terminal configuration +{ config, pkgs, lib, ... }: + +let + appearance = import ./appearance.nix { inherit config lib; }; +in +{ + nixpkgs.overlays = [(final: prev: { + st = prev.st.overrideAttrs (oldAttrs: { + buildInputs = oldAttrs.buildInputs ++ [ final.harfbuzz ]; + patches = (oldAttrs.patches or []) ++ [ + # Scrollback (ringbuffer + float + mouse) + (final.fetchurl { + url = "https://st.suckless.org/patches/scrollback/st-scrollback-ringbuffer-0.9.2.diff"; + sha256 = "1r23q4mi5bkam49ld5c3ccwaa1li7bbjx0ndjgm207p02az9h4cn"; + }) + (final.fetchurl { + url = "https://st.suckless.org/patches/scrollback/st-scrollback-float-0.9.2.diff"; + sha256 = "01r1gdgkcpf9194257myjnr5nn1fj1baj13wjm9rf2nclbagifgm"; + }) + (final.fetchurl { + url = "https://st.suckless.org/patches/scrollback/st-scrollback-mouse-0.9.2.diff"; + sha256 = "068s5rjvvw2174y34i5xxvpw4jvjy58akd1kgf025h1153hmf7jy"; + }) + # Alpha (transparency) + (final.fetchurl { + url = "https://st.suckless.org/patches/alpha/st-alpha-20240814-a0274bc.diff"; + sha256 = "0hld9dwkk7i1f0z0k9biigx2g4wzlqa2yb7vdn5rrf6ymr5nlbsn"; + }) + # Anysize (no gaps) + (final.fetchurl { + url = "https://st.suckless.org/patches/anysize/st-expected-anysize-0.9.diff"; + sha256 = "04gvkf80lhaiwyv3m7fdkf81msf8al1kfb7inx1bf02ygx9152v2"; + }) + # Bold is not bright + (final.fetchurl { + url = "https://st.suckless.org/patches/bold-is-not-bright/st-bold-is-not-bright-20190127-3be4cf1.diff"; + sha256 = "1cpap2jz80n90izhq5fdv2cvg29hj6bhhvjxk40zkskwmjn6k49j"; + }) + # Clipboard + (final.fetchurl { + url = "https://st.suckless.org/patches/clipboard/st-clipboard-0.8.3.diff"; + sha256 = "1h1nwilwws02h2lnxzmrzr69lyh6pwsym21hvalp9kmbacwy6p0g"; + }) + # Ligatures (scrollback-ringbuffer variant) + (final.fetchurl { + url = "https://st.suckless.org/patches/ligatures/0.9.3/st-ligatures-scrollback-ringbuffer-20251007-0.9.3.diff"; + sha256 = "0c2w1p0siafiyarfx6skdighwzw29d1mydpjfrwgrvdsywwyq2di"; + }) + ]; + postPatch = (oldAttrs.postPatch or "") + '' + # Font + substituteInPlace config.def.h \ + --replace '"Liberation Mono:pixelsize=12:antialias=true:autohint=true"' \ + '"${appearance.font}"' + + # Alpha + substituteInPlace config.def.h \ + --replace 'float alpha = 0.8;' \ + 'float alpha = ${appearance.alpha};' + + # Colors + ${appearance.colorsSed} + ''; + }); + })]; + + environment.systemPackages = [ pkgs.st ]; +} diff --git a/Suckless/suckless.nix b/Suckless/suckless.nix new file mode 100644 index 0000000..0b5c019 --- /dev/null +++ b/Suckless/suckless.nix @@ -0,0 +1,121 @@ +# Suckless configuration entry point +# X11 window manager setup with dwm, st, and related tools +{ pkgs, ... }: + +{ + imports = [ + ./dwm/dwm.nix + ./st/st.nix + ./slstatus.nix + ]; + + # ============================================================================ + # X11 Display Server + # ============================================================================ + services.xserver = { + enable = true; + + # Keyboard layout + xkb.layout = "us"; + + displayManager.startx.enable = true; + + # Auto-lock after 10 minutes of inactivity + xautolock = { + enable = true; + time = 10; + locker = "${pkgs.slock}/bin/slock"; + }; + }; + + # Input (libinput) + services.libinput.enable = true; + + # Tablet/stylus support + services.xserver.wacom.enable = true; + + # Accelerometer for auto-rotation + hardware.sensor.iio.enable = true; + + # ============================================================================ + # Screen Locker + # ============================================================================ + programs.slock = { + enable = true; + package = pkgs.slock; + }; + + # ============================================================================ + # XDG Portal for X11 + # ============================================================================ + xdg.portal = { + enable = true; + extraPortals = [ pkgs.xdg-desktop-portal-gtk ]; + config.common.default = "gtk"; + }; + + # ============================================================================ + # X11 ecosystem packages + # ============================================================================ + environment.systemPackages = with pkgs; [ + # Clipboard + xclip + + # Wallpaper + xwallpaper + + # Numlock on startup + numlockx + + # Touchpad gestures + libinput-gestures + + # Auto-rotate based on accelerometer (with wallpaper switching, mobile only) + (writeShellScriptBin "auto-rotate" '' + export DISPLAY=''${DISPLAY:-:0} + WALLPAPER_DIR="$HOME/NixOS/Wallpapers" + + ${iio-sensor-proxy}/bin/monitor-sensor | while read -r line; do + # Skip rotation when docked (would mess up monitor positions) + [[ $(${autorandr}/bin/autorandr --detected) != "mobile" ]] && continue + + case "$line" in + *"normal"*) + ${xorg.xrandr}/bin/xrandr --output eDP-1 --rotate normal + ${xwallpaper}/bin/xwallpaper --output eDP-1 --zoom "$WALLPAPER_DIR/siege.png" + ;; + *"left-up"*) + ${xorg.xrandr}/bin/xrandr --output eDP-1 --rotate left + ${xwallpaper}/bin/xwallpaper --output eDP-1 --zoom "$WALLPAPER_DIR/pearl.jpg" + ;; + *"right-up"*) + ${xorg.xrandr}/bin/xrandr --output eDP-1 --rotate right + ${xwallpaper}/bin/xwallpaper --output eDP-1 --zoom "$WALLPAPER_DIR/pearl.jpg" + ;; + *"bottom-up"*) + ${xorg.xrandr}/bin/xrandr --output eDP-1 --rotate inverted + ${xwallpaper}/bin/xwallpaper --output eDP-1 --zoom "$WALLPAPER_DIR/siege.png" + ;; + esac + done + '') + ]; + + # ============================================================================ + # Polkit authentication agent + # ============================================================================ + security.polkit.enable = true; + systemd.user.services.polkit-gnome-authentication-agent-1 = { + description = "polkit-gnome-authentication-agent-1"; + wantedBy = [ "graphical-session.target" ]; + wants = [ "graphical-session.target" ]; + after = [ "graphical-session.target" ]; + serviceConfig = { + Type = "simple"; + ExecStart = "${pkgs.polkit_gnome}/libexec/polkit-gnome-authentication-agent-1"; + Restart = "on-failure"; + RestartSec = 1; + TimeoutStopSec = 10; + }; + }; +} diff --git a/Wallpapers/Shift.png b/Wallpapers/Shift.png new file mode 100755 index 0000000..56793c5 Binary files /dev/null and b/Wallpapers/Shift.png differ diff --git a/Wallpapers/caesar.png b/Wallpapers/caesar.png new file mode 100644 index 0000000..d3a6680 Binary files /dev/null and b/Wallpapers/caesar.png differ diff --git a/Wallpapers/norse.png b/Wallpapers/norse.png new file mode 100644 index 0000000..f99b598 Binary files /dev/null and b/Wallpapers/norse.png differ diff --git a/Wallpapers/pearl.jpg b/Wallpapers/pearl.jpg new file mode 100644 index 0000000..81d1603 Binary files /dev/null and b/Wallpapers/pearl.jpg differ diff --git a/Wallpapers/shift_base.png b/Wallpapers/shift_base.png new file mode 100755 index 0000000..4241b7a Binary files /dev/null and b/Wallpapers/shift_base.png differ diff --git a/Wallpapers/siege.png b/Wallpapers/siege.png new file mode 100644 index 0000000..658500e Binary files /dev/null and b/Wallpapers/siege.png differ diff --git a/configuration.nix b/configuration.nix new file mode 100755 index 0000000..0bd157c --- /dev/null +++ b/configuration.nix @@ -0,0 +1,176 @@ +{ + pkgs, + ... +}: + +{ + imports = [ + ./hardware-configuration.nix + ./stylix.nix + # ./Hyprsuck/hyprsuck.nix + ./Suckless/suckless.nix + ./ryzenadj.nix + # ./virtualisation.nix + # ./Hetzner/setup.nix + ]; + + nix.settings = { + experimental-features = [ + "nix-command" + "flakes" + ]; + # Axium binary caches + substituters = [ + "https://cache.nixos.org" + "https://axium.cachix.org" + "https://cache.axiomania.org/main" + ]; + + http-connections = 128; + max-substitution-jobs = 128; + max-jobs = "auto"; + + trusted-public-keys = [ + "axium.cachix.org-1:BfzPfRTbbCYmaQrVLSWchgsR4ScA9ZCZ389FyWspUH8=" + "main:Uz5F0MbXItVx2XCmBbEAMmQ0T6+DZDgLaXWalh1k++o=" + ]; + }; + + nix.distributedBuilds = true; + + # Required for home-manager xdg.portal with useUserPackages + environment.pathsToLink = [ + "/share/applications" + "/share/xdg-desktop-portal" + ]; + + boot.loader.systemd-boot.enable = true; + boot.loader.efi.canTouchEfiVariables = true; + boot.loader.systemd-boot.configurationLimit = 10; + boot.loader.timeout = 1; + + networking.hostName = "v3"; + + networking.networkmanager.enable = true; + hardware.bluetooth.enable = true; + + time.timeZone = "Asia/Ho_Chi_Minh"; + i18n.defaultLocale = "en_US.UTF-8"; + i18n.extraLocaleSettings = { + LC_ADDRESS = "en_US.UTF-8"; + LC_IDENTIFICATION = "en_US.UTF-8"; + LC_MEASUREMENT = "en_US.UTF-8"; + LC_MONETARY = "en_US.UTF-8"; + LC_NAME = "en_US.UTF-8"; + LC_NUMERIC = "en_US.UTF-8"; + LC_PAPER = "en_US.UTF-8"; + LC_TELEPHONE = "en_US.UTF-8"; + LC_TIME = "en_US.UTF-8"; + }; + + services.getty.autologinUser = "lebowski"; + + programs.nix-ld.enable = true; + + services.pulseaudio.enable = false; + security.rtkit.enable = true; + + services.pipewire = { + enable = true; + alsa.enable = true; + alsa.support32Bit = true; + pulse.enable = true; + }; + + users.users.lebowski = { + isNormalUser = true; + description = "Antoine Lespinasse"; + extraGroups = [ + "networkmanager" + "wheel" + "input" + ]; + }; + + # GVfs - for Nautilus trash, network shares, MTP devices + services.gvfs.enable = true; + + # UPower - battery/power device monitoring + services.upower.enable = true; + + # Power Profiles Daemon - power management (performance/balanced/power-saver) + services.power-profiles-daemon.enable = true; + + # System packages (stable for core tools) + environment.systemPackages = with pkgs; [ + wget + git + bat + btop + ncdu + nmap + neofetch + xdotool + jq + attic-client + wireguard-tools + sox + + ffmpegthumbnailer + gdk-pixbuf + librsvg + evince + libgsf + libjxl + libavif + + # Disk utilities + gparted + gnome-disk-utility + + # Image / PDF viewers + kdePackages.gwenview + kdePackages.okular + + # File browser + xfce.thunar + xfce.thunar-volman + xfce.tumbler # thumbnails + ]; + + # ============================================================================ + # WireGuard VPN profiles + # ============================================================================ + # Generate keys: wg genkey | sudo tee /etc/wireguard/private.key | wg pubkey | sudo tee /etc/wireguard/public.key + # Then add the public key as a peer on the VPS. + # Start/stop: sudo systemctl start/stop wg-quick-wg-services / wg-quick-wg-vpn + + # Profile 1: Internal services only (split tunnel) + networking.wg-quick.interfaces.wg-services = { + autostart = false; + address = [ "10.100.0.2/24" ]; + privateKeyFile = "/etc/wireguard/private.key"; + peers = [{ + publicKey = "F2hvz4vx9VrM6IZ2zMUG2FMPMCMwmfxGH9qocbe4q3U="; + endpoint = "178.104.15.221:51820"; + allowedIPs = [ "10.100.0.0/24" "178.104.15.221/32" ]; + persistentKeepalive = 25; + }]; + }; + + # Profile 2: Full VPN + AdGuard DNS ad-blocking + networking.wg-quick.interfaces.wg-vpn = { + autostart = false; + address = [ "10.100.0.2/24" ]; + dns = [ "10.100.0.1" ]; + privateKeyFile = "/etc/wireguard/private.key"; + peers = [{ + publicKey = "F2hvz4vx9VrM6IZ2zMUG2FMPMCMwmfxGH9qocbe4q3U="; + endpoint = "178.104.15.221:51820"; + allowedIPs = [ "0.0.0.0/0" ]; + persistentKeepalive = 25; + }]; + }; + + system.stateVersion = "25.11"; +} diff --git a/flake.lock b/flake.lock new file mode 100755 index 0000000..8fa3a58 --- /dev/null +++ b/flake.lock @@ -0,0 +1,420 @@ +{ + "nodes": { + "base16": { + "inputs": { + "fromYaml": "fromYaml" + }, + "locked": { + "lastModified": 1755819240, + "narHash": "sha256-qcMhnL7aGAuFuutH4rq9fvAhCpJWVHLcHVZLtPctPlo=", + "owner": "SenchoPens", + "repo": "base16.nix", + "rev": "75ed5e5e3fce37df22e49125181fa37899c3ccd6", + "type": "github" + }, + "original": { + "owner": "SenchoPens", + "repo": "base16.nix", + "type": "github" + } + }, + "base16-fish": { + "flake": false, + "locked": { + "lastModified": 1765809053, + "narHash": "sha256-XCUQLoLfBJ8saWms2HCIj4NEN+xNsWBlU1NrEPcQG4s=", + "owner": "tomyun", + "repo": "base16-fish", + "rev": "86cbea4dca62e08fb7fd83a70e96472f92574782", + "type": "github" + }, + "original": { + "owner": "tomyun", + "repo": "base16-fish", + "rev": "86cbea4dca62e08fb7fd83a70e96472f92574782", + "type": "github" + } + }, + "base16-helix": { + "flake": false, + "locked": { + "lastModified": 1760703920, + "narHash": "sha256-m82fGUYns4uHd+ZTdoLX2vlHikzwzdu2s2rYM2bNwzw=", + "owner": "tinted-theming", + "repo": "base16-helix", + "rev": "d646af9b7d14bff08824538164af99d0c521b185", + "type": "github" + }, + "original": { + "owner": "tinted-theming", + "repo": "base16-helix", + "type": "github" + } + }, + "base16-vim": { + "flake": false, + "locked": { + "lastModified": 1732806396, + "narHash": "sha256-e0bpPySdJf0F68Ndanwm+KWHgQiZ0s7liLhvJSWDNsA=", + "owner": "tinted-theming", + "repo": "base16-vim", + "rev": "577fe8125d74ff456cf942c733a85d769afe58b7", + "type": "github" + }, + "original": { + "owner": "tinted-theming", + "repo": "base16-vim", + "rev": "577fe8125d74ff456cf942c733a85d769afe58b7", + "type": "github" + } + }, + "disko": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1773889306, + "narHash": "sha256-PAqwnsBSI9SVC2QugvQ3xeYCB0otOwCacB1ueQj2tgw=", + "owner": "nix-community", + "repo": "disko", + "rev": "5ad85c82cc52264f4beddc934ba57f3789f28347", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "disko", + "type": "github" + } + }, + "firefox-gnome-theme": { + "flake": false, + "locked": { + "lastModified": 1764873433, + "narHash": "sha256-1XPewtGMi+9wN9Ispoluxunw/RwozuTRVuuQOmxzt+A=", + "owner": "rafaelmardojai", + "repo": "firefox-gnome-theme", + "rev": "f7ffd917ac0d253dbd6a3bf3da06888f57c69f92", + "type": "github" + }, + "original": { + "owner": "rafaelmardojai", + "repo": "firefox-gnome-theme", + "type": "github" + } + }, + "flake-parts": { + "inputs": { + "nixpkgs-lib": [ + "stylix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1767609335, + "narHash": "sha256-feveD98mQpptwrAEggBQKJTYbvwwglSbOv53uCfH9PY=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "250481aafeb741edfe23d29195671c19b36b6dca", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "fromYaml": { + "flake": false, + "locked": { + "lastModified": 1731966426, + "narHash": "sha256-lq95WydhbUTWig/JpqiB7oViTcHFP8Lv41IGtayokA8=", + "owner": "SenchoPens", + "repo": "fromYaml", + "rev": "106af9e2f715e2d828df706c386a685698f3223b", + "type": "github" + }, + "original": { + "owner": "SenchoPens", + "repo": "fromYaml", + "type": "github" + } + }, + "gnome-shell": { + "flake": false, + "locked": { + "host": "gitlab.gnome.org", + "lastModified": 1767737596, + "narHash": "sha256-eFujfIUQDgWnSJBablOuG+32hCai192yRdrNHTv0a+s=", + "owner": "GNOME", + "repo": "gnome-shell", + "rev": "ef02db02bf0ff342734d525b5767814770d85b49", + "type": "gitlab" + }, + "original": { + "host": "gitlab.gnome.org", + "owner": "GNOME", + "ref": "gnome-49", + "repo": "gnome-shell", + "type": "gitlab" + } + }, + "home-manager": { + "inputs": { + "nixpkgs": [ + "nixpkgs-unstable" + ] + }, + "locked": { + "lastModified": 1774738535, + "narHash": "sha256-2jfBEZUC67IlnxO5KItFCAd7Oc+1TvyV/jQlR+2ykGQ=", + "owner": "nix-community", + "repo": "home-manager", + "rev": "769e07ef8f4cf7b1ec3b96ef015abec9bc6b1e2a", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "home-manager", + "type": "github" + } + }, + "nixos-hardware": { + "locked": { + "lastModified": 1766517411, + "narHash": "sha256-qilXvRpeyefgLRl1oyitQsacWGP5TrWf7f6V+KOUt38=", + "owner": "GammaKinematics", + "repo": "nixos-hardware", + "rev": "b1dfc15a77366c8f163af12b8b3c5f4d4f56812c", + "type": "github" + }, + "original": { + "owner": "GammaKinematics", + "repo": "nixos-hardware", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1774388614, + "narHash": "sha256-tFwzTI0DdDzovdE9+Ras6CUss0yn8P9XV4Ja6RjA+nU=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "1073dad219cb244572b74da2b20c7fe39cb3fa9e", + "type": "github" + }, + "original": { + "id": "nixpkgs", + "ref": "nixos-25.11", + "type": "indirect" + } + }, + "nixpkgs-unstable": { + "locked": { + "lastModified": 1774386573, + "narHash": "sha256-4hAV26quOxdC6iyG7kYaZcM3VOskcPUrdCQd/nx8obc=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "46db2e09e1d3f113a13c0d7b81e2f221c63b8ce9", + "type": "github" + }, + "original": { + "id": "nixpkgs", + "ref": "nixos-unstable", + "type": "indirect" + } + }, + "nur": { + "inputs": { + "flake-parts": [ + "stylix", + "flake-parts" + ], + "nixpkgs": [ + "stylix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1767810917, + "narHash": "sha256-ZKqhk772+v/bujjhla9VABwcvz+hB2IaRyeLT6CFnT0=", + "owner": "nix-community", + "repo": "NUR", + "rev": "dead29c804adc928d3a69dfe7f9f12d0eec1f1a4", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "NUR", + "type": "github" + } + }, + "root": { + "inputs": { + "disko": "disko", + "home-manager": "home-manager", + "nixos-hardware": "nixos-hardware", + "nixpkgs": "nixpkgs", + "nixpkgs-unstable": "nixpkgs-unstable", + "stylix": "stylix", + "zen-browser": "zen-browser" + } + }, + "stylix": { + "inputs": { + "base16": "base16", + "base16-fish": "base16-fish", + "base16-helix": "base16-helix", + "base16-vim": "base16-vim", + "firefox-gnome-theme": "firefox-gnome-theme", + "flake-parts": "flake-parts", + "gnome-shell": "gnome-shell", + "nixpkgs": [ + "nixpkgs-unstable" + ], + "nur": "nur", + "systems": "systems", + "tinted-foot": "tinted-foot", + "tinted-kitty": "tinted-kitty", + "tinted-schemes": "tinted-schemes", + "tinted-tmux": "tinted-tmux", + "tinted-zed": "tinted-zed" + }, + "locked": { + "lastModified": 1774124764, + "narHash": "sha256-Poz9WTjiRlqZIf197CrMMJfTifZhrZpbHFv0eU1Nhtg=", + "owner": "nix-community", + "repo": "stylix", + "rev": "e31c79f571c5595a155f84b9d77ce53a84745494", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "stylix", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "tinted-foot": { + "flake": false, + "locked": { + "lastModified": 1726913040, + "narHash": "sha256-+eDZPkw7efMNUf3/Pv0EmsidqdwNJ1TaOum6k7lngDQ=", + "owner": "tinted-theming", + "repo": "tinted-foot", + "rev": "fd1b924b6c45c3e4465e8a849e67ea82933fcbe4", + "type": "github" + }, + "original": { + "owner": "tinted-theming", + "repo": "tinted-foot", + "rev": "fd1b924b6c45c3e4465e8a849e67ea82933fcbe4", + "type": "github" + } + }, + "tinted-kitty": { + "flake": false, + "locked": { + "lastModified": 1735730497, + "narHash": "sha256-4KtB+FiUzIeK/4aHCKce3V9HwRvYaxX+F1edUrfgzb8=", + "owner": "tinted-theming", + "repo": "tinted-kitty", + "rev": "de6f888497f2c6b2279361bfc790f164bfd0f3fa", + "type": "github" + }, + "original": { + "owner": "tinted-theming", + "repo": "tinted-kitty", + "type": "github" + } + }, + "tinted-schemes": { + "flake": false, + "locked": { + "lastModified": 1767710407, + "narHash": "sha256-+W1EB79Jl0/gm4JqmO0Nuc5C7hRdp4vfsV/VdzI+des=", + "owner": "tinted-theming", + "repo": "schemes", + "rev": "2800e2b8ac90f678d7e4acebe4fa253f602e05b2", + "type": "github" + }, + "original": { + "owner": "tinted-theming", + "repo": "schemes", + "type": "github" + } + }, + "tinted-tmux": { + "flake": false, + "locked": { + "lastModified": 1767489635, + "narHash": "sha256-e6nnFnWXKBCJjCv4QG4bbcouJ6y3yeT70V9MofL32lU=", + "owner": "tinted-theming", + "repo": "tinted-tmux", + "rev": "3c32729ccae99be44fe8a125d20be06f8d7d8184", + "type": "github" + }, + "original": { + "owner": "tinted-theming", + "repo": "tinted-tmux", + "type": "github" + } + }, + "tinted-zed": { + "flake": false, + "locked": { + "lastModified": 1767488740, + "narHash": "sha256-wVOj0qyil8m+ouSsVZcNjl5ZR+1GdOOAooAatQXHbuU=", + "owner": "tinted-theming", + "repo": "base16-zed", + "rev": "11abb0b282ad3786a2aae088d3a01c60916f2e40", + "type": "github" + }, + "original": { + "owner": "tinted-theming", + "repo": "base16-zed", + "type": "github" + } + }, + "zen-browser": { + "inputs": { + "home-manager": [ + "home-manager" + ], + "nixpkgs": [ + "nixpkgs-unstable" + ] + }, + "locked": { + "lastModified": 1774708879, + "narHash": "sha256-rTYvYkQL69/YkZB+MRA/IaX1qJ1lPx5KXoQS2/9+7Mw=", + "owner": "0xc000022070", + "repo": "zen-browser-flake", + "rev": "d01d23c798cceef42307d5789bfbce70515e8800", + "type": "github" + }, + "original": { + "owner": "0xc000022070", + "repo": "zen-browser-flake", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100755 index 0000000..75b3fb4 --- /dev/null +++ b/flake.nix @@ -0,0 +1,112 @@ +{ + description = "NixOS - AL"; + + inputs = { + # Stable nixpkgs for core system (kernel, boot, virtualization) + nixpkgs.url = "nixpkgs/nixos-25.11"; + + # Unstable nixpkgs for desktop/dev tools + nixpkgs-unstable.url = "nixpkgs/nixos-unstable"; + + # Home Manager + home-manager = { + url = "github:nix-community/home-manager"; + inputs.nixpkgs.follows = "nixpkgs-unstable"; + }; + + # Zen Browser + zen-browser = { + url = "github:0xc000022070/zen-browser-flake"; + inputs.nixpkgs.follows = "nixpkgs-unstable"; + inputs.home-manager.follows = "home-manager"; + }; + + # Hardware-specific optimizations (fork with Minisforum V3 SE support) + nixos-hardware.url = "github:GammaKinematics/nixos-hardware"; + + # Stylix - system-wide theming + stylix = { + url = "github:nix-community/stylix"; + inputs.nixpkgs.follows = "nixpkgs-unstable"; + }; + + # Disko - declarative disk partitioning (for VPS) + disko = { + url = "github:nix-community/disko"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = + { + self, + nixpkgs, + nixpkgs-unstable, + home-manager, + zen-browser, + ... + }@inputs: + let + system = "x86_64-linux"; + + + # Stable pkgs for CAD/manufacturing (more reliable builds) + pkgs-stable = import nixpkgs { + inherit system; + config.allowUnfree = true; + }; + + # Unstable pkgs for home-manager / desktop / dev + pkgs-unstable = import nixpkgs-unstable { + inherit system; + config.allowUnfree = true; + }; + in + { + # ── Hetzner VPS ── + nixosConfigurations.axiomania = nixpkgs.lib.nixosSystem { + inherit system; + modules = [ + inputs.disko.nixosModules.disko + ./Hetzner/axiomania.nix + ]; + }; + + # ── Hetzner ephemeral builder ── + nixosConfigurations.builder = nixpkgs.lib.nixosSystem { + inherit system; + modules = [ + inputs.disko.nixosModules.disko + ./Hetzner/builder.nix + ]; + }; + + # ── Local: Minisforum V3 SE ── + nixosConfigurations.v3 = nixpkgs.lib.nixosSystem { + inherit system; + + specialArgs = { inherit inputs; }; + + modules = [ + inputs.stylix.nixosModules.stylix + inputs.nixos-hardware.nixosModules.minisforum-v3-se + + ./configuration.nix + + home-manager.nixosModules.home-manager + { + home-manager = { + useGlobalPkgs = false; + useUserPackages = true; + extraSpecialArgs = { + inherit inputs pkgs-unstable pkgs-stable; + }; + users.lebowski = import ./home.nix; + backupFileExtension = "backup"; + }; + } + ]; + }; + + }; +} diff --git a/git.nix b/git.nix new file mode 100644 index 0000000..dbab37c --- /dev/null +++ b/git.nix @@ -0,0 +1,68 @@ +{ pkgs-stable, ... }: + +{ + # ============================================================================ + # Git Configuration + # ============================================================================ + programs.git = { + enable = true; + package = pkgs-stable.git; + + settings = { + user.name = "GammaKinematics"; + user.email = "gamma.kinematics@gmail.com"; + init.defaultBranch = "main"; + pull.rebase = true; + push.autoSetupRemote = true; + # Use SSH for GitHub + url."ssh://git@github.com/".insteadOf = "https://github.com/"; + }; + + # Delta - better diff viewer + # delta = { + # enable = true; + # options = { + # navigate = true; + # light = false; + # line-numbers = true; + # }; + # }; + + # Aliases + # aliases = { + # co = "checkout"; + # br = "branch"; + # ci = "commit"; + # st = "status"; + # lg = "log --oneline --graph --decorate"; + # }; + + # SSH signing (for verified commits) + # signing = { + # key = "~/.ssh/id_ed25519.pub"; + # signByDefault = true; + # }; + # extraConfig.gpg.format = "ssh"; + }; + + # ============================================================================ + # SSH Configuration + # ============================================================================ + programs.ssh = { + enable = true; + enableDefaultConfig = false; + matchBlocks = { + "github.com" = { + host = "github.com"; + identityFile = "~/.ssh/id_ed25519"; + identitiesOnly = true; + }; + # Add more hosts as needed + # "gitlab.com" = { + # host = "gitlab.com"; + # identityFile = "~/.ssh/id_ed25519"; + # identitiesOnly = true; + # }; + }; + }; +} diff --git a/hardware-configuration.nix b/hardware-configuration.nix new file mode 100755 index 0000000..8c2d0b1 --- /dev/null +++ b/hardware-configuration.nix @@ -0,0 +1,73 @@ +# Do not modify this file! It was generated by ‘nixos-generate-config’ +# and may be overwritten by future invocations. Please make changes +# to /etc/nixos/configuration.nix instead. +{ + config, + lib, + pkgs, + modulesPath, + ... +}: + +{ + imports = [ + (modulesPath + "/installer/scan/not-detected.nix") + ]; + + boot.initrd.availableKernelModules = [ + "nvme" + "xhci_pci" + "thunderbolt" + "usb_storage" + "usbhid" + "uas" + "sd_mod" + "rtsx_pci_sdmmc" + ]; + boot.initrd.kernelModules = [ ]; + boot.kernelModules = [ "kvm-amd" ]; + boot.extraModulePackages = [ ]; + + fileSystems."/" = { + device = "/dev/disk/by-uuid/ea496278-ae69-48b0-9edb-b351b9620829"; + fsType = "ext4"; + }; + + fileSystems."/boot" = { + device = "/dev/disk/by-uuid/C267-DCFD"; + fsType = "vfat"; + options = [ + "fmask=0077" + "dmask=0077" + ]; + }; + + fileSystems."/data" = { + device = "/dev/disk/by-label/DATA"; + fsType = "ntfs"; + options = [ + "rw" + "uid=1000" + "gid=100" + "umask=022" + "nofail" + ]; + }; + + fileSystems."/data-bis" = { + device = "/dev/disk/by-label/DATA-bis"; + fsType = "ntfs"; + options = [ + "rw" + "uid=1000" + "gid=100" + "umask=022" + "nofail" + ]; + }; + + swapDevices = [ ]; + + nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux"; + hardware.cpu.amd.updateMicrocode = lib.mkDefault config.hardware.enableRedistributableFirmware; +} diff --git a/home.nix b/home.nix new file mode 100644 index 0000000..014e584 --- /dev/null +++ b/home.nix @@ -0,0 +1,141 @@ +{ + config, + pkgs-unstable, + pkgs-stable, + inputs, + ... +}: + +let + flakeDir = "${config.home.homeDirectory}/NixOS"; +in + +{ + # Fix home-manager xresources bug: pkgs.xrdb doesn't exist, should be xorg.xrdb + nixpkgs.overlays = [ (final: prev: { xrdb = prev.xorg.xrdb; }) ]; + + # Disable xresources - Stylix enables it by default but we don't need it (ST uses compile-time colors) + stylix.targets.xresources.enable = false; + + imports = [ + ./git.nix + ./zed.nix + ./zen.nix + ./Rofi/rofi.nix + ./KiCad/kicad.nix + + # ./Hyprsuck/home.nix + ./Suckless/home.nix # Uncomment for dwm (and comment Hyprsuck) + ]; + + home.username = "lebowski"; + home.homeDirectory = "/home/lebowski"; + + # Let Home Manager manage itself + programs.home-manager.enable = true; + + # ============================================================================ + # User Packages (stable by default, unstable for bleeding-edge tools) + # ============================================================================ + home.packages = with pkgs-stable; [ + # Media & Creative + haruna + krita + xournalpp + + # Office & Productivity + libreoffice-fresh + + # CAD & Manufacturing + freecad + bambu-studio + + # Dev tools (unstable for latest features) + pkgs-unstable.claude-code + pkgs-unstable.hcloud + ]; + + # ============================================================================ + # GTK Icon Theme + # ============================================================================ + gtk.iconTheme = { + package = pkgs-stable.papirus-icon-theme; + name = "Papirus-Dark"; + }; + gtk.gtk4.theme = null; + + # ============================================================================ + # Notifications (shared between Hyprland and dwm) + # ============================================================================ + services.dunst = { + enable = true; + package = pkgs-stable.dunst; + settings.global.timeout = 3; + }; + + # ============================================================================ + # Shell Aliases + # ============================================================================ + home.shellAliases = { + # NixOS rebuild shortcuts + nrs = "sudo nixos-rebuild switch --flake ${flakeDir}"; + nrb = "sudo nixos-rebuild boot --flake ${flakeDir}"; + nrt = "sudo nixos-rebuild test --flake ${flakeDir}"; + + # Nix utilities + nfu = "nix flake update --flake ${flakeDir}"; + ncg = "sudo nix-collect-garbage -d"; + nso = "nix store optimise"; + }; + + # ============================================================================ + # Optional: Additional program configurations + # ============================================================================ + + # --- Bash shell --- + programs.bash = { + enable = true; + package = pkgs-stable.bash; + }; + + # --- KeePassXC --- + programs.keepassxc = { + enable = true; + package = pkgs-stable.keepassxc; + }; + + programs.foliate = { + enable = true; + package = pkgs-stable.foliate; + }; + + # --- SSH --- + programs.ssh = { + enable = true; + package = pkgs-stable.openssh; + + matchBlocks = { + "*" = { + addKeysToAgent = "yes"; + serverAliveInterval = 60; + controlMaster = "auto"; + controlPersist = "10m"; + }; + "git.axiomania.org" = { + port = 2222; + }; + "vps" = { + hostname = "178.104.15.221"; + user = "root"; + identityFile = "~/.ssh/id_ed25519"; + }; + }; + }; + + # ============================================================================ + # Disable version check - we intentionally use unstable home-manager with stable nixpkgs + # Note: This may cause issues if home-manager uses features not yet in stable + home.enableNixpkgsReleaseCheck = false; + + home.stateVersion = "25.11"; +} diff --git a/ryzenadj.nix b/ryzenadj.nix new file mode 100644 index 0000000..6e85e07 --- /dev/null +++ b/ryzenadj.nix @@ -0,0 +1,181 @@ +# RyzenAdj power tuning configuration +# Provides power-tuning script for Minisforum V3 SE power profile management +{ pkgs, ... }: + +let + # Power profiles (values in mW for power, °C for temp) + profiles = { + saver = { + stapm = 15000; + fast = 18000; + slow = 16000; + temp = 80; + }; + balanced = { + stapm = 22500; + fast = 30000; + slow = 25000; + temp = 90; + }; + performance = { + stapm = 30000; + fast = 40000; + slow = 35000; + temp = 100; + }; + compile = { + stapm = 40000; + fast = 50000; + slow = 45000; + temp = 100; + }; + }; + + configFile = pkgs.writeText "power-tuning-config.json" (builtins.toJSON profiles); + + power-tuning = pkgs.writeShellScriptBin "power-tuning" '' + CONFIG_FILE="${configFile}" + COMPILE_FLAG="/tmp/.power-tuning-compile-mode" + + # Read a profile from config + get_profile() { + local profile="$1" + ${pkgs.jq}/bin/jq -r ".$profile" "$CONFIG_FILE" + } + + # Apply ryzenadj settings + apply_settings() { + local profile="$1" + local settings + settings=$(get_profile "$profile") + + if [[ "$settings" == "null" ]]; then + echo "Error: Profile '$profile' not found in config" + return 1 + fi + + local stapm fast slow temp + stapm=$(echo "$settings" | ${pkgs.jq}/bin/jq -r '.stapm') + fast=$(echo "$settings" | ${pkgs.jq}/bin/jq -r '.fast') + slow=$(echo "$settings" | ${pkgs.jq}/bin/jq -r '.slow') + temp=$(echo "$settings" | ${pkgs.jq}/bin/jq -r '.temp') + + echo "Applying $profile: STAPM=''${stapm}mW Fast=''${fast}mW Slow=''${slow}mW Temp=''${temp}°C" + + ${pkgs.ryzenadj}/bin/ryzenadj \ + --stapm-limit="$stapm" \ + --fast-limit="$fast" \ + --slow-limit="$slow" \ + --tctl-temp="$temp" \ + 2>&1 + } + + # Get current power profile from power-profiles-daemon + get_current_ppd_profile() { + local profile + profile=$(${pkgs.power-profiles-daemon}/bin/powerprofilesctl get 2>/dev/null) + + case "$profile" in + "power-saver") echo "saver" ;; + "balanced") echo "balanced" ;; + "performance") echo "performance" ;; + *) echo "balanced" ;; + esac + } + + # Check if compile mode is active + is_compile_mode() { + [[ -f "$COMPILE_FLAG" ]] + } + + # Main command handling + case "''${1:-apply}" in + apply) + if is_compile_mode; then + echo "Compile mode active, using compile profile" + apply_settings "compile" + else + profile=$(get_current_ppd_profile) + echo "Current PPD profile: $profile" + apply_settings "$profile" + fi + ;; + + compile-on) + touch "$COMPILE_FLAG" + echo "Compile mode enabled" + apply_settings "compile" + ;; + + compile-off) + rm -f "$COMPILE_FLAG" + echo "Compile mode disabled" + profile=$(get_current_ppd_profile) + apply_settings "$profile" + ;; + + status) + echo "=== Power Tuning Status ===" + echo "Config file: $CONFIG_FILE" + echo "PPD Profile: $(${pkgs.power-profiles-daemon}/bin/powerprofilesctl get 2>/dev/null || echo 'unknown')" + echo "Compile mode: $(is_compile_mode && echo 'ON' || echo 'OFF')" + echo "" + echo "Current presets:" + ${pkgs.jq}/bin/jq '.' "$CONFIG_FILE" + ;; + + *) + echo "Usage: power-tuning {apply|compile-on|compile-off|status}" + exit 1 + ;; + esac + ''; +in +{ + # Kernel module for SMU access + hardware.cpu.amd.ryzen-smu.enable = true; + + # Permissions for ryzenadj to access SMU + systemd.tmpfiles.rules = [ + "z /sys/kernel/ryzen_smu_drv/smn 0660 root wheel -" + "z /sys/kernel/ryzen_smu_drv/smu_args 0660 root wheel -" + "z /sys/kernel/ryzen_smu_drv/mp1_smu_cmd 0660 root wheel -" + "z /sys/kernel/ryzen_smu_drv/rsmu_cmd 0660 root wheel -" + ]; + + environment.systemPackages = [ + power-tuning + pkgs.ryzenadj + ]; + + # Apply power tuning on boot and when power profile changes + systemd.services.power-tuning = { + description = "Apply RyzenAdj power tuning"; + wantedBy = [ "multi-user.target" ]; + after = [ "power-profiles-daemon.service" ]; + serviceConfig = { + Type = "oneshot"; + ExecStart = "${power-tuning}/bin/power-tuning apply"; + }; + }; + + # Re-apply when power profile changes + systemd.services.power-tuning-on-profile-change = { + description = "Apply RyzenAdj on power profile change"; + after = [ "power-profiles-daemon.service" ]; + serviceConfig = { + Type = "oneshot"; + ExecStart = "${power-tuning}/bin/power-tuning apply"; + }; + }; + + # Path trigger to watch for profile changes + systemd.paths.power-tuning-watch = { + description = "Watch for power profile changes"; + wantedBy = [ "multi-user.target" ]; + pathConfig = { + PathChanged = "/sys/firmware/acpi/platform_profile"; + Unit = "power-tuning-on-profile-change.service"; + }; + }; +} diff --git a/stylix.nix b/stylix.nix new file mode 100644 index 0000000..593bc82 --- /dev/null +++ b/stylix.nix @@ -0,0 +1,53 @@ +{ pkgs, ... }: + +{ + stylix = { + enable = true; + enableReleaseChecks = false; + + polarity = "dark"; + + # Catppuccin Mocha theme + base16Scheme = "${pkgs.base16-schemes}/share/themes/catppuccin-mocha.yaml"; + + # Wallpaper (used for color scheme generation) + image = ./Wallpapers/siege.png; + + # Cursor + cursor = { + package = pkgs.bibata-cursors; + name = "Bibata-Modern-Ice"; + size = 20; + }; + + # Fonts + fonts = { + monospace = { + package = pkgs.nerd-fonts.jetbrains-mono; + name = "JetBrainsMono Nerd Font"; + }; + sansSerif = { + package = pkgs.dejavu_fonts; + name = "DejaVu Sans"; + }; + serif = { + package = pkgs.dejavu_fonts; + name = "DejaVu Serif"; + }; + emoji = { + package = pkgs.noto-fonts-color-emoji; + name = "Noto Color Emoji"; + }; + sizes = { + terminal = 11; + applications = 11; + desktop = 11; + }; + }; + + # Opacity settings + opacity = { + terminal = 0.9; + }; + }; +} diff --git a/virtualisation.nix b/virtualisation.nix new file mode 100644 index 0000000..f9fc22b --- /dev/null +++ b/virtualisation.nix @@ -0,0 +1,19 @@ +{ config, pkgs, ... }: + +{ + # Libvirt/QEMU virtualisation + programs.virt-manager.enable = true; + users.groups.libvirtd.members = [ "lebowski" ]; + virtualisation.libvirtd.enable = true; + virtualisation.spiceUSBRedirection.enable = true; + + # Docker + virtualisation.docker = { + enable = true; + # Use the rootless mode - run Docker daemon as non-root user + rootless = { + enable = true; + setSocketVariable = true; + }; + }; +} diff --git a/zed.nix b/zed.nix new file mode 100644 index 0000000..3471efe --- /dev/null +++ b/zed.nix @@ -0,0 +1,73 @@ +# Zed Editor configuration +{ pkgs-unstable, ... }: + +{ + + programs.zed-editor = { + enable = true; + package = pkgs-unstable.zed-editor; + + # This populates the userSettings "auto_install_extensions" + extensions = [ + "nix" + "odin" + "astro" + "slint" + "dockerfile" + "docker-compose" + "latex" + "make" + "log" + "csv" + ]; + + # Everything inside of these brackets are Zed options + userSettings = { + # Disable all AI features (including Claude Code ACP agents) + disable_ai = true; + + # Disable collaboration/AI features + assistant = { enabled = false; }; + collaboration_panel = { button = false; }; + chat_panel = { button = false; }; + notification_panel = { button = false; }; + title_bar = { show_sign_in = false; }; + + # Hide UI panels + diagnostics = { button = false; }; + debugger = { button = false; }; + + # Disable edit predictions completely + features = { edit_prediction_provider = "none"; }; + show_edit_predictions = false; + + hour_format = "hour24"; + auto_update = false; + + terminal = { + alternate_scroll = "off"; + blinking = "off"; + copy_on_select = true; + dock = "right"; + line_height = "comfortable"; + shell = "system"; + toolbar = { + title = true; # Shows terminal title (e.g. "zsh" or running command) in panel header + }; + working_directory = "current_project_directory"; + }; + + vim_mode = false; + + base_keymap = "VSCode"; + + show_whitespaces = "selection"; + + # Disable all language servers globally + enable_language_server = false; + + # Disable Claude Code integration (prevents auto-launching claude-code-acp) + context_servers = { }; + }; + }; +} diff --git a/zen.nix b/zen.nix new file mode 100644 index 0000000..004996f --- /dev/null +++ b/zen.nix @@ -0,0 +1,322 @@ +{ config, lib, pkgs-unstable, inputs, ... }: + +{ + # Workaround for https://github.com/0xc000022070/zen-browser-flake/issues/285 + # The twilight module generates profiles.ini as a read-only nix store symlink, + # which Zen can't write to, so it ignores it and creates a random-prefix profile. + # This replaces the symlink with a mutable copy. + home.activation.fixZenProfiles = lib.hm.dag.entryAfter ["linkGeneration"] '' + ZEN_DIR="$HOME/.zen" + if [ -L "$ZEN_DIR/profiles.ini" ]; then + REAL=$(readlink -f "$ZEN_DIR/profiles.ini") + rm "$ZEN_DIR/profiles.ini" + cp "$REAL" "$ZEN_DIR/profiles.ini" + chmod u+w "$ZEN_DIR/profiles.ini" + fi + ''; + # Tell Stylix which Zen profiles to theme + stylix.targets.zen-browser.profileNames = [ "default" ]; + + imports = [ + inputs.zen-browser.homeModules.twilight + ]; + + # Set Zen as default browser for all relevant MIME types + xdg.mimeApps = let + associations = builtins.listToAttrs (map (name: { + inherit name; + value = let + zen-browser = config.programs.zen-browser.package; + in + zen-browser.meta.desktopFileName; + }) [ + "application/x-extension-shtml" + "application/x-extension-xhtml" + "application/x-extension-html" + "application/x-extension-xht" + "application/x-extension-htm" + "x-scheme-handler/unknown" + "x-scheme-handler/mailto" + "x-scheme-handler/chrome" + "x-scheme-handler/about" + "x-scheme-handler/https" + "x-scheme-handler/http" + "application/xhtml+xml" + "application/json" + "text/plain" + "text/html" + ]); + in { + associations.added = associations; + defaultApplications = associations; + }; + + programs.zen-browser = { + enable = true; + + # Native Messaging Hosts - required for KeePassXC browser integration + nativeMessagingHosts = [ pkgs-unstable.keepassxc ]; + + policies = let + # Helper to lock preferences + mkLockedAttrs = builtins.mapAttrs (_: value: { + Value = value; + Status = "locked"; + }); + + # Helper to create extension install URL + mkPluginUrl = id: "https://addons.mozilla.org/firefox/downloads/latest/${id}/latest.xpi"; + + # Helper to create extension entry with options + mkExtensionEntry = { + id, + pinned ? false, + private_browsing ? true, + }: let + base = { + install_url = mkPluginUrl id; + installation_mode = "force_installed"; + inherit private_browsing; + }; + in + if pinned + then base // { default_area = "navbar"; } + else base; + + # Helper to process extension settings + mkExtensionSettings = builtins.mapAttrs (_: entry: + if builtins.isAttrs entry + then entry + else mkExtensionEntry { id = entry; }); + in { + # App behavior + DisableAppUpdate = true; + DontCheckDefaultBrowser = true; + DisableFeedbackCommands = true; + + # Privacy & Telemetry + DisableTelemetry = true; + DisableFirefoxStudies = true; + DisablePocket = true; + + # Autofill + AutofillAddressEnabled = true; + AutofillCreditCardEnabled = false; + OfferToSaveLogins = false; + + # Tracking Protection + EnableTrackingProtection = { + Value = true; + Locked = true; + Cryptomining = true; + Fingerprinting = true; + }; + + # Clean up on shutdown (fresh instance) + SanitizeOnShutdown = { + Cache = true; + Cookies = false; # Keep logins + Downloads = true; + FormData = true; + History = false; # Keep history + Sessions = false; # Keep tabs on restart + SiteSettings = false; + OfflineApps = true; + Locked = true; + }; + + # Extensions + ExtensionSettings = mkExtensionSettings { + # uBlock Origin - ad blocker (pinned to navbar) + "uBlock0@raymondhill.net" = mkExtensionEntry { + id = "ublock-origin"; + pinned = true; + private_browsing = true; + }; + # KeePassXC - password manager (pinned to navbar) + "keepassxc-browser@keepassxc.org" = mkExtensionEntry { + id = "keepassxc-browser"; + pinned = true; + private_browsing = true; + }; + + # --- Privacy & Security --- + "{74145f27-f039-47ce-a470-a662b129930a}" = "clearurls"; # Remove tracking from URLs + "jid1-BoFifL9Vbdl2zQ@jetpack" = "decentraleyes"; # Local CDN emulation + "{3579f63b-d8ee-424f-bbb6-6d0ce3285e6a}" = "chameleon-ext"; # Spoof browser profile + + # --- GitHub --- + "{a4c4eda4-fb84-4a84-b4a1-f7c1cbf2a1ad}" = "refined-github-"; # GitHub enhancements + "{85860b32-02a8-431a-b2b1-40fbd64c9c69}" = "github-file-icons"; # File icons in GitHub + "github-repository-size@pranavmangal" = "gh-repo-size"; # Show repo size + + # --- YouTube --- + "{762f9885-5a13-4abd-9c77-433dcd38b8fd}" = "return-youtube-dislikes"; + }; + + # Locked preferences + Preferences = mkLockedAttrs { + "browser.aboutConfig.showWarning" = false; + "browser.tabs.warnOnClose" = false; + "media.videocontrols.picture-in-picture.video-toggle.enabled" = true; + + # Translation (auto-translate all languages except en/fr) + "browser.translations.enable" = true; + "browser.translations.panelShown" = true; + "browser.translations.automaticallyPopup" = true; + "browser.translations.alwaysTranslateLanguages" = "ar,bg,cs,da,de,el,es,et,fi,hu,id,it,ja,ko,lt,lv,nl,no,pl,pt,ro,ru,sk,sl,sv,th,tr,uk,vi,zh"; + "browser.translations.neverTranslateLanguages" = "en,fr"; + + # Privacy (balanced - removed slow options) + "privacy.spoof_english" = 1; + "network.cookie.cookieBehavior" = 5; + "dom.battery.enabled" = false; + + # Performance & Speed + "gfx.webrender.all" = true; + "layers.acceleration.force-enabled" = true; + "network.http.http3.enabled" = true; + "network.dns.disablePrefetch" = false; + "network.prefetch-next" = true; + "browser.sessionstore.interval" = 60000; # Less frequent session saves (60s) + "browser.cache.memory.enable" = true; + "browser.cache.memory.capacity" = 524288; # 512MB memory cache + "content.notify.interval" = 100000; + "ui.submenuDelay" = 0; # Instant submenus + + # Disable animations for snappiness + "toolkit.cosmeticAnimations.enabled" = false; + "browser.fullscreen.animate" = false; + }; + }; + + profiles.default = { + # Zen-specific settings + settings = { + "zen.workspaces.continue-where-left-off" = true; # Restore tabs + "zen.workspaces.natural-scroll" = true; + "zen.view.compact.hide-tabbar" = true; + "zen.view.compact.hide-toolbar" = true; + "zen.view.compact.animate-sidebar" = false; + "zen.welcome-screen.seen" = true; + "zen.urlbar.behavior" = "float"; + + # Restore previous session + "browser.startup.page" = 3; # Restore previous session + "browser.sessionstore.resume_from_crash" = true; + }; + + # --- Bookmarks --- + bookmarks = { + force = true; + settings = [ + { + name = "Dev"; + toolbar = true; + bookmarks = [ + { name = "GitHub"; url = "https://github.com"; } + { name = "NixOS Wiki"; url = "https://wiki.nixos.org/"; } + ]; + } + ]; + }; + + # --- Containers --- + # containersForce = true; + # containers = { + # Personal = { color = "blue"; icon = "fingerprint"; id = 1; }; + # Work = { color = "orange"; icon = "briefcase"; id = 2; }; + # Shopping = { color = "yellow"; icon = "dollar"; id = 3; }; + # Banking = { color = "green"; icon = "dollar"; id = 4; }; + # }; + + # --- Workspaces/Spaces --- + # spacesForce = true; + # spaces = { + # "Default" = { + # id = "generate-your-own-uuid"; # Use: uuidgen + # icon = "🏠"; + # position = 1000; + # }; + # "Work" = { + # id = "generate-your-own-uuid"; + # icon = "💼"; + # position = 1001; + # # container = containers."Work".id; # Link to container + # theme = { + # type = "gradient"; + # colors = [ + # { + # red = 100; + # green = 150; + # blue = 200; + # algorithm = "floating"; + # type = "explicit-lightness"; + # } + # ]; + # opacity = 0.5; + # texture = 0.3; + # }; + # }; + # }; + + # --- Pinned tabs --- + # pinsForce = true; + # pins = { + # "GitHub" = { + # id = "generate-your-own-uuid"; + # # workspace = spaces."Work".id; + # url = "https://github.com"; + # position = 100; + # isEssential = false; + # }; + # }; + + # --- Search engines --- + search = { + force = true; + default = "google"; + engines = let + nixIcon = "${pkgs-unstable.nixos-icons}/share/icons/hicolor/scalable/apps/nix-snowflake.svg"; + in { + "Nix Packages" = { + urls = [{ + template = "https://search.nixos.org/packages"; + params = [ + { name = "type"; value = "packages"; } + { name = "channel"; value = "unstable"; } + { name = "query"; value = "{searchTerms}"; } + ]; + }]; + icon = nixIcon; + definedAliases = [ "np" ]; + }; + "Nix Options" = { + urls = [{ + template = "https://search.nixos.org/options"; + params = [ + { name = "channel"; value = "unstable"; } + { name = "query"; value = "{searchTerms}"; } + ]; + }]; + icon = nixIcon; + definedAliases = [ "no" ]; + }; + "Home Manager Options" = { + urls = [{ + template = "https://home-manager-options.extranix.com/"; + params = [ + { name = "query"; value = "{searchTerms}"; } + { name = "release"; value = "master"; } + ]; + }]; + icon = nixIcon; + definedAliases = [ "hm" ]; + }; + # Hide unwanted default engines + "bing".metaData.hidden = true; + }; + }; + }; + }; +}