diff --git a/cli/grapho.lux b/cli/grapho.lux new file mode 100644 index 0000000..16d4428 --- /dev/null +++ b/cli/grapho.lux @@ -0,0 +1,452 @@ +// grapho - Unified CLI for the Ultimate Notetaking, Sync & Backup System +// +// Usage: +// grapho One-line health check (default) +// grapho status Full status dashboard +// grapho status -v Verbose status with all details +// grapho doctor Diagnose issues and suggest fixes +// grapho sync Sync all (nb + syncthing) +// grapho backup Run backup now +// grapho help Show help +// grapho --json Machine-readable JSON output + +// ============================================================================= +// Types +// ============================================================================= + +type SyncStatus = + | StatusOk + | StatusWarn + | StatusErr + | StatusNone + +// ============================================================================= +// Status icons +// ============================================================================= + +fn statusIcon(s: SyncStatus): String = + match s { + StatusOk => "[ok]", + StatusWarn => "[!!]", + StatusErr => "[ERR]", + StatusNone => "[--]" + } + +fn statusEmoji(s: SyncStatus): String = + match s { + StatusOk => "✓", + StatusWarn => "⚠", + StatusErr => "✗", + StatusNone => "-" + } + +// ============================================================================= +// Shell helpers +// ============================================================================= + +fn hasCommand(cmd: String): Bool with {Process} = { + let result = Process.exec("command -v " + cmd + " 2>/dev/null || true") + String.length(String.trim(result)) > 0 +} + +fn execQuiet(cmd: String): String with {Process} = + String.trim(Process.exec(cmd + " 2>/dev/null || true")) + +fn homeDir(): String with {Process} = + match Process.env("HOME") { + Some(h) => h, + None => "/tmp" + } + +// ============================================================================= +// Syncthing helpers +// ============================================================================= + +fn syncthingRunning(): Bool with {Process} = { + let result = Process.exec("syncthing cli show system 2>/dev/null | jq -r '.myID' 2>/dev/null || true") + String.length(String.trim(result)) > 10 +} + +fn getSyncthingUptime(): String with {Process} = + Process.exec("syncthing cli show system 2>/dev/null | jq -r '.uptime // 0' 2>/dev/null || echo 0") + +fn getSyncthingFolderCount(): String with {Process} = + String.trim(Process.exec("syncthing cli show config 2>/dev/null | jq -r '.folders | length' 2>/dev/null || echo 0")) + +fn getSyncthingDeviceCount(): String with {Process} = + String.trim(Process.exec("syncthing cli show config 2>/dev/null | jq -r '.devices | length' 2>/dev/null || echo 0")) + +// ============================================================================= +// One-liner health check (new default) +// ============================================================================= + +fn showHealthCheck(): Unit with {Console, Process} = { + let hasIssues = false + let hasWarnings = false + let summary = "" + + // Check nb + let nbOk = hasCommand("nb") + let nbCount = if nbOk then execQuiet("nb notebooks --names 2>/dev/null | wc -l") else "0" + + // Check syncthing + let stOk = hasCommand("syncthing") + let stRunning = if stOk then syncthingRunning() else false + let stFolders = if stRunning then getSyncthingFolderCount() else "0" + let stDevices = if stRunning then getSyncthingDeviceCount() else "0" + + // Check backup + let resticOk = hasCommand("restic") + let timerActive = execQuiet("systemctl is-active restic-backup.timer") + let backupOk = if timerActive == "active" then true else false + + // Determine overall status and print appropriate message + if nbOk == false || stOk == false || resticOk == false then { + Console.print(statusEmoji(StatusErr) + " Issues detected") + if nbOk == false then + Console.print(" " + statusIcon(StatusErr) + " nb: not installed -> nix develop") + else () + if stOk == false then + Console.print(" " + statusIcon(StatusErr) + " syncthing: not installed -> nix develop") + else () + if resticOk == false then + Console.print(" " + statusIcon(StatusErr) + " restic: not installed -> nix develop") + else () + } else if stRunning == false || backupOk == false then { + Console.print(statusEmoji(StatusWarn) + " Warnings") + if stRunning == false then + Console.print(" " + statusIcon(StatusWarn) + " syncthing: not running -> systemctl --user start syncthing") + else () + if backupOk == false then + Console.print(" " + statusIcon(StatusWarn) + " backup: timer inactive -> sudo systemctl enable --now restic-backup.timer") + else () + } else { + Console.print(statusEmoji(StatusOk) + " All systems healthy (" + nbCount + " notebooks, " + stFolders + " folders, " + stDevices + " devices, backup active)") + } +} + +// ============================================================================= +// Status dashboard +// ============================================================================= + +fn showStatus(verbose: Bool): Unit with {Console, Process} = { + Console.print("grapho status") + Console.print("") + + // nb + if hasCommand("nb") then { + let nbCount = execQuiet("nb notebooks --names 2>/dev/null | wc -l") + Console.print(statusIcon(StatusOk) + " nb: " + nbCount + " notebooks") + if verbose then { + let notebooks = execQuiet("nb notebooks --names") + if String.length(notebooks) > 0 then { + let lines = String.lines(notebooks) + printNotebooks(lines) + } else () + } else () + } else { + Console.print(statusIcon(StatusErr) + " nb: not installed") + Console.print(" Fix: nix develop") + } + + // Syncthing + if hasCommand("syncthing") then { + if syncthingRunning() then { + let folders = getSyncthingFolderCount() + let devices = getSyncthingDeviceCount() + Console.print(statusIcon(StatusOk) + " syncthing: " + folders + " folders, " + devices + " devices") + if verbose then { + let folderLabels = execQuiet("syncthing cli show config | jq -r '.folders[].label'") + if String.length(folderLabels) > 0 then { + Console.print(" Folders:") + let flines = String.lines(folderLabels) + printLinesIndented(flines) + } else () + let deviceNames = execQuiet("syncthing cli show config | jq -r '.devices[].name'") + if String.length(deviceNames) > 0 then { + Console.print(" Devices:") + let dlines = String.lines(deviceNames) + printLinesIndented(dlines) + } else () + } else () + } else { + Console.print(statusIcon(StatusWarn) + " syncthing: not running") + Console.print(" Fix: systemctl --user start syncthing") + } + } else { + Console.print(statusIcon(StatusErr) + " syncthing: not installed") + Console.print(" Fix: nix develop") + } + + // Backup + if hasCommand("restic") then { + let timerActive = execQuiet("systemctl is-active restic-backup.timer") + if timerActive == "active" then { + Console.print(statusIcon(StatusOk) + " backup: timer active") + } else { + Console.print(statusIcon(StatusWarn) + " backup: timer inactive") + Console.print(" Fix: sudo systemctl enable --now restic-backup.timer") + } + } else { + Console.print(statusIcon(StatusErr) + " backup: restic not installed") + Console.print(" Fix: nix develop") + } +} + +fn printNotebooks(lines: List): Unit with {Console, Process} = + match List.head(lines) { + Some(name) => { + if String.length(name) > 0 then { + let count = execQuiet("find ~/.nb/" + name + " -name '*.md' 2>/dev/null | wc -l") + Console.print(" " + name + ": " + count + " notes") + } else () + match List.tail(lines) { + Some(rest) => printNotebooks(rest), + None => () + } + }, + None => () + } + +fn printLinesIndented(lines: List): Unit with {Console} = + match List.head(lines) { + Some(line) => { + if String.length(line) > 0 then + Console.print(" - " + line) + else () + match List.tail(lines) { + Some(rest) => printLinesIndented(rest), + None => () + } + }, + None => () + } + +// ============================================================================= +// Doctor command +// ============================================================================= + +fn showDoctor(): Unit with {Console, Process} = { + Console.print("grapho doctor") + Console.print("") + Console.print("Checking system health...") + Console.print("") + + // Check nix + if hasCommand("nix") then { + let nixVer = execQuiet("nix --version") + Console.print(statusIcon(StatusOk) + " nix: " + nixVer) + } else { + Console.print(statusIcon(StatusErr) + " nix: not found") + Console.print(" Install: https://nixos.org/download") + } + + // Check git + if hasCommand("git") then { + let gitVer = execQuiet("git --version | cut -d' ' -f3") + Console.print(statusIcon(StatusOk) + " git: " + gitVer) + } else { + Console.print(statusIcon(StatusErr) + " git: not found") + } + + // Check nb + if hasCommand("nb") then { + let nbCount = execQuiet("nb notebooks --names 2>/dev/null | wc -l") + Console.print(statusIcon(StatusOk) + " nb: " + nbCount + " notebooks") + } else { + Console.print(statusIcon(StatusErr) + " nb: not installed") + Console.print(" Fix: nix develop") + } + + // Check syncthing + if hasCommand("syncthing") then { + if syncthingRunning() then { + let folders = getSyncthingFolderCount() + Console.print(statusIcon(StatusOk) + " syncthing: running (" + folders + " folders)") + } else { + Console.print(statusIcon(StatusWarn) + " syncthing: not running") + Console.print(" Fix: systemctl --user start syncthing") + } + } else { + Console.print(statusIcon(StatusErr) + " syncthing: not installed") + Console.print(" Fix: nix develop") + } + + // Check backup + if hasCommand("restic") then { + let timerActive = execQuiet("systemctl is-active restic-backup.timer") + if timerActive == "active" then { + Console.print(statusIcon(StatusOk) + " backup: timer active") + } else { + Console.print(statusIcon(StatusWarn) + " backup: timer inactive") + Console.print(" Fix: sudo systemctl enable --now restic-backup.timer") + } + } else { + Console.print(statusIcon(StatusErr) + " restic: not installed") + Console.print(" Fix: nix develop") + } + + // Additional checks + Console.print("") + Console.print("Additional checks:") + + // Check age key + let ageKey = execQuiet("test -f ~/.config/sops/age/keys.txt && echo yes") + if ageKey == "yes" then + Console.print(statusIcon(StatusOk) + " Age encryption key exists") + else { + Console.print(statusIcon(StatusWarn) + " No age key") + Console.print(" Fix: age-keygen -o ~/.config/sops/age/keys.txt") + } + + Console.print("") + Console.print("Run 'grapho status -v' for detailed component info") +} + +// ============================================================================= +// JSON output +// ============================================================================= + +fn showJson(): Unit with {Console, Process} = { + // Note: JSON output with quotes is difficult in Lux due to escape sequence limitations + // Using a simple key=value format instead + let nbOk = hasCommand("nb") + let nbCount = if nbOk then execQuiet("nb notebooks --names 2>/dev/null | wc -l") else "0" + let nbStatus = if nbOk then "healthy" else "error" + let nbMsg = if nbOk then nbCount + " notebooks" else "not installed" + + let stOk = hasCommand("syncthing") + let stRunning = if stOk then syncthingRunning() else false + let stStatus = if stOk == false then "error" else if stRunning then "healthy" else "warning" + let stMsg = if stOk == false then "not installed" else if stRunning then "running" else "not running" + + let resticOk = hasCommand("restic") + let timerResult = execQuiet("systemctl is-active restic-backup.timer") + let timerActive = if timerResult == "active" then true else false + let backupStatus = if resticOk == false then "error" else if timerActive then "healthy" else "warning" + let backupMsg = if resticOk == false then "not installed" else if timerActive then "timer active" else "timer inactive" + + // Output in a simple parseable format + Console.print("nb.status=" + nbStatus) + Console.print("nb.message=" + nbMsg) + Console.print("syncthing.status=" + stStatus) + Console.print("syncthing.message=" + stMsg) + Console.print("backup.status=" + backupStatus) + Console.print("backup.message=" + backupMsg) +} + +// ============================================================================= +// Sync command +// ============================================================================= + +fn doSync(): Unit with {Console, Process} = { + Console.print("Syncing...") + + // Sync nb notebooks + if hasCommand("nb") then { + Console.print("-> nb sync --all") + let result = Process.exec("nb sync --all 2>&1 || true") + if String.contains(result, "error") then + Console.print(statusIcon(StatusWarn) + " nb sync had issues") + else + Console.print(statusIcon(StatusOk) + " nb synced") + } else () + + // Trigger Syncthing scan + if hasCommand("syncthing") then { + if syncthingRunning() then { + Console.print("-> syncthing cli scan") + let scanResult = Process.exec("syncthing cli scan 2>/dev/null || true") + Console.print(statusIcon(StatusOk) + " Syncthing scan triggered") + } else () + } else () + + Console.print("Done!") +} + +// ============================================================================= +// Backup command +// ============================================================================= + +fn doBackup(): Unit with {Console, Process} = { + Console.print("Running backup...") + + if hasCommand("restic") then { + let hasService = execQuiet("systemctl cat restic-backup.service") + if String.length(hasService) > 0 then { + Console.print("-> sudo systemctl start restic-backup.service") + let startResult = Process.exec("sudo systemctl start restic-backup.service 2>&1 || true") + Console.print(statusIcon(StatusOk) + " Backup service triggered") + } else { + Console.print(statusIcon(StatusWarn) + " No systemd backup service configured") + Console.print(" Configure in modules/backup.nix") + } + } else { + Console.print(statusIcon(StatusErr) + " restic not installed") + } +} + +// ============================================================================= +// Help +// ============================================================================= + +fn showHelp(): Unit with {Console} = { + Console.print("grapho - Personal Data Infrastructure") + Console.print("") + Console.print("Usage:") + Console.print(" grapho Health check (one-liner)") + Console.print(" grapho status Component status") + Console.print(" grapho status -v Verbose status with details") + Console.print(" grapho doctor Diagnose issues and fixes") + Console.print(" grapho sync Sync all (nb + syncthing)") + Console.print(" grapho backup Run backup now") + Console.print(" grapho --json Machine-readable output (key=value format)") + Console.print(" grapho help Show this help") + Console.print("") + Console.print("Quick start:") + Console.print(" nb add Create a new note") + Console.print(" nb search Search notes") + Console.print(" grapho sync Sync to all devices") + Console.print("") + Console.print("More info: https://git.qrty.ink/blu/grapho") +} + +// ============================================================================= +// Main +// ============================================================================= + +fn main(): Unit with {Console, Process} = { + let args = Process.args() + let cmd = match List.get(args, 1) { + Some(c) => c, + None => "" + } + + match cmd { + "" => showHealthCheck(), + "status" => { + let subcmd = match List.get(args, 2) { + Some(s) => s, + None => "" + } + match subcmd { + "-v" => showStatus(true), + "--verbose" => showStatus(true), + "verbose" => showStatus(true), + _ => showStatus(false) + } + }, + "doctor" => showDoctor(), + "sync" => doSync(), + "backup" => doBackup(), + "help" => showHelp(), + "-h" => showHelp(), + "--help" => showHelp(), + "--json" => showJson(), + "-j" => showJson(), + "json" => showJson(), + _ => showHelp() + } +} + +let result = run main() with {} diff --git a/docs/LUX-LIMITATIONS.md b/docs/LUX-LIMITATIONS.md new file mode 100644 index 0000000..e6be597 --- /dev/null +++ b/docs/LUX-LIMITATIONS.md @@ -0,0 +1,137 @@ +# Lux Language Limitations (grapho CLI) + +This document tracks limitations encountered while developing the grapho CLI in Lux, to help improve the language. + +## Fixed Issues + +### 1. Double Execution Bug (FIXED) +**Severity:** Critical +**Status:** Fixed in Lux c_backend.rs + +When using `let result = run main() with {}` to invoke the main function, the entire program was executing twice. + +**Root Cause:** In `c_backend.rs:3878-3907`, the generated C code was: +1. Executing all `run` expressions (including `run main() with {}`) +2. Then ALSO calling `main_lux()` separately because `has_main` was true + +**Fix:** Added tracking of whether main was already called via a `run` expression, and skip the separate `main_lux()` call if so. + +--- + +## String Handling Issues + +### 2. No Escape Sequences in String Literals +**Severity:** High +**Status:** Confirmed + +Lux does not support backslash escape sequences like `\"`, `\n`, `\t` in string literals. + +```lux +// This FAILS - backslash causes parse error +Console.print("Hello \"World\"") // ERROR: Unexpected character: '\' + +// This FAILS +Console.print("Line1\nLine2") // ERROR: Unexpected character: '\' +``` + +**Impact:** Cannot include quotes in strings, cannot create multi-line strings, cannot output JSON with proper formatting. + +**Workaround:** +- Use shell commands via `Process.exec` to generate quoted output +- Use `String.fromChar('"')` for quotes (but this had issues too) +- For JSON output, use key=value format instead + +### 3. Dollar Sign in Strings Causes Parse Error +**Severity:** Medium +**Status:** Confirmed + +The `$` character in strings triggers the string interpolation lexer, even inside shell command strings. + +```lux +// This FAILS +execQuiet("jq -n --arg x '$foo' ...") // ERROR: Unexpected character: '$' +``` + +**Impact:** Cannot use shell variable syntax or jq arguments in command strings. + +**Workaround:** Avoid `$` in strings, or construct commands differently. + +### 4. String.fromChar Returns Int, Not String +**Severity:** Medium +**Status:** Bug + +`String.fromChar('"')` appears to return an Int instead of a String, causing C compilation errors. + +```lux +let q = String.fromChar('"') // Compiles but C code is wrong +Console.print(q + "hello") // C error: int + string +``` + +**Impact:** Cannot use character literals to build strings. + +**Workaround:** Use `execQuiet("printf '%s' '\"'")` to get a quote character. + +--- + +## Type System Issues + +### 5. Record Type Definitions Don't Work as Expected +**Severity:** Medium +**Status:** Needs Investigation + +Defining a record type and then creating values of that type doesn't work: + +```lux +type ComponentStatus = { + name: String, + status: HealthStatus, + message: String, + fix: String +} + +fn checkNb(): ComponentStatus with {Process} = { + // ... + { name: "nb", status: Healthy, message: "ok", fix: "" } + // ERROR: Cannot unify { name: String, ... } with ComponentStatus +} +``` + +**Impact:** Cannot use structured types for cleaner code organization. + +**Workaround:** Avoid record types, use multiple return values via tuples or restructure code. + +### 6. Int.parse Doesn't Exist or Has Wrong Signature +**Severity:** Low +**Status:** Confirmed + +There's no obvious way to parse a string to an integer. + +```lux +let count = Int.parse(someString) // ERROR: Unknown effect operation +``` + +**Impact:** Cannot convert string output from shell commands to numbers. + +**Workaround:** Keep numbers as strings, use shell for numeric comparisons. + +--- + +## Suggestions for Lux + +1. **Add escape sequence support** - At minimum `\"`, `\\`, `\n`, `\t` +2. **Fix String.fromChar** to return String, not Int +3. **Add raw string literals** - Something like `r"..."` or `'''...'''` for shell commands +4. **Fix the double execution bug** in the runtime +5. **Support record type literals** matching their declared type +6. **Add Int.parse and Float.parse** for string-to-number conversion +7. **Consider a heredoc syntax** for multi-line strings with special characters + +--- + +## Current Workarounds in grapho CLI + +1. **Double output:** FIXED in Lux c_backend.rs +2. **JSON output:** Using key=value format instead of proper JSON +3. **Quotes in output:** Avoided entirely or generated via shell +4. **Structured types:** Using individual variables instead of records +5. **Numeric parsing:** Keeping counts as strings throughout diff --git a/docs/MARKDOWN-EDITORS.md b/docs/MARKDOWN-EDITORS.md new file mode 100644 index 0000000..48bac7c --- /dev/null +++ b/docs/MARKDOWN-EDITORS.md @@ -0,0 +1,238 @@ +# Markdown Editors for grapho + +This document covers recommended markdown editors for use with grapho 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 grapho +- 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:** grapho 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 grapho 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 grapho) + +### 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 grapho + +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 +grapho 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/) diff --git a/flake.lock b/flake.lock index 2dae45c..0e9f001 100644 --- a/flake.lock +++ b/flake.lock @@ -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": 1771221263, + "narHash": "sha256-Av4s4pelV+ueIMSY61aHuT8KjKZ6ekXtJsnjVc89gtQ=", + "path": "/home/blu/src/lux", + "type": "path" + }, + "original": { + "path": "/home/blu/src/lux", + "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", diff --git a/grapho b/grapho new file mode 100755 index 0000000..eba82db Binary files /dev/null and b/grapho differ