diff --git a/flake-parts/hosts/default.nix b/flake-parts/hosts/default.nix index 5fecc417..56298663 100644 --- a/flake-parts/hosts/default.nix +++ b/flake-parts/hosts/default.nix @@ -97,6 +97,7 @@ in extraOverlays = sharedOverlays ++ [ inputs.emacs-overlay.overlays.default inputs.nur.overlays.default + inputs.niri.overlays.niri # neovim-nightly-overlay.overlays.default # (final: _prev: { nur = import inputs.nur { pkgs = final; }; }) ]; @@ -115,6 +116,7 @@ in extraOverlays = sharedOverlays ++ [ inputs.emacs-overlay.overlays.default inputs.nur.overlays.default + inputs.niri.overlays.niri # neovim-nightly-overlay.overlays.default # (final: _prev: { nur = import inputs.nur { pkgs = final; }; }) ]; diff --git a/flake-parts/hosts/flatbundle/default.nix b/flake-parts/hosts/flatbundle/default.nix index 5661374c..fdda2b10 100644 --- a/flake-parts/hosts/flatbundle/default.nix +++ b/flake-parts/hosts/flatbundle/default.nix @@ -43,11 +43,13 @@ in # | ADDITIONAL SYSTEM PACKAGES | # ------------------------------ environment.systemPackages = [ - pkgs.libva-utils - pkgs.docker-compose - pkgs.wireguard-tools - pkgs.claude-code - pkgs.codex + pkgs.libva-utils # Collection of utilities and examples for VA-API + pkgs.docker-compose # Docker CLI plugin to define and run multi-container applications with Docker + pkgs.wireguard-tools # Tools for the WireGuard secure network tunnel + pkgs.claude-code # Agentic coding tool that lives in your terminal, understands your codebase, and helps you code faster + pkgs.codex # Lightweight coding agent that runs in your terminal + pkgs.bitwarden-desktop # Secure and free password manager for all of your devices + pkgs.bitwarden-cli # Secure and free password manager for all of your devices ]; # --------------------- @@ -55,7 +57,7 @@ in # --------------------- tensorfiles = { profiles = { - graphical-plasma6.enable = true; + graphical-dms-niri.enable = true; packages-base.enable = true; packages-extra.enable = true; @@ -89,6 +91,7 @@ in # nix-mineral.enable = true; programs.nh.flake = "/home/tsandrini/ProjectBundle/tsandrini/tensorfiles"; + programs.nh.clean.enable = false; # NOTE We have enough space buddy programs.fish.enable = true; users.defaultUserShell = pkgs.bash; @@ -127,7 +130,6 @@ in }; services.tailscale.enable = true; - networking.wireguard.enable = true; networking.firewall = { allowedUDPPorts = [ @@ -152,11 +154,12 @@ in home-manager.users."tsandrini" = { tensorfiles.hm = { - profiles.graphical-plasma.enable = true; + profiles.graphical-dms-niri.enable = true; + programs.pywal.enable = true; + services.pywalfox-native.enable = true; + profiles.accounts.tsandrini.enable = true; security.agenix.enable = true; - - programs.pywal.enable = true; programs.editors.emacs-doom.enable = true; services.keepassxc.enable = true; }; @@ -173,6 +176,7 @@ in programs.git.signing.key = "3E83AD690FA4F657"; # pragma: allowlist secret home.packages = [ + inputs.self.packages.${system}.cc-switcher pkgs-osu-lazer-bin.osu-lazer-bin pkgs.olympus ]; diff --git a/flake-parts/hosts/flatbundle/hardware-configuration.nix b/flake-parts/hosts/flatbundle/hardware-configuration.nix index 9f775c55..838673a8 100644 --- a/flake-parts/hosts/flatbundle/hardware-configuration.nix +++ b/flake-parts/hosts/flatbundle/hardware-configuration.nix @@ -21,7 +21,9 @@ { imports = [ (modulesPath + "/installer/scan/not-detected.nix") ]; - environment.systemPackages = [ pkgs.libva-utils ]; + environment.systemPackages = [ + pkgs.libva-utils + ]; networking.useDHCP = lib.mkDefault true; @@ -66,12 +68,21 @@ powerManagement = { enable = true; - cpuFreqGovernor = "performance"; + # cpuFreqGovernor = "schedutil"; }; programs.gamemode.enable = true; services.fwupd.enable = true; + services.fprintd = { + enable = true; + }; + + services.fstrim = { + enable = true; + interval = "weekly"; # the default + }; + boot = { loader = { timeout = 1; diff --git a/flake-parts/hosts/jetbundle/default.nix b/flake-parts/hosts/jetbundle/default.nix index dd3ed3c5..cd7f87fc 100644 --- a/flake-parts/hosts/jetbundle/default.nix +++ b/flake-parts/hosts/jetbundle/default.nix @@ -48,11 +48,13 @@ in # | ADDITIONAL SYSTEM PACKAGES | # ------------------------------ environment.systemPackages = [ - pkgs.libva-utils - pkgs.docker-compose - pkgs.wireguard-tools - pkgs.claude-code - pkgs.codex + pkgs.libva-utils # Collection of utilities and examples for VA-API + pkgs.docker-compose # Docker CLI plugin to define and run multi-container applications with Docker + pkgs.wireguard-tools # Tools for the WireGuard secure network tunnel + pkgs.claude-code # Agentic coding tool that lives in your terminal, understands your codebase, and helps you code faster + pkgs.codex # Lightweight coding agent that runs in your terminal + pkgs.bitwarden-desktop # Secure and free password manager for all of your devices + pkgs.bitwarden-cli # Secure and free password manager for all of your devices ]; # --------------------- @@ -61,12 +63,12 @@ in tensorfiles = { profiles = { graphical-dms-niri.enable = true; + packages-base.enable = true; packages-extra.enable = true; packages-graphical-extra.enable = true; }; - services.networking.ssh.enable = true; security.agenix.enable = true; # Use the `nh` garbage collect to also collect .direnv and XDG profiles @@ -93,10 +95,8 @@ in }; # nix-mineral.enable = true; - # TODO maybe use github:tsandrini/tensorfiles instead? programs.nh.flake = "/home/tsandrini/ProjectBundle/tsandrini/tensorfiles"; - # programs.shadow-client.forceDriver = "iHD"; programs.fish.enable = true; users.defaultUserShell = pkgs.bash; @@ -128,13 +128,12 @@ in secrets = [ "ipsec.d/ipsec.nm-l2tp.secrets" ]; }; - services.pcscd.enable = true; # needed for gpg pinentry - virtualisation.docker = { enable = true; autoPrune.enable = true; }; + services.tailscale.enable = true; networking.wireguard.enable = true; networking.firewall = { allowedUDPPorts = [ @@ -157,11 +156,12 @@ in ]; }; - # Small QoL for Wayland apps (optional) - home-manager.users."tsandrini" = { tensorfiles.hm = { profiles.graphical-dms-niri.enable = true; + programs.pywal.enable = true; + services.pywalfox-native.enable = true; + profiles.accounts.tsandrini.enable = true; security.agenix.enable = true; services.keepassxc.enable = true; diff --git a/flake-parts/hosts/jetbundle/hardware-configuration.nix b/flake-parts/hosts/jetbundle/hardware-configuration.nix index 6464e5c5..5b6e7014 100644 --- a/flake-parts/hosts/jetbundle/hardware-configuration.nix +++ b/flake-parts/hosts/jetbundle/hardware-configuration.nix @@ -44,12 +44,16 @@ powerManagement = { enable = true; - cpuFreqGovernor = "performance"; + # cpuFreqGovernor = "schedutil"; }; programs.gamemode.enable = true; services.fwupd.enable = true; + services.fprintd = { + enable = true; + }; + # Thinkpad x270 fingreprint reader # Unfortunately the official services.fprintd option doesn't work and any # custom tos drivers didn't work either. The only way to make it work was to diff --git a/flake-parts/hosts/spinorbundle/default.nix b/flake-parts/hosts/spinorbundle/default.nix index f2521193..93df4de1 100644 --- a/flake-parts/hosts/spinorbundle/default.nix +++ b/flake-parts/hosts/spinorbundle/default.nix @@ -29,11 +29,6 @@ inputs.nix-gaming.nixosModules.platformOptimizations (inputs.nix-mineral + "/nix-mineral.nix") - # TODO fails with The option `programs.steam.extraCompatPackages' in - # `/nix/store/nra828scc8qs92b9pxra5csqzffb6hpl-source/nixos/modules/programs/steam.nix' - # is already declared in - # `/nix/store/cqapfi5bvhzvarrbi2h1qrf2dav5r1nd-source/flake.nix#nixosModules.steamCompat'. - # nix-gaming.nixosModules.steamCompat ./hardware-configuration.nix ./disko.nix ./nm-overrides.nix @@ -43,7 +38,7 @@ # | ADDITIONAL SYSTEM PACKAGES | # ------------------------------ environment.systemPackages = [ - pkgs.networkmanagerapplet # need this to configure L2TP ipsec + pkgs.libva-utils # Collection of utilities and examples for VA-API ]; # --------------------- @@ -110,22 +105,23 @@ ]; }; - # virtualisation.docker = { - # enable = true; - # autoPrune.enable = true; - # storageDriver = "btrfs"; - # }; - - # NOTE for wireguard - # networking.wireguard.enable = true; + services.tailscale.enable = true; + networking.wireguard.enable = true; networking.firewall = { allowedUDPPorts = [ + # WG 51820 + 51821 + # Dev ports 8000 8080 5173 ]; allowedTCPPorts = [ + # WG + 51820 + 51821 + # Dev ports 8000 8080 5173 @@ -135,11 +131,9 @@ home-manager.users."tsandrini" = { tensorfiles.hm = { profiles.graphical-plasma.enable = true; - # profiles.accounts.tsandrini.enable = true; security.agenix.enable = true; programs.pywal.enable = true; - # programs.editors.emacs-doom.enable = true; services.keepassxc.enable = true; }; diff --git a/flake-parts/modules/home-manager/default.nix b/flake-parts/modules/home-manager/default.nix index e1bf718c..7dc6c42b 100644 --- a/flake-parts/modules/home-manager/default.nix +++ b/flake-parts/modules/home-manager/default.nix @@ -47,7 +47,7 @@ in profiles_base = importApply ./profiles/base.nix { inherit localFlake; }; profiles_graphical-plasma = importApply ./profiles/graphical-plasma { inherit localFlake; }; profiles_graphical-dms-niri = importApply ./profiles/graphical-dms-niri.nix { - inherit localFlake inputs; + inherit localFlake; }; profiles_graphical-xmonad = importApply ./profiles/graphical-xmonad.nix { inherit localFlake; }; profiles_headless = importApply ./profiles/headless.nix { inherit localFlake; }; @@ -66,9 +66,13 @@ in programs_file-managers_lf = importApply ./programs/file-managers/lf { inherit localFlake; }; programs_file-managers_yazi = importApply ./programs/file-managers/yazi.nix { inherit localFlake; }; programs_git = importApply ./programs/git.nix { inherit localFlake; }; + programs_dank-material-shell = importApply ./programs/dank-material-shell.nix { + inherit localFlake inputs; + }; programs_delta = importApply ./programs/delta.nix { inherit localFlake; }; programs_dsearch = importApply ./programs/dsearch.nix { inherit localFlake inputs; }; programs_gpg = importApply ./programs/gpg.nix { inherit localFlake; }; + programs_niri-flake = importApply ./programs/niri-flake.nix { inherit localFlake; }; programs_newsboat = importApply ./programs/newsboat.nix { inherit localFlake; }; programs_pywal = importApply ./programs/pywal.nix { inherit localFlake; }; programs_shadow-nix = importApply ./programs/shadow-nix.nix { inherit localFlake inputs; }; diff --git a/flake-parts/modules/home-manager/profiles/graphical-dms-niri.nix b/flake-parts/modules/home-manager/profiles/graphical-dms-niri.nix index a97e6800..ef3d253f 100644 --- a/flake-parts/modules/home-manager/profiles/graphical-dms-niri.nix +++ b/flake-parts/modules/home-manager/profiles/graphical-dms-niri.nix @@ -12,7 +12,7 @@ # 888 88888888 888 888 "Y8888b. 888 888 888 888 888 888 88888888 "Y8888b. # Y88b. Y8b. 888 888 X88 Y88..88P 888 888 888 888 Y8b. X88 # "Y888 "Y8888 888 888 88888P' "Y88P" 888 888 888 888 "Y8888 88888P' -{ localFlake, inputs }: +{ localFlake }: { pkgs, config, @@ -29,6 +29,7 @@ let optional ; inherit (localFlake.lib.modules) mkOverrideAtHmProfileLevel; + inherit (localFlake.lib.options) mkPywalEnableOption; cfg = config.tensorfiles.hm.profiles.graphical-dms-niri; _ = mkOverrideAtHmProfileLevel; @@ -46,13 +47,11 @@ in // { default = true; }; - }; - imports = [ - inputs.dms.homeModules.dank-material-shell - inputs.dms.homeModules.niri - # inputs.niri.homeModules.niri # NOTE: included in NixOS profile - ]; + pywal = { + enable = mkPywalEnableOption; + }; + }; config = mkIf cfg.enable (mkMerge [ # |----------------------------------------------------------------------| # @@ -69,23 +68,94 @@ in thunderbird.enable = _ true; + niri-flake = { + enable = _ true; + binds.dms.enable = _ true; + binds.flameshot.enable = _ true; + }; + dank-material-shell = { + enable = _ true; + niri-flake.enable = _ true; + }; dsearch.enable = _ true; }; }; home.packages = [ + # --- Fonts --- pkgs.neovide # This is a simple graphical user interface for Neovim + pkgs.nerd-fonts.jetbrains-mono # Nerd Fonts: JetBrains officially created font for developers + pkgs.nerd-fonts.symbols-only # Nerd Fonts: Just the Nerd Font Icons. I.e Symbol font only + pkgs.noto-fonts # Beautiful and free fonts for many languages + pkgs.noto-fonts-color-emoji # Color emoji font + + # --- GTK Stuff & Themes --- + pkgs.hicolor-icon-theme # Default fallback theme used by implementations of the icon theme specification + pkgs.adwaita-icon-theme + pkgs.papirus-icon-theme # Pixel perfect icon theme for Linux + pkgs.kdePackages.breeze-icons # Breeze icon theme. + + # --- GNOME apps --- + pkgs.nautilus # File manager for GNOME + pkgs.gvfs # Virtual Filesystem support library + pkgs.udiskie # Removable disk automounter for udisks + pkgs.file-roller # Archive manager for the GNOME desktop environment + pkgs.loupe # Simple image viewer application written with GTK4 and Rust + pkgs.evince # GNOME's document viewer + pkgs.tumbler # D-Bus thumbnailer service + pkgs.sushi # Quick previewer for Nautilus + pkgs.ffmpegthumbnailer # Lightweight video thumbnailer + pkgs.shared-mime-info # Database of common MIME types + pkgs.desktop-file-utils # Command line utilities for working with .desktop files + pkgs.gnome-disk-utility # Udisks graphical front-end + pkgs.totem # Movie player for the GNOME desktop based on GStreamer + pkgs.dconf-editor # GSettings editor for GNOME + pkgs.kooha # Elegantly record your screen + pkgs.gnome-calculator # Application that solves mathematical equations and is suitable as a default application in a Desktop environment + pkgs.snapshot # Take pictures and videos on your computer, tablet, or phone + pkgs.baobab # Graphical application to analyse disk usage in any GNOME environment + pkgs.gnome-connections # Remote desktop client for the GNOME desktop environment + pkgs.gnome-clocks # Simple and elegant clock application for GNOME + pkgs.gnome-console # Simple user-friendly terminal emulator for the GNOME desktop + pkgs.gnome-characters # Simple utility application to find and insert unusual characters + pkgs.gnome-logs # Log viewer for the systemd journal + pkgs.gnome-font-viewer # Program that can preview fonts and create thumbnails for fonts + pkgs.gnome-maps # Map application for GNOME 3 + pkgs.gnome-music # Music player and management application for the GNOME desktop environment + pkgs.gnome-weather # Access current weather conditions and forecasts + # pkgs.constrict # Compresses your videos to your chosen file size + pkgs.gnome-decoder # Scan and generate QR codes + pkgs.curtail # Simple & useful image compressor + pkgs.deja-dup # Simple backup tool + pkgs.impression # Straight-forward and modern application to create bootable drives + pkgs.tuba # Browse the Fediverse + pkgs.wike # Wikipedia Reader for the GNOME Desktop + pkgs.lorem # Generate placeholder text + + # --- KDE apps --- + # NOTE: I have these mostly just for kdeconnect to work and be able to mount the drives + pkgs.kdePackages.qt6ct # Qt6 Configuration Tool + pkgs.kdePackages.ark # File archiver by KDE + pkgs.kdePackages.kio # KIO + pkgs.kdePackages.kio-extras # Additional components to increase the functionality of KIO + pkgs.kdePackages.kio-fuse # FUSE Interface for KIO + pkgs.kdePackages.dolphin # File manager by KDE + # pkgs.kdePackages.gwenview # Image viewer by KDE + # pkgs.qimgv # Qt6 image viewer with optional video support + # pkgs.imv # Command line image viewer for tiling window managers + + # --- General apps --- + pkgs.mpv # General-purpose media player, fork of MPlayer and mplayer2 + pkgs.vlc # Cross-platform media player and streaming server + pkgs.pavucontrol # PulseAudio Volume Control + pkgs.blueman # GTK-based Bluetooth Manager + pkgs.matugen # Material you color generation tool + + # pkgs.python3Packages.aiohttp-oauthlib # NOTE: required for calendar integration + ] ++ (optional cfg.include-nvim localFlake.packages.${system}.nvim-ide-config); - services.flameshot = { - enable = _ true; - settings = { - General.showStartupLaunchMessage = _ false; - General.useGrimAdapter = _ true; - }; - }; - home.shellAliases = { "graphical-nvim" = _ (getExe localFlake.packages.${system}.nvim-graphical-config); "ide-nvim" = _ (getExe localFlake.packages.${system}.nvim-ide-config); @@ -97,36 +167,101 @@ in TERMINAL = _ "wezterm"; IDE = _ "nvim"; EMAIL = _ "thunderbird"; + QT_QPA_PLATFORMTHEME = _ "qt6ct"; }; fonts.fontconfig.enable = _ true; - programs.dank-material-shell = { + gtk = { enable = _ true; - systemd = { - enable = true; # Systemd service for auto-start - restartIfChanged = true; # Auto-restart dms.service when dank-material-shell changes + theme = { + name = _ "adw-gtk3-dark"; + package = _ pkgs.adw-gtk3; }; - niri = { - enableSpawn = _ false; # Auto-start DMS with niri, if enabled - enableKeybinds = _ true; - includes = { - enable = _ false; - originalFileName = "hm"; - override = true; - }; + iconTheme = { + name = _ "Papirus-Dark"; + package = _ pkgs.papirus-icon-theme; }; + }; + + services.udiskie = { + enable = _ true; + automount = _ false; + notify = _ true; + tray = _ "auto"; + }; - enableSystemMonitoring = _ true; - enableVPN = _ true; - enableDynamicTheming = _ true; - enableAudioWavelength = _ true; - enableCalendarEvents = _ true; - enableClipboardPaste = _ true; + services.polkit-gnome.enable = _ true; + + services.kdeconnect = { + enable = _ true; + indicator = _ true; }; + # TODO move this elsewhere + + xdg.mimeApps.defaultApplications = { + "application/pdf" = [ "org.gnome.Evince.desktop" ]; + "application/x-pdf" = [ "org.gnome.Evince.desktop" ]; + + "image/png" = [ "org.gnome.Loupe.desktop" ]; + "image/jpeg" = [ "org.gnome.Loupe.desktop" ]; + "image/webp" = [ "org.gnome.Loupe.desktop" ]; + "image/gif" = [ "org.gnome.Loupe.desktop" ]; + "image/svg+xml" = [ "org.gnome.Loupe.desktop" ]; + "image/bmp" = [ "org.gnome.Loupe.desktop" ]; + "image/tiff" = [ "org.gnome.Loupe.desktop" ]; + + "application/zip" = [ "org.gnome.FileRoller.desktop" ]; + "application/x-tar" = [ "org.gnome.FileRoller.desktop" ]; + "application/x-7z-compressed" = [ "org.gnome.FileRoller.desktop" ]; + "application/x-rar" = [ "org.gnome.FileRoller.desktop" ]; + + "video/mp4" = [ "org.gnome.Totem.desktop" ]; + "video/x-matroska" = [ "org.gnome.Totem.desktop" ]; # mkv + "video/webm" = [ "org.gnome.Totem.desktop" ]; + "video/quicktime" = [ "org.gnome.Totem.desktop" ]; # mov + "video/x-msvideo" = [ "org.gnome.Totem.desktop" ]; # avi + "video/x-ms-wmv" = [ "org.gnome.Totem.desktop" ]; # wmv + "video/mpeg" = [ "org.gnome.Totem.desktop" ]; # mpg/mpeg + "video/ogg" = [ "org.gnome.Totem.desktop" ]; + "video/x-flv" = [ "org.gnome.Totem.desktop" ]; + + "application/gzip" = [ "org.gnome.FileRoller.desktop" ]; + "application/x-bzip2" = [ "org.gnome.FileRoller.desktop" ]; + "application/x-xz" = [ "org.gnome.FileRoller.desktop" ]; + "application/x-compressed-tar" = [ "org.gnome.FileRoller.desktop" ]; + "application/x-zip-compressed" = [ "org.gnome.FileRoller.desktop" ]; + + "text/html" = [ "firefox.desktop" ]; + "application/xhtml+xml" = [ "firefox.desktop" ]; + "x-scheme-handler/http" = [ "firefox.desktop" ]; + "x-scheme-handler/https" = [ "firefox.desktop" ]; + "x-scheme-handler/about" = [ "firefox.desktop" ]; + "x-scheme-handler/unknown" = [ "firefox.desktop" ]; + + "inode/directory" = [ "org.gnome.Nautilus.desktop" ]; + + # # Text / code -> your Neovim desktop entry + "text/plain" = [ "neovide.desktop" ]; + "text/markdown" = [ "neovide.desktop" ]; + "application/json" = [ "neovide.desktop" ]; + "application/x-yaml" = [ "neovide.desktop" ]; + "text/x-python" = [ "neovide.desktop" ]; + "text/x-shellscript" = [ "neovide.desktop" ]; + "text/x-csrc" = [ "neovide.desktop" ]; + "text/x-chdr" = [ "neovide.desktop" ]; + "text/x-c++src" = [ "neovide.desktop" ]; + "text/x-c++hdr" = [ "neovide.desktop" ]; + "text/x-rust" = [ "neovide.desktop" ]; + "text/x-go" = [ "neovide.desktop" ]; + "text/x-toml" = [ "neovide.desktop" ]; + "text/x-nix" = [ "neovide.desktop" ]; + "application/xml" = [ "neovide.desktop" ]; + "text/xml" = [ "neovide.desktop" ]; + }; } # |----------------------------------------------------------------------| # ]); diff --git a/flake-parts/modules/home-manager/programs/dank-material-shell.nix b/flake-parts/modules/home-manager/programs/dank-material-shell.nix new file mode 100644 index 00000000..0772ea6a --- /dev/null +++ b/flake-parts/modules/home-manager/programs/dank-material-shell.nix @@ -0,0 +1,99 @@ +# --- flake-parts/modules/home-manager/programs/dank-material-shell.nix +# +# Author: tsandrini +# URL: https://github.com/tsandrini/tensorfiles +# License: MIT +# +# 888 .d888 d8b 888 +# 888 d88P" Y8P 888 +# 888 888 888 +# 888888 .d88b. 88888b. .d8888b .d88b. 888d888 888888 888 888 .d88b. .d8888b +# 888 d8P Y8b 888 "88b 88K d88""88b 888P" 888 888 888 d8P Y8b 88K +# 888 88888888 888 888 "Y8888b. 888 888 888 888 888 888 88888888 "Y8888b. +# Y88b. Y8b. 888 888 X88 Y88..88P 888 888 888 888 Y8b. X88 +# "Y888 "Y8888 888 888 88888P' "Y88P" 888 888 888 888 "Y8888 88888P' +{ localFlake, inputs }: +{ config, lib, ... }: +let + inherit (lib) mkIf mkMerge mkEnableOption; + inherit (localFlake.lib.modules) mkOverrideAtHmModuleLevel; + inherit (localFlake.lib.modules) isModuleLoadedAndEnabled; + inherit (localFlake.lib.options) mkPywalEnableOption; + + cfg = config.tensorfiles.hm.programs.dank-material-shell; + _ = mkOverrideAtHmModuleLevel; + + pywalCheck = (isModuleLoadedAndEnabled config "tensorfiles.hm.programs.pywal") && cfg.pywal.enable; + niri-flakeCheck = + (isModuleLoadedAndEnabled config "tensorfiles.hm.programs.niri-flake") && cfg.niri-flake.enable; +in +{ + options.tensorfiles.hm.programs.dank-material-shell = { + enable = mkEnableOption '' + + ''; + + niri-flake = { + enable = mkEnableOption "Enables binding for the niri-flake project"; + }; + + pywal = { + enable = mkPywalEnableOption; + }; + }; + + imports = [ + inputs.dms.homeModules.dank-material-shell + inputs.dms.homeModules.niri # TODO No better place to have this unfortunately + ]; + + config = mkIf cfg.enable (mkMerge [ + # |----------------------------------------------------------------------| # + { + programs.dank-material-shell = { + enable = _ true; + systemd = { + enable = _ (!niri-flakeCheck); + restartIfChanged = _ true; + }; + + enableSystemMonitoring = _ true; + enableVPN = _ true; + enableDynamicTheming = _ true; + enableAudioWavelength = _ true; + enableCalendarEvents = _ true; + enableClipboardPaste = _ true; + }; + } + # |----------------------------------------------------------------------| # + (mkIf pywalCheck { + systemd.user.tmpfiles.rules = [ + "d ${config.xdg.cacheHome}/wal 0700 - - -" + "L+ ${config.xdg.cacheHome}/wal/colors.json - - - - ${config.xdg.cacheHome}/wal/dank-pywalfox.json" + ]; + }) + # |----------------------------------------------------------------------| # + (mkIf niri-flakeCheck { + programs.dank-material-shell.niri = { + enableSpawn = _ true; + enableKeybinds = _ false; + includes = { + enable = _ true; + override = _ true; + filesToInclude = [ + "alttab" + "binds" + "cursor" + "colors" + "layout" + "outputs" + "wpblur" + ]; + }; + }; + }) + # |----------------------------------------------------------------------| # + ]); + + meta.maintainers = with localFlake.lib.maintainers; [ tsandrini ]; +} diff --git a/flake-parts/modules/home-manager/programs/dsearch.nix b/flake-parts/modules/home-manager/programs/dsearch.nix index 3dd9e55f..31a3ead4 100644 --- a/flake-parts/modules/home-manager/programs/dsearch.nix +++ b/flake-parts/modules/home-manager/programs/dsearch.nix @@ -83,6 +83,21 @@ in "target" ]; } + { + path = config.home.sessionVariables.DOWNLOADS_DIR; + max_depth = 3; + exclude_hidden = true; + exclude_dirs = [ + ".git" + "target" + "dist" + "node_modules" + ".direnv" + ".devenv" + "venv" + "target" + ]; + } { path = config.home.sessionVariables.MISC_DATA_DIR; max_depth = 6; diff --git a/flake-parts/modules/home-manager/programs/niri-flake.nix b/flake-parts/modules/home-manager/programs/niri-flake.nix new file mode 100644 index 00000000..88e0b3fe --- /dev/null +++ b/flake-parts/modules/home-manager/programs/niri-flake.nix @@ -0,0 +1,384 @@ +# --- flake-parts/modules/home-manager/programs/niri-flake.nix +# +# Author: tsandrini +# URL: https://github.com/tsandrini/tensorfiles +# License: MIT +# +# 888 .d888 d8b 888 +# 888 d88P" Y8P 888 +# 888 888 888 +# 888888 .d88b. 88888b. .d8888b .d88b. 888d888 888888 888 888 .d88b. .d8888b +# 888 d8P Y8b 888 "88b 88K d88""88b 888P" 888 888 888 d8P Y8b 88K +# 888 88888888 888 888 "Y8888b. 888 888 888 888 888 888 88888888 "Y8888b. +# Y88b. Y8b. 888 888 X88 Y88..88P 888 888 888 888 Y8b. X88 +# "Y888 "Y8888 888 888 88888P' "Y88P" 888 888 888 888 "Y8888 88888P' +{ localFlake }: +{ + config, + lib, + pkgs, + ... +}: +let + inherit (lib) + mkIf + mkMerge + mkEnableOption + mkOption + types + ; + inherit (localFlake.lib.modules) mkOverrideAtHmModuleLevel; + + cfg = config.tensorfiles.hm.programs.niri-flake; + _ = mkOverrideAtHmModuleLevel; + + toggleEdp = pkgs.writeShellScriptBin "toggle-edp" '' + set -euo pipefail + + EDP="eDP-1" + + # Extract the block for eDP-1 (from its "Output ..." header until the next "Output ..." or EOF) + block="$( + niri msg outputs \ + | awk -v edp="(''${EDP})" ' + $0 ~ "^Output " { + in_block = ($0 ~ edp) + } + in_block { print } + ' + )" + + if [ -z "$block" ]; then + exit 0 + fi + + if echo "$block" | grep -q "^[[:space:]]*Disabled[[:space:]]*$"; then + niri msg output "$EDP" on + else + niri msg output "$EDP" off + fi + ''; +in +{ + options.tensorfiles.hm.programs.niri-flake = { + enable = mkEnableOption '' + TODO + ''; + + binds = { + mod = mkOption { + type = types.str; + default = "Mod"; + description = "Default modkey to be used"; + }; + + dms = { + enable = mkEnableOption "Enables various default DMS keybinds"; + }; + + flameshot = { + enable = mkEnableOption "Enables flameshot as the screenshot backend for niri"; + }; + }; + }; + + imports = [ + # TODO: This is problematic, we would ideally import `inputs.niri.homeModules.niri` + # however, `inputs.dms.homeModules.niri` also includes this module and that needs + # to be imported as well, importing both leads to conflict so this leads us + # with only a single option to import only `inputs.dms.homeModules.niri`, but + # we logically can't do that here => we import nothing 💀💀💀 + + # inputs.niri.homeModules.niri + ]; + + config = mkIf cfg.enable (mkMerge [ + # |----------------------------------------------------------------------| # + { + home.packages = [ toggleEdp ]; + + programs.niri = { + package = _ pkgs.niri-unstable; + settings = { + prefer-no-csd = _ true; + workspaces = { + "01" = { + name = _ "1"; + }; + "02" = { + name = _ "2"; + }; + "03" = { + name = _ "3"; + }; + "04" = { + name = _ "4"; + }; + "05" = { + name = _ "5"; + }; + "06" = { + name = _ "6"; + }; + "07" = { + name = _ "7"; + }; + "08" = { + name = _ "8"; + }; + }; + + input = { + keyboard = { + xkb = { + layout = _ "us,cz"; + variant = _ ",qwerty"; + options = _ "grp:alt_caps_toggle"; + }; + + track-layout = _ "global"; + }; + }; + + binds = + let + a = config.lib.niri.actions; + in + { + # --- Columns --- + "${cfg.binds.mod}+H".action = _ a.focus-column-left; + "${cfg.binds.mod}+J".action = _ a.focus-window-down; + "${cfg.binds.mod}+K".action = _ a.focus-window-up; + "${cfg.binds.mod}+L".action = _ a.focus-column-right; + + "${cfg.binds.mod}+MouseBack".action = _ a.focus-column-left; + "${cfg.binds.mod}+MouseForward".action = _ a.focus-column-right; + + # --- Workspaces --- + "${cfg.binds.mod}+U".action = _ a.focus-workspace-down; + "${cfg.binds.mod}+I".action = _ a.focus-workspace-up; + + "${cfg.binds.mod}+WheelScrollDown" = { + action = _ a.focus-workspace-down; + cooldown-ms = _ 150; + }; + "${cfg.binds.mod}+WheelScrollUp" = { + action = _ a.focus-workspace-up; + cooldown-ms = _ 150; + }; + + # --- Moving stuff --- + "${cfg.binds.mod}+Shift+H".action = _ a.move-column-left; + "${cfg.binds.mod}+Shift+J".action = _ a.move-window-down; + "${cfg.binds.mod}+Shift+K".action = _ a.move-window-up; + "${cfg.binds.mod}+Shift+L".action = _ a.move-column-right; + + # --- Resizing windowws ---- + "${cfg.binds.mod}+Left".action = _ (a.set-column-width "-10%"); + "${cfg.binds.mod}+Right".action = _ (a.set-column-width "+10%"); + "${cfg.binds.mod}+Up".action = _ (a.set-window-height "-10%"); + "${cfg.binds.mod}+Down".action = _ (a.set-window-height "+10%"); + + # --- Windows and columns manipulation --- + "${cfg.binds.mod}+F".action = _ a.maximize-column; + "${cfg.binds.mod}+T".action = _ a.toggle-window-floating; + "${cfg.binds.mod}+R".action = _ a.switch-preset-column-width; + "${cfg.binds.mod}+Comma".action = _ a.consume-or-expel-window-right; + + # --- Workspaces --- + "${cfg.binds.mod}+Tab".action = _ a.focus-workspace-previous; + + "${cfg.binds.mod}+1".action = _ (a.focus-workspace 1); + "${cfg.binds.mod}+2".action = _ (a.focus-workspace 2); + "${cfg.binds.mod}+3".action = _ (a.focus-workspace 3); + "${cfg.binds.mod}+4".action = _ (a.focus-workspace 4); + "${cfg.binds.mod}+5".action = _ (a.focus-workspace 5); + "${cfg.binds.mod}+6".action = _ (a.focus-workspace 6); + "${cfg.binds.mod}+7".action = _ (a.focus-workspace 7); + "${cfg.binds.mod}+8".action = _ (a.focus-workspace 8); + "${cfg.binds.mod}+9".action = _ (a.focus-workspace 9); + + # --- Apps --- + "${cfg.binds.mod}+Q".action = _ a.close-window; + "${cfg.binds.mod}+W".action = _ a.toggle-overview; + "${cfg.binds.mod}+Return".action = _ (a.spawn config.home.sessionVariables.TERMINAL); + + "XF86Display" = { + action = _ (a.spawn [ "toggle-edp" ]); + allow-when-locked = _ true; + }; + "Mod+F7" = { + action = _ (a.spawn [ "toggle-edp" ]); + allow-when-locked = _ true; + }; + }; + }; + }; + } + # |----------------------------------------------------------------------| # + (mkIf cfg.binds.dms.enable { + programs.niri.settings.binds = + let + a = config.lib.niri.actions; + dms = + cmd: + _ ( + a.spawn ( + [ + "dms" + "ipc" + "call" + ] + ++ cmd + ) + ); + in + { + "Ctrl+Alt+Q".action = dms [ + "powermenu" + "toggle" + ]; + + # DMS toggles + "${cfg.binds.mod}+Space".action = dms [ + "spotlight" + "toggle" + ]; + "${cfg.binds.mod}+V".action = dms [ + "clipboard" + "toggle" + ]; + "${cfg.binds.mod}+M".action = dms [ + "processlist" + "toggle" + ]; + "${cfg.binds.mod}+P".action = dms [ + "notepad" + "toggle" + ]; + "${cfg.binds.mod}+Shift+Q".action = dms [ + "" + "toggle" + ]; + "${cfg.binds.mod}+N".action = dms [ + "notifications" + "toggle" + ]; + "Ctrl+Alt+L".action = dms [ + "lock" + "lock" + ]; + + # --- Media keys via DMS IPC --- + "XF86AudioRaiseVolume" = { + action = dms [ + "audio" + "increment" + "5" + ]; + allow-when-locked = _ true; + }; + "XF86AudioLowerVolume" = { + action = dms [ + "audio" + "decrement" + "5" + ]; + allow-when-locked = _ true; + }; + "XF86AudioMute" = { + action = dms [ + "audio" + "mute" + ]; + allow-when-locked = _ true; + }; + "XF86AudioMicMute" = { + action = dms [ + "audio" + "micmute" + ]; + allow-when-locked = _ true; + }; + + # Media playback via DMS (MPRIS) + "XF86AudioPlay".action = dms [ + "mpris" + "playPause" + ]; + "XF86AudioPause".action = dms [ + "mpris" + "pause" + ]; + "XF86AudioNext".action = dms [ + "mpris" + "next" + ]; + "XF86AudioPrev".action = dms [ + "mpris" + "previous" + ]; + "XF86AudioStop".action = dms [ + "mpris" + "stop" + ]; + + "XF86MonBrightnessUp".action = dms [ + "brightness" + "increment" + "10" + "" + ]; + "XF86MonBrightnessDown".action = dms [ + "brightness" + "decrement" + "10" + "" + ]; + }; + }) + # |----------------------------------------------------------------------| # + (mkIf cfg.binds.flameshot.enable { + services.flameshot = { + enable = _ true; + package = pkgs.flameshot.override { + enableWlrSupport = true; + }; + settings = _ { + General = { + showStartupLaunchMessage = false; + useGrimAdapter = true; + }; + }; + }; + + programs.niri.settings.binds = + let + a = config.lib.niri.actions; + in + { + "Print".action = _ ( + a.spawn [ + "flameshot" + "gui" + ] + ); + "Ctrl+Print".action = _ ( + a.spawn [ + "flameshot" + "gui" + ] + ); + "Alt+Print".action = _ ( + a.spawn [ + "flameshot" + "gui" + ] + ); + }; + }) + # |----------------------------------------------------------------------| # + ]); + + meta.maintainers = with localFlake.lib.maintainers; [ tsandrini ]; +} diff --git a/flake-parts/modules/home-manager/programs/shells/fish.nix b/flake-parts/modules/home-manager/programs/shells/fish.nix index 8c51b2d8..a93f487f 100644 --- a/flake-parts/modules/home-manager/programs/shells/fish.nix +++ b/flake-parts/modules/home-manager/programs/shells/fish.nix @@ -193,11 +193,11 @@ in }) # |----------------------------------------------------------------------| # (mkIf ((isModuleLoadedAndEnabled config "tensorfiles.hm.programs.pywal") && cfg.pywal.enable) { - programs.fish.interactiveShellInit = mkBefore '' - # Import colorscheme from 'wal' asynchronously - set -l wal_seq (cat ${config.xdg.cacheHome}/wal/sequences)"" - echo -e $wal_seq & - ''; + # programs.fish.interactiveShellInit = mkBefore '' + # # Import colorscheme from 'wal' asynchronously + # set -l wal_seq (cat ${config.xdg.cacheHome}/wal/sequences)"" + # echo -e $wal_seq & + # ''; }) # |----------------------------------------------------------------------| # (mkIf cfg.shellAliases.catToBat { diff --git a/flake-parts/modules/home-manager/programs/terminals/wezterm.nix b/flake-parts/modules/home-manager/programs/terminals/wezterm.nix index c72ecd44..a29acd88 100644 --- a/flake-parts/modules/home-manager/programs/terminals/wezterm.nix +++ b/flake-parts/modules/home-manager/programs/terminals/wezterm.nix @@ -53,10 +53,20 @@ in local modal = wezterm.plugin.require("https://github.com/MLFlexer/modal.wezterm") ${ - if pywalCheck then + if pywalCheck then # TODO '' - wezterm.add_to_config_reload_watch_list("~/.cache/wal") - config.color_scheme_dirs = {"~/.cache/wal"} + local home = wezterm.home_dir + local wal_dir = home .. "/.cache/wal" + + -- wezterm.add_to_config_reload_watch_list(wal_dir) + -- config.color_scheme_dirs = { wal_dir } + + local scheme_path = wezterm.config_dir .. "/colors/dank-theme.toml" + local colors, _meta = wezterm.color.load_scheme(scheme_path) + config.colors = colors + config.color_scheme = "dank-theme" + + wezterm.add_to_config_reload_watch_list(scheme_path) '' else "" diff --git a/flake-parts/modules/home-manager/services/pywalfox-native.nix b/flake-parts/modules/home-manager/services/pywalfox-native.nix index 8ffe3e4f..79aae745 100644 --- a/flake-parts/modules/home-manager/services/pywalfox-native.nix +++ b/flake-parts/modules/home-manager/services/pywalfox-native.nix @@ -20,32 +20,67 @@ ... }: let - inherit (lib) mkIf mkMerge mkEnableOption; - - # pywalfox-wrapper = pkgs.writeShellScriptBin "pywalfox-wrapper" '' - # ${pywalfox-native}/bin/pywalfox start - # ''; + inherit (lib) + mkIf + mkMerge + mkEnableOption + getExe + mkPackageOption + ; cfg = config.tensorfiles.hm.services.pywalfox-native; + + pywalfoxUpdateHandleSocket = pkgs.writeShellScript "pywalfox-update-handle-socket" '' + set -euo pipefail + + # TODO: Not sure why, but thunderbird creates a stale socket and the client + # is then unable to send the update commands to the native messaging hosts + # so I temporarily just delete it + sock=/tmp/pywalfox_socket + if [ ! -S "$sock" ]; then + rm -f $sock 2>/dev/null + fi + + exec ${getExe cfg.package} update + ''; in { options.tensorfiles.hm.services.pywalfox-native = { - enable = mkEnableOption '' - Enables NixOS module that configures/handles terminals.kitty colorscheme generator. - ''; + enable = mkEnableOption "Enable pywalfox-native helpers"; + package = mkPackageOption pkgs "pywalfox-native" { }; }; - # TODO config = mkIf cfg.enable (mkMerge [ - # |----------------------------------------------------------------------| # { - home.packages = with pkgs; [ pywalfox-native ]; + home.packages = [ cfg.package ]; + + systemd.user.services.pywalfox-update = { + Unit = { + Description = "Update Pywalfox theme"; + PartOf = [ "graphical-session.target" ]; + After = [ "graphical-session.target" ]; + }; + Service = { + Type = "oneshot"; + ExecStart = pywalfoxUpdateHandleSocket; + PrivateTmp = false; + }; + }; - # home.file.".mozilla/native-messaging-hosts/pywalfox.json".text = replaceStrings [ "" ] [ - # "${pywalfox-wrapper}/bin/pywalfox-wrapper" - # ] (readFile "${pywalfox-native}/lib/python3.11/site-packages/pywalfox/assets/manifest.json"); + systemd.user.paths.pywalfox-update = { + Unit = { + Description = "Run pywalfox update when wal colors.json changes"; + PartOf = [ "graphical-session.target" ]; + }; + Path = { + PathChanged = "%h/.cache/wal/colors.json"; + Unit = "pywalfox-update.service"; + }; + Install = { + WantedBy = [ "graphical-session.target" ]; + }; + }; } - # |----------------------------------------------------------------------| # ]); meta.maintainers = with localFlake.lib.maintainers; [ tsandrini ]; diff --git a/flake-parts/modules/nixos/networking/firewall/subnets-firewall.nix b/flake-parts/modules/nixos/networking/firewall/subnets-firewall.nix index 39067e47..1ae88c2e 100644 --- a/flake-parts/modules/nixos/networking/firewall/subnets-firewall.nix +++ b/flake-parts/modules/nixos/networking/firewall/subnets-firewall.nix @@ -37,16 +37,61 @@ let cfg = config.tensorfiles.networking.firewall.subnets-firewall; _ = mkOverrideAtModuleLevel; + policyType = types.submodule (_: { + options = { + allowedTCPPorts = mkOption { + type = types.listOf types.port; + default = [ ]; + }; + allowedUDPPorts = mkOption { + type = types.listOf types.port; + default = [ ]; + }; + + allowedTCPPortRanges = mkOption { + type = types.listOf ( + types.submodule (_: { + options = { + from = mkOption { type = types.port; }; + to = mkOption { type = types.port; }; + }; + }) + ); + default = [ ]; + }; + + allowedUDPPortRanges = mkOption { + type = types.listOf ( + types.submodule (_: { + options = { + from = mkOption { type = types.port; }; + to = mkOption { type = types.port; }; + }; + }) + ); + default = [ ]; + }; + }; + }); + isV6 = cidr: hasInfix ":" cidr; - subnetsV4 = filterAttrs (cidr: _: !isV6 cidr) cfg.subnets; - subnetsV6 = filterAttrs (cidr: _: isV6 cidr) cfg.subnets; + defaultSubnetsRendered = builtins.listToAttrs ( + map (cidr: { + name = cidr; + value = cfg.defaultSubnets; + }) cfg.defaultSubnetsList + ); + + effectiveSubnets = defaultSubnetsRendered // cfg.subnets; + + subnetsV4 = filterAttrs (cidr: _: !isV6 cidr) effectiveSubnets; + subnetsV6 = filterAttrs (cidr: _: isV6 cidr) effectiveSubnets; # ----- helpers to collect unions across *all* subnets (v4+v6) ----- - allPolicies = attrValues cfg.subnets; + allPolicies = attrValues effectiveSubnets; unionPorts = protoKey: unique (flatten (map (p: p.${protoKey}) allPolicies)); - unionRanges = rangeKey: unique (flatten (map (p: p.${rangeKey}) allPolicies)); allTcpPorts = unionPorts "allowedTCPPorts"; @@ -56,12 +101,9 @@ let # ----- iptables rendering ----- iptActionTcp4 = if cfg.defaultAction == "reject" then "REJECT --reject-with tcp-reset" else "DROP"; - iptActionUdp4 = if cfg.defaultAction == "reject" then "REJECT --reject-with icmp-port-unreachable" else "DROP"; - iptActionTcp6 = if cfg.defaultAction == "reject" then "REJECT --reject-with tcp-reset" else "DROP"; - iptActionUdp6 = if cfg.defaultAction == "reject" then "REJECT --reject-with icmp6-port-unreachable" else "DROP"; @@ -224,49 +266,78 @@ in description = "What to do with non-allowlisted traffic for the declared ports/ranges."; }; - subnets = mkOption { - type = types.attrsOf ( - types.submodule (_: { - options = { - allowedTCPPorts = mkOption { - type = types.listOf types.port; - default = [ ]; - }; - allowedUDPPorts = mkOption { - type = types.listOf types.port; - default = [ ]; - }; + # passthrough to NixOS networking.firewall allowed* ports/ranges + nixosPassthrough = mkOption { + type = policyType; + default = { }; + description = '' + Pass-through for NixOS `networking.firewall.allowed*` options (global, non-subnet-scoped). + Useful to keep all firewall declarations under this module. + ''; + example = { + allowedTCPPorts = [ + 22 + 443 + ]; + allowedUDPPorts = [ 53 ]; + allowedTCPPortRanges = [ + { + from = 8000; + to = 8080; + } + ]; + }; + }; - allowedTCPPortRanges = mkOption { - type = types.listOf ( - types.submodule (_: { - options = { - from = mkOption { type = types.port; }; - to = mkOption { type = types.port; }; - }; - }) - ); - default = [ ]; - }; + defaultSubnetsList = mkOption { + type = types.listOf types.str; + default = [ ]; + description = '' + List of CIDRs that should automatically receive the policy defined in + `defaultSubnets`. These are materialized into `subnets` at eval time, + and any explicitly defined `subnets.` entry overrides the default. + ''; + example = [ + "10.5.0.0/24" + "10.0.33.13/32" + "10.0.0.0/24" + ]; + }; - allowedUDPPortRanges = mkOption { - type = types.listOf ( - types.submodule (_: { - options = { - from = mkOption { type = types.port; }; - to = mkOption { type = types.port; }; - }; - }) - ); - default = [ ]; - }; - }; - }) - ); + defaultSubnets = mkOption { + type = policyType; + default = { }; + description = '' + The policy applied to every CIDR in `defaultSubnetsList`. + (Same schema as a single `subnets.` entry.) + ''; + example = { + allowedTCPPorts = [ + 22 + 2222 + ]; + allowedUDPPorts = [ + 80 + 443 + ]; + allowedTCPPortRanges = [ + { + from = 8000; + to = 8080; + } + ]; + }; + }; + + subnets = mkOption { + type = types.attrsOf policyType; default = { }; description = '' Attrset keyed by CIDR (IPv4 or IPv6). Each entry defines ports (and port ranges) that are reachable *only* from that CIDR. + + Note: defaults from `defaultSubnetsList/defaultSubnets` are merged in automatically, + and explicit entries here override those defaults on key collision. ''; example = { "10.10.0.0/24" = { @@ -289,6 +360,17 @@ in networking.firewall.enable = _ true; } # |----------------------------------------------------------------------| # + { + networking.firewall = { + inherit (cfg.nixosPassthrough) + allowedTCPPorts + allowedUDPPorts + allowedTCPPortRanges + allowedUDPPortRanges + ; + }; + } + # |----------------------------------------------------------------------| # (mkIf (!config.networking.nftables.enable) { networking.firewall.extraCommands = lib.mkAfter iptablesBlock; networking.firewall.extraStopCommands = lib.mkAfter iptablesStopBlock; @@ -300,7 +382,7 @@ in ${lib.concatMapStringsSep "\n" ( cidr: let - pol = cfg.subnets.${cidr}; + pol = effectiveSubnets.${cidr}; tcpPorts = pol.allowedTCPPorts or [ ]; udpPorts = pol.allowedUDPPorts or [ ]; tcpRanges = pol.allowedTCPPortRanges or [ ]; @@ -327,9 +409,11 @@ in insert rule inet nixos-fw input-allow ${saddr} ${cidr} udp dport ${mkPortSet udpPorts udpRanges} accept ''} '' - ) (lib.attrNames cfg.subnets)} + ) (lib.attrNames effectiveSubnets)} ''; }) # |----------------------------------------------------------------------| # ]); + + meta.maintainers = with localFlake.lib.maintainers; [ tsandrini ]; } diff --git a/flake-parts/modules/nixos/profiles/graphical-dms-niri.nix b/flake-parts/modules/nixos/profiles/graphical-dms-niri.nix index 420d8d04..26ad0e92 100644 --- a/flake-parts/modules/nixos/profiles/graphical-dms-niri.nix +++ b/flake-parts/modules/nixos/profiles/graphical-dms-niri.nix @@ -49,65 +49,33 @@ in networking.nftables.enable = _ true; networking.firewall.enable = _ true; - environment.systemPackages = with pkgs; [ + environment.systemPackages = [ # -- GENERAL PACKAGES -- - libnotify # A library that sends desktop notifications to a notification daemon - notify-desktop # Little application that lets you send desktop notifications with one command - wl-clipboard # Command-line copy/paste utilities for Wayland - maim # A command-line screenshot utility - xxdiff # Graphical file and directories comparator and merge tool - networkmanagerapplet # need this to configure L2TP ipsec + pkgs.libnotify # A library that sends desktop notifications to a notification daemon + pkgs.notify-desktop # Little application that lets you send desktop notifications with one command + pkgs.wl-clipboard # Command-line copy/paste utilities for Wayland + pkgs.maim # A command-line screenshot utility + pkgs.xxdiff # Graphical file and directories comparator and merge tool + pkgs.networkmanagerapplet # need this to configure L2TP ipsec # -- UTILS NEEDED FOR INFO-CENTER -- - clinfo # Print all known information about all available OpenCL platforms and devices in the system - mesa-demos # Test utilities for OpenGL - vulkan-tools # Khronos official Vulkan Tools and Utilities - wayland-utils # Wayland utilities (wayland-info) - aha # ANSI HTML Adapter - - # -- KDE PACKAGES -- - # kdePackages.ark # Graphical file compression/decompression utility - # haruna # Open source video player built with Qt/QML and libmpv - # kdePackages.kate # Advanced text editor - # kdePackages.kcalc # Scientific calculator - # kdiff3 # Compares and merges 2 or 3 files or directories - # krename # A powerful batch renamer for KDE - # krusader # Norton/Total Commander clone for KDE - # kdePackages.filelight # Disk usage statistics - # kdePackages.kfind # File search utility by KDE - # kdePackages.kweather - # # kdePackages.kweathercore - # kdePackages.quazip # Provides access to ZIP archives from Qt programs - # kdePackages.ksshaskpass - # kdePackages.accounts-qt # Qt library for accessing the online accounts database - # kdePackages.calendarsupport - # kdePackages.kaccounts-providers # Online account providers - # kdePackages.kaccounts-integration # Online accounts integration - # kdePackages.kdeplasma-addons - # kdePackages.plasma-browser-integration - # kdePackages.kaddressbook # KDE contact manager - # kdePackages.merkuro # A calendar application using Akonadi to sync with external services - # kdePackages.kcontacts # KContacts - Library for working with contact information - # kdePackages.kpeople # A library that provides access to all contacts and the people who hold them - # kdePackages.kompare # Graphical File Differences Tool - - # krita # A free and open source painting application - # kdePackages.kdenlive # Video editor - # kdePackages.kcolorpicker # Qt based Color Picker with popup menu - # kdePackages.kcolorchooser - # kdePackages.kolourpaint # Paint program - # NOTE KNotes is unmaintained upstream, - # kdePackages.knotes # Popup notes - # kdePackages.kalarm # Personal alarm scheduler - # kdePackages.kamoso # A simple and friendly program to use your camera - # kdePackages.kruler # Screen ruler - # kdePackages.kclock # Clock app for plasma mobile - # okteta # A hex editor - # kdePackages.elisa # A simple media player for KDE - # kdePackages.kmag # A small Linux utility to magnify a part of the screen - # kdePackages.itinerary + pkgs.clinfo # Print all known information about all available OpenCL platforms and devices in the system + pkgs.mesa-demos # Test utilities for OpenGL + pkgs.vulkan-tools # Khronos official Vulkan Tools and Utilities + pkgs.wayland-utils # Wayland utilities (wayland-info) + pkgs.aha # ANSI HTML Adapter + + # -- DMS + NIRI stuff -- + pkgs.i2c-tools # Set of I2C tools for Linux + pkgs.seahorse # Application for managing encryption keys and passwords in the GnomeKeyring + # xwayland-satellite # Xwayland outside your Wayland compositor ]; + programs.xwayland = { + enable = _ true; + package = _ pkgs.xwayland-satellite; + }; + programs.dank-material-shell.greeter = { enable = _ true; configHome = _ "/home/tsandrini"; # TODO probably find a better way to do this @@ -115,28 +83,66 @@ in }; programs.ssh.startAgent = _ false; # NOTE: using gnome agent - programs.niri.enable = _ true; + + # NOTE: It's required to have the niri executable in $PATH to populate + # the wayland-sessions for the dms-greeter. Niri itself will then + # load any configuration provided by HM without any issues, but we + # have to traverse from NixOS -> HM somehow. + programs.niri = { + enable = _ true; + package = _ pkgs.niri-unstable; + }; + + services.accounts-daemon.enable = _ true; # Required to persist user info services.dbus.enable = _ true; security.polkit.enable = _ true; + programs.dconf.enable = _ true; + services.udisks2.enable = _ true; # udisks2, a DBus service that allows applications to query and manipulate storage devices. + services.gvfs.enable = _ true; # GVfs, a userspace virtual filesystem. + xdg.portal = { enable = _ true; - extraPortals = [ pkgs.xdg-desktop-portal-wlr ]; - config.common.default = "*"; + extraPortals = [ + pkgs.xdg-desktop-portal-wlr # xdg-desktop-portal backend for wlroots + pkgs.xdg-desktop-portal-gtk # Desktop integration portals for sandboxed apps + pkgs.xdg-desktop-portal-gnome # Backend implementation for xdg-desktop-portal for the GNOME desktop environment + ]; + config.common.default = [ + "wlr" + "gtk" + ]; }; environment.sessionVariables = { NIXOS_OZONE_WL = _ "1"; + QT_QPA_PLATFORMTHEME = _ "gtk3"; + XDG_CURRENT_DESKTOP = _ "niri"; + XDG_SESSION_DESKTOP = _ "niri"; }; + hardware.i2c.enable = _ true; # Required to control brightness of external monitors + + # Power management and additional power statistics + services.power-profiles-daemon.enable = _ true; + services.upower.enable = _ true; + + # Pass various secret management to gnome keyring and autounlock after login + services.gnome.gnome-keyring.enable = _ true; # provides Secret Service + keyring daemon + security.pam.services.greetd.enableGnomeKeyring = _ true; + security.pam.services.login.enableGnomeKeyring = _ true; + services.pcscd.enable = _ true; # needed for gpg pinentry + + # AUDIO stuff services.pipewire = { enable = _ true; alsa.enable = _ true; pulse.enable = _ true; jack.enable = _ true; }; + security.rtkit.enable = _ true; # realtime audio scheduling - programs.kdeconnect.enable = _ true; + programs.kdeconnect.enable = _ true; # Required to expose ports systemd.user.services.niri-flake-polkit.enable = _ false; } # |----------------------------------------------------------------------| # diff --git a/flake-parts/modules/nixos/profiles/graphical-plasma6.nix b/flake-parts/modules/nixos/profiles/graphical-plasma6.nix index 15ef82b9..17e9d884 100644 --- a/flake-parts/modules/nixos/profiles/graphical-plasma6.nix +++ b/flake-parts/modules/nixos/profiles/graphical-plasma6.nix @@ -132,6 +132,11 @@ in programs.kdeconnect.enable = _ true; services.pcscd.enable = _ true; # needed for gpg pinentry + # Power management and additional power statistics + services.power-profiles-daemon.enable = _ true; + services.upower.enable = _ true; + + # AUDIO stuff services.pipewire = { enable = _ true; alsa.enable = _ true; diff --git a/flake-parts/modules/nixos/profiles/packages-graphical-extra.nix b/flake-parts/modules/nixos/profiles/packages-graphical-extra.nix index fc399de7..1d61cdf4 100644 --- a/flake-parts/modules/nixos/profiles/packages-graphical-extra.nix +++ b/flake-parts/modules/nixos/profiles/packages-graphical-extra.nix @@ -21,10 +21,10 @@ }: let inherit (lib) mkIf mkMerge mkEnableOption; - # inherit (localFlake.lib.modules) mkOverrideAtProfileLevel; + inherit (localFlake.lib.modules) mkOverrideAtProfileLevel; cfg = config.tensorfiles.profiles.packages-graphical-extra; - # _ = mkOverrideAtProfileLevel; + _ = mkOverrideAtProfileLevel; in { options.tensorfiles.profiles.packages-graphical-extra = { @@ -85,11 +85,14 @@ in # lapack # openblas with just the LAPACK C and FORTRAN ABI # github-desktop # GitHub Desktop - winbox4 # Graphical configuration utility for RouterOS-based devices + # winbox4 # Graphical configuration utility for RouterOS-based devices hoppscotch # Open source API development ecosystem ]; - programs.winbox.enable = true; + programs.winbox = { + enable = _ true; + package = _ pkgs.winbox4; + }; programs.nix-index-database.comma.enable = true; } # |----------------------------------------------------------------------| # diff --git a/flake-parts/modules/nixvim/default.nix b/flake-parts/modules/nixvim/default.nix index 2ee1ab85..142bf1a2 100644 --- a/flake-parts/modules/nixvim/default.nix +++ b/flake-parts/modules/nixvim/default.nix @@ -38,7 +38,9 @@ in profiles_base = importApply ./profiles/base.nix { inherit localFlake; }; profiles_minimal = importApply ./profiles/minimal.nix { inherit localFlake; }; profiles_graphical = importApply ./profiles/graphical.nix { inherit localFlake; }; - profiles_ide = importApply ./profiles/ide.nix { inherit localFlake; }; + profiles_ide = importApply ./profiles/ide.nix { + inherit localFlake; + }; plugins_git_neogit = importApply ./plugins/git/neogit.nix { inherit localFlake; }; diff --git a/flake-parts/modules/nixvim/plugins/editor/treesitter.nix b/flake-parts/modules/nixvim/plugins/editor/treesitter.nix index 82e3fcb1..8fae8df2 100644 --- a/flake-parts/modules/nixvim/plugins/editor/treesitter.nix +++ b/flake-parts/modules/nixvim/plugins/editor/treesitter.nix @@ -37,20 +37,21 @@ in { plugins.treesitter = { enable = _ true; - settings = { - indent.enable = _ true; - highlight.enable = _ true; - }; + indent.enable = _ true; + highlight.enable = _ false; folding.enable = _ true; nixvimInjections = _ true; nixGrammars = _ true; - # grammarPackages = _ pkgs.vimPlugins.nvim-treesitter.allGrammars; }; plugins.treesitter-context = { enable = _ true; }; + # plugins.treesitter-refactor = { + # enable = _ true; + # }; + # plugins.treesitter-textobjects = { # enable = _ true; # select = { diff --git a/flake-parts/modules/nixvim/plugins/lsp/lsp.nix b/flake-parts/modules/nixvim/plugins/lsp/lsp.nix index f3fbb5e4..818be76a 100644 --- a/flake-parts/modules/nixvim/plugins/lsp/lsp.nix +++ b/flake-parts/modules/nixvim/plugins/lsp/lsp.nix @@ -106,14 +106,99 @@ in r_language_server.package = _ pkgs.rPackages.languageserver; sqls.enable = _ true; # sqls for SQL terraformls.enable = _ true; # terraformls for Terraform - ts_ls.enable = true; # typst_lsp.enable = _ true; # typst-lsp for the Typst language tinymist.enable = _ true; # tinymist for typst texlab.enable = _ true; # texlab for LaTeX - # volar.enable = _ true; # volar for Vue, replaces vuels - vue_ls.enable = _ true; - # vuels.enable = _ true; # vuels for Vue - # vuels.package = _ pkgs.nodePackages_latest.vls; + + vue_ls = { + enable = _ true; + extraOptions.init_options = { + vue = { + hybridMode = _ false; + }; + tracing = _ true; + }; + settings = { + typescript = { + inlayHints = { + enumMemberValues = { + enabled = false; + }; + functionLikeReturnTypes = { + enabled = false; + }; + propertyDeclarationTypes = { + enabled = false; + }; + parameterTypes = { + enabled = false; + suppressWhenArgumentMatchesName = true; + }; + variableTypes = { + enabled = false; + }; + }; + }; + }; + }; + + vtsls = { + enable = _ true; + filetypes = [ + "vue" + "javascript" + "javascriptreact" + "javascript.jsx" + "typescript" + "typescriptreact" + "typescript.tsx" + ]; + settings = { + vtsls.tsserver.globalPlugins = [ + { + name = "@vue/typescript-plugin"; + location = "${lib.getBin pkgs.vue-language-server}/lib/language-tools/packages/language-server"; + languages = [ "vue" ]; + } + ]; + }; + }; + + ts_ls = { + enable = _ true; + filetypes = [ + "vue" + "javascript" + "javascriptreact" + "javascript.jsx" + "typescript" + "typescriptreact" + "typescript.tsx" + ]; + extraOptions.init_options.plugins = lib.mkForce [ + { + name = "@vue/typescript-plugin"; + location = "${lib.getBin pkgs.vue-language-server}/lib/language-tools/packages/language-server"; + languages = [ "vue" ]; + configNamespace = "typescript"; + } + ]; + settings = { + typescript = { + inlayHints = { + includeInlayParameterNameHints = "all"; + includeInlayParameterNameHintsWhenArgumentMatchesName = false; + includeInlayFunctionParameterTypeHints = false; + includeInlayVariableTypeHints = false; + includeInlayVariableTypeHintsWhenTypeMatchesName = false; + includeInlayPropertyDeclarationTypeHints = false; + includeInlayFunctionLikeReturnTypeHints = false; + includeInlayEnumMemberValueHints = false; + }; + }; + }; + }; + zls.enable = _ true; # zls for Zig # rust-analyzer = { # enable = _ true; diff --git a/flake-parts/modules/nixvim/profiles/graphical.nix b/flake-parts/modules/nixvim/profiles/graphical.nix index ae071f59..b0bb2b32 100644 --- a/flake-parts/modules/nixvim/profiles/graphical.nix +++ b/flake-parts/modules/nixvim/profiles/graphical.nix @@ -42,7 +42,7 @@ in plugins = { utils.markdown-preview.enable = _ true; - editor.obsidian.enable = _ true; + # editor.obsidian.enable = _ true; editor.leetcode.enable = _ true; # editor.image.enable = _ true; }; diff --git a/flake-parts/modules/nixvim/profiles/ide.nix b/flake-parts/modules/nixvim/profiles/ide.nix index 7b8e3f9e..21419007 100644 --- a/flake-parts/modules/nixvim/profiles/ide.nix +++ b/flake-parts/modules/nixvim/profiles/ide.nix @@ -60,6 +60,33 @@ in plugins.direnv.enable = _ true; plugins.crates.enable = _ true; + + colorschemes.nightfox.enable = false; + colorschemes.base16.enable = _ true; + + # TODO + extraConfigLuaPost = '' + local path = vim.fn.stdpath("config") .. "/lua/plugins/dankcolors.lua" + + local function apply_dms_theme() + local ok, spec = pcall(dofile, path) + if not ok then return end + + -- DMS writes a Lazy-style spec table; we just want to run its config() + local entry = spec and spec[1] + if entry and type(entry.config) == "function" then + pcall(entry.config) + end + end + + -- Run once after startup to ensure plugins are available + vim.api.nvim_create_autocmd("VimEnter", { + once = true, + callback = function() + vim.schedule(apply_dms_theme) + end, + }) + ''; } # |----------------------------------------------------------------------| # ]); diff --git a/flake-parts/pkgs/cc-switcher.sh b/flake-parts/pkgs/cc-switcher.sh new file mode 100644 index 00000000..ad081cba --- /dev/null +++ b/flake-parts/pkgs/cc-switcher.sh @@ -0,0 +1,833 @@ +#!/usr/bin/env bash + +# Multi-Account Switcher for Claude Code +# Simple tool to manage and switch between multiple Claude Code accounts + +set -euo pipefail + +# Configuration +readonly BACKUP_DIR="$HOME/.claude-switch-backup" +readonly SEQUENCE_FILE="$BACKUP_DIR/sequence.json" + +# Container detection +is_running_in_container() { + # Check for Docker environment file + if [[ -f /.dockerenv ]]; then + return 0 + fi + + # Check cgroup for container indicators + if [[ -f /proc/1/cgroup ]] && grep -q 'docker\|lxc\|containerd\|kubepods' /proc/1/cgroup 2>/dev/null; then + return 0 + fi + + # Check mount info for container filesystems + if [[ -f /proc/self/mountinfo ]] && grep -q 'docker\|overlay' /proc/self/mountinfo 2>/dev/null; then + return 0 + fi + + # Check for common container environment variables + if [[ -n ${CONTAINER:-} ]] || [[ -n ${container:-} ]]; then + return 0 + fi + + return 1 +} + +# Platform detection +detect_platform() { + case "$(uname -s)" in + Darwin) echo "macos" ;; + Linux) + if [[ -n ${WSL_DISTRO_NAME:-} ]]; then + echo "wsl" + else + echo "linux" + fi + ;; + *) echo "unknown" ;; + esac +} + +# Get Claude configuration file path with fallback +get_claude_config_path() { + local primary_config="$HOME/.claude/.claude.json" + local fallback_config="$HOME/.claude.json" + + # Check primary location first + if [[ -f $primary_config ]]; then + # Verify it has valid oauthAccount structure + if jq -e '.oauthAccount' "$primary_config" >/dev/null 2>&1; then + echo "$primary_config" + return + fi + fi + + # Fallback to standard location + echo "$fallback_config" +} + +# Basic validation that JSON is valid +validate_json() { + local file="$1" + if ! jq . "$file" >/dev/null 2>&1; then + echo "Error: Invalid JSON in $file" + return 1 + fi +} + +# Email validation function +validate_email() { + local email="$1" + # Use robust regex for email validation + if [[ $email =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then + return 0 + else + return 1 + fi +} + +# Account identifier resolution function +resolve_account_identifier() { + local identifier="$1" + if [[ $identifier =~ ^[0-9]+$ ]]; then + echo "$identifier" # It's a number + else + # Look up account number by email + local account_num + account_num=$(jq -r --arg email "$identifier" '.accounts | to_entries[] | select(.value.email == $email) | .key' "$SEQUENCE_FILE" 2>/dev/null) + if [[ -n $account_num && $account_num != "null" ]]; then + echo "$account_num" + else + echo "" + fi + fi +} + +# Safe JSON write with validation +write_json() { + local file="$1" + local content="$2" + local temp_file + temp_file=$(mktemp "${file}.XXXXXX") + + echo "$content" >"$temp_file" + if ! jq . "$temp_file" >/dev/null 2>&1; then + rm -f "$temp_file" + echo "Error: Generated invalid JSON" + return 1 + fi + + mv "$temp_file" "$file" + chmod 600 "$file" +} + +# Check Bash version (4.4+ required) +check_bash_version() { + local version + version=$(bash --version | head -n1 | grep -oE '[0-9]+\.[0-9]+' | head -n1) + if ! awk -v ver="$version" 'BEGIN { exit (ver >= 4.4 ? 0 : 1) }'; then + echo "Error: Bash 4.4+ required (found $version)" + exit 1 + fi +} + +# Check dependencies +check_dependencies() { + if ! command -v jq >/dev/null 2>&1; then + echo "Error: Required command 'jq' not found" + echo "Install with: apt install jq (Linux) or brew install jq (macOS)" + exit 1 + fi +} + +# Setup backup directories +setup_directories() { + mkdir -p "$BACKUP_DIR"/{configs,credentials} + chmod 700 "$BACKUP_DIR" + chmod 700 "$BACKUP_DIR"/{configs,credentials} +} + +# Claude Code process detection (Node.js app) +is_claude_running() { + ps -eo pid,comm,args | awk '$2 == "claude" || $3 == "claude" {exit 0} END {exit 1}' +} + +# Wait for Claude Code to close (no timeout - user controlled) +wait_for_claude_close() { + if ! is_claude_running; then + return 0 + fi + + echo "Claude Code is running. Please close it first." + echo "Waiting for Claude Code to close..." + + while is_claude_running; do + sleep 1 + done + + echo "Claude Code closed. Continuing..." +} + +# Get current account info from .claude.json +get_current_account() { + if [[ ! -f "$(get_claude_config_path)" ]]; then + echo "none" + return + fi + + if ! validate_json "$(get_claude_config_path)"; then + echo "none" + return + fi + + local email + email=$(jq -r '.oauthAccount.emailAddress // empty' "$(get_claude_config_path)" 2>/dev/null) + echo "${email:-none}" +} + +# Detect which Claude Code service name is used in keychain (macOS only) +get_claude_service_name() { + if security find-generic-password -s "Claude Code-credentials" >/dev/null 2>&1; then + echo "Claude Code-credentials" + elif security find-generic-password -s "Claude Code" >/dev/null 2>&1; then + echo "Claude Code" + else + echo "" + fi +} + +# Read credentials based on platform +read_credentials() { + local platform + platform=$(detect_platform) + + case "$platform" in + macos) + local service_name + service_name=$(get_claude_service_name) + if [[ -n $service_name ]]; then + security find-generic-password -s "$service_name" -w 2>/dev/null || echo "" + else + echo "" + fi + ;; + linux | wsl) + if [[ -f "$HOME/.claude/.credentials.json" ]]; then + cat "$HOME/.claude/.credentials.json" + else + echo "" + fi + ;; + *) + echo "" + ;; + esac +} + +# Write credentials based on platform +write_credentials() { + local credentials="$1" + local platform + platform=$(detect_platform) + + case "$platform" in + macos) + local service_name + service_name=$(get_claude_service_name) + if [[ -z $service_name ]]; then + # Default to -credentials for new installations + service_name="Claude Code-credentials" + fi + security add-generic-password -U -s "$service_name" -a "$USER" -w "$credentials" 2>/dev/null + ;; + linux | wsl) + mkdir -p "$HOME/.claude" + printf '%s' "$credentials" >"$HOME/.claude/.credentials.json" + chmod 600 "$HOME/.claude/.credentials.json" + ;; + esac +} + +# Read account credentials from backup +read_account_credentials() { + local account_num="$1" + local email="$2" + local platform + platform=$(detect_platform) + + case "$platform" in + macos) + security find-generic-password -s "Claude Code-Account-${account_num}-${email}" -w 2>/dev/null || echo "" + ;; + linux | wsl) + local cred_file="$BACKUP_DIR/credentials/.claude-credentials-${account_num}-${email}.json" + if [[ -f $cred_file ]]; then + cat "$cred_file" + else + echo "" + fi + ;; + *) + echo "" + ;; + esac +} + +# Write account credentials to backup +write_account_credentials() { + local account_num="$1" + local email="$2" + local credentials="$3" + local platform + platform=$(detect_platform) + + case "$platform" in + macos) + security add-generic-password -U -s "Claude Code-Account-${account_num}-${email}" -a "$USER" -w "$credentials" 2>/dev/null + ;; + linux | wsl) + local cred_file="$BACKUP_DIR/credentials/.claude-credentials-${account_num}-${email}.json" + printf '%s' "$credentials" >"$cred_file" + chmod 600 "$cred_file" + ;; + esac +} + +# Read account config from backup +read_account_config() { + local account_num="$1" + local email="$2" + local config_file="$BACKUP_DIR/configs/.claude-config-${account_num}-${email}.json" + + if [[ -f $config_file ]]; then + cat "$config_file" + else + echo "" + fi +} + +# Write account config to backup +write_account_config() { + local account_num="$1" + local email="$2" + local config="$3" + local config_file="$BACKUP_DIR/configs/.claude-config-${account_num}-${email}.json" + + echo "$config" >"$config_file" + chmod 600 "$config_file" +} + +# Initialize sequence.json if it doesn't exist +init_sequence_file() { + if [[ ! -f $SEQUENCE_FILE ]]; then + local init_content + init_content='{ + "activeAccountNumber": null, + "lastUpdated": "'$(date -u +%Y-%m-%dT%H:%M:%SZ)'", + "sequence": [], + "accounts": {} +}' + write_json "$SEQUENCE_FILE" "$init_content" + fi +} + +# Get next account number +get_next_account_number() { + if [[ ! -f $SEQUENCE_FILE ]]; then + echo "1" + return + fi + + local max_num + max_num=$(jq -r '.accounts | keys | map(tonumber) | max // 0' "$SEQUENCE_FILE") + echo $((max_num + 1)) +} + +# Check if account exists by email +account_exists() { + local email="$1" + if [[ ! -f $SEQUENCE_FILE ]]; then + return 1 + fi + + jq -e --arg email "$email" '.accounts[] | select(.email == $email)' "$SEQUENCE_FILE" >/dev/null 2>&1 +} + +# Add account +cmd_add_account() { + setup_directories + init_sequence_file + + local current_email + current_email=$(get_current_account) + + if [[ $current_email == "none" ]]; then + echo "Error: No active Claude account found. Please log in first." + exit 1 + fi + + if account_exists "$current_email"; then + echo "Account $current_email is already managed." + exit 0 + fi + + local account_num + account_num=$(get_next_account_number) + + local platform + platform=$(detect_platform) + + # Get current service name (macOS only) + credentials/config + local service_name current_creds current_config + service_name="" + if [[ $platform == "macos" ]]; then + service_name=$(get_claude_service_name) + fi + current_creds=$(read_credentials) + current_config=$(cat "$(get_claude_config_path)") + + if [[ -z $current_creds ]]; then + echo "Error: No credentials found for current account" + exit 1 + fi + + if [[ $platform == "macos" && -z $service_name ]]; then + echo "Error: Could not determine Claude Code service name" + exit 1 + fi + + # Get account UUID + local account_uuid + account_uuid=$(jq -r '.oauthAccount.accountUuid' "$(get_claude_config_path)") + + # Store backups + write_account_credentials "$account_num" "$current_email" "$current_creds" + write_account_config "$account_num" "$current_email" "$current_config" + + # Update sequence.json (serviceName only for macOS) + local updated_sequence + updated_sequence=$( + jq --arg num "$account_num" \ + --arg email "$current_email" \ + --arg uuid "$account_uuid" \ + --arg service "$service_name" \ + --arg now "$(date -u +%Y-%m-%dT%H:%M:%SZ)" ' + .accounts[$num] = { + email: $email, + uuid: $uuid, + serviceName: (if $service == "" then null else $service end), + added: $now + } | + .sequence += [$num | tonumber] | + .activeAccountNumber = ($num | tonumber) | + .lastUpdated = $now + ' "$SEQUENCE_FILE" + ) + + write_json "$SEQUENCE_FILE" "$updated_sequence" + + if [[ $platform == "macos" ]]; then + echo "Added Account $account_num: $current_email (service: $service_name)" + else + echo "Added Account $account_num: $current_email" + fi +} + +# Remove account +cmd_remove_account() { + if [[ $# -eq 0 ]]; then + echo "Usage: $0 --remove-account " + exit 1 + fi + + local identifier="$1" + local account_num + + if [[ ! -f $SEQUENCE_FILE ]]; then + echo "Error: No accounts are managed yet" + exit 1 + fi + + # Handle email vs numeric identifier + if [[ $identifier =~ ^[0-9]+$ ]]; then + account_num="$identifier" + else + # Validate email format + if ! validate_email "$identifier"; then + echo "Error: Invalid email format: $identifier" + exit 1 + fi + + # Resolve email to account number + account_num=$(resolve_account_identifier "$identifier") + if [[ -z $account_num ]]; then + echo "Error: No account found with email: $identifier" + exit 1 + fi + fi + + local account_info + account_info=$(jq -r --arg num "$account_num" '.accounts[$num] // empty' "$SEQUENCE_FILE") + + if [[ -z $account_info ]]; then + echo "Error: Account-$account_num does not exist" + exit 1 + fi + + local email + email=$(echo "$account_info" | jq -r '.email') + + local active_account + active_account=$(jq -r '.activeAccountNumber' "$SEQUENCE_FILE") + + if [[ $active_account == "$account_num" ]]; then + echo "Warning: Account-$account_num ($email) is currently active" + fi + + echo -n "Are you sure you want to permanently remove Account-$account_num ($email)? [y/N] " + read -r confirm + + if [[ $confirm != "y" && $confirm != "Y" ]]; then + echo "Cancelled" + exit 0 + fi + + # Remove backup files + local platform + platform=$(detect_platform) + case "$platform" in + macos) + security delete-generic-password -s "Claude Code-Account-${account_num}-${email}" 2>/dev/null || true + ;; + linux | wsl) + rm -f "$BACKUP_DIR/credentials/.claude-credentials-${account_num}-${email}.json" + ;; + esac + rm -f "$BACKUP_DIR/configs/.claude-config-${account_num}-${email}.json" + + # Update sequence.json + local updated_sequence + updated_sequence=$( + jq --arg num "$account_num" --arg now "$(date -u +%Y-%m-%dT%H:%M:%SZ)" ' + del(.accounts[$num]) | + .sequence = (.sequence | map(select(. != ($num | tonumber)))) | + .lastUpdated = $now + ' "$SEQUENCE_FILE" + ) + + write_json "$SEQUENCE_FILE" "$updated_sequence" + + echo "Account-$account_num ($email) has been removed" +} + +# First-run setup workflow +first_run_setup() { + local current_email + current_email=$(get_current_account) + + if [[ $current_email == "none" ]]; then + echo "No active Claude account found. Please log in first." + return 1 + fi + + echo -n "No managed accounts found. Add current account ($current_email) to managed list? [Y/n] " + read -r response + + if [[ $response == "n" || $response == "N" ]]; then + echo "Setup cancelled. You can run '$0 --add-account' later." + return 1 + fi + + cmd_add_account + return 0 +} + +# List accounts +cmd_list() { + if [[ ! -f $SEQUENCE_FILE ]]; then + echo "No accounts are managed yet." + first_run_setup + exit 0 + fi + + # Get current active account from .claude.json + local current_email + current_email=$(get_current_account) + + # Find which account number corresponds to the current email + local active_account_num="" + if [[ $current_email != "none" ]]; then + active_account_num=$(jq -r --arg email "$current_email" '.accounts | to_entries[] | select(.value.email == $email) | .key' "$SEQUENCE_FILE" 2>/dev/null) + fi + + echo "Accounts:" + jq -r --arg active "$active_account_num" ' + .sequence[] as $num | + .accounts["\($num)"] | + if "\($num)" == $active then + " \($num): \(.email) (active)" + else + " \($num): \(.email)" + end + ' "$SEQUENCE_FILE" +} + +# Switch to next account +cmd_switch() { + if [[ ! -f $SEQUENCE_FILE ]]; then + echo "Error: No accounts are managed yet" + exit 1 + fi + + local current_email + current_email=$(get_current_account) + + if [[ $current_email == "none" ]]; then + echo "Error: No active Claude account found" + exit 1 + fi + + # Check if current account is managed + if ! account_exists "$current_email"; then + echo "Notice: Active account '$current_email' was not managed." + cmd_add_account + local account_num + account_num=$(jq -r '.activeAccountNumber' "$SEQUENCE_FILE") + echo "It has been automatically added as Account-$account_num." + echo "Please run './ccswitch.sh --switch' again to switch to the next account." + exit 0 + fi + + # wait_for_claude_close + + local active_account + active_account=$(jq -r '.activeAccountNumber' "$SEQUENCE_FILE") + + local -a sequence=() + mapfile -t sequence < <(jq -r '.sequence[]' "$SEQUENCE_FILE") + + # Find next account in sequence + local next_account current_index=0 + for i in "${!sequence[@]}"; do + if [[ ${sequence[i]} == "$active_account" ]]; then + current_index=$i + break + fi + done + + next_account="${sequence[$(((current_index + 1) % ${#sequence[@]}))]}" + + perform_switch "$next_account" +} + +# Switch to specific account +cmd_switch_to() { + if [[ $# -eq 0 ]]; then + echo "Usage: $0 --switch-to " + exit 1 + fi + + local identifier="$1" + local target_account + + if [[ ! -f $SEQUENCE_FILE ]]; then + echo "Error: No accounts are managed yet" + exit 1 + fi + + # Handle email vs numeric identifier + if [[ $identifier =~ ^[0-9]+$ ]]; then + target_account="$identifier" + else + # Validate email format + if ! validate_email "$identifier"; then + echo "Error: Invalid email format: $identifier" + exit 1 + fi + + # Resolve email to account number + target_account=$(resolve_account_identifier "$identifier") + if [[ -z $target_account ]]; then + echo "Error: No account found with email: $identifier" + exit 1 + fi + fi + + local account_info + account_info=$(jq -r --arg num "$target_account" '.accounts[$num] // empty' "$SEQUENCE_FILE") + + if [[ -z $account_info ]]; then + echo "Error: Account-$target_account does not exist" + exit 1 + fi + + # wait_for_claude_close + perform_switch "$target_account" +} + +# Perform the actual account switch +perform_switch() { + local target_account="$1" + + local platform + platform=$(detect_platform) + + # Get current and target account info + local current_account target_email current_email target_service current_service + current_account=$(jq -r '.activeAccountNumber' "$SEQUENCE_FILE") + target_email=$(jq -r --arg num "$target_account" '.accounts[$num].email' "$SEQUENCE_FILE") + target_service=$(jq -r --arg num "$target_account" '.accounts[$num].serviceName // empty' "$SEQUENCE_FILE") + current_email=$(get_current_account) + + current_service="" + if [[ $platform == "macos" ]]; then + current_service=$(get_claude_service_name) + fi + + if [[ $platform == "macos" && -z $target_service ]]; then + echo "Error: No service name stored for Account-$target_account. Re-add this account." + exit 1 + fi + + # Step 1: Backup current account + local current_creds current_config + current_creds=$(read_credentials) + current_config=$(cat "$(get_claude_config_path)") + + write_account_credentials "$current_account" "$current_email" "$current_creds" + write_account_config "$current_account" "$current_email" "$current_config" + + # Step 2: Retrieve target account + local target_creds target_config + target_creds=$(read_account_credentials "$target_account" "$target_email") + target_config=$(read_account_config "$target_account" "$target_email") + + if [[ -z $target_creds || -z $target_config ]]; then + echo "Error: Missing backup data for Account-$target_account" + exit 1 + fi + + # Step 3: Write credentials (Keychain on macOS, file on Linux) + if [[ $platform == "macos" ]]; then + if [[ -n $current_service && $current_service != "$target_service" ]]; then + security delete-generic-password -s "$current_service" 2>/dev/null || true + echo "Removed old keychain entry: $current_service" + fi + + security add-generic-password -U -s "$target_service" -a "$USER" -w "$target_creds" 2>/dev/null + echo "Added keychain entry: $target_service" + else + write_credentials "$target_creds" + echo "Wrote credentials to $HOME/.claude/.credentials.json" + fi + + # Step 4: Update config file + local oauth_section + oauth_section=$(echo "$target_config" | jq '.oauthAccount' 2>/dev/null) + if [[ -z $oauth_section || $oauth_section == "null" ]]; then + echo "Error: Invalid oauthAccount in backup" + exit 1 + fi + + # Merge oauthAccount into current config file (preserve other local settings) + local merged_config + merged_config=$(jq --argjson oauth "$oauth_section" '.oauthAccount = $oauth' "$(get_claude_config_path)" 2>/dev/null) || { + echo "Error: Failed to merge config" + exit 1 + } + + write_json "$(get_claude_config_path)" "$merged_config" + + # Step 5: Update state + local updated_sequence + updated_sequence=$( + jq --arg num "$target_account" --arg now "$(date -u +%Y-%m-%dT%H:%M:%SZ)" ' + .activeAccountNumber = ($num | tonumber) | + .lastUpdated = $now + ' "$SEQUENCE_FILE" + ) + + write_json "$SEQUENCE_FILE" "$updated_sequence" + + if [[ $platform == "macos" ]]; then + echo "Switched to Account-$target_account ($target_email) using service: $target_service" + else + echo "Switched to Account-$target_account ($target_email)" + fi + + cmd_list + echo "" + echo "Please restart Claude Code to use the new authentication." + echo "" +} + +# Show usage +show_usage() { + echo "Multi-Account Switcher for Claude Code" + echo "Usage: $0 [COMMAND]" + echo "" + echo "Commands:" + echo " --add-account Add current account to managed accounts" + echo " --remove-account Remove account by number or email" + echo " --list List all managed accounts" + echo " --switch Rotate to next account in sequence" + echo " --switch-to Switch to specific account number or email" + echo " --help Show this help message" + echo "" + echo "Examples:" + echo " $0 --add-account" + echo " $0 --list" + echo " $0 --switch" + echo " $0 --switch-to 2" + echo " $0 --switch-to user@example.com" + echo " $0 --remove-account user@example.com" +} + +# Main script logic +main() { + # Basic checks - allow root execution in containers + if [[ $EUID -eq 0 ]] && ! is_running_in_container; then + echo "Error: Do not run this script as root (unless running in a container)" + exit 1 + fi + + check_bash_version + check_dependencies + + case "${1:-}" in + --add-account) + cmd_add_account + ;; + --remove-account) + shift + cmd_remove_account "$@" + ;; + --list) + cmd_list + ;; + --switch) + cmd_switch + ;; + --switch-to) + shift + cmd_switch_to "$@" + ;; + --help) + show_usage + ;; + "") + show_usage + ;; + *) + echo "Error: Unknown command '$1'" + show_usage + exit 1 + ;; + esac +} + +# Check if script is being sourced or executed +if [[ ${BASH_SOURCE[0]} == "${0}" ]]; then + main "$@" +fi diff --git a/flake-parts/pkgs/default.nix b/flake-parts/pkgs/default.nix index 3bd53207..1100a579 100644 --- a/flake-parts/pkgs/default.nix +++ b/flake-parts/pkgs/default.nix @@ -29,6 +29,18 @@ my_cookies = pkgs.callPackage ./my_cookies.nix { }; polonium-nightly = pkgs.libsForQt5.callPackage ./polonium-nightly.nix { inherit lib; }; certbot-dns-wedos = pkgs.callPackage ./certbot-dns-wedos.nix { }; + cc-switcher = pkgs.writeShellApplication { + name = "cc-switcher"; + runtimeInputs = [ + pkgs.jq + pkgs.coreutils + pkgs.gnugrep + pkgs.gawk + pkgs.procps + ]; + + text = builtins.readFile ./cc-switcher.sh; + }; # awatcher = pkgs.callPackage ./awatcher.nix { }; }; }; diff --git a/flake.lock b/flake.lock index bd661598..f8aff6b5 100644 --- a/flake.lock +++ b/flake.lock @@ -193,16 +193,15 @@ "quickshell": "quickshell" }, "locked": { - "lastModified": 1768575133, - "narHash": "sha256-P//moH3z9r4PXirTzXVsccQINsK5AIlF9RWOBwK3vLc=", + "lastModified": 1769493135, + "narHash": "sha256-9h3lV7MpAHvugCCUyOEmwThpJp7aSA4qE9UTQR/8KOc=", "owner": "AvengeMedia", "repo": "DankMaterialShell", - "rev": "a7cdb39b0b89b9af86160ad4e847a7d14ea44512", + "rev": "9553cb06d34a255486733e17c11f6874dd9d99a3", "type": "github" }, "original": { "owner": "AvengeMedia", - "ref": "stable", "repo": "DankMaterialShell", "type": "github" } diff --git a/flake.nix b/flake.nix index cadcf402..cd5c258a 100644 --- a/flake.nix +++ b/flake.nix @@ -110,7 +110,7 @@ inputs.nixpkgs.follows = "nixpkgs"; }; dms = { - url = "github:AvengeMedia/DankMaterialShell/stable"; + url = "github:AvengeMedia/DankMaterialShell"; inputs.nixpkgs.follows = "nixpkgs"; }; niri = {