diff --git a/flake-parts/hosts/flatbundle/default.nix b/flake-parts/hosts/flatbundle/default.nix index c5d98716..38035d95 100644 --- a/flake-parts/hosts/flatbundle/default.nix +++ b/flake-parts/hosts/flatbundle/default.nix @@ -13,7 +13,11 @@ # 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' { inputs }: -{ pkgs, system, ... }: +{ + pkgs, + system, + ... +}: let pkgs-osu-lazer-bin = import inputs.nixpkgs-osu-lazer-bin { inherit system; @@ -97,6 +101,34 @@ in }; # nix-mineral.enable = true; + tensorfiles.networking.firewall.subnets-firewall = { + nixosPassthrough = { + allowedTCPPorts = [ + # + ]; + }; + defaultSubnets = { + allowedTCPPorts = [ + # WG + 51820 + 51821 + # Dev ports + 8000 + 8080 + 5173 + ]; + allowedUDPPorts = [ + # WG + 51820 + 51821 + # Dev ports + 8000 + 8080 + 5173 + ]; + }; + }; + programs.nh.flake = "/home/tsandrini/ProjectBundle/tsandrini/tensorfiles"; programs.nh.clean.enable = false; # NOTE We have enough space buddy @@ -138,28 +170,6 @@ in 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 - ]; - }; - - services.keybase.enable = true; home-manager.users."tsandrini" = { imports = [ @@ -192,10 +202,12 @@ in mcp-servers.programs = { playwright.enable = true; + playwright.args = [ "--headless" ]; nixos.enable = true; time.enable = true; fetch.enable = true; - github.enable = true; + # everything.enable = true; + # github.enable = true; }; programs.claude-code = { diff --git a/flake-parts/hosts/jetbundle/default.nix b/flake-parts/hosts/jetbundle/default.nix index 377f79f3..7f5c645f 100644 --- a/flake-parts/hosts/jetbundle/default.nix +++ b/flake-parts/hosts/jetbundle/default.nix @@ -95,6 +95,34 @@ in }; # nix-mineral.enable = true; + tensorfiles.networking.firewall.subnets-firewall = { + nixosPassthrough = { + allowedTCPPorts = [ + # + ]; + }; + defaultSubnets = { + allowedTCPPorts = [ + # WG + 51820 + 51821 + # Dev ports + 8000 + 8080 + 5173 + ]; + allowedUDPPorts = [ + # WG + 51820 + 51821 + # Dev ports + 8000 + 8080 + 5173 + ]; + }; + }; + programs.nh.flake = "/home/tsandrini/ProjectBundle/tsandrini/tensorfiles"; programs.fish.enable = true; @@ -135,26 +163,6 @@ in 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 - ]; - }; home-manager.users."tsandrini" = { tensorfiles.hm = { diff --git a/flake-parts/hosts/pupibundle/default.nix b/flake-parts/hosts/pupibundle/default.nix index 840f1801..82f5e5b0 100644 --- a/flake-parts/hosts/pupibundle/default.nix +++ b/flake-parts/hosts/pupibundle/default.nix @@ -62,9 +62,6 @@ in tensorfiles = { profiles = { headless.enable = true; - packages-base.enable = true; - # packages-extra.enable = true; - with-base-monitoring-exports.enable = true; }; @@ -98,32 +95,19 @@ in ]; tensorfiles.networking.firewall.subnets-firewall = { - enable = true; - subnets = { - "${infraVars.common.networking.defaultSubnet}" = { - allowedTCPPorts = [ - 80 - 443 - prometheusExporters.pihole.port - prometheusExporters.unbound.port - ]; - }; - "${infraVars.common.networking.intranetSubnet}" = { - allowedTCPPorts = [ - 80 - 443 - prometheusExporters.pihole.port - prometheusExporters.unbound.port - ]; - }; + nixosPassthrough = { + allowedTCPPorts = [ + # + ]; + }; + defaultSubnets = { + allowedTCPPorts = [ + 80 + 443 + prometheusExporters.pihole.port + prometheusExporters.unbound.port + ]; }; - }; - - networking.firewall = { - allowedTCPPorts = [ - ]; - allowedUDPPorts = [ - ]; }; networking = { diff --git a/flake-parts/hosts/remotebundle/default.nix b/flake-parts/hosts/remotebundle/default.nix index 2335790c..08b19690 100644 --- a/flake-parts/hosts/remotebundle/default.nix +++ b/flake-parts/hosts/remotebundle/default.nix @@ -108,8 +108,6 @@ in tensorfiles = { profiles = { headless.enable = true; - packages-base.enable = true; - # packages-extra.enable = true; with-base-monitoring-exports.enable = true; }; @@ -156,6 +154,24 @@ in }; }; + tensorfiles.networking.firewall.subnets-firewall = { + nixosPassthrough = { + allowedTCPPorts = [ + 80 + 443 + ]; + allowedUDPPorts = [ + config.networking.wireguard.interfaces.wg-home-tunnel.listenPort + ]; + }; + defaultSubnets = { + allowedTCPPorts = [ + config.services.postgresql.settings.port + config.services.loki.configuration.server.http_listen_port + ]; + }; + }; + # NAS fileSystems."/mnt/NAS" = { device = "172.16.131.12:/nas/5829"; @@ -188,34 +204,6 @@ in } ]; - tensorfiles.networking.firewall.subnets-firewall = { - enable = true; - subnets = { - "${infraVars.common.networking.defaultSubnet}" = { - allowedTCPPorts = [ - config.services.postgresql.settings.port - config.services.loki.configuration.server.http_listen_port - ]; - }; - "${infraVars.common.networking.intranetSubnet}" = { - allowedTCPPorts = [ - config.services.postgresql.settings.port - config.services.loki.configuration.server.http_listen_port - ]; - }; - }; - }; - - networking.firewall = { - allowedTCPPorts = [ - 80 - 443 - ]; - allowedUDPPorts = [ - config.networking.wireguard.interfaces.wg-home-tunnel.listenPort - ]; - }; - networking.wireguard.interfaces = { wg-home-tunnel = { ips = [ "${selfVars.wgAddress}/32" ]; diff --git a/flake-parts/hosts/spinorbundle/default.nix b/flake-parts/hosts/spinorbundle/default.nix index 93df4de1..602e5d5e 100644 --- a/flake-parts/hosts/spinorbundle/default.nix +++ b/flake-parts/hosts/spinorbundle/default.nix @@ -78,6 +78,34 @@ }; # nix-mineral.enable = true; + tensorfiles.networking.firewall.subnets-firewall = { + nixosPassthrough = { + allowedTCPPorts = [ + # + ]; + }; + defaultSubnets = { + allowedTCPPorts = [ + # WG + 51820 + 51821 + # Dev ports + 8000 + 8080 + 5173 + ]; + allowedUDPPorts = [ + # WG + 51820 + 51821 + # Dev ports + 8000 + 8080 + 5173 + ]; + }; + }; + programs.nh.flake = "/home/tsandrini/ProjectBundle/tsandrini/tensorfiles"; programs.fish.enable = true; @@ -107,26 +135,6 @@ 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 - ]; - }; home-manager.users."tsandrini" = { tensorfiles.hm = { diff --git a/flake-parts/infra-vars/variables.nix b/flake-parts/infra-vars/variables.nix index 0a2c05f4..fd780133 100644 --- a/flake-parts/infra-vars/variables.nix +++ b/flake-parts/infra-vars/variables.nix @@ -26,6 +26,10 @@ _: rec { defaultSubnet = "10.10.0.0/24"; intranetSubnet = "10.0.33.0/24"; defaultGateway = "10.10.0.1"; + defaultFirewallSubnets = [ + common.networking.defaultSubnet + common.networking.intranetSubnet + ]; defaultNameservers = [ "10.10.0.10" "8.8.8.8" diff --git a/flake-parts/modules/home-manager/programs/ssh.nix b/flake-parts/modules/home-manager/programs/ssh.nix index e4d7c681..62dff4f9 100644 --- a/flake-parts/modules/home-manager/programs/ssh.nix +++ b/flake-parts/modules/home-manager/programs/ssh.nix @@ -99,6 +99,14 @@ in programs.ssh = { enable = _ true; enableDefaultConfig = _ true; + + # NOTE: Override TERM for all SSH connections to avoid issues with + # remote servers that lack the ghostty terminfo entry (xterm-ghostty). + # This preserves the full xterm-ghostty features locally while + # ensuring compatibility over SSH. + matchBlocks."*".setEnv = { + TERM = _ "xterm-256color"; + }; }; programs.keychain = { diff --git a/flake-parts/modules/nixos/default.nix b/flake-parts/modules/nixos/default.nix index cdb650c5..e32e8190 100644 --- a/flake-parts/modules/nixos/default.nix +++ b/flake-parts/modules/nixos/default.nix @@ -53,8 +53,8 @@ in profiles_graphical-startx-home-manager = importApply ./profiles/graphical-startx-home-manager.nix { inherit localFlake; }; - profiles_headless = importApply ./profiles/headless.nix { inherit localFlake infraVars; }; - profiles_minimal = importApply ./profiles/minimal.nix { inherit localFlake; }; + profiles_headless = importApply ./profiles/headless.nix { inherit localFlake; }; + profiles_minimal = importApply ./profiles/minimal.nix { inherit localFlake infraVars; }; profiles_with-base-monitoring-exports = importApply ./profiles/with-base-monitoring-exports.nix { inherit localFlake infraVars; }; @@ -82,6 +82,7 @@ in services_monit = importApply ./services/monit.nix { inherit localFlake; }; + services_fail2ban = importApply ./services/fail2ban.nix { inherit localFlake; }; services_monitoring_loki = importApply ./services/monitoring/loki.nix { inherit localFlake; }; diff --git a/flake-parts/modules/nixos/networking/firewall/subnets-firewall.nix b/flake-parts/modules/nixos/networking/firewall/subnets-firewall.nix index 1ae88c2e..72243e58 100644 --- a/flake-parts/modules/nixos/networking/firewall/subnets-firewall.nix +++ b/flake-parts/modules/nixos/networking/firewall/subnets-firewall.nix @@ -30,6 +30,7 @@ let filterAttrs hasInfix lists + zipAttrsWith ; inherit (localFlake.lib.modules) mkOverrideAtModuleLevel; @@ -83,7 +84,18 @@ let }) cfg.defaultSubnetsList ); - effectiveSubnets = defaultSubnetsRendered // cfg.subnets; + effectiveSubnets = + zipAttrsWith + (_: policies: { + allowedTCPPorts = unique (flatten (map (p: p.allowedTCPPorts) policies)); + allowedUDPPorts = unique (flatten (map (p: p.allowedUDPPorts) policies)); + allowedTCPPortRanges = unique (flatten (map (p: p.allowedTCPPortRanges) policies)); + allowedUDPPortRanges = unique (flatten (map (p: p.allowedUDPPortRanges) policies)); + }) + [ + defaultSubnetsRendered + cfg.subnets + ]; subnetsV4 = filterAttrs (cidr: _: !isV6 cidr) effectiveSubnets; subnetsV6 = filterAttrs (cidr: _: isV6 cidr) effectiveSubnets; diff --git a/flake-parts/modules/nixos/profiles/headless.nix b/flake-parts/modules/nixos/profiles/headless.nix index 6dd93168..e2a1de26 100644 --- a/flake-parts/modules/nixos/profiles/headless.nix +++ b/flake-parts/modules/nixos/profiles/headless.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, infraVars }: +{ localFlake }: { config, lib, @@ -20,7 +20,12 @@ ... }: let - inherit (lib) mkIf mkMerge mkEnableOption; + inherit (lib) + mkIf + mkMerge + mkEnableOption + getExe + ; inherit (localFlake.lib.modules) mkOverrideAtProfileLevel; cfg = config.tensorfiles.profiles.headless; @@ -45,35 +50,42 @@ in services.networking.networkmanager.enable = _ true; services.networking.ssh.enable = _ true; + services.fail2ban.enable = _ true; }; programs.bash = { interactiveShellInit = lib.mkBefore '' - ${lib.getExe pkgs.microfetch} + ${getExe pkgs.microfetch} + last -x --fulltimes --limit 30 ''; }; services.openssh.openFirewall = false; + tensorfiles.networking.firewall.subnets-firewall = { - enable = true; - subnets = { - "${infraVars.common.networking.defaultSubnet}" = { - allowedTCPPorts = config.services.openssh.ports; - }; - "${infraVars.common.networking.intranetSubnet}" = { - allowedTCPPorts = config.services.openssh.ports; - }; + defaultSubnets = { + allowedTCPPorts = config.services.openssh.ports; }; }; services.getty.autologinUser = _ "root"; - services.fail2ban.enable = _ true; networking.nftables.enable = _ true; - networking.firewall.enable = _ true; + networking.firewall = { + enable = _ true; + pingLimit = _ ( + if config.networking.nftables.enable then "2/second" else "--limit 1/minute --limit-burst 5" + ); + }; - services.rsyslogd.enable = _ true; - services.journald.forwardToSyslog = _ true; + # NOTE: rsyslog is intentionally not enabled. journald handles log + # storage and rotation natively, and Promtail reads from the journal + # directly. rsyslog would only produce duplicate text log files on + # disk (~530MB closure cost). + services.journald.extraConfig = _ '' + SystemMaxUse=100M + MaxRetentionSec=7day + ''; services.logrotate.enable = _ true; } # |----------------------------------------------------------------------| # diff --git a/flake-parts/modules/nixos/profiles/minimal.nix b/flake-parts/modules/nixos/profiles/minimal.nix index 38498a40..c0df9953 100644 --- a/flake-parts/modules/nixos/profiles/minimal.nix +++ b/flake-parts/modules/nixos/profiles/minimal.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 }: +{ localFlake, infraVars }: { config, lib, @@ -56,20 +56,32 @@ in }; }; + tensorfiles.networking.firewall.subnets-firewall = { + enable = _ true; + defaultSubnetsList = infraVars.common.networking.defaultFirewallSubnets; + }; + time.timeZone = _ "Europe/Prague"; - i18n.defaultLocale = _ "en_US.UTF-8"; - i18n.extraLocaleSettings = { - LANGUAGE = "en_US.UTF-8"; - LC_ADDRESS = _ "cs_CZ.UTF-8"; - LC_IDENTIFICATION = _ "cs_CZ.UTF-8"; - LC_MEASUREMENT = _ "cs_CZ.UTF-8"; - LC_MONETARY = _ "cs_CZ.UTF-8"; - LC_NAME = _ "cs_CZ.UTF-8"; - LC_NUMERIC = _ "cs_CZ.UTF-8"; - LC_PAPER = _ "cs_CZ.UTF-8"; - LC_TELEPHONE = _ "cs_CZ.UTF-8"; - LC_TIME = _ "cs_CZ.UTF-8"; + i18n = { + defaultLocale = _ "en_US.UTF-8"; + supportedLocales = [ + "en_US.UTF-8/UTF-8" + "cs_CZ.UTF-8/UTF-8" + "it_IT.UTF-8/UTF-8" + ]; + extraLocaleSettings = { + LANGUAGE = "en_US.UTF-8"; + LC_ADDRESS = _ "cs_CZ.UTF-8"; + LC_IDENTIFICATION = _ "cs_CZ.UTF-8"; + LC_MEASUREMENT = _ "cs_CZ.UTF-8"; + LC_MONETARY = _ "cs_CZ.UTF-8"; + LC_NAME = _ "cs_CZ.UTF-8"; + LC_NUMERIC = _ "cs_CZ.UTF-8"; + LC_PAPER = _ "cs_CZ.UTF-8"; + LC_TELEPHONE = _ "cs_CZ.UTF-8"; + LC_TIME = _ "cs_CZ.UTF-8"; + }; }; console = { @@ -77,21 +89,6 @@ in useXkbConfig = _ true; font = _ "${pkgs.terminus_font}/share/consolefonts/ter-132n.psf.gz"; }; - - # NOTE: We only set the defaults, end configurations can enable/disable - # them as needed. - services.fail2ban = { - maxretry = _ 6; - bantime = _ "11m"; - bantime-increment = { - enable = _ true; - rndtime = _ "7m"; - overalljails = _ true; - }; - }; - networking.firewall.pingLimit = _ ( - if config.networking.nftables.enable then "2/second" else "--limit 1/minute --limit-burst 5" - ); } # |----------------------------------------------------------------------| # ]); diff --git a/flake-parts/modules/nixos/profiles/with-base-monitoring-exports.nix b/flake-parts/modules/nixos/profiles/with-base-monitoring-exports.nix index d40488a6..65041c15 100644 --- a/flake-parts/modules/nixos/profiles/with-base-monitoring-exports.nix +++ b/flake-parts/modules/nixos/profiles/with-base-monitoring-exports.nix @@ -142,14 +142,8 @@ in # |----------------------------------------------------------------------| # (mkIf cfg.prometheus.exporters.node.enable { tensorfiles.networking.firewall.subnets-firewall = { - enable = true; - subnets = { - "${infraVars.common.networking.defaultSubnet}" = { - allowedTCPPorts = [ cfg.prometheus.exporters.node.port ]; - }; - "${infraVars.common.networking.intranetSubnet}" = { - allowedTCPPorts = [ cfg.prometheus.exporters.node.port ]; - }; + defaultSubnets = { + allowedTCPPorts = [ cfg.prometheus.exporters.node.port ]; }; }; diff --git a/flake-parts/modules/nixos/services/fail2ban.nix b/flake-parts/modules/nixos/services/fail2ban.nix new file mode 100644 index 00000000..58894967 --- /dev/null +++ b/flake-parts/modules/nixos/services/fail2ban.nix @@ -0,0 +1,60 @@ +# --- flake-parts/modules/nixos/services/fail2ban.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, + ... +}: +let + inherit (lib) + mkIf + mkMerge + mkEnableOption + ; + inherit (localFlake.lib.modules) mkOverrideAtProfileLevel; + + cfg = config.tensorfiles.services.fail2ban; + _ = mkOverrideAtProfileLevel; +in +{ + options.tensorfiles.services.fail2ban = { + enable = mkEnableOption '' + TODO + ''; + + }; + + config = mkIf cfg.enable (mkMerge [ + # |----------------------------------------------------------------------| # + { + services.fail2ban = { + enable = _ true; + maxretry = _ 3; + bantime = _ "11m"; + bantime-increment = { + enable = _ true; + rndtime = _ "7m"; + overalljails = _ true; + }; + }; + } + # |----------------------------------------------------------------------| # + ]); + + meta.maintainers = with localFlake.lib.maintainers; [ tsandrini ]; +} diff --git a/flake-parts/modules/nixvim/default.nix b/flake-parts/modules/nixvim/default.nix index ab64a305..67507f28 100644 --- a/flake-parts/modules/nixvim/default.nix +++ b/flake-parts/modules/nixvim/default.nix @@ -43,11 +43,13 @@ in }; plugins_git_neogit = importApply ./plugins/git/neogit.nix { inherit localFlake; }; + plugins_git_blame = importApply ./plugins/git/blame.nix { inherit localFlake; }; plugins_utils_hop = importApply ./plugins/utils/hop.nix { inherit localFlake; }; plugins_utils_faster = importApply ./plugins/utils/faster.nix { inherit localFlake; }; plugins_utils_orgmode = importApply ./plugins/utils/orgmode.nix { inherit localFlake; }; plugins_utils_project-nvim = importApply ./plugins/utils/project-nvim.nix { inherit localFlake; }; + plugins_utils_projections = importApply ./plugins/utils/projections.nix { inherit localFlake; }; plugins_utils_telescope = importApply ./plugins/utils/telescope.nix { inherit localFlake; }; plugins_utils_which-key = importApply ./plugins/utils/which-key.nix { inherit localFlake; }; plugins_utils_markdown-preview = importApply ./plugins/utils/markdown-preview.nix { diff --git a/flake-parts/modules/nixvim/plugins/git/blame.nix b/flake-parts/modules/nixvim/plugins/git/blame.nix new file mode 100644 index 00000000..1dec3832 --- /dev/null +++ b/flake-parts/modules/nixvim/plugins/git/blame.nix @@ -0,0 +1,70 @@ +# --- flake-parts/modules/nixvim/plugins/git/blame.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, + ... +}: +let + inherit (lib) mkIf mkMerge mkEnableOption; + inherit (localFlake.lib.modules) mkOverrideAtNixvimModuleLevel; + + cfg = config.tensorfiles.nixvim.plugins.git.blame; + _ = mkOverrideAtNixvimModuleLevel; +in +{ + options.tensorfiles.nixvim.plugins.git.blame = { + enable = mkEnableOption '' + TODO + ''; + + withKeymaps = + mkEnableOption '' + Enable the related included keymaps. + '' + // { + default = true; + }; + }; + + config = mkIf cfg.enable (mkMerge [ + # |----------------------------------------------------------------------| # + { + plugins.blame = { + enable = _ true; + settings = { + # + }; + }; + } + # |----------------------------------------------------------------------| # + (mkIf cfg.withKeymaps { + keymaps = [ + { + mode = "n"; + key = "gB"; + action = "BlameToggle"; + options = { + desc = "BlameToggle"; + }; + } + ]; + }) + # |----------------------------------------------------------------------| # + ]); + + meta.maintainers = with localFlake.lib.maintainers; [ tsandrini ]; +} diff --git a/flake-parts/modules/nixvim/plugins/utils/project-nvim.nix b/flake-parts/modules/nixvim/plugins/utils/project-nvim.nix index 189fc6cb..9112ccef 100644 --- a/flake-parts/modules/nixvim/plugins/utils/project-nvim.nix +++ b/flake-parts/modules/nixvim/plugins/utils/project-nvim.nix @@ -41,6 +41,128 @@ in config = mkIf cfg.enable (mkMerge [ # |----------------------------------------------------------------------| # { + # NOTE: Monkey-patch project.nvim's history read/write to handle + # concurrent nvim instances. The upstream plugin has no file locking, + # so simultaneous writes corrupt the JSON history file. + # + # Three problems fixed here: + # 1. write_history reads the file then writes — races with other instances + # 2. The fs_event watcher fires read_history in a libuv fast-event + # context, where vim.fn.* / vim.notify (via fidget.nvim) cannot run + # 3. read_history calls write_history when the file is empty (mid-truncate + # by another instance), cascading the failure + # + # The fix: wrap both read and write in pcall, defer all vim.fn/notify + # calls via vim.schedule, and use atomic write-to-temp + rename. + extraConfigLuaPost = '' + do + local ok_hist, History = pcall(require, 'project.util.history') + if not (ok_hist and History) then return end + + local Path = require('project.util.path') + local uv = vim.uv or vim.loop + + -- Safe notify that works even inside libuv fast-event callbacks + local function safe_notify(msg, level) + vim.schedule(function() + vim.notify(msg, level) + end) + end + + -- Atomic write: write to a temp file then rename, so readers + -- never see a half-written file. + local function atomic_write_json(filepath, data) + local tmp = filepath .. '.tmp.' .. uv.getpid() + local fd = uv.fs_open(tmp, 'w', 438) -- 0666 + if not fd then return false end + local ok_enc, json = pcall(vim.json.encode, data) + if not (ok_enc and json) then + uv.fs_close(fd) + uv.fs_unlink(tmp) + return false + end + uv.fs_write(fd, json) + uv.fs_close(fd) + local ok_rename = uv.fs_rename(tmp, filepath) + if not ok_rename then + uv.fs_unlink(tmp) + return false + end + return true + end + + -- Safe JSON read: returns decoded table or nil + local function safe_read_json(filepath) + local fd, stat + if filepath == Path.historyfile then + fd, stat = History.open_history('r') + else + fd, stat = Path.open_file(filepath, 'r') + end + if not (fd and stat) then + if fd then uv.fs_close(fd) end + return nil + end + if stat.size == 0 then + uv.fs_close(fd) + return {} + end + local raw = uv.fs_read(fd, stat.size) + uv.fs_close(fd) + if not raw then return nil end + local ok_dec, data = pcall(vim.json.decode, raw) + if ok_dec and type(data) == 'table' then + return data + end + return nil + end + + if History.write_history then + local orig_write = History.write_history + History.write_history = function(path, ...) + -- Try the original first + local ok_w, err = pcall(orig_write, path, ...) + if not ok_w then + -- Original failed (likely corrupt JSON during read-back), + -- attempt an atomic write with just our session projects + safe_notify( + '[project.nvim] History write failed, attempting atomic recovery.', + vim.log.levels.WARN + ) + local target = path or Path.historyfile + if not target then return end + -- Collect what we can from memory + local projects = {} + if History.recent_projects then + vim.list_extend(projects, History.recent_projects) + end + if History.session_projects then + vim.list_extend(projects, History.session_projects) + end + if #projects > 0 then + atomic_write_json(target, projects) + else + atomic_write_json(target, {}) + end + end + end + end + + if History.read_history then + local orig_read = History.read_history + History.read_history = function(...) + local ok_r, err = pcall(orig_read, ...) + if not ok_r then + safe_notify( + '[project.nvim] History read failed, file may be temporarily corrupt.', + vim.log.levels.WARN + ) + end + end + end + end + ''; + # NOTE: This fixes a check build time issue where project-nvim # tries to search for history inside $HOME (which doesn't work inside a sandbox) plugins.project-nvim = { diff --git a/flake-parts/modules/nixvim/plugins/utils/projections.nix b/flake-parts/modules/nixvim/plugins/utils/projections.nix new file mode 100644 index 00000000..3a02edfa --- /dev/null +++ b/flake-parts/modules/nixvim/plugins/utils/projections.nix @@ -0,0 +1,185 @@ +# --- flake-parts/modules/nixvim/plugins/utils/projections.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, + ... +}: +let + inherit (lib) mkIf mkMerge mkEnableOption; + inherit (localFlake.lib.modules) mkOverrideAtNixvimModuleLevel isModuleLoadedAndEnabled; + + cfg = config.tensorfiles.nixvim.plugins.utils.projections; + _ = mkOverrideAtNixvimModuleLevel; + + telescopeCheck = isModuleLoadedAndEnabled config "tensorfiles.nixvim.plugins.utils.telescope"; +in +{ + options.tensorfiles.nixvim.plugins.utils.projections = { + enable = mkEnableOption '' + Projections.nvim project and session manager. Concurrent-safe + alternative to project.nvim using per-project session files. + ''; + + withKeymaps = + mkEnableOption '' + Enable the related included keymaps. + '' + // { + default = true; + }; + }; + + config = mkIf cfg.enable (mkMerge [ + # |----------------------------------------------------------------------| # + { + plugins.projections = { + enable = _ true; + settings = { + patterns = [ + ".git" + ".projectfile" + ]; + # NOTE workspaces are registered dynamically in extraConfigLuaPost + # below since ~/ProjectBundle has a two-level structure (org/repo) + # and projections only scans direct children of a workspace. + store_hooks = { + pre = lib.nixvim.mkRaw '' + function() + -- Close neo-tree before storing session to avoid empty buffer issues + if pcall(require, "neo-tree") then + vim.cmd([[Neotree action=close]]) + end + end + ''; + }; + }; + }; + + extraConfigLuaPost = '' + do + local uv = vim.uv or vim.loop + local Session = require("projections.session") + local Workspace = require("projections.workspace") + local patterns = { ".git", ".projectfile" } + + -- Check if a directory is a project (contains a pattern marker) + local function is_project(dir) + for _, pat in ipairs(patterns) do + if uv.fs_stat(dir .. "/" .. pat) then return true end + end + return false + end + + -- Auto-register parent of cwd as workspace if cwd is a project. + -- Projections models projects as direct children of workspaces, + -- so to register /a/b/myrepo we add /a/b as a workspace. + local function auto_register_project() + local cwd = uv.cwd() + if cwd and is_project(cwd) then + local parent = vim.fn.fnamemodify(cwd, ":h") + if parent and parent ~= cwd then + Workspace.add(parent, patterns) + end + end + end + + -- Auto-register on enter and exit + auto_register_project() + vim.api.nvim_create_autocmd({ "VimLeavePre" }, { + callback = function() + auto_register_project() + Session.store(uv.cwd()) + end, + }) + + -- Switch to project if nvim was started in a project dir + local switcher = require("projections.switcher") + vim.api.nvim_create_autocmd({ "VimEnter" }, { + callback = function() + if vim.fn.argc() == 0 then + switcher.switch(uv.cwd()) + end + end, + }) + + -- Manual commands + vim.api.nvim_create_user_command("AddWorkspace", function() + Workspace.add(uv.cwd(), patterns) + end, { desc = "Add cwd as a projections workspace" }) + + vim.api.nvim_create_user_command("AddProject", function() + auto_register_project() + vim.notify("Registered project: " .. uv.cwd(), vim.log.levels.INFO) + end, { desc = "Register current project (adds parent as workspace)" }) + + vim.api.nvim_create_user_command("RemoveWorkspace", function(opts) + local target = opts.args ~= "" and vim.fn.expand(opts.args) or uv.cwd() + local cfg = require("projections.config") + local path = cfg.workspaces_file + if not path then return end + local f = io.open(path, "r") + if not f then return end + local ok, data = pcall(vim.json.decode, f:read("*a")) + f:close() + if not (ok and data) then return end + local filtered = vim.tbl_filter(function(ws) + return vim.fn.resolve(ws.path) ~= vim.fn.resolve(target) + end, data) + f = io.open(path, "w") + if f then + f:write(vim.json.encode(filtered)) + f:close() + vim.notify("Removed workspace: " .. target, vim.log.levels.INFO) + end + end, { nargs = "?", complete = "dir", desc = "Remove a workspace from projections" }) + + vim.api.nvim_create_user_command("StoreProjectSession", function() + Session.store(uv.cwd()) + end, { desc = "Store session for current project" }) + + vim.api.nvim_create_user_command("RestoreProjectSession", function() + Session.restore(uv.cwd()) + end, { desc = "Restore session for current project" }) + end + ''; + + opts.sessionoptions = _ "buffers,curdir,folds,globals,help,localoptions,tabpages,winsize"; + } + # |----------------------------------------------------------------------| # + (mkIf telescopeCheck { + extraConfigLuaPost = '' + require("telescope").load_extension("projections") + ''; + }) + # |----------------------------------------------------------------------| # + (mkIf (cfg.withKeymaps && telescopeCheck) { + keymaps = [ + { + mode = "n"; + key = "pp"; + action = "Telescope projections"; + options = { + desc = "Telescope projections."; + }; + } + ]; + }) + # |----------------------------------------------------------------------| # + ]); + + meta.maintainers = with localFlake.lib.maintainers; [ tsandrini ]; +} diff --git a/flake-parts/modules/nixvim/profiles/minimal.nix b/flake-parts/modules/nixvim/profiles/minimal.nix index 4babef1b..3275e427 100644 --- a/flake-parts/modules/nixvim/profiles/minimal.nix +++ b/flake-parts/modules/nixvim/profiles/minimal.nix @@ -45,9 +45,13 @@ in utils.faster.enable = _ true; utils.which-key.enable = _ true; utils.orgmode.enable = _ true; - utils.project-nvim.enable = _ true; + # project.nvim is concurrent and isn't built to + # handle a ton of neovim instances open, projections is better + # utils.project-nvim.enable = _ true; + utils.projections.enable = _ true; git.neogit.enable = _ true; + git.blame.enable = _ true; editor.neo-tree.enable = _ true; editor.treesitter.enable = _ true; diff --git a/flake.lock b/flake.lock index 1ddee10b..8712e75c 100644 --- a/flake.lock +++ b/flake.lock @@ -99,11 +99,11 @@ ] }, "locked": { - "lastModified": 1771437256, - "narHash": "sha256-bLqwib+rtyBRRVBWhMuBXPCL/OThfokA+j6+uH7jDGU=", + "lastModified": 1776249299, + "narHash": "sha256-Dt9t1TGRmJFc0xVYhttNBD6QsAgHOHCArqGa0AyjrJY=", "owner": "numtide", "repo": "blueprint", - "rev": "06ee7190dc2620ea98af9eb225aa9627b68b0e33", + "rev": "56131e8628f173d24a27f6d27c0215eff57e40dd", "type": "github" }, "original": { @@ -149,15 +149,16 @@ ] }, "locked": { - "lastModified": 1770895533, - "narHash": "sha256-v3QaK9ugy9bN9RXDnjw0i2OifKmz2NnKM82agtqm/UY=", - "owner": "nix-community", + "lastModified": 1776182890, + "narHash": "sha256-+/VOe8XGq5klpU+I19D+3TcaR7o+Cwbq67KNF7mcFak=", + "owner": "Mic92", "repo": "bun2nix", - "rev": "c843f477b15f51151f8c6bcc886954699440a6e1", + "rev": "648d293c51e981aec9cb07ba4268bc19e7a8c575", "type": "github" }, "original": { - "owner": "nix-community", + "owner": "Mic92", + "ref": "catalog-support", "repo": "bun2nix", "type": "github" } @@ -303,15 +304,15 @@ "flake-compat_2": { "flake": false, "locked": { - "lastModified": 1696426674, - "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", - "owner": "edolstra", + "lastModified": 1767039857, + "narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=", + "owner": "NixOS", "repo": "flake-compat", - "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", + "rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab", "type": "github" }, "original": { - "owner": "edolstra", + "owner": "NixOS", "repo": "flake-compat", "type": "github" } @@ -454,11 +455,11 @@ "systems": "systems_4" }, "locked": { - "lastModified": 1747145434, - "narHash": "sha256-IE+J7TdsjTXiDGngWiHCE7oD/0/8XDQ8HiWgqvpaREo=", + "lastModified": 1759773842, + "narHash": "sha256-E/6qVxlyWav3Zt8Xmwmow6V7ncdOMK3zm2DWIbgOXPM=", "owner": "tsandrini", "repo": "flake-parts-builder", - "rev": "7927b57fe64ed99544e7960a4a072012bea2040f", + "rev": "7627679442eeff07539a5309ea2c0b7b4d4bc27c", "type": "github" }, "original": { @@ -490,11 +491,11 @@ "nixpkgs-lib": "nixpkgs-lib_3" }, "locked": { - "lastModified": 1743550720, - "narHash": "sha256-hIshGgKZCgWh6AYJpJmRgFdR3WUbkY04o82X05xqQiY=", + "lastModified": 1775087534, + "narHash": "sha256-91qqW8lhL7TLwgQWijoGBbiD4t7/q75KTi8NxjVmSmA=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "c621e8422220273271f52058f618c94e405bb0f5", + "rev": "3107b77cd68437b9a76194f0f7f9c55f2329ca5b", "type": "github" }, "original": { @@ -529,11 +530,11 @@ ] }, "locked": { - "lastModified": 1772408722, - "narHash": "sha256-rHuJtdcOjK7rAHpHphUb1iCvgkU3GpfvicLMwwnfMT0=", + "lastModified": 1775087534, + "narHash": "sha256-91qqW8lhL7TLwgQWijoGBbiD4t7/q75KTi8NxjVmSmA=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "f20dc5d9b8027381c474144ecabc9034d6a839a3", + "rev": "3107b77cd68437b9a76194f0f7f9c55f2329ca5b", "type": "github" }, "original": { @@ -765,11 +766,11 @@ "nixpkgs": "nixpkgs_5" }, "locked": { - "lastModified": 1709087332, - "narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=", + "lastModified": 1762808025, + "narHash": "sha256-XmjITeZNMTQXGhhww6ed/Wacy2KzD6svioyCX7pkUu4=", "owner": "hercules-ci", "repo": "gitignore.nix", - "rev": "637db329424fd7e46cf4185293b9cc8c88c95394", + "rev": "cb5e3fdca1de58ccbc3ef53de65bd372b48f567c", "type": "github" }, "original": { @@ -915,11 +916,11 @@ "systems": "systems_5" }, "locked": { - "lastModified": 1771695284, - "narHash": "sha256-ZfLvviImDapoMWg3lRpVdRdFunUA6YxXzNup5lLXi5g=", + "lastModified": 1776702401, + "narHash": "sha256-1bcqkTLsvaCDuUL6LR/4ylpWV7D4lDj0RoMRqtABv+8=", "owner": "tsandrini", "repo": "immutable-insights", - "rev": "d0a9aa5afe39c180cacc4e274b3dcd6f8230d24a", + "rev": "57d0f532602930b74b409aeedeff34197db45f20", "type": "github" }, "original": { @@ -1042,11 +1043,11 @@ "treefmt-nix": "treefmt-nix" }, "locked": { - "lastModified": 1774333104, - "narHash": "sha256-acWWrX21golBLxEQ+RxEow2MZ7KxFi8iCH1LUXUL4eg=", + "lastModified": 1776484001, + "narHash": "sha256-YBkhD3aKU/5q0G4xABRaPLsg49o8hjgYvEFyZrOr6a4=", "owner": "numtide", "repo": "llm-agents.nix", - "rev": "2935ee5defa6159e56cc31ee122f8caa3772c174", + "rev": "a350931b10f337e1d6c32a814c410feb3d4fa97a", "type": "github" }, "original": { @@ -1060,11 +1061,11 @@ "nixpkgs": "nixpkgs_10" }, "locked": { - "lastModified": 1774335578, - "narHash": "sha256-SQr8Sn33FQY20ChlFJP+GDSC+VLq2T9lRlmkAKdYPsQ=", + "lastModified": 1776495112, + "narHash": "sha256-uQRogOfHgCnzSS9E8kb8Q8wXRR58eoYozXP2xDoF8qs=", "owner": "natsukium", "repo": "mcp-servers-nix", - "rev": "52db71b8cb8b39bce3e12f1ba3ed009aaf97c15a", + "rev": "c7c9cfbf2f70fd3ea658dd44d475c5b5217aa746", "type": "github" }, "original": { @@ -1453,11 +1454,11 @@ }, "nixpkgs-lib_3": { "locked": { - "lastModified": 1743296961, - "narHash": "sha256-b1EdN3cULCqtorQ4QeWgLMrd5ZGOjLSLemfa00heasc=", + "lastModified": 1774748309, + "narHash": "sha256-+U7gF3qxzwD5TZuANzZPeJTZRHS29OFQgkQ2kiTJBIQ=", "owner": "nix-community", "repo": "nixpkgs.lib", - "rev": "e4822aea2a6d1cdd36653c134cacfd64c97ff4fa", + "rev": "333c4e0545a6da976206c74db8773a1645b5870a", "type": "github" }, "original": { @@ -1833,11 +1834,11 @@ }, "nixpkgs_6": { "locked": { - "lastModified": 1745526057, - "narHash": "sha256-ITSpPDwvLBZBnPRS2bUcHY3gZSwis/uTe255QgMtTLA=", + "lastModified": 1775036866, + "narHash": "sha256-ZojAnPuCdy657PbTq5V0Y+AHKhZAIwSIT2cb8UgAz/U=", "owner": "nixos", "repo": "nixpkgs", - "rev": "f771eb401a46846c1aebd20552521b233dd7e18b", + "rev": "6201e203d09599479a3b3450ed24fa81537ebc4e", "type": "github" }, "original": { @@ -1849,11 +1850,11 @@ }, "nixpkgs_7": { "locked": { - "lastModified": 1730768919, - "narHash": "sha256-8AKquNnnSaJRXZxc5YmF/WfmxiHX6MMZZasRP6RRQkE=", + "lastModified": 1770073757, + "narHash": "sha256-Vy+G+F+3E/Tl+GMNgiHl9Pah2DgShmIUBJXmbiQPHbI=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "a04d33c0c3f1a59a2c1cb0c6e34cd24500e5a1dc", + "rev": "47472570b1e607482890801aeaf29bfb749884f6", "type": "github" }, "original": { @@ -1881,11 +1882,11 @@ }, "nixpkgs_9": { "locked": { - "lastModified": 1773840656, - "narHash": "sha256-9tpvMGFteZnd3gRQZFlRCohVpqooygFuy9yjuyRL2C0=", + "lastModified": 1776255774, + "narHash": "sha256-psVTpH6PK3q1htMJpmdz1hLF5pQgEshu7gQWgKO6t6Y=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "9cf7092bdd603554bd8b63c216e8943cf9b12512", + "rev": "566acc07c54dc807f91625bb286cb9b321b5f42a", "type": "github" }, "original": { @@ -1980,11 +1981,11 @@ "nixpkgs": "nixpkgs_7" }, "locked": { - "lastModified": 1742649964, - "narHash": "sha256-DwOTp7nvfi8mRfuL1escHDXabVXFGT1VlPD1JHrtrco=", + "lastModified": 1775036584, + "narHash": "sha256-zW0lyy7ZNNT/x8JhzFHBsP2IPx7ATZIPai4FJj12BgU=", "owner": "cachix", "repo": "pre-commit-hooks.nix", - "rev": "dcf5072734cb576d2b0c59b2ac44f5050b5eac82", + "rev": "4e0eb042b67d863b1b34b3f64d52ceb9cd926735", "type": "github" }, "original": { @@ -2269,11 +2270,11 @@ ] }, "locked": { - "lastModified": 1773297127, - "narHash": "sha256-6E/yhXP7Oy/NbXtf1ktzmU8SdVqJQ09HC/48ebEGBpk=", + "lastModified": 1775636079, + "narHash": "sha256-pc20NRoMdiar8oPQceQT47UUZMBTiMdUuWrYu2obUP0=", "owner": "numtide", "repo": "treefmt-nix", - "rev": "71b125cd05fbfd78cab3e070b73544abe24c5016", + "rev": "790751ff7fd3801feeaf96d7dc416a8d581265ba", "type": "github" }, "original": {