diff --git a/flake.nix b/flake.nix index 5789a4f..4c52d7e 100644 --- a/flake.nix +++ b/flake.nix @@ -94,6 +94,13 @@ restic # Backup rclone # Cloud storage + # Setup & Pairing + qrencode # QR codes for mobile pairing + zbar # QR scanning (zbarimg) + ffmpeg # Camera preview + capture + age # Encryption for secrets + sops # Secret management + # Development git neovim @@ -107,16 +114,12 @@ ]; shellHook = '' - echo "Ultimate Notetaking, Sync & Backup System" + printf '\033[1m%s\033[0m\n' "Ultimate Notetaking, Sync & Backup System" echo "" - echo "Available tools:" - echo " nb - Note-taking CLI" - echo " syncthing - File synchronization" - echo " unison - Alternative file sync" - echo " restic - Backup" - echo " rclone - Cloud storage" - echo "" - echo "Run 'nb help' to get started with notes." + echo "Get started: ./setup" + echo "Pair mobile: ./setup mobile" + echo "Sync: ./scripts/usync" + echo "Status: ./scripts/ustatus" ''; }; } diff --git a/scripts/usync b/scripts/usync index 28bdd1b..dba71f1 100755 --- a/scripts/usync +++ b/scripts/usync @@ -1,63 +1,375 @@ #!/usr/bin/env bash # -# usync - Unified sync command for the Ultimate Notetaking System +# usync - Unified sync & setup for Ultimate Notetaking System # -# Syncs both nb notebooks and triggers Syncthing scans. +# SETUP (one-liner): +# usync init +# usync init git@server:user/config.git AGE-SECRET-KEY-1XXXXX... # -# Usage: -# usync - Sync everything -# usync nb - Sync only nb notebooks -# usync st - Sync only Syncthing folders -# usync status - Show sync status +# MOBILE PAIRING: +# usync invite # Show QR code for phone to scan +# usync join # Join from this device using invite +# +# SYNC: +# usync # Sync everything +# usync nb # Sync only nb notebooks +# usync st # Sync only Syncthing folders +# usync status # Show sync status set -euo pipefail +USYNC_DIR="${USYNC_DIR:-$HOME/.config/usync}" +USYNC_DEVICES="$USYNC_DIR/devices.json" + # Colors RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' -NC='\033[0m' # No Color +BOLD='\033[1m' +DIM='\033[2m' +NC='\033[0m' -log_info() { - echo -e "${BLUE}[INFO]${NC} $1" +log_info() { echo -e "${BLUE}::${NC} $1"; } +log_success() { echo -e "${GREEN}OK${NC} $1"; } +log_warn() { echo -e "${YELLOW}!!${NC} $1"; } +log_error() { echo -e "${RED}ERR${NC} $1" >&2; } +log_step() { echo -e "${DIM}->>${NC} $1"; } + +die() { log_error "$1"; exit 1; } + +# Check dependencies +need_cmd() { + command -v "$1" &>/dev/null || die "Required: $1 (not found)" } -log_success() { - echo -e "${GREEN}[OK]${NC} $1" +ensure_dir() { + mkdir -p "$USYNC_DIR" } -log_warn() { - echo -e "${YELLOW}[WARN]${NC} $1" +# ============================================================================ +# INIT - Bootstrap new device from git repo + age key +# ============================================================================ +cmd_init() { + local git_url="${1:-}" + local age_key="${2:-}" + + if [[ -z "$git_url" ]]; then + echo "Usage: usync init " + echo "" + echo "Examples:" + echo " usync init git@server:user/config.git AGE-SECRET-KEY-1XXXX..." + echo " usync init https://git.example.com/user/config.git @/path/to/key.txt" + echo "" + echo "The age key can be:" + echo " - The key directly: AGE-SECRET-KEY-1..." + echo " - A file path with @: @/path/to/age-key.txt" + exit 1 + fi + + if [[ -z "$age_key" ]]; then + echo -n "Age secret key (or @/path/to/key.txt): " + read -r age_key + fi + + need_cmd git + need_cmd age + need_cmd sops + + ensure_dir + + log_info "Initializing device from $git_url" + + # Handle @file syntax for age key + if [[ "$age_key" == @* ]]; then + local keyfile="${age_key:1}" + [[ -f "$keyfile" ]] || die "Key file not found: $keyfile" + age_key=$(cat "$keyfile") + fi + + # Validate age key format + if [[ ! "$age_key" =~ ^AGE-SECRET-KEY-1[A-Z0-9]+$ ]]; then + die "Invalid age key format. Expected: AGE-SECRET-KEY-1..." + fi + + # Store age key securely + local age_key_file="$USYNC_DIR/age-key.txt" + echo "$age_key" > "$age_key_file" + chmod 600 "$age_key_file" + log_step "Stored age key" + + # Clone config repo + local config_dir="$USYNC_DIR/config" + if [[ -d "$config_dir" ]]; then + log_step "Updating existing config..." + git -C "$config_dir" pull --ff-only + else + log_step "Cloning config repo..." + git clone "$git_url" "$config_dir" + fi + + # Set up sops to use our age key + export SOPS_AGE_KEY="$age_key" + + # Decrypt secrets if they exist + if [[ -f "$config_dir/secrets/secrets.yaml" ]]; then + log_step "Decrypting secrets..." + sops -d "$config_dir/secrets/secrets.yaml" > "$USYNC_DIR/secrets.yaml" + chmod 600 "$USYNC_DIR/secrets.yaml" + fi + + # Get/generate device ID + local device_id + device_id=$(get_or_create_device_id) + + # Register this device + register_device "$device_id" + + log_success "Device initialized!" + echo "" + echo "Your device ID:" + echo -e "${BOLD}$device_id${NC}" + echo "" + echo "Next steps:" + echo " 1. Add this device ID to other machines" + echo " 2. Run: usync invite # to pair mobile devices" + echo " 3. Run: usync # to sync" } -log_error() { - echo -e "${RED}[ERROR]${NC} $1" +# ============================================================================ +# DEVICE ID MANAGEMENT +# ============================================================================ +get_or_create_device_id() { + # Try syncthing first + if command -v syncthing &>/dev/null; then + local st_id + st_id=$(syncthing cli show system 2>/dev/null | jq -r '.myID // empty' 2>/dev/null || true) + if [[ -n "$st_id" ]]; then + echo "$st_id" + return + fi + fi + + # Try to get from config + local config_file="$HOME/.config/syncthing/config.xml" + if [[ -f "$config_file" ]]; then + local st_id + st_id=$(grep -oP '(?<=/dev/null; then + log_step "Generating Syncthing device ID..." + syncthing generate --skip-port-probing &>/dev/null || true + syncthing cli show system 2>/dev/null | jq -r '.myID' 2>/dev/null || echo "UNKNOWN" + else + echo "UNKNOWN-$(hostname)-$(date +%s)" + fi } +register_device() { + local device_id="$1" + local hostname + hostname=$(hostname) + + ensure_dir + + # Load or create devices file + if [[ ! -f "$USYNC_DEVICES" ]]; then + echo '{"devices":{}}' > "$USYNC_DEVICES" + fi + + # Add this device + local tmp + tmp=$(mktemp) + jq --arg id "$device_id" --arg name "$hostname" \ + '.devices[$name] = {id: $id, added: (now | todate)}' \ + "$USYNC_DEVICES" > "$tmp" && mv "$tmp" "$USYNC_DEVICES" + + log_step "Registered device: $hostname" +} + +# ============================================================================ +# INVITE - Generate QR/invite for mobile device +# ============================================================================ +cmd_invite() { + local device_id + device_id=$(get_or_create_device_id) + + echo "" + echo -e "${BOLD}=== Add Mobile Device ===${NC}" + echo "" + + echo "Device ID:" + echo -e "${GREEN}$device_id${NC}" + echo "" + + # Generate QR code if qrencode is available + if command -v qrencode &>/dev/null; then + echo "Scan this QR code with Syncthing app:" + echo "" + qrencode -t ANSIUTF8 "$device_id" + echo "" + else + log_warn "Install 'qrencode' for QR codes: nix-shell -p qrencode" + echo "" + fi + + echo -e "${DIM}--- Manual setup ---${NC}" + echo "" + echo "In Syncthing Android app:" + echo " 1. Open Syncthing" + echo " 2. Tap menu (3 dots) > Show device ID" + echo " 3. Run on this machine:" + echo "" + echo -e " ${BOLD}usync add-device ${NC}" + echo "" + echo " 4. Back in app: Add Device > paste this ID:" + echo "" + echo -e " ${GREEN}$device_id${NC}" + echo "" + + # Show folders that can be shared + if command -v syncthing &>/dev/null && syncthing cli show system &>/dev/null 2>&1; then + local folders + folders=$(syncthing cli show config 2>/dev/null | jq -r '.folders[].id' 2>/dev/null | tr '\n' ' ') + if [[ -n "$folders" ]]; then + echo "Available folders to share: $folders" + fi + fi +} + +# ============================================================================ +# ADD-DEVICE - Add a device by ID (for mobile pairing) +# ============================================================================ +cmd_add_device() { + local device_id="${1:-}" + local device_name="${2:-mobile}" + + if [[ -z "$device_id" ]]; then + echo "Usage: usync add-device [name]" + echo "" + echo "Get the device ID from Syncthing app:" + echo " Menu > Show device ID" + exit 1 + fi + + # Clean up device ID (remove spaces, lowercase) + device_id=$(echo "$device_id" | tr -d ' \n' | tr '[:lower:]' '[:upper:]') + + # Validate format (basic check) + if [[ ! "$device_id" =~ ^[A-Z0-9-]{52,63}$ ]]; then + log_warn "Device ID looks unusual (expected 52-63 chars). Continuing anyway..." + fi + + if command -v syncthing &>/dev/null && syncthing cli show system &>/dev/null 2>&1; then + log_info "Adding device via Syncthing CLI..." + + # Add device to syncthing + syncthing cli config devices add --device-id "$device_id" --name "$device_name" + + # Auto-accept folders from this device (convenient for mobile) + syncthing cli config devices "$device_id" auto-accept-folders set true + + log_success "Added device: $device_name ($device_id)" + echo "" + echo "The device should appear in Syncthing shortly." + echo "You may need to accept the connection on the mobile device." + else + # Store for later if syncthing not running + register_device_manual "$device_id" "$device_name" + log_success "Stored device: $device_name" + echo "Device will be added when Syncthing starts." + fi +} + +register_device_manual() { + local device_id="$1" + local device_name="$2" + + ensure_dir + + if [[ ! -f "$USYNC_DEVICES" ]]; then + echo '{"devices":{}}' > "$USYNC_DEVICES" + fi + + local tmp + tmp=$(mktemp) + jq --arg id "$device_id" --arg name "$device_name" \ + '.devices[$name] = {id: $id, added: (now | todate), pending: true}' \ + "$USYNC_DEVICES" > "$tmp" && mv "$tmp" "$USYNC_DEVICES" +} + +# ============================================================================ +# JOIN - Join an existing setup (interactive, mobile-friendly output) +# ============================================================================ +cmd_join() { + echo "" + echo -e "${BOLD}=== Join Existing Setup ===${NC}" + echo "" + + local device_id + device_id=$(get_or_create_device_id) + + echo "This device's ID:" + echo "" + echo -e "${GREEN}$device_id${NC}" + echo "" + + # Generate QR for easy sharing + if command -v qrencode &>/dev/null; then + echo "Other devices can scan this:" + qrencode -t ANSIUTF8 "$device_id" + echo "" + fi + + echo "To complete setup:" + echo " 1. On another device, run:" + echo -e " ${BOLD}usync add-device $device_id $(hostname)${NC}" + echo "" + echo " 2. Or add manually in Syncthing GUI" + echo "" + + echo -n "Press Enter when the other device has added you..." + read -r + + # Trigger a rescan + if command -v syncthing &>/dev/null && syncthing cli show system &>/dev/null 2>&1; then + syncthing cli scan --all 2>/dev/null || true + fi + + log_success "Done! Folders will sync automatically." +} + +# ============================================================================ +# SYNC COMMANDS (original functionality) +# ============================================================================ sync_nb() { log_info "Syncing nb notebooks..." - if ! command -v nb &> /dev/null; then - log_error "nb not found in PATH" - return 1 + if ! command -v nb &>/dev/null; then + log_warn "nb not found" + return 0 fi - # Get list of notebooks + local notebooks notebooks=$(nb notebooks --names 2>/dev/null || echo "") - if [ -z "$notebooks" ]; then + if [[ -z "$notebooks" ]]; then log_warn "No nb notebooks found" return 0 fi - # Sync each notebook for notebook in $notebooks; do - log_info " Syncing notebook: $notebook" + log_step "Syncing: $notebook" if nb "$notebook:sync" 2>/dev/null; then - log_success " $notebook synced" + log_success "$notebook" else - log_warn " $notebook sync failed (no remote?)" + log_warn "$notebook (no remote?)" fi done } @@ -65,88 +377,132 @@ sync_nb() { sync_syncthing() { log_info "Triggering Syncthing scan..." - if ! command -v syncthing &> /dev/null; then - log_error "syncthing not found in PATH" - return 1 + if ! command -v syncthing &>/dev/null; then + log_warn "syncthing not found" + return 0 fi - # Check if Syncthing is running - if ! syncthing cli show system &> /dev/null; then - log_warn "Syncthing is not running" - return 1 + if ! syncthing cli show system &>/dev/null 2>&1; then + log_warn "Syncthing not running" + return 0 fi - # Trigger scan on all folders if syncthing cli scan --all 2>/dev/null; then - log_success "Syncthing scan triggered" + log_success "Scan triggered" else - log_error "Failed to trigger Syncthing scan" - return 1 + log_warn "Scan failed" fi } show_status() { - echo "=== Sync Status ===" + echo -e "${BOLD}=== Sync Status ===${NC}" echo "" - # nb status - echo "--- nb Notebooks ---" - if command -v nb &> /dev/null; then - nb notebooks 2>/dev/null || echo " No notebooks" - echo "" - echo "Last sync times:" - for notebook in $(nb notebooks --names 2>/dev/null); do - nb_dir="${NB_DIR:-$HOME/.nb}" - if [ -d "$nb_dir/$notebook/.git" ]; then - last_commit=$(git -C "$nb_dir/$notebook" log -1 --format="%ar" 2>/dev/null || echo "never") - echo " $notebook: $last_commit" + # Device ID + local device_id + device_id=$(get_or_create_device_id 2>/dev/null || echo "unknown") + echo -e "This device: ${DIM}${device_id:0:20}...${NC}" + echo "" + + # nb + echo "nb notebooks:" + if command -v nb &>/dev/null; then + local nb_dir="${NB_DIR:-$HOME/.nb}" + for notebook in $(nb notebooks --names 2>/dev/null || true); do + if [[ -d "$nb_dir/$notebook/.git" ]]; then + local last + last=$(git -C "$nb_dir/$notebook" log -1 --format="%ar" 2>/dev/null || echo "never") + echo -e " ${GREEN}*${NC} $notebook (synced $last)" + else + echo -e " ${YELLOW}*${NC} $notebook (local only)" fi done else - echo " nb not installed" + echo " (nb not installed)" fi echo "" - # Syncthing status - echo "--- Syncthing ---" - if command -v syncthing &> /dev/null && syncthing cli show system &> /dev/null; then - syncthing cli show system 2>/dev/null | grep -E "(myID|startTime)" || true + # Syncthing + echo "Syncthing:" + if command -v syncthing &>/dev/null && syncthing cli show system &>/dev/null 2>&1; then + local folders + folders=$(syncthing cli show config 2>/dev/null | jq -r '.folders[] | " \(.id): \(.path)"' 2>/dev/null || echo " (error)") + echo "$folders" + echo "" - echo "Folders:" - syncthing cli show config 2>/dev/null | jq -r '.folders[] | " \(.label // .id): \(.path)"' 2>/dev/null || echo " Unable to get folder info" + echo "Connected devices:" + syncthing cli show connections 2>/dev/null | jq -r '.connections | to_entries[] | select(.value.connected) | " \(.key[:15])... (\(.value.type))"' 2>/dev/null || echo " (none)" else - echo " Syncthing not running" + echo " (not running)" fi } -# Main -case "${1:-all}" in +# ============================================================================ +# MAIN +# ============================================================================ +show_help() { + cat <<'EOF' +usync - Unified sync & setup + +SETUP (first time): + usync init Bootstrap from config repo + usync join Join existing setup (shows QR) + +PAIRING: + usync invite Show QR code for mobile to scan + usync add-device [name] Add a device by ID + +SYNC: + usync Sync everything + usync nb Sync nb notebooks only + usync st Sync Syncthing only + usync status Show sync status + +EXAMPLES: + # Bootstrap new desktop from config repo: + usync init git@server:me/config.git AGE-SECRET-KEY-1XXXX... + + # Pair mobile phone: + usync invite # on desktop, scan QR with phone + usync add-device XXXXX mobile # after getting phone's ID + + # Daily sync: + usync +EOF +} + +case "${1:-sync}" in + init) + shift + cmd_init "$@" + ;; + invite|pair|qr) + cmd_invite + ;; + add-device|add) + shift + cmd_add_device "$@" + ;; + join) + cmd_join + ;; nb) sync_nb ;; st|syncthing) sync_syncthing ;; - status) + status|s) show_status ;; - all|"") + sync|"") sync_nb echo "" sync_syncthing - echo "" - log_success "All sync operations complete" ;; -h|--help|help) - echo "usync - Unified sync command" - echo "" - echo "Usage:" - echo " usync Sync everything (nb + Syncthing)" - echo " usync nb Sync only nb notebooks" - echo " usync st Trigger Syncthing scan" - echo " usync status Show sync status" - echo " usync help Show this help" + show_help ;; *) log_error "Unknown command: $1" diff --git a/setup b/setup new file mode 100755 index 0000000..ecb8477 --- /dev/null +++ b/setup @@ -0,0 +1,526 @@ +#!/usr/bin/env bash +# +# setup - One command setup for Ultimate Notetaking System +# +# Usage: +# ./setup # Interactive setup +# ./setup # Non-interactive join +# ./setup mobile # Pair mobile device +# +set -euo pipefail + +# Colors +R='\033[0;31m' G='\033[0;32m' Y='\033[1;33m' B='\033[0;34m' +BOLD='\033[1m' DIM='\033[2m' NC='\033[0m' + +info() { echo -e "${B}::${NC} $1"; } +ok() { echo -e "${G}OK${NC} $1"; } +warn() { echo -e "${Y}!!${NC} $1"; } +err() { echo -e "${R}ERR${NC} $1" >&2; } +step() { echo -e "${DIM}->>${NC} $1"; } + +# Config +USYNC_DIR="$HOME/.config/usync" +AGE_KEY_FILE="$USYNC_DIR/age-key.txt" + +has() { command -v "$1" &>/dev/null; } + +ensure_deps() { + local missing=() + for cmd in git jq; do + has "$cmd" || missing+=("$cmd") + done + + if [[ ${#missing[@]} -gt 0 ]]; then + err "Missing: ${missing[*]}" + echo "Install with: nix-shell -p ${missing[*]}" + exit 1 + fi + + # Optional but recommended + has syncthing || warn "syncthing not found - file sync won't work" + has age || warn "age not found - install for secret management" + has qrencode || warn "qrencode not found - QR codes won't work" +} + +get_device_id() { + if has syncthing; then + # Try running instance + local id + id=$(syncthing cli show system 2>/dev/null | jq -r '.myID // empty' 2>/dev/null || true) + [[ -n "$id" ]] && { echo "$id"; return; } + + # Try config file + local cfg="$HOME/.config/syncthing/config.xml" + if [[ -f "$cfg" ]]; then + id=$(grep -oP '(?<=/dev/null | head -1 || true) + [[ -n "$id" ]] && { echo "$id"; return; } + fi + + # Generate new + step "Generating Syncthing identity..." + syncthing generate --skip-port-probing &>/dev/null || true + syncthing cli show system 2>/dev/null | jq -r '.myID' 2>/dev/null || echo "" + fi +} + +# ============================================================================= +# SETUP NEW (first device) +# ============================================================================= +setup_new() { + echo "" + echo -e "${BOLD}Setting up first device...${NC}" + echo "" + + mkdir -p "$USYNC_DIR" + + # Generate age key + if [[ -f "$AGE_KEY_FILE" ]]; then + warn "Age key already exists at $AGE_KEY_FILE" + else + if has age-keygen; then + step "Generating age encryption key..." + age-keygen -o "$AGE_KEY_FILE" 2>&1 | grep "public key" + chmod 600 "$AGE_KEY_FILE" + else + err "age not installed. Run: nix-shell -p age" + exit 1 + fi + fi + + # Get/create syncthing ID + local device_id + device_id=$(get_device_id) + + # Create local config structure + local config_dir="$USYNC_DIR/config" + if [[ ! -d "$config_dir" ]]; then + step "Creating config structure..." + mkdir -p "$config_dir/hosts/$(hostname)" "$config_dir/secrets" + + # Create minimal secrets template + cat > "$config_dir/secrets/secrets.yaml" <<'EOF' +# Encrypted with sops - edit with: sops secrets/secrets.yaml +restic-password: "changeme" +EOF + + # Init git repo + git -C "$config_dir" init -q + git -C "$config_dir" add -A + git -C "$config_dir" commit -q -m "Initial setup" + fi + + # Summary + echo "" + echo -e "${G}=== Setup Complete ===${NC}" + echo "" + echo "Your age key (SAVE THIS SOMEWHERE SAFE):" + echo -e "${BOLD}$(cat "$AGE_KEY_FILE" | grep 'AGE-SECRET-KEY')${NC}" + echo "" + if [[ -n "$device_id" ]]; then + echo "Device ID: ${device_id:0:20}..." + fi + echo "" + echo "Next steps:" + echo " 1. Push config to git server:" + echo " cd $config_dir && git remote add origin && git push -u origin main" + echo "" + echo " 2. On other devices, run:" + echo " ./setup " + echo "" + echo " 3. To pair mobile:" + echo " ./setup mobile" +} + +# ============================================================================= +# SETUP JOIN (existing setup) +# ============================================================================= +setup_join() { + local git_url="$1" + local age_key="$2" + + echo "" + echo -e "${BOLD}Joining existing setup...${NC}" + echo "" + + mkdir -p "$USYNC_DIR" + + # Handle @file syntax + if [[ "$age_key" == @* ]]; then + local keyfile="${age_key:1}" + [[ -f "$keyfile" ]] || { err "Key file not found: $keyfile"; exit 1; } + age_key=$(cat "$keyfile") + fi + + # Save age key + echo "$age_key" > "$AGE_KEY_FILE" + chmod 600 "$AGE_KEY_FILE" + step "Saved age key" + + # Clone config + local config_dir="$USYNC_DIR/config" + if [[ -d "$config_dir" ]]; then + step "Updating config..." + git -C "$config_dir" pull --ff-only 2>/dev/null || git -C "$config_dir" fetch + else + step "Cloning config..." + git clone "$git_url" "$config_dir" + fi + + # Decrypt secrets + if has sops && [[ -f "$config_dir/secrets/secrets.yaml" ]]; then + step "Decrypting secrets..." + export SOPS_AGE_KEY="$age_key" + sops -d "$config_dir/secrets/secrets.yaml" > "$USYNC_DIR/secrets.yaml" 2>/dev/null || warn "Could not decrypt secrets" + chmod 600 "$USYNC_DIR/secrets.yaml" 2>/dev/null || true + fi + + # Setup syncthing + local device_id + device_id=$(get_device_id) + + # Register device + local devices_file="$USYNC_DIR/devices.json" + echo "{\"devices\":{\"$(hostname)\":{\"id\":\"$device_id\",\"added\":\"$(date -Iseconds)\"}}}" > "$devices_file" + + echo "" + echo -e "${G}=== Joined! ===${NC}" + echo "" + if [[ -n "$device_id" ]]; then + echo "This device: ${device_id:0:20}..." + echo "" + echo "Add this device on your other machines:" + echo -e " ${BOLD}./setup add $device_id $(hostname)${NC}" + fi + echo "" + echo "To pair mobile: ./setup mobile" + echo "To sync: ./scripts/usync" +} + +# ============================================================================= +# MOBILE PAIRING +# ============================================================================= +scan_qr_camera() { + # Check for video devices + if [[ ! -e /dev/video0 ]]; then + err "No camera found (/dev/video0 doesn't exist)" >&2 + return 1 + fi + + if ! has ffmpeg; then + err "ffmpeg not found. Run: nix-shell -p ffmpeg zbar" >&2 + return 1 + fi + if ! has zbarimg; then + err "zbarimg not found. Run: nix-shell -p zbar" >&2 + return 1 + fi + + local tmpdir="/tmp/usync_qr_$$" + local result_file="$tmpdir/result" + local ffmpeg_pid="" + + mkdir -p "$tmpdir" + + cleanup_camera() { + [[ -n "$ffmpeg_pid" ]] && kill "$ffmpeg_pid" 2>/dev/null || true + rm -rf "$tmpdir" + } + trap cleanup_camera RETURN EXIT + + echo "" >&2 + echo "Point camera at QR code - it will scan automatically." >&2 + echo "(Press Ctrl+C to cancel)" >&2 + echo "" >&2 + + # Start ffmpeg: show preview with crosshair AND save frames for scanning + ffmpeg -f v4l2 -video_size 640x480 -i /dev/video0 \ + -vf "drawbox=x=iw/2-100:y=ih/2-100:w=200:h=200:color=green:t=3" \ + -f sdl2 -window_title "Scan QR Code" - \ + -vf "fps=2" -update 1 -q:v 2 "$tmpdir/frame.jpg" \ + -loglevel quiet 2>/dev/null & + ffmpeg_pid=$! + + sleep 1 # Let camera warm up + + # Scan frames as they're captured + for attempt in {1..60}; do + sleep 0.5 + + # Check if ffmpeg died + if ! kill -0 "$ffmpeg_pid" 2>/dev/null; then + break + fi + + if [[ -f "$tmpdir/frame.jpg" ]]; then + local result + result=$(zbarimg --raw -q "$tmpdir/frame.jpg" 2>/dev/null || true) + + if [[ -n "$result" ]]; then + result=$(echo "$result" | tr -d ' \n\r' | tr '[:lower:]' '[:upper:]') + + if [[ "$result" =~ ^[A-Z0-9]{7}-[A-Z0-9]{7}-[A-Z0-9]{7}-[A-Z0-9]{7}-[A-Z0-9]{7}-[A-Z0-9]{7}-[A-Z0-9]{7}-[A-Z0-9]{7}$ ]]; then + echo "" >&2 + ok "Scanned!" >&2 + echo "$result" + return 0 + fi + fi + fi + done + + echo "" >&2 + warn "No QR code detected (timed out after 30s)" >&2 + return 1 +} + +check_pending_devices() { + if ! has syncthing || ! syncthing cli show system &>/dev/null 2>&1; then + return 1 + fi + + # Get pending devices (devices trying to connect that we haven't approved) + local pending + pending=$(syncthing cli show pending devices 2>/dev/null || true) + + if [[ -n "$pending" && "$pending" != "{}" && "$pending" != "null" ]]; then + echo "$pending" | jq -r 'to_entries[] | "\(.key)|\(.value.name // "unknown")"' 2>/dev/null + return 0 + fi + return 1 +} + +check_local_discovery() { + if ! has syncthing || ! syncthing cli show system &>/dev/null 2>&1; then + return 1 + fi + + # Check discovery cache for LAN devices + local discovery + discovery=$(syncthing cli show discovery 2>/dev/null || true) + + if [[ -n "$discovery" && "$discovery" != "{}" && "$discovery" != "null" ]]; then + echo "$discovery" | jq -r 'to_entries[] | select(.value[] | contains("tcp://192.") or contains("tcp://10.") or contains("tcp://172.")) | .key' 2>/dev/null | head -5 + return 0 + fi + return 1 +} + +add_phone_device() { + local phone_id="$1" + local name="${2:-phone}" + + phone_id=$(echo "$phone_id" | tr -d ' \n' | tr '[:lower:]' '[:upper:]') + + if has syncthing && syncthing cli show system &>/dev/null 2>&1; then + syncthing cli config devices add --device-id "$phone_id" --name "$name" 2>/dev/null || true + syncthing cli config devices "$phone_id" auto-accept-folders set true 2>/dev/null || true + ok "Added: $name" + return 0 + else + mkdir -p "$USYNC_DIR" + echo "$phone_id" > "$USYNC_DIR/pending-phone.id" + ok "Saved (will add when syncthing starts)" + return 0 + fi +} + +setup_mobile() { + echo "" + echo -e "${BOLD}=== Pair Mobile Device ===${NC}" + echo "" + + local device_id + device_id=$(get_device_id) + + if [[ -z "$device_id" ]]; then + err "Could not get device ID. Is syncthing installed?" + exit 1 + fi + + # Check for pending devices first (phone already trying to connect) + local pending + pending=$(check_pending_devices 2>/dev/null || true) + + if [[ -n "$pending" ]]; then + echo -e "${G}Found device(s) waiting to connect:${NC}" + echo "" + local i=1 + declare -a pending_ids + while IFS='|' read -r id name; do + echo " [$i] $name (${id:0:20}...)" + pending_ids+=("$id") + ((i++)) + done <<< "$pending" + echo "" + echo -n "Accept which? [1-$((i-1))], or Enter to continue: " + read -r choice + + if [[ -n "$choice" && "$choice" =~ ^[0-9]+$ && "$choice" -ge 1 && "$choice" -lt "$i" ]]; then + local selected_id="${pending_ids[$((choice-1))]}" + add_phone_device "$selected_id" "phone" + echo "" + ok "Mobile pairing complete!" + return + fi + echo "" + fi + + # Step 1: Show computer's ID for phone + echo -e "${BOLD}Step 1: Add this computer on your phone${NC}" + echo "" + echo "Computer's Device ID:" + echo "" + echo -e "${G}$device_id${NC}" + echo "" + + if has qrencode; then + echo "Or scan this QR in Syncthing app:" + echo "" + qrencode -t ANSIUTF8 "$device_id" + fi + + echo "" + echo "In Syncthing app: tap (+) > paste or scan above" + echo "" + echo -n "Press Enter when done..." + read -r + + # Step 2: Get phone's ID + echo "" + echo -e "${BOLD}Step 2: Add your phone on this computer${NC}" + echo "" + echo "In Syncthing app: Menu (3 dots) > Show device ID" + echo "" + + # Try camera first if available + if has zbarimg && has ffmpeg && [[ -e /dev/video0 ]]; then + echo " [1] Scan phone's QR with camera" + echo " [2] Paste phone's device ID" + echo "" + echo -n "Choice [1/2]: " + read -r method + + if [[ "$method" == "1" ]]; then + local scanned + if scanned=$(scan_qr_camera); then + add_phone_device "$scanned" "phone" + echo "" + ok "Pairing complete! Devices should connect shortly." + return + fi + echo "" + echo "Camera didn't work. Paste the ID instead:" + fi + fi + + echo -n "Paste phone's device ID: " + read -r phone_id + + if [[ -n "$phone_id" ]]; then + add_phone_device "$phone_id" "phone" + echo "" + ok "Pairing complete! Devices should connect shortly." + else + warn "No ID entered. Run './setup mobile' to try again." + fi +} + +# ============================================================================= +# ADD DEVICE +# ============================================================================= +add_device() { + local device_id="$1" + local name="${2:-device}" + + device_id=$(echo "$device_id" | tr -d ' \n' | tr '[:lower:]' '[:upper:]') + + if has syncthing && syncthing cli show system &>/dev/null 2>&1; then + syncthing cli config devices add --device-id "$device_id" --name "$name" + syncthing cli config devices "$device_id" auto-accept-folders set true + ok "Added device: $name" + else + mkdir -p "$USYNC_DIR" + echo "{\"id\":\"$device_id\",\"name\":\"$name\"}" >> "$USYNC_DIR/pending-devices.json" + ok "Saved device (will add when syncthing runs)" + fi +} + +# ============================================================================= +# INTERACTIVE +# ============================================================================= +interactive() { + echo "" + echo -e "${BOLD}Ultimate Notetaking System - Setup${NC}" + echo "" + echo " [1] New setup (first device)" + echo " [2] Join existing setup" + echo " [3] Pair mobile device" + echo "" + echo -n "Choice [1/2/3]: " + read -r choice + + case "$choice" in + 1|n|new) + setup_new + ;; + 2|j|join) + echo "" + echo -n "Config git URL: " + read -r git_url + echo -n "Age secret key: " + read -r age_key + setup_join "$git_url" "$age_key" + ;; + 3|m|mobile) + setup_mobile + ;; + *) + err "Invalid choice" + exit 1 + ;; + esac +} + +# ============================================================================= +# MAIN +# ============================================================================= +ensure_deps + +case "${1:-}" in + "") + interactive + ;; + mobile|phone|m) + setup_mobile + ;; + add) + [[ -z "${2:-}" ]] && { err "Usage: ./setup add [name]"; exit 1; } + add_device "$2" "${3:-}" + ;; + help|-h|--help) + cat <<'EOF' +setup - One command setup + +Usage: + ./setup Interactive setup + ./setup Join existing setup + ./setup mobile Pair mobile device + ./setup add [name] Add a device + +Examples: + ./setup # Interactive + ./setup git@srv:me/cfg.git AGE-SECRET-KEY-1... # Join + ./setup mobile # Pair phone +EOF + ;; + *) + # Assume it's: ./setup + if [[ -n "${2:-}" ]]; then + setup_join "$1" "$2" + else + err "Usage: ./setup " + exit 1 + fi + ;; +esac