Compare commits

..

1 commit

Author SHA1 Message Date
Geoffrey Frogeye d8ae0467c3 Attempt at recoloring the background declaratively 2023-12-24 22:01:58 +01:00
185 changed files with 4373 additions and 7707 deletions

5
.gitignore vendored
View file

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

View file

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

View file

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

8
add_channels.sh Executable file
View file

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

70
build_hm.sh Executable file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,45 +0,0 @@
{ pkgs, lib, config, nixos-hardware, ... }:
{
config = {
boot = {
# From nixos-generate-config
initrd.availableKernelModules = [ "nvme" "xhci_pci" "usb_storage" "sd_mod" "sdhci_pci" ];
kernelModules = [ "kvm-amd" ];
};
# Needed for Wi-Fi
hardware.enableRedistributableFirmware = true;
frogeye.desktop = {
x11_screens = [ "eDP-1" ];
maxVideoHeight = 1080;
phasesCommands = {
jour = ''
echo 0 | sudo tee /sys/class/leds/chromeos::kbd_backlight/brightness &
${pkgs.brightnessctl}/bin/brightnessctl set 30% &
'';
crepuscule = ''
echo 1 | sudo tee /sys/class/leds/chromeos::kbd_backlight/brightness &
${pkgs.brightnessctl}/bin/brightnessctl set 10% &
'';
nuit = ''
echo 10 | sudo tee /sys/class/leds/chromeos::kbd_backlight/brightness &
${pkgs.brightnessctl}/bin/brightnessctl set 0% &
'';
};
};
# Alt key swallowed the Meta one
home-manager.users.geoffrey = { ... }: {
xsession.windowManager.i3.config.modifier = "Mod1";
};
};
imports = [
nixos-hardware.nixosModules.common-cpu-amd
nixos-hardware.nixosModules.common-gpu-amd
nixos-hardware.nixosModules.common-pc-laptop
nixos-hardware.nixosModules.common-pc-ssd
];
}

View file

@ -2,13 +2,13 @@
# MANU Snapper is not able to create the snapshot directory, so you'll need to do this after eventually running the backup script:
# sudo btrfs subvol create /mnt/razmo/$subvolume/.snapshots
let
backup_subvolumes = [ "nixos" "home.rapido" "home.nixos" ];
backup_subvolumes = [ "nixos" "home.rapido" ];
backup_app = pkgs.writeShellApplication {
name = "backup-subvolume";
runtimeInputs = with pkgs; [ coreutils btrfs-progs ];
text = builtins.readFile ./backup.sh;
};
snapper_subvolumes = [ "nixos" "home.rapido" "home.razmo" "home.nixos" ];
snapper_subvolumes = [ "nixos" "home.rapido" "home.razmo" ];
in
{
services =
@ -28,11 +28,11 @@ in
# cleanup hourly snapshots after some time
TIMELINE_CLEANUP = true;
TIMELINE_MIN_AGE = 1800;
TIMELINE_LIMIT_HOURLY = "24";
TIMELINE_LIMIT_DAILY = "31";
TIMELINE_LIMIT_WEEKLY = "8";
TIMELINE_LIMIT_MONTHLY = "0";
TIMELINE_LIMIT_YEARLY = "0";
TIMELINE_LIMIT_HOURLY = 24;
TIMELINE_LIMIT_DAILY = 31;
TIMELINE_LIMIT_WEEKLY = 8;
TIMELINE_LIMIT_MONTHLY = 0;
TIMELINE_LIMIT_YEARLY = 0;
# cleanup empty pre-post-pairs
EMPTY_PRE_POST_CLEANUP = true;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

22
curacao/options.nix Normal file
View file

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

18
curacao/os.nix Normal file
View file

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

View file

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

View file

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

12
curacao_test/hm.nix Normal file
View file

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

2
curacao_usb/dk.nix Normal file
View file

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

22
curacao_usb/os.nix Normal file
View file

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

View file

@ -1,12 +1,10 @@
{ pkgs, lib, config, ... }:
let
passwordFile = "/tmp/dotfiles_${config.frogeye.name}_password";
in
{ id, name, passwordFile ? "/should_not_be_needed_in_this_context", ... }:
{
disko.devices = {
disk = {
"${config.frogeye.name}" = {
"${name}" = {
type = "disk";
device = "/dev/disk/by-id/${id}";
content = {
type = "gpt";
partitions = {
@ -27,7 +25,7 @@ in
size = "100%";
content = {
type = "luks";
name = "${config.frogeye.name}";
name = "${name}";
passwordFile = passwordFile;
settings = {
# Not having SSDs die fast is more important than crypto

55
ensure_nix.sh Executable file
View file

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

View file

@ -1,718 +0,0 @@
{
"nodes": {
"base16": {
"inputs": {
"fromYaml": "fromYaml"
},
"locked": {
"lastModified": 1708890466,
"narHash": "sha256-LlrC09LoPi8OPYOGPXegD72v+//VapgAqhbOFS3i8sc=",
"owner": "SenchoPens",
"repo": "base16.nix",
"rev": "665b3c6748534eb766c777298721cece9453fdae",
"type": "github"
},
"original": {
"owner": "SenchoPens",
"repo": "base16.nix",
"type": "github"
}
},
"base16-fish": {
"flake": false,
"locked": {
"lastModified": 1622559957,
"narHash": "sha256-PebymhVYbL8trDVVXxCvZgc0S5VxI7I1Hv4RMSquTpA=",
"owner": "tomyun",
"repo": "base16-fish",
"rev": "2f6dd973a9075dabccd26f1cded09508180bf5fe",
"type": "github"
},
"original": {
"owner": "tomyun",
"repo": "base16-fish",
"type": "github"
}
},
"base16-foot": {
"flake": false,
"locked": {
"lastModified": 1696725948,
"narHash": "sha256-65bz2bUL/yzZ1c8/GQASnoiGwaF8DczlxJtzik1c0AU=",
"owner": "tinted-theming",
"repo": "base16-foot",
"rev": "eedbcfa30de0a4baa03e99f5e3ceb5535c2755ce",
"type": "github"
},
"original": {
"owner": "tinted-theming",
"repo": "base16-foot",
"type": "github"
}
},
"base16-helix": {
"flake": false,
"locked": {
"lastModified": 1696727917,
"narHash": "sha256-FVrbPk+NtMra0jtlC5oxyNchbm8FosmvXIatkRbYy1g=",
"owner": "tinted-theming",
"repo": "base16-helix",
"rev": "dbe1480d99fe80f08df7970e471fac24c05f2ddb",
"type": "github"
},
"original": {
"owner": "tinted-theming",
"repo": "base16-helix",
"type": "github"
}
},
"base16-kitty": {
"flake": false,
"locked": {
"lastModified": 1665001328,
"narHash": "sha256-aRaizTYPpuWEcvoYE9U+YRX+Wsc8+iG0guQJbvxEdJY=",
"owner": "kdrag0n",
"repo": "base16-kitty",
"rev": "06bb401fa9a0ffb84365905ffbb959ae5bf40805",
"type": "github"
},
"original": {
"owner": "kdrag0n",
"repo": "base16-kitty",
"type": "github"
}
},
"base16-tmux": {
"flake": false,
"locked": {
"lastModified": 1696725902,
"narHash": "sha256-wDPg5elZPcQpu7Df0lI5O8Jv4A3T6jUQIVg63KDU+3Q=",
"owner": "tinted-theming",
"repo": "base16-tmux",
"rev": "c02050bebb60dbb20cb433cd4d8ce668ecc11ba7",
"type": "github"
},
"original": {
"owner": "tinted-theming",
"repo": "base16-tmux",
"type": "github"
}
},
"base16-vim": {
"flake": false,
"locked": {
"lastModified": 1663659192,
"narHash": "sha256-uJvaYYDMXvoo0fhBZUhN8WBXeJ87SRgof6GEK2efFT0=",
"owner": "chriskempson",
"repo": "base16-vim",
"rev": "3be3cd82cd31acfcab9a41bad853d9c68d30478d",
"type": "github"
},
"original": {
"owner": "chriskempson",
"repo": "base16-vim",
"type": "github"
}
},
"devshell": {
"inputs": {
"nixpkgs": [
"nixvim",
"nixpkgs"
]
},
"locked": {
"lastModified": 1728330715,
"narHash": "sha256-xRJ2nPOXb//u1jaBnDP56M7v5ldavjbtR6lfGqSvcKg=",
"owner": "numtide",
"repo": "devshell",
"rev": "dd6b80932022cea34a019e2bb32f6fa9e494dfef",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "devshell",
"type": "github"
}
},
"disko": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1730190761,
"narHash": "sha256-o5m5WzvY6cGIDupuOvjgNSS8AN6yP2iI9MtUC6q/uos=",
"owner": "nix-community",
"repo": "disko",
"rev": "3979285062d6781525cded0f6c4ff92e71376b55",
"type": "github"
},
"original": {
"id": "disko",
"type": "indirect"
}
},
"flake-compat": {
"locked": {
"lastModified": 1696426674,
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
"revCount": 57,
"type": "tarball",
"url": "https://api.flakehub.com/f/pinned/edolstra/flake-compat/1.0.1/018afb31-abd1-7bff-a5e4-cff7e18efb7a/source.tar.gz"
},
"original": {
"type": "tarball",
"url": "https://flakehub.com/f/edolstra/flake-compat/1.tar.gz"
}
},
"flake-compat_2": {
"flake": false,
"locked": {
"lastModified": 1696426674,
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flake-compat_3": {
"flake": false,
"locked": {
"lastModified": 1673956053,
"narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flake-parts": {
"inputs": {
"nixpkgs-lib": [
"nixvim",
"nixpkgs"
]
},
"locked": {
"lastModified": 1727826117,
"narHash": "sha256-K5ZLCyfO/Zj9mPFldf3iwS6oZStJcU4tSpiXTMYaaL0=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "3d04084d54bedc3d6b8b736c70ef449225c361b1",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1726560853,
"narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"fromYaml": {
"flake": false,
"locked": {
"lastModified": 1689549921,
"narHash": "sha256-iX0pk/uB019TdBGlaJEWvBCfydT6sRq+eDcGPifVsCM=",
"owner": "SenchoPens",
"repo": "fromYaml",
"rev": "11fbbbfb32e3289d3c631e0134a23854e7865c84",
"type": "github"
},
"original": {
"owner": "SenchoPens",
"repo": "fromYaml",
"type": "github"
}
},
"git-hooks": {
"inputs": {
"flake-compat": "flake-compat_2",
"gitignore": "gitignore",
"nixpkgs": [
"nixvim",
"nixpkgs"
],
"nixpkgs-stable": [
"nixvim",
"nixpkgs"
]
},
"locked": {
"lastModified": 1729104314,
"narHash": "sha256-pZRZsq5oCdJt3upZIU4aslS9XwFJ+/nVtALHIciX/BI=",
"owner": "cachix",
"repo": "git-hooks.nix",
"rev": "3c3e88f0f544d6bb54329832616af7eb971b6be6",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "git-hooks.nix",
"type": "github"
}
},
"gitignore": {
"inputs": {
"nixpkgs": [
"nixvim",
"git-hooks",
"nixpkgs"
]
},
"locked": {
"lastModified": 1709087332,
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
"owner": "hercules-ci",
"repo": "gitignore.nix",
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "gitignore.nix",
"type": "github"
}
},
"gnome-shell": {
"flake": false,
"locked": {
"lastModified": 1713702291,
"narHash": "sha256-zYP1ehjtcV8fo+c+JFfkAqktZ384Y+y779fzmR9lQAU=",
"owner": "GNOME",
"repo": "gnome-shell",
"rev": "0d0aadf013f78a7f7f1dc984d0d812971864b934",
"type": "github"
},
"original": {
"owner": "GNOME",
"ref": "46.1",
"repo": "gnome-shell",
"type": "github"
}
},
"home-manager": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1726989464,
"narHash": "sha256-Vl+WVTJwutXkimwGprnEtXc/s/s8sMuXzqXaspIGlwM=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "2f23fa308a7c067e52dfcc30a0758f47043ec176",
"type": "github"
},
"original": {
"id": "home-manager",
"ref": "release-24.05",
"type": "indirect"
}
},
"home-manager_2": {
"inputs": {
"nixpkgs": [
"nixvim",
"nixpkgs"
]
},
"locked": {
"lastModified": 1726989464,
"narHash": "sha256-Vl+WVTJwutXkimwGprnEtXc/s/s8sMuXzqXaspIGlwM=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "2f23fa308a7c067e52dfcc30a0758f47043ec176",
"type": "github"
},
"original": {
"owner": "nix-community",
"ref": "release-24.05",
"repo": "home-manager",
"type": "github"
}
},
"home-manager_3": {
"inputs": {
"nixpkgs": [
"stylix",
"nixpkgs"
]
},
"locked": {
"lastModified": 1714981474,
"narHash": "sha256-b3/U21CJjCjJKmA9WqUbZGZgCvospO3ArOUTgJugkOY=",
"owner": "nix-community",
"repo": "home-manager",
"rev": "6ebe7be2e67be7b9b54d61ce5704f6fb466c536f",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "home-manager",
"type": "github"
}
},
"nix-darwin": {
"inputs": {
"nixpkgs": [
"nixvim",
"nixpkgs"
]
},
"locked": {
"lastModified": 1729826725,
"narHash": "sha256-w3WNlYxqWYsuzm/jgFPyhncduoDNjot28aC8j39TW0U=",
"owner": "lnl7",
"repo": "nix-darwin",
"rev": "7840909b00fbd5a183008a6eb251ea307fe4a76e",
"type": "github"
},
"original": {
"owner": "lnl7",
"repo": "nix-darwin",
"type": "github"
}
},
"nix-formatter-pack": {
"inputs": {
"nixpkgs": [
"nix-on-droid",
"nixpkgs"
],
"nmd": [
"nix-on-droid",
"nmd"
],
"nmt": "nmt"
},
"locked": {
"lastModified": 1705252799,
"narHash": "sha256-HgSTREh7VoXjGgNDwKQUYcYo13rPkltW7IitHrTPA5c=",
"owner": "Gerschtli",
"repo": "nix-formatter-pack",
"rev": "2de39dedd79aab14c01b9e2934842051a160ffa5",
"type": "github"
},
"original": {
"owner": "Gerschtli",
"repo": "nix-formatter-pack",
"type": "github"
}
},
"nix-on-droid": {
"inputs": {
"home-manager": [
"home-manager"
],
"nix-formatter-pack": "nix-formatter-pack",
"nixpkgs": [
"nixpkgs"
],
"nixpkgs-docs": "nixpkgs-docs",
"nixpkgs-for-bootstrap": "nixpkgs-for-bootstrap",
"nmd": "nmd"
},
"locked": {
"lastModified": 1725658585,
"narHash": "sha256-P29z4Gt89n5ps1U7+qmIrj0BuRXGZQSIaOe2+tsPgfw=",
"owner": "nix-community",
"repo": "nix-on-droid",
"rev": "5d88ff2519e4952f8d22472b52c531bb5f1635fc",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "nix-on-droid",
"type": "github"
}
},
"nixos-hardware": {
"locked": {
"lastModified": 1730161780,
"narHash": "sha256-z5ILcmwMtiCoHTXS1KsQWqigO7HJO8sbyK7f7wn9F/E=",
"owner": "NixOS",
"repo": "nixos-hardware",
"rev": "07d15e8990d5d86a631641b4c429bc0a7400cfb8",
"type": "github"
},
"original": {
"id": "nixos-hardware",
"type": "indirect"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1730137625,
"narHash": "sha256-9z8oOgFZiaguj+bbi3k4QhAD6JabWrnv7fscC/mt0KE=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "64b80bfb316b57cdb8919a9110ef63393d74382a",
"type": "github"
},
"original": {
"id": "nixpkgs",
"ref": "nixos-24.05",
"type": "indirect"
}
},
"nixpkgs-docs": {
"locked": {
"lastModified": 1705957679,
"narHash": "sha256-Q8LJaVZGJ9wo33wBafvZSzapYsjOaNjP/pOnSiKVGHY=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "9a333eaa80901efe01df07eade2c16d183761fa3",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "release-23.05",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-for-bootstrap": {
"locked": {
"lastModified": 1720244366,
"narHash": "sha256-WrDV0FPMVd2Sq9hkR5LNHudS3OSMmUrs90JUTN+MXpA=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "49ee0e94463abada1de470c9c07bfc12b36dcf40",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "49ee0e94463abada1de470c9c07bfc12b36dcf40",
"type": "github"
}
},
"nixvim": {
"inputs": {
"devshell": "devshell",
"flake-compat": "flake-compat",
"flake-parts": "flake-parts",
"git-hooks": "git-hooks",
"home-manager": "home-manager_2",
"nix-darwin": "nix-darwin",
"nixpkgs": [
"nixpkgs"
],
"treefmt-nix": "treefmt-nix"
},
"locked": {
"lastModified": 1729945968,
"narHash": "sha256-4u+nbBSMuXWGCtXxUPPEflRm54+y/HLIbhIep9do8Ew=",
"owner": "nix-community",
"repo": "nixvim",
"rev": "c05ac01070425ed0797b1ff678dc690c333cea74",
"type": "github"
},
"original": {
"owner": "nix-community",
"ref": "nixos-24.05",
"repo": "nixvim",
"type": "github"
}
},
"nmd": {
"inputs": {
"nixpkgs": [
"nix-on-droid",
"nixpkgs-docs"
],
"scss-reset": "scss-reset"
},
"locked": {
"lastModified": 1705050560,
"narHash": "sha256-x3zzcdvhJpodsmdjqB4t5mkVW22V3wqHLOun0KRBzUI=",
"owner": "~rycee",
"repo": "nmd",
"rev": "66d9334933119c36f91a78d565c152a4fdc8d3d3",
"type": "sourcehut"
},
"original": {
"owner": "~rycee",
"repo": "nmd",
"type": "sourcehut"
}
},
"nmt": {
"flake": false,
"locked": {
"lastModified": 1648075362,
"narHash": "sha256-u36WgzoA84dMVsGXzml4wZ5ckGgfnvS0ryzo/3zn/Pc=",
"owner": "rycee",
"repo": "nmt",
"rev": "d83601002c99b78c89ea80e5e6ba21addcfe12ae",
"type": "gitlab"
},
"original": {
"owner": "rycee",
"repo": "nmt",
"type": "gitlab"
}
},
"nur": {
"locked": {
"lastModified": 1730300129,
"narHash": "sha256-QZm3ZsHn/75VsGg7ScPGfdByqBPFIQHmbpjT37iQp2g=",
"owner": "nix-community",
"repo": "NUR",
"rev": "656dcf946af3e368dd872fe525439518d8423080",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "NUR",
"type": "github"
}
},
"root": {
"inputs": {
"disko": "disko",
"flake-utils": "flake-utils",
"home-manager": "home-manager",
"nix-on-droid": "nix-on-droid",
"nixos-hardware": "nixos-hardware",
"nixpkgs": "nixpkgs",
"nixvim": "nixvim",
"nur": "nur",
"stylix": "stylix",
"unixpkgs": "unixpkgs"
}
},
"scss-reset": {
"flake": false,
"locked": {
"lastModified": 1631450058,
"narHash": "sha256-muDlZJPtXDIGevSEWkicPP0HQ6VtucbkMNygpGlBEUM=",
"owner": "andreymatin",
"repo": "scss-reset",
"rev": "0cf50e27a4e95e9bb5b1715eedf9c54dee1a5a91",
"type": "github"
},
"original": {
"owner": "andreymatin",
"repo": "scss-reset",
"type": "github"
}
},
"stylix": {
"inputs": {
"base16": "base16",
"base16-fish": "base16-fish",
"base16-foot": "base16-foot",
"base16-helix": "base16-helix",
"base16-kitty": "base16-kitty",
"base16-tmux": "base16-tmux",
"base16-vim": "base16-vim",
"flake-compat": "flake-compat_3",
"gnome-shell": "gnome-shell",
"home-manager": "home-manager_3",
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1718122552,
"narHash": "sha256-A+dBkSwp8ssHKV/WyXb9uqIYrHBqHvtSedU24Lq9lqw=",
"owner": "danth",
"repo": "stylix",
"rev": "e59d2c1725b237c362e4a62f5722f5b268d566c7",
"type": "github"
},
"original": {
"owner": "danth",
"ref": "release-24.05",
"repo": "stylix",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
},
"treefmt-nix": {
"inputs": {
"nixpkgs": [
"nixvim",
"nixpkgs"
]
},
"locked": {
"lastModified": 1729613947,
"narHash": "sha256-XGOvuIPW1XRfPgHtGYXd5MAmJzZtOuwlfKDgxX5KT3s=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "aac86347fb5063960eccb19493e0cadcdb4205ca",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "treefmt-nix",
"type": "github"
}
},
"unixpkgs": {
"locked": {
"lastModified": 1730298926,
"narHash": "sha256-ao1BYrrOB8SGdvOul6hGJYqp/QqEJTwZRViRXFvNnTQ=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "838f2f70e0e44d957009bf5a4fc0aa9c931b680e",
"type": "github"
},
"original": {
"id": "nixpkgs",
"ref": "master",
"type": "indirect"
}
}
},
"root": "root",
"version": 7
}

160
flake.nix
View file

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

6
full/README.md Normal file
View file

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

View file

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

10
full/os.nix Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

706
hm/desktop.nix Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,759 +0,0 @@
#!/usr/bin/env python3
import asyncio
import datetime
import enum
import ipaddress
import logging
import random
import signal
import socket
import typing
import coloredlogs
import i3ipc
import i3ipc.aio
import psutil
coloredlogs.install(level="DEBUG", fmt="%(levelname)s %(message)s")
log = logging.getLogger()
T = typing.TypeVar("T", bound="ComposableText")
P = typing.TypeVar("P", bound="ComposableText")
C = typing.TypeVar("C", bound="ComposableText")
Sortable = str | int
def humanSize(numi: int) -> str:
"""
Returns a string of width 3+3
"""
num = float(numi)
for unit in ("B ", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB"):
if abs(num) < 1000:
if num >= 10:
return "{:3d}{}".format(int(num), unit)
else:
return "{:.1f}{}".format(num, unit)
num /= 1024
return "{:d}YiB".format(numi)
class ComposableText(typing.Generic[P, C]):
def __init__(
self,
parent: typing.Optional[P] = None,
sortKey: Sortable = 0,
) -> None:
self.parent: typing.Optional[P] = None
self.children: typing.MutableSequence[C] = list()
self.sortKey = sortKey
if parent:
self.setParent(parent)
self.bar = self.getFirstParentOfType(Bar)
def setParent(self, parent: P) -> None:
assert self.parent is None
parent.children.append(self)
assert isinstance(parent.children, list)
parent.children.sort(key=lambda c: c.sortKey)
self.parent = parent
self.parent.updateMarkup()
def unsetParent(self) -> None:
assert self.parent
self.parent.children.remove(self)
self.parent.updateMarkup()
self.parent = None
def getFirstParentOfType(self, typ: typing.Type[T]) -> T:
parent = self
while not isinstance(parent, typ):
assert parent.parent, f"{self} doesn't have a parent of {typ}"
parent = parent.parent
return parent
def updateMarkup(self) -> None:
self.bar.refresh.set()
# TODO OPTI See if worth caching the output
def generateMarkup(self) -> str:
raise NotImplementedError(f"{self} cannot generate markup")
def getMarkup(self) -> str:
return self.generateMarkup()
def randomColor(seed: int | bytes | None = None) -> str:
if seed is not None:
random.seed(seed)
return "#" + "".join(f"{random.randint(0, 0xff):02x}" for _ in range(3))
class Button(enum.Enum):
CLICK_LEFT = "1"
CLICK_MIDDLE = "2"
CLICK_RIGHT = "3"
SCROLL_UP = "4"
SCROLL_DOWN = "5"
class Section(ComposableText):
"""
Colorable block separated by chevrons
"""
def __init__(self, parent: "Module", sortKey: Sortable = 0) -> None:
super().__init__(parent=parent, sortKey=sortKey)
self.parent: "Module"
self.color = randomColor()
self.desiredText: str | None = None
self.text = ""
self.targetSize = -1
self.size = -1
self.animationTask: asyncio.Task | None = None
self.actions: dict[Button, str] = dict()
def isHidden(self) -> bool:
return self.size < 0
# Geometric series, with a cap
ANIM_A = 0.025
ANIM_R = 0.9
ANIM_MIN = 0.001
async def animate(self) -> None:
increment = 1 if self.size < self.targetSize else -1
loop = asyncio.get_running_loop()
frameTime = loop.time()
animTime = self.ANIM_A
while self.size != self.targetSize:
self.size += increment
self.updateMarkup()
animTime *= self.ANIM_R
animTime = max(self.ANIM_MIN, animTime)
frameTime += animTime
sleepTime = frameTime - loop.time()
# In case of stress, skip refreshing by not awaiting
if sleepTime > 0:
await asyncio.sleep(sleepTime)
else:
log.warning("Skipped an animation frame")
def setText(self, text: str | None) -> None:
# OPTI Don't redraw nor reset animation if setting the same text
if self.desiredText == text:
return
self.desiredText = text
if text is None:
self.text = ""
self.targetSize = -1
else:
self.text = f" {text} "
self.targetSize = len(self.text)
if self.animationTask:
self.animationTask.cancel()
# OPTI Skip the whole animation task if not required
if self.size == self.targetSize:
self.updateMarkup()
else:
self.animationTask = self.bar.taskGroup.create_task(self.animate())
def setAction(self, button: Button, callback: typing.Callable | None) -> None:
if button in self.actions:
command = self.actions[button]
self.bar.removeAction(command)
del self.actions[button]
if callback:
command = self.bar.addAction(callback)
self.actions[button] = command
def generateMarkup(self) -> str:
assert not self.isHidden()
pad = max(0, self.size - len(self.text))
text = self.text[: self.size] + " " * pad
for button, command in self.actions.items():
text = "%{A" + button.value + ":" + command + ":}" + text + "%{A}"
return text
class Module(ComposableText):
"""
Sections handled by a same updater
"""
def __init__(self, parent: "Side") -> None:
super().__init__(parent=parent)
self.parent: "Side"
self.children: typing.MutableSequence[Section]
self.mirroring: Module | None = None
self.mirrors: list[Module] = list()
def mirror(self, module: "Module") -> None:
self.mirroring = module
module.mirrors.append(self)
def getSections(self) -> typing.Sequence[Section]:
if self.mirroring:
return self.mirroring.children
else:
return self.children
def updateMarkup(self) -> None:
super().updateMarkup()
for mirror in self.mirrors:
mirror.updateMarkup()
class Alignment(enum.Enum):
LEFT = "l"
RIGHT = "r"
CENTER = "c"
class Side(ComposableText):
def __init__(self, parent: "Screen", alignment: Alignment) -> None:
super().__init__(parent=parent)
self.parent: Screen
self.children: typing.MutableSequence[Module] = []
self.alignment = alignment
def generateMarkup(self) -> str:
if not self.children:
return ""
text = "%{" + self.alignment.value + "}"
lastSection: Section | None = None
for module in self.children:
for section in module.getSections():
if section.isHidden():
continue
if lastSection is None:
if self.alignment == Alignment.LEFT:
text += "%{B" + section.color + "}%{F-}"
else:
text += "%{B-}%{F" + section.color + "}%{R}%{F-}"
else:
if self.alignment == Alignment.RIGHT:
if lastSection.color == section.color:
text += ""
else:
text += "%{F" + section.color + "}%{R}"
else:
if lastSection.color == section.color:
text += ""
else:
text += "%{R}%{B" + section.color + "}"
text += "%{F-}"
text += section.getMarkup()
lastSection = section
if self.alignment != Alignment.RIGHT:
text += "%{R}%{B-}"
return text
class Screen(ComposableText):
def __init__(self, parent: "Bar", output: str) -> None:
super().__init__(parent=parent)
self.parent: "Bar"
self.children: typing.MutableSequence[Side]
self.output = output
for alignment in Alignment:
Side(parent=self, alignment=alignment)
def generateMarkup(self) -> str:
return ("%{Sn" + self.output + "}") + "".join(
side.getMarkup() for side in self.children
)
class Bar(ComposableText):
"""
Top-level
"""
def __init__(self) -> None:
super().__init__()
self.parent: None
self.children: typing.MutableSequence[Screen]
self.longRunningTasks: list[asyncio.Task] = list()
self.refresh = asyncio.Event()
self.taskGroup = asyncio.TaskGroup()
self.providers: list["Provider"] = list()
self.actionIndex = 0
self.actions: dict[str, typing.Callable] = dict()
self.periodicProviderTask: typing.Coroutine | None = None
i3 = i3ipc.Connection()
for output in i3.get_outputs():
if not output.active:
continue
Screen(parent=self, output=output.name)
def addLongRunningTask(self, coro: typing.Coroutine) -> None:
task = self.taskGroup.create_task(coro)
self.longRunningTasks.append(task)
async def run(self) -> None:
cmd = [
"lemonbar",
"-b",
"-a",
"64",
"-f",
"DejaVuSansM Nerd Font:size=10",
]
proc = await asyncio.create_subprocess_exec(
*cmd, stdout=asyncio.subprocess.PIPE, stdin=asyncio.subprocess.PIPE
)
async def refresher() -> None:
assert proc.stdin
while True:
await self.refresh.wait()
self.refresh.clear()
markup = self.getMarkup()
proc.stdin.write(markup.encode())
async def actionHandler() -> None:
assert proc.stdout
while True:
line = await proc.stdout.readline()
command = line.decode().strip()
callback = self.actions[command]
callback()
async with self.taskGroup:
self.addLongRunningTask(refresher())
self.addLongRunningTask(actionHandler())
for provider in self.providers:
self.addLongRunningTask(provider.run())
def exit() -> None:
log.info("Terminating")
for task in self.longRunningTasks:
task.cancel()
loop = asyncio.get_event_loop()
loop.add_signal_handler(signal.SIGINT, exit)
def generateMarkup(self) -> str:
return "".join(screen.getMarkup() for screen in self.children) + "\n"
def addProvider(
self,
provider: "Provider",
alignment: Alignment = Alignment.LEFT,
screenNum: int | None = None,
) -> None:
"""
screenNum: the provider will be added on this screen if set, all otherwise
"""
modules = list()
for s, screen in enumerate(self.children):
if screenNum is None or s == screenNum:
side = next(filter(lambda s: s.alignment == alignment, screen.children))
module = Module(parent=side)
modules.append(module)
provider.modules = modules
if modules:
self.providers.append(provider)
def addAction(self, callback: typing.Callable) -> str:
command = f"{self.actionIndex:x}"
self.actions[command] = callback
self.actionIndex += 1
return command
def removeAction(self, command: str) -> None:
del self.actions[command]
class Provider:
def __init__(self) -> None:
self.modules: list[Module] = list()
async def run(self) -> None:
# Not a NotImplementedError, otherwise can't combine all classes
pass
class MirrorProvider(Provider):
def __init__(self) -> None:
super().__init__()
self.module: Module
async def run(self) -> None:
await super().run()
self.module = self.modules[0]
for module in self.modules[1:]:
module.mirror(self.module)
class SingleSectionProvider(MirrorProvider):
async def run(self) -> None:
await super().run()
self.section = Section(parent=self.module)
class StaticProvider(SingleSectionProvider):
def __init__(self, text: str) -> None:
self.text = text
async def run(self) -> None:
await super().run()
self.section.setText(self.text)
class StatefulSection(Section):
def __init__(self, parent: Module, sortKey: Sortable = 0) -> None:
super().__init__(parent=parent, sortKey=sortKey)
self.state = 0
self.numberStates: int
self.setAction(Button.CLICK_LEFT, self.incrementState)
self.setAction(Button.CLICK_RIGHT, self.decrementState)
def incrementState(self) -> None:
self.state += 1
self.changeState()
def decrementState(self) -> None:
self.state -= 1
self.changeState()
def setChangedState(self, callback: typing.Callable) -> None:
self.callback = callback
def changeState(self) -> None:
self.state %= self.numberStates
self.bar.taskGroup.create_task(self.callback())
class SingleStatefulSectionProvider(MirrorProvider):
async def run(self) -> None:
await super().run()
self.section = StatefulSection(parent=self.module)
class PeriodicProvider(Provider):
async def init(self) -> None:
pass
async def loop(self) -> None:
raise NotImplementedError()
@classmethod
async def task(cls, bar: Bar) -> None:
providers = list()
for provider in bar.providers:
if isinstance(provider, PeriodicProvider):
providers.append(provider)
await provider.init()
while True:
# TODO Block bar update during the periodic update of the loops
loops = [provider.loop() for provider in providers]
asyncio.gather(*loops)
now = datetime.datetime.now()
# Hardcoded to 1 second... not sure if we want some more than that,
# and if the logic to check if a task should run would be a win
# compared to the task itself
remaining = 1 - now.microsecond / 1000000
await asyncio.sleep(remaining)
async def run(self) -> None:
await super().run()
for module in self.modules:
bar = module.getFirstParentOfType(Bar)
assert bar
if not bar.periodicProviderTask:
bar.periodicProviderTask = PeriodicProvider.task(bar)
bar.addLongRunningTask(bar.periodicProviderTask)
class PeriodicStatefulProvider(SingleStatefulSectionProvider, PeriodicProvider):
async def run(self) -> None:
await super().run()
self.section.setChangedState(self.loop)
# Providers
class I3ModeProvider(SingleSectionProvider):
def on_mode(self, i3: i3ipc.Connection, e: i3ipc.Event) -> None:
self.section.setText(None if e.change == "default" else e.change)
async def run(self) -> None:
await super().run()
i3 = await i3ipc.aio.Connection(auto_reconnect=True).connect()
i3.on(i3ipc.Event.MODE, self.on_mode)
await i3.main()
class I3WindowTitleProvider(SingleSectionProvider):
# TODO FEAT To make this available from start, we need to find the
# `focused=True` element following the `focus` array
def on_window(self, i3: i3ipc.Connection, e: i3ipc.Event) -> None:
self.section.setText(e.container.name)
async def run(self) -> None:
await super().run()
i3 = await i3ipc.aio.Connection(auto_reconnect=True).connect()
i3.on(i3ipc.Event.WINDOW, self.on_window)
await i3.main()
class I3WorkspacesProvider(Provider):
# TODO Custom names
# TODO Colors
async def updateWorkspaces(self, i3: i3ipc.Connection) -> None:
"""
Since the i3 IPC interface cannot really tell you by events
when workspaces get invisible or not urgent anymore.
Relying on those exclusively would require reimplementing some of i3 logic.
Fetching all the workspaces on event looks ugly but is the most maintainable.
Times I tried to challenge this and failed: 2.
"""
workspaces = await i3.get_workspaces()
for workspace in workspaces:
module = self.modulesFromOutput[workspace.output]
if workspace.num in self.sections:
section = self.sections[workspace.num]
if section.parent != module:
section.unsetParent()
section.setParent(module)
else:
section = Section(parent=module, sortKey=workspace.num)
self.sections[workspace.num] = section
def generate_switch_workspace(num: int) -> typing.Callable:
def switch_workspace() -> None:
self.bar.taskGroup.create_task(
i3.command(f"workspace number {num}")
)
return switch_workspace
section.setAction(
Button.CLICK_LEFT, generate_switch_workspace(workspace.num)
)
name = workspace.name
if workspace.urgent:
name = f"{name} !"
elif workspace.focused:
name = f"{name} +"
elif workspace.visible:
name = f"{name} *"
section.setText(name)
workspacesNums = set(workspace.num for workspace in workspaces)
for num, section in self.sections.items():
if num not in workspacesNums:
# This should delete the Section but it turned out to be hard
section.setText(None)
def onWorkspaceChange(
self, i3: i3ipc.Connection, e: i3ipc.Event | None = None
) -> None:
# Cancelling the task doesn't seem to prevent performance double-events
self.bar.taskGroup.create_task(self.updateWorkspaces(i3))
def __init__(
self,
) -> None:
super().__init__()
self.sections: dict[int, Section] = dict()
self.modulesFromOutput: dict[str, Module] = dict()
self.bar: Bar
async def run(self) -> None:
for module in self.modules:
screen = module.getFirstParentOfType(Screen)
output = screen.output
self.modulesFromOutput[output] = module
self.bar = module.bar
i3 = await i3ipc.aio.Connection(auto_reconnect=True).connect()
i3.on(i3ipc.Event.WORKSPACE, self.onWorkspaceChange)
self.onWorkspaceChange(i3)
await i3.main()
class NetworkProviderSection(StatefulSection):
def __init__(self, parent: Module, iface: str, provider: "NetworkProvider") -> None:
super().__init__(parent=parent, sortKey=iface)
self.iface = iface
self.provider = provider
self.ignore = False
self.icon = "?"
self.wifi = False
if iface == "lo":
self.ignore = True
elif iface.startswith("eth") or iface.startswith("enp"):
if "u" in iface:
self.icon = ""
else:
self.icon = ""
elif iface.startswith("wlan") or iface.startswith("wl"):
self.icon = ""
self.wifi = True
elif (
iface.startswith("tun") or iface.startswith("tap") or iface.startswith("wg")
):
self.icon = ""
elif iface.startswith("docker"):
self.icon = ""
elif iface.startswith("veth"):
self.icon = ""
elif iface.startswith("vboxnet"):
self.icon = ""
self.numberStates = 5 if self.wifi else 4
self.state = 1 if self.wifi else 0
self.setChangedState(self.update)
async def update(self) -> None:
if self.ignore or not self.provider.if_stats[self.iface].isup:
self.setText(None)
return
text = self.icon
state = self.state + (0 if self.wifi else 1) # SSID
if self.wifi and state >= 1:
cmd = ["iwgetid", self.iface, "--raw"]
proc = await asyncio.create_subprocess_exec(
*cmd, stdout=asyncio.subprocess.PIPE
)
stdout, stderr = await proc.communicate()
text += f" {stdout.decode().strip()}"
if state >= 2: # Address
for address in self.provider.if_addrs[self.iface]:
if address.family == socket.AF_INET:
net = ipaddress.IPv4Network(
(address.address, address.netmask), strict=False
)
text += f" {net.with_prefixlen}"
break
if state >= 3: # Speed
prevRecv = self.provider.prev_io_counters[self.iface].bytes_recv
recv = self.provider.io_counters[self.iface].bytes_recv
prevSent = self.provider.prev_io_counters[self.iface].bytes_sent
sent = self.provider.io_counters[self.iface].bytes_sent
dt = self.provider.time - self.provider.prev_time
recvDiff = (recv - prevRecv) / dt
sentDiff = (sent - prevSent) / dt
text += f"{humanSize(recvDiff)}{humanSize(sentDiff)}"
if state >= 4: # Counter
text += f"{humanSize(recv)}{humanSize(sent)}"
self.setText(text)
class NetworkProvider(MirrorProvider, PeriodicProvider):
def __init__(self) -> None:
super().__init__()
self.sections: dict[str, NetworkProviderSection] = dict()
async def init(self) -> None:
loop = asyncio.get_running_loop()
self.time = loop.time()
self.io_counters = psutil.net_io_counters(pernic=True)
async def loop(self) -> None:
loop = asyncio.get_running_loop()
async with asyncio.TaskGroup() as tg:
self.prev_io_counters = self.io_counters
self.prev_time = self.time
# On-demand would only benefit if_addrs:
# stats are used to determine display,
# and we want to keep previous io_counters
# so displaying stats is ~instant.
self.time = loop.time()
self.if_stats = psutil.net_if_stats()
self.if_addrs = psutil.net_if_addrs()
self.io_counters = psutil.net_io_counters(pernic=True)
for iface in self.if_stats:
section = self.sections.get(iface)
if not section:
section = NetworkProviderSection(
parent=self.module, iface=iface, provider=self
)
self.sections[iface] = section
tg.create_task(section.update())
for iface, section in self.sections.items():
if iface not in self.if_stats:
section.setText(None)
async def onStateChange(self, section: StatefulSection) -> None:
assert isinstance(section, NetworkProviderSection)
await section.update()
class TimeProvider(PeriodicStatefulProvider):
FORMATS = ["%H:%M", "%m-%d %H:%M:%S", "%a %y-%m-%d %H:%M:%S"]
async def init(self) -> None:
self.section.state = 1
self.section.numberStates = len(self.FORMATS)
async def loop(self) -> None:
now = datetime.datetime.now()
format = self.FORMATS[self.section.state]
self.section.setText(now.strftime(format))
async def main() -> None:
bar = Bar()
dualScreen = len(bar.children) > 1
bar.addProvider(I3ModeProvider(), alignment=Alignment.LEFT)
bar.addProvider(I3WorkspacesProvider(), alignment=Alignment.LEFT)
if dualScreen:
bar.addProvider(
I3WindowTitleProvider(), screenNum=0, alignment=Alignment.CENTER
)
bar.addProvider(
StaticProvider(text="mpris"),
screenNum=1 if dualScreen else None,
alignment=Alignment.CENTER,
)
bar.addProvider(StaticProvider("C L M T B"), alignment=Alignment.RIGHT)
bar.addProvider(
StaticProvider("pulse"),
screenNum=1 if dualScreen else None,
alignment=Alignment.RIGHT,
)
bar.addProvider(
NetworkProvider(),
screenNum=0 if dualScreen else None,
alignment=Alignment.RIGHT,
)
bar.addProvider(TimeProvider(), alignment=Alignment.RIGHT)
await bar.run()
asyncio.run(main())

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

65
hm/dev.nix Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -21,7 +21,6 @@
# Communication
signal-desktop
(pkgs.callPackage ./whisperx.nix {}) # Transcribe voice messages
# downloading
# transmission TODO Collision if both transmissions are active?
@ -35,20 +34,21 @@
# 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
hunspell
hunspellDicts.en_GB-ize
hunspellDicts.en_US
hunspellDicts.fr-moderne
hunspellDicts.nl_NL
# TODO libreoffice-extension-languagetool or libreoffice-extension-grammalecte-fr
] ++ lib.optionals config.frogeye.desktop.xorg [
# multimedia editors
gimp
inkscape
darktable
puddletag
audacity
xournalpp
krita
# downloading
transmission-qt
@ -64,5 +64,8 @@
# https://hydra.nixos.org/job/nixos/release-23.11/nixpkgs.blender.aarch64-linux
blender
]);
services = {
syncthing.enable = true;
};
};
}

View file

@ -1,37 +0,0 @@
{ pkgs ? import <nixpkgs> { } }:
pkgs.python3Packages.buildPythonPackage {
pname = "whisperx";
version = "2024-08-19";
# pypi doesn't have the requirements.txt file, and it's required
src = pkgs.fetchFromGitHub {
owner = "m-bain";
repo = "whisperX";
rev = "9e3a9e0e38fcec1304e1784381059a0e2c670be5"; # git doesn't have tags
hash = "sha256-IVtn9fe/yi4+fbH57s9LoiREnMZ2nhEObp1a4R/7gHg=";
};
pyproject = true;
dependencies = [
pkgs.python3Packages.torch
pkgs.python3Packages.torchaudio
(pkgs.python3Packages.faster-whisper.overrideAttrs (old: {
# 1.0.2 is actually breaking APIs (requires hotwords argument)
src = pkgs.fetchFromGitHub {
owner = "SYSTRAN";
repo = "faster-whisper";
rev = "v1.0.0";
hash = "sha256-0fE8X1d6CgDrrHtRudksN/tIGRtBKMvoNwkSVyFNda4=";
};
}))
pkgs.python3Packages.transformers
pkgs.python3Packages.pyannote-audio # Not in the requirements.txt for some reason
pkgs.python3Packages.pandas
pkgs.python3Packages.nltk
];
build-system = [
pkgs.python3Packages.setuptools
];
pythonImportsCheck = [
"whisperx"
];
}

View file

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

94
hm/frobar/.dev/barng.py Executable file
View file

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

199
hm/frobar/.dev/oldbar.py Executable file
View file

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

327
hm/frobar/.dev/pip.py Executable file
View file

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

10
hm/frobar/.dev/x.py Executable file
View file

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

48
hm/frobar/default.nix Normal file
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,6 +1,6 @@
#!/usr/bin/env nix-shell
#! nix-shell -i python3
#! nix-shell -p python3 python3Packages.coloredlogs python3Packages.configargparse python3Packages.filelock python3Packages.requests python3Packages.yt-dlp ffmpeg
#! nix-shell -p python3 python3Packages.coloredlogs python3Packages.configargparse python3Packages.filelock python3Packages.filelock python3Packages.requests python3Packages.yt-dlp ffmpeg
# Also needs mpv but if I put it there it's not using the configured one
@ -189,13 +189,7 @@ class RVElement:
def ytdl_infos(self) -> typing.Optional[dict]:
try:
return self.metafile_read("ytdl")
# If the metafile doesn't exist or is corrupted
except (FileNotFoundError, TypeError, AttributeError, EOFError):
# Delete file otherwise __str__ won't be happy
metafile = self.metafile("ytdl")
if os.path.isfile(metafile):
os.unlink(metafile)
infos = self._ytdl_infos()
self.metafile_write("ytdl", infos)
return infos

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