diff options
Diffstat (limited to 'modules/workstation')
54 files changed, 1601 insertions, 0 deletions
diff --git a/modules/workstation/audio/default.nix b/modules/workstation/audio/default.nix new file mode 100644 index 000000000000..46b22655f50b --- /dev/null +++ b/modules/workstation/audio/default.nix @@ -0,0 +1,9 @@ +{ pkgs, ... }: + +{ + environment.systemPackages = with pkgs; [ pavucontrol ]; + + services.pipewire.enable = true; + services.pipewire.jack.enable = true; + services.pipewire.pulse.enable = true; +} diff --git a/modules/workstation/default.nix b/modules/workstation/default.nix new file mode 100644 index 000000000000..d1b4ae4da0da --- /dev/null +++ b/modules/workstation/default.nix @@ -0,0 +1,27 @@ +{ lib, pkgs, ... }: + +{ + imports = [ + ../nix ../shell ../users ../ssh + ./documentation ./windowing ./fonts ./gh ./hardware ./locale + ./dict ./emacs ./gnupg ./lorri ./mail ./mpv ./weechat + ]; + + boot.kernelParams = [ "preempt=full" ]; + + environment.systemPackages = with pkgs; [ + dino ffmpeg mosh mpv nix-index qemu youtube-dl + ]; + + services.getty.autologinUser = "qyliss"; + services.getty.loginOptions = "-- \\u"; + services.locate.enable = true; + + time.timeZone = "Europe/Berlin"; + + virtualisation.podman.enable = true; + + programs.system-config-printer.enable = true; + services.printing.enable = true; + services.printing.drivers = with pkgs; [ foo2zjs ]; +} diff --git a/modules/workstation/dict/default.nix b/modules/workstation/dict/default.nix new file mode 100644 index 000000000000..92a619e75d23 --- /dev/null +++ b/modules/workstation/dict/default.nix @@ -0,0 +1,6 @@ +{ pkgs, ... }: + +{ + services.dictd.enable = true; + services.dictd.DBs = with pkgs.dictdDBs; [ wiktionary ]; +} diff --git a/modules/workstation/documentation/default.nix b/modules/workstation/documentation/default.nix new file mode 100644 index 000000000000..590da0e7329b --- /dev/null +++ b/modules/workstation/documentation/default.nix @@ -0,0 +1,14 @@ +{ pkgs, ... }: + +{ + environment.systemPackages = with pkgs; [ + autoconf.doc gcc.info glibcInfo gnumake.info gnused.info groff.doc + groff.man libbsd man-pages posix_man_pages (lowPrio mandoc) + wayland.man + ]; + + documentation.man.generateCaches = true; + documentation.man.extraConfig = '' + SECTION 1 n l 8 3 0 2 3p 3am 5 4 9 6 7 + ''; +} diff --git a/modules/workstation/emacs/default.nix b/modules/workstation/emacs/default.nix new file mode 100644 index 000000000000..fa26e98ce64e --- /dev/null +++ b/modules/workstation/emacs/default.nix @@ -0,0 +1,82 @@ +{ config, pkgs, ... }: + +let + emacs = ((with pkgs; emacsPackagesFor (emacs29-pgtk.overrideAttrs ( + { patches ? [], ... }: + { + patches = patches ++ [ + (fetchpatch { + url = "https://git.savannah.gnu.org/cgit/emacs.git/patch/?id=e4e89e2cb663c730fd563d89228fe3a9a34e63e5"; + hash = "sha256-/7vWMFXjmmmTg6nNvEPOM3LkZ+j3IVV9W127anx5deI="; + }) + ]; + } + ))).emacsWithPackages (epkgs: with epkgs; [ + adoc-mode + cmake-mode + code-review + csharp-mode + d-mode + direnv + dockerfile-mode + dts-mode + ebuild-mode + editorconfig + eglot + elpher + execline + forge + gn-mode + gnuplot-mode + go-mode + graphql-mode + graphviz-dot-mode + groovy-mode + haskell-mode + jam-mode + just-mode + kotlin-mode + lua-mode + magit + markdown-mode + meson-mode + monokai-theme + mutt-mode + ninja-mode + nix-mode + notmuch + org-roam + pass + pod-mode + protobuf-mode + rainbow-delimiters + rust-mode + sdlang-mode + sort-words + toml-mode + tuareg + typescript-mode + udev-mode + vala-mode + yaml-mode + zig-mode + ])); +in + +{ + environment.systemPackages = [ emacs ]; + + systemd.tmpfiles.rules = [ + "d ${config.users.users.qyliss.home}/notes 0700 qyliss qyliss" + ]; + + users.users.qyliss.xdg.config.paths."emacs" = pkgs.runCommand ".emacs.d" { + nativeBuildInputs = [ emacs ]; + } '' + cp ${./early-init.el} early-init.el + cp ${./init.el} init.el + emacs -L . --batch -f batch-byte-compile *.el + install -d $out + install *.el *.elc $out + ''; +} diff --git a/modules/workstation/emacs/early-init.el b/modules/workstation/emacs/early-init.el new file mode 100644 index 000000000000..8cac0647dd4e --- /dev/null +++ b/modules/workstation/emacs/early-init.el @@ -0,0 +1,4 @@ +;; This defaults to XDG_CONFIG_HOME, which is bad because lots of +;; state gets written to it. +(setq user-emacs-directory + (concat (file-name-as-directory (getenv "XDG_DATA_HOME")) "emacs")) diff --git a/modules/workstation/emacs/init.el b/modules/workstation/emacs/init.el new file mode 100644 index 000000000000..8370fe153fd7 --- /dev/null +++ b/modules/workstation/emacs/init.el @@ -0,0 +1,284 @@ +;; -*- lexical-binding: t -*- + +;;; UI customization ;;; +(column-number-mode) +(electric-pair-mode) +(global-so-long-mode) +(menu-bar-mode -1) +(scroll-bar-mode -1) +(show-paren-mode) +(tool-bar-mode -1) + +(setq frame-title-format "%b — Emacs") +(setq uniquify-buffer-name-style 'forward) +(setq-default truncate-lines t) + +(load-theme 'monokai t) + +;; <102 or >109 will make Ioveska look squished rather than tall, +;; narrow, and beautiful. +(set-face-attribute 'default nil :height 104) + +;; Disabling bidirectional text will make Emacs less slow with very +;; long lines. +(setq bidi-paragraph-direction 'left-to-right) +(setq bidi-inhibit-bpa t) + +;;; Utility functions ;;; + +;; Set an environment variable value for a single buffer. +(defun setenv-local (variable &optional value substitute-env-vars) + ;; If process-environment is the global one, make a copy of it. + (when (eq process-environment (default-value 'process-environment)) + (make-local-variable 'process-environment) + (setq process-environment (mapcar 'concat process-environment))) + (setenv variable value substitute-env-vars)) + +(defun enable-ansi-color () + (setenv-local "TERM" "dumb-emacs-ansi")) + +;;; Major modes ;;; +(add-to-list 'auto-mode-alist '("\\.adoc\\'" . adoc-mode)) +(add-to-list 'auto-mode-alist '("\\.quirks\\'" . conf-unix-mode)) +(add-to-list 'auto-mode-alist '("\\.gni?\\'" . gn-mode)) +(add-to-list 'auto-mode-alist '("/muttrc\\'" . mutt-mode)) +(add-to-list 'auto-mode-alist '("\\.tmac\\'" . nroff-mode)) +(add-to-list 'auto-mode-alist '("/Cargo\\.lock\\'" . toml-mode)) +(add-to-list 'auto-mode-alist '("/\\.clang-format\\'" . yaml-mode)) +(add-to-list 'auto-mode-alist '("/up\\'" . execline-mode)) +(add-to-list 'auto-mode-alist '("/down\\'" . execline-mode)) + +;;; auth-source ;;; +(auth-source-pass-enable) + +;;; Backups ;;; +(let ((backup-dir + (concat (file-name-as-directory user-emacs-directory) "backups"))) + (make-directory backup-dir t) + (setq backup-directory-alist `(("." . ,backup-dir))) + (setq message-auto-save-directory backup-dir)) + +;;; CC Mode ;;; +(with-eval-after-load 'cc-styles + ;; Default to kernel style. + (add-to-list 'c-default-style '(other . "linux"))) + +;;; Comint ;;; +;; This relies on an Emacs patch to apply to async-shell-command. +(setq comint-terminfo-terminal "dumb-emacs-ansi") + +;;; Diff Mode ;;; +;; Unbind M-DEL from scroll-down-command, so it does the same thing as +;; in the rest of Emacs (backward-kill-word). +(with-eval-after-load 'diff-mode + (define-key diff-mode-map (kbd "M-DEL") nil)) + +;;; Dired ;;; +(with-eval-after-load 'dired + ;; Use human-readable sizes. + (setq dired-listing-switches + (combine-and-quote-strings `(,dired-listing-switches "-h"))) + + ;; Don't open new windows when clicking directory entries. + (define-key dired-mode-map [mouse-2] 'dired-find-file)) + +;;; Direnv ;;; +(direnv-mode) +(global-set-key (kbd "C-c d") #'direnv-update-environment) + +;;; Ediff ;;; +;; Don't make a new frame for Ediff controls. +(setq ediff-window-setup-function 'ediff-setup-windows-plain) + +;;; EditorConfig ;;; +(editorconfig-mode) + +;;; Eglot ;;; +(with-eval-after-load 'eglot + ;; Override default LSPs for languages. + (add-to-list 'eglot-server-programs '((c++-mode c-mode) "clangd")) + (add-to-list 'eglot-server-programs '(rust-mode "rust-analyzer")) + (add-to-list 'eglot-server-programs '(nix-mode "rnix-lsp")) + + ;; Underline all occurrences of the symbol at point in the current + ;; buffer (default is to bold instead). + (set-face-attribute 'eglot-highlight-symbol-face nil :inherit 'underline) + + ;; Add keybindings for eglot actions. + (define-key eglot-mode-map (kbd "C-c e a") #'eglot-code-actions) + (define-key eglot-mode-map (kbd "C-c e r") #'eglot-rename)) + +;; Tell rust-analyzer not to allow colons inside Rust import. +(setq-default eglot-workspace-configuration + '((:rust-analyzer . (:assist (:importMergeBehaviour "last"))))) + +;;; Environment ;;; + +;; Provide a way to turn paging back on for modes like terminal emulators. +(let ((pager (getenv "PAGER"))) + (defun enable-pager () + (setenv-local "PAGER" pager))) + +;; Set PAGER to the empty string, which Git and journalctl will +;; interpret as an explicit opt-out of paging. +(setenv "PAGER" "") + +;;; Eshell ;;; +(add-hook 'eshell-mode-hook #'enable-ansi-color) + +;;; gdb-mi ;;; +(with-eval-after-load "gdb-mi" + ;; Hide all the copyright and documentation messages at gdb startup. + (setq gud-gdb-command-name (concat gud-gdb-command-name " -q"))) + +;;; Ibuffer ;;; +;; Open ibuffer in other window, to match behaviour of the default +;; list-buffers. +(global-set-key (kbd "C-x C-b") (lambda () (interactive) (ibuffer t))) + +;;; ispell ;;; +;; System locale is Esperanto, but I write in English much more, and +;; spelling in Esperanto is easy anyway. ;) +(setq ispell-dictionary "english") + +;;; Magit ;;; +(setq magit-delete-by-moving-to-trash delete-by-moving-to-trash) +(setq magit-repository-directories + `((,(expand-file-name "~/src") . 1))) +(global-set-key (kbd "C-x g") #'magit-status) +(global-set-key (kbd "C-x M-g") #'magit-dispatch) +(with-eval-after-load "git-commit" + (add-to-list 'git-commit-trailers "Change-Id") + (add-to-list 'git-commit-trailers "Fixes") + (add-to-list 'git-commit-trailers "Message-Id")) + +;; Don't open a second window with a diff when committing. I have +;; commit.verbose set in git, so the diff is shown below the commit +;; anyway, and it's annoying to have magit take over the whole frame +;; because it means I can't refer to something else, like an email +;; thread discussing the commit. And sometimes the diff shown by +;; magit doesn't agree with the diff shown by git (and git is always +;; in the right when this happens). +(setq magit-commit-show-diff nil) + +(defun git-commit-message-pretty-ref (commit) + "Generate a reference to git COMMIT in the format used in the +Linux kernel (`SHORT-HASH (\"SHORT-MESSAGE\")') and insert it at +point." + (interactive + (list (or (nreverse (magit-region-values 'commit)) + (magit-read-other-branch-or-commit "Reference commit")))) + (call-process (magit-git-executable) nil t nil + "show" "--no-patch" "--pretty=format:%h (\"%s\")" commit)) + +(with-eval-after-load "magit" + (define-key magit-process-mode-map (kbd "k") #'magit-process-kill) + + ;; Add a --no-gpg-sign option to Magit. This is only useful with + ;; commit.gpgsign=true. It would be nice if commit.gpgsign=true just + ;; meant that Magit showed the --gpg-sign option as enabled by + ;; default, and disabling it would make Magit pass --no-gpg-sign to + ;; git, but that's not currently the case, so we need a seperate + ;; option for --no-gpg-sign. See + ;; <https://github.com/magit/magit/issues/3832>. + (dolist (command '(magit-commit magit-merge magit-cherry-pick + magit-revert magit-am magit-rebase)) + (transient-append-suffix command ["-S"] + '("=S" "Don't sign using gpg" "--no-gpg-sign")))) + +;;; Markdown ;;; +(setq-default markdown-hide-markup t) + +;;; Man ;;; +;; Open man pages in the current window. I don't really care what +;; happens when I do M-x man, but I don't want my whole frame to be +;; taken over with man buffers if I'm following a bunch of +;; cross-references. +(setq Man-notify-method 'pushy) + +;;; MML ;;; +(setq mml-secure-openpgp-encrypt-to-self t) +(setq mml-secure-openpgp-signers '("757356D779BBB888773E415E736CCDF9EF51BD97")) +(add-hook 'message-setup-hook 'mml-secure-message-sign-pgpmime) + +;;; Nix support ;;; +(defun browse-url-nixpkgs (attr) + "Open a browser to the homepage for the Nixpkgs attribute ATTR." + (interactive "sAttribute: ") + (let* ((full-attr-quoted (shell-quote-argument (concat attr ".meta.homepage"))) + (command (concat "nix --extra-experimental-features nix-command eval --raw -f '<nixpkgs>' " full-attr-quoted)) + (homepage (shell-command-to-string command))) + (browse-url homepage))) + +;;; notmuch ;;; +(setq notmuch-search-oldest-first nil) +(setq notmuch-fcc-dirs "Sent") +(setq notmuch-draft-folder "Drafts") +(setq notmuch-saved-searches + `((:name "direct" :query ,(concat "-tag:done -folder:Spam -from:discourse@discourse.nixos.org to:" (getenv "EMAIL")) :key "d") + (:name "github" :query "-tag:done -to:your_activity@noreply.github.com from:notifications@github.com" :key "g") + (:name "lists" :query "-tag:done to:afra@afra-berlin.de OR to:@list.skarnet.org OR wayland-devel@lists.freedesktop.org OR to:distributions@lists.linux.dev OR to:virglrenderer-devel@lists.freedesktop.org OR to:config-patches@gnu.org OR to:linux-kernel-announce@vger.kernel.org" :key "l"))) +(setq notmuch-tagging-keys '(("d" ("+done") "done"))) + +;;; mail ;;; +; This has to go after we set the notmuch settings, for some reason, +; or they are not applied. +(require 'notmuch-mua) +(setq mail-user-agent 'notmuch-user-agent) + +;;; Org-mode ;;; +(setq org-id-locations-file (concat user-emacs-directory "/org-id-locations")) + +;;; Org-roam ;;; +(setq org-roam-directory "~/notes") +(setq org-roam-v2-ack t) +(org-roam-db-autosync-mode) +(global-set-key (kbd "C-c o c") #'org-roam-capture) +(global-set-key (kbd "C-c o f") #'org-roam-node-find) +(define-key org-mode-map (kbd "C-c o i") #'org-roam-node-insert) +(define-key org-mode-map (kbd "C-c o r") #'org-roam-ref-add) + +;;; Rainbow Delimiters ;;; +(add-hook 'prog-mode-hook #'rainbow-delimiters-mode) + +;;; Revert buffers ;;; +(global-set-key (kbd "C-c r") #'revert-buffer) + +;;; Ruby ;;; +(setq ruby-align-to-stmt-keywords t) + +;;; Rust ;;; +;; Indent with spaces in Rust code. +(add-hook 'rust-mode-hook (lambda () (setq indent-tabs-mode nil))) + +(with-eval-after-load 'rust-mode + ;; Cargo keybindings + (define-key rust-mode-map (kbd "C-c c b") 'rust-compile) + (define-key rust-mode-map (kbd "C-c c c") 'rust-check) + (define-key rust-mode-map (kbd "C-c c r") 'rust-run) + (define-key rust-mode-map (kbd "C-c c t") 'rust-test)) + +;;; save-some-buffers ;;; +;; Allow reverting buffers directly from the "Save file ...?" message. +(add-to-list 'save-some-buffers-action-alist + `(?r ,(lambda (buf) (with-current-buffer buf (revert-buffer t t))) + ,(purecopy "revert this buffer"))) + +;;; sendmail ;;; +;; Use the system "sendmail" program to send mail. +(setq mail-envelope-from 'header) +(setq mail-specify-envelope-from t) +(setq send-mail-function 'sendmail-send-it) + +;;; Term ;;; +(add-hook 'term-mode-hook #'enable-pager) + +;;; Transient ;;; +;; Make all Magit options available, even those that are disabled by +;; default because they're too obscure. +(setq transient-default-level 7) + +;;; with-editor ;;; +(add-hook 'eshell-mode-hook 'with-editor-export-editor) +(add-hook 'shell-mode-hook 'with-editor-export-editor) +(add-hook 'term-exec-hook 'with-editor-export-editor) diff --git a/modules/workstation/fonts/default.nix b/modules/workstation/fonts/default.nix new file mode 100644 index 000000000000..309a863cd826 --- /dev/null +++ b/modules/workstation/fonts/default.nix @@ -0,0 +1,9 @@ +{ pkgs, ... }: + +{ + fonts.packages = with pkgs; [ + ccsymbols fira iosevka twemoji-color-font noto-fonts-cjk + ]; + fonts.fontconfig.allowBitmaps = false; + fonts.fontconfig.defaultFonts.monospace = [ "Iosevka" ]; +} diff --git a/modules/workstation/gh/default.nix b/modules/workstation/gh/default.nix new file mode 100644 index 000000000000..b3dc60ed9407 --- /dev/null +++ b/modules/workstation/gh/default.nix @@ -0,0 +1,21 @@ +{ lib, pkgs, ... }: + +let + wrapper = with pkgs; writeScriptBin "gh" '' + #! ${execline}/bin/execlineb -s0 + + fdmove 3 0 + pipeline { ${pass}/bin/pass show api.github.com } + fdswap 0 3 + export GH_TOKEN_FD 3 + + importas -i config_home XDG_CONFIG_HOME + export GH_CONFIG_DIR ''${config_home}/gh + + ${pkgs.gh}/bin/gh $@ + ''; +in + +{ + environment.systemPackages = [ pkgs.gh (lib.hiPrio wrapper) ]; +} diff --git a/modules/workstation/gnupg/default.nix b/modules/workstation/gnupg/default.nix new file mode 100644 index 000000000000..eda898314258 --- /dev/null +++ b/modules/workstation/gnupg/default.nix @@ -0,0 +1,38 @@ +{ config, pkgs, ... }: + +let + gnupgHome = "${config.users.users.qyliss.home}/state/gnupg"; + + pinentryProgram = + if pkgs.stdenv.isDarwin then + "/Applications/pinentry-mac.app/Contents/MacOS/pinentry-mac" + else + "${pkgs.pinentry.qt}/bin/pinentry"; + + gpg-agent-conf = pkgs.writeText "gpg-agent.conf" '' + pinentry-program ${pinentryProgram} + ''; +in + +{ + systemd.tmpfiles.rules = [ + "d ${gnupgHome} 0700 qyliss qyliss" + "L+ ${gnupgHome}/dirmngr.conf - - - - ${./dirmngr.conf}" + "L+ ${gnupgHome}/gpg.conf - - - - ${./gpg.conf}" + "L+ ${gnupgHome}/gpg-agent.conf - - - - ${gpg-agent-conf}" + ]; + + environment.systemPackages = with pkgs; [ gnupg pinentry ]; + + environment.extraInit = '' + export GNUPGHOME="$HOME/state/gnupg" + + if [ -z "$SSH_AUTH_SOCK" ]; then + export SSH_AUTH_SOCK="$(gpgconf --list-dirs agent-ssh-socket)" + fi + ''; + + programs.sway.extraConfig = '' + exec gpg-connect-agent /bye + ''; +} diff --git a/modules/workstation/gnupg/dirmngr.conf b/modules/workstation/gnupg/dirmngr.conf new file mode 100644 index 000000000000..14114144cc9d --- /dev/null +++ b/modules/workstation/gnupg/dirmngr.conf @@ -0,0 +1 @@ +keyserver hkps://keys.openpgp.org diff --git a/modules/workstation/gnupg/gpg.conf b/modules/workstation/gnupg/gpg.conf new file mode 100644 index 000000000000..ab999bfbc32a --- /dev/null +++ b/modules/workstation/gnupg/gpg.conf @@ -0,0 +1,3 @@ +ask-cert-level +auto-key-retrieve +trust-model tofu+pgp diff --git a/modules/workstation/hardware/bluetooth/default.nix b/modules/workstation/hardware/bluetooth/default.nix new file mode 100644 index 000000000000..fb6a06a03e5f --- /dev/null +++ b/modules/workstation/hardware/bluetooth/default.nix @@ -0,0 +1,5 @@ +{ ... }: + +{ + hardware.bluetooth.enable = true; +} diff --git a/modules/workstation/hardware/default.nix b/modules/workstation/hardware/default.nix new file mode 100644 index 000000000000..64e06aecf363 --- /dev/null +++ b/modules/workstation/hardware/default.nix @@ -0,0 +1,11 @@ +{ pkgs, ... }: + +{ + imports = [ ./keyboard ./libinput ./yubikey ]; + + environment.systemPackages = with pkgs; [ brightnessctl ddrescue usbutils ]; + + hardware.sane.enable = true; + + sound.enable = true; +} diff --git a/modules/workstation/hardware/keyboard/default.nix b/modules/workstation/hardware/keyboard/default.nix new file mode 100644 index 000000000000..3bd9dc4280db --- /dev/null +++ b/modules/workstation/hardware/keyboard/default.nix @@ -0,0 +1,15 @@ +{ pkgs, config, ... }: + +let + xcfg = config.services.xserver; + +in +{ + console.useXkbConfig = true; + services.xserver.xkb.variant = "dvorak"; + services.xserver.xkb.options = "caps:escape,compose:menu,compose:prsc"; + + environment.variables.XKB_DEFAULT_LAYOUT = xcfg.xkb.layout; + environment.variables.XKB_DEFAULT_VARIANT = xcfg.xkb.variant; + environment.variables.XKB_DEFAULT_OPTIONS = xcfg.xkb.options; +} diff --git a/modules/workstation/hardware/libinput/default.nix b/modules/workstation/hardware/libinput/default.nix new file mode 100644 index 000000000000..c3a56e22feb7 --- /dev/null +++ b/modules/workstation/hardware/libinput/default.nix @@ -0,0 +1,13 @@ +{ ... }: + +{ + environment.etc."libinput/local-overrides.quirks".text = '' + [Google Chromebook Eve] + MatchUdevType=touchpad + MatchName=ACPI0C50:00 18D1:5028 + MatchDMIModalias=dmi:*svnGoogle:pnEve* + ModelChromebook=1 + AttrPressureRange=6:4 + AttrThumbPressureThreshold=45 + ''; +} diff --git a/modules/workstation/hardware/pixelbook/default.nix b/modules/workstation/hardware/pixelbook/default.nix new file mode 100644 index 000000000000..b708abc85a3d --- /dev/null +++ b/modules/workstation/hardware/pixelbook/default.nix @@ -0,0 +1,17 @@ +{ ... }: + +{ + imports = [ ../../../nixos-hardware/google/pixelbook ]; + + boot.postBootCommands = '' + # Remap Google Assistant to left super. + /run/current-system/sw/bin/setkeycodes e058 125 + + # Remap menu to compose. + /run/current-system/sw/bin/setkeycodes 5d 127 + ''; + + services.logind.extraConfig = '' + HandlePowerKey=suspend + ''; +} diff --git a/modules/workstation/hardware/yubikey/default.nix b/modules/workstation/hardware/yubikey/default.nix new file mode 100644 index 000000000000..0f2d63e742b9 --- /dev/null +++ b/modules/workstation/hardware/yubikey/default.nix @@ -0,0 +1,14 @@ +{ pkgs, ... }: + +{ + services.udev.packages = with pkgs; [ yubikey-personalization ]; + + security.pam.services.sudo.u2fAuth = true; + security.sudo.extraConfig = '' + Defaults timestamp_timeout=0 + ''; + + security.pam.u2f.appId = "pam://qyliss.net"; + security.pam.u2f.cue = true; + security.pam.u2f.authFile = pkgs.copyPathToStore ./u2f_keys; +} diff --git a/modules/workstation/hardware/yubikey/u2f_keys b/modules/workstation/hardware/yubikey/u2f_keys new file mode 100644 index 000000000000..1bb4f2f4c3a4 --- /dev/null +++ b/modules/workstation/hardware/yubikey/u2f_keys @@ -0,0 +1 @@ +qyliss:xEFvYinTZARyMSapx3wLN6U0w5oy7he66DM9Ww1EIUwr1eEsdEVX6JTayiol+XyGkThCMCRPJcEvTqR8cgXd8A==,0gl+uRidS5Dhx1S54aniQhJE+anN6hhTelZ8DncvpM2Z6MLfc/QUotFFE0cCBEA6KbQOIK9+WtgabFGOqBmljw==,es256,+presence diff --git a/modules/workstation/locale/default.nix b/modules/workstation/locale/default.nix new file mode 100644 index 000000000000..c079ab19bc88 --- /dev/null +++ b/modules/workstation/locale/default.nix @@ -0,0 +1,12 @@ +{ ... }: + +{ + i18n.defaultLocale = "eo"; + i18n.extraLocaleSettings.LC_ADDRESS = "de_DE.UTF-8"; + i18n.extraLocaleSettings.LC_CTYPE = "de_DE.UTF-8"; + i18n.extraLocaleSettings.LC_NAME = "en_GB.UTF-8"; + + # Like Germany, but uses thin spaces as thousands separators. + i18n.extraLocaleSettings.LC_MONETARY = "fr_FR.UTF-8"; + i18n.extraLocaleSettings.LC_NUMERIC = "fr_FR.UTF-8"; +} diff --git a/modules/workstation/lorri/default.nix b/modules/workstation/lorri/default.nix new file mode 100644 index 000000000000..602fac6106f7 --- /dev/null +++ b/modules/workstation/lorri/default.nix @@ -0,0 +1,9 @@ +{ config, ... }: + +{ + services.lorri.enable = true; + + # FIXME: systemd should have this set globally. + systemd.user.services.lorri.environment.XDG_CACHE_HOME = + "${config.users.users.qyliss.home}/state/cache"; +} diff --git a/modules/workstation/mail/default.nix b/modules/workstation/mail/default.nix new file mode 100644 index 000000000000..ff34fab8b98e --- /dev/null +++ b/modules/workstation/mail/default.nix @@ -0,0 +1,11 @@ +{ pkgs, config, ... }: + +{ + imports = [ ./isync ./mutt ./notmuch ./postfix ./rss2email ]; + + environment.systemPackages = with pkgs; [ isync ]; + + systemd.tmpfiles.rules = [ + "d ${config.users.users.qyliss.home}/mail 0770 qyliss qyliss" + ]; +} diff --git a/modules/workstation/mail/isync/default.nix b/modules/workstation/mail/isync/default.nix new file mode 100644 index 000000000000..22d64e45b1ba --- /dev/null +++ b/modules/workstation/mail/isync/default.nix @@ -0,0 +1,27 @@ +{ config, pkgs, ... }: + +let + maildir = "${config.users.users.qyliss.home}/mail"; + mbsyncrc = pkgs.substituteAll { inherit maildir; src = ./mbsyncrc.in; }; +in + +{ + systemd.services.mbsync = { + serviceConfig.Type = "oneshot"; + after = [ "network-online.target" ]; + before = [ "notmuch.service" ]; + requires = [ "network-online.target" ]; + wants = [ "notmuch.service" ]; + serviceConfig.ExecStart = "${pkgs.isync}/bin/mbsync -a -V -c ${mbsyncrc}"; + serviceConfig.User = "qyliss"; + unitConfig.ConditionPathExists = "${maildir}/imappass"; + }; + + systemd.timers.mbsync = { + timerConfig.OnCalendar = "*:0/5"; + timerConfig.Persistent = true; + after = [ "network-online.target" ]; + requires = [ "network-online.target" ]; + wantedBy = [ "timers.target" ]; + }; +} diff --git a/modules/workstation/mail/isync/mbsyncrc.in b/modules/workstation/mail/isync/mbsyncrc.in new file mode 100644 index 000000000000..4c44891acb46 --- /dev/null +++ b/modules/workstation/mail/isync/mbsyncrc.in @@ -0,0 +1,22 @@ +Create Both + +MaildirStore local + Path @maildir@/ + Inbox @maildir@/INBOX + Subfolders Verbatim + +IMAPAccount fastmail + Host imap.fastmail.com + User alyssa@fastmail.com + PassCmd "cat ~/mail/imappass" + SSLType IMAPS + SSLVersions TLSv1.3 + +IMAPStore fastmail-remote + Account fastmail + +Channel fastmail + Far :fastmail-remote: + Near :local: + Patterns * + SyncState * diff --git a/modules/workstation/mail/mutt/default.nix b/modules/workstation/mail/mutt/default.nix new file mode 100644 index 000000000000..96c0e6867545 --- /dev/null +++ b/modules/workstation/mail/mutt/default.nix @@ -0,0 +1,16 @@ +{ config, pkgs, ... }: + +let + stateDir = "${config.users.users.qyliss.home}/state/mutt"; +in + +{ + environment.systemPackages = with pkgs; [ neomutt ]; + + users.users.qyliss.xdg.config.paths."mutt/muttrc" = pkgs.copyPathToStore ./muttrc; + + systemd.tmpfiles.rules = [ + "d ${stateDir} 0700 qyliss qyliss" + "d ${stateDir}/header_cache 0700 qyliss qyliss" + ]; +} diff --git a/modules/workstation/mail/mutt/muttrc b/modules/workstation/mail/mutt/muttrc new file mode 100644 index 000000000000..6d80aced3bef --- /dev/null +++ b/modules/workstation/mail/mutt/muttrc @@ -0,0 +1,64 @@ +color index red default ~P + +alternates alyssa\.ross@unikie\.com +alternates @alyssa\.is @hi\.alyssa\.is + +unignore List-Id: +unignore Message-Id: +ignore User-Agent: + +set beep = no +set beep_new = yes +set edit_headers = yes +set fast_reply = yes +set folder = ~/mail +set header_cache = ~/state/mutt/header_cache +set help = no +set mark_old = no +set mime_forward = ask-no +set quit = ask-yes +set reverse_name = yes +set sort = last-date-received +set sort_browser = new +set strict_threads = yes +set use_envelope_from = yes +set use_threads = yes +set user_agent = no + +set newsrc = $XDG_DATA_HOME/mutt/newsrc +set nntp_context = 32767 + +unset prompt_after + +set spoolfile = +INBOX + +# set record = "=[Gmail]/Sent Mail" +# set postponed = "=[Gmail]/Drafts" +mailboxes `cd ~/mail; find . -name cur -print0 | sed -z -e 's|^\./||' -e 's|/cur$||' -e 's/\\/\\\\/' -e 's/"/\\"/g' -e 's/^/"=/' -e 's/$/"/' | xargs -0` +set record = "=Sent" +set trash = "=Archive" +set postponed = "=Drafts" +set sendmail = "sendmail" + +set pgp_use_gpg_agent = yes +set crypt_autosign = yes +set crypt_opportunistic_encrypt = yes +set postpone_encrypt = yes + +# Required for postpone_encrypt to work +set pgp_default_key = 757356D779BBB888773E415E736CCDF9EF51BD97 + +set pgp_decode_command = "gpg --status-fd=2 %?p?--pinentry-mode loopback --passphrase-fd 0? --no-verbose --quiet --batch --output - %f" +set pgp_verify_command = "gpg --status-fd=2 --no-verbose --quiet --batch --output - --verify %s %f" +set pgp_decrypt_command = "gpg --status-fd=2 %?p?--pinentry-mode loopback --passphrase-fd 0? --no-verbose --quiet --batch --output - --decrypt %f" +set pgp_sign_command = "gpg %?p?--pinentry-mode loopback --passphrase-fd 0? --no-verbose --batch --quiet --output - --armor --textmode %?a?--local-user %a? --detach-sign %f" +set pgp_clearsign_command = "gpg %?p?--pinentry-mode loopback --passphrase-fd 0? --no-verbose --batch --quiet --output - --armor --textmode %?a?--local-user %a? --clearsign %f" +set pgp_encrypt_only_command = "pgpewrap gpg --trust-model always --batch --quiet --no-verbose --output - --textmode --armor --encrypt -- --recipient %r -- %f" +set pgp_encrypt_sign_command = "pgpewrap gpg %?p?--pinentry-mode loopback --passphrase-fd 0? --trust-model always --batch --quiet --no-verbose --textmode --output - %?a?--local-user %a? --armor --sign --encrypt -- --recipient %r -- %f" +set pgp_import_command = "gpg --no-verbose --import %f" +set pgp_export_command = "gpg --no-verbose --armor --export %r" +set pgp_verify_key_command = "gpg --verbose --batch --fingerprint --check-sigs %r" +set pgp_list_pubring_command = "gpg --no-verbose --batch --quiet --with-colons --with-fingerprint --with-fingerprint --list-keys %r" +set pgp_list_secring_command = "gpg --no-verbose --batch --quiet --with-colons --with-fingerprint --with-fingerprint --list-secret-keys %r" +set pgp_good_sign = "^\\[GNUPG:\\] GOODSIG" +set pgp_decryption_okay = "^\\[GNUPG:\\] DECRYPTION_OKAY" diff --git a/modules/workstation/mail/notmuch/config b/modules/workstation/mail/notmuch/config new file mode 100644 index 000000000000..12b4435f10a2 --- /dev/null +++ b/modules/workstation/mail/notmuch/config @@ -0,0 +1,12 @@ +[user] +other_email=alyssa.ross@freeagent.com;alyssa.ross@unikie.com; + +[new] +tags=unread;inbox; +ignore=.uidvalidity;.mbsyncstate;.mbsyncstate.new;.mbsyncstate.journal; + +[search] +exclude_tags= + +[maildir] +synchronize_flags=true \ No newline at end of file diff --git a/modules/workstation/mail/notmuch/default.nix b/modules/workstation/mail/notmuch/default.nix new file mode 100644 index 000000000000..cb7ccd877950 --- /dev/null +++ b/modules/workstation/mail/notmuch/default.nix @@ -0,0 +1,21 @@ +{ pkgs, ... }: + +{ + environment.extraInit = '' + export NOTMUCH_CONFIG="/etc/xdg/nixos/per-user/$USER/notmuch/config" + ''; + + environment.systemPackages = with pkgs; [ notmuch ]; + + users.users.qyliss.xdg.config.paths."notmuch/config" = + pkgs.copyPathToStore ./config; + + systemd.services.notmuch = { + serviceConfig.Type = "oneshot"; + environment.NOTMUCH_CONFIG = "/etc/xdg/nixos/per-user/qyliss/notmuch/config"; + serviceConfig.ExecStart = "${pkgs.notmuch}/bin/notmuch new"; + serviceConfig.IOSchedulingClass = "idle"; + serviceConfig.Nice = 1; + serviceConfig.User = "qyliss"; + }; +} diff --git a/modules/workstation/mail/postfix/default.nix b/modules/workstation/mail/postfix/default.nix new file mode 100644 index 000000000000..db16bced3a5e --- /dev/null +++ b/modules/workstation/mail/postfix/default.nix @@ -0,0 +1,43 @@ +{ pkgs, lib, config, ... }: + +{ + services.postfix.enable = true; + + services.postfix.hostname = with lib; with config.networking; + concatStringsSep "." (filter (x: x != null) [ hostName domain ]); + + services.postfix.relayHost = "smtp.fastmail.com"; + services.postfix.relayPort = 465; + + services.postfix.recipientDelimiter = "+"; + services.postfix.config.home_mailbox = "mail/INBOX/"; + services.postfix.canonical = '' + qyliss hi@alyssa.is + ''; + services.postfix.virtual = '' + hi@alyssa.is qyliss + ''; + + # NixOS links /var/lib/postfix/conf to /etc/postfix, but + # postfix.service deletes /var/lib/postfix in an ExecStartPre, so we + # can't keep files there without adding them to the store. + # + # Work around this with a layer of symlink indirection. + services.postfix.mapFiles.sasl_passwd = pkgs.runCommand "sasl_passwd" {} '' + ln -s /var/lib/postfix/sasl_passwd $out + ''; + services.postfix.config.smtp_sasl_password_maps = "hash:/etc/postfix/sasl_passwd"; + + services.postfix.config.sender_dependent_relayhost_maps = "hash:/etc/postfix/sender_dependent_relayhost"; + services.postfix.mapFiles.sender_dependent_relayhost = pkgs.writeText "sender_dependent_relayhost" '' + @unikie.com [smtp.gmail.com]:465 + ''; + + services.postfix.config.smtp_sasl_auth_enable = true; + services.postfix.config.smtp_sasl_tls_security_options = "noanonymous"; + services.postfix.config.smtp_tls_security_level = "encrypt"; + services.postfix.config.smtp_tls_wrappermode = true; + + systemd.services.postfix-setup.unitConfig.ConditionPathExists = "/var/lib/postfix/sasl_passwd"; + systemd.services.postfix.unitConfig.ConditionPathExists = "/var/lib/postfix/sasl_passwd"; +} diff --git a/modules/workstation/mail/rss2email/default.nix b/modules/workstation/mail/rss2email/default.nix new file mode 100644 index 000000000000..614b7f9c0cb0 --- /dev/null +++ b/modules/workstation/mail/rss2email/default.nix @@ -0,0 +1,17 @@ +{ config, ... }: + +{ + services.rss2email.enable = true; + services.rss2email.to = "hi+rss2email@alyssa.is"; + services.rss2email.config.date-header = true; + services.rss2email.config.from = + "rss2email@${config.services.postfix.hostname}"; + + services.rss2email.feeds = { + fading-memories = { url = "https://valdyas.org/fading/feed/"; }; + flak = { url = "https://flak.tedunangst.com/rss"; }; + wandering-thoughts = + { url = "https://utcc.utoronto.ca/~cks/space/blog/?atom"; }; + repology = { url = "https://repology.org/maintainer/hi%40alyssa.is/feed-for-repo/nix_unstable/atom"; }; + }; +} diff --git a/modules/workstation/mpv/default.nix b/modules/workstation/mpv/default.nix new file mode 100644 index 000000000000..4e41f6566ecf --- /dev/null +++ b/modules/workstation/mpv/default.nix @@ -0,0 +1,15 @@ +{ pkgs, ... }: + +{ + environment.systemPackages = with pkgs; [ mpv ]; + + users.users.qyliss.xdg.config.paths."mpv/input.conf" = + pkgs.writeText "input.conf" '' + F11 cycle fullscreen + ''; + + users.users.qyliss.xdg.config.paths."mpv/mpv.conf" = + pkgs.writeText "mpv.conf" '' + audio-display=no + ''; +} diff --git a/modules/workstation/networking/castnow/default.nix b/modules/workstation/networking/castnow/default.nix new file mode 100644 index 000000000000..19f09723ad7e --- /dev/null +++ b/modules/workstation/networking/castnow/default.nix @@ -0,0 +1,8 @@ +{ pkgs, ... }: + +{ + environment.systemPackages = with pkgs; [ castnow ]; + + networking.firewall.allowedUDPPorts = [ 5353 ]; + networking.firewall.allowedTCPPortRanges = [ { from = 4100; to = 4105; } ]; +} diff --git a/modules/workstation/networking/default.nix b/modules/workstation/networking/default.nix new file mode 100644 index 000000000000..56703070ae64 --- /dev/null +++ b/modules/workstation/networking/default.nix @@ -0,0 +1,30 @@ +{ config, ... }: + +{ + imports = [ ../../hosts ./castnow ]; + + services.avahi.enable = true; + services.resolved.enable = true; + + networking.domain = "qyliss.net"; + networking.hosts = with config.networking; + { "127.0.1.1" = [ "${hostName}.${domain}" ]; }; + + networking.networkmanager.enable = true; + networking.networkmanager.wifi.backend = "iwd"; + + users.users.qyliss.extraGroups = [ "networkmanager" ]; + + # Plausible MAC randomization + networking.networkmanager.ethernet.macAddress = "random"; + networking.networkmanager.wifi.macAddress = "random"; + networking.networkmanager.extraConfig = '' + [connection-extra] + ethernet.generate-mac-address-mask=FE:FF:FF:00:00:00 + wifi.generate-mac-address-mask=FE:FF:FF:00:00:00 + ''; + + networking.nameservers = [ "::1" ]; + + programs.mtr.enable = true; +} diff --git a/modules/workstation/physical/default.nix b/modules/workstation/physical/default.nix new file mode 100644 index 000000000000..fc5cad3b2787 --- /dev/null +++ b/modules/workstation/physical/default.nix @@ -0,0 +1,9 @@ +{ ... }: + +{ + imports = [ ../. ../networking ]; + + programs.swayidle.enable = true; + + zramSwap.enable = true; +} diff --git a/modules/workstation/weechat/default.nix b/modules/workstation/weechat/default.nix new file mode 100644 index 000000000000..95ba03c1ecdc --- /dev/null +++ b/modules/workstation/weechat/default.nix @@ -0,0 +1,152 @@ +{ config, pkgs, lib, ... }: + +with lib; + +let + scripts = with pkgs.weechatScripts; + [ colorize_nicks weechat-go weechat-matrix zncplayback ]; + + networks = [ + "blitzed" "gnome" "hackint" "indymedia" "ircnet" "libera" + "oftc" "pissnet" "tilde" + ]; + + matrixServers = { fairydust = "fairydust.space"; }; + + toWeeChat = value: + /**/ if value == true then "on" + else if value == false then "off" + else if isList value then concatStringsSep "," value + else toString value; + + sec = [ "znc.username" "znc.password" ]; + + ignores = [ + "osmbot-test oftc #osm-gb" + ]; + + cfgin = { + alias.cmd.B = "buffer"; + alias.cmd.ZNC = "quote znc"; + irc.look.buffer_switch_autojoin = false; + irc.look.color_nicks_in_nicklist = true; + irc.look.display_join_message = false; + irc.look.server_buffer = "independent"; + irc.look.temporary_servers = true; + + irc.server_default = { + addresses = "znc.qyliss.net/6697"; + autoconnect = true; + capabilities = [ "account-notify" "away-notify" "cap-notify" "multi-prefix" "server-time" "znc.in/server-time-iso" "znc.in/self-message" "znc.in/playback" ]; + nicks = "qyliss"; + password = "\\\${sec.data.znc.password}"; + ssl = true; + username = "\\\${sec.data.znc.username}"; + }; + + irc.server = genAttrs networks (s: + { username = "'\\\${sec.data.znc.username}/${s}'"; }); + plugins.var.python.zncplayback.servers = networks; + + logger.color.backlog_end = "*default"; + logger.look.backlog = 200; + + matrix.look.human_buffer_names = true; + matrix.server = flip mapAttrs matrixServers (name: _: { + autoconnect = true; + device_name = "WeeChat (${config.networking.hostName})"; + username = "qyliss"; + password = "\\\${sec.data.fairydust.password}"; + }); + + script.look.sort = "p,n"; + + weechat.bar.buflist.hidden = true; + weechat.bar.fset.items = "fset"; + weechat.bar.input.items = "[mode_indicator]+ [input_prompt]+(away),[input_search],[input_paste],input_text"; + weechat.bar.nicklist.size_max = 12; + weechat.bar.status.color_bg = 53; + weechat.bar.status.items = "[otr],[buffer_plugin],buffer_name+(buffer_modes)+{buffer_nicklist_count}+buffer_zoom+buffer_filter,[matrix_typing_notice],scroll,[lag],[hotlist],completion"; + weechat.bar.title.color_bg = 53; + + weechat.color.chat_nick_colors = [ "cyan" "magenta" "green" "brown" "lightblue" "lightcyan" "lightmagenta" "lightgreen" "blue" ]; + weechat.color.chat_nick_self = "default"; + weechat.color.status_count_other = "white"; + weechat.color.status_data_other = "white"; + weechat.color.status_nicklist_count = "white"; + weechat.color.status_time = "white"; + + weechat.completion.default_template = "%(nicks)|%(irc_channels)|%(emoji)"; + weechat.completion.nick_completer = ":"; + + weechat.look.buffer_notify_default = "message"; + weechat.look.highlight = [ + "spectrum" "spectrumos" "pr-tracker" "crosvm" + "qyliss" "alyssa*" "*dntmissher" "*@alyssa" + ]; + weechat.look.hotlist_names_count = 10; + weechat.look.hotlist_names_level = 14; + weechat.look.mouse = true; + weechat.look.prefix_align_max = 12; + weechat.look.save_config_on_exit = false; + weechat.look.window_title = "WeeChat \\\${info:version}"; + }; + + commands = + map (n: "/server add ${n} ${cfgin.irc.server_default.addresses}") networks ++ + [ "/matrix server delete matrix_org" ] ++ + mapAttrsToList (n: d: "/matrix server add ${n} ${d}") matrixServers ++ + map (i: "/ignore add ${i}") ignores ++ + mapAttrsToList (name: value: "/set ${name} ${toWeeChat value}") + (flattenAttrs cfgin); + + flattenAttrs' = sep: attrs: + listToAttrs (concatLists (flip mapAttrsToList attrs (k: v: + if isAttrs v then mapAttrsToList (k': nameValuePair "${k}${sep}${k'}") (flattenAttrs' sep v) + else [ (nameValuePair k v) ]))); + + flattenAttrs = flattenAttrs' "."; + + # If we were to pass --run-command ourselves, it would get forwarded + # to WeeChat before the /script loads, so scripts wouldn't be + # available and we wouldn't be able to do stuff like set up Matrix. + cfg = pkgs.runCommand "weechat-config" {} '' + LC_ALL=C.UTF-8 ${pkgs.weechat.override { + configure = { ... }: { + inherit scripts; + init = concatStringsSep ";" (commands ++ [ "/save" "/exit" ]); + }; + }}/bin/weechat-headless -d $out + ''; + + weechatHome = "${config.users.users.qyliss.home}/state/weechat"; + +in + +{ + environment.extraInit = '' + export WEECHAT_HOME="$HOME/state/weechat" + ''; + + environment.systemPackages = with pkgs; [ + (weechat.override { + configure = { ...}: { + inherit scripts; + }; + }) + + weechatScripts.weechat-matrix # for helper scripts + ]; + + systemd.tmpfiles.packages = [ (pkgs.runCommand "weechat-tmpfiles" {} '' + conf=$out/lib/tmpfiles.d/weechat.conf + mkdir -p $(dirname $conf) + echo "d ${weechatHome} 0700 qyliss qyliss" > $conf + for file in ${cfg}/*.conf + do + if [ "$file" != ${cfg}/sec.conf ] + then echo "L+ ${weechatHome}/$(basename "$file") - - - - $file" >> $conf + fi + done + '') ]; +} diff --git a/modules/workstation/windowing/default.nix b/modules/workstation/windowing/default.nix new file mode 100644 index 000000000000..f7ff2b8a4272 --- /dev/null +++ b/modules/workstation/windowing/default.nix @@ -0,0 +1,13 @@ +{ pkgs, ... }: + +{ + imports = [ + ./firefox ./foot ./gtk ./sway + ]; + + environment.systemPackages = with pkgs; [ + breeze-icons gnome3.adwaita-icon-theme gnome3.gnome-mines + gnome-podcasts hicolor-icon-theme imv libreoffice okular pinball + playerctl wf-recorder + ]; +} diff --git a/modules/workstation/windowing/firefox/default.nix b/modules/workstation/windowing/firefox/default.nix new file mode 100644 index 000000000000..ccd9afd87843 --- /dev/null +++ b/modules/workstation/windowing/firefox/default.nix @@ -0,0 +1,19 @@ +{ pkgs, ... }: + +{ + environment.systemPackages = with pkgs; [ + (wrapFirefox firefox-unwrapped { + extraPolicies = { + DefaultDownloadDirectory = "/tmp"; + DisablePocket = true; + FirefoxHome = { + TopSites = false; + SponsoredTopSites = false; + SponsoredPocket = false; + }; + }; + }) + ]; + + environment.variables.BROWSER = "firefox"; +} diff --git a/modules/workstation/windowing/foot/default.nix b/modules/workstation/windowing/foot/default.nix new file mode 100644 index 000000000000..5c66f18eb03f --- /dev/null +++ b/modules/workstation/windowing/foot/default.nix @@ -0,0 +1,7 @@ +{ pkgs, ... }: + +{ + users.users.qyliss.xdg.config.paths."foot/foot.ini" = ./foot.ini; + + environment.systemPackages = with pkgs; [ foot ]; +} diff --git a/modules/workstation/windowing/foot/foot.ini b/modules/workstation/windowing/foot/foot.ini new file mode 100644 index 000000000000..d041f42bceac --- /dev/null +++ b/modules/workstation/windowing/foot/foot.ini @@ -0,0 +1,2 @@ +[main] +font = monospace:size=12 diff --git a/modules/workstation/windowing/gtk/default.nix b/modules/workstation/windowing/gtk/default.nix new file mode 100644 index 000000000000..df66f8fbb72d --- /dev/null +++ b/modules/workstation/windowing/gtk/default.nix @@ -0,0 +1,9 @@ +{ pkgs, ... }: + +{ + users.users.qyliss.xdg.config.paths."gtk-3.0/settings.ini" = + pkgs.copyPathToStore ./settings.ini; + + # Needed for Dino to not draw its title bar. + environment.variables.GTK_CSD = "0"; +} diff --git a/modules/workstation/windowing/gtk/settings.ini b/modules/workstation/windowing/gtk/settings.ini new file mode 100644 index 000000000000..29322c1b3a0d --- /dev/null +++ b/modules/workstation/windowing/gtk/settings.ini @@ -0,0 +1,2 @@ +[Settings] +gtk-application-prefer-dark-theme=1 diff --git a/modules/workstation/windowing/streaming/default.nix b/modules/workstation/windowing/streaming/default.nix new file mode 100644 index 000000000000..d6d5451ca6ff --- /dev/null +++ b/modules/workstation/windowing/streaming/default.nix @@ -0,0 +1,30 @@ +{ config, pkgs, ... }: + +let + obsStateDir = "${config.users.users.qyliss.home}/state/obs-studio"; +in + +{ + environment.systemPackages = with pkgs; [ carla obs-studio qpwgraph ]; + + environment.variables.LV2_PATH = with pkgs; lib.makeSearchPathOutput "out" "lib/lv2" [ + calf + dragonfly-reverb + lsp-plugins + x42-plugins + + # Workaround for https://github.com/werman/noise-suppression-for-voice/issues/158 + (runCommand "rrnoise-plugin-mono-lv2" {} '' + mkdir $out + cd ${rnnoise-plugin} + cp -R --parents lib/lv2/rnnoise_mono.lv2 $out + '') + ]; + + systemd.tmpfiles.rules = [ + "d ${obsStateDir} 0700 qyliss qyliss" + ]; + + users.users.qyliss.xdg.config.paths."obs-studio" = + pkgs.runCommand "obs-studio" {} "ln -s ${obsStateDir} $out"; +} diff --git a/modules/workstation/windowing/sway/choose_workspace.nix b/modules/workstation/windowing/sway/choose_workspace.nix new file mode 100644 index 000000000000..fc162d627b60 --- /dev/null +++ b/modules/workstation/windowing/sway/choose_workspace.nix @@ -0,0 +1,9 @@ +{ substituteAll, bemenu, jq }: + +substituteAll { + dir = "bin"; + name = "choose_workspace"; + src = ./choose_workspace.sh.in; + isExecutable = true; + inherit bemenu jq; +} diff --git a/modules/workstation/windowing/sway/choose_workspace.sh.in b/modules/workstation/windowing/sway/choose_workspace.sh.in new file mode 100644 index 000000000000..963746e0c810 --- /dev/null +++ b/modules/workstation/windowing/sway/choose_workspace.sh.in @@ -0,0 +1,17 @@ +#! @shell@ -ue +swaymsg -t get_workspaces | + @jq@/bin/jq -r \ + '(to_entries | map(select(.value.focused)) | .[0].key), .[].name' | + ( + read index + exec @bemenu@/bin/bemenu \ + -p workspace \ + -I "$index" \ + -H 24 \ + --fn 'monospace 10' \ + --nf '#777777' \ + --hb '#285577' \ + --hf '#ffffff' \ + --tf '#777777' \ + --ff '#ffffff' + ) diff --git a/modules/workstation/windowing/sway/config.in b/modules/workstation/windowing/sway/config.in new file mode 100644 index 000000000000..52efc1fa90e3 --- /dev/null +++ b/modules/workstation/windowing/sway/config.in @@ -0,0 +1,125 @@ +set $mod Mod4 +set $left h +set $down j +set $up k +set $right l + +default_border pixel +default_floating_border normal + +client.focused_inactive #333333 #5f676a #ffffff #484e50 #5f676a00 +client.unfocused #333333 #222222 #888888 #292d2e #22222200 + +for_window [app_id="float"] floating enable +for_window [app_id="firefox" title="Picture-in-Picture"] floating enable +for_window [class="Tor Browser"] floating enable +for_window [class="XDvi"] floating enable +for_window [instance="xdvi"] floating enable +for_window [class="XDvi" instance="xdvi"] floating disable +for_window [app_id="firefox" title="NoScript.*"] floating enable + +# Stop the Firefox sharing indicator (that appears when on an +# audio/video call) tiling, or appearing in the center of the display, +# or being focused. +no_focus [title="(Firefox|Nightly) . Sharing Indicator"] +for_window [title="(Firefox|Nightly) . Sharing Indicator"] { + floating enable + sticky enable + + # I'd really like this to be at the top, horizontally centered. + # Or maybe at the bottom right. But there's not really a way to + # do that in sway configuration, as far as I can tell. I think + # I'd need to exec a program that would measure the display size + # and compute the coordinates or something. That sounds horrible + # and fragile, so top left it is. + move position 0 0 +} + +input * natural_scroll enabled + +bindsym $mod+Return exec foot +bindsym $mod+backslash exec firefox +bindsym $mod+BackSpace kill +bindsym $mod+d exec swaymsg exec "$(choosebin --tiebreak=begin,length,index)" + +# Brightness control +bindsym $mod+F5 exec brightnessctl s 10%- +bindsym $mod+F6 exec brightnessctl s 10%+ + +# MPRIS +bindsym $mod+F7 exec playerctl play-pause + +# PulseAudio +bindsym $mod+F8 exec pactl set-sink-mute @DEFAULT_SINK@ toggle +bindsym $mod+F9 exec pactl set-sink-volume @DEFAULT_SINK@ -5% +bindsym $mod+F10 exec pactl set-sink-volume @DEFAULT_SINK@ +5% +bindsym XF86AudioLowerVolume exec pactl set-sink-volume @DEFAULT_SINK@ -5% +bindsym XF86AudioRaiseVolume exec pactl set-sink-volume @DEFAULT_SINK@ +5% + +# Drag floating windows by holding down $mod and left mouse button. +# Resize them with right mouse button + $mod. +# Despite the name, also works for non-floating windows. +# Change normal to inverse to use left mouse button for resizing and right +# mouse button for dragging. +floating_modifier $mod normal + +# reload the configuration file +bindsym $mod+Shift+c reload + +# exit sway (logs you out of your Wayland session) +bindsym $mod+Shift+e exec swaynag -t warning -m 'You pressed the exit shortcut. Do you really want to exit sway? This will end your Wayland session.' -b 'Yes, exit sway' 'swaymsg exit' + +bindsym $mod+$left focus left +bindsym $mod+$down focus down +bindsym $mod+$up focus up +bindsym $mod+$right focus right + +bindsym $mod+Shift+$left move left +bindsym $mod+Shift+$down move down +bindsym $mod+Shift+$up move up +bindsym $mod+Shift+$right move right + +bindsym $mod+g exec swaymsg workspace "$(@choose_workspace@)" +bindsym $mod+Shift+g exec swaymsg move container to workspace "$(@choose_workspace@)" + +bindsym $mod+b splith +bindsym $mod+v splitv + +bindsym $mod+s layout stacking +bindsym $mod+w layout tabbed +bindsym $mod+e layout toggle split + +bindsym $mod+f fullscreen + +bindsym $mod+Shift+space floating toggle +bindsym $mod+space focus mode_toggle + +bindsym $mod+a focus parent + +bindsym $mod+Shift+minus move scratchpad +bindsym $mod+minus scratchpad show + +mode "resize" { + bindsym $left resize shrink width 10px + bindsym $down resize grow height 10px + bindsym $up resize shrink height 10px + bindsym $right resize grow width 10px + + bindsym Return mode "default" + bindsym Escape mode "default" +} +bindsym $mod+r mode "resize" + +bar { + position top + + status_command @status_command@ + + colors { + statusline #ffffff + background #00000077 + inactive_workspace #33333377 #00000077 #FFFFFF77 + } +} + +@extraConfig@ diff --git a/modules/workstation/windowing/sway/default.nix b/modules/workstation/windowing/sway/default.nix new file mode 100644 index 000000000000..675ef8dbc031 --- /dev/null +++ b/modules/workstation/windowing/sway/default.nix @@ -0,0 +1,49 @@ +{ pkgs, lib, config, ... }: + +let + inherit (lib) mdDoc mkOption optionalString; + inherit (lib.types) lines nullOr path; + inherit (pkgs) callPackage substituteAll; + + cfg = config.programs.sway; +in + +{ + imports = [ ./swayidle ./swaylock ./wlsunset ./xdg-desktop-portal-wlr ]; + + options = { + programs.sway.extraConfig = mkOption { + type = lines; + description = mdDoc "Lines to append to sway's config file"; + default = ""; + }; + + programs.sway.wallpaper = mkOption { + type = nullOr path; + description = mdDoc "Path to wallpaper for sway and swaylock"; + default = null; + }; + }; + + config = { + environment.systemPackages = with pkgs; [ bemenu choose swayidle ]; + + programs.sway.enable = true; + programs.sway.wallpaper = callPackage ./wallpaper.nix { }; + programs.sway.extraPackages = []; # extra packages can go in systemPackages. + + programs.swayidle.enable = true; + + users.users.qyliss.xdg.config.paths."sway/config" = substituteAll { + src = ./config.in; + choose_workspace = + "${callPackage ./choose_workspace.nix { }}/bin/choose_workspace"; + status_command = "${callPackage ./status.nix { }}/bin/status"; + extraConfig = cfg.extraConfig + optionalString (cfg.wallpaper != null) '' + output * bg ${cfg.wallpaper} fill + ''; + }; + + xdg.portal.extraPortals = with pkgs; [ xdg-desktop-portal-gtk ]; + }; +} diff --git a/modules/workstation/windowing/sway/status.cpp b/modules/workstation/windowing/sway/status.cpp new file mode 100644 index 000000000000..1f73458d86b2 --- /dev/null +++ b/modules/workstation/windowing/sway/status.cpp @@ -0,0 +1,201 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +// Copyright 2020 Alyssa Ross +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +#include <chrono> +#include <fcntl.h> +#include <filesystem> +#include <iostream> +#include <thread> +#include <unistd.h> + +namespace fs = std::filesystem; + +using fs::directory_iterator; +using std::chrono::seconds; +using std::generic_category; +using std::put_time; +using std::stoi; +using std::string; +using std::stringstream; +using std::system_error; +using std::this_thread::sleep_for; +using std::time; + +enum BatteryStatus { + Unknown, + Charging, + Discharging, + NotCharging, + Full, +}; + +class Battery { +public: + Battery(string name); + + string name() { return m_name; } + fs::path path(); + BatteryStatus status(); + int capacity(); + int charge_now(); + +private: + string m_name; + string attr(string name); +}; + +Battery::Battery(string name) +{ + m_name = name; +} + +fs::path Battery::path() +{ + static fs::path base = "/sys/class/power_supply"; + return base / name(); +} + +BatteryStatus Battery::status() +{ + auto status = attr("status"); + + if (status == "Charging") + return Charging; + if (status == "Discharging") + return Discharging; + if (status == "Not charging") + return NotCharging; + if (status == "Full") + return Full; + + return Unknown; +} + +int Battery::capacity() +{ + return stoi(attr("capacity")); +} + +int Battery::charge_now() +{ + return stoi(attr("charge_now")); +} + +string Battery::attr(string name) +{ + // Use read() to make sure this is done in a single read syscall, + // because sysfs doesn't like multiple reads. + int fd = open((path() / name).c_str(), O_RDONLY); + if (fd == -1) + throw system_error(errno, generic_category()); + char buf[13]; + int len = read(fd, &buf, 13); + if (len == -1) + throw system_error(errno, generic_category()); + close(fd); + string value (buf, len); + if (value.back() == '\n') + value.pop_back(); + return value; +} + +int main(void) +{ + while (true) { + // Buffer output so it can be done all at once in a single write(), + // because of <https://github.com/swaywm/sway/issues/3857>. + stringstream out; + + for (const auto& entry : directory_iterator("/sys/class/power_supply")) { + auto name = entry.path().filename().string(); + + Battery battery (name); + auto batdisplay = false; + + try { + switch (battery.status()) { + case Charging: + out << "↑"; + break; + case Discharging: + out << "↓"; + break; + default: + out << " "; + } + batdisplay = true; + } catch (const system_error& ex) { + switch (ex.code().value()) { + case ENOENT: + break; + case ENODEV: + out << "? "; + batdisplay = true; + break; + default: + throw ex; + } + } + + try { + int capacity = battery.capacity(); + out << capacity << "%"; + batdisplay = true; + } catch (const system_error& ex) { + switch (ex.code().value()) { + case ENOENT: + break; + case ENODEV: + out << "??%"; + batdisplay = true; + break; + default: + throw ex; + } + + try { + int charge_now = battery.charge_now(); + out << charge_now; + batdisplay = true; + } catch (const system_error& ex) { + switch (ex.code().value()) { + case ENOENT: + break; + case ENODEV: + out << "??????"; + batdisplay = true; + break; + default: + throw ex; + } + } + } + + if (batdisplay) + out << " "; + } + + auto t = time(nullptr); + out << put_time(localtime(&t), "%F %T") << "\n"; + + auto buf = out.str(); + if (write(STDOUT_FILENO, buf.c_str(), buf.length()) == -1) + perror("write"); + + sleep_for(seconds(1)); + } +} diff --git a/modules/workstation/windowing/sway/status.nix b/modules/workstation/windowing/sway/status.nix new file mode 100644 index 000000000000..2697317d7611 --- /dev/null +++ b/modules/workstation/windowing/sway/status.nix @@ -0,0 +1,6 @@ +{ runCommandCC }: + +runCommandCC "status" {} '' + mkdir -p $out/bin + c++ -std=c++17 -o $out/bin/status ${./status.cpp} +'' diff --git a/modules/workstation/windowing/sway/swayidle/default.nix b/modules/workstation/windowing/sway/swayidle/default.nix new file mode 100644 index 000000000000..8e5f264b5038 --- /dev/null +++ b/modules/workstation/windowing/sway/swayidle/default.nix @@ -0,0 +1,23 @@ +{ lib, config, ... }: + +let + cfg = config.programs.swayidle; +in + +with lib; + +{ + options = { + programs.swayidle.enable = mkEnableOption "swayidle"; + }; + + config = mkIf cfg.enable { + programs.sway.extraConfig = '' + exec swayidle \ + timeout 300 'swaylock -c 000000' \ + timeout 600 'swaymsg "output * dpms off"' \ + resume 'swaymsg "output * dpms on"' \ + before-sleep 'swaylock -c 000000' + ''; + }; +} diff --git a/modules/workstation/windowing/sway/swaylock/config.in b/modules/workstation/windowing/sway/swaylock/config.in new file mode 100644 index 000000000000..c6f280aa8f5b --- /dev/null +++ b/modules/workstation/windowing/sway/swaylock/config.in @@ -0,0 +1,3 @@ +image=@wallpaper@ +indicator-idle-visible +show-failed-attempts diff --git a/modules/workstation/windowing/sway/swaylock/default.nix b/modules/workstation/windowing/sway/swaylock/default.nix new file mode 100644 index 000000000000..be15c87ade32 --- /dev/null +++ b/modules/workstation/windowing/sway/swaylock/default.nix @@ -0,0 +1,12 @@ +{ pkgs, config, ... }: + +{ + imports = [ ../../../../xdg ]; + + environment.systemPackages = with pkgs; [ swaylock ]; + + users.users.qyliss.xdg.config.paths."swaylock/config" = pkgs.substituteAll { + src = ./config.in; + wallpaper = config.programs.sway.wallpaper; + }; +} diff --git a/modules/workstation/windowing/sway/wallpaper.nix b/modules/workstation/windowing/sway/wallpaper.nix new file mode 100644 index 000000000000..bed6bbe33a6b --- /dev/null +++ b/modules/workstation/windowing/sway/wallpaper.nix @@ -0,0 +1,10 @@ +{ fetchurl }: + +fetchurl { + url = "https://mir-s3-cdn-cf.behance.net/project_modules/2800_opt_1/36731876964505.5c793fa788b5d.jpg"; + sha256 = "1c6camdipng8ws41sgpcxzrxb96crgip3wirqjgf2ajn60qg3v64"; + + meta = { + homepage = "https://www.behance.net/gallery/76964505/IQOO-style-frame-and-scene-design"; + }; +} diff --git a/modules/workstation/windowing/sway/wlsunset/default.nix b/modules/workstation/windowing/sway/wlsunset/default.nix new file mode 100644 index 000000000000..ba954f3cd3bd --- /dev/null +++ b/modules/workstation/windowing/sway/wlsunset/default.nix @@ -0,0 +1,9 @@ +{ pkgs, ... }: + +{ + environment.systemPackages = with pkgs; [ wlsunset ]; + + programs.sway.extraConfig = '' + exec wlsunset -l 51.5 -L 13.6 + ''; +} diff --git a/modules/workstation/windowing/sway/xdg-desktop-portal-wlr/default.nix b/modules/workstation/windowing/sway/xdg-desktop-portal-wlr/default.nix new file mode 100644 index 000000000000..892e6a280d4c --- /dev/null +++ b/modules/workstation/windowing/sway/xdg-desktop-portal-wlr/default.nix @@ -0,0 +1,13 @@ +{ pkgs, ... }: + +{ + xdg.portal.wlr.enable = true; + + programs.sway.extraConfig = '' + exec ${pkgs.writeShellScript "sway-portal-environment" '' + set -e + dbus-update-activation-environment --systemd WAYLAND_DISPLAY XDG_CURRENT_DESKTOP + systemctl --user stop pipewire xdg-desktop-portal xdg-desktop-portal-wlr + ''} + ''; +} |