Compare commits
9 Commits
b3380c6ce4
...
e36767b625
| Author | SHA1 | Date | |
|---|---|---|---|
| e36767b625 | |||
| 2585a0e9d3 | |||
| ac6c15f931 | |||
| b880a194ba | |||
| 8b633f598c | |||
| da5df102cd | |||
| 5ec9f2c528 | |||
| 80420198e8 | |||
| 53976751cd |
@@ -83,7 +83,7 @@ azos/
|
|||||||
├── shells/ # Dev shells
|
├── shells/ # Dev shells
|
||||||
└── azos-core/ # Shared feature library (git submodule)
|
└── azos-core/ # Shared feature library (git submodule)
|
||||||
├── flake.nix
|
├── flake.nix
|
||||||
├── features/ # Shared/reusable features (auto-discovered)
|
├── suites/ # Suites, each grouping related sub-features (auto-discovered)
|
||||||
├── overlays/ # Core overlays
|
├── overlays/ # Core overlays
|
||||||
└── _lib/ # Module schema helpers
|
└── _lib/ # Module schema helpers
|
||||||
```
|
```
|
||||||
@@ -93,7 +93,10 @@ Features are auto-discovered by `import-tree` — no central imports file. Each
|
|||||||
then explicitly imported in `home-manager/home.nix` via `suiteModules.homeManager.<name>`.
|
then explicitly imported in `home-manager/home.nix` via `suiteModules.homeManager.<name>`.
|
||||||
|
|
||||||
**Where to put a new feature:**
|
**Where to put a new feature:**
|
||||||
- Shared / reusable across machines → `azos-core/features/<name>/default.nix`
|
- Shared / reusable across machines → inside an existing or new suite directory,
|
||||||
|
`azos-core/suites/<suite>/<name>/default.nix` (registering
|
||||||
|
`config.flake.modules.homeManager.<suite>`) — never a bare file directly under
|
||||||
|
`azos-core/suites/`; every azos-core feature belongs to a suite.
|
||||||
- Machine-specific → `azos/features/<name>/default.nix`
|
- Machine-specific → `azos/features/<name>/default.nix`
|
||||||
|
|
||||||
## Code Style Guidelines
|
## Code Style Guidelines
|
||||||
@@ -125,7 +128,7 @@ All features follow this flake-parts registration pattern:
|
|||||||
|
|
||||||
### Naming Conventions
|
### Naming Conventions
|
||||||
- Option paths: `azos.<module-name>.<option>`
|
- Option paths: `azos.<module-name>.<option>`
|
||||||
- Module names: kebab-case (`git-config`, `claude-memory`)
|
- Module names: kebab-case (`git-config`, `desktop`)
|
||||||
- Feature directories: kebab-case, one `default.nix` per feature
|
- Feature directories: kebab-case, one `default.nix` per feature
|
||||||
- Variables: kebab-case
|
- Variables: kebab-case
|
||||||
|
|
||||||
@@ -171,8 +174,13 @@ Top-level suites group related features (defined in `azos-core/`):
|
|||||||
- `azos.suites.base` — base packages and config
|
- `azos.suites.base` — base packages and config
|
||||||
- `azos.suites.editor` — editor tools
|
- `azos.suites.editor` — editor tools
|
||||||
- `azos.suites.dev` — development tools
|
- `azos.suites.dev` — development tools
|
||||||
- `azos.suites.station` — desktop applications
|
- `azos.suites.desktop` — desktop session plumbing (flameshot, dunst, keyboard layout) plus generally-useful apps (browser, libreoffice, zoom, spotify, gimp, whatsapp). Shared, lives in azos-core — wanted on any GUI machine, work included.
|
||||||
- `azos.suites.exwm` — EXWM window manager
|
- `azos.suites.exwm` — EXWM window manager, depends on `desktop`
|
||||||
|
|
||||||
|
Machine-specific, personal-only apps (steam, discord) are **not** part of any azos-core
|
||||||
|
suite — they live as standalone features directly in `azos/features/`, imported only on
|
||||||
|
machines that want them (e.g. `azos/features/discord`, `azos/features/steam`), since any
|
||||||
|
such machine already uses the `azos` project rather than a different downstream repo.
|
||||||
|
|
||||||
The machine suite (`azos.suites.lauretta`) enables the relevant suites for this machine.
|
The machine suite (`azos.suites.lauretta`) enables the relevant suites for this machine.
|
||||||
|
|
||||||
@@ -193,26 +201,38 @@ specialisation = {
|
|||||||
|
|
||||||
## Adding New Modules
|
## Adding New Modules
|
||||||
|
|
||||||
1. Create `features/<name>/default.nix` (machine-specific) or `azos-core/features/<name>/default.nix` (shared)
|
1. Create `features/<name>/default.nix` (machine-specific) or, for shared features,
|
||||||
|
`azos-core/suites/<suite>/<name>/default.nix` inside an existing or new suite
|
||||||
|
(registering `config.flake.modules.homeManager.<suite>`, not a standalone module name)
|
||||||
2. Use the module template above
|
2. Use the module template above
|
||||||
3. Add `suiteModules.homeManager.<name>` to the imports list in `home-manager/home.nix`
|
3. Machine-specific: add `suiteModules.homeManager.<name>` to the imports list in
|
||||||
|
`home-manager/home.nix`. Shared: no change needed if the suite is already imported.
|
||||||
4. Run `nix fmt`
|
4. Run `nix fmt`
|
||||||
|
|
||||||
> **Submodule gotcha**: New files in `azos-core/` must be `git add`-ed inside the submodule
|
> **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.
|
> before `nix build` will see them — Nix flakes only evaluate git-tracked files.
|
||||||
> ```bash
|
> ```bash
|
||||||
> cd azos-core && git add features/<name>/
|
> cd azos-core && git add suites/<suite>/<name>/
|
||||||
> ```
|
> ```
|
||||||
|
|
||||||
## Claude Integration
|
## Agent Memory/Skills Integration
|
||||||
|
|
||||||
Claude Code is integrated with this environment via several azos-core features:
|
Memory (org-roam) and skills (slash commands/CLAUDE.md) are backend-pluggable, not
|
||||||
|
Claude-specific, even though Claude is currently the only skills backend and one of two
|
||||||
|
memory backends:
|
||||||
|
|
||||||
| Feature | Option | What it does |
|
| 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/suites/base/agent-memory` | `azos.agent-memory.backends.claude.enable` | Registers org-roam-mcp as a global MCP server in `~/.claude.json`; hooks resync org-roam's index on edit and inject project context at session start |
|
||||||
| `azos-core/features/claude-skills` | `azos.claude-skills.enable` | Deploys skills to `~/.claude/commands/` and content to `~/.claude/CLAUDE.md` |
|
| `azos-core/suites/base/agent-memory` | `azos.agent-memory.backends.agent-shell.enable` | Registers org-roam-mcp via `agent-shell-mcp-servers` and resyncs on `file-write` events — applies regardless of which ACP backend (Claude, Gemini CLI, Codex, ...) a given agent-shell session runs. Also marks Claude sessions agent-shell launches (`AZOS_AGENT_SHELL_SESSION=1`) so the claude backend's own hooks no-op for those sessions instead of double-acting |
|
||||||
| `azos/features/claude` | `azos.claude.enable` | Installs claude-code, auto-enables the above two |
|
| `azos-core/suites/base/agent-skills` | `azos.agent-skills.backends.claude.enable` | Deploys skills to `~/.claude/commands/` and content to `~/.claude/CLAUDE.md` |
|
||||||
|
| `azos/features/claude` | `azos.claude.enable` | Installs claude-code, auto-enables the claude backend for both of the above |
|
||||||
|
| `azos-core/suites/editor/emacs` | (bundled, not a separate option) | Installs agent-shell, auto-enables `azos.agent-memory.backends.agent-shell.enable` |
|
||||||
|
|
||||||
|
Both agent-memory and agent-skills register `config.flake.modules.homeManager.base`
|
||||||
|
(they live inside the `base` suite, since that's where agent-shell is introduced) — not
|
||||||
|
standalone module names, so no separate `suiteModules.homeManager.*` import is needed for
|
||||||
|
them in `home-manager/home.nix`.
|
||||||
|
|
||||||
### Adding Claude skills or standing instructions
|
### Adding Claude skills or standing instructions
|
||||||
From any module — no need to touch azos-core:
|
From any module — no need to touch azos-core:
|
||||||
@@ -223,7 +243,9 @@ azos.claude.globalMdSections.my-rule = ''
|
|||||||
Always do X when Y.
|
Always do X when Y.
|
||||||
'';
|
'';
|
||||||
```
|
```
|
||||||
See `azos-core/features/claude-skills/README.md` for full documentation.
|
`globalSkills`/`globalMdSections` stay Claude-specific (`~/.claude/commands/`,
|
||||||
|
`~/.claude/CLAUDE.md`) deliberately — no other backend has a uniform equivalent yet. See
|
||||||
|
`azos-core/suites/base/agent-skills/README.md` for full documentation.
|
||||||
|
|
||||||
## Key Files
|
## Key Files
|
||||||
|
|
||||||
@@ -236,10 +258,10 @@ See `azos-core/features/claude-skills/README.md` for full documentation.
|
|||||||
| `nixos/configuration-vm.nix` | Test VM config |
|
| `nixos/configuration-vm.nix` | Test VM config |
|
||||||
| `overlays/` | Package overlays |
|
| `overlays/` | Package overlays |
|
||||||
| `azos-core/` | Shared feature library (git submodule) |
|
| `azos-core/` | Shared feature library (git submodule) |
|
||||||
| `azos-core/features/` | Shared features (auto-discovered by import-tree) |
|
| `azos-core/suites/` | Suites, each grouping related shared sub-features (auto-discovered by import-tree) |
|
||||||
| `features/` | Machine-specific 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/suites/editor/emacs/config.org` | Literate Emacs config |
|
||||||
| `azos-core/features/claude-skills/README.md` | Claude skills extensibility docs |
|
| `azos-core/suites/base/claude-skills/README.md` | Claude skills extensibility docs |
|
||||||
|
|
||||||
## Common Tasks
|
## Common Tasks
|
||||||
|
|
||||||
|
|||||||
+1
-1
Submodule azos-core updated: 5669ae0692...2ba6b6a5d0
@@ -1,41 +0,0 @@
|
|||||||
{...}: {
|
|
||||||
config.flake.modules.homeManager.audio = {
|
|
||||||
lib,
|
|
||||||
config,
|
|
||||||
...
|
|
||||||
}: {
|
|
||||||
options.azos.home-audio.enable = lib.mkOption {
|
|
||||||
default = true;
|
|
||||||
type = lib.types.bool;
|
|
||||||
};
|
|
||||||
config = lib.mkIf config.azos.home-audio.enable {};
|
|
||||||
};
|
|
||||||
|
|
||||||
config.flake.modules.nixos.audio = {
|
|
||||||
lib,
|
|
||||||
config,
|
|
||||||
pkgs,
|
|
||||||
...
|
|
||||||
}: {
|
|
||||||
options.azos.hardware-audio.enable = lib.mkOption {
|
|
||||||
default = true;
|
|
||||||
type = lib.types.bool;
|
|
||||||
};
|
|
||||||
config = lib.mkIf config.azos.hardware-audio.enable {
|
|
||||||
services = {
|
|
||||||
pipewire = {
|
|
||||||
enable = true;
|
|
||||||
audio.enable = true;
|
|
||||||
pulse.enable = true;
|
|
||||||
alsa = {
|
|
||||||
enable = true;
|
|
||||||
support32Bit = true;
|
|
||||||
};
|
|
||||||
jack.enable = true;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
environment.systemPackages = with pkgs; [pavucontrol];
|
|
||||||
musnix.enable = true;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
{...}: {
|
|
||||||
config.flake.modules.nixos.binfmt = {
|
|
||||||
lib,
|
|
||||||
config,
|
|
||||||
...
|
|
||||||
}: {
|
|
||||||
options.azos.binfmt.enable = lib.mkOption {
|
|
||||||
default = true;
|
|
||||||
type = lib.types.bool;
|
|
||||||
};
|
|
||||||
config = lib.mkIf config.azos.binfmt.enable {
|
|
||||||
boot.binfmt.emulatedSystems = ["aarch64-linux"];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
{...}: {
|
|
||||||
config.flake.modules.nixos.bluetooth = {
|
|
||||||
lib,
|
|
||||||
config,
|
|
||||||
...
|
|
||||||
}: {
|
|
||||||
options.azos.bluetooth.enable = lib.mkOption {
|
|
||||||
default = true;
|
|
||||||
type = lib.types.bool;
|
|
||||||
};
|
|
||||||
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
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
{...}: {
|
|
||||||
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"];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
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
|
|
||||||
];
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
#+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
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
{...}: {
|
|
||||||
config.flake.overlayPkgs.azos-emacs-chess = pkgs: pkgs.localEmacsPkg ./_pkg.nix;
|
|
||||||
}
|
|
||||||
@@ -11,8 +11,8 @@
|
|||||||
};
|
};
|
||||||
config = lib.mkIf config.azos.claude.enable {
|
config = lib.mkIf config.azos.claude.enable {
|
||||||
home.packages = with pkgs; [claude-code claude-agent-acp];
|
home.packages = with pkgs; [claude-code claude-agent-acp];
|
||||||
azos.claude-memory.enable = lib.mkDefault true;
|
azos.agent-memory.backends.claude.enable = lib.mkDefault true;
|
||||||
azos.claude-skills.enable = lib.mkDefault true;
|
azos.agent-skills.backends.claude.enable = lib.mkDefault true;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{...}: {
|
||||||
|
config.flake.modules.homeManager.discord = {
|
||||||
|
lib,
|
||||||
|
config,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}: {
|
||||||
|
options.azos.discord.enable = lib.mkOption {
|
||||||
|
default = true;
|
||||||
|
type = lib.types.bool;
|
||||||
|
};
|
||||||
|
config = lib.mkIf config.azos.discord.enable {
|
||||||
|
home.packages = with pkgs; [discord];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
{...}: {
|
|
||||||
config.flake.modules.homeManager.encryption = {
|
|
||||||
lib,
|
|
||||||
config,
|
|
||||||
pkgs,
|
|
||||||
...
|
|
||||||
}: {
|
|
||||||
options.azos.encryption.enable = lib.mkOption {
|
|
||||||
default = true;
|
|
||||||
type = lib.types.bool;
|
|
||||||
};
|
|
||||||
config = lib.mkIf config.azos.encryption.enable {
|
|
||||||
programs.password-store = {
|
|
||||||
enable = true;
|
|
||||||
settings = {
|
|
||||||
PASSWORD_STORE_KEY = "26B851F81F8F7C55";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
home.packages = with pkgs; [
|
|
||||||
yubikey-personalization
|
|
||||||
];
|
|
||||||
programs.gpg = {
|
|
||||||
enable = true;
|
|
||||||
};
|
|
||||||
programs.browserpass = {
|
|
||||||
enable = true;
|
|
||||||
browsers = ["chromium"];
|
|
||||||
};
|
|
||||||
services.gpg-agent = {
|
|
||||||
enable = true;
|
|
||||||
enableSshSupport = true;
|
|
||||||
grabKeyboardAndMouse = false;
|
|
||||||
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 +0,0 @@
|
|||||||
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 +0,0 @@
|
|||||||
IdentityAgent ${XDG_RUNTIME_DIR}/gnupg/S.gpg-agent.ssh
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
37710BAB5F6DC9878A450CDA8A4661D932C04ACB
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
{...}: {
|
|
||||||
config.flake.modules.homeManager.git-config = {
|
|
||||||
lib,
|
|
||||||
config,
|
|
||||||
pkgs,
|
|
||||||
...
|
|
||||||
}: {
|
|
||||||
options.azos.git-config.enable = lib.mkOption {
|
|
||||||
default = true;
|
|
||||||
type = lib.types.bool;
|
|
||||||
};
|
|
||||||
config = lib.mkIf config.azos.git-config.enable {
|
|
||||||
home.packages = with pkgs; [pass-git-helper unzip];
|
|
||||||
programs.git = {
|
|
||||||
enable = true;
|
|
||||||
signing = {
|
|
||||||
key = "BB141234A3DD953D";
|
|
||||||
signByDefault = true;
|
|
||||||
};
|
|
||||||
settings = {
|
|
||||||
credential.helper = "!pass-git-helper $@";
|
|
||||||
user = {
|
|
||||||
name = "Aner Zakobar";
|
|
||||||
email = "aner@zakobar.com";
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
home.file.".config/pass-git-helper/git-pass-mapping.ini".source =
|
|
||||||
./pass-git-mapping.ini;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
[*git.zakobar.com*]
|
|
||||||
target=zakobar.com
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
{...}: {
|
|
||||||
config.flake.modules.homeManager.headphones = {
|
|
||||||
lib,
|
|
||||||
config,
|
|
||||||
pkgs,
|
|
||||||
...
|
|
||||||
}: {
|
|
||||||
options.azos.headphones = {
|
|
||||||
enable = lib.mkOption {
|
|
||||||
default = false;
|
|
||||||
example = true;
|
|
||||||
type = lib.types.bool;
|
|
||||||
};
|
|
||||||
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
|
|
||||||
'')
|
|
||||||
];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
{...}: {
|
|
||||||
config.flake.modules.homeManager.hfsprogs = {
|
|
||||||
lib,
|
|
||||||
config,
|
|
||||||
pkgs,
|
|
||||||
...
|
|
||||||
}: {
|
|
||||||
options.azos.hfsprogs.enable = lib.mkOption {
|
|
||||||
default = true;
|
|
||||||
type = lib.types.bool;
|
|
||||||
};
|
|
||||||
config = lib.mkIf config.azos.hfsprogs.enable {
|
|
||||||
home.packages = with pkgs; [hfsprogs];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,191 +1,28 @@
|
|||||||
{...}: {
|
{...}: {
|
||||||
config.flake.modules.homeManager.homey-alert = {
|
config.flake.overlayPkgs.azos-homey-ntfy-alert = pkgs:
|
||||||
lib,
|
pkgs.writeShellApplication {
|
||||||
config,
|
name = "azos-homey-ntfy-alert";
|
||||||
pkgs,
|
runtimeInputs = with pkgs; [curl pass];
|
||||||
...
|
|
||||||
}: {
|
|
||||||
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 = ''
|
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)"
|
TOKEN="$(pass zakobar-homecenter/ntfy-token)"
|
||||||
|
|
||||||
# Build the curl invocation as an array — single source of truth for both modes
|
|
||||||
CMD=(
|
CMD=(
|
||||||
curl -sf
|
curl -sf
|
||||||
-H "Authorization: Bearer $TOKEN"
|
-H "Authorization: Bearer $TOKEN"
|
||||||
-H "Priority: $PRIORITY"
|
-H "Priority: ''${PRIORITY:-default}"
|
||||||
)
|
)
|
||||||
[[ -n "$TITLE" ]] && CMD+=(-H "Title: $TITLE")
|
[[ -n "''${TITLE:-}" ]] && CMD+=(-H "Title: $TITLE")
|
||||||
[[ -n "$TAGS" ]] && CMD+=(-H "Tags: $TAGS")
|
[[ -n "''${TAGS:-}" ]] && CMD+=(-H "Tags: $TAGS")
|
||||||
CMD+=(-d "$MESSAGE" https://ntfy.zakobar.com/alerts)
|
CMD+=(-d "''${MESSAGE:-}" https://ntfy.zakobar.com/alerts)
|
||||||
|
|
||||||
if $PRINT_REMOTE; then
|
if [[ -n "''${AZOS_SEND_ALERT_PRINT_REMOTE:-}" ]]; then
|
||||||
# Shell-quote each element so the output is safe to embed in SSH commands
|
# Shell-quote each element so the output is safe to embed in SSH commands --
|
||||||
|
# the token is already resolved above, so the remote host only needs curl.
|
||||||
printf '%q ' "''${CMD[@]}"
|
printf '%q ' "''${CMD[@]}"
|
||||||
printf '\n'
|
printf '\n'
|
||||||
else
|
else
|
||||||
"''${CMD[@]}" || true
|
"''${CMD[@]}"
|
||||||
|
|
||||||
# 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
|
fi
|
||||||
'';
|
'';
|
||||||
})
|
|
||||||
];
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
# 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`
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
{...}: {
|
|
||||||
config.flake.modules.homeManager.kubernetes = {
|
|
||||||
lib,
|
|
||||||
config,
|
|
||||||
pkgs,
|
|
||||||
...
|
|
||||||
}: {
|
|
||||||
options.azos.kubectl.enable = lib.mkOption {
|
|
||||||
default = false;
|
|
||||||
type = lib.types.bool;
|
|
||||||
};
|
|
||||||
config = lib.mkIf config.azos.kubectl.enable {
|
|
||||||
home.packages = with pkgs; [kubectl kubernetes-helm velero];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -12,7 +12,7 @@ orgTrivialBuild {
|
|||||||
pkgs.azos-emacs-dev
|
pkgs.azos-emacs-dev
|
||||||
pkgs.azos-emacs-editor
|
pkgs.azos-emacs-editor
|
||||||
pkgs.azos-emacs-exwm
|
pkgs.azos-emacs-exwm
|
||||||
pkgs.azos-emacs-station
|
pkgs.azos-emacs-desktop
|
||||||
|
|
||||||
agent-shell
|
agent-shell
|
||||||
elfeed-protocol
|
elfeed-protocol
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
(require 'azos-emacs-dev)
|
(require 'azos-emacs-dev)
|
||||||
(require 'azos-emacs-editor)
|
(require 'azos-emacs-editor)
|
||||||
(require 'azos-emacs-exwm)
|
(require 'azos-emacs-exwm)
|
||||||
(require 'azos-emacs-station)
|
(require 'azos-emacs-desktop)
|
||||||
#+end_src
|
#+end_src
|
||||||
|
|
||||||
* Lauretta specific
|
* Lauretta specific
|
||||||
|
|||||||
@@ -27,54 +27,15 @@
|
|||||||
certificatesFile = "/etc/ssl/certs/ca-certificates.crt";
|
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 {
|
in {
|
||||||
options.azos.mail.enable = lib.mkOption {
|
config = {
|
||||||
default = true;
|
azos.mail.enable = true;
|
||||||
type = lib.types.bool;
|
|
||||||
};
|
# mbsync's PassCmd shells out to `pass`, which needs the GPG smartcard to
|
||||||
config = lib.mkIf config.azos.mail.enable {
|
# decrypt -- only meaningful to gate on when the smartcard/YubiKey setup
|
||||||
programs = {
|
# is actually in play.
|
||||||
mu.enable = true;
|
azos.mail.presenceCheckCommand = lib.mkIf config.azos.encryption.yubikey.enable "${pkgs.azos-card-present}/bin/azos-card-present";
|
||||||
mbsync.enable = true;
|
|
||||||
msmtp.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 = {
|
accounts.email.accounts = {
|
||||||
gmail =
|
gmail =
|
||||||
default_gmail_params
|
default_gmail_params
|
||||||
@@ -106,21 +67,6 @@
|
|||||||
passwordCommand = "pass privateemail.com | head -1";
|
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"];
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,12 +23,12 @@
|
|||||||
nextcloudMountSafe = pkgs.writeShellScript "nextcloud-mount-safe" ''
|
nextcloudMountSafe = pkgs.writeShellScript "nextcloud-mount-safe" ''
|
||||||
set -eu
|
set -eu
|
||||||
mountpoint="$HOME/${mountDir}"
|
mountpoint="$HOME/${mountDir}"
|
||||||
mkdir -p "$mountpoint"
|
${pkgs.coreutils}/bin/mkdir -p "$mountpoint"
|
||||||
while ! ${pkgs.azos-card-present}/bin/azos-card-present; do
|
while ! ${pkgs.azos-card-present}/bin/azos-card-present; do
|
||||||
${pkgs.coreutils}/bin/sleep 10
|
${pkgs.coreutils}/bin/sleep 10
|
||||||
done
|
done
|
||||||
password=$(${pkgs.pass}/bin/pass zakobar.com | head -1)
|
password=$(${pkgs.pass}/bin/pass zakobar.com | ${pkgs.coreutils}/bin/head -1)
|
||||||
uid=$(${pkgs.pass}/bin/pass nextcloud-username/aner | head -1)
|
uid=$(${pkgs.pass}/bin/pass nextcloud-username/aner | ${pkgs.coreutils}/bin/head -1)
|
||||||
export RCLONE_CONFIG_${remoteNameEnv}_TYPE=webdav
|
export RCLONE_CONFIG_${remoteNameEnv}_TYPE=webdav
|
||||||
export RCLONE_CONFIG_${remoteNameEnv}_URL="https://nextcloud.zakobar.com/remote.php/dav/files/$uid/"
|
export RCLONE_CONFIG_${remoteNameEnv}_URL="https://nextcloud.zakobar.com/remote.php/dav/files/$uid/"
|
||||||
export RCLONE_CONFIG_${remoteNameEnv}_VENDOR=nextcloud
|
export RCLONE_CONFIG_${remoteNameEnv}_VENDOR=nextcloud
|
||||||
@@ -63,9 +63,9 @@
|
|||||||
Unit.Description = "Mount Nextcloud ${remoteSubfolder} via rclone (waits silently for YubiKey)";
|
Unit.Description = "Mount Nextcloud ${remoteSubfolder} via rclone (waits silently for YubiKey)";
|
||||||
Service = {
|
Service = {
|
||||||
Type = "simple";
|
Type = "simple";
|
||||||
Environment = "PATH=${pkgs.fuse3}/bin:${pkgs.fuse}/bin";
|
Environment = "PATH=/run/wrappers/bin:${pkgs.fuse3}/bin:${pkgs.fuse}/bin";
|
||||||
ExecStart = "${nextcloudMountSafe}";
|
ExecStart = "${nextcloudMountSafe}";
|
||||||
ExecStop = "${pkgs.fuse3}/bin/fusermount3 -u %h/${mountDir}";
|
ExecStop = "/run/wrappers/bin/fusermount3 -u %h/${mountDir}";
|
||||||
Restart = "always";
|
Restart = "always";
|
||||||
RestartSec = "10s";
|
RestartSec = "10s";
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
{...}: {
|
|
||||||
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"];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -10,6 +10,14 @@
|
|||||||
};
|
};
|
||||||
config = lib.mkIf config.azos.steam.enable {
|
config = lib.mkIf config.azos.steam.enable {
|
||||||
programs.steam.enable = true;
|
programs.steam.enable = true;
|
||||||
|
|
||||||
|
# HORI STICK A fight stick (0f0d:0040) is missing from Steam's bundled
|
||||||
|
# 60-steam-input.rules, so Steam can't open its hidraw node for Steam
|
||||||
|
# Input and falls back to a generic joystick mapping that drops BTN_C
|
||||||
|
# (Circle). Grant the same uaccess the other HORI devices get.
|
||||||
|
services.udev.extraRules = ''
|
||||||
|
KERNEL=="hidraw*", ATTRS{idVendor}=="0f0d", ATTRS{idProduct}=="0040", MODE="0660", TAG+="uaccess"
|
||||||
|
'';
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
{...}: {
|
|
||||||
config.flake.modules.nixos.virtualization = {
|
|
||||||
lib,
|
|
||||||
config,
|
|
||||||
pkgs,
|
|
||||||
...
|
|
||||||
}: {
|
|
||||||
options.azos.virtualization.enable = lib.mkOption {
|
|
||||||
default = true;
|
|
||||||
type = lib.types.bool;
|
|
||||||
};
|
|
||||||
config = lib.mkIf config.azos.virtualization.enable {
|
|
||||||
virtualisation.libvirtd = {
|
|
||||||
enable = true;
|
|
||||||
qemu = {
|
|
||||||
swtpm.enable = true;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
environment.systemPackages = with pkgs; [
|
|
||||||
qemu_kvm
|
|
||||||
libvirt
|
|
||||||
virt-manager
|
|
||||||
virt-viewer
|
|
||||||
];
|
|
||||||
services.spice-vdagentd.enable = true;
|
|
||||||
networking.firewall.allowedTCPPorts = [5900 5901];
|
|
||||||
networking.firewall.allowedUDPPorts = [5900 5901];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
+22
-12
@@ -11,19 +11,12 @@
|
|||||||
suiteModules.homeManager.base
|
suiteModules.homeManager.base
|
||||||
suiteModules.homeManager.dev
|
suiteModules.homeManager.dev
|
||||||
suiteModules.homeManager.editor
|
suiteModules.homeManager.editor
|
||||||
suiteModules.homeManager.station
|
suiteModules.homeManager.desktop
|
||||||
|
suiteModules.homeManager.discord
|
||||||
suiteModules.homeManager.exwm
|
suiteModules.homeManager.exwm
|
||||||
suiteModules.homeManager.lauretta
|
suiteModules.homeManager.lauretta
|
||||||
suiteModules.homeManager.audio
|
|
||||||
suiteModules.homeManager.claude
|
suiteModules.homeManager.claude
|
||||||
suiteModules.homeManager.claude-memory
|
|
||||||
suiteModules.homeManager.claude-skills
|
|
||||||
suiteModules.homeManager.encryption
|
|
||||||
suiteModules.homeManager.git-config
|
|
||||||
suiteModules.homeManager.hfsprogs
|
|
||||||
suiteModules.homeManager.headphones
|
|
||||||
suiteModules.homeManager.hyprland
|
suiteModules.homeManager.hyprland
|
||||||
suiteModules.homeManager.kubernetes
|
|
||||||
suiteModules.homeManager.mail
|
suiteModules.homeManager.mail
|
||||||
suiteModules.homeManager.mpris-proxy
|
suiteModules.homeManager.mpris-proxy
|
||||||
suiteModules.homeManager.nextcloud
|
suiteModules.homeManager.nextcloud
|
||||||
@@ -33,8 +26,6 @@
|
|||||||
suiteModules.homeManager.snx-rs
|
suiteModules.homeManager.snx-rs
|
||||||
suiteModules.homeManager.roam-backup
|
suiteModules.homeManager.roam-backup
|
||||||
suiteModules.homeManager.ytdl
|
suiteModules.homeManager.ytdl
|
||||||
suiteModules.homeManager.chess
|
|
||||||
suiteModules.homeManager.homey-alert
|
|
||||||
suiteModules.homeManager.leetcode
|
suiteModules.homeManager.leetcode
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -61,7 +52,26 @@
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
azos.roam-backup.enable = true;
|
azos.roam-backup.enable = true;
|
||||||
azos.homey-alert.enable = true;
|
|
||||||
|
azos.send-alert.enable = true;
|
||||||
|
azos.send-alert.remoteCommand = "${pkgs.azos-homey-ntfy-alert}/bin/azos-homey-ntfy-alert";
|
||||||
|
|
||||||
|
azos.git-config.userName = "Aner Zakobar";
|
||||||
|
azos.git-config.userEmail = "aner@zakobar.com";
|
||||||
|
azos.git-config.signing.enable = true;
|
||||||
|
azos.git-config.signing.key = "BB141234A3DD953D";
|
||||||
|
azos.git-config.passCredentialHelper.enable = true;
|
||||||
|
azos.git-config.passCredentialHelper.mapping = ''
|
||||||
|
[*git.zakobar.com*]
|
||||||
|
target=zakobar.com
|
||||||
|
'';
|
||||||
|
|
||||||
|
azos.encryption.passwordStore.gpgKeyId = "26B851F81F8F7C55";
|
||||||
|
azos.encryption.yubikey.enable = true;
|
||||||
|
azos.encryption.browserpass.enable = true;
|
||||||
|
azos.encryption.gpgSsh.enable = true;
|
||||||
|
azos.encryption.gpgSsh.keygrip = "37710BAB5F6DC9878A450CDA8A4661D932C04ACB";
|
||||||
|
azos.encryption.gpgSsh.publicKey = "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";
|
||||||
|
|
||||||
home = {
|
home = {
|
||||||
username = "aner";
|
username = "aner";
|
||||||
|
|||||||
@@ -14,13 +14,10 @@
|
|||||||
inputs.home-manager.nixosModules.home-manager
|
inputs.home-manager.nixosModules.home-manager
|
||||||
suiteModules.nixos.base
|
suiteModules.nixos.base
|
||||||
suiteModules.nixos.exwm
|
suiteModules.nixos.exwm
|
||||||
suiteModules.nixos.audio
|
suiteModules.nixos.desktop
|
||||||
suiteModules.nixos.bluetooth
|
suiteModules.nixos.dev
|
||||||
suiteModules.nixos.steam
|
suiteModules.nixos.steam
|
||||||
suiteModules.nixos.virtualization
|
|
||||||
suiteModules.nixos.binfmt
|
|
||||||
suiteModules.nixos.attic
|
suiteModules.nixos.attic
|
||||||
suiteModules.nixos.printing
|
|
||||||
suiteModules.nixos.hardware-auth
|
suiteModules.nixos.hardware-auth
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -96,6 +93,7 @@
|
|||||||
azos.suites.exwm.enable = true;
|
azos.suites.exwm.enable = true;
|
||||||
azos.attic.enable = true;
|
azos.attic.enable = true;
|
||||||
azos.printing.enable = true;
|
azos.printing.enable = true;
|
||||||
|
azos.printing.username = "aner";
|
||||||
azos.hardware-auth.enable = true;
|
azos.hardware-auth.enable = true;
|
||||||
|
|
||||||
# Needed for unprivileged FUSE mounts (e.g. rclone mount) -- without this,
|
# Needed for unprivileged FUSE mounts (e.g. rclone mount) -- without this,
|
||||||
|
|||||||
Reference in New Issue
Block a user