dotfiles/flake.nix
2026-07-02 20:52:39 +02:00

355 lines
14 KiB
Nix

{
description = "mads NixOS for Apple Silicon (mbp) and x86 (pc), one flake";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
apple-silicon = {
url = "github:nix-community/nixos-apple-silicon";
inputs.nixpkgs.follows = "nixpkgs";
};
home-manager = {
url = "github:nix-community/home-manager";
inputs.nixpkgs.follows = "nixpkgs";
};
firefox-addons = {
url = "gitlab:rycee/nur-expressions?dir=pkgs/firefox-addons";
inputs.nixpkgs.follows = "nixpkgs";
};
zen-browser = {
url = "github:0xc000022070/zen-browser-flake";
inputs.nixpkgs.follows = "nixpkgs";
inputs.home-manager.follows = "home-manager";
};
};
outputs = { self, nixpkgs, apple-silicon, home-manager, firefox-addons, zen-browser, ... }:
let
# ───────────────────── shared home-manager user ─────────────────────
home = { pkgs, osConfig, ... }: {
imports = [ zen-browser.homeModules.beta ];
home.stateVersion = "25.11";
programs.eww = {
enable = true;
package = pkgs.eww;
};
# Raw configs that stay files (no good native module).
# `hypr` is recursive so we can drop a host-specific monitors.conf in
# alongside the shared files (hyprland.conf `source`s it).
xdg.configFile = {
"hypr" = { source = ./config/hypr; recursive = true; };
"hypr/monitors.conf".text =
if osConfig.networking.hostName == "mbp"
then ''
monitor = , preferred, auto, 2.0
''
else ''
monitor = desc:Lenovo Group Limited P27q-20 V909LRFR, 2560x1440@60, 0x0, 1.0
monitor = desc:Lenovo Group Limited P27q-20 V909LPNY, 2560x1440@60, 2560x0, 1.0
monitor = desc:Lenovo Group Limited P27q-20 V909LRDD, 2560x1440@60, 5120x0, 1.0
workspace = 1, monitor:desc:Lenovo Group Limited P27q-20 V909LRFR, default:true
workspace = 2, monitor:desc:Lenovo Group Limited P27q-20 V909LPNY, default:true
workspace = 3, monitor:desc:Lenovo Group Limited P27q-20 V909LRDD, default:true
'';
"eww".source = ./config/eww;
"fuzzel".source = ./config/fuzzel;
};
programs.kitty = {
enable = true;
settings = {
font_size = "11.5";
# cyberdream theme
background = "#16181a";
foreground = "#ffffff";
cursor = "#ffffff";
cursor_text_color = "#16181a";
selection_background = "#3c4048";
selection_foreground = "#ffffff";
color0 = "#16181a";
color8 = "#3c4048";
color1 = "#ff6e5e";
color9 = "#ff6e5e";
color2 = "#5eff6c";
color10 = "#5eff6c";
color3 = "#f1ff5e";
color11 = "#f1ff5e";
color4 = "#5ea1ff";
color12 = "#5ea1ff";
color5 = "#bd5eff";
color13 = "#bd5eff";
color6 = "#5ef1ff";
color14 = "#5ef1ff";
color7 = "#ffffff";
color15 = "#ffffff";
active_tab_foreground = "#000000";
active_tab_background = "#ffbd5e";
inactive_tab_foreground = "#ffffff";
inactive_tab_background = "#16181a";
};
};
home.pointerCursor = {
package = pkgs.bibata-cursors;
name = "Bibata-Modern-Ice";
size = 24;
gtk.enable = true;
x11.enable = true;
hyprcursor.enable = true;
};
# Dark GTK apps (gimp, file dialogs).
gtk = {
enable = true;
theme = { name = "Adwaita-dark"; package = pkgs.gnome-themes-extra; };
gtk3.extraConfig.gtk-application-prefer-dark-theme = 1;
gtk4.extraConfig.gtk-application-prefer-dark-theme = 1;
# Adopt the new default: don't force a theme on GTK4/libadwaita apps
# (dark still applies via prefer-dark-theme above). Silences the
# stateVersion < 26.05 legacy-default warning.
gtk4.theme = null;
};
# Neovim: init.lua verbatim; plugins + LSP pinned by Nix.
programs.neovim = {
enable = true;
withPython3 = false;
withRuby = false;
withNodeJs = false;
initLua = "vim.g.nix_managed = true\n"
+ builtins.readFile ./config/nvim/init.lua;
plugins = with pkgs.vimPlugins; [
nvim-web-devicons cyberdream-nvim oil-nvim nvim-lspconfig
typst-preview-nvim blink-cmp Navigator-nvim
snacks-nvim todo-comments-nvim trouble-nvim
nvim-treesitter.withAllGrammars
fidget-nvim lualine-nvim
];
extraPackages = with pkgs; [
lua-language-server rust-analyzer tinymist nixd zls
yaml-language-server vscode-langservers-extracted
typescript-language-server svelte-language-server
];
};
# Zen browser + Tridactyl, extensions pinned (works on aarch64 + x86).
programs.zen-browser = {
enable = true;
setAsDefaultBrowser = true;
profiles.default = {
settings = {
"browser.newtabpage.enabled" = false;
"privacy.donottrackheader.enabled" = true;
"signon.rememberSignons" = false;
# Dark mode
"browser.theme.toolbar-theme" = 0;
"browser.theme.content-theme" = 0;
"layout.css.prefers-color-scheme.content-override" = 0;
"ui.systemUsesDarkTheme" = 1;
};
# Re-instantiate pkgs with the firefox-addons overlay + allowUnfree so
# the (unfree) 1Password addon evaluates under *our* config.
extensions.packages = let
addonsPkgs = import pkgs.path {
inherit (pkgs.stdenv.hostPlatform) system;
config.allowUnfree = true;
overlays = [ firefox-addons.overlays.default ];
};
in with addonsPkgs.firefox-addons; [
tridactyl
ublock-origin
onepassword-password-manager
];
};
};
# zsh + starship, no framework.
programs.zsh = {
enable = true;
autosuggestion.enable = true;
syntaxHighlighting.enable = true;
shellAliases = { ll = "ls -alh"; gs = "git status"; };
};
programs.starship.enable = true;
# SSH auth via the 1Password agent — private keys stay in the vault,
# never on disk or in the Nix store.
programs.ssh = {
enable = true;
enableDefaultConfig = false;
settings."*".IdentityAgent = "~/.1password/agent.sock";
};
# git identity + SSH commit signing through 1Password's op-ssh-sign.
programs.git = {
enable = true;
settings = {
user.name = "Mads Fagerlund";
user.email = "mads@deepvis.ai";
user.signingkey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAPlpVGZLhfrJqh2j5q3reyGOPQQ9BgW8syQ28tYjlg2";
gpg.format = "ssh";
# NixOS store path — NOT 1Password's suggested /opt/1Password/... which doesn't exist here.
"gpg \"ssh\"".program =
"${pkgs._1password-gui}/share/1password/op-ssh-sign";
commit.gpgsign = true;
# Transparently use SSH (1Password agent) for the deepvis server,
# so existing HTTPS remotes need no manual `git remote set-url`.
url."git@git.deepvis.ai:".insteadOf = "https://git.deepvis.ai/";
};
};
home.packages = with pkgs; [ ripgrep fd tridactyl-native lazygit rustc cargo clippy rustfmt rust-analyzer claude-code gcc bluetui impala];
};
# ───────────────────── shared system (both hosts) ─────────────────────
common = { pkgs, ... }: {
nix.settings.experimental-features = [ "nix-command" "flakes" ];
networking.networkmanager.enable = true;
networking.networkmanager.wifi.backend = "wpa_supplicant";
programs._1password.enable = true;
programs._1password-gui = {
enable = true;
polkitPolicyOwners = [ "mads" ];
};
# Zen (a Firefox fork) runs as "zen-beta"; 1Password only trusts an
# allowlist of browser process names, so add it or the extension can't
# unlock via the desktop app. Manifest already lives in ~/.mozilla.
environment.etc."1password/custom_allowed_browsers".text = ''
zen-beta
'';
services.tailscale.enable = true;
# Tailscale SSH: keyless, portless SSH gated by tailnet ACLs.
# Needs a matching "ssh" rule in the admin console to actually permit access.
services.tailscale.extraUpFlags = [ "--ssh" ];
time.timeZone = "Europe/Copenhagen"; # set yours
i18n.defaultLocale = "en_US.UTF-8";
programs.hyprland = { enable = true; withUWSM = true; };
programs.hyprlock.enable = true;
services.hypridle.enable = true;
xdg.portal = {
enable = true;
extraPortals = [ pkgs.xdg-desktop-portal-hyprland ];
};
# greetd autologins mads straight into Hyprland (via uwsm).
services.greetd = {
enable = true;
settings = {
initial_session = { user = "mads"; command = "uwsm start hyprland"; };
default_session = { user = "mads"; command = "uwsm start hyprland"; };
};
};
hardware.bluetooth.enable = true;
programs.zsh.enable = true;
fonts.packages = [ pkgs.nerd-fonts.jetbrains-mono ];
environment.systemPackages = with pkgs; [
git kitty fuzzel gimp
wl-clipboard grim slurp swaybg brightnessctl playerctl grimblast
hyprpolkitagent xdg-utils
];
users.users.mads = {
isNormalUser = true;
extraGroups = [ "wheel" "networkmanager" "video" ];
shell = pkgs.zsh;
initialPassword = "changeme"; # CHANGE after first boot (`passwd`)
};
system.stateVersion = "25.11";
home-manager.useGlobalPkgs = true;
home-manager.useUserPackages = true;
# Back up (don't clobber) any pre-existing files HM wants to manage.
home-manager.backupFileExtension = "hm-bak";
home-manager.users.mads = home;
};
mkHost = { system, modules }: nixpkgs.lib.nixosSystem {
inherit system;
modules = [
home-manager.nixosModules.home-manager
common
({ pkgs, ... }: {
nixpkgs.config.allowUnfree = true;
})
] ++ modules;
};
in
{
nixosConfigurations = {
# ── Apple Silicon MacBook (Asahi) ──
mbp = mkHost {
system = "aarch64-linux";
modules = [
apple-silicon.nixosModules.apple-silicon-support
./hosts/mbp/hardware-configuration.nix # generated on the Mac
({ ... }: {
networking.hostName = "mbp";
# Wi-Fi/BT firmware, vendored (a pure flake can't read /boot/asahi).
hardware.asahi.peripheralFirmwareDirectory = ./firmware;
boot.loader.systemd-boot.enable = true;
boot.loader.systemd-boot.configurationLimit = 3; # 500M ESP is small
boot.extraModprobeConfig = "options hid_apple fnmode=1";
boot.blacklistedKernelModules = [ "pcspkr" ];
# Asahi kernel + Mesa cache (else compile from source).
nix.settings = {
extra-substituters = [ "https://nixos-apple-silicon.cachix.org" ];
extra-trusted-public-keys = [
"nixos-apple-silicon.cachix.org-1:8psDu5SA5dAD7qA0zMy5UT292TxeEPzIz8VVEr2Js20="
];
};
})
];
};
# ── Arbitrary x86_64 PC ──
pc = mkHost {
system = "x86_64-linux";
modules = [
./hosts/pc/hardware-configuration.nix # generated on the PC
({ config, ... }: {
networking.hostName = "pc";
boot.loader.systemd-boot.enable = true;
boot.loader.efi.canTouchEfiVariables = true; # normal x86 UEFI
# NVIDIA (RTX 20-series or newer → open kernel module).
services.xserver.videoDrivers = [ "nvidia" ];
hardware.graphics.enable = true; # OpenGL/Vulkan userspace (was hardware.opengl)
hardware.nvidia = {
modesetting.enable = true; # required for Wayland/Hyprland
open = true; # open kernel module (Turing+)
nvidiaSettings = true;
package = config.boot.kernelPackages.nvidiaPackages.stable;
};
})
];
};
};
# `nix develop` on either arch.
devShells = nixpkgs.lib.genAttrs [ "aarch64-linux" "x86_64-linux" ] (system: {
default = nixpkgs.legacyPackages.${system}.mkShell {
packages = [ nixpkgs.legacyPackages.${system}.nodejs ];
};
});
};
}