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>
This commit is contained in:
452
cli/grapho.lux
Normal file
452
cli/grapho.lux
Normal file
@@ -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<String>): 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<String>): 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 <term> 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 {}
|
||||||
137
docs/LUX-LIMITATIONS.md
Normal file
137
docs/LUX-LIMITATIONS.md
Normal file
@@ -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
|
||||||
238
docs/MARKDOWN-EDITORS.md
Normal file
238
docs/MARKDOWN-EDITORS.md
Normal file
@@ -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/)
|
||||||
89
flake.lock
generated
89
flake.lock
generated
@@ -1,5 +1,23 @@
|
|||||||
{
|
{
|
||||||
"nodes": {
|
"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": {
|
"home-manager": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"nixpkgs": [
|
"nixpkgs": [
|
||||||
@@ -20,7 +38,42 @@
|
|||||||
"type": "github"
|
"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": {
|
"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": {
|
"locked": {
|
||||||
"lastModified": 1770841267,
|
"lastModified": 1770841267,
|
||||||
"narHash": "sha256-9xejG0KoqsoKEGp2kVbXRlEYtFFcDTHjidiuX8hGO44=",
|
"narHash": "sha256-9xejG0KoqsoKEGp2kVbXRlEYtFFcDTHjidiuX8hGO44=",
|
||||||
@@ -39,10 +92,29 @@
|
|||||||
"root": {
|
"root": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"home-manager": "home-manager",
|
"home-manager": "home-manager",
|
||||||
"nixpkgs": "nixpkgs",
|
"lux": "lux",
|
||||||
|
"nixpkgs": "nixpkgs_2",
|
||||||
"sops-nix": "sops-nix"
|
"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": {
|
"sops-nix": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"nixpkgs": [
|
"nixpkgs": [
|
||||||
@@ -62,6 +134,21 @@
|
|||||||
"repo": "sops-nix",
|
"repo": "sops-nix",
|
||||||
"type": "github"
|
"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",
|
"root": "root",
|
||||||
|
|||||||
Reference in New Issue
Block a user