main
  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    # Force Gigabit advertisement on ThinkPad dock ethernet (r8152/RTL8153).
133    # Without this, auto-negotiation only advertises 100Mbps.
134    ACTION=="add", SUBSYSTEM=="net", DRIVERS=="r8152", ATTR{address}=="00:50:b6:b4:2c:14", RUN+="${pkgs.ethtool}/bin/ethtool -s $env{INTERFACE} advertise 0x03F"
135  '';
136  hardware.sane = {
137    enable = true;
138    extraBackends = [ pkgs.sane-airscan ];
139    openFirewall = true;
140    netConf = "192.168.12.70";
141  };
142
143  # Battery charge thresholds: default to office mode on boot
144  systemd.services.battery-charge-threshold = {
145    description = "Set battery charge threshold to office mode (80%)";
146    wantedBy = [ "multi-user.target" ];
147    after = [ "multi-user.target" ];
148    serviceConfig = {
149      Type = "oneshot";
150      ExecStart = "${officemode}/bin/officemode";
151    };
152  };
153
154  security.sudo.extraRules = [
155    {
156      groups = [ "wheel" ];
157      commands = [
158        {
159          command = "${officemode}/bin/officemode";
160          options = [ "NOPASSWD" ];
161        }
162        {
163          command = "${roadmode}/bin/roadmode";
164          options = [ "NOPASSWD" ];
165        }
166      ];
167    }
168  ];
169
170  environment.systemPackages = with pkgs; [
171    kanata
172    nixos-rebuild-ng
173    battery-monitor
174    # backup
175    virt-manager
176  ];
177
178  # Make sure we don't start docker until required
179  systemd.services.docker.wantedBy = lib.mkForce [ ];
180
181  # Slack Archive - daily backup of public Slack channels
182  systemd.tmpfiles.rules = [
183    "d /var/lib/slack-archive 0750 vincent users -"
184  ];
185
186  systemd.services.slack-archive = {
187    description = "Slack Public Channel Archiver";
188    after = [ "network-online.target" ];
189    wants = [ "network-online.target" ];
190
191    serviceConfig = {
192      Type = "oneshot";
193      User = "vincent";
194      Group = "users";
195      ExecStart = "${pkgs.slack-archive}/bin/slack-archive archive";
196      Environment = [
197        "SLACK_ARCHIVE_DIR=/var/lib/slack-archive"
198        "SLACK_ARCHIVE_HTML_DIR=/home/vincent/src/experiments/tektoncd-slack-archive"
199        "HOME=/home/vincent"
200        "XDG_CACHE_HOME=/home/vincent/.local/cache"
201      ];
202
203      # Security hardening
204      PrivateTmp = true;
205      ProtectSystem = "strict";
206      ProtectHome = "read-only";
207      ReadWritePaths = [
208        "/var/lib/slack-archive"
209        "/home/vincent/.local/cache/slackdump"
210        "/home/vincent/.local/cache/uv"
211        "/home/vincent/.local/share/uv"
212        "/home/vincent/src/experiments/tektoncd-slack-archive"
213      ];
214      NoNewPrivileges = true;
215
216      # Logging
217      StandardOutput = "journal";
218      StandardError = "journal";
219      SyslogIdentifier = "slack-archive";
220    };
221  };
222
223  systemd.timers.slack-archive = {
224    description = "Daily Slack Archive Timer";
225    wantedBy = [ "timers.target" ];
226
227    timerConfig = {
228      OnCalendar = "09:00"; # Run at 9 AM daily (system should be fully up by then)
229      RandomizedDelaySec = 1800; # 0-30 min random delay
230      Persistent = true;
231    };
232  };
233}