Compare commits

..

No commits in common. "main" and "push-kmvskqxkozop" have entirely different histories.

77 changed files with 3494 additions and 2294 deletions

View file

@ -2,9 +2,9 @@
{
config = {
frogeye = {
name = "morton";
name = "abavorana";
storageSize = "big";
syncthing.name = "Morton";
syncthing.name = "Abavorana";
};
};
}

View file

@ -1,4 +1,6 @@
{
pkgs,
lib,
config,
...
}:

View file

@ -1,4 +1,6 @@
{
pkgs,
lib,
config,
...
}:

View file

@ -4,7 +4,8 @@
frogeye = {
desktop.xorg = true;
dev = {
"3d" = true;
c = true;
vm = true;
};
extra = true;
};

View file

@ -1,5 +1,7 @@
{
pkgs,
lib,
config,
nixos-hardware,
...
}:
@ -24,7 +26,7 @@
hardware.enableRedistributableFirmware = true;
frogeye.desktop = {
x11_screens = [ "DP-2" "eDP-1" ];
x11_screens = [ "eDP-1" ];
maxVideoHeight = 1080;
phasesCommands = {

View file

@ -1,4 +1,7 @@
{
pkgs,
lib,
config,
...
}:
{

View file

@ -1,4 +1,7 @@
{
pkgs,
lib,
config,
...
}:
{

View file

@ -1,4 +1,6 @@
{
pkgs,
lib,
config,
...
}:

View file

@ -5,7 +5,6 @@
xorg = true;
};
dev = {
"3d" = true;
c = true;
docker = true;
vm = true;

View file

@ -49,9 +49,6 @@ in
hardware.enableRedistributableFirmware = true;
# TODO Do we really need that? Besides maybe microcode?
# AnnePro 2
hardware.keyboard.qmk.enable = true;
frogeye.desktop = {
x11_screens = [
displays.deskLeft.output
@ -62,18 +59,18 @@ in
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 &
${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 &
${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 &
${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?
};

View file

@ -1,4 +1,7 @@
{
pkgs,
lib,
config,
...
}:
{

View file

@ -1,4 +1,6 @@
{
pkgs,
lib,
config,
...
}:

View file

@ -1,5 +1,7 @@
{
pkgs,
lib,
config,
...
}:
{

View file

@ -37,11 +37,11 @@
"base16-helix": {
"flake": false,
"locked": {
"lastModified": 1736852337,
"narHash": "sha256-esD42YdgLlEh7koBrSqcT7p2fsMctPAcGl/+2sYJa2o=",
"lastModified": 1725860795,
"narHash": "sha256-Z2o8VBPW3I+KKTSfe25kskz0EUj7MpUh8u355Z1nVsU=",
"owner": "tinted-theming",
"repo": "base16-helix",
"rev": "03860521c40b0b9c04818f2218d9cc9efc21e7a5",
"rev": "7f795bf75d38e0eea9fed287264067ca187b88a9",
"type": "github"
},
"original": {
@ -53,17 +53,16 @@
"base16-vim": {
"flake": false,
"locked": {
"lastModified": 1732806396,
"narHash": "sha256-e0bpPySdJf0F68Ndanwm+KWHgQiZ0s7liLhvJSWDNsA=",
"lastModified": 1731949548,
"narHash": "sha256-XIDexXM66sSh5j/x70e054BnUsviibUShW7XhbDGhYo=",
"owner": "tinted-theming",
"repo": "base16-vim",
"rev": "577fe8125d74ff456cf942c733a85d769afe58b7",
"rev": "61165b1632409bd55e530f3dbdd4477f011cadc6",
"type": "github"
},
"original": {
"owner": "tinted-theming",
"repo": "base16-vim",
"rev": "577fe8125d74ff456cf942c733a85d769afe58b7",
"type": "github"
}
},
@ -75,11 +74,11 @@
]
},
"locked": {
"lastModified": 1741473158,
"narHash": "sha256-kWNaq6wQUbUMlPgw8Y+9/9wP0F8SHkjy24/mN3UAppg=",
"lastModified": 1728330715,
"narHash": "sha256-xRJ2nPOXb//u1jaBnDP56M7v5ldavjbtR6lfGqSvcKg=",
"owner": "numtide",
"repo": "devshell",
"rev": "7c9e793ebe66bcba8292989a68c0419b737a22a0",
"rev": "dd6b80932022cea34a019e2bb32f6fa9e494dfef",
"type": "github"
},
"original": {
@ -95,11 +94,11 @@
]
},
"locked": {
"lastModified": 1743598667,
"narHash": "sha256-ViE7NoFWytYO2uJONTAX35eGsvTYXNHjWALeHAg8OQY=",
"lastModified": 1735048446,
"narHash": "sha256-Tc35Y8H+krA6rZeOIczsaGAtobSSBPqR32AfNTeHDRc=",
"owner": "nix-community",
"repo": "disko",
"rev": "329d3d7e8bc63dd30c39e14e6076db590a6eabe6",
"rev": "3a4de9fa3a78ba7b7170dda6bd8b4cdab87c0b21",
"type": "github"
},
"original": {
@ -107,30 +106,14 @@
"type": "indirect"
}
},
"firefox-gnome-theme": {
"flake": false,
"locked": {
"lastModified": 1743774811,
"narHash": "sha256-oiHLDHXq7ymsMVYSg92dD1OLnKLQoU/Gf2F1GoONLCE=",
"owner": "rafaelmardojai",
"repo": "firefox-gnome-theme",
"rev": "df53a7a31872faf5ca53dd0730038a62ec63ca9e",
"type": "github"
},
"original": {
"owner": "rafaelmardojai",
"repo": "firefox-gnome-theme",
"type": "github"
}
},
"flake-compat": {
"locked": {
"lastModified": 1733328505,
"narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=",
"rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec",
"revCount": 69,
"lastModified": 1696426674,
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
"revCount": 57,
"type": "tarball",
"url": "https://api.flakehub.com/f/pinned/edolstra/flake-compat/1.1.0/01948eb7-9cba-704f-bbf3-3fa956735b52/source.tar.gz"
"url": "https://api.flakehub.com/f/pinned/edolstra/flake-compat/1.0.1/018afb31-abd1-7bff-a5e4-cff7e18efb7a/source.tar.gz"
},
"original": {
"type": "tarball",
@ -140,11 +123,11 @@
"flake-compat_2": {
"flake": false,
"locked": {
"lastModified": 1733328505,
"narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=",
"lastModified": 1696426674,
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec",
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
"type": "github"
},
"original": {
@ -161,11 +144,11 @@
]
},
"locked": {
"lastModified": 1743550720,
"narHash": "sha256-hIshGgKZCgWh6AYJpJmRgFdR3WUbkY04o82X05xqQiY=",
"lastModified": 1733312601,
"narHash": "sha256-4pDvzqnegAfRkPwO3wmwBhVi/Sye1mzps0zHWYnP88c=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "c621e8422220273271f52058f618c94e405bb0f5",
"rev": "205b12d8b7cd4802fbcb8e8ef6a0f1408781a4f9",
"type": "github"
},
"original": {
@ -278,40 +261,18 @@
"nixpkgs": [
"nixvim",
"nixpkgs"
]
},
"locked": {
"lastModified": 1742649964,
"narHash": "sha256-DwOTp7nvfi8mRfuL1escHDXabVXFGT1VlPD1JHrtrco=",
"owner": "cachix",
"repo": "git-hooks.nix",
"rev": "dcf5072734cb576d2b0c59b2ac44f5050b5eac82",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "git-hooks.nix",
"type": "github"
}
},
"git-hooks_2": {
"inputs": {
"flake-compat": [
"stylix",
"flake-compat"
],
"gitignore": "gitignore_2",
"nixpkgs": [
"stylix",
"nixpkgs-stable": [
"nixvim",
"nixpkgs"
]
},
"locked": {
"lastModified": 1742649964,
"narHash": "sha256-DwOTp7nvfi8mRfuL1escHDXabVXFGT1VlPD1JHrtrco=",
"lastModified": 1734425854,
"narHash": "sha256-nzE5UbJ41aPEKf8R2ZFYtLkqPmF7EIUbNEdHMBLg0Ig=",
"owner": "cachix",
"repo": "git-hooks.nix",
"rev": "dcf5072734cb576d2b0c59b2ac44f5050b5eac82",
"rev": "0ddd26d0925f618c3a5d85a4fa5eb1e23a09491d",
"type": "github"
},
"original": {
@ -342,28 +303,6 @@
"type": "github"
}
},
"gitignore_2": {
"inputs": {
"nixpkgs": [
"stylix",
"git-hooks",
"nixpkgs"
]
},
"locked": {
"lastModified": 1709087332,
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
"owner": "hercules-ci",
"repo": "gitignore.nix",
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "gitignore.nix",
"type": "github"
}
},
"gnome-shell": {
"flake": false,
"locked": {
@ -388,11 +327,11 @@
]
},
"locked": {
"lastModified": 1743808813,
"narHash": "sha256-2lDQBOmlz9ggPxcS7/GvcVdzXMIiT+PpMao6FbLJSr0=",
"lastModified": 1734366194,
"narHash": "sha256-vykpJ1xsdkv0j8WOVXrRFHUAdp9NXHpxdnn1F4pYgSw=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "a9f8b3db211b4609ddd83683f9db89796c7f6ac6",
"rev": "80b0fdf483c5d1cb75aaad909bd390d48673857f",
"type": "github"
},
"original": {
@ -409,11 +348,11 @@
]
},
"locked": {
"lastModified": 1743808813,
"narHash": "sha256-2lDQBOmlz9ggPxcS7/GvcVdzXMIiT+PpMao6FbLJSr0=",
"lastModified": 1734366194,
"narHash": "sha256-vykpJ1xsdkv0j8WOVXrRFHUAdp9NXHpxdnn1F4pYgSw=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "a9f8b3db211b4609ddd83683f9db89796c7f6ac6",
"rev": "80b0fdf483c5d1cb75aaad909bd390d48673857f",
"type": "github"
},
"original": {
@ -431,11 +370,11 @@
]
},
"locked": {
"lastModified": 1743808813,
"narHash": "sha256-2lDQBOmlz9ggPxcS7/GvcVdzXMIiT+PpMao6FbLJSr0=",
"lastModified": 1733572789,
"narHash": "sha256-zjO6m5BqxXIyjrnUziAzk4+T4VleqjstNudSqWcpsHI=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "a9f8b3db211b4609ddd83683f9db89796c7f6ac6",
"rev": "c7ffc9727d115e433fd884a62dc164b587ff651d",
"type": "github"
},
"original": {
@ -473,6 +412,38 @@
"type": "github"
}
},
"jjuinixpkgs": {
"locked": {
"lastModified": 1734688116,
"narHash": "sha256-Ex3o8880p+yZ9915s46/4XtnN4jS6tqp2TlfGR1+l1w=",
"owner": "Adda0",
"repo": "nixpkgs",
"rev": "c951613d3cb3d61b14c890238017d0685ff359f9",
"type": "github"
},
"original": {
"owner": "Adda0",
"ref": "jjui",
"repo": "nixpkgs",
"type": "github"
}
},
"labellenixpkgs": {
"locked": {
"lastModified": 1733305049,
"narHash": "sha256-j3L36nA0PTjVA6gtMVILBhrBSMxuhevlDW9Nfws1oII=",
"owner": "FabianRig",
"repo": "nixpkgs",
"rev": "88ac05665bc6a85aabe78070b99fd23ad1675409",
"type": "github"
},
"original": {
"owner": "FabianRig",
"ref": "update-labelle-1.3.2",
"repo": "nixpkgs",
"type": "github"
}
},
"nix-darwin": {
"inputs": {
"nixpkgs": [
@ -481,16 +452,15 @@
]
},
"locked": {
"lastModified": 1743127615,
"narHash": "sha256-+sMGqywrSr50BGMLMeY789mSrzjkoxZiu61eWjYS/8o=",
"lastModified": 1733570843,
"narHash": "sha256-sQJAxY1TYWD1UyibN/FnN97paTFuwBw3Vp3DNCyKsMk=",
"owner": "lnl7",
"repo": "nix-darwin",
"rev": "fc843893cecc1838a59713ee3e50e9e7edc6207c",
"rev": "a35b08d09efda83625bef267eb24347b446c80b8",
"type": "github"
},
"original": {
"owner": "lnl7",
"ref": "nix-darwin-24.11",
"repo": "nix-darwin",
"type": "github"
}
@ -550,11 +520,11 @@
},
"nixos-hardware": {
"locked": {
"lastModified": 1743420942,
"narHash": "sha256-b/exDDQSLmENZZgbAEI3qi9yHkuXAXCPbormD8CSJXo=",
"lastModified": 1734954597,
"narHash": "sha256-QIhd8/0x30gEv8XEE1iAnrdMlKuQ0EzthfDR7Hwl+fk=",
"owner": "NixOS",
"repo": "nixos-hardware",
"rev": "de6fc5551121c59c01e2a3d45b277a6d05077bc4",
"rev": "def1d472c832d77885f174089b0d34854b007198",
"type": "github"
},
"original": {
@ -564,11 +534,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1743813633,
"narHash": "sha256-BgkBz4NpV6Kg8XF7cmHDHRVGZYnKbvG0Y4p+jElwxaM=",
"lastModified": 1734991663,
"narHash": "sha256-8T660guvdaOD+2/Cj970bWlQwAyZLKrrbkhYOFcY1YE=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "7819a0d29d1dd2bc331bec4b327f0776359b1fa6",
"rev": "6c90912761c43e22b6fb000025ab96dd31c971ff",
"type": "github"
},
"original": {
@ -611,11 +581,11 @@
},
"nixpkgs_2": {
"locked": {
"lastModified": 1743827369,
"narHash": "sha256-rpqepOZ8Eo1zg+KJeWoq1HAOgoMCDloqv5r2EAa9TSA=",
"lastModified": 1734649271,
"narHash": "sha256-4EVBRhOjMDuGtMaofAIqzJbg4Ql7Ai0PSeuVZTHjyKQ=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "42a1c966be226125b48c384171c44c651c236c22",
"rev": "d70bd19e0a38ad4790d3913bf08fcbfc9eeca507",
"type": "github"
},
"original": {
@ -640,11 +610,11 @@
"treefmt-nix": "treefmt-nix"
},
"locked": {
"lastModified": 1743856924,
"narHash": "sha256-CgCbUGd9y639PfcuzA0TrA6O5N1ICl+mB95+qTG52+E=",
"lastModified": 1734784342,
"narHash": "sha256-uap4LcvjpTz5WTgDfQYtL3QCpGmtee7DuD5mB8AIiLw=",
"owner": "nix-community",
"repo": "nixvim",
"rev": "d209a04d349febe85c777078ca2eeea5e8bbc8a1",
"rev": "334947672f1eb05488e69657b9c412230bd658b4",
"type": "github"
},
"original": {
@ -699,11 +669,11 @@
"treefmt-nix": "treefmt-nix_2"
},
"locked": {
"lastModified": 1743941131,
"narHash": "sha256-8nTzPXoAQConOnOqG2ZxB8nuUvrGkPuNuqVao9ZNXcI=",
"lastModified": 1735130532,
"narHash": "sha256-efntkb+ydFSI2kvLn6SURQEp4KnThRGZ2eeJHiKL93o=",
"owner": "nix-community",
"repo": "NUR",
"rev": "aa39cbcf5d0bec9b2b3205ae82baa7aac4d6042c",
"rev": "b9f4b07220fa430240dba1825cdac9d673dedf55",
"type": "github"
},
"original": {
@ -722,11 +692,11 @@
]
},
"locked": {
"lastModified": 1743683223,
"narHash": "sha256-LdXtHFvhEC3S64dphap1pkkzwjErbW65eH1VRerCUT0=",
"lastModified": 1733773348,
"narHash": "sha256-Y47y+LesOCkJaLvj+dI/Oa6FAKj/T9sKVKDXLNsViPw=",
"owner": "NuschtOS",
"repo": "search",
"rev": "56a49ffef2908dad1e9a8adef1f18802bc760962",
"rev": "3051be7f403bff1d1d380e4612f0c70675b44fc9",
"type": "github"
},
"original": {
@ -740,13 +710,14 @@
"disko": "disko",
"flake-utils": "flake-utils",
"home-manager": "home-manager",
"jjuinixpkgs": "jjuinixpkgs",
"labellenixpkgs": "labellenixpkgs",
"nix-on-droid": "nix-on-droid",
"nixos-hardware": "nixos-hardware",
"nixpkgs": "nixpkgs",
"nixvim": "nixvim",
"nur": "nur",
"stylix": "stylix",
"unixpkgs": "unixpkgs"
"stylix": "stylix"
}
},
"scss-reset": {
@ -771,10 +742,8 @@
"base16-fish": "base16-fish",
"base16-helix": "base16-helix",
"base16-vim": "base16-vim",
"firefox-gnome-theme": "firefox-gnome-theme",
"flake-compat": "flake-compat_2",
"flake-utils": "flake-utils_3",
"git-hooks": "git-hooks_2",
"gnome-shell": "gnome-shell",
"home-manager": "home-manager_3",
"nixpkgs": [
@ -786,11 +755,11 @@
"tinted-tmux": "tinted-tmux"
},
"locked": {
"lastModified": 1743892916,
"narHash": "sha256-RWvfosAHobUiGMhWIS915WF4TsrQYDXv1gJk59TLAdU=",
"lastModified": 1734110444,
"narHash": "sha256-fp1iV2JldCSvz+7ODzXYUkQ+H7zyiWw5E0MQ4ILC4vw=",
"owner": "danth",
"repo": "stylix",
"rev": "aebfec1998ebbc087de0104e4a4cec99ec1e3f7a",
"rev": "9015d5d0d5d100f849129c43d257b827d300b089",
"type": "github"
},
"original": {
@ -882,11 +851,11 @@
"tinted-tmux": {
"flake": false,
"locked": {
"lastModified": 1743296873,
"narHash": "sha256-8IQulrb1OBSxMwdKijO9fB70ON//V32dpK9Uioy7FzY=",
"lastModified": 1729501581,
"narHash": "sha256-1ohEFMC23elnl39kxWnjzH1l2DFWWx4DhFNNYDTYt54=",
"owner": "tinted-theming",
"repo": "tinted-tmux",
"rev": "af5152c8d7546dfb4ff6df94080bf5ff54f64e3a",
"rev": "f0e7f7974a6441033eb0a172a0342e96722b4f14",
"type": "github"
},
"original": {
@ -903,11 +872,11 @@
]
},
"locked": {
"lastModified": 1743748085,
"narHash": "sha256-uhjnlaVTWo5iD3LXics1rp9gaKgDRQj6660+gbUU3cE=",
"lastModified": 1734704479,
"narHash": "sha256-MMi74+WckoyEWBRcg/oaGRvXC9BVVxDZNRMpL+72wBI=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "815e4121d6a5d504c0f96e5be2dd7f871e4fd99d",
"rev": "65712f5af67234dad91a5a4baee986a8b62dbf8f",
"type": "github"
},
"original": {
@ -936,21 +905,6 @@
"repo": "treefmt-nix",
"type": "github"
}
},
"unixpkgs": {
"locked": {
"lastModified": 1743942655,
"narHash": "sha256-EtFQJXP5L2S8IgGx/AsCACYdzwf0sGiriyxN1BH2f6Q=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "6e0ed24a84eeaf3f5b94bd35e2c2d2de5ea8fede",
"type": "github"
},
"original": {
"id": "nixpkgs",
"ref": "master",
"type": "indirect"
}
}
},
"root": "root",

View file

@ -4,7 +4,8 @@
inputs = {
# Packages
nixpkgs.url = "nixpkgs/nixos-24.11";
unixpkgs.url = "nixpkgs/master";
jjuinixpkgs.url = "github:Adda0/nixpkgs/jjui"; # Testing PR
labellenixpkgs.url = "github:FabianRig/nixpkgs/update-labelle-1.3.2"; # Current 24.11 version doesn't match dependencies
# OS
disko = {
url = "disko";
@ -39,7 +40,6 @@
{
self,
nixpkgs,
unixpkgs,
disko,
nix-on-droid,
flake-utils,
@ -56,19 +56,6 @@
overlays = [
(import ./common/update-local-flakes/overlay.nix)
nur.overlays.default
(
# Cherry-pick packages from future
self: super:
let
upkgs = import unixpkgs { inherit (super) system; };
in
{
jjui = upkgs.jjui;
labelle = upkgs.labelle;
orca-slicer = upkgs.orca-slicer; # Not prebuilt in 24.11 for some reason
nextcloud-client = upkgs.nextcloud-client; # Need https://github.com/nextcloud/desktop/pull/7714
}
)
];
};
homeManagerConfig = {
@ -83,11 +70,7 @@
}:
nixpkgs.lib.nixosSystem {
inherit system;
specialArgs = attrs // {
upkgs = import unixpkgs {
inherit system;
};
};
specialArgs = attrs;
modules = modules ++ [
self.nixosModules.dotfiles
# nur.modules.nixos.default
@ -133,7 +116,6 @@
# We don't do an overlay for the whole system because lix is not binary compatible.
overlays = [
(self: super: { nix = super.lix; })
(import ./common/update-local-flakes/overlay.nix)
];
}
);
@ -171,7 +153,6 @@
runtimeInputs = with pkgs; [
nix-output-monitor
nixos-rebuild
jq
];
text = builtins.readFile ./os/rebuild.sh;
}
@ -212,9 +193,9 @@
};
nixOnDroidConfigurations.sprinkles = lib.nixOnDroidConfiguration { };
# Fake systems
nixosConfigurations.morton = lib.nixosSystem {
nixosConfigurations.abavorana = lib.nixosSystem {
system = "x86_64-linux";
modules = [ ./morton/standin.nix ];
modules = [ ./abavorana/standin.nix ];
};
nixosConfigurations.sprinkles = lib.nixosSystem {
system = "aarch64-linux";

View file

@ -1,4 +1,5 @@
{
pkgs,
config,
lib,
...
@ -27,8 +28,7 @@ let
"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}.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;
@ -79,10 +79,6 @@ in
};
};
};
imports = [
./netrc.nix
./nextcloud.nix
];
# UPST Thunderbird-specific options (should be named so), to be included in HM Thunderbird module
options = {
frogeye.accounts.calendar.accounts = lib.mkOption {

View file

@ -1,83 +0,0 @@
{
lib,
config,
...
}:
# Does not implement everything .netrc allows
# (starting with: changing the .netrc position)
# but neither do clients anyways.
let
cfg = config.frogeye.netrc;
in
{
config = {
frogeye.passwordFiles = [
{
path = "${config.home.homeDirectory}/.netrc";
text = lib.trivial.pipe cfg [
builtins.attrValues
(builtins.map (
n: "machine ${n.machine} login @${n.machine}_login@ password @${n.machine}_password@"
))
lib.strings.concatLines
];
passwords = lib.trivial.pipe cfg [
builtins.attrValues
(builtins.map (n: [
{
name = "@${n.machine}_login@";
value = {
inherit (n) path;
selector = n.login;
};
}
{
name = "@${n.machine}_password@";
value = {
inherit (n) path;
selector = n.password;
};
}
]))
lib.lists.flatten
builtins.listToAttrs
];
}
];
};
options = {
frogeye.netrc = lib.mkOption {
default = { };
description = "Entries to add to .netrc";
type = lib.types.attrsOf (
lib.types.submodule (
{ config, name, ... }:
{
options = {
machine = lib.mkOption {
type = lib.types.str;
default = name;
readOnly = true;
internal = true;
};
login = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = "email";
description = "Password selector that will be used as the login field";
};
password = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = "Password selector that will be used as the password field";
};
path = lib.mkOption {
type = lib.types.str;
description = "Path to the password store entry";
};
};
}
)
);
};
};
}

View file

@ -1,109 +0,0 @@
{
pkgs,
lib,
config,
...
}:
let
cfg = config.frogeye.nextcloud;
in
{
config = {
systemd.user = lib.mkIf (builtins.length cfg > 0) {
services.nextcloud_sync = {
Service = {
Type = "oneshot";
ExecStart = lib.getExe (
pkgs.writeShellApplication {
name = "nextcloud_sync";
runtimeInputs = with pkgs; [
nextcloud-client
];
text =
''
sync() {
url="$1"
remote="$2"
local="$3"
echo "Syncing $url $remote to $local"
# Adding to Syncthing ignores (so it's not double-synced)
dir="$local"
while [ "$dir" != "/" ]
do
if [ -d "$dir/.stfolder" ]
then
if [ ! -f "$dir/.stignore" ]
then
touch "$dir/.stignore"
fi
rule="''${local#"$dir/"}/**"
if ! grep -qFx "$rule" "$dir/.stignore"
then
echo "$rule" >> "$dir/.stignore"
fi
fi
dir="$(dirname "$dir")"
done
# Actual syncing
mkdir -p "$local"
nextcloudcmd -n -h --path "$remote" "$local" "$url"
}
''
+ (lib.trivial.pipe cfg [
(builtins.map (n: ''
sync ${
lib.strings.escapeShellArgs [
n.url
n.remote
"${config.home.homeDirectory}/${n.local}"
]
}
''))
lib.strings.concatLines
]);
}
);
};
};
timers.nextcloud_sync = {
Timer = {
OnCalendar = "*:1/5";
};
Install = {
WantedBy = [ "timers.target" ];
};
};
};
};
options = {
frogeye.nextcloud = lib.mkOption {
default = [ ];
description = "Sync Nextcloud folders. Uses netrc for authentication";
type = lib.types.listOf (
lib.types.submodule (
{ config, ... }:
{
options = {
url = lib.mkOption {
type = lib.types.str;
description = "URL of the Nextcloud instance";
};
local = lib.mkOption {
type = lib.types.str;
description = "Local path (relative to home) to sync";
};
remote = lib.mkOption {
type = lib.types.str;
description = "Remote path to sync";
default = "/";
};
};
}
)
);
};
};
}

View file

@ -2,8 +2,12 @@
pkgs,
config,
lib,
labellenixpkgs,
...
}:
let
labellepkgs = import labellenixpkgs { inherit (pkgs) system; };
in
{
frogeye.hooks.lock = ''
${pkgs.coreutils}/bin/rm -rf "/tmp/cached_pass_$UID"
@ -175,10 +179,19 @@
translate-shell.enable = true; # TODO Cool config?
};
home = {
stateVersion = "24.11";
activation = {
# Prevent Virtualbox from creating a "VirtualBox VMs" folder in $HOME
setVirtualboxSettings = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
if command -v VBoxManage > /dev/null
then
VBoxManage setproperty machinefolder ${config.xdg.dataHome}/virtualbox
fi
'';
};
stateVersion = "24.05";
packages = with pkgs; [
# Terminal utils
coreutils-full
coreutils
moreutils
rename
which
@ -220,14 +233,13 @@
# toolbox
imagemagick
numbat
bc
# hardware
pciutils
usbutils
dmidecode
lshw
labelle # Label printer
labellepkgs.labelle # Label printer
# Locker
(pkgs.writeShellApplication {

View file

@ -1,6 +1,7 @@
{
pkgs,
config,
lib,
...
}:
{

View file

@ -5,9 +5,6 @@
...
}:
{
imports = [
./homepage.nix
];
config = lib.mkIf config.frogeye.desktop.xorg {
home.sessionVariables = {
BROWSER = "qutebrowser";
@ -24,6 +21,7 @@
profiles.hm = {
extensions = with pkgs.nur.repos.rycee.firefox-addons; [
(buildFirefoxXpiAddon {
pname = "onetab";
version = "0.1.0";
addonId = "onetab@nated";
@ -75,6 +73,7 @@
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
@ -147,13 +146,17 @@
show = "never";
tabs_are_windows = true;
};
url.open_base_url = 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 = "en-GB,en;q=0.9";
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
};
@ -184,8 +187,7 @@
};
};
xsession.windowManager.i3.config.keybindings = {
"${config.xsession.windowManager.i3.config.modifier}+m" =
"exec ${config.programs.qutebrowser.package}/bin/qutebrowser --override-restore";
"${config.xsession.windowManager.i3.config.modifier}+m" = "exec ${config.programs.qutebrowser.package}/bin/qutebrowser --override-restore";
};
};
}

View file

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

View file

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

View file

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

View file

@ -113,12 +113,19 @@
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 = {
@ -138,7 +145,6 @@
};
};
};
keynav.enable = true;
};
home = {
@ -172,14 +178,13 @@
zathura
meld
python3Packages.magic
bluetuith
# x11-exclusive
simplescreenrecorder
trayer
xclip
keynav
xorg.xinit
scrot
];
sessionVariables = {
# XAUTHORITY = "${config.xdg.configHome}/Xauthority"; # Disabled as this causes lock-ups with DMs

File diff suppressed because it is too large Load diff

View file

@ -22,13 +22,17 @@ in
# is called pyton-mpd2 on PyPi but mpd2 in nixpkgs.
pkgs.python3Packages.buildPythonApplication rec {
pname = "frobar";
version = "3.0";
version = "2.0";
propagatedBuildInputs = with pkgs.python3Packages; [
coloredlogs # old only
i3ipc
mpd2
notmuch
psutil
pulsectl # old only
pulsectl-asyncio
pygobject3
pyinotify
rich
];
nativeBuildInputs =
@ -37,15 +41,7 @@ pkgs.python3Packages.buildPythonApplication rec {
wirelesstools
playerctl
]);
makeWrapperArgs = [
"--prefix PATH : ${pkgs.lib.makeBinPath nativeBuildInputs}"
"--prefix GI_TYPELIB_PATH : ${GI_TYPELIB_PATH}"
];
GI_TYPELIB_PATH = pkgs.lib.makeSearchPath "lib/girepository-1.0" [
pkgs.glib.out
pkgs.playerctl
];
makeWrapperArgs = [ "--prefix PATH : ${pkgs.lib.makeBinPath nativeBuildInputs}" ];
src = ./.;
}

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

@ -24,7 +24,7 @@ let
in
{
config = lib.mkIf config.frogeye.desktop.xorg {
home.packages = [
home.packages = with pkgs; [
(pkgs.writeShellApplication {
name = "xlock";
text = ''

View file

@ -3,6 +3,7 @@
# Config mentions pdfpc, although the last thing I used was Impressive, even made patches to it.
# UPST Add Impressive to nixpkgs
{
pkgs,
lib,
config,
...

View file

@ -1,35 +0,0 @@
{
pkgs,
lib,
config,
...
}:
{
config = lib.mkIf config.frogeye.dev."3d" {
home = {
packages = with pkgs; [
blender
openscad-unstable # 2022+ parses files much faster
orca-slicer
];
};
programs.nixvim.plugins = {
# openscad.enable = true; # Doesn't do anything besides annoying popups
lsp.servers.openscad_lsp.enable = true;
};
xdg.dataFile = {
"OpenSCAD/libraries/BOSL2".source = pkgs.fetchFromGitHub {
owner = "BelfrySCAD";
repo = "BOSL2";
rev = "ff7e8b8611022b1ce58ea2a1e076028d3d7d40ff"; # no tags, no release
hash = "sha256-esKgXyKLudDfWT2pxOjltZsQ9N6Whlf4zhxd071COzQ=";
};
"OpenSCAD/libraries/MCAD".source = pkgs.fetchFromGitHub {
owner = "openscad";
repo = "MCAD";
rev = "bd0a7ba3f042bfbced5ca1894b236cea08904e26"; # no tags, no release
hash = "sha256-rnrapCe5BkdibbCYVyGZi0l1/8DZxoDnulK37fwZbqo=";
};
};
};
}

View file

@ -37,12 +37,33 @@
# 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) [

View file

@ -1,9 +1,6 @@
{
...
}:
{ pkgs, config, ... }:
{
imports = [
./3d.nix
./c.nix
./common.nix
./go.nix

View file

@ -1,5 +1,6 @@
# Untested post-nix
{
pkgs,
lib,
config,
...

View file

@ -17,7 +17,6 @@
sub-langs = "en,fr";
write-auto-subs = true;
write-subs = true;
mark-watched = true; # Give something to creators, maybe
};
};
};
@ -40,24 +39,41 @@
# documents
visidata
# texlive.combined.scheme-full
# TODO Convert existing LaTeX documents into using Nix build system
# texlive is big and not that much used, sooo
pdftk
pdfgrep
# Misc
haskellPackages.dice
rustdesk-flutter
]
++ lib.optionals config.frogeye.desktop.xorg [
# multimedia editors
darktable
puddletag
audacity
xournalpp
krita
# downloading
transmission_4-qt
# wine only makes sense on x86_64
]
++ lib.optionals pkgs.stdenv.isx86_64 [
wine
# TODO wine-gecko wine-mono lib32-libpulse (?)
# Misc
rustdesk-flutter
]
++ lib.optionals (!stdenv.isAarch64) [
# Musescore is broken on aarch64
musescore
# Blender 4.0.1 can't compile on aarch64
# https://hydra.nixos.org/job/nixos/release-23.11/nixpkgs.blender.aarch64-linux
blender
]
);
};

View file

@ -2,15 +2,17 @@
pkgs,
lib,
config,
jjuinixpkgs,
...
}:
let
cfg = config.programs.git;
jjuipkgs = import jjuinixpkgs { inherit (pkgs) system; };
in
{
config = lib.mkIf cfg.enable {
home.packages = [
pkgs.jjui
jjuipkgs.jjui
pkgs.lazyjj
(pkgs.writeShellApplication {
name = "git-sync";
@ -105,7 +107,7 @@ in
diff-editor = "meld-3";
merge-editor = "meld";
};
signing = lib.mkIf (!builtins.isNull cfg.signing) {
signing = {
sign-all = true;
backend = "gpg";
inherit (cfg.signing) key;

View file

@ -33,7 +33,6 @@
};
publicKeys = [
{
# Always install my own public key
source = builtins.fetchurl {
url = "https://keys.openpgp.org/vks/v1/by-fingerprint/4FBA930D314A03215E2CDB0A8312C8CAC1BAC289";
sha256 = "sha256:10y9xqcy1vyk2p8baay14p3vwdnlwynk0fvfbika65hz2z8yw2cm";
@ -45,14 +44,12 @@
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.
pinentryPackage = pkgs.pinentry-gnome3;
# If inactive, the key will be forgotten after this time
defaultCacheTtl = 3600;
defaultCacheTtlSsh = defaultCacheTtl;
# If active, the key will be forgotten adfter this time
maxCacheTtl = 3 * 3600;
maxCacheTtlSsh = maxCacheTtl;
};

View file

@ -61,6 +61,6 @@
'';
type = lib.types.listOf lib.types.str;
};
# TODO Should make a nix package wrapper instead, so it also works from rofi
# TODO Should make a nix package wrapper instead, so it also works from dmenu
};
}

View file

@ -1,5 +1,6 @@
{
pkgs,
lib,
config,
...
}:

View file

@ -1,6 +1,7 @@
{
pkgs,
lib,
config,
...
}:
{

View file

@ -1,18 +1,20 @@
{
pkgs,
lib,
config,
...
}:
{
config = {
config = lib.mkIf config.programs.less.enable {
programs.powerline-go = {
enable = true;
modules = [
"user"
"host"
"direnv"
"venv"
"cwd"
"perms"
"nix-shell"
"venv"
"git"
];
modulesRight = [

View file

@ -58,4 +58,15 @@ then
fi
ssh -t "$@" "$(cat "${CACHE_DIR}/cmd")"
# To keep until https://github.com/openssh/openssh-portable/commit/f64f8c00d158acc1359b8a096835849b23aa2e86
# is merged
function _ssh {
if [ "${TERM}" = "alacritty" ]
then
TERM=xterm-256color ssh "$@"
else
ssh "$@"
fi
}
alias ssh='_ssh'
_ssh -t "$@" "$(cat "${CACHE_DIR}/cmd")"

75
hm/scripts/replayGain Executable file
View file

@ -0,0 +1,75 @@
#!/usr/bin/env nix-shell
#! nix-shell -i python3 --pure
#! nix-shell -p python3 python3Packages.coloredlogs r128gain
# TODO r128gain is not maintainted anymore
# rsgain replaces it, does the same job as I do with albums
# Normalisation is done at the default of each program,
# which is usually -89.0 dB
# TODO The simplifications/fixes I've done makes it consider
# multi-discs albums as multiple albums
import logging
import os
import sys
import typing
import coloredlogs
import r128gain
coloredlogs.install(level="DEBUG", fmt="%(levelname)s %(message)s")
log = logging.getLogger()
# TODO Remove debug
# Constants
FORCE = "-f" in sys.argv
if FORCE:
sys.argv.remove("-f")
if len(sys.argv) >= 2:
SOURCE_FOLDER = os.path.realpath(sys.argv[1])
else:
SOURCE_FOLDER = os.path.join(os.path.expanduser("~"), "Musiques")
def isMusic(f: str) -> bool:
ext = os.path.splitext(f)[1][1:].lower()
return ext in r128gain.AUDIO_EXTENSIONS
# Get album paths
log.info("Listing albums and tracks")
albums = list()
singleFiles = list()
for root, dirs, files in os.walk(SOURCE_FOLDER):
folder_has_music = False
for f in files:
if isMusic(f):
folder_has_music = True
fullPath = os.path.join(root, f)
singleFiles.append(fullPath)
if folder_has_music:
albums.append(root)
# log.info("Processing single files")
# r128gain.process(singleFiles, album_gain=False,
# skip_tagged=not FORCE, report=True)
for album in albums:
albumName = os.path.relpath(album, SOURCE_FOLDER)
log.info("Processing album {}".format(albumName))
musicFiles = list()
for f in os.listdir(album):
if isMusic(f):
fullPath = os.path.join(album, f)
musicFiles.append(fullPath)
if not musicFiles:
continue
r128gain.process(musicFiles, album_gain=True, skip_tagged=not FORCE, report=True)
print("==============================")

0
hm/scripts/updateCompressedMusic Normal file → Executable file
View file

View file

@ -1,4 +1,7 @@
{
pkgs,
lib,
config,
...
}:
{

View file

@ -1,5 +1,6 @@
{
pkgs,
lib,
config,
...
}:
@ -39,23 +40,10 @@ in
expireDuplicatesFirst = true;
};
shellAliases = cfg.shellAliases;
plugins = [
{
name = "zsh-nix-shell";
file = "nix-shell.plugin.zsh";
src = pkgs.fetchFromGitHub {
owner = "chisui";
repo = "zsh-nix-shell";
rev = "v0.8.0";
sha256 = "1lzrn0n4fxfcgg65v0qhnj7wnybybqzs4adz7xsrkgmcsr0ii8b7";
};
}
];
};
};
};
imports = [
./atuin.nix
./direnv.nix
];
}

View file

@ -1,17 +0,0 @@
# Allow switching to a specific environment when going into a directory
{
...
}:
{
config = {
programs = {
direnv = {
enable = true;
enableBashIntegration = true;
enableZshIntegration = true;
nix-direnv.enable = true;
};
git.ignores = [ ".envrc" ".direnv" ];
};
};
}

View file

@ -1,5 +1,7 @@
{
pkgs,
lib,
config,
...
}:
{
@ -10,7 +12,7 @@
programs.ssh = {
enable = true;
controlMaster = "auto";
controlPersist = "60s"; # Enough to cache Ansible stuff, not too long so I don't have remember which shenanigans I did with my last connection
controlPersist = "60s"; # TODO Default is 10minutes... makes more sense no?
# Ping the server frequently enough so it doesn't think we left (non-spoofable)
serverAliveInterval = 30;
matchBlocks."*" = {
@ -18,7 +20,8 @@
# as it is kinda a security concern
forwardAgent = false;
# Restrict terminal features (servers don't necessarily have the terminfo for my cutting edge terminal)
setEnv.TERM = "xterm-256color";
sendEnv = [ "!TERM" ];
# TODO Why not TERM=xterm-256color?
extraOptions = {
# Check SSHFP records
VerifyHostKeyDNS = "yes";

View file

@ -1,6 +1,7 @@
{
pkgs,
config,
lib,
stylix,
...
}:

View file

@ -1,5 +1,7 @@
{
pkgs,
lib,
config,
...
}:
let
@ -30,7 +32,7 @@ in
{
config = {
programs.nixvim = {
extraPlugins = [
extraPlugins = with pkgs.vimPlugins; [
# f/F mode
vim-shot-f # Highlight relevant characters for f/F/t/T modes
quick-scope # Highlight relevant characters for f/F modes one per word but always

View file

@ -1,5 +1,7 @@
{
pkgs,
lib,
config,
...
}:
{

View file

@ -1,4 +1,7 @@
{
pkgs,
lib,
config,
...
}:
{

View file

@ -1,5 +1,7 @@
{
pkgs,
lib,
config,
...
}:
{

View file

@ -15,7 +15,7 @@
};
};
};
system.stateVersion = "24.11";
system.stateVersion = "24.05";
terminal.font = "${
pkgs.nerdfonts.override {
fonts = [ "DejaVuSansMono" ];

View file

@ -64,10 +64,10 @@ in
};
};
dev = {
"3d" = lib.mkEnableOption "3D (printing) / CAD stuff";
ansible = lib.mkEnableOption "Ansible dev stuff";
c = lib.mkEnableOption "C/C++ dev stuff";
docker = lib.mkEnableOption "Docker dev stuff";
fpga = lib.mkEnableOption "FPGA dev stuff";
go = lib.mkEnableOption "Go dev stuff";
node = lib.mkEnableOption "NodeJS dev stuff";
perl = lib.mkEnableOption "Perl dev stuff";

View file

@ -1,4 +1,6 @@
{
pkgs,
lib,
config,
...
}:

View file

@ -1,5 +1,6 @@
{
pkgs,
lib,
config,
...
}:
@ -43,21 +44,15 @@
];
i18n = {
defaultLocale = "nl_NL.UTF-8";
defaultLocale = "en_GB.UTF-8";
extraLocaleSettings = {
LC_TIME = "en_DK.UTF-8"; # Use YYYY-MM-DD, Thunderbird & qutebrowser use own settings
LC_TIME = "en_DK.UTF-8"; # Should give YYYY-MM-DD but doesn't work for browser & Thunderbird :(
};
};
nix = {
gc = {
automatic = true;
persistent = true;
options = "--delete-older-than 14d";
};
package = pkgs.lix;
settings = {
auto-optimise-store = true;
experimental-features = [
"nix-command"
"flakes"
@ -79,12 +74,15 @@
};
services = {
# More aggressive OoM killer so we never hang
earlyoom.enable = true;
# Enable the OpenSSH daemon
openssh.enable = true;
# Time sychronisation
chrony = {
enable = true;
servers = map (n: "${toString n}.europe.pool.ntp.org") (lib.lists.range 0 3);
};
# Prevent power button from shutting down the computer.
# On Pinebook it's too easy to hit,
# on others I sometimes turn it off when unsuspending.
@ -100,6 +98,6 @@
# TODO Hibernation?
# Use defaults from
system.stateVersion = "24.11";
system.stateVersion = "24.05";
}

View file

@ -1,5 +1,6 @@
# Need nvidia proprietary drivers to work
{
pkgs,
nixpkgs,
config,
lib,

View file

@ -6,12 +6,11 @@
./battery.nix
./boot
./ccc
./common.nix
./cuda
./common.nix
./desktop
./dev
disko.nixosModules.disko
./dns
./gaming
./geoffrey.nix
./password
@ -19,7 +18,6 @@
./remote-builds
./style
./syncthing
./time
./wireless
];
}

View file

@ -4,26 +4,25 @@
config,
...
}:
let
setupScript = "${
pkgs.writeShellApplication {
name = "greeter-setup-script";
runtimeInputs = [ pkgs.autorandr ];
text = ''
autorandr --change
'';
}
}/bin/greeter-setup-script";
in
{
config = lib.mkIf (builtins.length config.frogeye.desktop.x11_screens > 1) {
config = {
services = {
autorandr.enable = true;
xserver.displayManager.lightdm.extraConfig =
let
setupScript = "${
pkgs.writeShellApplication {
name = "greeter-setup-script";
runtimeInputs = [ pkgs.autorandr ];
text = ''
autorandr --change
'';
}
}/bin/greeter-setup-script";
in
''
[Seat:*]
display-setup-script = ${setupScript}
'';
xserver.displayManager.lightdm.extraConfig = ''
[Seat:*]
display-setup-script = ${setupScript}
'';
};
};
}

View file

@ -1,13 +0,0 @@
{
...
}:
{
config = {
services.dnsmasq = {
# We want to be able to have two VPNs active at once.
# Not an issue for routing, but we need local DNS with conditional forwarding.
enable = true;
resolveLocalQueries = true;
};
};
}

View file

@ -1,12 +1,10 @@
{ lib, config, ... }:
{
config = lib.mkIf config.services.printing.enable {
hardware.sane.enable = true;
services.avahi = {
enable = true;
nssmdns4 = true;
openFirewall = true;
};
users.users.geoffrey.extraGroups = [ "lp" "scanner" ];
};
}

View file

@ -4,78 +4,42 @@ verb="$2"
shift
shift
# Helpers
error() {
echo -e '\033[1;31m'"$*"'\033[0m'
}
warn() {
echo -e '\033[1;33m'"$*"'\033[0m'
}
info() {
echo -e '\033[1;34m'"$*"'\033[0m'
}
debug() {
[ ! -v DEBUG ] || echo -e '\033[1;35m'"$*"'\033[0m'
}
if [ "$verb" != "build" ] && [ "$verb" != "test" ] && [ "$verb" != "boot" ] && [ "$verb" != "switch" ] && [ "$verb" != "confirm" ]
then
error "Action should be one of: build, test, boot, switch, confirm"
echo "Action should be one of: build, test, boot, switch, confirm"
exit 2
fi
info "Evaluating"
# Evaluating can take a lot of memory, and Nix doesn't free it until the program ends,
# which can be limiting on memory-constrained devices. Hence the build step is separate.
# Drawback: it will query info about missing paths twice
# nix eval doesn't use the eval cache, so we do a nix build --dry-run
# Build, looking nice
tmpdir="$(mktemp -d)"
# sudo so the eval cache is shared with nixos-rebuild
json=$(time sudo nix build "$self#nixosConfigurations.$HOSTNAME.config.system.build.toplevel" --dry-run --json )
toplevel=$(echo "$json" | jq '.[0].outputs.out' -r)
derivation=$(echo "$json" | jq '.[0].drvPath' -r)
sudo nom build "$self#nixosConfigurations.$HOSTNAME.config.system.build.toplevel" -o "$tmpdir/toplevel" "$@"
toplevel="$(readlink -f "$tmpdir/toplevel")"
rm -rf "$tmpdir"
info "Building"
sudo nom build "$derivation^*" --no-link "$@"
info "Showing diff"
# Show diff
nvd diff "$(readlink -f /nix/var/nix/profiles/system)" "$toplevel"
info "Figuring current specialisation"
systemLink=
currentSystem="$(readlink -f /run/current-system)"
while read -r system
do
if [ "$(readlink -f "$system")" = "$currentSystem" ]
then
systemLink="$system"
break
fi
done <<< "$(ls -d /nix/var/nix/profiles/system-*-link{,/specialisation/*})"
# Figure out specialisation
specialisationArgs=()
if [ -n "$systemLink" ]
then
specialisation=$(echo "$systemLink" | cut -d/ -f8)
if [ -n "$specialisation" ]
currentSystem="$(readlink -f /run/current-system)"
while read -r specialisation
do
if [ "$(readlink -f "/nix/var/nix/profiles/system/specialisation/$specialisation")" = "$currentSystem" ]
then
specialisationArgs=("--specialisation" "$specialisation")
fi
else
warn "Could not find link for $currentSystem, will switch to non-specialized system"
fi
done <<< "$(ls /nix/var/nix/profiles/system/specialisation)"
# Apply
confirm="n"
if [ "$verb" = "confirm" ]
then
warn "Apply configuration? [y/N]"
echo "Apply configuration? [y/N]"
read -r confirm
fi
if [ "$verb" = "test" ] || [ "$verb" = "switch" ] || [ "$confirm" = "y" ]
then
info "Applying"
"$toplevel/bin/update-password-store"
sudo nixos-rebuild --flake "$self#$HOSTNAME" test "${specialisationArgs[@]}" "$@"
fi
@ -83,11 +47,10 @@ fi
# Set as boot
if [ "$verb" = "confirm" ]
then
warn "Set configuration as boot? [y/N]"
echo "Set configuration as boot? [y/N]"
read -r confirm
fi
if [ "$verb" = "boot" ] || [ "$verb" = "switch" ] || [ "$confirm" = "y" ]
then
info "Setting as boot"
sudo nixos-rebuild --flake "$self#$HOSTNAME" boot "$@"
fi

View file

@ -1,4 +1,5 @@
{
pkgs,
lib,
config,
...
@ -7,55 +8,66 @@ let
vivariumBuilderDefault = {
systems = [
"x86_64-linux"
"aarch64-linux"
];
protocol = "ssh-ng";
sshUser = "nixremote";
# sshKey doesn't work
};
# MANU ssh-keygen -y -f /etc/ssh/ssh_host_ed25519_key
# TODO Proper configuration option instead of pile of defs and hacks
# MANU ssh-keygen -y -f /etc/ssh/ssh_host_ed25519_key | base64 -w0
vivariumBuilders = [
{
hostName = "morton.frogeye.fr";
publicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEetvIp4ZrP+ofXNDypnrLxdU034SBYg7fx9FxClDJA3";
hostName = "abavorana.frogeye.fr";
publicHostKey = "c3NoLWVkMjU1MTkgQUFBQUMzTnphQzFsWkRJMU5URTVBQUFBSU5iNzcrS01tRHI0MVhZdmZITXQvK3NHMkJCSEIzYUl4M045WDNVejhFaUogZ2VvZmZyZXlAY3VyYWNhbwo=";
supportedFeatures = [
"nixos-test"
"benchmark"
"big-parallel"
"kvm"
];
maxJobs = 12; # 8 cores, 16 with hyperthreading, trying not to overload the thing
maxJobs = 8;
}
{
hostName = "ludwig.clowncar.frogeye.fr";
publicHostKey = "c3NoLWVkMjU1MTkgQUFBQUMzTnphQzFsWkRJMU5URTVBQUFBSU41SXZhMzNXeGplN095cHVEUHBSakFNMTlvRUtEVDRiYlpUTm82V1FLZTAgZ2VvZmZyZXlAY3VyYWNhbwo=";
maxJobs = 4;
}
];
# MANU pass vivarium/lemmy/remote-builds/cache | nix key convert-secret-to-public | cat
publicKeys = [
"morton.frogeye.fr:rSjbCZ4mgXkb+ENKI7sk/KIbftlQzCTQA7pWkdfS2r4="
"abavorana.frogeye.fr:rcKZ9gwaIQLcst/vbhbF7meUQD5sveT2QQN4a+Zo1BM="
"ludwig.clowncar.frogeye.fr:jTlN0fCOLU49M3LQw5j/u++Gmwrsv3m9RGs0slSg6r0="
];
in
{
config = {
programs.ssh.knownHosts = lib.trivial.pipe vivariumBuilders [
(builtins.map (builder: {
name = builder.hostName;
value.publicKey = builder.publicKey;
}))
builtins.listToAttrs
];
# Currently using port 22 only because:
# - Morton has to use it for git
# - Hopefully allowed on some firewalls
# - Thought you couldn't set SSH config
# You might be able to set SSH config with porgrams.ssh, although I only tried creating a /root/.ssh/config file
# (which does not work, unless logged in as root. host keys from root are used regardless of the user, though)
system.activationScripts.remote = {
supportsDryActivation = true;
text = ''
mkdir -p /root/.ssh
cat ${
pkgs.writeText "root-ssh-config" (
lib.strings.concatLines (
builtins.map (builder: ''
Host ${builder.hostName}
ControlMaster auto
ControlPath ~/.ssh/master-%r@%n:%p
ControlPersist 60s
'') vivariumBuilders
)
)
} > /root/.ssh/config
'';
};
nix = {
buildMachines = builtins.map (
vivariumBuilder:
lib.attrsets.filterAttrs (k: v: k != "publicKey") (vivariumBuilderDefault // vivariumBuilder)
vivariumBuilder: vivariumBuilderDefault // vivariumBuilder
) vivariumBuilders;
distributedBuilds = true;
distributedBuilds = false;
settings = {
builders-use-substitutes = true;
trusted-public-keys = publicKeys;
substituters = builtins.map (
trusted-substituters = builtins.map (
builder: "${builder.protocol}://${builder.sshUser}@${builder.hostName}"
) config.nix.buildMachines;
};

View file

@ -1,5 +1,6 @@
{
pkgs,
lib,
config,
stylix,
...

View file

@ -24,9 +24,7 @@ let
);
allDevices = nixosDevices;
syncingDevices = builtins.filter (device: device.syncthing.id != null) allDevices;
peerDevices = builtins.filter (
device: device.syncthing.id != config.frogeye.syncthing.id
) syncingDevices;
peerDevices = builtins.filter (device: device.name != config.frogeye.name) syncingDevices;
# Can't use the module's folders enable option, as it still requests things somehow
allFolders = builtins.attrValues config.frogeye.folders;
@ -36,58 +34,12 @@ let
folder: device:
(lib.hasAttrByPath [ folder.name ] device.folders)
&& device.folders.${folder.name}.syncthing.enable;
folderDeviceEntry = folder: device: { deviceID = device.syncthing.id; };
enable = (builtins.length syncedFolders) > 0;
in
{
config = {
# Allow to export configuration to other systems
system.build.syncthingConfig = {
folders = lib.trivial.pipe syncedFolders [
(builtins.map (folder: {
name = folder.name;
value = folder;
}))
builtins.listToAttrs
(lib.attrsets.mapAttrs (
folderName: folder:
(lib.attrsets.filterAttrs (
k: v:
builtins.elem k [
"label"
"path"
"syncthing"
"user"
]
))
folder
))
];
devices = lib.trivial.pipe syncingDevices [
(builtins.map (device: {
name = device.name;
value = device;
}))
builtins.listToAttrs
(lib.attrsets.mapAttrs (
deviceName: device:
{
folders = lib.trivial.pipe device.folders [
(lib.attrsets.filterAttrs (folderName: folder: folder.syncthing.enable))
(lib.attrsets.mapAttrs (folderName: folder: { syncthing.enable = true; }))
];
}
//
(lib.attrsets.filterAttrs (
k: v:
builtins.elem k [
"syncthing"
]
))
device
))
];
};
services.${service} = {
inherit enable;
openDefaultPorts = true;
@ -111,6 +63,8 @@ in
value = {
label = "${capitalizeFirstLetter folder.user} ${folder.label}";
path = "${config.users.users.${folder.user}.home}/${folder.path}";
# Despite further in the code indicating this is possible, it is, actually not
# devices = builtins.map (folderDeviceEntry folder) (builtins.filter (folderShouldSyncWith folder) peerDevices);
devices = builtins.map (device: device.name) (
builtins.filter (folderShouldSyncWith folder) peerDevices
);

View file

@ -1,47 +0,0 @@
{
lib,
config,
...
}:
{
config = {
# Apparently better than reference implementation
services.chrony.enable = true;
networking = {
# Using community provided service
timeServers = map (n: "${toString n}.europe.pool.ntp.org") (lib.lists.range 0 3);
# Only try to sync time when we have internet connection
dhcpcd.runHook = ''
if $if_up
then
/run/wrappers/bin/sudo ${config.services.chrony.package}/bin/chronyc online
elif $if_down
then
/run/wrappers/bin/sudo ${config.services.chrony.package}/bin/chronyc offline
fi
'';
};
# Allow dhcpcd to control chrony
security.sudo.extraRules = [
{
users = [ "dhcpcd" ];
commands =
builtins.map
(arg: {
command = "${config.services.chrony.package}/bin/chronyc ${arg}";
options = [ "NOPASSWD" ];
})
[
"online"
"offline"
];
}
];
systemd.services.dhcpcd.serviceConfig.NoNewPrivileges = false;
};
}

View file

@ -1,6 +1,7 @@
{
pkgs,
lib,
config,
...
}:
let
@ -53,6 +54,15 @@ in
];
# wireless support via wpa_supplicant
networking = {
# Tell the time synchronisation service when we got/lost the connection
dhcpcd.runHook = ''
if $if_up; then
${config.services.chrony.package}/bin/chronyc online
elif $if_down; then
${config.services.chrony.package}/bin/chronyc offline
fi
'';
wireless = {
enable = true;
extraConfig = ''

View file

@ -1,4 +1,6 @@
{
pkgs,
lib,
config,
...
}:

View file

@ -1,4 +1,7 @@
{
pkgs,
lib,
config,
...
}:
{

View file

@ -1,6 +1,7 @@
{
pkgs,
lib,
config,
nixos-hardware,
...
}:

View file

@ -1,4 +1,6 @@
{
pkgs,
lib,
config,
...
}: