Practical Lua example: My project management

I’ve been trying out Silverbullet 2, making sure that I can port my project management space to the new lua system. Perhaps this can be helpful/interesting to Zef in the Road To Silverbullet 2.0.

The expected result looks something like this (formatting slightly altered; some real projects included so you can get a sense of how I use this)

Tasks

These are all open tasks from Journal or ones tagged #next.

  • SilverBullet offline requirements@710 Double-check syncthing

Active

:yellow_square:P1 :japan:Japanese classes #japanese
:yellow_square:P1 :medical_symbol:Medical log #medical #reminder
:green_square:P2 :house::sun_with_face::high_voltage:Solar system homeassistant
:green_square:P2 Try Silverbullet 2

Snoozed

(:sleeping_face:2025-03-29) :yellow_square:P1 :house::high_voltage:Switch electricity
(:sleeping_face:2025-03-30) :red_square:P0 Dust top of office #house
(:sleeping_face:2025-04-10) :yellow_square:P1 :bicycle:Ride bike to office
(:sleeping_face:2025-05-01) :green_square:P2 :house:Water heat efficiency and info #house

More: Snoozed reminders

Ready

:green_square:P2 :high_voltage:Air quality sensors #electronics
:green_square:P2 🕮Books to read
:green_square:P2 :television:Shows to watch
:green_square:P2 SilverBullet sync issues #coding
:green_square:P2 🎜Songs to learn #music
:person_shrugging:P3 Advent of Code #coding

This system worked just fine in Silverbullet 1, though the code has a lot of repetition. I’ve created a Silverbullet 2.0 version to make sure it’ll suit my needs. The output looks the same; the code now looks like this:

# Tasks
These are all open tasks from Journal or ones tagged `#next`.
${tasks_widget(active_tasks())}
# Active
${projects_widget(active_projects())}
# Snoozed
${projects_widget(snoozed_projects())}
# Ready
${projects_widget(ready_projects(20))}
# Blocked
${projects_widget(blocked_projects())}

…with the space-lua section below.

function active_tasks()
  return query[[
    from index.tag "task"
    where done == false and (
      table.includes(tags, "next") or
      string.startsWith(ref, "Journal")
    )
  ]]
end

function tasks_widget(tasks)
  local task_template = template.new[==[- [ ] [[${ref}]] ${text}
]==]
  return widget.new{
    markdown=template.each(tasks,task_template)
  }
end

function active_projects()
  return query[[from index.tag "page" 
    where string.startsWith(name, "Projects/")
    and status == "Active"
    and not is_snoozed(snooze_until)
    order by priority
  ]]
end

function ready_projects(n)
  return query[[from index.tag "page" 
    where string.startsWith(name, "Projects/")
    and status == "Ready"
    order by priority
    limit n
  ]]
end

function blocked_projects()
  return query[[from index.tag "page" 
    where string.startsWith(name, "Projects/")
    and status == "Blocked"
    order by priority
  ]]
end

function snoozed_projects()
  return query[[from index.tag "page" 
    where string.startsWith(name, "Projects/")
    and status == "Active"
    and is_snoozed(snooze_until)
    and not is_reminder(_)
    order by snooze_until
  ]]
end

function is_snoozed(snooze_until)
  if snooze_until == nil then
    return false
  end
  if not string.match(snooze_until, "^....%-..%-..$") then
    -- TODO: These should pop a warning at the top,
    -- like the one I already have for projects with
    -- an invalid state.
    return false
  end
  return snooze_until > os.date("%Y-%m-%d")
end

function is_reminder(project)
  return table.includes(project.tags, "reminder")
end

function tagify(tags)
  return tags and
    '#' .. table.concat(tags, ' #') or
    ''
end

function priority_string(priority)
  local priority_icon = ({
    ["P0"]="🟥",
    ["P1"]="🟨",
    ["P2"]="🟩",
  })[priority] or "🤷"
  return priority_icon .. priority
end

function snooze_prefix(snooze_until)
  if not is_snoozed(snooze_until) then
    return ""
  end
  return "(😴" .. snooze_until .. ")"
end

function projects_widget(projects)
  local project_template = template.new[==[${snooze_prefix(snooze_until)}${priority_string(priority)}[[${name}]] ${tagify(tags)}
]==]
  return widget.new{
    markdown=template.each(projects,project_template)
  }
end

Overall notes:

  • It’s really nice to be able to put functions together in one page, I believe multiple separate pages were needed for this before.
  • New-style ${} templates don’t appear to support nested loops, which is why I needed the tagify function.
  • I didn’t think too hard about the function names here, but… namespacing is perhaps something to consider? Apparently all lua functions are meant to be public across the entire space, which is not necessarily desirable, but I guess it does keep things simple.
10 Likes

Good stuff!

This looks very similar to how I implemented the Eisenhower Matrix in SilverBullet. I tag all my tasks with either #important or #not-important and the same for #urgent/#not-urgent and then show four different Headings with lists of those tasks. :slight_smile: Helps keeping things prioritized.

This is a bit more extensive, because you also use (custom) page/object properties. Nice to see that with all the colours and emojis!

Happy to see this is already working in v2!

1 Like

Awesome, thanks for doing and sharing this! :clap:

2 Likes