From 32b7e28c25762f32a8ebfba906fa220df61ff06f Mon Sep 17 00:00:00 2001 From: Dave Hammer Date: Sat, 8 Aug 2026 17:48:59 -0400 Subject: [PATCH] Add davemhammer/obsidian (#285) * Add davemhammer/obsidian Address review: vault path confinement, open_daily vault check, declare runtime deps (xdg-open, find, sort, head), version 1.0.3. * Address review: realpath + symlink confinement for vault paths Reject symlink path components and require realpath -m under vault before daily read/mkdir/write. Add realpath dependency; v1.0.4. * Harden recent-notes scan: find -P + path normalize (v1.0.5) Explicitly never follow symlinks when listing recent markdown; drop invalid relative paths before display. Align README Notes and bump version so PR trust attestation matches code. * Docs: clarify obsidian URI handler and IPC (v1.0.6) Align README with code for dependency role, capture IPC, and git abort. --- obsidian/README.md | 90 +++ obsidian/assets/obsidian-green.png | Bin 0 -> 2553 bytes obsidian/assets/obsidian-green.svg | 1 + obsidian/assets/obsidian-grey.png | Bin 0 -> 2480 bytes obsidian/assets/obsidian-grey.svg | 1 + obsidian/assets/obsidian-purple.png | Bin 0 -> 2550 bytes obsidian/assets/obsidian-purple.svg | 1 + obsidian/assets/obsidian.svg | 1 + obsidian/launcher.luau | 191 +++++ obsidian/panel.luau | 568 +++++++++++++++ obsidian/plugin.toml | 97 +++ obsidian/service.luau | 1010 +++++++++++++++++++++++++++ obsidian/thumbnail.webp | Bin 0 -> 52438 bytes obsidian/translations/en.json | 120 ++++ obsidian/widget.luau | 103 +++ 15 files changed, 2183 insertions(+) create mode 100644 obsidian/README.md create mode 100644 obsidian/assets/obsidian-green.png create mode 100644 obsidian/assets/obsidian-green.svg create mode 100644 obsidian/assets/obsidian-grey.png create mode 100644 obsidian/assets/obsidian-grey.svg create mode 100644 obsidian/assets/obsidian-purple.png create mode 100644 obsidian/assets/obsidian-purple.svg create mode 100644 obsidian/assets/obsidian.svg create mode 100644 obsidian/launcher.luau create mode 100644 obsidian/panel.luau create mode 100644 obsidian/plugin.toml create mode 100644 obsidian/service.luau create mode 100644 obsidian/thumbnail.webp create mode 100644 obsidian/translations/en.json create mode 100644 obsidian/widget.luau diff --git a/obsidian/README.md b/obsidian/README.md new file mode 100644 index 0000000..2756c27 --- /dev/null +++ b/obsidian/README.md @@ -0,0 +1,90 @@ +# Obsidian + +Quick daily capture and git sync for a local [Obsidian](https://obsidian.md/) vault from Noctalia. + +## Plugin + +| Field | Value | +| --- | --- | +| ID | `davemhammer/obsidian` | +| Entries | Bar widget: `status`; panel: `manager`; service: `service`; launcher: `ob` | +| Launcher Prefix | `/ob` | + +## Requirements + +Install these on `PATH` (declared in `plugin.toml` `dependencies`): + +- `obsidian` — desktop app / URI handler so `obsidian://` opens work (the plugin launches URIs via `xdg-open`, not the CLI) +- `git` — status, commit, pull, push, abort +- `xdg-open` — launches `obsidian://` URIs +- `find`, `sort`, `head` — recent-notes scan under the vault (`find -P`) +- `realpath` — canonicalize paths before read/write (symlink escape checks) + +Also configure a local vault path (must contain `.obsidian`). + +## Usage + +Set **Vault path** (and optional daily folder/format) in plugin settings. Daily notes default to the vault root with filenames `%Y-%m-%d` (override folder/format to match your Daily Notes plugin). + +Add the **status** bar widget (`davemhammer/obsidian:status`): + +- **Click** — open panel +- **Right-click** — open today’s daily note + +Panel tabs: + +- **Daily** — type a line and **Add to daily** (appends `- HH:MM text`); open daily +- **Recent** — last modified markdown notes; open / copy `[[link]]` / path +- **Git** — dirty list; commit (optional message), pull, push, pull+push; **Abort** if rebase/merge stuck + +Launcher: + +- `/ob` — categories +- `/ob capture buy milk` — append timestamped line to daily +- `/ob daily` — open daily +- `/ob git` — git actions + +```sh +noctalia msg panel-toggle davemhammer/obsidian:manager +``` + +## Settings + +| Setting | Type | Default | Description | +| --- | --- | --- | --- | +| `vault_path` | `folder` | `~/Documents/Obsidian Vault` | Vault root (must contain `.obsidian`). | +| `vault_name` | `string` | _(empty)_ | Name for `obsidian://` URIs; empty = folder name. | +| `daily_folder` | `string` | _(empty)_ | Daily notes folder relative to vault (empty = vault root). Must stay under the vault (`..` rejected). | +| `daily_format` | `string` | `%Y-%m-%d` | Daily filename stem without path separators (strftime); `.md` is appended. | +| `git_commit_message` | `string` | `vault: capture from Noctalia` | Default commit message. | +| `refresh_interval` | `int` | `20` | Rescan recent notes + git every N seconds. | +| `notify_on_action` | `bool` | `true` | Notify after capture/git actions. | +| `show_dirty` | `bool` (widget) | `true` | Show dirty file count on the bar. | + +## IPC + +```sh +noctalia msg panel-toggle davemhammer/obsidian:manager +noctalia msg plugin davemhammer/obsidian:service all refresh +noctalia msg plugin davemhammer/obsidian:service all daily +noctalia msg plugin davemhammer/obsidian:service all pull +noctalia msg plugin davemhammer/obsidian:service all push +noctalia msg plugin davemhammer/obsidian:service all sync +``` + +Capture with payload (text line for daily): + +```sh +noctalia msg plugin davemhammer/obsidian:service all capture '{"text":"buy milk"}' +``` + +(Exact payload syntax depends on your Noctalia IPC version; the service expects event `capture` with a table containing `text` or `line`.) + +## Notes + +- **Filesystem:** reads/writes daily note markdown **only under** the configured vault; `daily_folder` / note paths reject `..` and absolute paths; existing path components that are symlinks are refused; `realpath` must keep the target under the vault. Recent scan uses `find -P` (never follows symlinks) for `*.md` mtimes (skips `.obsidian`, `.git`, `.claudian`); listed paths are re-normalized before display and again before open. +- **Processes:** `find -P`, `sort`, `head` (recent notes); `realpath` + `test -L` (path confinement); `git status|add|commit|pull|push` and `git rebase|merge --abort` when requested; `xdg-open` for `obsidian://` URIs only. +- **Vault required:** capture and open-daily refuse to create files unless `vault_path` is a real vault (contains `.obsidian`). +- **Git pull** uses `git pull --no-rebase --autostash` so the vault is not left mid-rebase. If a rebase/merge is already in progress, use **Abort rebase/merge** on the Git tab. +- Does not talk to Obsidian Sync cloud APIs; git is your sync layer. +- Brand assets under `assets/` (Simple Icons–style mark). diff --git a/obsidian/assets/obsidian-green.png b/obsidian/assets/obsidian-green.png new file mode 100644 index 0000000000000000000000000000000000000000..696755d7a46108cf8e9c6be7c035fefe06f13c5c GIT binary patch literal 2553 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 \ No newline at end of file diff --git a/obsidian/assets/obsidian-grey.png b/obsidian/assets/obsidian-grey.png new file mode 100644 index 0000000000000000000000000000000000000000..881b3dc920c190552868047275a7334e7d539201 GIT binary patch literal 2480 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 KxvXObsidian \ No newline at end of file diff --git a/obsidian/assets/obsidian-purple.png b/obsidian/assets/obsidian-purple.png new file mode 100644 index 0000000000000000000000000000000000000000..b672072721772aad764826524e8874374f485512 GIT binary patch literal 2550 zcmeAS@N?(olHy`uVBq!ia0y~yU~m9o4mJh`hE7etSW1 zxz&S+qh69H-28o;7Hs7a%`JVkrEh!Cn=2dFhAh|8x+$Bra{ERrzp0uP2PfPWX!Y2% z`}te>*}h^oPVZD%cs~BV<^A9L?r%On?|sp|_tu9BkF0$-@3U@?#vNHZ|%!xjXdhGN!AgZGt-l zBdp_UdOl`x9GJ6O^rgoq!BDQhKXhF5<=nj)uRt&!TXspsXEMER04KYP0w zp6$G=zN~gv?1LKVXYxK_g?)%Ej{rYT0eX`Y!4J|UdyH3 zAd6|nii={NIZUzr-J;75R2A=D!nkTiiEcpnfmK1em4|xXXa_v!TdZ_|S3EP-YEq5Q z!{d6+Z$5r!l|OaVDI)YxSz46#wi&usTB~ma3NHBN@Tu-W^*ZJi_cHgXHZuhOu3FFN zrNe|(Vrv*uI6J15t?!WZ>S zc0XI&zqvo;Y`AQ*TbULZ%iZFtTD^JN+Jg@_S)7&? zX?U&oB#OZ|ebLKTN17KfT@YOT*5!-g?tjxf&VFc}pp%%zel=~!@9sRASk4V)|I{zD z?&JT|yshBUnY;e7W?>Bfw~DGvHZNLT_omI=?o!)(A@jO*EFUEQi5_5K5M7ilJz1PF z=Z5GT<0HGIq#rjX_U!r0eN4{W=F>v0$I1szeYn@Dwado7ZQ-vCmiC6i55BF-)bIFk zx~Nb`_NIEk#xkp?g(fVyX5wGY6}@^M(V(?znwUpvyHVS{>ANj5zOG;D6E0v;z9i>O z!KuXze>6|t@Lk4pcp`&Zmcp)00du=zE=l?6Fy4?@|34<=)Y}hrC+Ezc!SsOp%U-50 z)(?X?lw#&q3%xxwk>O>*`=bsQpRVVaoo87bFX~Xe=UL%}@a~&T`=;36&OiHhGk^2H zlbq9)c(g9tl}^zvxyE`#Yx+jZm7BB-Ozb*VooJiXtL@!(T8ZECQIJSc=$6d-?%3?x zOlx8{{9mmQuYY^bhEsFCUsP!*PiL7QP_ci;+C}0|=1DBp{vosBy59Ur8+y)`eHY5* zPLL?t?0b3FmzGnn8Mft3}#pO*2nftQ?D(%=eF?&=Zo?=Ka(Vrba(B|zQvuoW173koE@9b`{y8-s}~wyBw!MKrN1C~ zr(4&9IafbVy2>Egvt8r&Ztg?s8+6`2etYfMlhB>B-)?+RyFzEXWO4P%QsQdKLR51tlb8mJwP4u0~>ni<0w25nl#=Ez&Ns|9RJ+jwoUuqrC^;Y$Pb^O$;pM$;JnO%vR9{6jpl#n z-}g-Qz>Hc;sa}Obsidian \ No newline at end of file diff --git a/obsidian/assets/obsidian.svg b/obsidian/assets/obsidian.svg new file mode 100644 index 0000000..fc35287 --- /dev/null +++ b/obsidian/assets/obsidian.svg @@ -0,0 +1 @@ +Obsidian \ No newline at end of file diff --git a/obsidian/launcher.luau b/obsidian/launcher.luau new file mode 100644 index 0000000..4ce7a60 --- /dev/null +++ b/obsidian/launcher.luau @@ -0,0 +1,191 @@ +--!nonstrict +-- /ob launcher: daily, capture, git, panel. + +local STATE_KEY = "obs_snapshot" +local COMMAND_KEY = "obs_command" +local PANEL_ID = "davemhammer/obsidian:manager" + +local snapshot = noctalia.state.get(STATE_KEY) or { + available = false, + loading = true, + vaultName = "", + dailyRel = "", + git = { dirty = 0, isRepo = false, clean = true }, + recent = {}, +} + +noctalia.state.watch(STATE_KEY, function(value) + if type(value) == "table" then + snapshot = value + end +end) + +local function trim(s) + return noctalia.string.trim(tostring(s or "")) +end + +local function lower(s) + return string.lower(tostring(s or "")) +end + +local function send(action, values) + local command = { action = action, requestId = "launcher-" .. tostring(os.time()) } + if type(values) == "table" then + for k, v in pairs(values) do + command[k] = v + end + end + noctalia.state.set(COMMAND_KEY, command) +end + +local function statusRow(title, subtitle, glyph) + return { id = "", title = title, subtitle = subtitle, glyph = glyph or "notebook" } +end + +local function topCategories() + local g = snapshot.git or {} + local gitSub = "—" + if g.isRepo then + gitSub = g.clean and "clean" or (tostring(g.dirty or 0) .. " dirty") + else + gitSub = "no git" + end + return { + { id = "act:daily", title = noctalia.tr("launcher.cat.daily"), subtitle = snapshot.dailyRel or "", glyph = "calendar", score = 100 }, + { id = "act:capture", title = noctalia.tr("launcher.cat.capture"), subtitle = noctalia.tr("launcher.cat.capture-sub"), glyph = "plus", score = 95 }, + { id = "act:git", title = noctalia.tr("launcher.cat.git"), subtitle = gitSub, glyph = "git-commit", score = 90 }, + { id = "act:panel", title = noctalia.tr("launcher.cat.panel"), subtitle = noctalia.tr("launcher.cat.panel-sub"), glyph = "layout-dashboard", score = 80 }, + { id = "act:refresh", title = noctalia.tr("launcher.cat.refresh"), subtitle = noctalia.tr("launcher.cat.refresh-sub"), glyph = "refresh", score = 50 }, + } +end + +function search(query) + query = trim(query) + if not snapshot.available and not snapshot.loading then + launcher.setResults(query, { statusRow(noctalia.tr("launcher.missing"), "", "alert-triangle") }) + return + end + if snapshot.loading and not snapshot.available then + launcher.setResults(query, { statusRow(noctalia.tr("launcher.loading"), snapshot.vaultName, "loader") }) + send("refresh") + return + end + + local head, rest = query:match("^(%S+)%s*(.-)$") + head = lower(head or "") + rest = trim(rest or "") + + if head == "" then + launcher.setResults(query, topCategories()) + return + end + + if head == "capture" or head == "c" or head == "note" then + if rest ~= "" then + launcher.setResults(query, { + { + id = "do:capture:" .. rest, + title = "Add to daily", + subtitle = rest, + glyph = "plus", + score = 100, + }, + }) + return + end + launcher.setResults(query, { + statusRow(noctalia.tr("launcher.cat.capture"), noctalia.tr("launcher.cat.capture-sub"), "plus"), + }) + return + end + + if head == "daily" or head == "d" then + launcher.setResults(query, { + { id = "act:daily", title = noctalia.tr("launcher.cat.daily"), subtitle = snapshot.dailyRel or "", glyph = "calendar", score = 100 }, + }) + return + end + + if head == "git" or head == "g" then + launcher.setResults(query, { + { id = "act:git", title = noctalia.tr("launcher.cat.git"), subtitle = "", glyph = "git-commit", score = 100 }, + { id = "act:commit", title = "Commit", subtitle = "git add -A && commit", glyph = "git-commit", score = 90 }, + { id = "act:pull", title = "Pull", subtitle = "git pull (merge)", glyph = "cloud-download", score = 80 }, + { id = "act:push", title = "Push", subtitle = "git push", glyph = "cloud-upload", score = 70 }, + { id = "act:sync", title = "Sync", subtitle = "pull + push", glyph = "refresh", score = 60 }, + }) + return + end + + -- free text: match recent note titles + local rows = {} + local q = lower(query) + for _, n in ipairs(snapshot.recent or {}) do + if lower(n.name):find(q, 1, true) or lower(n.rel):find(q, 1, true) then + table.insert(rows, { + id = "note:" .. n.rel, + title = n.name, + subtitle = n.rel, + glyph = "file-text", + score = 50, + }) + end + end + for _, row in ipairs(topCategories()) do + if lower(row.title):find(q, 1, true) then + table.insert(rows, row) + end + end + if #rows == 0 then + rows = topCategories() + end + launcher.setResults(query, rows) +end + +function activate(id) + if type(id) ~= "string" or id == "" then return end + + if id == "act:daily" then + send("open_daily") + return + end + if id == "act:capture" then + launcher.setQuery("capture ") + return + end + if id == "act:git" or id == "act:panel" then + noctalia.togglePanel(PANEL_ID) + return + end + if id == "act:refresh" then + send("refresh") + return + end + if id == "act:commit" then + send("git_commit", {}) + return + end + if id == "act:pull" then + send("git_pull") + return + end + if id == "act:push" then + send("git_push") + return + end + if id == "act:sync" then + send("git_sync") + return + end + + local cap = id:match("^do:capture:(.+)$") + if cap then + send("capture", { text = cap }) + return + end + + local note = id:match("^note:(.+)$") + if note then + send("open_note", { rel = note }) + end +end diff --git a/obsidian/panel.luau b/obsidian/panel.luau new file mode 100644 index 0000000..a281167 --- /dev/null +++ b/obsidian/panel.luau @@ -0,0 +1,568 @@ +--!nonstrict +-- Obsidian panel: daily capture, recent notes, git sync. + +local STATE_KEY = "obs_snapshot" +local COMMAND_KEY = "obs_command" +local RESULT_KEY = "obs_action_result" + +local snapshot = noctalia.state.get(STATE_KEY) or { + available = false, + loading = true, + busy = false, + vaultName = "", + dailyRel = "", + dailyExists = false, + dailyPreview = "", + recent = {}, + git = { + isRepo = false, + branch = "", + dirty = 0, + ahead = 0, + behind = 0, + clean = true, + files = {}, + }, + error = "", + updatedAt = 0, + revision = 0, +} + +local tab = "daily" -- daily | recent | git +local selectedId = "" +local filterText = "" +local filterKey = 0 +local captureText = "" +local captureKey = 0 +local commitText = "" +local commitKey = 0 +local requestCounter = 0 +local feedback = "" +local feedbackError = false +local dirty = true + +local render + +local function tr(key, subst) + return noctalia.tr(key, subst) +end + +local function nextRequestId() + requestCounter += 1 + return `panel-{requestCounter}` +end + +local function send(action, values) + local command = { action = action, requestId = nextRequestId() } + if type(values) == "table" then + for k, v in pairs(values) do + command[k] = v + end + end + noctalia.state.set(COMMAND_KEY, command) + return command.requestId +end + +local function lower(s) + return string.lower(tostring(s or "")) +end + +local function matchesFilter(...) + local q = noctalia.string.trim(filterText) + if q == "" then return true end + for raw in q:gmatch("%S+") do + local term = lower(raw) + local hit = false + for i = 1, select("#", ...) do + local part = lower(select(i, ...)) + if part ~= "" and part:find(term, 1, true) then + hit = true + break + end + end + if not hit then return false end + end + return true +end + +local function listButton(props) + props.contentAlign = "start" + props.controlSize = props.controlSize or "md" + return ui.button(props) +end + +local function selectedRecent() + if tab ~= "recent" then return nil end + for _, n in ipairs(snapshot.recent or {}) do + if n.id == selectedId then return n end + end + return nil +end + +local function gitSummary() + local g = snapshot.git or {} + if not g.isRepo then + return tr("panel.git_nogit") + end + if g.clean then + return tr("panel.git_clean") .. (g.branch ~= "" and (` · {g.branch}`) or "") + end + return tr("panel.git_branch", { + branch = g.branch ~= "" and g.branch or "—", + ahead = g.ahead or 0, + behind = g.behind or 0, + dirty = g.dirty or 0, + }) +end + +local function emptyList(msg) + return ui.column({ + key = "empty-" .. tab, + align = "center", + justify = "center", + padding = 24, + gap = 8, + flexGrow = 1, + }, { + ui.glyph({ name = "search", size = 36, color = "on_surface_variant" }), + ui.label({ text = msg, color = "on_surface_variant", textAlign = "center" }), + }) +end + +local function recentRows() + local rows = {} + for _, n in ipairs(snapshot.recent or {}) do + if matchesFilter(n.name, n.rel) then + local selected = n.id == selectedId + local when = "" + if (n.mtime or 0) > 0 then + when = noctalia.formatTime("%m-%d %H:%M", n.mtime) + end + table.insert(rows, listButton({ + key = "note-" .. n.id, + text = (when ~= "" and (when .. " · ") or "") .. n.rel, + glyph = "file-text", + variant = selected and "primary" or "outline", + selected = selected, + onClick = function() + selectedId = n.id + feedback = "" + render() + end, + })) + end + end + return rows +end + +local function gitFileRows() + local rows = {} + for _, f in ipairs((snapshot.git and snapshot.git.files) or {}) do + if matchesFilter(f.path, f.code) then + table.insert(rows, listButton({ + key = "gf-" .. f.id, + text = `{f.code} {f.path}`, + glyph = "git-commit", + variant = "outline", + onClick = function() + selectedId = f.id + render() + end, + })) + end + end + return rows +end + +local function itemList() + if tab == "daily" then + local preview = snapshot.dailyPreview + if type(preview) ~= "string" or preview == "" then + return emptyList( + snapshot.dailyExists and tr("panel.empty") or tr("panel.daily_missing") + ) + end + return ui.column({ + key = "daily-preview", + gap = 8, + padding = 8, + align = "stretch", + flexGrow = 1, + }, { + ui.label({ + text = preview, + color = "on_surface", + fontSize = 12, + maxLines = 24, + }), + }) + end + + local rows + if tab == "recent" then + rows = recentRows() + else + rows = gitFileRows() + end + if #rows == 0 then + return emptyList(tr("panel.empty")) + end + return ui.column({ + key = "items-" .. tab, + align = "stretch", + justify = "start", + gap = 8, + flexGrow = 1, + }, rows) +end + +local function toolbar() + local busy = snapshot.busy == true + + if tab == "daily" then + return ui.column({ gap = 8, padding = 10, fill = "surface_variant/0.45", radius = 10, align = "stretch" }, { + ui.label({ + text = tr("panel.daily_path", { + path = snapshot.dailyRel ~= "" and snapshot.dailyRel or "—", + }), + color = "on_surface_variant", + fontSize = 12, + maxLines = 2, + }), + ui.input({ + key = `capture-{captureKey}`, + value = captureText, + placeholder = tr("panel.capture_placeholder"), + flexGrow = 1, + controlSize = "md", + onChange = "onCaptureChange", + onSubmit = "onCapture", + submitOnEnter = true, + }), + ui.row({ gap = 6 }, { + ui.button({ + text = tr("actions.capture"), + glyph = "plus", + variant = "primary", + enabled = not busy and snapshot.available == true, + onClick = "onCapture", + }), + ui.button({ + text = tr("actions.open_daily"), + glyph = "external-link", + variant = "outline", + enabled = snapshot.available == true, + onClick = "onOpenDaily", + }), + }), + }) + end + + if tab == "recent" then + local n = selectedRecent() + if not n then + return ui.label({ text = tr("panel.select_hint"), color = "on_surface_variant" }) + end + return ui.column({ gap = 6, padding = 10, fill = "surface_variant/0.45", radius = 10, align = "stretch" }, { + ui.label({ text = n.rel, fontWeight = "bold", maxLines = 2 }), + ui.row({ gap = 6 }, { + ui.button({ text = tr("actions.open_note"), glyph = "external-link", variant = "primary", onClick = "onOpenNote" }), + ui.button({ text = tr("actions.copy_link"), glyph = "link", variant = "outline", onClick = "onCopyLink" }), + ui.button({ text = tr("actions.copy_path"), glyph = "copy", variant = "ghost", onClick = "onCopyPath" }), + }), + }) + end + + -- git tab + local g = snapshot.git or {} + local inProg = type(g.inProgress) == "string" and g.inProgress ~= "" + local gitOk = g.isRepo == true and not inProg + local rows = { + ui.label({ + text = inProg + and tr("panel.git_in_progress", { kind = g.inProgress }) + or gitSummary(), + color = inProg and "error" + or ((g.isRepo and not g.clean) and "#73c936" or "on_surface_variant"), + fontSize = 12, + maxLines = 2, + }), + } + if inProg then + table.insert(rows, ui.row({ gap = 6 }, { + ui.button({ + text = tr("actions.git_abort"), + glyph = "x", + variant = "primary", + enabled = not busy, + onClick = "onGitAbort", + }), + })) + else + table.insert(rows, ui.input({ + key = `commit-{commitKey}`, + value = commitText, + placeholder = tr("panel.commit_placeholder"), + flexGrow = 1, + controlSize = "md", + onChange = "onCommitChange", + onSubmit = "onGitCommit", + submitOnEnter = true, + })) + table.insert(rows, ui.row({ gap = 6 }, { + ui.button({ + text = tr("actions.git_commit"), + glyph = "git-commit", + variant = "primary", + enabled = not busy and gitOk, + onClick = "onGitCommit", + }), + ui.button({ + text = tr("actions.git_pull"), + glyph = "cloud-download", + variant = "outline", + enabled = not busy and gitOk, + onClick = "onGitPull", + }), + ui.button({ + text = tr("actions.git_push"), + glyph = "cloud-upload", + variant = "outline", + enabled = not busy and gitOk, + onClick = "onGitPush", + }), + ui.button({ + text = tr("actions.git_sync"), + glyph = "refresh", + variant = "outline", + enabled = not busy and gitOk, + onClick = "onGitSync", + }), + })) + end + return ui.column({ + gap = 8, + padding = 10, + fill = "surface_variant/0.45", + radius = 10, + align = "stretch", + }, rows) +end + +local function tabButton(label, id, cb) + return ui.button({ + text = label, + selected = tab == id, + variant = tab == id and "primary" or "ghost", + onClick = cb, + }) +end + +render = function() + dirty = false + local notes = {} + if snapshot.loading then + table.insert(notes, ui.label({ text = tr("panel.loading"), color = "on_surface_variant" })) + end + if snapshot.busy then + table.insert(notes, ui.label({ text = tr("panel.busy"), color = "primary" })) + end + if type(snapshot.error) == "string" and snapshot.error ~= "" then + table.insert(notes, ui.label({ text = snapshot.error, color = "error", maxLines = 3 })) + end + if feedback ~= "" then + table.insert(notes, ui.label({ + text = feedback, + color = feedbackError and "error" or "tertiary", + maxLines = 2, + })) + end + + local g = snapshot.git or {} + local gitLabel = "—" + if g.isRepo then + gitLabel = g.clean and "clean" or (tostring(g.dirty or 0) .. " dirty") + else + gitLabel = "no git" + end + + local summary = tr("panel.summary", { + daily = snapshot.dailyExists and "✓" or "·", + recent = #(snapshot.recent or {}), + git = gitLabel, + }) + + local titleIcon = snapshot.available and "assets/obsidian-purple.png" or "assets/obsidian-grey.png" + if snapshot.available and g.isRepo and not g.clean then + titleIcon = "assets/obsidian-green.png" + end + + panel.render(ui.column({ flexGrow = 1, gap = 10 }, { + ui.row({ align = "center", gap = 10 }, { + ui.image({ path = titleIcon, width = 28, height = 28, fit = "contain" }), + ui.column({ flexGrow = 1, gap = 0 }, { + ui.label({ text = tr("title"), fontSize = 18, fontWeight = "bold" }), + ui.label({ + text = tr("panel.vault", { + name = snapshot.vaultName ~= "" and snapshot.vaultName or "—", + }), + fontSize = 11, + color = "on_surface_variant", + }), + }), + ui.button({ text = tr("actions.open_vault"), glyph = "external-link", variant = "outline", onClick = "onOpenVault" }), + ui.button({ glyph = "refresh", variant = "ghost", onClick = "onRefresh" }), + ui.button({ glyph = "close", onClick = "onClose" }), + }), + + ui.row({ gap = 4, align = "center" }, { + tabButton(tr("tabs.daily"), "daily", "onTabDaily"), + tabButton(tr("tabs.recent"), "recent", "onTabRecent"), + tabButton(tr("tabs.git"), "git", "onTabGit"), + }), + + ui.label({ + text = summary, + color = "on_surface_variant", + fontSize = 11, + maxLines = 1, + }), + + ui.row({ gap = 8, align = "center", visible = tab == "recent" or tab == "git" }, { + ui.input({ + key = `filter-{tab}-{filterKey}`, + value = filterText, + placeholder = tr("filter.placeholder"), + flexGrow = 1, + controlSize = "sm", + onChange = "onFilterChange", + }), + ui.button({ + glyph = "x", + variant = "ghost", + visible = filterText ~= "", + onClick = "onClearFilter", + }), + }), + + toolbar(), + ui.column({ gap = 3, align = "stretch" }, notes), + ui.scroll({ + key = "scroll-" .. tab, + flexGrow = 1, + gap = 8, + align = "stretch", + }, { itemList() }), + ui.label({ + text = (snapshot.updatedAt or 0) > 0 + and tr("panel.updated", { time = noctalia.formatTime("%H:%M:%S", snapshot.updatedAt) }) + or "", + color = "on_surface_variant", + fontSize = 11, + }), + })) +end + +noctalia.state.watch(STATE_KEY, function(value) + if type(value) ~= "table" then return end + snapshot = value + if selectedId ~= "" and tab == "recent" and not selectedRecent() then + selectedId = "" + end + dirty = true +end) + +noctalia.state.watch(RESULT_KEY, function(result) + if type(result) ~= "table" then return end + if type(result.requestId) ~= "string" or not result.requestId:match("^panel%-") then return end + feedback = tostring(result.message or "") + feedbackError = result.ok ~= true + if result.ok == true and result.action == "capture" then + captureText = "" + captureKey += 1 + end + if result.ok == true and result.action == "git_commit" then + commitText = "" + commitKey += 1 + end + dirty = true +end) + +panel.setWantsSecondTicks(true) + +function onOpen(_context) + feedback = "" + send("refresh") + render() +end + +function update() + if dirty then render() end +end + +function onClose() panel.close() end +function onRefresh() send("refresh") end +function onOpenVault() send("open_vault") end +function onOpenDaily() send("open_daily") end + +function onTabDaily() + tab = "daily" + selectedId = "" + filterKey += 1 + render() +end +function onTabRecent() + tab = "recent" + selectedId = "" + filterKey += 1 + render() +end +function onTabGit() + tab = "git" + selectedId = "" + filterKey += 1 + render() +end + +function onFilterChange(value) + filterText = if type(value) == "string" then value else "" + render() +end +function onClearFilter() + filterText = "" + filterKey += 1 + render() +end + +function onCaptureChange(value) + captureText = if type(value) == "string" then value else "" +end +function onCapture() + local text = noctalia.string.trim(captureText) + if text == "" then return end + send("capture", { text = text }) +end + +function onOpenNote() + local n = selectedRecent() + if n then send("open_note", { rel = n.rel }) end +end +function onCopyLink() + local n = selectedRecent() + if n then send("copy", { text = "[[" .. n.name .. "]]" }) end +end +function onCopyPath() + local n = selectedRecent() + if n then send("copy", { text = n.rel }) end +end + +function onCommitChange(value) + commitText = if type(value) == "string" then value else "" +end +function onGitCommit() + send("git_commit", { message = commitText }) +end +function onGitPull() send("git_pull") end +function onGitPush() send("git_push") end +function onGitSync() send("git_sync") end +function onGitAbort() send("git_abort") end diff --git a/obsidian/plugin.toml b/obsidian/plugin.toml new file mode 100644 index 0000000..7eccc35 --- /dev/null +++ b/obsidian/plugin.toml @@ -0,0 +1,97 @@ +# Obsidian vault: daily capture + git sync. + +id = "davemhammer/obsidian" +name = "Obsidian" +version = "1.0.6" +plugin_api = 10 +author = "davemhammer" +license = "MIT" +dependencies = ["obsidian", "git", "xdg-open", "find", "sort", "head", "realpath"] +tags = ["productivity", "utility", "bar", "panel", "service", "launcher"] +icon = "notebook" +description = "Daily capture and git status, commit, pull, and push for an Obsidian vault." + +[[setting]] +key = "vault_path" +type = "folder" +label_key = "settings.vault_path.label" +description_key = "settings.vault_path.description" +default = "~/Documents/Obsidian Vault" + +[[setting]] +key = "vault_name" +type = "string" +label_key = "settings.vault_name.label" +description_key = "settings.vault_name.description" +default = "" + +[[setting]] +key = "daily_folder" +type = "string" +label_key = "settings.daily_folder.label" +description_key = "settings.daily_folder.description" +default = "" + +[[setting]] +key = "daily_format" +type = "string" +label_key = "settings.daily_format.label" +description_key = "settings.daily_format.description" +default = "%Y-%m-%d" + +[[setting]] +key = "git_commit_message" +type = "string" +label_key = "settings.git_commit_message.label" +description_key = "settings.git_commit_message.description" +default = "vault: capture from Noctalia" + +[[setting]] +key = "refresh_interval" +type = "int" +label_key = "settings.refresh_interval.label" +description_key = "settings.refresh_interval.description" +default = 20 +min = 5 +max = 300 + +[[setting]] +key = "notify_on_action" +type = "bool" +label_key = "settings.notify_on_action.label" +description_key = "settings.notify_on_action.description" +default = true + +[[widget]] +id = "status" +entry = "widget.luau" + + [[widget.setting]] + key = "show_dirty" + type = "bool" + label_key = "settings.show_dirty.label" + description_key = "settings.show_dirty.description" + default = true + +[[panel]] +id = "manager" +entry = "panel.luau" +width = 700 +height = 620 +placement = "floating" +position = "center" +open_near_click = true +keyboard_focus = "exclusive" +dismiss_on_outside_click = true + +[[service]] +id = "service" +entry = "service.luau" + +[[launcher_provider]] +id = "ob" +entry = "launcher.luau" +prefix = "ob" +glyph = "notebook" +include_in_global_search = false +debounce_ms = 80 diff --git a/obsidian/service.luau b/obsidian/service.luau new file mode 100644 index 0000000..13a031f --- /dev/null +++ b/obsidian/service.luau @@ -0,0 +1,1010 @@ +--!nonstrict +-- Obsidian vault: daily capture + git status/commit/pull/push. + +local STATE_KEY = "obs_snapshot" +local COMMAND_KEY = "obs_command" +local RESULT_KEY = "obs_action_result" + +local MAX_RECENT = 40 +local MAX_GIT_FILES = 40 + +local snapshot = { + available = false, + loading = true, + busy = false, + vaultPath = "", + vaultName = "", + dailyPath = "", + dailyRel = "", + dailyExists = false, + dailyPreview = "", + recent = {}, + git = { + isRepo = false, + branch = "", + dirty = 0, + ahead = 0, + behind = 0, + clean = true, + files = {}, + statusLine = "", + inProgress = "", -- "rebase" | "merge" | "" + }, + error = "", + updatedAt = 0, + revision = 0, +} + +local refreshGeneration = 0 +local refreshPending = false +local refreshAgain = false +local actionBusy = false +local dataSignature = "" + +local function trim(v) + return noctalia.string.trim(tostring(v or "")) +end + +local function shellQuote(v) + return "'" .. tostring(v):gsub("'", "'\\''") .. "'" +end + +local function shellCommand(args) + local q = {} + for _, a in ipairs(args) do + table.insert(q, shellQuote(a)) + end + return table.concat(q, " ") +end + +local function expand(path) + return noctalia.expandPath(trim(path)) +end + +local function nowSec() + if type(noctalia.nowMs) == "function" then + local ms = noctalia.nowMs() + if type(ms) == "number" and ms > 0 then + return math.floor(ms / 1000) + end + end + return os.time() +end + +local function refreshIntervalMs() + local s = tonumber(noctalia.getConfig("refresh_interval")) or 20 + s = math.max(5, math.min(300, math.floor(s))) + return s * 1000 +end + +local function shouldNotify() + return noctalia.getConfig("notify_on_action") ~= false +end + +local function vaultRoot() + return expand(noctalia.getConfig("vault_path") or "") +end + +local function vaultName() + local n = trim(noctalia.getConfig("vault_name")) + if n ~= "" then + return n + end + local root = vaultRoot() + return root:match("([^/]+)$") or "vault" +end + +local function dailyFolder() + return trim(noctalia.getConfig("daily_folder")) +end + +local function dailyFormat() + local f = trim(noctalia.getConfig("daily_format")) + if f == "" then + return "%Y-%m-%d" + end + return f +end + +local function isVault(path) + if path == "" or not noctalia.fileExists(path) then + return false + end + return noctalia.fileExists(path .. "/.obsidian") +end + +local function joinPath(a, b) + a = tostring(a or ""):gsub("/+$", "") + b = tostring(b or ""):gsub("^/+", "") + if a == "" then return b end + if b == "" then return a end + return a .. "/" .. b +end + +-- Strict relative path: no absolute paths, no ".." / ".", no backslashes/NUL. +-- Returns cleaned relative path, or nil if rejected. +local function normalizeRelPath(rel) + rel = trim(rel):gsub("\\", "/") + if rel == "" then + return "" + end + if rel:find("\0", 1, true) then + return nil + end + -- Absolute (Unix or Windows drive) + if rel:sub(1, 1) == "/" or rel:match("^%a:[/\\]") then + return nil + end + -- Collapse slashes and reject . / .. segments + local parts = {} + for seg in (rel .. "/"):gmatch("([^/]*)/") do + if seg == "" then + -- skip leading/duplicate slashes + elseif seg == "." or seg == ".." then + return nil + else + table.insert(parts, seg) + end + end + return table.concat(parts, "/") +end +-- Join vault root + relative path; require result stays under root (string check). +-- Returns absPath, cleanRel or nil, errKey +local function pathUnderVault(root, rel) + root = expand(root):gsub("/+$", "") + if root == "" then + return nil, nil, "empty vault" + end + local clean = normalizeRelPath(rel) + if clean == nil then + return nil, nil, "invalid path" + end + local abs = clean == "" and root or (root .. "/" .. clean) + if abs == root or abs:sub(1, #root + 1) == root .. "/" then + return abs, clean, nil + end + return nil, nil, "path outside vault" +end + +-- Async: reject symlink path components and require realpath(target) under realpath(vault). +-- onDone(ok, errMsg). Uses realpath(1) + test -L (same pattern as other community plugins). +local function assertSafeVaultTarget(root, abs, rel, onDone) + root = expand(root):gsub("/+$", "") + abs = expand(abs) + rel = normalizeRelPath(rel or "") + if type(onDone) ~= "function" then + return + end + if root == "" or abs == "" or rel == nil then + onDone(false, "invalid path") + return + end + -- Shell: canonicalize vault; walk each segment of rel and refuse -L; realpath -m must stay under vault. + local cmd = "V=$(realpath -e " + .. shellQuote(root) + .. " 2>/dev/null) || exit 1; " + .. "A=" + .. shellQuote(abs) + .. "; " + .. "R=" + .. shellQuote(rel) + .. "; " + .. "cur=" + .. shellQuote(root) + .. "; " + .. 'IFS=/; set -f; for s in $R; do ' + .. '[ -n "$s" ] || continue; ' + .. 'next="$cur/$s"; ' + .. 'if [ -L "$next" ]; then exit 2; fi; ' + .. 'cur="$next"; ' + .. "done; set +f; " + .. 'T=$(realpath -m "$A" 2>/dev/null) || exit 1; ' + .. 'case "$T" in "$V"|"$V"/*) exit 0 ;; *) exit 3 ;; esac' + + local started = noctalia.runAsync(cmd, function(result) + if result and result.exitCode == 0 and not result.timedOut then + onDone(true, nil) + elseif result and result.exitCode == 2 then + onDone(false, "symlink in path") + elseif result and result.exitCode == 3 then + onDone(false, "path outside vault") + else + onDone(false, "path check failed") + end + end, 4000) + if not started then + onDone(false, "path check failed") + end +end + +-- Build today's daily note relative + absolute path with strict validation. +-- daily_format must expand to a single path segment (no / or ..). +-- Returns rel, abs, errMessageOrNil +local function dailyRelAndAbs() + local fmt = dailyFormat() + if fmt:find("[/\\]") or fmt:find("%.%.") then + return nil, nil, "invalid daily_format" + end + local stem = noctalia.formatTime(fmt, nowSec()) + if type(stem) ~= "string" or trim(stem) == "" then + return nil, nil, "invalid daily name" + end + stem = trim(stem) + if stem:find("[/\\]") or stem:find("\0", 1, true) or stem == "." or stem == ".." then + return nil, nil, "invalid daily name" + end + -- Single path segment only (format must not introduce directories) + if stem:find("/") then + return nil, nil, "invalid daily name" + end + local name = stem + if not name:match("%.[Mm][Dd]$") then + name = name .. ".md" + end + + local folder = dailyFolder() + local rel + if folder == "" then + rel = name + else + local folderClean = normalizeRelPath(folder) + if folderClean == nil then + return nil, nil, "invalid daily_folder" + end + if folderClean == "" then + rel = name + else + rel = folderClean .. "/" .. name + end + end + + local abs, clean, err = pathUnderVault(vaultRoot(), rel) + if not abs then + return nil, nil, err or "invalid path" + end + return clean, abs, nil +end + +-- Back-compat helpers used only when path is known-good; prefer dailyRelAndAbs. +local function dailyRelPath() + local rel = dailyRelAndAbs() + return rel or "" +end + +local function dailyAbsPath() + local _, abs = dailyRelAndAbs() + return abs or "" +end + +local function ensureParentDir(path) + local parent = path:match("(.+)/[^/]+$") + if parent and parent ~= "" and not noctalia.fileExists(parent) then + -- Only create parents that still lie under the vault root (string + prior symlink check). + local root = vaultRoot():gsub("/+$", "") + if root ~= "" and (parent == root or parent:sub(1, #root + 1) == root .. "/") then + noctalia.mkdirAll(parent) + end + end +end + +local function updateRevision(sig) + if sig ~= dataSignature then + dataSignature = sig + snapshot.revision += 1 + end +end + +local function publishSnapshot() + snapshot.busy = actionBusy + noctalia.state.set(STATE_KEY, snapshot) +end + +local function actionResult(command, ok, message, extra) + local r = { + requestId = command and command.requestId or "", + action = command and command.action or "", + ok = ok, + message = message or "", + } + if type(extra) == "table" then + for k, v in pairs(extra) do + r[k] = v + end + end + noctalia.state.set(RESULT_KEY, r) +end + +local function notifyOk(msg) + if shouldNotify() then + noctalia.notify(noctalia.tr("title"), msg) + end +end + +local function notifyErr(msg) + noctalia.notifyError(noctalia.tr("title"), msg) +end + +local function runGit(args, callback, timeoutMs) + local root = vaultRoot() + local cmd = "cd " .. shellQuote(root) .. " && " .. shellCommand(args) + return noctalia.runAsync(cmd, callback, timeoutMs or 60000) +end + +-- Detect stuck rebase/merge so we never leave the vault detached. +local function gitInProgressKind() + local root = vaultRoot() + if root == "" then + return "" + end + if noctalia.fileExists(root .. "/.git/rebase-merge") + or noctalia.fileExists(root .. "/.git/rebase-apply") + then + return "rebase" + end + if noctalia.fileExists(root .. "/.git/MERGE_HEAD") then + return "merge" + end + -- worktree / .git file case: still check common paths via git + return "" +end + +local function refuseIfGitBusy(command) + local kind = gitInProgressKind() + if kind == "" then + -- also check snapshot if refresh already saw it + kind = (snapshot.git and snapshot.git.inProgress) or "" + end + if kind ~= "" then + actionResult( + command, + false, + noctalia.tr("result.git_in_progress", { kind = kind }) + ) + notifyErr(noctalia.tr("result.git_in_progress", { kind = kind })) + return true + end + return false +end + +local function urlEncode(s) + return noctalia.string.urlEncode(tostring(s or "")) +end + +local function openUri(uri) + noctalia.runAsync("xdg-open " .. shellQuote(uri)) +end + +local function openNoteRel(rel) + local vault = vaultName() + -- file param is vault-relative path without leading slash (already confined) + rel = tostring(rel or ""):gsub("^/+", "") + local uri = "obsidian://open?vault=" .. urlEncode(vault) .. "&file=" .. urlEncode(rel) + openUri(uri) +end + +-- Ensure daily file exists under a validated vault path (string + realpath/symlink checks). +-- onReady(rel, abs) when safe and ready; actionResult already set on failure. +local function ensureDailyFile(command, onReady) + local root = vaultRoot() + if not isVault(root) then + actionResult(command, false, noctalia.tr("result.missing_vault")) + return + end + local rel, abs, err = dailyRelAndAbs() + if not abs then + actionResult(command, false, noctalia.tr("result.invalid_daily_path", { error = err or "invalid" })) + return + end + assertSafeVaultTarget(root, abs, rel, function(ok, safeErr) + if not ok then + actionResult(command, false, noctalia.tr("result.invalid_daily_path", { error = safeErr or "unsafe" })) + return + end + if not noctalia.fileExists(abs) then + ensureParentDir(abs) + local title = noctalia.formatTime(dailyFormat(), nowSec()) + title = tostring(title or ""):gsub("[/\\]", "-") + local wOk, werr = noctalia.writeFile(abs, "# " .. title .. "\n") + if not wOk then + actionResult(command, false, noctalia.tr("result.failed", { error = werr or "write failed" })) + return + end + end + if type(onReady) == "function" then + onReady(rel, abs) + end + end) +end +local function openVault() + local vault = vaultName() + openUri("obsidian://open?vault=" .. urlEncode(vault)) +end + +local function readPreview(path, maxBytes) + local raw = noctalia.readFile(path) + if not raw then + return "" + end + raw = tostring(raw) + if #raw > (maxBytes or 400) then + raw = raw:sub(1, maxBytes or 400) .. "…" + end + return raw +end + +local function parseGitStatus(stdout) + local files = {} + local dirty = 0 + for line in (tostring(stdout or "") .. "\n"):gmatch("(.-)\n") do + if line ~= "" then + dirty += 1 + if #files < MAX_GIT_FILES then + local xy = line:sub(1, 2) + local path = trim(line:sub(4)) + -- rename: "R old -> new" + path = path:gsub("^.*%->%s*", "") + table.insert(files, { + id = path, + path = path, + code = xy, + }) + end + end + end + return dirty, files +end + +local function parseBranchLine(stdout) + -- ## main...origin/main [ahead 1, behind 2] + local line = tostring(stdout or ""):match("([^\n]+)") or "" + local branch = line:match("^##%s+([^%.%s]+)") or line:match("^##%s+(%S+)") or "" + branch = branch:gsub("%.%.%..*$", "") + local ahead = tonumber(line:match("ahead%s+(%d+)")) or 0 + local behind = tonumber(line:match("behind%s+(%d+)")) or 0 + return branch, ahead, behind, line +end + +local function parseRecent(stdout) + local recent = {} + for line in (tostring(stdout or "") .. "\n"):gmatch("(.-)\n") do + local ts, rel = line:match("^([%d%.]+)\t(.+)$") + if rel and rel ~= "" then + -- Drop any path find might surface that fails strict relative rules + -- (absolute, .., NUL, etc.). open_note also re-checks + assertSafeVaultTarget. + local clean = normalizeRelPath(rel) + if clean ~= nil and clean ~= "" then + local name = clean:match("([^/]+)$") or clean + table.insert(recent, { + id = clean, + rel = clean, + name = name:gsub("%.md$", ""), + mtime = math.floor(tonumber(ts) or 0), + }) + end + end + end + return recent +end + +local refreshAll + +local function finishAction(command, ok, message) + actionBusy = false + actionResult(command, ok, message) + if ok then + notifyOk(message) + else + notifyErr(message) + end + publishSnapshot() + refreshAll() +end + +refreshAll = function() + if refreshPending then + refreshAgain = true + return + end + refreshPending = true + refreshAgain = false + refreshGeneration += 1 + local generation = refreshGeneration + + local root = vaultRoot() + snapshot.vaultPath = root + snapshot.vaultName = vaultName() + + local dailyRel, dailyAbs, dailyErr = dailyRelAndAbs() + snapshot.dailyRel = dailyRel or "" + snapshot.dailyPath = dailyAbs or "" + + if not isVault(root) then + snapshot.available = false + snapshot.loading = false + snapshot.error = noctalia.tr("result.missing_vault") + snapshot.recent = {} + snapshot.dailyExists = false + snapshot.dailyPreview = "" + snapshot.git = { + isRepo = false, + branch = "", + dirty = 0, + ahead = 0, + behind = 0, + clean = true, + files = {}, + statusLine = "", + inProgress = "", + } + refreshPending = false + updateRevision("no-vault") + publishSnapshot() + return + end + + snapshot.available = true + if dailyErr then + snapshot.error = noctalia.tr("result.invalid_daily_path", { error = dailyErr }) + else + snapshot.error = "" + end + if (snapshot.updatedAt or 0) == 0 then + snapshot.loading = true + publishSnapshot() + end + + local pending = 3 + local bag = { recent = {}, gitPorcelain = "", gitBranch = "" } + local finished = false + + local function finish() + if generation ~= refreshGeneration then + return + end + pending -= 1 + if pending > 0 or finished then + return + end + finished = true + + local function applySnapshot(dailyExists, dailyPreview) + local okP, errP = pcall(function() + snapshot.dailyExists = dailyExists == true + snapshot.dailyPreview = dailyPreview or "" + snapshot.recent = parseRecent(bag.recent) + local dirty, files = parseGitStatus(bag.gitPorcelain) + local branch, ahead, behind, statusLine = parseBranchLine(bag.gitBranch) + local isRepo = not bag.gitFailed and (branch ~= "" or statusLine:match("^##") ~= nil) + + snapshot.git = { + isRepo = isRepo, + branch = branch, + dirty = dirty, + ahead = ahead, + behind = behind, + clean = dirty == 0, + files = files, + statusLine = statusLine, + inProgress = gitInProgressKind(), + } + snapshot.loading = false + snapshot.updatedAt = nowSec() + refreshPending = false + updateRevision(table.concat({ + snapshot.dailyRel, + tostring(snapshot.dailyExists), + tostring(#snapshot.recent), + branch, + tostring(dirty), + tostring(ahead), + tostring(behind), + }, "|")) + publishSnapshot() + end) + + if not okP then + noctalia.log(`obsidian: apply failed: {tostring(errP)}`) + snapshot.loading = false + snapshot.error = tostring(errP) + refreshPending = false + publishSnapshot() + end + + if refreshAgain then + refreshAgain = false + refreshAll() + end + end + + -- Only read daily preview after symlink/realpath confinement passes + if snapshot.dailyPath ~= "" and snapshot.dailyRel ~= "" and noctalia.fileExists(snapshot.dailyPath) then + assertSafeVaultTarget(root, snapshot.dailyPath, snapshot.dailyRel, function(ok, _err) + if generation ~= refreshGeneration then + return + end + if ok then + applySnapshot(true, readPreview(snapshot.dailyPath, 500)) + else + -- Exists but unsafe (symlink escape) — do not read through it + applySnapshot(false, "") + end + end) + else + applySnapshot(false, "") + end + end + + -- recent files: -P never follows symlinks (GNU default, explicit for review/portability). + -- -type f excludes symlink notes; symlink dirs are not descended into. + local recentCmd = "find -P " + .. shellQuote(root) + .. " -type f -name '*.md'" + .. " ! -path '*/.obsidian/*' ! -path '*/.git/*' ! -path '*/.claudian/*'" + .. " -printf '%T@\\t%P\\n' 2>/dev/null | sort -nr | head -n " + .. tostring(MAX_RECENT) + noctalia.runAsync(recentCmd, function(result) + if generation ~= refreshGeneration then return end + bag.recent = (result and result.stdout) or "" + finish() + end, 20000) + + -- git porcelain + runGit({ "git", "status", "--porcelain" }, function(result) + if generation ~= refreshGeneration then return end + if result and result.exitCode == 0 then + bag.gitPorcelain = result.stdout or "" + else + bag.gitPorcelain = "" + bag.gitFailed = true + end + finish() + end, 15000) + + -- git branch / ahead behind + runGit({ "git", "status", "-sb" }, function(result) + if generation ~= refreshGeneration then return end + if result and result.exitCode == 0 then + bag.gitBranch = result.stdout or "" + else + bag.gitBranch = "" + bag.gitFailed = true + end + finish() + end, 15000) +end + +local function appendDaily(command) + local text = trim(command.text or command.line or "") + if text == "" then + actionResult(command, false, noctalia.tr("result.failed", { error = "empty note" })) + return + end + local root = vaultRoot() + if not isVault(root) then + actionResult(command, false, noctalia.tr("result.missing_vault")) + return + end + + local rel, path, pathErr = dailyRelAndAbs() + if not path then + actionResult(command, false, noctalia.tr("result.invalid_daily_path", { error = pathErr or "invalid" })) + return + end + + assertSafeVaultTarget(root, path, rel, function(ok, safeErr) + if not ok then + actionResult(command, false, noctalia.tr("result.invalid_daily_path", { error = safeErr or "unsafe" })) + return + end + + ensureParentDir(path) + + local ts = noctalia.formatTime("%H:%M", nowSec()) + local line = "- " .. ts .. " " .. text + line = line:gsub("[\r\n]+", " ") + + local existing = "" + if noctalia.fileExists(path) then + existing = noctalia.readFile(path) or "" + else + local title = tostring(noctalia.formatTime(dailyFormat(), nowSec()) or ""):gsub("[/\\]", "-") + existing = "# " .. title .. "\n" + end + + if existing ~= "" and not existing:match("\n$") then + existing = existing .. "\n" + end + local newBody = existing .. line .. "\n" + local wOk, err = noctalia.writeFile(path, newBody) + if not wOk then + finishAction(command, false, noctalia.tr("result.failed", { error = err or "write failed" })) + return + end + + snapshot.dailyExists = true + snapshot.dailyPath = path + snapshot.dailyRel = rel or "" + finishAction(command, true, noctalia.tr("result.captured", { name = snapshot.dailyRel })) + end) +end +local function gitCommit(command) + if actionBusy then + actionResult(command, false, noctalia.tr("result.busy")) + return + end + if refuseIfGitBusy(command) then + return + end + local msg = trim(command.message or command.text) + if msg == "" then + msg = trim(noctalia.getConfig("git_commit_message")) + end + if msg == "" then + msg = "vault: capture from Noctalia" + end + + actionBusy = true + publishSnapshot() + + -- add all + commit only if there is something to commit + runGit({ "git", "status", "--porcelain" }, function(st) + if not st or st.exitCode ~= 0 then + finishAction(command, false, noctalia.tr("result.failed", { error = "git status failed" })) + return + end + if trim(st.stdout) == "" then + actionBusy = false + actionResult(command, true, noctalia.tr("result.git_nothing")) + notifyOk(noctalia.tr("result.git_nothing")) + publishSnapshot() + refreshAll() + return + end + local n = 0 + for _ in (st.stdout .. "\n"):gmatch("(.-)\n") do + if _ ~= "" then n += 1 end + end + + runGit({ "git", "add", "-A" }, function(addRes) + if not addRes or addRes.exitCode ~= 0 then + local err = trim(addRes and (addRes.stderr ~= "" and addRes.stderr or addRes.stdout) or "git add failed") + finishAction(command, false, noctalia.tr("result.failed", { error = err })) + return + end + runGit({ "git", "commit", "-m", msg }, function(cRes) + if cRes and cRes.exitCode == 0 then + finishAction(command, true, noctalia.tr("result.git_committed", { n = n })) + else + local err = trim(cRes and (cRes.stderr ~= "" and cRes.stderr or cRes.stdout) or "commit failed") + finishAction(command, false, noctalia.tr("result.failed", { error = err })) + end + end, 60000) + end, 60000) + end, 15000) +end + +-- Merge pull (no rebase): never leaves the vault detached mid-rebase. +-- Autostash keeps uncommitted capture edits safe across the pull. +local PULL_ARGS = { "git", "pull", "--no-rebase", "--autostash" } + +local function gitPull(command) + if actionBusy then + actionResult(command, false, noctalia.tr("result.busy")) + return + end + if refuseIfGitBusy(command) then + return + end + actionBusy = true + publishSnapshot() + runGit(PULL_ARGS, function(result) + if result and result.exitCode == 0 then + finishAction(command, true, noctalia.tr("result.git_pulled")) + else + local err = trim(result and (result.stderr ~= "" and result.stderr or result.stdout) or "pull failed") + finishAction(command, false, noctalia.tr("result.failed", { error = err })) + end + end, 120000) +end + +local function gitPush(command) + if actionBusy then + actionResult(command, false, noctalia.tr("result.busy")) + return + end + if refuseIfGitBusy(command) then + return + end + actionBusy = true + publishSnapshot() + runGit({ "git", "push" }, function(result) + if result and result.exitCode == 0 then + finishAction(command, true, noctalia.tr("result.git_pushed")) + else + local err = trim(result and (result.stderr ~= "" and result.stderr or result.stdout) or "push failed") + finishAction(command, false, noctalia.tr("result.failed", { error = err })) + end + end, 120000) +end + +local function gitSync(command) + if actionBusy then + actionResult(command, false, noctalia.tr("result.busy")) + return + end + if refuseIfGitBusy(command) then + return + end + actionBusy = true + publishSnapshot() + runGit(PULL_ARGS, function(pullRes) + if not pullRes or pullRes.exitCode ~= 0 then + local err = trim(pullRes and (pullRes.stderr ~= "" and pullRes.stderr or pullRes.stdout) or "pull failed") + finishAction(command, false, noctalia.tr("result.failed", { error = err })) + return + end + runGit({ "git", "push" }, function(pushRes) + if pushRes and pushRes.exitCode == 0 then + finishAction(command, true, noctalia.tr("result.git_synced")) + else + local err = trim(pushRes and (pushRes.stderr ~= "" and pushRes.stderr or pushRes.stdout) or "push failed") + finishAction(command, false, noctalia.tr("result.failed", { error = err })) + end + end, 120000) + end, 120000) +end + +local function gitAbort(command) + if actionBusy then + actionResult(command, false, noctalia.tr("result.busy")) + return + end + local kind = gitInProgressKind() + if kind == "" then + actionResult(command, false, noctalia.tr("result.git_not_in_progress")) + return + end + actionBusy = true + publishSnapshot() + local args + if kind == "rebase" then + args = { "git", "rebase", "--abort" } + else + args = { "git", "merge", "--abort" } + end + runGit(args, function(result) + if result and result.exitCode == 0 then + finishAction(command, true, noctalia.tr("result.git_aborted", { kind = kind })) + else + local err = trim(result and (result.stderr ~= "" and result.stderr or result.stdout) or "abort failed") + finishAction(command, false, noctalia.tr("result.failed", { error = err })) + end + end, 30000) +end + +local function executeAction(command) + if type(command) ~= "table" or type(command.action) ~= "string" then + return + end + local action = command.action + + if action == "refresh" then + refreshAll() + return + end + if action == "open_vault" then + openVault() + actionResult(command, true, noctalia.tr("result.opened")) + return + end + if action == "open_daily" then + ensureDailyFile(command, function(rel, _abs) + openNoteRel(rel) + actionResult(command, true, noctalia.tr("result.opened")) + refreshAll() + end) + return + end + if action == "open_note" then + local rel = trim(command.rel or command.path or command.id) + if rel == "" then + actionResult(command, false, noctalia.tr("result.failed", { error = "missing note" })) + return + end + local root = vaultRoot() + if not isVault(root) then + actionResult(command, false, noctalia.tr("result.missing_vault")) + return + end + local abs, clean, err = pathUnderVault(root, rel) + if not abs then + actionResult(command, false, noctalia.tr("result.invalid_note_path", { error = err or "invalid" })) + return + end + -- URI open only (no filesystem write), still refuse symlink escape for consistency + assertSafeVaultTarget(root, abs, clean, function(ok, safeErr) + if not ok then + actionResult(command, false, noctalia.tr("result.invalid_note_path", { error = safeErr or "unsafe" })) + return + end + openNoteRel(clean) + actionResult(command, true, noctalia.tr("result.opened")) + end) + return + end + if action == "capture" or action == "append_daily" then + appendDaily(command) + return + end + if action == "copy" then + local text = trim(command.text or command.name) + if text ~= "" then + noctalia.copyToClipboard(text, "text/plain") + actionResult(command, true, noctalia.tr("result.copied", { name = text })) + notifyOk(noctalia.tr("result.copied", { name = text })) + end + return + end + if action == "git_commit" then + gitCommit(command) + return + end + if action == "git_pull" then + gitPull(command) + return + end + if action == "git_push" then + gitPush(command) + return + end + if action == "git_sync" then + gitSync(command) + return + end + if action == "git_abort" then + gitAbort(command) + return + end + if action == "git_status" then + refreshAll() + actionResult(command, true, noctalia.tr("result.success")) + return + end + + actionResult(command, false, "Unknown action: " .. action) +end + +noctalia.state.watch(COMMAND_KEY, executeAction) +noctalia.setUpdateInterval(refreshIntervalMs()) +refreshAll() + +function update() + refreshAll() +end + +function onConfigChanged() + noctalia.setUpdateInterval(refreshIntervalMs()) + refreshPending = false + refreshAll() +end + +function onIpc(event, payload) + if event == "refresh" then + refreshPending = false + refreshAll() + elseif event == "daily" then + executeAction({ action = "open_daily", requestId = "ipc-daily" }) + elseif event == "capture" and type(payload) == "table" then + executeAction({ + action = "capture", + text = payload.text or payload.line or "", + requestId = "ipc-capture", + }) + elseif event == "pull" then + executeAction({ action = "git_pull", requestId = "ipc-pull" }) + elseif event == "push" then + executeAction({ action = "git_push", requestId = "ipc-push" }) + elseif event == "sync" then + executeAction({ action = "git_sync", requestId = "ipc-sync" }) + end +end diff --git a/obsidian/thumbnail.webp b/obsidian/thumbnail.webp new file mode 100644 index 0000000000000000000000000000000000000000..c7309d28043b3a5dd003a47e2d7f7598129e22f6 GIT binary patch literal 52438 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 VO_?Hl4#l@+GuY4H)%KTx0RW{I 0 then + icon = "assets/obsidian-green.png" -- has local changes to sync + end + + local children = { + ui.image({ + path = icon, + width = 16, + height = 16, + fit = "contain", + }), + } + + if showDirty and available and dirty > 0 then + table.insert(children, ui.label({ + text = tostring(dirty), + fontWeight = "bold", + color = "on_surface", + })) + end + + if available then + table.insert(children, ui.box({ + width = 7, + height = 7, + radius = 4, + fill = dirty > 0 and "#73c936" or "#7C3AED", + })) + end + + local container = barWidget.isVertical() and ui.column or ui.row + barWidget.render(container({ gap = 5, align = "center" }, children)) + + if not available then + barWidget.setTooltip(noctalia.tr("widget.tooltip_missing")) + else + local git = "—" + if type(snapshot.git) == "table" then + if not snapshot.git.isRepo then + git = "no repo" + elseif snapshot.git.clean then + git = "clean" + else + git = tostring(dirty) .. " dirty" + end + end + barWidget.setTooltip(noctalia.tr("widget.tooltip_ok", { + vault = snapshot.vaultName ~= "" and snapshot.vaultName or "vault", + daily = snapshot.dailyExists and "yes" or "new", + git = git, + })) + end +end + +noctalia.state.watch(STATE_KEY, function(value) + if type(value) == "table" then + snapshot = value + render() + end +end) + +noctalia.setUpdateInterval(8000) +render() + +function update() + render() +end + +function onClick() + noctalia.togglePanel(PANEL_ID) +end + +function onRightClick() + -- Quick open today's daily note (service rejects missing/invalid vault) + if snapshot.available ~= true then + return + end + requestId += 1 + noctalia.state.set(COMMAND_KEY, { action = "open_daily", requestId = `widget-{requestId}` }) +end