Compare commits
54 Commits
dendritic
...
b3380c6ce4
| Author | SHA1 | Date | |
|---|---|---|---|
| b3380c6ce4 | |||
| 97fdb926ac | |||
| 04427ddcda | |||
| 0ea6d168fd | |||
| b6f3eee1b1 | |||
| 1d661943cf | |||
| d9fbf95ead | |||
| 0a3a1746cb | |||
| f079a39cbc | |||
| edd18bafdd | |||
| c1cb2490ed | |||
| 2f4f4257a3 | |||
| cdb8e15060 | |||
| 3b389a5f2a | |||
| b20a67fdc6 | |||
| 858f68f9c6 | |||
| 8578e23114 | |||
| 98b2f461cd | |||
| f80ed5863e | |||
| 1d9784877b | |||
| e5455f35f9 | |||
| fc41d9aef3 | |||
| d6916a3fa3 | |||
| a9b5cd2847 | |||
| 177963b64e | |||
| e5cc25eb49 | |||
| 8fde1cd5fe | |||
| 21be2b0c87 | |||
| e27b7cb777 | |||
| f4923cbcad | |||
| 630054fb8d | |||
| 9909fb411b | |||
| 9c64e1513e | |||
| 943498ce52 | |||
| df97c76527 | |||
| 8be92183bb | |||
| 4ea2421734 | |||
| fa2c62e2f4 | |||
| a6ff616c2d | |||
| 6e0f938e65 | |||
| 59c05832b1 | |||
| b8eb351e6c | |||
| 78afc9e5bf | |||
| ebdee10fba | |||
| 890f39079b | |||
| e0a5891cd8 | |||
| 7f27339312 | |||
| bd64633454 | |||
| 7c187d1c3d | |||
| 1adc0439ea | |||
| 2254fd419d | |||
| cf976ae14a | |||
| c928f5f748 | |||
| 8d0f541fbe |
@@ -22,7 +22,7 @@ nix build .#formatter.x86_64-linux
|
||||
### Deploying
|
||||
```bash
|
||||
# Rebuild the azos environment for lauretta (this machine) — use this when asked to "rebuild"
|
||||
sudo nixos-rebuild switch --flake '.?submodules=1#lauretta'
|
||||
azos-lauretta-update
|
||||
nix flake update --flake '.?submodules=1' # Update all inputs
|
||||
nix flake lock --flake '.?submodules=1' --update-input X # Update specific input
|
||||
```
|
||||
@@ -70,76 +70,111 @@ emacsclient -c -e '(switch-to-buffer (get-buffer-create "test"))'
|
||||
emacsclient -e '(message "hello")'
|
||||
```
|
||||
|
||||
## Code Style Guidelines
|
||||
## Repository Structure
|
||||
|
||||
### File Organization
|
||||
- **Home-manager modules**: `modules/home-manager/<name>.nix`
|
||||
- **NixOS modules**: `modules/nixos/<name>.nix`
|
||||
- **Imports**: All modules in `modules/home-manager/default.nix` and `modules/nixos/default.nix` (alphabetical order)
|
||||
- **Home config**: `home-manager/home.nix` - main home-manager user config
|
||||
- **Custom packages**: `pkgs/` - custom package definitions
|
||||
- **Overlays**: `overlays/` - package overlays (addpkgs, modifications, unstable-packages)
|
||||
```
|
||||
azos/
|
||||
├── flake.nix # Main flake
|
||||
├── home-manager/home.nix # Home-manager entry point; manually imports all modules
|
||||
├── _machines/ # Per-machine NixOS configs (pass suiteModules as specialArgs)
|
||||
├── nixos/ # NixOS system configs (configuration.nix, configuration-vm.nix, etc.)
|
||||
├── features/ # Machine-specific home-manager features (auto-discovered)
|
||||
├── overlays/ # Package overlays
|
||||
├── shells/ # Dev shells
|
||||
└── azos-core/ # Shared feature library (git submodule)
|
||||
├── flake.nix
|
||||
├── features/ # Shared/reusable features (auto-discovered)
|
||||
├── overlays/ # Core overlays
|
||||
└── _lib/ # Module schema helpers
|
||||
```
|
||||
|
||||
Features are auto-discovered by `import-tree` — no central imports file. Each feature's
|
||||
`default.nix` registers itself via `config.flake.modules.homeManager.<name>`. Modules are
|
||||
then explicitly imported in `home-manager/home.nix` via `suiteModules.homeManager.<name>`.
|
||||
|
||||
**Where to put a new feature:**
|
||||
- Shared / reusable across machines → `azos-core/features/<name>/default.nix`
|
||||
- Machine-specific → `azos/features/<name>/default.nix`
|
||||
|
||||
## Code Style Guidelines
|
||||
|
||||
### Nix Module Template
|
||||
|
||||
All features follow this flake-parts registration pattern:
|
||||
|
||||
```nix
|
||||
{
|
||||
{...}: {
|
||||
config.flake.modules.homeManager.<name> = {
|
||||
lib,
|
||||
config,
|
||||
pkgs,
|
||||
...
|
||||
}: let
|
||||
isEnabled =
|
||||
config.azos.<module-name>.enable;
|
||||
in {
|
||||
options.azos.<module-name>.enable = lib.mkOption {
|
||||
default = true;
|
||||
}: {
|
||||
options.azos.<name>.enable = lib.mkOption {
|
||||
default = false;
|
||||
example = true;
|
||||
type = lib.types.bool;
|
||||
};
|
||||
|
||||
config = lib.mkIf isEnabled {
|
||||
config = lib.mkIf config.azos.<name>.enable {
|
||||
home.packages = with pkgs; [pkg1 pkg2];
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Naming Conventions
|
||||
- Option paths: `config.azos.<module-name>.<option>`
|
||||
- Enable option: `enable` (bool), default `true`
|
||||
- Module names: kebab-case (`libreoffice.nix`, `git-config.nix`)
|
||||
- Option names: may differ from filename (e.g., `git.nix` uses `azos.git-config`)
|
||||
- Variables: kebab-case (`isEnabled`)
|
||||
- Option paths: `azos.<module-name>.<option>`
|
||||
- Module names: kebab-case (`git-config`, `claude-memory`)
|
||||
- Feature directories: kebab-case, one `default.nix` per feature
|
||||
- Variables: kebab-case
|
||||
|
||||
### Formatting
|
||||
- Indentation: 2 spaces
|
||||
- Let bindings: multi-line with `isEnabled` on its own line
|
||||
- Packages: Use `with pkgs; [ ... ]` syntax
|
||||
- Packages: `with pkgs; [pkg1 pkg2]` (no trailing semicolon inside list)
|
||||
|
||||
### Imports
|
||||
- Always include `{...}` for module args
|
||||
- Standard args: `lib, config, pkgs, ...`
|
||||
- NixOS config args: `inputs, outputs, lib, config, pkgs, ...`
|
||||
- Use `./relative/path.nix` for local imports
|
||||
- Always include `{...}` for the outer module args
|
||||
- Inner module args: `lib, config, pkgs, ...`
|
||||
|
||||
### Option Patterns
|
||||
- Boolean enable: `lib.mkOption { default = true; example = true; type = lib.types.bool; }`
|
||||
- Conditional config: `lib.mkIf isEnabled { ... }`
|
||||
- User-overridable: `lib.mkDefault`
|
||||
- Boolean enable: `lib.mkOption { default = false; example = true; type = lib.types.bool; }`
|
||||
- Conditional config: `lib.mkIf config.azos.<name>.enable { ... }`
|
||||
- Auto-enable a dependency: `azos.<dep>.enable = lib.mkDefault true;`
|
||||
- Force value: `lib.mkForce` (sparingly)
|
||||
|
||||
### Extensible Options
|
||||
For options that multiple modules should be able to contribute to, use attrset options:
|
||||
|
||||
```nix
|
||||
options.azos.<name>.things = lib.mkOption {
|
||||
default = {};
|
||||
type = lib.types.attrsOf lib.types.path; # or lines, str, etc.
|
||||
};
|
||||
```
|
||||
|
||||
Any module can then add entries without modifying the owning feature. Examples in use:
|
||||
- `azos.claude.globalSkills` — attrset of skill name → markdown file path
|
||||
- `azos.claude.globalMdSections` — attrset of key → markdown string (merged into `~/.claude/CLAUDE.md`)
|
||||
|
||||
### Home-Manager vs NixOS
|
||||
- Home-manager: `home.packages`, `home.file`, `programs.<program>`
|
||||
- NixOS: `config.services`, `environment.systemPackages`
|
||||
|
||||
### Deploying Files
|
||||
- Static file: `home.file."dest".source = ./file;`
|
||||
- Generated text: `home.file."dest".text = "...";`
|
||||
- Runtime script (e.g. merging JSON at activation): `home.activation.<name> = lib.hm.dag.entryAfter ["writeBoundary"] ''...'';`
|
||||
|
||||
### Suites System
|
||||
Modules are organized into suites (defined in `azos-core/`):
|
||||
- `azos.suites.base` - Base packages and config
|
||||
- `azos.suites.editor` - Editor tools
|
||||
- `azos.suites.dev` - Development tools
|
||||
- `azos.suites.station` - Desktop applications
|
||||
- `azos.suites.exwm` - EXWM window manager
|
||||
- Enable in `home-manager/home.nix` with `azos.suites.<name>.enable = true;`
|
||||
Top-level suites group related features (defined in `azos-core/`):
|
||||
- `azos.suites.base` — base packages and config
|
||||
- `azos.suites.editor` — editor tools
|
||||
- `azos.suites.dev` — development tools
|
||||
- `azos.suites.station` — desktop applications
|
||||
- `azos.suites.exwm` — EXWM window manager
|
||||
|
||||
The machine suite (`azos.suites.lauretta`) enables the relevant suites for this machine.
|
||||
|
||||
### Specializations
|
||||
NixOS supports specializations for alternative configurations:
|
||||
@@ -158,33 +193,62 @@ specialisation = {
|
||||
|
||||
## Adding New Modules
|
||||
|
||||
1. Create file in `modules/home-manager/` or `modules/nixos/`
|
||||
2. Add import to `modules/<type>/default.nix` (alphabetical)
|
||||
3. Use module template above
|
||||
1. Create `features/<name>/default.nix` (machine-specific) or `azos-core/features/<name>/default.nix` (shared)
|
||||
2. Use the module template above
|
||||
3. Add `suiteModules.homeManager.<name>` to the imports list in `home-manager/home.nix`
|
||||
4. Run `nix fmt`
|
||||
|
||||
> **Submodule gotcha**: New files in `azos-core/` must be `git add`-ed inside the submodule
|
||||
> before `nix build` will see them — Nix flakes only evaluate git-tracked files.
|
||||
> ```bash
|
||||
> cd azos-core && git add features/<name>/
|
||||
> ```
|
||||
|
||||
## Claude Integration
|
||||
|
||||
Claude Code is integrated with this environment via several azos-core features:
|
||||
|
||||
| Feature | Option | What it does |
|
||||
|---------|--------|--------------|
|
||||
| `azos-core/features/claude-memory` | `azos.claude-memory.enable` | Registers org-roam-mcp as a global MCP server in `~/.claude.json` |
|
||||
| `azos-core/features/claude-skills` | `azos.claude-skills.enable` | Deploys skills to `~/.claude/commands/` and content to `~/.claude/CLAUDE.md` |
|
||||
| `azos/features/claude` | `azos.claude.enable` | Installs claude-code, auto-enables the above two |
|
||||
|
||||
### Adding Claude skills or standing instructions
|
||||
From any module — no need to touch azos-core:
|
||||
```nix
|
||||
azos.claude.globalSkills.my-skill = ./skills/my-skill.md;
|
||||
azos.claude.globalMdSections.my-rule = ''
|
||||
# My Rule
|
||||
Always do X when Y.
|
||||
'';
|
||||
```
|
||||
See `azos-core/features/claude-skills/README.md` for full documentation.
|
||||
|
||||
## Key Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `flake.nix` | Main flake, defines systems, overlays |
|
||||
| `modules/home-manager/default.nix` | Home-manager imports |
|
||||
| `modules/nixos/default.nix` | NixOS imports |
|
||||
| `home-manager/home.nix` | User home-manager config, enables suites |
|
||||
| `nixos/configuration.nix` | Lauretta laptop config |
|
||||
| `flake.nix` | Main flake, defines systems and overlays |
|
||||
| `home-manager/home.nix` | Home-manager entry point, imports all modules |
|
||||
| `_machines/lauretta.nix` | Lauretta machine config, passes `suiteModules` as specialArgs |
|
||||
| `nixos/configuration.nix` | Lauretta NixOS system config |
|
||||
| `nixos/configuration-vm.nix` | Test VM config |
|
||||
| `overlays/` | Custom package overlays |
|
||||
| `pkgs/` | Custom packages |
|
||||
| `azos-core/` | Shared modules (submodule) |
|
||||
| `overlays/` | Package overlays |
|
||||
| `azos-core/` | Shared feature library (git submodule) |
|
||||
| `azos-core/features/` | Shared features (auto-discovered by import-tree) |
|
||||
| `features/` | Machine-specific features (auto-discovered by import-tree) |
|
||||
| `azos-core/features/editor/emacs/config.org` | Literate Emacs config |
|
||||
| `azos-core/features/claude-skills/README.md` | Claude skills extensibility docs |
|
||||
|
||||
## Common Tasks
|
||||
|
||||
**Add system package**: Edit NixOS module, add to `environment.systemPackages = with pkgs; [ pkg ]`
|
||||
**Add system package**: Edit a NixOS feature, add to `environment.systemPackages = with pkgs; [ pkg ]`
|
||||
|
||||
**Add home-manager package**: Edit module, add to `home.packages = with pkgs; [ pkg ]`
|
||||
**Add home-manager package**: Edit a feature's config block, add to `home.packages = with pkgs; [ pkg ]`
|
||||
|
||||
**Add system service**: Edit NixOS module, add under `services.<service>`
|
||||
**Add system service**: Edit a NixOS feature, add under `services.<service>`
|
||||
|
||||
**Add custom package**: Create file in `pkgs/`, add to `pkgs/default.nix`, use via overlay
|
||||
**Add custom package**: Add to `azos-core/overlays/` via `config.flake.overlayPkgs.<name>`, then use as `pkgs.<name>`
|
||||
|
||||
**Access unstable packages**: Use `pkgs.unstable.<package>` (via unstable-packages overlay)
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
config,
|
||||
inputs,
|
||||
...
|
||||
}: {
|
||||
config.flake.nixosConfigurations.beacon-wsl = inputs.nixpkgs.lib.nixosSystem {
|
||||
specialArgs = {
|
||||
inherit inputs;
|
||||
outputs = config.flake;
|
||||
suiteModules = config.flake.modules;
|
||||
};
|
||||
modules = [../nixos/configuration-beacon-wsl.nix];
|
||||
};
|
||||
|
||||
config.flake.packages.x86_64-linux.beacon-wsl-tarball =
|
||||
config.flake.nixosConfigurations.beacon-wsl.config.system.build.tarballBuilder;
|
||||
}
|
||||
+1
-1
Submodule azos-core updated: 99e97c9489...5669ae0692
@@ -11,6 +11,12 @@
|
||||
config = lib.mkIf config.azos.bluetooth.enable {
|
||||
services.blueman.enable = true;
|
||||
hardware.bluetooth.enable = true;
|
||||
environment.etc."bluetooth/input.conf" = lib.mkForce {
|
||||
text = ''
|
||||
[General]
|
||||
ClassicBondedOnly=false
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
{...}: {
|
||||
config.flake.overlayPkgs.emacs-lichess = pkgs: let
|
||||
epkgs = pkgs.emacs.pkgs;
|
||||
in
|
||||
epkgs.trivialBuild {
|
||||
pname = "lichess";
|
||||
version = "1.0";
|
||||
src = pkgs.fetchFromGitHub {
|
||||
owner = "tmythicator";
|
||||
repo = "lichess.el";
|
||||
rev = "572a6a353e2113c8a95564b9bea1f2896e6d3dab";
|
||||
sha256 = "1d87qjmdpizggkrzq39i9vqgvbpjf5mv97vm7zy28ksbqxmfasqa";
|
||||
};
|
||||
# trivialBuild's installPhase only installs *.el/*.elc; the piece SVGs
|
||||
# under assets/ must be copied alongside so the default
|
||||
# lichess-board-gui-asset-path (relative to the installed .el file) resolves.
|
||||
postInstall = ''
|
||||
cp -r assets $out/share/emacs/site-lisp/
|
||||
'';
|
||||
};
|
||||
|
||||
config.flake.modules.homeManager.chess = {
|
||||
lib,
|
||||
config,
|
||||
pkgs,
|
||||
...
|
||||
}: {
|
||||
options.azos.chess.enable = lib.mkOption {
|
||||
default = false;
|
||||
example = true;
|
||||
type = lib.types.bool;
|
||||
};
|
||||
|
||||
config = lib.mkIf config.azos.chess.enable {
|
||||
azos.emacs.pkgs = [pkgs.azos-emacs-chess];
|
||||
azos.emacs.enabledSuites = ["azos-emacs-chess"];
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
orgTrivialBuild,
|
||||
epkgs,
|
||||
pkgs,
|
||||
}:
|
||||
orgTrivialBuild {
|
||||
pname = "azos-emacs-chess";
|
||||
version = "0.1.0";
|
||||
src = ./config.org;
|
||||
packageRequires = with epkgs; [
|
||||
pkgs.azos-emacs-base
|
||||
pkgs.emacs-lichess
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
#+title: Aner's Emacs Chess Configuration
|
||||
#+property: header-args :results silent
|
||||
|
||||
* Base setup
|
||||
** Require
|
||||
|
||||
#+begin_src emacs-lisp
|
||||
(require 'azos-emacs-base)
|
||||
#+end_src
|
||||
|
||||
* Lichess
|
||||
|
||||
#+begin_src emacs-lisp
|
||||
(use-package lichess
|
||||
:demand t
|
||||
:init
|
||||
(defun azos/chess/open ()
|
||||
"Open lichess in its dedicated \"chess\" tab."
|
||||
(interactive)
|
||||
(azos/tab-bar/open-singleton-tab "chess" #'lichess))
|
||||
:bind
|
||||
(:map azos/global-minor-mode/open-keymap
|
||||
("k" . azos/chess/open)))
|
||||
#+end_src
|
||||
|
||||
* Provide
|
||||
|
||||
#+begin_src emacs-lisp
|
||||
(provide 'azos-emacs-chess)
|
||||
#+end_src
|
||||
@@ -0,0 +1,3 @@
|
||||
{...}: {
|
||||
config.flake.overlayPkgs.azos-emacs-chess = pkgs: pkgs.localEmacsPkg ./_pkg.nix;
|
||||
}
|
||||
@@ -11,6 +11,8 @@
|
||||
};
|
||||
config = lib.mkIf config.azos.claude.enable {
|
||||
home.packages = with pkgs; [claude-code claude-agent-acp];
|
||||
azos.claude-memory.enable = lib.mkDefault true;
|
||||
azos.claude-skills.enable = lib.mkDefault true;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -13,25 +13,68 @@
|
||||
programs.password-store = {
|
||||
enable = true;
|
||||
settings = {
|
||||
PASSWORD_STORE_KEY = "076AA297579A0064";
|
||||
PASSWORD_STORE_KEY = "26B851F81F8F7C55";
|
||||
};
|
||||
};
|
||||
home.packages = with pkgs; [
|
||||
yubikey-personalization
|
||||
pinentry-gtk2
|
||||
];
|
||||
programs.gpg = {
|
||||
enable = true;
|
||||
};
|
||||
programs.browserpass = {
|
||||
enable = true;
|
||||
browsers = ["chromium"];
|
||||
};
|
||||
services.gpg-agent = {
|
||||
enable = true;
|
||||
enableSshSupport = true;
|
||||
grabKeyboardAndMouse = false;
|
||||
pinentry.package = pkgs.pinentry-gtk2;
|
||||
pinentry.package = pkgs.pinentry-emacs;
|
||||
extraConfig = ''
|
||||
allow-emacs-pinentry
|
||||
allow-loopback-pinentry
|
||||
'';
|
||||
};
|
||||
|
||||
# pinentry-emacs needs INSIDE_EMACS set, but NOT on gpg-agent itself:
|
||||
# gpg-agent calls gnupg_unsetenv("INSIDE_EMACS") on its own process at
|
||||
# startup (agent/gpg-agent.c, both supervised and daemon mode) -- by
|
||||
# design, it expects this to arrive per-connection from the CLIENT,
|
||||
# not statically from its own daemon environment. Each gpg/git
|
||||
# invocation reads INSIDE_EMACS from its own env and forwards it to
|
||||
# gpg-agent via "OPTION putenv=INSIDE_EMACS=..." (common/asshelp.c:
|
||||
# send_pinentry_environment); gpg-agent stores that per-session and
|
||||
# copies it into the forked pinentry's actual env for that one
|
||||
# request (agent/call-pinentry.c:atfork_core). So the var has to be
|
||||
# set on whatever shell/process runs git or gpg -- a prior attempt
|
||||
# at this set it on gpg-agent.service's systemd Environment instead,
|
||||
# which gpg-agent silently discards before it can ever be read.
|
||||
#
|
||||
# Only safe to set globally when Emacs is structurally guaranteed to
|
||||
# be up and responsive -- i.e. under EXWM, where Emacs *is* the
|
||||
# window manager. Elsewhere Emacs may not be running (fine: the
|
||||
# loopback socket connect fails instantly, same fast fallback as
|
||||
# today) or may be running but transiently busy (not fine: pinentry
|
||||
# then blocks on the Assuan handshake for up to its 60s timeout
|
||||
# before falling back, which is strictly worse than today's instant
|
||||
# failure).
|
||||
home.sessionVariables = lib.mkIf config.azos.suites.exwm.enable {
|
||||
INSIDE_EMACS = "t";
|
||||
};
|
||||
|
||||
home.file.".ssh/config".source = ./ssh-config;
|
||||
home.file.".ssh/gpg-as-ssh.pub".source = ./gpg-as-ssh.pub;
|
||||
home.file.".gnupg/sshcontrol".source = ./sshcontrol;
|
||||
|
||||
# home-manager creates ~/.gnupg (via the files above) with the default
|
||||
# umask, which can leave it group/other-readable. gpg refuses to trust
|
||||
# a homedir like that and prints an "unsafe permissions" warning on
|
||||
# every invocation, which pollutes anything that captures gpg/pass
|
||||
# output (e.g. shell-command-to-string). Enforce 700 on every switch.
|
||||
home.activation.gnupgPerms = lib.hm.dag.entryAfter ["writeBoundary"] ''
|
||||
$DRY_RUN_CMD chmod 700 "$HOME/.gnupg"
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDBFZRqiTsOCAJPMqUyMeLd2MbyjdGoyqDVq5/Inhb6EOaM1NUGG4b6FPmYgFLyJIm5LC9BOo6M7npiaiOs/zMqp+hoGLNQUNwm5/G0uy1bjkEfKdUTdGnJ2+M9rkxrR1c+KXrjkiqECqTbnPE4mJbGyVxBW2MwMeP5w8c0DB5KO528PetvHMPPQuEdXyZzDI4kKtVpMlJoPIrIGlNFX0G/wrgXcM4zU1snOTuYGqZnWW++4kBsgIlRKpf/bLJyUMTp30eLVr0fQ6OMBtj1tzUUBaaowU6VGYQQDU/rIh/NpkA2cEVPXZegM4OohkAqrJBFPIAg90WD9Z/SyQlz0Jn8PpAloP0Cuq2vVRr+QLEwxqGiFq91YQ2VtwksMHwJGVrXRCNegpxTZQijWMEd+o0FD2cEd7Ftw6v2L6g12GJ3QGX/q0d/u0GongLLa9fPXl4VoAu7AL+cUcbX/SS7RCG8kYAR3DwOazVbK0NWEdwvWdoSU4lZ3j2at1xqMGjHjyLiTeUqZBjm+Sl5MJWIYNg+8hnONljvggg4SzDFDAkgVLZtOCaZibsMA1ucGR7VRCM09uoaEI4/ZS5pCBtYcp8X67Bv67Og8s2NFf5sUfYBPPKpdBSs+dEPycNVff6JlmzfNiyzLawacGKIDWYSgkOl43N/5ehtpsL3HMZ+5SVNIw== (none)
|
||||
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDMdozJewuuqG4VuwGhI5Uw2xDIUrkQwyPGQ2q3tvnPq/VoV+zfdnou42I6XYEfOGd6xfLR0byOGCk1Ia1F/picBsxADEaJFBnTny5MWNA5MxwJIYUPW5+kFN1TmxdNGmj0WJ8qZeKkS3ffixdHlHTVu4Rzrlu8qDdw8TmgcvD7sHpNaBB+hwG9B/s5BAozfXvyXtW51lLLrEZdmUXJxAP51ubVgysZ985PA89o4G7LUE20IKAj3rgylLwq+Lz2PoGmVCptPTknLLzGlxTy2mU/tESJtS4iUvshSYiKak73BSen88xLWL/eZGFzOLW4TNem/qT0ykqK1s1BaGWCcQiDWN+Lpa3XqTbFN054MfhLdEMp+NjrdsCbZORzp9p2ZPDqi5jI1Hy7IN/8VzzB+T13MX6YhW7CBBqUZaivGhVQGsNjrFusPGhCaZrzNJ1l4qssLAlOh9qdaYzqDe+BIHsty55vw4Ph+Axou82babpLoiQzkCol2TWjPr7JSfHFkm6CmoTQ7mK8m7jRCodmxF+BEe4Usr+HrTKDc9fr5P4mdfL0O+ZPrlkPuG1zBTMal9MPxeCk4emzOZf7vBMNzb0aKHbOtSdVk/IcjatlDd2EIo34e5A6anLRMAdxb7901mnCAcjvCn6+ArNV8nr+NBk1+UvumZ+Q59LTrAkgRt3rBw== openpgp:0x13E0C25D
|
||||
|
||||
@@ -1 +1 @@
|
||||
61D809B46CEE2A0AF799C4C2FADB0F61A19EEABD
|
||||
37710BAB5F6DC9878A450CDA8A4661D932C04ACB
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
programs.git = {
|
||||
enable = true;
|
||||
signing = {
|
||||
key = "6D17E295C70E2674";
|
||||
key = "BB141234A3DD953D";
|
||||
signByDefault = true;
|
||||
};
|
||||
settings = {
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
[*git.zakobar.com*]
|
||||
target=zakobar.com/users/aner
|
||||
target=zakobar.com
|
||||
@@ -0,0 +1,52 @@
|
||||
{...}: {
|
||||
config.flake.modules.nixos.hardware-auth = {
|
||||
lib,
|
||||
config,
|
||||
pkgs,
|
||||
...
|
||||
}: {
|
||||
options.azos.hardware-auth.enable = lib.mkOption {
|
||||
default = false;
|
||||
example = true;
|
||||
type = lib.types.bool;
|
||||
description = ''
|
||||
Enables YubiKey (U2F/FIDO2) as a "sufficient" PAM factor ahead of
|
||||
the password, for both sudo and login (which greetd/tuigreet
|
||||
substacks) -- touch the key to auth, or just type your password as
|
||||
normal if it's not plugged in.
|
||||
|
||||
After enabling and rebuilding, enroll each key on the new machine
|
||||
(per-user, not machine-wide state, so this must be redone per
|
||||
machine):
|
||||
|
||||
mkdir -p ~/.config/Yubico
|
||||
pamu2fcfg > ~/.config/Yubico/u2f_keys
|
||||
|
||||
To add a second/backup key without overwriting the first:
|
||||
|
||||
pamu2fcfg -n >> ~/.config/Yubico/u2f_keys
|
||||
|
||||
Test with `sudo true` (touching the key) before trusting it on the
|
||||
greeter -- a login lockout is harder to recover from than a failed
|
||||
sudo in an already-open session.
|
||||
'';
|
||||
};
|
||||
|
||||
config = lib.mkIf config.azos.hardware-auth.enable {
|
||||
# Default NixOS PAM rule ordering already tries u2f before pam_unix
|
||||
# (password), both "sufficient" -- so YubiKey touch wins first,
|
||||
# password always remains as fallback.
|
||||
#
|
||||
# Fingerprint (fprintd) is deliberately not wired up here: lauretta's
|
||||
# reader is a Synaptics Metallica MIS (06cb:009a), a match-on-chip
|
||||
# sensor unsupported by upstream libfprint. The only known fix is an
|
||||
# unofficial python-validity + open-fprintd stack, not packaged in
|
||||
# nixpkgs and fragile enough (suspend/resume, PAM lockout risk) to not
|
||||
# be worth it for now.
|
||||
security.pam.u2f = {
|
||||
enable = true;
|
||||
settings.cue = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -5,15 +5,51 @@
|
||||
pkgs,
|
||||
...
|
||||
}: {
|
||||
options.azos.headphones-whmx4000.enable = lib.mkOption {
|
||||
default = true;
|
||||
options.azos.headphones = {
|
||||
enable = lib.mkOption {
|
||||
default = false;
|
||||
example = true;
|
||||
type = lib.types.bool;
|
||||
};
|
||||
config = lib.mkIf config.azos.headphones-whmx4000.enable {
|
||||
home.packages = [
|
||||
(pkgs.writeShellScriptBin
|
||||
"azos-connect-headphones-whmx4000"
|
||||
"echo \"connect AC:80:0A:AF:E1:C2\" | bluetoothctl")
|
||||
devices = lib.mkOption {
|
||||
default = [];
|
||||
type = lib.types.listOf (lib.types.submodule {
|
||||
options = {
|
||||
mac = lib.mkOption {type = lib.types.str;};
|
||||
name = lib.mkOption {type = lib.types.str;};
|
||||
};
|
||||
});
|
||||
description = "Headphone devices; first entry is the default connect target.";
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf config.azos.headphones.enable {
|
||||
home.packages = let
|
||||
deviceEntries =
|
||||
lib.concatMapStrings
|
||||
(d: " ${lib.escapeShellArg "${d.mac} ${d.name}"}\n")
|
||||
config.azos.headphones.devices;
|
||||
defaultMac =
|
||||
if config.azos.headphones.devices == []
|
||||
then ""
|
||||
else (lib.head config.azos.headphones.devices).mac;
|
||||
in [
|
||||
(pkgs.writeShellScriptBin "azos-connect-headphones" ''
|
||||
DEVICES=(
|
||||
${deviceEntries})
|
||||
|
||||
case "$1" in
|
||||
--list)
|
||||
printf '%s\n' "''${DEVICES[@]}"
|
||||
;;
|
||||
--mac)
|
||||
echo "connect $2" | bluetoothctl
|
||||
;;
|
||||
*)
|
||||
echo "connect ${defaultMac}" | bluetoothctl
|
||||
;;
|
||||
esac
|
||||
'')
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
{...}: {
|
||||
config.flake.modules.homeManager.homey-alert = {
|
||||
lib,
|
||||
config,
|
||||
pkgs,
|
||||
...
|
||||
}: {
|
||||
options.azos.homey-alert.enable = lib.mkOption {
|
||||
default = false;
|
||||
example = true;
|
||||
type = lib.types.bool;
|
||||
};
|
||||
options.azos.homey-alert.longCommandThresholdSeconds = lib.mkOption {
|
||||
default = 300;
|
||||
example = 600;
|
||||
type = lib.types.int;
|
||||
description = ''
|
||||
Installs a global PreToolUse hook (matcher "Bash") in
|
||||
~/.claude/settings.json that transparently wraps every Bash command
|
||||
Claude Code runs: it times the command, and if it ran at or past
|
||||
this threshold, fires homey-alert with the elapsed time and exit
|
||||
code once it finishes. Commands that finish faster are unaffected.
|
||||
|
||||
This exists so long-running commands (big rclone transfers, builds)
|
||||
notify automatically even when Claude never thinks to chain
|
||||
homey-alert itself, and even if the harness moves the command to
|
||||
the background partway through.
|
||||
|
||||
The hook only adds timing + notification around the command as
|
||||
given -- it does not change permissions, so normal confirmation
|
||||
prompts for risky commands still happen exactly as before.
|
||||
'';
|
||||
};
|
||||
|
||||
config = lib.mkIf config.azos.homey-alert.enable (let
|
||||
threshold = toString config.azos.homey-alert.longCommandThresholdSeconds;
|
||||
# Rewrites every Bash tool command to time itself and fire
|
||||
# homey-alert if it ran at or past the threshold. The original
|
||||
# command is spliced in verbatim (not eval'd from an encoded blob)
|
||||
# so it stays human-readable in any permission prompt, and it's run
|
||||
# in a subshell so the wrapper's own timing/notify code still runs
|
||||
# even if the original command uses `set -e` or calls `exit`.
|
||||
# permissionDecision is deliberately omitted from the hook's JSON
|
||||
# output: supplying it (even as "allow") would bypass the normal
|
||||
# confirmation flow, which must stay untouched here.
|
||||
bashNotifyHook = pkgs.writeShellApplication {
|
||||
name = "azos-bash-notify-hook";
|
||||
runtimeInputs = [pkgs.jq];
|
||||
text = ''
|
||||
input=$(cat)
|
||||
command=$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null) || command=""
|
||||
|
||||
if [ -z "$command" ]; then
|
||||
echo '{}'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
wrapped="__azos_notify_start=\$(date +%s)
|
||||
(
|
||||
$command
|
||||
)
|
||||
__azos_notify_rc=\$?
|
||||
__azos_notify_dur=\$(( \$(date +%s) - __azos_notify_start ))
|
||||
if [ \"\$__azos_notify_dur\" -ge ${threshold} ]; then
|
||||
__azos_notify_tag=\$([ \"\$__azos_notify_rc\" -eq 0 ] && echo white_check_mark || echo x)
|
||||
homey-alert -t \"Long command\" -T \"\$__azos_notify_tag\" \"Finished in \$__azos_notify_dur s (exit \$__azos_notify_rc)\" >/dev/null 2>&1 || true
|
||||
fi
|
||||
exit \"\$__azos_notify_rc\""
|
||||
|
||||
jq -n --arg cmd "$wrapped" \
|
||||
'{hookSpecificOutput: {hookEventName: "PreToolUse", updatedInput: {command: $cmd}}}' 2>/dev/null || echo '{}'
|
||||
'';
|
||||
};
|
||||
in {
|
||||
azos.claude.globalSkills.notify = ./notify-skill.md;
|
||||
|
||||
home.activation.configureBashNotifyHook = lib.hm.dag.entryAfter ["writeBoundary"] ''
|
||||
SETTINGS_JSON="$HOME/.claude/settings.json"
|
||||
HOOK_ENTRY=$(${pkgs.jq}/bin/jq -n \
|
||||
--arg cmd "${bashNotifyHook}/bin/azos-bash-notify-hook" \
|
||||
'{
|
||||
matcher: "Bash",
|
||||
hooks: [{type: "command", command: $cmd}]
|
||||
}')
|
||||
|
||||
if [ -f "$SETTINGS_JSON" ]; then
|
||||
tmp=$(mktemp)
|
||||
${pkgs.jq}/bin/jq --argjson entry "$HOOK_ENTRY" \
|
||||
'.hooks.PreToolUse = ((.hooks.PreToolUse // []) | map(select(.matcher != $entry.matcher))) + [$entry]' \
|
||||
"$SETTINGS_JSON" > "$tmp" && mv "$tmp" "$SETTINGS_JSON"
|
||||
else
|
||||
${pkgs.jq}/bin/jq -n --argjson entry "$HOOK_ENTRY" \
|
||||
'{hooks: {PreToolUse: [$entry]}}' > "$SETTINGS_JSON"
|
||||
fi
|
||||
'';
|
||||
|
||||
home.packages = [
|
||||
bashNotifyHook
|
||||
(pkgs.writeShellApplication {
|
||||
name = "homey-alert";
|
||||
runtimeInputs = with pkgs; [curl pass libnotify];
|
||||
text = ''
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: homey-alert [OPTIONS] [MESSAGE]
|
||||
|
||||
Send a notification to the homey ntfy server (ntfy.zakobar.com/alerts),
|
||||
and (when run locally, not with --print-remote) also via notify-send --
|
||||
the ntfy server isn't always reachable, so the local desktop notification
|
||||
always fires too, independent of whether the ntfy push succeeds.
|
||||
MESSAGE can be given as a positional argument or piped via stdin.
|
||||
|
||||
Options:
|
||||
-t, --title TITLE notification title
|
||||
-p, --priority PRIO priority: min|low|default|high|urgent (default: default)
|
||||
-T, --tags TAGS comma-separated emoji shortcodes (e.g. warning,rotating_light)
|
||||
-r, --print-remote print the curl command instead of running it
|
||||
(for pasting into SSH commands to remote machines)
|
||||
-h, --help show this help
|
||||
|
||||
Examples:
|
||||
# Local
|
||||
long_cmd && homey-alert -t "Training" -p high -T white_check_mark "done"
|
||||
|
||||
# Remote — expands locally, runs on the remote machine
|
||||
ssh user@host "python train.py && \$(homey-alert -r -t 'Training' 'done')"
|
||||
EOF
|
||||
}
|
||||
|
||||
TITLE=""
|
||||
PRIORITY="default"
|
||||
TAGS=""
|
||||
PRINT_REMOTE=false
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-t|--title) TITLE="$2"; shift 2 ;;
|
||||
-p|--priority) PRIORITY="$2"; shift 2 ;;
|
||||
-T|--tags) TAGS="$2"; shift 2 ;;
|
||||
-r|--print-remote) PRINT_REMOTE=true; shift ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
--) shift; break ;;
|
||||
-*) echo "Unknown option: $1" >&2; usage >&2; exit 1 ;;
|
||||
*) break ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ $# -gt 0 ]]; then
|
||||
MESSAGE="$*"
|
||||
elif [[ ! -t 0 ]]; then
|
||||
MESSAGE="$(cat)"
|
||||
else
|
||||
echo "homey-alert: no message provided" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TOKEN="$(pass zakobar-homecenter/ntfy-token)"
|
||||
|
||||
# Build the curl invocation as an array — single source of truth for both modes
|
||||
CMD=(
|
||||
curl -sf
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
-H "Priority: $PRIORITY"
|
||||
)
|
||||
[[ -n "$TITLE" ]] && CMD+=(-H "Title: $TITLE")
|
||||
[[ -n "$TAGS" ]] && CMD+=(-H "Tags: $TAGS")
|
||||
CMD+=(-d "$MESSAGE" https://ntfy.zakobar.com/alerts)
|
||||
|
||||
if $PRINT_REMOTE; then
|
||||
# Shell-quote each element so the output is safe to embed in SSH commands
|
||||
printf '%q ' "''${CMD[@]}"
|
||||
printf '\n'
|
||||
else
|
||||
"''${CMD[@]}" || true
|
||||
|
||||
# Local desktop notification, independent of the ntfy push above --
|
||||
# the server isn't always up, so this always fires as a fallback.
|
||||
URGENCY="normal"
|
||||
case "$PRIORITY" in
|
||||
min|low) URGENCY="low" ;;
|
||||
high|urgent) URGENCY="critical" ;;
|
||||
esac
|
||||
notify-send -u "$URGENCY" "''${TITLE:-homey-alert}" "$MESSAGE" || true
|
||||
fi
|
||||
'';
|
||||
})
|
||||
];
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
# Homey Alert — sending ntfy notifications when tasks finish
|
||||
|
||||
Use `homey-alert` to push notifications to Aner's homelab ntfy server when long tasks complete.
|
||||
|
||||
## When to use proactively
|
||||
|
||||
When running (or suggesting) a command that will take more than ~30 seconds — training runs,
|
||||
builds, deploys, downloads, test suites — append a notification automatically. Don't wait to
|
||||
be asked.
|
||||
|
||||
## Background / async tasks (Claude Code specifically)
|
||||
|
||||
If you (the agent) are running a command via Bash that could take longer than
|
||||
~10 minutes — large rclone/rsync transfers, builds, training runs — and it
|
||||
might run in the background (either because you set `run_in_background`, or
|
||||
because the harness auto-backgrounds it after its timeout), chain the
|
||||
`homey-alert` call **into that same command string from the start**. Don't
|
||||
wait until you notice it's taking a while and then think about notifying —
|
||||
by then the command is already running without it, and the harness's own
|
||||
"task finished" signal only reaches this session, not Aner's phone. Chaining
|
||||
upfront is the only way to get a real ntfy push the moment it actually
|
||||
finishes:
|
||||
|
||||
```bash
|
||||
rclone copy ... ; homey-alert -t "Backup" -p default -T "$([ $? -eq 0 ] && echo white_check_mark || echo x)" "rclone copy finished (exit $?)"
|
||||
```
|
||||
|
||||
## Local commands
|
||||
|
||||
Append after the command so it fires on success only (`&&`) or always (`;`):
|
||||
|
||||
```bash
|
||||
# Notify on success only
|
||||
long_cmd && homey-alert "Task done"
|
||||
|
||||
# Always notify (even on failure) — prefer this for deploys/training
|
||||
long_cmd; homey-alert -t "Training" -p high -T "$([ $? -eq 0 ] && echo white_check_mark || echo x)" "Exited $?"
|
||||
```
|
||||
|
||||
## Remote commands (SSH / training machines)
|
||||
|
||||
Remote machines don't have `homey-alert`. Use `-r`/`--print-remote` to get a self-contained
|
||||
`curl` command with the token already embedded. The `$(...)` expands **locally**; the resulting
|
||||
command string is what the remote machine executes.
|
||||
|
||||
```bash
|
||||
ssh user@host "python train.py && $(homey-alert -r -t 'Training' -p high -T white_check_mark 'Model finished')"
|
||||
```
|
||||
|
||||
The remote machine only needs `curl` (universally available).
|
||||
|
||||
## Flags
|
||||
|
||||
| Flag | Short | Values / notes |
|
||||
|------|-------|----------------|
|
||||
| `--title TITLE` | `-t` | Notification title |
|
||||
| `--priority PRIO` | `-p` | `min` / `low` / `default` / `high` / `urgent` |
|
||||
| `--tags TAGS` | `-T` | Comma-separated emoji shortcodes: `white_check_mark`, `x`, `warning`, `rotating_light`, `tada` |
|
||||
| `--print-remote` | `-r` | Print curl command instead of running (embed token locally, run remotely) |
|
||||
|
||||
## Choosing priority and tags
|
||||
|
||||
- Routine completion (backup, build): `default` priority, `white_check_mark`
|
||||
- Training / long job: `high` priority, `white_check_mark` on success / `x` on failure
|
||||
- Something broke: `urgent` priority, `rotating_light`
|
||||
@@ -6,7 +6,7 @@
|
||||
...
|
||||
}: {
|
||||
options.azos.kubectl.enable = lib.mkOption {
|
||||
default = true;
|
||||
default = false;
|
||||
type = lib.types.bool;
|
||||
};
|
||||
config = lib.mkIf config.azos.kubectl.enable {
|
||||
|
||||
@@ -15,6 +15,8 @@ orgTrivialBuild {
|
||||
pkgs.azos-emacs-station
|
||||
|
||||
agent-shell
|
||||
elfeed-protocol
|
||||
org-caldav
|
||||
pinentry
|
||||
];
|
||||
}
|
||||
|
||||
@@ -14,6 +14,12 @@
|
||||
|
||||
* Lauretta specific
|
||||
|
||||
** Weather
|
||||
|
||||
#+begin_src emacs-lisp
|
||||
(setq wttrin-default-cities '("Meitar, Israel"))
|
||||
#+end_src
|
||||
|
||||
** LLM
|
||||
|
||||
#+begin_src emacs-lisp
|
||||
@@ -28,19 +34,46 @@
|
||||
:models '(openai/gpt-oss-120b)))
|
||||
#+end_src
|
||||
|
||||
** GPG Pinentry
|
||||
|
||||
gpg-agent is configured with =pinentry.package = pkgs.pinentry-emacs=, which
|
||||
needs a running Emacs-side pinentry service to hand it prompts over a
|
||||
loopback socket. Without =pinentry-start=, the pinentry-emacs binary can't
|
||||
reach Emacs and falls back to the curses pinentry (visible as a TUI prompt
|
||||
when committing from vterm) or hangs entirely (as seen from Magit).
|
||||
|
||||
#+begin_src emacs-lisp
|
||||
(require 'pinentry)
|
||||
(pinentry-start)
|
||||
#+end_src
|
||||
|
||||
** Agent Shell
|
||||
|
||||
#+begin_src emacs-lisp
|
||||
(setq agent-shell-opencode-default-model-id "opencode/big-pickle")
|
||||
;; (setq agent-shell-preferred-agent-config (agent-shell-opencode-make-agent-config))
|
||||
(setq agent-shell-preferred-agent-config 'claude-code)
|
||||
#+end_src
|
||||
|
||||
** Headphones
|
||||
|
||||
#+begin_src emacs-lisp
|
||||
(defun azos/connect-headphones ()
|
||||
(interactive)
|
||||
(start-process-shell-command "connect-headphones" nil "azos-connect-headphones-whmx4000"))
|
||||
(defun azos/connect-headphones (&optional arg)
|
||||
"Connect to headphones via bluetoothctl.
|
||||
Without prefix ARG, connects to the first device in the list.
|
||||
With prefix ARG, prompts to choose from all configured devices."
|
||||
(interactive "P")
|
||||
(if (not arg)
|
||||
(start-process-shell-command "connect-headphones" nil "azos-connect-headphones")
|
||||
(let* ((raw (shell-command-to-string "azos-connect-headphones --list"))
|
||||
(lines (split-string (string-trim raw) "\n" t))
|
||||
(choices (mapcar (lambda (line)
|
||||
(let ((space (string-match " " line)))
|
||||
(cons (substring line (1+ space))
|
||||
(substring line 0 space))))
|
||||
lines))
|
||||
(name (completing-read "Connect headphones: " (mapcar #'car choices) nil t))
|
||||
(mac (cdr (assoc name choices))))
|
||||
(start-process-shell-command "connect-headphones" nil
|
||||
(concat "azos-connect-headphones --mac " mac)))))
|
||||
|
||||
(define-key azos/global-minor-mode/open-keymap
|
||||
(kbd "h") 'azos/connect-headphones)
|
||||
@@ -147,7 +180,7 @@
|
||||
(interactive)
|
||||
(let* ((password (string-trim
|
||||
(shell-command-to-string
|
||||
(format "pass zakobar.com/users/%s" azos/lauretta/nextcloud-user))))
|
||||
"pass zakobar.com | head -1")))
|
||||
(url-http-real-basic-auth-storage
|
||||
(list (list "nextcloud.zakobar.com:443"
|
||||
(cons azos/lauretta/nextcloud-user password)))))
|
||||
@@ -157,6 +190,149 @@
|
||||
(kbd "C") 'azos/caldav-sync)
|
||||
#+end_src
|
||||
|
||||
** Elfeed Nextcloud News Sync
|
||||
|
||||
[[https://github.com/fasheng/elfeed-protocol][elfeed-protocol]] adds an =owncloud+= protocol that speaks the
|
||||
ownCloud News API, which the Nextcloud News app also implements, so
|
||||
it syncs elfeed's feed list and read/unread state with the Nextcloud
|
||||
News app instead of just reading a local feed list one-way.
|
||||
|
||||
Reuses the same =zakobar.com= pass entry as the CalDAV sync above,
|
||||
since it's the same Nextcloud account.
|
||||
|
||||
As of elfeed-protocol 1.0.0 (paired with elfeed >= 4.0.0), the
|
||||
credentials go directly on =elfeed-feeds= — the older
|
||||
=elfeed-protocol-feeds= variable is dead: setting it only logs a
|
||||
deprecation warning, and =elfeed-feeds= ends up with the bare URL
|
||||
and no =:password=, so requests silently authenticate with an empty
|
||||
password and nothing ever lands in the db.
|
||||
|
||||
Fetching the password with =shell-command-to-string= used to
|
||||
deadlock Emacs whenever the GPG PIN cache had expired: that call
|
||||
blocks Emacs's single thread on the =pass|gpg= subprocess, but
|
||||
=pinentry-emacs= (see the GPG Pinentry section above) needs Emacs's
|
||||
command loop running to answer its loopback prompt — so Emacs waits
|
||||
on gpg-agent while gpg-agent waits on Emacs. Fetching the password
|
||||
with =make-process= instead keeps the command loop alive, so
|
||||
=pinentry-emacs= can prompt normally; =elfeed-feeds= and
|
||||
=elfeed-protocol-enable= are only set once the process sentinel
|
||||
fires. Same stderr-redirect reasoning as before, to keep a stray
|
||||
gpg/pass warning off the captured password.
|
||||
|
||||
The fetch is wrapped in a named, interactive function rather than
|
||||
inlined in =:config= so a failed attempt (e.g. =pass= produced no
|
||||
output because the YubiKey wasn't plugged in yet) can be retried
|
||||
with =M-x azos/elfeed-protocol-init-feeds= — re-running =:config=
|
||||
itself would require reloading the =elfeed-protocol= feature
|
||||
entirely. An earlier version passed =(process-get proc 'output)=
|
||||
straight to =string-trim=, which signals (not just returns nil) when
|
||||
no output was ever read, aborting the sentinel before =elfeed-feeds=
|
||||
or =elfeed-protocol-enable= ran — so a failed first attempt left
|
||||
=elfeed-feeds= permanently =nil= for the rest of the session with no
|
||||
visible error beyond a =*Messages*= line, and every subsequent
|
||||
=elfeed-update= silently had nothing to fetch.
|
||||
|
||||
#+begin_src emacs-lisp
|
||||
(defun azos/elfeed-protocol-init-feeds ()
|
||||
"Fetch the Nextcloud password and (re)enable elfeed-protocol sync.
|
||||
Unlike the one-shot `use-package' :config form this is called from,
|
||||
safe to call again (e.g. via \\[azos/elfeed-protocol-init-feeds]) if
|
||||
a previous attempt failed, such as the YubiKey not being plugged in
|
||||
yet — it re-spawns the `pass' process and re-prompts for the PIN."
|
||||
(interactive)
|
||||
(make-process
|
||||
:name "elfeed-protocol-pass"
|
||||
:command '("sh" "-c" "pass zakobar.com 2>/dev/null | head -1")
|
||||
:noquery t
|
||||
:filter (lambda (proc chunk)
|
||||
(process-put proc 'output (concat (or (process-get proc 'output) "") chunk)))
|
||||
:sentinel (lambda (proc _event)
|
||||
(let ((password (string-trim (or (process-get proc 'output) ""))))
|
||||
(if (string-empty-p password)
|
||||
(message "elfeed-protocol: `pass zakobar.com' produced no output; retry with M-x azos/elfeed-protocol-init-feeds")
|
||||
(setq elfeed-feeds
|
||||
`(("owncloud+https://aner@nextcloud.zakobar.com"
|
||||
:password ,password)))
|
||||
(elfeed-protocol-enable)
|
||||
(elfeed-update))))))
|
||||
|
||||
(use-package elfeed-protocol
|
||||
:after elfeed
|
||||
:config
|
||||
(setq elfeed-protocol-enabled-protocols '(owncloud))
|
||||
(azos/elfeed-protocol-init-feeds))
|
||||
#+end_src
|
||||
|
||||
** Elfeed Full Article Fetch
|
||||
|
||||
Many feeds only publish a short excerpt. elfeed >= 4.0 has built-in
|
||||
commands for this — =f= (=elfeed-show-fetch-link=) fetches the
|
||||
linked page and appends it to the show buffer, =R=
|
||||
(=elfeed-show-readable=) strips the fetched page down to
|
||||
article text via eww's readability scorer — and both can run
|
||||
automatically via =elfeed-show-update-hook=, gated on a feed's
|
||||
=:fetch-link=/=:readable= metadata.
|
||||
|
||||
That gating keys off an exact match on the feed's URL/id in
|
||||
=elfeed-feeds=, which breaks for ownCloud/Nextcloud subfeeds:
|
||||
elfeed-protocol synthesizes a per-subfeed id of the form
|
||||
=<host-url>::<subfeed-url>= that never appears in =elfeed-feeds=
|
||||
(only the bare host URL does), so the metadata lookup always misses
|
||||
and the stock hooks never fire. Replacing them with unconditional
|
||||
versions makes full-text + readable-mode the default for every
|
||||
entry instead.
|
||||
|
||||
#+begin_src emacs-lisp
|
||||
(defun azos/elfeed-show-auto-readable ()
|
||||
"Like `elfeed-show-auto-readable', but unconditional."
|
||||
(unless (local-variable-p 'elfeed-transform-html-functions)
|
||||
(let ((inhibit-message t))
|
||||
(elfeed-show-readable))))
|
||||
|
||||
(defun azos/elfeed-show-auto-fetch-link ()
|
||||
"Like `elfeed-show-auto-fetch-link', but unconditional."
|
||||
(elfeed-show-fetch-link))
|
||||
|
||||
(setq elfeed-show-update-hook
|
||||
(list #'azos/elfeed-show-auto-readable #'azos/elfeed-show-auto-fetch-link))
|
||||
#+end_src
|
||||
|
||||
** Lichess
|
||||
|
||||
=lichess.org= in pass already holds the website login password, and
|
||||
lichess.el's =auth-source-pass= lookup resolves by bare hostname, so
|
||||
the API token can't live under that same entry without colliding.
|
||||
Store it separately under =lichess.org/token= (=pass insert
|
||||
lichess.org/token=) and set the =lichess-token= variable directly —
|
||||
=lichess-token= (the function) checks this variable before falling
|
||||
back to =auth-source=, so this takes precedence without disturbing
|
||||
the existing =lichess.org= entry or its consumers (e.g. browserpass).
|
||||
|
||||
=shell-command-to-string= merges stderr into its return value, so any
|
||||
gpg/pass warning on stderr (e.g. an unsafe-permissions notice) would
|
||||
otherwise get concatenated onto the token and break auth silently.
|
||||
Redirect stderr away so only the token itself is ever captured.
|
||||
|
||||
#+begin_src emacs-lisp
|
||||
(setq lichess-token
|
||||
(string-trim (shell-command-to-string "pass lichess.org/token 2>/dev/null | head -1")))
|
||||
#+end_src
|
||||
|
||||
=gpg-agent= may not have a cached passphrase yet when the =setq= above
|
||||
runs at Emacs startup, in which case =pass= fails silently (stderr is
|
||||
redirected away) and =lichess-token= is left as =""=. Retry the fetch
|
||||
lazily right before each actual use, so a cold agent at startup
|
||||
doesn't permanently wedge the token for the rest of the session.
|
||||
|
||||
#+begin_src emacs-lisp
|
||||
(with-eval-after-load 'lichess
|
||||
(advice-add 'lichess-token :before
|
||||
(lambda ()
|
||||
(when (string-empty-p lichess-token)
|
||||
(setq lichess-token
|
||||
(string-trim (shell-command-to-string "pass lichess.org/token 2>/dev/null | head -1")))))))
|
||||
#+end_src
|
||||
|
||||
** Beacon Remote Jobs
|
||||
|
||||
#+begin_src emacs-lisp
|
||||
@@ -317,6 +493,62 @@ With prefix ARG, prompt for a pattern then select from matches."
|
||||
(define-key azos/beacon/keymap (kbd "t") #'azos/beacon/tail-log)
|
||||
#+end_src
|
||||
|
||||
** Roam Backup
|
||||
|
||||
=roam-backup= signs its commit with gpg, which can prompt for the YubiKey PIN
|
||||
via pinentry. =async-shell-command= spawns the process with =TERM=dumb= and
|
||||
no real controlling tty, so pinentry's curses fallback fails with
|
||||
\"Inappropriate ioctl for device\" whenever the PIN cache has expired. Run it
|
||||
in a =vterm= instead, which gives pinentry a real pty to prompt on.
|
||||
|
||||
#+begin_src emacs-lisp
|
||||
(defun azos/roam/backup ()
|
||||
"Commit and push ~/roam to the remote git repo."
|
||||
(interactive)
|
||||
(let ((vterm-buffer-name "*roam-backup*"))
|
||||
(with-current-buffer (vterm-other-window)
|
||||
(vterm-send-string "roam-backup")
|
||||
(vterm-send-return))))
|
||||
|
||||
(define-key azos/roam-keymap (kbd "B") #'azos/roam/backup)
|
||||
#+end_src
|
||||
|
||||
** Printing
|
||||
|
||||
#+begin_src emacs-lisp
|
||||
(defun azos/print-buffer (&optional arg)
|
||||
"Print the current buffer.
|
||||
For image/PDF/doc buffers, sends the file path to lp directly.
|
||||
For text buffers, uses lpr-buffer.
|
||||
With C-u, interactively prompt for scaling, B&W, and orientation options."
|
||||
(interactive "P")
|
||||
(let* ((image-p (derived-mode-p 'image-mode 'pdf-view-mode 'doc-view-mode))
|
||||
(scale (when arg (y-or-n-p "Fit to page? ")))
|
||||
(bw (when arg (y-or-n-p "Black and white? ")))
|
||||
(landscape (when (and arg image-p) (y-or-n-p "Landscape? ")))
|
||||
(lp-opts (concat
|
||||
(when scale " -o fit-to-page")
|
||||
(when bw " -o print-color-mode=monochrome")
|
||||
(when landscape " -o landscape"))))
|
||||
(if image-p
|
||||
(if buffer-file-name
|
||||
(progn
|
||||
(shell-command
|
||||
(concat "lp" lp-opts " " (shell-quote-argument buffer-file-name)))
|
||||
(message "Sent %s to printer%s%s%s."
|
||||
(file-name-nondirectory buffer-file-name)
|
||||
(if scale " [scaled]" "")
|
||||
(if bw " [B&W]" "")
|
||||
(if landscape " [landscape]" "")))
|
||||
(error "Buffer has no associated file"))
|
||||
(let ((lpr-switches
|
||||
(append lpr-switches
|
||||
(when bw '("-o print-color-mode=monochrome")))))
|
||||
(lpr-buffer)))))
|
||||
|
||||
(define-key azos/global-minor-mode/open-keymap (kbd "P") #'azos/print-buffer)
|
||||
#+end_src
|
||||
|
||||
* Provide
|
||||
|
||||
#+begin_src emacs-lisp
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
{...}: {
|
||||
config.flake.modules.homeManager.leetcode = {
|
||||
lib,
|
||||
config,
|
||||
pkgs,
|
||||
...
|
||||
}: {
|
||||
options.azos.leetcode.enable = lib.mkOption {
|
||||
default = false;
|
||||
example = true;
|
||||
type = lib.types.bool;
|
||||
};
|
||||
|
||||
config = lib.mkIf config.azos.leetcode.enable {
|
||||
azos.emacs.pkgs = [pkgs.azos-emacs-leetcode];
|
||||
azos.emacs.enabledSuites = ["azos-emacs-leetcode"];
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
orgTrivialBuild,
|
||||
epkgs,
|
||||
pkgs,
|
||||
}:
|
||||
orgTrivialBuild {
|
||||
pname = "azos-emacs-leetcode";
|
||||
version = "0.1.0";
|
||||
src = ./config.org;
|
||||
packageRequires = with epkgs; [
|
||||
pkgs.azos-emacs-base
|
||||
leetcode
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
#+title: Aner's Emacs Leetcode Configuration
|
||||
#+property: header-args :results silent
|
||||
|
||||
* Base setup
|
||||
** Require
|
||||
|
||||
#+begin_src emacs-lisp
|
||||
(require 'azos-emacs-base)
|
||||
#+end_src
|
||||
|
||||
* Leetcode
|
||||
|
||||
#+begin_src emacs-lisp
|
||||
(use-package leetcode
|
||||
:init
|
||||
(defun azos/leetcode/open ()
|
||||
"Open leetcode in its dedicated \"leetcode\" tab."
|
||||
(interactive)
|
||||
(azos/tab-bar/open-singleton-tab "leetcode" #'leetcode))
|
||||
:bind
|
||||
(:map azos/global-minor-mode/open-keymap
|
||||
("L" . azos/leetcode/open)))
|
||||
#+end_src
|
||||
|
||||
* Provide
|
||||
|
||||
#+begin_src emacs-lisp
|
||||
(provide 'azos-emacs-leetcode)
|
||||
#+end_src
|
||||
@@ -0,0 +1,3 @@
|
||||
{...}: {
|
||||
config.flake.overlayPkgs.azos-emacs-leetcode = pkgs: pkgs.localEmacsPkg ./_pkg.nix;
|
||||
}
|
||||
+61
-11
@@ -2,15 +2,18 @@
|
||||
config.flake.modules.homeManager.mail = {
|
||||
lib,
|
||||
config,
|
||||
pkgs,
|
||||
...
|
||||
}: let
|
||||
default_account_params = {
|
||||
realName = "Aner Zakobar";
|
||||
notmuch.enable = true;
|
||||
mbsync = {
|
||||
enable = true;
|
||||
create = "maildir";
|
||||
remove = "both";
|
||||
expunge = "both";
|
||||
};
|
||||
msmtp.enable = true;
|
||||
};
|
||||
default_gmail_params =
|
||||
default_account_params
|
||||
@@ -20,9 +23,38 @@
|
||||
default_smtp = {
|
||||
tls = {
|
||||
enable = true;
|
||||
useStartTls = true;
|
||||
certificatesFile = "/etc/ssl/certs/ca-certificates.crt";
|
||||
};
|
||||
};
|
||||
# mbsync's PassCmd shells out to `pass`, which needs the GPG smartcard to
|
||||
# decrypt. On a timer, a missing card would otherwise surface as a
|
||||
# pinentry "please insert card" prompt every run. azos-card-present is a
|
||||
# silent presence check (see overlays/default.nix): skip the sync
|
||||
# entirely when the card isn't there, instead of letting mbsync hit that
|
||||
# prompt.
|
||||
#
|
||||
# New mail is detected by counting files under every account's Maildir
|
||||
# "new" subdirectory before and after the sync -- that's where messages
|
||||
# the server hasn't marked seen land, across all three accounts at once,
|
||||
# with no need to touch mu's index. A rise in that count means mail
|
||||
# actually arrived, so only then does it post a desktop notification via
|
||||
# notify-send -- intentionally separate from mu4e's own "mbsync -a" (its
|
||||
# mu4e-get-mail-command), which doesn't go through this script at all.
|
||||
mbsyncSafe = pkgs.writeShellScript "mbsync-safe" ''
|
||||
if ! ${pkgs.azos-card-present}/bin/azos-card-present; then
|
||||
exit 0
|
||||
fi
|
||||
before=$(find "$HOME/Maildir" -path '*/new/*' -type f 2>/dev/null | wc -l)
|
||||
${pkgs.isync}/bin/mbsync -a
|
||||
after=$(find "$HOME/Maildir" -path '*/new/*' -type f 2>/dev/null | wc -l)
|
||||
new=$((after - before))
|
||||
if [ "$new" -gt 0 ]; then
|
||||
label="new message"
|
||||
[ "$new" -ne 1 ] && label="new messages"
|
||||
${pkgs.libnotify}/bin/notify-send "Mail" "$new $label"
|
||||
fi
|
||||
'';
|
||||
in {
|
||||
options.azos.mail.enable = lib.mkOption {
|
||||
default = true;
|
||||
@@ -30,35 +62,38 @@
|
||||
};
|
||||
config = lib.mkIf config.azos.mail.enable {
|
||||
programs = {
|
||||
notmuch = {
|
||||
enable = true;
|
||||
hooks = {preNew = "mbsync -a";};
|
||||
};
|
||||
mu.enable = true;
|
||||
mbsync.enable = true;
|
||||
msmtp.enable = true;
|
||||
lieer.enable = true;
|
||||
};
|
||||
services.lieer.enable = true;
|
||||
home.activation.muInit = lib.hm.dag.entryAfter ["writeBoundary"] ''
|
||||
if [ ! -d "$HOME/.cache/mu" ]; then
|
||||
$DRY_RUN_CMD ${pkgs.mu}/bin/mu init \
|
||||
--maildir="$HOME/Maildir" \
|
||||
--my-address=aner@zakobar.com \
|
||||
--my-address=anerisgreat@gmail.com \
|
||||
--my-address=anerz@post.bgu.ac.il
|
||||
fi
|
||||
'';
|
||||
accounts.email.accounts = {
|
||||
gmail =
|
||||
default_gmail_params
|
||||
// {
|
||||
address = "anerisgreat@gmail.com";
|
||||
userName = "anerisgreat";
|
||||
passwordCommand = "pass gmail.com/mbsync-anerisgreat";
|
||||
passwordCommand = "pass mbsync/anerisgreat@gmail.com | head -1";
|
||||
};
|
||||
bgu =
|
||||
default_gmail_params
|
||||
// {
|
||||
address = "anerz@post.bgu.ac.il";
|
||||
userName = "anerz@post.bgu.ac.il";
|
||||
passwordCommand = "pass post.bgu.ac.il/mbsync-anerz";
|
||||
passwordCommand = "pass mbsync/anerz@post.bgu.ac.il | head -1";
|
||||
};
|
||||
zakobar =
|
||||
default_account_params
|
||||
// {
|
||||
address = "aner@zakobar.com";
|
||||
msmtp.enable = true;
|
||||
primary = true;
|
||||
userName = "aner@zakobar.com";
|
||||
imap = {host = "mail.privateemail.com";};
|
||||
@@ -68,9 +103,24 @@
|
||||
port = 587;
|
||||
host = "mail.privateemail.com";
|
||||
};
|
||||
passwordCommand = "pass zakobar.com/mail/aner";
|
||||
passwordCommand = "pass privateemail.com | head -1";
|
||||
};
|
||||
};
|
||||
systemd.user.services.mbsync-timer = {
|
||||
Unit.Description = "Sync mail via mbsync (skips silently if smartcard is absent)";
|
||||
Service = {
|
||||
Type = "oneshot";
|
||||
ExecStart = "${mbsyncSafe}";
|
||||
};
|
||||
};
|
||||
systemd.user.timers.mbsync-timer = {
|
||||
Unit.Description = "Periodic mbsync mail sync";
|
||||
Timer = {
|
||||
OnStartupSec = "2m";
|
||||
OnUnitActiveSec = "5m";
|
||||
};
|
||||
Install.WantedBy = ["timers.target"];
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -4,17 +4,74 @@
|
||||
config,
|
||||
pkgs,
|
||||
...
|
||||
}: {
|
||||
}: let
|
||||
mountDir = "synced-docs";
|
||||
remoteSubfolder = "synced-docs";
|
||||
remoteName = "ncsynceddocs";
|
||||
remoteNameEnv = lib.toUpper remoteName;
|
||||
# rclone's webdav backend needs the account's Nextcloud *user ID*, not
|
||||
# its login name, in the /dav/files/<uid>/ path -- the two differ on
|
||||
# this server. The login name ("aner") still goes in the `user` field
|
||||
# for Basic Auth. Both the uid and password come from `pass` at service
|
||||
# start, same as mbsync in features/mail, so neither lands in the Nix
|
||||
# store.
|
||||
#
|
||||
# Shares the azos-card-present silent presence check (overlays/default.nix)
|
||||
# with the mail sync timer: loop quietly until the YubiKey is plugged in
|
||||
# (no pinentry prompt either way), then exec rclone in the foreground.
|
||||
# Restart=always means unplugging/replugging the key remounts naturally.
|
||||
nextcloudMountSafe = pkgs.writeShellScript "nextcloud-mount-safe" ''
|
||||
set -eu
|
||||
mountpoint="$HOME/${mountDir}"
|
||||
mkdir -p "$mountpoint"
|
||||
while ! ${pkgs.azos-card-present}/bin/azos-card-present; do
|
||||
${pkgs.coreutils}/bin/sleep 10
|
||||
done
|
||||
password=$(${pkgs.pass}/bin/pass zakobar.com | head -1)
|
||||
uid=$(${pkgs.pass}/bin/pass nextcloud-username/aner | head -1)
|
||||
export RCLONE_CONFIG_${remoteNameEnv}_TYPE=webdav
|
||||
export RCLONE_CONFIG_${remoteNameEnv}_URL="https://nextcloud.zakobar.com/remote.php/dav/files/$uid/"
|
||||
export RCLONE_CONFIG_${remoteNameEnv}_VENDOR=nextcloud
|
||||
export RCLONE_CONFIG_${remoteNameEnv}_USER=aner
|
||||
export RCLONE_CONFIG_${remoteNameEnv}_PASS="$(${pkgs.rclone}/bin/rclone obscure "$password")"
|
||||
exec ${pkgs.rclone}/bin/rclone mount ${remoteName}:${remoteSubfolder} "$mountpoint" \
|
||||
--vfs-cache-mode writes
|
||||
'';
|
||||
in {
|
||||
options.azos.nextcloud-client.enable = lib.mkOption {
|
||||
default = true;
|
||||
type = lib.types.bool;
|
||||
};
|
||||
config = lib.mkIf config.azos.nextcloud-client.enable {
|
||||
options.azos.nextcloud-mount.enable = lib.mkOption {
|
||||
default = true;
|
||||
type = lib.types.bool;
|
||||
};
|
||||
config = lib.mkMerge [
|
||||
(lib.mkIf config.azos.nextcloud-client.enable {
|
||||
home.packages = with pkgs; [nextcloud-client];
|
||||
services.nextcloud-client = {
|
||||
enable = true;
|
||||
startInBackground = false;
|
||||
};
|
||||
})
|
||||
(lib.mkIf config.azos.nextcloud-mount.enable {
|
||||
home.packages = with pkgs; [rclone];
|
||||
# default.target, not graphical-session.target: the latter never
|
||||
# activates under EXWM in this setup (nothing explicitly starts it),
|
||||
# which is why nextcloud-client itself silently never autostarts.
|
||||
systemd.user.services.nextcloud-mount = {
|
||||
Unit.Description = "Mount Nextcloud ${remoteSubfolder} via rclone (waits silently for YubiKey)";
|
||||
Service = {
|
||||
Type = "simple";
|
||||
Environment = "PATH=${pkgs.fuse3}/bin:${pkgs.fuse}/bin";
|
||||
ExecStart = "${nextcloudMountSafe}";
|
||||
ExecStop = "${pkgs.fuse3}/bin/fusermount3 -u %h/${mountDir}";
|
||||
Restart = "always";
|
||||
RestartSec = "10s";
|
||||
};
|
||||
Install.WantedBy = ["default.target"];
|
||||
};
|
||||
})
|
||||
];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
{...}: {
|
||||
config.flake.modules.nixos.printing = {
|
||||
lib,
|
||||
config,
|
||||
pkgs,
|
||||
...
|
||||
}: let
|
||||
webpFilter = pkgs.writeShellScript "webptopdf" ''
|
||||
if [ $# -ge 6 ]; then
|
||||
INPUT="$6"
|
||||
TMPFILE=""
|
||||
else
|
||||
TMPFILE=$(mktemp /tmp/cups-webp-XXXXXX.webp)
|
||||
cat > "$TMPFILE"
|
||||
INPUT="$TMPFILE"
|
||||
fi
|
||||
TMPOUT=$(mktemp /tmp/cups-webp-XXXXXX.pdf)
|
||||
${pkgs.imagemagick}/bin/magick "$INPUT" "$TMPOUT"
|
||||
cat "$TMPOUT"
|
||||
rm -f "$TMPOUT"
|
||||
[ -n "$TMPFILE" ] && rm -f "$TMPFILE"
|
||||
'';
|
||||
|
||||
webpCupsMime = pkgs.runCommand "webp-cups-mime" {} ''
|
||||
mkdir -p $out/lib/cups/filter $out/share/cups/mime
|
||||
cp ${webpFilter} $out/lib/cups/filter/webptopdf
|
||||
chmod +x $out/lib/cups/filter/webptopdf
|
||||
echo 'image/webp webp string(0,"RIFF") string(8,"WEBP")' > $out/share/cups/mime/webp.types
|
||||
echo 'image/webp application/pdf 0 webptopdf' > $out/share/cups/mime/webp.convs
|
||||
'';
|
||||
in {
|
||||
options.azos.printing.enable = lib.mkOption {
|
||||
default = false;
|
||||
example = true;
|
||||
type = lib.types.bool;
|
||||
};
|
||||
|
||||
config = lib.mkIf config.azos.printing.enable {
|
||||
services.printing = {
|
||||
enable = true;
|
||||
drivers = [pkgs.hplip webpCupsMime];
|
||||
};
|
||||
|
||||
environment.systemPackages = [pkgs.hplip];
|
||||
|
||||
services.avahi = {
|
||||
enable = true;
|
||||
nssmdns4 = true;
|
||||
openFirewall = true;
|
||||
};
|
||||
|
||||
users.users.aner.extraGroups = ["lp"];
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -58,3 +58,6 @@ c.fonts.hints = "bold 13px 'LiberationMono'"
|
||||
#Set highdpi
|
||||
c.qt.highdpi = True
|
||||
c.zoom.default = 70
|
||||
|
||||
c.qt.args = ['--disable-gpu']
|
||||
c.qt.environ = {'QT_XCB_GL_INTEGRATION': 'none', 'LIBGL_ALWAYS_SOFTWARE': '1'}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
...
|
||||
}: {
|
||||
options.azos.qutebrowser-config.enable = lib.mkOption {
|
||||
default = true;
|
||||
default = false;
|
||||
type = lib.types.bool;
|
||||
};
|
||||
config = lib.mkIf config.azos.qutebrowser-config.enable {
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
{...}: {
|
||||
config.flake.modules.homeManager.roam-backup = {
|
||||
lib,
|
||||
config,
|
||||
pkgs,
|
||||
...
|
||||
}: {
|
||||
options.azos.roam-backup.enable = lib.mkOption {
|
||||
default = false;
|
||||
example = true;
|
||||
type = lib.types.bool;
|
||||
};
|
||||
|
||||
config = lib.mkIf config.azos.roam-backup.enable {
|
||||
home.packages = [
|
||||
(pkgs.writeShellScriptBin "roam-backup" ''
|
||||
set -e
|
||||
cd "$HOME/roam"
|
||||
git add -A
|
||||
if git diff --cached --quiet; then
|
||||
echo "Nothing to commit."
|
||||
else
|
||||
git commit -m "backup: $(date '+%Y-%m-%d %H:%M')"
|
||||
fi
|
||||
git push --set-upstream origin HEAD
|
||||
'')
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -9,7 +9,7 @@
|
||||
in {
|
||||
options.azos.snx-rs = {
|
||||
enable = lib.mkOption {
|
||||
default = true;
|
||||
default = false;
|
||||
type = lib.types.bool;
|
||||
};
|
||||
server = lib.mkOption {
|
||||
|
||||
Generated
+76
-22
@@ -56,6 +56,22 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-compat": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1767039857,
|
||||
"narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=",
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-parts": {
|
||||
"inputs": {
|
||||
"nixpkgs-lib": "nixpkgs-lib"
|
||||
@@ -79,11 +95,11 @@
|
||||
"nixpkgs-lib": "nixpkgs-lib_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1778716662,
|
||||
"narHash": "sha256-m1Yf0wZ8j1OHjTc2UwHwyQRSnNeSgLJOd7q5Y45hzi4=",
|
||||
"lastModified": 1788450739,
|
||||
"narHash": "sha256-glZLQlzIn1fXH6PazR2iUmTo7kzzyYSshrWhLS9TqCU=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"rev": "f7c1a2d347e4c52d5fb8d10cb4d94b5884e546fb",
|
||||
"rev": "31729ca8cbdb4fa927b34e5f4353e6a83f39e993",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -139,11 +155,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1779969295,
|
||||
"narHash": "sha256-HwIJ3tOcwSMiV75L7KqJXciXR9UfT+d7rwOZMX7cTnA=",
|
||||
"lastModified": 1788651960,
|
||||
"narHash": "sha256-v9wJd32eZ2bvhBzVOd7TIjLQd011P7nwOhjKtWlci5I=",
|
||||
"owner": "nix-community",
|
||||
"repo": "home-manager",
|
||||
"rev": "61e2c9659324181e0f0ed911958c536333b1d4f6",
|
||||
"rev": "2c0350c759688177331b8f5242311fae8877bdb3",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -170,11 +186,11 @@
|
||||
},
|
||||
"import-tree_2": {
|
||||
"locked": {
|
||||
"lastModified": 1778781969,
|
||||
"narHash": "sha256-Jjuz5CmSkur8KvLDoGa+vylEp+RkQtv4mt/qcMznpH0=",
|
||||
"lastModified": 1788467110,
|
||||
"narHash": "sha256-ljEMTXP/rH0tOvDzc9gzwww6KcHRPRnEHzd9lK48V7s=",
|
||||
"owner": "vic",
|
||||
"repo": "import-tree",
|
||||
"rev": "d321337efd0f23a9eb14a42adb7b2c29313ab274",
|
||||
"rev": "eb1b52eaecc57f7c136d07ae8a93e724dfecac46",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -202,12 +218,15 @@
|
||||
}
|
||||
},
|
||||
"nixos-hardware": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs_4"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1779826373,
|
||||
"narHash": "sha256-3sRzgLX86qV5NlhWUAufLmHwkyP03tmL3VdZIM13dEo=",
|
||||
"lastModified": 1788335262,
|
||||
"narHash": "sha256-3jBEq8avfzlrsY4UpW4d5TOmm7ocseZfnitEJhqauyc=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixos-hardware",
|
||||
"rev": "ef4efb84766a166c906bd55759574676bf91267c",
|
||||
"rev": "44d95795ee2d475b3d687325e26dcf4ca9104557",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -217,6 +236,27 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixos-wsl": {
|
||||
"inputs": {
|
||||
"flake-compat": "flake-compat",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1784642409,
|
||||
"narHash": "sha256-hcbDqFuySAJawljt5r0sKBCJKYnbtGD0T/ZIozH1Dq0=",
|
||||
"owner": "nix-community",
|
||||
"repo": "nixos-wsl",
|
||||
"rev": "eaeb18da90024448a60eb1ec7132eafa4003404e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"repo": "nixos-wsl",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1769421245,
|
||||
@@ -248,11 +288,11 @@
|
||||
},
|
||||
"nixpkgs-lib_2": {
|
||||
"locked": {
|
||||
"lastModified": 1777168982,
|
||||
"narHash": "sha256-GOkGPcboWE9BmGCRMLX3worL4EMnsnG8MyKmXNeYuhQ=",
|
||||
"lastModified": 1788057806,
|
||||
"narHash": "sha256-DTQSMxzDWmT0zhguthvegnVkn7CFqGCv4IHCzk5ZUpM=",
|
||||
"owner": "nix-community",
|
||||
"repo": "nixpkgs.lib",
|
||||
"rev": "f5901329dade4a6ea039af1433fb087bd9c1fe14",
|
||||
"rev": "596e2e3940e09b2abbeb03f75fa1828c57fcd72c",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -263,11 +303,11 @@
|
||||
},
|
||||
"nixpkgs-unstable": {
|
||||
"locked": {
|
||||
"lastModified": 1779560665,
|
||||
"narHash": "sha256-tpyBcxPpcQb8ukyNF7DoCwfSY3VPsxHoYwj00Cayv5o=",
|
||||
"lastModified": 1788614874,
|
||||
"narHash": "sha256-7QYjT2vHLuX9Z1pdxHXDKCbh1CR3D/2rywB9Tx0MPRg=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "64c08a7ca051951c8eae34e3e3cb1e202fe36786",
|
||||
"rev": "c043004d1c6985732bcc1cbc5a9c9aecbbb4e0f0",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -311,11 +351,24 @@
|
||||
},
|
||||
"nixpkgs_4": {
|
||||
"locked": {
|
||||
"lastModified": 1779877693,
|
||||
"narHash": "sha256-NOF9NAREhxr50bbBfVcVOq+ArCMSoe8dP79Pk2uyARk=",
|
||||
"lastModified": 1767892417,
|
||||
"narHash": "sha256-8bW3q88CEg2u4hSP66Vf4lpbLonHz7hqDNBMcCY7E9U=",
|
||||
"rev": "3497aa5c9457a9d88d71fa93a4a8368816fbeeba",
|
||||
"type": "tarball",
|
||||
"url": "https://releases.nixos.org/nixos/unstable/nixos-26.05pre924538.3497aa5c9457/nixexprs.tar.xz"
|
||||
},
|
||||
"original": {
|
||||
"type": "tarball",
|
||||
"url": "https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz"
|
||||
}
|
||||
},
|
||||
"nixpkgs_5": {
|
||||
"locked": {
|
||||
"lastModified": 1788549839,
|
||||
"narHash": "sha256-kOrCcSIA6w9J1hX5DqHy2k9pDTJymExTsbV74U9UtCA=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "4100e830e085863741bc69b156ec4ccd53ab5be0",
|
||||
"rev": "17de0b976395537756f30a3e78f2f06e5cec89ed",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -333,7 +386,8 @@
|
||||
"import-tree": "import-tree_2",
|
||||
"musnix": "musnix",
|
||||
"nixos-hardware": "nixos-hardware",
|
||||
"nixpkgs": "nixpkgs_4",
|
||||
"nixos-wsl": "nixos-wsl",
|
||||
"nixpkgs": "nixpkgs_5",
|
||||
"nixpkgs-unstable": "nixpkgs-unstable"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -21,6 +21,11 @@
|
||||
flake = true;
|
||||
};
|
||||
|
||||
nixos-wsl = {
|
||||
url = "github:nix-community/nixos-wsl";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
flake-parts.url = "github:hercules-ci/flake-parts";
|
||||
import-tree.url = "github:vic/import-tree";
|
||||
};
|
||||
@@ -34,6 +39,7 @@
|
||||
./_machines/lauretta.nix
|
||||
./_machines/vm.nix
|
||||
./_machines/beacon.nix
|
||||
./_machines/beacon-wsl.nix
|
||||
];
|
||||
|
||||
systems = [
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
suiteModules.homeManager.lauretta
|
||||
suiteModules.homeManager.audio
|
||||
suiteModules.homeManager.claude
|
||||
suiteModules.homeManager.claude-memory
|
||||
suiteModules.homeManager.claude-skills
|
||||
suiteModules.homeManager.encryption
|
||||
suiteModules.homeManager.git-config
|
||||
suiteModules.homeManager.hfsprogs
|
||||
@@ -29,14 +31,38 @@
|
||||
suiteModules.homeManager.qutebrowser
|
||||
suiteModules.homeManager.reaper
|
||||
suiteModules.homeManager.snx-rs
|
||||
suiteModules.homeManager.roam-backup
|
||||
suiteModules.homeManager.ytdl
|
||||
suiteModules.homeManager.chess
|
||||
suiteModules.homeManager.homey-alert
|
||||
suiteModules.homeManager.leetcode
|
||||
];
|
||||
|
||||
programs.home-manager.enable = true;
|
||||
|
||||
azos.suites.lauretta.enable = true;
|
||||
azos.chess.enable = true;
|
||||
azos.leetcode.enable = true;
|
||||
azos.name = "Aner Zakobar";
|
||||
|
||||
azos.headphones.enable = true;
|
||||
azos.headphones.devices = [
|
||||
{
|
||||
mac = "AC:80:0A:AF:E1:C2";
|
||||
name = "Aner's Headphones";
|
||||
}
|
||||
{
|
||||
mac = "AC:80:0A:A1:5F:53";
|
||||
name = "Chen's Headphones";
|
||||
}
|
||||
{
|
||||
mac = "E4:17:D8:29:9B:16";
|
||||
name = "8bitdo";
|
||||
}
|
||||
];
|
||||
azos.roam-backup.enable = true;
|
||||
azos.homey-alert.enable = true;
|
||||
|
||||
home = {
|
||||
username = "aner";
|
||||
homeDirectory = "/home/aner";
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
{
|
||||
lib,
|
||||
config,
|
||||
pkgs,
|
||||
inputs,
|
||||
suiteModules,
|
||||
...
|
||||
}: {
|
||||
imports = [
|
||||
inputs.nixos-wsl.nixosModules.default
|
||||
suiteModules.nixos.attic
|
||||
];
|
||||
|
||||
wsl.enable = true;
|
||||
wsl.defaultUser = "aner";
|
||||
|
||||
nixpkgs.hostPlatform = "x86_64-linux";
|
||||
nixpkgs.config.allowUnfree = true;
|
||||
|
||||
nix.settings = {
|
||||
experimental-features = ["nix-command" "flakes"];
|
||||
auto-optimise-store = true;
|
||||
substituters = [
|
||||
"https://cache.nixos.org"
|
||||
"https://cuda-maintainers.cachix.org"
|
||||
];
|
||||
trusted-public-keys = [
|
||||
"cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="
|
||||
"cuda-maintainers.cachix.org-1:0dq3bujKpuEPMCX6U4WylrUDZ9JyUG0VpVZa7CNfq5E="
|
||||
];
|
||||
};
|
||||
|
||||
security.sudo.wheelNeedsPassword = false;
|
||||
|
||||
networking.hostName = "beacon";
|
||||
networking.firewall.allowedTCPPorts = [ 22 ];
|
||||
programs.nix-ld.enable = true;
|
||||
time.timeZone = "Asia/Jerusalem";
|
||||
|
||||
# GPU is exposed through WSL2 paravirtualization; no Linux NVIDIA KM needed
|
||||
hardware.graphics.enable = true;
|
||||
|
||||
services.openssh = {
|
||||
enable = true;
|
||||
settings = {
|
||||
PermitRootLogin = "no";
|
||||
PasswordAuthentication = false;
|
||||
};
|
||||
};
|
||||
|
||||
users.users.aner = {
|
||||
isNormalUser = true;
|
||||
extraGroups = ["wheel" "video"];
|
||||
openssh.authorizedKeys.keys = [
|
||||
"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDMdozJewuuqG4VuwGhI5Uw2xDIUrkQwyPGQ2q3tvnPq/VoV+zfdnou42I6XYEfOGd6xfLR0byOGCk1Ia1F/picBsxADEaJFBnTny5MWNA5MxwJIYUPW5+kFN1TmxdNGmj0WJ8qZeKkS3ffixdHlHTVu4Rzrlu8qDdw8TmgcvD7sHpNaBB+hwG9B/s5BAozfXvyXtW51lLLrEZdmUXJxAP51ubVgysZ985PA89o4G7LUE20IKAj3rgylLwq+Lz2PoGmVCptPTknLLzGlxTy2mU/tESJtS4iUvshSYiKak73BSen88xLWL/eZGFzOLW4TNem/qT0ykqK1s1BaGWCcQiDWN+Lpa3XqTbFN054MfhLdEMp+NjrdsCbZORzp9p2ZPDqi5jI1Hy7IN/8VzzB+T13MX6YhW7CBBqUZaivGhVQGsNjrFusPGhCaZrzNJ1l4qssLAlOh9qdaYzqDe+BIHsty55vw4Ph+Axou82babpLoiQzkCol2TWjPr7JSfHFkm6CmoTQ7mK8m7jRCodmxF+BEe4Usr+HrTKDc9fr5P4mdfL0O+ZPrlkPuG1zBTMal9MPxeCk4emzOZf7vBMNzb0aKHbOtSdVk/IcjatlDd2EIo34e5A6anLRMAdxb7901mnCAcjvCn6+ArNV8nr+NBk1+UvumZ+Q59LTrAkgRt3rBw== cardno:29_109_265"
|
||||
];
|
||||
};
|
||||
|
||||
environment.systemPackages = with pkgs; [
|
||||
git
|
||||
rsync
|
||||
tmux
|
||||
vim
|
||||
wget
|
||||
rclone
|
||||
pciutils
|
||||
nvtopPackages.nvidia
|
||||
cudaPackages.cudatoolkit
|
||||
cudaPackages.cudnn
|
||||
cudaPackages.nccl
|
||||
python3
|
||||
direnv
|
||||
];
|
||||
|
||||
azos.attic.enable = true;
|
||||
|
||||
system.stateVersion = "25.11";
|
||||
}
|
||||
@@ -28,7 +28,7 @@
|
||||
nixpkgs.config.cudaSupport = true;
|
||||
|
||||
nix.settings = {
|
||||
experimental-features = "nix-command flakes";
|
||||
experimental-features = ["nix-command" "flakes"];
|
||||
auto-optimise-store = true;
|
||||
substituters = [
|
||||
"https://cache.nixos.org"
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
config.nix.registry;
|
||||
|
||||
nix.settings = {
|
||||
experimental-features = "nix-command flakes";
|
||||
experimental-features = ["nix-command" "flakes"];
|
||||
auto-optimise-store = true;
|
||||
};
|
||||
|
||||
|
||||
+18
-2
@@ -20,12 +20,15 @@
|
||||
suiteModules.nixos.virtualization
|
||||
suiteModules.nixos.binfmt
|
||||
suiteModules.nixos.attic
|
||||
suiteModules.nixos.printing
|
||||
suiteModules.nixos.hardware-auth
|
||||
];
|
||||
|
||||
boot.loader.grub = {
|
||||
enable = true;
|
||||
efiSupport = true;
|
||||
device = "nodev";
|
||||
configurationLimit = 5;
|
||||
};
|
||||
boot.loader.efi.canTouchEfiVariables = true;
|
||||
|
||||
@@ -80,12 +83,25 @@
|
||||
};
|
||||
|
||||
nix.settings = {
|
||||
experimental-features = "nix-command flakes";
|
||||
experimental-features = ["nix-command" "flakes"];
|
||||
auto-optimise-store = true;
|
||||
substituters = [
|
||||
"https://cache.nixos.org"
|
||||
];
|
||||
trusted-public-keys = [
|
||||
"cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY="
|
||||
];
|
||||
};
|
||||
|
||||
azos.suites.exwm.enable = true;
|
||||
azos.attic.enable = true;
|
||||
azos.printing.enable = true;
|
||||
azos.hardware-auth.enable = true;
|
||||
|
||||
# Needed for unprivileged FUSE mounts (e.g. rclone mount) -- without this,
|
||||
# fusermount3 has no setuid wrapper and mounting fails with
|
||||
# "Operation not permitted" even though /dev/fuse itself is world-writable.
|
||||
programs.fuse.enable = true;
|
||||
|
||||
home-manager = {
|
||||
extraSpecialArgs = {inherit inputs outputs suiteModules pkgs;};
|
||||
@@ -108,7 +124,7 @@
|
||||
services.greetd.settings = lib.mkForce {
|
||||
terminal.vt = 1;
|
||||
default_session = {
|
||||
command = "${pkgs.tuigreet}/bin/tuigreet --time --user-menu --cmd '/home/aner/.login.sh'";
|
||||
command = "${pkgs.tuigreet}/bin/tuigreet --time --user-menu --user-menu-min-uid 1000 --user-menu-max-uid 29999 --cmd '/home/aner/.login.sh'";
|
||||
user = "greeter";
|
||||
};
|
||||
initial_session = {
|
||||
|
||||
@@ -11,4 +11,14 @@
|
||||
config.allowUnfree = true;
|
||||
};
|
||||
};
|
||||
|
||||
# Shared silent presence check for the GPG smartcard (the YubiKey's OpenPGP
|
||||
# applet): `gpg --card-status` only queries the reader, never invoking
|
||||
# pinentry, so it's safe to poll without ever producing a "please insert
|
||||
# card" prompt. Used by both the mail sync timer and the Nextcloud mount
|
||||
# service to gate on `pass` being decryptable.
|
||||
config.flake.overlayPkgs.azos-card-present = pkgs:
|
||||
pkgs.writeShellScriptBin "azos-card-present" ''
|
||||
exec ${pkgs.gnupg}/bin/gpg --card-status >/dev/null 2>&1
|
||||
'';
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
{pkgs} @ args: {
|
||||
#TODO since I do not pass along packages, this cannot work
|
||||
# debugTex = import ./debugTex args;
|
||||
default = import ./defaultShell.nix args;
|
||||
}
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
alejandra
|
||||
nh
|
||||
(pkgs.writeShellScriptBin
|
||||
"azos-lauretta-update"
|
||||
"sudo nixos-rebuild switch --flake '.?submodules=1#lauretta'")
|
||||
"nh os switch '.?submodules=1#lauretta'")
|
||||
(pkgs.writeShellScriptBin
|
||||
"azos-update-azos-core"
|
||||
"nix flake update --flake '.?submodules=1' azos-core")
|
||||
|
||||
Reference in New Issue
Block a user