Compare commits

..

316 commits

Author SHA1 Message Date
Geoffrey Frogeye ea37f3d67b
Forgot to remove this file 2025-02-14 04:24:25 +01:00
Geoffrey Frogeye de17a48de3
Friendship ended with systemd-resolved. Now dnsmasq is my new best friend.
Welp, it was short-lived.
2025-02-14 04:14:28 +01:00
Geoffrey Frogeye 78027a7f48
Moulti DNS 2025-02-14 03:15:36 +01:00
Geoffrey Frogeye 0229cab124
Show time
Ok no actually I meant Fix time but Show time sounds good so that's
officially the commit's name now.
2025-02-13 13:39:39 +01:00
Geoffrey Frogeye 13eeeabc92
Add more utils 2025-02-13 13:38:54 +01:00
Geoffrey Frogeye 49b8dd0b5e
homepage: Allow an URL for categories 2025-02-02 23:24:15 +01:00
Geoffrey Frogeye 7ca1e0576b
Music handlers moved to server 2025-02-01 01:38:37 +01:00
Geoffrey Frogeye e26fa6a011
Local homepage
Basically the same thing as currently online, except templated, and
trimmed from useless stuff, but needs modernization.
2025-01-31 00:05:27 +01:00
Geoffrey Frogeye 8d07123713
Keyboardz 2025-01-30 21:22:26 +01:00
Geoffrey Frogeye 684d9b4d8b
frobar: More corner cases 2025-01-28 19:07:52 +01:00
Geoffrey Frogeye aceb44293f
syncthing: Support remote declarative sync 2025-01-27 00:15:28 +01:00
Geoffrey Frogeye 460ab8938f
frobar: Workaround missing workspace
Not sure what causes it, not interested in figuring out right now.
2025-01-26 17:18:40 +01:00
Geoffrey Frogeye 3335f90de4
Fix specialisation detection
I think
2025-01-26 17:18:28 +01:00
Geoffrey Frogeye 8fd6a0d2d8
Misc 2025-01-23 20:17:28 +01:00
Geoffrey Frogeye f3e7b29a98
Upgrade 2025-01-23 18:57:51 +01:00
Geoffrey Frogeye bc9fefe8ee
Add earlyoom 2025-01-23 18:46:31 +01:00
Geoffrey Frogeye e113b70357
rebuild: Fix 2025-01-23 18:46:13 +01:00
Geoffrey Frogeye cce96e063d
rebuild: Fix display 2025-01-23 18:19:49 +01:00
Geoffrey Frogeye cd4536c53b
Rebuild: Separate evaluation 2025-01-23 18:17:09 +01:00
Geoffrey Frogeye fafc4d45b7
remote-builds: Fix host key verification 2025-01-23 17:34:20 +01:00
Geoffrey Frogeye 875ea5d137
deploy: Colorize steps 2025-01-23 17:31:48 +01:00
Geoffrey Frogeye 50abb95e4c
Re-enable remote builds 2025-01-23 15:34:21 +01:00
Geoffrey Frogeye 6b662477c4
Shell goodness
Also potential for RCE, yay?
2025-01-21 23:43:01 +01:00
Geoffrey Frogeye 6d6a991fa5
Switch up the languages 2025-01-21 00:21:00 +01:00
Geoffrey Frogeye e6656923e0
frobar: Fix previous commit
Yeah I definitely did testing 🙃
2025-01-16 19:28:38 +01:00
Geoffrey Frogeye fa11c76d46
frobar: Sort screens by x position 2025-01-16 18:18:24 +01:00
Geoffrey Frogeye 04a08faa7f
frobar: Fix ramp doubles on 1.0 2025-01-15 16:32:06 +01:00
Geoffrey Frogeye c4fdd50a61
New ability get: Scanning! 2025-01-15 16:22:48 +01:00
Geoffrey Frogeye 08f1049cc1
Bit more comments 2025-01-13 12:37:37 +01:00
Geoffrey Frogeye 3ccb0abfe5
frobar: Handle PulseAudio sinks without active port
No idea in which conditions those happen, but apparently they do happen.
2025-01-11 15:01:50 +01:00
Geoffrey Frogeye d6e95da9dc
frobar: Fix empty container name 2025-01-10 22:18:15 +01:00
Geoffrey Frogeye 9adfcd2377
frobar: Now version 3! 2025-01-10 19:09:15 +01:00
Geoffrey Frogeye 42d3d1b3a6
frobar-ng: Fix last few issues 2025-01-10 18:35:10 +01:00
Geoffrey Frogeye 24dd070bd9
frobar-ng: Fix mpris issues 2025-01-10 17:46:37 +01:00
Geoffrey Frogeye 9916cb0170
frobar-ng: Fix Glib preventing closing 2025-01-10 15:19:51 +01:00
Geoffrey Frogeye f22b42a711
frobar-ng: Broken mpris! 2025-01-10 00:17:06 +01:00
Geoffrey Frogeye ffd402f57c
frobar-ng: Sections factory! 2025-01-06 23:08:52 +01:00
Geoffrey Frogeye ecf831d4ac
frobar-ng: Improve animation 2025-01-06 00:58:05 +01:00
Geoffrey Frogeye bfd31bb742
frobar-ng: Rudimentary Pulseaudio 2025-01-06 00:57:53 +01:00
Geoffrey Frogeye 13502edf3b
Fix zytemp permission error 2025-01-05 23:21:16 +01:00
Geoffrey Frogeye 6f8f877fb5
curaco desk: Be less noisy 2025-01-05 23:21:16 +01:00
Geoffrey Frogeye 0ddcdc4aeb
frobar-ng: Alerting sections 2025-01-03 19:59:23 +01:00
Geoffrey Frogeye 953aa13cb6
frobar-ng: Colors 2025-01-03 18:56:33 +01:00
Geoffrey Frogeye 7b54ad0578
Moar free Wi-Fi 2025-01-03 16:26:31 +01:00
Geoffrey Frogeye 8950def0bd
38C3 Wi-Fi 2024-12-26 22:22:45 +01:00
Geoffrey Frogeye 7cb3dfd9cf
Upgrade 2024-12-25 16:19:22 +01:00
Geoffrey Frogeye bd8e6d9fdc
cranberry: Some adjustments 2024-12-25 15:29:27 +01:00
Geoffrey Frogeye 8c45c1989c
Fix time syncing
Can't believe I was ~5 seconds off on all my computers for this long x)
2024-12-25 15:22:27 +01:00
Geoffrey Frogeye 0ac79c3d0c
Add lazyjj as a test 2024-12-23 21:25:27 +01:00
Geoffrey Frogeye 128bdc5004
Review jjui PR 2024-12-20 23:45:13 +01:00
Geoffrey Frogeye b61685f677
Upgrade whisperx 2024-12-20 23:24:55 +01:00
Geoffrey Frogeye a5be7cb695
Enable picom
Should reduce a few of the graphical glitches.
Also can do the "Activate Linux" joke muhahah
2024-12-20 22:50:19 +01:00
Geoffrey Frogeye 12074f6d2e
jlab: Don't push empty commits 2024-12-20 22:49:51 +01:00
Geoffrey Frogeye 355b63cf73
Reformat all Nix files 2024-12-15 00:31:26 +01:00
Geoffrey Frogeye 9e0c1102a9
Use official nix formatter 2024-12-15 00:29:35 +01:00
Geoffrey Frogeye a06fb16394
Update stylix to 24.11 2024-12-14 21:51:11 +01:00
Geoffrey Frogeye 1fb9a149a5
NixOS 24.11: Remove remaining warnings 2024-12-03 23:07:56 +01:00
Geoffrey Frogeye 495b9cb2e1
Remove pirate usage of Nix
Apparently Nix 2.24 doesn't like dirty git repos
("lock file contains unlocked input") and makes it known by failing,
which tipped me that I wasn't using Lix everywhere.
2024-12-03 22:45:13 +01:00
Geoffrey Frogeye f9d943ef63
NixOS 24.11: Some warnings removed
The lualine ones I'll have to see if it changes the way I want.
2024-12-03 19:50:55 +01:00
Geoffrey Frogeye b9bd255a55
NixOS 24.11 builds 2024-12-02 23:54:26 +01:00
Geoffrey Frogeye 2951e81a77
Add jjui 2024-11-28 17:08:12 +01:00
Geoffrey Frogeye b16db77f04
Add CUDA support
I bothered with that only to realize that I'm not using proprietary drivers 🙃
2024-11-16 15:49:31 +01:00
Geoffrey Frogeye 84268f3c47
Add whisperx 2024-11-15 22:41:32 +01:00
Geoffrey Frogeye 22b844df2c
Misc fixes 2024-11-07 14:48:25 +01:00
Geoffrey Frogeye 0d1c2f1975
Add jlab script 2024-10-31 20:21:52 +01:00
Geoffrey Frogeye a08d09328f
Updates 2024-10-30 16:11:07 +01:00
Geoffrey Frogeye 441177a263
zytempo: Report availability 2024-10-19 13:34:56 +02:00
Geoffrey Frogeye cde7df57eb
Also doing C dev on cranberry 2024-10-09 19:00:28 +02:00
Geoffrey Frogeye 1615af8730
Remove fetchGit for performance 2024-10-09 19:00:17 +02:00
Geoffrey Frogeye 9389d1d284
git-sync: Use proper thing 2024-10-06 22:07:49 +02:00
Geoffrey Frogeye a7e2b49bea
git-sync supports jj
So much nicer!
2024-10-06 22:02:00 +02:00
Geoffrey Frogeye d2dbb5bbde
More dev things 2024-10-06 21:51:55 +02:00
Geoffrey Frogeye 64ab21f7dd
I'm working on micro-controllers again, can you tell? 2024-10-04 14:41:43 +02:00
Geoffrey Frogeye c2068a30ff
vm: Fix virtio 2024-10-03 14:20:46 +02:00
Geoffrey Frogeye e305c6b1de
Add rustdesk 2024-10-03 13:55:28 +02:00
Geoffrey Frogeye 5804ff31ff
Convert Virtualbox to KVM 2024-10-03 12:49:36 +02:00
Geoffrey Frogeye 6618fbee9d
rssVideos: Handle corrupt metadata better 2024-10-01 15:17:06 +02:00
Geoffrey Frogeye dee90d9a96
vim: Another nix lsp
Might be a tad too powerful for its own good, let's see
2024-10-01 14:35:28 +02:00
Geoffrey Frogeye 812e5acf6c
Update
Cups or something (I do print from time to time ><")
2024-10-01 14:24:16 +02:00
Geoffrey Frogeye 768b38b87f
Merge curacao and cranberry work 2024-10-01 14:22:16 +02:00
Geoffrey Frogeye 6644e85c30
frobarng: Some refactor 2024-10-01 14:12:24 +02:00
Geoffrey Frogeye 1ae7d6b447
frobarng: NetworkProvider done 2024-09-27 23:06:06 +02:00
Geoffrey Frogeye 01934563a5
Label printer support
Such a good name, Labelle. Glad they didn't go with the AI-generated one.
2024-09-15 22:31:01 +02:00
Geoffrey Frogeye 171232cb2e
jj config from curacao 2024-09-15 22:19:12 +02:00
Geoffrey Frogeye 36df032ecd
Hmm, jujutsu merge commit I guess? 2024-09-15 15:42:14 +02:00
Geoffrey Frogeye a43209a902
Add JJ 2024-09-15 00:41:42 +02:00
Geoffrey Frogeye 38ff39bc78
Remove pager by default
Hopefully this should remove the pesky auto pager for stuff I don't need
it for.
2024-09-11 00:38:02 +02:00
Geoffrey Frogeye fc744fd73b Merge remote-tracking branch 'origin/main' 2024-09-10 23:16:46 +02:00
Geoffrey Frogeye f633761d54
Add pdfgrep 2024-09-10 23:16:18 +02:00
Geoffrey Frogeye ae61f0c6d4 Merge remote-tracking branch 'origin/main' 2024-09-02 16:28:40 +02:00
Geoffrey Frogeye 532b3628d3
frobar: More pulseaudio device descriptions
Is there not a standard thing I can check?
2024-09-02 16:26:40 +02:00
Geoffrey Frogeye 83c24f2fb2
scripts: Fix overpdf in Nix 2024-09-02 16:26:21 +02:00
Geoffrey Frogeye 830552f8c3
Upgrade 2024-09-01 14:17:35 +02:00
Geoffrey Frogeye 4d39ac0769
displaylink: Use upstream 2024-08-25 10:43:05 +02:00
Geoffrey Frogeye c375cb2e11
frobarng: Forgotten dev 2024-08-25 09:48:36 +02:00
Geoffrey Frogeye f81fd6bfd2
frobarng: Even more dev 2024-08-17 00:57:06 +02:00
Geoffrey Frogeye c7535d8ed8
frobarng: Graceful exit 2024-08-14 03:29:34 +02:00
Geoffrey Frogeye 6690f3aa0d
frobarng: Mirroring and more 2024-08-14 02:45:25 +02:00
Geoffrey Frogeye 7d60269b49
frobarng: animations 2024-08-14 00:28:17 +02:00
Geoffrey Frogeye 139d3a3ea8
frobar: Some dev 2024-08-13 01:58:51 +02:00
Geoffrey Frogeye 86f9f75bd7
Add mpris support 2024-08-11 14:54:51 +02:00
Geoffrey Frogeye f011b376bb
Fix wol 2024-08-11 10:01:36 +02:00
Geoffrey Frogeye ce636f08ff
mpd: Fix remote volume control 2024-08-11 10:01:20 +02:00
Geoffrey Frogeye 0fc6a51cb0
Upgrade 2024-07-30 22:36:55 +02:00
Geoffrey Frogeye c90590640f
Attempt at stuff 2024-07-30 20:24:44 +02:00
Geoffrey Frogeye 2329d67d16
password: Don't delete hashes immediately 2024-07-12 23:05:44 +02:00
Geoffrey Frogeye f664b51c85
Update 2024-07-02 19:53:52 +02:00
Geoffrey Frogeye 63531c4bc6
audio: Attempt at keeping speakers on 2024-07-02 18:34:22 +02:00
Geoffrey Frogeye 77ec4574f1
desk: Logging improvements 2024-07-02 15:50:37 +02:00
Geoffrey Frogeye a6becdae70
desk: Fix the fixes 2024-07-01 13:50:24 +02:00
Geoffrey Frogeye bafb22c573
Smol fixes 2024-06-30 23:01:09 +02:00
Geoffrey Frogeye dd2e510473
pindakaas: Remove no longer needed kernel argument
Maybe new kernel don't need it.
Or it has to do with using systemd for boot.
2024-06-30 20:55:51 +02:00
Geoffrey Frogeye ce516fffe9
curacao: Desk control 2024-06-30 17:41:29 +02:00
Geoffrey Frogeye 88e63aaf7f
Update 2024-06-29 18:10:50 +02:00
Geoffrey Frogeye a6a1e32ade
passwords: Only read passwords if needs updating
No need for unlocking keys on each rebuild anymore!
2024-06-29 01:50:18 +02:00
Geoffrey Frogeye 7b9c4fb004
passwords: Refactor 2024-06-26 02:16:50 +02:00
Geoffrey Frogeye 81b1307609
syncthing: Enable if needed 2024-06-25 13:26:38 +02:00
Geoffrey Frogeye 91e71bec07
xdg: Only create needed dirs 2024-06-24 15:53:09 +02:00
Geoffrey Frogeye ea1c390bc0
cranberry: Backlighting 2024-06-24 14:03:59 +02:00
Geoffrey Frogeye 498357fffc
pipewire: Replace pulseaudio
Required to make sound work on cranberry.
2024-06-24 03:51:43 +02:00
Geoffrey Frogeye 91df3670f6
frobar: Fix bytes sizes 2024-06-24 00:34:42 +02:00
Geoffrey Frogeye 8edb670486
syncthing: Declarative
I could split this commit in more but I won't.
The first commit in this repository said it would be the last legible
one, and I haven't followed that, so YOLO.
2024-06-23 23:57:03 +02:00
Geoffrey Frogeye b7d56a3118
atuin: More settings 2024-06-20 22:52:20 +02:00
Geoffrey Frogeye 8909b2bfa3
atuin: Added 2024-06-20 20:43:54 +02:00
Geoffrey Frogeye ac43ef6548
More stuff for cranberry 2024-06-20 16:47:58 +02:00
Geoffrey Frogeye 93d55c8c5c
frogarized: Vendor in result to prevent IFDs
Prevent accessing config from other machines, which is not great.
2024-06-19 17:37:14 +02:00
Geoffrey Frogeye eab20b4339
syncthing: OS-level barebones test
To test OS-level passwords
2024-06-18 22:58:58 +02:00
Geoffrey Frogeye a39118d439
Allow setting OS-level password
Bit ugly as it is, but we're slowly iterating towards a secret manager
I'm happy with.
2024-06-18 22:56:44 +02:00
Geoffrey Frogeye 5462fa43fa
frobar: Fix temperature for sensor without high values 2024-06-18 04:25:18 +02:00
Geoffrey Frogeye 795ed034f8
cranberry: Use Alt for modifier
Not everything fixed but it's a start
2024-06-18 04:17:35 +02:00
Geoffrey Frogeye 445c2b8a99
frobar: Mutli-display support
Freaking finally
2024-06-18 04:09:59 +02:00
Geoffrey Frogeye e09774c4ca
frobar: Type hint
That was TEDIOUS
2024-06-18 00:31:29 +02:00
Geoffrey Frogeye a489830949
xautolock: Key is now a toggle 2024-06-17 20:36:50 +02:00
Geoffrey Frogeye 42034eb5d8
frobar: Add load provider 2024-06-17 19:31:16 +02:00
Geoffrey Frogeye d6d3df65df
frobar: Display temperature for other CPU types 2024-06-17 19:30:48 +02:00
Geoffrey Frogeye a3fcaf9d27
unprocessed: Cleanup 2024-06-17 18:45:35 +02:00
Geoffrey Frogeye 2951280faa
wifi: Fix fix wifi_apply failing on boot 2024-06-17 18:26:49 +02:00
Geoffrey Frogeye 0d047d3e46
update-local-flakes: Make available as package
If extensions have their lock file updated in some cases,
running .#updateLocalFlakes will not work.
2024-06-17 18:21:09 +02:00
Geoffrey Frogeye c4058e8102
rebuild: Fix not working on light theme
Now I see how this kind of cursed issues arrive...
2024-06-17 17:01:05 +02:00
Geoffrey Frogeye aa5847ec76 Merge remote-tracking branch 'origin/main' 2024-06-17 15:24:36 +02:00
Geoffrey Frogeye 6570e71eca
wifi: Fix wifi_apply failing on boot 2024-06-17 15:23:25 +02:00
Geoffrey Frogeye 28ab3b0665
install: Fixed 2024-06-12 23:47:20 +02:00
Geoffrey Frogeye 10b48b22e1
cranberry: Added 2024-06-12 23:47:00 +02:00
Geoffrey Frogeye c31f1ba8dd
i3: Fix cardinals
Even with reducing a lot of the duplicates and risk to typo, I still
make typos...
2024-06-12 17:18:15 +02:00
Geoffrey Frogeye 865bffa641
phases: Allow loosen brightness setting again
Didn't manage to do what I wanted to do for some reason, but hey,
at least ddcutils is there.
2024-06-10 03:00:52 +02:00
Geoffrey Frogeye d5917b1264
Remove duplicate specialisation
Saves 16s on eval time
2024-06-10 02:21:32 +02:00
Geoffrey Frogeye 17f0ba3370
New rebuild mechanism
Put most of it as a flake app, so we can mess with it without relying on
`rb` being rebuilt. Also nom nom!
2024-06-10 02:12:59 +02:00
Geoffrey Frogeye 7b9d9053bf
autorandr: deterministic and applies for LigthDM 2024-06-09 14:53:54 +02:00
Geoffrey Frogeye f72112f332
powerline-go improvements 2024-06-08 19:35:21 +02:00
Geoffrey Frogeye 92ea60bbc8
wireless: Don't restart wpa_supplicant when restarting wifi_apply 2024-06-08 16:19:16 +02:00
Geoffrey Frogeye 96dea140be
Make Wi-Fi semi-declarative 2024-06-08 15:54:33 +02:00
Geoffrey Frogeye bc53468373
Use upstreaming displaylink 2024-06-05 00:46:10 +02:00
Geoffrey Frogeye 5297f8478a
24.05: Upped stateVersion according to release notes 2024-06-03 19:20:17 +02:00
Geoffrey Frogeye 48bf80f1c0
24.05 Fixed terminal, vim, lix and stuff
Also "fixed" display link, but turned out that the display cable wasn't
plugged in correctly anymore...
2024-06-02 22:30:18 +02:00
Geoffrey Frogeye 3479927d32
24.05: Fixed warnings 2024-06-01 21:32:11 +02:00
Geoffrey Frogeye 71385d9ba9
24.05: Buildable 2024-06-01 18:22:50 +02:00
Geoffrey Frogeye fe33f30bce
No more port 2278 2024-05-18 17:05:56 +02:00
Geoffrey Frogeye a95ae5f568
curacao: Reduce beesd load targets 2024-05-13 09:19:55 +02:00
Geoffrey Frogeye de187c6044 Revert "curacao: Relieve razmo of stress"
This reverts commit 2a3624af09.

With new razmo this should be fine.
2024-05-13 09:16:02 +02:00
Geoffrey Frogeye 2804086233
curacao: New razmo! 2024-05-12 20:34:22 +02:00
Geoffrey Frogeye 88e0a1eb09
Revert kernel upgrade
I need 6.2+ for DS4,
evdi doesn't work with kernel 6.6+,
all versions matching are EOL.

Probably won't game much before 24.05 release which hopefully helps
things?
2024-05-11 00:09:53 +02:00
Geoffrey Frogeye 995c115c90
🎮 DualShock 4 (or is it?) love
ddcci removed again because it still doesn't do anything and also doesn't compile
with latest kernel.
2024-05-10 23:27:45 +02:00
Geoffrey Frogeye 7c6e8adbed
Re-add itch 2024-05-10 17:31:33 +02:00
Geoffrey Frogeye 536eee36ad
remote-builds: Disable by default
It makes things slower and my server unresponsive sometimes.
Probably more work to be done but I don't feel like doing that now.
2024-05-10 14:50:02 +02:00
Geoffrey Frogeye 2a3624af09
curacao: Relieve razmo of stress
Maybe I'm pushing its limits too hard... it doesn't seem to be doing
great. Maybe because it knows I'll replace it soon.
2024-05-10 14:44:37 +02:00
Geoffrey Frogeye 4ff4e0cc99
curacao: No more archlinux ~~dataset~~ subvolume 2024-05-10 13:28:25 +02:00
Geoffrey Frogeye 6e8d8b43c2
Replace rnix with nixd 2024-05-10 11:05:48 +02:00
Geoffrey Frogeye 7c74c5e1d9
Lix: Remove substituters
Those are mostly misses, even for Lix.
2024-05-10 01:51:23 +02:00
Geoffrey Frogeye 552e1c1cf2
Upgrade 2024-05-10 01:51:16 +02:00
Geoffrey Frogeye 056e3447e4
remote-builds: Specify max jobs
Should make better use of the hardware. Otherwise it's roundtrip frenzy.
2024-05-09 23:13:08 +02:00
Geoffrey Frogeye bd84dd7fd7
remote-builds: Reduce amount of SSH connections
It seems to be doing a SSH connection for each path it has to check...
wow.
2024-05-09 23:07:51 +02:00
Geoffrey Frogeye cc46352873
Misc fixes 2024-05-08 13:24:03 +02:00
Geoffrey Frogeye c770380328
Lix! 😋🍦 2024-05-08 13:08:39 +02:00
Geoffrey Frogeye a2e15e8c33
Attempt at using nom 2024-05-08 12:56:05 +02:00
Geoffrey Frogeye bdabf30728
Add repl 2024-05-08 09:47:57 +02:00
Geoffrey Frogeye 176be4f218
Shorten flake a bit 2024-05-08 09:37:04 +02:00
Geoffrey Frogeye 82d5e8a466
remote-builds: Fix 🙈 2024-05-07 23:05:32 +02:00
Geoffrey Frogeye 7a612754f6
curacao: Ok nvidia modules brings problems, let's revert 2024-05-07 22:59:16 +02:00
Geoffrey Frogeye 0663e3755b
remote-builds: Fix SSH host keys
vivarium doesn't actually transfer the public keys, so the .pub files
are from somewhere during installation... oops.
2024-05-07 00:00:36 +02:00
Geoffrey Frogeye bd538785b8
remote-builds: Add aarch64 support 2024-05-06 22:50:01 +02:00
Geoffrey Frogeye c4bb02b16e
Attempt at using lix
Without remote builds and with my laptop setup it's not really viable
for now :(
2024-05-06 22:26:36 +02:00
Geoffrey Frogeye e68be9e665
curacao: nvidia drivers were not there? 2024-05-06 22:26:15 +02:00
Geoffrey Frogeye 836f8ee8b4
Further attempt at remote builds 2024-05-06 22:25:35 +02:00
Geoffrey Frogeye b0168f4354 gaming: Add dolphin 2024-05-05 13:32:36 +02:00
Geoffrey Frogeye 0e1d387069
Upgrade 2024-04-29 12:29:45 +02:00
Geoffrey Frogeye 7b8ff04f5d
gpg: Increase passphrase remembering timeout
Sweet relief.
2024-04-29 12:26:39 +02:00
Geoffrey Frogeye d276581d94
Add/refresh Nix-related search engines 2024-04-29 12:26:23 +02:00
Geoffrey Frogeye 5924bd59c6
Make Docker work
This tells you how much I use it 😅
2024-04-29 12:25:47 +02:00
Geoffrey Frogeye 173a231556
Add more network/hardware debug tools 2024-04-24 13:25:59 +02:00
Geoffrey Frogeye 98af492b75
curacao: Allow Wake On Lan 2024-04-23 17:57:20 +02:00
Geoffrey Frogeye 6e15aa2ea7
Upgrade 2024-04-20 09:42:53 +02:00
Geoffrey Frogeye 86019601f8
cranberry: Redo hardware configuration while being awake 2024-04-14 06:15:59 +02:00
Geoffrey Frogeye 028cadb6ab
Update 2024-04-11 10:43:14 +02:00
Geoffrey Frogeye 8005cbfbc1
Attempt at controlling external screens brightness 2024-04-10 16:32:14 +02:00
Geoffrey Frogeye b7d8797a6d
remote-builds: WIP 2024-04-10 01:05:38 +02:00
Geoffrey Frogeye 96ddd61320 Merge remote-tracking branch 'origin' 2024-04-05 13:50:28 +02:00
Geoffrey Frogeye cbc9a87f09
Update 2024-04-04 21:15:23 +02:00
Geoffrey Frogeye ab30bdf6a8 Merge remote-tracking branch 'origin/main' 2024-04-04 20:14:09 +02:00
Geoffrey Frogeye 067cfc3d7a
aerc: Added
More of a test for now?
2024-04-04 20:12:24 +02:00
Geoffrey Frogeye 044318babc
Display changed derivations on activation 2024-04-01 14:59:07 +02:00
Geoffrey Frogeye c319ee1394
Revert "desktop: Wii pointer"
This reverts commit a3999cc9b1.

It's fun 5 minutes, but missing the I thing is a bit annoying :(
2024-04-01 12:26:37 +02:00
Geoffrey Frogeye a3999cc9b1
desktop: Wii pointer 2024-04-01 12:25:53 +02:00
Geoffrey Frogeye 1615abd814
mpd/curacao: Can be controlled remotely 2024-03-29 10:42:25 +01:00
Geoffrey Frogeye 0c59a713da
Update 2024-03-28 16:16:40 +01:00
Geoffrey Frogeye 4e68c3ccf7
dedup: Keep load under control 2024-03-27 13:09:33 +01:00
Geoffrey Frogeye 5148643a64
curacao: Add cameractrls (sorta) 2024-03-27 13:09:00 +01:00
Geoffrey Frogeye 4358f717d0
curacao: Scrub scrub scrub 2024-03-26 18:28:21 +01:00
Geoffrey Preud'homme 25c00be8fd
Ability to use unstable packages 2024-03-26 17:04:16 +01:00
Geoffrey Preud'homme fe468eebd7
Remove nixGL 2024-03-26 16:49:09 +01:00
Geoffrey Preud'homme 7973e2ccd7
Remove usernix and home-manager standalone 2024-03-26 16:47:59 +01:00
Geoffrey Preud'homme 76a594ca9f Import some dependencies where they are needed 2024-03-26 16:18:17 +01:00
Geoffrey Frogeye 8d1d15a08e
Update 2024-03-23 09:48:04 +01:00
Geoffrey Frogeye b02ec1c28c
curacao: Apply intel microcode 2024-03-23 09:46:59 +01:00
Geoffrey Frogeye ce5a099899
curacao: Add CO2 sensor 2024-03-22 20:22:23 +01:00
Geoffrey Frogeye 20dd333799
curacao: Try deduplication 2024-03-22 18:04:23 +01:00
Geoffrey Frogeye f04f8160db
Update 2024-03-21 23:23:22 +01:00
Geoffrey Frogeye 35783ea086
curacao: New screen disposition 2024-03-21 23:23:08 +01:00
Geoffrey Frogeye 440b1e0563
Fix locale 2024-03-10 19:13:57 +01:00
Geoffrey Frogeye a0d7e43a9d
account: Add support 2024-03-09 23:50:31 +01:00
Geoffrey Frogeye b3f1d95634
lock: More goodness 2024-03-09 19:09:30 +01:00
Geoffrey Frogeye 82bafb3428
i3: Use --release 2024-03-09 19:02:57 +01:00
Geoffrey Frogeye 2fa993ad2d
i3: Reduce invalid binding warnings 2024-03-09 19:01:27 +01:00
Geoffrey Frogeye 14f7199d65
xlock: Add option 2024-03-09 18:22:51 +01:00
Geoffrey Frogeye c7c2c89f15 Merge remote-tracking branch 'origin/main' 2024-03-04 16:53:10 +01:00
Geoffrey Frogeye 2b76db290c
Upgrade 2024-03-04 16:52:01 +01:00
Geoffrey Frogeye ac0724d97a
pindakaas: Fix video stuttering 2024-02-19 00:55:45 +01:00
Geoffrey Frogeye 5d4908d2e2
update-local-flakes: Fail early 2024-02-18 14:12:32 +01:00
Geoffrey Frogeye ee4e45905a
wifi: Make more user-friendly 2024-02-18 13:38:01 +01:00
Geoffrey Frogeye 097d53807d
install_os: Fix flake selection 2024-02-18 00:09:08 +01:00
Geoffrey Frogeye 833320e3fa
Fix OS scripts for flakes 2024-02-17 23:35:53 +01:00
Geoffrey Frogeye 448a154d74
Fix previous for pindakaas 2024-02-17 19:05:50 +01:00
Geoffrey Frogeye 8476bbde12
Re-add variants and reorganize things 2024-02-17 18:39:09 +01:00
Geoffrey Frogeye bf803d18a6
Update and restore white for music 2024-02-12 12:31:59 +01:00
Geoffrey Frogeye 6e176fe61b
frobar: Use frogarized (dark) colors
Wanted to refactor things first, but hmmm, no sweet consistency has
priority.
2024-02-10 13:30:11 +01:00
Geoffrey Frogeye f65f6853ee
firefox: Some settings 2024-02-09 19:10:23 +01:00
Geoffrey Frogeye 972dcaae1f
Oupdate 2024-02-07 22:12:24 +01:00
Geoffrey Frogeye 659f6ae806 Merge remote-tracking branch 'origin/main' 2024-02-04 15:01:27 +01:00
Geoffrey Frogeye e5b034781d
Add evince
For forms. Crap, I should have put that as a file comment.
2024-02-04 14:54:35 +01:00
Geoffrey Frogeye 11c1c8d9f1 Merge remote-tracking branch 'office/main' 2024-02-03 19:40:42 +01:00
Geoffrey Frogeye 21aed8114f
btdu 2024-02-03 19:40:06 +01:00
Geoffrey Preud'homme 8e6203ce7d
scripts/lip: Fix 2024-02-01 16:24:19 +01:00
Geoffrey Frogeye 16f5a0a9a5
Merge remote-tracking branch 'origin/main' 2024-01-28 12:13:58 +01:00
Geoffrey Frogeye f30abd991c
Printing support 2024-01-28 12:13:27 +01:00
Geoffrey Frogeye c936d859c7
Frogarized! 2024-01-27 14:23:26 +01:00
Geoffrey Frogeye 5bba711d3c
Plymouth!
Yeah yeah I know...
2024-01-27 00:23:38 +01:00
Geoffrey Frogeye 9c6a2f69f0
vim/git: Use gitlinker 2024-01-26 22:02:25 +01:00
Geoffrey Frogeye 85cd61d206
thefuck: Added
Mostly as an experiment for now.
2024-01-26 18:38:28 +01:00
Geoffrey Frogeye ffd871299b
Update nixpkgs 2024-01-26 18:37:56 +01:00
Geoffrey Frogeye ca2dc262b7
gpg: Fancy pinentry 2024-01-26 00:23:52 +01:00
Geoffrey Frogeye 59db464987 Merge remote-tracking branch 'office/main' 2024-01-25 23:55:55 +01:00
Geoffrey Frogeye 0bb5981f3a
vim: Fix tabline bindings 2024-01-25 23:53:59 +01:00
Geoffrey Preud'homme 5b3c887b41
git-sync: Only push when there's something to push 2024-01-25 13:03:30 +01:00
Geoffrey Frogeye dfc8d68495
vim/decoration: Small adjustments 2024-01-23 23:38:48 +01:00
Geoffrey Frogeye 09b201ca24
Change status line
Time spent on writing Nix config:
5%: Testing new shiny things
7%: Debugging issues
88%: Gettings the colors and theming juuuuuust right
Help, my sleep schedule is dying
2024-01-22 00:02:13 +01:00
Geoffrey Frogeye 7cd77af9bf
vim: Move more things to prose 2024-01-20 19:36:49 +01:00
Geoffrey Frogeye a57c6527ce
vim: Fix lsp todos 2024-01-20 19:36:47 +01:00
Geoffrey Frogeye 97a3e5f6e4
vim: Fix Reload command 2024-01-20 19:36:45 +01:00
Geoffrey Frogeye 55756e4ae7
English is a programming language, fight me 2024-01-19 22:50:01 +01:00
Geoffrey Frogeye eac22be095
No Ansible by default 2024-01-19 00:50:45 +01:00
Geoffrey Frogeye fdf6725dc9
vim: Configure fugitive-gitlab 2024-01-18 22:49:46 +01:00
Geoffrey Frogeye 46db2dd34f
stylix: Workaround for non-DE environments 2024-01-17 00:08:07 +01:00
Geoffrey Frogeye bac1813c77
nod: Fifth attempt at flakes
Also simplified a few things, nice
2024-01-16 23:48:41 +01:00
Geoffrey Frogeye e56514890d
nod: 4th attempt at flakes
Is it the time I do the joke "Xth time the charm"?
2024-01-16 23:31:54 +01:00
Geoffrey Frogeye 30f1880f29
nod: 3rd attempt at flakes 2024-01-16 23:27:15 +01:00
Geoffrey Frogeye 770697f9f3
nod: Second attempt at flakes 2024-01-16 23:07:33 +01:00
Geoffrey Frogeye 65205a2fb8
nod: Flake test 2024-01-16 22:39:29 +01:00
Geoffrey Frogeye 55641fe958
passwordFiles: Added 2024-01-16 17:04:30 +01:00
Geoffrey Frogeye 6cee16924c
firefox: Rudimentary config, tridactyl support 2024-01-15 23:12:02 +01:00
Geoffrey Frogeye 1dbfd6cf88
terminal: Split out 2024-01-15 21:36:20 +01:00
Geoffrey Frogeye fbde2f5028
i3: Compress repetitions
For workspaces it makes sense, for cardinals maybe it was a tad
overkill. Oh well, at least it's ready for 3D 🙃
2024-01-15 20:11:54 +01:00
Geoffrey Frogeye 43e7a5af46
style: Split out and fixes 2024-01-15 19:26:44 +01:00
Geoffrey Frogeye 1b008c1ae8
presentation: Split out 2024-01-15 18:54:19 +01:00
Geoffrey Frogeye c1d8bc65af
Update and more backups 2024-01-15 18:36:51 +01:00
Geoffrey Frogeye 26e70acb2f
autorandr: Split out 2024-01-15 14:50:02 +01:00
Geoffrey Frogeye e9a8d16ece
i3: Add config to easily create modes
With their associated "switch-to" keybinding.
2024-01-13 22:58:45 +01:00
Geoffrey Frogeye e4c407fb28
Move some things where they belong 2024-01-13 01:51:42 +01:00
Geoffrey Frogeye d994dfb9fb
i3/desktop: Split out 2024-01-12 23:52:53 +01:00
Geoffrey Frogeye 2ad4bee0f9
vim: Split out 2024-01-12 18:08:11 +01:00
Geoffrey Frogeye 4412180b3a
Split out hm/common
I went nuclear...
2024-01-11 23:54:03 +01:00
Geoffrey Frogeye 033f411060
git-sync: Replace with git-sync 2024-01-11 22:25:52 +01:00
Geoffrey Frogeye f83806a307
go: Separate file 2024-01-10 14:01:39 +01:00
Geoffrey Frogeye bf796d9587
script: Various fixes 2024-01-10 13:55:15 +01:00
Geoffrey Frogeye 6e4130fd26
vim: Better snippets (and split completion file) 2024-01-09 21:53:00 +01:00
Geoffrey Frogeye d325eb2d27
vim: Replace nvim-compe with nvim-cmp 2024-01-09 20:00:12 +01:00
Geoffrey Frogeye aeccc22857
vim: Replace old plugin with rainbow-delimiter
In case you're wondering
3c8a185da4?tab=readme-ov-file#rainbow-delimitersnvim-integration
is not worth the trouble...
2024-01-09 00:13:20 +01:00
Geoffrey Frogeye 8f370c5040
python: Move to separate file 2024-01-08 23:24:17 +01:00
Geoffrey Frogeye 83b38ddf61
vim: Put in correct folder 2024-01-08 23:10:50 +01:00
Geoffrey Frogeye 42bc007ed4
c: Move into own file 2024-01-08 22:59:06 +01:00
Geoffrey Frogeye 881b22c9b2
Move gpg, git, tmux/screen to separate file 2024-01-08 21:48:31 +01:00
Geoffrey Frogeye 124df42fd8
hm: Reorganize installed programs 2024-01-07 23:41:35 +01:00
Geoffrey Frogeye 5360f8ff10
i3: Separate file 2024-01-07 23:29:16 +01:00
Geoffrey Frogeye 4190299030
qutebrowser: Add nix search engines 2024-01-07 22:51:09 +01:00
Geoffrey Frogeye 66f3179d41 Merge remote-tracking branch 'origin/main' 2024-01-07 22:39:54 +01:00
Geoffrey Frogeye ecc6cb983d
qutebrowser: Own file 2024-01-07 22:38:42 +01:00
Geoffrey Frogeye e0fb3fcb22
Flake fixes for new systems 2024-01-07 19:37:06 +01:00
Geoffrey Frogeye 597b50ebef
More flake fixes 2024-01-07 18:33:00 +01:00
Geoffrey Frogeye 6d98d85642
Fix Wi-Fi flakes 2024-01-06 19:10:47 +01:00
Geoffrey Frogeye e013bcfdba
Almost working flakes 2024-01-06 18:40:20 +01:00
Geoffrey Frogeye 25130195ec
Remove currently unused configs
Just to make transition to flakes easier.
We'll restore them later, maybe.
2024-01-06 17:20:37 +01:00
Geoffrey Frogeye 7506f55468
Merge branch 'main' into flakes 2024-01-06 12:39:01 +01:00
Geoffrey Frogeye 1abf3d503d
Add sha256 to go closer to purity 2024-01-05 19:04:44 +01:00
Geoffrey Frogeye c954f0df5f Quick commit 2024-01-05T18:42:21+01:00 2024-01-05 18:42:21 +01:00
Geoffrey Frogeye 3477528dd5
Add sha256 to go closer to purity 2024-01-05 18:41:10 +01:00
Geoffrey Frogeye e2bb686d12
Allow experimental nix features 2024-01-05 18:37:39 +01:00
Geoffrey Frogeye 74585ec4a7
lsd: Improve colors 2024-01-05 17:04:37 +01:00
Geoffrey Frogeye 5b70c2f448
Smol improvements 2024-01-04 23:18:06 +01:00
Geoffrey Frogeye 241ec71350
Remove already processed things 2024-01-04 22:14:31 +01:00
Geoffrey Frogeye 3755ab251d
Make fpga installable on aarch64
... when was the last time I had to do FPGA stuff though?
2024-01-04 22:11:11 +01:00
Geoffrey Frogeye 0bc0aaa9bf
Add git-sync-pull 2024-01-04 22:10:44 +01:00
Geoffrey Frogeye c7d69cd100
Start using lsd
Amazing project and commit name
2024-01-04 21:35:29 +01:00
Geoffrey Preud'homme e1c041368b
usernix: Fixes following testing 2024-01-04 19:45:07 +01:00
Geoffrey Frogeye 8b78cad60c
nod: Fourth attempt 2023-12-25 12:11:23 +01:00
Geoffrey Frogeye becf0c961f
nod: Third attempt 2023-12-25 12:07:17 +01:00
Geoffrey Frogeye 9362e78f87
nod: Second attempt 2023-12-25 12:02:24 +01:00
Geoffrey Frogeye f94e741948
nod: First attempt 2023-12-25 11:04:01 +01:00
Geoffrey Frogeye ec1d120f12
Add Wi-Fi for 37C3
Yes that was the laziest option to do that.
2023-12-24 22:24:12 +01:00
195 changed files with 9700 additions and 5963 deletions

5
.gitignore vendored
View file

@ -1,5 +1,2 @@
*/hm result
*/system
*/vm
*/vmWithBootLoader
*.qcow2 *.qcow2

View file

@ -28,7 +28,6 @@ It is built on top of the Nix ecosystem
## Scripts ## Scripts
They all have a `-h` flag. They all have a `-h` flag.
Except `add_channels.sh`, which should be removed as soon as I migrate to Flakes.
## Extensions ## Extensions

10
abavorana/standin.nix Normal file
View file

@ -0,0 +1,10 @@
{ ... }:
{
config = {
frogeye = {
name = "abavorana";
storageSize = "big";
syncthing.name = "Abavorana";
};
};
}

View file

@ -1,8 +0,0 @@
#!/usr/bin/env bash
# TODO Flakes
nix-channel --add https://nixos.org/channels/nixos-23.11 nixpkgs
nix-channel --add https://github.com/nix-community/home-manager/archive/release-23.11.tar.gz home-manager
nix-channel --add https://github.com/NixOS/nixos-hardware/archive/8772491ed75f150f02552c60694e1beff9f46013.tar.gz nixos-hardware
nix-channel --update

View file

@ -1,70 +0,0 @@
#!/usr/bin/env nix-shell
#! nix-shell -i bash
#! nix-shell -p bash nix-output-monitor
set -euo pipefail
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
# Parse arguments
function help {
echo "Usage: $0 [-h|-v|-b] profile"
echo "Build Home Manager configuration on the local machine."
echo
echo "Arguments:"
echo " profile: Home Manager profile to use"
echo
echo "Options:"
echo " -h: Display this help message."
}
while getopts "h" OPTION
do
case "$OPTION" in
h)
help
exit 0
;;
?)
help
exit 2
;;
esac
done
shift "$(($OPTIND -1))"
if [ "$#" -ne 1 ]
then
help
exit 2
fi
profile="$1"
profile_dir="${SCRIPT_DIR}/${profile}"
if [ ! -d "$profile_dir" ]
then
echo "Profile not found."
fi
home_manager_config="${profile_dir}/hm.nix"
if [ ! -f "$home_manager_config" ]
then
echo "Home Manager configuration not found."
fi
set -x
nom-build '<home-manager/home-manager/home-manager.nix>' --argstr confPath "${home_manager_config}" -o "${profile_dir}/hm"
set +x
echo 
path="$(readlink -f "${profile_dir}/hm")"
echo "Manual installation instructions:"
echo "- Transfer $path and dependencies to the destination machine (somehow)"
echo "- Run $path/activate as the destination user"
echo "- Log into the user again to make sure everything is sourced"
echo "- Transfer necessary private keys (or use ssh -A for testing)"
echo "- Run git-sync-init"
echo "- Check that the system can build itself"

View file

@ -1,14 +1,14 @@
#!/usr/bin/env nix-shell #!/usr/bin/env nix-shell
#! nix-shell -i bash #! nix-shell -i bash
#! nix-shell -p bash nix-output-monitor #! nix-shell -p nix
set -euo pipefail set -euo pipefail
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
# Parse arguments # Parse arguments
function help { function help {
echo "Usage: $0 [-h|-v|-b] profile" echo "Usage: $0 [-h|-e|-b] [flake-uri#]name"
echo "Build NixOS configuration on the local machine." echo "Build a NixOS configuration on the local machine."
echo echo
echo "Arguments:" echo "Arguments:"
echo " profile: OS/disk profile to use" echo " profile: OS/disk profile to use"
@ -19,7 +19,7 @@ function help {
echo " -b: Build a virtual machine with boot loader." echo " -b: Build a virtual machine with boot loader."
} }
attr=system arg=build
while getopts "hvb" OPTION while getopts "hvb" OPTION
do do
case "$OPTION" in case "$OPTION" in
@ -28,10 +28,10 @@ do
exit 0 exit 0
;; ;;
v) v)
attr=vm arg=build-vm
;; ;;
b) b)
attr=vmWithBootLoader arg=build-vm-with-bootloader
;; ;;
?) ?)
help help
@ -39,29 +39,35 @@ do
;; ;;
esac esac
done done
shift "$(($OPTIND -1))" shift "$((OPTIND -1))"
if [ "$#" -ne 1 ] if [ "$#" -ne 1 ]
then then
help help
exit 2 exit 2
fi fi
profile="$1"
profile_dir="${SCRIPT_DIR}/${profile}" if [[ "$1" == *"#"* ]]
if [ ! -d "$profile_dir" ]
then then
echo "Profile not found." flake_uri="$(echo "$1" | cut -d'#' -f1)"
flake_uri=$( cd -- "$flake_uri" &> /dev/null && pwd )
name="$(echo "$1" | cut -d'#' -f2)"
else
flake_uri="$SCRIPT_DIR"
name="$1"
fi fi
nixos_config="${profile_dir}/os.nix" if [ ! -f "$flake_uri/flake.nix" ]
if [ ! -f "$nixos_config" ]
then then
echo "NixOS configuration not found." echo "Flake not found."
fi fi
flake="${flake_uri}#${name}"
set -x set -x
nom-build '<nixpkgs/nixos>' -I "nixos-config=${nixos_config}" -A "$attr" -o "${profile_dir}/${attr}" nix --extra-experimental-features "nix-command flakes" run "${SCRIPT_DIR}#nixos-rebuild" -- "$arg" --flake "$flake"
echo  echo 
# TODO Use update-local-flakes?

View file

@ -1,10 +1,17 @@
{ id, name, passwordFile ? "/should_not_be_needed_in_this_context", ... }: {
pkgs,
lib,
config,
...
}:
let
passwordFile = "/tmp/dotfiles_${config.frogeye.name}_password";
in
{ {
disko.devices = { disko.devices = {
disk = { disk = {
"${name}" = { "${config.frogeye.name}" = {
type = "disk"; type = "disk";
device = "/dev/disk/by-id/${id}";
content = { content = {
type = "gpt"; type = "gpt";
partitions = { partitions = {
@ -25,7 +32,7 @@
size = "100%"; size = "100%";
content = { content = {
type = "luks"; type = "luks";
name = "${name}"; name = "${config.frogeye.name}";
passwordFile = passwordFile; passwordFile = passwordFile;
settings = { settings = {
# Not having SSDs die fast is more important than crypto # Not having SSDs die fast is more important than crypto
@ -39,15 +46,24 @@
subvolumes = { subvolumes = {
"/nixos" = { "/nixos" = {
mountpoint = "/"; mountpoint = "/";
mountOptions = [ "compress=zstd" "noatime" ]; mountOptions = [
"compress=zstd"
"noatime"
];
}; };
"/home" = { "/home" = {
mountpoint = "/home"; mountpoint = "/home";
mountOptions = [ "compress=zstd" "relatime" ]; mountOptions = [
"compress=zstd"
"relatime"
];
}; };
"/nix" = { "/nix" = {
mountpoint = "/nix"; mountpoint = "/nix";
mountOptions = [ "compress=zstd" "noatime" ]; mountOptions = [
"compress=zstd"
"noatime"
];
}; };
# Maybe later # Maybe later
# "/swap" = { # "/swap" = {

View file

@ -0,0 +1,54 @@
{ config, ... }:
let
# Use ./frogarized.py to generate
# Vendored to prevent IFDs
frogarized = rec {
common = {
author = "Geoffrey Frogeye (with work from Ethan Schoonover)";
base08 = "#e0332e";
base09 = "#cf4b15";
base0A = "#bb8801";
base0B = "#8d9800";
base0C = "#1fa198";
base0D = "#008dd1";
base0E = "#5c73c4";
base0F = "#d43982";
};
light = common // {
base00 = "#fff0f1";
base01 = "#fae2e3";
base02 = "#99a08d";
base03 = "#89947f";
base04 = "#677d64";
base05 = "#5a7058";
base06 = "#143718";
base07 = "#092c0e";
scheme = "Frogarized Light";
slug = "frogarized-light";
};
dark = common // {
base00 = "#092c0e";
base01 = "#143718";
base02 = "#5a7058";
base03 = "#677d64";
base04 = "#89947f";
base05 = "#99a08d";
base06 = "#fae2e3";
base07 = "#fff0f1";
scheme = "Frogarized Dark";
slug = "frogarized-dark";
};
};
in
{
config = {
stylix = {
base16Scheme = frogarized.${config.stylix.polarity};
# On purpose also enable without a DE because stylix complains otherwise
image = builtins.fetchurl {
url = "https://get.wallhere.com/photo/sunlight-abstract-minimalism-green-simple-circle-light-leaf-wave-material-line-wing-computer-wallpaper-font-close-up-macro-photography-124350.png";
sha256 = "sha256:1zfq3f3v34i45mi72pkfqphm8kbhczsg260xjfl6dbydy91d7y93";
};
};
};
}

112
common/frogarized/frogarized.py Executable file
View file

@ -0,0 +1,112 @@
import argparse
import json
import colorspacious
import numpy as np
# Original values for the Solarized color scheme,
# created by Ethan Schoonover (https://ethanschoonover.com/solarized/)
SOLARIZED_LAB = np.array(
[
[15, -12, -12],
[20, -12, -12],
[45, -7, -7],
[50, -7, -7],
[60, -6, -3],
[65, -5, -2],
[92, -0, 10],
[97, 0, 10],
[50, 65, 45],
[50, 50, 55],
[60, 10, 65],
[60, -20, 65],
[60, -35, -5],
[55, -10, -45],
[50, 15, -45],
[50, 65, -5],
]
)
# I couldn't get a perfect translation of Solarized L*a*b values into sRGB,
# so here is upstream's translation for reference
SOLARIZED_RGB = np.array(
[
[0, 43, 54],
[7, 54, 66],
[88, 110, 117],
[101, 123, 131],
[131, 148, 150],
[147, 161, 161],
[238, 232, 213],
[253, 246, 227],
[220, 50, 47],
[203, 75, 22],
[181, 137, 0],
[133, 153, 0],
[42, 161, 152],
[38, 139, 210],
[108, 113, 196],
[211, 54, 130],
]
)
# Parse arguments
parser = argparse.ArgumentParser(
description="Generate a base16-theme based derived from Solarized"
)
parser.add_argument("--source", choices=["lab", "rgb"], default="lab")
parser.add_argument("--lightness_factor", type=float, default=1.0)
parser.add_argument("--chroma-factor", type=float, default=1.0)
parser.add_argument("--hue_shift", type=float, default=-75.0)
parser.add_argument("--polarity", choices=["dark", "light"], default="dark")
parser.add_argument(
"--output", choices=["json", "truecolor"], default="truecolor"
)
args = parser.parse_args()
# Convert source to JCh color space
if args.source == "lab":
solarized_jch = colorspacious.cspace_convert(
SOLARIZED_LAB, "CIELab", "JCh"
)
elif args.source == "rgb":
solarized_jch = colorspacious.cspace_convert(
SOLARIZED_RGB, "sRGB255", "JCh"
)
# Build frogarized theme
jch_factor = [args.lightness_factor, args.chroma_factor, 1]
jch_shift = [0, 0, args.hue_shift]
frogarzied_jch = np.vstack(
[solarized_jch[:8] * jch_factor + jch_shift, solarized_jch[8:]]
)
# Convert frogarized to RGB
frogarized_srgb = colorspacious.cspace_convert(
frogarzied_jch, "JCh", "sRGB255"
)
frogarized_rgb = np.uint8(np.rint(np.clip(frogarized_srgb, 0, 255)))
if args.polarity == "light":
frogarized_rgb = np.vstack([frogarized_rgb[7::-1], frogarized_rgb[8:]])
# Output
palette = dict()
for i in range(16):
rgb = frogarized_rgb[i]
r, g, b = rgb
hex = f"#{r:02x}{g:02x}{b:02x}"
palette[f"base{i:02X}"] = hex
if args.output == "truecolor":
print(f"\033[48;2;{r};{g};{b}m{hex}\033[0m") # ]]
# treesitter is silly and will consider brackets in strings
# as indentation, hence the comment above
if args.output == "json":
scheme = palette.copy()
scheme.update(
{
"slug": f"frogarized-{args.polarity}",
"scheme": f"Frogarized {args.polarity.title()}",
"author": "Geoffrey Frogeye (with work from Ethan Schoonover)",
}
)
print(json.dumps(scheme, indent=4))

View file

@ -0,0 +1,2 @@
{ pkgs, ... }:
pkgs.writers.writePython3Bin "update-local-flakes" { } (builtins.readFile ./update-local-flakes.py)

View file

@ -0,0 +1,3 @@
(self: super: {
update-local-flakes = super.callPackage ./. { };
})

View file

@ -0,0 +1,62 @@
import argparse
import json
import os
import subprocess
GET_INPUTS_CMD = [
"nix-instantiate",
"--eval",
"--json", # This parser is stupid, better provide it with pre-eaten stuff
"--expr",
"builtins.fromJSON (builtins.toJSON (import ./flake.nix).inputs)",
]
def process_flake(flakeUri: str) -> None:
# get full path
flakeUri = os.path.normpath(flakeUri)
flakeFile = os.path.join(flakeUri, "flake.nix")
if not os.path.isfile(flakeFile):
raise FileNotFoundError(f"Flake not found: {flakeUri}")
# import dependencies
p = subprocess.run(GET_INPUTS_CMD, cwd=flakeUri, stdout=subprocess.PIPE)
deps = json.loads(p.stdout)
p.check_returncode()
# for each dependency
for dep_name, dep in deps.items():
dep_url = dep["url"]
# if not local path, continue
if not (
dep_url.startswith("path:")
or dep_url.startswith("git+file:")
):
continue
if dep.get("flake", True):
# get flake file corresponding
dep_path = dep_url.split(":")[1]
if not dep_path.startswith("/"):
dep_path = os.path.join(flakeUri, dep_path)
process_flake(dep_path)
# update lockfile
cmd = [
"nix",
"--extra-experimental-features",
"nix-command",
"--extra-experimental-features",
"flakes",
"flake",
"update",
dep_name,
]
p = subprocess.run(cmd, cwd=flakeUri)
p.check_returncode()
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Recursively update lockfiles "
"of flakes located on the system"
)
parser.add_argument("flake", help="Starting flake", default="/")
args = parser.parse_args()
process_flake(args.flake)

18
cranberry/default.nix Normal file
View file

@ -0,0 +1,18 @@
{
pkgs,
lib,
config,
...
}:
{
config = {
frogeye.name = "cranberry";
disko.devices.disk."${config.frogeye.name
}".device = "/dev/disk/by-id/nvme-UMIS_RPJTJ128MEE1MWX_SS0L25188X3RC12121TP";
};
imports = [
../common/disko/single_uefi_btrfs.nix
./hardware.nix
./features.nix
];
}

13
cranberry/features.nix Normal file
View file

@ -0,0 +1,13 @@
{ ... }:
{
config = {
frogeye = {
desktop.xorg = true;
dev = {
c = true;
vm = true;
};
extra = true;
};
};
}

65
cranberry/hardware.nix Normal file
View file

@ -0,0 +1,65 @@
{
pkgs,
lib,
config,
nixos-hardware,
...
}:
{
config = {
boot = {
# From nixos-generate-config
initrd.availableKernelModules = [
"nvme"
"xhci_pci"
"usb_storage"
"sd_mod"
"sdhci_pci"
];
kernelModules = [ "kvm-amd" ];
# Times tpm2.target times out waiting for /dev/tpmrm0,
# it's fine after unlocking though
initrd.systemd.tpm2.enable = false;
};
# Needed for Wi-Fi
hardware.enableRedistributableFirmware = true;
frogeye.desktop = {
x11_screens = [ "eDP-1" ];
maxVideoHeight = 1080;
phasesCommands = {
jour = ''
echo 0 | sudo tee /sys/class/leds/chromeos::kbd_backlight/brightness &
${pkgs.brightnessctl}/bin/brightnessctl set 30% &
'';
crepuscule = ''
echo 1 | sudo tee /sys/class/leds/chromeos::kbd_backlight/brightness &
${pkgs.brightnessctl}/bin/brightnessctl set 10% &
'';
nuit = ''
echo 10 | sudo tee /sys/class/leds/chromeos::kbd_backlight/brightness &
${pkgs.brightnessctl}/bin/brightnessctl set 0% &
'';
};
};
# Alt key swallowed the Meta one
home-manager.users.geoffrey =
{ ... }:
{
xsession.windowManager.i3.config.modifier = "Mod1";
};
# 8 makes it run out of memory when rebuilding.
nix.settings.max-jobs = 1;
};
imports = [
nixos-hardware.nixosModules.common-cpu-amd
nixos-hardware.nixosModules.common-gpu-amd
nixos-hardware.nixosModules.common-pc-laptop
nixos-hardware.nixosModules.common-pc-ssd
];
}

View file

@ -2,13 +2,25 @@
# MANU Snapper is not able to create the snapshot directory, so you'll need to do this after eventually running the backup script: # MANU Snapper is not able to create the snapshot directory, so you'll need to do this after eventually running the backup script:
# sudo btrfs subvol create /mnt/razmo/$subvolume/.snapshots # sudo btrfs subvol create /mnt/razmo/$subvolume/.snapshots
let let
backup_subvolumes = [ "nixos" "home.rapido" ]; backup_subvolumes = [
"nixos"
"home.rapido"
"home.nixos"
];
backup_app = pkgs.writeShellApplication { backup_app = pkgs.writeShellApplication {
name = "backup-subvolume"; name = "backup-subvolume";
runtimeInputs = with pkgs; [ coreutils btrfs-progs ]; runtimeInputs = with pkgs; [
coreutils
btrfs-progs
];
text = builtins.readFile ./backup.sh; text = builtins.readFile ./backup.sh;
}; };
snapper_subvolumes = [ "nixos" "home.rapido" "home.razmo" ]; snapper_subvolumes = [
"nixos"
"home.rapido"
"home.razmo"
"home.nixos"
];
in in
{ {
services = services =
@ -28,21 +40,26 @@ in
# cleanup hourly snapshots after some time # cleanup hourly snapshots after some time
TIMELINE_CLEANUP = true; TIMELINE_CLEANUP = true;
TIMELINE_MIN_AGE = 1800; TIMELINE_MIN_AGE = 1800;
TIMELINE_LIMIT_HOURLY = 24; TIMELINE_LIMIT_HOURLY = "24";
TIMELINE_LIMIT_DAILY = 31; TIMELINE_LIMIT_DAILY = "31";
TIMELINE_LIMIT_WEEKLY = 8; TIMELINE_LIMIT_WEEKLY = "8";
TIMELINE_LIMIT_MONTHLY = 0; TIMELINE_LIMIT_MONTHLY = "0";
TIMELINE_LIMIT_YEARLY = 0; TIMELINE_LIMIT_YEARLY = "0";
# cleanup empty pre-post-pairs # cleanup empty pre-post-pairs
EMPTY_PRE_POST_CLEANUP = true; EMPTY_PRE_POST_CLEANUP = true;
}; };
in in
{ {
snapper.configs = lib.attrsets.mergeAttrsList (map (s: { "${s}" = default // { SUBVOLUME = "/mnt/razmo/${s}"; }; }) snapper_subvolumes); snapper.configs = lib.attrsets.mergeAttrsList (
map (s: {
"${s}" = default // {
SUBVOLUME = "/mnt/razmo/${s}";
};
}) snapper_subvolumes
);
}; };
systemd = { systemd = {
services.bkp_rapido = { services.bkp_rapido = {
description = "Make a snapshot of the SSD to the HDD"; description = "Make a snapshot of the SSD to the HDD";

View file

@ -0,0 +1,84 @@
{
pkgs,
lib,
...
}:
let
zytemp_mqtt_src = pkgs.fetchFromGitHub {
# owner = "patrislav1";
owner = "GeoffreyFrogeye";
repo = "zytemp_mqtt";
rev = "push-nurpouorqoyr"; # Humidity + availability support
sha256 = "sha256-nOhyBAgvjeQh9ys3cBJOVR67SDs96zBzxIRGpaq4yoA=";
};
zytemp_mqtt = pkgs.python3Packages.buildPythonPackage {
name = "zytemp_mqtt";
src = zytemp_mqtt_src;
propagatedBuildInputs = with pkgs.python3Packages; [
hidapi
paho-mqtt
pyaml
];
};
usb_zytemp_udev = pkgs.stdenv.mkDerivation {
pname = "usb-zytemp-udev-rules";
version = "unstable-2023-05-24";
src = zytemp_mqtt_src;
dontConfigure = true;
dontBuild = true;
dontFixup = true;
installPhase = ''
mkdir -p $out/lib/udev/rules.d
cp udev/90-usb-zytemp-permissions.rules $out/lib/udev/rules.d/90-usb-zytemp.rules
sed -i 's|"usb"|"hidraw"|' $out/lib/udev/rules.d/90-usb-zytemp.rules
'';
};
mqtt_host = "192.168.7.53"; # Ludwig
in
{
config = {
environment.etc."zytempmqtt/config.yaml".text = lib.generators.toYAML { } {
decrypt = true;
mqtt_host = mqtt_host;
friendly_name = "Desk sensor";
};
services.udev.packages = [ usb_zytemp_udev ];
systemd = {
services.zytemp_mqtt = {
description = "Forward zyTemp CO2 sensor to MQTT";
wantedBy = [ "multi-user.target" ];
serviceConfig = {
ExecStart = "${zytemp_mqtt}/bin/zytempmqtt";
# Hardening (hapazardeous)
CapabilityBoundingSet = "";
DynamicUser = true;
LockPersonality = true;
MemoryDenyWriteExecute = false;
NoNewPrivileges = true;
PrivateTmp = true;
PrivateUsers = true;
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
RemoveIPC = true;
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallFilter = [
"@system-service"
"~@privileged"
"~@resources"
];
UMask = "0077";
};
};
};
};
}

28
curacao/dedup/default.nix Normal file
View file

@ -0,0 +1,28 @@
{
pkgs,
lib,
config,
...
}:
{
config = {
services.beesd.filesystems = {
razmo = {
spec = "/mnt/razmo";
hashTableSizeMB = 512; # Recommended for 1 TiB, ×2 for compression, x2 for time
extraOptions = [
"--loadavg-target"
"7.5"
];
};
rapido = {
spec = "/mnt/rapido";
hashTableSizeMB = 128; # 4 times smaller disk, 4 times smaller hashtable?
extraOptions = [
"--loadavg-target"
"5"
];
};
};
};
}

22
curacao/default.nix Normal file
View file

@ -0,0 +1,22 @@
{
pkgs,
lib,
config,
...
}:
{
config = {
frogeye.name = "curacao";
};
imports = [
./backup
./co2meter
./dedup
./desk
./disko.nix
./features.nix
./hardware.nix
./homeautomation
./webcam
];
}

61
curacao/desk/default.nix Normal file
View file

@ -0,0 +1,61 @@
{
pkgs,
...
}:
let
desk_mqtt = pkgs.writers.writePython3 "desk_mqtt" {
libraries = with pkgs.python3Packages; [
pyusb
ha-mqtt-discoverable
];
} (builtins.readFile ./desk_mqtt.py);
usb2lin06_udev = pkgs.writeTextFile {
name = "usb2lin06-udev-rules";
text = ''
SUBSYSTEM=="usb", ATTR{idVendor}=="12d3", ATTR{idProduct}=="0002", MODE="0666"
'';
destination = "/lib/udev/rules.d/90-usb2lin06.rules";
};
in
{
config = {
services.udev.packages = [ usb2lin06_udev ];
systemd = {
services.desk_mqtt = {
description = "Control desk height via MQTT";
wantedBy = [ "multi-user.target" ];
serviceConfig = {
ExecStart = "${desk_mqtt}";
RestartSec = 10;
Restart = "on-failure";
# Hardening (hapazardeous)
CapabilityBoundingSet = "";
DynamicUser = true;
LockPersonality = true;
MemoryDenyWriteExecute = false;
NoNewPrivileges = true;
PrivateTmp = true;
PrivateUsers = true;
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
RemoveIPC = true;
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallFilter = [
"@system-service"
"~@privileged"
"~@resources"
];
UMask = "0077";
};
};
};
};
}

381
curacao/desk/desk_mqtt.py Executable file
View file

@ -0,0 +1,381 @@
import logging
import struct
import time
import typing
import ha_mqtt_discoverable
import ha_mqtt_discoverable.sensors
import paho.mqtt.client
import usb.core
import usb.util
class Desk:
"""
Controls my Linak desk, which is a CBD4P controller connected via USB2LIN06
This particular combination doesn't seem to report desk height,
so it is estimated from the physical controller that does work.
"""
# Source of data:
# https://github.com/UrbanskiDawid/usb2lin06-HID-in-linux-for-LINAK-Desk-Control-Cable
# https://github.com/monofox/python-linak-desk-control
# https://github.com/gryf/linak-ctrl
# Desk Control Basic Software
# https://www.linak-us.com/products/controls/desk-control-basic-software/
# Says it's connected but doesn't report height and buttons do nothing
# Expected, as manual says it only works with CBD4A or CBD6
# Decompiled with ILSpy (easy), doesn't offer much though
# CBD4+5 Configurator
# https://www.linak.nl/technische-ondersteuning/#/cbd4-cbd6s-configurator
# Connects, and settings can be changed.
# Don't think there's much that would help with our problem.
# Tried to decompile with Ghidra (hard), didn't go super far
VEND = 0x12D3
PROD = 0x0002
# Official apps use HID library, although only managed to barely make
# pyhidapi read manufacturer and product once after device reset
BUF_LEN = 64
MOVE_CMD_REPEAT_INTERVAL = 0.2 # s
STOP_CMD_INTERVAL = 1 # s
MAX_EST_INTERVAL = 10 # s
# Theoritical height values
VALUE_MIN = 0x0000
VALUE_MAX = 0x7FFE
VALUE_DOWN = 0x7FFF
VALUE_UP = 0x8000
VALUE_STOP = 0x8001
# Measured values
VALUE_BOT = 0x0001
VALUE_TOP = 0x1A50
HEIGHT_BOT = 68
HEIGHT_TOP = 135
FULL_RISE_TIME = 17.13 # s
FULL_FALL_TIME = 16.64 # s
# Computed values
HEIGHT_OFFSET = HEIGHT_BOT # cm
HEIGHT_MULT = VALUE_TOP / (HEIGHT_TOP - HEIGHT_BOT) # unit / cm
# Should be 100 in theory (1 unit = 0.1 mm)
FULL_TIME = (FULL_FALL_TIME + FULL_RISE_TIME) / 2 # s
SPEED_MARGIN = 0.9
# Better estimate a bit slower
SPEED = (VALUE_TOP - VALUE_BOT) / FULL_TIME * SPEED_MARGIN # unit / s
def _cmToUnit(self, height: float) -> int:
return round((height - self.HEIGHT_OFFSET) * self.HEIGHT_MULT)
def _unitToCm(self, height: int) -> float:
return height / self.HEIGHT_MULT + self.HEIGHT_OFFSET
def _get(self, typ: int, overflow_ok: bool = False) -> bytes:
# Magic numbers: get class interface, HID get report
raw = self._dev.ctrl_transfer(
0xA1, 0x01, 0x300 + typ, 0, self.BUF_LEN
).tobytes()
self.log.debug(f"Received {raw.hex()}")
assert raw[0] == typ
size = raw[1]
end = 2 + size
if not overflow_ok:
assert end < self.BUF_LEN
return raw[2:end]
# Non-implemented types:
# 1, 7: some kind of stream when the device isn't initialized?
# size reduces the faster you poll, increases when buttons are held
# 9: unknown, always report 0
def _set(self, typ: int, buf: bytes) -> None:
buf = bytes([typ]) + buf
# The official apps pad, not that it doesn't seem to work without
buf = buf + b"\x00" * (self.BUF_LEN - len(buf))
self.log.debug(f"Sending {buf.hex()}")
# Magic numbers: set class interface, HID set report
self._dev.ctrl_transfer(0x21, 0x09, 0x300 + typ, 0, buf)
# Non-implemented types:
# Some stuff < 10
def _reset_estimations(self) -> None:
self.est_value: None | int = None
self.est_value_bot = float(self.VALUE_BOT)
self.est_value_top = float(self.VALUE_TOP)
self.last_est: float = 0.0
def _initialize(self) -> None:
"""
Seems to take the USB2LIN06 out of "boot mode"
(name according to CBD4 Controller) which it is after reset.
Permits control and reading the report.
"""
buf = bytes([0x04, 0x00, 0xFB])
self._set(3, buf)
time.sleep(0.5)
def __init__(self) -> None:
self.log = logging.getLogger("Desk")
self._dev = usb.core.find(idVendor=Desk.VEND, idProduct=Desk.PROD)
if not self._dev:
raise ValueError(
f"Device {Desk.VEND}:" f"{Desk.PROD:04d} " f"not found!"
)
if self._dev.is_kernel_driver_active(0):
self._dev.detach_kernel_driver(0)
self._initialize()
self._reset_estimations()
self.last_destination = None
self.fetch_callback: typing.Callable[["Desk"], None] | None = None
def _get_report(self) -> bytes:
raw = self._get(4)
assert len(raw) == 0x38
return raw
def _update_estimations(self) -> None:
now = time.time()
delta_s = now - self.last_est
if delta_s > self.MAX_EST_INTERVAL:
# Attempt at fixing the issue of
# the service not working after the night
self._initialize()
self.log.warning(
"Too long without getting a report, "
"assuming the desk might be anywhere now."
)
self._reset_estimations()
else:
delta_u = delta_s * self.SPEED
if self.destination == self.VALUE_STOP:
pass
elif self.destination == self.VALUE_UP:
self.est_value_bot += delta_u
self.est_value_top += delta_u
elif self.destination == self.VALUE_DOWN:
self.est_value_bot -= delta_u
self.est_value_top -= delta_u
else:
def move_closer(start_val: float) -> float:
if start_val < self.destination:
end_val = start_val + delta_u
return min(end_val, self.destination)
else:
end_val = start_val - delta_u
return max(end_val, self.destination)
self.est_value_bot = move_closer(self.est_value_bot)
self.est_value_top = move_closer(self.est_value_top)
# Clamp
self.est_value_bot = max(self.VALUE_BOT, self.est_value_bot)
self.est_value_top = min(self.VALUE_TOP, self.est_value_top)
if self.est_value_top == self.est_value_bot:
if self.est_value is None:
self.log.info("Height estimation converged")
self.est_value = int(self.est_value_top)
self.last_est = now
def fetch(self) -> None:
for _ in range(3):
try:
raw = self._get_report()
break
except usb.USBError as e:
self.log.error(e)
else:
raw = self._get_report()
# Allegedly, from decompiling:
# https://www.linak-us.com/products/controls/desk-control-basic-software/
# Never reports anything in practice
self.value = struct.unpack("<H", raw[0:2])[0]
unk = struct.unpack("<H", raw[2:4])[0]
self.initalized = (unk & 0xF) != 0
# From observation. Reliable
self.destination = (struct.unpack("<H", raw[18:20])[0],)[0]
if self.destination != self.last_destination:
self.log.info(f"Destination changed to {self.destination:04x}")
self.last_destination = self.destination
self._update_estimations()
if self.fetch_callback is not None:
self.fetch_callback(self)
def _move(self, position: int) -> None:
buf = struct.pack("<H", position) * 4
self._set(5, buf)
def _move_to(self, position: int) -> None:
# Clamp
position = max(self.VALUE_BOT, position)
position = min(self.VALUE_TOP, position)
self.log.info(f"Start moving to {position:04x}")
self.fetch()
while self.est_value != position:
self._move(position)
time.sleep(self.MOVE_CMD_REPEAT_INTERVAL)
self.fetch()
self.stop()
def move_to(self, position: float) -> None:
"""
If any button is held during movement, the desk will stop moving,
yet this will think it's still moving, throwing off the estimates.
It's not a bug, it's a safety feature.
Also if you try to make it move when it's already moving,
it's going to keep moving while desyncing.
That one is a bug.
"""
# Would to stop for a while before reversing course, without being able
# to read the actual height it's just too annoying to implement
return self._move_to(self._cmToUnit(position))
def stop(self) -> None:
self.log.info("Stop moving")
self._move(self.VALUE_STOP)
time.sleep(0.5)
def get_height_bounds(self) -> tuple[float, float]:
return (
self._unitToCm(int(self.est_value_bot)),
self._unitToCm(int(self.est_value_top)),
)
def get_height(self) -> float | None:
if self.est_value is None:
return None
else:
return self._unitToCm(self.est_value)
if __name__ == "__main__":
logging.basicConfig()
log = logging.getLogger(__name__)
desk = Desk()
serial = "000C-34E7"
# Configure the required parameters for the MQTT broker
mqtt_settings = ha_mqtt_discoverable.Settings.MQTT(host="192.168.7.53")
ndigits = 1
target_height: float | None = None
device_info = ha_mqtt_discoverable.DeviceInfo(
name="Desk",
identifiers=["Linak", serial],
manufacturer="Linak",
model="CBD4P",
suggested_area="Desk",
hw_version="77402",
sw_version="1.91",
serial_number=serial,
)
common_opts = {
"device": device_info,
"icon": "mdi:desk",
"unit_of_measurement": "cm",
"device_class": "distance",
"expire_after": 10,
}
# TODO Implement proper availability in hq-mqtt-discoverable
height_info = ha_mqtt_discoverable.sensors.NumberInfo(
name="Height ",
min=desk.HEIGHT_BOT,
max=desk.HEIGHT_TOP,
mode="slider",
step=10 ** (-ndigits),
unique_id="desk_height",
**common_opts,
)
height_settings = ha_mqtt_discoverable.Settings(
mqtt=mqtt_settings, entity=height_info
)
def height_callback(
client: paho.mqtt.client.Client,
user_data: None,
message: paho.mqtt.client.MQTTMessage,
) -> None:
global target_height
target_height = float(message.payload.decode())
log.info(f"Requested height to {target_height:.1f}")
height = ha_mqtt_discoverable.sensors.Number(
height_settings, height_callback
)
height_max_info = ha_mqtt_discoverable.sensors.SensorInfo(
name="Estimated height max",
unique_id="desk_height_max",
entity_category="diagnostic",
**common_opts,
)
height_max_settings = ha_mqtt_discoverable.Settings(
mqtt=mqtt_settings, entity=height_max_info
)
height_max = ha_mqtt_discoverable.sensors.Sensor(height_max_settings)
height_min_info = ha_mqtt_discoverable.sensors.SensorInfo(
name="Estimated height min",
unique_id="desk_height_min",
entity_category="diagnostic",
**common_opts,
)
height_min_settings = ha_mqtt_discoverable.Settings(
mqtt=mqtt_settings, entity=height_min_info
)
height_min = ha_mqtt_discoverable.sensors.Sensor(height_min_settings)
last_published_state = None
def fetch_callback(desk: Desk) -> None:
hcur = desk.get_height()
hmin, hmax = desk.get_height_bounds()
global last_published_state
state = hcur, hmin, hmax
if state == last_published_state:
return
last_published_state = state
# If none this will set as unknown
# Also readings can be a bit outside the boundaries,
# so this skips verification
if isinstance(hcur, float):
hcur = round(hcur, ndigits=ndigits)
height._update_state(hcur)
height_max._update_state(round(hmax, ndigits=ndigits))
height_min._update_state(round(hmin, ndigits=ndigits))
desk.fetch_callback = fetch_callback
interval = 0.2
# Need to be rective to catch
while True:
if target_height:
temp_target_height = target_height
# Allows queuing of other instructions while moving
target_height = None
desk.move_to(temp_target_height)
else:
time.sleep(interval)
desk.fetch()

View file

@ -1,29 +1,46 @@
{ passwordFile ? "/should_not_be_needed_in_this_context", ... }: {
pkgs,
lib,
config,
...
}:
# TODO Find a way to use keys in filesystem # TODO Find a way to use keys in filesystem
# TODO Not relatime everywhere, thank you # TODO Not relatime everywhere, thank you
# TODO Default options # TODO Default options
let let
btrfs_args_hdd = [ btrfs_args_ssd = [
"rw" "rw"
"relatime" "relatime"
"compress=zstd:3" "compress=zstd:3"
"space_cache" "space_cache"
"ssd"
]; ];
btrfs_args_ssd = btrfs_args_hdd ++ [ "ssd" ]; passwordFile = "/tmp/dotfiles_${config.frogeye.name}_password";
in in
{ {
disko.devices = { disko.devices = {
disk = { disk = {
razmo = { razmo = {
type = "disk"; type = "disk";
device = "/dev/disk/by-id/ata-ST1000LM048-2E7172_WKP8925H"; device = "/dev/disk/by-id/ata-SDLF1DAR-960G-1HA1_A027C1A3";
content = { content = {
type = "gpt"; type = "gpt";
partitions = { partitions = {
ESP = {
# Needs enough to store multiple kernel generations
size = "512M";
type = "EF00";
content = {
type = "filesystem";
format = "vfat";
mountpoint = "/boot";
mountOptions = [
"defaults"
];
};
};
swap = { swap = {
priority = 10; size = "8G";
start = "2048";
size = "6G";
content = { content = {
type = "swap"; type = "swap";
randomEncryption = true; randomEncryption = true;
@ -32,81 +49,35 @@ in
# hibernation image is saved. That's what I'm doing with Arch, # hibernation image is saved. That's what I'm doing with Arch,
# but I'm setting resume=, should test if it actually works? # but I'm setting resume=, should test if it actually works?
# Untranslated options from /etc/crypttab: swap,cipher=aes-xts-plain64,size=256 # Untranslated options from /etc/crypttab: swap,cipher=aes-xts-plain64,size=256
# Untranslated options from /etc/fstab: defaults,pri=100
}; };
}; };
nixosboot = { luks = {
priority = 15; size = "100%";
size = "2G";
content = {
type = "filesystem";
format = "vfat";
mountpoint = "/boot";
};
};
esp = {
priority = 20;
size = "128M";
type = "EF00"; # EFI system partition
content = {
type = "filesystem";
format = "vfat";
mountpoint = "/efi";
mountOptions = [
"rw"
"relatime"
"fmask=0022"
"dmask=0022"
"codepage=437"
"iocharset=iso8859-1"
"shortname=mixed"
"utf8"
"errors=remount-ro"
"noauto"
];
};
};
boot = {
priority = 30;
size = "128M";
content = {
type = "luks";
name = "boot";
extraFormatArgs = [ "--type luks1" ];
passwordFile = passwordFile;
settings = {
# keyFile = "/etc/keys/boot";
};
content = {
type = "filesystem";
format = "ext2";
mountpoint = "/mnt/old/boot";
mountOptions = [
"rw"
"relatime"
# "stripe=4" # For some reason doesn't work on NixOS
];
};
};
};
main = {
priority = 40;
content = { content = {
type = "luks"; type = "luks";
name = "razmo"; name = "razmo";
passwordFile = passwordFile; passwordFile = passwordFile;
settings = { settings = {
# keyFile = "/etc/keys/razmo"; allowDiscards = true;
}; };
content = { content = {
type = "btrfs"; type = "btrfs";
# extraArgs = [ "-f" ]; extraArgs = [ "-f" ];
mountpoint = "/mnt/razmo"; mountpoint = "/mnt/razmo";
mountOptions = btrfs_args_hdd;
subvolumes = { subvolumes = {
"home.razmo" = { "home.razmo" = {
mountpoint = "/home.heavy"; mountpoint = "/home.heavy";
mountOptions = btrfs_args_hdd; mountOptions = [
"compress=zstd"
"relatime"
];
};
"steam" = {
mountpoint = "/opt/steam.razmo";
mountOptions = [
"compress=zstd"
"noatime"
];
}; };
}; };
}; };
@ -158,10 +129,6 @@ in
mountpoint = "/mnt/rapido"; mountpoint = "/mnt/rapido";
mountOptions = btrfs_args_ssd; mountOptions = btrfs_args_ssd;
subvolumes = { subvolumes = {
archlinux = {
mountpoint = "/mnt/old";
mountOptions = btrfs_args_ssd;
};
# Should be temporary, to make sure we can revert to Arch anytime # Should be temporary, to make sure we can revert to Arch anytime
"home.nixos" = { "home.nixos" = {
mountpoint = "/home"; mountpoint = "/home";
@ -187,4 +154,12 @@ in
}; };
}; };
}; };
services.btrfs.autoScrub = {
enable = true;
fileSystems = [
"/mnt/razmo"
"/mnt/rapido"
];
# TODO Should be generable from disko config, right?
};
} }

View file

@ -1,17 +1,16 @@
{ ... }: { ... }:
{ {
frogeye = { frogeye = {
desktop.xorg = true; desktop = {
xorg = true;
};
dev = { dev = {
ansible = true;
c = true; c = true;
docker = true; docker = true;
fpga = true; vm = true;
perl = true;
php = true;
python = true;
}; };
extra = true; extra = true;
gaming = true; gaming = true;
storageSize = "big";
}; };
} }

View file

@ -1,17 +1,123 @@
{ lib, ... }:
{ {
imports = [ pkgs,
<nixos-hardware/dell/g3/3779> lib,
]; nixos-hardware,
...
# UEFI works here, and variables can be touched }:
boot.loader = { let
efi.canTouchEfiVariables = lib.mkDefault true; displays = {
grub = { embedded = {
enable = true; output = "eDP-1";
efiSupport = true; edid = "00ffffffffffff000dae381700000000011c01049526157802a155a556519d280b505400000001010101010101010101010101010101b43b804a71383440302035007dd61000001ac32f804a71383440302035007dd61000001a000000fe003059395747803137334843450a00000000000041319e001000000a010a2020004f";
device = "nodev"; # Don't install on MBR };
# TODO Maybe we could? In case the HDD doesn't boot anymore? deskLeft = {
output = "HDMI-1-3"; # Internal HDMI port
edid = "00ffffffffffff004c2d7b09333032302f160103803420782a01f1a257529f270a505423080081c0810081809500a9c0b300d1c00101283c80a070b023403020360006442100001a000000fd00353f1e5111000a202020202020000000fc00533234423432300a2020202020000000ff0048344d434230333533340a2020010702010400023a80d072382d40102c458006442100001e011d007251d01e206e28550006442100001e011d00bc52d01e20b828554006442100001e8c0ad090204031200c4055000644210000188c0ad08a20e02d10103e9600064421000018000000000000000000000000000000000000000000000000000000000000000000d2";
};
deskRight = {
output = "DVI-I-2-1"; # DisplayLink
edid = "00ffffffffffff004c2d7b093330323020160103803420782a01f1a257529f270a505423080081c0810081809500a9c0b300d1c00101283c80a070b023403020360006442100001a000000fd00353f1e5111000a202020202020000000fc00533234423432300a2020202020000000ff0048344d433830303836350a2020011c02010400023a80d072382d40102c458006442100001e011d007251d01e206e28550006442100001e011d00bc52d01e20b828554006442100001e8c0ad090204031200c4055000644210000188c0ad08a20e02d10103e9600064421000018000000000000000000000000000000000000000000000000000000000000000000d2";
}; };
}; };
in
{
config = {
boot = {
# From nixos-generate-config
initrd.availableKernelModules = [
"xhci_pci"
"ahci"
"nvme"
"usbhid"
"sd_mod"
"rtsx_usb_sdmmc"
];
kernelModules = [ "kvm-intel" ];
# UEFI works here, and variables can be touched
loader = {
efi.canTouchEfiVariables = lib.mkDefault true;
grub = {
enable = true;
efiSupport = true;
device = "nodev"; # Don't install on MBR
# TODO Maybe we could? In case the HDD doesn't boot anymore?
};
};
};
# Also from nixos-generate-config
hardware.enableRedistributableFirmware = true;
# TODO Do we really need that? Besides maybe microcode?
# AnnePro 2
hardware.keyboard.qmk.enable = true;
frogeye.desktop = {
x11_screens = [
displays.deskLeft.output
displays.deskRight.output
];
maxVideoHeight = 1440;
numlock = true;
phasesCommands = {
jour = ''
${pkgs.brightnessctl}/bin/brightnessctl set 40000 &
${pkgs.ddcutil}/bin/ddcutil setvcp 10 20 -d 1 &
${pkgs.ddcutil}/bin/ddcutil setvcp 10 20 -d 2 &
'';
crepuscule = ''
${pkgs.brightnessctl}/bin/brightnessctl set 10000 &
${pkgs.ddcutil}/bin/ddcutil setvcp 10 10 -d 1 &
${pkgs.ddcutil}/bin/ddcutil setvcp 10 10 -d 2 &
'';
nuit = ''
${pkgs.brightnessctl}/bin/brightnessctl set 1 &
${pkgs.ddcutil}/bin/ddcutil setvcp 10 0 -d 1 &
${pkgs.ddcutil}/bin/ddcutil setvcp 10 0 -d 2 &
'';
# TODO Display 2 doesn't work anymore?
};
};
services = {
autorandr = {
profiles = {
portable = {
fingerprint.${displays.embedded.output} = displays.embedded.edid;
config.${displays.embedded.output} = { };
};
extOnly = {
fingerprint = {
${displays.embedded.output} = displays.embedded.edid;
${displays.deskLeft.output} = displays.deskLeft.edid;
${displays.deskRight.output} = displays.deskRight.edid;
};
config = {
${displays.embedded.output}.enable = false;
${displays.deskLeft.output} = {
primary = true;
mode = "1920x1200";
rate = "59.95";
position = "0x0";
};
${displays.deskRight.output} = {
mode = "1920x1200";
rate = "59.95";
position = "1920x0";
};
};
};
# TODO leftOnly and other things.Might want to abstract a few things first.
};
};
# Needs prefetched binary blobs, see https://nixos.wiki/wiki/Displaylink
xserver.videoDrivers = [
"displaylink"
"modesetting"
];
# TODO See if nvidia and DL can work together.
};
};
imports = [
nixos-hardware.nixosModules.dell-g3-3779
];
} }

View file

@ -0,0 +1,19 @@
{
pkgs,
lib,
config,
...
}:
{
config = {
networking = {
# Allow mpd control from home assistant and phone
firewall.extraCommands = ''
iptables -A nixos-fw -p tcp -m tcp --dport 6600 -s 192.168.7.53 -j nixos-fw-accept
iptables -A nixos-fw -p tcp -m tcp --dport 6600 -s 192.168.7.92 -j nixos-fw-accept
'';
interfaces.enp3s0.wakeOnLan.enable = true;
};
services.tlp.settings.WOL_DISABLE = false;
};
}

View file

@ -1,22 +0,0 @@
{ ... }:
{
frogeye = {
desktop = {
xorg = true;
x11_screens = [ "HDMI-1-0" "eDP-1" ];
maxVideoHeight = 1440;
numlock = true;
phasesBrightness = {
enable = true;
jour = "40000";
crepuscule = "10000";
nuit = "1";
};
};
dev = {
docker = true;
};
extra = true;
gaming = true;
};
}

View file

@ -1,18 +0,0 @@
{ ... }:
{
imports = [
../os
./options.nix
./hardware.nix
./dk.nix
./backup
];
networking.hostName = "curacao";
boot = {
initrd.luks.reusePassphrases = true;
loader = {
efi.efiSysMountPoint = "/efi";
};
};
}

19
curacao/usb.nix Normal file
View file

@ -0,0 +1,19 @@
{
pkgs,
lib,
config,
...
}:
{
config = {
boot.loader.efi.canTouchEfiVariables = false;
disko.devices.disk."${config.frogeye.name
}".device = "/dev/disk/by-id/usb-Kingston_DataTraveler_3.0_E0D55EA57414F510489F0F1A-0:0";
frogeye.name = "curacao-usb";
};
imports = [
../common/disko/single_uefi_btrfs.nix
./features.nix
./hardware.nix
];
}

View file

@ -0,0 +1,19 @@
{
pkgs,
lib,
config,
...
}:
{
config = {
# TODO This should install cameractrls, but it seems like it's not easy to install.
# In the meantime, we install Flatpak and do:
# flatpak run hu.irl.cameractrls
services.flatpak.enable = true;
xdg.portal = {
config.common.default = "*";
enable = true;
extraPortals = [ pkgs.xdg-desktop-portal-gtk ];
};
};
}

View file

@ -1,12 +0,0 @@
{ ... }:
{
imports = [
../hm
../curacao/options.nix
];
home.username = "gnix";
home.homeDirectory = "/home/gnix";
frogeye.desktop.nixGLIntel = true;
}

View file

@ -1,2 +0,0 @@
{ ... } @ args:
import ../dk/single_uefi_btrfs.nix (args // { id = "usb-Kingston_DataTraveler_3.0_E0D55EA57414F510489F0F1A-0:0"; name = "curacao_usb"; })

View file

@ -1,22 +0,0 @@
{ pkgs, config, ... }:
{
imports = [
../os
../curacao/options.nix
../curacao/hardware.nix
./dk.nix
];
networking.hostName = "curacao_usb";
# It's a removable drive, so no touching EFI vars
# (quite a lot of stuff to set for that!)
boot.loader = {
efi.canTouchEfiVariables = false;
grub = {
efiInstallAsRemovable = true;
device = "nodev";
};
};
}

View file

@ -1,55 +0,0 @@
#!/usr/bin/env bash
# Runs the command given in a Nix environment, and create it if it doesn't exist.
# Useful for environments where nix isn't installed / you do not have root access
# If you need a fresh slate:
# chmod +w .nix -R
# rm -rf .nix .nix-defexpr .nix-profile .config/nix .local/state/nix .local/share/nix .cache/nix
set -euo pipefail
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
if [ ! -d /nix ]
then
# Doesn't support architectures other than x86_64
NIX_USER_CHROOT_URL=https://github.com/nix-community/nix-user-chroot/releases/download/1.2.2/nix-user-chroot-bin-1.2.2-x86_64-unknown-linux-musl
NIX_USER_CHROOT_SHA256SUM=e11aff604bb8d3ffd1d9c0c68cd636816d7eb8da540de18ee3a41ccad7ac0972
nix_user_chroot="$HOME/.local/bin/nix-user-chroot"
mkdir -p "$(dirname "$nix_user_chroot")"
nix_directory="$HOME/.nix"
mkdir -p "$nix_directory"
if [ ! -x "$nix_user_chroot" ] || ! echo "$NIX_USER_CHROOT_SHA256SUM $nix_user_chroot" | sha256sum --check --status
then
wget "$NIX_USER_CHROOT_URL" -O "$nix_user_chroot"
echo "$NIX_USER_CHROOT_SHA256SUM $nix_user_chroot" | sha256sum --check --status
chmod +x "$nix_user_chroot"
fi
exec "$nix_user_chroot" "$nix_directory" "$0" "$@"
exit 1
fi
nix_profile_path="$HOME/.nix-profile/etc/profile.d/nix.sh"
if [ ! -f "$nix_profile_path" ]
then
NIX_INSTALLER_URL=https://releases.nixos.org/nix/nix-2.19.2/install
NIX_INSTALLER_SHA256SUM=435f0d7e11f7c7dffeeab0ec9cc55723f6d3c03352379d785633cf4ddb5caf90
nix_installer="$(mktemp)"
wget "$NIX_INSTALLER_URL" -O "$nix_installer"
echo "$NIX_INSTALLER_SHA256SUM $nix_installer" | sha256sum --check --status
chmod +x "$nix_installer"
"$nix_installer" --no-daemon --yes --no-channel-add --no-modify-profile
fi
. "$nix_profile_path"
"${SCRIPT_DIR}/add_channels.sh"
exec "$@"

962
flake.lock Normal file
View file

@ -0,0 +1,962 @@
{
"nodes": {
"base16": {
"inputs": {
"fromYaml": "fromYaml"
},
"locked": {
"lastModified": 1732200724,
"narHash": "sha256-+R1BH5wHhfnycySb7Sy5KbYEaTJZWm1h+LW1OtyhiTs=",
"owner": "SenchoPens",
"repo": "base16.nix",
"rev": "153d52373b0fb2d343592871009a286ec8837aec",
"type": "github"
},
"original": {
"owner": "SenchoPens",
"repo": "base16.nix",
"type": "github"
}
},
"base16-fish": {
"flake": false,
"locked": {
"lastModified": 1622559957,
"narHash": "sha256-PebymhVYbL8trDVVXxCvZgc0S5VxI7I1Hv4RMSquTpA=",
"owner": "tomyun",
"repo": "base16-fish",
"rev": "2f6dd973a9075dabccd26f1cded09508180bf5fe",
"type": "github"
},
"original": {
"owner": "tomyun",
"repo": "base16-fish",
"type": "github"
}
},
"base16-helix": {
"flake": false,
"locked": {
"lastModified": 1725860795,
"narHash": "sha256-Z2o8VBPW3I+KKTSfe25kskz0EUj7MpUh8u355Z1nVsU=",
"owner": "tinted-theming",
"repo": "base16-helix",
"rev": "7f795bf75d38e0eea9fed287264067ca187b88a9",
"type": "github"
},
"original": {
"owner": "tinted-theming",
"repo": "base16-helix",
"type": "github"
}
},
"base16-vim": {
"flake": false,
"locked": {
"lastModified": 1731949548,
"narHash": "sha256-XIDexXM66sSh5j/x70e054BnUsviibUShW7XhbDGhYo=",
"owner": "tinted-theming",
"repo": "base16-vim",
"rev": "61165b1632409bd55e530f3dbdd4477f011cadc6",
"type": "github"
},
"original": {
"owner": "tinted-theming",
"repo": "base16-vim",
"type": "github"
}
},
"devshell": {
"inputs": {
"nixpkgs": [
"nixvim",
"nixpkgs"
]
},
"locked": {
"lastModified": 1735644329,
"narHash": "sha256-tO3HrHriyLvipc4xr+Ewtdlo7wM1OjXNjlWRgmM7peY=",
"owner": "numtide",
"repo": "devshell",
"rev": "f7795ede5b02664b57035b3b757876703e2c3eac",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "devshell",
"type": "github"
}
},
"disko": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1737038063,
"narHash": "sha256-rMEuiK69MDhjz1JgbaeQ9mBDXMJ2/P8vmOYRbFndXsk=",
"owner": "nix-community",
"repo": "disko",
"rev": "bf0abfde48f469c256f2b0f481c6281ff04a5db2",
"type": "github"
},
"original": {
"id": "disko",
"type": "indirect"
}
},
"firefox-gnome-theme": {
"flake": false,
"locked": {
"lastModified": 1734969791,
"narHash": "sha256-A9PxLienMYJ/WUvqFie9qXrNC2MeRRYw7TG/q7DRjZg=",
"owner": "rafaelmardojai",
"repo": "firefox-gnome-theme",
"rev": "92f4890bd150fc9d97b61b3583680c0524a8cafe",
"type": "github"
},
"original": {
"owner": "rafaelmardojai",
"repo": "firefox-gnome-theme",
"type": "github"
}
},
"flake-compat": {
"locked": {
"lastModified": 1696426674,
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
"revCount": 57,
"type": "tarball",
"url": "https://api.flakehub.com/f/pinned/edolstra/flake-compat/1.0.1/018afb31-abd1-7bff-a5e4-cff7e18efb7a/source.tar.gz"
},
"original": {
"type": "tarball",
"url": "https://flakehub.com/f/edolstra/flake-compat/1.tar.gz"
}
},
"flake-compat_2": {
"flake": false,
"locked": {
"lastModified": 1696426674,
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flake-parts": {
"inputs": {
"nixpkgs-lib": [
"nixvim",
"nixpkgs"
]
},
"locked": {
"lastModified": 1736143030,
"narHash": "sha256-+hu54pAoLDEZT9pjHlqL9DNzWz0NbUn8NEAHP7PQPzU=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "b905f6fc23a9051a6e1b741e1438dbfc0634c6de",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"flake-parts_2": {
"inputs": {
"nixpkgs-lib": [
"nur",
"nixpkgs"
]
},
"locked": {
"lastModified": 1733312601,
"narHash": "sha256-4pDvzqnegAfRkPwO3wmwBhVi/Sye1mzps0zHWYnP88c=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "205b12d8b7cd4802fbcb8e8ef6a0f1408781a4f9",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_2": {
"inputs": {
"systems": "systems_2"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flake-utils_3": {
"inputs": {
"systems": [
"stylix",
"systems"
]
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"fromYaml": {
"flake": false,
"locked": {
"lastModified": 1731966426,
"narHash": "sha256-lq95WydhbUTWig/JpqiB7oViTcHFP8Lv41IGtayokA8=",
"owner": "SenchoPens",
"repo": "fromYaml",
"rev": "106af9e2f715e2d828df706c386a685698f3223b",
"type": "github"
},
"original": {
"owner": "SenchoPens",
"repo": "fromYaml",
"type": "github"
}
},
"git-hooks": {
"inputs": {
"flake-compat": [
"nixvim",
"flake-compat"
],
"gitignore": "gitignore",
"nixpkgs": [
"nixvim",
"nixpkgs"
]
},
"locked": {
"lastModified": 1737043064,
"narHash": "sha256-I/OuxGwXwRi5gnFPsyCvVR+IfFstA+QXEpHu1hvsgD8=",
"owner": "cachix",
"repo": "git-hooks.nix",
"rev": "94ee657f6032d913fe0ef49adaa743804635b0bb",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "git-hooks.nix",
"type": "github"
}
},
"git-hooks_2": {
"inputs": {
"flake-compat": [
"stylix",
"flake-compat"
],
"gitignore": "gitignore_2",
"nixpkgs": [
"stylix",
"nixpkgs"
],
"nixpkgs-stable": [
"stylix",
"git-hooks",
"nixpkgs"
]
},
"locked": {
"lastModified": 1731363552,
"narHash": "sha256-vFta1uHnD29VUY4HJOO/D6p6rxyObnf+InnSMT4jlMU=",
"owner": "cachix",
"repo": "git-hooks.nix",
"rev": "cd1af27aa85026ac759d5d3fccf650abe7e1bbf0",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "git-hooks.nix",
"type": "github"
}
},
"gitignore": {
"inputs": {
"nixpkgs": [
"nixvim",
"git-hooks",
"nixpkgs"
]
},
"locked": {
"lastModified": 1709087332,
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
"owner": "hercules-ci",
"repo": "gitignore.nix",
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "gitignore.nix",
"type": "github"
}
},
"gitignore_2": {
"inputs": {
"nixpkgs": [
"stylix",
"git-hooks",
"nixpkgs"
]
},
"locked": {
"lastModified": 1709087332,
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
"owner": "hercules-ci",
"repo": "gitignore.nix",
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "gitignore.nix",
"type": "github"
}
},
"gnome-shell": {
"flake": false,
"locked": {
"lastModified": 1732369855,
"narHash": "sha256-JhUWbcYPjHO3Xs3x9/Z9RuqXbcp5yhPluGjwsdE2GMg=",
"owner": "GNOME",
"repo": "gnome-shell",
"rev": "dadd58f630eeea41d645ee225a63f719390829dc",
"type": "github"
},
"original": {
"owner": "GNOME",
"ref": "47.2",
"repo": "gnome-shell",
"type": "github"
}
},
"home-manager": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1736373539,
"narHash": "sha256-dinzAqCjenWDxuy+MqUQq0I4zUSfaCvN9rzuCmgMZJY=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "bd65bc3cde04c16755955630b344bc9e35272c56",
"type": "github"
},
"original": {
"id": "home-manager",
"ref": "release-24.11",
"type": "indirect"
}
},
"home-manager_2": {
"inputs": {
"nixpkgs": [
"nixvim",
"nixpkgs"
]
},
"locked": {
"lastModified": 1736373539,
"narHash": "sha256-dinzAqCjenWDxuy+MqUQq0I4zUSfaCvN9rzuCmgMZJY=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "bd65bc3cde04c16755955630b344bc9e35272c56",
"type": "github"
},
"original": {
"owner": "nix-community",
"ref": "release-24.11",
"repo": "home-manager",
"type": "github"
}
},
"home-manager_3": {
"inputs": {
"nixpkgs": [
"stylix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1733572789,
"narHash": "sha256-zjO6m5BqxXIyjrnUziAzk4+T4VleqjstNudSqWcpsHI=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "c7ffc9727d115e433fd884a62dc164b587ff651d",
"type": "github"
},
"original": {
"owner": "nix-community",
"ref": "release-24.11",
"repo": "home-manager",
"type": "github"
}
},
"ixx": {
"inputs": {
"flake-utils": [
"nixvim",
"nuschtosSearch",
"flake-utils"
],
"nixpkgs": [
"nixvim",
"nuschtosSearch",
"nixpkgs"
]
},
"locked": {
"lastModified": 1729958008,
"narHash": "sha256-EiOq8jF4Z/zQe0QYVc3+qSKxRK//CFHMB84aYrYGwEs=",
"owner": "NuschtOS",
"repo": "ixx",
"rev": "9fd01aad037f345350eab2cd45e1946cc66da4eb",
"type": "github"
},
"original": {
"owner": "NuschtOS",
"ref": "v0.0.6",
"repo": "ixx",
"type": "github"
}
},
"nix-darwin": {
"inputs": {
"nixpkgs": [
"nixvim",
"nixpkgs"
]
},
"locked": {
"lastModified": 1736820923,
"narHash": "sha256-SDuKLOWAh8VJRXlNWQn9QE99bjeEUAAbYXqrKGbsiyk=",
"owner": "lnl7",
"repo": "nix-darwin",
"rev": "944c2b181792ae7ae6b20c0df3f44879c11706c9",
"type": "github"
},
"original": {
"owner": "lnl7",
"ref": "nix-darwin-24.11",
"repo": "nix-darwin",
"type": "github"
}
},
"nix-formatter-pack": {
"inputs": {
"nixpkgs": [
"nix-on-droid",
"nixpkgs"
],
"nmd": [
"nix-on-droid",
"nmd"
],
"nmt": "nmt"
},
"locked": {
"lastModified": 1705252799,
"narHash": "sha256-HgSTREh7VoXjGgNDwKQUYcYo13rPkltW7IitHrTPA5c=",
"owner": "Gerschtli",
"repo": "nix-formatter-pack",
"rev": "2de39dedd79aab14c01b9e2934842051a160ffa5",
"type": "github"
},
"original": {
"owner": "Gerschtli",
"repo": "nix-formatter-pack",
"type": "github"
}
},
"nix-on-droid": {
"inputs": {
"home-manager": [
"home-manager"
],
"nix-formatter-pack": "nix-formatter-pack",
"nixpkgs": [
"nixpkgs"
],
"nixpkgs-docs": "nixpkgs-docs",
"nixpkgs-for-bootstrap": "nixpkgs-for-bootstrap",
"nmd": "nmd"
},
"locked": {
"lastModified": 1725658585,
"narHash": "sha256-P29z4Gt89n5ps1U7+qmIrj0BuRXGZQSIaOe2+tsPgfw=",
"owner": "nix-community",
"repo": "nix-on-droid",
"rev": "5d88ff2519e4952f8d22472b52c531bb5f1635fc",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nix-on-droid",
"type": "github"
}
},
"nixos-hardware": {
"locked": {
"lastModified": 1737590910,
"narHash": "sha256-qM/y6Dtpu9Wmf5HqeZajQdn+cS0aljdYQQQnrvx+LJE=",
"owner": "NixOS",
"repo": "nixos-hardware",
"rev": "9368027715d8dde4b84c79c374948b5306fdd2db",
"type": "github"
},
"original": {
"id": "nixos-hardware",
"type": "indirect"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1737569578,
"narHash": "sha256-6qY0pk2QmUtBT9Mywdvif0i/CLVgpCjMUn6g9vB+f3M=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "47addd76727f42d351590c905d9d1905ca895b82",
"type": "github"
},
"original": {
"id": "nixpkgs",
"ref": "nixos-24.11",
"type": "indirect"
}
},
"nixpkgs-docs": {
"locked": {
"lastModified": 1705957679,
"narHash": "sha256-Q8LJaVZGJ9wo33wBafvZSzapYsjOaNjP/pOnSiKVGHY=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "9a333eaa80901efe01df07eade2c16d183761fa3",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "release-23.05",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-for-bootstrap": {
"locked": {
"lastModified": 1720244366,
"narHash": "sha256-WrDV0FPMVd2Sq9hkR5LNHudS3OSMmUrs90JUTN+MXpA=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "49ee0e94463abada1de470c9c07bfc12b36dcf40",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "49ee0e94463abada1de470c9c07bfc12b36dcf40",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1737469691,
"narHash": "sha256-nmKOgAU48S41dTPIXAq0AHZSehWUn6ZPrUKijHAMmIk=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "9e4d5190a9482a1fb9d18adf0bdb83c6e506eaab",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixvim": {
"inputs": {
"devshell": "devshell",
"flake-compat": "flake-compat",
"flake-parts": "flake-parts",
"git-hooks": "git-hooks",
"home-manager": "home-manager_2",
"nix-darwin": "nix-darwin",
"nixpkgs": [
"nixpkgs"
],
"nuschtosSearch": "nuschtosSearch",
"treefmt-nix": "treefmt-nix"
},
"locked": {
"lastModified": 1737283156,
"narHash": "sha256-FyHmM6vvz+UxCrPZo/poIaZBZejLHVKkAH4cjtUxZDA=",
"owner": "nix-community",
"repo": "nixvim",
"rev": "abcbd250b8a2c7aab1f4b2b9e01598ee24b42337",
"type": "github"
},
"original": {
"owner": "nix-community",
"ref": "nixos-24.11",
"repo": "nixvim",
"type": "github"
}
},
"nmd": {
"inputs": {
"nixpkgs": [
"nix-on-droid",
"nixpkgs-docs"
],
"scss-reset": "scss-reset"
},
"locked": {
"lastModified": 1705050560,
"narHash": "sha256-x3zzcdvhJpodsmdjqB4t5mkVW22V3wqHLOun0KRBzUI=",
"owner": "~rycee",
"repo": "nmd",
"rev": "66d9334933119c36f91a78d565c152a4fdc8d3d3",
"type": "sourcehut"
},
"original": {
"owner": "~rycee",
"repo": "nmd",
"type": "sourcehut"
}
},
"nmt": {
"flake": false,
"locked": {
"lastModified": 1648075362,
"narHash": "sha256-u36WgzoA84dMVsGXzml4wZ5ckGgfnvS0ryzo/3zn/Pc=",
"owner": "rycee",
"repo": "nmt",
"rev": "d83601002c99b78c89ea80e5e6ba21addcfe12ae",
"type": "gitlab"
},
"original": {
"owner": "rycee",
"repo": "nmt",
"type": "gitlab"
}
},
"nur": {
"inputs": {
"flake-parts": "flake-parts_2",
"nixpkgs": "nixpkgs_2",
"treefmt-nix": "treefmt-nix_2"
},
"locked": {
"lastModified": 1737643235,
"narHash": "sha256-wv3JCT3vfYUodDmBbRxtOxkWxMQ605K9viJ1AmZlU7I=",
"owner": "nix-community",
"repo": "NUR",
"rev": "2eb191a4ca7354482dca8a55ce4fa73b986f9617",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "NUR",
"type": "github"
}
},
"nuschtosSearch": {
"inputs": {
"flake-utils": "flake-utils_2",
"ixx": "ixx",
"nixpkgs": [
"nixvim",
"nixpkgs"
]
},
"locked": {
"lastModified": 1735854821,
"narHash": "sha256-Iv59gMDZajNfezTO0Fw6LHE7uKAShxbvMidmZREit7c=",
"owner": "NuschtOS",
"repo": "search",
"rev": "836908e3bddd837ae0f13e215dd48767aee355f0",
"type": "github"
},
"original": {
"owner": "NuschtOS",
"repo": "search",
"type": "github"
}
},
"root": {
"inputs": {
"disko": "disko",
"flake-utils": "flake-utils",
"home-manager": "home-manager",
"nix-on-droid": "nix-on-droid",
"nixos-hardware": "nixos-hardware",
"nixpkgs": "nixpkgs",
"nixvim": "nixvim",
"nur": "nur",
"stylix": "stylix",
"unixpkgs": "unixpkgs"
}
},
"scss-reset": {
"flake": false,
"locked": {
"lastModified": 1631450058,
"narHash": "sha256-muDlZJPtXDIGevSEWkicPP0HQ6VtucbkMNygpGlBEUM=",
"owner": "andreymatin",
"repo": "scss-reset",
"rev": "0cf50e27a4e95e9bb5b1715eedf9c54dee1a5a91",
"type": "github"
},
"original": {
"owner": "andreymatin",
"repo": "scss-reset",
"type": "github"
}
},
"stylix": {
"inputs": {
"base16": "base16",
"base16-fish": "base16-fish",
"base16-helix": "base16-helix",
"base16-vim": "base16-vim",
"firefox-gnome-theme": "firefox-gnome-theme",
"flake-compat": "flake-compat_2",
"flake-utils": "flake-utils_3",
"git-hooks": "git-hooks_2",
"gnome-shell": "gnome-shell",
"home-manager": "home-manager_3",
"nixpkgs": [
"nixpkgs"
],
"systems": "systems_3",
"tinted-foot": "tinted-foot",
"tinted-kitty": "tinted-kitty",
"tinted-tmux": "tinted-tmux"
},
"locked": {
"lastModified": 1737585583,
"narHash": "sha256-lU00TEdqCCWJNtMbR0l779xRJHtMf5FKCGKcsC+/Hr8=",
"owner": "danth",
"repo": "stylix",
"rev": "9409ae8a925aacc7ea9a794e0bde159b876572a3",
"type": "github"
},
"original": {
"owner": "danth",
"ref": "release-24.11",
"repo": "stylix",
"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"
}
},
"systems_2": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"systems_3": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"tinted-foot": {
"flake": false,
"locked": {
"lastModified": 1726913040,
"narHash": "sha256-+eDZPkw7efMNUf3/Pv0EmsidqdwNJ1TaOum6k7lngDQ=",
"owner": "tinted-theming",
"repo": "tinted-foot",
"rev": "fd1b924b6c45c3e4465e8a849e67ea82933fcbe4",
"type": "github"
},
"original": {
"owner": "tinted-theming",
"repo": "tinted-foot",
"rev": "fd1b924b6c45c3e4465e8a849e67ea82933fcbe4",
"type": "github"
}
},
"tinted-kitty": {
"flake": false,
"locked": {
"lastModified": 1716423189,
"narHash": "sha256-2xF3sH7UIwegn+2gKzMpFi3pk5DlIlM18+vj17Uf82U=",
"owner": "tinted-theming",
"repo": "tinted-kitty",
"rev": "eb39e141db14baef052893285df9f266df041ff8",
"type": "github"
},
"original": {
"owner": "tinted-theming",
"repo": "tinted-kitty",
"rev": "eb39e141db14baef052893285df9f266df041ff8",
"type": "github"
}
},
"tinted-tmux": {
"flake": false,
"locked": {
"lastModified": 1729501581,
"narHash": "sha256-1ohEFMC23elnl39kxWnjzH1l2DFWWx4DhFNNYDTYt54=",
"owner": "tinted-theming",
"repo": "tinted-tmux",
"rev": "f0e7f7974a6441033eb0a172a0342e96722b4f14",
"type": "github"
},
"original": {
"owner": "tinted-theming",
"repo": "tinted-tmux",
"type": "github"
}
},
"treefmt-nix": {
"inputs": {
"nixpkgs": [
"nixvim",
"nixpkgs"
]
},
"locked": {
"lastModified": 1737103437,
"narHash": "sha256-uPNWcYbhY2fjY3HOfRCR5jsfzdzemhfxLSxwjXYXqNc=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "d1ed3b385f8130e392870cfb1dbfaff8a63a1899",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "treefmt-nix",
"type": "github"
}
},
"treefmt-nix_2": {
"inputs": {
"nixpkgs": [
"nur",
"nixpkgs"
]
},
"locked": {
"lastModified": 1733222881,
"narHash": "sha256-JIPcz1PrpXUCbaccEnrcUS8jjEb/1vJbZz5KkobyFdM=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "49717b5af6f80172275d47a418c9719a31a78b53",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "treefmt-nix",
"type": "github"
}
},
"unixpkgs": {
"locked": {
"lastModified": 1737644226,
"narHash": "sha256-75r2eJS7PKc+ZCdaIeF0PKQ9JM3w0YQvfzfsQfJq6EY=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "09b24484a773a6632f0234b10a33a300ceed73dc",
"type": "github"
},
"original": {
"id": "nixpkgs",
"ref": "master",
"type": "indirect"
}
}
},
"root": "root",
"version": 7
}

227
flake.nix Normal file
View file

@ -0,0 +1,227 @@
{
description = "Geoffrey Frogeye's base configurations";
inputs = {
# Packages
nixpkgs.url = "nixpkgs/nixos-24.11";
unixpkgs.url = "nixpkgs/master";
# OS
disko = {
url = "disko";
inputs.nixpkgs.follows = "nixpkgs";
};
nixos-hardware.url = "nixos-hardware";
# NOD
nix-on-droid = {
url = "github:nix-community/nix-on-droid"; # No 24.11 yet
inputs.nixpkgs.follows = "nixpkgs";
inputs.home-manager.follows = "home-manager";
};
# HM
home-manager = {
url = "home-manager/release-24.11";
inputs.nixpkgs.follows = "nixpkgs";
};
stylix = {
url = "github:danth/stylix/release-24.11";
inputs.nixpkgs.follows = "nixpkgs";
};
nixvim = {
url = "github:nix-community/nixvim/nixos-24.11";
inputs.nixpkgs.follows = "nixpkgs";
};
nur.url = "github:nix-community/NUR";
# Local
flake-utils.url = "github:numtide/flake-utils";
};
outputs =
{
self,
nixpkgs,
unixpkgs,
disko,
nix-on-droid,
flake-utils,
nur,
...
}@attrs:
# Machine independant outputs
let
nixpkgsConfig = {
config = {
allowUnfree = true;
# TODO Handpick exceptions
};
overlays = [
(import ./common/update-local-flakes/overlay.nix)
nur.overlays.default
(
# Cherry-pick packages from future
self: super:
let
upkgs = import unixpkgs { inherit (super) system; };
in
{
jjui = upkgs.jjui;
labelle = upkgs.labelle;
}
)
];
};
homeManagerConfig = {
sharedModules = [ self.homeManagerModules.dotfiles ];
extraSpecialArgs = attrs;
};
lib = {
nixosSystem =
{
system,
modules ? [ ],
}:
nixpkgs.lib.nixosSystem {
inherit system;
specialArgs = attrs // {
upkgs = import unixpkgs {
inherit system;
};
};
modules = modules ++ [
self.nixosModules.dotfiles
# nur.modules.nixos.default
{
nixpkgs = nixpkgsConfig;
home-manager = homeManagerConfig;
frogeye.toplevel = {
_type = "override";
content = self;
priority = 1000;
};
}
];
};
nixOnDroidConfiguration =
{
modules ? [ ],
}:
nix-on-droid.lib.nixOnDroidConfiguration {
pkgs = import nixpkgs (
nixpkgsConfig
// {
system = "aarch64-linux"; # nod doesn't support anything else
}
);
modules = modules ++ [
self.nixOnDroidModules.dotfiles
{
home-manager = homeManagerConfig;
}
];
};
flakeTools =
{ self }:
flake-utils.lib.eachDefaultSystem (
system:
let
pkgs = import nixpkgs (
nixpkgsConfig
// {
inherit system;
# We do an overlay here so nixos-rebuild and other use lix.
# We don't do an overlay for the whole system because lix is not binary compatible.
overlays = [
(self: super: { nix = super.lix; })
];
}
);
in
{
apps = {
disko = {
type = "app";
program = "${disko.packages.${system}.default}/bin/disko";
};
nixos-install = {
type = "app";
program = "${pkgs.nixos-install-tools}/bin/nixos-install";
};
nixos-rebuild = {
type = "app";
program = "${pkgs.nixos-rebuild}/bin/nixos-rebuild";
};
repl = {
type = "app";
program = "${pkgs.writeShellScript "vivarium-repl" ''
${pkgs.lix}/bin/nix repl --expr 'let flake = builtins.getFlake "${self}"; in flake // flake.nixosConfigurations // rec { pkgs = import ${nixpkgs} {}; lib = pkgs.lib; }'
''}";
};
# Available globally should this be needed in times of shenanigans
updateLocalFlakes = {
type = "app";
program = "${pkgs.update-local-flakes}/bin/update-local-flakes";
};
nixosRebuild = {
type = "app";
program = "${pkgs.writeShellScript "rebuild" ''${
pkgs.writeShellApplication {
name = "rebuild";
runtimeInputs = with pkgs; [
nix-output-monitor
nixos-rebuild
jq
];
text = builtins.readFile ./os/rebuild.sh;
}
}/bin/rebuild ${self} "$@"''}";
};
};
formatter = pkgs.nixfmt-rfc-style;
}
);
};
in
{
# Reusable configurations
inherit lib;
nixosModules.dotfiles.imports = [ ./os ];
nixOnDroidModules.dotfiles.imports = [ ./nod ];
homeManagerModules.dotfiles.imports = [ ./hm ];
# Actual configurations
nixosConfigurations.curacao = lib.nixosSystem {
system = "x86_64-linux";
modules = [ ./curacao ];
};
nixosConfigurations.curacao-usb = lib.nixosSystem {
system = "x86_64-linux";
modules = [ ./curacao/usb.nix ];
};
nixosConfigurations.pindakaas = lib.nixosSystem {
system = "aarch64-linux";
modules = [ ./pindakaas ];
};
nixosConfigurations.pindakaas-sd = lib.nixosSystem {
system = "aarch64-linux";
modules = [ ./pindakaas/sd.nix ];
};
nixosConfigurations.cranberry = lib.nixosSystem {
system = "x86_64-linux";
modules = [ ./cranberry ];
};
nixOnDroidConfigurations.sprinkles = lib.nixOnDroidConfiguration { };
# Fake systems
nixosConfigurations.abavorana = lib.nixosSystem {
system = "x86_64-linux";
modules = [ ./abavorana/standin.nix ];
};
nixosConfigurations.ludwig = lib.nixosSystem {
system = "x86_64-linux";
modules = [ ./ludwig/standin.nix ];
};
nixosConfigurations.sprinkles = lib.nixosSystem {
system = "aarch64-linux";
modules = [ ./sprinkles/standin.nix ];
};
# TODO devices/ or configs/ folders
}
// (lib.flakeTools { inherit self; });
}

View file

@ -1,6 +0,0 @@
# full profile
Fake configuration that contains everything I could ever need,
used for debugging.
Can't build a full system due to not having a filesystem / bootloader configuration,
build as a VM (without bootloader).

View file

@ -1,10 +0,0 @@
{ ... }:
{
imports = [
../os
./options.nix
];
# Create a different disk image depending on the architecture
networking.hostName = "${builtins.currentSystem}";
}

127
hm/accounts/default.nix Normal file
View file

@ -0,0 +1,127 @@
{
pkgs,
config,
lib,
...
}:
let
mkUserJs =
with lib;
prefs: extraPrefs: ''
// Generated by Geoffrey's dotfiles.
${concatStrings (
mapAttrsToList (name: value: ''
user_pref("${name}", ${builtins.toJSON value});
'') prefs
)}
${extraPrefs}
'';
toThunderbirdCalendar =
account:
let
id = builtins.hashString "sha256" account.name;
thunderbird = config.frogeye.accounts.calendar.accounts.${account.name};
in
{
"calendar.registry.${id}.cache.enabled" = thunderbird.offlineSupport; # TODO Check this actually corresponds
"calendar.registry.${id}.color" = thunderbird.color;
"calendar.registry.${id}.forceEmailScheduling" = thunderbird.clientSideEmailScheduling;
"calendar.registry.${id}.imip.identity.key" = "id_${builtins.hashString "sha256" thunderbird.email}";
"calendar.registry.${id}.name" = account.name;
"calendar.registry.${id}.readOnly" = thunderbird.readOnly;
"calendar.registry.${id}.refreshInterval" = builtins.toString thunderbird.refreshInterval;
"calendar.registry.${id}.suppressAlarms" = !thunderbird.showReminders; # TODO Check this actually corresponds
"calendar.registry.${id}.type" = account.remote.type; # TODO Check and validate supported types
"calendar.registry.${id}.uri" = account.remote.url;
"calendar.registry.${id}.username" = account.remote.userName;
# Unimplemented
"calendar.registry.${id}.notifications.times" = "";
# Unknown
# "calendar.registry.${id}.calendar-main-in-composite" = true;
};
in
{
config = {
programs.aerc = {
enable = true;
extraConfig.general.unsafe-accounts-conf = true;
};
programs.thunderbird = {
enable = config.frogeye.desktop.xorg;
profiles.hm = {
isDefault = true;
withExternalGnupg = true;
extraConfig = mkUserJs (lib.attrsets.mergeAttrsList (
# Add calendar config
(lib.mapAttrsToList (
name: account: (toThunderbirdCalendar account)
) config.accounts.calendar.accounts)
++
# Add config for every identity (kinda)
(lib.mapAttrsToList (name: account: ({
# UPST Make signature be used in Thunderbird
"mail.identity.id_${builtins.hashString "sha256" account.address}.htmlSigText" =
account.signature.text;
"mail.identity.id_${builtins.hashString "sha256" account.address}.compose_html" = false;
})) config.accounts.email.accounts)
++
# General settings
[
{
"mail.pane_config.dynamic" = 0;
"intl.date_time.pattern_override.date_short" = "yyyy-MM-dd";
}
]
)) "";
};
};
};
# UPST Thunderbird-specific options (should be named so), to be included in HM Thunderbird module
options = {
frogeye.accounts.calendar.accounts = lib.mkOption {
default = { };
type = lib.types.attrsOf (
lib.types.submodule (
{ config, name, ... }:
{
# TODO Set defaults as Thunderbird sets it
options = {
color = lib.mkOption {
type = lib.types.str;
default = "#5277c3";
};
refreshInterval = lib.mkOption {
type = lib.types.int;
default = 0; # 0 = Manual
};
readOnly = lib.mkOption {
type = lib.types.bool;
default = false;
};
showReminders = lib.mkOption {
type = lib.types.bool;
default = true;
};
offlineSupport = lib.mkOption {
type = lib.types.bool;
default = true;
};
email = lib.mkOption {
type = lib.types.str;
# TODO Nullable
# TODO Ensure it actually matches an email identity
};
clientSideEmailScheduling = lib.mkOption {
type = lib.types.bool;
default = false;
};
};
}
)
);
};
};
}

53
hm/brightness/default.nix Normal file
View file

@ -0,0 +1,53 @@
# Light theme during the day, dark theme during the night (not automatic)
{
pkgs,
lib,
config,
...
}:
let
phases = [
{
command = "jour";
specialisation = null;
}
{
command = "crepuscule";
specialisation = "dark";
}
{
command = "nuit";
specialisation = "dark";
}
];
mod = config.xsession.windowManager.i3.config.modifier;
in
{
config = {
home.packages =
(map (
phase:
(pkgs.writeShellScriptBin phase.command ''
switch="/nix/var/nix/profiles/system${
lib.strings.optionalString (phase.specialisation != null) "/specialisation/${phase.specialisation}"
}/bin/switch-to-configuration"
if [ -x "$switch" ]
then
sudo "$switch" test &
sudo "$switch" boot &
fi
${builtins.getAttr phase.command config.frogeye.desktop.phasesCommands}
wait
'')
) phases)
++ (with pkgs; [
brightnessctl
]);
xsession.windowManager.i3.config.keybindings = {
XF86MonBrightnessUp = "exec ${pkgs.brightnessctl}/bin/brightnessctl set +5%";
XF86MonBrightnessDown = "exec ${pkgs.brightnessctl}/bin/brightnessctl set 5%-";
"${mod}+F6" = "exec ${pkgs.brightnessctl}/bin/brightnessctl set 1%-";
"${mod}+F7" = "exec ${pkgs.brightnessctl}/bin/brightnessctl set +1%";
};
};
}

View file

@ -1,389 +1,178 @@
{ pkgs, config, lib, ... }:
let
direnv = {
# Environment variables making programs stay out of $HOME, but also needing we create a directory for them
CARGOHOME = "${config.xdg.cacheHome}/cargo"; # There are config in there that we can version if one want
CCACHE_DIR = "${config.xdg.cacheHome}/ccache"; # The config file alone seems to be not enough
DASHT_DOCSETS_DIR = "${config.xdg.cacheHome}/dash_docsets";
GOPATH = "${config.xdg.cacheHome}/go";
GRADLE_USER_HOME = "${config.xdg.cacheHome}/gradle";
MIX_ARCHIVES = "${config.xdg.cacheHome}/mix/archives";
MONO_GAC_PREFIX = "${config.xdg.cacheHome}/mono";
npm_config_cache = "${config.xdg.cacheHome}/npm";
PARALLEL_HOME = "${config.xdg.cacheHome}/parallel";
TERMINFO = "${config.xdg.configHome}/terminfo";
WINEPREFIX = "${config.xdg.stateHome}/wineprefix/default";
YARN_CACHE_FOLDER = "${config.xdg.cacheHome}/yarn";
# TODO Some of that stuff is not really relavant any more
};
in
{ {
pkgs,
nixpkgs.config.allowUnfree = true; config,
lib,
programs = ...
let }:
commonRc = lib.strings.concatLines ([ {
'' frogeye.hooks.lock = ''
# Colored ls ${pkgs.coreutils}/bin/rm -rf "/tmp/cached_pass_$UID"
# TODO Doesn't allow completion. Check out lsd instead '';
_colored_ls() { programs = {
${pkgs.coreutils}/bin/ls -lh --color=always $@ | ${pkgs.gawk}/bin/awk ' home-manager.enable = true;
BEGIN { bat = {
FPAT = "([[:space:]]*[^[:space:]]+)"; enable = true;
OFS = ""; config.style = "full";
}
{
$1 = "\033[36m" $1 "\033[0m";
$2 = "\033[31m" $2 "\033[0m";
$3 = "\033[32m" $3 "\033[0m";
$4 = "\033[32m" $4 "\033[0m";
$5 = "\033[31m" $5 "\033[0m";
$6 = "\033[34m" $6 "\033[0m";
$7 = "\033[34m" $7 "\033[0m";
print
}
'
}
alias ll="_colored_ls"
alias la="_colored_ls -a"
''
] ++ map (d: "mkdir -p ${d}") (builtins.attrValues direnv));
# TODO Those directory creations should probably done on home-manager activation
commonSessionVariables = {
TIME_STYLE = "+%Y-%m-%d %H:%M:%S";
# Less colors
LESS = "-R";
LESS_TERMCAP_mb = "$(echo $'\\E[1;31m')"; # begin blink
LESS_TERMCAP_md = "$(echo $'\\E[1;36m')"; # begin bold
LESS_TERMCAP_me = "$(echo $'\\E[0m')"; # reset bold/blink
LESS_TERMCAP_so = "$(echo $'\\E[01;44;33m')"; # begin reverse video
LESS_TERMCAP_se = "$(echo $'\\E[0m')"; # reset reverse video
LESS_TERMCAP_us = "$(echo $'\\E[1;32m')"; # begin underline
LESS_TERMCAP_ue = "$(echo $'\\E[0m')"; # reset underline
# Fzf
FZF_COMPLETION_OPTS = "${lib.strings.concatStringsSep " " config.programs.fzf.fileWidgetOptions}";
};
treatsHomeAsJunk = [
# Programs that think $HOME is a reasonable place to put their junk
# and don't allow the user to change those questionable choices
"adb"
"audacity"
"binwalk" # Should use .config according to the GitHub code though
"cabal" # TODO May have options but last time I tried it it crashed
"cmake"
"ddd"
"ghidra"
"itch"
"simplescreenrecorder" # Easy fix https://github.com/MaartenBaert/ssr/blob/1556ae456e833992fb6d39d40f7c7d7c337a4160/src/Main.cpp#L252
"vd"
"wpa_cli"
# TODO Maybe we can do something about node-gyp
];
commonShellAliases = {
# Completion for existing commands
ls = "ls -h --color=auto";
mkdir = "mkdir -v";
# cp = "cp -i"; # Disabled because conflicts with the ZSH/Bash one. This separation is confusing I swear.
mv = "mv -iv";
free = "free -h";
df = "df -h";
ffmpeg = "ffmpeg -hide_banner";
ffprobe = "ffprobe -hide_banner";
ffplay = "ffplay -hide_banner";
# TODO Add ipython --no-confirm-exit --pdb
# Frequent mistakes
sl = "ls";
al = "la";
mdkir = "mkdir";
systemclt = "systemctl";
please = "sudo";
# Shortcuts for commonly used commands
# ll = "ls -l"; # Disabled because would overwrite the colored one
# la = "ls -la"; # Eh maybe it's not that bad, but for now let's keep compatibility
s = "sudo -s -E";
# Give additional config to those programs, and not have them in my path
bower = "bower --config.storage.packages=${config.xdg.cacheHome}/bower/packages --config.storage.registry=${config.xdg.cacheHome}/bower/registry --config.storage.links=${config.xdg.cacheHome}/bower/links";
gdb = "gdb -x ${config.xdg.configHome}/gdbinit";
iftop = "iftop -c ${config.xdg.configHome}/iftoprc";
lmms = "lmms --config ${config.xdg.configHome}/lmmsrc.xml";
# Preference
vi = "nvim";
vim = "nvim";
wol = "wakeonlan"; # TODO Really, isn't wol better? Also wtf Arch aliases to pass because neither is installed anyways x)
mutt = "neomutt";
# Bash/Zsh only
cp = "cp -i --reflink=auto";
grep = "grep --color=auto";
dd = "dd status=progress";
rm = "rm -v --one-file-system";
# free = "free -m"; # Disabled because... no? Why?
diff = "diff --color=auto";
dmesg = "dmesg --ctime";
wget = "wget --hsts-file ${config.xdg.cacheHome}/wget-hsts";
# Imported from scripts
rms = ''${pkgs.findutils}/bin/find . -name "*.sync-conflict-*" -delete''; # Remove syncthing conflict files
pw = ''${pkgs.pwgen}/bin/pwgen 32 -y''; # Generate passwords. ln((26*2+10)**32)/ln(2) ≅ 190 bits of entropy
newestFile = ''${pkgs.findutils}/bin/find -type f -printf '%T+ %p\n' | sort | tail'';
oldestFile = ''${pkgs.findutils}/bin/find -type f -printf '%T+ %p\n' | sort | head'';
tracefiles = ''${pkgs.strace}/bin/strace -f -t -e trace=file'';
} // lib.attrsets.mergeAttrsList (map (p: { "${p}" = "HOME=${config.xdg.cacheHome}/junkhome ${p}"; }) treatsHomeAsJunk);
# TODO Maybe make nixpkg wrapper instead? So it also works from dmenu
# Could also accept my fate... Home-manager doesn't necessarily make it easy to put things out of the home directory
historySize = 100000;
historyFile = "${config.xdg.stateHome}/shell_history";
in
{
home-manager.enable = true;
bash = {
enable = true;
bashrcExtra = lib.strings.concatLines [
commonRc
''
shopt -s expand_aliases
shopt -s histappend
''
];
sessionVariables = commonSessionVariables;
historySize = historySize;
historyFile = historyFile;
historyFileSize = historySize;
historyControl = [ "erasedups" "ignoredups" "ignorespace" ];
shellAliases = commonShellAliases // config.frogeye.shellAliases;
};
zsh = {
enable = true;
enableAutosuggestions = true;
enableCompletion = true;
syntaxHighlighting.enable = true;
historySubstringSearch.enable = true;
initExtra = lib.strings.concatLines [
commonRc
(builtins.readFile ./zshrc.sh)
];
defaultKeymap = "viins";
history = {
size = historySize;
save = historySize;
path = historyFile;
expireDuplicatesFirst = true;
};
sessionVariables = commonSessionVariables;
shellAliases = commonShellAliases // config.frogeye.shellAliases;
};
dircolors = {
enable = true;
enableBashIntegration = true;
enableZshIntegration = true;
# UPST This thing put stuff in .dircolors when it actually doesn't have to
};
powerline-go = {
enable = true;
modules = [ "user" "host" "venv" "cwd" "perms" "git" ];
modulesRight = [ "jobs" "exit" "duration" "load" ];
settings = {
colorize-hostname = true;
max-width = 25;
cwd-max-dir-size = 10;
duration = "$( test -n \"$__TIMER\" && echo $(( $EPOCHREALTIME - $\{__TIMER:-EPOCHREALTIME})) || echo 0 )";
# UPST Implement this properly in home-manager, would allow for bash support
};
extraUpdatePS1 = ''
unset __TIMER
echo -en "\033]0; $USER@$HOST $PWD\007"
'';
};
gpg = {
enable = true;
homedir = "${config.xdg.stateHome}/gnupg";
settings = {
# Remove fluff
no-greeting = true;
no-emit-version = true;
no-comments = true;
# Output format that I prefer
keyid-format = "0xlong";
# Show fingerprints
with-fingerprint = true;
# Make sure to show if key is invalid
# (should be default on most platform,
# but just to be sure)
list-options = "show-uid-validity";
verify-options = "show-uid-validity";
# Stronger algorithm (https://wiki.archlinux.org/title/GnuPG#Different_algorithm)
personal-digest-preferences = "SHA512";
cert-digest-algo = "SHA512";
default-preference-list = "SHA512 SHA384 SHA256 SHA224 AES256 AES192 AES CAST5 ZLIB BZIP2 ZIP Uncompressed";
personal-cipher-preferences = "TWOFISH CAMELLIA256 AES 3DES";
};
publicKeys = [{
source = builtins.fetchurl {
url = "https://keys.openpgp.org/vks/v1/by-fingerprint/4FBA930D314A03215E2CDB0A8312C8CAC1BAC289";
sha256 = "sha256:10y9xqcy1vyk2p8baay14p3vwdnlwynk0fvfbika65hz2z8yw2cm";
};
trust = "ultimate";
}];
};
fzf = {
enable = true;
enableZshIntegration = true;
defaultOptions = [ "--height 40%" "--layout=default" ];
fileWidgetOptions = [ "--preview '[[ -d {} ]] && ${pkgs.coreutils}/bin/ls -l --color=always {} || [[ \$(${pkgs.file}/bin/file --mime {}) =~ binary ]] && ${pkgs.file}/bin/file --brief {} || (${pkgs.highlight}/bin/highlight -O ansi -l {} || coderay {} || rougify {} || ${pkgs.coreutils}/bin/cat {}) 2> /dev/null | head -500'" ];
# file and friends are not in PATH by default... so here we want aboslute paths, which means those won't get reloaded. Meh.
};
# TODO highlight or bat
nix-index = {
enable = false; # TODO Index is impossible to generate, should use https://github.com/nix-community/nix-index-database
# but got no luck without flakes
enableZshIntegration = true;
};
less.enable = true;
git = {
enable = true;
package = pkgs.gitFull;
aliases = {
"git" = "!exec git"; # In case I write one too many git
};
ignores = [
"*.swp"
"*.swo"
"*.ycm_extra_conf.py"
"tags"
".mypy_cache"
];
lfs.enable = true;
userEmail = lib.mkDefault "geoffrey@frogeye.fr";
userName = lib.mkDefault "Geoffrey Frogeye";
extraConfig = {
core = {
editor = "nvim";
};
push = {
default = "matching";
};
pull = {
ff = "only";
};
} // lib.optionalAttrs config.frogeye.desktop.xorg {
diff.tool = "meld";
difftool.prompt = false;
"difftool \"meld\"".cmd = "${pkgs.meld}/bin/meld \"$LOCAL\" \"$REMOTE\"";
# This escapes quotes, which isn't the case in the original, hoping this isn't an issue.
};
# TODO Delta syntax highlighter... and other cool-looking options?
};
readline = {
enable = true;
variables = {
"bell-style" = "none";
"colored-completion-prefix" = true;
"colored-stats" = true;
"completion-ignore-case" = true;
"completion-query-items" = 200;
"editing-mode" = "vi";
"history-preserve-point" = true;
"history-size" = 10000;
"horizontal-scroll-mode" = false;
"mark-directories" = true;
"mark-modified-lines" = false;
"mark-symlinked-directories" = true;
"match-hidden-files" = true;
"menu-complete-display-prefix" = true;
"page-completions" = true;
"print-completions-horizontally" = false;
"revert-all-at-newline" = false;
"show-all-if-ambiguous" = true;
"show-all-if-unmodified" = true;
"show-mode-in-prompt" = true;
"skip-completed-text" = true;
"visible-stats" = false;
};
extraConfig = builtins.readFile ./inputrc;
};
tmux =
let
themepack = pkgs.tmuxPlugins.mkTmuxPlugin
rec {
pluginName = "tmux-themepack";
version = "1.1.0";
rtpFilePath = "themepack.tmux";
src = pkgs.fetchFromGitHub {
owner = "jimeh";
repo = "tmux-themepack";
rev = "${version}";
sha256 = "f6y92kYsKDFanNx5ATx4BkaB/E7UrmyIHU/5Z01otQE=";
};
};
in
{
enable = true;
mouse = false;
clock24 = true;
# TODO Vim mode?
plugins = with pkgs.tmuxPlugins; [
sensible
];
extraConfig = builtins.readFile ./tmux.conf + "source-file ${themepack}/share/tmux-plugins/tmux-themepack/powerline/default/green.tmuxtheme\n";
};
translate-shell.enable = true; # TODO Cool config?
password-store.enable = true;
}; };
services = { bash.shellAliases = {
gpg-agent = { # Replacement commands
enable = true; # TODO Consider not enabling it when not having any private key # ls = "lsd"; # lsd is suuuper slow for large directories
cat = "bat -pp";
# Completion for existing commands
mkdir = "mkdir -v";
# cp = "cp -i"; # Disabled because conflicts with the ZSH/Bash one. This separation is confusing I swear.
mv = "mv -iv";
free = "free -h";
df = "df -h";
ffmpeg = "ffmpeg -hide_banner";
ffprobe = "ffprobe -hide_banner";
ffplay = "ffplay -hide_banner";
numbat = "numbat --intro-banner off";
insect = "numbat";
# Frequent mistakes
sl = "ls";
al = "la";
mdkir = "mkdir";
systemclt = "systemctl";
please = "sudo";
# Shortcuts for commonly used commands
ll = "lsd -l";
la = "lsd -la";
s = "sudo -s -E";
# Preference
wol = "wakeonlan"; # TODO Really, isn't wol better? Also wtf Arch aliases to pass because neither is installed anyways x)
mutt = "neomutt";
# Bash/Zsh only
cp = "cp -i --reflink=auto";
grep = "grep --color=auto";
dd = "dd status=progress";
rm = "rm -v --one-file-system";
# free = "free -m"; # Disabled because... no? Why?
diff = "diff --color=auto";
dmesg = "dmesg --ctime";
wget = "wget --hsts-file ${config.xdg.cacheHome}/wget-hsts";
# Imported from scripts
rms = ''${pkgs.findutils}/bin/find . -name "*.sync-conflict-*" -delete''; # Remove syncthing conflict files
newestFile = ''${pkgs.findutils}/bin/find -type f -printf '%T+ %p\n' | sort | tail'';
oldestFile = ''${pkgs.findutils}/bin/find -type f -printf '%T+ %p\n' | sort | head'';
};
thefuck = {
enable = true;
enableBashIntegration = true; enableBashIntegration = true;
enableZshIntegration = true; enableZshIntegration = true;
pinentryFlavor = "gtk2"; # Falls back to curses when needed
}; };
# TODO Syncs a bit too often, also constantly asks for passphrase, which is annoying. lsd = {
git-sync = { enable = true;
enable = false; settings = {
repositories = { size = "short";
dotfiles = { };
path = "${config.xdg.configHome}/dotfiles"; colors = {
uri = lib.mkDefault "https://git.frogeye.fr/geoffrey/dotfiles.git"; # Base16 only, so it reuses the current theme.
date = {
day-old = 4;
hour-old = 6;
older = 5;
}; };
git-status = {
conflicted = 14;
default = 13;
deleted = 1;
ignored = 13;
modified = 3;
new-in-index = 2;
new-in-workdir = 2;
renamed = 4;
typechange = 3;
unmodified = 13;
};
group = 6;
inode = {
invalid = 245;
valid = 13;
};
links = {
invalid = 9;
valid = 14;
};
permission = {
acl = 6;
context = 14;
exec = 1;
exec-sticky = 5;
no-access = 245;
octal = 6;
read = 2;
write = 3;
};
size = {
large = 1;
medium = 9;
none = 11;
small = 3;
};
tree-edge = 13;
user = 2;
}; };
}; };
}; dircolors = {
xdg = { enable = true;
configFile = { enableBashIntegration = true;
"ccache.conf" = { enableZshIntegration = true;
text = "ccache_dir = ${config.xdg.cacheHome}/ccache"; # UPST This thing put stuff in .dircolors when it actually doesn't have to
};
"gdbinit" = {
text = ''
define hook-quit
set confirm off
end
'';
};
"iftoprc" = {
text = ''
port-resolution: no
promiscuous: no
port-display: on
link-local: yes
use-bytes: yes
show-totals: yes
log-scale: yes
'';
};
"pythonstartup.py" = {
text = (builtins.readFile ./pythonstartup.py);
};
"screenrc" = {
text = (builtins.readFile ./screenrc);
};
}; };
git.enable = true;
gpg.enable = true;
fzf = {
enable = true;
enableZshIntegration = true;
defaultOptions = [
"--height 40%"
"--layout=default"
];
fileWidgetOptions = [
"--preview '[[ -d {} ]] && ${pkgs.coreutils}/bin/ls -l --color=always {} || [[ \$(${pkgs.file}/bin/file --mime {}) =~ binary ]] && ${pkgs.file}/bin/file --brief {} || (${pkgs.highlight}/bin/highlight -O ansi -l {} || coderay {} || rougify {} || ${pkgs.coreutils}/bin/cat {}) 2> /dev/null | head -500'"
];
# TODO Above not working... not really used either?
# file and friends are not in PATH by default... so here we want aboslute paths, which means those won't get reloaded. Meh.
};
less.enable = true;
nixvim.enable = true;
readline = {
enable = true;
variables = {
"bell-style" = "none";
"colored-completion-prefix" = true;
"colored-stats" = true;
"completion-ignore-case" = true;
"completion-query-items" = 200;
"editing-mode" = "vi";
"history-preserve-point" = true;
"history-size" = 10000;
"horizontal-scroll-mode" = false;
"mark-directories" = true;
"mark-modified-lines" = false;
"mark-symlinked-directories" = true;
"match-hidden-files" = true;
"menu-complete-display-prefix" = true;
"page-completions" = true;
"print-completions-horizontally" = false;
"revert-all-at-newline" = false;
"show-all-if-ambiguous" = true;
"show-all-if-unmodified" = true;
"show-mode-in-prompt" = true;
"skip-completed-text" = true;
"visible-stats" = false;
};
extraConfig = builtins.readFile ./inputrc;
};
tmux.enable = true;
translate-shell.enable = true; # TODO Cool config?
}; };
home = { home = {
activation = { activation = {
@ -395,137 +184,86 @@ in
fi fi
''; '';
}; };
stateVersion = "23.11"; stateVersion = "24.05";
language = {
base = "en_US.UTF-8";
# time = "en_DK.UTF-8"; # TODO Disabled because complaints during nixos-rebuild switch
};
packages = with pkgs; [ packages = with pkgs; [
# dotfiles dependencies # Terminal utils
coreutils coreutils-full
bash moreutils
gnugrep
gnused
gnutar
openssl
wget
curl
python3Packages.pip
rename rename
which which
# shell
zsh-completions
nix-zsh-completions
zsh-history-substring-search
powerline-go
neofetch
# nix utils
nix-diff
nix-tree
nix-output-monitor
# terminal essentials
file file
moreutils cached-nix-shell # For scripts
man
# Pipe utils
gnugrep
gnused
gawk
# Extraction
gnutar
unzip unzip
unrar unrar
p7zip p7zip
# Documentation
man
tldr
neofetch
# remote # remote
wget
curl
openssl
openssh openssh
rsync rsync
borgbackup borgbackup
sshfs
# cleanup # cleanup
ncdu ncdu
jdupes jdupes
duperemove duperemove
compsize
btdu
# local monitoring # toolbox
htop
iotop
iftop
lsof
strace
pv
progress
speedtest-cli
# multimedia toolbox
sox
imagemagick imagemagick
numbat
bc
# password # hardware
pwgen pciutils
usbutils
dmidecode
lshw
labelle # Label printer
# Locker
(pkgs.writeShellApplication { (pkgs.writeShellApplication {
name = "git-sync-init"; name = "lock";
# runtimeInputs = with pkgs; [ coreutils libnotify ]; text = ''
text = (lib.strings.concatLines ${config.frogeye.hooks.lock}
(map (r: ''[ -d "${r.path}" ] || ${pkgs.git}/bin/git clone "${r.uri}" "${r.path}"'')
(lib.attrsets.attrValues config.services.git-sync.repositories) ${pkgs.vlock}/bin/vlock --all
) '';
);
}) })
# Mail
isync
msmtp
notmuch
neomutt
lynx
# Organisation
vdirsyncer
khard
khal
todoman
# TODO Lots of redundancy with other way things are defined here
] ++ lib.optionals pkgs.stdenv.isx86_64 [
nodePackages.insect
# TODO Use whatever replaces insect, hopefully that works on aarch64
]; ];
sessionVariables = { sessionVariables = {
# Favourite commands # Favourite commands
PAGER = "less";
EDITOR = "nvim";
# Extra config # Extra config
BOOT9_PATH = "${config.xdg.dataHome}/citra-emu/sysdata/boot9.bin";
CCACHE_CONFIGPATH = "${config.xdg.configHome}/ccache.conf";
# INPUTRC = "${config.xdg.configHome}/inputrc"; # UPST Will use programs.readline, but doesn't allow path setting # INPUTRC = "${config.xdg.configHome}/inputrc"; # UPST Will use programs.readline, but doesn't allow path setting
LESSHISTFILE = "${config.xdg.stateHome}/lesshst";
NODE_REPL_HISTORY = "${config.xdg.cacheHome}/node_repl_history";
PYTHONSTARTUP = "${config.xdg.configHome}/pythonstartup.py";
# TODO I think we're not using the urxvt daemon on purpose?
# TODO this should be desktop only, as a few things are too.
SCREENRC = "${config.xdg.configHome}/screenrc";
SQLITE_HISTFILE = "${config.xdg.stateHome}/sqlite_history"; SQLITE_HISTFILE = "${config.xdg.stateHome}/sqlite_history";
YARN_DISABLE_SELF_UPDATE_CHECK = "true"; # This also disable the creation of a ~/.yarnrc file
} // lib.optionalAttrs config.frogeye.desktop.xorg {
# Favourite commands
VISUAL = "nvim";
BROWSER = "${config.programs.qutebrowser.package}/bin/qutebrowser";
# Extra config # Bash/ZSH only?
RXVT_SOCKET = "${config.xdg.stateHome}/urxvtd"; # Used to want -$HOME suffix, hopefullt this isn't needed TIME_STYLE = "+%Y-%m-%d %H:%M:%S";
# XAUTHORITY = "${config.xdg.configHome}/Xauthority"; # Disabled as this causes lock-ups with DMs # Fzf
} // direnv; FZF_COMPLETION_OPTS = "${lib.strings.concatStringsSep " " config.programs.fzf.fileWidgetOptions}";
# TODO Session variables only get reloaded on login I think. };
sessionPath = [ sessionPath = [
"${config.home.homeDirectory}/.local/bin" "${config.home.homeDirectory}/.local/bin"
"${config.home.sessionVariables.GOPATH}" "${config.home.homeDirectory}/.config/dotfiles/hm/scripts" # Not Nix path otherwise it gets converted into store,
(builtins.toString ./scripts) # and then every time you want to modify a script you have to rebuild and re-login...
]; ];
file = {
".face" = { # TODO Doesn't show on NixOS. See https://wiki.archlinux.org/title/LightDM#Changing_your_avatar ?
source = pkgs.runCommand "face.png" { } "${pkgs.inkscape}/bin/inkscape ${./face.svg} -w 1024 -o $out";
};
};
# FIXME .config/home-manager/home.nix link. Using hostname?
}; };
} }

View file

@ -1,15 +1,28 @@
{ ... }: { ... }:
{ {
imports = [ imports = [
../common/frogarized
../options.nix ../options.nix
./accounts
./brightness
./common.nix ./common.nix
./desktop.nix ./desktop
./dev.nix ./dev
./extra.nix ./extra
./gaming ./gaming
./git
./gpg
./homealone.nix
./monitoring
./nix
./pager
./password
./prompt
./rebuild
./shell
./ssh.nix ./ssh.nix
./style.nix ./theme
./usernix ./tmux
./vim.nix ./vim
]; ];
} }

View file

@ -1,706 +0,0 @@
{ pkgs, config, lib, ... }:
let
nixgl = import
(builtins.fetchGit {
url = "https://github.com/nix-community/nixGL";
rev = "489d6b095ab9d289fe11af0219a9ff00fe87c7c5";
})
{ };
nixGLIntelPrefix = "${nixgl.nixVulkanIntel}/bin/nixVulkanIntel ${nixgl.nixGLIntel}/bin/nixGLIntel ";
wmPrefix = "${lib.optionalString config.frogeye.desktop.nixGLIntel nixGLIntelPrefix}";
in
{
imports = [
./frobar
];
config = lib.mkIf config.frogeye.desktop.xorg {
frogeye.shellAliases = {
noise = ''${pkgs.sox}/bin/play -c 2 -n synth $'' + ''{1}noise'';
beep = ''${pkgs.sox}/bin/play -n synth sine E5 sine A4 remix 1-2 fade 0.5 1.2 0.5 2> /dev/null'';
# n = "$HOME/.config/i3/terminal & disown"; # Not used anymore since alacritty daemon mode doesn't preserve environment variables
x = "startx ${config.home.homeDirectory}/${config.xsession.scriptPath}; logout";
# TODO Is it possible to not start nvidia stuff on nixOS?
# nx = "nvidia-xrun ${config.xsession.scriptPath}; sudo systemctl start nvidia-xrun-pm; logout";
};
xsession = {
enable = true;
# Not using config.xdg.configHome because it needs to be $HOME-relative paths and path manipulation is hard
scriptPath = ".config/xsession";
profilePath = ".config/xprofile";
windowManager = {
command = lib.mkForce "${wmPrefix} ${config.xsession.windowManager.i3.package}/bin/i3";
i3 = {
enable = true;
config =
let
# lockColors = with config.lib.stylix.colors.withHashtag; { a = base00; b = base01; d = base00; }; # Black or White, depending on current theme
# lockColors = with config.lib.stylix.colors.withHashtag; { a = base0A; b = base0B; d = base00; }; # Green + Yellow
lockColors = { a = "#82a401"; b = "#466c01"; d = "#648901"; }; # Old
lockSvg = pkgs.writeText "lock.svg" "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 50 50\" height=\"50\" width=\"50\"><path fill=\"${lockColors.a}\" d=\"M0 50h50V0H0z\"/><path d=\"M0 0l50 50H25L0 25zm50 0v25L25 0z\" fill=\"${lockColors.b}\"/></svg>";
lockPng = pkgs.runCommand "lock.png" { } "${pkgs.imagemagick}/bin/convert ${lockSvg} $out";
locker = pkgs.writeShellScript "i3-locker"
''
# Remove SSH and GPG keys from keystores
${pkgs.openssh}/bin/ssh-add -D
echo RELOADAGENT | ${pkgs.gnupg}/bin/gpg-connect-agent
${pkgs.coreutils}/bin/rm -rf "/tmp/cached_pass_$UID"
${pkgs.lightdm}/bin/dm-tool lock
# TODO Does that work for all DMs?
# TODO Might want to use i3lock on NixOS configs still?
if [ $? -ne 0 ]; then
if [ -d ${config.xdg.cacheHome}/lockpatterns ]
then
pattern=$(${pkgs.findutils} ${config.xdg.cacheHome}/lockpatterns | sort -R | head -1)
else
pattern=${lockPng}
fi
revert() {
${pkgs.xorg.xset}/bin/xset dpms 0 0 0
}
trap revert SIGHUP SIGINT SIGTERM
${pkgs.xorg.xset}/bin/xset dpms 5 5 5
${pkgs.i3lock}/bin/i3lock --nofork --color ${builtins.substring 1 6 lockColors.d} --image=$pattern --tiling --ignore-empty-password
revert
fi
'';
focus = "exec ${ pkgs.writeShellScript "i3-focus-window"
''
WINDOW=`${pkgs.xdotool}/bin/xdotool getwindowfocus`
eval `${pkgs.xdotool}/bin/xdotool getwindowgeometry --shell $WINDOW` # this brings in variables WIDTH and HEIGHT
TX=`${pkgs.coreutils}/bin/expr $WIDTH / 2`
TY=`${pkgs.coreutils}/bin/expr $HEIGHT / 2`
${pkgs.xdotool}/bin/xdotool mousemove -window $WINDOW $TX $TY
''
}";
mode_system = "[L] Vérouillage [E] Déconnexion [S] Veille [H] Hibernation [R] Redémarrage [P] Extinction";
mode_resize = "Resize";
mode_pres_main = "Presentation (main display)";
mode_pres_sec = "Presentation (secondary display)";
mode_screen = "Screen setup [A] Auto [L] Load [S] Save [R] Remove [D] Default";
mode_temp = "Temperature [R] Red [D] Dust storm [C] Campfire [O] Normal [A] All nighter [B] Blue";
fonts = config.stylix.fonts;
in
{
modifier = "Mod4";
fonts = {
names = [ fonts.sansSerif.name ];
};
terminal = "alacritty";
colors = let ignore = "#ff00ff"; in
with config.lib.stylix.colors.withHashtag; lib.mkForce {
focused = { border = base0B; background = base0B; text = base00; indicator = base00; childBorder = base0B; };
focusedInactive = { border = base02; background = base02; text = base05; indicator = base02; childBorder = base02; };
unfocused = { border = base05; background = base04; text = base00; indicator = base04; childBorder = base00; };
urgent = { border = base0F; background = base08; text = base00; indicator = base08; childBorder = base0F; };
placeholder = { border = ignore; background = base00; text = base05; indicator = ignore; childBorder = base00; };
background = base07;
# I set the color of the active tab as the the background color of the terminal so they merge together.
};
focus.followMouse = false;
keybindings =
let
mod = config.xsession.windowManager.i3.config.modifier;
rofi = "exec --no-startup-id ${config.programs.rofi.package}/bin/rofi";
pactl = "exec ${pkgs.pulseaudio}/bin/pactl"; # TODO Use NixOS package if using NixOS
screenshots_dir = config.xdg.userDirs.extraConfig.XDG_SCREENSHOTS_DIR;
scrot = "${pkgs.scrot}/bin/scrot --exec '${pkgs.coreutils}/bin/mv $f ${screenshots_dir}/ && ${pkgs.optipng}/bin/optipng ${screenshots_dir}/$f'";
in
{
# Compatibility layer for people coming from other backgrounds
"Mod1+Tab" = "${rofi} -modi window -show window";
"Mod1+F2" = "${rofi} -modi drun -show drun";
"Mod1+F4" = "kill";
# kill focused window
"${mod}+z" = "kill";
button2 = "kill";
# Rofi
"${mod}+c" = "exec --no-startup-id ${config.programs.rofi.pass.package}/bin/rofi-pass --last-used";
# TODO Try autopass.cr
# 23.11 config.programs.rofi.pass.package
"${mod}+i" = "exec --no-startup-id ${pkgs.rofimoji}/bin/rofimoji";
"${mod}+plus" = "${rofi} -modi ssh -show ssh";
"${mod}+ù" = "${rofi} -modi ssh -show ssh -ssh-command '{terminal} -e {ssh-client} {host} -t \"sudo -s -E\"'";
# TODO In which keyboard layout?
"${mod}+Tab" = "${rofi} -modi window -show window";
# start program launcher
"${mod}+d" = "${rofi} -modi run -show run";
"${mod}+Shift+d" = "${rofi} -modi drun -show drun";
# Start Applications
"${mod}+Return" = "exec ${
pkgs.writeShellScript "terminal" "${config.programs.alacritty.package}/bin/alacritty msg create-window || exec ${config.programs.alacritty.package}/bin/alacritty -e zsh"
# -e zsh is for systems where I can't configure my user's shell
# TODO Is a shell script even required?
}";
"${mod}+Shift+Return" = "exec ${config.programs.urxvt.package}/bin/urxvt";
"${mod}+p" = "exec ${pkgs.xfce.thunar}/bin/thunar";
"${mod}+m" = "exec ${config.programs.qutebrowser.package}/bin/qutebrowser --override-restore --backend=webengine";
# TODO --backend not useful anymore
# Volume control
"XF86AudioRaiseVolume" = "${pactl} set-sink-mute @DEFAULT_SINK@ false; ${pactl} set-sink-volume @DEFAULT_SINK@ +5%";
"XF86AudioLowerVolume" = "${pactl} set-sink-mute @DEFAULT_SINK@ false; ${pactl} set-sink-volume @DEFAULT_SINK@ -5%";
"XF86AudioMute" = "${pactl} set-sink-mute @DEFAULT_SINK@ true";
"${mod}+F7" = "${pactl} suspend-sink @DEFAULT_SINK@ 1; ${pactl} suspend-sink @DEFAULT_SINK@ 0"; # Re-synchronize bluetooth headset
"${mod}+F11" = "exec ${pkgs.pavucontrol}/bin/pavucontrol";
"${mod}+F12" = "exec ${pkgs.pavucontrol}/bin/pavucontrol";
# TODO Find pacmixer?
# Media control
"XF86AudioPrev" = "exec ${pkgs.mpc-cli}/bin/mpc prev";
"XF86AudioPlay" = "exec ${pkgs.mpc-cli}/bin/mpc toggle";
"XF86AudioNext" = "exec ${pkgs.mpc-cli}/bin/mpc next";
# Backlight
"XF86MonBrightnessUp" = "exec ${pkgs.brightnessctl}/bin/brightnessctl set +5%";
"XF86MonBrightnessDown" = "exec ${pkgs.brightnessctl}/bin/brightnessctl set 5%-";
# Misc
"${mod}+F10" = "exec ${ pkgs.writeShellScript "show-keyboard-layout"
''
layout=`${pkgs.xorg.setxkbmap}/bin/setxkbmap -query | ${pkgs.gnugrep}/bin/grep ^layout: | ${pkgs.gawk}/bin/awk '{ print $2 }'`
${pkgs.libgnomekbd}/bin/gkbd-keyboard-display -l $layout
''
}";
# Screenshots
"Print" = "exec ${scrot} --focused";
"${mod}+Print" = "exec ${scrot}";
"Ctrl+Print" = "exec ${pkgs.coreutils}/bin/sleep 1 && ${scrot} --select";
# TODO Try using bindsym --release instead of sleep
# change focus
"${mod}+h" = "focus left; ${focus}";
"${mod}+j" = "focus down; ${focus}";
"${mod}+k" = "focus up; ${focus}";
"${mod}+l" = "focus right; ${focus}";
# move focused window
"${mod}+Shift+h" = "move left; ${focus}";
"${mod}+Shift+j" = "move down; ${focus}";
"${mod}+Shift+k" = "move up; ${focus}";
"${mod}+Shift+l" = "move right; ${focus}";
# workspace back and forth (with/without active container)
"${mod}+b" = "workspace back_and_forth; ${focus}";
"${mod}+Shift+b" = "move container to workspace back_and_forth; workspace back_and_forth; ${focus}";
# Change container layout
"${mod}+g" = "split h; ${focus}";
"${mod}+v" = "split v; ${focus}";
"${mod}+f" = "fullscreen toggle; ${focus}";
"${mod}+s" = "layout stacking; ${focus}";
"${mod}+w" = "layout tabbed; ${focus}";
"${mod}+e" = "layout toggle split; ${focus}";
"${mod}+Shift+space" = "floating toggle; ${focus}";
# Focus container
"${mod}+space" = "focus mode_toggle; ${focus}";
"${mod}+a" = "focus parent; ${focus}";
"${mod}+q" = "focus child; ${focus}";
# Switch to workspace
"${mod}+1" = "workspace 1; ${focus}";
"${mod}+2" = "workspace 2; ${focus}";
"${mod}+3" = "workspace 3; ${focus}";
"${mod}+4" = "workspace 4; ${focus}";
"${mod}+5" = "workspace 5; ${focus}";
"${mod}+6" = "workspace 6; ${focus}";
"${mod}+7" = "workspace 7; ${focus}";
"${mod}+8" = "workspace 8; ${focus}";
"${mod}+9" = "workspace 9; ${focus}";
"${mod}+0" = "workspace 10; ${focus}";
# TODO Prevent repetitions, see workspace assignation for example
#navigate workspaces next / previous
"${mod}+Ctrl+h" = "workspace prev_on_output; ${focus}";
"${mod}+Ctrl+l" = "workspace next_on_output; ${focus}";
"${mod}+Ctrl+j" = "workspace prev; ${focus}";
"${mod}+Ctrl+k" = "workspace next; ${focus}";
# Move to workspace next / previous with focused container
"${mod}+Ctrl+Shift+h" = "move container to workspace prev_on_output; workspace prev_on_output; ${focus}";
"${mod}+Ctrl+Shift+l" = "move container to workspace next_on_output; workspace next_on_output; ${focus}";
"${mod}+Ctrl+Shift+j" = "move container to workspace prev; workspace prev; ${focus}";
"${mod}+Ctrl+Shift+k" = "move container to workspace next; workspace next; ${focus}";
# move focused container to workspace
"${mod}+ctrl+1" = "move container to workspace 1; ${focus}";
"${mod}+ctrl+2" = "move container to workspace 2; ${focus}";
"${mod}+ctrl+3" = "move container to workspace 3; ${focus}";
"${mod}+ctrl+4" = "move container to workspace 4; ${focus}";
"${mod}+ctrl+5" = "move container to workspace 5; ${focus}";
"${mod}+ctrl+6" = "move container to workspace 6; ${focus}";
"${mod}+ctrl+7" = "move container to workspace 7; ${focus}";
"${mod}+ctrl+8" = "move container to workspace 8; ${focus}";
"${mod}+ctrl+9" = "move container to workspace 9; ${focus}";
"${mod}+ctrl+0" = "move container to workspace 10; ${focus}";
# move to workspace with focused container
"${mod}+shift+1" = "move container to workspace 1; workspace 1; ${focus}";
"${mod}+shift+2" = "move container to workspace 2; workspace 2; ${focus}";
"${mod}+shift+3" = "move container to workspace 3; workspace 3; ${focus}";
"${mod}+shift+4" = "move container to workspace 4; workspace 4; ${focus}";
"${mod}+shift+5" = "move container to workspace 5; workspace 5; ${focus}";
"${mod}+shift+6" = "move container to workspace 6; workspace 6; ${focus}";
"${mod}+shift+7" = "move container to workspace 7; workspace 7; ${focus}";
"${mod}+shift+8" = "move container to workspace 8; workspace 8; ${focus}";
"${mod}+shift+9" = "move container to workspace 9; workspace 9; ${focus}";
"${mod}+shift+0" = "move container to workspace 10; workspace 10; ${focus}";
# move workspaces to screen (arrow keys)
"${mod}+ctrl+shift+Right" = "move workspace to output right; ${focus}";
"${mod}+ctrl+shift+Left" = "move workspace to output left; ${focus}";
"${mod}+Ctrl+Shift+Up" = "move workspace to output above; ${focus}";
"${mod}+Ctrl+Shift+Down" = "move workspace to output below; ${focus}";
# i3 control
"${mod}+Shift+c" = "reload";
"${mod}+Shift+r" = "restart";
"${mod}+Shift+e" = "exit";
# Screen off commands
"${mod}+F1" = "exec --no-startup-id ${pkgs.bash}/bin/sh -c \"${pkgs.coreutils}/bin/sleep .25 && ${pkgs.xorg.xset}/bin/xset dpms force off\"";
# TODO --release?
"${mod}+F4" = "exec --no-startup-id ${pkgs.xautolock}/bin/xautolock -disable";
"${mod}+F5" = "exec --no-startup-id ${pkgs.xautolock}/bin/xautolock -enable";
# Modes
"${mod}+Escape" = "mode ${mode_system}";
"${mod}+r" = "mode ${mode_resize}";
"${mod}+Shift+p" = "mode ${mode_pres_main}";
"${mod}+t" = "mode ${mode_screen}";
"${mod}+y" = "mode ${mode_temp}";
};
modes = let return_bindings = {
"Return" = "mode default";
"Escape" = "mode default";
}; in
{
"${mode_system}" = {
"l" = "exec --no-startup-id exec ${locker}, mode default";
"e" = "exit, mode default";
"s" = "exec --no-startup-id exec ${locker} & ${pkgs.systemd}/bin/systemctl suspend --check-inhibitors=no, mode default";
"h" = "exec --no-startup-id exec ${locker} & ${pkgs.systemd}/bin/systemctl hibernate, mode default";
"r" = "exec --no-startup-id ${pkgs.systemd}/bin/systemctl reboot, mode default";
"p" = "exec --no-startup-id ${pkgs.systemd}/bin/systemctl poweroff -i, mode default";
} // return_bindings;
"${mode_resize}" = {
"h" = "resize shrink width 10 px or 10 ppt; ${focus}";
"j" = "resize grow height 10 px or 10 ppt; ${focus}";
"k" = "resize shrink height 10 px or 10 ppt; ${focus}";
"l" = "resize grow width 10 px or 10 ppt; ${focus}";
} // return_bindings;
"${mode_pres_main}" = {
"b" = "workspace 3, workspace 4, mode ${mode_pres_sec}";
"q" = "mode default";
"Return" = "mode default";
};
"${mode_pres_sec}" = {
"b" = "workspace 1, workspace 2, mode ${mode_pres_main}";
"q" = "mode default";
"Return" = "mode default";
};
"${mode_screen}" =
let
builtin_configs = [ "off" "common" "clone-largest" "horizontal" "vertical" "horizontal-reverse" "vertical-reverse" ];
autorandrmenu = { title, option, builtin ? false }: pkgs.writeShellScript "autorandrmenu"
''
shopt -s nullglob globstar
profiles="${if builtin then lib.strings.concatLines builtin_configs else ""}$(${pkgs.autorandr}/bin/autorandr | ${pkgs.gawk}/bin/awk '{ print $1 }')"
profile="$(echo "$profiles" | ${config.programs.rofi.package}/bin/rofi -dmenu -p "${title}")"
[[ -n "$profile" ]] || exit
${pkgs.autorandr}/bin/autorandr ${option} "$profile"
'';
in
{
"a" = "exec ${pkgs.autorandr}/bin/autorandr --change --force, mode default";
"l" = "exec ${autorandrmenu {title="Load profile"; option="--load"; builtin = true;}}, mode default";
"s" = "exec ${autorandrmenu {title="Save profile"; option="--save";}}, mode default";
"r" = "exec ${autorandrmenu {title="Remove profile"; option="--remove";}}, mode default";
"d" = "exec ${autorandrmenu {title="Default profile"; option="--default"; builtin = true;}}, mode default";
} // return_bindings;
"${mode_temp}" = {
"r" = "exec ${pkgs.sct}/bin/sct 1000";
"d" = "exec ${pkgs.sct}/bin/sct 2000";
"c" = "exec ${pkgs.sct}/bin/sct 4500";
"o" = "exec ${pkgs.sct}/bin/sct";
"a" = "exec ${pkgs.sct}/bin/sct 8000";
"b" = "exec ${pkgs.sct}/bin/sct 10000";
} // return_bindings;
};
window = {
hideEdgeBorders = "both";
titlebar = false; # So that single-container screens are basically almost fullscreen
commands = [
# Open specific applications in floating mode
{ criteria = { class = "Firefox"; }; command = "layout tabbed"; } # Doesn't seem to work anymore
{ criteria = { class = "qutebrowser"; }; command = "layout tabbed"; }
{ criteria = { title = "^pdfpc.*"; window_role = "presenter"; }; command = "move to output left, fullscreen"; }
{ criteria = { title = "^pdfpc.*"; window_role = "presentation"; }; command = "move to output right, fullscreen"; }
# switch to workspace with urgent window automatically
{ criteria = { urgent = "latest"; }; command = "focus"; }
];
};
floating = {
criteria = [
{ title = "pacmixer"; }
{ window_role = "pop-up"; }
{ window_role = "task_dialog"; }
];
};
startup = [
# Lock screen after 10 minutes
{ notification = false; command = "${pkgs.xautolock}/bin/xautolock -time 10 -locker '${pkgs.xorg.xset}/bin/xset dpms force standby' -killtime 1 -killer ${locker}"; }
{
notification = false;
command = "${pkgs.writeShellApplication {
name = "batteryNotify";
runtimeInputs = with pkgs; [coreutils libnotify];
text = builtins.readFile ./batteryNotify.sh;
# TODO Use batsignal instead?
# TODO Only on computers with battery
}}/bin/batteryNotify";
}
# TODO There's a services.screen-locker.xautolock but not sure it can match the above command
];
workspaceLayout = "tabbed";
focus.mouseWarping = true; # i3 only supports warping to workspace, hence ${focus}
workspaceOutputAssign =
let
x11_screens = config.frogeye.desktop.x11_screens;
workspaces = map (i: { name = toString i; key = toString (lib.mod i 10); }) (lib.lists.range 1 10);
forEachWorkspace = f: map (w: f { w = w; workspace = ((builtins.elemAt workspaces w)); }) (lib.lists.range 0 ((builtins.length workspaces) - 1));
in
forEachWorkspace ({ w, workspace }: { output = builtins.elemAt x11_screens (lib.mod w (builtins.length x11_screens)); workspace = workspace.name; });
};
};
};
numlock.enable = config.frogeye.desktop.numlock;
};
programs = {
# Browser
qutebrowser = {
enable = true;
keyBindings = {
normal = {
# Match tab behaviour to i3. Not that I use them.
"H" = "tab-prev";
"J" = "back";
"K" = "forward";
"L" = "tab-next";
# "T" = null;
"af" = "spawn --userscript freshrss"; # TODO Broken?
"as" = "spawn --userscript shaarli"; # TODO I don't use shaarli anymore
# "d" = null;
"u" = "undo --window";
# TODO Unbind d and T (?)
};
};
loadAutoconfig = true;
searchEngines = rec {
DEFAULT = ecosia;
alpinep = "https://pkgs.alpinelinux.org/packages?name={}&branch=edge";
ampwhat = "http://www.amp-what.com/unicode/search/{}";
arch = "https://wiki.archlinux.org/?search={}";
archp = "https://www.archlinux.org/packages/?q={}";
aur = "https://aur.archlinux.org/packages/?K={}";
aw = ampwhat;
ddg = duckduckgo;
dockerhub = "https://hub.docker.com/search/?isAutomated=0&isOfficial=0&page=1&pullCount=0&q={}&starCount=0";
duckduckgo = "https://duckduckgo.com/?q={}&ia=web";
ecosia = "https://www.ecosia.org/search?q={}";
gfr = "https://www.google.fr/search?hl=fr&q={}";
g = google;
gh = github;
gi = "http://images.google.com/search?q={}";
giphy = "https://giphy.com/search/{}";
github = "https://github.com/search?q={}";
google = "https://www.google.fr/search?q={}";
invidious = "https://invidious.frogeye.fr/search?q={}";
inv = invidious;
npm = "https://www.npmjs.com/search?q={}";
q = qwant;
qwant = "https://www.qwant.com/?t=web&q={}";
wolfram = "https://www.wolframalpha.com/input/?i={}";
youtube = "https://www.youtube.com/results?search_query={}";
yt = youtube;
};
settings = {
downloads.location.prompt = false;
tabs = {
show = "never";
tabs_are_windows = true;
};
url = rec {
open_base_url = true;
start_pages = lib.mkDefault "https://geoffrey.frogeye.fr/blank.html";
default_page = start_pages;
};
content = {
# I had this setting below, not sure if it did something special
# config.set("content.cookies.accept", "no-3rdparty", "chrome://*/*")
cookies.accept = "no-3rdparty";
prefers_reduced_motion = true;
headers.accept_language = "fr-FR, fr;q=0.9, en-GB;q=0.8, en-US;q=0.7, en;q=0.6";
tls.certificate_errors = "ask-block-thirdparty";
};
editor.command = [ "${pkgs.neovide}/bin/neovide" "--" "-f" "{file}" "-c" "normal {line}G{column0}l" ];
# TODO Doesn't work on Arch. Does it even load the right profile on Nix?
# TODO spellcheck.languages = ["fr-FR" "en-GB" "en-US"];
};
};
# Terminal
alacritty = {
# TODO Emojis. Or maybe they work on NixOS?
# Arch (working) shows this with alacritty -vvv:
# [TRACE] [crossfont] Got font path="/usr/share/fonts/twemoji/twemoji.ttf", index=0
# [DEBUG] [crossfont] Loaded Face Face { ft_face: Font Face: Regular, load_flags: MONOCHROME | TARGET_MONO | COLOR, render_mode: "Mono", lcd_filter: 1 }
# Nix (not working) shows this:
# [TRACE] [crossfont] Got font path="/nix/store/872g3w9vcr5nh93r0m83a3yzmpvd2qrj-home-manager-path/share/fonts/truetype/TwitterColorEmoji-SVGinOT.ttf", index=0
# [DEBUG] [crossfont] Loaded Face Face { ft_face: Font Face: Regular, load_flags: TARGET_LIGHT | COLOR, render_mode: "Lcd", lcd_filter: 1 }
enable = true;
settings = {
bell = {
animation = "EaseOutExpo";
color = "#000000";
command = { program = "${pkgs.sox}/bin/play"; args = [ "-n" "synth" "sine" "C5" "sine" "E4" "remix" "1-2" "fade" "0.1" "0.2" "0.1" ]; };
duration = 100;
};
cursor = { vi_mode_style = "Underline"; };
env = {
WINIT_X11_SCALE_FACTOR = "1";
# Prevents Alacritty from resizing from one monitor to another.
# Might cause issue on HiDPI screens but we'll get there when we get there
};
hints = {
enabled = [
{
binding = { mods = "Control|Alt"; key = "F"; };
command = "${pkgs.xdg-utils}/bin/xdg-open";
mouse = { enabled = true; mods = "Control"; };
post_processing = true;
regex = "(mailto:|gemini:|gopher:|https:|http:|news:|file:|git:|ssh:|ftp:)[^\\u0000-\\u001F\\u007F-\\u009F<>\"\\\\s{-}\\\\^`]+";
}
];
};
key_bindings = [
{ mode = "~Search"; mods = "Alt|Control"; key = "Space"; action = "ToggleViMode"; }
{ mode = "Vi|~Search"; mods = "Control"; key = "K"; action = "ScrollHalfPageUp"; }
{ mode = "Vi|~Search"; mods = "Control"; key = "J"; action = "ScrollHalfPageDown"; }
{ mode = "~Vi"; mods = "Control|Alt"; key = "V"; action = "Paste"; }
{ mods = "Control|Alt"; key = "C"; action = "Copy"; }
{ mode = "~Search"; mods = "Control|Alt"; key = "F"; action = "SearchForward"; }
{ mode = "~Search"; mods = "Control|Alt"; key = "B"; action = "SearchBackward"; }
{ mode = "Vi|~Search"; mods = "Control|Alt"; key = "C"; action = "ClearSelection"; }
];
window = {
dynamic_padding = false;
dynamic_title = true;
};
};
};
# Backup terminal
urxvt = {
enable = true;
package = pkgs.rxvt-unicode-emoji;
scroll = {
bar.enable = false;
};
iso14755 = false; # Disable Ctrl+Shift default bindings
keybindings = {
"Shift-Control-C" = "eval:selection_to_clipboard";
"Shift-Control-V" = "eval:paste_clipboard";
# TODO Not sure resizing works, Nix doesn't have the package (urxvt-resize-font-git on Arch)
"Control-KP_Subtract" = "resize-font:smaller";
"Control-KP_Add" = "resize-font:bigger";
};
extraConfig = {
"letterSpace" = 0;
"perl-ext-common" = "resize-font,bell-command,readline,selection";
"bell-command" = "${pkgs.sox}/bin/play -n synth sine C5 sine E4 remix 1-2 fade 0.1 0.2 0.1 &> /dev/null";
};
};
rofi = {
# TODO This theme template, that was used for Arch, looks much better:
# https://gitlab.com/jordiorlando/base16-rofi/-/blob/master/templates/default.mustache
enable = true;
pass.enable = true;
extraConfig = {
lazy-grab = false;
matching = "regex";
};
};
autorandr = {
enable = true;
hooks.postswitch = {
background = "${pkgs.feh}/bin/feh --no-fehbg --bg-fill ${config.stylix.image}";
};
};
mpv = {
enable = true;
config = {
audio-display = false;
save-position-on-quit = true;
osc = false; # Required by thumbnail script
# Hardware acceleration (from https://nixos.wiki/wiki/Accelerated_Video_Playback#MPV)
hwdec = "auto-safe";
vo = "gpu";
profile = "gpu-hq";
};
scripts = with pkgs.mpvScripts; [ thumbnail ];
scriptOpts = {
mpv_thumbnail_script = {
autogenerate = false; # TODO It creates too many processes at once, crashing the system
cache_directory = "/tmp/mpv_thumbs_${config.home.username}";
mpv_hwdec = "auto-safe";
};
};
};
};
xdg = {
mimeApps = {
enable = true;
defaultApplications = {
"text/html" = "org.qutebrowser.qutebrowser.desktop";
"x-scheme-handler/http" = "org.qutebrowser.qutebrowser.desktop";
"x-scheme-handler/https" = "org.qutebrowser.qutebrowser.desktop";
"x-scheme-handler/about" = "org.qutebrowser.qutebrowser.desktop";
"x-scheme-handler/unknown" = "org.qutebrowser.qutebrowser.desktop";
};
};
userDirs = {
enable = true; # TODO Which ones do we want?
createDirectories = true;
# French, because then it there's a different initial for each, making navigation easier
desktop = null;
download = "${config.home.homeDirectory}/Téléchargements";
music = "${config.home.homeDirectory}/Musiques";
pictures = "${config.home.homeDirectory}/Images";
publicShare = null;
templates = null;
videos = "${config.home.homeDirectory}/Vidéos";
extraConfig = {
XDG_SCREENSHOTS_DIR = "${config.home.homeDirectory}/Screenshots";
};
};
configFile = {
"pulse/client.conf" = {
text = ''cookie-file = .config/pulse/pulse-cookie'';
};
"rofimoji.rc" = {
text = ''
skin-tone = neutral
files = [emojis, math]
action = clipboard
'';
};
"vimpc/vimpcrc" = {
text = ''
map FF :browse<C-M>gg/
map à :set add next<C-M>a:set add end<C-M>
map @ :set add next<C-M>a:set add end<C-M>:next<C-M>
map ° D:browse<C-M>A:shuffle<C-M>:play<C-M>:playlist<C-M>
set songformat {%a - %b: %t}|{%f}$E$R $H[$H%l$H]$H
set libraryformat %n \| {%t}|{%f}$E$R $H[$H%l$H]$H
set ignorecase
set sort library
'';
};
};
};
services = {
blueman-applet.enable = true;
unclutter.enable = true;
dunst =
{
enable = true;
settings =
# TODO Change dmenu for rofi, so we can use context
with config.lib.stylix.colors.withHashtag; {
global = {
separator_color = lib.mkForce base05;
idle_threshold = 120;
markup = "full";
max_icon_size = 48;
# TODO Those shortcuts don't seem to work, maybe try:
# > define shortcuts inside your window manager and bind them to dunstctl(1) commands
close_all = "ctrl+mod4+n";
close = "mod4+n";
context = "mod1+mod4+n";
history = "shift+mod4+n";
};
urgency_low = {
background = lib.mkForce base01;
foreground = lib.mkForce base03;
frame_color = lib.mkForce base05;
};
urgency_normal = {
background = lib.mkForce base02;
foreground = lib.mkForce base05;
frame_color = lib.mkForce base05;
};
urgency_critical = {
background = lib.mkForce base08;
foreground = lib.mkForce base06;
frame_color = lib.mkForce base05;
};
};
};
mpd = {
enable = true;
network = {
listenAddress = "0.0.0.0"; # So it can be controlled from home
# TODO ... and whoever is the Wi-Fi network I'm using, which, not great
startWhenNeeded = true;
};
extraConfig = ''
restore_paused "yes"
'';
};
autorandr.enable = true;
};
home = {
packages = with pkgs; [
pavucontrol # Because can't use Win+F1X on Pinebook 🙃
# remote
tigervnc
# music
mpc-cli
ashuffle
vimpc
# multimedia common
gimp
inkscape
libreoffice
# data management
freefilesync
# browsers
firefox
# fonts
dejavu_fonts
twemoji-color-font
gnome.gedit
feh
zbar
zathura
meld
python3Packages.magic
# x11-exclusive
numlockx
simplescreenrecorder
trayer
xclip
keynav
xorg.xinit
# TODO Make this clean. Service?
# organisation
pass
thunderbird
];
sessionVariables = {
MPD_PORT = "${toString config.services.mpd.network.port}";
ALSA_PLUGIN_DIR = "${pkgs.alsa-plugins}/lib/alsa-lib"; # Fixes an issue with sox (Cannot open shared library libasound_module_pcm_pulse.so)
# UPST Patch this upstream like: https://github.com/NixOS/nixpkgs/blob/216b111fb87091632d077898df647d1438fc2edb/pkgs/applications/audio/espeak-ng/default.nix#L84
};
};
};
}

View file

@ -0,0 +1,44 @@
{
pkgs,
lib,
config,
...
}:
let
pactl = "exec ${pkgs.pulseaudio}/bin/pactl"; # TODO Use NixOS package if using NixOS
mod = config.xsession.windowManager.i3.config.modifier;
in
{
config = lib.mkIf config.frogeye.desktop.xorg {
home = {
packages = with pkgs; [
pwvucontrol # Because can't use Win+F1X on Pinebook 🙃
pavucontrol # Just in case
helvum
qpwgraph
sox
];
sessionVariables = {
ALSA_PLUGIN_DIR = "${pkgs.alsa-plugins}/lib/alsa-lib"; # Fixes an issue with sox (Cannot open shared library libasound_module_pcm_pulse.so)
# UPST Patch this upstream like: https://github.com/NixOS/nixpkgs/blob/216b111fb87091632d077898df647d1438fc2edb/pkgs/applications/audio/espeak-ng/default.nix#L84
};
};
programs.bash.shellAliases = {
beep = ''${pkgs.sox}/bin/play -n synth sine E5 sine A4 remix 1-2 fade 0.5 1.2 0.5 2> /dev/null'';
noise = ''${pkgs.sox}/bin/play -c 2 -n synth $'' + ''{1}noise'';
};
xdg.configFile = {
"pulse/client.conf" = {
text = ''cookie-file = .config/pulse/pulse-cookie'';
};
};
xsession.windowManager.i3.config.keybindings = {
"XF86AudioRaiseVolume" = "${pactl} set-sink-mute @DEFAULT_SINK@ false; ${pactl} set-sink-volume @DEFAULT_SINK@ +5%";
"XF86AudioLowerVolume" = "${pactl} set-sink-mute @DEFAULT_SINK@ false; ${pactl} set-sink-volume @DEFAULT_SINK@ -5%";
"XF86AudioMute" = "${pactl} set-sink-mute @DEFAULT_SINK@ true";
"${mod}+F8" = "${pactl} suspend-sink @DEFAULT_SINK@ 1; ${pactl} suspend-sink @DEFAULT_SINK@ 0"; # Re-synchronize bluetooth headset
"${mod}+F11" = "exec ${pkgs.pavucontrol}/bin/pwvucontrol";
# TODO Find pacmixer?
};
};
}

View file

@ -0,0 +1,72 @@
{
pkgs,
lib,
config,
...
}:
let
builtin_configs = [
"off"
"common"
"clone-largest"
"horizontal"
"vertical"
"horizontal-reverse"
"vertical-reverse"
];
autorandrmenu =
{
title,
option,
builtin ? false,
}:
pkgs.writeShellScript "autorandrmenu" ''
shopt -s nullglob globstar
profiles="${
if builtin then lib.strings.concatLines builtin_configs else ""
}$(${pkgs.autorandr}/bin/autorandr | ${pkgs.gawk}/bin/awk '{ print $1 }')"
profile="$(echo "$profiles" | ${config.programs.rofi.package}/bin/rofi -dmenu -p "${title}")"
[[ -n "$profile" ]] || exit
${pkgs.autorandr}/bin/autorandr ${option} "$profile"
'';
in
{
config = lib.mkIf config.frogeye.desktop.xorg {
frogeye.desktop.i3.bindmodes = {
"Screen setup [A] Auto [L] Load [S] Save [R] Remove [D] Default" = {
bindings = {
"a" = "exec ${pkgs.autorandr}/bin/autorandr --change --force, mode default";
"l" = "exec ${
autorandrmenu {
title = "Load profile";
option = "--load";
builtin = true;
}
}, mode default";
"s" = "exec ${
autorandrmenu {
title = "Save profile";
option = "--save";
}
}, mode default";
"r" = "exec ${
autorandrmenu {
title = "Remove profile";
option = "--remove";
}
}, mode default";
"d" = "exec ${
autorandrmenu {
title = "Default profile";
option = "--default";
builtin = true;
}
}, mode default";
};
mod_enter = "t";
};
};
programs.autorandr.enable = true;
services.autorandr.enable = true;
};
}

View file

@ -0,0 +1,14 @@
{
pkgs,
config,
lib,
...
}:
{
config = {
# This correctly sets the background on some occasions, below does the rest
programs.autorandr.hooks.postswitch = {
background = "${pkgs.feh}/bin/feh --no-fehbg --bg-fill ${config.stylix.image}";
};
};
}

View file

@ -0,0 +1,191 @@
{
pkgs,
lib,
config,
...
}:
{
imports = [
./homepage.nix
];
config = lib.mkIf config.frogeye.desktop.xorg {
home.sessionVariables = {
BROWSER = "qutebrowser";
};
programs = {
firefox = {
enable = true;
package = pkgs.firefox.override {
nativeMessagingHosts = [
pkgs.tridactyl-native
];
};
profiles.hm = {
extensions = with pkgs.nur.repos.rycee.firefox-addons; [
(buildFirefoxXpiAddon {
pname = "onetab";
version = "0.1.0";
addonId = "onetab@nated";
url = "https://addons.mozilla.org/firefox/downloads/file/4118712/one_tab_per_window-0.1.0.xpi";
sha256 = "sha256-64DeL2xgXpqz32LJWDx4jhS2Fvbld8re3z8fdwnNTw0=";
meta = with lib; {
homepage = "https://git.sr.ht/~nated/onetab";
description = "When a new tab is opened, redirects it to a new window instead.";
license = licenses.unfree;
mozPermissions = [ "tabs" ];
platforms = platforms.all;
};
})
tridactyl
ublock-origin
];
search = {
default = "DuckDuckGo";
engines = {
# TODO Harmonize with qutebrowser search engines
"Nix Packages" = {
urls = [
{
template = "https://search.nixos.org/packages";
params = [
{
name = "type";
value = "packages";
}
{
name = "query";
value = "{searchTerms}";
}
];
}
];
icon = "${pkgs.nixos-icons}/share/icons/hicolor/scalable/apps/nix-snowflake.svg";
definedAliases = [ "@np" ];
};
"NixOS Wiki" = {
urls = [ { template = "https://nixos.wiki/index.php?search={searchTerms}"; } ];
iconUpdateURL = "https://nixos.wiki/favicon.png";
updateInterval = 24 * 60 * 60 * 1000; # every day
definedAliases = [ "@nw" ];
};
"Bing".metaData.hidden = true;
"Google".metaData.alias = "@g"; # builtin engines only support specifying one additional alias
};
force = true;
};
settings = {
"signon.rememberSignons" = false; # Don't save passwords
"browser.newtabpage.enabled" = false; # Best would be homepage but not possible without extension?
# Europe please
"browser.search.region" = "GB";
"browser.search.isUS" = false;
"distribution.searchplugins.defaultLocale" = "en-GB";
"general.useragent.locale" = "en-GB";
};
};
};
qutebrowser = {
enable = true;
keyBindings = {
normal = {
# Match tab behaviour to i3. Not that I use tabs.
"H" = "tab-prev";
"J" = "back";
"K" = "forward";
"L" = "tab-next";
# "T" = null;
"af" = "spawn --userscript freshrss"; # TODO Broken?
"as" = "spawn --userscript shaarli"; # TODO I don't use shaarli anymore
# "d" = null;
"u" = "undo --window";
# TODO Unbind d and T (?)
};
};
loadAutoconfig = true;
searchEngines = rec {
DEFAULT = ecosia;
alpinep = "https://pkgs.alpinelinux.org/packages?name={}&branch=edge";
ampwhat = "http://www.amp-what.com/unicode/search/{}";
arch = "https://wiki.archlinux.org/?search={}";
archp = "https://www.archlinux.org/packages/?q={}";
aur = "https://aur.archlinux.org/packages/?K={}";
aw = ampwhat;
ddg = duckduckgo;
dockerhub = "https://hub.docker.com/search/?isAutomated=0&isOfficial=0&page=1&pullCount=0&q={}&starCount=0";
duckduckgo = "https://duckduckgo.com/?q={}&ia=web";
ecosia = "https://www.ecosia.org/search?q={}";
gfr = "https://www.google.fr/search?hl=fr&q={}";
g = google;
gh = github;
gi = "http://images.google.com/search?q={}";
giphy = "https://giphy.com/search/{}";
github = "https://github.com/search?q={}";
google = "https://www.google.fr/search?q={}";
hm = homemanager;
homemanager = "https://home-manager-options.extranix.com/?query={}&release=${config.home.version.release}";
invidious = "https://invidious.frogeye.fr/search?q={}";
inv = invidious;
nixos = "https://search.nixos.org/options?channel=${config.home.version.release}&query={}";
nixoswiki = "https://wiki.nixos.org/w/index.php?search={}";
nixpkgs = "https://search.nixos.org/packages?channel=${config.home.version.release}&query={}";
noogle = "https://noogle.dev/q?term={}";
npm = "https://www.npmjs.com/search?q={}";
nw = nixoswiki;
os = nixos;
pkgs = nixpkgs;
q = qwant;
qwant = "https://www.qwant.com/?t=web&q={}";
wolfram = "https://www.wolframalpha.com/input/?i={}";
youtube = "https://www.youtube.com/results?search_query={}";
yt = youtube;
};
settings = {
colors.webpage.darkmode.policy.images = "never"; # No inverting images in dark mode, is ugly
downloads.location.prompt = false;
tabs = {
show = "never";
tabs_are_windows = true;
};
url.open_base_url = true;
content = {
# I had this setting below, not sure if it did something special
# config.set("content.cookies.accept", "no-3rdparty", "chrome://*/*")
cookies.accept = "no-3rdparty";
prefers_reduced_motion = true;
headers.accept_language = "en-GB,en;q=0.9";
tls.certificate_errors = "ask-block-thirdparty";
javascript.clipboard = "access"; # copy-paste is fine
};
editor.command = [
"${pkgs.neovide}/bin/neovide"
"--"
"-f"
"{file}"
"-c"
"normal {line}G{column0}l"
];
# TODO Doesn't work on Arch. Does it even load the right profile on Nix?
# TODO spellcheck.languages = ["fr-FR" "en-GB" "en-US"];
};
};
};
xdg = {
configFile."tridactyl/tridactylrc".source = ./tridactylrc; # TODO Improve that :)
mimeApps = {
enable = true;
defaultApplications = {
"text/html" = "org.qutebrowser.qutebrowser.desktop";
"x-scheme-handler/http" = "org.qutebrowser.qutebrowser.desktop";
"x-scheme-handler/https" = "org.qutebrowser.qutebrowser.desktop";
"x-scheme-handler/about" = "org.qutebrowser.qutebrowser.desktop";
"x-scheme-handler/unknown" = "org.qutebrowser.qutebrowser.desktop";
};
};
};
xsession.windowManager.i3.config.keybindings = {
"${config.xsession.windowManager.i3.config.modifier}+m" =
"exec ${config.programs.qutebrowser.package}/bin/qutebrowser --override-restore";
};
};
}

View file

@ -0,0 +1,86 @@
html {
background-image: linear-gradient(#e6f0a3 0%, #d2e638 50%, #c3d825 51%, #dbf043 100%);
min-height: 100%;
}
body {
font: 20px Helvetica, sans-serif;
padding: 2.5% 0;
}
article {
margin: 0 auto;
max-width: 95%;
}
h1, h2 {
display: none;
}
nav a {
background: rgba(255, 255, 255, 0.8);
width: 110px;
height: 100px;
display: inline-block;
padding: 15px 0;
margin: 0px 5px 10px;
border: 1px solid #ddd;
border-radius: 5px;
text-align: center;
vertical-align: top;
position: relative;
}
@media only screen and (min-width: 768px) {
nav {
margin-left: 110px;
position: relative;
}
nav .main {
position: absolute;
left: -130px;
}
}
nav img {
margin: auto;
max-width: 90%;
max-height: 70%;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
a {
text-decoration: none;
color: inherit;
}
nav a:hover {
background: rgba(240, 240, 240, 0.8);
}
nav a:active {
background: rgba(220, 220, 220, 0.8);
}
nav a>.fa, nav a>.fa-stack{
width: 100%;
margin-top: .25em;
margin-bottom: .35em;
font-size: 32px;
}
nav a span {
display: block;
margin-top: .55em;
font-weight: 400;
line-height: 1.1;
}

View file

@ -0,0 +1,32 @@
<!doctype html>
<html lang="fr">
<head>
<title>Homepage</title>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width;minimum-scale=0.5,maximum-scale=1.0; user-scalable=1;" />
<link rel="stylesheet" type="text/css" href="{{css}}"/>
<link rel="stylesheet" type="text/css" href="{{fa_css}}"/>
</head>
<body>
<article>
<h1>Homepage</h1>
{{#sections}}
<h2>{{title}}</h2>
<nav style="color: {{color}};">
{{#image}}
<a href="{{url}}" class="main">
<img alt="Logo for {{title}}" src="{{image}}" />
</a>
{{/image}}
{{#links}}
<a href="{{url}}">
<i class="fa fa-{{icon}}" aria-label="Icon for {{name}} ({{icon}})"></i>
<span>{{name}}</span>
</a>
{{/links}}
</nav>
{{/sections}}
</article>
</body>
</html>

View file

@ -0,0 +1,110 @@
{
pkgs,
lib,
config,
...
}:
let
# TODO ForkAwesome is deprecated, find something else
fa = pkgs.fetchFromGitHub {
owner = "ForkAwesome";
repo = "Fork-Awesome";
rev = "1.2.0";
sha256 = "sha256-zG6/0dWjU7/y/oDZuSEv+54Mchng64LVyV8bluskYzc=";
};
data = config.frogeye.homepage // {
sections = builtins.attrValues config.frogeye.homepage.sections;
css = ./homepage.css;
fa_css = "${fa}/css/fork-awesome.min.css";
};
# Blatantly stolen from https://pablo.tools/blog/computers/nix-mustache-templates/
homepage = builtins.toString (
pkgs.stdenv.mkDerivation {
name = "homepage.html";
nativeBuildInpts = [ pkgs.mustache-go ];
passAsFile = [ "jsonData" ];
jsonData = builtins.toJSON data;
phases = [
"buildPhase"
"installPhase"
];
buildPhase = ''
${pkgs.mustache-go}/bin/mustache $jsonDataPath ${./homepage.html.mustache} > homepage.html
'';
installPhase = ''
cp homepage.html $out
'';
}
);
in
{
config.programs = {
firefox.profiles.hm.settings."browser.startup.homepage" = homepage;
qutebrowser.settings.url = {
start_pages = homepage;
default_page = homepage;
};
};
options.frogeye.homepage = {
sections = lib.mkOption {
default = { };
description = "Folders used by users";
# Top-level so Syncthing can work for all users. Also there's no real home-manager syncthing module.
type = lib.types.attrsOf (
lib.types.submodule (
{ config, name, ... }:
{
options = {
title = lib.mkOption {
type = lib.types.str;
default = "Section title";
};
color = lib.mkOption {
type = lib.types.str;
default = "#337ab7";
};
image = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
};
url = lib.mkOption {
type = lib.types.str;
default = "about:blank";
};
links = lib.mkOption {
default = [ ];
type = lib.types.listOf (
lib.types.submodule (
{ config, ... }:
{
options = {
name = lib.mkOption {
type = lib.types.str;
default = "Link";
};
url = lib.mkOption {
type = lib.types.str;
default = "about:blank";
};
icon = lib.mkOption {
type = lib.types.str;
default = "question-circle";
};
};
}
)
);
};
};
}
)
);
};
};
}

View file

@ -79,14 +79,14 @@ set searchurls.npm https://www.npmjs.com/search?q=%s
set searchurls.pypi https://pypi.org/search/?q=%s set searchurls.pypi https://pypi.org/search/?q=%s
set searchurls.python https://docs.python.org/3/search.html?q=%s set searchurls.python https://docs.python.org/3/search.html?q=%s
set searchurls.qwant https://www.qwant.com/?t=web&q=%s set searchurls.qwant https://www.qwant.com/?t=web&q=%s
set searchurls.invidious https://invidious.drycat.fr/search?q=%s set searchurls.invidious https://invidious.frogeye.fr/search?q=%s
set searchurls.id https://invidious.drycat.fr/search?q=%s set searchurls.id https://invidious.drycat.fr/search?q=%s
set searchurls.wa https://www.wolframalpha.com/input/?i=%s set searchurls.wa https://www.wolframalpha.com/input/?i=%s
set searchurls.yt https://www.youtube.com/results?search_query=%s set searchurls.yt https://www.youtube.com/results?search_query=%s
" Firefox GUI " Firefox GUI
" This can still be shown with F6! " This can still be shown with F6!
" guiset_quiet gui none guiset_quiet gui none
" Never autofocus " Never autofocus
set allowautofocus false set allowautofocus false

195
hm/desktop/default.nix Normal file
View file

@ -0,0 +1,195 @@
{
pkgs,
config,
lib,
...
}:
{
imports = [
./audio
./autorandr
./background
./browser
./frobar/module.nix
./i3.nix
./lock
./mpd
./presentation
./redness
./screenshots
./terminal
];
config = lib.mkIf config.frogeye.desktop.xorg {
xsession = {
enable = true;
# Not using config.xdg.configHome because it needs to be $HOME-relative paths and path manipulation is hard
scriptPath = ".config/xsession";
profilePath = ".config/xprofile";
windowManager = {
i3.enable = true;
};
numlock.enable = config.frogeye.desktop.numlock;
};
programs = {
# Terminal
bash.shellAliases = {
x = "startx ${config.home.homeDirectory}/${config.xsession.scriptPath}; logout";
lmms = "lmms --config ${config.xdg.configHome}/lmmsrc.xml";
};
rofi = {
# TODO This theme template, that was used for Arch, looks much better:
# https://gitlab.com/jordiorlando/base16-rofi/-/blob/master/templates/default.mustache
enable = true;
pass.enable = true;
extraConfig = {
lazy-grab = false;
matching = "regex";
};
};
mpv = {
enable = true;
config = {
audio-display = false;
save-position-on-quit = true;
osc = false; # Required by thumbnail script
# Hardware acceleration (from https://nixos.wiki/wiki/Accelerated_Video_Playback#MPV, vo=gpu already default)
hwdec = "auto-safe";
profile = "gpu-hq";
};
scripts = with pkgs.mpvScripts; [
thumbnail
mpris
];
scriptOpts = {
mpv_thumbnail_script = {
autogenerate = false; # TODO It creates too many processes at once, crashing the system
cache_directory = "/tmp/mpv_thumbs_${config.home.username}";
mpv_hwdec = "auto-safe";
};
};
};
};
xdg = {
userDirs =
let
wellKnownUserDirs = [
"desktop"
"documents"
"download"
"music"
"pictures"
"publicShare"
"templates"
"videos"
];
wellKnownUserDirsNulled = builtins.listToAttrs (
builtins.map (name: {
inherit name;
value = null;
}) wellKnownUserDirs
);
allFolders = builtins.attrValues config.frogeye.folders;
folders = builtins.filter (
folder: folder.xdgUserDirVariable != null && folder.user == config.home.username
) allFolders;
in
{
enable = true;
createDirectories = true;
extraConfig = builtins.listToAttrs (
builtins.map (folder: {
name = folder.xdgUserDirVariable;
value = "${config.home.homeDirectory}/${folder.path}";
}) folders
);
}
// wellKnownUserDirsNulled; # Don't use defaults dirs
};
services = {
blueman-applet.enable = true;
unclutter.enable = true;
dunst = {
enable = true;
settings =
# TODO Change dmenu for rofi, so we can use context
with config.lib.stylix.colors.withHashtag; {
global = {
separator_color = lib.mkForce base05;
idle_threshold = 120;
markup = "full";
max_icon_size = 48;
# TODO Those shortcuts don't seem to work, maybe try:
# > define shortcuts inside your window manager and bind them to dunstctl(1) commands
close_all = "ctrl+mod4+n";
close = "mod4+n";
context = "mod1+mod4+n";
history = "shift+mod4+n";
};
urgency_low = {
background = lib.mkForce base01;
foreground = lib.mkForce base03;
frame_color = lib.mkForce base05;
};
urgency_normal = {
background = lib.mkForce base02;
foreground = lib.mkForce base05;
frame_color = lib.mkForce base05;
};
urgency_critical = {
background = lib.mkForce base08;
foreground = lib.mkForce base06;
frame_color = lib.mkForce base05;
};
};
};
};
home = {
file = {
".face" = {
# TODO Only works on pindakaas? See https://wiki.archlinux.org/title/LightDM#Changing_your_avatar
source =
pkgs.runCommand "face.png" { }
"${pkgs.inkscape}/bin/inkscape ${./face.svg} -w 1024 -o $out";
};
};
packages = with pkgs; [
# remote
tigervnc
# multimedia common
gimp
inkscape
libreoffice
# data management
freefilesync
# misc
gedit
xfce.thunar
nomacs
feh
zbar
evince
zathura
meld
python3Packages.magic
# x11-exclusive
simplescreenrecorder
trayer
xclip
keynav
xorg.xinit
scrot
];
sessionVariables = {
# XAUTHORITY = "${config.xdg.configHome}/Xauthority"; # Disabled as this causes lock-ups with DMs
};
};
};
}

View file

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View file

@ -0,0 +1,51 @@
{
pkgs ? import <nixpkgs> {
config = { };
overlays = [ ];
},
...
}:
let
lemonbar = (
pkgs.lemonbar-xft.overrideAttrs (old: {
src = pkgs.fetchFromGitHub {
owner = "drscream";
repo = "lemonbar-xft";
rev = "a64a2a6a6d643f4d92f9d7600722710eebce7bdb";
sha256 = "sha256-T5FhEPIiDt/9paJwL9Sj84CBtA0YFi1hZz0+87Hd6jU=";
# https://github.com/drscream/lemonbar-xft/pull/2
};
})
);
in
# Tried using pyproject.nix but mpd2 dependency wouldn't resolve,
# is called pyton-mpd2 on PyPi but mpd2 in nixpkgs.
pkgs.python3Packages.buildPythonApplication rec {
pname = "frobar";
version = "3.0";
propagatedBuildInputs = with pkgs.python3Packages; [
i3ipc
psutil
pulsectl-asyncio
pygobject3
rich
];
nativeBuildInputs =
[ lemonbar ]
++ (with pkgs; [
wirelesstools
playerctl
]);
makeWrapperArgs = [
"--prefix PATH : ${pkgs.lib.makeBinPath nativeBuildInputs}"
"--prefix GI_TYPELIB_PATH : ${GI_TYPELIB_PATH}"
];
GI_TYPELIB_PATH = pkgs.lib.makeSearchPath "lib/girepository-1.0" [
pkgs.glib.out
pkgs.playerctl
];
src = ./.;
}

View file

@ -0,0 +1,146 @@
import rich.color
import rich.logging
import rich.terminal_theme
import frobar.common
import frobar.providers
from frobar.common import Alignment
def main() -> None:
# TODO Configurable
FROGARIZED = [
"#092c0e",
"#143718",
"#5a7058",
"#677d64",
"#89947f",
"#99a08d",
"#fae2e3",
"#fff0f1",
"#e0332e",
"#cf4b15",
"#bb8801",
"#8d9800",
"#1fa198",
"#008dd1",
"#5c73c4",
"#d43982",
]
# TODO Not super happy with the color management,
# while using an existing library is great, it's limited to ANSI colors
def base16_color(color: int) -> tuple[int, int, int]:
hexa = FROGARIZED[color]
return tuple(rich.color.parse_rgb_hex(hexa[1:]))
theme = rich.terminal_theme.TerminalTheme(
base16_color(0x0),
base16_color(0x0), # TODO should be 7, currently 0 so it's compatible with v2
[
base16_color(0x0), # black
base16_color(0x8), # red
base16_color(0xB), # green
base16_color(0xA), # yellow
base16_color(0xD), # blue
base16_color(0xE), # magenta
base16_color(0xC), # cyan
base16_color(0x5), # white
],
[
base16_color(0x3), # bright black
base16_color(0x8), # bright red
base16_color(0xB), # bright green
base16_color(0xA), # bright yellow
base16_color(0xD), # bright blue
base16_color(0xE), # bright magenta
base16_color(0xC), # bright cyan
base16_color(0x7), # bright white
],
)
bar = frobar.common.Bar(theme=theme)
dualScreen = len(bar.children) > 1
leftPreferred = 0 if dualScreen else None
rightPreferred = 1 if dualScreen else None
workspaces_suffixes = "▲■"
workspaces_names = dict(
(str(i + 1), f"{i+1} {c}") for i, c in enumerate(workspaces_suffixes)
)
color = rich.color.Color.parse
bar.addProvider(
frobar.providers.I3ModeProvider(color=color("red")), alignment=Alignment.LEFT
)
bar.addProvider(
frobar.providers.I3WorkspacesProvider(custom_names=workspaces_names),
alignment=Alignment.LEFT,
)
if dualScreen:
bar.addProvider(
frobar.providers.I3WindowTitleProvider(color=color("white")),
screenNum=0,
alignment=Alignment.CENTER,
)
bar.addProvider(
frobar.providers.MprisProvider(color=color("bright_white")),
screenNum=rightPreferred,
alignment=Alignment.CENTER,
)
else:
bar.addProvider(
frobar.common.SpacerProvider(),
alignment=Alignment.LEFT,
)
bar.addProvider(
frobar.providers.MprisProvider(color=color("bright_white")),
alignment=Alignment.LEFT,
)
bar.addProvider(
frobar.providers.CpuProvider(),
screenNum=leftPreferred,
alignment=Alignment.RIGHT,
)
bar.addProvider(
frobar.providers.LoadProvider(),
screenNum=leftPreferred,
alignment=Alignment.RIGHT,
)
bar.addProvider(
frobar.providers.RamProvider(),
screenNum=leftPreferred,
alignment=Alignment.RIGHT,
)
bar.addProvider(
frobar.providers.TemperatureProvider(),
screenNum=leftPreferred,
alignment=Alignment.RIGHT,
)
bar.addProvider(
frobar.providers.BatteryProvider(),
screenNum=leftPreferred,
alignment=Alignment.RIGHT,
)
bar.addProvider(
frobar.providers.PulseaudioProvider(color=color("magenta")),
screenNum=rightPreferred,
alignment=Alignment.RIGHT,
)
bar.addProvider(
frobar.providers.NetworkProvider(color=color("blue")),
screenNum=leftPreferred,
alignment=Alignment.RIGHT,
)
bar.addProvider(
frobar.providers.TimeProvider(color=color("cyan")), alignment=Alignment.RIGHT
)
bar.launch()
if __name__ == "__main__":
main()

View file

@ -0,0 +1,629 @@
import asyncio
import collections
import datetime
import enum
import logging
import signal
import typing
import gi
import gi.events
import gi.repository.GLib
import i3ipc
import i3ipc.aio
import rich.color
import rich.logging
import rich.terminal_theme
logging.basicConfig(
level="DEBUG",
format="%(message)s",
datefmt="[%X]",
handlers=[rich.logging.RichHandler()],
)
log = logging.getLogger("frobar")
T = typing.TypeVar("T", bound="ComposableText")
P = typing.TypeVar("P", bound="ComposableText")
C = typing.TypeVar("C", bound="ComposableText")
Sortable = str | int
# Display utilities
def humanSize(numi: int) -> str:
"""
Returns a string of width 3+3
"""
num = float(numi)
for unit in ("B ", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB"):
if abs(num) < 1000:
if num >= 10:
return f"{int(num):3d}{unit}"
else:
return f"{num:.1f}{unit}"
num /= 1024
return f"{numi:d}YiB"
def ramp(p: float, states: str = " ▁▂▃▄▅▆▇█") -> str:
if p < 0:
return ""
d, m = divmod(p, 1.0)
text = states[-1] * int(d)
if m > 0:
text += states[round(m * (len(states) - 1))]
return text
def clip(text: str, length: int = 30) -> str:
if len(text) > length:
text = text[: length - 1] + ""
return text
class ComposableText(typing.Generic[P, C]):
def __init__(
self,
parent: typing.Optional[P] = None,
sortKey: Sortable = 0,
) -> None:
self.parent: typing.Optional[P] = None
self.children: typing.MutableSequence[C] = list()
self.sortKey = sortKey
if parent:
self.setParent(parent)
self.bar = self.getFirstParentOfType(Bar)
def setParent(self, parent: P) -> None:
assert self.parent is None
parent.children.append(self)
assert isinstance(parent.children, list)
parent.children.sort(key=lambda c: c.sortKey)
self.parent = parent
self.parent.updateMarkup()
def unsetParent(self) -> None:
assert self.parent
self.parent.children.remove(self)
self.parent.updateMarkup()
self.parent = None
def getFirstParentOfType(self, typ: typing.Type[T]) -> T:
parent = self
while not isinstance(parent, typ):
assert parent.parent, f"{self} doesn't have a parent of {typ}"
parent = parent.parent
return parent
def updateMarkup(self) -> None:
self.bar.refresh.set()
# TODO OPTI See if worth caching the output
def generateMarkup(self) -> str:
raise NotImplementedError(f"{self} cannot generate markup")
def getMarkup(self) -> str:
return self.generateMarkup()
class Button(enum.Enum):
CLICK_LEFT = "1"
CLICK_MIDDLE = "2"
CLICK_RIGHT = "3"
SCROLL_UP = "4"
SCROLL_DOWN = "5"
class Section(ComposableText):
"""
Colorable block separated by chevrons
"""
def __init__(
self,
parent: "Module",
sortKey: Sortable = 0,
color: rich.color.Color = rich.color.Color.default(),
) -> None:
super().__init__(parent=parent, sortKey=sortKey)
self.parent: "Module"
self.color = color
self.desiredText: str | None = None
self.text = ""
self.targetSize = -1
self.size = -1
self.animationTask: asyncio.Task | None = None
self.actions: dict[Button, str] = dict()
def isHidden(self) -> bool:
return self.size < 0
# Geometric series, with a cap
ANIM_A = 0.025
ANIM_R = 0.9
ANIM_MIN = 0.001
async def animate(self) -> None:
increment = 1 if self.size < self.targetSize else -1
loop = asyncio.get_running_loop()
frameTime = loop.time()
animTime = self.ANIM_A
skipped = 0
while self.size != self.targetSize:
self.size += increment
self.updateMarkup()
animTime *= self.ANIM_R
animTime = max(self.ANIM_MIN, animTime)
frameTime += animTime
sleepTime = frameTime - loop.time()
# In case of stress, skip refreshing by not awaiting
if sleepTime > 0:
if skipped > 0:
log.warning(f"Skipped {skipped} animation frame(s)")
skipped = 0
await asyncio.sleep(sleepTime)
else:
skipped += 1
def setText(self, text: str | None) -> None:
# OPTI Don't redraw nor reset animation if setting the same text
if self.desiredText == text:
return
self.desiredText = text
if text is None:
self.text = ""
self.targetSize = -1
else:
self.text = f" {text} "
self.targetSize = len(self.text)
if self.animationTask:
self.animationTask.cancel()
# OPTI Skip the whole animation task if not required
if self.size == self.targetSize:
self.updateMarkup()
else:
self.animationTask = self.bar.taskGroup.create_task(self.animate())
def setAction(self, button: Button, callback: typing.Callable | None) -> None:
if button in self.actions:
command = self.actions[button]
self.bar.removeAction(command)
del self.actions[button]
if callback:
command = self.bar.addAction(callback)
self.actions[button] = command
def generateMarkup(self) -> str:
assert not self.isHidden()
pad = max(0, self.size - len(self.text))
text = self.text[: self.size] + " " * pad
for button, command in self.actions.items():
text = "%{A" + button.value + ":" + command + ":}" + text + "%{A}"
return text
class Module(ComposableText):
"""
Sections handled by a same updater
"""
def __init__(self, parent: "Side") -> None:
super().__init__(parent=parent)
self.parent: "Side"
self.children: typing.MutableSequence[Section]
self.mirroring: Module | None = None
self.mirrors: list[Module] = list()
def mirror(self, module: "Module") -> None:
self.mirroring = module
module.mirrors.append(self)
def getSections(self) -> typing.Sequence[Section]:
if self.mirroring:
return self.mirroring.children
else:
return self.children
def updateMarkup(self) -> None:
super().updateMarkup()
for mirror in self.mirrors:
mirror.updateMarkup()
class Alignment(enum.Enum):
LEFT = "l"
RIGHT = "r"
CENTER = "c"
class Side(ComposableText):
def __init__(self, parent: "Screen", alignment: Alignment) -> None:
super().__init__(parent=parent)
self.parent: Screen
self.children: typing.MutableSequence[Module] = []
self.alignment = alignment
self.bar = parent.getFirstParentOfType(Bar)
def generateMarkup(self) -> str:
if not self.children:
return ""
text = "%{" + self.alignment.value + "}"
lastSection: Section | None = None
for module in self.children:
for section in module.getSections():
if section.isHidden():
continue
hexa = section.color.get_truecolor(theme=self.bar.theme).hex
if lastSection is None:
if self.alignment == Alignment.LEFT:
text += "%{B" + hexa + "}%{F-}"
else:
text += "%{B-}%{F" + hexa + "}%{R}%{F-}"
elif isinstance(lastSection, SpacerSection):
text += "%{B-}%{F" + hexa + "}%{R}%{F-}"
else:
if self.alignment == Alignment.RIGHT:
if lastSection.color == section.color:
text += ""
else:
text += "%{F" + hexa + "}%{R}"
else:
if lastSection.color == section.color:
text += ""
else:
text += "%{R}%{B" + hexa + "}"
text += "%{F-}"
text += section.getMarkup()
lastSection = section
if self.alignment != Alignment.RIGHT and lastSection:
text += "%{R}%{B-}"
return text
class Screen(ComposableText):
def __init__(self, parent: "Bar", output: str) -> None:
super().__init__(parent=parent)
self.parent: "Bar"
self.children: typing.MutableSequence[Side]
self.output = output
for alignment in Alignment:
Side(parent=self, alignment=alignment)
def generateMarkup(self) -> str:
return ("%{Sn" + self.output + "}") + "".join(
side.getMarkup() for side in self.children
)
class Bar(ComposableText):
"""
Top-level
"""
def __init__(
self,
theme: rich.terminal_theme.TerminalTheme = rich.terminal_theme.DEFAULT_TERMINAL_THEME,
) -> None:
super().__init__()
self.parent: None
self.children: typing.MutableSequence[Screen]
self.longRunningTasks: list[asyncio.Task] = list()
self.theme = theme
self.refresh = asyncio.Event()
self.taskGroup = asyncio.TaskGroup()
self.providers: list["Provider"] = list()
self.actionIndex = 0
self.actions: dict[str, typing.Callable] = dict()
self.periodicProviderTask: typing.Coroutine | None = None
i3 = i3ipc.Connection()
outputs = i3.get_outputs()
outputs.sort(key=lambda output: output.rect.x)
for output in outputs:
if not output.active:
continue
Screen(parent=self, output=output.name)
def addLongRunningTask(self, coro: typing.Coroutine) -> None:
task = self.taskGroup.create_task(coro)
self.longRunningTasks.append(task)
async def run(self) -> None:
cmd = [
"lemonbar",
"-b",
"-a",
"64",
"-f",
"DejaVuSansM Nerd Font:size=10",
"-F",
self.theme.foreground_color.hex,
"-B",
self.theme.background_color.hex,
]
proc = await asyncio.create_subprocess_exec(
*cmd, stdout=asyncio.subprocess.PIPE, stdin=asyncio.subprocess.PIPE
)
async def refresher() -> None:
assert proc.stdin
while True:
await self.refresh.wait()
self.refresh.clear()
markup = self.getMarkup()
proc.stdin.write(markup.encode())
async def actionHandler() -> None:
assert proc.stdout
while True:
line = await proc.stdout.readline()
command = line.decode().strip()
callback = self.actions.get(command)
if callback is None:
# In some conditions on start it's empty
log.error(f"Unknown command: {command}")
return
callback()
async with self.taskGroup:
self.addLongRunningTask(refresher())
self.addLongRunningTask(actionHandler())
for provider in self.providers:
self.addLongRunningTask(provider.run())
def exit() -> None:
log.info("Terminating")
for task in self.longRunningTasks:
task.cancel()
loop = asyncio.get_event_loop()
loop.add_signal_handler(signal.SIGINT, exit)
def generateMarkup(self) -> str:
return "".join(screen.getMarkup() for screen in self.children) + "\n"
def addProvider(
self,
provider: "Provider",
alignment: Alignment = Alignment.LEFT,
screenNum: int | None = None,
) -> None:
"""
screenNum: the provider will be added on this screen if set, all otherwise
"""
modules = list()
for s, screen in enumerate(self.children):
if screenNum is None or s == screenNum:
side = next(filter(lambda s: s.alignment == alignment, screen.children))
module = Module(parent=side)
modules.append(module)
provider.modules = modules
if modules:
self.providers.append(provider)
def addAction(self, callback: typing.Callable) -> str:
command = f"{self.actionIndex:x}"
self.actions[command] = callback
self.actionIndex += 1
return command
def removeAction(self, command: str) -> None:
del self.actions[command]
def launch(self) -> None:
# Using GLib's event loop so we can run GLib's code
policy = gi.events.GLibEventLoopPolicy()
asyncio.set_event_loop_policy(policy)
loop = policy.get_event_loop()
loop.run_until_complete(self.run())
class Provider:
sectionType: type[Section] = Section
def __init__(self, color: rich.color.Color = rich.color.Color.default()) -> None:
self.modules: list[Module] = list()
self.color = color
async def run(self) -> None:
# Not a NotImplementedError, otherwise can't combine all classes
pass
class MirrorProvider(Provider):
def __init__(self, color: rich.color.Color = rich.color.Color.default()) -> None:
super().__init__(color=color)
self.module: Module
async def run(self) -> None:
await super().run()
self.module = self.modules[0]
for module in self.modules[1:]:
module.mirror(self.module)
class SingleSectionProvider(MirrorProvider):
async def run(self) -> None:
await super().run()
self.section = self.sectionType(parent=self.module, color=self.color)
class StaticProvider(SingleSectionProvider):
def __init__(
self, text: str, color: rich.color.Color = rich.color.Color.default()
) -> None:
super().__init__(color=color)
self.text = text
async def run(self) -> None:
await super().run()
self.section.setText(self.text)
class SpacerSection(Section):
pass
class SpacerProvider(SingleSectionProvider):
sectionType = SpacerSection
def __init__(self, length: int = 5) -> None:
super().__init__(color=rich.color.Color.default())
self.length = length
async def run(self) -> None:
await super().run()
assert isinstance(self.section, SpacerSection)
self.section.setText(" " * self.length)
class StatefulSection(Section):
def __init__(
self,
parent: Module,
sortKey: Sortable = 0,
color: rich.color.Color = rich.color.Color.default(),
) -> None:
super().__init__(parent=parent, sortKey=sortKey, color=color)
self.state = 0
self.numberStates: int
self.setAction(Button.CLICK_LEFT, self.incrementState)
self.setAction(Button.CLICK_RIGHT, self.decrementState)
def incrementState(self) -> None:
self.state += 1
self.changeState()
def decrementState(self) -> None:
self.state -= 1
self.changeState()
def setChangedState(self, callback: typing.Callable) -> None:
self.callback = callback
def changeState(self) -> None:
self.state %= self.numberStates
self.bar.taskGroup.create_task(self.callback())
class StatefulSectionProvider(Provider):
sectionType = StatefulSection
class SingleStatefulSectionProvider(StatefulSectionProvider, SingleSectionProvider):
section: StatefulSection
class MultiSectionsProvider(Provider):
def __init__(self, color: rich.color.Color = rich.color.Color.default()) -> None:
super().__init__(color=color)
self.sectionKeys: dict[Module, dict[Sortable, Section]] = (
collections.defaultdict(dict)
)
self.updaters: dict[Section, typing.Callable] = dict()
async def getSectionUpdater(self, section: Section) -> typing.Callable:
raise NotImplementedError()
@staticmethod
async def doNothing() -> None:
pass
async def updateSections(self, sections: set[Sortable], module: Module) -> None:
moduleSections = self.sectionKeys[module]
async with asyncio.TaskGroup() as tg:
for sortKey in sections:
section = moduleSections.get(sortKey)
if not section:
section = self.sectionType(
parent=module, sortKey=sortKey, color=self.color
)
self.updaters[section] = await self.getSectionUpdater(section)
moduleSections[sortKey] = section
updater = self.updaters[section]
tg.create_task(updater())
missingKeys = set(moduleSections.keys()) - sections
for missingKey in missingKeys:
section = moduleSections.get(missingKey)
assert section
section.setText(None)
class PeriodicProvider(Provider):
async def init(self) -> None:
pass
async def loop(self) -> None:
raise NotImplementedError()
@classmethod
async def task(cls, bar: Bar) -> None:
providers = list()
for provider in bar.providers:
if isinstance(provider, PeriodicProvider):
providers.append(provider)
await provider.init()
while True:
# TODO Block bar update during the periodic update of the loops
loops = [provider.loop() for provider in providers]
asyncio.gather(*loops)
now = datetime.datetime.now()
# Hardcoded to 1 second... not sure if we want some more than that,
# and if the logic to check if a task should run would be a win
# compared to the task itself
remaining = 1 - now.microsecond / 1000000
await asyncio.sleep(remaining)
async def run(self) -> None:
await super().run()
for module in self.modules:
bar = module.getFirstParentOfType(Bar)
assert bar
if not bar.periodicProviderTask:
bar.periodicProviderTask = PeriodicProvider.task(bar)
bar.addLongRunningTask(bar.periodicProviderTask)
class PeriodicStatefulProvider(SingleStatefulSectionProvider, PeriodicProvider):
async def run(self) -> None:
await super().run()
self.section.setChangedState(self.loop)
class AlertingProvider(Provider):
COLOR_NORMAL = rich.color.Color.parse("green")
COLOR_WARNING = rich.color.Color.parse("yellow")
COLOR_DANGER = rich.color.Color.parse("red")
warningThreshold: float
dangerThreshold: float
def updateLevel(self, level: float) -> None:
if level > self.dangerThreshold:
color = self.COLOR_DANGER
elif level > self.warningThreshold:
color = self.COLOR_WARNING
else:
color = self.COLOR_NORMAL
for module in self.modules:
for section in module.getSections():
section.color = color

View file

@ -0,0 +1,624 @@
import asyncio
import collections
import datetime
import ipaddress
import os
import socket
import time
import typing
import gi
import i3ipc
import i3ipc.aio
import psutil
import pulsectl
import pulsectl_asyncio
import rich.color
from frobar.common import (AlertingProvider, Button, MirrorProvider, Module,
MultiSectionsProvider, PeriodicProvider,
PeriodicStatefulProvider, Screen, Section,
SingleSectionProvider, StatefulSection,
StatefulSectionProvider, clip, humanSize, log, ramp)
gi.require_version("Playerctl", "2.0")
import gi.repository.Playerctl
class I3ModeProvider(SingleSectionProvider):
def on_mode(self, i3: i3ipc.Connection, e: i3ipc.Event) -> None:
self.section.setText(None if e.change == "default" else e.change)
async def run(self) -> None:
await super().run()
i3 = await i3ipc.aio.Connection(auto_reconnect=True).connect()
i3.on(i3ipc.Event.MODE, self.on_mode)
await i3.main()
# TODO Hide WorkspaceProvider when this is active
class I3WindowTitleProvider(SingleSectionProvider):
# TODO FEAT To make this available from start, we need to find the
# `focused=True` element following the `focus` array
def on_window(self, i3: i3ipc.Connection, e: i3ipc.Event) -> None:
if e.container.name is None:
self.section.setText(None)
else:
self.section.setText(clip(e.container.name, 60))
async def run(self) -> None:
await super().run()
i3 = await i3ipc.aio.Connection(auto_reconnect=True).connect()
i3.on(i3ipc.Event.WINDOW, self.on_window)
await i3.main()
class I3WorkspacesProvider(MultiSectionsProvider):
COLOR_URGENT = rich.color.Color.parse("red")
COLOR_FOCUSED = rich.color.Color.parse("yellow")
# TODO Should be orange (not a terminal color)
COLOR_VISIBLE = rich.color.Color.parse("cyan")
COLOR_DEFAULT = rich.color.Color.parse("bright_black")
def __init__(
self,
custom_names: dict[str, str] = {},
) -> None:
super().__init__()
self.workspaces: dict[int, i3ipc.WorkspaceReply]
self.custom_names = custom_names
self.modulesFromOutput: dict[str, Module] = dict()
async def getSectionUpdater(self, section: Section) -> typing.Callable:
assert isinstance(section.sortKey, int)
num = section.sortKey
def switch_to_workspace() -> None:
self.bar.taskGroup.create_task(self.i3.command(f"workspace number {num}"))
section.setAction(Button.CLICK_LEFT, switch_to_workspace)
async def update() -> None:
workspace = self.workspaces.get(num)
if workspace is None:
log.warning(f"Can't find workspace {num}")
section.setText("X")
return
name = workspace.name
if workspace.urgent:
section.color = self.COLOR_URGENT
elif workspace.focused:
section.color = self.COLOR_FOCUSED
elif workspace.visible:
section.color = self.COLOR_VISIBLE
else:
section.color = self.COLOR_DEFAULT
if workspace.focused:
name = self.custom_names.get(name, name)
section.setText(name)
return update
async def updateWorkspaces(self) -> None:
"""
Since the i3 IPC interface cannot really tell you by events
when workspaces get invisible or not urgent anymore.
Relying on those exclusively would require reimplementing some of i3 logic.
Fetching all the workspaces on event looks ugly but is the most maintainable.
Times I tried to challenge this and failed: 2.
"""
workspaces = await self.i3.get_workspaces()
self.workspaces = dict()
modules = collections.defaultdict(set)
for workspace in workspaces:
self.workspaces[workspace.num] = workspace
module = self.modulesFromOutput[workspace.output]
modules[module].add(workspace.num)
await asyncio.gather(
*[self.updateSections(nums, module) for module, nums in modules.items()]
)
def onWorkspaceChange(
self, i3: i3ipc.Connection, e: i3ipc.Event | None = None
) -> None:
# Cancelling the task doesn't seem to prevent performance double-events
self.bar.taskGroup.create_task(self.updateWorkspaces())
async def run(self) -> None:
for module in self.modules:
screen = module.getFirstParentOfType(Screen)
output = screen.output
self.modulesFromOutput[output] = module
self.bar = module.bar
self.i3 = await i3ipc.aio.Connection(auto_reconnect=True).connect()
self.i3.on(i3ipc.Event.WORKSPACE, self.onWorkspaceChange)
self.onWorkspaceChange(self.i3)
await self.i3.main()
class MprisProvider(MirrorProvider):
STATUSES = {
gi.repository.Playerctl.PlaybackStatus.PLAYING: "",
gi.repository.Playerctl.PlaybackStatus.PAUSED: "",
gi.repository.Playerctl.PlaybackStatus.STOPPED: "",
}
PROVIDERS = {
"mpd": "",
"firefox": "",
"chromium": "",
"mpv": "",
}
async def run(self) -> None:
await super().run()
self.status = self.sectionType(parent=self.module, color=self.color)
self.album = self.sectionType(parent=self.module, color=self.color)
self.artist = self.sectionType(parent=self.module, color=self.color)
self.title = self.sectionType(parent=self.module, color=self.color)
self.manager = gi.repository.Playerctl.PlayerManager()
self.manager.connect("name-appeared", self.on_name_appeared)
self.manager.connect("player-vanished", self.on_player_vanished)
self.playerctldName = gi.repository.Playerctl.PlayerName()
self.playerctldName.name = "playerctld"
self.playerctldName.source = gi.repository.Playerctl.Source.DBUS_SESSION
self.player: gi.repository.Playerctl.Player | None = None
self.playing = asyncio.Event()
for name in self.manager.props.player_names:
self.init_player(name)
self.updateSections()
while True:
# Occasionally it will skip a second
# but haven't managed to reproduce with debug info
await self.playing.wait()
self.updateTitle()
if self.player:
pos = self.player.props.position
rem = 1 - (pos % 1000000) / 1000000
await asyncio.sleep(rem)
else:
self.playing.clear()
@staticmethod
def get(
something: gi.overrides.GLib.Variant, key: str, default: typing.Any = None
) -> typing.Any:
if key in something.keys():
return something[key]
else:
return default
@staticmethod
def formatUs(ms: int) -> str:
if ms < 60 * 60 * 1000000:
return time.strftime("%M:%S", time.gmtime(ms // 1000000))
else:
return str(datetime.timedelta(microseconds=ms))
def findCurrentPlayer(self) -> None:
for name in [self.playerctldName] + self.manager.props.player_names:
# TODO Test what happens when playerctld is not available
self.player = gi.repository.Playerctl.Player.new_from_name(name)
if not self.player.props.can_play:
continue
break
else:
self.player = None
def updateSections(self) -> None:
self.findCurrentPlayer()
if self.player is None:
self.status.setText(None)
self.album.setText(None)
self.artist.setText(None)
self.title.setText(None)
self.playing.clear()
return
player = self.player.props.player_name
player = self.PROVIDERS.get(player, player)
status = self.STATUSES.get(self.player.props.playback_status, "?")
self.status.setText(f"{player} {status}")
if (
self.player.props.playback_status
== gi.repository.Playerctl.PlaybackStatus.PLAYING
):
self.playing.set()
else:
self.playing.clear()
metadata = self.player.props.metadata
album = self.get(metadata, "xesam:album")
if album:
self.album.setText(f"{clip(album)}")
else:
self.album.setText(None)
artists = self.get(metadata, "xesam:artist")
if artists:
artist = ", ".join(artists)
self.artist.setText(f"{clip(artist)}")
else:
self.artist.setText(None)
self.updateTitle()
def updateTitle(self) -> None:
if self.player is None:
return
metadata = self.player.props.metadata
pos = self.player.props.position # In µs
text = f"{self.formatUs(pos)}"
dur = self.get(metadata, "mpris:length")
if dur:
text += f"/{self.formatUs(dur)}"
title = self.get(metadata, "xesam:title")
if title:
text += f" {clip(title)}"
self.title.setText(text)
def on_player_vanished(
self,
manager: gi.repository.Playerctl.PlayerManager,
player: gi.repository.Playerctl.Player,
) -> None:
self.updateSections()
def on_event(
self,
player: gi.repository.Playerctl.Player,
_: typing.Any,
manager: gi.repository.Playerctl.PlayerManager,
) -> None:
self.updateSections()
def init_player(self, name: gi.repository.Playerctl.PlayerName) -> None:
player = gi.repository.Playerctl.Player.new_from_name(name)
# All events will cause the active player to change,
# so we listen on all events, even if the display won't change
player.connect("playback-status", self.on_event, self.manager)
player.connect("loop-status", self.on_event, self.manager)
player.connect("shuffle", self.on_event, self.manager)
player.connect("metadata", self.on_event, self.manager)
player.connect("volume", self.on_event, self.manager)
player.connect("seeked", self.on_event, self.manager)
self.manager.manage_player(player)
def on_name_appeared(
self, manager: gi.repository.Playerctl.PlayerManager, name: str
) -> None:
self.init_player(name)
self.updateSections()
class CpuProvider(AlertingProvider, PeriodicStatefulProvider):
async def init(self) -> None:
self.section.numberStates = 3
self.warningThreshold = 75
self.dangerThreshold = 95
async def loop(self) -> None:
percent = psutil.cpu_percent(percpu=False)
self.updateLevel(percent)
text = ""
if self.section.state >= 2:
percents = psutil.cpu_percent(percpu=True)
text += " " + "".join([ramp(p / 100) for p in percents])
elif self.section.state >= 1:
text += " " + ramp(percent / 100)
self.section.setText(text)
class LoadProvider(AlertingProvider, PeriodicStatefulProvider):
async def init(self) -> None:
self.section.numberStates = 3
self.warningThreshold = 5
self.dangerThreshold = 10
async def loop(self) -> None:
load = os.getloadavg()
self.updateLevel(load[0])
text = ""
loads = 3 if self.section.state >= 2 else self.section.state
for load_index in range(loads):
text += f" {load[load_index]:.2f}"
self.section.setText(text)
class RamProvider(AlertingProvider, PeriodicStatefulProvider):
async def init(self) -> None:
self.section.numberStates = 4
self.warningThreshold = 75
self.dangerThreshold = 95
async def loop(self) -> None:
mem = psutil.virtual_memory()
self.updateLevel(mem.percent)
text = ""
if self.section.state >= 1:
text += " " + ramp(mem.percent / 100)
if self.section.state >= 2:
text += humanSize(mem.total - mem.available)
if self.section.state >= 3:
text += "/" + humanSize(mem.total)
self.section.setText(text)
class TemperatureProvider(AlertingProvider, PeriodicStatefulProvider):
RAMP = ""
MAIN_TEMPS = ["coretemp", "amdgpu", "cpu_thermal"]
# For Intel, AMD and ARM respectively.
main: str
async def init(self) -> None:
self.section.numberStates = 2
allTemp = psutil.sensors_temperatures()
for main in self.MAIN_TEMPS:
if main in allTemp:
self.main = main
break
else:
raise IndexError("Could not find suitable temperature sensor")
temp = allTemp[self.main][0]
self.warningThreshold = temp.high or 90.0
self.dangerThreshold = temp.critical or 100.0
async def loop(self) -> None:
allTemp = psutil.sensors_temperatures()
temp = allTemp[self.main][0]
self.updateLevel(temp.current)
text = ramp(temp.current / self.warningThreshold, self.RAMP)
if self.section.state >= 1:
text += f" {temp.current:.0f}°C"
self.section.setText(text)
class BatteryProvider(AlertingProvider, PeriodicStatefulProvider):
# TODO Support ACPID for events
RAMP = ""
async def init(self) -> None:
self.section.numberStates = 3
# TODO 1 refresh rate is too quick
self.warningThreshold = 75
self.dangerThreshold = 95
async def loop(self) -> None:
bat = psutil.sensors_battery()
if not bat:
self.section.setText(None)
self.updateLevel(100 - bat.percent)
text = "" if bat.power_plugged else ""
text += ramp(bat.percent / 100, self.RAMP)
if self.section.state >= 1:
text += f" {bat.percent:.0f}%"
if self.section.state >= 2:
h = int(bat.secsleft / 3600)
m = int((bat.secsleft - h * 3600) / 60)
text += f" ({h:d}:{m:02d})"
self.section.setText(text)
class PulseaudioProvider(
MirrorProvider, StatefulSectionProvider, MultiSectionsProvider
):
async def getSectionUpdater(self, section: Section) -> typing.Callable:
assert isinstance(section, StatefulSection)
assert isinstance(section.sortKey, str)
sink = self.sinks[section.sortKey]
icon = "?"
if sink.port_active is None:
pass
elif (
sink.port_active.name == "analog-output-headphones"
or sink.port_active.description == "Headphones"
):
icon = ""
elif (
sink.port_active.name == "analog-output-speaker"
or sink.port_active.description == "Speaker"
):
icon = ""
elif sink.port_active.name in ("headset-output", "headphone-output"):
icon = ""
section.numberStates = 3
section.state = 1
# TODO Change volume with wheel
async def updater() -> None:
assert isinstance(section, StatefulSection)
text = icon
sink = self.sinks[section.sortKey]
async with pulsectl_asyncio.PulseAsync("frobar-get-volume") as pulse:
vol = await pulse.volume_get_all_chans(sink)
if section.state == 1:
text += f" {ramp(vol)}"
elif section.state == 2:
text += f" {vol:.0%}"
# TODO Show which is default
section.setText(text)
section.setChangedState(updater)
return updater
async def update(self) -> None:
async with pulsectl_asyncio.PulseAsync("frobar-list-sinks") as pulse:
self.sinks = dict((sink.name, sink) for sink in await pulse.sink_list())
await self.updateSections(set(self.sinks.keys()), self.module)
async def run(self) -> None:
await super().run()
await self.update()
async with pulsectl_asyncio.PulseAsync("frobar-events-listener") as pulse:
async for event in pulse.subscribe_events(pulsectl.PulseEventMaskEnum.sink):
await self.update()
class NetworkProvider(
MirrorProvider, PeriodicProvider, StatefulSectionProvider, MultiSectionsProvider
):
def __init__(
self,
color: rich.color.Color = rich.color.Color.default(),
) -> None:
super().__init__(color=color)
async def init(self) -> None:
loop = asyncio.get_running_loop()
self.time = loop.time()
self.io_counters = psutil.net_io_counters(pernic=True)
@staticmethod
def getIfaceAttributes(iface: str) -> tuple[bool, str, bool]:
relevant = True
icon = "?"
wifi = False
if iface == "lo":
relevant = False
elif iface.startswith("eth") or iface.startswith("enp"):
if "u" in iface:
icon = ""
else:
icon = ""
elif iface.startswith("wlan") or iface.startswith("wl"):
icon = ""
wifi = True
elif (
iface.startswith("tun") or iface.startswith("tap") or iface.startswith("wg")
):
icon = ""
elif iface.startswith("docker"):
icon = ""
elif iface.startswith("veth"):
icon = ""
elif iface.startswith("vboxnet"):
icon = ""
return relevant, icon, wifi
async def getSectionUpdater(self, section: Section) -> typing.Callable:
assert isinstance(section, StatefulSection)
assert isinstance(section.sortKey, str)
iface = section.sortKey
relevant, icon, wifi = self.getIfaceAttributes(iface)
if not relevant:
return self.doNothing
section.numberStates = 5 if wifi else 4
section.state = 1 if wifi else 0
async def update() -> None:
assert isinstance(section, StatefulSection)
if not self.if_stats[iface].isup:
section.setText(None)
return
text = icon
state = section.state + (0 if wifi else 1)
if wifi and state >= 1: # SSID
cmd = ["iwgetid", iface, "--raw"]
proc = await asyncio.create_subprocess_exec(
*cmd, stdout=asyncio.subprocess.PIPE
)
stdout, stderr = await proc.communicate()
text += f" {stdout.decode().strip()}"
if state >= 2: # Address
for address in self.if_addrs[iface]:
if address.family == socket.AF_INET:
net = ipaddress.IPv4Network(
(address.address, address.netmask), strict=False
)
text += f" {address.address}/{net.prefixlen}"
break
if state >= 3: # Speed
prevRecv = self.prev_io_counters[iface].bytes_recv
recv = self.io_counters[iface].bytes_recv
prevSent = self.prev_io_counters[iface].bytes_sent
sent = self.io_counters[iface].bytes_sent
dt = self.time - self.prev_time
recvDiff = (recv - prevRecv) / dt
sentDiff = (sent - prevSent) / dt
text += f"{humanSize(recvDiff)}{humanSize(sentDiff)}"
if state >= 4: # Counter
text += f"{humanSize(recv)}{humanSize(sent)}"
section.setText(text)
section.setChangedState(update)
return update
async def loop(self) -> None:
loop = asyncio.get_running_loop()
self.prev_io_counters = self.io_counters
self.prev_time = self.time
# On-demand would only benefit if_addrs:
# stats are used to determine display,
# and we want to keep previous io_counters
# so displaying stats is ~instant.
self.time = loop.time()
self.if_stats = psutil.net_if_stats()
self.if_addrs = psutil.net_if_addrs()
self.io_counters = psutil.net_io_counters(pernic=True)
await self.updateSections(set(self.if_stats.keys()), self.module)
class TimeProvider(PeriodicStatefulProvider):
FORMATS = ["%H:%M", "%m-%d %H:%M:%S", "%a %y-%m-%d %H:%M:%S"]
async def init(self) -> None:
self.section.state = 1
self.section.numberStates = len(self.FORMATS)
async def loop(self) -> None:
now = datetime.datetime.now()
format = self.FORMATS[self.section.state]
self.section.setText(now.strftime(format))

View file

@ -0,0 +1,32 @@
{
pkgs,
lib,
config,
...
}:
{
config = lib.mkIf config.frogeye.desktop.xorg {
xsession.windowManager.i3.config.bars = [ ];
programs.autorandr.hooks.postswitch = {
frobar = "${pkgs.systemd}/bin/systemctl --user restart frobar";
};
systemd.user.services.frobar = {
Unit = {
Description = "frobar";
After = [ "graphical-session-pre.target" ];
PartOf = [ "graphical-session.target" ];
};
Service = {
# Wait for i3 to start. Can't use ExecStartPre because otherwise it blocks graphical-session.target, and there's nothing i3/systemd
# TODO Do that better
ExecStart = ''${pkgs.bash}/bin/bash -c "while ! ${pkgs.i3}/bin/i3-msg; do ${pkgs.coreutils}/bin/sleep 1; done; ${pkgs.callPackage ./. { }}/bin/frobar"'';
};
Install = {
WantedBy = [ "graphical-session.target" ];
};
};
};
}
# TODO Connection with i3 is lost on start sometimes, more often than with Arch?

View file

@ -2,19 +2,17 @@ from setuptools import setup
setup( setup(
name="frobar", name="frobar",
version="2.0", version="3.0",
install_requires=[ install_requires=[
"coloredlogs",
"notmuch",
"i3ipc", "i3ipc",
"python-mpd2",
"psutil", "psutil",
"pulsectl", "pulsectl-asyncio",
"pyinotify", "pygobject3",
"rich",
], ],
entry_points={ entry_points={
"console_scripts": [ "console_scripts": [
"frobar = frobar:run", "frobar = frobar:main",
] ]
}, },
) )

306
hm/desktop/i3.nix Normal file
View file

@ -0,0 +1,306 @@
{
pkgs,
lib,
config,
...
}:
let
# FOCUS
focus = "exec ${pkgs.writeShellScript "i3-focus-window" ''
WINDOW=`${pkgs.xdotool}/bin/xdotool getwindowfocus`
eval `${pkgs.xdotool}/bin/xdotool getwindowgeometry --shell $WINDOW` # this brings in variables WIDTH and HEIGHT
TX=`${pkgs.coreutils}/bin/expr $WIDTH / 2`
TY=`${pkgs.coreutils}/bin/expr $HEIGHT / 2`
${pkgs.xdotool}/bin/xdotool mousemove -window $WINDOW $TX $TY
''}";
# CARDINALS
cardinals = [
{
vi = "h";
arrow = "Left";
container = "left";
workspace = "prev_on_output";
output = "left";
}
{
vi = "l";
arrow = "Right";
container = "right";
workspace = "next_on_output";
output = "right";
}
{
vi = "j";
arrow = "Down";
container = "down";
workspace = "prev";
output = "below";
}
{
vi = "k";
arrow = "Up";
container = "up";
workspace = "next";
output = "above";
}
];
forEachCardinal = f: map (c: f c) cardinals;
# WORKSPACES
workspaces_keys = lib.strings.stringToCharacters "1234567890";
workspaces = map (i: {
id = i;
name = builtins.toString (i + 1);
key = builtins.elemAt workspaces_keys i;
}) (lib.lists.range 0 ((builtins.length workspaces_keys) - 1));
forEachWorkspace = f: map (w: f w) workspaces;
# MISC
mod = config.xsession.windowManager.i3.config.modifier;
rofi = "exec --no-startup-id ${config.programs.rofi.package}/bin/rofi";
modes = config.frogeye.desktop.i3.bindmodes;
x11_screens = config.frogeye.desktop.x11_screens;
in
{
config = lib.mkIf config.xsession.windowManager.i3.enable {
stylix.targets.i3.enable = false;
services.picom.enable = true;
xdg.configFile = {
"rofimoji.rc" = {
text = ''
skin-tone = neutral
files = [emojis, math]
action = clipboard
'';
};
};
xsession.windowManager.i3.config = {
modifier = lib.mkDefault "Mod4";
fonts = {
names = [ config.stylix.fonts.sansSerif.name ];
};
terminal = "alacritty";
colors =
let
ignore = "#ff00ff";
in
with config.lib.stylix.colors.withHashtag;
lib.mkForce {
focused = {
border = base0B;
background = base0B;
text = base00;
indicator = base00;
childBorder = base0B;
};
focusedInactive = {
border = base02;
background = base02;
text = base05;
indicator = base02;
childBorder = base02;
};
unfocused = {
border = base05;
background = base04;
text = base00;
indicator = base04;
childBorder = base00;
};
urgent = {
border = base0F;
background = base08;
text = base00;
indicator = base08;
childBorder = base0F;
};
placeholder = {
border = ignore;
background = base00;
text = base05;
indicator = ignore;
childBorder = base00;
};
background = base07;
# I set the color of the active tab as the the background color of the terminal so they merge together.
};
focus.followMouse = false;
keybindings =
{
# Compatibility layer for people coming from other backgrounds
"Mod1+Tab" = "${rofi} -modi window -show window";
"Mod1+F2" = "${rofi} -modi drun -show drun";
"Mod1+F4" = "kill";
# kill focused window
"${mod}+z" = "kill";
button2 = "kill";
# Rofi
"${mod}+i" = "exec --no-startup-id ${pkgs.rofimoji}/bin/rofimoji";
# start program launcher
"${mod}+d" = "${rofi} -modi run -show run";
"${mod}+Shift+d" = "${rofi} -modi drun -show drun";
# Start Applications
"${mod}+p" = "exec ${pkgs.xfce.thunar}/bin/thunar";
# Misc
"${mod}+F10" = "exec ${pkgs.writeShellScript "show-keyboard-layout" ''
layout=`${pkgs.xorg.setxkbmap}/bin/setxkbmap -query | ${pkgs.gnugrep}/bin/grep ^layout: | ${pkgs.gawk}/bin/awk '{ print $2 }'`
${pkgs.libgnomekbd}/bin/gkbd-keyboard-display -l $layout
''}";
# workspace back and forth (with/without active container)
"${mod}+b" = "workspace back_and_forth; ${focus}";
"${mod}+Shift+b" = "move container to workspace back_and_forth; workspace back_and_forth; ${focus}";
# Change container layout
"${mod}+g" = "split h; ${focus}";
"${mod}+v" = "split v; ${focus}";
"${mod}+f" = "fullscreen toggle; ${focus}";
"${mod}+s" = "layout stacking; ${focus}";
"${mod}+w" = "layout tabbed; ${focus}";
"${mod}+e" = "layout toggle split; ${focus}";
"${mod}+Shift+space" = "floating toggle; ${focus}";
# Focus container
"${mod}+space" = "focus mode_toggle; ${focus}";
"${mod}+a" = "focus parent; ${focus}";
"${mod}+q" = "focus child; ${focus}";
# i3 control
"${mod}+Shift+c" = "reload";
"${mod}+Shift+r" = "restart";
"${mod}+Shift+e" = "exit";
}
// lib.mapAttrs' (k: v: lib.nameValuePair v.enter "mode ${v.name}") (
lib.filterAttrs (k: v: v.enter != null) modes
)
// lib.attrsets.mergeAttrsList (
forEachCardinal (c: {
# change focus
"${mod}+${c.vi}" = "focus ${c.container}; ${focus}";
# move focused window
"${mod}+Shift+${c.vi}" = "move ${c.container}; ${focus}";
#navigate workspaces next / previous
"${mod}+Ctrl+${c.vi}" = "workspace ${c.workspace}; ${focus}";
# Move to workspace next / previous with focused container
"${mod}+Ctrl+Shift+${c.vi}" = "move container to workspace ${c.workspace}; workspace ${c.workspace}; ${focus}";
# move workspaces to screen (arrow keys)
"${mod}+Ctrl+Shift+${c.arrow}" = "move workspace to output ${c.output}; ${focus}";
})
)
// lib.attrsets.mergeAttrsList (
forEachWorkspace (w: {
# Switch to workspace
"${mod}+${w.key}" = "workspace ${w.name}; ${focus}";
# move focused container to workspace
"${mod}+ctrl+${w.key}" = "move container to workspace ${w.name}; ${focus}";
# move to workspace with focused container
"${mod}+shift+${w.key}" = "move container to workspace ${w.name}; workspace ${w.name}; ${focus}";
})
);
modes = lib.mapAttrs' (
k: v:
lib.nameValuePair v.name (
v.bindings
// lib.optionalAttrs v.return_bindings {
"Return" = "mode default";
"Escape" = "mode default";
}
)
) modes;
window = {
hideEdgeBorders = "both";
titlebar = false; # So that single-container screens are basically almost fullscreen
commands = [
# switch to workspace with urgent window automatically
{
criteria = {
urgent = "latest";
};
command = "focus";
}
];
};
floating = {
criteria = [
{ window_role = "pop-up"; }
{ window_role = "task_dialog"; }
];
};
startup = [
{
notification = false;
command = "${
pkgs.writeShellApplication {
name = "batteryNotify";
runtimeInputs = with pkgs; [
coreutils
libnotify
];
text = builtins.readFile ./batteryNotify.sh;
# TODO Use batsignal instead?
# TODO Only on computers with battery
}
}/bin/batteryNotify";
}
];
workspaceLayout = "tabbed";
focus.mouseWarping = true; # i3 only supports warping to workspace, hence ${focus}
workspaceOutputAssign = forEachWorkspace (w: {
output = builtins.elemAt x11_screens (lib.mod w.id (builtins.length x11_screens));
workspace = w.name;
});
};
frogeye.desktop.i3.bindmodes = {
"Resize" = {
bindings = {
"h" = "resize shrink width 10 px or 10 ppt; ${focus}";
"j" = "resize grow height 10 px or 10 ppt; ${focus}";
"k" = "resize shrink height 10 px or 10 ppt; ${focus}";
"l" = "resize grow width 10 px or 10 ppt; ${focus}";
};
mod_enter = "r";
};
"[L] Vérouillage [E] Déconnexion [S] Veille [H] Hibernation [R] Redémarrage [P] Extinction" = {
bindings = {
"l" = "exec --no-startup-id exec xlock, mode default";
"e" = "exit, mode default";
"s" = "exec --no-startup-id exec xlock & ${pkgs.systemd}/bin/systemctl suspend --check-inhibitors=no, mode default";
"h" = "exec --no-startup-id exec xlock & ${pkgs.systemd}/bin/systemctl hibernate, mode default";
"r" = "exec --no-startup-id ${pkgs.systemd}/bin/systemctl reboot, mode default";
"p" = "exec --no-startup-id ${pkgs.systemd}/bin/systemctl poweroff -i, mode default";
};
mod_enter = "Escape";
};
};
};
options = {
frogeye.desktop.i3.bindmodes = lib.mkOption {
default = { };
type = lib.types.attrsOf (
lib.types.submodule (
{ config, name, ... }:
{
options = {
name = lib.mkOption {
type = lib.types.str;
default = name;
};
bindings = lib.mkOption {
type = lib.types.attrsOf lib.types.str;
default = { };
};
enter = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = "${mod}+${config.mod_enter}";
};
mod_enter = lib.mkOption {
type = lib.types.str;
};
return_bindings = lib.mkOption {
type = lib.types.bool;
default = true;
};
};
}
)
);
};
};
}

View file

@ -0,0 +1,86 @@
{
pkgs,
lib,
config,
...
}:
let
# lockColors = with config.lib.stylix.colors.withHashtag; { a = base00; b = base01; d = base00; }; # Black or White, depending on current theme
# lockColors = with config.lib.stylix.colors.withHashtag; { a = base0A; b = base0B; d = base00; }; # Green + Yellow
lockColors = {
a = "#82a401";
b = "#466c01";
d = "#648901";
}; # Old
lockSvg = pkgs.writeText "lock.svg" ''
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50" height="50" width="50">
<path fill="${lockColors.a}" d="M0 50h50V0H0z"/>
<path d="M0 0l50 50H25L0 25zm50 0v25L25 0z" fill="${lockColors.b}"/>
</svg>
'';
lockPng = pkgs.runCommand "lock.png" { } "${pkgs.imagemagick}/bin/convert ${lockSvg} $out";
mod = config.xsession.windowManager.i3.config.modifier;
xautolockState = "${config.xdg.cacheHome}/xautolock";
in
{
config = lib.mkIf config.frogeye.desktop.xorg {
home.packages = with pkgs; [
(pkgs.writeShellApplication {
name = "xlock";
text = ''
${config.frogeye.hooks.lock}
# TODO Reevaluate whether we want this or not
if ! ${pkgs.lightdm}/bin/dm-tool lock
then
if [ -d ${config.xdg.cacheHome}/lockpatterns ]
then
pattern=$(${pkgs.findutils} ${config.xdg.cacheHome}/lockpatterns | sort -R | head -1)
else
pattern=${lockPng}
fi
revert() {
${pkgs.xorg.xset}/bin/xset dpms 0 0 0
}
trap revert SIGHUP SIGINT SIGTERM
${pkgs.xorg.xset}/bin/xset dpms 5 5 5
${pkgs.i3lock}/bin/i3lock --nofork --color ${
builtins.substring 1 6 lockColors.d
} --image="$pattern" --tiling --ignore-empty-password
revert
fi
'';
})
];
xsession.windowManager.i3.config = {
keybindings = {
# Screen off commands
"${mod}+F1" = "--release exec --no-startup-id ${pkgs.xorg.xset}/bin/xset dpms force off";
# Toggle to save on buttons
# xautolock -toggle doesn't allow to read state.
# Writing into a file also allows frobar to display a lock icon
"${mod}+F5" = "exec --no-startup-id ${pkgs.writeShellScript "xautolock-toggle" ''
state="$(cat "${xautolockState}")"
if [ "$state" = "disabled" ]
then
${pkgs.xautolock}/bin/xautolock -enable
echo enabled > ${xautolockState}
else
${pkgs.xautolock}/bin/xautolock -disable
echo disabled > ${xautolockState}
fi
''}";
};
startup = [
# Stop screen after 10 minutes, 1 minutes after lock it
{
notification = false;
command = "${pkgs.writeShellScript "xautolock-start" ''
echo enabled > ${xautolockState}
${pkgs.xautolock}/bin/xautolock -time 10 -locker '${pkgs.xorg.xset}/bin/xset dpms force standby' -killtime 1 -killer xlock
''}";
}
# services.screen-locker.xautolock is hardcoded to use systemd for -locker (doesn't even work...)
];
};
};
}

View file

@ -0,0 +1,67 @@
{
pkgs,
lib,
config,
...
}:
{
config = lib.mkIf config.frogeye.desktop.xorg {
home = {
packages = with pkgs; [
ashuffle
mpc-cli
vimpc
playerctl
];
sessionVariables = {
MPD_PORT = "${toString config.services.mpd.network.port}";
};
};
services = {
mpd = {
enable = true;
network = {
listenAddress = "0.0.0.0"; # Can be controlled remotely, determined with firewall
startWhenNeeded = true;
};
extraConfig = ''
restore_paused "yes"
audio_output {
type "pipewire"
name "PipeWire Sound Server"
}
'';
# UPST auto audio_output ?
musicDirectory = "${config.home.homeDirectory}/Musiques";
};
# Expose mpd to mpris
# mpd-mpris also exists but is MIT and make playerctld not pick up on play/pause events
mpdris2.enable = true;
# Allow control from headset
mpris-proxy.enable = true;
# Remember the last player
playerctld.enable = true;
};
xdg = {
configFile = {
"vimpc/vimpcrc" = {
text = ''
map FF :browse<C-M>gg/
map à :set add next<C-M>a:set add end<C-M>
map @ :set add next<C-M>a:set add end<C-M>:next<C-M>
map ° D:browse<C-M>A:shuffle<C-M>:play<C-M>:playlist<C-M>
set songformat {%a - %b: %t}|{%f}$E$R $H[$H%l$H]$H
set libraryformat %n \| {%t}|{%f}$E$R $H[$H%l$H]$H
set ignorecase
set sort library
'';
};
};
};
xsession.windowManager.i3.config.keybindings = {
"XF86AudioPrev" = "exec ${lib.getExe pkgs.playerctl} previous";
"XF86AudioPlay" = "exec ${lib.getExe pkgs.playerctl} play-pause";
"XF86AudioNext" = "exec ${lib.getExe pkgs.playerctl} next";
};
};
}

View file

@ -0,0 +1,56 @@
# Dual-screen presenting for slideshows and stuff.
# Not tested since Nix.
# Config mentions pdfpc, although the last thing I used was Impressive, even made patches to it.
# UPST Add Impressive to nixpkgs
{
pkgs,
lib,
config,
...
}:
let
mode_pres_main = "Presentation (main display)";
mode_pres_sec = "Presentation (secondary display)";
in
{
config = lib.mkIf config.frogeye.desktop.xorg {
frogeye.desktop.i3.bindmodes = {
"${mode_pres_main}" = {
mod_enter = "Shift+p";
bindings = {
"b" = "workspace 3, workspace 4, mode ${mode_pres_sec}";
"q" = "mode default";
"Return" = "mode default";
};
return_bindings = false;
};
"${mode_pres_sec}" = {
enter = null;
bindings = {
"b" = "workspace 1, workspace 2, mode ${mode_pres_main}";
"q" = "mode default";
"Return" = "mode default";
};
return_bindings = false;
};
};
xsession.windowManager.i3.config.window.commands = [
# Open specific applications in floating mode
{
criteria = {
title = "^pdfpc.*";
window_role = "presenter";
};
command = "move to output left, fullscreen";
}
{
criteria = {
title = "^pdfpc.*";
window_role = "presentation";
};
command = "move to output right, fullscreen";
}
];
};
}

View file

@ -0,0 +1,32 @@
{
pkgs,
lib,
config,
...
}:
let
# UPST
sct = pkgs.sct.overrideAttrs (old: {
patches = (old.patches or [ ]) ++ [
./sct_aarch64.patch
];
});
in
{
config = lib.mkIf config.frogeye.desktop.xorg {
frogeye.desktop.i3.bindmodes = {
"Temperature [R] Red [D] Dust storm [C] Campfire [O] Normal [A] All nighter [B] Blue" = {
bindings = {
"r" = "exec ${sct}/bin/sct 1000";
"d" = "exec ${sct}/bin/sct 2000";
"c" = "exec ${sct}/bin/sct 4500";
"o" = "exec ${sct}/bin/sct";
"a" = "exec ${sct}/bin/sct 8000";
"b" = "exec ${sct}/bin/sct 10000";
};
mod_enter = "y";
};
};
home.packages = [ sct ];
};
}

View file

@ -0,0 +1,21 @@
{
pkgs,
lib,
config,
...
}:
let
dir = config.xdg.userDirs.extraConfig.XDG_SCREENSHOTS_DIR;
scrot = "${pkgs.scrot}/bin/scrot --exec '${pkgs.coreutils}/bin/mv $f ${dir}/ && ${pkgs.optipng}/bin/optipng ${dir}/$f'";
mod = config.xsession.windowManager.i3.config.modifier;
in
{
config = lib.mkIf config.frogeye.desktop.xorg {
frogeye.folders.screenshots.path = "Screenshots";
xsession.windowManager.i3.config.keybindings = {
"Print" = "exec ${scrot} --focused";
"${mod}+Print" = "exec ${scrot}";
"Ctrl+Print" = "--release exec ${scrot} --select";
};
};
}

View file

@ -0,0 +1,158 @@
{
pkgs,
lib,
config,
...
}:
let
mod = config.xsession.windowManager.i3.config.modifier;
in
{
config = lib.mkIf config.frogeye.desktop.xorg {
home.sessionVariables = {
RXVT_SOCKET = "${config.xdg.stateHome}/urxvtd";
# We don't use urxvt deamon mode as we use it as a backup, but just in case, this helps keep it out of the home directory.
};
programs = {
alacritty = {
# TODO Emojis
# Arch (working) shows this with alacritty -vvv:
# [TRACE] [crossfont] Got font path="/usr/share/fonts/twemoji/twemoji.ttf", index=0
# [DEBUG] [crossfont] Loaded Face Face { ft_face: Font Face: Regular, load_flags: MONOCHROME | TARGET_MONO | COLOR, render_mode: "Mono", lcd_filter: 1 }
# Nix (not working) shows this:
# [TRACE] [crossfont] Got font path="/nix/store/872g3w9vcr5nh93r0m83a3yzmpvd2qrj-home-manager-path/share/fonts/truetype/TwitterColorEmoji-SVGinOT.ttf", index=0
# [DEBUG] [crossfont] Loaded Face Face { ft_face: Font Face: Regular, load_flags: TARGET_LIGHT | COLOR, render_mode: "Lcd", lcd_filter: 1 }
enable = true;
settings = {
bell = {
animation = "EaseOutExpo";
color = "#000000";
command = {
program = "${pkgs.sox}/bin/play";
args = [
"-n"
"synth"
"sine"
"C5"
"sine"
"E4"
"remix"
"1-2"
"fade"
"0.1"
"0.2"
"0.1"
];
};
duration = 100;
};
cursor = {
vi_mode_style = "Underline";
};
env = {
WINIT_X11_SCALE_FACTOR = "1";
# Prevents Alacritty from resizing from one monitor to another.
# Might cause issue on HiDPI screens but we'll get there when we get there
};
hints = {
enabled = [
{
binding = {
mods = "Control|Alt";
key = "F";
};
command = "${pkgs.xdg-utils}/bin/xdg-open";
mouse = {
enabled = true;
mods = "Control";
};
post_processing = true;
regex = "(mailto:|gemini:|gopher:|https:|http:|news:|file:|git:|ssh:|ftp:)[^\\u0000-\\u001F\\u007F-\\u009F<>\"\\\\s{-}\\\\^`]+";
}
];
};
keyboard.bindings = [
{
mode = "~Search";
mods = "Alt|Control";
key = "Space";
action = "ToggleViMode";
}
{
mode = "Vi|~Search";
mods = "Control";
key = "K";
action = "ScrollHalfPageUp";
}
{
mode = "Vi|~Search";
mods = "Control";
key = "J";
action = "ScrollHalfPageDown";
}
{
mode = "~Vi";
mods = "Control|Alt";
key = "V";
action = "Paste";
}
{
mods = "Control|Alt";
key = "C";
action = "Copy";
}
{
mode = "~Search";
mods = "Control|Alt";
key = "F";
action = "SearchForward";
}
{
mode = "~Search";
mods = "Control|Alt";
key = "B";
action = "SearchBackward";
}
{
mode = "Vi|~Search";
mods = "Control|Alt";
key = "C";
action = "ClearSelection";
}
];
window = {
dynamic_padding = false;
dynamic_title = true;
};
};
};
# Backup terminal
urxvt = {
enable = true;
package = pkgs.rxvt-unicode-emoji;
scroll = {
bar.enable = false;
};
iso14755 = false; # Disable Ctrl+Shift default bindings
keybindings = {
"Shift-Control-C" = "eval:selection_to_clipboard";
"Shift-Control-V" = "eval:paste_clipboard";
# TODO Not sure resizing works, Nix doesn't have the package (urxvt-resize-font-git on Arch)
"Control-KP_Subtract" = "resize-font:smaller";
"Control-KP_Add" = "resize-font:bigger";
};
extraConfig = {
"letterSpace" = 0;
"perl-ext-common" = "resize-font,bell-command,readline,selection";
"bell-command" = "${pkgs.sox}/bin/play -n synth sine C5 sine E4 remix 1-2 fade 0.1 0.2 0.1 &> /dev/null";
};
};
};
xsession.windowManager.i3.config.keybindings = {
"${mod}+Return" = "exec ${config.programs.alacritty.package}/bin/alacritty msg create-window -e zsh || exec ${config.programs.alacritty.package}/bin/alacritty -e zsh";
# -e zsh is for systems where I can't configure my user's shell
"${mod}+Shift+Return" = "exec ${config.programs.urxvt.package}/bin/urxvt";
};
};
}

View file

@ -1,65 +0,0 @@
{ pkgs, config, ... }: {
# TODO Maybe should be per-directory dotenv
# Or not, for neovim
# Always on
home.packages = with pkgs; [
# Common
perf-tools
jq
yq
universal-ctags
highlight
# Network
socat
dig
whois
nmap
tcpdump
# nix
nix
# Always on (graphical)
] ++ lib.optionals config.frogeye.desktop.xorg [
# Common
zeal-qt6 # Offline documentation
# Network
wireshark-qt
# Ansible
] ++ lib.optionals config.frogeye.dev.ansible [
ansible
ansible-lint
# C/C++
] ++ lib.optionals config.frogeye.dev.c [
cmake
clang
ccache
gdb
# Docker
] ++ lib.optionals config.frogeye.dev.docker [
docker
docker-compose
# FPGA
] ++ lib.optionals config.frogeye.dev.fpga [
verilog
# ghdl # TODO Not on aarch64
# FPGA (graphical)
] ++ lib.optionals (config.frogeye.desktop.xorg && config.frogeye.dev.fpga) [
yosys
gtkwave
# Python
] ++ lib.optionals config.frogeye.dev.python [
python3Packages.ipython
];
}

55
hm/dev/c.nix Normal file
View file

@ -0,0 +1,55 @@
{
pkgs,
lib,
config,
...
}:
{
config = lib.mkIf config.frogeye.dev.c {
frogeye = {
direnv = {
CCACHE_DIR = "${config.xdg.cacheHome}/ccache"; # The config file alone seems to be not enough
};
junkhome = [
"binwalk" # Should use .config according to the GitHub code though
"cmake"
"ddd"
"ghidra"
];
};
home = {
packages = with pkgs; [
binwalk
ccache
clang
cmake
ddd
gdb
gnumake
valgrind
];
sessionVariables = {
CCACHE_CONFIGPATH = "${config.xdg.configHome}/ccache.conf";
};
};
programs.bash.shellAliases = {
gdb = "gdb -x ${config.xdg.configHome}/gdbinit";
};
programs.nixvim.plugins = {
dap.enable = true; # Debug Adapter Protocol client
lsp.servers.clangd.enable = true;
};
xdg.configFile = {
"ccache.conf" = {
text = "ccache_dir = ${config.xdg.cacheHome}/ccache";
};
gdbinit = {
text = ''
define hook-quit
set confirm off
end
'';
};
};
};
}

96
hm/dev/common.nix Normal file
View file

@ -0,0 +1,96 @@
{ pkgs, config, ... }:
{
# TODO Maybe should be per-directory dotenv
# Or not, for neovim
config = {
# Always on
home.packages =
with pkgs;
[
# Common
perf-tools
jq
yq
universal-ctags
cloc
# Network
socat
dig
whois
nmap
tcpdump
mtr
traceroute
# nix
lix
nixfmt-rfc-style
# Always on (graphical)
]
++ lib.optionals config.frogeye.desktop.xorg [
# Common
# zeal-qt6 # Offline documentation
sqlitebrowser
# Network
wireshark-qt
# Ansible
]
++ lib.optionals config.frogeye.dev.ansible [
ansible
ansible-lint
# Docker
]
++ lib.optionals config.frogeye.dev.docker [
docker
docker-compose
# FPGA
]
++ lib.optionals config.frogeye.dev.fpga [
verilog
]
++ lib.optionals (config.frogeye.dev.fpga && pkgs.stdenv.isx86_64) [
ghdl
# FPGA (graphical)
]
++ lib.optionals (config.frogeye.desktop.xorg && config.frogeye.dev.fpga) [
yosys
gtkwave
# VM (graphical)
]
++ lib.optionals (config.frogeye.desktop.xorg && config.frogeye.dev.vm) [
virt-manager
];
programs.nixvim.plugins.lsp.servers = {
ansiblels.enable = config.frogeye.dev.ansible; # Ansible
bashls.enable = true; # Bash
jsonls.enable = true; # JSON
lua_ls.enable = true; # Lua (for Neovim debugging)
perlpls.enable = config.frogeye.dev.perl; # Perl
phpactor.enable = config.frogeye.dev.php; # PHP
# Nix
nil_ls = {
enable = true;
settings = {
formatting.command = [ "nixfmt" ];
nix.flake = {
autoArchive = true;
autoEvalInputs = true;
};
};
};
# TODO Something for SQL. sqls is deprecated, sqlls is not in Nixpkgs. Probably needs a DB connection configured anyways?
yamlls.enable = true; # YAML
# TODO Check out none-ls
};
};
}

11
hm/dev/default.nix Normal file
View file

@ -0,0 +1,11 @@
{ pkgs, config, ... }:
{
imports = [
./c.nix
./common.nix
./go.nix
./node.nix
./prose.nix
./python.nix
];
}

24
hm/dev/go.nix Normal file
View file

@ -0,0 +1,24 @@
# Untested post-nix
{
pkgs,
lib,
config,
...
}:
{
config = lib.mkIf config.frogeye.dev.go {
frogeye = {
direnv = {
GOPATH = "${config.xdg.cacheHome}/go";
};
};
home = {
packages = with pkgs; [
go
];
sessionPath = [
"${config.home.sessionVariables.GOPATH}"
];
};
};
}

26
hm/dev/node.nix Normal file
View file

@ -0,0 +1,26 @@
# Untested post-nix
{
pkgs,
lib,
config,
...
}:
{
config = lib.mkIf config.frogeye.dev.node {
frogeye = {
direnv = {
npm_config_cache = "${config.xdg.cacheHome}/npm";
YARN_CACHE_FOLDER = "${config.xdg.cacheHome}/yarn";
};
};
home = {
sessionVariables = {
NODE_REPL_HISTORY = "${config.xdg.cacheHome}/node_repl_history";
YARN_DISABLE_SELF_UPDATE_CHECK = "true"; # This also disable the creation of a ~/.yarnrc file
};
};
programs.bash.shellAliases = {
bower = "bower --config.storage.packages=${config.xdg.cacheHome}/bower/packages --config.storage.registry=${config.xdg.cacheHome}/bower/registry --config.storage.links=${config.xdg.cacheHome}/bower/links";
};
};
}

49
hm/dev/prose.nix Normal file
View file

@ -0,0 +1,49 @@
# Prose is a programming language, fight me
{
pkgs,
lib,
config,
...
}:
{
config = lib.mkIf config.frogeye.dev.prose {
home = {
packages = with pkgs; [
hunspell
hunspellDicts.en_GB-ize
hunspellDicts.en_US
hunspellDicts.fr-moderne
hunspellDicts.nl_NL
# TODO libreoffice-extension-languagetool or libreoffice-extension-grammalecte-fr
];
};
programs.nixvim = {
autoCmd = [
# vim-easy-align: Align Markdown tables with |
{
event = "FileType";
pattern = "markdown";
command = "vmap <Bar> :EasyAlign*<Bar><Enter>";
}
];
extraPlugins =
with pkgs.vimPlugins;
lib.optionals config.programs.pandoc.enable [
vim-pandoc # Pandoc-specific stuff because there's no LSP for it
vim-pandoc-syntax
];
extraConfigVim = lib.optionalString config.programs.pandoc.enable ''
let g:pandoc#modules#disabled = ["folding"]
let g:pandoc#spell#enabled = 0
let g:pandoc#syntax#conceal#use = 0
'';
plugins.none-ls = {
enable = true;
sources = {
# LanguageTool
diagnostics.ltrs.enable = true;
};
};
};
};
}

59
hm/dev/python.nix Normal file
View file

@ -0,0 +1,59 @@
{
pkgs,
lib,
config,
...
}:
{
config = lib.mkIf config.frogeye.dev.python {
home = {
packages = with pkgs; [
python3
python3Packages.ipython
];
sessionVariables = {
PYTHONSTARTUP = "${config.xdg.configHome}/pythonstartup.py";
};
};
programs.bash.shellAliases = {
ipython = "ipython --no-confirm-exit --pdb";
};
programs.nixvim.plugins.lsp.servers.pylsp = {
# Python
enable = config.frogeye.dev.python;
settings.plugins = {
black.enabled = true;
flake8 = {
enabled = true;
maxLineLength = 88; # Compatibility with Black
};
isort.enabled = true;
mccabe.enabled = true;
pycodestyle = {
enabled = true;
maxLineLength = 88; # Compatibility with Black
};
pyflakes.enabled = true;
pylint.enabled = true;
pylsp_mypy = {
enabled = true;
overrides = [
"--cache-dir=${config.xdg.cacheHome}/mypy"
"--ignore-missing-imports"
"--disallow-untyped-defs"
"--disallow-untyped-calls"
"--disallow-incomplete-defs"
"--disallow-untyped-decorators"
true
];
};
# TODO Could add some, could also remove some
};
};
xdg.configFile = {
"pythonstartup.py" = {
text = (builtins.readFile ./pythonstartup.py);
};
};
};
}

View file

@ -1,71 +0,0 @@
{ pkgs, lib, config, ... }:
{
config = lib.mkIf config.frogeye.extra {
programs = {
pandoc.enable = true;
yt-dlp = {
enable = true;
settings = {
format = "bestvideo[height<=${builtins.toString config.frogeye.desktop.maxVideoHeight}]+bestaudio/best";
sponsorblock-mark = "all";
sponsorblock-remove = "intro,outro,sponsor,selfpromo,preview,interaction,music_offtopic";
sub-langs = "en,fr";
write-auto-subs = true;
write-subs = true;
};
};
};
home.packages = with pkgs; ([
# android tools
android-tools
# Communication
signal-desktop
# downloading
# transmission TODO Collision if both transmissions are active?
# Multimedia toolbox
ffmpeg
# documents
visidata
# texlive.combined.scheme-full
# TODO Convert existing LaTeX documents into using Nix build system
# texlive is big and not that much used, sooo
pdftk
hunspell
hunspellDicts.en_GB-ize
hunspellDicts.en_US
hunspellDicts.fr-moderne
hunspellDicts.nl_NL
# TODO libreoffice-extension-languagetool or libreoffice-extension-grammalecte-fr
] ++ lib.optionals config.frogeye.desktop.xorg [
# multimedia editors
gimp
inkscape
darktable
puddletag
audacity
# downloading
transmission-qt
# wine only makes sense on x86_64
] ++ lib.optionals pkgs.stdenv.isx86_64 [
wine
# TODO wine-gecko wine-mono lib32-libpulse (?)
] ++ lib.optionals (!stdenv.isAarch64) [
# Musescore is broken on aarch64
musescore
# Blender 4.0.1 can't compile on aarch64
# https://hydra.nixos.org/job/nixos/release-23.11/nixpkgs.blender.aarch64-linux
blender
]);
services = {
syncthing.enable = true;
};
};
}

80
hm/extra/default.nix Normal file
View file

@ -0,0 +1,80 @@
{
pkgs,
lib,
config,
...
}:
{
config = lib.mkIf config.frogeye.extra {
programs = {
pandoc.enable = true;
yt-dlp = {
enable = true;
settings = {
format = "bestvideo[height<=${builtins.toString config.frogeye.desktop.maxVideoHeight}]+bestaudio/best";
sponsorblock-mark = "all";
sponsorblock-remove = "intro,outro,sponsor,selfpromo,preview,interaction,music_offtopic";
sub-langs = "en,fr";
write-auto-subs = true;
write-subs = true;
};
};
};
home.packages =
with pkgs;
(
[
# android tools
android-tools
# Communication
signal-desktop
(pkgs.callPackage ./whisperx.nix { }) # Transcribe voice messages
# downloading
# transmission TODO Collision if both transmissions are active?
# Multimedia toolbox
ffmpeg
# documents
visidata
# texlive.combined.scheme-full
# TODO Convert existing LaTeX documents into using Nix build system
# texlive is big and not that much used, sooo
pdftk
pdfgrep
# Misc
haskellPackages.dice
rustdesk-flutter
]
++ lib.optionals config.frogeye.desktop.xorg [
# multimedia editors
darktable
puddletag
audacity
xournalpp
krita
# downloading
transmission_4-qt
# wine only makes sense on x86_64
]
++ lib.optionals pkgs.stdenv.isx86_64 [
wine
# TODO wine-gecko wine-mono lib32-libpulse (?)
]
++ lib.optionals (!stdenv.isAarch64) [
# Musescore is broken on aarch64
musescore
# Blender 4.0.1 can't compile on aarch64
# https://hydra.nixos.org/job/nixos/release-23.11/nixpkgs.blender.aarch64-linux
blender
]
);
};
}

44
hm/extra/whisperx.nix Normal file
View file

@ -0,0 +1,44 @@
{
pkgs ? import <nixpkgs> { },
}:
pkgs.python3Packages.buildPythonPackage rec {
pname = "whisperx";
version = "3.2.0";
# pypi doesn't have the requirements.txt file, and it's required
src = pkgs.fetchFromGitHub {
owner = "m-bain";
repo = "whisperX";
rev = "v${version}"; # git doesn't have tags
hash = "sha256-JQvyR9JW8OiSRI0eywTyWB4VMXmu6cTpBekBWonoJa4=";
};
pyproject = true;
dependencies = [
pkgs.python3Packages.torch
pkgs.python3Packages.torchaudio
(pkgs.python3Packages.faster-whisper.overrideAttrs (old: {
# 1.0.2 is actually breaking APIs (requires hotwords argument)
src = pkgs.fetchFromGitHub {
owner = "SYSTRAN";
repo = "faster-whisper";
rev = "v1.0.0";
hash = "sha256-0fE8X1d6CgDrrHtRudksN/tIGRtBKMvoNwkSVyFNda4=";
};
}))
pkgs.python3Packages.transformers
pkgs.python3Packages.pyannote-audio # Not in the requirements.txt for some reason
pkgs.python3Packages.pandas
pkgs.python3Packages.nltk
];
pythonRelaxDeps = true;
# torchaudio: 2.5.1a0 is >=2, despite dependency check saying otherwise
# ctranslate2: was pinned to 4.4.0 to fix some nvidia pinning issue or whatnot
# pyannote-audio: not sure what pins this to 3.1.1, but 3.3.1 works fine
# For some reason pyannote-audio doesn't get relaxed when listed in a pythonRelaxDeps array,
# hence why using true
build-system = [
pkgs.python3Packages.setuptools
];
pythonImportsCheck = [
"whisperx"
];
}

View file

@ -1,94 +0,0 @@
#!/usr/bin/env python3
import typing
import subprocess
import time
# CORE
class Notifier:
pass
class Section:
def __init__(self) -> None:
self.text = b"(Loading)"
class Module:
def __init__(self) -> None:
self.bar: "Bar"
self.section = Section()
self.sections = [self.section]
class Alignment:
def __init__(self, *modules: Module) -> None:
self.bar: "Bar"
self.modules = modules
for module in modules:
module.bar = self.bar
class Screen:
def __init__(self, left: Alignment = Alignment(), right: Alignment = Alignment()) -> None:
self.bar: "Bar"
self.left = left
self.left.bar = self.bar
self.right = right or Alignment()
self.right.bar = self.bar
class Bar:
def __init__(self, *screens: Screen) -> None:
self.screens = screens
for screen in screens:
screen.bar = self
self.process = subprocess.Popen(["lemonbar"], stdin=subprocess.PIPE)
def display(self) -> None:
string = b""
for s, screen in enumerate(self.screens):
string += b"%%{S%d}" % s
for control, alignment in [(b'%{l}', screen.left), (b'%{r}', screen.right)]:
string += control
for module in alignment.modules:
for section in module.sections:
string += b"<%b> |" % section.text
string += b"\n"
print(string)
assert self.process.stdin
self.process.stdin.write(string)
self.process.stdin.flush()
def run(self) -> None:
while True:
self.display()
time.sleep(1)
# REUSABLE
class ClockNotifier(Notifier):
def run(self) -> None:
while True:
def __init__(self, text: bytes):
super().__init__()
self.section.text = text
class StaticModule(Module):
def __init__(self, text: bytes):
super().__init__()
self.section.text = text
# USER
if __name__ == "__main__":
bar = Bar(
Screen(Alignment(StaticModule(b"A"))),
Screen(Alignment(StaticModule(b"B"))),
)
bar.run()

View file

@ -1,199 +0,0 @@
#!/usr/bin/env python3
"""
Debugging script
"""
import i3ipc
import os
import psutil
# import alsaaudio
from time import time
import subprocess
i3 = i3ipc.Connection()
lemonbar = subprocess.Popen(["lemonbar", "-b"], stdin=subprocess.PIPE)
# Utils
def upChart(p):
block = " ▁▂▃▄▅▆▇█"
return block[round(p * (len(block) - 1))]
def humanSizeOf(num, suffix="B"): # TODO Credit
for unit in ["", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi"]:
if abs(num) < 1024.0:
return "%3.0f%2s%s" % (num, unit, suffix)
num /= 1024.0
return "%.0f%2s%s" % (num, "Yi", suffix)
# Values
mode = ""
container = i3.get_tree().find_focused()
workspaces = i3.get_workspaces()
outputs = i3.get_outputs()
username = os.environ["USER"]
hostname = os.environ["HOSTNAME"]
if "-" in hostname:
hostname = hostname.split("-")[-1]
oldNetIO = dict()
oldTime = time()
def update():
activeOutputs = sorted(
sorted(list(filter(lambda o: o.active, outputs)), key=lambda o: o.rect.y),
key=lambda o: o.rect.x,
)
z = ""
for aOutput in range(len(activeOutputs)):
output = activeOutputs[aOutput]
# Mode || Workspaces
t = []
if mode != "":
t.append(mode)
else:
t.append(
" ".join(
[
(w.name.upper() if w.focused else w.name)
for w in workspaces
if w.output == output.name
]
)
)
# Windows Title
# if container:
# t.append(container.name)
# CPU
t.append(
"C" + "".join([upChart(p / 100) for p in psutil.cpu_percent(percpu=True)])
)
# Memory
t.append(
"M"
+ str(round(psutil.virtual_memory().percent))
+ "% "
+ "S"
+ str(round(psutil.swap_memory().percent))
+ "%"
)
# Disks
d = []
for disk in psutil.disk_partitions():
e = ""
if disk.device.startswith("/dev/sd"):
e += "S" + disk.device[-2:].upper()
elif disk.device.startswith("/dev/mmcblk"):
e += "M" + disk.device[-3] + disk.device[-1]
else:
e += "?"
e += " "
e += str(round(psutil.disk_usage(disk.mountpoint).percent)) + "%"
d.append(e)
t.append(" ".join(d))
# Network
netStats = psutil.net_if_stats()
netIO = psutil.net_io_counters(pernic=True)
net = []
for iface in filter(lambda i: i != "lo" and netStats[i].isup, netStats.keys()):
s = ""
if iface.startswith("eth"):
s += "E"
elif iface.startswith("wlan"):
s += "W"
else:
s += "?"
s += " "
now = time()
global oldNetIO, oldTime
sent = (
(oldNetIO[iface].bytes_sent if iface in oldNetIO else 0)
- (netIO[iface].bytes_sent if iface in netIO else 0)
) / (oldTime - now)
recv = (
(oldNetIO[iface].bytes_recv if iface in oldNetIO else 0)
- (netIO[iface].bytes_recv if iface in netIO else 0)
) / (oldTime - now)
s += (
""
+ humanSizeOf(abs(recv), "B/s")
+ ""
+ humanSizeOf(abs(sent), "B/s")
)
oldNetIO = netIO
oldTime = now
net.append(s)
t.append(" ".join(net))
# Battery
if os.path.isdir("/sys/class/power_supply/BAT0"):
with open("/sys/class/power_supply/BAT0/charge_now") as f:
charge_now = int(f.read())
with open("/sys/class/power_supply/BAT0/charge_full_design") as f:
charge_full = int(f.read())
t.append("B" + str(round(100 * charge_now / charge_full)) + "%")
# Volume
# t.append('V ' + str(alsaaudio.Mixer('Master').getvolume()[0]) + '%')
t.append(username + "@" + hostname)
# print(' - '.join(t))
# t = [output.name]
z += " - ".join(t) + "%{S" + str(aOutput + 1) + "}"
# lemonbar.stdin.write(bytes(' - '.join(t), 'utf-8'))
# lemonbar.stdin.write(bytes('%{S' + str(aOutput + 1) + '}', 'utf-8'))
lemonbar.stdin.write(bytes(z + "\n", "utf-8"))
lemonbar.stdin.flush()
# Event listeners
def on_mode(i3, e):
global mode
if e.change == "default":
mode = ""
else:
mode = e.change
update()
i3.on("mode", on_mode)
# def on_window_focus(i3, e):
# global container
# container = e.container
# update()
#
# i3.on("window::focus", on_window_focus)
def on_workspace_focus(i3, e):
global workspaces
workspaces = i3.get_workspaces()
update()
i3.on("workspace::focus", on_workspace_focus)
# Starting
update()
i3.main()

View file

@ -1,327 +0,0 @@
#!/usr/bin/env python3
"""
Beautiful script
"""
import subprocess
import time
import datetime
import os
import multiprocessing
import i3ipc
import difflib
# Constants
FONT = "DejaVuSansMono Nerd Font Mono"
# TODO Update to be in sync with base16
thm = [
"#002b36",
"#dc322f",
"#859900",
"#b58900",
"#268bd2",
"#6c71c4",
"#2aa198",
"#93a1a1",
"#657b83",
"#dc322f",
"#859900",
"#b58900",
"#268bd2",
"#6c71c4",
"#2aa198",
"#fdf6e3",
]
fg = "#93a1a1"
bg = "#002b36"
THEMES = {
"CENTER": (fg, bg),
"DEFAULT": (thm[0], thm[8]),
"1": (thm[0], thm[9]),
"2": (thm[0], thm[10]),
"3": (thm[0], thm[11]),
"4": (thm[0], thm[12]),
"5": (thm[0], thm[13]),
"6": (thm[0], thm[14]),
"7": (thm[0], thm[15]),
}
# Utils
def fitText(text, size):
"""
Add spaces or cut a string to be `size` characters long
"""
if size > 0:
t = len(text)
if t >= size:
return text[:size]
else:
diff = size - t
return text + " " * diff
else:
return ""
def fgColor(theme):
global THEMES
return THEMES[theme][0]
def bgColor(theme):
global THEMES
return THEMES[theme][1]
class Section:
def __init__(self, theme="DEFAULT"):
self.text = ""
self.size = 0
self.toSize = 0
self.theme = theme
self.visible = False
self.name = ""
def update(self, text):
if text == "":
self.toSize = 0
else:
if len(text) < len(self.text):
self.text = text + self.text[len(text) :]
else:
self.text = text
self.toSize = len(text) + 3
def updateSize(self):
"""
Set the size for the next frame of animation
Return if another frame is needed
"""
if self.toSize > self.size:
self.size += 1
elif self.toSize < self.size:
self.size -= 1
self.visible = self.size
return self.toSize == self.size
def draw(self, left=True, nextTheme="DEFAULT"):
s = ""
if self.visible:
if not left:
if self.theme == nextTheme:
s += ""
else:
s += "%{F" + bgColor(self.theme) + "}"
s += "%{B" + bgColor(nextTheme) + "}"
s += ""
s += "%{F" + fgColor(self.theme) + "}"
s += "%{B" + bgColor(self.theme) + "}"
s += " " if self.size > 1 else ""
s += fitText(self.text, self.size - 3)
s += " " if self.size > 2 else ""
if left:
if self.theme == nextTheme:
s += ""
else:
s += "%{F" + bgColor(self.theme) + "}"
s += "%{B" + bgColor(nextTheme) + "}"
s += ""
return s
# Section definition
sTime = Section("3")
hostname = os.environ["HOSTNAME"].split(".")[0]
sHost = Section("2")
sHost.update(
os.environ["USER"] + "@" + hostname.split("-")[-1] if "-" in hostname else hostname
)
# Groups definition
gLeft = []
gRight = [sTime, sHost]
# Bar handling
bar = subprocess.Popen(["lemonbar", "-f", FONT, "-b"], stdin=subprocess.PIPE)
def updateBar():
global timeLastUpdate, timeUpdate
global gLeft, gRight
global outputs
text = ""
for oi in range(len(outputs)):
output = outputs[oi]
gLeftFiltered = list(
filter(
lambda s: s.visible and (not s.output or s.output == output.name), gLeft
)
)
tLeft = ""
l = len(gLeftFiltered)
for gi in range(l):
g = gLeftFiltered[gi]
# Next visible section for transition
nextTheme = gLeftFiltered[gi + 1].theme if gi + 1 < l else "CENTER"
tLeft = tLeft + g.draw(True, nextTheme)
tRight = ""
for gi in range(len(gRight)):
g = gRight[gi]
nextTheme = "CENTER"
for gn in gRight[gi + 1 :]:
if gn.visible:
nextTheme = gn.theme
break
tRight = g.draw(False, nextTheme) + tRight
text += (
"%{l}"
+ tLeft
+ "%{r}"
+ tRight
+ "%{B"
+ bgColor("CENTER")
+ "}"
+ "%{S"
+ str(oi + 1)
+ "}"
)
bar.stdin.write(bytes(text + "\n", "utf-8"))
bar.stdin.flush()
# Values
i3 = i3ipc.Connection()
outputs = []
def on_output():
global outputs
outputs = sorted(
sorted(
list(filter(lambda o: o.active, i3.get_outputs())), key=lambda o: o.rect.y
),
key=lambda o: o.rect.x,
)
on_output()
def on_workspace_focus():
global i3
global gLeft
workspaces = i3.get_workspaces()
wNames = [w.name for w in workspaces]
sNames = [s.name for s in gLeft]
newGLeft = []
def actuate(section, workspace):
if workspace:
section.name = workspace.name
section.output = workspace.output
if workspace.visible:
section.update(workspace.name)
else:
section.update(workspace.name.split(" ")[0])
if workspace.focused:
section.theme = "4"
elif workspace.urgent:
section.theme = "1"
else:
section.theme = "6"
else:
section.update("")
section.theme = "6"
for tag, i, j, k, l in difflib.SequenceMatcher(None, sNames, wNames).get_opcodes():
if tag == "equal": # If the workspaces didn't changed
for a in range(j - i):
workspace = workspaces[k + a]
section = gLeft[i + a]
actuate(section, workspace)
newGLeft.append(section)
if tag in ("delete", "replace"): # If the workspaces were removed
for section in gLeft[i:j]:
if section.visible:
actuate(section, None)
newGLeft.append(section)
else:
del section
if tag in ("insert", "replace"): # If the workspaces were removed
for workspace in workspaces[k:l]:
section = Section()
actuate(section, workspace)
newGLeft.append(section)
gLeft = newGLeft
updateBar()
on_workspace_focus()
def i3events(i3childPipe):
global i3
# Proxy functions
def on_workspace_focus(i3, e):
global i3childPipe
i3childPipe.send("on_workspace_focus")
i3.on("workspace::focus", on_workspace_focus)
def on_output(i3, e):
global i3childPipe
i3childPipe.send("on_output")
i3.on("output", on_output)
i3.main()
i3parentPipe, i3childPipe = multiprocessing.Pipe()
i3process = multiprocessing.Process(target=i3events, args=(i3childPipe,))
i3process.start()
def updateValues():
# Time
now = datetime.datetime.now()
sTime.update(now.strftime("%x %X"))
def updateAnimation():
for s in set(gLeft + gRight):
s.updateSize()
updateBar()
lastUpdate = 0
while True:
now = time.time()
if i3parentPipe.poll():
msg = i3parentPipe.recv()
if msg == "on_workspace_focus":
on_workspace_focus()
elif msg == "on_output":
on_output()
# TODO Restart lemonbar
else:
print(msg)
updateAnimation()
if now >= lastUpdate + 1:
updateValues()
lastUpdate = now
time.sleep(0.05)

View file

@ -1,10 +0,0 @@
#!/usr/bin/env python3
import Xlib.display
dis = Xlib.display.Display()
nb = dis.screen_count()
for s in range(nb):
print(s)

View file

@ -1,48 +0,0 @@
{ pkgs ? import <nixpkgs> { config = { }; overlays = [ ]; }, ... }:
# Tried using pyproject.nix but mpd2 dependency wouldn't resolve,
# is called pyton-mpd2 on PyPi but mpd2 in nixpkgs.
let
frobar = pkgs.python3Packages.buildPythonApplication {
pname = "frobar";
version = "2.0";
runtimeInputs = with pkgs; [ lemonbar-xft wirelesstools ];
propagatedBuildInputs = with pkgs.python3Packages; [
coloredlogs
notmuch
i3ipc
mpd2
psutil
pulsectl
pyinotify
];
makeWrapperArgs = [ "--prefix PATH : ${pkgs.lib.makeBinPath (with pkgs; [ lemonbar-xft wirelesstools ])}" ];
src = ./.;
};
in
{
config = {
xsession.windowManager.i3.config.bars = [ ];
programs.autorandr.hooks.postswitch = {
frobar = "${pkgs.systemd}/bin/systemctl --user restart frobar";
};
systemd.user.services.frobar = {
Unit = {
Description = "frobar";
After = [ "graphical-session-pre.target" ];
PartOf = [ "graphical-session.target" ];
};
Service = {
# Wait for i3 to start. Can't use ExecStartPre because otherwise it blocks graphical-session.target, and there's nothing i3/systemd
# TODO Do that better
ExecStart = ''${pkgs.bash}/bin/bash -c "while ! ${pkgs.i3}/bin/i3-msg; do ${pkgs.coreutils}/bin/sleep 1; done; ${frobar}/bin/frobar"'';
};
Install = { WantedBy = [ "graphical-session.target" ]; };
};
};
}
# TODO Connection with i3 is lost on start sometimes, more often than with Arch?
# TODO Restore ability to build frobar with nix-build

View file

@ -1,64 +0,0 @@
#!/usr/bin/env python3
from frobar.providers import *
# TODO If multiple screen, expand the sections and share them
# TODO Graceful exit
def run():
Bar.init()
Updater.init()
WORKSPACE_THEME = 0
FOCUS_THEME = 3
URGENT_THEME = 1
CUSTOM_SUFFIXES = "▲■"
customNames = dict()
for i in range(len(CUSTOM_SUFFIXES)):
short = str(i + 1)
full = short + " " + CUSTOM_SUFFIXES[i]
customNames[short] = full
Bar.addSectionAll(
I3WorkspacesProvider(
theme=WORKSPACE_THEME,
themeFocus=FOCUS_THEME,
themeUrgent=URGENT_THEME,
themeMode=URGENT_THEME,
customNames=customNames,
),
BarGroupType.LEFT,
)
# TODO Middle
Bar.addSectionAll(MpdProvider(theme=7), BarGroupType.LEFT)
# Bar.addSectionAll(I3WindowTitleProvider(), BarGroupType.LEFT)
# TODO Computer modes
SYSTEM_THEME = 2
DANGER_THEME = FOCUS_THEME
CRITICAL_THEME = URGENT_THEME
Bar.addSectionAll(CpuProvider(), BarGroupType.RIGHT)
Bar.addSectionAll(RamProvider(), BarGroupType.RIGHT)
Bar.addSectionAll(TemperatureProvider(), BarGroupType.RIGHT)
Bar.addSectionAll(BatteryProvider(), BarGroupType.RIGHT)
# Peripherals
PERIPHERAL_THEME = 5
NETWORK_THEME = 4
# TODO Disk space provider
# TODO Screen (connected, autorandr configuration, bbswitch) provider
Bar.addSectionAll(PulseaudioProvider(theme=PERIPHERAL_THEME), BarGroupType.RIGHT)
Bar.addSectionAll(RfkillProvider(theme=PERIPHERAL_THEME), BarGroupType.RIGHT)
Bar.addSectionAll(NetworkProvider(theme=NETWORK_THEME), BarGroupType.RIGHT)
# Personal
PERSONAL_THEME = 0
# Bar.addSectionAll(KeystoreProvider(theme=PERSONAL_THEME), BarGroupType.RIGHT)
# Bar.addSectionAll(NotmuchUnreadProvider(dir='~/.mail/', theme=PERSONAL_THEME), BarGroupType.RIGHT)
# Bar.addSectionAll(TodoProvider(dir='~/.vdirsyncer/currentCalendars/', theme=PERSONAL_THEME), BarGroupType.RIGHT)
TIME_THEME = 6
Bar.addSectionAll(TimeProvider(theme=TIME_THEME), BarGroupType.RIGHT)
# Bar.run()

View file

@ -1,723 +0,0 @@
#!/usr/bin/env python3
import enum
import logging
import os
import signal
import subprocess
import threading
import time
import coloredlogs
import i3ipc
from frobar.notbusy import notBusy
coloredlogs.install(level="DEBUG", fmt="%(levelname)s %(message)s")
log = logging.getLogger()
# TODO Allow deletion of Bar, BarGroup and Section for screen changes
# IDEA Use i3 ipc events rather than relying on xrandr or Xlib (less portable
# but easier)
# TODO Optimize to use write() calls instead of string concatenation (writing
# BarGroup strings should be a good compromise)
# TODO Use bytes rather than strings
# TODO Use default colors of lemonbar sometimes
# TODO Adapt bar height with font height
# TODO OPTI Static text objects that update its parents if modified
# TODO forceSize and changeText are different
class BarGroupType(enum.Enum):
LEFT = 0
RIGHT = 1
# TODO Middle
# MID_LEFT = 2
# MID_RIGHT = 3
class BarStdoutThread(threading.Thread):
def run(self) -> None:
while Bar.running:
handle = Bar.process.stdout.readline().strip()
if not len(handle):
Bar.stop()
if handle not in Bar.actionsH2F:
log.error("Unknown action: {}".format(handle))
continue
function = Bar.actionsH2F[handle]
function()
class Bar:
"""
One bar for each screen
"""
# Constants
FONTS = ["DejaVuSansM Nerd Font"]
FONTSIZE = 10
@staticmethod
def init() -> None:
Bar.running = True
Section.init()
cmd = ["lemonbar", "-b", "-a", "64"]
for font in Bar.FONTS:
cmd += ["-f", "{}:size={}".format(font, Bar.FONTSIZE)]
Bar.process = subprocess.Popen(
cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE
)
Bar.stdoutThread = BarStdoutThread()
Bar.stdoutThread.start()
# Debug
Bar(0)
# Bar(1)
@staticmethod
def stop() -> None:
Bar.running = False
Bar.process.kill()
# TODO This is not really the best way to do it I guess
os.killpg(os.getpid(), signal.SIGTERM)
@staticmethod
def run() -> None:
Bar.forever()
i3 = i3ipc.Connection()
def doStop(*args) -> None:
Bar.stop()
print(88)
try:
i3.on("ipc_shutdown", doStop)
i3.main()
except BaseException:
print(93)
Bar.stop()
# Class globals
everyone = set()
string = ""
process = None
running = False
nextHandle = 0
actionsF2H = dict()
actionsH2F = dict()
@staticmethod
def getFunctionHandle(function):
assert callable(function)
if function in Bar.actionsF2H.keys():
return Bar.actionsF2H[function]
handle = "{:x}".format(Bar.nextHandle).encode()
Bar.nextHandle += 1
Bar.actionsF2H[function] = handle
Bar.actionsH2F[handle] = function
return handle
@staticmethod
def forever():
Bar.process.wait()
Bar.stop()
def __init__(self, screen):
assert isinstance(screen, int)
self.screen = "%{S" + str(screen) + "}"
self.groups = dict()
for groupType in BarGroupType:
group = BarGroup(groupType, self)
self.groups[groupType] = group
self.childsChanged = False
self.everyone.add(self)
@staticmethod
def addSectionAll(section, group, screens=None):
"""
.. note::
Add the section before updating it for the first time.
"""
assert isinstance(section, Section)
assert isinstance(group, BarGroupType)
# TODO screens selection
for bar in Bar.everyone:
bar.addSection(section, group=group)
def addSection(self, section, group):
assert isinstance(section, Section)
assert isinstance(group, BarGroupType)
self.groups[group].addSection(section)
def update(self):
if self.childsChanged:
self.string = self.screen
self.string += self.groups[BarGroupType.LEFT].string
self.string += self.groups[BarGroupType.RIGHT].string
self.childsChanged = False
@staticmethod
def updateAll():
if Bar.running:
Bar.string = ""
for bar in Bar.everyone:
bar.update()
Bar.string += bar.string
# Color for empty sections
Bar.string += BarGroup.color(*Section.EMPTY)
# print(Bar.string)
Bar.process.stdin.write(bytes(Bar.string + "\n", "utf-8"))
Bar.process.stdin.flush()
class BarGroup:
"""
One for each group of each bar
"""
everyone = set()
def __init__(self, groupType, parent):
assert isinstance(groupType, BarGroupType)
assert isinstance(parent, Bar)
self.groupType = groupType
self.parent = parent
self.sections = list()
self.string = ""
self.parts = []
#: One of the sections that had their theme or visibility changed
self.childsThemeChanged = False
#: One of the sections that had their text (maybe their size) changed
self.childsTextChanged = False
BarGroup.everyone.add(self)
def addSection(self, section):
self.sections.append(section)
section.addParent(self)
def addSectionAfter(self, sectionRef, section):
index = self.sections.index(sectionRef)
self.sections.insert(index + 1, section)
section.addParent(self)
ALIGNS = {BarGroupType.LEFT: "%{l}", BarGroupType.RIGHT: "%{r}"}
@staticmethod
def fgColor(color):
return "%{F" + (color or "-") + "}"
@staticmethod
def bgColor(color):
return "%{B" + (color or "-") + "}"
@staticmethod
def color(fg, bg):
return BarGroup.fgColor(fg) + BarGroup.bgColor(bg)
def update(self):
if self.childsThemeChanged:
parts = [BarGroup.ALIGNS[self.groupType]]
secs = [sec for sec in self.sections if sec.visible]
lenS = len(secs)
for s in range(lenS):
sec = secs[s]
theme = Section.THEMES[sec.theme]
if self.groupType == BarGroupType.LEFT:
oSec = secs[s + 1] if s < lenS - 1 else None
else:
oSec = secs[s - 1] if s > 0 else None
oTheme = (
Section.THEMES[oSec.theme] if oSec is not None else Section.EMPTY
)
if self.groupType == BarGroupType.LEFT:
if s == 0:
parts.append(BarGroup.bgColor(theme[1]))
parts.append(BarGroup.fgColor(theme[0]))
parts.append(sec)
if theme == oTheme:
parts.append("")
else:
parts.append(BarGroup.color(theme[1], oTheme[1]) + "")
else:
if theme is oTheme:
parts.append("")
else:
parts.append(BarGroup.fgColor(theme[1]) + "")
parts.append(BarGroup.color(*theme))
parts.append(sec)
# TODO OPTI Concatenate successive strings
self.parts = parts
if self.childsTextChanged or self.childsThemeChanged:
self.string = ""
for part in self.parts:
if isinstance(part, str):
self.string += part
elif isinstance(part, Section):
self.string += part.curText
self.parent.childsChanged = True
self.childsThemeChanged = False
self.childsTextChanged = False
@staticmethod
def updateAll():
for group in BarGroup.everyone:
group.update()
Bar.updateAll()
class SectionThread(threading.Thread):
ANIMATION_START = 0.025
ANIMATION_STOP = 0.001
ANIMATION_EVOLUTION = 0.9
def run(self):
while Section.somethingChanged.wait():
notBusy.wait()
Section.updateAll()
animTime = self.ANIMATION_START
frameTime = time.perf_counter()
while len(Section.sizeChanging) > 0:
frameTime += animTime
curTime = time.perf_counter()
sleepTime = frameTime - curTime
time.sleep(sleepTime if sleepTime > 0 else 0)
Section.updateAll()
animTime *= self.ANIMATION_EVOLUTION
if animTime < self.ANIMATION_STOP:
animTime = self.ANIMATION_STOP
class Section:
# TODO Update all of that to base16
# COLORS = ['#272822', '#383830', '#49483e', '#75715e', '#a59f85', '#f8f8f2',
# '#f5f4f1', '#f9f8f5', '#f92672', '#fd971f', '#f4bf75', '#a6e22e',
# '#a1efe4', '#66d9ef', '#ae81ff', '#cc6633']
COLORS = [
"#181818",
"#AB4642",
"#A1B56C",
"#F7CA88",
"#7CAFC2",
"#BA8BAF",
"#86C1B9",
"#D8D8D8",
"#585858",
"#AB4642",
"#A1B56C",
"#F7CA88",
"#7CAFC2",
"#BA8BAF",
"#86C1B9",
"#F8F8F8",
]
FGCOLOR = "#F8F8F2"
BGCOLOR = "#272822"
THEMES = list()
EMPTY = (FGCOLOR, BGCOLOR)
ICON = None
PERSISTENT = False
#: Sections that do not have their destination size
sizeChanging = set()
updateThread = SectionThread(daemon=True)
somethingChanged = threading.Event()
lastChosenTheme = 0
@staticmethod
def init():
for t in range(8, 16):
Section.THEMES.append((Section.COLORS[0], Section.COLORS[t]))
Section.updateThread.start()
def __init__(self, theme=None):
#: Displayed section
#: Note: A section can be empty and displayed!
self.visible = False
if theme is None:
theme = Section.lastChosenTheme
Section.lastChosenTheme = (Section.lastChosenTheme + 1) % len(
Section.THEMES
)
self.theme = theme
#: Displayed text
self.curText = ""
#: Displayed text size
self.curSize = 0
#: Destination text
self.dstText = Text(" ", Text(), " ")
#: Destination size
self.dstSize = 0
#: Groups that have this section
self.parents = set()
self.icon = self.ICON
self.persistent = self.PERSISTENT
def __str__(self):
try:
return "<{}><{}>{:01d}{}{:02d}/{:02d}".format(
self.curText,
self.dstText,
self.theme,
"+" if self.visible else "-",
self.curSize,
self.dstSize,
)
except:
return super().__str__()
def addParent(self, parent):
self.parents.add(parent)
def appendAfter(self, section):
assert len(self.parents)
for parent in self.parents:
parent.addSectionAfter(self, section)
def informParentsThemeChanged(self):
for parent in self.parents:
parent.childsThemeChanged = True
def informParentsTextChanged(self):
for parent in self.parents:
parent.childsTextChanged = True
def updateText(self, text):
if isinstance(text, str):
text = Text(text)
elif isinstance(text, Text) and not len(text.elements):
text = None
self.dstText[0] = (
None
if (text is None and not self.persistent)
else ((" " + self.icon + " ") if self.icon else " ")
)
self.dstText[1] = text
self.dstText[2] = (
" " if self.dstText[1] is not None and len(self.dstText[1]) else None
)
self.dstSize = len(self.dstText)
self.dstText.setSection(self)
if self.curSize == self.dstSize:
if self.dstSize > 0:
self.curText = str(self.dstText)
self.informParentsTextChanged()
else:
Section.sizeChanging.add(self)
Section.somethingChanged.set()
def setDecorators(self, **kwargs):
self.dstText.setDecorators(**kwargs)
self.curText = str(self.dstText)
self.informParentsTextChanged()
Section.somethingChanged.set()
def updateTheme(self, theme):
assert isinstance(theme, int)
assert theme < len(Section.THEMES)
if theme == self.theme:
return
self.theme = theme
self.informParentsThemeChanged()
Section.somethingChanged.set()
def updateVisibility(self, visibility):
assert isinstance(visibility, bool)
self.visible = visibility
self.informParentsThemeChanged()
Section.somethingChanged.set()
@staticmethod
def fit(text, size):
t = len(text)
return text[:size] if t >= size else text + [" "] * (size - t)
def update(self):
# TODO Might profit of a better logic
if not self.visible:
self.updateVisibility(True)
return
if self.dstSize > self.curSize:
self.curSize += 1
elif self.dstSize < self.curSize:
self.curSize -= 1
else:
# Visibility toggling must be done one step after curSize = 0
if self.dstSize == 0:
self.updateVisibility(False)
Section.sizeChanging.remove(self)
return
self.curText = self.dstText.text(size=self.curSize, pad=True)
self.informParentsTextChanged()
@staticmethod
def updateAll():
"""
Process all sections for text size changes
"""
for sizeChanging in Section.sizeChanging.copy():
sizeChanging.update()
BarGroup.updateAll()
Section.somethingChanged.clear()
@staticmethod
def ramp(p, ramp=" ▁▂▃▄▅▆▇█"):
if p > 1:
return ramp[-1]
elif p < 0:
return ramp[0]
else:
return ramp[round(p * (len(ramp) - 1))]
class StatefulSection(Section):
# TODO FEAT Allow to temporary expand the section (e.g. when important change)
NUMBER_STATES = None
DEFAULT_STATE = 0
def __init__(self, *args, **kwargs):
Section.__init__(self, *args, **kwargs)
self.state = self.DEFAULT_STATE
if hasattr(self, "onChangeState"):
self.onChangeState(self.state)
self.setDecorators(
clickLeft=self.incrementState, clickRight=self.decrementState
)
def incrementState(self):
newState = min(self.state + 1, self.NUMBER_STATES - 1)
self.changeState(newState)
def decrementState(self):
newState = max(self.state - 1, 0)
self.changeState(newState)
def changeState(self, state):
assert isinstance(state, int)
assert state < self.NUMBER_STATES
self.state = state
if hasattr(self, "onChangeState"):
self.onChangeState(state)
self.refreshData()
class ColorCountsSection(StatefulSection):
# TODO FEAT Blend colors when not expanded
# TODO FEAT Blend colors with importance of count
# TODO FEAT Allow icons instead of counts
NUMBER_STATES = 3
COLORABLE_ICON = "?"
def __init__(self, theme=None):
StatefulSection.__init__(self, theme=theme)
def fetcher(self):
counts = self.subfetcher()
# Nothing
if not len(counts):
return None
# Icon colored
elif self.state == 0 and len(counts) == 1:
count, color = counts[0]
return Text(self.COLORABLE_ICON, fg=color)
# Icon
elif self.state == 0 and len(counts) > 1:
return Text(self.COLORABLE_ICON)
# Icon + Total
elif self.state == 1 and len(counts) > 1:
total = sum([count for count, color in counts])
return Text(self.COLORABLE_ICON, " ", total)
# Icon + Counts
else:
text = Text(self.COLORABLE_ICON)
for count, color in counts:
text.append(" ", Text(count, fg=color))
return text
class Text:
def _setElements(self, elements):
# TODO OPTI Concatenate consecutrive string
self.elements = list(elements)
def _setDecorators(self, decorators):
# TODO OPTI Convert no decorator to strings
self.decorators = decorators
self.prefix = None
self.suffix = None
def __init__(self, *args, **kwargs):
self._setElements(args)
self._setDecorators(kwargs)
self.section = None
def append(self, *args):
self._setElements(self.elements + list(args))
def prepend(self, *args):
self._setElements(list(args) + self.elements)
def setElements(self, *args):
self._setElements(args)
def setDecorators(self, **kwargs):
self._setDecorators(kwargs)
def setSection(self, section):
assert isinstance(section, Section)
self.section = section
for element in self.elements:
if isinstance(element, Text):
element.setSection(section)
def _genFixs(self):
if self.prefix is not None and self.suffix is not None:
return
self.prefix = ""
self.suffix = ""
def nest(prefix, suffix):
self.prefix = self.prefix + "%{" + prefix + "}"
self.suffix = "%{" + suffix + "}" + self.suffix
def getColor(val):
# TODO Allow themes
assert isinstance(val, str) and len(val) == 7
return val
def button(number, function):
handle = Bar.getFunctionHandle(function)
nest("A" + number + ":" + handle.decode() + ":", "A" + number)
for key, val in self.decorators.items():
if val is None:
continue
if key == "fg":
reset = self.section.THEMES[self.section.theme][0]
nest("F" + getColor(val), "F" + reset)
elif key == "bg":
reset = self.section.THEMES[self.section.theme][1]
nest("B" + getColor(val), "B" + reset)
elif key == "clickLeft":
button("1", val)
elif key == "clickMiddle":
button("2", val)
elif key == "clickRight":
button("3", val)
elif key == "scrollUp":
button("4", val)
elif key == "scrollDown":
button("5", val)
else:
log.warn("Unkown decorator: {}".format(key))
def _text(self, size=None, pad=False):
self._genFixs()
curString = self.prefix
curSize = 0
remSize = size
for element in self.elements:
if element is None:
continue
elif isinstance(element, Text):
newString, newSize = element._text(size=remSize)
else:
newString = str(element)
if remSize is not None:
newString = newString[:remSize]
newSize = len(newString)
curString += newString
curSize += newSize
if remSize is not None:
remSize -= newSize
if remSize <= 0:
break
curString += self.suffix
if pad and remSize > 0:
curString += " " * remSize
curSize += remSize
if size is not None:
if pad:
assert size == curSize
else:
assert size >= curSize
return curString, curSize
def text(self, *args, **kwargs):
string, size = self._text(*args, **kwargs)
return string
def __str__(self):
self._genFixs()
curString = self.prefix
for element in self.elements:
if element is None:
continue
else:
curString += str(element)
curString += self.suffix
return curString
def __len__(self):
curSize = 0
for element in self.elements:
if element is None:
continue
elif isinstance(element, Text):
curSize += len(element)
else:
curSize += len(str(element))
return curSize
def __getitem__(self, index):
return self.elements[index]
def __setitem__(self, index, data):
self.elements[index] = data

View file

@ -1,5 +0,0 @@
#!/usr/bin/env python3
import threading
notBusy = threading.Event()

View file

@ -1,816 +0,0 @@
#!/usr/bin/env python3
import datetime
import ipaddress
import json
import logging
import random
import socket
import subprocess
import coloredlogs
import mpd
import notmuch
import psutil
import pulsectl
from frobar.display import *
from frobar.updaters import *
coloredlogs.install(level="DEBUG", fmt="%(levelname)s %(message)s")
log = logging.getLogger()
# TODO Generator class (for I3WorkspacesProvider, NetworkProvider and later
# PulseaudioProvider and MpdProvider)
def humanSize(num):
"""
Returns a string of width 3+3
"""
for unit in ("B ", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB"):
if abs(num) < 1000:
if num >= 10:
return "{:3d}{}".format(int(num), unit)
else:
return "{:.1f}{}".format(num, unit)
num /= 1024.0
return "{:d}YiB".format(num)
def randomColor(seed=0):
random.seed(seed)
return "#{:02x}{:02x}{:02x}".format(*[random.randint(0, 255) for _ in range(3)])
class TimeProvider(StatefulSection, PeriodicUpdater):
FORMATS = ["%H:%M", "%m-%d %H:%M:%S", "%a %y-%m-%d %H:%M:%S"]
NUMBER_STATES = len(FORMATS)
DEFAULT_STATE = 1
def fetcher(self):
now = datetime.datetime.now()
return now.strftime(self.FORMATS[self.state])
def __init__(self, theme=None):
PeriodicUpdater.__init__(self)
StatefulSection.__init__(self, theme)
self.changeInterval(1) # TODO OPTI When state < 1
class AlertLevel(enum.Enum):
NORMAL = 0
WARNING = 1
DANGER = 2
class AlertingSection(StatefulSection):
# TODO EASE Correct settings for themes
THEMES = {AlertLevel.NORMAL: 2, AlertLevel.WARNING: 3, AlertLevel.DANGER: 1}
PERSISTENT = True
def getLevel(self, quantity):
if quantity > self.dangerThresold:
return AlertLevel.DANGER
elif quantity > self.warningThresold:
return AlertLevel.WARNING
else:
return AlertLevel.NORMAL
def updateLevel(self, quantity):
self.level = self.getLevel(quantity)
self.updateTheme(self.THEMES[self.level])
if self.level == AlertLevel.NORMAL:
return
# TODO Temporary update state
def __init__(self, theme):
StatefulSection.__init__(self, theme)
self.dangerThresold = 0.90
self.warningThresold = 0.75
class CpuProvider(AlertingSection, PeriodicUpdater):
NUMBER_STATES = 3
ICON = ""
def fetcher(self):
percent = psutil.cpu_percent(percpu=False)
self.updateLevel(percent / 100)
if self.state >= 2:
percents = psutil.cpu_percent(percpu=True)
return "".join([Section.ramp(p / 100) for p in percents])
elif self.state >= 1:
return Section.ramp(percent / 100)
def __init__(self, theme=None):
AlertingSection.__init__(self, theme)
PeriodicUpdater.__init__(self)
self.changeInterval(1)
class RamProvider(AlertingSection, PeriodicUpdater):
"""
Shows free RAM
"""
NUMBER_STATES = 4
ICON = ""
def fetcher(self):
mem = psutil.virtual_memory()
freePerc = mem.percent / 100
self.updateLevel(freePerc)
if self.state < 1:
return None
text = Text(Section.ramp(freePerc))
if self.state >= 2:
freeStr = humanSize(mem.total - mem.available)
text.append(freeStr)
if self.state >= 3:
totalStr = humanSize(mem.total)
text.append("/", totalStr)
return text
def __init__(self, theme=None):
AlertingSection.__init__(self, theme)
PeriodicUpdater.__init__(self)
self.changeInterval(1)
class TemperatureProvider(AlertingSection, PeriodicUpdater):
NUMBER_STATES = 2
RAMP = ""
def fetcher(self):
allTemp = psutil.sensors_temperatures()
if "coretemp" not in allTemp:
# TODO Opti Remove interval
return ""
temp = allTemp["coretemp"][0]
self.warningThresold = temp.high
self.dangerThresold = temp.critical
self.updateLevel(temp.current)
self.icon = Section.ramp(temp.current / temp.high, self.RAMP)
if self.state >= 1:
return "{:.0f}°C".format(temp.current)
def __init__(self, theme=None):
AlertingSection.__init__(self, theme)
PeriodicUpdater.__init__(self)
self.changeInterval(5)
class BatteryProvider(AlertingSection, PeriodicUpdater):
# TODO Support ACPID for events
NUMBER_STATES = 3
RAMP = ""
def fetcher(self):
bat = psutil.sensors_battery()
if not bat:
self.icon = None
return None
self.icon = ("" if bat.power_plugged else "") + Section.ramp(
bat.percent / 100, self.RAMP
)
self.updateLevel(1 - bat.percent / 100)
if self.state < 1:
return
t = Text("{:.0f}%".format(bat.percent))
if self.state < 2:
return t
h = int(bat.secsleft / 3600)
m = int((bat.secsleft - h * 3600) / 60)
t.append(" ({:d}:{:02d})".format(h, m))
return t
def __init__(self, theme=None):
AlertingSection.__init__(self, theme)
PeriodicUpdater.__init__(self)
self.changeInterval(5)
class PulseaudioProvider(StatefulSection, ThreadedUpdater):
NUMBER_STATES = 3
DEFAULT_STATE = 1
def __init__(self, theme=None):
ThreadedUpdater.__init__(self)
StatefulSection.__init__(self, theme)
self.pulseEvents = pulsectl.Pulse("event-handler")
self.pulseEvents.event_mask_set(pulsectl.PulseEventMaskEnum.sink)
self.pulseEvents.event_callback_set(self.handleEvent)
self.start()
self.refreshData()
def fetcher(self):
sinks = []
with pulsectl.Pulse("list-sinks") as pulse:
for sink in pulse.sink_list():
if sink.port_active.name == "analog-output-headphones":
icon = ""
elif sink.port_active.name == "analog-output-speaker":
icon = "" if sink.mute else ""
elif sink.port_active.name == "headset-output":
icon = ""
else:
icon = "?"
vol = pulse.volume_get_all_chans(sink)
fg = (sink.mute and "#333333") or (vol > 1 and "#FF0000") or None
t = Text(icon, fg=fg)
sinks.append(t)
if self.state < 1:
continue
if self.state < 2:
if not sink.mute:
ramp = " "
while vol >= 0:
ramp += self.ramp(vol if vol < 1 else 1)
vol -= 1
t.append(ramp)
else:
t.append(" {:2.0f}%".format(vol * 100))
return Text(*sinks)
def loop(self):
self.pulseEvents.event_listen()
def handleEvent(self, ev):
self.refreshData()
class NetworkProviderSection(StatefulSection, Updater):
NUMBER_STATES = 5
DEFAULT_STATE = 1
def actType(self):
self.ssid = None
if self.iface.startswith("eth") or self.iface.startswith("enp"):
if "u" in self.iface:
self.icon = ""
else:
self.icon = ""
elif self.iface.startswith("wlan") or self.iface.startswith("wl"):
self.icon = ""
if self.showSsid:
cmd = ["iwgetid", self.iface, "--raw"]
p = subprocess.run(cmd, stdout=subprocess.PIPE)
self.ssid = p.stdout.strip().decode()
elif self.iface.startswith("tun") or self.iface.startswith("tap"):
self.icon = ""
elif self.iface.startswith("docker"):
self.icon = ""
elif self.iface.startswith("veth"):
self.icon = ""
elif self.iface.startswith("vboxnet"):
self.icon = ""
else:
self.icon = "?"
def getAddresses(self):
ipv4 = None
ipv6 = None
for address in self.parent.addrs[self.iface]:
if address.family == socket.AF_INET:
ipv4 = address
elif address.family == socket.AF_INET6:
ipv6 = address
return ipv4, ipv6
def fetcher(self):
self.icon = None
self.persistent = False
if (
self.iface not in self.parent.stats
or not self.parent.stats[self.iface].isup
or self.iface.startswith("lo")
):
return None
# Get addresses
ipv4, ipv6 = self.getAddresses()
if ipv4 is None and ipv6 is None:
return None
text = []
self.persistent = True
self.actType()
if self.showSsid and self.ssid:
text.append(self.ssid)
if self.showAddress:
if ipv4:
netStrFull = "{}/{}".format(ipv4.address, ipv4.netmask)
addr = ipaddress.IPv4Network(netStrFull, strict=False)
addrStr = "{}/{}".format(ipv4.address, addr.prefixlen)
text.append(addrStr)
# TODO IPV6
# if ipv6:
# text += ' ' + ipv6.address
if self.showSpeed:
recvDiff = (
self.parent.IO[self.iface].bytes_recv
- self.parent.prevIO[self.iface].bytes_recv
)
sentDiff = (
self.parent.IO[self.iface].bytes_sent
- self.parent.prevIO[self.iface].bytes_sent
)
recvDiff /= self.parent.dt
sentDiff /= self.parent.dt
text.append("{}{}".format(humanSize(recvDiff), humanSize(sentDiff)))
if self.showTransfer:
text.append(
"{}{}".format(
humanSize(self.parent.IO[self.iface].bytes_recv),
humanSize(self.parent.IO[self.iface].bytes_sent),
)
)
return " ".join(text)
def onChangeState(self, state):
self.showSsid = state >= 1
self.showAddress = state >= 2
self.showSpeed = state >= 3
self.showTransfer = state >= 4
def __init__(self, iface, parent):
Updater.__init__(self)
StatefulSection.__init__(self, theme=parent.theme)
self.iface = iface
self.parent = parent
class NetworkProvider(Section, PeriodicUpdater):
def fetchData(self):
self.prev = self.last
self.prevIO = self.IO
self.stats = psutil.net_if_stats()
self.addrs = psutil.net_if_addrs()
self.IO = psutil.net_io_counters(pernic=True)
self.ifaces = self.stats.keys()
self.last = time.perf_counter()
self.dt = self.last - self.prev
def fetcher(self):
self.fetchData()
# Add missing sections
lastSection = self
for iface in sorted(list(self.ifaces)):
if iface not in self.sections.keys():
section = NetworkProviderSection(iface, self)
lastSection.appendAfter(section)
self.sections[iface] = section
else:
section = self.sections[iface]
lastSection = section
# Refresh section text
for section in self.sections.values():
section.refreshData()
return None
def addParent(self, parent):
self.parents.add(parent)
self.refreshData()
def __init__(self, theme=None):
PeriodicUpdater.__init__(self)
Section.__init__(self, theme)
self.sections = dict()
self.last = 0
self.IO = dict()
self.fetchData()
self.changeInterval(5)
class RfkillProvider(Section, PeriodicUpdater):
# TODO FEAT rfkill doesn't seem to indicate that the hardware switch is
# toggled
PATH = "/sys/class/rfkill"
def fetcher(self):
t = Text()
for device in os.listdir(self.PATH):
with open(os.path.join(self.PATH, device, "soft"), "rb") as f:
softBlocked = f.read().strip() != b"0"
with open(os.path.join(self.PATH, device, "hard"), "rb") as f:
hardBlocked = f.read().strip() != b"0"
if not hardBlocked and not softBlocked:
continue
with open(os.path.join(self.PATH, device, "type"), "rb") as f:
typ = f.read().strip()
fg = (hardBlocked and "#CCCCCC") or (softBlocked and "#FF0000")
if typ == b"wlan":
icon = ""
elif typ == b"bluetooth":
icon = ""
else:
icon = "?"
t.append(Text(icon, fg=fg))
return t
def __init__(self, theme=None):
PeriodicUpdater.__init__(self)
Section.__init__(self, theme)
self.changeInterval(5)
class SshAgentProvider(PeriodicUpdater):
def fetcher(self):
cmd = ["ssh-add", "-l"]
proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
if proc.returncode != 0:
return None
text = Text()
for line in proc.stdout.split(b"\n"):
if not len(line):
continue
fingerprint = line.split()[1]
text.append(Text("", fg=randomColor(seed=fingerprint)))
return text
def __init__(self):
PeriodicUpdater.__init__(self)
self.changeInterval(5)
class GpgAgentProvider(PeriodicUpdater):
def fetcher(self):
cmd = ["gpg-connect-agent", "keyinfo --list", "/bye"]
proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
# proc = subprocess.run(cmd)
if proc.returncode != 0:
return None
text = Text()
for line in proc.stdout.split(b"\n"):
if not len(line) or line == b"OK":
continue
spli = line.split()
if spli[6] != b"1":
continue
keygrip = spli[2]
text.append(Text("", fg=randomColor(seed=keygrip)))
return text
def __init__(self):
PeriodicUpdater.__init__(self)
self.changeInterval(5)
class KeystoreProvider(Section, MergedUpdater):
# TODO OPTI+FEAT Use ColorCountsSection and not MergedUpdater, this is useless
ICON = ""
def __init__(self, theme=None):
MergedUpdater.__init__(self, SshAgentProvider(), GpgAgentProvider())
Section.__init__(self, theme)
class NotmuchUnreadProvider(ColorCountsSection, InotifyUpdater):
COLORABLE_ICON = ""
def subfetcher(self):
db = notmuch.Database(mode=notmuch.Database.MODE.READ_ONLY, path=self.dir)
counts = []
for account in self.accounts:
queryStr = "folder:/{}/ and tag:unread".format(account)
query = notmuch.Query(db, queryStr)
nbMsgs = query.count_messages()
if account == "frogeye":
global q
q = query
if nbMsgs < 1:
continue
counts.append((nbMsgs, self.colors[account]))
# db.close()
return counts
def __init__(self, dir="~/.mail/", theme=None):
PeriodicUpdater.__init__(self)
ColorCountsSection.__init__(self, theme)
self.dir = os.path.realpath(os.path.expanduser(dir))
assert os.path.isdir(self.dir)
# Fetching account list
self.accounts = sorted(
[a for a in os.listdir(self.dir) if not a.startswith(".")]
)
# Fetching colors
self.colors = dict()
for account in self.accounts:
filename = os.path.join(self.dir, account, "color")
with open(filename, "r") as f:
color = f.read().strip()
self.colors[account] = color
self.addPath(os.path.join(self.dir, ".notmuch", "xapian"))
class TodoProvider(ColorCountsSection, InotifyUpdater):
# TODO OPT/UX Maybe we could get more data from the todoman python module
# TODO OPT Specific callback for specific directory
COLORABLE_ICON = ""
def updateCalendarList(self):
calendars = sorted(os.listdir(self.dir))
for calendar in calendars:
# If the calendar wasn't in the list
if calendar not in self.calendars:
self.addPath(os.path.join(self.dir, calendar), refresh=False)
# Fetching name
path = os.path.join(self.dir, calendar, "displayname")
with open(path, "r") as f:
self.names[calendar] = f.read().strip()
# Fetching color
path = os.path.join(self.dir, calendar, "color")
with open(path, "r") as f:
self.colors[calendar] = f.read().strip()
self.calendars = calendars
def __init__(self, dir, theme=None):
"""
:parm str dir: [main]path value in todoman.conf
"""
InotifyUpdater.__init__(self)
ColorCountsSection.__init__(self, theme=theme)
self.dir = os.path.realpath(os.path.expanduser(dir))
assert os.path.isdir(self.dir)
self.calendars = []
self.colors = dict()
self.names = dict()
self.updateCalendarList()
self.refreshData()
def countUndone(self, calendar):
cmd = ["todo", "--porcelain", "list"]
if calendar:
cmd.append(self.names[calendar])
proc = subprocess.run(cmd, stdout=subprocess.PIPE)
data = json.loads(proc.stdout)
return len(data)
def subfetcher(self):
counts = []
# TODO This an ugly optimisation that cuts on features, but todoman
# calls are very expensive so we keep that in the meanwhile
if self.state < 2:
c = self.countUndone(None)
if c > 0:
counts.append((c, "#00000"))
counts.append((0, "#FFFFF"))
return counts
# Optimisation ends here
for calendar in self.calendars:
c = self.countUndone(calendar)
if c <= 0:
continue
counts.append((c, self.colors[calendar]))
return counts
class I3WindowTitleProvider(Section, I3Updater):
# TODO FEAT To make this available from start, we need to find the
# `focused=True` element following the `focus` array
# TODO Feat Make this output dependant if wanted
def on_window(self, i3, e):
self.updateText(e.container.name)
def __init__(self, theme=None):
I3Updater.__init__(self)
Section.__init__(self, theme=theme)
self.on("window", self.on_window)
class I3WorkspacesProviderSection(Section):
def selectTheme(self):
if self.urgent:
return self.parent.themeUrgent
elif self.focused:
return self.parent.themeFocus
else:
return self.parent.themeNormal
# TODO On mode change the state (shown / hidden) gets overriden so every
# tab is shown
def show(self):
self.updateTheme(self.selectTheme())
self.updateText(self.fullName if self.focused else self.shortName)
def changeState(self, focused, urgent):
self.focused = focused
self.urgent = urgent
self.show()
def setName(self, name):
self.shortName = name
self.fullName = (
self.parent.customNames[name] if name in self.parent.customNames else name
)
def switchTo(self):
self.parent.i3.command("workspace {}".format(self.shortName))
def __init__(self, name, parent):
Section.__init__(self)
self.parent = parent
self.setName(name)
self.setDecorators(clickLeft=self.switchTo)
self.tempText = None
def empty(self):
self.updateTheme(self.parent.themeNormal)
self.updateText(None)
def tempShow(self):
self.updateText(self.tempText)
def tempEmpty(self):
self.tempText = self.dstText[1]
self.updateText(None)
class I3WorkspacesProvider(Section, I3Updater):
# TODO FEAT Multi-screen
def initialPopulation(self, parent):
"""
Called on init
Can't reuse addWorkspace since i3.get_workspaces() gives dict and not
ConObjects
"""
workspaces = self.i3.get_workspaces()
lastSection = self.modeSection
for workspace in workspaces:
# if parent.display != workspace["display"]:
# continue
section = I3WorkspacesProviderSection(workspace.name, self)
section.focused = workspace.focused
section.urgent = workspace.urgent
section.show()
parent.addSectionAfter(lastSection, section)
self.sections[workspace.num] = section
lastSection = section
def on_workspace_init(self, i3, e):
workspace = e.current
i = workspace.num
if i in self.sections:
section = self.sections[i]
else:
# Find the section just before
while i not in self.sections.keys() and i > 0:
i -= 1
prevSection = self.sections[i] if i != 0 else self.modeSection
section = I3WorkspacesProviderSection(workspace.name, self)
prevSection.appendAfter(section)
self.sections[workspace.num] = section
section.focused = workspace.focused
section.urgent = workspace.urgent
section.show()
def on_workspace_empty(self, i3, e):
self.sections[e.current.num].empty()
def on_workspace_focus(self, i3, e):
self.sections[e.old.num].focused = False
self.sections[e.old.num].show()
self.sections[e.current.num].focused = True
self.sections[e.current.num].show()
def on_workspace_urgent(self, i3, e):
self.sections[e.current.num].urgent = e.current.urgent
self.sections[e.current.num].show()
def on_workspace_rename(self, i3, e):
self.sections[e.current.num].setName(e.name)
self.sections[e.current.num].show()
def on_mode(self, i3, e):
if e.change == "default":
self.modeSection.updateText(None)
for section in self.sections.values():
section.tempShow()
else:
self.modeSection.updateText(e.change)
for section in self.sections.values():
section.tempEmpty()
def __init__(
self, theme=0, themeFocus=3, themeUrgent=1, themeMode=2, customNames=dict()
):
I3Updater.__init__(self)
Section.__init__(self)
self.themeNormal = theme
self.themeFocus = themeFocus
self.themeUrgent = themeUrgent
self.customNames = customNames
self.sections = dict()
self.on("workspace::init", self.on_workspace_init)
self.on("workspace::focus", self.on_workspace_focus)
self.on("workspace::empty", self.on_workspace_empty)
self.on("workspace::urgent", self.on_workspace_urgent)
self.on("workspace::rename", self.on_workspace_rename)
# TODO Un-handled/tested: reload, rename, restored, move
self.on("mode", self.on_mode)
self.modeSection = Section(theme=themeMode)
def addParent(self, parent):
self.parents.add(parent)
parent.addSection(self.modeSection)
self.initialPopulation(parent)
class MpdProvider(Section, ThreadedUpdater):
# TODO FEAT More informations and controls
MAX_LENGTH = 50
def connect(self):
self.mpd.connect("localhost", 6600)
def __init__(self, theme=None):
ThreadedUpdater.__init__(self)
Section.__init__(self, theme)
self.mpd = mpd.MPDClient()
self.connect()
self.refreshData()
self.start()
def fetcher(self):
stat = self.mpd.status()
if not len(stat) or stat["state"] == "stop":
return None
cur = self.mpd.currentsong()
if not len(cur):
return None
infos = []
def tryAdd(field):
if field in cur:
infos.append(cur[field])
tryAdd("title")
tryAdd("album")
tryAdd("artist")
infosStr = " - ".join(infos)
if len(infosStr) > MpdProvider.MAX_LENGTH:
infosStr = infosStr[: MpdProvider.MAX_LENGTH - 1] + ""
return "{}".format(infosStr)
def loop(self):
try:
self.mpd.idle("player")
self.refreshData()
except mpd.base.ConnectionError as e:
log.warn(e, exc_info=True)
self.connect()
except BaseException as e:
log.error(e, exc_info=True)

View file

@ -1,271 +0,0 @@
#!/usr/bin/env python3
import functools
import logging
import math
import os
import threading
import time
import coloredlogs
import i3ipc
import pyinotify
from frobar.display import Text
from frobar.notbusy import notBusy
coloredlogs.install(level="DEBUG", fmt="%(levelname)s %(message)s")
log = logging.getLogger()
# TODO Sync bar update with PeriodicUpdater updates
class Updater:
@staticmethod
def init():
PeriodicUpdater.init()
InotifyUpdater.init()
notBusy.set()
def updateText(self, text):
print(text)
def fetcher(self):
return "{} refreshed".format(self)
def __init__(self):
self.lock = threading.Lock()
def refreshData(self):
# TODO OPTI Maybe discard the refresh if there's already another one?
self.lock.acquire()
try:
data = self.fetcher()
except BaseException as e:
log.error(e, exc_info=True)
data = ""
self.updateText(data)
self.lock.release()
class PeriodicUpdaterThread(threading.Thread):
def run(self):
# TODO Sync with system clock
counter = 0
while True:
notBusy.set()
if PeriodicUpdater.intervalsChanged.wait(
timeout=PeriodicUpdater.intervalStep
):
# ↑ sleeps here
notBusy.clear()
PeriodicUpdater.intervalsChanged.clear()
counter = 0
for providerList in PeriodicUpdater.intervals.copy().values():
for provider in providerList.copy():
provider.refreshData()
else:
notBusy.clear()
counter += PeriodicUpdater.intervalStep
counter = counter % PeriodicUpdater.intervalLoop
for interval in PeriodicUpdater.intervals.keys():
if counter % interval == 0:
for provider in PeriodicUpdater.intervals[interval]:
provider.refreshData()
class PeriodicUpdater(Updater):
"""
Needs to call :func:`PeriodicUpdater.changeInterval` in `__init__`
"""
intervals = dict()
intervalStep = None
intervalLoop = None
updateThread = PeriodicUpdaterThread(daemon=True)
intervalsChanged = threading.Event()
@staticmethod
def gcds(*args):
return functools.reduce(math.gcd, args)
@staticmethod
def lcm(a, b):
"""Return lowest common multiple."""
return a * b // math.gcd(a, b)
@staticmethod
def lcms(*args):
"""Return lowest common multiple."""
return functools.reduce(PeriodicUpdater.lcm, args)
@staticmethod
def updateIntervals():
intervalsList = list(PeriodicUpdater.intervals.keys())
PeriodicUpdater.intervalStep = PeriodicUpdater.gcds(*intervalsList)
PeriodicUpdater.intervalLoop = PeriodicUpdater.lcms(*intervalsList)
PeriodicUpdater.intervalsChanged.set()
@staticmethod
def init():
PeriodicUpdater.updateThread.start()
def __init__(self):
Updater.__init__(self)
self.interval = None
def changeInterval(self, interval):
assert isinstance(interval, int)
if self.interval is not None:
PeriodicUpdater.intervals[self.interval].remove(self)
self.interval = interval
if interval not in PeriodicUpdater.intervals:
PeriodicUpdater.intervals[interval] = set()
PeriodicUpdater.intervals[interval].add(self)
PeriodicUpdater.updateIntervals()
class InotifyUpdaterEventHandler(pyinotify.ProcessEvent):
def process_default(self, event):
# DEBUG
# from pprint import pprint
# pprint(event.__dict__)
# return
assert event.path in InotifyUpdater.paths
if 0 in InotifyUpdater.paths[event.path]:
for provider in InotifyUpdater.paths[event.path][0]:
provider.refreshData()
if event.name in InotifyUpdater.paths[event.path]:
for provider in InotifyUpdater.paths[event.path][event.name]:
provider.refreshData()
class InotifyUpdater(Updater):
"""
Needs to call :func:`PeriodicUpdater.changeInterval` in `__init__`
"""
wm = pyinotify.WatchManager()
paths = dict()
@staticmethod
def init():
notifier = pyinotify.ThreadedNotifier(
InotifyUpdater.wm, InotifyUpdaterEventHandler()
)
notifier.start()
# TODO Mask for folders
MASK = pyinotify.IN_CREATE | pyinotify.IN_MODIFY | pyinotify.IN_DELETE
def addPath(self, path, refresh=True):
path = os.path.realpath(os.path.expanduser(path))
# Detect if file or folder
if os.path.isdir(path):
self.dirpath = path
# 0: Directory watcher
self.filename = 0
elif os.path.isfile(path):
self.dirpath = os.path.dirname(path)
self.filename = os.path.basename(path)
else:
raise FileNotFoundError("No such file or directory: '{}'".format(path))
# Register watch action
if self.dirpath not in InotifyUpdater.paths:
InotifyUpdater.paths[self.dirpath] = dict()
if self.filename not in InotifyUpdater.paths[self.dirpath]:
InotifyUpdater.paths[self.dirpath][self.filename] = set()
InotifyUpdater.paths[self.dirpath][self.filename].add(self)
# Add watch
InotifyUpdater.wm.add_watch(self.dirpath, InotifyUpdater.MASK)
if refresh:
self.refreshData()
class ThreadedUpdaterThread(threading.Thread):
def __init__(self, updater, *args, **kwargs):
self.updater = updater
threading.Thread.__init__(self, *args, **kwargs)
self.looping = True
def run(self):
try:
while self.looping:
self.updater.loop()
except BaseException as e:
log.error("Error with {}".format(self.updater))
log.error(e, exc_info=True)
self.updater.updateText("")
class ThreadedUpdater(Updater):
"""
Must implement loop(), and call start()
"""
def __init__(self):
Updater.__init__(self)
self.thread = ThreadedUpdaterThread(self, daemon=True)
def loop(self):
self.refreshData()
time.sleep(10)
def start(self):
self.thread.start()
class I3Updater(ThreadedUpdater):
# TODO OPTI One i3 connection for all
def __init__(self):
ThreadedUpdater.__init__(self)
self.i3 = i3ipc.Connection()
self.start()
def on(self, event, function):
self.i3.on(event, function)
def loop(self):
self.i3.main()
class MergedUpdater(Updater):
# TODO OPTI Do not update until end of periodic batch
def fetcher(self):
text = Text()
for updater in self.updaters:
text.append(self.texts[updater])
if not len(text):
return None
return text
def __init__(self, *args):
Updater.__init__(self)
self.updaters = []
self.texts = dict()
for updater in args:
assert isinstance(updater, Updater)
def newUpdateText(updater, text):
self.texts[updater] = text
self.refreshData()
updater.updateText = newUpdateText.__get__(updater, Updater)
self.updaters.append(updater)
self.texts[updater] = ""

View file

@ -1,15 +1,35 @@
{ pkgs, lib, config, ... }: {
pkgs,
lib,
config,
...
}:
{ {
config = lib.mkIf config.frogeye.gaming { config = lib.mkIf config.frogeye.gaming {
# Using config.nixpkgs.<something> creates an infinite recursion, # Using config.nixpkgs.<something> creates an infinite recursion,
# but the above might not be correct in case of cross-compiling? # but the above might not be correct in case of cross-compiling?
home.packages = with pkgs; [ home = {
# gaming packages = with pkgs; [
yuzu-mainline # gaming
minecraft dolphin-emu
# TODO factorio ryujinx
prismlauncher
# TODO factorio
steam # Common pitfall: https://github.com/NixOS/nixpkgs/issues/86506#issuecomment-623746883 steam # Common pitfall: https://github.com/NixOS/nixpkgs/issues/86506#issuecomment-623746883
]; # itch # butler-15.21.0 is broken
(pkgs.python3Packages.ds4drv.overrideAttrs (old: {
src = fetchFromGitHub {
owner = "TheDrHax";
repo = "ds4drv-cemuhook";
rev = "a58f63b70f8d8efa33e5e82a8888a1e08754aeed";
sha256 = "sha256-oMvHw5zeO0skoiqLU+EdjUabTvkipeBh+m8RHJcWZP8=";
};
}))
];
sessionVariables = {
BOOT9_PATH = "${config.xdg.dataHome}/citra-emu/sysdata/boot9.bin";
};
};
}; };
} }

130
hm/git/default.nix Normal file
View file

@ -0,0 +1,130 @@
{
pkgs,
lib,
config,
...
}:
let
cfg = config.programs.git;
in
{
config = lib.mkIf cfg.enable {
home.packages = [
pkgs.jjui
pkgs.lazyjj
(pkgs.writeShellApplication {
name = "git-sync";
text = (
lib.strings.concatLines (
map (r: ''
echo "===== ${r.path}"
if [ ! -d "${r.path}" ]
then
${pkgs.git}/bin/git clone "${r.uri}" "${r.path}"
else
(
cd "${r.path}"
if [ -d .jj ]
then
${lib.getExe config.programs.jujutsu.package} git fetch
${lib.getExe config.programs.jujutsu.package} rebase -d main@origin
${lib.getExe config.programs.jujutsu.package} bookmark set main -r @-
${lib.getExe config.programs.jujutsu.package} git push
else
${pkgs.git}/bin/git --no-optional-locks diff --quiet || echo "Repository is dirty!"
${pkgs.git}/bin/git pull || true
# Only push if there's something to push. Also prevents from trying to push on repos where we don't have rights.
(${pkgs.git}/bin/git --no-optional-locks status --porcelain -b --ignore-submodules | grep ' \[ahead [0-9]\+\]' && ${pkgs.git}/bin/git push) || true
fi
)
fi
'') (lib.attrsets.attrValues config.services.git-sync.repositories)
)
);
})
];
programs = {
git = {
package = pkgs.gitFull;
aliases = {
"git" = "!exec git"; # In case I write one too many git
};
ignores = [
"*.swp"
"*.swo"
"*.ycm_extra_conf.py"
"tags"
".mypy_cache"
];
delta = {
enable = true;
options = {
line-numbers = true;
syntax-theme = "base16";
};
};
# Also tried difftastic, and while I like the default theme it's a bit
# less configurable
lfs.enable = true;
userEmail = lib.mkDefault "geoffrey@frogeye.fr";
userName = lib.mkDefault "Geoffrey Frogeye";
extraConfig =
{
core = {
editor = "nvim";
};
push = {
default = "matching";
};
pull = {
ff = "only";
};
}
// lib.optionalAttrs config.frogeye.desktop.xorg {
diff.tool = "meld";
difftool.prompt = false;
"difftool \"meld\"".cmd = "${pkgs.meld}/bin/meld \"$LOCAL\" \"$REMOTE\"";
# This escapes quotes, which isn't the case in the original, hoping this isn't an issue.
};
};
jujutsu = {
enable = true;
# Current version doesn't have the "none" signing backend
settings = {
git = {
auto-local-bookmark = true;
auto-local-branch = true;
};
user = {
email = cfg.userEmail;
name = cfg.userName;
};
ui = {
pager = "delta";
diff.format = "git";
diff-editor = "meld-3";
merge-editor = "meld";
};
signing = {
sign-all = true;
backend = "gpg";
inherit (cfg.signing) key;
backends.gpg.allow-expired-keys = false;
};
};
};
};
services = {
git-sync = {
enable = false; # The real thing syncs too quickly and asks for passphrase, which is annoying
# So for now it's just a way to park config which will be reused by git-sync-* commands
repositories = {
dotfiles = {
path = "${config.xdg.configHome}/dotfiles";
uri = lib.mkDefault "https://git.frogeye.fr/geoffrey/dotfiles.git";
};
};
};
};
};
}

60
hm/gpg/default.nix Normal file
View file

@ -0,0 +1,60 @@
{
pkgs,
lib,
config,
...
}:
{
config = lib.mkIf config.programs.gpg.enable {
frogeye.hooks.lock = ''
echo RELOADAGENT | ${pkgs.gnupg}/bin/gpg-connect-agent
'';
programs.gpg = {
homedir = "${config.xdg.stateHome}/gnupg";
settings = {
# Remove fluff
no-greeting = true;
no-emit-version = true;
no-comments = true;
# Output format that I prefer
keyid-format = "0xlong";
# Show fingerprints
with-fingerprint = true;
# Make sure to show if key is invalid
# (should be default on most platform,
# but just to be sure)
list-options = "show-uid-validity";
verify-options = "show-uid-validity";
# Stronger algorithm (https://wiki.archlinux.org/title/GnuPG#Different_algorithm)
personal-digest-preferences = "SHA512";
cert-digest-algo = "SHA512";
default-preference-list = "SHA512 SHA384 SHA256 SHA224 AES256 AES192 AES CAST5 ZLIB BZIP2 ZIP Uncompressed";
personal-cipher-preferences = "TWOFISH CAMELLIA256 AES 3DES";
};
publicKeys = [
{
# Always install my own public key
source = builtins.fetchurl {
url = "https://keys.openpgp.org/vks/v1/by-fingerprint/4FBA930D314A03215E2CDB0A8312C8CAC1BAC289";
sha256 = "sha256:10y9xqcy1vyk2p8baay14p3vwdnlwynk0fvfbika65hz2z8yw2cm";
};
trust = "ultimate";
}
];
};
services.gpg-agent = rec {
enableBashIntegration = true;
enableZshIntegration = true;
# gnome3 is nicer, but requires gcr as a dbus package.
# Which is in my NixOS config, and on non-NixOS too.
# It will fall back to ncurses when running in non-graphics mode.
pinentryPackage = pkgs.pinentry-gnome3;
# If inactive, the key will be forgotten after this time
defaultCacheTtl = 3600;
defaultCacheTtlSsh = defaultCacheTtl;
# If active, the key will be forgotten adfter this time
maxCacheTtl = 3 * 3600;
maxCacheTtlSsh = maxCacheTtl;
};
};
}

66
hm/homealone.nix Normal file
View file

@ -0,0 +1,66 @@
{ lib, config, ... }:
{
config = {
frogeye = {
# TODO Move to relevant config file. Rest can probably removed.
direnv = {
CARGOHOME = "${config.xdg.cacheHome}/cargo"; # There are config in there that we can version if one want
DASHT_DOCSETS_DIR = "${config.xdg.cacheHome}/dash_docsets";
GRADLE_USER_HOME = "${config.xdg.cacheHome}/gradle";
MIX_ARCHIVES = "${config.xdg.cacheHome}/mix/archives";
MONO_GAC_PREFIX = "${config.xdg.cacheHome}/mono";
PARALLEL_HOME = "${config.xdg.cacheHome}/parallel";
TERMINFO = "${config.xdg.configHome}/terminfo";
WINEPREFIX = "${config.xdg.stateHome}/wineprefix/default";
};
junkhome = [
"adb"
"audacity"
"cabal" # TODO May have options but last time I tried it it crashed
"itch"
"simplescreenrecorder" # Easy fix https://github.com/MaartenBaert/ssr/blob/1556ae456e833992fb6d39d40f7c7d7c337a4160/src/Main.cpp#L252
"vd"
"wpa_cli"
# TODO Maybe we can do something about node-gyp
];
};
home = {
activation.createDirenvFolders = lib.hm.dag.entryAfter [ "writeBoundary" ] (
lib.strings.concatLines (
map (d: "mkdir -p ${d}") (
(builtins.attrValues config.frogeye.direnv) ++ [ "${config.xdg.cacheHome}/junkhome" ]
)
)
);
sessionVariables = config.frogeye.direnv;
};
programs.bash.shellAliases = lib.attrsets.mergeAttrsList (
map (p: { "${p}" = "HOME=${config.xdg.cacheHome}/junkhome ${p}"; }) config.frogeye.junkhome
);
};
options.frogeye = {
direnv = lib.mkOption {
default = { };
example = lib.literalExpression ''
{
DASHT_DOCSETS_DIR = "''${config.xdg.cacheHome}/dash_docsets";
}
'';
description = ''
Environment variables for which the value is a folder that will be automatically created.
Useful for keeping programs data out of $HOME for programs that won't create the directory themselves.
'';
type = lib.types.attrsOf lib.types.str;
};
junkhome = lib.mkOption {
default = [ ];
description = ''
Program names that will be run with a different HOME so they don't clutter the real one.
Useful for programs that don't follow the XDG specification and tend to advertise themselves.
'';
type = lib.types.listOf lib.types.str;
};
# TODO Should make a nix package wrapper instead, so it also works from dmenu
};
}

40
hm/monitoring/default.nix Normal file
View file

@ -0,0 +1,40 @@
{
pkgs,
lib,
config,
...
}:
{
config = {
home.packages = with pkgs; [
htop
iftop
iotop
lsof
progress
pv
speedtest-cli
strace
];
programs.bash.shellAliases = {
iftop = "iftop -c ${config.xdg.configHome}/iftoprc";
tracefiles = ''${pkgs.strace}/bin/strace -f -t -e trace=file'';
};
xdg = {
configFile = {
"iftoprc" = {
text = ''
port-resolution: no
promiscuous: no
port-display: on
link-local: yes
use-bytes: yes
show-totals: yes
log-scale: yes
'';
};
};
};
};
}

32
hm/nix/default.nix Normal file
View file

@ -0,0 +1,32 @@
{
pkgs,
lib,
config,
...
}:
{
config = {
home.packages = with pkgs; [
nvd
nix-diff
nix-tree
nix-output-monitor
];
programs.nix-index = {
# For non-NixOS systems
enable = false; # TODO Index is impossible to generate, should use https://github.com/nix-community/nix-index-database
# but got no luck without flakes
enableZshIntegration = true;
};
nix = {
package = lib.mkDefault pkgs.lix;
settings = {
experimental-features = [
"nix-command"
"flakes"
];
warn-dirty = false;
};
};
};
}

18
hm/pager/default.nix Normal file
View file

@ -0,0 +1,18 @@
{ config, ... }:
{
config = {
home = {
sessionVariables = {
LESSHISTFILE = "${config.xdg.stateHome}/lesshst";
LESS = "-R";
LESS_TERMCAP_mb = "$(echo $'\\E[1;31m')"; # begin blink
LESS_TERMCAP_md = "$(echo $'\\E[1;36m')"; # begin bold
LESS_TERMCAP_me = "$(echo $'\\E[0m')"; # reset bold/blink
LESS_TERMCAP_se = "$(echo $'\\E[0m')"; # reset reverse video
LESS_TERMCAP_so = "$(echo $'\\E[01;44;33m')"; # begin reverse video
LESS_TERMCAP_ue = "$(echo $'\\E[0m')"; # reset underline
LESS_TERMCAP_us = "$(echo $'\\E[1;32m')"; # begin underline
};
};
};
}

143
hm/password/default.nix Normal file
View file

@ -0,0 +1,143 @@
{
pkgs,
lib,
config,
...
}:
let
mod = config.xsession.windowManager.i3.config.modifier;
in
{
config = {
home.packages = with pkgs; [
pwgen
(pkgs.writeShellApplication {
name = "install-passwords";
runtimeInputs = [
yq
gawk
moreutils
];
text = (
lib.strings.concatLines (
map (
file:
''
(
echo "===== Preparing to write ${file.path}"
temp="$(mktemp --tmpdir="${builtins.dirOf file.path}")"
cat "${file.template}" > "$temp"
''
+ (lib.strings.concatLines (
map (
password:
(
if password.selector == null then
''
echo "Reading ${password.path} for substituting ${password.variable}"
value="$(pass "${password.path}" | head -n1)"
''
else
''
echo "Reading ${password.path} -> ${password.selector} for substituting ${password.variable}"
value="$(pass "${password.path}" | tail -n +2 | yq -r '.${password.selector}')"
''
)
+ ''
key="${password.variable}"
K="$key" V="$value" awk '{ gsub (ENVIRON["K"], ENVIRON["V"]); print }' "$temp" | sponge "$temp"
''
) (lib.attrsets.attrValues file.passwords)
))
+ ''
echo "Moving the file in place"
chown "${file.owner}" "$temp"
chmod u=r "$temp"
mv -f "$temp" "${file.path}"
)
''
) config.frogeye.passwordFiles
)
);
})
];
programs = {
bash.shellAliases = {
pw = ''${pkgs.pwgen}/bin/pwgen 32 -y''; # Generate passwords. ln((26*2+10)**32)/ln(2) ≅ 190 bits of entropy
};
password-store.enable = true;
};
xsession.windowManager.i3.config.keybindings."${mod}+c" = "exec --no-startup-id ${config.programs.rofi.pass.package}/bin/rofi-pass --last-used";
# TODO Try autopass.cr
};
options = {
frogeye.passwordFiles =
let
defaultvar = "@PASSWORD@";
pwtype =
{ name, ... }:
{
options = {
variable = lib.mkOption {
type = lib.types.str;
default = name;
description = "String in the template that will be substituted by the actual password";
};
path = lib.mkOption {
type = lib.types.str;
description = "Path to the password store entry";
};
selector = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "If set, will parse the password metadata as YML and use selector (yq) instead of the password.";
};
};
};
mainConfig = config;
in
lib.mkOption {
default = [ ];
type = lib.types.listOf (
lib.types.submodule (
{ config, ... }:
{
options = {
path = lib.mkOption {
type = lib.types.str;
description = "Where to place the file.";
};
owner = lib.mkOption {
type = lib.types.str;
default = mainConfig.home.username;
description = "Who will own the file.";
};
template = lib.mkOption {
type = lib.types.path;
default = pkgs.writeTextFile {
name = "pwfile-template";
text = config.text;
};
description = "Path to the template used to make the file. Exclusive with `text`.";
};
text = lib.mkOption {
type = lib.types.str;
default = defaultvar;
description = "Content of the template used to make the file. Exclusive with `template`.";
};
passwords = lib.mkOption {
default = lib.optionalAttrs (config.password != null) { ${defaultvar} = config.password; };
type = lib.types.attrsOf (lib.types.submodule pwtype);
description = "Paths to passwords that will substitute the variables in the template. Exclusive with `password`";
};
password = lib.mkOption {
type = lib.types.submodule ({ ... }@args: pwtype (args // { name = defaultvar; }));
description = "Path to password that will substitute '@PASSWORD@' in the template. Exclusive with `passwords`.";
};
};
}
)
);
};
};
}

37
hm/prompt/default.nix Normal file
View file

@ -0,0 +1,37 @@
{
...
}:
{
config = {
programs.powerline-go = {
enable = true;
modules = [
"user"
"host"
"direnv"
"cwd"
"perms"
"nix-shell"
"venv"
"git"
];
modulesRight = [
"jobs"
"exit"
"duration"
];
settings = {
colorize-hostname = true;
hostname-only-if-ssh = true;
max-width = 25;
cwd-max-dir-size = 10;
duration = "$( test -n \"$__TIMER\" && echo $(( $EPOCHREALTIME - $\{__TIMER:-EPOCHREALTIME})) || echo 0 )";
# UPST Implement this properly in home-manager, would allow for bash support
};
extraUpdatePS1 = ''
unset __TIMER
echo -en "\033]0; $USER@$HOST $PWD\007"
'';
};
};
}

36
hm/rebuild/default.nix Normal file
View file

@ -0,0 +1,36 @@
{ pkgs, config, ... }:
{
home.packages = [
pkgs.update-local-flakes
(pkgs.writeShellApplication {
name = "rb";
text = ''
verb="switch"
if [ "$#" -ge 1 ]
then
verb="$1"
shift
fi
nixos_flake="$(readlink -f /etc/nixos)"
if [ -f "$nixos_flake/flake.nix" ]
then
update-local-flakes "$nixos_flake"
nix run "$nixos_flake#nixosRebuild" -- "$verb" "$@"
fi
# TODO Fix nix-on-droid and home-manager
# hm_flake="${config.xdg.configHome}/home-manager/flake.nix"
# if [ -f "$hm_flake" ]
# then
# update-local-flakes "$hm_flake"
# home-manager "$verb" "$@"
# fi
# nod_flake="${config.xdg.configHome}/nix-on-droid/flake.nix"
# if [ -f "$nod_flake" ]
# then
# update-local-flakes "$nod_flake"
# nix-on-droid "$verb" --flake "$(dirname "$nod_flake")" "$@"
# fi
'';
})
];
}

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