flake-update-20260505
  1{
  2  pkgs,
  3  lib,
  4  ...
  5}:
  6let
  7  officemode = pkgs.writeShellScriptBin "officemode" ''
  8    echo "80" > /sys/class/power_supply/BAT0/charge_control_end_threshold
  9    echo "70" > /sys/class/power_supply/BAT0/charge_control_start_threshold
 10    echo "Office mode: charging between 70%80%"
 11  '';
 12  roadmode = pkgs.writeShellScriptBin "roadmode" ''
 13    echo "100" > /sys/class/power_supply/BAT0/charge_control_end_threshold
 14    echo "0" > /sys/class/power_supply/BAT0/charge_control_start_threshold
 15    echo "Road mode: charging to 100%"
 16  '';
 17in
 18{
 19
 20  imports = [
 21    ../common/hardware/laptop.nix
 22    ../common/programs/direnv.nix
 23    ../common/programs/git.nix
 24    ../common/programs/tmux.nix
 25    ../common/services/networkmanager.nix
 26    ../common/services/containers.nix
 27    ../common/services/docker.nix
 28    ../common/services/libvirt.nix
 29    ../common/services/binfmt.nix
 30    ../common/services/oomd.nix
 31    ../../modules/laptop-keyboard-inhibit
 32
 33    ../redhat
 34  ];
 35
 36  # It takes.. multiple GB, and I don't really use it...
 37  programs.obs-studio = {
 38    enable = false;
 39    plugins = with pkgs.obs-studio-plugins; [
 40      wlrobs
 41      obs-backgroundremoval
 42      obs-pipewire-audio-capture
 43      input-overlay
 44    ];
 45  };
 46
 47  # Auto-disable internal keyboard when Moonlander (USB) is connected
 48  services.laptop-keyboard-inhibit = {
 49    enable = true;
 50    keyboards = [ "Eyelash Corne" ];
 51    usbKeyboards = [
 52      {
 53        vendor = "3297";
 54        product = "1969";
 55      }
 56    ];
 57  };
 58
 59  services.keybase.enable = true;
 60  services.kbfs = {
 61    enable = true;
 62    mountPoint = "%t/keybase";
 63  };
 64
 65  services = {
 66    getty = {
 67      autologinOnce = true;
 68      autologinUser = "vincent";
 69    };
 70    # TODO probably migrate elsewhere
 71    kanata = {
 72      enable = true;
 73      package = pkgs.kanata-with-cmd;
 74      keyboards.x1 = {
 75        devices = [ "/dev/input/event0" ]; # internal keyboard
 76        config = builtins.readFile (./. + "/main.kbd");
 77        extraDefCfg = ''
 78          	danger-enable-cmd yes
 79            process-unmapped-keys yes
 80            override-release-on-activation yes
 81            concurrent-tap-hold yes
 82        '';
 83      };
 84    };
 85    locate = {
 86      enable = true;
 87      pruneBindMounts = true;
 88    };
 89
 90    hardware.bolt.enable = true;
 91    printing = {
 92      enable = true;
 93      drivers = with pkgs; [
 94        # cnijfilter2 # Disabled: broken in nixpkgs-unstable (bool typedef error)
 95        gutenprint
 96        gutenprintBin
 97      ];
 98    };
 99  };
100
101  # Canon MX530 printer via Gutenprint driver (driverless/IPP auto-config
102  # defaults to Photographic media type, causing documents to print tiny)
103  hardware.printers = {
104    ensurePrinters = [
105      {
106        name = "Canon_MX530_Gutenprint";
107        description = "Canon MX530 series (Gutenprint)";
108        location = "Home";
109        deviceUri = "ipp://192.168.1.16:631/ipp/print";
110        model = "gutenprint.${lib.versions.majorMinor (lib.getVersion pkgs.gutenprint)}://bjc-MX530-series/expert";
111        ppdOptions = {
112          PageSize = "A4";
113          MediaType = "Plain";
114          InputSlot = "Front";
115          ColorModel = "RGB";
116          Duplex = "None";
117          StpQuality = "Standard";
118        };
119      }
120    ];
121    ensureDefaultPrinter = "Canon_MX530_Gutenprint";
122  };
123
124  hardware.keyboard.qmk.enable = true;
125
126  services.udev.packages = [ pkgs.sane-airscan ];
127  services.udev.extraRules = ''
128    # Disable autosuspend for Logitech C920 to prevent xHCI controller resets
129    # that cascade to the r8152 USB ethernet adapter, causing packet drops.
130    ACTION=="add", SUBSYSTEM=="usb", ATTR{idVendor}=="046d", ATTR{idProduct}=="082d", ATTR{power/control}="on"
131  '';
132  hardware.sane = {
133    enable = true;
134    extraBackends = [ pkgs.sane-airscan ];
135    openFirewall = true;
136    netConf = "192.168.12.70";
137  };
138
139  # Battery charge thresholds: default to office mode on boot
140  systemd.services.battery-charge-threshold = {
141    description = "Set battery charge threshold to office mode (80%)";
142    wantedBy = [ "multi-user.target" ];
143    after = [ "multi-user.target" ];
144    serviceConfig = {
145      Type = "oneshot";
146      ExecStart = "${officemode}/bin/officemode";
147    };
148  };
149
150  security.sudo.extraRules = [
151    {
152      groups = [ "wheel" ];
153      commands = [
154        {
155          command = "${officemode}/bin/officemode";
156          options = [ "NOPASSWD" ];
157        }
158        {
159          command = "${roadmode}/bin/roadmode";
160          options = [ "NOPASSWD" ];
161        }
162      ];
163    }
164  ];
165
166  environment.systemPackages = with pkgs; [
167    kanata
168    nixos-rebuild-ng
169    battery-monitor
170    # backup
171    virt-manager
172  ];
173
174  # Make sure we don't start docker until required
175  systemd.services.docker.wantedBy = lib.mkForce [ ];
176
177  # Slack Archive - daily backup of public Slack channels
178  systemd.tmpfiles.rules = [
179    "d /var/lib/slack-archive 0750 vincent users -"
180  ];
181
182  systemd.services.slack-archive = {
183    description = "Slack Public Channel Archiver";
184    after = [ "network-online.target" ];
185    wants = [ "network-online.target" ];
186
187    serviceConfig = {
188      Type = "oneshot";
189      User = "vincent";
190      Group = "users";
191      ExecStart = "${pkgs.slack-archive}/bin/slack-archive archive";
192      Environment = [
193        "SLACK_ARCHIVE_DIR=/var/lib/slack-archive"
194        "SLACK_ARCHIVE_HTML_DIR=/home/vincent/src/experiments/tektoncd-slack-archive"
195        "HOME=/home/vincent"
196        "XDG_CACHE_HOME=/home/vincent/.local/cache"
197      ];
198
199      # Security hardening
200      PrivateTmp = true;
201      ProtectSystem = "strict";
202      ProtectHome = "read-only";
203      ReadWritePaths = [
204        "/var/lib/slack-archive"
205        "/home/vincent/.local/cache/slackdump"
206        "/home/vincent/.local/cache/uv"
207        "/home/vincent/.local/share/uv"
208        "/home/vincent/src/experiments/tektoncd-slack-archive"
209      ];
210      NoNewPrivileges = true;
211
212      # Logging
213      StandardOutput = "journal";
214      StandardError = "journal";
215      SyslogIdentifier = "slack-archive";
216    };
217  };
218
219  systemd.timers.slack-archive = {
220    description = "Daily Slack Archive Timer";
221    wantedBy = [ "timers.target" ];
222
223    timerConfig = {
224      OnCalendar = "09:00"; # Run at 9 AM daily (system should be fully up by then)
225      RandomizedDelaySec = 1800; # 0-30 min random delay
226      Persistent = true;
227    };
228  };
229}