Compare commits

..

11 Commits

Author SHA1 Message Date
ec5e77f796 Prompt for backup location during re-onboard instead of skipping
When step 4 finds an existing backup repo, show the current location
and ask to reconfigure with the existing path as default, rather than
silently skipping the step.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 13:51:24 -05:00
3be9586238 Fix design issues and add comprehensive test suite
Guard doSetup() against re-initialization, route welcome to onboard
wizard, fix import confirmation logic, remove invalid syncthing
generate flags, and show full export path. Add shell-based integration
test suite (123 tests) at ~/src/test/pal/ covering all commands.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 13:50:04 -05:00
4a5fa4415c Fix interactive input by using Console.readLine instead of Process.exec
Process.exec uses popen(cmd, "r") which creates a read-only pipe where
the subprocess stdin is disconnected from the terminal. Console.readLine
reads directly from the parent process stdin via fgets, fixing all
interactive prompts (askConfirm, askInput, choice menus).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 23:48:33 -05:00
d30d2efa4e Rename grapho to pal, add onboard command, fix interactive input
- Rename grapho → pal across entire codebase (CLI, NixOS module,
  flake, docs, config paths)
- Add `pal onboard` interactive setup wizard with 4 steps:
  device, config repo, sync, backups
- Shows current setup summary when re-running onboard on an
  existing installation with warnings about what will change
- Fix askConfirm/askInput to read from /dev/tty so interactive
  prompts work correctly
- Remove || operator usage in askConfirm (not reliable in Lux)
- Add pal package to nix develop shell
- Document ~/.config/pal/ directory structure in README

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 22:38:37 -05:00
05c04b209c Clean up repo: gitignore binaries, remove stale docs
- Add compiled binaries (grapho, *_test) to .gitignore
- Remove grapho binary from tracking (build from source)
- Delete obsolete LUX-LIMITATIONS.md
- Minor README wording fix

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-16 23:05:36 -05:00
13fe22a804 Improve server mount help with detailed instructions
- Add SSHFS section with common options explained
- Add NFS mount example
- Reference NixOS wiki for declarative mounts
- Show unmount hint

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-16 22:58:19 -05:00
dedfbfce64 Add QR code support for Device IDs
- Add printQR() helper function using qrencode
- Display QR code in sync setup for easy mobile pairing
- Add qrencode to doctor dependency check

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-16 22:28:10 -05:00
d3d720b3bc Modernize CLI UX with gh/cargo-style polish
- Add --version/-V and per-subcommand --help
- Replace logo spam with compact status dashboard
- Add aligned step output (label    done format)
- Add confirmation prompts for destructive operations
- Interactive first-run wizard when uninitialized
- Consistent color language and status line formatting
- Clean help with USAGE/COMMANDS/OPTIONS structure

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-16 22:15:35 -05:00
afe7826d58 Complete self-contained grapho CLI with all 8 phases
Phase 5 (Server):
- grapho server setup/mount/unmount/ls commands
- SSHFS/NFS mount instructions

Phase 6 (SQLite):
- State database for event tracking
- grapho history command
- Events logged for sync/backup operations

Phase 7 (Export):
- grapho export creates tar.zst archive
- grapho import restores from archive
- Full data portability between machines

Phase 8 (Dashboard):
- grapho dashboard generates HTML status page
- Dark theme, mobile-friendly
- Can be served with python http.server

Self-contained improvements:
- grapho setup now auto-initializes Syncthing
- grapho backup init <repo> runs restic init directly
- grapho backup runs restic backup directly
- grapho backup list shows snapshots directly
- All configs saved to ~/.config/grapho/

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-16 06:56:24 -05:00
117e6af528 Implement self-contained grapho architecture with four data types
Major rewrite of grapho CLI to support:
- Type 1 (Config): grapho init <repo-url> clones NixOS config
- Type 2 (Sync): Isolated Syncthing on port 8385 (separate from system)
- Type 3 (Backup): Restic integration with systemd timer
- Type 4 (Server): Mount point for central server data

New features:
- Welcome flow on first run (detects ~/.config/grapho/grapho.toml)
- grapho setup wizard creates directory structure
- grapho sync/backup/server subcommands
- grapho status shows all four data types
- grapho doctor checks system health

Added modules/grapho.nix NixOS module:
- Configures isolated Syncthing (ports 8385, 22001, 21028)
- Sets up grapho-backup systemd service and timer
- Creates directory structure via tmpfiles
- Optional NFS server mount

Updated flake.nix:
- Export grapho NixOS module
- Add grapho CLI package (nix build .#grapho)

Documented additional Lux language limitations:
- String == comparison broken in C backend
- let _ = pattern not supported
- List literals with recursion cause segfaults

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-16 06:12:58 -05:00
63fedfb525 Add grapho CLI with improved UX
New CLI features:
- One-liner health check as default (grapho)
- Component status dashboard (grapho status)
- Verbose mode with details (grapho status -v)
- System diagnostics with fix commands (grapho doctor)
- Machine-readable output (grapho --json)
- Actionable fix suggestions for all warnings/errors

Also adds documentation:
- docs/MARKDOWN-EDITORS.md - Editor recommendations for mobile/desktop
- docs/LUX-LIMITATIONS.md - Tracking Lux language issues

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-16 01:06:55 -05:00
8 changed files with 2501 additions and 23 deletions

4
.gitignore vendored
View File

@@ -3,6 +3,10 @@ result
result-*
.direnv/
# Compiled binaries
pal
*_test
# Secrets (NEVER commit unencrypted secrets)
secrets/*.yaml
!secrets/secrets.yaml.example

View File

@@ -1,6 +1,6 @@
# Ultimate Notetaking, Sync & Backup System
A NixOS-based system for managing the three types of data in a computer:
A NixOS-based system for managing the three types of data across devices:
| Tier | Type | Examples | Sync Model |
|------|------|----------|------------|
@@ -12,11 +12,11 @@ A NixOS-based system for managing the three types of data in a computer:
```bash
# One-command setup (public repo, no SSH key needed)
nix run 'git+https://git.qrty.ink/blu/grapho.git'
nix run 'git+https://git.qrty.ink/blu/pal.git'
# Or clone first, then run
git clone https://git.qrty.ink/blu/grapho.git
cd grapho
git clone https://git.qrty.ink/blu/pal.git
cd pal
nix run .
```
@@ -26,8 +26,8 @@ nix run .
```bash
# 1. Clone the repo
git clone https://git.qrty.ink/blu/grapho.git
cd grapho
git clone https://git.qrty.ink/blu/pal.git
cd pal
# 2. Run setup (one command - includes all dependencies)
nix run .
@@ -39,18 +39,18 @@ nix run .
# 4. (Optional) Set up SSH for push access
# Add your SSH key to Gitea: https://git.qrty.ink/user/settings/keys
# Then switch to SSH remote:
git remote set-url origin ssh://git@git.qrty.ink:2222/blu/grapho.git
git remote set-url origin ssh://git@git.qrty.ink:2222/blu/pal.git
```
### Additional Computers (Joining)
```bash
# One command (no SSH key needed for public repo)
nix run 'git+https://git.qrty.ink/blu/grapho.git'
nix run 'git+https://git.qrty.ink/blu/pal.git'
# Choose option [2], enter your config git URL and age key
# Or clone first:
git clone https://git.qrty.ink/blu/grapho.git && cd grapho
git clone https://git.qrty.ink/blu/pal.git && cd pal
nix run . -- <config-git-url> <your-age-key>
```
@@ -78,10 +78,10 @@ Add to your flake.nix inputs, then import the module:
```nix
{
inputs.grapho.url = "git+https://git.qrty.ink/blu/grapho.git";
inputs.pal.url = "git+https://git.qrty.ink/blu/pal.git";
# In your configuration:
imports = [ inputs.grapho.nixosModules.default ];
imports = [ inputs.pal.nixosModules.default ];
}
```
@@ -306,19 +306,22 @@ No. See [our research](./docs/research/sync-tools-comparison.md). Common issues
But cloud is fine too. This system works with GitHub, Backblaze B2, etc.
## Directory Structure
## Repository Structure
```
.
├── flake.nix # Entry point
├── flake.lock
├── README.md
├── cli/
│ └── pal.lux # CLI source (Lux language)
├── docs/
│ ├── research/
│ │ └── sync-tools-comparison.md
│ ├── ARCHITECTURE.md
│ └── LLM-CONTEXT.md # For AI assistants
├── modules/
│ ├── pal.nix # Core pal NixOS module
│ ├── nb.nix
│ ├── syncthing.nix
│ ├── neovim.nix
@@ -342,6 +345,47 @@ But cloud is fine too. This system works with GitHub, Backblaze B2, etc.
└── ustatus
```
## Data Directory (`~/.config/pal/`)
When you run `pal onboard` or `pal setup`, this directory is created to hold all your data and configuration. Everything is human-readable unless noted.
```
~/.config/pal/
├── pal.toml # Main config (TOML) — device name, ports, schedule
├── age-key.txt # Age encryption private key
├── state.db # Event history (SQLite)
├── config-repo/ # Your system config (git-managed)
│ └── .git/
├── sync/ # Syncthing-managed data (syncs across devices)
│ ├── notes/ # Your notes
│ ├── documents/ # Your documents
│ └── dotfiles/ # Your dotfiles
├── syncthing/ # Syncthing runtime (auto-generated)
│ ├── config/ # config.xml, TLS certs, keys
│ └── db/ # Syncthing index database
├── restic/ # Backup settings
│ ├── password # Repository password (auto-generated, plaintext)
│ ├── repository # Repository URL/path (plaintext)
│ └── cache/ # Local cache for faster operations
├── backups/ # Restic repository (if backing up locally)
│ ├── config # ⚠ Encrypted — restic internal, not human-readable
│ ├── data/ # Encrypted, deduplicated backup chunks
│ ├── index/ # Backup index
│ ├── keys/ # Repository encryption keys
│ ├── locks/ # Lock files
│ └── snapshots/ # Snapshot metadata
└── server/ # Mount point for remote server storage
```
**What's human-readable?** `pal.toml`, `restic/password`, `restic/repository`, and everything in `sync/` and `config-repo/`. The `syncthing/config/` directory is auto-generated XML. The `backups/` directory is a restic repository where everything is encrypted by design — use `pal backup list` to inspect snapshots.
## Contributing
PRs welcome! Please read [ARCHITECTURE.md](./docs/ARCHITECTURE.md) first.

1722
cli/pal.lux Normal file

File diff suppressed because it is too large Load Diff

238
docs/MARKDOWN-EDITORS.md Normal file
View File

@@ -0,0 +1,238 @@
# Markdown Editors for pal
This document covers recommended markdown editors for use with pal across desktop and mobile platforms.
## Recommended: md (PWA)
**URL:** https://md-ashy.vercel.app
A lightweight, browser-based markdown editor that works on both desktop and mobile.
### Features
- WYSIWYG editing with inline markdown transformation
- Source mode toggle for raw editing
- Offline support via PWA (installable as app)
- Dark theme
- File drag-and-drop support
- Share documents via compressed URL links
- GitHub Flavored Markdown (GFM) support including tables and task lists
- Syntax highlighting for code blocks
- Keyboard shortcuts (Ctrl+S to download, Ctrl+B/I for formatting)
### Why It's Good for pal
- Works on any device with a browser
- Can be installed as a PWA on mobile home screen
- No account required
- Files stay local (privacy-first)
- Can edit files from Syncthing-synced folders
### Setup
1. Visit https://md-ashy.vercel.app
2. Click the install prompt (or use browser menu > "Add to Home Screen")
3. Open markdown files from your synced folders
---
## Desktop Editors
### MarkText (Recommended for Desktop)
**Open Source** | **Cross-platform** | [GitHub](https://github.com/marktext/marktext)
A simple, elegant markdown editor with real-time preview.
**Pros:**
- Clean, distraction-free interface
- WYSIWYG preview (like Typora, but free)
- Multiple editing modes: Source, Typewriter, Focus
- Six themes (light/dark variants)
- Supports CommonMark, GFM, and Pandoc markdown
- Diagrams (flowcharts, sequence, Gantt via Mermaid)
- Math expressions via KaTeX
- Auto-save and file recovery
**Cons:**
- Last release was March 2022 (minimally maintained)
- No mobile version
**Best for:** Writers who want a polished, free Typora alternative.
---
### Visual Studio Code
**Open Source** | **Cross-platform** | [Website](https://code.visualstudio.com)
The developer's Swiss Army knife with excellent markdown support.
**Pros:**
- Built-in markdown preview
- Extensive extension ecosystem (markdownlint, Markdown All in One, etc.)
- Git integration built-in
- Works with any programming workflow
- Highly customizable
**Cons:**
- Resource-heavy for just markdown editing
- Can feel like overkill for simple notes
**Best for:** Developers who want one editor for code and notes.
---
### Obsidian
**Freemium** | **Cross-platform** | [Website](https://obsidian.md)
A powerful knowledge base that works on local markdown files.
**Pros:**
- Bidirectional linking between notes
- Graph view of note connections
- Extensive plugin ecosystem (900+ plugins)
- Local-first, privacy-focused
- Mobile apps (iOS/Android)
- Sync available (paid) or use Syncthing
**Cons:**
- Not fully open source (free for personal use)
- Learning curve for advanced features
- Can become complex with too many plugins
**Best for:** Building a personal knowledge base / "second brain".
---
### Zettlr
**Open Source** | **Cross-platform** | [Website](https://www.zettlr.com)
Built for academics and researchers.
**Pros:**
- Built-in citation management (Zotero integration)
- Footnotes and LaTeX support
- Zettelkasten method support
- Export to PDF, Word, LaTeX via Pandoc
- Focus on long-form writing
**Cons:**
- No mobile app
- Steeper learning curve
- Requires Pandoc for some exports
**Best for:** Academic writing, research papers, thesis work.
---
### Joplin
**Open Source** | **Cross-platform** | [Website](https://joplinapp.org)
Note-taking with sync and mobile apps.
**Pros:**
- End-to-end encryption
- Mobile apps (iOS/Android)
- Sync with Nextcloud, Dropbox, OneDrive, WebDAV
- Import from Evernote
- Notebooks and tagging
- Web clipper extension
**Cons:**
- Notes stored in SQLite database, not plain files
- Can be resource-intensive
- Less suited for power users who want plain markdown
**Best for:** Evernote replacement with cross-platform sync.
---
## Mobile Editors
### Markor (Android)
**Open Source** | [GitHub](https://github.com/gsantner/markor)
The best open-source markdown editor for Android.
**Pros:**
- Works with any folder (including Syncthing)
- No account required
- Supports markdown, todo.txt, and more
- Offline-first
**Best for:** pal users on Android.
### iA Writer (iOS/Android)
**Paid** | [Website](https://ia.net/writer)
Premium minimalist writing experience.
**Pros:**
- Beautiful, distraction-free interface
- Works with iCloud/Dropbox folders
- Focus mode highlights current sentence
**Cons:**
- Paid app
- File management less flexible than Markor
**Best for:** iOS users who value polish.
### Obsidian Mobile (iOS/Android)
**Free** | [Website](https://obsidian.md)
Mobile companion to Obsidian desktop.
**Pros:**
- Full Obsidian features on mobile
- Sync via iCloud, Obsidian Sync, or Syncthing
**Best for:** Existing Obsidian users.
---
## Recommendation for pal Users
### Simple Setup (Recommended)
1. **Desktop:** MarkText or VS Code
2. **Mobile:** md PWA (https://md-ashy.vercel.app) or Markor (Android)
3. **Sync:** Syncthing (already part of pal)
### Power User Setup
1. **Desktop:** Obsidian with Syncthing sync
2. **Mobile:** Obsidian Mobile
3. **Notes in:** `~/.nb/` or a dedicated Syncthing folder
### Academic Setup
1. **Desktop:** Zettlr with Zotero
2. **Mobile:** md PWA for quick edits
3. **Export:** Pandoc for final documents
---
## Integration with pal
All recommended editors work with plain markdown files, which means:
1. Store notes in an `nb` notebook or Syncthing folder
2. Edit with any editor on any device
3. Changes sync automatically via Syncthing
4. Backup happens via restic
Example workflow:
```bash
# Create a note with nb
nb add "Meeting notes"
# Edit in your preferred editor
marktext ~/.nb/home/meeting-notes.md
# Or on mobile, open the same file via Syncthing folder
# Sync happens automatically
pal sync
```
## Sources
- [MarkText GitHub](https://github.com/marktext/marktext)
- [Obsidian](https://obsidian.md)
- [Zettlr](https://www.zettlr.com)
- [Joplin](https://joplinapp.org)
- [awesome-markdown-editors](https://github.com/mundimark/awesome-markdown-editors)
- [Markdown Guide Tools](https://www.markdownguide.org/tools/)

89
flake.lock generated
View File

@@ -1,5 +1,23 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"home-manager": {
"inputs": {
"nixpkgs": [
@@ -20,7 +38,42 @@
"type": "github"
}
},
"lux": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": [
"nixpkgs"
],
"rust-overlay": "rust-overlay"
},
"locked": {
"lastModified": 1771638380,
"narHash": "sha256-RLGfahDSlYi8ec50DtmfOZn9q8JpF2xBTcUb8K2ZQ3Q=",
"path": "/home/blu/src/lux/lang",
"type": "path"
},
"original": {
"path": "/home/blu/src/lux/lang",
"type": "path"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1744536153,
"narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1770841267,
"narHash": "sha256-9xejG0KoqsoKEGp2kVbXRlEYtFFcDTHjidiuX8hGO44=",
@@ -39,10 +92,29 @@
"root": {
"inputs": {
"home-manager": "home-manager",
"nixpkgs": "nixpkgs",
"lux": "lux",
"nixpkgs": "nixpkgs_2",
"sops-nix": "sops-nix"
}
},
"rust-overlay": {
"inputs": {
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1770952264,
"narHash": "sha256-CjymNrJZWBtpavyuTkfPVPaZkwzIzGaf0E/3WgcwM14=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "ec6a3d5cdf14bb5a1dd03652bd3f6351004d2188",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
},
"sops-nix": {
"inputs": {
"nixpkgs": [
@@ -62,6 +134,21 @@
"repo": "sops-nix",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",

View File

@@ -14,6 +14,11 @@
inputs.nixpkgs.follows = "nixpkgs";
};
lux = {
url = "path:/home/blu/src/lux/lang";
inputs.nixpkgs.follows = "nixpkgs";
};
# Optional: Neovim distribution
# nixvim = {
# url = "github:nix-community/nixvim";
@@ -21,7 +26,7 @@
# };
};
outputs = { self, nixpkgs, home-manager, sops-nix, ... }@inputs:
outputs = { self, nixpkgs, home-manager, sops-nix, lux, ... }@inputs:
let
# Supported systems
supportedSystems = [ "x86_64-linux" "aarch64-linux" ];
@@ -37,6 +42,7 @@
# Shared modules for all hosts
sharedModules = [
./modules/pal.nix
./modules/nb.nix
./modules/syncthing.nix
./modules/backup.nix
@@ -60,6 +66,39 @@
};
in {
# Pal CLI package
packages = forAllSystems (system:
let
pkgs = nixpkgsFor.${system};
luxPkg = lux.packages.${system}.default;
in {
pal = pkgs.stdenv.mkDerivation {
pname = "pal";
version = "0.1.0";
src = ./cli;
nativeBuildInputs = [ luxPkg pkgs.gcc ];
buildPhase = ''
${luxPkg}/bin/lux compile pal.lux -o pal
'';
installPhase = ''
mkdir -p $out/bin
cp pal $out/bin/
'';
meta = {
description = "Personal data infrastructure CLI";
homepage = "https://github.com/user/pal";
license = pkgs.lib.licenses.mit;
};
};
default = self.packages.${system}.pal;
}
);
# NixOS configurations
# Uncomment and customize for your hosts:
#
@@ -105,7 +144,7 @@
else
echo "Error: Cannot find setup script"
echo "Run from the repo directory, or clone it first:"
echo " git clone ssh://git@your-server:2222/you/grapho.git"
echo " git clone ssh://git@your-server:2222/you/pal.git"
exit 1
fi
fi
@@ -132,7 +171,10 @@
default = pkgs.mkShell {
name = "unsbs-dev";
packages = with pkgs; [
packages = [
lux.packages.${system}.default
self.packages.${system}.pal
] ++ (with pkgs; [
# Tier 2: Notes & Sync
nb # Notebook CLI
syncthing # File sync
@@ -159,15 +201,14 @@
# Nix tools
nil # Nix LSP
nixpkgs-fmt # Nix formatter
];
]);
shellHook = ''
printf '\033[1m%s\033[0m\n' "Ultimate Notetaking, Sync & Backup System"
printf '\033[1m%s\033[0m\n' "pal - Your personal data, everywhere"
echo ""
echo "Get started: ./setup"
echo "Pair mobile: ./setup mobile"
echo "Sync: ./scripts/usync"
echo "Status: ./scripts/ustatus"
echo "Get started: pal onboard"
echo "Status: pal status"
echo "Help: pal help"
'';
};
}
@@ -175,6 +216,7 @@
# Export modules for use in other flakes
nixosModules = {
pal = import ./modules/pal.nix;
nb = import ./modules/nb.nix;
syncthing = import ./modules/syncthing.nix;
backup = import ./modules/backup.nix;

341
modules/pal.nix Normal file
View File

@@ -0,0 +1,341 @@
# Pal Module
#
# Unified personal data infrastructure module for NixOS.
# Sets up Syncthing (isolated) + Restic backup + directory structure.
#
# Usage:
# services.pal.enable = true;
# services.pal.user = "youruser";
#
# This creates:
# - ~/.config/pal/ directory structure
# - Isolated Syncthing on port 8385 (separate from system Syncthing)
# - Restic backup timer for pal data
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.pal;
home = config.users.users.${cfg.user}.home;
palDir = "${home}/.config/pal";
in {
options.services.pal = {
enable = mkEnableOption "pal personal data infrastructure";
user = mkOption {
type = types.str;
description = "User to run pal services as.";
example = "alice";
};
group = mkOption {
type = types.str;
default = "users";
description = "Group to run pal services as.";
};
# Syncthing options
syncthing = {
enable = mkOption {
type = types.bool;
default = true;
description = "Enable pal's isolated Syncthing instance.";
};
guiPort = mkOption {
type = types.port;
default = 8385;
description = "Port for Syncthing web GUI (separate from system Syncthing).";
};
syncPort = mkOption {
type = types.port;
default = 22001;
description = "Port for Syncthing file sync (separate from system Syncthing).";
};
discoveryPort = mkOption {
type = types.port;
default = 21028;
description = "Port for Syncthing local discovery.";
};
openFirewall = mkOption {
type = types.bool;
default = true;
description = "Open firewall for pal's Syncthing ports.";
};
devices = mkOption {
type = types.attrsOf (types.submodule {
options = {
id = mkOption {
type = types.str;
description = "Device ID.";
};
name = mkOption {
type = types.nullOr types.str;
default = null;
description = "Friendly name.";
};
addresses = mkOption {
type = types.listOf types.str;
default = [ "dynamic" ];
description = "Connection addresses.";
};
};
});
default = {};
description = "Devices to connect to.";
};
extraFolders = mkOption {
type = types.attrsOf (types.submodule {
options = {
path = mkOption {
type = types.str;
description = "Local path to sync.";
};
devices = mkOption {
type = types.listOf types.str;
default = [];
description = "Devices to share with.";
};
type = mkOption {
type = types.enum [ "sendreceive" "sendonly" "receiveonly" ];
default = "sendreceive";
description = "Sync type.";
};
};
});
default = {};
description = "Additional folders to sync (beyond default notes/documents/dotfiles).";
};
};
# Backup options
backup = {
enable = mkOption {
type = types.bool;
default = false;
description = "Enable restic backup of pal data.";
};
repository = mkOption {
type = types.str;
default = "";
description = "Restic repository location (e.g., 'sftp:server:/backups/pal').";
example = "sftp:backup-server:/backups/pal";
};
passwordFile = mkOption {
type = types.nullOr types.path;
default = null;
description = "Path to file containing restic repository password.";
};
schedule = mkOption {
type = types.str;
default = "hourly";
description = "Backup schedule (systemd timer OnCalendar syntax).";
example = "*-*-* *:00:00";
};
extraPaths = mkOption {
type = types.listOf types.str;
default = [];
description = "Additional paths to include in backup.";
};
pruneOpts = mkOption {
type = types.listOf types.str;
default = [
"--keep-hourly 24"
"--keep-daily 7"
"--keep-weekly 4"
"--keep-monthly 12"
];
description = "Restic prune/forget options.";
};
};
# Server mount options
server = {
enable = mkOption {
type = types.bool;
default = false;
description = "Enable server data mount.";
};
type = mkOption {
type = types.enum [ "nfs" "sshfs" "syncthing" ];
default = "syncthing";
description = "Type of server mount.";
};
host = mkOption {
type = types.str;
default = "";
description = "Server hostname (for NFS/SSHFS).";
};
remotePath = mkOption {
type = types.str;
default = "";
description = "Path on server to mount.";
};
};
};
config = mkIf cfg.enable {
# Ensure user exists
users.users.${cfg.user} = {
isNormalUser = true;
};
# Create pal directory structure
systemd.tmpfiles.rules = [
"d ${palDir} 0755 ${cfg.user} ${cfg.group} -"
"d ${palDir}/config-repo 0755 ${cfg.user} ${cfg.group} -"
"d ${palDir}/syncthing/config 0755 ${cfg.user} ${cfg.group} -"
"d ${palDir}/syncthing/db 0755 ${cfg.user} ${cfg.group} -"
"d ${palDir}/sync 0755 ${cfg.user} ${cfg.group} -"
"d ${palDir}/sync/notes 0755 ${cfg.user} ${cfg.group} -"
"d ${palDir}/sync/documents 0755 ${cfg.user} ${cfg.group} -"
"d ${palDir}/sync/dotfiles 0755 ${cfg.user} ${cfg.group} -"
"d ${palDir}/restic/cache 0755 ${cfg.user} ${cfg.group} -"
"d ${palDir}/server 0755 ${cfg.user} ${cfg.group} -"
];
# Isolated Syncthing for pal
services.syncthing = mkIf cfg.syncthing.enable {
enable = true;
user = cfg.user;
group = cfg.group;
dataDir = "${palDir}/sync";
configDir = "${palDir}/syncthing/config";
guiAddress = "127.0.0.1:${toString cfg.syncthing.guiPort}";
overrideDevices = true;
overrideFolders = true;
settings = {
devices = mapAttrs (name: device: {
inherit (device) id addresses;
name = if device.name != null then device.name else name;
}) cfg.syncthing.devices;
folders = {
# Default pal folders
"pal-notes" = {
path = "${palDir}/sync/notes";
id = "pal-notes";
devices = attrNames cfg.syncthing.devices;
type = "sendreceive";
fsWatcherEnabled = true;
};
"pal-documents" = {
path = "${palDir}/sync/documents";
id = "pal-documents";
devices = attrNames cfg.syncthing.devices;
type = "sendreceive";
fsWatcherEnabled = true;
};
"pal-dotfiles" = {
path = "${palDir}/sync/dotfiles";
id = "pal-dotfiles";
devices = attrNames cfg.syncthing.devices;
type = "sendreceive";
fsWatcherEnabled = true;
};
} // (mapAttrs (name: folder: {
inherit (folder) path type;
id = name;
devices = if folder.devices == [] then attrNames cfg.syncthing.devices else folder.devices;
fsWatcherEnabled = true;
}) cfg.syncthing.extraFolders);
options = {
urAccepted = -1;
relaysEnabled = true;
globalAnnounceEnabled = true;
localAnnounceEnabled = true;
localAnnouncePort = cfg.syncthing.discoveryPort;
listenAddresses = [
"tcp://0.0.0.0:${toString cfg.syncthing.syncPort}"
"quic://0.0.0.0:${toString cfg.syncthing.syncPort}"
];
};
};
};
# Firewall for pal Syncthing
networking.firewall = mkIf (cfg.syncthing.enable && cfg.syncthing.openFirewall) {
allowedTCPPorts = [ cfg.syncthing.syncPort ];
allowedUDPPorts = [ cfg.syncthing.syncPort cfg.syncthing.discoveryPort ];
};
# Restic backup service
systemd.services.pal-backup = mkIf (cfg.backup.enable && cfg.backup.repository != "") {
description = "Pal data backup";
wants = [ "network-online.target" ];
after = [ "network-online.target" ];
serviceConfig = {
Type = "oneshot";
User = cfg.user;
Group = cfg.group;
ExecStart = let
paths = [ "${palDir}/sync" ] ++ cfg.backup.extraPaths;
pathArgs = concatMapStringsSep " " (p: "'${p}'") paths;
in ''
${pkgs.restic}/bin/restic backup \
--cache-dir ${palDir}/restic/cache \
${optionalString (cfg.backup.passwordFile != null) "--password-file ${cfg.backup.passwordFile}"} \
-r ${cfg.backup.repository} \
${pathArgs}
'';
ExecStartPost = ''
${pkgs.restic}/bin/restic forget \
--cache-dir ${palDir}/restic/cache \
${optionalString (cfg.backup.passwordFile != null) "--password-file ${cfg.backup.passwordFile}"} \
-r ${cfg.backup.repository} \
${concatStringsSep " " cfg.backup.pruneOpts}
'';
};
};
systemd.timers.pal-backup = mkIf (cfg.backup.enable && cfg.backup.repository != "") {
description = "Pal backup timer";
wantedBy = [ "timers.target" ];
timerConfig = {
OnCalendar = cfg.backup.schedule;
Persistent = true;
RandomizedDelaySec = "5min";
};
};
# Server mount (NFS)
fileSystems."${palDir}/server" = mkIf (cfg.server.enable && cfg.server.type == "nfs" && cfg.server.host != "") {
device = "${cfg.server.host}:${cfg.server.remotePath}";
fsType = "nfs";
options = [
"x-systemd.automount"
"noauto"
"x-systemd.idle-timeout=600"
"soft"
"timeo=15"
];
};
# Install pal CLI and dependencies
environment.systemPackages = with pkgs; [
syncthing
restic
age
jq
];
};
}

2
setup
View File

@@ -133,7 +133,7 @@ EOF
age_key=$(grep 'AGE-SECRET-KEY' "$AGE_KEY_FILE")
# Build join command for other devices
local join_cmd="nix run '<grapho-flake>' -- '${config_url}' '${age_key}'"
local join_cmd="nix run '<pal-flake>' -- '${config_url}' '${age_key}'"
# Summary
echo ""