* 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.
192 lines
5.3 KiB
Luau
192 lines
5.3 KiB
Luau
--!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
|