From 5fe4270f68e7607bb6978d6dd03f0e5600573c0d Mon Sep 17 00:00:00 2001 From: 0xrsydn Date: Fri, 21 Aug 2026 11:22:27 +0700 Subject: [PATCH] feat(nvim): add Notes authoring workflow --- README.MD | 1 + modules/home/programs/nvim/NOTES.md | 57 +++++ modules/home/programs/nvim/init.lua | 1 + .../home/programs/nvim/lua/config/notes.lua | 218 ++++++++++++++++++ .../programs/nvim/lua/plugins/which-key.lua | 2 + 5 files changed, 279 insertions(+) create mode 100644 modules/home/programs/nvim/NOTES.md create mode 100644 modules/home/programs/nvim/lua/config/notes.lua diff --git a/README.MD b/README.MD index a86569c..2f7a023 100644 --- a/README.MD +++ b/README.MD @@ -14,6 +14,7 @@ So far, the dotfiles only used for my macOS configuration, maybe in the future I ## Developer Tools - IDE: neovim (lazyvim bundle) & Helix +- Neovim workflow: [public Notes authoring integration](modules/home/programs/nvim/NOTES.md) - Containers: docker, lazydocker, orbstack - CLI utils: fzf, ripgrep, ast-grep, jq, yq, pandoc, ffmpeg, tree, htop - Tmux with gruvbox theme (plugins: cpu-usage, battery, network-bandwidth monitoring) diff --git a/modules/home/programs/nvim/NOTES.md b/modules/home/programs/nvim/NOTES.md new file mode 100644 index 0000000..f69964d --- /dev/null +++ b/modules/home/programs/nvim/NOTES.md @@ -0,0 +1,57 @@ +# Notes authoring in Neovim + +This module provides a Neovim interface for the personal website's public Notes +workflow. It is a native Lua integration and does not add a third-party plugin. + +The website repository remains the source of truth for Entry creation, +validation, publication timestamps, media conversion, and builds. Neovim only +calls the existing Bun scripts. + +## Requirements + +- The website repository is available at + `~/Development/MyWeb/magnum-opus/main`. +- `direnv` is installed and the website `.envrc` is allowed. +- The website development shell provides Bun. + +Set `RASYIDANAF_SITE_ROOT` before starting Neovim to use another checkout: + +```sh +export RASYIDANAF_SITE_ROOT=/path/to/rasyidan-personal-web +``` + +## Commands and mappings + +The Notes group uses the local leader (`\`). + +| Command | Mapping | Action | +| --- | --- | --- | +| `:NotesNewLog` | `\nl` | Prompt for an optional title and tags, create a Log draft, and open it | +| `:NotesNewNote` | `\nn` | Prompt for a required title and optional tags, create a Note draft, and open it | +| `:NotesPublish` | `\np` | Save and publish the current Entry after confirmation | +| `:NotesValidate` | `\nv` | Validate public Notes metadata, routes, lifecycle, tags, and media | +| `:NotesBuild` | `\nb` | Run the complete production build | + +`NotesPublish` accepts only Markdown files inside `vault/public-notes/`. It +does not commit, push, or deploy. Review the Jujutsu diff and push the website +repository separately. + +## Normal workflow + +1. Press `\nl` for a Log or `\nn` for a titled Note. +2. Write and save the Entry. +3. Press `\np` and confirm publication. +4. Press `\nv`, then `\nb`. +5. Review, describe, and push the website change with Jujutsu. + +The publication command reloads the current buffer after the repository script +writes `publishedAt` and processes referenced images. + +## Apply the configuration + +Home Manager owns the Neovim configuration. Apply dotfiles changes with: + +```sh +cd ~/Development/dotfiles +sudo darwin-rebuild switch --flake .#macbook-pro +``` diff --git a/modules/home/programs/nvim/init.lua b/modules/home/programs/nvim/init.lua index 40f66c5..1d676cc 100644 --- a/modules/home/programs/nvim/init.lua +++ b/modules/home/programs/nvim/init.lua @@ -2,6 +2,7 @@ require("config.options") require("config.keymaps") require("config.autocmds") +require("config.notes") -- Load LazyVim utilities before lazy plugin manager require("config.lazyvim") diff --git a/modules/home/programs/nvim/lua/config/notes.lua b/modules/home/programs/nvim/lua/config/notes.lua new file mode 100644 index 0000000..faa4ab5 --- /dev/null +++ b/modules/home/programs/nvim/lua/config/notes.lua @@ -0,0 +1,218 @@ +local M = {} + +local default_root = vim.fn.expand("~/Development/MyWeb/magnum-opus/main") +local root = vim.fs.normalize(vim.env.RASYIDANAF_SITE_ROOT or default_root) +local notes_directory = vim.fs.normalize(root .. "/vault/public-notes") + +local function notify(message, level) + vim.notify(message, level or vim.log.levels.INFO, { title = "Notes" }) +end + +local function last_line(value) + local output = vim.trim(value or "") + return output:match("([^\r\n]+)$") +end + +local function run(arguments, options) + options = options or {} + + if vim.fn.executable("direnv") ~= 1 then + notify("direnv is not available in PATH.", vim.log.levels.ERROR) + return + end + + local command = { + "direnv", + "exec", + root, + "bun", + "run", + } + vim.list_extend(command, arguments) + + notify(options.progress or "Running Notes command…") + vim.system( + command, + { + cwd = root, + text = true, + }, + vim.schedule_wrap(function(result) + if result.code ~= 0 then + local message = last_line(result.stderr) or last_line(result.stdout) or "Notes command failed." + notify(message, vim.log.levels.ERROR) + return + end + + if options.on_success then + options.on_success(vim.trim(result.stdout or "")) + return + end + + notify(last_line(result.stdout) or options.success or "Notes command finished.") + end) + ) +end + +local function prompt_tags(callback) + vim.ui.input({ + prompt = "Tags (comma-separated, optional): ", + }, function(tags) + if tags == nil then + return + end + + callback(vim.trim(tags)) + end) +end + +local function open_created_entry(output) + local relative_path = last_line(output) + if not relative_path then + notify("The Notes command did not return a file path.", vim.log.levels.ERROR) + return + end + + local path = relative_path + if not vim.startswith(path, "/") then + path = root .. "/" .. path + end + + vim.cmd.edit(vim.fn.fnameescape(vim.fs.normalize(path))) + notify("Draft created: " .. relative_path) +end + +local function create_entry(kind, title, tags) + local arguments = { "notes:new", "--", "--kind", kind } + + if title and title ~= "" then + vim.list_extend(arguments, { "--title", title }) + end + + if tags ~= "" then + vim.list_extend(arguments, { "--tags", tags }) + end + + run(arguments, { + progress = "Creating " .. kind .. " draft…", + on_success = open_created_entry, + }) +end + +function M.new_log() + vim.ui.input({ + prompt = "Log title (optional): ", + }, function(title) + if title == nil then + return + end + + prompt_tags(function(tags) + create_entry("log", vim.trim(title), tags) + end) + end) +end + +function M.new_note() + vim.ui.input({ + prompt = "Note title: ", + }, function(title) + if title == nil then + return + end + + title = vim.trim(title) + if title == "" then + notify("A Note requires a title.", vim.log.levels.WARN) + return + end + + prompt_tags(function(tags) + create_entry("note", title, tags) + end) + end) +end + +local function current_entry() + local path = vim.fs.normalize(vim.api.nvim_buf_get_name(0)) + if path == "" then + notify("The current buffer has no file.", vim.log.levels.ERROR) + return nil + end + + if not vim.startswith(path, notes_directory .. "/") or not path:match("%.md$") then + notify("Open a Markdown file inside vault/public-notes/ first.", vim.log.levels.ERROR) + return nil + end + + return path +end + +function M.publish() + local path = current_entry() + if not path then + return + end + + if vim.bo.modified then + local saved, error_message = pcall(vim.cmd.write) + if not saved then + notify("Could not save the Entry: " .. tostring(error_message), vim.log.levels.ERROR) + return + end + end + + vim.ui.select({ "Publish", "Cancel" }, { + prompt = "Publish " .. vim.fn.fnamemodify(path, ":t") .. "?", + }, function(choice) + if choice ~= "Publish" then + return + end + + run({ "notes:publish", path }, { + progress = "Publishing Entry…", + on_success = function(output) + vim.cmd.edit(vim.fn.fnameescape(path)) + notify(last_line(output) or "Entry published.") + end, + }) + end) +end + +function M.validate() + run({ "notes:validate" }, { + progress = "Validating Notes…", + success = "Notes validation passed.", + }) +end + +function M.build() + run({ "build" }, { + progress = "Building the website…", + success = "Website build passed.", + }) +end + +vim.api.nvim_create_user_command("NotesNewLog", M.new_log, { + desc = "Create a public Log draft", +}) +vim.api.nvim_create_user_command("NotesNewNote", M.new_note, { + desc = "Create a public Note draft", +}) +vim.api.nvim_create_user_command("NotesPublish", M.publish, { + desc = "Publish the current public Notes Entry", +}) +vim.api.nvim_create_user_command("NotesValidate", M.validate, { + desc = "Validate public Notes", +}) +vim.api.nvim_create_user_command("NotesBuild", M.build, { + desc = "Build the website", +}) + +vim.keymap.set("n", "nl", M.new_log, { desc = "New log" }) +vim.keymap.set("n", "nn", M.new_note, { desc = "New note" }) +vim.keymap.set("n", "np", M.publish, { desc = "Publish current note" }) +vim.keymap.set("n", "nv", M.validate, { desc = "Validate notes" }) +vim.keymap.set("n", "nb", M.build, { desc = "Build website" }) + +return M diff --git a/modules/home/programs/nvim/lua/plugins/which-key.lua b/modules/home/programs/nvim/lua/plugins/which-key.lua index 8571924..cedd681 100644 --- a/modules/home/programs/nvim/lua/plugins/which-key.lua +++ b/modules/home/programs/nvim/lua/plugins/which-key.lua @@ -13,6 +13,7 @@ return { -- Leader triggers { mode = "n", keys = "" }, { mode = "x", keys = "" }, + { mode = "n", keys = "" }, -- Built-in completion { mode = "i", keys = "" }, @@ -64,6 +65,7 @@ return { { mode = "n", keys = "r", desc = "+rename" }, { mode = "n", keys = "s", desc = "+search" }, { mode = "n", keys = "t", desc = "+toggle" }, + { mode = "n", keys = "n", desc = "+notes" }, }, window = {