Compare commits

..

254 commits

Author SHA1 Message Date
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
185 changed files with 7708 additions and 4374 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,12 @@
{ 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 +27,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

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)

12
cranberry/default.nix Normal file
View file

@ -0,0 +1,12 @@
{ 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;
};
};
}

45
cranberry/hardware.nix Normal file
View file

@ -0,0 +1,45 @@
{ 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" ];
};
# 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";
};
};
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,13 @@
# 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,11 +28,11 @@ 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;

View file

@ -0,0 +1,72 @@
{ pkgs, lib, config, ... }:
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
rec {
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
'';
};
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" "~@resouces" ];
UMask = "0077";
};
};
};
};
}

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

@ -0,0 +1,17 @@
{ 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" ];
};
};
};
}

17
curacao/default.nix Normal file
View file

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

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

@ -0,0 +1,53 @@
{ pkgs, lib, config, ... }:
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" "~@resouces" ];
UMask = "0077";
};
};
};
};
}

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

@ -0,0 +1,374 @@
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.
# Decompiled 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(level=logging.DEBUG)
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)
def fetch_callback(desk: Desk) -> None:
log.debug("Received state, sending")
hcur = desk.get_height()
hmin, hmax = desk.get_height_bounds()
# 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,41 @@
{ 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 +44,29 @@ 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 +118,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 +143,9 @@ 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,113 @@
{ lib, ... }: { pkgs, lib, nixos-hardware, unixpkgs, ... }:
{ let
imports = [ displays = {
<nixos-hardware/dell/g3/3779> embedded = {
]; output = "eDP-1";
edid = "00ffffffffffff000dae381700000000011c01049526157802a155a556519d280b505400000001010101010101010101010101010101b43b804a71383440302035007dd61000001ac32f804a71383440302035007dd61000001a000000fe003059395747803137334843450a00000000000041319e001000000a010a2020004f";
# UEFI works here, and variables can be touched };
boot.loader = { deskLeft = {
efi.canTouchEfiVariables = lib.mkDefault true; output = "HDMI-1-3"; # Internal HDMI port
grub = { edid = "00ffffffffffff004c2d7b09333032302f160103803420782a01f1a257529f270a505423080081c0810081809500a9c0b300d1c00101283c80a070b023403020360006442100001a000000fd00353f1e5111000a202020202020000000fc00533234423432300a2020202020000000ff0048344d434230333533340a2020010702010400023a80d072382d40102c458006442100001e011d007251d01e206e28550006442100001e011d00bc52d01e20b828554006442100001e8c0ad090204031200c4055000644210000188c0ad08a20e02d10103e9600064421000018000000000000000000000000000000000000000000000000000000000000000000d2";
enable = true; };
efiSupport = true; deskRight = {
device = "nodev"; # Don't install on MBR output = "DVI-I-2-1"; # DisplayLink
# TODO Maybe we could? In case the HDD doesn't boot anymore? 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?
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?
};
};
nixpkgs.overlays = [
(self: super: {
displaylink = (import unixpkgs {
inherit (super) system;
config.allowUnfree = true;
}).displaylink;
})
];
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,14 @@
{ 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";
};
};
}

13
curacao/usb.nix Normal file
View file

@ -0,0 +1,13 @@
{ 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,14 @@
{ 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 "$@"

718
flake.lock Normal file
View file

@ -0,0 +1,718 @@
{
"nodes": {
"base16": {
"inputs": {
"fromYaml": "fromYaml"
},
"locked": {
"lastModified": 1708890466,
"narHash": "sha256-LlrC09LoPi8OPYOGPXegD72v+//VapgAqhbOFS3i8sc=",
"owner": "SenchoPens",
"repo": "base16.nix",
"rev": "665b3c6748534eb766c777298721cece9453fdae",
"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-foot": {
"flake": false,
"locked": {
"lastModified": 1696725948,
"narHash": "sha256-65bz2bUL/yzZ1c8/GQASnoiGwaF8DczlxJtzik1c0AU=",
"owner": "tinted-theming",
"repo": "base16-foot",
"rev": "eedbcfa30de0a4baa03e99f5e3ceb5535c2755ce",
"type": "github"
},
"original": {
"owner": "tinted-theming",
"repo": "base16-foot",
"type": "github"
}
},
"base16-helix": {
"flake": false,
"locked": {
"lastModified": 1696727917,
"narHash": "sha256-FVrbPk+NtMra0jtlC5oxyNchbm8FosmvXIatkRbYy1g=",
"owner": "tinted-theming",
"repo": "base16-helix",
"rev": "dbe1480d99fe80f08df7970e471fac24c05f2ddb",
"type": "github"
},
"original": {
"owner": "tinted-theming",
"repo": "base16-helix",
"type": "github"
}
},
"base16-kitty": {
"flake": false,
"locked": {
"lastModified": 1665001328,
"narHash": "sha256-aRaizTYPpuWEcvoYE9U+YRX+Wsc8+iG0guQJbvxEdJY=",
"owner": "kdrag0n",
"repo": "base16-kitty",
"rev": "06bb401fa9a0ffb84365905ffbb959ae5bf40805",
"type": "github"
},
"original": {
"owner": "kdrag0n",
"repo": "base16-kitty",
"type": "github"
}
},
"base16-tmux": {
"flake": false,
"locked": {
"lastModified": 1696725902,
"narHash": "sha256-wDPg5elZPcQpu7Df0lI5O8Jv4A3T6jUQIVg63KDU+3Q=",
"owner": "tinted-theming",
"repo": "base16-tmux",
"rev": "c02050bebb60dbb20cb433cd4d8ce668ecc11ba7",
"type": "github"
},
"original": {
"owner": "tinted-theming",
"repo": "base16-tmux",
"type": "github"
}
},
"base16-vim": {
"flake": false,
"locked": {
"lastModified": 1663659192,
"narHash": "sha256-uJvaYYDMXvoo0fhBZUhN8WBXeJ87SRgof6GEK2efFT0=",
"owner": "chriskempson",
"repo": "base16-vim",
"rev": "3be3cd82cd31acfcab9a41bad853d9c68d30478d",
"type": "github"
},
"original": {
"owner": "chriskempson",
"repo": "base16-vim",
"type": "github"
}
},
"devshell": {
"inputs": {
"nixpkgs": [
"nixvim",
"nixpkgs"
]
},
"locked": {
"lastModified": 1728330715,
"narHash": "sha256-xRJ2nPOXb//u1jaBnDP56M7v5ldavjbtR6lfGqSvcKg=",
"owner": "numtide",
"repo": "devshell",
"rev": "dd6b80932022cea34a019e2bb32f6fa9e494dfef",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "devshell",
"type": "github"
}
},
"disko": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1730190761,
"narHash": "sha256-o5m5WzvY6cGIDupuOvjgNSS8AN6yP2iI9MtUC6q/uos=",
"owner": "nix-community",
"repo": "disko",
"rev": "3979285062d6781525cded0f6c4ff92e71376b55",
"type": "github"
},
"original": {
"id": "disko",
"type": "indirect"
}
},
"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-compat_3": {
"flake": false,
"locked": {
"lastModified": 1673956053,
"narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flake-parts": {
"inputs": {
"nixpkgs-lib": [
"nixvim",
"nixpkgs"
]
},
"locked": {
"lastModified": 1727826117,
"narHash": "sha256-K5ZLCyfO/Zj9mPFldf3iwS6oZStJcU4tSpiXTMYaaL0=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "3d04084d54bedc3d6b8b736c70ef449225c361b1",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1726560853,
"narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"fromYaml": {
"flake": false,
"locked": {
"lastModified": 1689549921,
"narHash": "sha256-iX0pk/uB019TdBGlaJEWvBCfydT6sRq+eDcGPifVsCM=",
"owner": "SenchoPens",
"repo": "fromYaml",
"rev": "11fbbbfb32e3289d3c631e0134a23854e7865c84",
"type": "github"
},
"original": {
"owner": "SenchoPens",
"repo": "fromYaml",
"type": "github"
}
},
"git-hooks": {
"inputs": {
"flake-compat": "flake-compat_2",
"gitignore": "gitignore",
"nixpkgs": [
"nixvim",
"nixpkgs"
],
"nixpkgs-stable": [
"nixvim",
"nixpkgs"
]
},
"locked": {
"lastModified": 1729104314,
"narHash": "sha256-pZRZsq5oCdJt3upZIU4aslS9XwFJ+/nVtALHIciX/BI=",
"owner": "cachix",
"repo": "git-hooks.nix",
"rev": "3c3e88f0f544d6bb54329832616af7eb971b6be6",
"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"
}
},
"gnome-shell": {
"flake": false,
"locked": {
"lastModified": 1713702291,
"narHash": "sha256-zYP1ehjtcV8fo+c+JFfkAqktZ384Y+y779fzmR9lQAU=",
"owner": "GNOME",
"repo": "gnome-shell",
"rev": "0d0aadf013f78a7f7f1dc984d0d812971864b934",
"type": "github"
},
"original": {
"owner": "GNOME",
"ref": "46.1",
"repo": "gnome-shell",
"type": "github"
}
},
"home-manager": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1726989464,
"narHash": "sha256-Vl+WVTJwutXkimwGprnEtXc/s/s8sMuXzqXaspIGlwM=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "2f23fa308a7c067e52dfcc30a0758f47043ec176",
"type": "github"
},
"original": {
"id": "home-manager",
"ref": "release-24.05",
"type": "indirect"
}
},
"home-manager_2": {
"inputs": {
"nixpkgs": [
"nixvim",
"nixpkgs"
]
},
"locked": {
"lastModified": 1726989464,
"narHash": "sha256-Vl+WVTJwutXkimwGprnEtXc/s/s8sMuXzqXaspIGlwM=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "2f23fa308a7c067e52dfcc30a0758f47043ec176",
"type": "github"
},
"original": {
"owner": "nix-community",
"ref": "release-24.05",
"repo": "home-manager",
"type": "github"
}
},
"home-manager_3": {
"inputs": {
"nixpkgs": [
"stylix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1714981474,
"narHash": "sha256-b3/U21CJjCjJKmA9WqUbZGZgCvospO3ArOUTgJugkOY=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "6ebe7be2e67be7b9b54d61ce5704f6fb466c536f",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "home-manager",
"type": "github"
}
},
"nix-darwin": {
"inputs": {
"nixpkgs": [
"nixvim",
"nixpkgs"
]
},
"locked": {
"lastModified": 1729826725,
"narHash": "sha256-w3WNlYxqWYsuzm/jgFPyhncduoDNjot28aC8j39TW0U=",
"owner": "lnl7",
"repo": "nix-darwin",
"rev": "7840909b00fbd5a183008a6eb251ea307fe4a76e",
"type": "github"
},
"original": {
"owner": "lnl7",
"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": 1730161780,
"narHash": "sha256-z5ILcmwMtiCoHTXS1KsQWqigO7HJO8sbyK7f7wn9F/E=",
"owner": "NixOS",
"repo": "nixos-hardware",
"rev": "07d15e8990d5d86a631641b4c429bc0a7400cfb8",
"type": "github"
},
"original": {
"id": "nixos-hardware",
"type": "indirect"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1730137625,
"narHash": "sha256-9z8oOgFZiaguj+bbi3k4QhAD6JabWrnv7fscC/mt0KE=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "64b80bfb316b57cdb8919a9110ef63393d74382a",
"type": "github"
},
"original": {
"id": "nixpkgs",
"ref": "nixos-24.05",
"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"
}
},
"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"
],
"treefmt-nix": "treefmt-nix"
},
"locked": {
"lastModified": 1729945968,
"narHash": "sha256-4u+nbBSMuXWGCtXxUPPEflRm54+y/HLIbhIep9do8Ew=",
"owner": "nix-community",
"repo": "nixvim",
"rev": "c05ac01070425ed0797b1ff678dc690c333cea74",
"type": "github"
},
"original": {
"owner": "nix-community",
"ref": "nixos-24.05",
"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": {
"locked": {
"lastModified": 1730300129,
"narHash": "sha256-QZm3ZsHn/75VsGg7ScPGfdByqBPFIQHmbpjT37iQp2g=",
"owner": "nix-community",
"repo": "NUR",
"rev": "656dcf946af3e368dd872fe525439518d8423080",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "NUR",
"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-foot": "base16-foot",
"base16-helix": "base16-helix",
"base16-kitty": "base16-kitty",
"base16-tmux": "base16-tmux",
"base16-vim": "base16-vim",
"flake-compat": "flake-compat_3",
"gnome-shell": "gnome-shell",
"home-manager": "home-manager_3",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1718122552,
"narHash": "sha256-A+dBkSwp8ssHKV/WyXb9uqIYrHBqHvtSedU24Lq9lqw=",
"owner": "danth",
"repo": "stylix",
"rev": "e59d2c1725b237c362e4a62f5722f5b268d566c7",
"type": "github"
},
"original": {
"owner": "danth",
"ref": "release-24.05",
"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"
}
},
"treefmt-nix": {
"inputs": {
"nixpkgs": [
"nixvim",
"nixpkgs"
]
},
"locked": {
"lastModified": 1729613947,
"narHash": "sha256-XGOvuIPW1XRfPgHtGYXd5MAmJzZtOuwlfKDgxX5KT3s=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "aac86347fb5063960eccb19493e0cadcdb4205ca",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "treefmt-nix",
"type": "github"
}
},
"unixpkgs": {
"locked": {
"lastModified": 1730298926,
"narHash": "sha256-ao1BYrrOB8SGdvOul6hGJYqp/QqEJTwZRViRXFvNnTQ=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "838f2f70e0e44d957009bf5a4fc0aa9c931b680e",
"type": "github"
},
"original": {
"id": "nixpkgs",
"ref": "master",
"type": "indirect"
}
}
},
"root": "root",
"version": 7
}

160
flake.nix Normal file
View file

@ -0,0 +1,160 @@
{
description = "Geoffrey Frogeye's base configurations";
inputs = {
# Packages
nixpkgs.url = "nixpkgs/nixos-24.05";
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.05 yet
inputs.nixpkgs.follows = "nixpkgs";
inputs.home-manager.follows = "home-manager";
};
# HM
home-manager = {
url = "home-manager/release-24.05";
inputs.nixpkgs.follows = "nixpkgs";
};
stylix = {
url = "github:danth/stylix/release-24.05";
inputs.nixpkgs.follows = "nixpkgs";
};
nixvim = {
url = "github:nix-community/nixvim/nixos-24.05";
inputs.nixpkgs.follows = "nixpkgs";
};
nur.url = "github:nix-community/NUR";
# Local
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, disko, nix-on-droid, flake-utils, ... }@attrs:
# Machine independant outputs
let
nixpkgsConfig = {
config = {
allowUnfree = true;
};
overlays = [
(import ./common/update-local-flakes/overlay.nix)
];
};
homeManagerConfig = {
sharedModules = [ self.homeManagerModules.dotfiles ];
extraSpecialArgs = attrs;
};
lib = {
nixosSystem = { system, modules ? [ ] }: nixpkgs.lib.nixosSystem {
inherit system;
specialArgs = attrs;
modules = modules ++ [
self.nixosModules.dotfiles
{
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;
});
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 ];
text = builtins.readFile ./os/rebuild.sh;
}}/bin/rebuild ${self} "$@"''}";
};
};
}
);
};
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.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}";
}

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

@ -0,0 +1,108 @@
{ 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;
};
};
}));
};
};
}

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

@ -0,0 +1,34 @@
# 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,134 @@
{ pkgs, config, lib, ... }: { 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
{ {
frogeye.hooks.lock = ''
nixpkgs.config.allowUnfree = true; ${pkgs.coreutils}/bin/rm -rf "/tmp/cached_pass_$UID"
'';
programs = programs = {
let home-manager.enable = true;
commonRc = lib.strings.concatLines ([ bat = {
'' enable = true;
# Colored ls config.style = "full";
# TODO Doesn't allow completion. Check out lsd instead
_colored_ls() {
${pkgs.coreutils}/bin/ls -lh --color=always $@ | ${pkgs.gawk}/bin/awk '
BEGIN {
FPAT = "([[:space:]]*[^[:space:]]+)";
OFS = "";
}
{
$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 +140,85 @@ 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
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
# 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,40 @@
{ 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,31 @@
{ 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,9 @@
{ 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,180 @@
{ pkgs, lib, config, nur, ... }:
{
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 config.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 = {
"browser.startup.homepage" = "https://geoffrey.frogeye.fr/home.php";
"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 = 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";
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";
};
};
imports = [
nur.hmModules.nur
];
}

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

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

@ -0,0 +1,168 @@
{ 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
];
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,759 @@
#!/usr/bin/env python3
import asyncio
import datetime
import enum
import ipaddress
import logging
import random
import signal
import socket
import typing
import coloredlogs
import i3ipc
import i3ipc.aio
import psutil
coloredlogs.install(level="DEBUG", fmt="%(levelname)s %(message)s")
log = logging.getLogger()
T = typing.TypeVar("T", bound="ComposableText")
P = typing.TypeVar("P", bound="ComposableText")
C = typing.TypeVar("C", bound="ComposableText")
Sortable = str | int
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 "{:3d}{}".format(int(num), unit)
else:
return "{:.1f}{}".format(num, unit)
num /= 1024
return "{:d}YiB".format(numi)
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()
def randomColor(seed: int | bytes | None = None) -> str:
if seed is not None:
random.seed(seed)
return "#" + "".join(f"{random.randint(0, 0xff):02x}" for _ in range(3))
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) -> None:
super().__init__(parent=parent, sortKey=sortKey)
self.parent: "Module"
self.color = randomColor()
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
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:
await asyncio.sleep(sleepTime)
else:
log.warning("Skipped an animation frame")
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
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
if lastSection is None:
if self.alignment == Alignment.LEFT:
text += "%{B" + section.color + "}%{F-}"
else:
text += "%{B-}%{F" + section.color + "}%{R}%{F-}"
else:
if self.alignment == Alignment.RIGHT:
if lastSection.color == section.color:
text += ""
else:
text += "%{F" + section.color + "}%{R}"
else:
if lastSection.color == section.color:
text += ""
else:
text += "%{R}%{B" + section.color + "}"
text += "%{F-}"
text += section.getMarkup()
lastSection = section
if self.alignment != Alignment.RIGHT:
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) -> None:
super().__init__()
self.parent: None
self.children: typing.MutableSequence[Screen]
self.longRunningTasks: list[asyncio.Task] = list()
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()
for output in i3.get_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",
]
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[command]
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]
class Provider:
def __init__(self) -> None:
self.modules: list[Module] = list()
async def run(self) -> None:
# Not a NotImplementedError, otherwise can't combine all classes
pass
class MirrorProvider(Provider):
def __init__(self) -> None:
super().__init__()
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 = Section(parent=self.module)
class StaticProvider(SingleSectionProvider):
def __init__(self, text: str) -> None:
self.text = text
async def run(self) -> None:
await super().run()
self.section.setText(self.text)
class StatefulSection(Section):
def __init__(self, parent: Module, sortKey: Sortable = 0) -> None:
super().__init__(parent=parent, sortKey=sortKey)
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 SingleStatefulSectionProvider(MirrorProvider):
async def run(self) -> None:
await super().run()
self.section = StatefulSection(parent=self.module)
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)
# Providers
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()
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:
self.section.setText(e.container.name)
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(Provider):
# TODO Custom names
# TODO Colors
async def updateWorkspaces(self, i3: i3ipc.Connection) -> 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 i3.get_workspaces()
for workspace in workspaces:
module = self.modulesFromOutput[workspace.output]
if workspace.num in self.sections:
section = self.sections[workspace.num]
if section.parent != module:
section.unsetParent()
section.setParent(module)
else:
section = Section(parent=module, sortKey=workspace.num)
self.sections[workspace.num] = section
def generate_switch_workspace(num: int) -> typing.Callable:
def switch_workspace() -> None:
self.bar.taskGroup.create_task(
i3.command(f"workspace number {num}")
)
return switch_workspace
section.setAction(
Button.CLICK_LEFT, generate_switch_workspace(workspace.num)
)
name = workspace.name
if workspace.urgent:
name = f"{name} !"
elif workspace.focused:
name = f"{name} +"
elif workspace.visible:
name = f"{name} *"
section.setText(name)
workspacesNums = set(workspace.num for workspace in workspaces)
for num, section in self.sections.items():
if num not in workspacesNums:
# This should delete the Section but it turned out to be hard
section.setText(None)
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(i3))
def __init__(
self,
) -> None:
super().__init__()
self.sections: dict[int, Section] = dict()
self.modulesFromOutput: dict[str, Module] = dict()
self.bar: Bar
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
i3 = await i3ipc.aio.Connection(auto_reconnect=True).connect()
i3.on(i3ipc.Event.WORKSPACE, self.onWorkspaceChange)
self.onWorkspaceChange(i3)
await i3.main()
class NetworkProviderSection(StatefulSection):
def __init__(self, parent: Module, iface: str, provider: "NetworkProvider") -> None:
super().__init__(parent=parent, sortKey=iface)
self.iface = iface
self.provider = provider
self.ignore = False
self.icon = "?"
self.wifi = False
if iface == "lo":
self.ignore = True
elif iface.startswith("eth") or iface.startswith("enp"):
if "u" in iface:
self.icon = ""
else:
self.icon = ""
elif iface.startswith("wlan") or iface.startswith("wl"):
self.icon = ""
self.wifi = True
elif (
iface.startswith("tun") or iface.startswith("tap") or iface.startswith("wg")
):
self.icon = ""
elif iface.startswith("docker"):
self.icon = ""
elif iface.startswith("veth"):
self.icon = ""
elif iface.startswith("vboxnet"):
self.icon = ""
self.numberStates = 5 if self.wifi else 4
self.state = 1 if self.wifi else 0
self.setChangedState(self.update)
async def update(self) -> None:
if self.ignore or not self.provider.if_stats[self.iface].isup:
self.setText(None)
return
text = self.icon
state = self.state + (0 if self.wifi else 1) # SSID
if self.wifi and state >= 1:
cmd = ["iwgetid", self.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.provider.if_addrs[self.iface]:
if address.family == socket.AF_INET:
net = ipaddress.IPv4Network(
(address.address, address.netmask), strict=False
)
text += f" {net.with_prefixlen}"
break
if state >= 3: # Speed
prevRecv = self.provider.prev_io_counters[self.iface].bytes_recv
recv = self.provider.io_counters[self.iface].bytes_recv
prevSent = self.provider.prev_io_counters[self.iface].bytes_sent
sent = self.provider.io_counters[self.iface].bytes_sent
dt = self.provider.time - self.provider.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)}"
self.setText(text)
class NetworkProvider(MirrorProvider, PeriodicProvider):
def __init__(self) -> None:
super().__init__()
self.sections: dict[str, NetworkProviderSection] = dict()
async def init(self) -> None:
loop = asyncio.get_running_loop()
self.time = loop.time()
self.io_counters = psutil.net_io_counters(pernic=True)
async def loop(self) -> None:
loop = asyncio.get_running_loop()
async with asyncio.TaskGroup() as tg:
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)
for iface in self.if_stats:
section = self.sections.get(iface)
if not section:
section = NetworkProviderSection(
parent=self.module, iface=iface, provider=self
)
self.sections[iface] = section
tg.create_task(section.update())
for iface, section in self.sections.items():
if iface not in self.if_stats:
section.setText(None)
async def onStateChange(self, section: StatefulSection) -> None:
assert isinstance(section, NetworkProviderSection)
await section.update()
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))
async def main() -> None:
bar = Bar()
dualScreen = len(bar.children) > 1
bar.addProvider(I3ModeProvider(), alignment=Alignment.LEFT)
bar.addProvider(I3WorkspacesProvider(), alignment=Alignment.LEFT)
if dualScreen:
bar.addProvider(
I3WindowTitleProvider(), screenNum=0, alignment=Alignment.CENTER
)
bar.addProvider(
StaticProvider(text="mpris"),
screenNum=1 if dualScreen else None,
alignment=Alignment.CENTER,
)
bar.addProvider(StaticProvider("C L M T B"), alignment=Alignment.RIGHT)
bar.addProvider(
StaticProvider("pulse"),
screenNum=1 if dualScreen else None,
alignment=Alignment.RIGHT,
)
bar.addProvider(
NetworkProvider(),
screenNum=0 if dualScreen else None,
alignment=Alignment.RIGHT,
)
bar.addProvider(TimeProvider(), alignment=Alignment.RIGHT)
await bar.run()
asyncio.run(main())

View file

@ -0,0 +1,32 @@
{ 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 = "2.0";
propagatedBuildInputs = with pkgs.python3Packages; [
coloredlogs
notmuch
i3ipc
mpd2
psutil
pulsectl
pyinotify
];
nativeBuildInputs = [ lemonbar ] ++ (with pkgs; [ wirelesstools playerctl ]);
makeWrapperArgs = [ "--prefix PATH : ${pkgs.lib.makeBinPath nativeBuildInputs}" ];
src = ./.;
}

View file

@ -0,0 +1,77 @@
#!/usr/bin/env python3
from frobar import providers as fp
from frobar.display import Bar, BarGroupType
from frobar.updaters import Updater
# TODO If multiple screen, expand the sections and share them
# TODO Graceful exit
def run() -> None:
Bar.init()
Updater.init()
# Bar.addSectionAll(fp.CpuProvider(), BarGroupType.RIGHT)
# Bar.addSectionAll(fp.NetworkProvider(theme=2), BarGroupType.RIGHT)
WORKSPACE_THEME = 8
FOCUS_THEME = 2
URGENT_THEME = 0
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(
fp.I3WorkspacesProvider(
theme=WORKSPACE_THEME,
themeFocus=FOCUS_THEME,
themeUrgent=URGENT_THEME,
themeMode=URGENT_THEME,
customNames=customNames,
),
BarGroupType.LEFT,
)
# TODO Middle
Bar.addSectionAll(fp.MprisProvider(theme=9), BarGroupType.LEFT)
# Bar.addSectionAll(fp.MpdProvider(theme=9), BarGroupType.LEFT)
# Bar.addSectionAll(I3WindowTitleProvider(), BarGroupType.LEFT)
# TODO Computer modes
Bar.addSectionAll(fp.CpuProvider(), BarGroupType.RIGHT)
Bar.addSectionAll(fp.LoadProvider(), BarGroupType.RIGHT)
Bar.addSectionAll(fp.RamProvider(), BarGroupType.RIGHT)
Bar.addSectionAll(fp.TemperatureProvider(), BarGroupType.RIGHT)
Bar.addSectionAll(fp.BatteryProvider(), BarGroupType.RIGHT)
# Peripherals
PERIPHERAL_THEME = 6
NETWORK_THEME = 5
# TODO Disk space provider
# TODO Screen (connected, autorandr configuration, bbswitch) provider
Bar.addSectionAll(fp.XautolockProvider(theme=PERIPHERAL_THEME), BarGroupType.RIGHT)
Bar.addSectionAll(fp.PulseaudioProvider(theme=PERIPHERAL_THEME), BarGroupType.RIGHT)
Bar.addSectionAll(fp.RfkillProvider(theme=PERIPHERAL_THEME), BarGroupType.RIGHT)
Bar.addSectionAll(fp.NetworkProvider(theme=NETWORK_THEME), BarGroupType.RIGHT)
# Personal
# PERSONAL_THEME = 7
# Bar.addSectionAll(fp.KeystoreProvider(theme=PERSONAL_THEME), BarGroupType.RIGHT)
# Bar.addSectionAll(
# fp.NotmuchUnreadProvider(dir="~/.mail/", theme=PERSONAL_THEME),
# BarGroupType.RIGHT,
# )
# Bar.addSectionAll(
# fp.TodoProvider(dir="~/.vdirsyncer/currentCalendars/", theme=PERSONAL_THEME),
# BarGroupType.RIGHT,
# )
TIME_THEME = 4
Bar.addSectionAll(fp.TimeProvider(theme=TIME_THEME), BarGroupType.RIGHT)
# Bar.run()

View file

@ -1,4 +1,4 @@
#!/usr/bin/env python3 #!/usr/bin/env python3init
import enum import enum
import logging import logging
@ -7,11 +7,12 @@ import signal
import subprocess import subprocess
import threading import threading
import time import time
import typing
import coloredlogs import coloredlogs
import i3ipc import i3ipc
from frobar.notbusy import notBusy from frobar.common import notBusy
coloredlogs.install(level="DEBUG", fmt="%(levelname)s %(message)s") coloredlogs.install(level="DEBUG", fmt="%(levelname)s %(message)s")
log = logging.getLogger() log = logging.getLogger()
@ -29,6 +30,12 @@ log = logging.getLogger()
# TODO forceSize and changeText are different # TODO forceSize and changeText are different
Handle = typing.Callable[[], None]
Decorator = Handle | str | None
Element: typing.TypeAlias = typing.Union[str, "Text", None]
Part: typing.TypeAlias = typing.Union[str, "Text", "Section"]
class BarGroupType(enum.Enum): class BarGroupType(enum.Enum):
LEFT = 0 LEFT = 0
RIGHT = 1 RIGHT = 1
@ -40,6 +47,7 @@ class BarGroupType(enum.Enum):
class BarStdoutThread(threading.Thread): class BarStdoutThread(threading.Thread):
def run(self) -> None: def run(self) -> None:
while Bar.running: while Bar.running:
assert Bar.process.stdout
handle = Bar.process.stdout.readline().strip() handle = Bar.process.stdout.readline().strip()
if not len(handle): if not len(handle):
Bar.stop() Bar.stop()
@ -62,20 +70,31 @@ class Bar:
@staticmethod @staticmethod
def init() -> None: def init() -> None:
Bar.running = True Bar.running = True
Bar.everyone = set()
Section.init() Section.init()
cmd = ["lemonbar", "-b", "-a", "64"] cmd = [
"lemonbar",
"-b",
"-a",
"64",
"-F",
Section.FGCOLOR,
"-B",
Section.BGCOLOR,
]
for font in Bar.FONTS: for font in Bar.FONTS:
cmd += ["-f", "{}:size={}".format(font, Bar.FONTSIZE)] cmd += ["-f", "{}:size={}".format(font, Bar.FONTSIZE)]
Bar.process = subprocess.Popen( Bar.process = subprocess.Popen(
cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE
) )
Bar.stdoutThread = BarStdoutThread() BarStdoutThread().start()
Bar.stdoutThread.start()
# Debug i3 = i3ipc.Connection()
Bar(0) for output in i3.get_outputs():
# Bar(1) if not output.active:
continue
Bar(output.name)
@staticmethod @staticmethod
def stop() -> None: def stop() -> None:
@ -90,29 +109,27 @@ class Bar:
Bar.forever() Bar.forever()
i3 = i3ipc.Connection() i3 = i3ipc.Connection()
def doStop(*args) -> None: def doStop(*args: list) -> None:
Bar.stop() Bar.stop()
print(88)
try: try:
i3.on("ipc_shutdown", doStop) i3.on("ipc_shutdown", doStop)
i3.main() i3.main()
except BaseException: except BaseException:
print(93)
Bar.stop() Bar.stop()
# Class globals # Class globals
everyone = set() everyone: set["Bar"]
string = "" string = ""
process = None process: subprocess.Popen
running = False running = False
nextHandle = 0 nextHandle = 0
actionsF2H = dict() actionsF2H: dict[Handle, bytes] = dict()
actionsH2F = dict() actionsH2F: dict[bytes, Handle] = dict()
@staticmethod @staticmethod
def getFunctionHandle(function): def getFunctionHandle(function: typing.Callable[[], None]) -> bytes:
assert callable(function) assert callable(function)
if function in Bar.actionsF2H.keys(): if function in Bar.actionsF2H.keys():
return Bar.actionsF2H[function] return Bar.actionsF2H[function]
@ -126,13 +143,12 @@ class Bar:
return handle return handle
@staticmethod @staticmethod
def forever(): def forever() -> None:
Bar.process.wait() Bar.process.wait()
Bar.stop() Bar.stop()
def __init__(self, screen): def __init__(self, output: str) -> None:
assert isinstance(screen, int) self.output = output
self.screen = "%{S" + str(screen) + "}"
self.groups = dict() self.groups = dict()
for groupType in BarGroupType: for groupType in BarGroupType:
@ -140,36 +156,33 @@ class Bar:
self.groups[groupType] = group self.groups[groupType] = group
self.childsChanged = False self.childsChanged = False
Bar.everyone.add(self)
self.everyone.add(self)
@staticmethod @staticmethod
def addSectionAll(section, group, screens=None): def addSectionAll(
section: "Section", group: "BarGroupType"
) -> None:
""" """
.. note:: .. note::
Add the section before updating it for the first time. 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: for bar in Bar.everyone:
bar.addSection(section, group=group) bar.addSection(section, group=group)
section.added()
def addSection(self, section, group): def addSection(self, section: "Section", group: "BarGroupType") -> None:
assert isinstance(section, Section)
assert isinstance(group, BarGroupType)
self.groups[group].addSection(section) self.groups[group].addSection(section)
def update(self): def update(self) -> None:
if self.childsChanged: if self.childsChanged:
self.string = self.screen self.string = "%{Sn" + self.output + "}"
self.string += self.groups[BarGroupType.LEFT].string self.string += self.groups[BarGroupType.LEFT].string
self.string += self.groups[BarGroupType.RIGHT].string self.string += self.groups[BarGroupType.RIGHT].string
self.childsChanged = False self.childsChanged = False
@staticmethod @staticmethod
def updateAll(): def updateAll() -> None:
if Bar.running: if Bar.running:
Bar.string = "" Bar.string = ""
for bar in Bar.everyone: for bar in Bar.everyone:
@ -178,8 +191,10 @@ class Bar:
# Color for empty sections # Color for empty sections
Bar.string += BarGroup.color(*Section.EMPTY) Bar.string += BarGroup.color(*Section.EMPTY)
# print(Bar.string) string = Bar.string + "\n"
Bar.process.stdin.write(bytes(Bar.string + "\n", "utf-8")) # print(string)
assert Bar.process.stdin
Bar.process.stdin.write(string.encode())
Bar.process.stdin.flush() Bar.process.stdin.flush()
@ -188,18 +203,16 @@ class BarGroup:
One for each group of each bar One for each group of each bar
""" """
everyone = set() everyone: set["BarGroup"] = set()
def __init__(self, groupType, parent): def __init__(self, groupType: BarGroupType, parent: Bar):
assert isinstance(groupType, BarGroupType)
assert isinstance(parent, Bar)
self.groupType = groupType self.groupType = groupType
self.parent = parent self.parent = parent
self.sections = list() self.sections: list["Section"] = list()
self.string = "" self.string = ""
self.parts = [] self.parts: list[Part] = []
#: One of the sections that had their theme or visibility changed #: One of the sections that had their theme or visibility changed
self.childsThemeChanged = False self.childsThemeChanged = False
@ -209,11 +222,11 @@ class BarGroup:
BarGroup.everyone.add(self) BarGroup.everyone.add(self)
def addSection(self, section): def addSection(self, section: "Section") -> None:
self.sections.append(section) self.sections.append(section)
section.addParent(self) section.addParent(self)
def addSectionAfter(self, sectionRef, section): def addSectionAfter(self, sectionRef: "Section", section: "Section") -> None:
index = self.sections.index(sectionRef) index = self.sections.index(sectionRef)
self.sections.insert(index + 1, section) self.sections.insert(index + 1, section)
section.addParent(self) section.addParent(self)
@ -221,20 +234,20 @@ class BarGroup:
ALIGNS = {BarGroupType.LEFT: "%{l}", BarGroupType.RIGHT: "%{r}"} ALIGNS = {BarGroupType.LEFT: "%{l}", BarGroupType.RIGHT: "%{r}"}
@staticmethod @staticmethod
def fgColor(color): def fgColor(color: str) -> str:
return "%{F" + (color or "-") + "}" return "%{F" + (color or "-") + "}"
@staticmethod @staticmethod
def bgColor(color): def bgColor(color: str) -> str:
return "%{B" + (color or "-") + "}" return "%{B" + (color or "-") + "}"
@staticmethod @staticmethod
def color(fg, bg): def color(fg: str, bg: str) -> str:
return BarGroup.fgColor(fg) + BarGroup.bgColor(bg) return BarGroup.fgColor(fg) + BarGroup.bgColor(bg)
def update(self): def update(self) -> None:
if self.childsThemeChanged: if self.childsThemeChanged:
parts = [BarGroup.ALIGNS[self.groupType]] parts: list[Part] = [BarGroup.ALIGNS[self.groupType]]
secs = [sec for sec in self.sections if sec.visible] secs = [sec for sec in self.sections if sec.visible]
lenS = len(secs) lenS = len(secs)
@ -283,7 +296,7 @@ class BarGroup:
self.childsTextChanged = False self.childsTextChanged = False
@staticmethod @staticmethod
def updateAll(): def updateAll() -> None:
for group in BarGroup.everyone: for group in BarGroup.everyone:
group.update() group.update()
Bar.updateAll() Bar.updateAll()
@ -294,7 +307,7 @@ class SectionThread(threading.Thread):
ANIMATION_STOP = 0.001 ANIMATION_STOP = 0.001
ANIMATION_EVOLUTION = 0.9 ANIMATION_EVOLUTION = 0.9
def run(self): def run(self) -> None:
while Section.somethingChanged.wait(): while Section.somethingChanged.wait():
notBusy.wait() notBusy.wait()
Section.updateAll() Section.updateAll()
@ -311,52 +324,54 @@ class SectionThread(threading.Thread):
animTime = self.ANIMATION_STOP animTime = self.ANIMATION_STOP
Theme = tuple[str, str]
class Section: class Section:
# TODO Update all of that to base16 # 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 = [ COLORS = [
"#181818", "#092c0e",
"#AB4642", "#143718",
"#A1B56C", "#5a7058",
"#F7CA88", "#677d64",
"#7CAFC2", "#89947f",
"#BA8BAF", "#99a08d",
"#86C1B9", "#fae2e3",
"#D8D8D8", "#fff0f1",
"#585858", "#e0332e",
"#AB4642", "#cf4b15",
"#A1B56C", "#bb8801",
"#F7CA88", "#8d9800",
"#7CAFC2", "#1fa198",
"#BA8BAF", "#008dd1",
"#86C1B9", "#5c73c4",
"#F8F8F8", "#d43982",
] ]
FGCOLOR = "#F8F8F2" FGCOLOR = "#fff0f1"
BGCOLOR = "#272822" BGCOLOR = "#092c0e"
THEMES = list() THEMES: list[Theme] = list()
EMPTY = (FGCOLOR, BGCOLOR) EMPTY: Theme = (FGCOLOR, BGCOLOR)
ICON = None ICON: str | None = None
PERSISTENT = False PERSISTENT = False
#: Sections that do not have their destination size #: Sections that do not have their destination size
sizeChanging = set() sizeChanging: set["Section"] = set()
updateThread = SectionThread(daemon=True) updateThread: threading.Thread = SectionThread(daemon=True)
somethingChanged = threading.Event() somethingChanged = threading.Event()
lastChosenTheme = 0 lastChosenTheme = 0
@staticmethod @staticmethod
def init(): def init() -> None:
for t in range(8, 16): for t in range(8, 16):
Section.THEMES.append((Section.COLORS[0], Section.COLORS[t])) Section.THEMES.append((Section.COLORS[0], Section.COLORS[t]))
Section.THEMES.append((Section.COLORS[0], Section.COLORS[3]))
Section.THEMES.append((Section.COLORS[0], Section.COLORS[6]))
Section.updateThread.start() Section.updateThread.start()
def __init__(self, theme=None): def __init__(self, theme: int | None = None) -> None:
#: Displayed section #: Displayed section
#: Note: A section can be empty and displayed! #: Note: A section can be empty and displayed!
self.visible = False self.visible = False
@ -379,12 +394,12 @@ class Section:
self.dstSize = 0 self.dstSize = 0
#: Groups that have this section #: Groups that have this section
self.parents = set() self.parents: set[BarGroup] = set()
self.icon = self.ICON self.icon = self.ICON
self.persistent = self.PERSISTENT self.persistent = self.PERSISTENT
def __str__(self): def __str__(self) -> str:
try: try:
return "<{}><{}>{:01d}{}{:02d}/{:02d}".format( return "<{}><{}>{:01d}{}{:02d}/{:02d}".format(
self.curText, self.curText,
@ -394,26 +409,29 @@ class Section:
self.curSize, self.curSize,
self.dstSize, self.dstSize,
) )
except: except Exception:
return super().__str__() return super().__str__()
def addParent(self, parent): def addParent(self, parent: BarGroup) -> None:
self.parents.add(parent) self.parents.add(parent)
def appendAfter(self, section): def appendAfter(self, section: "Section") -> None:
assert len(self.parents) assert len(self.parents)
for parent in self.parents: for parent in self.parents:
parent.addSectionAfter(self, section) parent.addSectionAfter(self, section)
def informParentsThemeChanged(self): def added(self) -> None:
pass
def informParentsThemeChanged(self) -> None:
for parent in self.parents: for parent in self.parents:
parent.childsThemeChanged = True parent.childsThemeChanged = True
def informParentsTextChanged(self): def informParentsTextChanged(self) -> None:
for parent in self.parents: for parent in self.parents:
parent.childsTextChanged = True parent.childsTextChanged = True
def updateText(self, text): def updateText(self, text: Element) -> None:
if isinstance(text, str): if isinstance(text, str):
text = Text(text) text = Text(text)
elif isinstance(text, Text) and not len(text.elements): elif isinstance(text, Text) and not len(text.elements):
@ -440,14 +458,13 @@ class Section:
Section.sizeChanging.add(self) Section.sizeChanging.add(self)
Section.somethingChanged.set() Section.somethingChanged.set()
def setDecorators(self, **kwargs): def setDecorators(self, **kwargs: Handle) -> None:
self.dstText.setDecorators(**kwargs) self.dstText.setDecorators(**kwargs)
self.curText = str(self.dstText) self.curText = str(self.dstText)
self.informParentsTextChanged() self.informParentsTextChanged()
Section.somethingChanged.set() Section.somethingChanged.set()
def updateTheme(self, theme): def updateTheme(self, theme: int) -> None:
assert isinstance(theme, int)
assert theme < len(Section.THEMES) assert theme < len(Section.THEMES)
if theme == self.theme: if theme == self.theme:
return return
@ -455,19 +472,18 @@ class Section:
self.informParentsThemeChanged() self.informParentsThemeChanged()
Section.somethingChanged.set() Section.somethingChanged.set()
def updateVisibility(self, visibility): def updateVisibility(self, visibility: bool) -> None:
assert isinstance(visibility, bool)
self.visible = visibility self.visible = visibility
self.informParentsThemeChanged() self.informParentsThemeChanged()
Section.somethingChanged.set() Section.somethingChanged.set()
@staticmethod @staticmethod
def fit(text, size): def fit(text: str, size: int) -> str:
t = len(text) t = len(text)
return text[:size] if t >= size else text + [" "] * (size - t) return text[:size] if t >= size else text + " " * (size - t)
def update(self): def update(self) -> None:
# TODO Might profit of a better logic # TODO Might profit of a better logic
if not self.visible: if not self.visible:
self.updateVisibility(True) self.updateVisibility(True)
@ -488,7 +504,7 @@ class Section:
self.informParentsTextChanged() self.informParentsTextChanged()
@staticmethod @staticmethod
def updateAll(): def updateAll() -> None:
""" """
Process all sections for text size changes Process all sections for text size changes
""" """
@ -501,7 +517,7 @@ class Section:
Section.somethingChanged.clear() Section.somethingChanged.clear()
@staticmethod @staticmethod
def ramp(p, ramp=" ▁▂▃▄▅▆▇█"): def ramp(p: float, ramp: str = " ▁▂▃▄▅▆▇█") -> str:
if p > 1: if p > 1:
return ramp[-1] return ramp[-1]
elif p < 0: elif p < 0:
@ -512,11 +528,11 @@ class Section:
class StatefulSection(Section): class StatefulSection(Section):
# TODO FEAT Allow to temporary expand the section (e.g. when important change) # TODO FEAT Allow to temporary expand the section (e.g. when important change)
NUMBER_STATES = None NUMBER_STATES: int
DEFAULT_STATE = 0 DEFAULT_STATE = 0
def __init__(self, *args, **kwargs): def __init__(self, theme: int | None) -> None:
Section.__init__(self, *args, **kwargs) Section.__init__(self, theme=theme)
self.state = self.DEFAULT_STATE self.state = self.DEFAULT_STATE
if hasattr(self, "onChangeState"): if hasattr(self, "onChangeState"):
self.onChangeState(self.state) self.onChangeState(self.state)
@ -524,20 +540,22 @@ class StatefulSection(Section):
clickLeft=self.incrementState, clickRight=self.decrementState clickLeft=self.incrementState, clickRight=self.decrementState
) )
def incrementState(self): def incrementState(self) -> None:
newState = min(self.state + 1, self.NUMBER_STATES - 1) newState = min(self.state + 1, self.NUMBER_STATES - 1)
self.changeState(newState) self.changeState(newState)
def decrementState(self): def decrementState(self) -> None:
newState = max(self.state - 1, 0) newState = max(self.state - 1, 0)
self.changeState(newState) self.changeState(newState)
def changeState(self, state): def changeState(self, state: int) -> None:
assert isinstance(state, int)
assert state < self.NUMBER_STATES assert state < self.NUMBER_STATES
self.state = state self.state = state
if hasattr(self, "onChangeState"): if hasattr(self, "onChangeState"):
self.onChangeState(state) self.onChangeState(state)
assert hasattr(
self, "refreshData"
), "StatefulSection should be paired with some Updater"
self.refreshData() self.refreshData()
@ -548,10 +566,13 @@ class ColorCountsSection(StatefulSection):
NUMBER_STATES = 3 NUMBER_STATES = 3
COLORABLE_ICON = "?" COLORABLE_ICON = "?"
def __init__(self, theme=None): def __init__(self, theme: None | int = None) -> None:
StatefulSection.__init__(self, theme=theme) StatefulSection.__init__(self, theme=theme)
def fetcher(self): def subfetcher(self) -> list[tuple[int, str]]:
raise NotImplementedError("Interface must be implemented")
def fetcher(self) -> typing.Union[None, "Text"]:
counts = self.subfetcher() counts = self.subfetcher()
# Nothing # Nothing
if not len(counts): if not len(counts):
@ -566,67 +587,66 @@ class ColorCountsSection(StatefulSection):
# Icon + Total # Icon + Total
elif self.state == 1 and len(counts) > 1: elif self.state == 1 and len(counts) > 1:
total = sum([count for count, color in counts]) total = sum([count for count, color in counts])
return Text(self.COLORABLE_ICON, " ", total) return Text(self.COLORABLE_ICON, " ", str(total))
# Icon + Counts # Icon + Counts
else: else:
text = Text(self.COLORABLE_ICON) text = Text(self.COLORABLE_ICON)
for count, color in counts: for count, color in counts:
text.append(" ", Text(count, fg=color)) text.append(" ", Text(str(count), fg=color))
return text return text
class Text: class Text:
def _setElements(self, elements): def _setDecorators(self, decorators: dict[str, Decorator]) -> None:
# TODO OPTI Concatenate consecutrive string
self.elements = list(elements)
def _setDecorators(self, decorators):
# TODO OPTI Convert no decorator to strings # TODO OPTI Convert no decorator to strings
self.decorators = decorators self.decorators = decorators
self.prefix = None self.prefix: str | None = None
self.suffix = None self.suffix: str | None = None
def __init__(self, *args: Element, **kwargs: Decorator) -> None:
# TODO OPTI Concatenate consecutrive string
self.elements = list(args)
def __init__(self, *args, **kwargs):
self._setElements(args)
self._setDecorators(kwargs) self._setDecorators(kwargs)
self.section = None self.section: Section
def append(self, *args): def append(self, *args: Element) -> None:
self._setElements(self.elements + list(args)) self.elements += list(args)
def prepend(self, *args): def prepend(self, *args: Element) -> None:
self._setElements(list(args) + self.elements) self.elements = list(args) + self.elements
def setElements(self, *args): def setElements(self, *args: Element) -> None:
self._setElements(args) self.elements = list(args)
def setDecorators(self, **kwargs): def setDecorators(self, **kwargs: Decorator) -> None:
self._setDecorators(kwargs) self._setDecorators(kwargs)
def setSection(self, section): def setSection(self, section: Section) -> None:
assert isinstance(section, Section)
self.section = section self.section = section
for element in self.elements: for element in self.elements:
if isinstance(element, Text): if isinstance(element, Text):
element.setSection(section) element.setSection(section)
def _genFixs(self): def _genFixs(self) -> None:
if self.prefix is not None and self.suffix is not None: if self.prefix is not None and self.suffix is not None:
return return
self.prefix = "" self.prefix = ""
self.suffix = "" self.suffix = ""
def nest(prefix, suffix): def nest(prefix: str, suffix: str) -> None:
assert self.prefix is not None
assert self.suffix is not None
self.prefix = self.prefix + "%{" + prefix + "}" self.prefix = self.prefix + "%{" + prefix + "}"
self.suffix = "%{" + suffix + "}" + self.suffix self.suffix = "%{" + suffix + "}" + self.suffix
def getColor(val): def getColor(val: str) -> str:
# TODO Allow themes # TODO Allow themes
assert isinstance(val, str) and len(val) == 7 assert len(val) == 7
return val return val
def button(number, function): def button(number: str, function: Handle) -> None:
handle = Bar.getFunctionHandle(function) handle = Bar.getFunctionHandle(function)
nest("A" + number + ":" + handle.decode() + ":", "A" + number) nest("A" + number + ":" + handle.decode() + ":", "A" + number)
@ -635,25 +655,34 @@ class Text:
continue continue
if key == "fg": if key == "fg":
reset = self.section.THEMES[self.section.theme][0] reset = self.section.THEMES[self.section.theme][0]
assert isinstance(val, str)
nest("F" + getColor(val), "F" + reset) nest("F" + getColor(val), "F" + reset)
elif key == "bg": elif key == "bg":
reset = self.section.THEMES[self.section.theme][1] reset = self.section.THEMES[self.section.theme][1]
assert isinstance(val, str)
nest("B" + getColor(val), "B" + reset) nest("B" + getColor(val), "B" + reset)
elif key == "clickLeft": elif key == "clickLeft":
assert callable(val)
button("1", val) button("1", val)
elif key == "clickMiddle": elif key == "clickMiddle":
assert callable(val)
button("2", val) button("2", val)
elif key == "clickRight": elif key == "clickRight":
assert callable(val)
button("3", val) button("3", val)
elif key == "scrollUp": elif key == "scrollUp":
assert callable(val)
button("4", val) button("4", val)
elif key == "scrollDown": elif key == "scrollDown":
assert callable(val)
button("5", val) button("5", val)
else: else:
log.warn("Unkown decorator: {}".format(key)) log.warn("Unkown decorator: {}".format(key))
def _text(self, size=None, pad=False): def _text(self, size: int | None = None, pad: bool = False) -> tuple[str, int]:
self._genFixs() self._genFixs()
assert self.prefix is not None
assert self.suffix is not None
curString = self.prefix curString = self.prefix
curSize = 0 curSize = 0
remSize = size remSize = size
@ -679,9 +708,11 @@ class Text:
curString += self.suffix curString += self.suffix
if pad and remSize > 0: if pad:
curString += " " * remSize assert remSize is not None
curSize += remSize if remSize > 0:
curString += " " * remSize
curSize += remSize
if size is not None: if size is not None:
if pad: if pad:
@ -690,12 +721,14 @@ class Text:
assert size >= curSize assert size >= curSize
return curString, curSize return curString, curSize
def text(self, *args, **kwargs): def text(self, size: int | None = None, pad: bool = False) -> str:
string, size = self._text(*args, **kwargs) string, size = self._text(size=size, pad=pad)
return string return string
def __str__(self): def __str__(self) -> str:
self._genFixs() self._genFixs()
assert self.prefix is not None
assert self.suffix is not None
curString = self.prefix curString = self.prefix
for element in self.elements: for element in self.elements:
if element is None: if element is None:
@ -705,7 +738,7 @@ class Text:
curString += self.suffix curString += self.suffix
return curString return curString
def __len__(self): def __len__(self) -> int:
curSize = 0 curSize = 0
for element in self.elements: for element in self.elements:
if element is None: if element is None:
@ -716,8 +749,8 @@ class Text:
curSize += len(str(element)) curSize += len(str(element))
return curSize return curSize
def __getitem__(self, index): def __getitem__(self, index: int) -> Element:
return self.elements[index] return self.elements[index]
def __setitem__(self, index, data): def __setitem__(self, index: int, data: Element) -> None:
self.elements[index] = data self.elements[index] = data

View file

@ -1,21 +1,27 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import datetime import datetime
import enum
import ipaddress import ipaddress
import json import json
import logging import logging
import os
import random import random
import socket import socket
import subprocess import subprocess
import time
import coloredlogs import coloredlogs
import i3ipc
import mpd import mpd
import notmuch import notmuch
import psutil import psutil
import pulsectl import pulsectl
from frobar.display import * from frobar.display import (ColorCountsSection, Element, Section,
from frobar.updaters import * StatefulSection, Text)
from frobar.updaters import (I3Updater, InotifyUpdater, MergedUpdater,
PeriodicUpdater, ThreadedUpdater, Updater)
coloredlogs.install(level="DEBUG", fmt="%(levelname)s %(message)s") coloredlogs.install(level="DEBUG", fmt="%(levelname)s %(message)s")
log = logging.getLogger() log = logging.getLogger()
@ -24,21 +30,22 @@ log = logging.getLogger()
# PulseaudioProvider and MpdProvider) # PulseaudioProvider and MpdProvider)
def humanSize(num): def humanSize(numi: int) -> str:
""" """
Returns a string of width 3+3 Returns a string of width 3+3
""" """
num = float(numi)
for unit in ("B ", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB"): for unit in ("B ", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB"):
if abs(num) < 1000: if abs(num) < 1000:
if num >= 10: if num >= 10:
return "{:3d}{}".format(int(num), unit) return "{:3d}{}".format(int(num), unit)
else: else:
return "{:.1f}{}".format(num, unit) return "{:.1f}{}".format(num, unit)
num /= 1024.0 num /= 1024
return "{:d}YiB".format(num) return "{:d}YiB".format(numi)
def randomColor(seed=0): def randomColor(seed: int | bytes = 0) -> str:
random.seed(seed) random.seed(seed)
return "#{:02x}{:02x}{:02x}".format(*[random.randint(0, 255) for _ in range(3)]) return "#{:02x}{:02x}{:02x}".format(*[random.randint(0, 255) for _ in range(3)])
@ -48,11 +55,11 @@ class TimeProvider(StatefulSection, PeriodicUpdater):
NUMBER_STATES = len(FORMATS) NUMBER_STATES = len(FORMATS)
DEFAULT_STATE = 1 DEFAULT_STATE = 1
def fetcher(self): def fetcher(self) -> str:
now = datetime.datetime.now() now = datetime.datetime.now()
return now.strftime(self.FORMATS[self.state]) return now.strftime(self.FORMATS[self.state])
def __init__(self, theme=None): def __init__(self, theme: int | None = None):
PeriodicUpdater.__init__(self) PeriodicUpdater.__init__(self)
StatefulSection.__init__(self, theme) StatefulSection.__init__(self, theme)
self.changeInterval(1) # TODO OPTI When state < 1 self.changeInterval(1) # TODO OPTI When state < 1
@ -66,10 +73,10 @@ class AlertLevel(enum.Enum):
class AlertingSection(StatefulSection): class AlertingSection(StatefulSection):
# TODO EASE Correct settings for themes # TODO EASE Correct settings for themes
THEMES = {AlertLevel.NORMAL: 2, AlertLevel.WARNING: 3, AlertLevel.DANGER: 1} ALERT_THEMES = {AlertLevel.NORMAL: 3, AlertLevel.WARNING: 1, AlertLevel.DANGER: 0}
PERSISTENT = True PERSISTENT = True
def getLevel(self, quantity): def getLevel(self, quantity: float) -> AlertLevel:
if quantity > self.dangerThresold: if quantity > self.dangerThresold:
return AlertLevel.DANGER return AlertLevel.DANGER
elif quantity > self.warningThresold: elif quantity > self.warningThresold:
@ -77,14 +84,14 @@ class AlertingSection(StatefulSection):
else: else:
return AlertLevel.NORMAL return AlertLevel.NORMAL
def updateLevel(self, quantity): def updateLevel(self, quantity: float) -> None:
self.level = self.getLevel(quantity) self.level = self.getLevel(quantity)
self.updateTheme(self.THEMES[self.level]) self.updateTheme(self.ALERT_THEMES[self.level])
if self.level == AlertLevel.NORMAL: if self.level == AlertLevel.NORMAL:
return return
# TODO Temporary update state # TODO Temporary update state
def __init__(self, theme): def __init__(self, theme: int | None = None):
StatefulSection.__init__(self, theme) StatefulSection.__init__(self, theme)
self.dangerThresold = 0.90 self.dangerThresold = 0.90
self.warningThresold = 0.75 self.warningThresold = 0.75
@ -92,9 +99,9 @@ class AlertingSection(StatefulSection):
class CpuProvider(AlertingSection, PeriodicUpdater): class CpuProvider(AlertingSection, PeriodicUpdater):
NUMBER_STATES = 3 NUMBER_STATES = 3
ICON = "" ICON = ""
def fetcher(self): def fetcher(self) -> Element:
percent = psutil.cpu_percent(percpu=False) percent = psutil.cpu_percent(percpu=False)
self.updateLevel(percent / 100) self.updateLevel(percent / 100)
if self.state >= 2: if self.state >= 2:
@ -102,22 +109,44 @@ class CpuProvider(AlertingSection, PeriodicUpdater):
return "".join([Section.ramp(p / 100) for p in percents]) return "".join([Section.ramp(p / 100) for p in percents])
elif self.state >= 1: elif self.state >= 1:
return Section.ramp(percent / 100) return Section.ramp(percent / 100)
return ""
def __init__(self, theme=None): def __init__(self, theme: int | None = None):
AlertingSection.__init__(self, theme) AlertingSection.__init__(self, theme)
PeriodicUpdater.__init__(self) PeriodicUpdater.__init__(self)
self.changeInterval(1) self.changeInterval(1)
class LoadProvider(AlertingSection, PeriodicUpdater):
NUMBER_STATES = 3
ICON = ""
def fetcher(self) -> Element:
load = os.getloadavg()
self.updateLevel(load[0])
if self.state >= 2:
return " ".join(f"{load[i]:.2f}" for i in range(3))
elif self.state >= 1:
return f"{load[0]:.2f}"
return ""
def __init__(self, theme: int | None = None):
AlertingSection.__init__(self, theme)
PeriodicUpdater.__init__(self)
self.changeInterval(5)
self.warningThresold = 5
self.dangerThresold = 10
class RamProvider(AlertingSection, PeriodicUpdater): class RamProvider(AlertingSection, PeriodicUpdater):
""" """
Shows free RAM Shows free RAM
""" """
NUMBER_STATES = 4 NUMBER_STATES = 4
ICON = "" ICON = ""
def fetcher(self): def fetcher(self) -> Element:
mem = psutil.virtual_memory() mem = psutil.virtual_memory()
freePerc = mem.percent / 100 freePerc = mem.percent / 100
self.updateLevel(freePerc) self.updateLevel(freePerc)
@ -135,7 +164,7 @@ class RamProvider(AlertingSection, PeriodicUpdater):
return text return text
def __init__(self, theme=None): def __init__(self, theme: int | None = None):
AlertingSection.__init__(self, theme) AlertingSection.__init__(self, theme)
PeriodicUpdater.__init__(self) PeriodicUpdater.__init__(self)
self.changeInterval(1) self.changeInterval(1)
@ -144,23 +173,28 @@ class RamProvider(AlertingSection, PeriodicUpdater):
class TemperatureProvider(AlertingSection, PeriodicUpdater): class TemperatureProvider(AlertingSection, PeriodicUpdater):
NUMBER_STATES = 2 NUMBER_STATES = 2
RAMP = "" RAMP = ""
MAIN_TEMPS = ["coretemp", "amdgpu", "cpu_thermal"]
# For Intel, AMD and ARM respectively.
def fetcher(self): def fetcher(self) -> Element:
allTemp = psutil.sensors_temperatures() allTemp = psutil.sensors_temperatures()
if "coretemp" not in allTemp: for main in self.MAIN_TEMPS:
# TODO Opti Remove interval if main in allTemp:
return "" break
temp = allTemp["coretemp"][0] else:
return "?"
temp = allTemp[main][0]
self.warningThresold = temp.high self.warningThresold = temp.high or 90.0
self.dangerThresold = temp.critical self.dangerThresold = temp.critical or 100.0
self.updateLevel(temp.current) self.updateLevel(temp.current)
self.icon = Section.ramp(temp.current / temp.high, self.RAMP) self.icon = Section.ramp(temp.current / self.warningThresold, self.RAMP)
if self.state >= 1: if self.state >= 1:
return "{:.0f}°C".format(temp.current) return "{:.0f}°C".format(temp.current)
return ""
def __init__(self, theme=None): def __init__(self, theme: int | None = None):
AlertingSection.__init__(self, theme) AlertingSection.__init__(self, theme)
PeriodicUpdater.__init__(self) PeriodicUpdater.__init__(self)
self.changeInterval(5) self.changeInterval(5)
@ -171,10 +205,9 @@ class BatteryProvider(AlertingSection, PeriodicUpdater):
NUMBER_STATES = 3 NUMBER_STATES = 3
RAMP = "" RAMP = ""
def fetcher(self): def fetcher(self) -> Element:
bat = psutil.sensors_battery() bat = psutil.sensors_battery()
if not bat: if not bat:
self.icon = None
return None return None
self.icon = ("" if bat.power_plugged else "") + Section.ramp( self.icon = ("" if bat.power_plugged else "") + Section.ramp(
@ -184,7 +217,7 @@ class BatteryProvider(AlertingSection, PeriodicUpdater):
self.updateLevel(1 - bat.percent / 100) self.updateLevel(1 - bat.percent / 100)
if self.state < 1: if self.state < 1:
return return ""
t = Text("{:.0f}%".format(bat.percent)) t = Text("{:.0f}%".format(bat.percent))
@ -196,17 +229,38 @@ class BatteryProvider(AlertingSection, PeriodicUpdater):
t.append(" ({:d}:{:02d})".format(h, m)) t.append(" ({:d}:{:02d})".format(h, m))
return t return t
def __init__(self, theme=None): def __init__(self, theme: int | None = None):
AlertingSection.__init__(self, theme) AlertingSection.__init__(self, theme)
PeriodicUpdater.__init__(self) PeriodicUpdater.__init__(self)
self.changeInterval(5) self.changeInterval(5)
class XautolockProvider(Section, InotifyUpdater):
ICON = ""
def fetcher(self) -> str | None:
with open(self.path) as fd:
state = fd.read().strip()
if state == "enabled":
return None
elif state == "disabled":
return ""
else:
return "?"
def __init__(self, theme: int | None = None):
Section.__init__(self, theme=theme)
InotifyUpdater.__init__(self)
# TODO XDG
self.path = os.path.realpath(os.path.expanduser("~/.cache/xautolock"))
self.addPath(self.path)
class PulseaudioProvider(StatefulSection, ThreadedUpdater): class PulseaudioProvider(StatefulSection, ThreadedUpdater):
NUMBER_STATES = 3 NUMBER_STATES = 3
DEFAULT_STATE = 1 DEFAULT_STATE = 1
def __init__(self, theme=None): def __init__(self, theme: int | None = None):
ThreadedUpdater.__init__(self) ThreadedUpdater.__init__(self)
StatefulSection.__init__(self, theme) StatefulSection.__init__(self, theme)
self.pulseEvents = pulsectl.Pulse("event-handler") self.pulseEvents = pulsectl.Pulse("event-handler")
@ -216,15 +270,21 @@ class PulseaudioProvider(StatefulSection, ThreadedUpdater):
self.start() self.start()
self.refreshData() self.refreshData()
def fetcher(self): def fetcher(self) -> Element:
sinks = [] sinks = []
with pulsectl.Pulse("list-sinks") as pulse: with pulsectl.Pulse("list-sinks") as pulse:
for sink in pulse.sink_list(): for sink in pulse.sink_list():
if sink.port_active.name == "analog-output-headphones": if (
sink.port_active.name == "analog-output-headphones"
or sink.port_active.description == "Headphones"
):
icon = "" icon = ""
elif sink.port_active.name == "analog-output-speaker": elif (
sink.port_active.name == "analog-output-speaker"
or sink.port_active.description == "Speaker"
):
icon = "" if sink.mute else "" icon = "" if sink.mute else ""
elif sink.port_active.name == "headset-output": elif sink.port_active.name in ("headset-output", "headphone-output"):
icon = "" icon = ""
else: else:
icon = "?" icon = "?"
@ -249,10 +309,10 @@ class PulseaudioProvider(StatefulSection, ThreadedUpdater):
return Text(*sinks) return Text(*sinks)
def loop(self): def loop(self) -> None:
self.pulseEvents.event_listen() self.pulseEvents.event_listen()
def handleEvent(self, ev): def handleEvent(self, ev: pulsectl.PulseEventInfo) -> None:
self.refreshData() self.refreshData()
@ -260,7 +320,7 @@ class NetworkProviderSection(StatefulSection, Updater):
NUMBER_STATES = 5 NUMBER_STATES = 5
DEFAULT_STATE = 1 DEFAULT_STATE = 1
def actType(self): def actType(self) -> None:
self.ssid = None self.ssid = None
if self.iface.startswith("eth") or self.iface.startswith("enp"): if self.iface.startswith("eth") or self.iface.startswith("enp"):
if "u" in self.iface: if "u" in self.iface:
@ -281,10 +341,10 @@ class NetworkProviderSection(StatefulSection, Updater):
self.icon = "" self.icon = ""
elif self.iface.startswith("vboxnet"): elif self.iface.startswith("vboxnet"):
self.icon = "" self.icon = ""
else:
self.icon = "?"
def getAddresses(self): def getAddresses(
self,
) -> tuple[psutil._common.snicaddr, psutil._common.snicaddr]:
ipv4 = None ipv4 = None
ipv6 = None ipv6 = None
for address in self.parent.addrs[self.iface]: for address in self.parent.addrs[self.iface]:
@ -294,8 +354,8 @@ class NetworkProviderSection(StatefulSection, Updater):
ipv6 = address ipv6 = address
return ipv4, ipv6 return ipv4, ipv6
def fetcher(self): def fetcher(self) -> Element:
self.icon = None self.icon = "?"
self.persistent = False self.persistent = False
if ( if (
self.iface not in self.parent.stats self.iface not in self.parent.stats
@ -349,13 +409,13 @@ class NetworkProviderSection(StatefulSection, Updater):
return " ".join(text) return " ".join(text)
def onChangeState(self, state): def onChangeState(self, state: int) -> None:
self.showSsid = state >= 1 self.showSsid = state >= 1
self.showAddress = state >= 2 self.showAddress = state >= 2
self.showSpeed = state >= 3 self.showSpeed = state >= 3
self.showTransfer = state >= 4 self.showTransfer = state >= 4
def __init__(self, iface, parent): def __init__(self, iface: str, parent: "NetworkProvider"):
Updater.__init__(self) Updater.__init__(self)
StatefulSection.__init__(self, theme=parent.theme) StatefulSection.__init__(self, theme=parent.theme)
self.iface = iface self.iface = iface
@ -363,23 +423,23 @@ class NetworkProviderSection(StatefulSection, Updater):
class NetworkProvider(Section, PeriodicUpdater): class NetworkProvider(Section, PeriodicUpdater):
def fetchData(self): def fetchData(self) -> None:
self.prev = self.last self.prev = self.last
self.prevIO = self.IO self.prevIO = self.IO
self.stats = psutil.net_if_stats() self.stats = psutil.net_if_stats()
self.addrs = psutil.net_if_addrs() self.addrs: dict[str, list[psutil._common.snicaddr]] = psutil.net_if_addrs()
self.IO = psutil.net_io_counters(pernic=True) self.IO: dict[str, psutil._common.snetio] = psutil.net_io_counters(pernic=True)
self.ifaces = self.stats.keys() self.ifaces = self.stats.keys()
self.last = time.perf_counter() self.last: float = time.perf_counter()
self.dt = self.last - self.prev self.dt = self.last - self.prev
def fetcher(self): def fetcher(self) -> None:
self.fetchData() self.fetchData()
# Add missing sections # Add missing sections
lastSection = self lastSection: NetworkProvider | NetworkProviderSection = self
for iface in sorted(list(self.ifaces)): for iface in sorted(list(self.ifaces)):
if iface not in self.sections.keys(): if iface not in self.sections.keys():
section = NetworkProviderSection(iface, self) section = NetworkProviderSection(iface, self)
@ -395,15 +455,11 @@ class NetworkProvider(Section, PeriodicUpdater):
return None return None
def addParent(self, parent): def __init__(self, theme: int | None = None):
self.parents.add(parent)
self.refreshData()
def __init__(self, theme=None):
PeriodicUpdater.__init__(self) PeriodicUpdater.__init__(self)
Section.__init__(self, theme) Section.__init__(self, theme)
self.sections = dict() self.sections: dict[str, NetworkProviderSection] = dict()
self.last = 0 self.last = 0
self.IO = dict() self.IO = dict()
self.fetchData() self.fetchData()
@ -415,7 +471,7 @@ class RfkillProvider(Section, PeriodicUpdater):
# toggled # toggled
PATH = "/sys/class/rfkill" PATH = "/sys/class/rfkill"
def fetcher(self): def fetcher(self) -> Element:
t = Text() t = Text()
for device in os.listdir(self.PATH): for device in os.listdir(self.PATH):
with open(os.path.join(self.PATH, device, "soft"), "rb") as f: with open(os.path.join(self.PATH, device, "soft"), "rb") as f:
@ -429,7 +485,7 @@ class RfkillProvider(Section, PeriodicUpdater):
with open(os.path.join(self.PATH, device, "type"), "rb") as f: with open(os.path.join(self.PATH, device, "type"), "rb") as f:
typ = f.read().strip() typ = f.read().strip()
fg = (hardBlocked and "#CCCCCC") or (softBlocked and "#FF0000") fg = (hardBlocked and "#CCCCCC") or (softBlocked and "#FF0000") or None
if typ == b"wlan": if typ == b"wlan":
icon = "" icon = ""
elif typ == b"bluetooth": elif typ == b"bluetooth":
@ -440,14 +496,14 @@ class RfkillProvider(Section, PeriodicUpdater):
t.append(Text(icon, fg=fg)) t.append(Text(icon, fg=fg))
return t return t
def __init__(self, theme=None): def __init__(self, theme: int | None = None):
PeriodicUpdater.__init__(self) PeriodicUpdater.__init__(self)
Section.__init__(self, theme) Section.__init__(self, theme)
self.changeInterval(5) self.changeInterval(5)
class SshAgentProvider(PeriodicUpdater): class SshAgentProvider(PeriodicUpdater):
def fetcher(self): def fetcher(self) -> Element:
cmd = ["ssh-add", "-l"] cmd = ["ssh-add", "-l"]
proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL) proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
if proc.returncode != 0: if proc.returncode != 0:
@ -460,13 +516,13 @@ class SshAgentProvider(PeriodicUpdater):
text.append(Text("", fg=randomColor(seed=fingerprint))) text.append(Text("", fg=randomColor(seed=fingerprint)))
return text return text
def __init__(self): def __init__(self) -> None:
PeriodicUpdater.__init__(self) PeriodicUpdater.__init__(self)
self.changeInterval(5) self.changeInterval(5)
class GpgAgentProvider(PeriodicUpdater): class GpgAgentProvider(PeriodicUpdater):
def fetcher(self): def fetcher(self) -> Element:
cmd = ["gpg-connect-agent", "keyinfo --list", "/bye"] cmd = ["gpg-connect-agent", "keyinfo --list", "/bye"]
proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL) proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
# proc = subprocess.run(cmd) # proc = subprocess.run(cmd)
@ -483,7 +539,7 @@ class GpgAgentProvider(PeriodicUpdater):
text.append(Text("", fg=randomColor(seed=keygrip))) text.append(Text("", fg=randomColor(seed=keygrip)))
return text return text
def __init__(self): def __init__(self) -> None:
PeriodicUpdater.__init__(self) PeriodicUpdater.__init__(self)
self.changeInterval(5) self.changeInterval(5)
@ -492,7 +548,7 @@ class KeystoreProvider(Section, MergedUpdater):
# TODO OPTI+FEAT Use ColorCountsSection and not MergedUpdater, this is useless # TODO OPTI+FEAT Use ColorCountsSection and not MergedUpdater, this is useless
ICON = "" ICON = ""
def __init__(self, theme=None): def __init__(self, theme: int | None = None):
MergedUpdater.__init__(self, SshAgentProvider(), GpgAgentProvider()) MergedUpdater.__init__(self, SshAgentProvider(), GpgAgentProvider())
Section.__init__(self, theme) Section.__init__(self, theme)
@ -500,24 +556,21 @@ class KeystoreProvider(Section, MergedUpdater):
class NotmuchUnreadProvider(ColorCountsSection, InotifyUpdater): class NotmuchUnreadProvider(ColorCountsSection, InotifyUpdater):
COLORABLE_ICON = "" COLORABLE_ICON = ""
def subfetcher(self): def subfetcher(self) -> list[tuple[int, str]]:
db = notmuch.Database(mode=notmuch.Database.MODE.READ_ONLY, path=self.dir) db = notmuch.Database(mode=notmuch.Database.MODE.READ_ONLY, path=self.dir)
counts = [] counts = []
for account in self.accounts: for account in self.accounts:
queryStr = "folder:/{}/ and tag:unread".format(account) queryStr = "folder:/{}/ and tag:unread".format(account)
query = notmuch.Query(db, queryStr) query = notmuch.Query(db, queryStr)
nbMsgs = query.count_messages() nbMsgs = query.count_messages()
if account == "frogeye":
global q
q = query
if nbMsgs < 1: if nbMsgs < 1:
continue continue
counts.append((nbMsgs, self.colors[account])) counts.append((nbMsgs, self.colors[account]))
# db.close() # db.close()
return counts return counts
def __init__(self, dir="~/.mail/", theme=None): def __init__(self, dir: str = "~/.mail/", theme: int | None = None):
PeriodicUpdater.__init__(self) InotifyUpdater.__init__(self)
ColorCountsSection.__init__(self, theme) ColorCountsSection.__init__(self, theme)
self.dir = os.path.realpath(os.path.expanduser(dir)) self.dir = os.path.realpath(os.path.expanduser(dir))
@ -543,7 +596,7 @@ class TodoProvider(ColorCountsSection, InotifyUpdater):
# TODO OPT Specific callback for specific directory # TODO OPT Specific callback for specific directory
COLORABLE_ICON = "" COLORABLE_ICON = ""
def updateCalendarList(self): def updateCalendarList(self) -> None:
calendars = sorted(os.listdir(self.dir)) calendars = sorted(os.listdir(self.dir))
for calendar in calendars: for calendar in calendars:
# If the calendar wasn't in the list # If the calendar wasn't in the list
@ -559,9 +612,9 @@ class TodoProvider(ColorCountsSection, InotifyUpdater):
path = os.path.join(self.dir, calendar, "color") path = os.path.join(self.dir, calendar, "color")
with open(path, "r") as f: with open(path, "r") as f:
self.colors[calendar] = f.read().strip() self.colors[calendar] = f.read().strip()
self.calendars = calendars self.calendars: list[str] = calendars
def __init__(self, dir, theme=None): def __init__(self, dir: str, theme: int | None = None):
""" """
:parm str dir: [main]path value in todoman.conf :parm str dir: [main]path value in todoman.conf
""" """
@ -571,12 +624,12 @@ class TodoProvider(ColorCountsSection, InotifyUpdater):
assert os.path.isdir(self.dir) assert os.path.isdir(self.dir)
self.calendars = [] self.calendars = []
self.colors = dict() self.colors: dict[str, str] = dict()
self.names = dict() self.names: dict[str, str] = dict()
self.updateCalendarList() self.updateCalendarList()
self.refreshData() self.refreshData()
def countUndone(self, calendar): def countUndone(self, calendar: str | None) -> int:
cmd = ["todo", "--porcelain", "list"] cmd = ["todo", "--porcelain", "list"]
if calendar: if calendar:
cmd.append(self.names[calendar]) cmd.append(self.names[calendar])
@ -584,7 +637,7 @@ class TodoProvider(ColorCountsSection, InotifyUpdater):
data = json.loads(proc.stdout) data = json.loads(proc.stdout)
return len(data) return len(data)
def subfetcher(self): def subfetcher(self) -> list[tuple[int, str]]:
counts = [] counts = []
# TODO This an ugly optimisation that cuts on features, but todoman # TODO This an ugly optimisation that cuts on features, but todoman
@ -609,124 +662,107 @@ class I3WindowTitleProvider(Section, I3Updater):
# TODO FEAT To make this available from start, we need to find the # TODO FEAT To make this available from start, we need to find the
# `focused=True` element following the `focus` array # `focused=True` element following the `focus` array
# TODO Feat Make this output dependant if wanted # TODO Feat Make this output dependant if wanted
def on_window(self, i3, e): def on_window(self, i3: i3ipc.Connection, e: i3ipc.Event) -> None:
self.updateText(e.container.name) self.updateText(e.container.name)
def __init__(self, theme=None): def __init__(self, theme: int | None = None):
I3Updater.__init__(self) I3Updater.__init__(self)
Section.__init__(self, theme=theme) Section.__init__(self, theme=theme)
self.on("window", self.on_window) self.on("window", self.on_window)
class I3WorkspacesProviderSection(Section): class I3WorkspacesProviderSection(Section):
def selectTheme(self): def selectTheme(self) -> int:
if self.urgent: if self.workspace.urgent:
return self.parent.themeUrgent return self.parent.themeUrgent
elif self.focused: elif self.workspace.focused:
return self.parent.themeFocus return self.parent.themeFocus
elif self.workspace.visible:
return self.parent.themeVisible
else: else:
return self.parent.themeNormal return self.parent.themeNormal
# TODO On mode change the state (shown / hidden) gets overriden so every # TODO On mode change the state (shown / hidden) gets overriden so every
# tab is shown # tab is shown
def show(self): def show(self) -> None:
self.updateTheme(self.selectTheme()) self.updateTheme(self.selectTheme())
self.updateText(self.fullName if self.focused else self.shortName) self.updateText(
self.fullName if self.workspace.focused else self.workspace.name
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): def switchTo(self) -> None:
self.parent.i3.command("workspace {}".format(self.shortName)) self.parent.i3.command("workspace {}".format(self.workspace.name))
def __init__(self, name, parent): def updateWorkspace(self, workspace: i3ipc.WorkspaceReply) -> None:
self.workspace = workspace
self.fullName: str = self.parent.customNames.get(workspace.name, workspace.name)
self.show()
def __init__(self, parent: "I3WorkspacesProvider"):
Section.__init__(self) Section.__init__(self)
self.parent = parent self.parent = parent
self.setName(name)
self.setDecorators(clickLeft=self.switchTo) self.setDecorators(clickLeft=self.switchTo)
self.tempText = None self.tempText: Element = None
def empty(self): def empty(self) -> None:
self.updateTheme(self.parent.themeNormal) self.updateTheme(self.parent.themeNormal)
self.updateText(None) self.updateText(None)
def tempShow(self): def tempShow(self) -> None:
self.updateText(self.tempText) self.updateText(self.tempText)
def tempEmpty(self): def tempEmpty(self) -> None:
self.tempText = self.dstText[1] self.tempText = self.dstText[1]
self.updateText(None) self.updateText(None)
class I3WorkspacesProvider(Section, I3Updater): class I3WorkspacesProvider(Section, I3Updater):
# TODO FEAT Multi-screen
def initialPopulation(self, parent): def updateWorkspace(self, workspace: i3ipc.WorkspaceReply) -> None:
""" section: Section | None = None
Called on init lastSectionOnOutput = self.modeSection
Can't reuse addWorkspace since i3.get_workspaces() gives dict and not highestNumOnOutput = -1
ConObjects for sect in self.sections.values():
""" if sect.workspace.num == workspace.num:
workspaces = self.i3.get_workspaces() section = sect
lastSection = self.modeSection break
for workspace in workspaces: elif (
# if parent.display != workspace["display"]: sect.workspace.num > highestNumOnOutput
# continue and sect.workspace.num < workspace.num
and sect.workspace.output == workspace.output
section = I3WorkspacesProviderSection(workspace.name, self) ):
section.focused = workspace.focused lastSectionOnOutput = sect
section.urgent = workspace.urgent highestNumOnOutput = sect.workspace.num
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: else:
# Find the section just before section = I3WorkspacesProviderSection(self)
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 self.sections[workspace.num] = section
section.focused = workspace.focused
section.urgent = workspace.urgent
section.show()
def on_workspace_empty(self, i3, e): for bargroup in self.parents:
if bargroup.parent.output == workspace.output:
break
else:
bargroup = list(self.parents)[0]
bargroup.addSectionAfter(lastSectionOnOutput, section)
section.updateWorkspace(workspace)
def updateWorkspaces(self) -> None:
workspaces = self.i3.get_workspaces()
for workspace in workspaces:
self.updateWorkspace(workspace)
def added(self) -> None:
super().added()
self.appendAfter(self.modeSection)
self.updateWorkspaces()
def on_workspace_change(self, i3: i3ipc.Connection, e: i3ipc.Event) -> None:
self.updateWorkspaces()
def on_workspace_empty(self, i3: i3ipc.Connection, e: i3ipc.Event) -> None:
self.sections[e.current.num].empty() self.sections[e.current.num].empty()
def on_workspace_focus(self, i3, e): def on_mode(self, i3: i3ipc.Connection, e: i3ipc.Event) -> None:
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": if e.change == "default":
self.modeSection.updateText(None) self.modeSection.updateText(None)
for section in self.sections.values(): for section in self.sections.values():
@ -737,41 +773,46 @@ class I3WorkspacesProvider(Section, I3Updater):
section.tempEmpty() section.tempEmpty()
def __init__( def __init__(
self, theme=0, themeFocus=3, themeUrgent=1, themeMode=2, customNames=dict() self,
theme: int = 0,
themeVisible: int = 4,
themeFocus: int = 3,
themeUrgent: int = 1,
themeMode: int = 2,
customNames: dict[str, str] = dict(),
): ):
I3Updater.__init__(self) I3Updater.__init__(self)
Section.__init__(self) Section.__init__(self)
self.themeNormal = theme self.themeNormal = theme
self.themeFocus = themeFocus self.themeFocus = themeFocus
self.themeUrgent = themeUrgent self.themeUrgent = themeUrgent
self.themeVisible = themeVisible
self.customNames = customNames self.customNames = customNames
self.sections = dict() self.sections: dict[int, I3WorkspacesProviderSection] = dict()
self.on("workspace::init", self.on_workspace_init) # The event object doesn't have the visible property,
self.on("workspace::focus", self.on_workspace_focus) # so we have to fetch the list of workspaces anyways.
# This sacrifices a bit of performance for code simplicity.
self.on("workspace::init", self.on_workspace_change)
self.on("workspace::focus", self.on_workspace_change)
self.on("workspace::empty", self.on_workspace_empty) self.on("workspace::empty", self.on_workspace_empty)
self.on("workspace::urgent", self.on_workspace_urgent) self.on("workspace::urgent", self.on_workspace_change)
self.on("workspace::rename", self.on_workspace_rename) self.on("workspace::rename", self.on_workspace_change)
# TODO Un-handled/tested: reload, rename, restored, move # TODO Un-handled/tested: reload, rename, restored, move
self.on("mode", self.on_mode) self.on("mode", self.on_mode)
self.modeSection = Section(theme=themeMode) self.modeSection = Section(theme=themeMode)
def addParent(self, parent):
self.parents.add(parent)
parent.addSection(self.modeSection)
self.initialPopulation(parent)
class MpdProvider(Section, ThreadedUpdater): class MpdProvider(Section, ThreadedUpdater):
# TODO FEAT More informations and controls # TODO FEAT More informations and controls
MAX_LENGTH = 50 MAX_LENGTH = 50
def connect(self): def connect(self) -> None:
self.mpd.connect("localhost", 6600) self.mpd.connect("localhost", 6600)
def __init__(self, theme=None): def __init__(self, theme: int | None = None):
ThreadedUpdater.__init__(self) ThreadedUpdater.__init__(self)
Section.__init__(self, theme) Section.__init__(self, theme)
@ -780,7 +821,7 @@ class MpdProvider(Section, ThreadedUpdater):
self.refreshData() self.refreshData()
self.start() self.start()
def fetcher(self): def fetcher(self) -> Element:
stat = self.mpd.status() stat = self.mpd.status()
if not len(stat) or stat["state"] == "stop": if not len(stat) or stat["state"] == "stop":
return None return None
@ -791,7 +832,7 @@ class MpdProvider(Section, ThreadedUpdater):
infos = [] infos = []
def tryAdd(field): def tryAdd(field: str) -> None:
if field in cur: if field in cur:
infos.append(cur[field]) infos.append(cur[field])
@ -805,7 +846,7 @@ class MpdProvider(Section, ThreadedUpdater):
return "{}".format(infosStr) return "{}".format(infosStr)
def loop(self): def loop(self) -> None:
try: try:
self.mpd.idle("player") self.mpd.idle("player")
self.refreshData() self.refreshData()
@ -814,3 +855,104 @@ class MpdProvider(Section, ThreadedUpdater):
self.connect() self.connect()
except BaseException as e: except BaseException as e:
log.error(e, exc_info=True) log.error(e, exc_info=True)
class MprisProviderSection(Section, Updater):
def __init__(self, parent: "MprisProvider"):
Updater.__init__(self)
Section.__init__(self, theme=parent.theme)
self.parent = parent
class MprisProvider(Section, ThreadedUpdater):
# TODO Controls (select player at least)
# TODO Use the Python native thing for it:
# https://github.com/altdesktop/playerctl?tab=readme-ov-file#using-the-library
# TODO Make it less sucky
SECTIONS = [
"{{ playerName }} {{ status }}",
"{{ album }}",
"{{ artist }}",
"{{ duration(position) }}/{{ duration(mpris:length) }}" " {{ title }}",
]
# nf-fd icons don't work (UTF-16?)
SUBSTITUTIONS = {
"Playing": "",
"Paused": "",
"Stopped": "",
"mpd": "",
"firefox": "",
"chromium": "",
"mpv": "",
}
ICONS = {
1: "",
2: "",
3: "",
}
MAX_SECTION_LENGTH = 40
def __init__(self, theme: int | None = None):
ThreadedUpdater.__init__(self)
Section.__init__(self, theme)
self.line = ""
self.start()
self.sections: list[Section] = []
def fetcher(self) -> Element:
create = not len(self.sections)
populate = self.line
split = self.line.split("\t")
lastSection: Section = self
for i in range(len(self.SECTIONS)):
if create:
section = Section(theme=self.theme)
lastSection.appendAfter(section)
lastSection = section
self.sections.append(section)
else:
section = self.sections[i]
if populate:
text = split[i]
if i == 0:
for key, val in self.SUBSTITUTIONS.items():
text = text.replace(key, val)
if text:
if i in self.ICONS:
text = f"{self.ICONS[i]} {text}"
if len(text) > self.MAX_SECTION_LENGTH:
text = text[: self.MAX_SECTION_LENGTH - 1] + ""
section.updateText(text)
else:
section.updateText(None)
else:
section.updateText(None)
return None
def loop(self) -> None:
cmd = [
"playerctl",
"metadata",
"--format",
"\t".join(self.SECTIONS),
"--follow",
]
p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
assert p.stdout
while p.poll() is None:
self.line = p.stdout.readline().decode().strip()
self.refreshData()
p = subprocess.Popen(cmd, stdout=subprocess.PIPE)
assert p.stdout
while p.poll() is None:
self.line = p.stdout.readline().decode().strip()
self.refreshData()

View file

@ -4,6 +4,7 @@ import functools
import logging import logging
import math import math
import os import os
import subprocess
import threading import threading
import time import time
@ -11,8 +12,8 @@ import coloredlogs
import i3ipc import i3ipc
import pyinotify import pyinotify
from frobar.display import Text from frobar.common import notBusy
from frobar.notbusy import notBusy from frobar.display import Element
coloredlogs.install(level="DEBUG", fmt="%(levelname)s %(message)s") coloredlogs.install(level="DEBUG", fmt="%(levelname)s %(message)s")
log = logging.getLogger() log = logging.getLogger()
@ -20,24 +21,23 @@ log = logging.getLogger()
# TODO Sync bar update with PeriodicUpdater updates # TODO Sync bar update with PeriodicUpdater updates
class Updater: class Updater:
@staticmethod @staticmethod
def init(): def init() -> None:
PeriodicUpdater.init() PeriodicUpdater.init()
InotifyUpdater.init() InotifyUpdater.init()
notBusy.set() notBusy.set()
def updateText(self, text): def updateText(self, text: Element) -> None:
print(text) print(text)
def fetcher(self): def fetcher(self) -> Element:
return "{} refreshed".format(self) return "{} refreshed".format(self)
def __init__(self): def __init__(self) -> None:
self.lock = threading.Lock() self.lock = threading.Lock()
def refreshData(self): def refreshData(self) -> None:
# TODO OPTI Maybe discard the refresh if there's already another one? # TODO OPTI Maybe discard the refresh if there's already another one?
self.lock.acquire() self.lock.acquire()
try: try:
@ -50,7 +50,7 @@ class Updater:
class PeriodicUpdaterThread(threading.Thread): class PeriodicUpdaterThread(threading.Thread):
def run(self): def run(self) -> None:
# TODO Sync with system clock # TODO Sync with system clock
counter = 0 counter = 0
while True: while True:
@ -67,6 +67,7 @@ class PeriodicUpdaterThread(threading.Thread):
provider.refreshData() provider.refreshData()
else: else:
notBusy.clear() notBusy.clear()
assert PeriodicUpdater.intervalStep is not None
counter += PeriodicUpdater.intervalStep counter += PeriodicUpdater.intervalStep
counter = counter % PeriodicUpdater.intervalLoop counter = counter % PeriodicUpdater.intervalLoop
for interval in PeriodicUpdater.intervals.keys(): for interval in PeriodicUpdater.intervals.keys():
@ -80,43 +81,42 @@ class PeriodicUpdater(Updater):
Needs to call :func:`PeriodicUpdater.changeInterval` in `__init__` Needs to call :func:`PeriodicUpdater.changeInterval` in `__init__`
""" """
intervals = dict() intervals: dict[int, set["PeriodicUpdater"]] = dict()
intervalStep = None intervalStep: int | None = None
intervalLoop = None intervalLoop: int
updateThread = PeriodicUpdaterThread(daemon=True) updateThread: threading.Thread = PeriodicUpdaterThread(daemon=True)
intervalsChanged = threading.Event() intervalsChanged = threading.Event()
@staticmethod @staticmethod
def gcds(*args): def gcds(*args: int) -> int:
return functools.reduce(math.gcd, args) return functools.reduce(math.gcd, args)
@staticmethod @staticmethod
def lcm(a, b): def lcm(a: int, b: int) -> int:
"""Return lowest common multiple.""" """Return lowest common multiple."""
return a * b // math.gcd(a, b) return a * b // math.gcd(a, b)
@staticmethod @staticmethod
def lcms(*args): def lcms(*args: int) -> int:
"""Return lowest common multiple.""" """Return lowest common multiple."""
return functools.reduce(PeriodicUpdater.lcm, args) return functools.reduce(PeriodicUpdater.lcm, args)
@staticmethod @staticmethod
def updateIntervals(): def updateIntervals() -> None:
intervalsList = list(PeriodicUpdater.intervals.keys()) intervalsList = list(PeriodicUpdater.intervals.keys())
PeriodicUpdater.intervalStep = PeriodicUpdater.gcds(*intervalsList) PeriodicUpdater.intervalStep = PeriodicUpdater.gcds(*intervalsList)
PeriodicUpdater.intervalLoop = PeriodicUpdater.lcms(*intervalsList) PeriodicUpdater.intervalLoop = PeriodicUpdater.lcms(*intervalsList)
PeriodicUpdater.intervalsChanged.set() PeriodicUpdater.intervalsChanged.set()
@staticmethod @staticmethod
def init(): def init() -> None:
PeriodicUpdater.updateThread.start() PeriodicUpdater.updateThread.start()
def __init__(self): def __init__(self) -> None:
Updater.__init__(self) Updater.__init__(self)
self.interval = None self.interval: int | None = None
def changeInterval(self, interval): def changeInterval(self, interval: int) -> None:
assert isinstance(interval, int)
if self.interval is not None: if self.interval is not None:
PeriodicUpdater.intervals[self.interval].remove(self) PeriodicUpdater.intervals[self.interval].remove(self)
@ -131,12 +131,7 @@ class PeriodicUpdater(Updater):
class InotifyUpdaterEventHandler(pyinotify.ProcessEvent): class InotifyUpdaterEventHandler(pyinotify.ProcessEvent):
def process_default(self, event): def process_default(self, event: pyinotify.Event) -> None:
# DEBUG
# from pprint import pprint
# pprint(event.__dict__)
# return
assert event.path in InotifyUpdater.paths assert event.path in InotifyUpdater.paths
if 0 in InotifyUpdater.paths[event.path]: if 0 in InotifyUpdater.paths[event.path]:
@ -154,10 +149,10 @@ class InotifyUpdater(Updater):
""" """
wm = pyinotify.WatchManager() wm = pyinotify.WatchManager()
paths = dict() paths: dict[str, dict[str | int, set["InotifyUpdater"]]] = dict()
@staticmethod @staticmethod
def init(): def init() -> None:
notifier = pyinotify.ThreadedNotifier( notifier = pyinotify.ThreadedNotifier(
InotifyUpdater.wm, InotifyUpdaterEventHandler() InotifyUpdater.wm, InotifyUpdaterEventHandler()
) )
@ -166,14 +161,14 @@ class InotifyUpdater(Updater):
# TODO Mask for folders # TODO Mask for folders
MASK = pyinotify.IN_CREATE | pyinotify.IN_MODIFY | pyinotify.IN_DELETE MASK = pyinotify.IN_CREATE | pyinotify.IN_MODIFY | pyinotify.IN_DELETE
def addPath(self, path, refresh=True): def addPath(self, path: str, refresh: bool = True) -> None:
path = os.path.realpath(os.path.expanduser(path)) path = os.path.realpath(os.path.expanduser(path))
# Detect if file or folder # Detect if file or folder
if os.path.isdir(path): if os.path.isdir(path):
self.dirpath = path self.dirpath: str = path
# 0: Directory watcher # 0: Directory watcher
self.filename = 0 self.filename: str | int = 0
elif os.path.isfile(path): elif os.path.isfile(path):
self.dirpath = os.path.dirname(path) self.dirpath = os.path.dirname(path)
self.filename = os.path.basename(path) self.filename = os.path.basename(path)
@ -195,12 +190,12 @@ class InotifyUpdater(Updater):
class ThreadedUpdaterThread(threading.Thread): class ThreadedUpdaterThread(threading.Thread):
def __init__(self, updater, *args, **kwargs): def __init__(self, updater: "ThreadedUpdater") -> None:
self.updater = updater self.updater = updater
threading.Thread.__init__(self, *args, **kwargs) threading.Thread.__init__(self, daemon=True)
self.looping = True self.looping = True
def run(self): def run(self) -> None:
try: try:
while self.looping: while self.looping:
self.updater.loop() self.updater.loop()
@ -215,57 +210,31 @@ class ThreadedUpdater(Updater):
Must implement loop(), and call start() Must implement loop(), and call start()
""" """
def __init__(self): def __init__(self) -> None:
Updater.__init__(self) Updater.__init__(self)
self.thread = ThreadedUpdaterThread(self, daemon=True) self.thread = ThreadedUpdaterThread(self)
def loop(self): def loop(self) -> None:
self.refreshData() self.refreshData()
time.sleep(10) time.sleep(10)
def start(self): def start(self) -> None:
self.thread.start() self.thread.start()
class I3Updater(ThreadedUpdater): class I3Updater(ThreadedUpdater):
# TODO OPTI One i3 connection for all # TODO OPTI One i3 connection for all
def __init__(self): def __init__(self) -> None:
ThreadedUpdater.__init__(self) ThreadedUpdater.__init__(self)
self.i3 = i3ipc.Connection() self.i3 = i3ipc.Connection()
self.on = self.i3.on
self.start() self.start()
def on(self, event, function): def loop(self) -> None:
self.i3.on(event, function)
def loop(self):
self.i3.main() self.i3.main()
class MergedUpdater(Updater): class MergedUpdater(Updater):
# TODO OPTI Do not update until end of periodic batch def __init__(self, *args: Updater) -> None:
def fetcher(self): raise NotImplementedError("Deprecated, as hacky and currently unused")
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

@ -0,0 +1,25 @@
{ 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?

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

@ -0,0 +1,218 @@
{ 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;
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,72 @@
{ 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,63 @@
{ 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,39 @@
# 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,28 @@
{ 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,16 @@
{ 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,90 @@
{ 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
];
}

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

@ -0,0 +1,50 @@
{ 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
'';
};
};
};
}

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

@ -0,0 +1,86 @@
{ 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
nixpkgs-fmt
# 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 = [ "nixpkgs-fmt" ];
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
};
};
}

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

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

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

@ -0,0 +1,19 @@
# 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}"
];
};
};
}

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

@ -0,0 +1,21 @@
# 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";
};
};
}

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

@ -0,0 +1,38 @@
# 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;
};
};
};
};
}

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

@ -0,0 +1,54 @@
{ 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

@ -21,6 +21,7 @@
# Communication # Communication
signal-desktop signal-desktop
(pkgs.callPackage ./whisperx.nix {}) # Transcribe voice messages
# downloading # downloading
# transmission TODO Collision if both transmissions are active? # transmission TODO Collision if both transmissions are active?
@ -34,21 +35,20 @@
# TODO Convert existing LaTeX documents into using Nix build system # TODO Convert existing LaTeX documents into using Nix build system
# texlive is big and not that much used, sooo # texlive is big and not that much used, sooo
pdftk pdftk
hunspell pdfgrep
hunspellDicts.en_GB-ize
hunspellDicts.en_US # Misc
hunspellDicts.fr-moderne haskellPackages.dice
hunspellDicts.nl_NL rustdesk-flutter
# TODO libreoffice-extension-languagetool or libreoffice-extension-grammalecte-fr
] ++ lib.optionals config.frogeye.desktop.xorg [ ] ++ lib.optionals config.frogeye.desktop.xorg [
# multimedia editors # multimedia editors
gimp
inkscape
darktable darktable
puddletag puddletag
audacity audacity
xournalpp
krita
# downloading # downloading
transmission-qt transmission-qt
@ -64,8 +64,5 @@
# https://hydra.nixos.org/job/nixos/release-23.11/nixpkgs.blender.aarch64-linux # https://hydra.nixos.org/job/nixos/release-23.11/nixpkgs.blender.aarch64-linux
blender blender
]); ]);
services = {
syncthing.enable = true;
};
}; };
} }

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

@ -0,0 +1,37 @@
{ pkgs ? import <nixpkgs> { } }:
pkgs.python3Packages.buildPythonPackage {
pname = "whisperx";
version = "2024-08-19";
# pypi doesn't have the requirements.txt file, and it's required
src = pkgs.fetchFromGitHub {
owner = "m-bain";
repo = "whisperX";
rev = "9e3a9e0e38fcec1304e1784381059a0e2c670be5"; # git doesn't have tags
hash = "sha256-IVtn9fe/yi4+fbH57s9LoiREnMZ2nhEObp1a4R/7gHg=";
};
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
];
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

@ -3,13 +3,28 @@
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";
};
};
}; };
} }

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

@ -0,0 +1,125 @@
{ pkgs, lib, config, unixpkgs, ... }:
let
cfg = config.programs.git;
in
{
config = lib.mkIf cfg.enable {
home.packages = [
(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;
package = (import unixpkgs {
inherit (pkgs) system;
}).jujutsu;
# 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";
};
};
};
};
};
}

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

@ -0,0 +1,50 @@
{ 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 = [{
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;
pinentryPackage = pkgs.pinentry-gnome3;
# 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.
defaultCacheTtl = 3600;
defaultCacheTtlSsh = defaultCacheTtl;
maxCacheTtl = 3*3600;
maxCacheTtlSsh = maxCacheTtl;
};
};
}

61
hm/homealone.nix Normal file
View file

@ -0,0 +1,61 @@
{ 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
};
}

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

@ -0,0 +1,35 @@
{ 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
'';
};
};
};
};
}

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

@ -0,0 +1,24 @@
{ 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
};
};
};
}

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

@ -0,0 +1,112 @@
{ 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`.";
};
};
}));
};
};
}

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

@ -0,0 +1,22 @@
{ pkgs, lib, config, ... }:
{
config = lib.mkIf config.programs.less.enable {
programs.powerline-go = {
enable = true;
modules = [ "user" "host" "venv" "cwd" "perms" "nix-shell" "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
'';
})
];
}

265
hm/scripts/jlab Executable file
View file

@ -0,0 +1,265 @@
#!/usr/bin/env cached-nix-shell
#! nix-shell -i python3
#! nix-shell -p python3 python3Packages.pydantic
# vim: filetype=python
"""
glab wrapper for jujutsu,
with some convinience features.
"""
import re
import subprocess
import sys
import typing
import pydantic
import typing_extensions
class GitLabMR(pydantic.BaseModel):
"""
Represents a GitLab Merge Request.
"""
title: str
source_branch: str
target_branch: str
project_id: int
source_project_id: int
target_project_id: int
@pydantic.model_validator(mode="after")
def same_project(self) -> typing_extensions.Self:
if not (self.project_id == self.source_project_id == self.target_project_id):
raise NotImplementedError("Different project ids")
return self
def glab_get_mr(branch: str) -> GitLabMR:
"""
Get details about a GitLab MR.
"""
sp = subprocess.run(
["glab", "mr", "view", branch, "--output", "json"], stdout=subprocess.PIPE
)
sp.check_returncode()
return GitLabMR.model_validate_json(sp.stdout)
class JujutsuType:
"""
Utility to work with Template types.
https://martinvonz.github.io/jj/latest/templates/
"""
FIELD_SEPARATOR: typing.ClassVar[str] = "\0"
ESCAPED_SEPARATOR: typing.ClassVar[str] = r"\0"
@staticmethod
def template(base: str, type_: typing.Type) -> str:
"""
Generates a --template string that is machine-parseable for a given type.
"""
if typing.get_origin(type_) == list:
# If we have a list, prepend it with the number of items
# so we know how many fields we should consume.
(subtype,) = typing.get_args(type_)
subtype = typing.cast(typing.Type, subtype)
return (
f'{base}.len()++"{JujutsuType.ESCAPED_SEPARATOR}"'
f'++{base}.map(|l| {JujutsuType.template("l", subtype)})'
)
elif issubclass(type_, JujutsuObject):
return type_.template(base)
else:
return f'{base}++"{JujutsuType.ESCAPED_SEPARATOR}"'
@staticmethod
def parse(stack: list[str], type_: typing.Type) -> typing.Any:
"""
Unserialize the result of a template to a given type.
Needs to be provided the template as a list splitted by the field separator.
It will consume the fields it needs.
"""
if typing.get_origin(type_) == list:
(subtype,) = typing.get_args(type_)
subtype = typing.cast(typing.Type, subtype)
len = int(stack.pop(0))
return [JujutsuType.parse(stack, subtype) for i in range(len)]
elif issubclass(type_, JujutsuObject):
return type_.parse(stack)
else:
return stack.pop(0)
class JujutsuObject(pydantic.BaseModel):
@classmethod
def template(cls, base: str) -> str:
temp = []
for k, v in cls.model_fields.items():
key = f"{base}.{k}()"
temp.append(JujutsuType.template(key, v.annotation))
return "++".join(temp)
@classmethod
def parse(cls, stack: list[str]) -> typing_extensions.Self:
ddict = dict()
for k, v in cls.model_fields.items():
ddict[k] = JujutsuType.parse(stack, v.annotation)
return cls(**ddict)
class JujutsuShortestIdPrefix(JujutsuObject):
prefix: str
rest: str
@property
def full(self) -> str:
return self.prefix + self.rest
class JujutsuChangeId(JujutsuObject):
shortest: JujutsuShortestIdPrefix
@property
def full(self) -> str:
return self.shortest.full
class JujutsuRefName(JujutsuObject):
name: str
class JujutsuCommit(JujutsuObject):
change_id: JujutsuChangeId
bookmarks: list[JujutsuRefName]
class Jujutsu:
"""
Represents a Jujutsu repo.
Since there's no need for multi-repo, this is just the one in the current directory.
"""
def run(self, *args: str, **kwargs: typing.Any) -> subprocess.CompletedProcess:
cmd = ["jj"]
cmd.extend(args)
sp = subprocess.run(cmd, stdout=subprocess.PIPE)
sp.check_returncode()
return sp
def log(self, revset: str = "@") -> list[JujutsuCommit]:
cmd = [
"log",
"-r",
revset,
"--no-graph",
"-T",
JujutsuCommit.template("self"),
]
sp = self.run(*cmd, stdout=subprocess.PIPE)
stack = sp.stdout.decode().split(JujutsuType.FIELD_SEPARATOR)
assert stack[-1] == "", "No trailing NUL byte"
stack.pop()
commits = []
while len(stack):
commits.append(JujutsuCommit.parse(stack))
return commits
jj = Jujutsu()
def current_bookmark() -> JujutsuRefName | None:
"""
Replacement of git's current branch concept working with jj.
Needed for commodity features, such as not requiring to type the MR mumber / branch
for `glab mr`, or automatically advance the bookmark to the head before pushing.
"""
bookmarks = []
for commit in jj.log("reachable(@, trunk()..)"):
bookmarks.extend(commit.bookmarks)
if len(bookmarks) > 1:
raise NotImplementedError("Multiple bookmarks on trunk branch") # TODO
# If there's a split in the tree: TBD
# If there's no bookmark ahead: the bookmark behind
# If there's a bookmark ahead: that one
# (needs adjusting of push so it doesn't advance anything then)
if bookmarks:
return bookmarks[0]
else:
return None
def to_glab() -> None:
"""
Pass the remaining arguments to glab.
"""
sp = subprocess.run(["glab"] + sys.argv[1:])
sys.exit(sp.returncode)
if len(sys.argv) <= 1:
to_glab()
elif sys.argv[1] in ("merge", "mr"):
if len(sys.argv) <= 2:
to_glab()
elif sys.argv[2] == "checkout":
# Bypass the original checkout command so it doesn't run git commands.
# If there's no commit on the branch, add one with the MR title
# so jj has a current bookmark.
mr = glab_get_mr(sys.argv[3])
jj.run("git", "fetch")
if len(jj.log(f"{mr.source_branch} | {mr.target_branch}")) == 1:
title = re.sub(r"^(WIP|Draft): ", "", mr.title)
jj.run("new", mr.source_branch)
jj.run("describe", "-m", title)
jj.run("bookmark", "move", mr.source_branch)
else:
jj.run("edit", mr.source_branch)
elif sys.argv[2] in (
# If no MR number/branch is given, insert the current bookmark,
# as the current branch concept doesn't exist in jj
"approve",
"approvers",
"checkout",
"close",
"delete",
"diff",
"issues",
"merge",
"note",
"rebase",
"revoke",
"subscribe",
"todo",
"unsubscribe",
"update",
"view",
):
if len(sys.argv) <= 3 or sys.argv[3].startswith("-"):
bookmark = current_bookmark()
if bookmark:
sys.argv.insert(3, bookmark.name)
to_glab()
else:
to_glab()
elif sys.argv[1] == "push":
# Advance the current branch to the head and push
bookmark = current_bookmark()
if not bookmark:
raise RuntimeError("Couldn't find a current branch")
heads = jj.log("heads(@::)")
if len(heads) != 1:
raise RuntimeError("Multiple heads") # Or none if something goes horribly wrong
head = heads[0]
jj.run("bookmark", "set", bookmark.name, "-r", head.change_id.full)
jj.run("git", "push", "--bookmark", bookmark.name)
# TODO Sign https://github.com/martinvonz/jj/issues/4712
else:
to_glab()
# TODO Autocomplete

View file

@ -1,8 +1,8 @@
#!/usr/bin/env nix-shell #!/usr/bin/env nix-shell
#! nix-shell -i bash --pure #! nix-shell -i bash --pure
#! nix-shell -p bash jq curl findutils coreutils #! nix-shell -p bash jq curl cacert findutils coreutils
set -euo pipefail set -euxo pipefail
url="https://ip.frogeye.fr/json" url="https://ip.frogeye.fr/json"
cachedir="$HOME/.cache/lip" cachedir="$HOME/.cache/lip"
@ -16,7 +16,6 @@ then
jq_sel="$@" jq_sel="$@"
fi fi
if [ -n "$ip" ] if [ -n "$ip" ]
then then
cachefile="$cachedir/$ip" cachefile="$cachedir/$ip"

View file

@ -1,7 +1,9 @@
#!/usr/bin/env nix-shell #!/usr/bin/env nix-shell
#! nix-shell -i bash --pure #! nix-shell -i bash --pure
#! nix-shell -p bash coreutils imagemagick libjpeg optipng ffmpeg diffutils #! nix-shell -p bash coreutils imagemagick libjpeg optipng ffmpeg diffutils
# vim: filetype=sh
set -euo pipefail
# Optimizes everything the script can find in a folder, # Optimizes everything the script can find in a folder,
# meaning it will compress files as much as possible, # meaning it will compress files as much as possible,
@ -13,14 +15,13 @@
# TODO Lots of dupplicated code there # TODO Lots of dupplicated code there
# TODO Maybe replace part with https://github.com/toy/image_optim? # TODO Maybe replace part with https://github.com/toy/image_optim?
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
dir=${1:-$PWD} dir=${1:-$PWD}
total=$(mktemp) total=$(mktemp)
echo -n 0 > $total echo -n 0 > "$total"
function showtotal { function showtotal {
echo "Total saved: $(cat "$total") bytes" echo "Total saved: $(cat "$total") bytes"
rm $total rm "$total"
exit exit
} }
@ -28,11 +29,11 @@ trap showtotal SIGTERM SIGINT SIGFPE
function doReplace { # candidate original function doReplace { # candidate original
mv "$c" "$o" mv "$c" "$o"
saved=$(($os - $cs)) saved=$((os - cs))
perc=$((100 * $saved / $os)) perc=$((100 * saved / os))
echo "→ $os ⇒ $cs (saved $saved bytes, or ${perc}%)" echo "→ $os ⇒ $cs (saved $saved bytes, or ${perc}%)"
newtotal=$(($(cat $total) + $saved)) newtotal=$(($(cat "$total") + saved))
echo -n $newtotal > $total echo -n $newtotal > "$total"
} }
function replace { # candidate original function replace { # candidate original
@ -52,17 +53,17 @@ function replace { # candidate original
# Size verifications # Size verifications
cs=$(wc -c "$c" | cut -d' ' -f1) cs=$(wc -c "$c" | cut -d' ' -f1)
os=$(wc -c "$o" | cut -d' ' -f1) os=$(wc -c "$o" | cut -d' ' -f1)
if [ $cs -le 0 ]; then if [ "$cs" -le 0 ]; then
echo "→ Candidate is empty, skipping!" echo "→ Candidate is empty, skipping!"
rm "$c" rm "$c"
return return
fi fi
if [ $cs -eq $os ]; then if [ "$cs" -eq "$os" ]; then
echo "→ Candidate weight the same, skipping." echo "→ Candidate weight the same, skipping."
rm "$c" rm "$c"
return return
fi fi
if [ $cs -gt $os ]; then if [ "$cs" -gt "$os" ]; then
echo "→ Candidate is larger, skipping." echo "→ Candidate is larger, skipping."
rm "$c" rm "$c"
return return
@ -71,76 +72,75 @@ function replace { # candidate original
doReplace "$c" "$o" doReplace "$c" "$o"
} }
function replaceImg { # candidate original # function replaceImg { # candidate original
# With bitmap verification # # With bitmap verification
#
c="$1" # c="$1"
o="$2" # o="$2"
#
# File verifications # # File verifications
if [ ! -f "$o" ]; then # if [ ! -f "$o" ]; then
echo "→ Original is inexistant, skipping!" # echo "→ Original is inexistant, skipping!"
return # return
fi # fi
if [ ! -f "$c" ]; then # if [ ! -f "$c" ]; then
echo "→ Candidate is inexistant, skipping!" # echo "→ Candidate is inexistant, skipping!"
return # return
fi # fi
#
# Size verifications # # Size verifications
cs=$(wc -c "$c" | cut -d' ' -f1) # cs=$(wc -c "$c" | cut -d' ' -f1)
os=$(wc -c "$o" | cut -d' ' -f1) # os=$(wc -c "$o" | cut -d' ' -f1)
if [ $cs -le 0 ]; then # if [ $cs -le 0 ]; then
echo "→ Candidate is empty, skipping!" # echo "→ Candidate is empty, skipping!"
rm "$c" # rm "$c"
return # return
fi # fi
if [ $cs -eq $os ]; then # if [ $cs -eq $os ]; then
echo "→ Candidate weight the same, skipping." # echo "→ Candidate weight the same, skipping."
rm "$c" # rm "$c"
return # return
fi # fi
if [ $cs -gt $os ]; then # if [ $cs -gt $os ]; then
echo "→ Candidate is larger, skipping." # echo "→ Candidate is larger, skipping."
rm "$c" # rm "$c"
return # return
fi # fi
#
# Bitmap verification # # Bitmap verification
ppmc="$(mktemp --suffix .ppm)" # ppmc="$(mktemp --suffix .ppm)"
ppmo="$(mktemp --suffix .ppm)" # ppmo="$(mktemp --suffix .ppm)"
convert "$c" "$ppmc" # convert "$c" "$ppmc"
convert "$o" "$ppmo" # convert "$o" "$ppmo"
#
if cmp --silent "$ppmo" "$ppmc"; then # if cmp --silent "$ppmo" "$ppmc"; then
doReplace "$c" "$o" # doReplace "$c" "$o"
else # else
echo "→ Candidate don't have the same bit map as original, skipping!" # echo "→ Candidate don't have the same bit map as original, skipping!"
fi # fi
rm -f "$ppmc" "$ppmo" "$c" # rm -f "$ppmc" "$ppmo" "$c"
#
} # }
# JPEG (requires jpegtran) # JPEG (requires jpegtran)
while read image while read -r image
do do
if [ -z "$image" ]; then continue; fi if [ -z "$image" ]; then continue; fi
echo Processing $image echo Processing "$image"
prog=$(mktemp --suffix .jpg) prog=$(mktemp --suffix .jpg)
jpegtran -copy all -progressive "$image" > "$prog" jpegtran -copy all -progressive "$image" > "$prog"
echo "→ Progressive done" echo "→ Progressive done"
progs=$(wc -c "$prog" | cut -d' ' -f1)
replace "$prog" "$image" replace "$prog" "$image"
done <<< "$(find "$dir/" -type f -iregex ".+.jpe?g$")" done <<< "$(find "$dir/" -type f -iregex ".+.jpe?g$")"
# PNG (requires optipng) # PNG (requires optipng)
while read image while read -r image
do do
if [ -z "$image" ]; then continue; fi if [ -z "$image" ]; then continue; fi
echo Processing $image echo Processing "$image"
temp=$(mktemp --suffix .png) temp=$(mktemp --suffix .png)
cp "$image" "$temp" cp "$image" "$temp"
@ -152,14 +152,13 @@ do
done <<< "$(find "$dir/" -type f -iname "*.png")" done <<< "$(find "$dir/" -type f -iname "*.png")"
# FLAC (requires ffmpeg) # FLAC (requires ffmpeg)
while read music while read -r music
do do
if [ -z "$music" ]; then continue; fi if [ -z "$music" ]; then continue; fi
echo Processing $music echo "Processing $music"
temp=$(mktemp --suffix .flac) temp=$(mktemp --suffix .flac)
cp "$music" "$temp" ffmpeg -nostdin -y -i "$music" -compression_level 8 "$temp"
ffmpeg -8 -o "$temp"
echo "→ Optimize done" echo "→ Optimize done"
replace "$temp" "$music" replace "$temp" "$music"
@ -187,6 +186,6 @@ done <<< "$(find "$dir/" -type f -iname "*.flac")"
# - I might want to keep editor data and/or ids for some of them # - I might want to keep editor data and/or ids for some of them
# So rather use scour explicitely when needed # So rather use scour explicitely when needed
${SCRIPT_DIR}/cleandev cleandev
showtotal showtotal

View file

@ -1,7 +1,9 @@
#!/usr/bin/env nix-shell #!/usr/bin/env nix-shell
#! nix-shell -i bash --pure #! nix-shell -i bash
#! nix-shell -p bash pdftk inkscape gnused coreutils file #! nix-shell -p bash pdftk inkscape gnused coreutils file
set -euxo pipefail
# Utility to write over a PDF file pages # Utility to write over a PDF file pages
# TODO Inkscape vodoo: Put the original in its own layer and skip when merging # TODO Inkscape vodoo: Put the original in its own layer and skip when merging

View file

@ -1,8 +1,9 @@
#!/usr/bin/env nix-shell #!/usr/bin/env nix-shell
#! nix-shell -i python3 --pure #! nix-shell -i python3 --pure
#! nix-shell -p python3 python3Packages.coloredlogs python3Packages.r128gain #! nix-shell -p python3 python3Packages.coloredlogs r128gain
# TODO r128gain is not maintainted anymore # TODO r128gain is not maintainted anymore
# 24.05 rsgain replaces it, does the same job as I do with albums
# Normalisation is done at the default of each program, # Normalisation is done at the default of each program,
# which is usually -89.0 dB # which is usually -89.0 dB

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