Compare commits
11 Commits
9c7f47e727
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| ec5e77f796 | |||
| 3be9586238 | |||
| 4a5fa4415c | |||
| d30d2efa4e | |||
| 05c04b209c | |||
| 13fe22a804 | |||
| dedfbfce64 | |||
| d3d720b3bc | |||
| afe7826d58 | |||
| 117e6af528 | |||
| 63fedfb525 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -3,6 +3,10 @@ result
|
||||
result-*
|
||||
.direnv/
|
||||
|
||||
# Compiled binaries
|
||||
pal
|
||||
*_test
|
||||
|
||||
# Secrets (NEVER commit unencrypted secrets)
|
||||
secrets/*.yaml
|
||||
!secrets/secrets.yaml.example
|
||||
|
||||
68
README.md
68
README.md
@@ -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
1722
cli/pal.lux
Normal file
File diff suppressed because it is too large
Load Diff
238
docs/MARKDOWN-EDITORS.md
Normal file
238
docs/MARKDOWN-EDITORS.md
Normal 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
89
flake.lock
generated
@@ -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",
|
||||
|
||||
60
flake.nix
60
flake.nix
@@ -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
341
modules/pal.nix
Normal 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
|
||||
];
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user