Compare commits

..

1 Commits

Author SHA1 Message Date
3c5fee5d14 attempt to update swaynotificationcenter patch, but fail
the new version has problems: start swaync while one of the services is running, and then itll toggle that service on every control-center open -> close cycle
2023-11-08 21:26:42 +00:00
353 changed files with 55456 additions and 19264 deletions

3
.gitignore vendored
View File

@@ -1,4 +1,5 @@
.working /keep
result result
result-* result-*
/secrets/local.nix /secrets/local.nix
/working

View File

@@ -11,7 +11,7 @@ the only hard dependency for my exported pkgs/modules should be [nixpkgs][nixpkg
building <./hosts/> will require [sops][sops]. building <./hosts/> will require [sops][sops].
you might specifically be interested in these files (elaborated further in #key-points-of-interest): you might specifically be interested in these files (elaborated further in #key-points-of-interest):
- [`sxmo-utils`](./pkgs/additional/sxmo-utils/default.nix) - [`sxmo-utils-latest`](./pkgs/additional/sxmo-utils/default.nix)
- [example SXMO deployment](./hosts/modules/gui/sxmo/default.nix) - [example SXMO deployment](./hosts/modules/gui/sxmo/default.nix)
- [my implementation of impermanence](./modules/persist/default.nix) - [my implementation of impermanence](./modules/persist/default.nix)
- my way of deploying dotfiles/configuring programs per-user: - my way of deploying dotfiles/configuring programs per-user:

50
TODO.md
View File

@@ -1,13 +1,10 @@
## BUGS ## BUGS
- nixpkgs date is incorrect (1970.01.01...) - why i need to manually restart `wireguard-wg-ovpns` on servo periodically
- else DNS fails
- ringer (i.e. dino incoming call) doesn't prevent moby from sleeping - ringer (i.e. dino incoming call) doesn't prevent moby from sleeping
- `nix` operations from lappy hang when `desko` is unreachable
- could at least direct the cache to `http://desko-hn:5001`
## REFACTORING: ## REFACTORING:
- fold hosts/common/home/ssh.nix -> hosts/common/users/colin.nix
### sops/secrets ### sops/secrets
- attach secrets to the thing they're used by (sane.programs) - attach secrets to the thing they're used by (sane.programs)
- rework secrets to leverage `sane.fs` - rework secrets to leverage `sane.fs`
@@ -24,13 +21,17 @@
- fix lightdm-mobile-greeter for newer libhandy - fix lightdm-mobile-greeter for newer libhandy
- port zecwallet-lite to a from-source build - port zecwallet-lite to a from-source build
- REVIEW/integrate jellyfin dataDir config: <https://github.com/NixOS/nixpkgs/pull/233617> - REVIEW/integrate jellyfin dataDir config: <https://github.com/NixOS/nixpkgs/pull/233617>
- remove `libsForQt5.callPackage` broadly: <https://github.com/NixOS/nixpkgs/issues/180841>
#### upstreaming to non-nixpkgs repos #### upstreaming to non-nixpkgs repos
- gtk: build schemas even on cross compilation: <https://github.com/NixOS/nixpkgs/pull/247844> - gtk: build schemas even on cross compilation: <https://github.com/NixOS/nixpkgs/pull/247844>
- sxmo: add new app entries
## IMPROVEMENTS: ## IMPROVEMENTS:
### security/resilience ### security/resilience
- matrix/ntfy: automatically add the ntfy.uninsane.org push URL as part of synapse launch
- ntfy: use a more secure topic
- validate duplicity backups! - validate duplicity backups!
- encrypt more ~ dirs (~/archives, ~/records, ..?) - encrypt more ~ dirs (~/archives, ~/records, ..?)
- best to do this after i know for sure i have good backups - best to do this after i know for sure i have good backups
@@ -48,36 +49,19 @@
- e.g. daily email checks; daily backup checks - e.g. daily email checks; daily backup checks
- integrate `nix check` into Gitea actions? - integrate `nix check` into Gitea actions?
### faster/better deployments
- remove audacity's dependency on webkitgtk (via wxwidgets)
### user experience ### user experience
- install apps:
- display QR codes for WiFi endpoints: <https://linuxphoneapps.org/apps/noappid.wisperwind.wifi2qr/>
- shopping list (not in nixpkgs): <https://linuxphoneapps.org/apps/ro.hume.cosmin.shoppinglist/>
- offline Wikipedia (or, add to `wike`)
- offline docs viewer (gtk): <https://github.com/workbenchdev/Biblioteca>
- some type of games manager/launcher
- Gnome Highscore (retro games)?: <https://gitlab.gnome.org/World/highscore>
- better maps for mobile (Osmin (QtQuick)? Pure Maps (Qt/Kirigami)? Gnome Maps is improved in 45)
- note-taking app: <https://linuxphoneapps.org/categories/note-taking/>
- OSK overlay specifically for mobile gaming
- i.e. mock joysticks, for use with SuperTux and SuperTuxKart
- install mobile-friendly games:
- Shattered Pixel Dungeon (nixpkgs `shattered-pixel-dungeon`; doesn't cross-compile b/c openjdk/libIDL) <https://github.com/ebolalex/shattered-pixel-dungeon>
- UnCiv (Civ V clone; nixpkgs `unciv`; doesn't cross-compile): <https://github.com/yairm210/UnCiv>
- Simon Tatham's Puzzle Collection (not in nixpkgs) <https://git.tartarus.org/?p=simon/puzzles.git>
- Shootin Stars (Godot; not in nixpkgs) <https://gitlab.com/greenbeast/shootin-stars>
- numberlink (generic name for Flow Free). not packaged in Nix
- Neverball (https://neverball.org/screenshots.php). nix: as `neverball`
#### moby #### moby
- fix cpuidle (gets better power consumption): <https://xnux.eu/log/077.html> - fix cpuidle (gets better power consumption): <https://xnux.eu/log/077.html>
- install apps:
- display QR codes for WiFi endpoints: <https://linuxphoneapps.org/apps/noappid.wisperwind.wifi2qr/>
- shopping list: <https://linuxphoneapps.org/apps/ro.hume.cosmin.shoppinglist/>
- offline Wikipedia
- SwayNC: - SwayNC:
- don't show MPRIS if no players detected - don't show MPRIS if no players detected
- this is a problem of playerctld, i guess - this is a problem of playerctld, i guess
- add option to change audio output - add option to change audio output
- fix colors (red alert) to match overall theme - fix colors (red alert) to match overall theme
- extend width to 100% of portrait mode
- moby: tune GPS - moby: tune GPS
- run only geoclue, and not gpsd, to save power? - run only geoclue, and not gpsd, to save power?
- tune QGPS setting in eg25-control, for less jitter? - tune QGPS setting in eg25-control, for less jitter?
@@ -86,14 +70,17 @@
- manually do smoothing, as some layer between mepo and geoclue/gpsd? - manually do smoothing, as some layer between mepo and geoclue/gpsd?
- moby: show battery state on ssh login - moby: show battery state on ssh login
- moby: improve gPodder launch time - moby: improve gPodder launch time
- sxmo: port to swaybar like i use on desktop
- users in #sxmo claim it's way better perf
- sxmo: fix youtube scripts (package youtube-cli)
- moby: theme GTK apps (i.e. non-adwaita styles) - moby: theme GTK apps (i.e. non-adwaita styles)
- combine multiple icon themes to get one which has the full icon set?
- get adwaita-icon-theme to ship everything even when cross-compiled?
- especially, make the menubar collapsible - especially, make the menubar collapsible
- try Gradience tool specifically for theming adwaita? <https://linuxphoneapps.org/apps/com.github.gradienceteam.gradience/> - try Gradience tool specifically for theming adwaita? <https://linuxphoneapps.org/apps/com.github.gradienceteam.gradience/>
- phog: remove the gnome-shell runtime dependency to save hella closure size - phog: remove the gnome-shell runtime dependency to save hella closure size
#### non-moby #### non-moby
- RSS: integrate a paywall bypass
- e.g. self-hosted [ladder](https://github.com/everywall/ladder) (like 12ft.io)
- neovim: set up language server (lsp; rnix-lsp; nvim-lspconfig) - neovim: set up language server (lsp; rnix-lsp; nvim-lspconfig)
- Helix: make copy-to-system clipboard be the default - Helix: make copy-to-system clipboard be the default
- firefox/librewolf: persist history - firefox/librewolf: persist history
@@ -114,10 +101,13 @@
- add `pkgs.impure-cached.<foo>` package set to build things with ccache enabled - add `pkgs.impure-cached.<foo>` package set to build things with ccache enabled
- every package here can be auto-generated, and marked with some env var so that it doesn't pollute the pure package set - every package here can be auto-generated, and marked with some env var so that it doesn't pollute the pure package set
- would be super handy for package prototyping! - would be super handy for package prototyping!
- fix desko so it doesn't dispatch so many build jobs to servo by default - get moby to build without binfmt emulation (i.e. make all emulation explicit)
- then i can distribute builds across servo + desko, and also allow servo to pull packages from desko w/o worrying about purity
## NEW FEATURES: ## NEW FEATURES:
- migrate MAME cabinet to nix - migrate MAME cabinet to nix
- boot it from PXE from servo? - boot it from PXE from servo?
- deploy to new server, and use it as a remote builder
- enable IPv6 - enable IPv6
- package lemonade lemmy app: <https://linuxphoneapps.org/apps/ml.mdwalters.lemonade/> - package lemonade lemmy app: <https://linuxphoneapps.org/apps/ml.mdwalters.lemonade/>

79
flake.lock generated
View File

@@ -1,5 +1,23 @@
{ {
"nodes": { "nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1687709756,
"narHash": "sha256-Y5wKlQSkgEK2weWdOu4J3riRd+kV/VCgHsqLNTTWQ/0=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "dbabf0ca0c0c4bce6ea5eaf65af5cb694d2082c7",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"mobile-nixos": { "mobile-nixos": {
"flake": false, "flake": false,
"locked": { "locked": {
@@ -17,29 +35,13 @@
"type": "github" "type": "github"
} }
}, },
"nixpkgs-next-unpatched": {
"locked": {
"lastModified": 1705233677,
"narHash": "sha256-eq3VE8QGJsunqqF/BlLslWE1gASp4Hlgp0c78coxat0=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "724e39ebb9b8eda97f17d423f66fbc5a991f4f8d",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "staging-next",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-stable": { "nixpkgs-stable": {
"locked": { "locked": {
"lastModified": 1705033721, "lastModified": 1698544399,
"narHash": "sha256-K5eJHmL1/kev6WuqyqqbS1cdNnSidIZ3jeqJ7GbrYnQ=", "narHash": "sha256-vhRmPyEyoPkrXF2iykBsWHA05MIaOSmMRLMF7Hul6+s=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "a1982c92d8980a0114372973cbdfe0a307f1bdea", "rev": "d87c5d8c41c9b3b39592563242f3a448b5cc4bc9",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -51,16 +53,16 @@
}, },
"nixpkgs-unpatched": { "nixpkgs-unpatched": {
"locked": { "locked": {
"lastModified": 1705254014, "lastModified": 1698611440,
"narHash": "sha256-4RrVNEqxeji4vqDgzSl7JoCD6a0ag5LF9zXFndtqrpE=", "narHash": "sha256-jPjHjrerhYDy3q9+s5EAsuhyhuknNfowY6yt6pjn9pc=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "6c08fe3ccf437d8b26bec010fd925ddd6bb0d0d5", "rev": "0cbe9f69c234a7700596e943bfae7ef27a31b735",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "nixos", "owner": "nixos",
"ref": "master", "ref": "nixos-unstable",
"repo": "nixpkgs", "repo": "nixpkgs",
"type": "github" "type": "github"
} }
@@ -68,7 +70,6 @@
"root": { "root": {
"inputs": { "inputs": {
"mobile-nixos": "mobile-nixos", "mobile-nixos": "mobile-nixos",
"nixpkgs-next-unpatched": "nixpkgs-next-unpatched",
"nixpkgs-unpatched": "nixpkgs-unpatched", "nixpkgs-unpatched": "nixpkgs-unpatched",
"sops-nix": "sops-nix", "sops-nix": "sops-nix",
"uninsane-dot-org": "uninsane-dot-org" "uninsane-dot-org": "uninsane-dot-org"
@@ -82,11 +83,11 @@
"nixpkgs-stable": "nixpkgs-stable" "nixpkgs-stable": "nixpkgs-stable"
}, },
"locked": { "locked": {
"lastModified": 1705201153, "lastModified": 1698548647,
"narHash": "sha256-y0/a4IMDZrc7lAkR7Gcm5R3W2iCBiARHnYZe6vkmiNE=", "narHash": "sha256-7c03OjBGqnwDW0FBaBc+NjfEBxMkza+dxZGJPyIzfFE=",
"owner": "Mic92", "owner": "Mic92",
"repo": "sops-nix", "repo": "sops-nix",
"rev": "70dd0d521f7849338e487a219c1a07c429a66d77", "rev": "632c3161a6cc24142c8e3f5529f5d81042571165",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -95,18 +96,34 @@
"type": "github" "type": "github"
} }
}, },
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"uninsane-dot-org": { "uninsane-dot-org": {
"inputs": { "inputs": {
"flake-utils": "flake-utils",
"nixpkgs": [ "nixpkgs": [
"nixpkgs-unpatched" "nixpkgs-unpatched"
] ]
}, },
"locked": { "locked": {
"lastModified": 1704082841, "lastModified": 1698634059,
"narHash": "sha256-4g3lePnUALb8B1m3rEDD6rrZAq2pTN4qaSnStbd676U=", "narHash": "sha256-+Oyv6vDyCtBzab/5cTG0nUrHD9gj7KgGfD4D1Rn4fCk=",
"ref": "refs/heads/master", "ref": "refs/heads/master",
"rev": "4a1fa488e64e6c87c6c951e3fafb2684692f64d3", "rev": "2419750ca98fc04af42c91e50c49a29c68d465d2",
"revCount": 234, "revCount": 210,
"type": "git", "type": "git",
"url": "https://git.uninsane.org/colin/uninsane" "url": "https://git.uninsane.org/colin/uninsane"
}, },

259
flake.nix
View File

@@ -29,24 +29,22 @@
# - daily: # - daily:
# - nixos-unstable cut from master after enough packages have been built in caches. # - nixos-unstable cut from master after enough packages have been built in caches.
# - every 6 hours: # - every 6 hours:
# - master auto-merged into staging and staging-next # - master auto-merged into staging.
# - staging-next auto-merged into staging. # - staging-next auto-merged into staging.
# - manually, approximately once per month: # - manually, approximately once per month:
# - staging-next is cut from staging. # - staging-next is cut from staging.
# - staging-next merged into master. # - staging-next merged into master.
# #
# which branch to source from? # which branch to source from?
# - nixos-unstable: for everyday development; it provides good caching # - for everyday development, prefer `nixos-unstable` branch, as it provides good caching.
# - master: temporarily if i'm otherwise cherry-picking lots of already-applied patches # - if need to test bleeding updates (e.g. if submitting code into staging):
# - staging-next: if testing stuff that's been PR'd into staging, i.e. base library updates. # - use `staging-next` if it's been cut (i.e. if there's an active staging-next -> master PR)
# - staging: maybe if no staging-next -> master PR has been cut yet? # - use `staging` if no staging-next branch has been cut.
# #
# <https://github.com/nixos/nixpkgs/tree/nixos-unstable> # <https://github.com/nixos/nixpkgs/tree/nixos-unstable>
# nixpkgs-unpatched.url = "github:nixos/nixpkgs?ref=nixos-unstable"; nixpkgs-unpatched.url = "github:nixos/nixpkgs?ref=nixos-unstable";
nixpkgs-unpatched.url = "github:nixos/nixpkgs?ref=master"; # nixpkgs-unpatched.url = "github:nixos/nixpkgs?ref=staging-next";
# nixpkgs-unpatched.url = "github:nixos/nixpkgs?ref=nixos-staging"; # nixpkgs-unpatched.url = "github:nixos/nixpkgs?ref=staging";
# nixpkgs-unpatched.url = "github:nixos/nixpkgs?ref=nixos-staging-next";
nixpkgs-next-unpatched.url = "github:nixos/nixpkgs?ref=staging-next";
mobile-nixos = { mobile-nixos = {
# <https://github.com/nixos/mobile-nixos> # <https://github.com/nixos/mobile-nixos>
@@ -75,7 +73,6 @@
outputs = { outputs = {
self, self,
nixpkgs-unpatched, nixpkgs-unpatched,
nixpkgs-next-unpatched ? nixpkgs-unpatched,
mobile-nixos, mobile-nixos,
sops-nix, sops-nix,
uninsane-dot-org, uninsane-dot-org,
@@ -94,9 +91,9 @@
# rather than apply our nixpkgs patches as a flake input, do that here instead. # rather than apply our nixpkgs patches as a flake input, do that here instead.
# this (temporarily?) resolves the bad UX wherein a subflake residing in the same git # this (temporarily?) resolves the bad UX wherein a subflake residing in the same git
# repo as the main flake causes the main flake to have an unstable hash. # repo as the main flake causes the main flake to have an unstable hash.
patchNixpkgs = variant: nixpkgs: (import ./nixpatches/flake.nix).outputs { nixpkgs = (import ./nixpatches/flake.nix).outputs {
inherit variant nixpkgs; self = nixpkgs;
self = patchNixpkgs variant nixpkgs; nixpkgs = nixpkgs-unpatched;
} // { } // {
# provide values that nixpkgs ordinarily sources from the flake.lock file, # provide values that nixpkgs ordinarily sources from the flake.lock file,
# inaccessible to it here because of the import-from-derivation. # inaccessible to it here because of the import-from-derivation.
@@ -112,24 +109,21 @@
inherit (self) shortRev; inherit (self) shortRev;
}; };
nixpkgs' = patchNixpkgs "master" nixpkgs-unpatched; nixpkgsCompiledBy = system: nixpkgs.legacyPackages."${system}";
nixpkgsCompiledBy = system: nixpkgs'.legacyPackages."${system}";
evalHost = { name, local, target, light ? false, nixpkgs ? nixpkgs' }: nixpkgs.lib.nixosSystem { evalHost = { name, local, target }: nixpkgs.lib.nixosSystem {
system = target; system = target;
modules = [ modules = [
{ {
nixpkgs.buildPlatform.system = local; nixpkgs = (if (local != null) then {
buildPlatform = local;
} else {}) // {
# TODO: does the earlier `system` arg to nixosSystem make its way here?
hostPlatform.system = target;
};
# nixpkgs.buildPlatform = local; # set by instantiate.nix instead
# nixpkgs.config.replaceStdenv = { pkgs }: pkgs.ccacheStdenv; # nixpkgs.config.replaceStdenv = { pkgs }: pkgs.ccacheStdenv;
} }
(optionalAttrs (local != target) {
# XXX(2023/12/11): cache.nixos.org uses `system = ...` instead of `hostPlatform.system`, and that choice impacts the closure of every package.
# so avoid specifying hostPlatform.system on non-cross builds, so i can use upstream caches.
nixpkgs.hostPlatform.system = target;
})
(optionalAttrs light {
sane.enableSlowPrograms = false;
})
(import ./hosts/instantiate.nix { hostName = name; }) (import ./hosts/instantiate.nix { hostName = name; })
self.nixosModules.default self.nixosModules.default
self.nixosModules.passthru self.nixosModules.passthru
@@ -142,24 +136,39 @@
]; ];
}; };
in { in {
nixosConfigurations = let nixosConfigurations =
hosts = { let
servo = { name = "servo"; local = "x86_64-linux"; target = "x86_64-linux"; }; hosts = {
desko = { name = "desko"; local = "x86_64-linux"; target = "x86_64-linux"; }; servo = { name = "servo"; local = "x86_64-linux"; target = "x86_64-linux"; };
desko-light = { name = "desko"; local = "x86_64-linux"; target = "x86_64-linux"; light = true; }; desko = { name = "desko"; local = "x86_64-linux"; target = "x86_64-linux"; };
lappy = { name = "lappy"; local = "x86_64-linux"; target = "x86_64-linux"; }; lappy = { name = "lappy"; local = "x86_64-linux"; target = "x86_64-linux"; };
lappy-light = { name = "lappy"; local = "x86_64-linux"; target = "x86_64-linux"; light = true; }; moby = { name = "moby"; local = "x86_64-linux"; target = "aarch64-linux"; };
moby = { name = "moby"; local = "x86_64-linux"; target = "aarch64-linux"; }; rescue = { name = "rescue"; local = "x86_64-linux"; target = "x86_64-linux"; };
moby-light = { name = "moby"; local = "x86_64-linux"; target = "aarch64-linux"; light = true; }; };
rescue = { name = "rescue"; local = "x86_64-linux"; target = "x86_64-linux"; }; # cross-compiled builds: instead of emulating the host, build using a cross-compiler.
}; # - these are faster to *build* than the emulated variants (useful when tweaking packages),
hostsNext = mapAttrs' (h: v: { # - but fewer of their packages can be found in upstream caches.
name = "${h}-next"; cross = mapAttrValues evalHost hosts;
value = v // { nixpkgs = patchNixpkgs "staging-next" nixpkgs-next-unpatched; }; emulated = mapAttrValues
}) hosts; ({name, local, target}: evalHost {
in mapAttrValues evalHost ( inherit name target;
hosts // hostsNext local = null;
); })
hosts;
prefixAttrs = prefix: attrs: mapAttrs'
(name: value: {
name = prefix + name;
inherit value;
})
attrs;
in
(prefixAttrs "cross-" cross) //
(prefixAttrs "emulated-" emulated) // {
# prefer native builds for these machines:
inherit (emulated) servo desko lappy rescue;
# prefer cross-compiled builds for these machines:
inherit (cross) moby;
};
# unofficial output # unofficial output
# this produces a EFI-bootable .img file (GPT with a /boot partition and a system (/ or /nix) partition). # this produces a EFI-bootable .img file (GPT with a /boot partition and a system (/ or /nix) partition).
@@ -178,13 +187,9 @@
imgs = mapAttrValues (host: host.config.system.build.img) self.nixosConfigurations; imgs = mapAttrValues (host: host.config.system.build.img) self.nixosConfigurations;
# unofficial output # unofficial output
hostConfigs = mapAttrValues (host: host.config) self.nixosConfigurations;
hostSystems = mapAttrValues (host: host.config.system.build.toplevel) self.nixosConfigurations;
hostPkgs = mapAttrValues (host: host.config.system.build.pkgs) self.nixosConfigurations; hostPkgs = mapAttrValues (host: host.config.system.build.pkgs) self.nixosConfigurations;
hostPrograms = mapAttrValues (host: mapAttrValues (p: p.package) host.config.sane.programs) self.nixosConfigurations; hostPrograms = mapAttrValues (host: mapAttrValues (p: p.package) host.config.sane.programs) self.nixosConfigurations;
patched.nixpkgs = nixpkgs';
overlays = { overlays = {
# N.B.: `nix flake check` requires every overlay to take `final: prev:` at defn site, # N.B.: `nix flake check` requires every overlay to take `final: prev:` at defn site,
# hence the weird redundancy. # hence the weird redundancy.
@@ -198,7 +203,7 @@
passthru = final: prev: passthru = final: prev:
let let
mobile = (import "${mobile-nixos}/overlay/overlay.nix"); mobile = (import "${mobile-nixos}/overlay/overlay.nix");
uninsane = uninsane-dot-org.overlays.default; uninsane = uninsane-dot-org.overlay;
in in
(mobile final prev) (mobile final prev)
// (uninsane final prev) // (uninsane final prev)
@@ -229,27 +234,23 @@
# extract only our own packages from the full set. # extract only our own packages from the full set.
# because of `nix flake check`, we flatten the package set and only surface x86_64-linux packages. # because of `nix flake check`, we flatten the package set and only surface x86_64-linux packages.
packages = mapAttrs packages = mapAttrs
(system: passthruPkgs: passthruPkgs.lib.filterAttrs (system: allPkgs:
(name: pkg: allPkgs.lib.filterAttrs (name: pkg:
# keep only packages which will pass `nix flake check`, i.e. keep only: # keep only packages which will pass `nix flake check`, i.e. keep only:
# - derivations (not package sets) # - derivations (not package sets)
# - packages that build for the given platform # - packages that build for the given platform
(! elem name [ "feeds" "pythonPackagesExtensions" ]) (! elem name [ "feeds" "pythonPackagesExtensions" ])
&& (passthruPkgs.lib.meta.availableOn passthruPkgs.stdenv.hostPlatform pkg) && (allPkgs.lib.meta.availableOn allPkgs.stdenv.hostPlatform pkg)
) )
( (
# expose sane packages and chosen inputs (uninsane.org) # expose sane packages and chosen inputs (uninsane.org)
(import ./pkgs { pkgs = passthruPkgs; }) // { (import ./pkgs { pkgs = allPkgs; }) // {
inherit (passthruPkgs) uninsane-dot-org; inherit (allPkgs) uninsane-dot-org;
} }
) )
) )
# self.legacyPackages; # self.legacyPackages;
{ { inherit (self.legacyPackages) x86_64-linux; }
x86_64-linux = (nixpkgsCompiledBy "x86_64-linux").appendOverlays [
self.overlays.passthru
];
}
; ;
apps."x86_64-linux" = apps."x86_64-linux" =
@@ -257,27 +258,17 @@
pkgs = self.legacyPackages."x86_64-linux"; pkgs = self.legacyPackages."x86_64-linux";
sanePkgs = import ./pkgs { inherit pkgs; }; sanePkgs = import ./pkgs { inherit pkgs; };
deployScript = host: addr: action: pkgs.writeShellScript "deploy-${host}" '' deployScript = host: addr: action: pkgs.writeShellScript "deploy-${host}" ''
nix build '.#nixosConfigurations.${host}.config.system.build.toplevel' --out-link ./result-${host} "$@" nix build '.#nixosConfigurations.${host}.config.system.build.toplevel' --out-link ./result-${host} $@
sudo nix sign-paths -r -k /run/secrets/nix_serve_privkey $(readlink ./result-${host}) sudo nix sign-paths -r -k /run/secrets/nix_serve_privkey $(readlink ./result-${host})
# XXX: this triggers another config eval & (potentially) build. # XXX: this triggers another config eval & (potentially) build.
# if the config changed between these invocations, the above signatures might not apply to the deployed config. # if the config changed between these invocations, the above signatures might not apply to the deployed config.
# let the user handle that edge case by re-running this whole command. # let the user handle that edge case by re-running this whole command
# N.B.: `--fast` option here is critical to cross-compiled deployments: without it the build machine will try to invoke the host machine's `nix` binary. nixos-rebuild --flake '.#${host}' ${action} --target-host colin@${addr} --use-remote-sudo $@
# TODO: solve this by replacing the nixos-build invocation with:
# - nix-copy-closure --to $host $result
# - on target: nix-env set -p /nix/var/nix/profiles/system $result
# - on target: $result/bin/switch-to-configuration
nixos-rebuild --flake '.#${host}' ${action} --target-host colin@${addr} --use-remote-sudo "$@" --fast
''; '';
deployApp = host: addr: action: {
type = "app";
program = ''${deployScript host addr action}'';
};
# pkg updating. # pkg updating.
# a cleaner alternative lives here: <https://discourse.nixos.org/t/how-can-i-run-the-updatescript-of-personal-packages/25274/2> # a cleaner alternative lives here: <https://discourse.nixos.org/t/how-can-i-run-the-updatescript-of-personal-packages/25274/2>
# mkUpdater :: [ String ] -> { type = "app"; program = path; }
mkUpdater = attrPath: { mkUpdater = attrPath: {
type = "app"; type = "app";
program = let program = let
@@ -302,7 +293,7 @@
} else {} } else {}
) )
(pkgs.lib.getAttrFromPath basePath sanePkgs); (pkgs.lib.getAttrFromPath basePath sanePkgs);
mkUpdaters = { ignore ? [], flakePrefix ? [] }@opts: basePath: mkUpdaters = { ignore ? [] }@opts: basePath:
let let
updaters = mkUpdatersNoAliases opts basePath; updaters = mkUpdatersNoAliases opts basePath;
invokeUpdater = name: pkg: invokeUpdater = name: pkg:
@@ -312,7 +303,7 @@
# in case `name` has a `.` in it, we have to quote it # in case `name` has a `.` in it, we have to quote it
escapedPath = builtins.map (p: ''"${p}"'') fullPath; escapedPath = builtins.map (p: ''"${p}"'') fullPath;
updatePath = builtins.concatStringsSep "." (flakePrefix ++ escapedPath); updatePath = builtins.concatStringsSep "." ([ "update" "pkgs" ] ++ escapedPath);
in pkgs.lib.optionalString doUpdateByDefault ( in pkgs.lib.optionalString doUpdateByDefault (
pkgs.lib.escapeShellArgs [ pkgs.lib.escapeShellArgs [
"nix" "run" ".#${updatePath}" "nix" "run" ".#${updatePath}"
@@ -320,9 +311,8 @@
); );
in { in {
type = "app"; type = "app";
# top-level app just invokes the updater of everything one layer below it
program = builtins.toString (pkgs.writeShellScript program = builtins.toString (pkgs.writeShellScript
(builtins.concatStringsSep "-" (flakePrefix ++ basePath)) (builtins.concatStringsSep "-" (["update"] ++ basePath))
(builtins.concatStringsSep (builtins.concatStringsSep
"\n" "\n"
(pkgs.lib.mapAttrsToList invokeUpdater updaters) (pkgs.lib.mapAttrsToList invokeUpdater updaters)
@@ -342,7 +332,7 @@
- `nix run '.#update.feeds'` - `nix run '.#update.feeds'`
- updates metadata for all feeds - updates metadata for all feeds
- `nix run '.#init-feed' <url>` - `nix run '.#init-feed' <url>`
- `nix run '.#deploy.{desko,lappy,moby,servo}[-light][.test]' [nixos-rebuild args ...]` - `nix run '.#deploy-{lappy,moby,moby-test,servo}' [nixos-rebuild args ...]`
- `nix run '.#check'` - `nix run '.#check'`
- make sure all systems build; NUR evaluates - make sure all systems build; NUR evaluates
@@ -356,70 +346,48 @@
nix flake show --option allow-import-from-derivation true nix flake show --option allow-import-from-derivation true
''); '');
}; };
# wrangle some names to get package updaters which refer back into the flake, but also conditionally ignore certain paths (e.g. sane.feeds). update.pkgs = mkUpdaters { ignore = [ ["feeds"] ]; } [];
# TODO: better design update.feeds = mkUpdaters {} [ "feeds" ];
update = rec {
_impl.pkgs.sane = mkUpdaters { flakePrefix = [ "update" "_impl" "pkgs" ]; ignore = [ [ "sane" "feeds" ] ]; } [ "sane" ];
pkgs = _impl.pkgs.sane;
_impl.feeds.sane.feeds = mkUpdaters { flakePrefix = [ "update" "_impl" "feeds" ]; } [ "sane" "feeds" ];
feeds = _impl.feeds.sane.feeds;
};
init-feed = { init-feed = {
type = "app"; type = "app";
program = "${pkgs.feeds.init-feed}"; program = "${pkgs.feeds.init-feed}";
}; };
deploy = { deploy-lappy = {
lappy = deployApp "lappy" "lappy" "switch";
lappy-light = deployApp "lappy-light" "lappy" "switch";
moby = deployApp "moby" "moby" "switch";
moby-light = deployApp "moby-light" "moby" "switch";
moby-test = deployApp "moby" "moby" "test";
servo = deployApp "servo" "servo" "switch";
};
sync = {
type = "app"; type = "app";
program = builtins.toString (pkgs.writeShellScript "sync-all" '' program = ''${deployScript "lappy" "lappy" "switch"}'';
RC_lappy=$(nix run '.#sync.lappy' -- "$@")
RC_moby=$(nix run '.#sync.moby' -- "$@")
RC_desko=$(nix run '.#sync.desko' -- "$@")
echo "lappy: $RC_lappy"
echo "moby: $RC_moby"
echo "desko: $RC_desko"
'');
}; };
deploy-moby-test = {
sync.desko = {
# copy music from servo to desko
# can run this from any device that has ssh access to desko and servo
type = "app"; type = "app";
program = builtins.toString (pkgs.writeShellScript "sync-to-desko" '' program = ''${deployScript "moby" "moby-hn" "test"}'';
sudo mount /mnt/desko-home
${pkgs.sane-scripts.sync-music}/bin/sane-sync-music --compat /mnt/servo-media/Music /mnt/desko-home/Music "$@"
'');
}; };
deploy-moby = {
sync.lappy = {
# copy music from servo to lappy
# can run this from any device that has ssh access to lappy and servo
type = "app"; type = "app";
program = builtins.toString (pkgs.writeShellScript "sync-to-lappy" '' program = ''${deployScript "moby" "moby-hn" "switch"}'';
sudo mount /mnt/lappy-home };
${pkgs.sane-scripts.sync-music}/bin/sane-sync-music --compress --compat /mnt/servo-media/Music /mnt/lappy-home/Music "$@" deploy-servo = {
''); type = "app";
program = ''${deployScript "servo" "servo" "switch"}'';
}; };
sync.moby = { sync-moby = {
# copy music from servo to moby # copy music from the current device to moby
# can run this from any device that has ssh access to moby and servo # TODO: should i actually sync from /mnt/servo-media/Music instead of the local drive?
type = "app"; type = "app";
program = builtins.toString (pkgs.writeShellScript "sync-to-moby" '' program = builtins.toString (pkgs.writeShellScript "sync-to-moby" ''
sudo mount /mnt/moby-home sudo mount /mnt/moby-home
# N.B.: limited by network/disk -> reduce job count to improve pause/resume behavior ${pkgs.sane-scripts.sync-music}/bin/sane-sync-music ~/Music /mnt/moby-home/Music
${pkgs.sane-scripts.sync-music}/bin/sane-sync-music --compress --compat --jobs 4 /mnt/servo-media/Music /mnt/moby-home/Music "$@" '');
};
sync-lappy = {
# copy music from servo to lappy
# can run this from any device that has ssh access to lappy
type = "app";
program = builtins.toString (pkgs.writeShellScript "sync-to-lappy" ''
sudo mount /mnt/lappy-home
${pkgs.sane-scripts.sync-music}/bin/sane-sync-music /mnt/servo-media/Music /mnt/lappy-home/Music
''); '');
}; };
@@ -428,12 +396,12 @@
program = builtins.toString (pkgs.writeShellScript "check-all" '' program = builtins.toString (pkgs.writeShellScript "check-all" ''
nix run '.#check.nur' nix run '.#check.nur'
RC0=$? RC0=$?
nix run '.#check.hostConfigs' nix run '.#check.host-configs'
RC1=$? RC1=$?
nix run '.#check.rescue' nix run '.#check.rescue'
RC2=$? RC2=$?
echo "nur: $RC0" echo "nur: $RC0"
echo "hostConfigs: $RC1" echo "host-configs: $RC1"
echo "rescue: $RC2" echo "rescue: $RC2"
exit $(($RC0 | $RC1 | $RC2)) exit $(($RC0 | $RC1 | $RC2))
''); '');
@@ -450,59 +418,32 @@
--option restrict-eval true \ --option restrict-eval true \
--option allow-import-from-derivation true \ --option allow-import-from-derivation true \
--drv-path --show-trace \ --drv-path --show-trace \
-I nixpkgs=${nixpkgs-unpatched} \ -I nixpkgs=$(nix-instantiate --find-file nixpkgs) \
-I ../../ \ -I ../../ \
| tee # tee to prevent interactive mode | tee # tee to prevent interactive mode
''); '');
}; };
check.hostConfigs = { check.host-configs = {
type = "app"; type = "app";
program = let program = let
checkHost = host: let checkHost = host: ''
shellHost = pkgs.lib.replaceStrings [ "-" ] [ "_" ] host; nix build -v '.#nixosConfigurations.${host}.config.system.build.toplevel' --out-link ./result-${host} -j2 $@
in '' RC_${host}=$?
nix build -v '.#nixosConfigurations.${host}.config.system.build.toplevel' --out-link ./result-${host} -j2 "$@"
RC_${shellHost}=$?
''; '';
in builtins.toString (pkgs.writeShellScript in builtins.toString (pkgs.writeShellScript
"check-host-configs" "check-host-configs"
'' ''
# build minimally-usable hosts first, then their full image.
# this gives me a minimal image i can deploy or copy over, early.
${checkHost "desko-light"}
${checkHost "moby-light"}
${checkHost "lappy-light"}
${checkHost "desko"} ${checkHost "desko"}
${checkHost "lappy"} ${checkHost "lappy"}
${checkHost "servo"} ${checkHost "servo"}
${checkHost "moby"} ${checkHost "moby"}
${checkHost "rescue"} ${checkHost "rescue"}
# still want to build the -light variants first so as to avoid multiple simultaneous webkitgtk builds
${checkHost "desko-light-next"}
${checkHost "moby-light-next"}
${checkHost "desko-next"}
${checkHost "lappy-next"}
${checkHost "servo-next"}
${checkHost "moby-next"}
${checkHost "rescue-next"}
echo "desko: $RC_desko" echo "desko: $RC_desko"
echo "lappy: $RC_lappy" echo "lappy: $RC_lappy"
echo "servo: $RC_servo" echo "servo: $RC_servo"
echo "moby: $RC_moby" echo "moby: $RC_moby"
echo "rescue: $RC_rescue" echo "rescue: $RC_rescue"
echo "desko-next: $RC_desko_next"
echo "lappy-next: $RC_lappy_next"
echo "servo-next: $RC_servo_next"
echo "moby-next: $RC_moby_next"
echo "rescue-next: $RC_rescue_next"
# i don't really care if the -next hosts fail. i build them mostly to keep the cache fresh/ready
exit $(($RC_desko | $RC_lappy | $RC_servo | $RC_moby | $RC_rescue)) exit $(($RC_desko | $RC_lappy | $RC_servo | $RC_moby | $RC_rescue))
'' ''
); );

View File

@@ -9,34 +9,25 @@
# services.distccd.enable = true; # services.distccd.enable = true;
# sane.programs.distcc.enableFor.user.guest = true; # sane.programs.distcc.enableFor.user.guest = true;
# TODO: remove emulation, but need to fix nixos-rebuild to moby for that.
# sane.roles.build-machine.emulation = true;
sops.secrets.colin-passwd.neededForUsers = true; sops.secrets.colin-passwd.neededForUsers = true;
sane.ports.openFirewall = true; # for e.g. nix-serve
sane.roles.build-machine.enable = true; sane.roles.build-machine.enable = true;
sane.roles.ac = true;
sane.roles.client = true; sane.roles.client = true;
sane.roles.dev-machine = true; sane.roles.dev-machine = true;
sane.roles.pc = true;
sane.services.wg-home.enable = true; sane.services.wg-home.enable = true;
sane.services.wg-home.ip = config.sane.hosts.by-name."desko".wg-home.ip; sane.services.wg-home.ip = config.sane.hosts.by-name."desko".wg-home.ip;
sane.services.duplicity.enable = true; sane.services.duplicity.enable = true;
sane.services.nixserve.secretKeyFile = config.sops.secrets.nix_serve_privkey.path; sane.services.nixserve.secretKeyFile = config.sops.secrets.nix_serve_privkey.path;
sane.nixcache.substituters.desko = false;
sane.nixcache.remote-builders.desko = false;
sane.gui.sway.enable = true; sane.gui.sway.enable = true;
sane.programs.iphoneUtils.enableFor.user.colin = true; sane.programs.iphoneUtils.enableFor.user.colin = true;
sane.programs.steam.enableFor.user.colin = true; sane.programs.steam.enableFor.user.colin = true;
sane.programs.guiApps.suggestedPrograms = [ "desktopGuiApps" ];
sane.programs.consoleUtils.suggestedPrograms = [ "consoleMediaUtils" "desktopConsoleUtils" ];
# sane.programs.devPkgs.enableFor.user.colin = true; # sane.programs.devPkgs.enableFor.user.colin = true;
sane.programs.signal-desktop.config.autostart = true;
sane.programs."gnome.geary".config.autostart = true;
boot.loader.efi.canTouchEfiVariables = false; boot.loader.efi.canTouchEfiVariables = false;
sane.image.extraBootFiles = [ pkgs.bootpart-uefi-x86_64 ]; sane.image.extraBootFiles = [ pkgs.bootpart-uefi-x86_64 ];

View File

@@ -1,13 +1,14 @@
{ ... }: { ... }:
{ {
sane.persist.root-on-tmpfs = true;
# increase /tmp space (defaults to 50% of RAM) for building large nix things. # increase /tmp space (defaults to 50% of RAM) for building large nix things.
# a cross-compiled kernel, particularly, will easily use 30+GB of tmp # a cross-compiled kernel, particularly, will easily use 30+GB of tmp
fileSystems."/tmp".options = [ "size=64G" ]; fileSystems."/tmp".options = [ "size=64G" ];
fileSystems."/nix" = { fileSystems."/nix" = {
# device = "/dev/disk/by-uuid/0ab0770b-7734-4167-88d9-6e4e20bb2a56"; # device = "/dev/disk/by-uuid/985a0a32-da52-4043-9df7-615adec2e4ff";
device = "/dev/disk/by-uuid/845d85bf-761d-431b-a406-e6f20909154f"; device = "/dev/disk/by-uuid/0ab0770b-7734-4167-88d9-6e4e20bb2a56";
fsType = "btrfs"; fsType = "btrfs";
options = [ options = [
"compress=zstd" "compress=zstd"
@@ -16,8 +17,8 @@
}; };
fileSystems."/boot" = { fileSystems."/boot" = {
# device = "/dev/disk/by-uuid/41B6-BAEF"; # device = "/dev/disk/by-uuid/CAA7-E7D2";
device = "/dev/disk/by-uuid/5049-9AFD"; device = "/dev/disk/by-uuid/41B6-BAEF";
fsType = "vfat"; fsType = "vfat";
}; };
} }

View File

@@ -7,7 +7,6 @@
sane.roles.client = true; sane.roles.client = true;
sane.roles.dev-machine = true; sane.roles.dev-machine = true;
sane.roles.pc = true;
sane.services.wg-home.enable = true; sane.services.wg-home.enable = true;
sane.services.wg-home.ip = config.sane.hosts.by-name."lappy".wg-home.ip; sane.services.wg-home.ip = config.sane.hosts.by-name."lappy".wg-home.ip;
@@ -16,7 +15,11 @@
boot.loader.efi.canTouchEfiVariables = false; boot.loader.efi.canTouchEfiVariables = false;
sane.image.extraBootFiles = [ pkgs.bootpart-uefi-x86_64 ]; sane.image.extraBootFiles = [ pkgs.bootpart-uefi-x86_64 ];
sane.programs.stepmania.enableFor.user.colin = true; sane.programs.guiApps.suggestedPrograms = [
"desktopGuiApps"
"stepmania"
];
sane.programs.consoleUtils.suggestedPrograms = [ "consoleMediaUtils" "desktopConsoleUtils" ];
sops.secrets.colin-passwd.neededForUsers = true; sops.secrets.colin-passwd.neededForUsers = true;

View File

@@ -1,6 +1,8 @@
{ ... }: { ... }:
{ {
sane.persist.root-on-tmpfs = true;
fileSystems."/nix" = { fileSystems."/nix" = {
device = "/dev/disk/by-uuid/75230e56-2c69-4e41-b03e-68475f119980"; device = "/dev/disk/by-uuid/75230e56-2c69-4e41-b03e-68475f119980";
fsType = "btrfs"; fsType = "btrfs";

View File

@@ -22,6 +22,7 @@
# the device type informs (at least): # the device type informs (at least):
# - SXMO_WIFI_MODULE # - SXMO_WIFI_MODULE
# - SXMO_RTW_SCAN_INTERVAL # - SXMO_RTW_SCAN_INTERVAL
# - SXMO_SYS_FILES
# - SXMO_TOUCHSCREEN_ID # - SXMO_TOUCHSCREEN_ID
# - SXMO_MONITOR # - SXMO_MONITOR
# - SXMO_ALSA_CONTROL_NAME # - SXMO_ALSA_CONTROL_NAME
@@ -32,7 +33,7 @@
# and so it just wouldn't handle any button inputs (sxmo_hook_inputhandler.sh not on path) # and so it just wouldn't handle any button inputs (sxmo_hook_inputhandler.sh not on path)
SXMO_DEVICE_NAME = "three_button_touchscreen"; SXMO_DEVICE_NAME = "three_button_touchscreen";
}; };
package = (pkgs.sxmo-utils.override { preferSystemd = true; }).overrideAttrs (base: { package = (pkgs.sxmo-utils-latest.override { preferSystemd = true; }).overrideAttrs (base: {
postPatch = (base.postPatch or "") + '' postPatch = (base.postPatch or "") + ''
# after volume-button navigation mode, restore full keyboard functionality # after volume-button navigation mode, restore full keyboard functionality
cp ${./xkb_mobile_normal_buttons} ./configs/xkb/xkb_mobile_normal_buttons cp ${./xkb_mobile_normal_buttons} ./configs/xkb/xkb_mobile_normal_buttons

View File

@@ -20,7 +20,6 @@
]; ];
sane.roles.client = true; sane.roles.client = true;
sane.roles.handheld = true;
sane.zsh.showDeadlines = false; # unlikely to act on them when in shell sane.zsh.showDeadlines = false; # unlikely to act on them when in shell
sane.services.wg-home.enable = true; sane.services.wg-home.enable = true;
sane.services.wg-home.ip = config.sane.hosts.by-name."moby".wg-home.ip; sane.services.wg-home.ip = config.sane.hosts.by-name."moby".wg-home.ip;
@@ -33,11 +32,15 @@
sops.secrets.colin-passwd.neededForUsers = true; sops.secrets.colin-passwd.neededForUsers = true;
sane.gui.sxmo.enable = true; sane.gui.sxmo.enable = true;
sane.programs.guiApps.suggestedPrograms = [ "handheldGuiApps" ];
# sane.programs.consoleUtils.enableFor.user.colin = false; # sane.programs.consoleUtils.enableFor.user.colin = false;
# sane.programs.guiApps.enableFor.user.colin = false; # sane.programs.guiApps.enableFor.user.colin = false;
sane.programs.blueberry.enableFor.user.colin = false; # bluetooth manager: doesn't cross compile! sane.programs.blueberry.enableFor.user.colin = false; # bluetooth manager: doesn't cross compile!
sane.programs.mercurial.enableFor.user.colin = false; # does not cross compile sane.programs.mercurial.enableFor.user.colin = false; # does not cross compile
sane.programs.nvme-cli.enableFor.system = false; # does not cross compile (libhugetlbfs) sane.programs.sequoia.enableFor.user.colin = false;
sane.programs.tuiApps.enableFor.user.colin = false; # visidata, others, don't compile well
# disabled for faster deploys
sane.programs.soundconverter.enableFor.user.colin = false;
# enabled for easier debugging # enabled for easier debugging
sane.programs.eg25-control.enableFor.user.colin = true; sane.programs.eg25-control.enableFor.user.colin = true;
@@ -45,8 +48,6 @@
# sane.programs.ntfy-sh.config.autostart = true; # sane.programs.ntfy-sh.config.autostart = true;
sane.programs.dino.config.autostart = true; sane.programs.dino.config.autostart = true;
sane.programs.signal-desktop.config.autostart = true;
# sane.programs."gnome.geary".config.autostart = true;
# sane.programs.calls.config.autostart = true; # sane.programs.calls.config.autostart = true;
sane.programs.mpv.config.vo = "wlshm"; #< see hosts/common/programs/mpv.nix for details sane.programs.mpv.config.vo = "wlshm"; #< see hosts/common/programs/mpv.nix for details
@@ -54,6 +55,7 @@
# HACK/TODO: make `programs.P.env.VAR` behave according to `mime.priority` # HACK/TODO: make `programs.P.env.VAR` behave according to `mime.priority`
sane.programs.firefox.env = lib.mkForce {}; sane.programs.firefox.env = lib.mkForce {};
sane.programs.epiphany.env.BROWSER = "epiphany"; sane.programs.epiphany.env.BROWSER = "epiphany";
sane.programs.firefox.enableFor.user.colin = false; # use epiphany instead
# note the .conf.d approach: using ~/.config/pipewire/pipewire.conf directly breaks all audio, # note the .conf.d approach: using ~/.config/pipewire/pipewire.conf directly breaks all audio,
# presumably because that deletes the defaults entirely whereas the .conf.d approach selectively overrides defaults # presumably because that deletes the defaults entirely whereas the .conf.d approach selectively overrides defaults

View File

@@ -1,6 +1,7 @@
{ ... }: { ... }:
{ {
sane.persist.root-on-tmpfs = true;
fileSystems."/nix" = { fileSystems."/nix" = {
device = "/dev/disk/by-uuid/1f1271f8-53ce-4081-8a29-60a4a6b5d6f9"; device = "/dev/disk/by-uuid/1f1271f8-53ce-4081-8a29-60a4a6b5d6f9";
fsType = "btrfs"; fsType = "btrfs";

View File

@@ -74,7 +74,6 @@ in
# without this some GUI apps fail: `DRM_IOCTL_MODE_CREATE_DUMB failed: Cannot allocate memory` # without this some GUI apps fail: `DRM_IOCTL_MODE_CREATE_DUMB failed: Cannot allocate memory`
# this is because they can't allocate enough video ram. # this is because they can't allocate enough video ram.
# see related nixpkgs issue: <https://github.com/NixOS/nixpkgs/issues/260222> # see related nixpkgs issue: <https://github.com/NixOS/nixpkgs/issues/260222>
# TODO(2023/12/03): remove once mesa 23.3.1 lands: <https://github.com/NixOS/nixpkgs/pull/265740>
# #
# the default CMA seems to be 32M. # the default CMA seems to be 32M.
# i was running fine with 256MB from 2022/07-ish through 2022/12-ish, but then the phone quit reliably coming back from sleep (phosh): maybe a memory leak? # i was running fine with 256MB from 2022/07-ish through 2022/12-ish, but then the phone quit reliably coming back from sleep (phosh): maybe a memory leak?

View File

@@ -1,7 +1,7 @@
{ ... }: { ... }:
{ {
fileSystems."/nix" = { fileSystems."/" = {
device = "/dev/disk/by-uuid/44445555-6666-7777-8888-999900001111"; device = "/dev/disk/by-uuid/44445555-6666-7777-8888-999900001111";
fsType = "ext4"; fsType = "ext4";
}; };

View File

@@ -14,11 +14,12 @@
signaldctl.enableFor.user.colin = true; signaldctl.enableFor.user.colin = true;
}; };
sane.roles.ac = true;
sane.roles.build-machine.enable = true; sane.roles.build-machine.enable = true;
sane.roles.build-machine.emulation = false;
sane.zsh.showDeadlines = false; # ~/knowledge doesn't always exist sane.zsh.showDeadlines = false; # ~/knowledge doesn't always exist
sane.programs.consoleUtils.suggestedPrograms = [ sane.programs.consoleUtils.suggestedPrograms = [
"consoleMediaUtils" # notably, for go2tv / casting "desktopConsoleUtils"
"pcConsoleUtils"
"sane-scripts.stop-all-servo" "sane-scripts.stop-all-servo"
]; ];
sane.services.dyn-dns.enable = true; sane.services.dyn-dns.enable = true;
@@ -29,8 +30,6 @@
sane.services.wg-home.ip = config.sane.hosts.by-name."servo".wg-home.ip; sane.services.wg-home.ip = config.sane.hosts.by-name."servo".wg-home.ip;
sane.nixcache.substituters.servo = false; sane.nixcache.substituters.servo = false;
sane.nixcache.substituters.desko = false; sane.nixcache.substituters.desko = false;
sane.nixcache.remote-builders.desko = false;
sane.nixcache.remote-builders.servo = false;
# sane.services.duplicity.enable = true; # TODO: re-enable after HW upgrade # sane.services.duplicity.enable = true; # TODO: re-enable after HW upgrade
# automatically log in at the virtual consoles. # automatically log in at the virtual consoles.

View File

@@ -1,57 +1,7 @@
# zfs docs:
# - <https://nixos.wiki/wiki/ZFS>
# - <repo:nixos/nixpkgs:nixos/modules/tasks/filesystems/zfs.nix>
#
# zfs check health: `zpool status`
#
# zfs pool creation (requires `boot.supportedFilesystems = [ "zfs" ];`
# - 1. identify disk IDs: `ls -l /dev/disk/by-id`
# - 2. pool these disks: `zpool create -f -m legacy pool raidz ata-ST4000VN008-2DR166_WDH0VB45 ata-ST4000VN008-2DR166_WDH17616 ata-ST4000VN008-2DR166_WDH0VC8Q ata-ST4000VN008-2DR166_WDH17680`
# - legacy documented: <https://superuser.com/questions/790036/what-is-a-zfs-legacy-mount-point>
#
# import pools: `zpool import pool`
# show zfs datasets: `zfs list` (will be empty if haven't imported)
# show zfs properties (e.g. compression): `zfs get all pool`
# set zfs properties: `zfs set compression=on pool`
{ ... }: { ... }:
{ {
# hostId: not used for anything except zfs guardrail? sane.persist.root-on-tmpfs = true;
# [hex(ord(x)) for x in 'serv']
networking.hostId = "73657276";
boot.supportedFilesystems = [ "zfs" ];
# boot.zfs.enabled = true;
boot.zfs.forceImportRoot = false;
# scrub all zfs pools weekly:
services.zfs.autoScrub.enable = true;
boot.extraModprobeConfig = ''
# ZFS likes to use half the ram for its own cache and let the kernel push everything else to swap.
# so, reduce its cache size
# see: <https://askubuntu.com/a/1290387>
# see: <https://serverfault.com/a/1119083>
# see: <https://openzfs.github.io/openzfs-docs/Performance%20and%20Tuning/Module%20Parameters.html#zfs-arc-max>
# for all tunables, see: `man 4 zfs`
# to update these parameters without rebooting:
# - `echo '4294967296' | sane-sudo-redirect /sys/module/zfs/parameters/zfs_arc_max`
options zfs zfs_arc_max=4294967296
'';
# to be able to mount the pool like this, make sure to tell zfs to NOT manage it itself.
# otherwise local-fs.target will FAIL and you will be dropped into a rescue shell.
# - `zfs set mountpoint=legacy pool`
# if done correctly, the pool can be mounted before this `fileSystems` entry is created:
# - `sudo mount -t zfs pool /mnt/persist/pool`
fileSystems."/mnt/pool" = {
device = "pool";
fsType = "zfs";
};
# services.zfs.zed = ... # TODO: zfs can send me emails when disks fail
sane.programs.sysadminUtils.suggestedPrograms = [ "zfs" ];
sane.persist.stores."ext" = {
origin = "/mnt/pool/persist";
storeDescription = "external HDD storage";
};
# increase /tmp space (defaults to 50% of RAM) for building large nix things. # increase /tmp space (defaults to 50% of RAM) for building large nix things.
# even the stock `nixpkgs.linux` consumes > 16 GB of tmp # even the stock `nixpkgs.linux` consumes > 16 GB of tmp
fileSystems."/tmp".options = [ "size=32G" ]; fileSystems."/tmp".options = [ "size=32G" ];
@@ -71,7 +21,7 @@
}; };
# slow, external storage (for archiving, etc) # slow, external storage (for archiving, etc)
fileSystems."/mnt/usb-hdd" = { fileSystems."/mnt/persist/ext" = {
device = "/dev/disk/by-uuid/aa272cff-0fcc-498e-a4cb-0d95fb60631b"; device = "/dev/disk/by-uuid/aa272cff-0fcc-498e-a4cb-0d95fb60631b";
fsType = "btrfs"; fsType = "btrfs";
options = [ options = [
@@ -79,7 +29,12 @@
"defaults" "defaults"
]; ];
}; };
sane.fs."/mnt/usb-hdd".mount = {};
sane.persist.stores."ext" = {
origin = "/mnt/persist/ext/persist";
storeDescription = "external HDD storage";
};
sane.fs."/mnt/persist/ext".mount = {};
sane.persist.sys.byStore.plaintext = [ sane.persist.sys.byStore.plaintext = [
# TODO: this is overly broad; only need media and share directories to be persisted # TODO: this is overly broad; only need media and share directories to be persisted
@@ -114,7 +69,7 @@
the contents should be a subset of what's in ../media/datasets. the contents should be a subset of what's in ../media/datasets.
''; '';
# make sure large media is stored to the HDD # make sure large media is stored to the HDD
sane.persist.sys.byStore.ext = [ sane.persist.sys.ext = [
{ {
user = "colin"; user = "colin";
group = "users"; group = "users";

View File

@@ -27,7 +27,9 @@ in
# view refused packets with: `sudo journalctl -k` # view refused packets with: `sudo journalctl -k`
# networking.firewall.logRefusedPackets = true; # networking.firewall.logRefusedPackets = true;
# these useDHCP lines are legacy from the auto-generated config. might be safe to remove now? # The global useDHCP flag is deprecated, therefore explicitly set to false here.
# Per-interface useDHCP will be mandatory in the future, so this generated config
# replicates the default behaviour.
networking.useDHCP = false; networking.useDHCP = false;
networking.interfaces.eth0.useDHCP = true; networking.interfaces.eth0.useDHCP = true;
# XXX colin: probably don't need this. wlan0 won't be populated unless i touch a value in networking.interfaces.wlan0 # XXX colin: probably don't need this. wlan0 won't be populated unless i touch a value in networking.interfaces.wlan0
@@ -42,6 +44,41 @@ in
# "9.9.9.9" # "9.9.9.9"
# ]; # ];
# use systemd's stub resolver.
# /etc/resolv.conf isn't sophisticated enough to use different servers per net namespace (or link).
# instead, running the stub resolver on a known address in the root ns lets us rewrite packets
# in the ovnps namespace to use the provider's DNS resolvers.
# a weakness is we can only query 1 NS at a time (unless we were to clone the packets?)
# there also seems to be some cache somewhere that's shared between the two namespaces.
# i think this is a libc thing. might need to leverage proper cgroups to _really_ kill it.
# - getent ahostsv4 www.google.com
# - try fix: <https://serverfault.com/questions/765989/connect-to-3rd-party-vpn-server-but-dont-use-it-as-the-default-route/766290#766290>
services.resolved.enable = true;
# without DNSSEC:
# - dig matrix.org => works
# - curl https://matrix.org => works
# with default DNSSEC:
# - dig matrix.org => works
# - curl https://matrix.org => fails
# i don't know why. this might somehow be interfering with the DNS run on this device (trust-dns)
services.resolved.dnssec = "false";
networking.nameservers = [
# use systemd-resolved resolver
# full resolver (which understands /etc/hosts) lives on 127.0.0.53
# stub resolver (just forwards upstream) lives on 127.0.0.54
"127.0.0.53"
];
# nscd -- the Name Service Caching Daemon -- caches DNS query responses
# in a way that's unaware of my VPN routing, so routes are frequently poor against
# services which advertise different IPs based on geolocation.
# nscd claims to be usable without a cache, but in practice i can't get it to not cache!
# nsncd is the Name Service NON-Caching Daemon. it's a drop-in that doesn't cache;
# this is OK on the host -- because systemd-resolved caches. it's probably sub-optimal
# in the netns and we query upstream DNS more often than needed. hm.
# TODO: run a separate recursive resolver in each namespace.
services.nscd.enableNsncd = true;
# services.resolved.extraConfig = '' # services.resolved.extraConfig = ''
# # docs: `man resolved.conf` # # docs: `man resolved.conf`
# # DNS servers to use via the `wg-ovpns` interface. # # DNS servers to use via the `wg-ovpns` interface.

View File

@@ -1,84 +0,0 @@
# as of 2023/12/02: complete blockchain is 530 GiB (on-disk size may be larger)
#
# ports:
# - 8333: for node-to-node communications
# - 8332: rpc (client-to-node)
#
# rpc setup:
# - generate a password
# - use: <https://github.com/bitcoin/bitcoin/blob/master/share/rpcauth/rpcauth.py>
# (rpcauth.py is not included in the `'.#bitcoin'` package result)
# - `wget https://raw.githubusercontent.com/bitcoin/bitcoin/master/share/rpcauth/rpcauth.py`
# - `python ./rpcauth.py colin`
# - copy the hash here. it's SHA-256, so safe to be public.
# - add "rpcuser=colin" and "rpcpassword=<output>" to secrets/servo/bitcoin.conf (i.e. ~/.bitcoin/bitcoin.conf)
# - bitcoin.conf docs: <https://github.com/bitcoin/bitcoin/blob/master/doc/bitcoin-conf.md>
# - validate with `bitcoin-cli -netinfo`
{ config, lib, pkgs, sane-lib, ... }:
let
# wrapper to run bitcoind with the tor onion address as externalip (computed at runtime)
_bitcoindWithExternalIp = with pkgs; writeShellScriptBin "bitcoind" ''
externalip="$(cat /var/lib/tor/onion/bitcoind/hostname)"
exec ${bitcoind}/bin/bitcoind "-externalip=$externalip" "$@"
'';
# the package i provide to services.bitcoind ends up on system PATH, and used by other tools like clightning.
# therefore, even though services.bitcoind only needs `bitcoind` binary, provide all the other bitcoin-related binaries (notably `bitcoin-cli`) as well:
bitcoindWithExternalIp = with pkgs; symlinkJoin {
name = "bitcoind-with-external-ip";
paths = [ _bitcoindWithExternalIp bitcoind ];
};
in
{
sane.persist.sys.byStore.ext = [
# /var/lib/monero/lmdb is what consumes most of the space
{ user = "bitcoind-mainnet"; group = "bitcoind-mainnet"; path = "/var/lib/bitcoind-mainnet"; }
];
# sane.ports.ports."8333" = {
# # this allows other nodes and clients to download blocks from me.
# protocol = [ "tcp" ];
# visibleTo.wan = true;
# description = "colin-bitcoin";
# };
services.tor.relay.onionServices.bitcoind = {
version = 3;
map = [{
# by default tor will route public tor port P to 127.0.0.1:P.
# so if this port is the same as clightning would natively use, then no further config is needed here.
# see: <https://2019.www.torproject.org/docs/tor-manual.html.en#HiddenServicePort>
port = 8333;
# target.port; target.addr; #< set if tor port != clightning port
}];
# allow "tor" group (i.e. bitcoind-mainnet) to read /var/lib/tor/onion/bitcoind/hostname
settings.HiddenServiceDirGroupReadable = true;
};
services.bitcoind.mainnet = {
enable = true;
package = bitcoindWithExternalIp;
rpc.users.colin = {
# see docs at top of file for how to generate this
passwordHMAC = "30002c05d82daa210550e17a182db3f3$6071444151281e1aa8a2729f75e3e2d224e9d7cac3974810dab60e7c28ffaae4";
};
extraConfig = ''
# don't load the wallet, and disable wallet RPC calls
disablewallet=1
# proxy all outbound traffic through Tor
proxy=127.0.0.1:9050
'';
};
users.users.bitcoind-mainnet.extraGroups = [ "tor" ];
systemd.services.bitcoind-mainnet.serviceConfig.RestartSec = "30s"; #< default is 0
sane.users.colin.fs.".bitcoin/bitcoin.conf" = sane-lib.fs.wantedSymlinkTo config.sops.secrets."bitcoin.conf".path;
sops.secrets."bitcoin.conf" = {
mode = "0600";
owner = "colin";
group = "users";
};
sane.programs.bitcoind.enableFor.user.colin = true; # for debugging/administration: `bitcoin-cli`
}

View File

@@ -1,766 +0,0 @@
#!/usr/bin/env nix-shell
#!nix-shell -i python3 -p "python3.withPackages (ps: [ ps.pyln-client ])"
# pyln-client docs: <https://github.com/ElementsProject/lightning/tree/master/contrib/pyln-client>
# terminology:
# - "scid": "Short Channel ID", e.g. 123456x7890x0
# from this id, we can locate the actual channel, its peers, and its parameters
import argparse
import logging
import math
import sys
import time
from concurrent.futures import ThreadPoolExecutor
from dataclasses import dataclass
from enum import Enum
from pyln.client import LightningRpc, Millisatoshi, RpcError
logger = logging.getLogger(__name__)
RPC_FILE = "/var/lib/clightning/bitcoin/lightning-rpc"
# CLTV (HLTC delta) of the final hop
# set this too low and you might get inadvertent channel closures (?)
CLTV = 18
# for every sequentally failed transaction, delay this much before trying again.
# note that the initial route building process can involve 10-20 "transient" failures, as it discovers dead channels.
TX_FAIL_BACKOFF = 0.8
MAX_SEQUENTIAL_JOB_FAILURES = 200
class LoopError(Enum):
""" error when trying to loop sats, or when unable to calculate a route for the loop """
TRANSIENT = "TRANSIENT" # try again, we'll maybe find a different route
NO_ROUTE = "NO_ROUTE"
class RouteError(Enum):
""" error when calculated a route """
HAS_BASE_FEE = "HAS_BASE_FEE"
NO_ROUTE = "NO_ROUTE"
class Metrics:
looped_msat: int = 0
sendpay_fail: int = 0
sendpay_succeed: int = 0
own_bad_channel: int = 0
no_route: int = 0
in_ch_unsatisfiable: int = 0
def __repr__(self) -> str:
return f"looped:{self.looped_msat}, tx:{self.sendpay_succeed}, tx_fail:{self.sendpay_fail}, own_bad_ch:{self.own_bad_channel}, no_route:{self.no_route}, in_ch_restricted:{self.in_ch_unsatisfiable}"
@dataclass
class TxBounds:
max_msat: int
min_msat: int = 0
def __repr__(self) -> str:
return f"TxBounds({self.min_msat} <= msat <= {self.max_msat})"
def is_satisfiable(self) -> bool:
return self.min_msat <= self.max_msat
def raise_max_to_be_satisfiable(self) -> "Self":
if self.max_msat < self.min_msat:
logger.debug(f"raising max_msat to be consistent: {self.max_msat} -> {self.min_msat}")
return TxBounds(self.min_msat, self.min_msat)
return TxBounds(min_msat=self.min_msat, max_msat=self.max_msat)
def intersect(self, other: "TxBounds") -> "Self":
return TxBounds(
min_msat=max(self.min_msat, other.min_msat),
max_msat=min(self.max_msat, other.max_msat),
)
def restrict_to_htlc(self, ch: "LocalChannel", why: str = "") -> "Self":
"""
apply min/max HTLC size restrictions of the given channel.
"""
if ch:
why = why or ch.directed_scid_to_me
if why: why = f"{why}: "
new_min, new_max = self.min_msat, self.max_msat
if ch.htlc_minimum_to_me > self.min_msat:
new_min = ch.htlc_minimum_to_me
logger.debug(f"{why}raising min_msat due to HTLC requirements: {self.min_msat} -> {new_min}")
if ch.htlc_maximum_to_me < self.max_msat:
new_max = ch.htlc_maximum_to_me
logger.debug(f"{why}lowering max_msat due to HTLC requirements: {self.max_msat} -> {new_max}")
return TxBounds(min_msat=new_min, max_msat=new_max)
def restrict_to_zero_fees(self, ch: "LocalChannel"=None, base: int=0, ppm: int=0, why:str = "") -> "Self":
"""
restrict tx size such that PPM fees are zero.
if the channel has a base fee, then `max_msat` is forced to 0.
"""
if ch:
why = why or ch.directed_scid_to_me
self = self.restrict_to_zero_fees(base=ch.to_me["base_fee_millisatoshi"], ppm=ch.to_me["fee_per_millionth"], why=why)
if why: why = f"{why}: "
new_max = self.max_msat
ppm_max = math.ceil(1000000 / ppm) - 1 if ppm != 0 else new_max
if ppm_max < new_max:
logger.debug(f"{why}decreasing max_msat due to fee ppm: {new_max} -> {ppm_max}")
new_max = ppm_max
if base != 0:
logger.debug(f"{why}free route impossible: channel has base fees")
new_max = 0
return TxBounds(min_msat=self.min_msat, max_msat=new_max)
class LocalChannel:
def __init__(self, channels: list, rpc: "RpcHelper"):
assert 0 < len(channels) <= 2, f"unexpected: channel count: {channels}"
out = None
in_ = None
for c in channels:
if c["source"] == rpc.self_id:
assert out is None, f"unexpected: multiple channels from self: {channels}"
out = c
if c["destination"] == rpc.self_id:
assert in_ is None, f"unexpected: multiple channels to self: {channels}"
in_ = c
# assert out is not None, f"no channel from self: {channels}"
# assert in_ is not None, f"no channel to self: {channels}"
if out and in_:
assert out["destination"] == in_["source"], f"channel peers are asymmetric?! {channels}"
assert out["short_channel_id"] == in_["short_channel_id"], f"channel ids differ?! {channels}"
self.from_me = out
self.to_me = in_
self.remote_node = rpc.node(self.remote_peer)
self.peer_ch = rpc.peerchannel(self.scid, self.remote_peer)
self.forwards_from_me = rpc.rpc.listforwards(out_channel=self.scid, status="settled")["forwards"]
def __repr__(self) -> str:
return self.to_str(with_scid=True, with_bal_ratio=True, with_cost=False, with_ppm_theirs=False)
def to_str(
self,
with_peer_id:bool = False,
with_scid:bool = False,
with_bal_msat:bool = False,
with_bal_ratio:bool = False,
with_cost:bool = False,
with_ppm_theirs:bool = False,
with_ppm_mine:bool = False,
with_profits:bool = True,
with_payments:bool = False,
) -> str:
base_flag = "*" if not self.online or self.base_fee_to_me != 0 else ""
alias = f"({self.remote_alias}){base_flag}"
peerid = f" {self.remote_peer}" if with_peer_id else ""
scid = f" scid:{self.scid:>13}" if with_scid else ""
bal = f" S:{int(self.sendable):11}/R:{int(self.receivable):11}" if with_bal_msat else ""
ratio = f" MINE:{(100*self.send_ratio):>8.4f}%" if with_bal_ratio else ""
payments = f" OUT:{int(self.out_fulfilled_msat):>11}/IN:{int(self.in_fulfilled_msat):>11}" if with_payments else ""
profits = f" P$:{int(self.fees_lifetime_mine):>8}" if with_profits else ""
cost = f" COST:{self.opportunity_cost_lent:>8}" if with_cost else ""
ppm_theirs = self.ppm_to_me if self.to_me else "N/A"
ppm_theirs = f" PPM_THEIRS:{ppm_theirs:>6}" if with_ppm_theirs else ""
ppm_mine = self.ppm_from_me if self.from_me else "N/A"
ppm_mine = f" PPM_MINE:{ppm_mine:>6}" if with_ppm_mine else ""
return f"channel{alias:30}{peerid}{scid}{bal}{ratio}{payments}{profits}{cost}{ppm_theirs}{ppm_mine}"
@property
def online(self) -> bool:
return self.from_me and self.to_me
@property
def remote_peer(self) -> str:
if self.from_me:
return self.from_me["destination"]
else:
return self.to_me["source"]
@property
def remote_alias(self) -> str:
return self.remote_node["alias"]
@property
def scid(self) -> str:
if self.from_me:
return self.from_me["short_channel_id"]
else:
return self.to_me["short_channel_id"]
@property
def htlc_minimum_to_me(self) -> Millisatoshi:
return self.to_me["htlc_minimum_msat"]
@property
def htlc_minimum_from_me(self) -> Millisatoshi:
return self.from_me["htlc_minimum_msat"]
@property
def htlc_minimum(self) -> Millisatoshi:
return max(self.htlc_minimum_to_me, self.htlc_minimum_from_me)
@property
def htlc_maximum_to_me(self) -> Millisatoshi:
return self.to_me["htlc_maximum_msat"]
@property
def htlc_maximum_from_me(self) -> Millisatoshi:
return self.from_me["htlc_maximum_msat"]
@property
def htlc_maximum(self) -> Millisatoshi:
return min(self.htlc_maximum_to_me, self.htlc_maximum_from_me)
@property
def direction_to_me(self) -> int:
return self.to_me["direction"]
@property
def direction_from_me(self) -> int:
return self.from_me["direction"]
@property
def directed_scid_to_me(self) -> str:
return f"{self.scid}/{self.direction_to_me}"
@property
def directed_scid_from_me(self) -> str:
return f"{self.scid}/{self.direction_from_me}"
@property
def delay_them(self) -> str:
return self.to_me["delay"]
@property
def delay_me(self) -> str:
return self.from_me["delay"]
@property
def ppm_to_me(self) -> int:
return self.to_me["fee_per_millionth"]
@property
def ppm_from_me(self) -> int:
return self.from_me["fee_per_millionth"]
# return self.peer_ch["fee_proportional_millionths"]
@property
def base_fee_to_me(self) -> int:
return self.to_me["base_fee_millisatoshi"]
@property
def receivable(self) -> int:
return self.peer_ch["receivable_msat"]
@property
def sendable(self) -> int:
return self.peer_ch["spendable_msat"]
@property
def in_fulfilled_msat(self) -> Millisatoshi:
return self.peer_ch["in_fulfilled_msat"]
@property
def out_fulfilled_msat(self) -> Millisatoshi:
return self.peer_ch["out_fulfilled_msat"]
@property
def fees_lifetime_mine(self) -> Millisatoshi:
return sum(fwd["fee_msat"] for fwd in self.forwards_from_me)
@property
def send_ratio(self) -> float:
cap = self.receivable + self.sendable
return self.sendable / cap
@property
def opportunity_cost_lent(self) -> int:
""" how much msat did we gain by pushing their channel to its current balance? """
return int(self.receivable * self.ppm_from_me / 1000000)
class RpcHelper:
def __init__(self, rpc: LightningRpc):
self.rpc = rpc
self.self_id = rpc.getinfo()["id"]
def localchannel(self, scid: str) -> LocalChannel:
listchan = self.rpc.listchannels(scid)
# this assertion would probably indicate a typo in the scid
assert listchan and listchan.get("channels", []) != [], f"bad listchannels for {scid}: {listchan}"
return LocalChannel(listchan["channels"], self)
def node(self, id: str) -> dict:
nodes = self.rpc.listnodes(id)["nodes"]
assert len(nodes) == 1, f"unexpected: multiple nodes for {id}: {nodes}"
return nodes[0]
def peerchannel(self, scid: str, peer_id: str) -> dict:
peerchannels = self.rpc.listpeerchannels(peer_id)["channels"]
channels = [c for c in peerchannels if c["short_channel_id"] == scid]
assert len(channels) == 1, f"expected exactly 1 channel, got: {channels}"
return channels[0]
def try_getroute(self, *args, **kwargs) -> dict | None:
""" wrapper for getroute which returns None instead of error if no route exists """
try:
route = self.rpc.getroute(*args, **kwargs)
except RpcError as e:
logger.debug(f"rpc failed: {e}")
return None
else:
route = route["route"]
if route == []: return None
return route
class LoopRouter:
def __init__(self, rpc: RpcHelper, metrics: Metrics = None):
self.rpc = rpc
self.metrics = metrics or Metrics()
self.bad_channels = [] # list of directed scid
self.nonzero_base_channels = [] # list of directed scid
def drop_caches(self) -> None:
logger.info("LoopRouter.drop_caches()")
self.bad_channels = []
def _get_directed_scid(self, scid: str, direction: int) -> dict:
channels = self.rpc.rpc.listchannels(scid)["channels"]
channels = [c for c in channels if c["direction"] == direction]
assert len(channels) == 1, f"expected exactly 1 channel: {channels}"
return channels[0]
def loop_once(self, out_scid: str, in_scid: str, bounds: TxBounds) -> LoopError|int:
out_ch = self.rpc.localchannel(out_scid)
in_ch = self.rpc.localchannel(in_scid)
if out_ch.directed_scid_from_me in self.bad_channels or in_ch.directed_scid_to_me in self.bad_channels:
logger.info(f"loop {out_scid} -> {in_scid} failed in our own channel")
self.metrics.own_bad_channel += 1
return LoopError.TRANSIENT
# bounds = bounds.restrict_to_htlc(out_ch) # htlc bounds seem to be enforced only in the outward direction
bounds = bounds.restrict_to_htlc(in_ch)
bounds = bounds.restrict_to_zero_fees(in_ch)
if not bounds.is_satisfiable():
self.metrics.in_ch_unsatisfiable += 1
return LoopError.NO_ROUTE
logger.debug(f"route with bounds {bounds}")
route = self.route(out_ch, in_ch, bounds)
logger.debug(f"route: {route}")
if route == RouteError.NO_ROUTE:
self.metrics.no_route += 1
return LoopError.NO_ROUTE
elif route == RouteError.HAS_BASE_FEE:
# try again with a different route
return LoopError.TRANSIENT
amount_msat = route[0]["amount_msat"]
invoice_id = f"loop-{time.time():.6f}".replace(".", "_")
invoice_desc = f"bal {out_scid}:{in_scid}"
invoice = self.rpc.rpc.invoice("any", invoice_id, invoice_desc)
logger.debug(f"invoice: {invoice}")
payment = self.rpc.rpc.sendpay(route, invoice["payment_hash"], invoice_id, amount_msat, invoice["bolt11"], invoice["payment_secret"])
logger.debug(f"sent: {payment}")
try:
wait = self.rpc.rpc.waitsendpay(invoice["payment_hash"])
logger.debug(f"result: {wait}")
except RpcError as e:
self.metrics.sendpay_fail += 1
err_data = e.error["data"]
err_scid, err_dir = err_data["erring_channel"], err_data["erring_direction"]
err_directed_scid = f"{err_scid}/{err_dir}"
logger.debug(f"ch failed, adding to excludes: {err_directed_scid}; {e.error}")
self.bad_channels.append(err_directed_scid)
return LoopError.TRANSIENT
else:
self.metrics.sendpay_succeed += 1
self.metrics.looped_msat += int(amount_msat)
return int(amount_msat)
def route(self, out_ch: LocalChannel, in_ch: LocalChannel, bounds: TxBounds) -> list[dict] | RouteError:
exclude = [
# ensure the payment doesn't cross either channel in reverse.
# note that this doesn't preclude it from taking additional trips through self, with other peers.
# out_ch.directed_scid_to_me,
# in_ch.directed_scid_from_me,
# alternatively, never route through self. this avoids a class of logic error, like what to do with fees i charge "myself".
self.rpc.self_id
] + self.bad_channels + self.nonzero_base_channels
out_peer = out_ch.remote_peer
in_peer = in_ch.remote_peer
route_or_bounds = bounds
while isinstance(route_or_bounds, TxBounds):
old_bounds = route_or_bounds
route_or_bounds = self._find_partial_route(out_peer, in_peer, old_bounds, exclude=exclude)
if route_or_bounds == old_bounds:
return RouteError.NO_ROUTE
if isinstance(route_or_bounds, RouteError):
return route_or_bounds
route = self._add_route_endpoints(route_or_bounds, out_ch, in_ch)
return route
def _find_partial_route(self, out_peer: str, in_peer: str, bounds: TxBounds, exclude: list[str]=[]) -> list[dict] | RouteError | TxBounds:
route = self.rpc.try_getroute(in_peer, amount_msat=bounds.max_msat, riskfactor=0, fromid=out_peer, exclude=exclude, cltv=CLTV)
if route is None:
logger.debug(f"no route for {bounds.max_msat}msat {out_peer} -> {in_peer}")
return RouteError.NO_ROUTE
send_msat = route[0]["amount_msat"]
if send_msat != Millisatoshi(bounds.max_msat):
logger.debug(f"found route with non-zero fee: {send_msat} -> {bounds.max_msat}. {route}")
error = None
for hop in route:
hop_scid = hop["channel"]
hop_dir = hop["direction"]
directed_scid = f"{hop_scid}/{hop_dir}"
ch = self._get_directed_scid(hop_scid, hop_dir)
if ch["base_fee_millisatoshi"] != 0:
self.nonzero_base_channels.append(directed_scid)
error = RouteError.HAS_BASE_FEE
bounds = bounds.restrict_to_zero_fees(ppm=ch["fee_per_millionth"], why=directed_scid)
return bounds.raise_max_to_be_satisfiable() if error is None else error
return route
def _add_route_endpoints(self, route, out_ch: LocalChannel, in_ch: LocalChannel):
inbound_hop = dict(
id=self.rpc.self_id,
channel=in_ch.scid,
direction=in_ch.direction_to_me,
amount_msat=route[-1]["amount_msat"],
delay=route[-1]["delay"],
style="tlv",
)
route = self._add_route_delay(route, in_ch.delay_them) + [ inbound_hop ]
outbound_hop = dict(
id=out_ch.remote_peer,
channel=out_ch.scid,
direction=out_ch.direction_from_me,
amount_msat=route[0]["amount_msat"],
delay=route[0]["delay"] + out_ch.delay_them,
style="tlv",
)
route = [ outbound_hop ] + route
return route
def _add_route_delay(self, route: list[dict], delay: int) -> list[dict]:
return [ dict(hop, delay=hop["delay"] + delay) for hop in route ]
@dataclass
class LoopJob:
out: str # scid
in_: str # scid
amount: int
@dataclass
class LoopJobIdle:
sec: int = 10
class LoopJobDone(Enum):
COMPLETED = "COMPLETED"
ABORTED = "ABORTED"
class AbstractLoopRunner:
def __init__(self, looper: LoopRouter, bounds: TxBounds, parallelism: int):
self.looper = looper
self.bounds = bounds
self.parallelism = parallelism
self.bounds_map = {} # map (out:str, in_:str) -> TxBounds. it's a cache so we don't have to try 10 routes every time.
def pop_job(self) -> LoopJob | LoopJobIdle | LoopJobDone:
raise NotImplemented # abstract method
def finished_job(self, job: LoopJob, progress: int|LoopError) -> None:
raise NotImplemented # abstract method
def run_to_completion(self, exit_on_any_completed:bool = False) -> None:
self.exiting = False
self.exit_on_any_completed = exit_on_any_completed
if self.parallelism == 1:
# run inline to aid debugging
self._worker_thread()
else:
with ThreadPoolExecutor(max_workers=self.parallelism) as executor:
_ = list(executor.map(lambda _i: self._try_invoke(self._worker_thread), range(self.parallelism)))
def drop_caches(self) -> None:
logger.info("AbstractLoopRunner.drop_caches()")
self.looper.drop_caches()
self.bounds_map = {}
def _try_invoke(self, f, *args) -> None:
"""
try to invoke `f` with the provided `args`, and log if it fails.
this overcomes the issue that background tasks which fail via Exception otherwise do so silently.
"""
try:
f(*args)
except Exception as e:
logger.error(f"task failed: {e}")
def _worker_thread(self) -> None:
while not self.exiting:
job = self.pop_job()
logger.debug(f"popped job: {job}")
if isinstance(job, LoopJobDone):
return self._worker_finished(job)
if isinstance(job, LoopJobIdle):
logger.debug(f"idling for {job.sec}")
time.sleep(job.sec)
continue
result = self._execute_job(job)
logger.debug(f"finishing job {job} with {result}")
self.finished_job(job, result)
def _execute_job(self, job: LoopJob) -> LoopError|int:
bounds = self.bounds_map.get((job.out, job.in_), self.bounds)
bounds = bounds.intersect(TxBounds(max_msat=job.amount))
if not bounds.is_satisfiable():
logger.debug(f"TxBounds for job are unsatisfiable; skipping: {bounds} {job}")
return LoopError.NO_ROUTE
amt_looped = self.looper.loop_once(job.out, job.in_, bounds)
if amt_looped in (0, LoopError.NO_ROUTE, LoopError.TRANSIENT):
return amt_looped
logger.info(f"looped {amt_looped} from {job.out} -> {job.in_}")
bounds = bounds.intersect(TxBounds(max_msat=amt_looped))
self.bounds_map[(job.out, job.in_)] = bounds
return amt_looped
def _worker_finished(self, job: LoopJobDone) -> None:
if job == LoopJobDone.COMPLETED and self.exit_on_any_completed:
logger.debug(f"worker completed -> exiting pool")
self.exiting = True
class LoopPairState:
# TODO: use this in MultiLoopBalancer, or stop shoving state in here and put it on LoopBalancer instead.
def __init__(self, out: str, in_: str, amount: int):
self.out = out
self.in_ = in_
self.amount_target = amount
self.amount_looped = 0
self.amount_outstanding = 0
self.tx_fail_count = 0
self.route_fail_count = 0
self.last_job_start_time = None
self.failed_tx_throttler = 0 # increase by one every time we fail, decreases more gradually, when we succeed
class LoopBalancer(AbstractLoopRunner):
def __init__(self, out: str, in_: str, amount: int, looper: LoopRouter, bounds: TxBounds, parallelism: int=1):
super().__init__(looper, bounds, parallelism)
self.state = LoopPairState(out, in_, amount)
def pop_job(self) -> LoopJob | LoopJobIdle | LoopJobDone:
if self.state.tx_fail_count + 10*self.state.route_fail_count >= MAX_SEQUENTIAL_JOB_FAILURES:
logger.info(f"giving up ({self.state.out} -> {self.state.in_}): {self.state.tx_fail_count} tx failures, {self.state.route_fail_count} route failures")
return LoopJobDone.ABORTED
if self.state.tx_fail_count + self.state.route_fail_count > 0:
# N.B.: last_job_start_time is guaranteed to have been set by now
idle_until = self.state.last_job_start_time + TX_FAIL_BACKOFF*self.state.failed_tx_throttler
idle_for = idle_until - time.time()
if self.state.amount_outstanding != 0 or idle_for > 0:
# when we hit transient failures, restrict to just one job in flight at a time.
# this is aimed for the initial route building, where multiple jobs in flight is just useless,
# but it's not a bad idea for network blips, etc, either.
logger.info(f"throttling ({self.state.out} -> {self.state.in_}) for {idle_for:.0f}: {self.state.tx_fail_count} tx failures, {self.state.route_fail_count} route failures")
return LoopJobIdle(idle_for) if idle_for > 0 else LoopJobIdle()
amount_avail = self.state.amount_target - self.state.amount_looped - self.state.amount_outstanding
if amount_avail < self.bounds.min_msat:
if self.state.amount_outstanding == 0: return LoopJobDone.COMPLETED
return LoopJobIdle() # sending out another job would risk over-transferring
amount_this_job = min(amount_avail, self.bounds.max_msat)
self.state.amount_outstanding += amount_this_job
self.state.last_job_start_time = time.time()
return LoopJob(out=self.state.out, in_=self.state.in_, amount=amount_this_job)
def finished_job(self, job: LoopJob, progress: int) -> None:
self.state.amount_outstanding -= job.amount
if progress == LoopError.NO_ROUTE:
self.state.route_fail_count += 1
self.state.failed_tx_throttler += 10
elif progress == LoopError.TRANSIENT:
self.state.tx_fail_count += 1
self.state.failed_tx_throttler += 1
else:
self.state.amount_looped += progress
self.state.tx_fail_count = 0
self.state.route_fail_count = 0
self.state.failed_tx_throttler = max(0, self.state.failed_tx_throttler - 0.2)
logger.info(f"loop progressed ({job.out} -> {job.in_}) {progress}: {self.state.amount_looped} of {self.state.amount_target}")
class MultiLoopBalancer(AbstractLoopRunner):
"""
multiplexes jobs between multiple LoopBalancers.
note that the child LoopBalancers don't actually execute the jobs -- just produce them.
"""
def __init__(self, looper: LoopRouter, bounds: TxBounds, parallelism: int=1):
super().__init__(looper, bounds, parallelism)
self.loops = []
# job_index: increments on every job so we can grab jobs evenly from each LoopBalancer.
# in that event that producers are idling, it can actually increment more than once,
# so don't take this too literally
self.job_index = 0
def add_loop(self, out: LocalChannel, in_: LocalChannel, amount: int) -> None:
"""
start looping sats from out -> in_
"""
assert not any(l.state.out == out.scid and l.state.in_ == in_.scid for l in self.loops), f"tried to add duplicate loops from {out} -> {in_}"
logger.info(f"looping from ({out}) to ({in_})")
self.loops.append(LoopBalancer(out.scid, in_.scid, amount, self.looper, self.bounds, self.parallelism))
def pop_job(self) -> LoopJob | LoopJobIdle | LoopJobDone:
# N.B.: this can be called in parallel, so try to be consistent enough to not crash
idle_job = None
abort_job = None
for i, _ in enumerate(self.loops):
loop = self.loops[(self.job_index + i) % len(self.loops)]
self.job_index += 1
job = loop.pop_job()
if isinstance(job, LoopJob):
return job
if isinstance(job, LoopJobIdle):
idle_job = LoopJobIdle(min(job.sec, idle_job.sec)) if idle_job is not None else job
if job == LoopJobDone.ABORTED:
abort_job = job
# either there's a task to idle, or we have to terminate.
# if terminating, terminate ABORTED if any job aborted, else COMPLETED
if idle_job is not None: return idle_job
if abort_job is not None: return abort_job
return LoopJobDone.COMPLETED
def finished_job(self, job: LoopJob, progress: int) -> None:
# this assumes (enforced externally) that we have only one loop for a given out/in_ pair
for l in self.loops:
if l.state.out == job.out and l.state.in_ == job.in_:
l.finished_job(job, progress)
logger.info(f"total: {self.looper.metrics}")
def balance_loop(rpc: RpcHelper, out: str, in_: str, amount_msat: int, min_msat: int, max_msat: int, parallelism: int):
looper = LoopRouter(rpc)
bounds = TxBounds(min_msat=min_msat, max_msat=max_msat)
balancer = LoopBalancer(out, in_, amount_msat, looper, bounds, parallelism)
balancer.run_to_completion()
def autobalance_once(rpc: RpcHelper, metrics: Metrics, bounds: TxBounds, parallelism: int) -> bool:
"""
autobalances all channels.
returns True if channels are balanced (or as balanced as can be); False if in need of further balancing
"""
looper = LoopRouter(rpc, metrics)
balancer = MultiLoopBalancer(looper, bounds, parallelism)
channels = []
for peerch in rpc.rpc.listpeerchannels()["channels"]:
try:
channels.append(rpc.localchannel(peerch["short_channel_id"]))
except:
logger.info(f"NO CHANNELS for {peerch['peer_id']}")
channels = [ch for ch in channels if ch.online and ch.base_fee_to_me == 0]
give_to = [ ch for ch in channels if ch.send_ratio > 0.95 ]
take_from = [ ch for ch in channels if ch.send_ratio < 0.20 ]
if give_to == [] and take_from == []:
return True
for to in give_to:
for from_ in take_from:
balancer.add_loop(to, from_, 10000000)
balancer.run_to_completion(exit_on_any_completed=True)
return False
def autobalance(rpc: RpcHelper, min_msat: int, max_msat: int, parallelism: int):
bounds = TxBounds(min_msat=min_msat, max_msat=max_msat)
metrics = Metrics()
while not autobalance_once(rpc, metrics, bounds, parallelism):
pass
def show_status(rpc: RpcHelper, full: bool=False):
"""
show a table of channel balances between peers.
"""
for peerch in rpc.rpc.listpeerchannels()["channels"]:
try:
ch = rpc.localchannel(peerch["short_channel_id"])
except:
print(f"{peerch['peer_id']} scid:{peerch['short_channel_id']} state:{peerch['state']} NO CHANNELS")
else:
print(ch.to_str(with_scid=True, with_bal_ratio=True, with_payments=True, with_cost=full, with_ppm_theirs=True, with_ppm_mine=True, with_peer_id=full))
def main():
logging.basicConfig()
logger.setLevel(logging.INFO)
parser = argparse.ArgumentParser(description="rebalance lightning channel balances")
parser.add_argument("--verbose", action="store_true", help="more logging")
parser.add_argument("--min-msat", default="999", help="min transaction size")
parser.add_argument("--max-msat", default="1000000", help="max transaction size")
parser.add_argument("--jobs", default="1", help="how many HTLCs to keep in-flight at once")
subparsers = parser.add_subparsers(help="action")
status_parser = subparsers.add_parser("status")
status_parser.set_defaults(action="status")
status_parser.add_argument("--full", action="store_true", help="more info per channel")
loop_parser = subparsers.add_parser("loop")
loop_parser.set_defaults(action="loop")
loop_parser.add_argument("out", help="peer id to send tx through")
loop_parser.add_argument("in_", help="peer id to receive tx through")
loop_parser.add_argument("amount", help="total amount of msat to loop")
autobal_parser = subparsers.add_parser("autobalance")
autobal_parser.set_defaults(action="autobalance")
args = parser.parse_args()
if args.verbose:
logger.setLevel(logging.DEBUG)
rpc = RpcHelper(LightningRpc(RPC_FILE))
if args.action == "status":
show_status(rpc, full=args.full)
if args.action == "loop":
balance_loop(rpc, out=args.out, in_=args.in_, amount_msat=int(args.amount), min_msat=int(args.min_msat), max_msat=int(args.max_msat), parallelism=int(args.jobs))
if args.action == "autobalance":
autobalance(rpc, min_msat=int(args.min_msat), max_msat=int(args.max_msat), parallelism=int(args.jobs))
if __name__ == '__main__':
main()

View File

@@ -1,135 +0,0 @@
# clightning is an implementation of Bitcoin's Lightning Network.
# as such, this assumes that `services.bitcoin` is enabled.
# docs:
# - tor clightning config: <https://docs.corelightning.org/docs/tor>
# - `lightning-cli` and subcommands: <https://docs.corelightning.org/reference/lightning-cli>
# - `man lightningd-config`
#
# management/setup/use:
# - guide: <https://github.com/ElementsProject/lightning>
#
# debugging:
# - `lightning-cli getlog debug`
# - `lightning-cli listpays` -> show payments this node sent
# - `lightning-cli listinvoices` -> show payments this node received
#
# first, acquire peers:
# - `lightning-cli connect id@host`
# where `id` is the node's pubkey, and `host` is perhaps an ip:port tuple, or a hash.onion:port tuple.
# for testing, choose any node listed on <https://1ml.com>
# - `lightning-cli listpeers`
# should show the new peer, with `connected: true`
#
# then, fund the clightning wallet
# - `lightning-cli newaddr`
#
# then, open channels
# - `lightning-cli connect ...`
# - `lightning-cli fundchannel <node_id> <amount_in_satoshis>`
#
# who to federate with?
# - a lot of the larger nodes allow hands-free channel creation
# - either inbound or outbound, sometimes paid
# - find nodes on:
# - <https://terminal.lightning.engineering/>
# - <https://1ml.com>
# - tor nodes: <https://1ml.com/node?order=capacity&iponionservice=true>
# - <https://lightningnetwork.plus>
# - <https://mempool.space/lightning>
# - <https://amboss.space>
# - a few tor-capable nodes which allow channel creation:
# - <https://c-otto.de/>
# - <https://cyberdyne.sh/>
# - <https://yalls.org/about/>
# - <https://coincept.com/>
# - more resources: <https://www.lopp.net/lightning-information.html>
# - node routability: https://hashxp.org/lightning/node/<id>
# - especially, acquire inbound liquidity via lightningnetwork.plus's swap feature
# - most of the opportunities are gated behind a minimum connection or capacity requirement
#
# tune payment parameters
# - `lightning-cli setchannel <id> [feebase] [feeppm] [htlcmin] [htlcmax] [enforcedelay] [ignorefeelimits]`
# - e.g. `lightning-cli setchannel all 0 10`
# - it's suggested that feebase=0 simplifies routing.
#
# teardown:
# - `lightning-cli withdraw <bc1... dest addr> <amount in satoshis> [feerate]`
#
# sanity:
# - `lightning-cli listfunds`
#
# to receive a payment (do as `clightning` user):
# - `lightning-cli invoice <amount in millisatoshi> <label> <description>`
# - specify amount as `any` if undetermined
# - then give the resulting bolt11 URI to the payer
# to send a payment:
# - `lightning-cli pay <bolt11 URI>`
# - or `lightning-cli pay <bolt11 URI> [amount_msat] [label] [riskfactor] [maxfeepercent] ...`
# - amount_msat must be "null" if the bolt11 URI specifies a value
# - riskfactor defaults to 10
# - maxfeepercent defaults to 0.5
# - label is a human-friendly label for my records
{ config, pkgs, ... }:
{
sane.persist.sys.byStore.ext = [
{ user = "clightning"; group = "clightning"; mode = "0710"; path = "/var/lib/clightning"; }
];
# `lightning-cli` finds its RPC file via `~/.lightning/bitcoin/lightning-rpc`, to message the daemon
sane.user.fs.".lightning".symlink.target = "/var/lib/clightning";
# see bitcoin.nix for how to generate this
services.bitcoind.mainnet.rpc.users.clightning.passwordHMAC =
"befcb82d9821049164db5217beb85439$2c31ac7db3124612e43893ae13b9527dbe464ab2d992e814602e7cb07dc28985";
sane.services.clightning.enable = true;
sane.services.clightning.proxy = "127.0.0.1:9050"; # proxy outgoing traffic through tor
# sane.services.clightning.publicAddress = "statictor:127.0.0.1:9051";
sane.services.clightning.getPublicAddressCmd = "cat /var/lib/tor/onion/clightning/hostname";
services.tor.relay.onionServices.clightning = {
version = 3;
map = [{
# by default tor will route public tor port P to 127.0.0.1:P.
# so if this port is the same as clightning would natively use, then no further config is needed here.
# see: <https://2019.www.torproject.org/docs/tor-manual.html.en#HiddenServicePort>
port = 9735;
# target.port; target.addr; #< set if tor port != clightning port
}];
# allow "tor" group (i.e. clightning) to read /var/lib/tor/onion/clightning/hostname
settings.HiddenServiceDirGroupReadable = true;
};
# must be in "tor" group to read /var/lib/tor/onion/*/hostname
users.users.clightning.extraGroups = [ "tor" ];
systemd.services.clightning.after = [ "tor.service" ];
# lightning-config contains fields from here:
# - <https://docs.corelightning.org/docs/configuration>
# secret config includes:
# - bitcoin-rpcpassword
# - alias=nodename
# - rgb=rrggbb
# - fee-base=<millisatoshi>
# - fee-per-satoshi=<ppm>
# - feature configs (i.e. experimental-xyz options)
sane.services.clightning.extraConfig = ''
log-level=debug:lightningd
# peerswap:
# - config example: <https://github.com/fort-nix/nix-bitcoin/pull/462/files#diff-b357d832705b8ce8df1f41934d613f79adb77c4cd5cd9e9eb12a163fca3e16c6>
# XXX: peerswap crashes clightning on launch. stacktrace is useless.
# plugin=${pkgs.peerswap}/bin/peerswap
# peerswap-db-path=/var/lib/clightning/peerswap/swaps
# peerswap-policy-path=...
'';
sane.services.clightning.extraConfigFiles = [ config.sops.secrets."lightning-config".path ];
sops.secrets."lightning-config" = {
mode = "0640";
owner = "clightning";
group = "clightning";
};
sane.programs.clightning.enableFor.user.colin = true; # for debugging/admin: `lightning-cli`
}

View File

@@ -1,10 +0,0 @@
{ ... }:
{
imports = [
./bitcoin.nix
./clightning.nix
./i2p.nix
./monero.nix
./tor.nix
];
}

View File

@@ -1,4 +0,0 @@
{ ... }:
{
services.i2p.enable = true;
}

View File

@@ -1,31 +0,0 @@
# as of 2023/11/26: complete downloaded blockchain should be 200GiB on disk, give or take.
{ ... }:
{
sane.persist.sys.byStore.ext = [
# /var/lib/monero/lmdb is what consumes most of the space
{ user = "monero"; group = "monero"; path = "/var/lib/monero"; }
];
services.monero.enable = true;
services.monero.limits.upload = 5000; # in kB/s
services.monero.extraConfig = ''
# see: monero doc/ANONYMITY_NETWORKS.md
#
# "If any anonymity network is enabled, transactions being broadcast that lack a valid 'context'
# (i.e. the transaction did not come from a P2P connection) will only be sent to peers on anonymity networks."
#
# i think this means that setting tx-proxy here ensures any transactions sent locally to my node (via RPC)
# will be sent over an anonymity network.
tx-proxy=i2p,127.0.0.1:9000
tx-proxy=tor,127.0.0.1:9050
'';
# monero ports: <https://monero.stackexchange.com/questions/604/what-ports-does-monero-use-rpc-p2p-etc>
# - 18080 = "P2P" monero node <-> monero node connections
# - 18081 = "RPC" monero client -> monero node connections
sane.ports.ports."18080" = {
protocol = [ "tcp" ];
visibleTo.wan = true;
description = "colin-monero-p2p";
};
}

View File

@@ -1,25 +0,0 @@
# tor settings: <https://2019.www.torproject.org/docs/tor-manual.html.en>
{ lib, ... }:
{
# tor hidden service hostnames aren't deterministic, so persist.
# might be able to get away with just persisting /var/lib/tor/onion, not sure.
sane.persist.sys.byStore.plaintext = [
{ user = "tor"; group = "tor"; mode = "0710"; path = "/var/lib/tor"; }
];
# tor: `tor.enable` doesn't start a relay, exit node, proxy, etc. it's minimal.
# tor.client.enable configures a torsocks proxy, accessible *only* to localhost.
# at 127.0.0.1:9050
services.tor.enable = true;
services.tor.client.enable = true;
# in order for services to read /var/lib/tor/onion/*/hostname, they must be able to traverse /var/lib/tor,
# and /var/lib/tor must have g+x.
# DataDirectoryGroupReadable causes tor to use g+rx, technically more than we need, but all the files are 600 so it's fine.
services.tor.settings.DataDirectoryGroupReadable = true;
# StateDirectoryMode defaults to 0700, and thereby prevents the onion hostnames from being group readable
systemd.services.tor.serviceConfig.StateDirectoryMode = lib.mkForce "0710";
users.users.tor.homeMode = "0710"; # home mode defaults to 0700, causing readability problems, enforced by nixos "users" activation script
services.tor.settings.SafeLogging = false; # show actual .onion names in the syslog, else debugging is impossible
}

View File

@@ -0,0 +1,27 @@
{ config, lib, pkgs, ... }:
# using manual ddns now
lib.mkIf false
{
systemd.services.ddns-afraid = {
description = "update dynamic DNS entries for freedns.afraid.org";
serviceConfig = {
EnvironmentFile = config.sops.secrets."ddns_afraid.env".path;
# TODO: ProtectSystem = "strict";
# TODO: ProtectHome = "full";
# TODO: PrivateTmp = true;
};
script = let
curl = "${pkgs.curl}/bin/curl -4";
in ''
${curl} "https://freedns.afraid.org/dynamic/update.php?$AFRAID_KEY"
'';
};
systemd.timers.ddns-afraid = {
wantedBy = [ "multi-user.target" ];
timerConfig = {
OnStartupSec = "2min";
OnUnitActiveSec = "10min";
};
};
}

View File

@@ -0,0 +1,30 @@
{ config, lib, pkgs, ... }:
# we use manual DDNS now
lib.mkIf false
{
systemd.services.ddns-he = {
description = "update dynamic DNS entries for HurricaneElectric";
serviceConfig = {
EnvironmentFile = config.sops.secrets."ddns_he.env".path;
# TODO: ProtectSystem = "strict";
# TODO: ProtectHome = "full";
# TODO: PrivateTmp = true;
};
# HE DDNS API is documented: https://dns.he.net/docs.html
script = let
crl = "${pkgs.curl}/bin/curl -4";
in ''
${crl} "https://he.uninsane.org:$HE_PASSPHRASE@dyn.dns.he.net/nic/update?hostname=he.uninsane.org"
${crl} "https://native.uninsane.org:$HE_PASSPHRASE@dyn.dns.he.net/nic/update?hostname=native.uninsane.org"
${crl} "https://uninsane.org:$HE_PASSPHRASE@dyn.dns.he.net/nic/update?hostname=uninsane.org"
'';
};
systemd.timers.ddns-he = {
wantedBy = [ "multi-user.target" ];
timerConfig = {
OnStartupSec = "2min";
OnUnitActiveSec = "10min";
};
};
}

View File

@@ -3,7 +3,8 @@
imports = [ imports = [
./calibre.nix ./calibre.nix
./coturn.nix ./coturn.nix
./cryptocurrencies ./ddns-afraid.nix
./ddns-he.nix
./email ./email
./ejabberd.nix ./ejabberd.nix
./freshrss.nix ./freshrss.nix
@@ -19,14 +20,12 @@
./matrix ./matrix
./navidrome.nix ./navidrome.nix
./nginx.nix ./nginx.nix
./nixos-prebuild.nix
./nixserve.nix ./nixserve.nix
./ntfy ./ntfy
./pict-rs.nix ./pict-rs.nix
./pleroma.nix ./pleroma.nix
./postgres.nix ./postgres.nix
./prosody ./prosody
./slskd.nix
./transmission.nix ./transmission.nix
./trust-dns.nix ./trust-dns.nix
./wikipedia.nix ./wikipedia.nix

View File

@@ -29,7 +29,7 @@
# - `sudo btrfs qgroup limit 20G /mnt/persist/ext/persist/var/export/playground` # - `sudo btrfs qgroup limit 20G /mnt/persist/ext/persist/var/export/playground`
# to query the quota/status: # to query the quota/status:
# - `sudo btrfs qgroup show -re /var/export/playground` # - `sudo btrfs qgroup show -re /var/export/playground`
sane.persist.sys.byStore.ext = [ sane.persist.sys.ext = [
{ user = "root"; group = "export"; mode = "0775"; path = "/var/export/playground"; } { user = "root"; group = "export"; mode = "0775"; path = "/var/export/playground"; }
]; ];

View File

@@ -172,15 +172,13 @@ in
users.users.sftpgo.extraGroups = [ "export" ]; users.users.sftpgo.extraGroups = [ "export" ];
systemd.services.sftpgo = { systemd.services.sftpgo.serviceConfig = {
ReadOnlyPaths = [ "/var/export" ];
ReadWritePaths = [ "/var/export/playground" ];
after = [ "network-online.target" ]; after = [ "network-online.target" ];
wants = [ "network-online.target" ]; wants = [ "network-online.target" ];
serviceConfig = {
ReadOnlyPaths = [ "/var/export" ];
ReadWritePaths = [ "/var/export/playground" ];
Restart = "always"; Restart = "always";
RestartSec = "20s"; RestartSec = "20s";
};
}; };
} }

View File

@@ -13,10 +13,6 @@
services.gitea.appName = "Perfectly Sane Git"; services.gitea.appName = "Perfectly Sane Git";
# services.gitea.disableRegistration = true; # services.gitea.disableRegistration = true;
services.gitea.database.createDatabase = false; #< silence warning which wants db user and name to be equal
# TODO: remove this after merge: <https://github.com/NixOS/nixpkgs/pull/268849>
services.gitea.database.socket = "/run/postgresql"; #< would have been set if createDatabase = true
# gitea doesn't create the git user # gitea doesn't create the git user
users.users.git = { users.users.git = {
description = "Gitea Service"; description = "Gitea Service";

View File

@@ -1,19 +1,9 @@
# how to update wikipedia snapshot:
# - browse for later snapshots:
# - <https://mirror.accum.se/mirror/wikimedia.org/other/kiwix/zim/wikipedia>
# - DL directly, or via rsync (resumable):
# - `rsync --progress --append-verify rsync://mirror.accum.se/mirror/wikimedia.org/other/kiwix/zim/wikipedia/wikipedia_en_all_maxi_2022-05.zim .`
{ ... }: { ... }:
{ {
sane.persist.sys.byStore.ext = [
{ user = "colin"; group = "users"; path = "/var/lib/kiwix"; }
];
sane.services.kiwix-serve = { sane.services.kiwix-serve = {
enable = true; enable = true;
port = 8013; port = 8013;
zimPaths = [ "/var/lib/kiwix/wikipedia_en_all_maxi_2023-11.zim" ]; zimPaths = [ "/var/lib/uninsane/www-archive/wikipedia_en_all_maxi_2022-05.zim" ];
}; };
services.nginx.virtualHosts."w.uninsane.org" = { services.nginx.virtualHosts."w.uninsane.org" = {

View File

@@ -1,8 +1,6 @@
# config options: # config options:
# - <https://github.com/mautrix/signal/blob/master/mautrix_signal/example-config.yaml> # - <https://github.com/mautrix/signal/blob/master/mautrix_signal/example-config.yaml>
{ config, lib, pkgs, ... }: { config, pkgs, ... }:
lib.mkIf false # disabled 2024/01/11: i don't use it, and pkgs.mautrix-signal had some API changes
{ {
sane.persist.sys.byStore.plaintext = [ sane.persist.sys.byStore.plaintext = [
{ user = "mautrix-signal"; group = "mautrix-signal"; path = "/var/lib/mautrix-signal"; } { user = "mautrix-signal"; group = "mautrix-signal"; path = "/var/lib/mautrix-signal"; }

View File

@@ -54,10 +54,8 @@ in
services.nginx.recommendedOptimisation = true; services.nginx.recommendedOptimisation = true;
# web blog/personal site # web blog/personal site
# alternative way to link stuff into the share:
# sane.fs."/var/lib/uninsane/share/Ubunchu".mount.bind = "/var/lib/uninsane/media/Books/Visual/HiroshiSeo/Ubunchu";
# sane.fs."/var/lib/uninsane/media/Books/Visual/HiroshiSeo/Ubunchu".dir = {};
services.nginx.virtualHosts."uninsane.org" = publog { services.nginx.virtualHosts."uninsane.org" = publog {
root = "${pkgs.uninsane-dot-org}/share/uninsane-dot-org";
# a lot of places hardcode https://uninsane.org, # a lot of places hardcode https://uninsane.org,
# and then when we mix http + non-https, we get CORS violations # and then when we mix http + non-https, we get CORS violations
# and things don't look right. so force SSL. # and things don't look right. so force SSL.
@@ -67,28 +65,9 @@ in
# for OCSP stapling # for OCSP stapling
sslTrustedCertificate = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"; sslTrustedCertificate = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt";
locations."/" = { # uninsane.org/share/foo => /var/lib/uninsane/root/share/foo.
root = "${pkgs.uninsane-dot-org}/share/uninsane-dot-org"; # yes, nginx does not strip the prefix when evaluating against the root.
tryFiles = "$uri $uri/ @fallback"; locations."/share".root = "/var/lib/uninsane/root";
};
# unversioned files
locations."@fallback" = {
root = "/var/www/sites/uninsane.org";
};
# uninsane.org/share/foo => /var/www/sites/uninsane.org/share/foo.
# special-cased to enable directory listings
locations."/share" = {
root = "/var/www/sites/uninsane.org";
extraConfig = ''
# autoindex => render directory listings
autoindex on;
# don't follow any symlinks when serving files
# otherwise it allows a directory escape
disable_symlinks on;
'';
};
# allow matrix users to discover that @user:uninsane.org is reachable via matrix.uninsane.org # allow matrix users to discover that @user:uninsane.org is reachable via matrix.uninsane.org
locations."= /.well-known/matrix/server".extraConfig = locations."= /.well-known/matrix/server".extraConfig =
@@ -129,19 +108,6 @@ in
# proxyPass = "http://127.0.0.1:4000"; # proxyPass = "http://127.0.0.1:4000";
# extraConfig = pleromaExtraConfig; # extraConfig = pleromaExtraConfig;
# }; # };
# redirect common feed URIs to the canonical feed
locations."= /atom".extraConfig = "return 301 /atom.xml;";
locations."= /feed".extraConfig = "return 301 /atom.xml;";
locations."= /feed.xml".extraConfig = "return 301 /atom.xml;";
locations."= /rss".extraConfig = "return 301 /atom.xml;";
locations."= /rss.xml".extraConfig = "return 301 /atom.xml;";
locations."= /blog/atom".extraConfig = "return 301 /atom.xml;";
locations."= /blog/atom.xml".extraConfig = "return 301 /atom.xml;";
locations."= /blog/feed".extraConfig = "return 301 /atom.xml;";
locations."= /blog/feed.xml".extraConfig = "return 301 /atom.xml;";
locations."= /blog/rss".extraConfig = "return 301 /atom.xml;";
locations."= /blog/rss.xml".extraConfig = "return 301 /atom.xml;";
}; };
@@ -169,6 +135,7 @@ in
security.acme.defaults.email = "admin.acme@uninsane.org"; security.acme.defaults.email = "admin.acme@uninsane.org";
sane.persist.sys.byStore.plaintext = [ sane.persist.sys.byStore.plaintext = [
# TODO: mode?
{ user = "acme"; group = "acme"; path = "/var/lib/acme"; } { user = "acme"; group = "acme"; path = "/var/lib/acme"; }
{ user = "colin"; group = "users"; path = "/var/www/sites"; } { user = "colin"; group = "users"; path = "/var/www/sites"; }
]; ];

View File

@@ -1,26 +0,0 @@
{ lib, pkgs, ... }:
lib.optionalAttrs false # disabled until i can be sure it's not gonna OOM my server in the middle of the night
{
systemd.services.nixos-prebuild = {
description = "build a nixos image with all updated deps";
path = with pkgs; [ coreutils git nix ];
script = ''
working=$(mktemp -d /tmp/nixos-prebuild.XXXXXX)
pushd "$working"
git clone https://git.uninsane.org/colin/nix-files.git \
&& cd nix-files \
&& nix flake update \
|| true
RC=$(nix run "$working/nix-files#check" -- -j1 --cores 5 --builders "")
popd
rm -rf "$working"
exit "$RC"
'';
};
systemd.timers.nixos-prebuild = {
wantedBy = [ "multi-user.target" ];
timerConfig.OnCalendar = "11,23:00:00";
};
}

View File

@@ -64,7 +64,7 @@ in
protocol = [ "tcp" ]; protocol = [ "tcp" ];
visibleTo.lan = true; visibleTo.lan = true;
visibleTo.wan = true; visibleTo.wan = true;
description = "colin-notification-waiter-${builtins.toString (port - portLow + 1)}-of-${builtins.toString numPorts}"; description = "colin-notification-waiter-${builtins.toString (port+1)}-of-${builtins.toString numPorts}";
}; };
})); }));
systemd.services = lib.mkMerge (builtins.map mkService portRange); systemd.services = lib.mkMerge (builtins.map mkService portRange);

View File

@@ -1,74 +0,0 @@
# Soulseek daemon (p2p file sharing with an emphasis on Music)
# docs: <https://github.com/slskd/slskd/blob/master/docs/config.md>
#
# config precedence (higher precedence overrules lower precedence):
# - Default Values < Environment Variables < YAML Configuraiton File < Command Line Arguments
{ config, lib, ... }:
{
sane.persist.sys.byStore.plaintext = [
{ user = "slskd"; group = "slskd"; path = "/var/lib/slskd"; }
];
sops.secrets."slskd_env" = {
owner = config.users.users.slskd.name;
mode = "0400";
};
users.users.slskd.extraGroups = [ "media" ];
sane.ports.ports."50000" = {
protocol = [ "tcp" ];
# not visible to WAN: i run this in a separate netns
visibleTo.ovpn = true;
description = "colin-soulseek";
};
sane.dns.zones."uninsane.org".inet.CNAME."soulseek" = "native";
services.nginx.virtualHosts."soulseek.uninsane.org" = {
forceSSL = true;
enableACME = true;
locations."/" = {
proxyPass = "http://10.0.1.6:5001";
proxyWebsockets = true;
};
};
services.slskd.enable = true;
# env file, for auth (SLSKD_SLSK_PASSWORD, SLSKD_SLSK_USERNAME)
services.slskd.environmentFile = config.sops.secrets.slskd_env.path;
services.slskd.settings = {
soulseek.diagnostic_level = "Debug"; # one of "None"|"Warning"|"Info"|"Debug"
shares.directories = [
# folders to share
# syntax: <https://github.com/slskd/slskd/blob/master/docs/config.md#directories>
# [Alias]/path/on/disk
# NOTE: Music library is quick to scan; videos take a solid 10min to scan.
# TODO: re-enable the other libraries
# "[Audioooks]/var/lib/uninsane/media/Books/Audiobooks"
# "[Books]/var/lib/uninsane/media/Books/Books"
# "[Manga]/var/lib/uninsane/media/Books/Visual"
# "[games]/var/lib/uninsane/media/games"
"[Music]/var/lib/uninsane/media/Music"
# "[Film]/var/lib/uninsane/media/Videos/Film"
# "[Shows]/var/lib/uninsane/media/Videos/Shows"
];
# directories.downloads = "..." # TODO
# directories.incomplete = "..." # TODO
# what unit is this? kbps??
global.upload.speed_limit = 32000;
web.logging = true;
# debug = true;
flags.no_logo = true; # don't show logo at start
# flags.volatile = true; # store searches and active transfers in RAM (completed transfers still go to disk). rec for btrfs/zfs
};
systemd.services.slskd = {
serviceConfig = {
# run this behind the OVPN static VPN
NetworkNamespacePath = "/run/netns/ovpns";
Restart = lib.mkForce "always"; # exits "success" when it fails to connect to soulseek server
RestartSec = "60s";
Group = "media";
};
};
}

View File

@@ -16,7 +16,7 @@
# transmission will by default not allow the world to read its files. # transmission will by default not allow the world to read its files.
services.transmission.downloadDirPermissions = "775"; services.transmission.downloadDirPermissions = "775";
services.transmission.extraFlags = [ services.transmission.extraFlags = [
# "--log-level=debug" "--log-level=debug"
]; ];
services.transmission.settings = { services.transmission.settings = {
@@ -39,9 +39,9 @@
encryption = 2; encryption = 2;
# units in kBps # units in kBps
speed-limit-down = 12000; speed-limit-down = 3000;
speed-limit-down-enabled = true; speed-limit-down-enabled = true;
speed-limit-up = 800; speed-limit-up = 600;
speed-limit-up-enabled = true; speed-limit-up-enabled = true;
# see: https://git.zknt.org/mirror/transmission/commit/cfce6e2e3a9b9d31a9dafedd0bdc8bf2cdb6e876?lang=bg-BG # see: https://git.zknt.org/mirror/transmission/commit/cfce6e2e3a9b9d31a9dafedd0bdc8bf2cdb6e876?lang=bg-BG
@@ -91,10 +91,5 @@
}; };
sane.dns.zones."uninsane.org".inet.CNAME."bt" = "native"; sane.dns.zones."uninsane.org".inet.CNAME."bt" = "native";
sane.ports.ports."51413" = {
protocol = [ "tcp" "udp" ];
visibleTo.ovpn = true;
description = "colin-bittorrent";
};
} }

View File

@@ -11,8 +11,6 @@ in lib.mkMerge [
# don't bind to IPv6 until i explicitly test that stack # don't bind to IPv6 until i explicitly test that stack
services.trust-dns.settings.listen_addrs_ipv6 = []; services.trust-dns.settings.listen_addrs_ipv6 = [];
services.trust-dns.quiet = true; services.trust-dns.quiet = true;
# FIXME(2023/11/26): services.trust-dns.debug doesn't log requests: use RUST_LOG=debug env for that.
# - see: <https://github.com/hickory-dns/hickory-dns/issues/2082>
# services.trust-dns.debug = true; # services.trust-dns.debug = true;
sane.ports.ports."53" = { sane.ports.ports."53" = {

View File

@@ -8,58 +8,38 @@
./hosts.nix ./hosts.nix
./ids.nix ./ids.nix
./machine-id.nix ./machine-id.nix
./net ./net.nix
./nix-path ./nix-path
./persist.nix ./persist.nix
./programs ./programs
./secrets.nix ./secrets.nix
./ssh.nix ./ssh.nix
./users ./users
./vpn.nix
]; ];
sane.nixcache.enable-trusted-keys = true; sane.nixcache.enable-trusted-keys = true;
sane.nixcache.enable = lib.mkDefault true; sane.nixcache.enable = lib.mkDefault true;
sane.persist.enable = lib.mkDefault true; sane.persist.enable = lib.mkDefault true;
sane.root-on-tmpfs = lib.mkDefault true;
sane.programs.sysadminUtils.enableFor.system = lib.mkDefault true; sane.programs.sysadminUtils.enableFor.system = lib.mkDefault true;
sane.programs.consoleUtils.enableFor.user.colin = lib.mkDefault true; sane.programs.consoleUtils.enableFor.user.colin = lib.mkDefault true;
nixpkgs.config.allowUnfree = true; # NIXPKGS_ALLOW_UNFREE=1 nixpkgs.config.allowUnfree = true;
nixpkgs.config.allowBroken = true; # NIXPKGS_ALLOW_BROKEN=1 nixpkgs.config.allowBroken = true; # NIXPKGS_ALLOW_BROKEN
# time.timeZone = "America/Los_Angeles"; # time.timeZone = "America/Los_Angeles";
time.timeZone = "Etc/UTC"; # DST is too confusing for me => use a stable timezone time.timeZone = "Etc/UTC"; # DST is too confusing for me => use a stable timezone
# allow `nix flake ...` command
# TODO: is this still required?
nix.extraOptions = '' nix.extraOptions = ''
# see: `man nix.conf`
# useful when a remote builder has a faster internet connection than me
builders-use-substitutes = true # default: false
# maximum seconds to wait when connecting to binary substituter
connect-timeout = 3 # default: 0
# download-attempts = 5 # default: 5
# allow `nix flake ...` command
experimental-features = nix-command flakes experimental-features = nix-command flakes
# whether to build from source when binary substitution fails
fallback = true # default: false
# whether to keep building dependencies if any other one fails
keep-going = true # default: false
# whether to keep build-only dependencies of GC roots (e.g. C compiler) when doing GC
keep-outputs = true # default: false
# how many lines to show from failed build
log-lines = 30 # default: 10
# narinfo-cache-negative-ttl = 3600 # default: 3600
# whether to use ~/.local/state/nix/profile instead of ~/.nix-profile, etc
use-xdg-base-directories = true # default: false
# whether to warn if repository has uncommited changes
warn-dirty = false # default: true
''; '';
# hardlinks identical files in the nix store to save 25-35% disk space. # hardlinks identical files in the nix store to save 25-35% disk space.
# unclear _when_ this occurs. it's not a service. # unclear _when_ this occurs. it's not a service.
# does the daemon continually scan the nix store? # does the daemon continually scan the nix store?
# does the builder use some content-addressed db to efficiently dedupe? # does the builder use some content-addressed db to efficiently dedupe?
nix.settings.auto-optimise-store = true; nix.settings.auto-optimise-store = true;
# TODO: see if i can remove this?
nix.settings.trusted-users = [ "root" ];
services.journald.extraConfig = '' services.journald.extraConfig = ''
# docs: `man journald.conf` # docs: `man journald.conf`
@@ -92,21 +72,7 @@
text = '' text = ''
# show which packages changed versions or are new/removed in this upgrade # show which packages changed versions or are new/removed in this upgrade
# source: <https://github.com/luishfonseca/dotfiles/blob/32c10e775d9ec7cc55e44592a060c1c9aadf113e/modules/upgrade-diff.nix> # source: <https://github.com/luishfonseca/dotfiles/blob/32c10e775d9ec7cc55e44592a060c1c9aadf113e/modules/upgrade-diff.nix>
# modified to not error on boot (when /run/current-system doesn't exist) ${pkgs.nvd}/bin/nvd --nix-bin-dir=${pkgs.nix}/bin diff /run/current-system "$systemConfig"
if [ -d /run/current-system ]; then
${pkgs.nvd}/bin/nvd --nix-bin-dir=${pkgs.nix}/bin diff /run/current-system "$systemConfig"
fi
'';
};
system.activationScripts.notifyActive = {
text = ''
# send a notification to any sway users logged in, that the system has been activated/upgraded.
# this probably doesn't work if more than one sway session exists on the system.
_notifyActiveSwaySock="$(echo /run/user/*/sway-ipc.*.sock)"
if [ -e "$_notifyActiveSwaySock" ]; then
SWAYSOCK="$_notifyActiveSwaySock" ${pkgs.sway}/bin/swaymsg -- exec \
"${pkgs.libnotify}/bin/notify-send 'nixos activated' 'version: $(cat $systemConfig/nixos-version)'"
fi
''; '';
}; };

View File

@@ -50,8 +50,6 @@ let
else else
"infrequent" "infrequent"
)); ));
} // lib.optionalAttrs (lib.hasPrefix "https://www.youtube.com/" raw.url) {
format = "video";
} // lib.optionalAttrs (raw.is_podcast or false) { } // lib.optionalAttrs (raw.is_podcast or false) {
format = "podcast"; format = "podcast";
} // lib.optionalAttrs (raw.title or "" != "") { } // lib.optionalAttrs (raw.title or "" != "") {
@@ -62,7 +60,6 @@ let
(fromDb "acquiredlpbonussecretsecret.libsyn.com" // tech) # ACQ2 - more "Acquired" episodes (fromDb "acquiredlpbonussecretsecret.libsyn.com" // tech) # ACQ2 - more "Acquired" episodes
(fromDb "allinchamathjason.libsyn.com" // pol) (fromDb "allinchamathjason.libsyn.com" // pol)
(fromDb "anchor.fm/s/34c7232c/podcast/rss" // tech) # Civboot -- https://anchor.fm/civboot (fromDb "anchor.fm/s/34c7232c/podcast/rss" // tech) # Civboot -- https://anchor.fm/civboot
(fromDb "anchor.fm/s/2da69154/podcast/rss" // tech) # POD OF JAKE -- https://podofjake.com/
(fromDb "cast.postmarketos.org" // tech) (fromDb "cast.postmarketos.org" // tech)
(fromDb "congressionaldish.libsyn.com" // pol) # Jennifer Briney (fromDb "congressionaldish.libsyn.com" // pol) # Jennifer Briney
(fromDb "craphound.com" // pol) # Cory Doctorow -- both podcast & text entries (fromDb "craphound.com" // pol) # Cory Doctorow -- both podcast & text entries
@@ -74,22 +71,19 @@ let
(fromDb "feeds.feedburner.com/radiolab" // pol) # Radiolab -- also available here, but ONLY OVER HTTP: <http://feeds.wnyc.org/radiolab> (fromDb "feeds.feedburner.com/radiolab" // pol) # Radiolab -- also available here, but ONLY OVER HTTP: <http://feeds.wnyc.org/radiolab>
(fromDb "feeds.libsyn.com/421877" // rat) # Less Wrong Curated (fromDb "feeds.libsyn.com/421877" // rat) # Less Wrong Curated
(fromDb "feeds.megaphone.fm/behindthebastards" // pol) # also Maggie Killjoy (fromDb "feeds.megaphone.fm/behindthebastards" // pol) # also Maggie Killjoy
# (fromDb "feeds.megaphone.fm/hubermanlab" // uncat) # Daniel Huberman on sleep (fromDb "feeds.megaphone.fm/hubermanlab" // uncat) # Daniel Huberman on sleep
(fromDb "feeds.megaphone.fm/recodedecode" // tech) # The Verge - Decoder (fromDb "feeds.megaphone.fm/recodedecode" // tech) # The Verge - Decoder
(fromDb "feeds.simplecast.com/54nAGcIl" // pol) # The Daily (fromDb "feeds.simplecast.com/54nAGcIl" // pol) # The Daily
(fromDb "feeds.simplecast.com/82FI35Px" // pol) # Ezra Klein Show (fromDb "feeds.simplecast.com/82FI35Px" // pol) # Ezra Klein Show
(fromDb "feeds.simplecast.com/wgl4xEgL" // rat) # Econ Talk (fromDb "feeds.simplecast.com/wgl4xEgL" // rat) # Econ Talk
(fromDb "feeds.simplecast.com/xKJ93w_w" // uncat) # Atlas Obscura (fromDb "feeds.simplecast.com/xKJ93w_w" // uncat) # Atlas Obscura
# (fromDb "feeds.simplecast.com/l2i9YnTd" // tech // pol) # Hard Fork (NYtimes tech)
(fromDb "feeds.transistor.fm/acquired" // tech) (fromDb "feeds.transistor.fm/acquired" // tech)
(fromDb "lexfridman.com/podcast" // rat) (fromDb "lexfridman.com/podcast" // rat)
(fromDb "mapspodcast.libsyn.com" // uncat) # Multidisciplinary Association for Psychedelic Studies (fromDb "mapspodcast.libsyn.com" // uncat) # Multidisciplinary Association for Psychedelic Studies
(fromDb "omegataupodcast.net" // tech) # 3/4 German; 1/4 eps are English (fromDb "omegataupodcast.net" // tech) # 3/4 German; 1/4 eps are English
(fromDb "omny.fm/shows/cool-people-who-did-cool-stuff" // pol) # Maggie Killjoy -- referenced by Cory Doctorow (fromDb "omny.fm/shows/cool-people-who-did-cool-stuff" // pol) # Maggie Killjoy -- referenced by Cory Doctorow
(fromDb "omny.fm/shows/the-dollop-with-dave-anthony-and-gareth-reynolds") # The Dollop history/comedy
(fromDb "originstories.libsyn.com" // uncat)
(fromDb "podcast.posttv.com/itunes/post-reports.xml" // pol) (fromDb "podcast.posttv.com/itunes/post-reports.xml" // pol)
# (fromDb "podcast.thelinuxexp.com" // tech) # low-brow linux/foss PR announcements (fromDb "podcast.thelinuxexp.com" // tech)
(fromDb "politicalorphanage.libsyn.com" // pol) (fromDb "politicalorphanage.libsyn.com" // pol)
(fromDb "reverseengineering.libsyn.com/rss" // tech) # UnNamed Reverse Engineering Podcast (fromDb "reverseengineering.libsyn.com/rss" // tech) # UnNamed Reverse Engineering Podcast
(fromDb "rss.acast.com/deconstructed") # The Intercept - Deconstructed (fromDb "rss.acast.com/deconstructed") # The Intercept - Deconstructed
@@ -98,7 +92,6 @@ let
(fromDb "rss.art19.com/60-minutes" // pol) (fromDb "rss.art19.com/60-minutes" // pol)
(fromDb "rss.art19.com/the-portal" // rat) # Eric Weinstein (fromDb "rss.art19.com/the-portal" // rat) # Eric Weinstein
(fromDb "seattlenice.buzzsprout.com" // pol) (fromDb "seattlenice.buzzsprout.com" // pol)
(fromDb "srslywrong.com" // pol)
(fromDb "sharkbytes.transistor.fm" // tech) # Wireshark Podcast o_0 (fromDb "sharkbytes.transistor.fm" // tech) # Wireshark Podcast o_0
(fromDb "sscpodcast.libsyn.com" // rat) # Astral Codex Ten (fromDb "sscpodcast.libsyn.com" // rat) # Astral Codex Ten
(fromDb "talesfromthebridge.buzzsprout.com" // tech) # Sci-Fi? has Peter Watts; author of No Moods, Ads or Cutesy Fucking Icons (rifters.com) (fromDb "talesfromthebridge.buzzsprout.com" // tech) # Sci-Fi? has Peter Watts; author of No Moods, Ads or Cutesy Fucking Icons (rifters.com)
@@ -117,118 +110,135 @@ let
]; ];
texts = [ texts = [
(fromDb "amosbbatto.wordpress.com" // tech) # AGGREGATORS (> 1 post/day)
(fromDb "applieddivinitystudies.com" // rat)
(fromDb "artemis.sh" // tech)
(fromDb "ascii.textfiles.com" // tech) # Jason Scott
(fromDb "austinvernon.site" // tech)
(fromDb "balajis.com" // pol) # Balaji
(fromDb "ben-evans.com/benedictevans" // pol)
(fromDb "bitbashing.io" // tech)
(fromDb "bitsaboutmoney.com" // uncat)
(fromDb "blog.danieljanus.pl" // tech)
(fromDb "blog.dshr.org" // pol) # David Rosenthal
(fromDb "blog.jmp.chat" // tech)
(fromDb "blog.rust-lang.org" // tech)
(fromDb "blog.thalheim.io" // tech) # Mic92
(fromDb "bunniestudios.com" // tech) # Bunnie Juang
(fromDb "capitolhillseattle.com" // pol)
# (fromDb "drewdevault.com" // tech)
# (fromDb "econlib.org" // pol)
(fromDb "edwardsnowden.substack.com" // pol // text)
(fromDb "fasterthanli.me" // tech)
(fromDb "gwern.net" // rat)
(fromDb "harihareswara.net" // tech // pol) # rec by Cory Doctorow
(fromDb "ianthehenry.com" // tech)
(fromDb "idiomdrottning.org" // uncat)
(fromDb "interconnected.org/home/feed" // rat) # Matt Webb -- engineering-ish, but dreamy
(fromDb "jeffgeerling.com" // tech)
(fromDb "jefftk.com" // tech)
(fromDb "kosmosghost.github.io/index.xml" // tech)
# (fromDb "lesswrong.com" // rat)
(fromDb "linmob.net" // tech)
(fromDb "lwn.net" // tech) (fromDb "lwn.net" // tech)
(fromDb "lynalden.com" // pol) # (fromDb "lesswrong.com" // rat)
(fromDb "mako.cc/copyrighteous" // tech // pol) # rec by Cory Doctorow # (fromDb "econlib.org" // pol)
(fromDb "mg.lol" // tech)
(fromDb "mindingourway.com" // rat) # AGGREGATORS (< 1 post/day)
(fromDb "morningbrew.com/feed" // pol)
(fromDb "overcomingbias.com" // rat) # Robin Hanson
(fromDb "palladiummag.com" // uncat) (fromDb "palladiummag.com" // uncat)
(fromDb "philosopher.coach" // rat) # Peter Saint-Andre -- side project of stpeter.im
(fromDb "pomeroyb.com" // tech)
(fromDb "preposterousuniverse.com" // rat) # Sean Carroll
(fromDb "profectusmag.com" // uncat) (fromDb "profectusmag.com" // uncat)
(fromDb "project-insanity.org" // tech) # shared blog by a few NixOS devs, notably onny
(fromDb "putanumonit.com" // rat) # mostly dating topics. not advice, or humor, but looking through a social lens
(fromDb "richardcarrier.info" // rat)
(fromDb "rifters.com/crawl" // uncat) # No Moods, Ads or Cutesy Fucking Icons
(fromDb "righto.com" // tech) # Ken Shirriff
(fromDb "rootsofprogress.org" // rat) # Jason Crawford
(fromDb "sagacioussuricata.com" // tech) # ian (Sanctuary)
(fromDb "semiaccurate.com" // tech) (fromDb "semiaccurate.com" // tech)
(fromDb "sideways-view.com" // rat) # Paul Christiano (mkText "https://linuxphoneapps.org/blog/atom.xml" // tech // infrequent)
(fromDb "slimemoldtimemold.com" // rat) (fromDb "tuxphones.com" // tech)
(fromDb "spectrum.ieee.org" // tech) (fromDb "spectrum.ieee.org" // tech)
(fromDb "stpeter.im/atom.xml" // pol)
# (fromDb "theregister.com" // tech) # (fromDb "theregister.com" // tech)
(fromDb "thisweek.gnome.org" // tech) (fromDb "thisweek.gnome.org" // tech)
(fromDb "tuxphones.com" // tech) # more nixos stuff here, but unclear how to subscribe: <https://nixos.org/blog/categories.html>
(mkText "https://nixos.org/blog/announcements-rss.xml" // tech // infrequent)
(mkText "https://nixos.org/blog/stories-rss.xml" // tech // weekly)
## n.b.: quality RSS list here: <https://forum.merveilles.town/thread/57/share-your-rss-feeds%21-6/>
(mkText "https://forum.merveilles.town/rss.xml" // pol // infrequent)
## No Moods, Ads or Cutesy Fucking Icons
(fromDb "rifters.com/crawl" // uncat)
# DEVELOPERS
(fromDb "blog.jmp.chat" // tech)
(fromDb "uninsane.org" // tech) (fromDb "uninsane.org" // tech)
(fromDb "unintendedconsequenc.es" // rat) (fromDb "ascii.textfiles.com" // tech) # Jason Scott
# (fromDb "vitalik.ca" // tech) # moved to vitalik.eth.limo
(fromDb "vitalik.eth.limo" // tech) # Vitalik Buterin
(fromDb "webcurious.co.uk" // uncat)
(fromDb "xn--gckvb8fzb.com" // tech) (fromDb "xn--gckvb8fzb.com" // tech)
(mkSubstack "astralcodexten" // rat // daily) # Scott Alexander (fromDb "amosbbatto.wordpress.com" // tech)
(fromDb "fasterthanli.me" // tech)
(fromDb "mg.lol" // tech)
# (fromDb "drewdevault.com" // tech)
## Ken Shirriff
(fromDb "righto.com" // tech)
## shared blog by a few NixOS devs, notably onny
(fromDb "project-insanity.org" // tech)
## Vitalik Buterin
(fromDb "vitalik.ca" // tech)
## ian (Sanctuary)
(fromDb "sagacioussuricata.com" // tech)
(fromDb "artemis.sh" // tech)
## Bunnie Juang
(fromDb "bunniestudios.com" // tech)
(fromDb "blog.danieljanus.pl" // tech)
(fromDb "ianthehenry.com" // tech)
(fromDb "bitbashing.io" // tech)
(fromDb "idiomdrottning.org" // uncat)
(mkText "http://boginjr.com/feed" // tech // infrequent)
(mkText "https://anish.lakhwara.com/home.html" // tech // weekly)
(fromDb "jefftk.com" // tech)
(fromDb "pomeroyb.com" // tech)
(fromDb "harihareswara.net" // tech // pol) # rec by Cory Doctorow
(fromDb "mako.cc/copyrighteous" // tech // pol) # rec by Cory Doctorow
# (mkText "https://til.simonwillison.net/tils/feed.atom" // tech // weekly)
# TECH PROJECTS
(fromDb "blog.rust-lang.org" // tech)
# (TECH; POL) COMMENTATORS
## Matt Webb -- engineering-ish, but dreamy
(fromDb "interconnected.org/home/feed" // rat)
(fromDb "edwardsnowden.substack.com" // pol // text)
## Julia Evans
(mkText "https://jvns.ca/atom.xml" // tech // weekly)
(mkText "http://benjaminrosshoffman.com/feed" // pol // weekly)
## Ben Thompson
(mkText "https://www.stratechery.com/rss" // pol // weekly)
## Balaji
(fromDb "balajis.com" // pol)
(fromDb "ben-evans.com/benedictevans" // pol)
(fromDb "lynalden.com" // pol)
(fromDb "austinvernon.site" // tech)
(mkSubstack "oversharing" // pol // daily)
(mkSubstack "byrnehobart" // pol // infrequent) (mkSubstack "byrnehobart" // pol // infrequent)
# (mkSubstack "doomberg" // tech // weekly) # articles are all pay-walled # (mkSubstack "doomberg" // tech // weekly) # articles are all pay-walled
(mkSubstack "eliqian" // rat // weekly) ## David Rosenthal
(mkSubstack "oversharing" // pol // daily) (fromDb "blog.dshr.org" // pol)
(mkSubstack "samkriss" // humor // infrequent) ## Matt Levine
(mkText "http://benjaminrosshoffman.com/feed" // pol // weekly) (mkText "https://www.bloomberg.com/opinion/authors/ARbTQlRLRjE/matthew-s-levine.rss" // pol // weekly)
(mkText "http://boginjr.com/feed" // tech // infrequent) (fromDb "stpeter.im/atom.xml" // pol)
(mkText "https://acoup.blog/feed" // rat // weekly) ## Peter Saint-Andre -- side project of stpeter.im
(mkText "https://anish.lakhwara.com/home.html" // tech // weekly) (fromDb "philosopher.coach" // rat)
(mkText "https://forum.merveilles.town/rss.xml" // pol // infrequent) #quality RSS list here: <https://forum.merveilles.town/thread/57/share-your-rss-feeds%21-6/> (fromDb "morningbrew.com/feed" // pol)
# (mkText "https://github.com/Kaiteki-Fedi/Kaiteki/commits/master.atom" // tech // infrequent)
(mkText "https://jvns.ca/atom.xml" // tech // weekly) # Julia Evans
(mkText "https://linuxphoneapps.org/blog/atom.xml" // tech // infrequent)
(mkText "https://nixos.org/blog/announcements-rss.xml" // tech // infrequent) # more nixos stuff here, but unclear how to subscribe: <https://nixos.org/blog/categories.html>
(mkText "https://nixos.org/blog/stories-rss.xml" // tech // weekly)
# (mkText "https://til.simonwillison.net/tils/feed.atom" // tech // weekly)
(mkText "https://www.bloomberg.com/opinion/authors/ARbTQlRLRjE/matthew-s-levine.rss" // pol // weekly) # Matt Levine
(mkText "https://www.stratechery.com/rss" // pol // weekly) # Ben Thompson
];
videos = [ # RATIONALITY/PHILOSOPHY/ETC
(fromDb "youtube.com/@Channel5YouTube" // pol) (mkSubstack "samkriss" // humor // infrequent)
(fromDb "youtube.com/@ColdFusion") (fromDb "unintendedconsequenc.es" // rat)
(fromDb "youtube.com/@ContraPoints" // pol) (fromDb "applieddivinitystudies.com" // rat)
(fromDb "youtube.com/@Exurb1a") (fromDb "slimemoldtimemold.com" // rat)
(fromDb "youtube.com/@hbomberguy") (fromDb "richardcarrier.info" // rat)
(fromDb "youtube.com/@JackStauber") (fromDb "gwern.net" // rat)
(fromDb "youtube.com/@PolyMatter") ## Jason Crawford
# (fromDb "youtube.com/@rossmanngroup" // pol // tech) # Louis Rossmann (fromDb "rootsofprogress.org" // rat)
(fromDb "youtube.com/@TechnologyConnections" // tech) ## Robin Hanson
(fromDb "youtube.com/@TheB1M") (fromDb "overcomingbias.com" // rat)
(fromDb "youtube.com/@TomScottGo") ## Scott Alexander
(fromDb "youtube.com/@Vihart") (mkSubstack "astralcodexten" // rat // daily)
(fromDb "youtube.com/@Vox") ## Paul Christiano
(fromDb "youtube.com/@Vsauce") (fromDb "sideways-view.com" // rat)
## Sean Carroll
(fromDb "preposterousuniverse.com" // rat)
(mkSubstack "eliqian" // rat // weekly)
(mkText "https://acoup.blog/feed" // rat // weekly)
(fromDb "mindingourway.com" // rat)
## mostly dating topics. not advice, or humor, but looking through a social lens
(fromDb "putanumonit.com" // rat)
# LOCAL
(fromDb "capitolhillseattle.com" // pol)
# CODE
# (mkText "https://github.com/Kaiteki-Fedi/Kaiteki/commits/master.atom" // tech // infrequent)
]; ];
images = [ images = [
(fromDb "miniature-calendar.com" // img // art // daily)
(fromDb "pbfcomics.com" // img // humor)
(fromDb "poorlydrawnlines.com/feed" // img // humor)
(fromDb "smbc-comics.com" // img // humor) (fromDb "smbc-comics.com" // img // humor)
(fromDb "turnoff.us" // img // humor)
(fromDb "xkcd.com" // img // humor) (fromDb "xkcd.com" // img // humor)
(fromDb "turnoff.us" // img // humor)
(fromDb "pbfcomics.com" // img // humor)
# (mkImg "http://dilbert.com/feed" // humor // daily)
(fromDb "poorlydrawnlines.com/feed" // img // humor)
# ART
(fromDb "miniature-calendar.com" // img // art // daily)
]; ];
in in
{ {
sane.feeds = texts ++ images ++ podcasts ++ videos; sane.feeds = texts ++ images ++ podcasts;
assertions = builtins.map assertions = builtins.map
(p: { (p: {

View File

@@ -1,4 +1,4 @@
{ config, lib, pkgs, ... }: { lib, pkgs, ... }:
{ {
imports = [ imports = [
@@ -12,9 +12,8 @@
copy_bin_and_libs ${pkgs.util-linux}/bin/{cfdisk,lsblk,lscpu} copy_bin_and_libs ${pkgs.util-linux}/bin/{cfdisk,lsblk,lscpu}
copy_bin_and_libs ${pkgs.gptfdisk}/bin/{cgdisk,gdisk} copy_bin_and_libs ${pkgs.gptfdisk}/bin/{cgdisk,gdisk}
copy_bin_and_libs ${pkgs.smartmontools}/bin/smartctl copy_bin_and_libs ${pkgs.smartmontools}/bin/smartctl
copy_bin_and_libs ${pkgs.nvme-cli}/bin/nvme
copy_bin_and_libs ${pkgs.e2fsprogs}/bin/resize2fs copy_bin_and_libs ${pkgs.e2fsprogs}/bin/resize2fs
'' + lib.optionalString pkgs.stdenv.hostPlatform.isx86_64 ''
copy_bin_and_libs ${pkgs.nvme-cli}/bin/nvme # doesn't cross compile
''; '';
boot.kernelParams = [ boot.kernelParams = [
"boot.shell_on_fail" "boot.shell_on_fail"
@@ -40,12 +39,6 @@
# non-free firmware # non-free firmware
hardware.enableRedistributableFirmware = true; hardware.enableRedistributableFirmware = true;
# default is 252274, which is too low particularly for servo.
# manifests as spurious "No space left on device" when trying to install watches,
# e.g. in dyn-dns by `systemctl start dyn-dns-watcher.path`.
# see: <https://askubuntu.com/questions/828779/failed-to-add-run-systemd-ask-password-to-directory-watch-no-space-left-on-dev>
boot.kernel.sysctl."fs.inotify.max_user_watches" = 1048576;
# powertop will default to putting USB devices -- including HID -- to sleep after TWO SECONDS # powertop will default to putting USB devices -- including HID -- to sleep after TWO SECONDS
powerManagement.powertop.enable = false; powerManagement.powertop.enable = false;
# linux CPU governor: <https://www.kernel.org/doc/Documentation/cpu-freq/governors.txt> # linux CPU governor: <https://www.kernel.org/doc/Documentation/cpu-freq/governors.txt>
@@ -64,19 +57,10 @@
powerManagement.cpuFreqGovernor = "ondemand"; powerManagement.cpuFreqGovernor = "ondemand";
services.logind.extraConfig = '' services.logind.extraConfig = ''
# see: `man logind.conf` # dont shutdown when power button is short-pressed
# dont shutdown when power button is short-pressed (commonly done an accident, or by cats). HandlePowerKey=ignore
# but do on long-press: useful to gracefully power-off server.
HandlePowerKey=lock
HandlePowerKeyLongPress=poweroff
HandleLidSwitch=lock
''; '';
# some packages build only if binfmt *isn't* present
nix.settings.system-features = lib.mkIf (config.boot.binfmt.emulatedSystems == []) [
"no-binfmt"
];
# services.snapper.configs = { # services.snapper.configs = {
# root = { # root = {
# subvolume = "/"; # subvolume = "/";

View File

@@ -1,32 +1,19 @@
{ config, lib, ...}: { config, lib, ...}:
let let
# [ ProgramConfig ]
enabledPrograms = builtins.filter
(p: p.enabled)
(builtins.attrValues config.sane.programs);
# [ { "<mime-type>" = { prority, desktop } ]
enabledWeightedMimes = builtins.map weightedMimes enabledPrograms;
# ProgramConfig -> { "<mime-type>" = { priority, desktop }; } # ProgramConfig -> { "<mime-type>" = { priority, desktop }; }
weightedMimes = prog: builtins.mapAttrs weightedMimes = prog: builtins.mapAttrs (_key: desktop: { priority = prog.mime.priority; desktop = desktop; }) prog.mime.associations;
(_key: desktop: {
priority = prog.mime.priority; desktop = desktop;
})
prog.mime.associations;
# [ { "<mime-type>" = { priority, desktop } ]; } ] -> { "<mime-type>" = [ { priority, desktop } ... ]; } # [ { "<mime-type>" = { priority, desktop } ]; } ] -> { "<mime-type>" = [ { priority, desktop } ... ]; }
mergeMimes = mimes: lib.foldAttrs (item: acc: [item] ++ acc) [] mimes; mergeMimes = mimes: lib.foldAttrs (item: acc: [item] ++ acc) [] mimes;
# [ { priority, desktop } ... ] -> Self # [ { priority, desktop } ... ] -> Self
sortOneMimeType = associations: builtins.sort sortOneMimeType = associations: builtins.sort (l: r: assert l.priority != r.priority; l.priority < r.priority) associations;
(l: r: assert l.priority != r.priority; l.priority < r.priority)
associations;
sortMimes = mimes: builtins.mapAttrs (_k: sortOneMimeType) mimes; sortMimes = mimes: builtins.mapAttrs (_k: sortOneMimeType) mimes;
removePriorities = mimes: builtins.mapAttrs removePriorities = mimes: builtins.mapAttrs (_k: associations: builtins.map (a: a.desktop) associations) mimes;
(_k: associations: builtins.map (a: a.desktop) associations)
mimes; # [ ProgramConfig ]
enabledPrograms = builtins.filter (p: p.enabled) (builtins.attrValues config.sane.programs);
# [ { "<mime-type>" = { prority, desktop } ]
enabledWeightedMimes = builtins.map weightedMimes enabledPrograms;
in in
{ {
# the xdg mime type for a file can be found with: # the xdg mime type for a file can be found with:
@@ -38,6 +25,4 @@ in
# there's also options to *remove* [non-default] associations from specific apps # there's also options to *remove* [non-default] associations from specific apps
xdg.mime.enable = true; xdg.mime.enable = true;
xdg.mime.defaultApplications = removePriorities (sortMimes (mergeMimes enabledWeightedMimes)); xdg.mime.defaultApplications = removePriorities (sortMimes (mergeMimes enabledWeightedMimes));
} }

View File

@@ -19,7 +19,7 @@
}; };
sane.hosts.by-name."moby" = { sane.hosts.by-name."moby" = {
# ssh.authorized = lib.mkDefault false; # moby's too easy to hijack: don't let it ssh places ssh.authorized = lib.mkDefault false; # moby's too easy to hijack: don't let it ssh places
ssh.user_pubkey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICrR+gePnl0nV/vy7I5BzrGeyVL+9eOuXHU1yNE3uCwU"; ssh.user_pubkey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICrR+gePnl0nV/vy7I5BzrGeyVL+9eOuXHU1yNE3uCwU";
ssh.host_pubkey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIO1N/IT3nQYUD+dBlU1sTEEVMxfOyMkrrDeyHcYgnJvw"; ssh.host_pubkey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIO1N/IT3nQYUD+dBlU1sTEEVMxfOyMkrrDeyHcYgnJvw";
wg-home.pubkey = "I7XIR1hm8bIzAtcAvbhWOwIAabGkuEvbWH/3kyIB1yA="; wg-home.pubkey = "I7XIR1hm8bIzAtcAvbhWOwIAabGkuEvbWH/3kyIB1yA=";
@@ -36,10 +36,4 @@
wg-home.endpoint = "uninsane.org:51820"; wg-home.endpoint = "uninsane.org:51820";
lan-ip = "10.78.79.51"; lan-ip = "10.78.79.51";
}; };
sane.hosts.by-name."supercap" = {
ssh.authorized = false;
ssh.host_pubkey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHf/mqqkX45EWAcquV04MC3SUljTApdclH1gjI19F+PA";
lan-ip = "10.78.79.232";
};
} }

View File

@@ -49,14 +49,6 @@
sane.ids.media.gid = 2414; sane.ids.media.gid = 2414;
sane.ids.ntfy-sh.uid = 2415; sane.ids.ntfy-sh.uid = 2415;
sane.ids.ntfy-sh.gid = 2415; sane.ids.ntfy-sh.gid = 2415;
sane.ids.monero.uid = 2416;
sane.ids.monero.gid = 2416;
sane.ids.slskd.uid = 2417;
sane.ids.slskd.gid = 2417;
sane.ids.bitcoind-mainnet.uid = 2418;
sane.ids.bitcoind-mainnet.gid = 2418;
sane.ids.clightning.uid = 2419;
sane.ids.clightning.gid = 2419;
sane.ids.colin.uid = 1000; sane.ids.colin.uid = 1000;
sane.ids.guest.uid = 1100; sane.ids.guest.uid = 1100;
@@ -71,8 +63,6 @@
sane.ids.systemd-oom.uid = 2005; sane.ids.systemd-oom.uid = 2005;
sane.ids.systemd-oom.gid = 2005; sane.ids.systemd-oom.gid = 2005;
sane.ids.wireshark.gid = 2006; sane.ids.wireshark.gid = 2006;
sane.ids.nixremote.uid = 2007;
sane.ids.nixremote.gid = 2007;
# found on graphical hosts # found on graphical hosts
sane.ids.nm-iodine.uid = 2101; # desko/moby/lappy sane.ids.nm-iodine.uid = 2101; # desko/moby/lappy

View File

@@ -1,12 +1,6 @@
{ lib, ... }: { lib, ... }:
{ {
imports = [
./dns.nix
./hostnames.nix
./upnp.nix
./vpn.nix
];
# the default backend is "wpa_supplicant". # the default backend is "wpa_supplicant".
# wpa_supplicant reliably picks weak APs to connect to. # wpa_supplicant reliably picks weak APs to connect to.
# see: <https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/issues/474> # see: <https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/issues/474>
@@ -41,6 +35,10 @@
# e.g. openconnect drags in webkitgtk (for SSO)! # e.g. openconnect drags in webkitgtk (for SSO)!
networking.networkmanager.plugins = lib.mkForce []; networking.networkmanager.plugins = lib.mkForce [];
networking.firewall.allowedUDPPorts = [
1900 # to received UPnP advertisements. required by sane-ip-check-upnp
];
# keyfile.path = where networkmanager should look for connection credentials # keyfile.path = where networkmanager should look for connection credentials
networking.networkmanager.extraConfig = '' networking.networkmanager.extraConfig = ''
[keyfile] [keyfile]

View File

@@ -1,37 +0,0 @@
{ ... }:
{
# use systemd's stub resolver.
# /etc/resolv.conf isn't sophisticated enough to use different servers per net namespace (or link).
# instead, running the stub resolver on a known address in the root ns lets us rewrite packets
# in the ovnps namespace to use the provider's DNS resolvers.
# a weakness is we can only query 1 NS at a time (unless we were to clone the packets?)
# there also seems to be some cache somewhere that's shared between the two namespaces.
# i think this is a libc thing. might need to leverage proper cgroups to _really_ kill it.
# - getent ahostsv4 www.google.com
# - try fix: <https://serverfault.com/questions/765989/connect-to-3rd-party-vpn-server-but-dont-use-it-as-the-default-route/766290#766290>
services.resolved.enable = true;
# without DNSSEC:
# - dig matrix.org => works
# - curl https://matrix.org => works
# with default DNSSEC:
# - dig matrix.org => works
# - curl https://matrix.org => fails
# i don't know why. this might somehow be interfering with the DNS run on this device (trust-dns)
services.resolved.dnssec = "false";
networking.nameservers = [
# use systemd-resolved resolver
# full resolver (which understands /etc/hosts) lives on 127.0.0.53
# stub resolver (just forwards upstream) lives on 127.0.0.54
"127.0.0.53"
];
# nscd -- the Name Service Caching Daemon -- caches DNS query responses
# in a way that's unaware of my VPN routing, so routes are frequently poor against
# services which advertise different IPs based on geolocation.
# nscd claims to be usable without a cache, but in practice i can't get it to not cache!
# nsncd is the Name Service NON-Caching Daemon. it's a drop-in that doesn't cache;
# this is OK on the host -- because systemd-resolved caches. it's probably sub-optimal
# in the netns and we query upstream DNS more often than needed. hm.
# TODO: run a separate recursive resolver in each namespace.
services.nscd.enableNsncd = true;
}

View File

@@ -1,15 +0,0 @@
{ config, lib, ... }:
{
# give each host a shortname that all the other hosts know, to allow easy comms.
networking.hosts = lib.mkMerge (builtins.map
(host: let
cfg = config.sane.hosts.by-name."${host}";
in {
"${cfg.lan-ip}" = [ host ];
} // lib.optionalAttrs (cfg.wg-home.ip != null) {
"${cfg.wg-home.ip}" = [ "${host}-hn" ];
})
(builtins.attrNames config.sane.hosts.by-name)
);
}

View File

@@ -1,20 +0,0 @@
{ pkgs, ... }:
{
networking.firewall.allowedUDPPorts = [
# to receive UPnP advertisements. required by sane-ip-check.
# N.B. sane-ip-check isn't query/response based. it needs to receive on port 1900 -- not receive responses FROM port 1900.
1900
];
networking.firewall.extraCommands = with pkgs; ''
# after an outgoing SSDP query to the multicast address, open FW for incoming responses.
# necessary for anything DLNA, especially go2tv
# source: <https://serverfault.com/a/911286>
# context: <https://github.com/alexballas/go2tv/issues/72>
# ipset -! means "don't fail if set already exists"
${ipset}/bin/ipset create -! upnp hash:ip,port timeout 10
${iptables}/bin/iptables -A OUTPUT -d 239.255.255.250/32 -p udp -m udp --dport 1900 -j SET --add-set upnp src,src --exist
${iptables}/bin/iptables -A INPUT -p udp -m set --match-set upnp dst,dst -j ACCEPT
'';
}

View File

@@ -1,4 +1,4 @@
{ pkgs, sane-lib, ... }: { pkgs, ... }:
{ {
# allow `nix-shell` (and probably nix-index?) to locate our patched and custom packages # allow `nix-shell` (and probably nix-index?) to locate our patched and custom packages
@@ -10,7 +10,4 @@
# to avoid switching so much during development # to avoid switching so much during development
"nixpkgs-overlays=/home/colin/dev/nixos/hosts/common/nix-path/overlay" "nixpkgs-overlays=/home/colin/dev/nixos/hosts/common/nix-path/overlay"
]; ];
# ensure new deployments have a source of this repo with which they can bootstrap.
environment.etc."nixos".source = ../../..;
} }

View File

@@ -1,99 +0,0 @@
# discord gtk3 client
{ config, lib, pkgs, ... }:
let
cfg = config.sane.programs.abaddon;
in
{
sane.programs.abaddon = {
configOption = with lib; mkOption {
default = {};
type = types.submodule {
options.autostart = mkOption {
type = types.bool;
default = false;
};
};
};
package = pkgs.abaddon.overrideAttrs (upstream: {
patches = (upstream.patches or []) ++ [
(pkgs.fetchpatch {
url = "https://git.uninsane.org/colin/abaddon/commit/eb551f188d34679f75adcbc83cb8d5beb4d19fd6.patch";
name = ''"view members" default to false'';
hash = "sha256-9BX8iO86CU1lNrKS1G2BjDR+3IlV9bmhRNTsLrxChwQ=";
})
];
});
suggestedPrograms = [ "gnome-keyring" ];
fs.".config/abaddon/abaddon.ini".symlink.text = ''
# see abaddon README.md for options.
# at time of writing:
# | Setting | Type | Default | Description |
# |[discord]------|---------|---------|--------------------------------------------------------------------------------------------------|
# | `gateway` | string | | override url for Discord gateway. must be json format and use zlib stream compression |
# | `api_base` | string | | override base url for Discord API |
# | `memory_db` | boolean | false | if true, Discord data will be kept in memory as opposed to on disk |
# | `token` | string | | Discord token used to login, this can be set from the menu |
# | `prefetch` | boolean | false | if true, new messages will cause the avatar and image attachments to be automatically downloaded |
# | `autoconnect` | boolean | false | autoconnect to discord |
# |[http]--------|--------|---------|---------------------------------------------------------------------------------------------|
# | `user_agent` | string | | sets the user-agent to use in HTTP requests to the Discord API (not including media/images) |
# | `concurrent` | int | 20 | how many images can be concurrently retrieved |
# |[gui}------------------------|---------|---------|----------------------------------------------------------------------------------------------------------------------------|
# | `member_list_discriminator` | boolean | true | show user discriminators in the member list |
# | `stock_emojis` | boolean | true | allow abaddon to substitute unicode emojis with images from emojis.bin, must be false to allow GTK to render emojis itself |
# | `custom_emojis` | boolean | true | download and use custom Discord emojis |
# | `css` | string | | path to the main CSS file |
# | `animations` | boolean | true | use animated images where available (e.g. server icons, emojis, avatars). false means static images will be used |
# | `animated_guild_hover_only` | boolean | true | only animate guild icons when the guild is being hovered over |
# | `owner_crown` | boolean | true | show a crown next to the owner |
# | `unreads` | boolean | true | show unread indicators and mention badges |
# | `save_state` | boolean | true | save the state of the gui (active channels, tabs, expanded channels) |
# | `alt_menu` | boolean | false | keep the menu hidden unless revealed with alt key |
# | `hide_to_tray` | boolean | false | hide abaddon to the system tray on window close |
# | `show_deleted_indicator` | boolean | true | show \[deleted\] indicator next to deleted messages instead of actually deleting the message |
# | `font_scale` | double | | scale font rendering. 1 is unchanged |
# |[style]------------------|--------|-----------------------------------------------------|
# | `linkcolor` | string | color to use for links in messages |
# | `expandercolor` | string | color to use for the expander in the channel list |
# | `nsfwchannelcolor` | string | color to use for NSFW channels in the channel list |
# | `channelcolor` | string | color to use for SFW channels in the channel list |
# | `mentionbadgecolor` | string | background color for mention badges |
# | `mentionbadgetextcolor` | string | color to use for number displayed on mention badges |
# | `unreadcolor` | string | color to use for the unread indicator |
# |[notifications]|---------|--------------------------|-------------------------------------------------------------------------------|
# | `enabled` | boolean | true (if not on Windows) | Enable desktop notifications |
# | `playsound` | boolean | true | Enable notification sounds. Requires ENABLE_NOTIFICATION_SOUNDS=TRUE in CMake |
# |[voice]--|--------|------------------------------------|------------------------------------------------------------|
# | `vad` | string | rnnoise if enabled, gate otherwise | Method used for voice activity detection. Changeable in UI |
# |[windows]|---------|---------|-------------------------|
# | `hideconsole` | boolean | true | Hide console on startup |
# N.B.: abaddon writes this file itself (and even when i don't change anything internally).
# it prefers no spaces around the equal sign.
[discord]
autoconnect=true
[notifications]
# playsound: i manage sounds via swaync
playsound=false
'';
persist.byStore.private = [
".cache/abaddon"
];
services.abaddon = {
description = "unofficial Discord chat client";
wantedBy = lib.mkIf cfg.config.autostart [ "default.target" ];
serviceConfig = {
ExecStart = "${cfg.package}/bin/abaddon";
Type = "simple";
Restart = "always";
RestartSec = "20s";
};
};
};
}

View File

@@ -7,34 +7,18 @@
{ {
sane.programs.alacritty = { sane.programs.alacritty = {
env.TERMINAL = lib.mkDefault "alacritty"; env.TERMINAL = lib.mkDefault "alacritty";
fs.".config/alacritty/alacritty.toml".symlink.text = '' # note: alacritty will switch to .toml config in 13.0 release
[font] # - run `alacritty migrate` to convert the yaml to toml
size = 14 fs.".config/alacritty/alacritty.yml".symlink.text = ''
font:
size: 14
[[keyboard.bindings]] key_bindings:
mods = "Control" - { key: N, mods: Control, action: CreateNewWindow }
key = "N" - { key: PageUp, mods: Control, action: ScrollPageUp }
action = "CreateNewWindow" - { key: PageDown, mods: Control, action: ScrollPageDown }
- { key: PageUp, mods: Control|Shift, action: ScrollPageUp }
[[keyboard.bindings]] - { key: PageDown, mods: Control|Shift, action: ScrollPageDown }
mods = "Control"
key = "PageUp"
action = "ScrollPageUp"
[[keyboard.bindings]]
mods = "Control"
key = "PageDown"
action = "ScrollPageDown"
[[keyboard.bindings]]
mods = "Control|Shift"
key = "PageUp"
action = "ScrollPageUp"
[[keyboard.bindings]]
mods = "Control|Shift"
key = "PageDown"
action = "ScrollPageDown"
''; '';
}; };
} }

View File

@@ -1,10 +0,0 @@
{ ... }:
{
sane.programs.animatch = {
persist.byStore.plaintext = [
# game progress
".config/Holy Pangolin/Animatch"
".local/share/Holy Pangolin/Animatch" # i think this one might be wrong
];
};
}

View File

@@ -20,7 +20,6 @@ in
"sane-scripts.bt-show" "sane-scripts.bt-show"
]; ];
"sane-scripts.dev" = declPackageSet [ "sane-scripts.dev" = declPackageSet [
"sane-scripts.clone"
"sane-scripts.dev-cargo-loop" "sane-scripts.dev-cargo-loop"
"sane-scripts.git-init" "sane-scripts.git-init"
]; ];
@@ -42,12 +41,14 @@ in
"sane-scripts.secrets-unlock" "sane-scripts.secrets-unlock"
"sane-scripts.secrets-update-keys" "sane-scripts.secrets-update-keys"
"sane-scripts.shutdown" "sane-scripts.shutdown"
"sane-scripts.ssl-dump"
"sane-scripts.sudo-redirect" "sane-scripts.sudo-redirect"
"sane-scripts.sync-from-servo" "sane-scripts.sync-from-servo"
"sane-scripts.tag-music"
"sane-scripts.vpn" "sane-scripts.vpn"
"sane-scripts.which" "sane-scripts.which"
"sane-scripts.wipe" "sane-scripts.wipe-browser"
"sane-scripts.wipe-flare"
"sane-scripts.wipe-fractal"
]; ];
"sane-scripts.sys-utils" = declPackageSet [ "sane-scripts.sys-utils" = declPackageSet [
"sane-scripts.ip-port-forward" "sane-scripts.ip-port-forward"
@@ -108,8 +109,6 @@ in
"util-linux" # lsblk, lscpu, etc "util-linux" # lsblk, lscpu, etc
"wget" "wget"
"wirelesstools" # iwlist "wirelesstools" # iwlist
"xq" # jq for XML
# "zfs" # doesn't cross-compile (requires samba)
]; ];
sysadminExtraUtils = declPackageSet [ sysadminExtraUtils = declPackageSet [
"backblaze-b2" "backblaze-b2"
@@ -144,7 +143,6 @@ in
"lshw" "lshw"
# "memtester" # "memtester"
"mercurial" # hg "mercurial" # hg
"mimeo" # like xdg-open
"neovim" # needed as a user package, for swap persistence "neovim" # needed as a user package, for swap persistence
# "nettools" # "nettools"
# "networkmanager" # "networkmanager"
@@ -174,7 +172,7 @@ in
"zsh" "zsh"
]; ];
pcConsoleUtils = declPackageSet [ desktopConsoleUtils = declPackageSet [
"gh" # MS GitHub cli "gh" # MS GitHub cli
"nix-index" "nix-index"
"nixpkgs-review" "nixpkgs-review"
@@ -183,19 +181,17 @@ in
]; ];
consoleMediaUtils = declPackageSet [ consoleMediaUtils = declPackageSet [
"catt" # cast videos to chromecast
"ffmpeg" "ffmpeg"
"go2tv" # cast videos to UPNP/DLNA device (i.e. tv).
"imagemagick" "imagemagick"
"sox" "sox"
"yt-dlp" "yt-dlp"
]; ];
pcTuiApps = declPackageSet [ tuiApps = declPackageSet [
"aerc" # email client "aerc" # email client
"msmtp" # sendmail "msmtp" # sendmail
"offlineimap" # email mailbox sync "offlineimap" # email mailbox sync
# "sfeed" # RSS fetcher "sfeed" # RSS fetcher
"visidata" # TUI spreadsheet viewer/editor "visidata" # TUI spreadsheet viewer/editor
"w3m" # web browser "w3m" # web browser
]; ];
@@ -222,14 +218,9 @@ in
cargo.persist.byStore.plaintext = [ ".cargo" ]; cargo.persist.byStore.plaintext = [ ".cargo" ];
# auth token, preferences
delfin.persist.byStore.private = [ ".config/delfin" ];
# creds, but also 200 MB of node modules, etc # creds, but also 200 MB of node modules, etc
discord.persist.byStore.private = [ ".config/discord" ]; discord.persist.byStore.private = [ ".config/discord" ];
endless-sky.persist.byStore.plaintext = [ ".local/share/endless-sky" ];
# `emote` will show a first-run dialog based on what's in this directory. # `emote` will show a first-run dialog based on what's in this directory.
# mostly, it just keeps a LRU of previously-used emotes to optimize display order. # mostly, it just keeps a LRU of previously-used emotes to optimize display order.
# TODO: package [smile](https://github.com/mijorus/smile) for probably a better mobile experience. # TODO: package [smile](https://github.com/mijorus/smile) for probably a better mobile experience.
@@ -246,8 +237,6 @@ in
# TODO: we can populate gh's stuff statically; it even lets us use the same oauth across machines # TODO: we can populate gh's stuff statically; it even lets us use the same oauth across machines
gh.persist.byStore.private = [ ".config/gh" ]; gh.persist.byStore.private = [ ".config/gh" ];
gnome-2048.persist.byStore.plaintext = [ ".local/share/gnome-2048/scores" ];
"gnome.gnome-maps".persist.byStore.plaintext = [ ".cache/shumate" ]; "gnome.gnome-maps".persist.byStore.plaintext = [ ".cache/shumate" ];
"gnome.gnome-maps".persist.byStore.private = [ ".local/share/maps-places.json" ]; "gnome.gnome-maps".persist.byStore.private = [ ".local/share/maps-places.json" ];
@@ -264,24 +253,22 @@ in
requests requests
]); ]);
shattered-pixel-dungeon.persist.byStore.plaintext = [ ".local/share/.shatteredpixel/shattered-pixel-dungeon" ]; # creds, media
signal-desktop.persist.byStore.private = [ ".config/Signal" ];
# printer/filament settings # printer/filament settings
slic3r.persist.byStore.plaintext = [ ".Slic3r" ]; slic3r.persist.byStore.plaintext = [ ".Slic3r" ];
space-cadet-pinball.persist.byStore.plaintext = [ ".local/share/SpaceCadetPinball" ];
superTux.persist.byStore.plaintext = [ ".local/share/supertux2" ];
tdesktop.persist.byStore.private = [ ".local/share/TelegramDesktop" ]; tdesktop.persist.byStore.private = [ ".local/share/TelegramDesktop" ];
tokodon.persist.byStore.private = [ ".cache/KDE/tokodon" ]; tokodon.persist.byStore.private = [ ".cache/KDE/tokodon" ];
vvvvvv.persist.byStore.plaintext = [ ".local/share/VVVVVV" ];
whalebird.persist.byStore.private = [ ".config/Whalebird" ]; whalebird.persist.byStore.private = [ ".config/Whalebird" ];
yarn.persist.byStore.plaintext = [ ".cache/yarn" ]; yarn.persist.byStore.plaintext = [ ".cache/yarn" ];
# zcash coins. safe to delete, just slow to regenerate (10-60 minutes)
zecwallet-lite.persist.byStore.private = [ ".zcash" ];
}; };
programs.feedbackd = lib.mkIf config.sane.programs.feedbackd.enabled { programs.feedbackd = lib.mkIf config.sane.programs.feedbackd.enabled {

View File

@@ -1,22 +0,0 @@
{ pkgs, ... }:
{
sane.programs.audacity = {
package = pkgs.audacity.override {
# wxGTK32 uses webkitgtk-4.0.
# audacity doesn't actually need webkit though, so diable to reduce closure
wxGTK32 = pkgs.wxGTK32.override {
withWebKit = false;
};
};
# disable first-run splash screen
fs.".config/audacity/audacity.cfg".file.text = ''
PrefsVersion=1.1.1r1
[GUI]
ShowSplashScreen=0
[Version]
Major=3
Minor=4
'';
};
}

View File

@@ -1,29 +0,0 @@
# use like:
# - catt -d lgtv_chrome cast ./path/to.mp4
#
# support matrix:
# - webm: audio only
# - mp4: audio + video
{ config, lib, ... }:
let
cfg = config.sane.programs.catt;
in
{
sane.programs.catt = {
fs.".config/catt/catt.cfg".symlink.text = ''
[options]
device = lgtv_chrome
[aliases]
lgtv_chrome = 10.78.79.106
'';
};
# necessary to cast local files
networking.firewall.allowedTCPPortRanges = lib.mkIf cfg.enabled [
{
from = 45000;
to = 47000;
}
];
}

View File

@@ -1,8 +1,40 @@
{ pkgs, ... }: { pkgs, ... }:
let
chattyNoOauth = pkgs.chatty.override {
# the OAuth feature (presumably used for web-based logins) pulls a full webkitgtk.
# especially when using the gtk3 version of evolution-data-server, it's an ancient webkitgtk_4_1.
# disable OAuth for a faster build & smaller closure
evolution-data-server = pkgs.evolution-data-server.override {
enableOAuth2 = false;
gnome-online-accounts = pkgs.gnome-online-accounts.override {
# disables the upstream "goabackend" feature -- presumably "Gnome Online Accounts Backend"
# frees us from webkit_4_1, in turn.
enableBackend = false;
gvfs = pkgs.gvfs.override {
# saves 20 minutes of build time, for unused feature
samba = null;
};
};
};
};
chatty-latest = pkgs.chatty-latest.override {
evolution-data-server-gtk4 = pkgs.evolution-data-server-gtk4.override {
gnome-online-accounts = pkgs.gnome-online-accounts.override {
# disables the upstream "goabackend" feature -- presumably "Gnome Online Accounts Backend"
# frees us from webkit_4_1, in turn.
enableBackend = false;
gvfs = pkgs.gvfs.override {
# saves 20 minutes of build time and cross issues, for unused feature
samba = null;
};
};
};
};
in
{ {
sane.programs.chatty = { sane.programs.chatty = {
# package = chattyNoOauth; # package = chattyNoOauth;
package = pkgs.chatty-latest; package = chatty-latest;
suggestedPrograms = [ "gnome-keyring" ]; suggestedPrograms = [ "gnome-keyring" ];
persist.byStore.private = [ persist.byStore.private = [
".local/share/chatty" # matrix avatars and files ".local/share/chatty" # matrix avatars and files

View File

@@ -1,184 +1,32 @@
#!/bin/sh
#!/usr/bin/env nix-shell #!/usr/bin/env nix-shell
#!nix-shell -i bash #!nix-shell -i bash
usage() { full=$(cat /sys/class/power_supply/axp20x-battery/charge_full_design)
echo "usage: battery_estimate [options...]" rate=$(cat /sys/class/power_supply/axp20x-battery/current_now)
echo perc=$(cat /sys/class/power_supply/axp20x-battery/capacity)
echo "pretty-prints a battery estimate (icon to indicate state, and a duration estimate)" perc_left=$((100 - $perc))
echo
echo "options:"
echo " --debug: output additional information, to stderr"
echo " --minute-suffix <string>: use the provided string as a minutes suffix"
echo " --hour-suffix <string>: use the provided string as an hours suffix"
echo " --icon-suffix <string>: use the provided string as an icon suffix"
echo " --percent-suffix <string>: use the provided string when displaying percents"
}
# these icons come from sxmo; they only render in nerdfonts # these icons come from sxmo; they only render in nerdfonts
icon_bat_chg=("󰢟" "󱊤" "󱊥" "󰂅") bat_dis="󱊢"
icon_bat_dis=("󰂎" "󱊡" "󱊢" "󱊣") bat_chg="󱊥"
suffix_icon="" # thin space
suffix_percent="%"
# suffix_icon=" "
# render time like: 2ʰ08ᵐ
# unicode sub/super-scripts: <https://en.wikipedia.org/wiki/Unicode_subscripts_and_superscripts>
# symbol_hr="ʰ"
# symbol_min="ᵐ"
# render time like: 2ₕ08ₘ
# symbol_hr="ₕ"
# symbol_min="ₘ"
# render time like: 2h08m
# symbol_hr="h"
# symbol_min="m"
# render time like: 2:08
# symbol_hr=":"
# symbol_min=
# render time like: 208⧗
symbol_hr=""
symbol_min="⧗"
# variants:
# symbol_hr=":"
# symbol_min="⧖"
# symbol_min="⌛"
# render time like: 2'08"
# symbol_hr="'"
# symbol_min='"'
log() {
if [ "$BATTERY_ESTIMATE_DEBUG" = "1" ]; then
printf "$@" >&2
echo >&2
fi
}
render_icon() {
# args:
# 1: "chg" or "dis"
# 2: current battery percentage
level=$(($2 / 25))
level=$(($level > 3 ? 3 : $level))
level=$(($level < 0 ? 0 : $level))
log "icon: %s %d" "$1" "$level"
if [ "$1" = "dis" ]; then
printf "%s" "${icon_bat_dis[$level]}"
elif [ "$1" = "chg" ]; then
printf "%s" "${icon_bat_chg[$level]}"
fi
}
try_path() {
# assigns output variables:
# - perc, perc_from_full (0-100)
# - full, rate (pos means charging)
if [ -f "$1/capacity" ]; then
log "perc, perc_from_full from %s" "$1/capacity"
perc=$(cat "$1/capacity")
perc_from_full=$((100 - $perc))
fi
if [ -f "$1/charge_full_design" ] && [ -f "$1/current_now" ]; then
log "full, rate from %s and %s" "$1/charge_full_design" "$1/current_now"
# current is positive when charging
full=$(cat "$1/charge_full_design")
rate=$(cat "$1/current_now")
elif [ -f "$1/energy_full" ] && [ -f "$1/power_now" ]; then
log "full, rate from %s and %s" "$1/energy_full" "$1/power_now"
# power_now is positive when discharging
full=$(cat "$1/energy_full")
rate=-$(cat "$1/power_now")
elif [ -f "$1/energy_full" ] && [ -f "$1/energy_now" ]; then
log "full, rate from %s and %s" "$1/energy_full" "$1/energy_now"
log " this is a compatibility path for legacy Thinkpad batteries which do not populate the 'power_now' field, and incorrectly populate 'energy_now' with power info"
# energy_now is positive when discharging
full=$(cat "$1/energy_full")
rate=-$(cat "$1/energy_now")
fi
}
try_all_paths() {
try_path "/sys/class/power_supply/axp20x-battery" # Pinephone
try_path "/sys/class/power_supply/BAT0" # Thinkpad
log "perc: %d, perc_from_full: %d" "$perc" "$perc_from_full"
log "full: %f, rate: %f" "$full" "$rate"
log " rate > 0 means charging, else discharging"
}
fmt_minutes() { fmt_minutes() {
# args:
# 1: icon to render
# 2: string to show if charge/discharge time is indefinite
# 3: minutes to stable state (i.e. to full charge or full discharge)
# - we work in minutes instead of hours for precision: bash math is integer-only
log "charge/discharge time: %f min" "$3"
# args: <battery symbol> <text if ludicrous estimate> <estimated minutes to full/empty> # args: <battery symbol> <text if ludicrous estimate> <estimated minutes to full/empty>
if [ -n "$3" ] && [ "$3" -lt 1440 ]; then if [[ $3 -gt 1440 ]]; then
printf "%s %s" "$1" "$2" # more than 1d
else
hr=$(($3 / 60)) hr=$(($3 / 60))
hr_in_min=$(($hr * 60)) hr_in_min=$(($hr * 60))
min=$(($3 - $hr_in_min)) min=$(($3 - $hr_in_min))
printf "%s%s%d%s%02d%s" "$1" "$suffix_icon" "$hr" "$symbol_hr" "$min" "$symbol_min" printf "%s %dh%02dm" "$1" "$hr" "$min"
else
log "charge/discharge duration > 1d"
printf "%s%s%s" "$1" "$suffix_icon" "$2" # more than 1d
fi fi
} }
pretty_output() { if [[ $rate -lt 0 ]]; then
if [ -n "$perc" ]; then # discharging
duration="" fmt_minutes "$bat_dis" '∞' "$(($full * 60 * $perc / (-100 * $rate)))"
if [ "$rate" -gt 0 ]; then elif [[ $rate -gt 0 ]]; then
log "charging" # charging
icon="$(render_icon chg $perc)" fmt_minutes "$bat_chg" '100%' "$(($full * 60 * $perc_left / (100 * $rate)))"
duration="$(($full * 60 * $perc_from_full / (100 * $rate)))" else
else echo "$bat_dis $perc%"
log "discharging" fi
icon="$(render_icon dis $perc)"
if [ "$rate" -lt 0 ]; then
duration="$(($full * 60 * $perc / (-100 * $rate)))"
fi
fi
fmt_minutes "$icon" "$perc$suffix_percent" "$duration"
fi
}
while [ "$#" -gt 0 ]; do
case "$1" in
"--debug")
shift
BATTERY_ESTIMATE_DEBUG=1
;;
"--icon-suffix")
shift
suffix_icon="$1"
shift
;;
"--hour-suffix")
shift
symbol_hr="$1"
shift
;;
"--minute-suffix")
shift
symbol_min="$1"
shift
;;
"--percent-suffix")
shift
suffix_percent="$1"
shift
;;
*)
usage
exit 1
;;
esac
done
try_all_paths
pretty_output

View File

@@ -3,80 +3,48 @@
-- - can also use #rrggbb syntax -- - can also use #rrggbb syntax
-- example configs: <https://forum.manjaro.org/t/conky-showcase-2022/97123> -- example configs: <https://forum.manjaro.org/t/conky-showcase-2022/97123>
-- example configs: <https://www.reddit.com/r/Conkyporn/> -- example configs: <https://www.reddit.com/r/Conkyporn/>
--
-- exec options:
-- `exec <cmd>` => executes the command, synchronously, renders its output as text
-- `texeci <interval_sec> <cmd>` => executes the command periodically, async (to not block render), renders as text
-- `pexec <cmd>` => executes the command, synchronously, parses its output
conky.config = { conky.config = {
out_to_wayland = true, out_to_wayland = true,
update_interval = 10, update_interval = 10,
alignment = 'middle_middle', alignment = 'middle_middle',
own_window_type = 'desktop', own_window_type = 'desktop',
-- own_window_argb_value: opacity of the background (0-255) -- own_window_argb_value: opacity of the background (0-255)
own_window_argb_value = 0, own_window_argb_value = 0,
-- own_window_argb_value = 92, -- own_window_argb_value = 92,
-- own_window_colour = '#beebe5', -- beebe5 matches nixos flake bg color -- own_window_colour = '#beebe5', -- beebe5 matches nixos flake bg color
-- "border" pads the entire conky window -- "border" pads the entire conky window
-- this can be used to control the extent of the own_window background -- this can be used to control the extent of the own_window background
border_inner_margin = 8, border_inner_margin = 8,
-- optionally, actually draw borders -- optionally, actually draw borders
-- draw_borders = true, -- draw_borders = true,
-- shades are drop-shadows, outline is the centered version. both apply to text only -- shades are drop-shadows, outline is the centered version. both apply to text only
draw_shades = true, draw_shades = true,
draw_outline = false, draw_outline = false,
default_shade_color = '#beebe5', default_shade_color = '#beebe5',
default_outline_color = '#beebe5', default_outline_color = '#beebe5',
font = 'sans-serif:size=8', font = 'sans-serif:size=8',
use_xft = true, use_xft = true,
default_color = '#ffffff', default_color = '#ffffff',
color1 = '000000', color1 = '000000',
color2 = '404040', color2 = '404040',
} }
vars = { -- texeci <interval_sec> <cmd>: run the command periodically, _in a separate thread_ so as not to block rendering
-- kBps = 'K/s',
kBps = 'ᴷᐟˢ',
-- percent = '%',
-- percent = '﹪',
percent = '٪',
-- percent = '⁒',
-- percent = '',
icon_suffix = nil,
hour_suffix = nil,
minute_suffix = '${font sans-serif:size=14}${color2}⧗',
}
bat_args = ""
if vars.icon_suffix ~= nil then
bat_args = bat_args .. " --icon-suffix '" .. vars.icon_suffix .. "'"
end
if vars.hour_suffix ~= nil then
bat_args = bat_args .. " --hour-suffix '" .. vars.hour_suffix .. "'"
end
if vars.minute_suffix ~= nil then
bat_args = bat_args .. " --minute-suffix '" .. vars.minute_suffix .. "'"
end
if vars.percent ~= nil then
bat_args = bat_args .. " --percent-suffix '" .. vars.percent .. "'"
end
-- N.B.: `[[ <text> ]]` is Lua's multiline string literal
conky.text = [[ conky.text = [[
${color1}${shadecolor 707070}${font sans-serif:size=50:style=Bold}${alignc}${exec date +"%H:%M"}${font} ${color1}${shadecolor 707070}${font sans-serif:size=50:style=Bold}${alignc}${exec date +"%H:%M"}${font}
${color2}${shadecolor a4d7d0}${font sans-serif:size=20}${alignc}${exec date +"%a %d %b"}${font} ${color2}${shadecolor a4d7d0}${font sans-serif:size=20}${alignc}${exec date +"%a %d %b"}${font}
${color1}${shadecolor}${font sans-serif:size=22:style=Bold}${alignc}${execp @bat@ ]] .. bat_args .. [[ }${font} ${color1}${shadecolor}${font sans-serif:size=22:style=Bold}${alignc}${exec @bat@ }${font}
${color1}${shadecolor}${font sans-serif:size=20:style=Bold}${alignc}${texeci 600 @weather@ }${font} ${color1}${shadecolor}${font sans-serif:size=20:style=Bold}${alignc}${texeci 600 @weather@ }${font}
${color2}${shadecolor a4d7d0}${font sans-serif:size=16}${alignc}⇅ ${downspeedf wlan0}]] .. vars.kBps .. [[${font} ${color2}${shadecolor a4d7d0}${font sans-serif:size=16}${alignc}⇅ ${downspeedf wlan0}K/s${font}
${font sans-serif:size=16}${alignc}☵ $memperc]] .. vars.percent .. [[  $cpu]] .. vars.percent .. [[${font} ${font sans-serif:size=16}${alignc}☵ $memperc%  $cpu%${font}
]] ]]

View File

@@ -2,21 +2,16 @@
{ {
imports = [ imports = [
./abaddon.nix
./aerc.nix ./aerc.nix
./alacritty.nix ./alacritty.nix
./animatch.nix
./assorted.nix ./assorted.nix
./audacity.nix
./bemenu.nix ./bemenu.nix
./brave.nix ./brave.nix
./calls.nix ./calls.nix
./cantata.nix ./cantata.nix
./catt.nix
./chatty.nix ./chatty.nix
./conky ./conky
./cozy.nix ./cozy.nix
./dialect.nix
./dino.nix ./dino.nix
./element-desktop.nix ./element-desktop.nix
./epiphany.nix ./epiphany.nix
@@ -34,10 +29,8 @@
./gnome-feeds.nix ./gnome-feeds.nix
./gnome-keyring.nix ./gnome-keyring.nix
./gnome-weather.nix ./gnome-weather.nix
./go2tv.nix
./gpodder.nix ./gpodder.nix
./gthumb.nix ./gthumb.nix
./gtkcord4.nix
./helix.nix ./helix.nix
./imagemagick.nix ./imagemagick.nix
./jellyfin-media-player.nix ./jellyfin-media-player.nix
@@ -45,49 +38,36 @@
./koreader ./koreader
./libreoffice.nix ./libreoffice.nix
./lemoa.nix ./lemoa.nix
./loupe.nix
./mako.nix ./mako.nix
./megapixels.nix
./mepo.nix ./mepo.nix
./mimeo
./mopidy.nix ./mopidy.nix
./mpv.nix ./mpv.nix
./msmtp.nix ./msmtp.nix
./nautilus.nix
./neovim.nix ./neovim.nix
./newsflash.nix ./newsflash.nix
./nheko.nix ./nheko.nix
./nix-index.nix ./nix-index.nix
./notejot.nix
./ntfy-sh.nix ./ntfy-sh.nix
./obsidian.nix ./obsidian.nix
./offlineimap.nix ./offlineimap.nix
./open-in-mpv.nix
./planify.nix
./playerctl.nix ./playerctl.nix
./rhythmbox.nix ./rhythmbox.nix
./ripgrep.nix ./ripgrep.nix
./sfeed.nix ./sfeed.nix
./signal-desktop.nix
./splatmoji.nix ./splatmoji.nix
./spot.nix
./spotify.nix ./spotify.nix
./steam.nix ./steam.nix
./stepmania.nix ./stepmania.nix
./sublime-music.nix ./sublime-music.nix
./supertuxkart.nix
./sway-autoscaler
./swaynotificationcenter.nix ./swaynotificationcenter.nix
./tangram.nix ./tangram.nix
./tor-browser-bundle-bin.nix ./tor-browser-bundle-bin.nix
./tuba.nix ./tuba.nix
./vlc.nix ./vlc.nix
./wike.nix
./wine.nix
./wireshark.nix ./wireshark.nix
./wob.nix
./xarchiver.nix ./xarchiver.nix
./zeal.nix ./zeal.nix
./zecwallet-lite.nix
./zsh ./zsh
]; ];

View File

@@ -1,13 +0,0 @@
{ pkgs, ... }:
{
sane.programs.dialect = {
package = pkgs.dialect.overrideAttrs (upstream: {
# TODO: send upstream
# TODO: figure out how to get audio working
# TODO: move to runtimeDependencies?
buildInputs = upstream.buildInputs ++ [
pkgs.glib-networking # for TLS
];
});
};
}

View File

@@ -40,7 +40,7 @@ in
type = types.submodule { type = types.submodule {
options.autostart = mkOption { options.autostart = mkOption {
type = types.bool; type = types.bool;
default = true; default = false;
}; };
}; };
}; };
@@ -48,7 +48,7 @@ in
persist.byStore.private = [ ".local/share/dino" ]; persist.byStore.private = [ ".local/share/dino" ];
services.dino = { services.dino = {
description = "dino XMPP client"; description = "auto-start and maintain dino XMPP connection";
wantedBy = lib.mkIf cfg.config.autostart [ "default.target" ]; wantedBy = lib.mkIf cfg.config.autostart [ "default.target" ];
serviceConfig = { serviceConfig = {
ExecStart = "${cfg.package}/bin/dino"; ExecStart = "${cfg.package}/bin/dino";

View File

@@ -4,14 +4,9 @@
# - <https://github.com/vector-im/element-desktop/issues/1029#issuecomment-1632688224> # - <https://github.com/vector-im/element-desktop/issues/1029#issuecomment-1632688224>
# - `rm -rf ~/.config/Element/GPUCache` # - `rm -rf ~/.config/Element/GPUCache`
# - <https://github.com/NixOS/nixpkgs/issues/244486> # - <https://github.com/NixOS/nixpkgs/issues/244486>
{ pkgs, ... }: { ... }:
{ {
sane.programs.element-desktop = { sane.programs.element-desktop = {
package = pkgs.element-desktop.override {
# use pre-build electron because otherwise it takes 4 hrs to build from source.
electron = pkgs.electron-bin;
};
# creds/session keys, etc # creds/session keys, etc
persist.byStore.private = [ ".config/Element" ]; persist.byStore.private = [ ".config/Element" ];

View File

@@ -1,6 +1,3 @@
# test with e.g.
# - `fbcli --event proxied-message-new-instant`
{ config, lib, pkgs, ... }: { config, lib, pkgs, ... }:
let let
cfg = config.sane.programs.feedbackd; cfg = config.sane.programs.feedbackd;

View File

@@ -13,14 +13,7 @@ let
mobile-prefs = lib.optionals false pkgs.librewolf-pmos-mobile.extraPrefsFiles; mobile-prefs = lib.optionals false pkgs.librewolf-pmos-mobile.extraPrefsFiles;
# allow easy switching between firefox and librewolf with `defaultSettings`, below # allow easy switching between firefox and librewolf with `defaultSettings`, below
librewolfSettings = { librewolfSettings = {
browser = pkgs.librewolf-unwrapped.overrideAttrs (upstream: { browser = pkgs.librewolf-unwrapped;
# TEMP(2023/11/21): fix eval bug in wrapFirefox
# see: <https://github.com/NixOS/nixpkgs/pull/244591>
passthru = upstream.passthru // {
requireSigning = false;
allowAddonSideload = true;
};
});
extraPrefsFiles = pkgs.librewolf-unwrapped.extraPrefsFiles ++ mobile-prefs; extraPrefsFiles = pkgs.librewolf-unwrapped.extraPrefsFiles ++ mobile-prefs;
libName = "librewolf"; libName = "librewolf";
dotDir = ".librewolf"; dotDir = ".librewolf";
@@ -38,16 +31,25 @@ let
# defaultSettings = firefoxSettings; # defaultSettings = firefoxSettings;
defaultSettings = librewolfSettings; defaultSettings = librewolfSettings;
package = (pkgs.wrapFirefox cfg.browser.browser { addon = name: extid: hash: pkgs.fetchFirefoxAddon {
inherit name hash;
url = "https://addons.mozilla.org/firefox/downloads/latest/${name}/latest.xpi";
# extid can be found by unar'ing the above xpi, and copying browser_specific_settings.gecko.id field
fixedExtid = extid;
};
localAddon = pkg: pkgs.fetchFirefoxAddon {
inherit (pkg) name;
src = "${pkg}/share/mozilla/extensions/\\{ec8030f7-c20a-464f-9b0e-13a3a9e97384\\}/${pkg.extid}.xpi";
fixedExtid = pkg.extid;
};
package = pkgs.wrapFirefox cfg.browser.browser {
# inherit the default librewolf.cfg # inherit the default librewolf.cfg
# it can be further customized via ~/.librewolf/librewolf.overrides.cfg # it can be further customized via ~/.librewolf/librewolf.overrides.cfg
inherit (cfg.browser) extraPrefsFiles libName; inherit (cfg.browser) extraPrefsFiles libName;
nativeMessagingHosts = lib.optionals cfg.addons.browserpass-extension.enable [ extraNativeMessagingHosts = optional cfg.addons.browserpass-extension.enable pkgs.browserpass;
pkgs.browserpass # extraNativeMessagingHosts = [ pkgs.gopass-native-messaging-host ];
] ++ lib.optionals cfg.addons.fxCast.enable [
pkgs.fx-cast-bridge
];
nixExtensions = concatMap (ext: optional ext.enable ext.package) (attrValues cfg.addons); nixExtensions = concatMap (ext: optional ext.enable ext.package) (attrValues cfg.addons);
@@ -104,17 +106,7 @@ let
# NewTabPage = true; # NewTabPage = true;
}; };
# extraPrefs = ... # extraPrefs = ...
}).overrideAttrs (base: { };
# de-associate `ctrl+shift+c` from activating the devtools.
# based on <https://stackoverflow.com/a/54260938>
buildCommand = (base.buildCommand or "") + ''
mkdir omni
${pkgs.buildPackages.unzip}/bin/unzip $out/lib/${cfg.browser.libName}/browser/omni.ja -d omni
rm $out/lib/${cfg.browser.libName}/browser/omni.ja
${pkgs.buildPackages.gnused}/bin/sed -i s'/devtools-commandkey-inspector = C/devtools-commandkey-inspector = VK_F12/' omni/localization/en-US/devtools/startup/key-shortcuts.ftl
pushd omni; ${pkgs.buildPackages.zip}/bin/zip $out/lib/${cfg.browser.libName}/browser/omni.ja -r ./*; popd
'';
});
addonOpts = types.submodule { addonOpts = types.submodule {
options = { options = {
@@ -158,16 +150,6 @@ in
default = {}; default = {};
}; };
sane.programs.firefox.config.addons = { sane.programs.firefox.config.addons = {
fxCast = {
# add a menu to cast to chromecast devices, but it doesn't seem to work very well.
# right click (or shift+rc) a video, then select "cast".
# - asciinema.org: icon appears, but glitches when clicked.
# - youtube.com: no icon appears, even when site is whitelisted.
# future: maybe better to have browser open all videos in mpv, and then use mpv for casting.
# see e.g. `ff2mpv`, `open-in-mpv` (both are packaged in nixpkgs)
package = pkgs.firefox-extensions.fx_cast;
enable = lib.mkDefault false;
};
browserpass-extension = { browserpass-extension = {
package = pkgs.firefox-extensions.browserpass-extension; package = pkgs.firefox-extensions.browserpass-extension;
enable = lib.mkDefault true; enable = lib.mkDefault true;
@@ -176,10 +158,6 @@ in
package = pkgs.firefox-extensions.bypass-paywalls-clean; package = pkgs.firefox-extensions.bypass-paywalls-clean;
enable = lib.mkDefault true; enable = lib.mkDefault true;
}; };
ctrl-shift-c-should-copy = {
package = pkgs.firefox-extensions.ctrl-shift-c-should-copy;
enable = lib.mkDefault false; # prefer patching firefox source code, so it works in more places
};
ether-metamask = { ether-metamask = {
package = pkgs.firefox-extensions.ether-metamask; package = pkgs.firefox-extensions.ether-metamask;
enable = lib.mkDefault false; # until i can disable the first-run notification enable = lib.mkDefault false; # until i can disable the first-run notification
@@ -188,10 +166,6 @@ in
package = pkgs.firefox-extensions.i2p-in-private-browsing; package = pkgs.firefox-extensions.i2p-in-private-browsing;
enable = lib.mkDefault config.services.i2p.enable; enable = lib.mkDefault config.services.i2p.enable;
}; };
open-in-mpv = {
package = pkgs.firefox-extensions.open-in-mpv;
enable = lib.mkDefault config.sane.programs.open-in-mpv.enabled;
};
sidebery = { sidebery = {
package = pkgs.firefox-extensions.sidebery; package = pkgs.firefox-extensions.sidebery;
enable = lib.mkDefault true; enable = lib.mkDefault true;
@@ -214,10 +188,6 @@ in
sane.programs.firefox = { sane.programs.firefox = {
inherit package; inherit package;
suggestedPrograms = [
"open-in-mpv"
];
mime.associations = let mime.associations = let
inherit (cfg.browser) desktop; inherit (cfg.browser) desktop;
in { in {
@@ -259,21 +229,8 @@ in
// scrollbar configuration, see: <https://artemis.sh/2023/10/12/scrollbars.html> // scrollbar configuration, see: <https://artemis.sh/2023/10/12/scrollbars.html>
// style=4 gives rectangular scrollbars // style=4 gives rectangular scrollbars
// could also enable "always show scrollbars" in about:preferences -- not sure what the actual pref name for that is // could also enable "always show scrollbars" in about:preferences -- not sure what the actual pref name for that is
// note that too-large scrollbars (like 50px wide) tend to obscure content (and make buttons unclickable) defaultPref("widget.non-native-theme.scrollbar.size.override", 50);
defaultPref("widget.non-native-theme.scrollbar.size.override", 20);
defaultPref("widget.non-native-theme.scrollbar.style", 4); defaultPref("widget.non-native-theme.scrollbar.style", 4);
// disable inertial/kinetic/momentum scrolling because it just gets in the way on touchpads
// source: <https://kparal.wordpress.com/2019/10/31/disabling-kinetic-scrolling-in-firefox/>
defaultPref("apz.gtk.kinetic_scroll.enabled", false);
// auto-dispatch mpv:// URIs to xdg-open without prompting.
// can do this with other protocols too (e.g. matrix?). see about:config for common handlers.
defaultPref("network.protocol-handler.external.mpv", true);
// element:// for Element matrix client
defaultPref("network.protocol-handler.external.element", true);
// matrix: for Nheko matrix client
defaultPref("network.protocol-handler.external.matrix", true);
''; '';
fs."${cfg.browser.dotDir}/default".dir = {}; fs."${cfg.browser.dotDir}/default".dir = {};
# instruct Firefox to put the profile in a predictable directory (so we can do things like persist just it). # instruct Firefox to put the profile in a predictable directory (so we can do things like persist just it).

View File

@@ -1,18 +1,15 @@
# Flare is a 3rd-party GTK4 Signal app. # Flare is a 3rd-party GTK4 Signal app.
# UI is effectively a clone of Fractal. # UI is effectively a clone of Fractal.
# #
### compatibility: # compatibility:
# - desko: works fine. pairs, and exchanges contact list (but not message history) with the paired device. exchanges future messages fine. # - desko: works fine. pairs, and exchanges contact list (but not message history) with the paired device. exchanges future messages fine.
# - moby (cross compiled flare-signal-nixified): nope. it pairs, but can only *receive* messages and never *send* them. # - moby (cross compiled flare-signal-nixified): nope. it pairs, but can only *receive* messages and never *send* them.
# - even `rsync`ing the data and keyrings from desko -> moby, still fails in that same manner. # - even `rsync`ing the data and keyrings from desko -> moby, still fails in that same manner.
# - console shows error messages. quite possibly an endianness mismatch somewhere # - console shows error messages. quite possibly an endianness mismatch somewhere
# - moby (partially-emulated flare-signal): works! pairs and can send/receive messages, same as desko. # - moby (partially-emulated flare-signal): works! pairs and can send/receive messages, same as desko.
# #
### debugging: # error signatures (to reset, run `sane-wipe-fractal`):
# - `RUST_LOG=flare=trace flare` # - upon sending a message, the other side receives it, but Signal desktop gets "A message from Colin could not be delivered" and the local CLI shows:
#
### error signatures (to reset, run `sane-wipe flare`):
#### upon sending a message, the other side receives it, but Signal desktop gets "A message from Colin could not be delivered" and the local CLI shows:
# ``` # ```
# ERROR libsignal_service::websocket] SignalWebSocket: Websocket error: SignalWebSocket: end of application request stream; socket closing # ERROR libsignal_service::websocket] SignalWebSocket: Websocket error: SignalWebSocket: end of application request stream; socket closing
# ERROR presage::manager] Error opening envelope: ProtobufDecodeError(DecodeError { description: "invalid tag value: 0", stack: [("Content", "data_message")] }), message will be skipped! # ERROR presage::manager] Error opening envelope: ProtobufDecodeError(DecodeError { description: "invalid tag value: 0", stack: [("Content", "data_message")] }), message will be skipped!
@@ -21,8 +18,7 @@
# - this occurs on moby, desko, `flare-signal` and `flare-signal-nixified` # - this occurs on moby, desko, `flare-signal` and `flare-signal-nixified`
# - the Websocket error seems to be unrelated, occurs during normal/good operation # - the Websocket error seems to be unrelated, occurs during normal/good operation
# - related issues: <https://github.com/whisperfish/presage/issues/152> # - related issues: <https://github.com/whisperfish/presage/issues/152>
# # error when sending from Flare to other Flare device:
#### error when sending from Flare to other Flare device:
# - ``` # - ```
# ERROR libsignal_protocol::session_cipher] Message from <UUID>.3 failed to decrypt; sender ratchet public key <key> message counter 1 # ERROR libsignal_protocol::session_cipher] Message from <UUID>.3 failed to decrypt; sender ratchet public key <key> message counter 1
# No current session # No current session
@@ -30,32 +26,6 @@
# ``` # ```
# - but signal iOS will still read it. # - but signal iOS will still read it.
# #
#### HTTP 405 when linking flare to iOS signal:
# [DEBUG libsignal_service_hyper::push_service] HTTP request PUT https://chat.signal.org/v1/devices/{uuid}.{timestamp?}:{b64-string}
# [TRACE libsignal_service_hyper::push_service] Unhandled response 405 with body: {"code":405,"message":"HTTP 405 Method Not Allowed"}
# [ERROR flare::gui::error_dialog] ErrorDialog displaying error: Something unexpected happened with the signal backend. Please retry later.
# [TRACE flare::gui::error_dialog] ErrorDialog full error: Presage(
# ProvisioningError(
# ServiceError(
# UnhandledResponseCode {
# http_code: 405,
# },
# ),
# ),
# )
# flare matrix suggests the signal endpoint has changed:
# - "/v1/device/link instead of confirming via /v1/devices/{I'd}"
# - this endpoint is declared in libsignal-service-rs (used both by flare and presage)
# - libsignal-service/src/provisioning/manager.rs
# - libsignal-service issues a put_json to that URL (i.e. HTTP PUT)
# - libsignal-service is "based on" the official rust signal API <https://github.com/signalapp/libsignal>
# - did these guys recently change it?
# - no, but Signal-Desktop did. see ccb5eb0dd2 from 2023/08/29
# - that's a fairly involved change.
# - signalcli is reporting this same error: <https://github.com/AsamK/signal-cli/issues/1399>
# - Mixin Messenger / libsignal_protocol_dart doesn't seem to be reporting any issue
# - <https://github.com/MixinNetwork/flutter-app>
#
# well, seems to have unpredictable errors particularly when being used on multiple devices. # well, seems to have unpredictable errors particularly when being used on multiple devices.
# desktop _seems_ more reliable than on mobile, but not confident. # desktop _seems_ more reliable than on mobile, but not confident.

View File

@@ -23,7 +23,7 @@ let
in in
{ {
sane.programs.fractal = { sane.programs.fractal = {
package = pkgs.fractal-nixified.optimized; package = pkgs.fractal-nixified;
# package = pkgs.fractal-latest; # package = pkgs.fractal-latest;
# package = pkgs.fractal-next; # package = pkgs.fractal-next;
@@ -41,7 +41,6 @@ in
# XXX by default fractal stores its state in ~/.local/share/<build-profile>/<UUID>. # XXX by default fractal stores its state in ~/.local/share/<build-profile>/<UUID>.
".local/share/hack" # for debug-like builds ".local/share/hack" # for debug-like builds
".local/share/stable" # for normal releases ".local/share/stable" # for normal releases
".local/share/fractal" # for version 5+, i think?
]; ];
suggestedPrograms = [ "gnome-keyring" ]; suggestedPrograms = [ "gnome-keyring" ];

View File

@@ -3,23 +3,9 @@
# - it uses webkitgtk_4_1, which is expensive to build. # - it uses webkitgtk_4_1, which is expensive to build.
# could be upgraded to webkitgtk latest if upgraded to gtk4 # could be upgraded to webkitgtk latest if upgraded to gtk4
# <https://gitlab.gnome.org/GNOME/geary/-/issues/1212> # <https://gitlab.gnome.org/GNOME/geary/-/issues/1212>
{ config, lib, ... }: { ... }:
let
cfg = config.sane.programs."gnome.geary";
in
{ {
sane.programs."gnome.geary" = { sane.programs."gnome.geary" = {
configOption = with lib; mkOption {
default = {};
type = types.submodule {
options.autostart = mkOption {
type = types.bool;
default = false;
};
};
};
slowToBuild = true; # uses webkitgtk 4.1
persist.byStore.private = [ persist.byStore.private = [
# attachments, and email -- contained in a sqlite db # attachments, and email -- contained in a sqlite db
".local/share/geary" ".local/share/geary"
@@ -65,18 +51,5 @@ in
transport_security=transport transport_security=transport
credentials=use-incoming credentials=use-incoming
''; '';
secrets.".config/geary/account_02/geary.ini" = ../../../secrets/common/geary_account_02.ini.bin;
services.geary = {
description = "Geary email client";
wantedBy = lib.mkIf cfg.config.autostart [ "default.target" ];
serviceConfig = {
ExecStart = "${cfg.package}/bin/geary";
Type = "simple";
Restart = "always";
RestartSec = "20s";
};
};
}; };
} }

View File

@@ -13,7 +13,6 @@ in
user.name = "Colin"; user.name = "Colin";
user.email = "colin@uninsane.org"; user.email = "colin@uninsane.org";
alias.amend = "commit --amend --no-edit";
alias.br = "branch"; alias.br = "branch";
alias.co = "checkout"; alias.co = "checkout";
alias.cp = "cherry-pick"; alias.cp = "cherry-pick";
@@ -24,7 +23,6 @@ in
alias.st = "status"; alias.st = "status";
alias.stat = "status"; alias.stat = "status";
diff.noprefix = true; #< don't show a/ or b/ prefixes in diffs
# difftastic docs: # difftastic docs:
# - <https://difftastic.wilfred.me.uk/git.html> # - <https://difftastic.wilfred.me.uk/git.html>
diff.tool = "difftastic"; diff.tool = "difftastic";

View File

@@ -1,38 +0,0 @@
# TROUBLESHOOTING:
# - turn the tv off and on again (no, really...)
#
# SANITY CHECKS:
# - `go2tv -u 'https://uninsane.org/share/AmenBreak.mp4'`
# - LGTV: works, but not seekable
# - `go2tv -u 'https://youtu.be/p3G5IXn0K7A'`
# - LGTV: FAILS ("this file cannot be recognized")
# - no fix via transcoding, altering the URI, etc.
# - workable if you use an invidious frontend, but you lose seeking.
# - e.g. `go2tv -u 'https://inv.us.projectsegfau.lt/latest_version?id=qBzjHU_zEwM&itag=18'`
# - e.g. `go2tv -tc -u 'https://yt.artemislena.eu/latest_version?id=qBzjHU_zEwM&itag=22'`
# - sometimes transcoding is needed, sometimes not...
# - `go2tv -v /mnt/servo-media/Videos/Shows/bebop/session1.mkv`
# - LGTV: works
# - `go2tv -tc -v /mnt/servo-media/Videos/Shows/bebop/session1.mkv`
# - LGTV: works
#
# WHEN TO TRANSCODE:
# - mkv container + mpeg-2 video + AC-3/48k stereo audio:
# - LGTV: no transcoding needed
# - mkv container + H.264 video + AAC/48k 5.1 audio:
# - LGTV: no transcoding needed
# - mp4 container + H.264 video + MP3/48k stereo audio:
# - LGTV: no transcoding needed
# - mp4 container + H.264 video + AAC/44k1 stereo audio:
# - LGTV: no transcoding needed
# - mkv container + H.265 video + E-AC-3/48k stereo audio:
# - LGTV: no transcoding needed
{ config, lib, pkgs, ... }:
let
cfg = config.sane.programs.go2tv;
in
{
# for serving local files
# see: go2tv/soapcalls/utils/iptools.go
networking.firewall.allowedTCPPorts = lib.mkIf cfg.enabled [ 3500 ];
}

View File

@@ -5,7 +5,7 @@
let let
feeds = sane-lib.feeds; feeds = sane-lib.feeds;
all-feeds = config.sane.feeds; all-feeds = config.sane.feeds;
wanted-feeds = feeds.filterByFormat [ "podcast" "video" ] all-feeds; wanted-feeds = feeds.filterByFormat ["podcast"] all-feeds;
in { in {
sane.programs.gpodder = { sane.programs.gpodder = {
package = pkgs.gpodder-adaptive-configured.overrideAttrs (base: { package = pkgs.gpodder-adaptive-configured.overrideAttrs (base: {

View File

@@ -3,7 +3,6 @@
sane.programs.gthumb = { sane.programs.gthumb = {
# compile without webservices to avoid the expensive webkitgtk dependency # compile without webservices to avoid the expensive webkitgtk dependency
package = pkgs.gthumb.override { withWebservices = false; }; package = pkgs.gthumb.override { withWebservices = false; };
mime.priority = 200; # gthumb is kinda bloated image/gallery viewer
mime.associations = { mime.associations = {
"image/gif" = "org.gnome.gThumb.desktop"; "image/gif" = "org.gnome.gThumb.desktop";
"image/heif" = "org.gnome.gThumb.desktop"; # apple codec "image/heif" = "org.gnome.gThumb.desktop"; # apple codec

View File

@@ -1,51 +0,0 @@
# FIRST-TIME SETUP:
# - disable notification sounds: hamburger menu in bottom-left -> preferences
# - notification sounds can be handled by swaync
{ config, lib, pkgs, ... }:
let
cfg = config.sane.programs.gtkcord4;
in
{
sane.programs.gtkcord4 = {
configOption = with lib; mkOption {
default = {};
type = types.submodule {
options.autostart = mkOption {
type = types.bool;
default = true;
};
};
};
package = pkgs.gtkcord4.overrideAttrs (upstream: {
postConfigure = (upstream.postConfigure or "") + ''
# gtkcord4 uses go-keyring to interface with the org.freedesktop.secrets provider (i.e. gnome-keyring).
# go-keyring hardcodes `login.keyring` as the keyring to store secrets in, instead of reading `~/.local/share/keyring/default`.
# `login.keyring` seems to be a special keyring preconfigured (by gnome-keyring) to encrypt everything to the user's password.
# that's redundant with my fs-level encryption and makes the keyring less inspectable,
# so patch gtkcord4 to use Default_keyring instead.
# see:
# - <https://github.com/diamondburned/gtkcord4/issues/139>
# - <https://github.com/zalando/go-keyring/issues/46>
substituteInPlace vendor/github.com/zalando/go-keyring/secret_service/secret_service.go \
--replace '"login"' '"Default_keyring"'
'';
});
persist.byStore.private = [
".cache/gtkcord4"
".config/gtkcord4" # empty?
];
services.gtkcord4 = {
description = "unofficial Discord chat client";
wantedBy = lib.mkIf cfg.config.autostart [ "default.target" ];
serviceConfig = {
ExecStart = "${cfg.package}/bin/gtkcord4";
Type = "simple";
Restart = "always";
RestartSec = "20s";
};
};
};
}

View File

@@ -1,40 +0,0 @@
local logger = require("logger")
logger.info("applying colin patch colin-impl-clipboard")
-- source: <https://github.com/ncopa/lua-shell/blob/master/shell.lua>
local function shescape(arg)
if arg:match("[^A-Za-z0-9_/:=-]") then
arg = "'"..arg:gsub("'", "'\\''").."'"
end
return arg
end
-- 2023/12/15: the default setClipboardText doesn't do anything
-- frontend/device/sdl/device.lua calls frontend/device/input.lua is noop
local input = require("ffi/input")
input.setClipboardText = function(text)
logger.info("input.setClipboardText")
local cmd = "wl-copy " .. shescape(text)
logger.info("invoke: " .. cmd)
os.execute(cmd)
end
-- 2023/12/15: the default ReaderLink "Copy" option (when clicking a URL) doesn't do anything
-- patch so it calls `setClipboardText`
local ReaderLink = require("apps/reader/modules/readerlink")
local UIManager = require("ui/uimanager")
local _ = require("gettext")
local orig_ReaderLink_init = ReaderLink.init;
ReaderLink.init = function(self)
orig_ReaderLink_init(self)
self._external_link_buttons["10_copy"] = function(this, link_url)
return {
text = _("Copy"),
callback = function()
UIManager:close(this.external_link_dialog)
input.setClipboardText(link_url)
end,
}
end
end

View File

@@ -1,21 +1,9 @@
# docs:
# - <https://koreader.rocks/user_guide/>
# - <https://github.com/koreader/koreader/wiki>
#
# post-installation setup:
# - download dictionaries:
# - search icon > settings > dictionary settings > download dictionaries
# - these are stored in `~/.config/koreader/data/dict`
# - configure defaults:
# - edit keys in ~/.config/koreader/settings.reader.lua
# - default font size: `["copt_font_size"] = 28,`
# - home dir: `["home_dir"] = "/home/colin/Books",`
{ config, lib, pkgs, sane-lib, ... }: { config, lib, pkgs, sane-lib, ... }:
let let
feeds = sane-lib.feeds; feeds = sane-lib.feeds;
allFeeds = config.sane.feeds; allFeeds = config.sane.feeds;
wantedFeeds = feeds.filterByFormat [ "text" ] allFeeds; wantedFeeds = feeds.filterByFormat [ "image" "text" ] allFeeds;
koreaderRssEntries = builtins.map (feed: koreaderRssEntries = builtins.map (feed:
# format: # format:
# { "<rss/atom url>", limit = <int>, download_full_article=<bool>, include_images=<bool>, enable_filter=<bool>, filter_element = "<css selector>"}, # { "<rss/atom url>", limit = <int>, download_full_article=<bool>, include_images=<bool>, enable_filter=<bool>, filter_element = "<css selector>"},
@@ -26,7 +14,7 @@ let
# enable_filter = true => only render content that matches the filter_element css selector. # enable_filter = true => only render content that matches the filter_element css selector.
let fields = [ let fields = [
(lib.escapeShellArg feed.url) (lib.escapeShellArg feed.url)
"limit = 20" "limit = 5"
"download_full_article = true" "download_full_article = true"
"include_images = true" "include_images = true"
"enable_filter = false" "enable_filter = false"
@@ -38,13 +26,9 @@ in {
package = pkgs.koreader-from-src; package = pkgs.koreader-from-src;
# koreader applies these lua "patches" at boot: # koreader applies these lua "patches" at boot:
# - <https://github.com/koreader/koreader/wiki/User-patches> # - <https://github.com/koreader/koreader/wiki/User-patches>
# the naming is IMPORTANT. these must start with a `2-` in order to be invoked during the right initialization phase # - 2023/10/29: koreader code hasn't changed, but somehow FTP browser seems usable even without the isConnected patch now.
#
# 2023/10/29: koreader code hasn't changed, but somehow FTP browser seems usable even without the isConnected patch now.
# fs.".config/koreader/patches/2-colin-NetworkManager-isConnected.lua".symlink.target = "${./2-colin-NetworkManager-isConnected.lua}"; # fs.".config/koreader/patches/2-colin-NetworkManager-isConnected.lua".symlink.target = "${./2-colin-NetworkManager-isConnected.lua}";
fs.".config/koreader/patches/2-02-colin-impl-clipboard-ops.lua".symlink.target = "${./2-02-colin-impl-clipboard-ops.lua}";
# koreader news plugin, enabled by default. file format described here: # koreader news plugin, enabled by default. file format described here:
# - <repo:koreader/koreader:plugins/newsdownloader.koplugin/feed_config.lua> # - <repo:koreader/koreader:plugins/newsdownloader.koplugin/feed_config.lua>
fs.".config/koreader/news/feed_config.lua".symlink.text = '' fs.".config/koreader/news/feed_config.lua".symlink.text = ''

View File

@@ -6,8 +6,6 @@
# package = pkgs.libreoffice-still; # package = pkgs.libreoffice-still;
package = pkgs.libreoffice-fresh; package = pkgs.libreoffice-fresh;
slowToBuild = true;
# disable first-run stuff # disable first-run stuff
fs.".config/libreoffice/4/user/registrymodifications.xcu".symlink.text = '' fs.".config/libreoffice/4/user/registrymodifications.xcu".symlink.text = ''
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>

View File

@@ -1,14 +0,0 @@
{ pkgs, ... }:
{
sane.programs.loupe = {
mime.associations = {
"image/gif" = "org.gnome.Loupe.desktop";
"image/heif" = "org.gnome.Loupe.desktop"; # apple codec
"image/png" = "org.gnome.Loupe.desktop";
"image/jpeg" = "org.gnome.Loupe.desktop";
"image/svg+xml" = "org.gnome.Loupe.desktop";
"image/webp" = "org.gnome.Loupe.desktop";
};
};
}

View File

@@ -0,0 +1,11 @@
{ pkgs, ... }:
{
sane.programs.megapixels.package = pkgs.megapixels.override {
# megapixels uses zbar to read barcodes.
# zbar by default ships zbarcam-gtk and zbarcam-qt, neither of which megapixels needs.
# but the latter takes a dep on qt, which bloats the closure and the build, so disable this feature.
zbar = pkgs.zbar.override {
enableVideo = false;
};
};
}

View File

@@ -1,78 +0,0 @@
# mimeo is an exec dispatcher like xdg-open, but why allows mapping different URL regexes to different handlers.
# my setup sets mimeo as the default http/https handler,
# and from there it dispatches specialized rules, falling back to the original http/https handler if no URL specialization exists
{ config, lib, pkgs, ... }:
let
mimeo-open-desktop = pkgs.static-nix-shell.mkPython3Bin {
pname = "mimeo-open-desktop";
src = ./.;
pkgs = [ "mimeo" ];
};
# [ProgramConfig]
enabledPrograms = builtins.filter
(p: p.enabled)
(builtins.attrValues config.sane.programs);
# [ProgramConfig]
sortedPrograms = builtins.sort
(l: r: l.priority or 1000 < r.priority or 1000)
enabledPrograms;
fmtAssoc = regex: desktop: ''
${mimeo-open-desktop}/bin/mimeo-open-desktop ${desktop} %U
${regex}
'';
assocs = builtins.map
(program: lib.mapAttrsToList fmtAssoc program.mime.urlAssociations)
sortedPrograms;
assocs' = lib.flatten assocs;
fmtFallbackAssoc = mimeType: desktop: if mimeType == "x-scheme-handler/http" then ''
${mimeo-open-desktop}/bin/mimeo-open-desktop ${desktop} %U
^http://.*
'' else if mimeType == "x-scheme-handler/https" then ''
${mimeo-open-desktop}/bin/mimeo-open-desktop ${desktop} %U
^https://.*
'' else "";
fmtFallbackAssoc' = mimeType: desktop:
lib.optionalString (desktop != "mimeo.desktop") (fmtFallbackAssoc mimeType desktop);
fallbackAssocs = builtins.map
(program: lib.mapAttrsToList fmtFallbackAssoc' program.mime.associations)
sortedPrograms;
fallbackAssocs' = lib.flatten fallbackAssocs;
in
{
sane.programs.mimeo = {
package = pkgs.mimeo.overridePythonAttrs (upstream: {
nativeBuildInputs = (upstream.nativeBuildInputs or []) ++ [
pkgs.copyDesktopItems
];
desktopItems = [
(pkgs.makeDesktopItem {
name = "mimeo";
desktopName = "Mimeo";
exec = "mimeo %U";
comment = "Open files by MIME-type or file name using regular expressions.";
})
];
# upstream mimeo doesn't run preInstall/postInstall hooks, but we need that for the .desktop file
installPhase = ''
runHook preInstall
${upstream.installPhase}
runHook postInstall
'';
passthru = (upstream.passthru or {}) // {
inherit mimeo-open-desktop;
};
});
fs.".config/mimeo/associations.txt".symlink.text = lib.concatStringsSep "\n" (assocs' ++ fallbackAssocs');
mime.priority = 20;
mime.associations."x-scheme-handler/http" = "mimeo.desktop";
mime.associations."x-scheme-handler/https" = "mimeo.desktop";
};
}

View File

@@ -1,41 +0,0 @@
#!/usr/bin/env nix-shell
#!nix-shell -i python3 -p "python3.withPackages (ps: [ ])" -p mimeo
# TODO: migrate nixpkgs mimeo to be `buildPythonPackage` to make it importable here.
# see <doc/languages-frameworks/python.section.md>
import subprocess
import sys
desktop = sys.argv[1]
opener_args = sys.argv[2:]
desktop_fields=subprocess.check_output([
"mimeo",
"--desk2field",
"Exec",
desktop
]).decode('utf-8')
# print(f"fields: {desktop_fields!r}")
desktop_exec = '\n'.join(desktop_fields.split('\n')[1:])
# print(f"exec: {desktop_exec!r}")
# TODO: this is obviously not correct if any of the args included spaces
desktop_argv = [f.strip() for f in desktop_exec.split(' ') if f.strip()]
# print(f"desktop_argv: {desktop_argv!r}")
# fields explained: <https://specifications.freedesktop.org/desktop-entry-spec/latest/ar01s07.html>
# - %U = all URLs
# - %U = just the first URL
substituted_argv = []
for arg in desktop_argv:
if arg == '%U':
substituted_argv += opener_args
elif arg == '%u':
substituted_argv += opener_args[:1]
else:
substituted_argv += [arg]
# print(f"argv: {substituted_argv}")
print(subprocess.check_output(substituted_argv).decode())

View File

@@ -23,18 +23,15 @@ in
package = pkgs.wrapMpv pkgs.mpv-unwrapped { package = pkgs.wrapMpv pkgs.mpv-unwrapped {
scripts = with pkgs.mpvScripts; [ scripts = with pkgs.mpvScripts; [
mpris mpris
uosc # uosc
# pkgs.mpv-uosc-latest pkgs.mpv-uosc-latest
]; ];
extraMakeWrapperArgs = lib.optionals (cfg.config.vo != null) [ extraMakeWrapperArgs = lib.optionals (cfg.config.vo != null) [
# 2023/08/29: fixes an error where mpv on moby launches with the message # 2023/08/29: fixes an error where mpv on moby launches with the message
# "DRM_IOCTL_MODE_CREATE_DUMB failed: Cannot allocate memory" # "DRM_IOCTL_MODE_CREATE_DUMB failed: Cannot allocate memory"
# audio still works, and controls, screenshotting, etc -- just not the actual rendering # audio still works, and controls, screenshotting, etc -- just not the actual rendering
# # this is likely a regression for mpv 0.36.0.
# this is likely a regression for mpv 0.36.0. # the actual error message *appears* to come from the mesa library, but it's tough to trace.
# the actual error message *appears* to come from the mesa library, but it's tough to trace.
#
# TODO(2023/12/03): remove once mesa 23.3.1 lands: <https://github.com/NixOS/nixpkgs/pull/265740>
# #
# backend compatibility (2023/10/22): # backend compatibility (2023/10/22):
# run with `--vo=help` to see a list of all output options. # run with `--vo=help` to see a list of all output options.
@@ -57,46 +54,13 @@ in
]; ];
}; };
persist.byStore.plaintext = [ ".local/state/mpv/watch_later" ]; persist.byStore.plaintext = [ ".local/state/mpv/watch_later" ];
fs.".config/mpv/input.conf".symlink.text = let fs.".config/mpv/input.conf".symlink.text = ''
execInTerm = "${pkgs.xdg-terminal-exec}/bin/xdg-terminal-exec";
in ''
# docs:
# - <https://mpv.io/manual/master/#list-of-input-commands>
# - script-binding: <https://mpv.io/manual/master/#command-interface-script-binding>
# - properties: <https://mpv.io/manual/master/#property-list>
# let volume/power keys be interpreted by the system. # let volume/power keys be interpreted by the system.
# this is important for sxmo. # this is important for sxmo.
# mpv defaults is POWER = close, VOLUME_{UP,DOWN} = adjust application-level volume # mpv defaults is POWER = close, VOLUME_{UP,DOWN} = adjust application-level volume
POWER ignore POWER ignore
VOLUME_UP ignore VOLUME_UP ignore
VOLUME_DOWN ignore VOLUME_DOWN ignore
# uosc menu
# text after the shebang is parsed by uosc to construct the menu and names
menu script-binding uosc/menu
s script-binding uosc/subtitles #! Subtitles
a script-binding uosc/audio #! Audio tracks
q script-binding uosc/stream-quality #! Stream quality
p script-binding uosc/items #! Playlist
c script-binding uosc/chapters #! Chapters
> script-binding uosc/next #! Navigation > Next
< script-binding uosc/prev #! Navigation > Prev
o script-binding uosc/open-file #! Navigation > Open file
# set video-aspect-override "-1" #! Utils > Aspect ratio > Default
# set video-aspect-override "16:9" #! Utils > Aspect ratio > 16:9
# set video-aspect-override "4:3" #! Utils > Aspect ratio > 4:3
# set video-aspect-override "2.35:1" #! Utils > Aspect ratio > 2.35:1
# script-binding uosc/audio-device #! Utils > Audio devices
# script-binding uosc/editions #! Utils > Editions
ctrl+s async screenshot #! Utils > Screenshot
alt+i script-binding uosc/keybinds #! Utils > Key bindings
O script-binding uosc/show-in-directory #! Utils > Show in directory
# script-binding uosc/open-config-directory #! Utils > Open config directory
# set pause yes; run ${execInTerm} go2tv -v "''${stream-open-filename}" #! Cast
# set pause yes; run ${execInTerm} go2tv -u "''${stream-open-filename}" #! Cast (...) > Stream
# set pause yes; run go2tv #! Cast (...) > GUI
# TODO: unify "Cast" and "Cast (stream)" options above.
''; '';
fs.".config/mpv/mpv.conf".symlink.text = '' fs.".config/mpv/mpv.conf".symlink.text = ''
save-position-on-quit=yes save-position-on-quit=yes
@@ -127,7 +91,6 @@ in
in '' in ''
# docs: # docs:
# - <https://github.com/tomasklaen/uosc> # - <https://github.com/tomasklaen/uosc>
# - <https://github.com/tomasklaen/uosc/blob/main/src/uosc.conf>
# - <https://superuser.com/questions/1775550/add-new-buttons-to-mpv-uosc-ui> # - <https://superuser.com/questions/1775550/add-new-buttons-to-mpv-uosc-ui>
timeline_style=bar timeline_style=bar
timeline_persistency=paused,audio timeline_persistency=paused,audio
@@ -144,7 +107,8 @@ in
text_border=6.0 text_border=6.0
font_bold=yes font_bold=yes
color=foreground=ff8080,background_text=ff8080 background_text=ff8080
foreground=ff8080
ui_scale=1.0 ui_scale=1.0
''; '';
@@ -153,16 +117,11 @@ in
mime.priority = 50; # default = 100; 50 in order to take precedence over vlc. mime.priority = 50; # default = 100; 50 in order to take precedence over vlc.
mime.associations."audio/flac" = "mpv.desktop"; mime.associations."audio/flac" = "mpv.desktop";
mime.associations."audio/mpeg" = "mpv.desktop"; mime.associations."audio/mpeg" = "mpv.desktop";
mime.associations."audio/x-opus+ogg" = "mpv.desktop";
mime.associations."audio/x-vorbis+ogg" = "mpv.desktop"; mime.associations."audio/x-vorbis+ogg" = "mpv.desktop";
mime.associations."video/mp4" = "mpv.desktop"; mime.associations."video/mp4" = "mpv.desktop";
mime.associations."video/quicktime" = "mpv.desktop"; mime.associations."video/quicktime" = "mpv.desktop";
mime.associations."video/webm" = "mpv.desktop"; mime.associations."video/webm" = "mpv.desktop";
mime.associations."video/x-flv" = "mpv.desktop";
mime.associations."video/x-matroska" = "mpv.desktop"; mime.associations."video/x-matroska" = "mpv.desktop";
mime.urlAssociations."^https?://(www.)?youtube.com/watch\?.*v=" = "mpv.desktop";
mime.urlAssociations."^https?://(www.)?youtube.com/v/" = "mpv.desktop";
mime.urlAssociations."^https?://(www.)?youtu.be/.+" = "mpv.desktop";
}; };
} }

View File

@@ -1,12 +0,0 @@
{ pkgs, ... }:
{
sane.programs."gnome.nautilus" = {
package = pkgs.gnome.nautilus.overrideAttrs (orig: {
# enable the "Audio and Video Properties" pane. see: <https://nixos.wiki/wiki/Nautilus>
buildInputs = orig.buildInputs ++ (with pkgs.gst_all_1; [
gst-plugins-good
gst-plugins-bad
]);
});
};
}

View File

@@ -85,13 +85,27 @@ let
plugin-config-lua = concatMapStrings (p: optionalString (p.type or "" == "lua") p.config) plugins; plugin-config-lua = concatMapStrings (p: optionalString (p.type or "" == "lua") p.config) plugins;
in in
{ {
# private because there could be sensitive things in the swap
sane.programs.neovim = { sane.programs.neovim = {
# package = config.programs.neovim.finalPackage; persist.byStore.private = [ ".cache/vim-swap" ];
package = pkgs.wrapNeovimUnstable pkgs.neovim-unwrapped (pkgs.neovimUtils.makeNeovimConfig { env.EDITOR = "vim";
withRuby = false; #< doesn't cross-compile w/o binfmt # git claims it should use EDITOR, but it doesn't!
viAlias = true; env.GIT_EDITOR = "vim";
vimAlias = true; mime.priority = 200; # default=100 => yield to other, more specialized applications
plugins = plugin-packages; mime.associations."application/schema+json" = "nvim.desktop";
mime.associations."plain/text" = "nvim.desktop";
mime.associations."text/markdown" = "nvim.desktop";
};
programs.neovim = mkIf config.sane.programs.neovim.enabled {
# neovim: https://github.com/neovim/neovim
enable = true;
viAlias = true;
vimAlias = true;
configure = {
packages.plugins = {
start = plugin-packages;
};
customRC = '' customRC = ''
" let the terminal handle mouse events, that way i get OS-level ctrl+shift+c/etc " let the terminal handle mouse events, that way i get OS-level ctrl+shift+c/etc
" this used to be default, until <https://github.com/neovim/neovim/pull/19290> " this used to be default, until <https://github.com/neovim/neovim/pull/19290>
@@ -133,16 +147,6 @@ in
${plugin-config-lua} ${plugin-config-lua}
EOF EOF
''; '';
}); };
# private because there could be sensitive things in the swap
persist.byStore.private = [ ".cache/vim-swap" ];
env.EDITOR = "vim";
# git claims it should use EDITOR, but it doesn't!
env.GIT_EDITOR = "vim";
mime.priority = 200; # default=100 => yield to other, more specialized applications
mime.associations."application/schema+json" = "nvim.desktop";
mime.associations."plain/text" = "nvim.desktop";
mime.associations."text/markdown" = "nvim.desktop";
}; };
} }

View File

@@ -1,19 +1,12 @@
# news-flash RSS viewer # news-flash RSS viewer
# - feeds have to be manually imported:
# - Local RSS -> Import OPML -> ~/.config/newsflashFeeds.opml
# - clicking article-embedded links doesn't work because of xdg portal stuff
# - need to either run unsandboxed, or install a org.freedesktop.portal.OpenURI handler
{ config, sane-lib, ... }: { config, sane-lib, ... }:
let let
feeds = sane-lib.feeds; feeds = sane-lib.feeds;
all-feeds = config.sane.feeds; all-feeds = config.sane.feeds;
# text/image: newsflash renders these natively wanted-feeds = feeds.filterByFormat ["text" "image"] all-feeds;
# podcast/video: newsflash dispatches these to xdg-open
wanted-feeds = feeds.filterByFormat [ "text" "image" "podcast" "video" ] all-feeds;
in { in {
sane.programs.newsflash = { sane.programs.newsflash = {
slowToBuild = true; # mainly for desktop: webkitgtk-6.0
persist.byStore.plaintext = [ ".local/share/news-flash" ]; persist.byStore.plaintext = [ ".local/share/news-flash" ];
fs.".config/newsflashFeeds.opml".symlink.text = fs.".config/newsflashFeeds.opml".symlink.text =
feeds.feedsToOpml wanted-feeds feeds.feedsToOpml wanted-feeds

View File

@@ -1,8 +0,0 @@
{ ... }:
{
sane.programs.notejot = {
persist.byStore.private = [
".local/share/io.github.lainsce.Notejot"
];
};
}

View File

@@ -1,18 +0,0 @@
{ ... }:
{
sane.programs.open-in-mpv = {
# taken from <https://github.com/Baldomo/open-in-mpv>
fs.".config/open-in-mpv/config.yml".symlink.text = ''
players:
mpv:
name: mpv
executable: mpv
fullscreen: "--fs"
pip: "--ontop --no-border --autofit=384x216 --geometry=98\\%:98\\%"
enqueue: ""
new_window: ""
needs_ipc: true
flag_overrides: {}
'';
};
}

View File

@@ -1,11 +0,0 @@
{ ... }:
{
sane.programs.planify = {
persist.byStore.private = [
# TODO items as a sqlite database
".local/share/io.github.alainm23.planify"
];
slowToBuild = true; # webkitgtk-6.0; slow for desktop
};
}

View File

@@ -1,44 +0,0 @@
# TODO(bug): signal-desktop is known to hang on exit.
# particularly, it may fail to start (because e.g. there's no wayland session yet),
# and it will try to exit -- after which the service would restart -- but it hangs w/ no GUI instead.
# characterized by these log messages:
#
# Dec 03 13:46:23 moby signal-desktop[4097]: [4097:1203/134623.906367:ERROR:ozone_platform_x11.cc(240)] Missing X server or $DISPLAY
# Dec 03 13:46:23 moby signal-desktop[4097]: [4097:1203/134623.909667:ERROR:env.cc(255)] The platform failed to initialize. Exiting.
{ config, lib, pkgs, ... }:
let
cfg = config.sane.programs.signal-desktop;
in
{
sane.programs.signal-desktop = {
configOption = with lib; mkOption {
default = {};
type = types.submodule {
options.autostart = mkOption {
type = types.bool;
default = false;
};
};
};
package = pkgs.signal-desktop-from-src;
# creds, media
persist.byStore.private = [
".config/Signal"
];
services.signal-desktop = {
description = "Signal Messenger";
wantedBy = lib.mkIf cfg.config.autostart [ "default.target" ];
serviceConfig = {
ExecStart = "${cfg.package}/bin/signal-desktop";
Type = "simple";
Restart = "always";
RestartSec = "20s";
};
# for some reason the --ozone-platform-hint=auto flag fails when signal-desktop is launched from a service
environment.NIXOS_OZONE_WL = "1";
};
};
}

View File

@@ -1,11 +0,0 @@
{ ... }:
{
sane.programs.spot = {
secrets.".cache/spot/librespot/credentials/credentials.json" = ../../../secrets/common/spot_credentials.json.bin;
persist.byStore.plaintext = [
".cache/spot/img" # album art
".cache/spot/librespot/audio" # audio/track cache
".cache/spot/net" # album metadata
];
};
}

View File

@@ -1,10 +0,0 @@
{ ... }:
{
sane.programs.superTuxKart = {
persist.byStore.plaintext = [
".cache/supertuxkart"
".config/supertuxkart"
".local/share/supertuxkart"
];
};
}

View File

@@ -1,44 +0,0 @@
{ config, lib, pkgs, ... }:
let
cfg = config.sane.programs.sway-autoscaler;
in
{
sane.programs.sway-autoscaler = {
configOption = with lib; mkOption {
default = {};
type = types.submodule {
options.autostart = mkOption {
type = types.bool;
default = false;
};
options.defaultScale = mkOption {
type = types.number;
default = 1;
};
options.interval = mkOption {
type = types.number;
default = 5;
};
};
};
package = pkgs.static-nix-shell.mkBash {
pname = "sway-autoscaler";
pkgs = [ "jq" "sway" "util-linux" ];
src = ./.;
};
services.sway-autoscaler = {
description = "adjust global desktop scale to match the activate application";
wantedBy = lib.mkIf cfg.config.autostart [ "default.target" ];
serviceConfig = {
ExecStart = "${cfg.package}/bin/sway-autoscaler --loop-sec ${builtins.toString cfg.config.interval}";
Type = "simple";
Restart = "always";
RestartSec = "10s";
};
environment.SWAY_DEFAULT_SCALE = builtins.toString cfg.config.defaultScale;
};
};
}

View File

@@ -1,87 +0,0 @@
#!/usr/bin/env nix-shell
#!nix-shell -i bash -p jq -p sway -p util-linux
help() {
echo "queries the focused window and apply an appropriate display-wide scale."
echo "this should be considered a temporary workaround until more apps support small displays."
echo ""
echo "args:"
echo " -v | --verbose"
echo " --loop-sec N: re-compute the scale every N seconds. else, run once and exit."
echo ""
echo "environment variables:"
echo " SWAY_DEFAULT_SCALE=N: scale to apply when no known window is selected."
# TODO: could use map-style environment variables to allow external per-app config
# - SWAY_SCALE_org.gnome.Maps=1 ; ...
}
options=$(getopt -l h,loop-sec:,verbose -o h,v -- "" "${@}")
eval "set -- ${options}"
verbose=false
loop=false
loop_sec=
while true; do
case "$1" in
(-h|--help)
shift
help
exit
;;
(--loop-sec)
shift
loop=true
loop_sec="$1"
shift
;;
(-v|--verbose)
shift
verbose=true
;;
(--)
shift
if [ $# -eq 1 ]; then
break
fi
;;
(*)
echo "invalid arguments: '$1'"
echo ""
help
exit 1
;;
esac
done
autoscale() {
focused=$(swaymsg -t get_tree | jq --raw-output '.. | select(.type?) | select(.focused==true) | .app_id')
$verbose && echo "focused: '$focused'"
scale=${SWAY_DEFAULT_SCALE:-1}
case "$focused" in
(org.gnome.Maps)
# scale=1.6: 3 turns visible at once in landscape
# scale=1.25: max scale at which no text/menuitems are cutoff in vertical mode
# 5 turns visible at once in landscape
# scale=1.2: max of 6 turns visible at once in landscape
# scale=1.1: max of 7 turns visible at once in landscape
# scale=1: max of 8 turns visible at once in landscape
#
# as scale is increased past 1.1, vertical mode becomes substantially less usable
# as scale is decreased past 1.3, the whole desktop UI becomes less usable (keyboard, workspaces)
scale=1.2
;;
esac
$verbose && echo "scaling to $scale"
swaymsg -q -- output '*' scale "$scale"
}
if $loop; then
while true; do
autoscale
sleep "$loop_sec"
done
else
autoscale
fi

View File

@@ -17,9 +17,6 @@
{ config, lib, pkgs, ... }: { config, lib, pkgs, ... }:
let let
cfg = config.sane.programs.swaynotificationcenter; cfg = config.sane.programs.swaynotificationcenter;
mprisIconSize = 48;
fbcli-wrapper = pkgs.writeShellApplication { fbcli-wrapper = pkgs.writeShellApplication {
name = "swaync-fbcli"; name = "swaync-fbcli";
runtimeInputs = [ runtimeInputs = [
@@ -91,13 +88,28 @@ let
pkgs.systemd pkgs.systemd
]; ];
text = '' text = ''
if systemctl is-active "$@"; then echo "SWAYNC_TOGGLE_STATE: $SWAYNC_TOGGLE_STATE" | ${pkgs.systemd}/bin/systemd-cat --identifier=swaync
if systemctl is-active --quiet "$@"; then
systemctl stop "$@" systemctl stop "$@"
else else
systemctl start "$@" systemctl start "$@"
fi fi
''; '';
}; };
systemctl-is-active = pkgs.writeShellApplication {
name = "systemctl-is-active";
runtimeInputs = [
pkgs.systemd
];
text = ''
if systemctl is-active "$@"; then
echo true
else
echo false
fi
'';
};
in in
{ {
sane.programs.swaynotificationcenter = { sane.programs.swaynotificationcenter = {
@@ -117,32 +129,37 @@ in
}; };
# prevent dbus from automatically activating swaync so i can manage it as a systemd service instead # prevent dbus from automatically activating swaync so i can manage it as a systemd service instead
package = pkgs.rmDbusServices (pkgs.swaynotificationcenter.overrideAttrs (upstream: { package = pkgs.rmDbusServices (pkgs.swaynotificationcenter.overrideAttrs (upstream: {
# use swaync with PR 304 applied.
# i can't just `fetchpatch` the PR, nor even a subset of its individual commits,
# because the author does a `merge master` in the middle of it.
# <https://github.com/ErikReider/SwayNotificationCenter/pull/304>
version = "unstable-2023-07-08";
src = pkgs.fetchFromGitHub {
# owner = "ErikReider";
owner = "JannisPetschenka";
repo = "SwayNotificationCenter";
# rev = "90688d0fe916b3e0f764883bce7f19ded82f1476";
# hash = "sha256-Ycc3ja6bcqbVPxwKCPivamQsNRwFrKwIWpHTFmJ5S5g=";
# rev = "f1821a59223010b9c0719d4125bc013b91267800";
# hash = "sha256-ea3owxvvz79uTD7pzBgAL/BYhkyECdN/6qr8wH44DL4=";
# rev = "90cde83bde93bf5d2c14d26345ffbb75963da70c";
# hash = "sha256-cpR0bNG0xlzdg9Jj5O4gTqa/wPyB5nu1SgEGlSQkK4I=";
rev = "6dafd54929ad5fc0078458541c0e4437e5b8a4a9";
hash = "sha256-1mfy7Yzg8Mpbu7R/np15qTz0x2oZvNCZ4Y/9ll+CXv0=";
};
# allow toggle buttons: # allow toggle buttons:
patches = (upstream.patches or []) ++ [ patches = [
# (pkgs.fetchpatch { # (pkgs.fetchpatch {
# url = "https://github.com/ErikReider/SwayNotificationCenter/pull/304.patch"; # url = "https://github.com/ErikReider/SwayNotificationCenter/pull/304.patch";
# name = "Add toggle button"; # name = "Add toggle button";
# hash = "sha256-bove2EXc5FZ5nN1X1FYOn3czCgHG03ibIAupJNoctiM="; # hash = "sha256-zD1EUnMMSIlLS8uFS0YcwmbrHuntsKw5Y9fF0N1beIU=";
# })
# (pkgs.fetchpatch {
# url = "https://git.uninsane.org/colin/SwayNotificationCenter/commit/f5d9405e040fc42ea98dc4d37202c85728d0d4fd.patch";
# name = "toggleButton: change active field to be a command";
# hash = "sha256-Y8fiZbAP9yGOVU3rOkZKO8TnPPlrGpINWYGaqeeNzF0=";
# }) # })
(pkgs.fetchpatch {
# import of <https://github.com/ErikReider/SwayNotificationCenter/pull/304>
# as of 2023/11/08 the upstream patch has merge conflicts AND runtime issues (see wip-swaync-update nixos branch)
url = "https://git.uninsane.org/colin/SwayNotificationCenter/commit/d9a0d938b88cbee65cfaef887af77a5a23d5fe89.patch";
name = "Add toggle button";
hash = "sha256-bove2EXc5FZ5nN1X1FYOn3czCgHG03ibIAupJNoctiM=";
})
(pkgs.fetchpatch {
url = "https://git.uninsane.org/colin/SwayNotificationCenter/commit/f5d9405e040fc42ea98dc4d37202c85728d0d4fd.patch";
name = "toggleButton: change active field to be a command";
hash = "sha256-Y8fiZbAP9yGOVU3rOkZKO8TnPPlrGpINWYGaqeeNzF0=";
})
]; ];
postPatch = (upstream.postPatch or "") + ''
# XXX: this might actually be changing the DPI, not the scaling...
# in that case, it might be possible to do this in CSS
substituteInPlace src/controlCenter/widgets/mpris/mpris_player.ui \
--replace '96' '${builtins.toString mprisIconSize}'
'';
})); }));
suggestedPrograms = [ "feedbackd" ]; suggestedPrograms = [ "feedbackd" ];
fs.".config/swaync/style.css".symlink.text = '' fs.".config/swaync/style.css".symlink.text = ''
@@ -150,6 +167,8 @@ in
/* noti-bg defaults `rgb(48, 48, 48)` and is the default button/slider/grid background */ /* noti-bg defaults `rgb(48, 48, 48)` and is the default button/slider/grid background */
@define-color noti-bg rgb(36, 36, 36); @define-color noti-bg rgb(36, 36, 36);
@define-color noti-bg-darker rgb(24, 24, 24); @define-color noti-bg-darker rgb(24, 24, 24);
/* used for button.active background color, and also when dismissing notifs with keyboard? */
/* @define-color noti-bg-focus rgb(0, 110, 190); */
/* avoid black-on-black text that the default style ships */ /* avoid black-on-black text that the default style ships */
window { window {
@@ -161,14 +180,11 @@ in
background: rgba(0, 0, 0, 0.5); background: rgba(0, 0, 0, 0.5);
} }
.widget-buttons-grid>flowbox>flowboxchild>button.toggle { .widget-buttons-grid>flowbox>flowboxchild>button {
/* text color for inactive buttons, and "Clear All" button.*/ /* text color for inactive buttons, and "Clear All" button.*/
color: rgb(172, 172, 172); color: rgb(172, 172, 172);
/* padding defaults to 16px; tighten, so i can squish it all onto one row */
padding-left: 0px;
padding-right: 0px;
} }
.widget-buttons-grid>flowbox>flowboxchild>button.toggle.active { .widget-buttons-grid>flowbox>flowboxchild>button.toggle:checked {
color: rgb(255, 255, 255); color: rgb(255, 255, 255);
background-color: rgb(0, 110, 190); background-color: rgb(0, 110, 190);
} }
@@ -227,39 +243,12 @@ in
# - SWAYNC_REPLACES_ID # - SWAYNC_REPLACES_ID
# - SWAYNC_ID # - SWAYNC_ID
# - SWAYNC_SUMMARY # - SWAYNC_SUMMARY
incoming-im = {
# rules to use for testing. trigger with:
# - `notify-send test test:message` (etc)
# should also be possible to trigger via any messaging app
fbcli-test-im = {
body = "test:message";
exec = "${fbcli} --event proxied-message-new-instant";
};
fbcli-test-call = {
body = "test:call";
exec = "${fbcli} --event phone-incoming-call -t 20";
};
fbcli-test-call-stop = {
body = "test:call-stop";
exec = "${fbcli-stop} --event phone-incoming-call -t 20";
};
fbcli-test-timer = {
body = "test:timer";
exec = "${fbcli} --event timeout-completed";
};
incoming-im-known-app-name = {
# trigger notification sound on behalf of these IM clients. # trigger notification sound on behalf of these IM clients.
app-name = "(Chats|Dino|discord|Element|Fractal|gtkcord4)"; app-name = "(Chats|Dino|discord|Element|Fractal)";
body = "^(?!Incoming call).*$"; #< don't match Dino Incoming calls body = "^(?!Incoming call).*$"; #< don't match Dino Incoming calls
exec = "${fbcli} --event proxied-message-new-instant"; exec = "${fbcli} --event proxied-message-new-instant";
}; };
incoming-im-known-desktop-entry = {
# trigger notification sound on behalf of these IM clients.
# these clients don't have an app-name (listed as "<unknown>"), but do have a desktop-entry
desktop-entry = "com.github.uowuo.abaddon";
exec = "${fbcli} --event proxied-message-new-instant";
};
incoming-call = { incoming-call = {
app-name = "Dino"; app-name = "Dino";
body = "^Incoming call$"; body = "^Incoming call$";
@@ -359,79 +348,51 @@ in
# type = "toggle"; # type = "toggle";
# label = "feedbackd"; # label = "feedbackd";
# command = "${systemctl-toggle}/bin/systemctl-toggle --user feedbackd"; # command = "${systemctl-toggle}/bin/systemctl-toggle --user feedbackd";
# active = "${pkgs.systemd}/bin/systemctl is-active --user feedbackd.service"; # update-command = "${pkgs.systemd}/bin/systemctl is-active --user feedbackd.service && echo true || echo false";
# active = false;
# } # }
lib.optionals config.sane.programs.eg25-control.enabled [ lib.optionals config.sane.programs.eg25-control.enabled [
{ {
type = "toggle"; type = "toggle";
label = ""; # GPS services; other icons: gps, ⌖, 🛰, 🌎,  label = "gps";
command = "/run/wrappers/bin/sudo ${systemctl-toggle}/bin/systemctl-toggle eg25-control-gps"; command = "/run/wrappers/bin/sudo ${systemctl-toggle}/bin/systemctl-toggle eg25-control-gps";
active = "${pkgs.systemd}/bin/systemctl is-active eg25-control-gps.service"; update-command = "${systemctl-is-active}/bin/systemctl-is-active eg25-control-gps";
} }
{ {
type = "toggle"; type = "toggle";
label = "󰺐"; # icons: 5g, 📡, 📱, ᯤ, ⚡, , 🌐, 📶, 🗼, 󰀂, , 󰺐, 󰩯 label = "5g";
# modem and NetworkManager auto-establishes a connection when powered. # modem and NetworkManager auto-establishes a connection when powered.
# though some things like `wg-home` VPN tunnel will remain routed over the old interface. # though some things like `wg-home` VPN tunnel will remain routed over the old interface.
command = "/run/wrappers/bin/sudo ${systemctl-toggle}/bin/systemctl-toggle eg25-control-powered"; command = "/run/wrappers/bin/sudo ${systemctl-toggle}/bin/systemctl-toggle eg25-control-powered";
active = "${pkgs.systemd}/bin/systemctl is-active eg25-control-powered.service"; update-command = "${systemctl-is-active}/bin/systemctl-is-active eg25-control-powered";
} }
] ++ lib.optionals false [ ] ++ [
{ {
type = "toggle"; type = "toggle";
label = "vpn::hn"; # route all traffic through servo; useful to debug moby's networking label = "vpn::hn";
command = "/run/wrappers/bin/sudo ${systemctl-toggle}/bin/systemctl-toggle wg-quick-vpn-servo"; command = "/run/wrappers/bin/sudo ${systemctl-toggle}/bin/systemctl-toggle wg-quick-vpn-servo";
active = "${pkgs.systemd}/bin/systemctl is-active wg-quick-vpn-servo.service"; update-command = "${systemctl-is-active}/bin/systemctl-is-active wg-quick-vpn-servo";
} }
] ++ lib.optionals config.sane.programs.calls.config.autostart [ ] ++ lib.optionals config.sane.programs.calls.config.autostart [
{ {
type = "toggle"; type = "toggle";
label = "SIP"; label = "SIP";
command = "${systemctl-toggle}/bin/systemctl-toggle --user gnome-calls"; command = "${systemctl-toggle}/bin/systemctl-toggle --user gnome-calls";
active = "${pkgs.systemd}/bin/systemctl is-active --user gnome-calls"; update-command = "${systemctl-is-active}/bin/systemctl-is-active --user gnome-calls";
}
] ++ lib.optionals config.sane.programs."gnome.geary".enabled [
{
type = "toggle";
label = "󰇮"; # email (Geary); other icons: ✉, [E], 📧, 󰇮
command = "${systemctl-toggle}/bin/systemctl-toggle --user geary";
active = "${pkgs.systemd}/bin/systemctl is-active --user geary";
}
# ] ++ lib.optionals config.sane.programs.abaddon.enabled [
# # XXX: disabled in favor of gtkcord4: abaddon has troubles auto-connecting at start
# {
# type = "toggle";
# label = "󰊴"; # Discord chat client; icons: 󰊴, 🎮
# command = "${systemctl-toggle}/bin/systemctl-toggle --user abaddon";
# active = "${pkgs.systemd}/bin/systemctl is-active --user abaddon";
# }
] ++ lib.optionals config.sane.programs.gtkcord4.enabled [
{
type = "toggle";
label = "󰊴"; # Discord chat client; icons: 󰊴, 🎮
command = "${systemctl-toggle}/bin/systemctl-toggle --user gtkcord4";
active = "${pkgs.systemd}/bin/systemctl is-active --user gtkcord4";
}
] ++ lib.optionals config.sane.programs.signal-desktop.enabled [
{
type = "toggle";
label = "💬"; # Signal messenger; other icons: 󰍦
command = "${systemctl-toggle}/bin/systemctl-toggle --user signal-desktop";
active = "${pkgs.systemd}/bin/systemctl is-active --user signal-desktop";
} }
] ++ lib.optionals config.sane.programs.dino.enabled [ ] ++ lib.optionals config.sane.programs.dino.enabled [
{ {
type = "toggle"; type = "toggle";
label = "XMPP"; # XMPP calls (jingle) label = "XMPP"; # XMPP calls (jingle)
command = "${systemctl-toggle}/bin/systemctl-toggle --user dino"; command = "${systemctl-toggle}/bin/systemctl-toggle --user dino";
active = "${pkgs.systemd}/bin/systemctl is-active --user dino"; update-command = "${systemctl-is-active}/bin/systemctl-is-active --user dino";
} }
] ++ lib.optionals config.sane.programs.fractal.enabled [ ] ++ lib.optionals config.sane.programs.fractal.enabled [
{ {
type = "toggle"; type = "toggle";
label = "[m]"; # Matrix messages label = "[m]"; # Matrix messages
command = "${systemctl-toggle}/bin/systemctl-toggle --user fractal"; command = "${systemctl-toggle}/bin/systemctl-toggle --user fractal";
active = "${pkgs.systemd}/bin/systemctl is-active --user fractal"; update-command = "${systemctl-is-active}/bin/systemctl-is-active --user fractal";
} }
]; ];
}; };
@@ -444,7 +405,7 @@ in
clear-all-button = true; clear-all-button = true;
}; };
mpris = { mpris = {
image-size = mprisIconSize; image-size = 64;
image-radius = 8; image-radius = 8;
}; };
title = { title = {

View File

@@ -27,8 +27,6 @@ in
'' + (upstream.preFixup or ""); '' + (upstream.preFixup or "");
}); });
slowToBuild = true; # only true for cross-compiled tangram
persist.byStore.private = [ persist.byStore.private = [
".cache/Tangram" ".cache/Tangram"
".local/share/Tangram" ".local/share/Tangram"

View File

@@ -1,6 +1,26 @@
{ ... }: { pkgs, ... }:
{ {
sane.programs.tuba = { sane.programs.tuba = {
package = pkgs.tuba.overrideAttrs (upstream: {
postInstall = (upstream.postInstall or "") + ''
# ship a `tuba` alias to the actual tuba binary, since i can never remember its name
ln -s $out/bin/dev.geopjr.Tuba $out/bin/tuba
'';
preFixup = (upstream.preFixup or "") + ''
# 2023/09/24: fix so i can upload media when creating a post.
# see: <https://github.com/GeopJr/Tuba/issues/414#issuecomment-1732695845>
gappsWrapperArgs+=(
--prefix GDK_DEBUG , no-portals
)
'';
# alternative to disabling portals is to remove the filters on FileDialogs.
# done like so (but would want to apply to the other dialogs too)
# postPatch = (upstream.postPatch or "") + ''
# substituteInPlace src/Dialogs/ProfileEdit.vala \
# --replace "default_filter = filter" ""
# '';
});
suggestedPrograms = [ "gnome-keyring" ]; suggestedPrograms = [ "gnome-keyring" ];
}; };
} }

View File

@@ -32,7 +32,6 @@ in
mime.associations."video/mp4" = "vlc.desktop"; mime.associations."video/mp4" = "vlc.desktop";
mime.associations."video/quicktime" = "vlc.desktop"; mime.associations."video/quicktime" = "vlc.desktop";
mime.associations."video/webm" = "vlc.desktop"; mime.associations."video/webm" = "vlc.desktop";
mime.associations."video/x-flv" = "vlc.desktop";
mime.associations."video/x-matroska" = "vlc.desktop"; mime.associations."video/x-matroska" = "vlc.desktop";
}; };
} }

View File

@@ -1,18 +0,0 @@
{ ... }:
{
sane.programs.wike = {
# wike probably meant to put everything here in a subdir, but didn't.
persist.byStore.cryptClearOnBoot = [
".cache/webkitgtk"
".local/share/webkitgtk"
];
persist.byStore.private = [
".local/share/historic.json" # history
# .local/share/cookies (probably not necessary to persist?)
# .local/share/booklists.json (empty; not sure if wike's)
# .local/share/bookmarks.json (empty; not sure if wike's)
# .local/share/languages.json (not sure if wike's)
];
};
}

View File

@@ -1,11 +0,0 @@
{ ... }:
{
sane.programs.wine = {
# no need for the cryptographic nature, just needs to not use loads of / tmpfs.
persist.byStore.cryptClearOnBoot = [ ".wine" ];
persist.byStore.plaintext = [
# Power Bomberman: <https://www.bombermanboard.com/viewtopic.php?t=1925>
".wine/drive_c/users/colin/AppData/pb"
];
};
}

View File

@@ -1,6 +1,4 @@
{ config, ... }: { config, ... }:
{ {
sane.programs.wireshark.slowToBuild = true;
programs.wireshark.enable = config.sane.programs.wireshark.enabled; programs.wireshark.enable = config.sane.programs.wireshark.enabled;
} }

View File

@@ -1,143 +0,0 @@
# docs:
# - <https://github.com/francma/wob/blob/master/wob.ini.5.scd>
# - `wob -vv` to see config defaults
#
# the wob services defined here are largely based on those from SXMO.
#
# this should arguably be just a (user) service. nothing actually needs `wob` on the PATH.
#
{ config, lib, pkgs, ... }:
let
cfg = config.sane.programs.wob;
in
{
sane.programs.wob = {
configOption = with lib; mkOption {
default = {};
type = types.submodule {
options.autostart = mkOption {
type = types.bool;
default = true;
};
options.sock = mkOption {
type = types.str;
default = "sxmo.wobsock";
};
};
};
fs.".config/wob/wob.ini".symlink.text = ''
timeout = 900
anchor = top right
orientation = vertical
# margin top right bottom left
# note that wob is "aware" of the sway bar, so margin 0 never overlaps it.
# however it's not aware of sway's window title
margin = 54 3 54 3
height = 164
width = 30
border_offset = 0
border_size = 2
bar_padding = 0
# very light teal, derived from conky background
bar_color = e1f0efDC
background_color = 000000B4
border_color = 000000C8
overflow_bar_color = FF4040DC
overflow_background_color = FFFFFFC8
overflow_border_color = FF4040DC
'';
services.wob = {
description = "Wayland Overlay Bar (renders volume/backlight levels)";
wantedBy = lib.mkIf cfg.config.autostart [ "default.target" ];
serviceConfig = {
# ExecStart = "${cfg.package}/bin/wob";
Type = "simple";
Restart = "always";
RestartSec = "20s";
};
path = [ cfg.package ];
script = ''
wobsock="$XDG_RUNTIME_DIR/${cfg.config.sock}"
rm -f "$wobsock" || true
mkfifo "$wobsock" && wob <> "$wobsock"
# TODO: cleanup should be done in a systemd OnFailure, or OnExit, or whatever
rm -f "$wobsock"
'';
};
services.wob-pulse = {
description = "notify wob when pulseaudio volume changes";
wantedBy = [ "wob.service" ];
serviceConfig = {
Type = "simple";
Restart = "always";
RestartSec = "20s";
};
path = with pkgs; [
# coreutils
gnugrep
gnused
pulseaudio
];
# environment.WOB_VERBOSE = "1";
script = ''
debug() {
printf "$@" >&2
printf "\n" >&2
}
verbose() {
if [ "$WOB_VERBOSE" = "1" ]; then
debug "$@"
fi
}
volismuted() {
pactl get-sink-mute @DEFAULT_SINK@ | grep -q "Mute: yes"
}
volget() {
if volismuted; then
verbose "muted"
printf "0"
else
pactl get-sink-volume @DEFAULT_SINK@ | head -n1 | cut -d'/' -f2 | sed 's/ //g' | sed 's/\%//'
fi
}
notify_volume_change() {
verbose "notify_volume_change"
vol=$(volget)
verbose "got volume: %d -> %d" "$lastvol" "$vol"
if [ "$vol" != "$lastvol" ]; then
debug "notify wob: %d -> %d" "$lastvol" "$vol"
printf "%s\n" "$vol" > "$XDG_RUNTIME_DIR/${cfg.config.sock}"
fi
lastvol="$vol"
}
pactl subscribe | while read -r line; do
verbose "pactl says: %s" "$line"
case "$line" in
"Event 'change' on sink "*)
notify_volume_change
;;
"Event 'change' on source "*)
# microphone volume changed. ignore.
;;
esac
done
'';
};
};
}

View File

@@ -15,7 +15,6 @@ in {
sane.programs.zeal = { sane.programs.zeal = {
# package = pkgs.zeal-qt6; #< TODO: upgrade system to qt6 versions of everything (i.e. jellyfin-media-player, nheko) # package = pkgs.zeal-qt6; #< TODO: upgrade system to qt6 versions of everything (i.e. jellyfin-media-player, nheko)
package = pkgs.zeal-qt5; package = pkgs.zeal-qt5;
slowToBuild = true;
persist.byStore.plaintext = [ persist.byStore.plaintext = [
".cache/Zeal" ".cache/Zeal"
".local/share/Zeal" ".local/share/Zeal"

Some files were not shown because too many files have changed in this diff Show More