Compare commits

..

1 Commits

Author SHA1 Message Date
371fc689f5 WIP: mootube: init at 2022-08-28 2023-11-29 07:36:01 +00:00
175 changed files with 8837 additions and 8150 deletions

18
TODO.md
View File

@@ -1,8 +1,9 @@
## BUGS
- nixpkgs date is incorrect (1970.01.01...)
- ringer (i.e. dino incoming call) doesn't prevent moby from sleeping
- Fractal opens links with non-preferred web browser
- `nix` operations from lappy hang when `desko` is unreachable
- could at least direct the cache to `http://desko-hn:5001`
- waybar isn't visible on moby until after `swaymsg reload`
## REFACTORING:
@@ -27,6 +28,7 @@
#### upstreaming to non-nixpkgs repos
- gtk: build schemas even on cross compilation: <https://github.com/NixOS/nixpkgs/pull/247844>
- sxmo: add new app entries
## IMPROVEMENTS:
@@ -68,14 +70,14 @@
- 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
- fix cpuidle (gets better power consumption): <https://xnux.eu/log/077.html>
- SwayNC:
- don't show MPRIS if no players detected
- this is a problem of playerctld, i guess
- also, the album icon when "Not playing" doesn't follow the size we give in the config
- that means mpris always takes up excessive space on moby
- add option to change audio output
- fix colors (red alert) to match overall theme
- moby: tune GPS
@@ -86,7 +88,12 @@
- manually do smoothing, as some layer between mepo and geoclue/gpsd?
- moby: show battery state on ssh login
- 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)
- 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
- 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
@@ -114,10 +121,13 @@
- 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
- 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:
- migrate MAME cabinet to nix
- boot it from PXE from servo?
- deploy to new server, and use it as a remote builder
- enable IPv6
- package lemonade lemmy app: <https://linuxphoneapps.org/apps/ml.mdwalters.lemonade/>

77
flake.lock generated
View File

@@ -1,5 +1,23 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1694529238,
"narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"mobile-nixos": {
"flake": false,
"locked": {
@@ -17,29 +35,13 @@
"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": {
"locked": {
"lastModified": 1705033721,
"narHash": "sha256-K5eJHmL1/kev6WuqyqqbS1cdNnSidIZ3jeqJ7GbrYnQ=",
"lastModified": 1700905716,
"narHash": "sha256-w1vHn2MbGfdC+CrP3xLZ3scsI06N0iQLU7eTHIVEFGw=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "a1982c92d8980a0114372973cbdfe0a307f1bdea",
"rev": "dfb95385d21475da10b63da74ae96d89ab352431",
"type": "github"
},
"original": {
@@ -51,11 +53,11 @@
},
"nixpkgs-unpatched": {
"locked": {
"lastModified": 1705254014,
"narHash": "sha256-4RrVNEqxeji4vqDgzSl7JoCD6a0ag5LF9zXFndtqrpE=",
"lastModified": 1701180790,
"narHash": "sha256-kYWcHsk2A1VUpiOvSo7Pq175WnSVeltspTGM2q+Cr3U=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "6c08fe3ccf437d8b26bec010fd925ddd6bb0d0d5",
"rev": "c9702bf40b036c0f1d3d5b0aaf3eee2bf920124c",
"type": "github"
},
"original": {
@@ -68,7 +70,6 @@
"root": {
"inputs": {
"mobile-nixos": "mobile-nixos",
"nixpkgs-next-unpatched": "nixpkgs-next-unpatched",
"nixpkgs-unpatched": "nixpkgs-unpatched",
"sops-nix": "sops-nix",
"uninsane-dot-org": "uninsane-dot-org"
@@ -82,11 +83,11 @@
"nixpkgs-stable": "nixpkgs-stable"
},
"locked": {
"lastModified": 1705201153,
"narHash": "sha256-y0/a4IMDZrc7lAkR7Gcm5R3W2iCBiARHnYZe6vkmiNE=",
"lastModified": 1701127353,
"narHash": "sha256-qVNX0wOl0b7+I35aRu78xUphOyELh+mtUp1KBx89K1Q=",
"owner": "Mic92",
"repo": "sops-nix",
"rev": "70dd0d521f7849338e487a219c1a07c429a66d77",
"rev": "b1edbf5c0464b4cced90a3ba6f999e671f0af631",
"type": "github"
},
"original": {
@@ -95,18 +96,34 @@
"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": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": [
"nixpkgs-unpatched"
]
},
"locked": {
"lastModified": 1704082841,
"narHash": "sha256-4g3lePnUALb8B1m3rEDD6rrZAq2pTN4qaSnStbd676U=",
"lastModified": 1699515935,
"narHash": "sha256-cJIuVrYorhIzG5pRFZb+ZtaKhTFD92ThC42SaxvSe/E=",
"ref": "refs/heads/master",
"rev": "4a1fa488e64e6c87c6c951e3fafb2684692f64d3",
"revCount": 234,
"rev": "8a4273489d945f21d7e0ca6aac952460c7d4c391",
"revCount": 216,
"type": "git",
"url": "https://git.uninsane.org/colin/uninsane"
},

175
flake.nix
View File

@@ -29,7 +29,7 @@
# - daily:
# - nixos-unstable cut from master after enough packages have been built in caches.
# - every 6 hours:
# - master auto-merged into staging and staging-next
# - master auto-merged into staging.
# - staging-next auto-merged into staging.
# - manually, approximately once per month:
# - staging-next is cut from staging.
@@ -44,9 +44,8 @@
# <https://github.com/nixos/nixpkgs/tree/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=nixos-staging";
# nixpkgs-unpatched.url = "github:nixos/nixpkgs?ref=nixos-staging-next";
nixpkgs-next-unpatched.url = "github:nixos/nixpkgs?ref=staging-next";
# nixpkgs-unpatched.url = "github:nixos/nixpkgs?ref=staging-next";
# nixpkgs-unpatched.url = "github:nixos/nixpkgs?ref=staging";
mobile-nixos = {
# <https://github.com/nixos/mobile-nixos>
@@ -75,7 +74,6 @@
outputs = {
self,
nixpkgs-unpatched,
nixpkgs-next-unpatched ? nixpkgs-unpatched,
mobile-nixos,
sops-nix,
uninsane-dot-org,
@@ -94,9 +92,9 @@
# 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
# repo as the main flake causes the main flake to have an unstable hash.
patchNixpkgs = variant: nixpkgs: (import ./nixpatches/flake.nix).outputs {
inherit variant nixpkgs;
self = patchNixpkgs variant nixpkgs;
nixpkgs = (import ./nixpatches/flake.nix).outputs {
self = nixpkgs;
nixpkgs = nixpkgs-unpatched;
} // {
# provide values that nixpkgs ordinarily sources from the flake.lock file,
# inaccessible to it here because of the import-from-derivation.
@@ -112,21 +110,21 @@
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, light ? false }: nixpkgs.lib.nixosSystem {
system = target;
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;
}
(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;
})
@@ -142,7 +140,8 @@
];
};
in {
nixosConfigurations = let
nixosConfigurations =
let
hosts = {
servo = { name = "servo"; local = "x86_64-linux"; target = "x86_64-linux"; };
desko = { name = "desko"; local = "x86_64-linux"; target = "x86_64-linux"; };
@@ -153,13 +152,27 @@
moby-light = { name = "moby"; local = "x86_64-linux"; target = "aarch64-linux"; light = true; };
rescue = { name = "rescue"; local = "x86_64-linux"; target = "x86_64-linux"; };
};
hostsNext = mapAttrs' (h: v: {
name = "${h}-next";
value = v // { nixpkgs = patchNixpkgs "staging-next" nixpkgs-next-unpatched; };
}) hosts;
in mapAttrValues evalHost (
hosts // hostsNext
);
# 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),
# - but fewer of their packages can be found in upstream caches.
cross = mapAttrValues evalHost hosts;
emulated = mapAttrValues
(args: evalHost (args // { 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 desko-light lappy lappy-light rescue;
# prefer cross-compiled builds for these machines:
inherit (cross) moby moby-light;
};
# unofficial output
# this produces a EFI-bootable .img file (GPT with a /boot partition and a system (/ or /nix) partition).
@@ -179,12 +192,9 @@
# 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;
hostPrograms = mapAttrValues (host: mapAttrValues (p: p.package) host.config.sane.programs) self.nixosConfigurations;
patched.nixpkgs = nixpkgs';
overlays = {
# N.B.: `nix flake check` requires every overlay to take `final: prev:` at defn site,
# hence the weird redundancy.
@@ -198,7 +208,7 @@
passthru = final: prev:
let
mobile = (import "${mobile-nixos}/overlay/overlay.nix");
uninsane = uninsane-dot-org.overlays.default;
uninsane = uninsane-dot-org.overlay;
in
(mobile final prev)
// (uninsane final prev)
@@ -229,27 +239,23 @@
# 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.
packages = mapAttrs
(system: passthruPkgs: passthruPkgs.lib.filterAttrs
(name: pkg:
(system: allPkgs:
allPkgs.lib.filterAttrs (name: pkg:
# keep only packages which will pass `nix flake check`, i.e. keep only:
# - derivations (not package sets)
# - packages that build for the given platform
(! 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)
(import ./pkgs { pkgs = passthruPkgs; }) // {
inherit (passthruPkgs) uninsane-dot-org;
(import ./pkgs { pkgs = allPkgs; }) // {
inherit (allPkgs) uninsane-dot-org;
}
)
)
# self.legacyPackages;
{
x86_64-linux = (nixpkgsCompiledBy "x86_64-linux").appendOverlays [
self.overlays.passthru
];
}
{ inherit (self.legacyPackages) x86_64-linux; }
;
apps."x86_64-linux" =
@@ -257,18 +263,13 @@
pkgs = self.legacyPackages."x86_64-linux";
sanePkgs = import ./pkgs { inherit pkgs; };
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})
# 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.
# 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.
# 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
# let the user handle that edge case by re-running this whole command
nixos-rebuild --flake '.#${host}' ${action} --target-host colin@${addr} --use-remote-sudo $@
'';
deployApp = host: addr: action: {
type = "app";
@@ -379,47 +380,23 @@
servo = deployApp "servo" "servo" "switch";
};
sync = {
type = "app";
program = builtins.toString (pkgs.writeShellScript "sync-all" ''
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"
'');
};
sync.desko = {
# copy music from servo to desko
# can run this from any device that has ssh access to desko and servo
type = "app";
program = builtins.toString (pkgs.writeShellScript "sync-to-desko" ''
sudo mount /mnt/desko-home
${pkgs.sane-scripts.sync-music}/bin/sane-sync-music --compat /mnt/servo-media/Music /mnt/desko-home/Music "$@"
'');
};
sync.lappy = {
# copy music from servo to lappy
# can run this from any device that has ssh access to lappy and servo
type = "app";
program = builtins.toString (pkgs.writeShellScript "sync-to-lappy" ''
sudo mount /mnt/lappy-home
${pkgs.sane-scripts.sync-music}/bin/sane-sync-music --compress --compat /mnt/servo-media/Music /mnt/lappy-home/Music "$@"
'');
};
sync.moby = {
# copy music from servo to moby
# can run this from any device that has ssh access to moby and servo
sync-moby = {
# copy music from the current device to moby
# TODO: should i actually sync from /mnt/servo-media/Music instead of the local drive?
type = "app";
program = builtins.toString (pkgs.writeShellScript "sync-to-moby" ''
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 --compress --compat --jobs 4 /mnt/servo-media/Music /mnt/moby-home/Music "$@"
${pkgs.sane-scripts.sync-music}/bin/sane-sync-music ~/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 +405,12 @@
program = builtins.toString (pkgs.writeShellScript "check-all" ''
nix run '.#check.nur'
RC0=$?
nix run '.#check.hostConfigs'
nix run '.#check.host-configs'
RC1=$?
nix run '.#check.rescue'
RC2=$?
echo "nur: $RC0"
echo "hostConfigs: $RC1"
echo "host-configs: $RC1"
echo "rescue: $RC2"
exit $(($RC0 | $RC1 | $RC2))
'');
@@ -450,19 +427,19 @@
--option restrict-eval true \
--option allow-import-from-derivation true \
--drv-path --show-trace \
-I nixpkgs=${nixpkgs-unpatched} \
-I nixpkgs=$(nix-instantiate --find-file nixpkgs) \
-I ../../ \
| tee # tee to prevent interactive mode
'');
};
check.hostConfigs = {
check.host-configs = {
type = "app";
program = let
checkHost = host: let
shellHost = pkgs.lib.replaceStrings [ "-" ] [ "_" ] host;
in ''
nix build -v '.#nixosConfigurations.${host}.config.system.build.toplevel' --out-link ./result-${host} -j2 "$@"
nix build -v '.#nixosConfigurations.${host}.config.system.build.toplevel' --out-link ./result-${host} -j2 $@
RC_${shellHost}=$?
'';
in builtins.toString (pkgs.writeShellScript
@@ -480,29 +457,11 @@
${checkHost "moby"}
${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 "lappy: $RC_lappy"
echo "servo: $RC_servo"
echo "moby: $RC_moby"
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))
''
);

View File

@@ -9,9 +9,6 @@
# services.distccd.enable = 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;
sane.ports.openFirewall = true; # for e.g. nix-serve

View File

@@ -22,6 +22,7 @@
# the device type informs (at least):
# - SXMO_WIFI_MODULE
# - SXMO_RTW_SCAN_INTERVAL
# - SXMO_SYS_FILES
# - SXMO_TOUCHSCREEN_ID
# - SXMO_MONITOR
# - SXMO_ALSA_CONTROL_NAME

View File

@@ -36,6 +36,7 @@
# sane.programs.consoleUtils.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.dialect.enableFor.user.colin = false; # drags in 700MB of x86 dependencies (e.g. gtk4)
sane.programs.mercurial.enableFor.user.colin = false; # does not cross compile
sane.programs.nvme-cli.enableFor.system = false; # does not cross compile (libhugetlbfs)

View File

@@ -74,7 +74,6 @@ in
# 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.
# 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.
# 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

@@ -15,9 +15,9 @@
};
sane.roles.build-machine.enable = true;
sane.roles.build-machine.emulation = false;
sane.zsh.showDeadlines = false; # ~/knowledge doesn't always exist
sane.programs.consoleUtils.suggestedPrograms = [
"consoleMediaUtils" # notably, for go2tv / casting
"pcConsoleUtils"
"sane-scripts.stop-all-servo"
];

View File

@@ -1,57 +1,6 @@
# 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?
# [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.
# even the stock `nixpkgs.linux` consumes > 16 GB of tmp
fileSystems."/tmp".options = [ "size=32G" ];
@@ -71,7 +20,7 @@
};
# slow, external storage (for archiving, etc)
fileSystems."/mnt/usb-hdd" = {
fileSystems."/mnt/persist/ext" = {
device = "/dev/disk/by-uuid/aa272cff-0fcc-498e-a4cb-0d95fb60631b";
fsType = "btrfs";
options = [
@@ -79,7 +28,12 @@
"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 = [
# TODO: this is overly broad; only need media and share directories to be persisted

View File

@@ -27,7 +27,9 @@ in
# view refused packets with: `sudo journalctl -k`
# 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.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
@@ -42,6 +44,41 @@ in
# "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 = ''
# # docs: `man resolved.conf`
# # 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,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 = [
./calibre.nix
./coturn.nix
./cryptocurrencies
./ddns-afraid.nix
./ddns-he.nix
./email
./ejabberd.nix
./freshrss.nix
@@ -17,9 +18,9 @@
./komga.nix
./lemmy.nix
./matrix
./monero.nix
./navidrome.nix
./nginx.nix
./nixos-prebuild.nix
./nixserve.nix
./ntfy
./pict-rs.nix

View File

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

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 = {
enable = true;
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" = {

View File

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

View File

@@ -20,6 +20,12 @@
tx-proxy=tor,127.0.0.1:9050
'';
services.i2p.enable = true;
# 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.
services.tor.enable = true;
services.tor.client.enable = true;
# 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

View File

@@ -54,10 +54,8 @@ in
services.nginx.recommendedOptimisation = true;
# 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 {
root = "${pkgs.uninsane-dot-org}/share/uninsane-dot-org";
# a lot of places hardcode https://uninsane.org,
# and then when we mix http + non-https, we get CORS violations
# and things don't look right. so force SSL.
@@ -67,28 +65,9 @@ in
# for OCSP stapling
sslTrustedCertificate = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt";
locations."/" = {
root = "${pkgs.uninsane-dot-org}/share/uninsane-dot-org";
tryFiles = "$uri $uri/ @fallback";
};
# 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;
'';
};
# uninsane.org/share/foo => /var/lib/uninsane/root/share/foo.
# yes, nginx does not strip the prefix when evaluating against the root.
locations."/share".root = "/var/lib/uninsane/root";
# allow matrix users to discover that @user:uninsane.org is reachable via matrix.uninsane.org
locations."= /.well-known/matrix/server".extraConfig =
@@ -129,19 +108,6 @@ in
# proxyPass = "http://127.0.0.1:4000";
# 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";
sane.persist.sys.byStore.plaintext = [
# TODO: mode?
{ user = "acme"; group = "acme"; path = "/var/lib/acme"; }
{ 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" ];
visibleTo.lan = 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);

View File

@@ -57,7 +57,7 @@
# what unit is this? kbps??
global.upload.speed_limit = 32000;
web.logging = true;
# debug = 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
};
@@ -66,8 +66,8 @@
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";
Restart = "on-failure";
RestartSec = "30s";
Group = "media";
};
};

View File

@@ -16,7 +16,7 @@
# transmission will by default not allow the world to read its files.
services.transmission.downloadDirPermissions = "775";
services.transmission.extraFlags = [
# "--log-level=debug"
"--log-level=debug"
];
services.transmission.settings = {
@@ -39,9 +39,9 @@
encryption = 2;
# units in kBps
speed-limit-down = 12000;
speed-limit-down = 3000;
speed-limit-down-enabled = true;
speed-limit-up = 800;
speed-limit-up = 600;
speed-limit-up-enabled = true;
# see: https://git.zknt.org/mirror/transmission/commit/cfce6e2e3a9b9d31a9dafedd0bdc8bf2cdb6e876?lang=bg-BG

View File

@@ -5,16 +5,18 @@
./fs.nix
./hardware
./home
./hostnames.nix
./hosts.nix
./ids.nix
./machine-id.nix
./net
./net.nix
./nix-path
./persist.nix
./programs
./secrets.nix
./ssh.nix
./users
./vpn.nix
];
sane.nixcache.enable-trusted-keys = true;
@@ -92,21 +94,7 @@
text = ''
# show which packages changed versions or are new/removed in this upgrade
# 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)
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
"infrequent"
));
} // lib.optionalAttrs (lib.hasPrefix "https://www.youtube.com/" raw.url) {
format = "video";
} // lib.optionalAttrs (raw.is_podcast or false) {
format = "podcast";
} // lib.optionalAttrs (raw.title or "" != "") {
@@ -62,7 +60,6 @@ let
(fromDb "acquiredlpbonussecretsecret.libsyn.com" // tech) # ACQ2 - more "Acquired" episodes
(fromDb "allinchamathjason.libsyn.com" // pol)
(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 "congressionaldish.libsyn.com" // pol) # Jennifer Briney
(fromDb "craphound.com" // pol) # Cory Doctorow -- both podcast & text entries
@@ -74,22 +71,20 @@ let
(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.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.simplecast.com/54nAGcIl" // pol) # The Daily
(fromDb "feeds.simplecast.com/82FI35Px" // pol) # Ezra Klein Show
(fromDb "feeds.simplecast.com/wgl4xEgL" // rat) # Econ Talk
(fromDb "feeds.simplecast.com/xKJ93w_w" // uncat) # Atlas Obscura
# (fromDb "feeds.simplecast.com/l2i9YnTd" // tech // pol) # Hard Fork (NYtimes tech)
(fromDb "feeds.simplecast.com/l2i9YnTd" // tech // pol) # Hard Fork (NYtimes tech)
(fromDb "feeds.transistor.fm/acquired" // tech)
(fromDb "lexfridman.com/podcast" // rat)
(fromDb "mapspodcast.libsyn.com" // uncat) # Multidisciplinary Association for Psychedelic Studies
(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/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.thelinuxexp.com" // tech) # low-brow linux/foss PR announcements
(fromDb "podcast.thelinuxexp.com" // tech)
(fromDb "politicalorphanage.libsyn.com" // pol)
(fromDb "reverseengineering.libsyn.com/rss" // tech) # UnNamed Reverse Engineering Podcast
(fromDb "rss.acast.com/deconstructed") # The Intercept - Deconstructed
@@ -98,7 +93,6 @@ let
(fromDb "rss.art19.com/60-minutes" // pol)
(fromDb "rss.art19.com/the-portal" // rat) # Eric Weinstein
(fromDb "seattlenice.buzzsprout.com" // pol)
(fromDb "srslywrong.com" // pol)
(fromDb "sharkbytes.transistor.fm" // tech) # Wireshark Podcast o_0
(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)
@@ -117,118 +111,137 @@ let
];
texts = [
(fromDb "amosbbatto.wordpress.com" // tech)
(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)
# AGGREGATORS (> 1 post/day)
(fromDb "lwn.net" // tech)
(fromDb "lynalden.com" // pol)
(fromDb "mako.cc/copyrighteous" // tech // pol) # rec by Cory Doctorow
(fromDb "mg.lol" // tech)
(fromDb "mindingourway.com" // rat)
(fromDb "morningbrew.com/feed" // pol)
(fromDb "overcomingbias.com" // rat) # Robin Hanson
# (fromDb "lesswrong.com" // rat)
# (fromDb "econlib.org" // pol)
# AGGREGATORS (< 1 post/day)
(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 "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 "sideways-view.com" // rat) # Paul Christiano
(fromDb "slimemoldtimemold.com" // rat)
(mkText "https://linuxphoneapps.org/blog/atom.xml" // tech // infrequent)
(fromDb "tuxphones.com" // tech)
(fromDb "spectrum.ieee.org" // tech)
(fromDb "stpeter.im/atom.xml" // pol)
# (fromDb "theregister.com" // 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 "unintendedconsequenc.es" // rat)
# (fromDb "vitalik.ca" // tech) # moved to vitalik.eth.limo
(fromDb "vitalik.eth.limo" // tech) # Vitalik Buterin
(fromDb "webcurious.co.uk" // uncat)
(fromDb "blog.thalheim.io" // tech) # Mic92
(fromDb "ascii.textfiles.com" // tech) # Jason Scott
(fromDb "xn--gckvb8fzb.com" // tech)
(mkSubstack "astralcodexten" // rat // daily) # Scott Alexander
(fromDb "amosbbatto.wordpress.com" // tech)
(fromDb "fasterthanli.me" // tech)
(fromDb "jeffgeerling.com" // 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 "doomberg" // tech // weekly) # articles are all pay-walled
(mkSubstack "eliqian" // rat // weekly)
(mkSubstack "oversharing" // pol // daily)
(mkSubstack "samkriss" // humor // infrequent)
(mkText "http://benjaminrosshoffman.com/feed" // pol // weekly)
(mkText "http://boginjr.com/feed" // tech // infrequent)
(mkText "https://acoup.blog/feed" // rat // weekly)
(mkText "https://anish.lakhwara.com/home.html" // tech // weekly)
(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/>
# (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
];
## David Rosenthal
(fromDb "blog.dshr.org" // pol)
## Matt Levine
(mkText "https://www.bloomberg.com/opinion/authors/ARbTQlRLRjE/matthew-s-levine.rss" // pol // weekly)
(fromDb "stpeter.im/atom.xml" // pol)
## Peter Saint-Andre -- side project of stpeter.im
(fromDb "philosopher.coach" // rat)
(fromDb "morningbrew.com/feed" // pol)
videos = [
(fromDb "youtube.com/@Channel5YouTube" // pol)
(fromDb "youtube.com/@ColdFusion")
(fromDb "youtube.com/@ContraPoints" // pol)
(fromDb "youtube.com/@Exurb1a")
(fromDb "youtube.com/@hbomberguy")
(fromDb "youtube.com/@JackStauber")
(fromDb "youtube.com/@PolyMatter")
# (fromDb "youtube.com/@rossmanngroup" // pol // tech) # Louis Rossmann
(fromDb "youtube.com/@TechnologyConnections" // tech)
(fromDb "youtube.com/@TheB1M")
(fromDb "youtube.com/@TomScottGo")
(fromDb "youtube.com/@Vihart")
(fromDb "youtube.com/@Vox")
(fromDb "youtube.com/@Vsauce")
# RATIONALITY/PHILOSOPHY/ETC
(mkSubstack "samkriss" // humor // infrequent)
(fromDb "unintendedconsequenc.es" // rat)
(fromDb "applieddivinitystudies.com" // rat)
(fromDb "slimemoldtimemold.com" // rat)
(fromDb "richardcarrier.info" // rat)
(fromDb "gwern.net" // rat)
## Jason Crawford
(fromDb "rootsofprogress.org" // rat)
## Robin Hanson
(fromDb "overcomingbias.com" // rat)
## Scott Alexander
(mkSubstack "astralcodexten" // rat // daily)
## Paul Christiano
(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 = [
(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 "turnoff.us" // 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
{
sane.feeds = texts ++ images ++ podcasts ++ videos;
sane.feeds = texts ++ images ++ podcasts;
assertions = builtins.map
(p: {

View File

@@ -1,4 +1,4 @@
{ config, lib, pkgs, ... }:
{ lib, pkgs, ... }:
{
imports = [
@@ -40,12 +40,6 @@
# non-free firmware
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
powerManagement.powertop.enable = false;
# linux CPU governor: <https://www.kernel.org/doc/Documentation/cpu-freq/governors.txt>
@@ -64,19 +58,10 @@
powerManagement.cpuFreqGovernor = "ondemand";
services.logind.extraConfig = ''
# see: `man logind.conf`
# dont shutdown when power button is short-pressed (commonly done an accident, or by cats).
# but do on long-press: useful to gracefully power-off server.
HandlePowerKey=lock
HandlePowerKeyLongPress=poweroff
HandleLidSwitch=lock
# dont shutdown when power button is short-pressed
HandlePowerKey=ignore
'';
# some packages build only if binfmt *isn't* present
nix.settings.system-features = lib.mkIf (config.boot.binfmt.emulatedSystems == []) [
"no-binfmt"
];
# services.snapper.configs = {
# root = {
# subvolume = "/";

View File

@@ -1,32 +1,19 @@
{ config, lib, ...}:
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 }; }
weightedMimes = prog: builtins.mapAttrs
(_key: desktop: {
priority = prog.mime.priority; desktop = desktop;
})
prog.mime.associations;
weightedMimes = prog: builtins.mapAttrs (_key: desktop: { priority = prog.mime.priority; desktop = desktop; }) prog.mime.associations;
# [ { "<mime-type>" = { priority, desktop } ]; } ] -> { "<mime-type>" = [ { priority, desktop } ... ]; }
mergeMimes = mimes: lib.foldAttrs (item: acc: [item] ++ acc) [] mimes;
# [ { priority, desktop } ... ] -> Self
sortOneMimeType = associations: builtins.sort
(l: r: assert l.priority != r.priority; l.priority < r.priority)
associations;
sortOneMimeType = associations: builtins.sort (l: r: assert l.priority != r.priority; l.priority < r.priority) associations;
sortMimes = mimes: builtins.mapAttrs (_k: sortOneMimeType) mimes;
removePriorities = mimes: builtins.mapAttrs
(_k: associations: builtins.map (a: a.desktop) associations)
mimes;
removePriorities = mimes: builtins.mapAttrs (_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
{
# 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
xdg.mime.enable = true;
xdg.mime.defaultApplications = removePriorities (sortMimes (mergeMimes enabledWeightedMimes));
}

View File

@@ -1,3 +1,4 @@
# TODO: move to hosts/common/
{ config, lib, ... }:
{

View File

@@ -19,7 +19,7 @@
};
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.host_pubkey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIO1N/IT3nQYUD+dBlU1sTEEVMxfOyMkrrDeyHcYgnJvw";
wg-home.pubkey = "I7XIR1hm8bIzAtcAvbhWOwIAabGkuEvbWH/3kyIB1yA=";

View File

@@ -53,10 +53,6 @@
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.guest.uid = 1100;

View File

@@ -1,12 +1,6 @@
{ lib, ... }:
{
imports = [
./dns.nix
./hostnames.nix
./upnp.nix
./vpn.nix
];
# the default backend is "wpa_supplicant".
# wpa_supplicant reliably picks weak APs to connect to.
# see: <https://gitlab.freedesktop.org/NetworkManager/NetworkManager/-/issues/474>
@@ -41,6 +35,10 @@
# e.g. openconnect drags in webkitgtk (for SSO)!
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
networking.networkmanager.extraConfig = ''
[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,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

@@ -10,7 +10,7 @@ in
type = types.submodule {
options.autostart = mkOption {
type = types.bool;
default = false;
default = true;
};
};
};
@@ -22,6 +22,14 @@ in
name = ''"view members" default to false'';
hash = "sha256-9BX8iO86CU1lNrKS1G2BjDR+3IlV9bmhRNTsLrxChwQ=";
})
(pkgs.fetchpatch {
# this makes it so Abaddon reports its app_name in notifications.
# not 100% necessary; just a nice-to-have. maybe don't rely on it until it's merged upstream.
# upstream PR: <https://github.com/uowuo/abaddon/pull/247>
url = "https://git.uninsane.org/colin/abaddon/commit/18cd863fdbb5e6b1e9aaf9394dbd673d51839f30.patch";
name = "set glib application name";
hash = "sha256-IFYxf1D8hIsxgZehGd6hL3zJiBkPZfWGm+Faaa5ZFl4=";
})
];
});

View File

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

View File

@@ -44,10 +44,11 @@ in
"sane-scripts.shutdown"
"sane-scripts.sudo-redirect"
"sane-scripts.sync-from-servo"
"sane-scripts.tag-music"
"sane-scripts.vpn"
"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.ip-port-forward"
@@ -109,7 +110,6 @@ in
"wget"
"wirelesstools" # iwlist
"xq" # jq for XML
# "zfs" # doesn't cross-compile (requires samba)
];
sysadminExtraUtils = declPackageSet [
"backblaze-b2"
@@ -144,7 +144,6 @@ in
"lshw"
# "memtester"
"mercurial" # hg
"mimeo" # like xdg-open
"neovim" # needed as a user package, for swap persistence
# "nettools"
# "networkmanager"
@@ -183,9 +182,7 @@ in
];
consoleMediaUtils = declPackageSet [
"catt" # cast videos to chromecast
"ffmpeg"
"go2tv" # cast videos to UPNP/DLNA device (i.e. tv).
"imagemagick"
"sox"
"yt-dlp"
@@ -222,9 +219,6 @@ in
cargo.persist.byStore.plaintext = [ ".cargo" ];
# auth token, preferences
delfin.persist.byStore.private = [ ".config/delfin" ];
# creds, but also 200 MB of node modules, etc
discord.persist.byStore.private = [ ".config/discord" ];
@@ -282,6 +276,9 @@ in
whalebird.persist.byStore.private = [ ".config/Whalebird" ];
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 {

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, ... }:
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 = {
# package = chattyNoOauth;
package = pkgs.chatty-latest;
package = chatty-latest;
suggestedPrograms = [ "gnome-keyring" ];
persist.byStore.private = [
".local/share/chatty" # matrix avatars and files

View File

@@ -1,184 +1,52 @@
#!/bin/sh
#!/usr/bin/env nix-shell
#!nix-shell -i bash
usage() {
echo "usage: battery_estimate [options...]"
echo
echo "pretty-prints a battery estimate (icon to indicate state, and a duration estimate)"
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
icon_bat_chg=("󰢟" "󱊤" "󱊥" "󰂅")
icon_bat_dis=("󰂎" "󱊡" "󱊢" "󱊣")
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
}
bat_dis="󱊢"
bat_chg="󱊥"
try_path() {
# assigns output variables:
# - perc, perc_from_full (0-100)
# returns:
# - perc, perc_left (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))
perc_left=$((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
fi
if [ -f "$1/energy_full" ] && [ -f "$1/energy_now" ]; then
# energy 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"
}
try_path "/sys/class/power_supply/axp20x-battery" # Pinephone
try_path "/sys/class/power_supply/BAT0" # Thinkpad
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>
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_in_min=$(($hr * 60))
min=$(($3 - $hr_in_min))
printf "%s%s%d%s%02d%s" "$1" "$suffix_icon" "$hr" "$symbol_hr" "$min" "$symbol_min"
else
log "charge/discharge duration > 1d"
printf "%s%s%s" "$1" "$suffix_icon" "$2" # more than 1d
printf "%s %dh%02dm" "$1" "$hr" "$min"
fi
}
pretty_output() {
if [ -n "$perc" ]; then
duration=""
if [ "$rate" -gt 0 ]; then
log "charging"
icon="$(render_icon chg $perc)"
duration="$(($full * 60 * $perc_from_full / (100 * $rate)))"
else
log "discharging"
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
if [[ $rate -lt 0 ]]; then
# discharging
fmt_minutes "$bat_dis" '∞' "$(($full * 60 * $perc / (-100 * $rate)))"
elif [[ $rate -gt 0 ]]; then
# charging
fmt_minutes "$bat_chg" '100%' "$(($full * 60 * $perc_left / (100 * $rate)))"
elif [[ "$perc" != "" ]]; then
echo "$bat_dis $perc%"
fi

View File

@@ -3,11 +3,6 @@
-- - can also use #rrggbb syntax
-- example configs: <https://forum.manjaro.org/t/conky-showcase-2022/97123>
-- 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 = {
out_to_wayland = true,
@@ -40,43 +35,16 @@ conky.config = {
color2 = '404040',
}
vars = {
-- 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
-- texeci <interval_sec> <cmd>: run the command periodically, _in a separate thread_ so as not to block rendering
conky.text = [[
${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}
${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}
${color2}${shadecolor a4d7d0}${font sans-serif:size=16}${alignc}⇅ ${downspeedf wlan0}]] .. vars.kBps .. [[${font}
${font sans-serif:size=16}${alignc}☵ $memperc]] .. vars.percent .. [[  $cpu]] .. vars.percent .. [[${font}
${color2}${shadecolor a4d7d0}${font sans-serif:size=16}${alignc}⇅ ${downspeedf wlan0}K/s${font}
${font sans-serif:size=16}${alignc}☵ $memperc%  $cpu%${font}
]]

View File

@@ -7,12 +7,10 @@
./alacritty.nix
./animatch.nix
./assorted.nix
./audacity.nix
./bemenu.nix
./brave.nix
./calls.nix
./cantata.nix
./catt.nix
./chatty.nix
./conky
./cozy.nix
@@ -34,7 +32,6 @@
./gnome-feeds.nix
./gnome-keyring.nix
./gnome-weather.nix
./go2tv.nix
./gpodder.nix
./gthumb.nix
./gtkcord4.nix
@@ -45,24 +42,19 @@
./koreader
./libreoffice.nix
./lemoa.nix
./loupe.nix
./mako.nix
./megapixels.nix
./mepo.nix
./mimeo
./mopidy.nix
./mpv.nix
./msmtp.nix
./nautilus.nix
./neovim.nix
./newsflash.nix
./nheko.nix
./nix-index.nix
./notejot.nix
./ntfy-sh.nix
./obsidian.nix
./offlineimap.nix
./open-in-mpv.nix
./planify.nix
./playerctl.nix
./rhythmbox.nix
./ripgrep.nix
@@ -84,10 +76,8 @@
./wike.nix
./wine.nix
./wireshark.nix
./wob.nix
./xarchiver.nix
./zeal.nix
./zecwallet-lite.nix
./zsh
];

View File

@@ -40,7 +40,7 @@ in
type = types.submodule {
options.autostart = mkOption {
type = types.bool;
default = true;
default = false;
};
};
};

View File

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

View File

@@ -38,16 +38,25 @@ let
# defaultSettings = firefoxSettings;
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
# it can be further customized via ~/.librewolf/librewolf.overrides.cfg
inherit (cfg.browser) extraPrefsFiles libName;
nativeMessagingHosts = lib.optionals cfg.addons.browserpass-extension.enable [
pkgs.browserpass
] ++ lib.optionals cfg.addons.fxCast.enable [
pkgs.fx-cast-bridge
];
extraNativeMessagingHosts = optional cfg.addons.browserpass-extension.enable pkgs.browserpass;
# extraNativeMessagingHosts = [ pkgs.gopass-native-messaging-host ];
nixExtensions = concatMap (ext: optional ext.enable ext.package) (attrValues cfg.addons);
@@ -104,17 +113,7 @@ let
# NewTabPage = true;
};
# 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 {
options = {
@@ -158,16 +157,6 @@ in
default = {};
};
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 = {
package = pkgs.firefox-extensions.browserpass-extension;
enable = lib.mkDefault true;
@@ -176,10 +165,6 @@ in
package = pkgs.firefox-extensions.bypass-paywalls-clean;
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 = {
package = pkgs.firefox-extensions.ether-metamask;
enable = lib.mkDefault false; # until i can disable the first-run notification
@@ -188,10 +173,6 @@ in
package = pkgs.firefox-extensions.i2p-in-private-browsing;
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 = {
package = pkgs.firefox-extensions.sidebery;
enable = lib.mkDefault true;
@@ -214,10 +195,6 @@ in
sane.programs.firefox = {
inherit package;
suggestedPrograms = [
"open-in-mpv"
];
mime.associations = let
inherit (cfg.browser) desktop;
in {
@@ -262,18 +239,6 @@ in
// note that too-large scrollbars (like 50px wide) tend to obscure content (and make buttons unclickable)
defaultPref("widget.non-native-theme.scrollbar.size.override", 20);
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 = {};
# 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.
# 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.
# - 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.
# - 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.
#
### debugging:
# - `RUST_LOG=flare=trace flare`
#
### 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 signatures (to reset, run `sane-wipe-fractal`):
# - 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 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`
# - the Websocket error seems to be unrelated, occurs during normal/good operation
# - 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
# No current session
@@ -30,32 +26,6 @@
# ```
# - 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.
# desktop _seems_ more reliable than on mobile, but not confident.

View File

@@ -23,7 +23,7 @@ let
in
{
sane.programs.fractal = {
package = pkgs.fractal-nixified.optimized;
package = pkgs.fractal-nixified;
# package = pkgs.fractal-latest;
# package = pkgs.fractal-next;

View File

@@ -13,7 +13,6 @@ in
user.name = "Colin";
user.email = "colin@uninsane.org";
alias.amend = "commit --amend --no-edit";
alias.br = "branch";
alias.co = "checkout";
alias.cp = "cherry-pick";
@@ -24,7 +23,6 @@ in
alias.st = "status";
alias.stat = "status";
diff.noprefix = true; #< don't show a/ or b/ prefixes in diffs
# difftastic docs:
# - <https://difftastic.wilfred.me.uk/git.html>
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
feeds = sane-lib.feeds;
all-feeds = config.sane.feeds;
wanted-feeds = feeds.filterByFormat [ "podcast" "video" ] all-feeds;
wanted-feeds = feeds.filterByFormat ["podcast"] all-feeds;
in {
sane.programs.gpodder = {
package = pkgs.gpodder-adaptive-configured.overrideAttrs (base: {

View File

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

View File

@@ -1,6 +1,3 @@
# 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;
@@ -12,26 +9,11 @@ in
type = types.submodule {
options.autostart = mkOption {
type = types.bool;
default = true;
default = false;
};
};
};
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?

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, ... }:
let
feeds = sane-lib.feeds;
allFeeds = config.sane.feeds;
wantedFeeds = feeds.filterByFormat [ "text" ] allFeeds;
wantedFeeds = feeds.filterByFormat [ "image" "text" ] allFeeds;
koreaderRssEntries = builtins.map (feed:
# format:
# { "<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.
let fields = [
(lib.escapeShellArg feed.url)
"limit = 20"
"limit = 5"
"download_full_article = true"
"include_images = true"
"enable_filter = false"
@@ -38,13 +26,9 @@ in {
package = pkgs.koreader-from-src;
# koreader applies these lua "patches" at boot:
# - <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-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:
# - <repo:koreader/koreader:plugins/newsdownloader.koplugin/feed_config.lua>
fs.".config/koreader/news/feed_config.lua".symlink.text = ''

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,19 +23,16 @@ in
package = pkgs.wrapMpv pkgs.mpv-unwrapped {
scripts = with pkgs.mpvScripts; [
mpris
uosc
# pkgs.mpv-uosc-latest
# uosc
pkgs.mpv-uosc-latest
];
extraMakeWrapperArgs = lib.optionals (cfg.config.vo != null) [
# 2023/08/29: fixes an error where mpv on moby launches with the message
# "DRM_IOCTL_MODE_CREATE_DUMB failed: Cannot allocate memory"
# audio still works, and controls, screenshotting, etc -- just not the actual rendering
#
# 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.
#
# TODO(2023/12/03): remove once mesa 23.3.1 lands: <https://github.com/NixOS/nixpkgs/pull/265740>
#
# backend compatibility (2023/10/22):
# run with `--vo=help` to see a list of all output options.
# non-exhaustive (W=works, F=fails, A=audio-only, U=audio+ui only (no video))
@@ -57,46 +54,13 @@ in
];
};
persist.byStore.plaintext = [ ".local/state/mpv/watch_later" ];
fs.".config/mpv/input.conf".symlink.text = let
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>
fs.".config/mpv/input.conf".symlink.text = ''
# let volume/power keys be interpreted by the system.
# this is important for sxmo.
# mpv defaults is POWER = close, VOLUME_{UP,DOWN} = adjust application-level volume
POWER ignore
VOLUME_UP 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 = ''
save-position-on-quit=yes
@@ -127,7 +91,6 @@ in
in ''
# docs:
# - <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>
timeline_style=bar
timeline_persistency=paused,audio
@@ -144,7 +107,8 @@ in
text_border=6.0
font_bold=yes
color=foreground=ff8080,background_text=ff8080
background_text=ff8080
foreground=ff8080
ui_scale=1.0
'';
@@ -158,11 +122,7 @@ in
mime.associations."video/mp4" = "mpv.desktop";
mime.associations."video/quicktime" = "mpv.desktop";
mime.associations."video/webm" = "mpv.desktop";
mime.associations."video/x-flv" = "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

@@ -1,19 +1,12 @@
# 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, ... }:
let
feeds = sane-lib.feeds;
all-feeds = config.sane.feeds;
# text/image: newsflash renders these natively
# podcast/video: newsflash dispatches these to xdg-open
wanted-feeds = feeds.filterByFormat [ "text" "image" "podcast" "video" ] all-feeds;
wanted-feeds = feeds.filterByFormat ["text" "image"] all-feeds;
in {
sane.programs.newsflash = {
slowToBuild = true; # mainly for desktop: webkitgtk-6.0
persist.byStore.plaintext = [ ".local/share/news-flash" ];
fs.".config/newsflashFeeds.opml".symlink.text =
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,10 +1,3 @@
# 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;
@@ -37,8 +30,6 @@ in
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

@@ -17,9 +17,6 @@
{ config, lib, pkgs, ... }:
let
cfg = config.sane.programs.swaynotificationcenter;
mprisIconSize = 48;
fbcli-wrapper = pkgs.writeShellApplication {
name = "swaync-fbcli";
runtimeInputs = [
@@ -137,12 +134,6 @@ in
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" ];
fs.".config/swaync/style.css".symlink.text = ''
@@ -227,27 +218,6 @@ in
# - SWAYNC_REPLACES_ID
# - SWAYNC_ID
# - SWAYNC_SUMMARY
# 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.
app-name = "(Chats|Dino|discord|Element|Fractal|gtkcord4)";
@@ -397,21 +367,21 @@ in
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 [
] ++ lib.optionals config.sane.programs.abaddon.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";
command = "${systemctl-toggle}/bin/systemctl-toggle --user abaddon";
active = "${pkgs.systemd}/bin/systemctl is-active --user abaddon";
}
# ] ++ lib.optionals config.sane.programs.gtkcord4.enabled [
# # XXX: disabled in favor of abaddon: gtkcord4 leaks memory
# {
# 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";
@@ -444,7 +414,7 @@ in
clear-all-button = true;
};
mpris = {
image-size = mprisIconSize;
image-size = 64;
image-radius = 8;
};
title = {

View File

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

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

@@ -1,16 +0,0 @@
# zcash wallet
#
# N.B.: zecwallet is UNMAINTAINED. as of 2024/01/01 it requires manual intervention to sync:
# 1. launch it, and wait 5min for it to timeout.
# 2. set the node address to https://lwd3.zcash-infra.com:9067
# 3. close the application and re-open
# more info:
# - <https://forum.zcashcommunity.com/t/zecwallet-lightwalletd-server-continuation/45659/>
# - <https://status.zcash-infra.com/>
{ ... }:
{
sane.programs.zecwallet-lite = {
# zcash coins. safe to delete, just slow to regenerate (10-60 minutes)
persist.byStore.private = [ ".zcash" ];
};
}

View File

@@ -85,16 +85,6 @@ in
# emulate bash keybindings
bindkey -e
# fixup bindings not handled by bash, see: <https://wiki.archlinux.org/title/Zsh#Key_bindings>
# `bindkey -e` seems to define most of the `key` array. everything in the Arch defaults except for these:
key[Backspace]="''${terminfo[kbs]}"
key[Control-Left]="''${terminfo[kLFT5]}"
key[Control-Right]="''${terminfo[kRIT5]}"
key[Shift-Tab]="''${terminfo[kcbt]}"
bindkey -- "''${key[Delete]}" delete-char
bindkey -- "''${key[Control-Left]}" backward-word
bindkey -- "''${key[Control-Right]}" forward-word
# or manually recreate what i care about...
# key[Left]=''${terminfo[kcub1]}
# key[Right]=''${terminfo[kcuf1]}

View File

@@ -13,7 +13,6 @@
];
group = "users";
extraGroups = [
"clightning" # servo, for clightning-cli
"dialout" # required for modem access (moby)
"export" # to read filesystem exports (servo)
"feedbackd" # moby, so `fbcli` can control vibrator and LEDs

View File

@@ -8,41 +8,21 @@
# - copy the Address, PublicKey, Endpoint from OVPN's config
# N.B.: maximum interface name in Linux is 15 characters.
let
def-wg-vpn = name: { endpoint, publicKey, address, dns, privateKeyFile }: {
# networking.wg-quick.interfaces."${name}" = {
# inherit address privateKeyFile dns;
# peers = [
# {
# allowedIPs = [
# "0.0.0.0/0"
# "::/0"
# ];
# inherit endpoint publicKey;
# }
# ];
# # to start: `systemctl start wg-quick-${name}`
# autostart = false;
# };
systemd.network.netdevs."${name}" = {
# see: `man 5 systemd.netdev`
wireguardConfig = {
PrivateKeyFile = privateKeyFile;
};
wireguardPeers = [{
AllowedIPs = [
def-wg-vpn = name: { endpoint, publicKey, address, dns, privateKeyFile, extraOptions ? {} }: {
networking.wg-quick.interfaces."${name}" = {
inherit address privateKeyFile dns;
peers = [
{
allowedIPs = [
"0.0.0.0/0"
"::/0"
];
Endpoint = endpoint;
PublicKey = publicKey;
}];
};
systemd.network.networks."${name}" = {
# see: `man 5 systemd.network`
matchConfig.Name = name;
networkConfig.Address = address;
networkConfig.DNS = dns;
};
inherit endpoint publicKey;
}
];
# to start: `systemctl start wg-quick-${name}`
autostart = false;
} // extraOptions;
};
def-ovpn = name: { endpoint, publicKey, address }: def-wg-vpn "ovpnd-${name}" {
inherit endpoint publicKey address;

View File

@@ -57,12 +57,11 @@ in
sane.programs.guiBaseApps = declPackageSet [
"abaddon" # discord client
"alacritty" # terminal emulator
"delfin" # Jellyfin client
"dialect" # language translation
"dino" # XMPP client
# "emote"
"evince" # works on phosh
"flare-signal" # gtk4 signal client
# "flare-signal" # gtk4 signal client
# "foliate" # e-book reader
"fractal" # matrix client
"g4music" # local music player
@@ -78,23 +77,17 @@ in
# "gnome.gnome-system-monitor"
# "gnome.gnome-terminal" # works on phosh
"gnome.gnome-weather"
"gnome.seahorse" # keyring/secret manager
"gnome-frog" # OCR/QR decoder
"gpodder"
# "gthumb"
"gtkcord4" # Discord client. 2023/11/21: disabled because v0.0.12 leaks memory
"gthumb"
# "gtkcord4" # Discord client. 2023/11/21: disabled because it leaks memory
"lemoa" # lemmy app
"libnotify" # for notify-send; debugging
# "lollypop"
"loupe" # image viewer
"mate.engrampa" # archive manager
"mepo" # maps viewer
"mpv"
"networkmanagerapplet" # for nm-connection-editor: it's better than not having any gui!
"ntfy-sh" # notification service
# "newsflash" # RSS viewer
# "newsflash"
"pavucontrol"
"pwvucontrol" # pipewire version of pavu
# "picard" # music tagging
# "libsForQt5.plasmatube" # Youtube player
"signal-desktop"
@@ -103,7 +96,6 @@ in
# "tdesktop" # broken on phosh
# "tokodon"
"tuba" # mastodon/pleroma client (stores pw in keyring)
"vulkan-tools" # vulkaninfo
# "whalebird" # pleroma client (Electron). input is broken on phosh.
"xdg-terminal-exec"
"xterm" # broken on phosh
@@ -112,15 +104,13 @@ in
sane.programs.handheldGuiApps = declPackageSet [
"calls" # gnome calls (dialer/handler)
# "celluloid" # mpv frontend
# "chatty" # matrix/xmpp/irc client (2023/12/29: disabled because broken cross build)
"chatty" # matrix/xmpp/irc client
"cozy" # audiobook player
"epiphany" # gnome's web browser
# "iotas" # note taking app
"gpodder"
"komikku"
"koreader"
"megapixels" # camera app
"notejot" # note taking, e.g. shopping list
"planify" # todo-tracker/planner
"portfolio-filemanager"
"tangram" # web browser
"wike" # Wikipedia Reader
@@ -163,9 +153,8 @@ in
"mumble"
# "nheko" # Matrix chat client
# "obsidian"
"openscad" # 3d modeling
# "rhythmbox" # local music player
# "slic3r"
"slic3r"
"soundconverter"
"spotify" # x86-only
"steam"

View File

@@ -21,6 +21,7 @@ in
services.xserver.displayManager.gdm.enable = true;
# gnome does networking stuff with networkmanager
networking.useDHCP = false;
networking.networkmanager.enable = true;
networking.wireless.enable = lib.mkForce false;
};

View File

@@ -74,6 +74,7 @@ in
services.gnome.gnome-online-miners.enable = mkForce false;
# XXX: phosh enables networkmanager by default; can probably disable these lines
networking.useDHCP = false;
networking.networkmanager.enable = true;
networking.wireless.enable = lib.mkForce false;

View File

@@ -4,8 +4,8 @@
# sway-config docs: `man 5 sway`
let
cfg = config.sane.gui.sway;
wrapSway = sway': swayOverrideArgs: let
# `wrapSway` exists to create a `sway.desktop` file
defaultPackage = let
# `defaultPackage` exists to create a `sway.desktop` file
# which will launch sway with our desired debugging facilities.
# i.e. redirect output to syslog.
scfg = config.programs.sway;
@@ -14,36 +14,31 @@ let
echo "launching sway-session (sway.desktop)..." | ${systemd-cat} --identifier=sway-session
sway 2>&1 | ${systemd-cat} --identifier=sway-session
'';
origSway = pkgs.sway.override {
# this override is what `programs.nixos` would do internally if we left `package` unset.
configuredSway = sway'.override swayOverrideArgs;
extraSessionCommands = scfg.extraSessionCommands;
extraOptions = scfg.extraOptions;
withBaseWrapper = scfg.wrapperFeatures.base;
withGtkWrapper = scfg.wrapperFeatures.gtk;
isNixOS = true;
# TODO: `enableXWayland = ...`?
};
desktop-file = pkgs.runCommand "sway-desktop-wrapper" {} ''
mkdir -p $out/share/wayland-sessions
substitute ${configuredSway}/share/wayland-sessions/sway.desktop $out/share/wayland-sessions/sway.desktop \
substitute ${origSway}/share/wayland-sessions/sway.desktop $out/share/wayland-sessions/sway.desktop \
--replace 'Exec=sway' 'Exec=${swayWithLogger}/bin/sway-session'
# XXX(2023/09/24) phog greeter (mobile greeter) will crash if DesktopNames is not set
echo "DesktopNames=Sway" >> $out/share/wayland-sessions/sway.desktop
'';
in pkgs.symlinkJoin {
inherit (configuredSway) name meta;
inherit (origSway) name meta;
# the order of these `paths` is suchs that the desktop-file should claim share/wayland-sessions/sway.deskop,
# overriding whatever the configuredSway provides
paths = [ desktop-file configuredSway ];
# overriding whatever the origSway provides
paths = [ desktop-file origSway ];
passthru = {
inherit (configuredSway.passthru) providedSessions;
# nixos/modules/programs/wayland/sway.nix will call `.override` on the package we provide it
override = wrapSway sway';
inherit (origSway.passthru) providedSessions;
};
};
defaultPackage = wrapSway pkgs.sway {
# this is technically optional, in that the nixos sway module will call `override` with these args anyway.
# but that wasn't always the case; it may change again; so don't rely on it.
inherit (config.programs.sway)
extraSessionCommands extraOptions;
withBaseWrapper = config.programs.sway.wrapperFeatures.base;
withGtkWrapper = config.programs.sway.wrapperFeatures.gtk;
isNixOS = true;
# TODO: `enableXWayland = ...`?
};
in
{
options = with lib; {
@@ -181,7 +176,7 @@ in
"playerctl" # for waybar & particularly to have playerctld running
# "mako" # notification daemon
"swaynotificationcenter" # notification daemon
"wob" # render volume changes on-screen
# # "pavucontrol"
# "gnome.gnome-bluetooth" # XXX(2023/05/14): broken
# "gnome.gnome-control-center" # XXX(2023/06/28): depends on webkitgtk4_1
"sway-contrib.grimshot"
@@ -202,7 +197,7 @@ in
sane.gui.gtk.enable = lib.mkDefault true;
# sane.gui.gtk.gtk-theme = lib.mkDefault "Fluent-Light-compact";
sane.gui.gtk.gtk-theme = lib.mkDefault "Tokyonight-Light-B";
# sane.gui.gtk.icon-theme = lib.mkDefault "HighContrast"; # 4/5 coverage on moby
sane.gui.gtk.icon-theme = lib.mkDefault "HighContrast"; # 4/5 coverage on moby
# sane.gui.gtk.icon-theme = lib.mkDefault "WhiteSur"; # 3.5/5 coverage on moby, but it provides a bunch for Fractal/Dino
# sane.gui.gtk.icon-theme = lib.mkDefault "Humanity"; # 3.5/5 coverage on moby, but it provides the bookmark icon
# sane.gui.gtk.icon-theme = lib.mkDefault "Paper"; # 3.5/5 coverage on moby, but it provides the bookmark icon
@@ -235,7 +230,6 @@ in
# emulate pulseaudio for legacy apps (e.g. sxmo-utils)
pulse.enable = true;
};
services.gvfs.enable = true; # allow nautilus to mount remote filesystems (e.g. ftp://...)
# rtkit/RealtimeKit: allow applications which want realtime audio (e.g. Dino? Pulseaudio server?) to request it.
# this might require more configuration (e.g. polkit-related) to work exactly as desired.
# - readme outlines requirements: <https://github.com/heftig/rtkit>
@@ -260,6 +254,7 @@ in
# };
# sane.persist.sys.byStore.plaintext = [ "/var/lib/alsa" ];
networking.useDHCP = false;
networking.networkmanager.enable = true;
networking.wireless.enable = lib.mkForce false;
@@ -282,7 +277,6 @@ in
# - org.freedesktop.impl.portal.ScreenCast
# - org.freedesktop.impl.portal.Screenshot
enable = true;
package = cfg.package;
extraPackages = []; # nixos adds swaylock, swayidle, foot, dmenu by default
# extraOptions = [ "--debug" ];
# "wrapGAppsHook wrapper to execute sway with required environment variables for GTK applications."
@@ -293,6 +287,7 @@ in
# this sets XDG_CURRENT_DESKTOP=sway
# and makes sure that sway is launched dbus-run-session.
wrapperFeatures.base = true;
package = cfg.package;
};
programs.xwayland.enable = cfg.config.xwayland;
# provide portals for:

View File

@@ -1,6 +1,3 @@
# docs:
# - `man 5 sway`
#
# xwayland enable|disable|force
# - enable: lazily launch xwayland on first client connection
# - disable: never launch xwayland
@@ -151,17 +148,10 @@ for_window [title="megapixels"] inhibit_idle open
for_window [app_id="im.dino.Dino"] move container to workspace number 1
for_window [app_id="org.gnome.Fractal"] move container to workspace number 1
for_window [app_id="geary"] move container to workspace number 1
for_window [app_id="signal"] move container to workspace number 1
# class=Signal for when it's running with Xwayland
for_window [class="Signal"] move container to workspace number 1
for_window [app_id="so.libdb.gtkcord4"] move container to workspace number 1
for_window [app_id="xyz.diamondb.gtkcord4"] move container to workspace number 1
for_window [app_id="abaddon"] move container to workspace number 1
# window display settings
# force KOReader to always display a titlebar, even when the only window being rendered.
# desirable primarily to avoid slooow reflows when another app is opened. but also nice to have the book title rendered.
for_window [app_id="KOReader"] border normal
### displays
## DESKTOP
output "Goldstar Company Ltd LG ULTRAWIDE 0x00004E94" {

View File

@@ -59,8 +59,6 @@ window#waybar {
#cpu,
#custom-media,
#custom-swaync,
#custom-sxmo,
#custom-sxmo-sane,
#disk,
#idle_inhibitor,
#memory,

View File

@@ -32,8 +32,6 @@ in
# - source: <https://www.reddit.com/r/swaywm/comments/ni0vso/waybar_spotify_tracktitle/>
# - alternative: <https://github.com/Alexays/Waybar/wiki/Module:-MPRIS>
# - alternative: <https://github.com/Alexays/Waybar/wiki/Module:-Custom#mpris-controller>
# - alternative: <https://www.reddit.com/r/swaywm/comments/g6nw46/comment/fob604s/>
# - this one shades the background based on how far through the song you are
#
# N.B.: for this to behave well with multiple MPRIS clients,
# `playerctld` must be enabled. see: <https://github.com/altdesktop/playerctl/issues/161>

View File

@@ -68,11 +68,6 @@ let
'';
hookPkgs = {
block_suspend = pkgs.static-nix-shell.mkBash {
pname = "sxmo_hook_block_suspend.sh";
pkgs = [ "procps" ];
src = ./hooks;
};
inputhandler = pkgs.static-nix-shell.mkBash {
pname = "sxmo_hook_inputhandler.sh";
pkgs = [ "coreutils" "playerctl" "pulseaudio" ];
@@ -88,6 +83,11 @@ let
pkgs = [ "sway" ];
src = ./hooks;
};
screenoff = pkgs.static-nix-shell.mkBash {
pname = "sxmo_hook_screenoff.sh";
pkgs = [ "sway" ];
src = ./hooks;
};
start = pkgs.static-nix-shell.mkBash {
pname = "sxmo_hook_start.sh";
pkgs = [ "systemd" "xdg-user-dirs" ];
@@ -150,7 +150,7 @@ in
# by including hooks here, updating the sxmo package also updates the hooks
# without requiring any reboot
"sxmo_hook_apps.sh" = "${package}/share/sxmo/default_hooks/sxmo_hook_apps.sh";
# "sxmo_hook_block_suspend.sh" = "${package}/share/sxmo/default_hooks/sxmo_hook_block_suspend.sh";
"sxmo_hook_block_suspend.sh" = "${package}/share/sxmo/default_hooks/sxmo_hook_block_suspend.sh";
"sxmo_hook_call_audio.sh" = "${package}/share/sxmo/default_hooks/sxmo_hook_call_audio.sh";
"sxmo_hook_contextmenu_fallback.sh" = "${package}/share/sxmo/default_hooks/sxmo_hook_contextmenu_fallback.sh";
"sxmo_hook_contextmenu.sh" = "${package}/share/sxmo/default_hooks/sxmo_hook_contextmenu.sh";
@@ -176,7 +176,6 @@ in
"sxmo_hook_restart_modem_daemons.sh" = "${package}/share/sxmo/default_hooks/sxmo_hook_restart_modem_daemons.sh";
"sxmo_hook_ring.sh" = "${package}/share/sxmo/default_hooks/sxmo_hook_ring.sh";
"sxmo_hook_rotate.sh" = "${package}/share/sxmo/default_hooks/sxmo_hook_rotate.sh";
"sxmo_hook_screenoff.sh" = "${package}/share/sxmo/default_hooks/sxmo_hook_screenoff.sh";
"sxmo_hook_scripts.sh" = "${package}/share/sxmo/default_hooks/sxmo_hook_scripts.sh";
"sxmo_hook_sendsms.sh" = "${package}/share/sxmo/default_hooks/sxmo_hook_sendsms.sh";
"sxmo_hook_smslog.sh" = "${package}/share/sxmo/default_hooks/sxmo_hook_smslog.sh";
@@ -187,10 +186,10 @@ in
"sxmo_hook_tailtextlog.sh" = "${package}/share/sxmo/default_hooks/sxmo_hook_tailtextlog.sh";
} // {
# default hooks for this nix module, not upstreamable
"sxmo_hook_block_suspend.sh" = "${hookPkgs.block_suspend}/bin/sxmo_hook_block_suspend.sh";
"sxmo_hook_inputhandler.sh" = "${hookPkgs.inputhandler}/bin/sxmo_hook_inputhandler.sh";
"sxmo_hook_postwake.sh" = "${hookPkgs.postwake}/bin/sxmo_hook_postwake.sh";
"sxmo_hook_rotate.sh" = "${hookPkgs.rotate}/bin/sxmo_hook_rotate.sh";
"sxmo_hook_screenoff.sh" = "${hookPkgs.screenoff}/bin/sxmo_hook_screenoff.sh";
"sxmo_hook_start.sh" = "${hookPkgs.start}/bin/sxmo_hook_start.sh";
"sxmo_suspend.sh" = "${hookPkgs.suspend}/bin/sxmo_suspend.sh";
};
@@ -235,10 +234,9 @@ in
SXMO_DISABLE_CONFIGVERSION_CHECK = mkSettingsOpt "1" "allow omitting the configversion line from user-provided sxmo dotfiles";
SXMO_UNLOCK_IDLE_TIME = mkSettingsOpt "300" "how many seconds of inactivity before locking the screen"; # lock -> screenoff happens 8s later, not configurable
# SXMO_WM = mkSettingsOpt "sway" "sway or dwm. ordinarily initialized by sxmo_{x,w}init.sh";
SXMO_NO_AUDIO = mkSettingsOpt "" "don't start pipewire/pulseaudio in sxmo_hook_start.sh, don't show audio in statusbar, disable audio menu";
SXMO_NO_AUDIO = mkSettingsOpt "1" "don't start pipewire/pulseaudio in sxmo_hook_start.sh";
SXMO_STATES = mkSettingsOpt "unlock screenoff" "list of states the device should support (unlock, lock, screenoff)";
SXMO_SWAY_SCALE = mkSettingsOpt "1" "sway output scale";
SXMO_WOB_DISABLE = mkSettingsOpt "" "disable the on-screen volume display";
};
};
default = {};
@@ -295,7 +293,7 @@ in
enable = true;
# we manage the greeter ourselves (TODO: merge this into sway config as well)
useGreeter = false;
waybar.top = import ./waybar-top.nix { inherit pkgs; };
waybar.top = import ./waybar-top.nix;
# reset extra waybar style
waybar.extra_style = "";
config = {
@@ -309,12 +307,7 @@ in
# these could be added, but i don't see much benefit.
font = "pango:monospace 10";
mod = "Mod1"; # prefer Alt
# about xwayland:
# - required by many electron apps, though some electron apps support NIXOS_OZONE_WL=1 for native wayland.
# - when xwayland is enabled, KOreader incorrectly chooses the X11 backend
# -> slower; blurrier
# - xwayland uses a small amount of memory (like 30MiB, IIRC?)
xwayland = false;
# xwayland = false; # disable to reduce RAM usage. N.B.: xwayland is needed for electron apps!
workspace_layout = "tabbed";
brightness_down_cmd = "sxmo_brightness.sh down";
@@ -385,12 +378,7 @@ in
bindsym button2 kill
bindswitch lid:on exec sxmo_wm.sh dpms on
bindswitch lid:off exec sxmo_wm.sh dpms off
exec 'printf %s "$SWAYSOCK" > "$XDG_RUNTIME_DIR"/sxmo.swaysock'
# XXX(2023/12/04): this shouldn't be necessary, but without this Komikku fails to launch because XDG_SESSION_TYPE is unset
exec dbus-update-activation-environment --systemd XDG_SESSION_TYPE
exec_always ${sxmo_init}
'';
};
@@ -449,76 +437,57 @@ in
event_name = eventName;
inherit transitions;
};
friendlyToBonsai = { trigger ? null, terminal ? false, timeout ? {}, power_pressed ? {}, power_released ? {}, voldown_pressed ? {}, voldown_released ? {}, volup_pressed ? {}, volup_released ? {} }@args:
friendlyToBonsai = { timeout ? null, trigger ? null, power_pressed ? {}, power_released ? {}, voldown_pressed ? {}, voldown_released ? {}, volup_pressed ? {}, volup_released ? {} }@args:
if trigger != null then [
(doExec trigger (friendlyToBonsai (builtins.removeAttrs args ["trigger"])))
] else let
events = [ ]
++ (lib.optional (timeout != {}) (onDelay (timeout.ms or 400) (friendlyToBonsai (builtins.removeAttrs timeout ["ms"]))))
++ (lib.optional (power_pressed != {}) (onEvent "power_pressed" (friendlyToBonsai power_pressed)))
++ (lib.optional (power_released != {}) (onEvent "power_released" (friendlyToBonsai power_released)))
++ (lib.optional (voldown_pressed != {}) (onEvent "voldown_pressed" (friendlyToBonsai voldown_pressed)))
++ (lib.optional (voldown_released != {}) (onEvent "voldown_released" (friendlyToBonsai voldown_released)))
++ (lib.optional (volup_pressed != {}) (onEvent "volup_pressed" (friendlyToBonsai volup_pressed)))
++ (lib.optional (volup_released != {}) (onEvent "volup_released" (friendlyToBonsai volup_released)))
;
in assert terminal -> events == []; events;
] else [
(lib.mkIf (timeout != null) (onDelay (timeout.ms or 400) (friendlyToBonsai (builtins.removeAttrs timeout ["ms"]))))
(lib.mkIf (power_pressed != {}) (onEvent "power_pressed" (friendlyToBonsai power_pressed)))
(lib.mkIf (power_released != {}) (onEvent "power_released" (friendlyToBonsai power_released)))
(lib.mkIf (voldown_pressed != {}) (onEvent "voldown_pressed" (friendlyToBonsai voldown_pressed)))
(lib.mkIf (voldown_released != {}) (onEvent "voldown_released" (friendlyToBonsai voldown_released)))
(lib.mkIf (volup_pressed != {}) (onEvent "volup_pressed" (friendlyToBonsai volup_pressed)))
(lib.mkIf (volup_released != {}) (onEvent "volup_released" (friendlyToBonsai volup_released)))
];
recurseVolUpDown = ttl: if ttl == 0 then {
} else {
voldown_pressed = {
trigger = "powerhold_voldown";
timeout.ms = 1000;
power_released = {};
} // recurseVolUpDown (ttl - 1);
# trigger ${button}_hold_N every `holdTime` ms until ${button} is released
recurseHold = button: { count ? 1, maxHolds ? 5, prefix ? "", holdTime ? 600, ... }@opts: lib.optionalAttrs (count <= maxHolds) {
"${button}_released".terminal = true; # end the hold -> back to root state
timeout = {
ms = holdTime;
trigger = "${prefix}${button}_hold_${builtins.toString count}";
} // (recurseHold button (opts // { count = count+1; }));
};
# trigger volup_tap_N or voldown_tap_N on every tap.
# if a volume button is held, then switch into `recurseHold`'s handling instead
volumeActions = { count ? 1, maxTaps ? 5, prefix ? "", timeout ? 600, ... }@opts: lib.optionalAttrs (count != maxTaps) {
volup_pressed = (recurseHold "volup" opts) // {
volup_released = {
trigger = "${prefix}volup_tap_${builtins.toString count}";
timeout.ms = timeout;
} // (volumeActions (opts // { count = count+1; }));
};
voldown_pressed = (recurseHold "voldown" opts) // {
voldown_released = {
trigger = "${prefix}voldown_tap_${builtins.toString count}";
timeout.ms = timeout;
} // (volumeActions (opts // { count = count+1; }));
};
volup_pressed = {
trigger = "powerhold_volup";
timeout.ms = 1000;
power_released = {};
} // recurseVolUpDown (ttl - 1);
};
in friendlyToBonsai {
# map sequences of "events" to an argument to pass to sxmo_hook_inputhandler.sh
# map: power (short), power (short) x2, power (long)
power_pressed.timeout.ms = 900; # press w/o release. this is a long timeout because it's tied to the "kill window" action.
# tap the power button N times to trigger N different actions
power_pressed.timeout.ms = 1200; # press w/o release. this is a long timeout because it's tied to the "kill window" action.
power_pressed.timeout.trigger = "powerhold";
power_pressed.power_released.timeout.trigger = "powerbutton_one";
power_pressed.power_released.timeout.ms = 300;
power_pressed.power_released.timeout.ms = 600; # long timeout to make `powertoggle_*` easier
power_pressed.power_released.power_pressed.trigger = "powerbutton_two";
# map: volume taps and holds
volup_pressed = (recurseHold "volup" {}) // {
# this either becomes volup_hold_* (via recurseHold, above) or:
# - a short volup_tap_1 followed by:
# - a *finalized* volup_1 (i.e. end of action)
# - more taps/holds, in which case we prefix it with `modal_<action>`
# to denote that we very explicitly entered this state.
#
# it's clunky: i do it this way so that voldown can map to keyboard/terminal in unlock mode
# but trigger media controls in screenoff
# in a way which *still* allows media controls if explicitly entered into via a tap on volup first
volup_released = (volumeActions { prefix = "modal_"; }) // {
trigger = "volup_tap_1";
timeout.ms = 300;
timeout.trigger = "volup_1";
};
};
voldown_pressed = (volumeActions {}).voldown_pressed // {
trigger = "voldown_start";
};
# power_pressed.power_released.power_released.timeout.trigger = "powerbutton_two";
# power_pressed.power_released.power_released.power_released.trigger = "powerbutton_three";
# tap power, then tap up/down after releasing it
power_pressed.power_released.voldown_pressed.trigger = "powertoggle_voldown";
power_pressed.power_released.volup_pressed.trigger = "powertoggle_volup";
# chording: hold power and then tap vol-up N times to adjust the volume by N increments.
# XXX: HOLDING POWER LIKE THIS IS RISKY. but the default hard-power-off is like 10s, so... i guess this works until it becomes a problem...?
power_pressed.voldown_pressed = (recurseVolUpDown 5).voldown_pressed;
power_pressed.volup_pressed = (recurseVolUpDown 5).volup_pressed;
# tap just one of the volume buttons.
voldown_pressed.trigger = "voldown_one";
volup_pressed.trigger = "volup_one";
};
# sxmo puts in /share/sxmo:
@@ -528,6 +497,16 @@ in
# - and more
# environment.pathsToLink = [ "/share/sxmo" ];
systemd.services."sxmo-set-permissions" = {
# TODO: some of these could be modified to be udev rules
description = "configure specific /sys and /dev nodes to be writable by sxmo scripts";
serviceConfig = {
Type = "oneshot";
ExecStart = "${package}/bin/sxmo_setpermissions.sh";
};
wantedBy = [ "multi-user.target" ];
};
# if superd fails to start a service within 100ms, it'll try to start again
# the fallout of this is that during intense lag (e.g. OOM or swapping) it can
# start the service many times.
@@ -604,7 +583,11 @@ in
];
sane.user.services = let
sxmoPath = [ package ] ++ package.runtimeDeps;
sxmoPath = [
"/etc/profiles/per-user/colin" # so as to launch user-enabled applications (like g4music, etc)
"/run/wrappers" # for doas, and anything else suid
"/run/current-system/sw" # for things installed system-wide, especially flock
] ++ [ package ] ++ package.runtimeDeps;
sxmoEnvSetup = ''
# mimic my sxmo_init.sh a bit. refer to the actual sxmo_init.sh above for details.
# the specific ordering, and the duplicated profile sourcing, matters.
@@ -627,19 +610,16 @@ in
serviceConfig.RestartSec = "20s";
};
in {
# these are defined here, and started mostly in sxmo_hook_start.sh.
# the ones commented our here are the ones i explicitly no longer use.
# uncommenting them here *won't* cause them to be auto-started.
sxmo_autosuspend = sxmoService "autosuspend";
# sxmo_battery_monitor = sxmoService "battery_monitor";
sxmo_battery_monitor = sxmoService "battery_monitor";
sxmo_desktop_widget = sxmoService "hook_desktop_widget";
sxmo_hook_lisgd = sxmoService "hook_lisgdstart";
sxmo_menumode_toggler = sxmoService "menumode_toggler";
sxmo_modemmonitor = sxmoService "modemmonitor";
# sxmo_networkmonitor = sxmoService "networkmonitor";
sxmo_networkmonitor = sxmoService "networkmonitor";
sxmo_notificationmonitor = sxmoService "notificationmonitor";
# sxmo_soundmonitor = sxmoService "soundmonitor";
# sxmo_wob = sxmoService "wob";
sxmo_soundmonitor = sxmoService "soundmonitor";
sxmo_wob = sxmoService "wob";
sxmo-x11-status = sxmoService "status_xsetroot";
bonsaid.path = sxmoPath;

View File

@@ -1,57 +0,0 @@
#!/usr/bin/env nix-shell
#!nix-shell -i bash -p procps
# Basic exponential backoff, this should save some resources if we're blocked by
# the same thing for a while
delay() {
sleep "$delay_time"
delay_time="$((delay_time*2))"
if [ "$delay_time" -gt 45 ]; then
delay_time=45
fi
}
wait_item() {
delay_time=1
while $1 > /dev/null 2>&1; do
echo "Blocking suspend for $1"
waited=1
delay
done
}
##################################### below is original, not shared with upstream sxmo_hook_block_suspend.sh
casting_go2tv() {
pgrep -f go2tv
}
# forward to the next block_suspend.sh script (if any).
# have to handle the case where this script is on PATH, and also when it was called without being on PATH.
# the implementation here causes us to call ourselves exactly once, at most (or twice, if there are duplicate PATH entries)
SXMO_HOOK_BLOCK_SUSPEND_DEPTH=$((${SXMO_HOOK_BLOCK_SUSPEND_DEPTH:-0} + 1))
echo "recurse counter: $SXMO_HOOK_BLOCK_SUSPEND_DEPTH"
block_suspend_next=true
FWD_COUNT=0
IFS=:
for p in $PATH ; do
echo "testing: $p/sxmo_hook_block_suspend.sh"
if $(test -x "$p/sxmo_hook_block_suspend.sh"); then
FWD_COUNT=$(($FWD_COUNT + 1))
fi
if [ "$FWD_COUNT" -eq "$SXMO_HOOK_BLOCK_SUSPEND_DEPTH" ]; then
block_suspend_next="$p/sxmo_hook_block_suspend.sh"
break
fi
done
while [ "$waited" != "0" ]; do
waited=0
echo "forwarding to: $block_suspend_next"
SXMO_HOOK_BLOCK_SUSPEND_DEPTH="$SXMO_HOOK_BLOCK_SUSPEND_DEPTH" "$block_suspend_next"
# wait for my own items last. else, we could wait for go2tv, then wait for internals, and in the meantime go2tv was re-spawned.
wait_item casting_go2tv
done

View File

@@ -10,41 +10,40 @@
# - bonsai mappings are static, so buttons can't benefit from non-compounding unless they're mapped accordingly for all lock states
# - this limitation could be removed, but with work
#
# example of a design which considers these things:
# proposed future design:
# - when unlocked:
# - volup toggle -> app menu
# - voldown press -> keyboard
# - voldown hold -> terminal
# - power x2 -> screenoff
# - hold power -> kill app
# - when locked:
# - volup tap -> volume up
# - volup hold -> media seek forward
# - voldown tap -> volume down
# - voldown hold -> media seek backward
# - power x1 -> screen on
# - power x2 -> play/pause media
# some trickiness allows for media controls in unlocked mode:
# - volup tap -> enter media mode
# - i.e. in this state, vol tap/hold is mapped to volume/seek
# - if, after entering media mode, no more taps occur, then we trigger the default app-menu action
# limitations/downsides:
# - power mappings means phone is artificially slow to unlock.
# - media controls when unlocked have quirks:
# - mashing voldown to decrease the volume will leave you with a toggled keyboard.
# - seeking backward isn't possible except by first tapping volup.
# - volup-release -> app menu
# - volup-hold -> WM menu
# - voldown-release -> toggle keyboard
# - voldown-hold -> terminal
# - pow-volup xN -> volume up
# - pow-voldown xN -> volume down
# - pow-x2 -> screen off
# - pow-hold -> kill app
# - when screenoff:
# - volup -> volume up
# - voldown -> volume down
# - pow-x1 -> screen on
# - pow-x2 -> toggle player
# - pow-volup -> seek +30s
# - pow-voldown -> seek -10s
# benefits:
# - volup and voldown are able to be far more responsive
# - which means faster vkbd, menus, volume adjustment (when locked)
# - less mental load than the chording-based approach (where i hold power to adjust volume)
# - less risk due to not chording the power button
# drawbacks:
# - volup/down actions are triggered by the release instead of the press; slight additional latency for pulling open the keyboard
# - moving the WM menu into the top-level menu could allow keeping voldown free of complication
# increments to use for volume adjustment
VOL_INCR=5
VOL_INCR_1=5
VOL_INCR_2=10
# replicating the naming from upstream sxmo_hook_inputhandler.sh...
ACTION="$1"
STATE=$(cat "$SXMO_STATE")
noop() {
true
}
handle_with() {
echo "sxmo_hook_inputhandler.sh: STATE=$STATE ACTION=$ACTION: handle_with: $@"
@@ -52,6 +51,18 @@ handle_with() {
exit 0
}
# handle_with_state_toggle() {
# # - unlock,lock => screenoff
# # - screenoff => unlock
# #
# # probably not handling proximity* correctly here
# case "$STATE" in
# *lock)
# respond_with sxmo_state.sh set screenoff
# *)
# respond_with sxmo_state.sh set unlock
# esac
# }
# state is one of:
# - "unlock" => normal operation; display on and touchscreen on
@@ -68,35 +79,24 @@ if [ "$STATE" = "unlock" ]; then
handle_with sxmo_killwindow.sh
;;
"volup_tap_1")
# swallow: this could be the start to a media control (multi taps / holds),
# or it could be just a single tap -> release, handled next/below
handle_with noop
;;
"volup_1")
"volup_one")
# volume up once: app-specific menu w/ fallback to SXMO system menu
handle_with sxmo_appmenu.sh
;;
"voldown_start")
"voldown_one")
# volume down once: toggle keyboard
handle_with sxmo_keyboard.sh toggle
;;
"voldown_hold_2")
# hold voldown to launch terminal
# note we already triggered the keyboard; that's fine: usually keyboard + terminal go together :)
# voldown_hold_1 frequently triggers during short taps meant only to reveal the keyboard,
# so prefer a longer hold duration
"powertoggle_volup")
# power -> volume up: DE menu
handle_with sxmo_wmmenu.sh
;;
"powertoggle_voldown")
# power -> volume down: launch terminal
handle_with sxmo_terminal.sh
;;
"voldown_tap_1")
# swallow, to prevent keyboard from also triggering media controls
handle_with noop
;;
voldown_hold_*)
# swallow, to prevent terminal from also triggering media controls
handle_with noop
;;
esac
fi
@@ -110,6 +110,14 @@ if [ "$STATE" = "screenoff" ]; then
# power toggle during deep sleep often gets misread as power hold, so treat same
handle_with sxmo_state.sh set unlock
;;
"powertoggle_volup"|"powerhold_volup")
# power -> volume up: seek forward
handle_with playerctl position 30+
;;
"powertoggle_voldown"|"powerhold_voldown")
# power -> volume down: seek backward
handle_with playerctl position 10-
;;
esac
fi
@@ -125,20 +133,21 @@ case "$ACTION" in
;;
# powerbutton_three: intentional no-op because overloading the kill-window handler is risky
volup_tap*|modal_volup_tap*)
handle_with pactl set-sink-volume @DEFAULT_SINK@ +"$VOL_INCR%"
"volup_one")
handle_with pactl set-sink-volume @DEFAULT_SINK@ +"$VOL_INCR_1%"
;;
voldown_tap*|modal_voldown_tap*)
handle_with pactl set-sink-volume @DEFAULT_SINK@ -"$VOL_INCR%"
"voldown_one")
handle_with pactl set-sink-volume @DEFAULT_SINK@ -"$VOL_INCR_1%"
;;
volup_hold*|modal_volup_hold*)
handle_with playerctl position 30+
# HOLD power button and tap volup/down to adjust volume
"powerhold_volup")
handle_with pactl set-sink-volume @DEFAULT_SINK@ +"$VOL_INCR_1%"
;;
voldown_hold*|modal_voldown_hold*)
handle_with playerctl position 10-
"powerhold_voldown")
handle_with pactl set-sink-volume @DEFAULT_SINK@ -"$VOL_INCR_1%"
;;
esac
handle_with noop
handle_with echo "no-op"

View File

@@ -0,0 +1,22 @@
#!/usr/bin/env nix-shell
#!nix-shell -i bash -p sway
# this hook is mostly identical to default sxmo_hook_screenoff.sh except:
# - the LED frequency is adjusted from its default of "blink every 2s"
# - dwm-specific bits are removed
BLINK_FREQ=8
swaymsg mode default
sxmo_wm.sh dpms on
sxmo_wm.sh inputevent touchscreen off
sxmo_jobs.sh start periodic_blink sxmo_run_periodically.sh "$BLINK_FREQ" sxmo_led.sh blink red blue
wait
# avoid immediate suspension. particularly, ensure that we get at least one blink in
sxmo_wakelock.sh lock sxmo_hold_a_bit "$BLINK_FREQ"s
sxmo_wakelock.sh unlock sxmo_not_screenoff

View File

@@ -10,15 +10,14 @@ xdg-user-dirs-update
sxmo_jobs.sh start daemon_manager
# Periodically update some status bar components
# don't: statusbar is managed by waybar
# sxmo_hook_statusbar.sh all
# sxmo_jobs.sh start statusbar_periodics sxmo_run_aligned.sh 60 \
# sxmo_hook_statusbar.sh periodics
sxmo_hook_statusbar.sh all
sxmo_jobs.sh start statusbar_periodics sxmo_run_aligned.sh 60 \
sxmo_hook_statusbar.sh periodics
# TODO: start these externally, via `wantedBy` in nix
# don't: i don't use mako
# don't: mako is managed externally
# superctl start mako
# systemctl --user start sxmo_wob
systemctl --user start sxmo_wob
systemctl --user start sxmo_menumode_toggler
systemctl --user start bonsaid
# don't: sway background is managed externally
@@ -50,22 +49,16 @@ fi
# superctl start sxmo_conky
# Monitor the battery
# don't: this is *exclusively* for sxmo_hook_statusbar.sh, which i don't use.
# systemctl --user start sxmo_battery_monitor
systemctl --user start sxmo_battery_monitor
# It watch network changes and update the status bar icon by example
# don't: this is for sxmo_hook_statusbar.sh, which i don't use.
# this means we never call sxmo_hook_network_{up,down,...}, but the defaults are no-op anyway
# systemctl --user start sxmo_networkmonitor
systemctl --user start sxmo_networkmonitor
# The daemon that display notifications popup messages
# more importantly: it lights the led green when a notification arrives
systemctl --user start sxmo_notificationmonitor
# monitor for headphone for statusbar
# this also invokes `wob` whenever the volume is changed
# don't: my volume monitoring is handled by sway
# systemctl --user start sxmo_soundmonitor
systemctl --user start sxmo_soundmonitor
# rotate UI based on physical display angle by default
if [ -n "$SXMO_AUTOROTATE" ]; then

View File

@@ -30,22 +30,15 @@ logger = logging.getLogger(__name__)
NTFY_HOST = 'uninsane.org'
NTFY_PORT_BASE = 5550
# duration in seconds to sleep for
SUSPEND_TIME = 300
SUSPEND_TIME=300
# take care that WOWLAN_DELAY might include more than you think (e.g. time spent configuring wowlan pattern rules)
WOWLAN_DELAY = 6
# SXMO LED blink frequency to set on resume
BLINK_FREQ = 5
WOWLAN_DELAY=6
class Executor:
def __init__(self, dry_run: bool = False):
self.dry_run = dry_run
def exec(self, cmd: list[str], sudo: bool = False, check: bool = True, wait: bool = True):
if check: assert wait, "can't check_output without first waiting for process completion"
def exec(self, cmd: list[str], sudo: bool = False, check: bool = True):
if sudo:
cmd = [ 'doas' ] + cmd
@@ -53,7 +46,6 @@ class Executor:
if self.dry_run:
return
if wait:
try:
res = subprocess.run(cmd, capture_output=True)
except Exception as e:
@@ -67,11 +59,6 @@ class Executor:
if check:
res.check_returncode()
else:
res = subprocess.Popen(cmd)
return res
def try_connect(self, dest, delay):
logger.debug(f"opening socket to {dest} with timeout {delay}")
if self.dry_run:
@@ -137,33 +124,13 @@ class Suspender:
def suspend(self, duration: int, mode: str):
logger.info(f"calling suspend for duration: {duration}")
if mode == 'rtcwake':
self.executor.exec(['rtcwake', '-m', 'mem', '-s', str(duration)], sudo=True, check=False)
self.executor.exec(['rtcwake', '-m', 'mem', '-s', str(duration)], check=False)
elif mode == 'sleep':
time.sleep(duration)
else:
assert False, f"unknown suspend mode: {mode}"
class SxmoApi:
def __init__(self, executor: Executor):
self.executor = executor
def halt_services(self) -> None:
res = self.executor.exec(['sxmo_jobs.sh', 'running', 'periodic_blink'], check=False)
self.was_blinking = res and res.returncode == 0
if self.was_blinking:
self.executor.exec(['sxmo_jobs.sh', 'stop', 'periodic_blink'], check=False)
def resume_services(self) -> None:
if self.was_blinking:
# XXX: sxmo_jobs.sh is supposed to run the job in the background, but somehow it fails (blocks), only when invoked from Python.
# oh well, just call it asynchronously (wait=False)
self.executor.exec(['sxmo_jobs.sh', 'start', 'periodic_blink', 'sxmo_run_periodically.sh', str(BLINK_FREQ), 'sxmo_led.sh', 'blink', 'red', 'blue'], check=False, wait=False)
def call_postwake_hook(self) -> None:
self.executor.exec(['sxmo_hook_postwake.sh'], check=False)
def main():
logging.basicConfig()
logging.getLogger().setLevel(logging.INFO)
@@ -185,9 +152,7 @@ def main():
suspend_mode = args.suspend_mode
executor = Executor(dry_run=args.dry_run)
suspender = Suspender(executor, wowlan_delay=wowlan_delay)
sxmo_api = SxmoApi(executor)
sxmo_api.halt_services()
suspender.open_ntfy_stream()
suspender.configure_wowlan()
@@ -200,8 +165,7 @@ def main():
logger.info(f"suspended for {time_spent:.0f} seconds")
suspender.close_ntfy_stream()
sxmo_api.resume_services()
sxmo_api.call_postwake_hook()
executor.exec(['sxmo_hook_postwake.sh'], check=False)
if __name__ == '__main__':
main()

View File

@@ -1,122 +0,0 @@
#!/usr/bin/env nix-shell
#!nix-shell -i bash -p sxmo-utils -p sxmo-utils.runtimeDeps
#
# usage:
# waybar-sxmo-status widget1 [ widget2 [...]]
#
# where each widget is one of:
# - modem-state
# - modem-tech
# - modem-signal
# - wifi-status
# - volume
# sxmo_hook_statusbar.sh assumes:
# - mmcli, jq on PATH
# - sxmo_hook_icons.sh and sxmo_common.sh are sourcable
# - from sxmo_common, it only uses sxmobar (and aliases jq=gojq)
# setup environment so that the hooks will be on PATH:
# - sxmo_hook_statusbar.sh
# - sxmo_hook_icons.sh
export HOME="${HOME:-/home/colin}"
export XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}"
export PATH="$XDG_CONFIG_HOME/sxmo/hooks:$PATH"
# ensure that sxmo_audio.sh tells us the volume instead of early-returning
export SXMO_NO_AUDIO=
# clunky interaction between us and sxmo_hook_statusbar.sh:
# - we export `sxmobar` to it, but within that function cannot modify the environment
# of *this* script, because it gets run in a different process.
# - so, `sxmobar` prints info to stdout, and then this script re-interprets that info.
# - practically, `sxmobar` prints shell commands, and then this script `eval`s them, to achieve that IPC.
sxmobar() {
action="$1"
shift
if [ "$action" = "-a" ]; then
while [ -n "$*" ]; do
arg="$1"
case "$arg" in
"-f"|"-b"|"-t"|"-e")
# foreground/background/text/emphasis: ignore it
shift
shift
;;
*)
# begin arguments
break
;;
esac
done
echo "setitem $@"
fi
}
export -f sxmobar
setitem() {
id="$1"
priority="$2"
value="$3"
case "$id" in
modem-state)
modem_state="$value"
;;
modem-tech)
modem_tech="$value"
;;
modem-signal)
modem_signal="$value"
;;
wifi-status)
wifi_status="$value"
;;
volume)
volume="$value"
;;
esac
}
while [ -n "$*" ]; do
variable="$1"
shift
case "$variable" in
"--verbose")
set -x
;;
"modem-state")
if [ -z "$modem_state" ]; then
eval "$(sxmo_hook_statusbar.sh modem)"
fi
echo -n "$modem_state"
;;
"modem-tech")
if [ -z "$modem_tech" ]; then
eval "$(sxmo_hook_statusbar.sh modem)"
fi
echo -n "$modem_tech"
;;
"modem-signal")
if [ -z "$modem_signal" ]; then
eval "$(sxmo_hook_statusbar.sh modem)"
fi
echo -n "$modem_signal"
;;
"wifi-status")
if [ -z "$wifi_status" ]; then
eval "$(sxmo_hook_statusbar.sh network wifi wlan0)"
fi
echo -n "$wifi_status"
;;
"volume")
if [ -z "$volume" ]; then
eval "$(sxmo_hook_statusbar.sh volume)"
fi
echo -n "$volume"
;;
*)
echo -n "UNK: $variable"
;;
esac
done

View File

@@ -1,30 +1,12 @@
# docs: https://github.com/Alexays/Waybar/wiki/Configuration
# format specifiers: https://fmt.dev/latest/syntax.html#syntax
# this is merged with the sway/waybar-top.nix defaults
{ pkgs }:
let
waybar-sxmo-status = pkgs.static-nix-shell.mkBash {
pname = "waybar-sxmo-status";
src = ./.;
pkgs = [ "sxmo-utils" "sxmo-utils.runtimeDeps" ];
};
in
{
height = 26;
modules-left = [ "sway/workspaces" ];
modules-center = [ ];
modules-right = [
"custom/swaync"
"clock"
"battery"
"custom/sxmo-sane"
# "custom/sxmo"
# "custom/sxmo/modem-state"
# "custom/sxmo/modem-tech"
# "custom/sxmo/modem-signal"
# "custom/sxmo/wifi"
];
modules-right = [ "custom/swaync" "custom/sxmo" ];
"sway/workspaces" = {
all-outputs = true;
@@ -38,56 +20,12 @@ in
};
};
"custom/sxmo-sane" = {
# this calls all the SXMO indicators, inline.
# so it works even without the "statusbar periodics" sxmo service running.
interval = 2;
format = "{}";
exec = "${waybar-sxmo-status}/bin/waybar-sxmo-status modem-state modem-tech modem-signal wifi-status volume";
};
"custom/sxmo" = {
# this gives wifi state, battery, mic/speaker, lockstate, time all as one widget.
# this gives wifi state, batter, mic/speaker, lockstate, time all as one widget.
# a good starting point, but may want to split these apart later to make things configurable.
# the values for this bar are computed in sxmo:configs/default_hooks/sxmo_hook_statusbar.sh
# e.g. distinct vol-up & vol-down buttons next to the speaker?
exec = "sxmo_status.sh";
interval = 1;
format = "{}";
};
# not ported: battery, ethernet
"custom/sxmo/modem-state" = {
exec = "cat /run/user/1000/sxmo_status/default/10-modem-state";
interval = 2;
format = "{}";
};
"custom/sxmo/modem-tech" = {
exec = "cat /run/user/1000/sxmo_status/default/11-modem-tech";
interval = 2;
format = "{}";
};
"custom/sxmo/modem-signal" = {
exec = "cat /run/user/1000/sxmo_status/default/12-modem-signal";
interval = 2;
format = "{}";
};
"custom/sxmo/wifi" = {
exec = "cat /run/user/1000/sxmo_status/default/30-wifi-status";
interval = 2;
format = "{}";
};
"custom/sxmo/volume" = {
exec = "cat /run/user/1000/sxmo_status/default/50-volume";
interval = 2;
format = "{}";
};
"custom/sxmo/state" = {
exec = "cat /run/user/1000/sxmo_status/default/90-state";
interval = 2;
format = "{}";
};
"custom/sxmo/time" = {
exec = "cat /run/user/1000/sxmo_status/default/99-time";
interval = 2;
format = "{}";
};
}

View File

@@ -94,7 +94,6 @@ in
speedFactor = 2;
supportedFeatures = [
# "big-parallel" # it can't reliably build webkitgtk
"no-binfmt"
];
mandatoryFeatures = [ ];
sshUser = "nixremote";

View File

@@ -13,7 +13,7 @@ in
};
emulation = mkOption {
type = types.bool;
default = false;
default = true;
};
ccache = mkOption {
type = types.bool;
@@ -35,8 +35,6 @@ in
# it's nice to not be limited in that way, so increase this a bit.
nix.nrBuildUsers = 64;
nix.settings.system-features = [ "big-parallel" ];
# enable cross compilation
# TODO: do this via stdenv injection, linking into /run/binfmt the stuff in <nixpkgs:nixos/modules/system/boot/binfmt.nix>
boot.binfmt.emulatedSystems = lib.optionals cfg.emulation [

View File

@@ -10,7 +10,6 @@
config = lib.mkIf config.sane.roles.handheld {
sane.programs.guiApps.suggestedPrograms = [
"consoleMediaUtils" # overbroad, but handy on very rare occasion
"handheldGuiApps"
];
};

View File

@@ -1,9 +0,0 @@
{
"description": "A podcast for potential-pursuing optimists, for doers who want to make a dent in the universe, for those interested in crypto, longevity, city-building, etc., and for anyone who loves learning the stories of extraordinary entrepreneurs.",
"is_podcast": true,
"site_name": "",
"site_url": "",
"title": "POD OF JAKE",
"url": "https://anchor.fm/s/2da69154/podcast/rss",
"velocity": 0.124
}

View File

@@ -1,9 +0,0 @@
{
"description": "About the modern financial infrastructure that the world sits atop of.",
"is_podcast": false,
"site_name": "Bits about Money",
"site_url": "https://www.bitsaboutmoney.com",
"title": "Bits about Money",
"url": "https://www.bitsaboutmoney.com/archive/rss/",
"velocity": 0.041
}

View File

@@ -1,9 +0,0 @@
{
"description": "Recent content on The KosmosGhost Site",
"is_podcast": false,
"site_name": "The KosmosGhost Site",
"site_url": "https://kosmosghost.github.io",
"title": "The KosmosGhost Site",
"url": "https://kosmosghost.github.io/index.xml",
"velocity": 0.0
}

View File

@@ -1,9 +0,0 @@
{
"description": "LINMOB.net is a blog about LINux on MOBile devices. With the PinePhone (Pro) and Librem 5 shipping it is back to report on GNU+Linux on mobile devices.",
"is_podcast": false,
"site_name": "LINux on MOBile",
"site_url": "https://linmob.net",
"title": "LINux on MOBile",
"url": "https://linmob.net/feed.xml",
"velocity": 0.176
}

View File

@@ -1,9 +0,0 @@
{
"description": "<p>Comedians Dave Anthony and Gareth Reynolds picks a subject from history and examine it.</p>",
"is_podcast": true,
"site_name": "",
"site_url": "",
"title": "The Dollop with Dave Anthony and Gareth Reynolds",
"url": "https://www.omnycontent.com/d/playlist/885ace83-027a-47ad-ad67-aca7002f1df8/22b063ac-654d-428f-bd69-ae2400349cde/65ff0206-b585-4e2a-9872-ae240034c9c9/podcast.rss",
"velocity": 0.188
}

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