aboutsummaryrefslogtreecommitdiff
path: root/lua
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-06-15 21:15:19 -0400
committerhistoria <[not public]>2026-06-15 21:18:55 -0400
commit3cdd2f186c302f2f6c9e63cf035e4f51a434a97b (patch)
tree5070ce4f33dfbca5978cc16928b545e0b50bb186 /lua
parent69c854a8e4a8b10f940d9a0192d2d40d3b306943 (diff)
downloaddenote-fzf-lua-3cdd2f186c302f2f6c9e63cf035e4f51a434a97b.tar.gz
refactor: rewrote shell in lua. removed dependencies.
Diffstat (limited to 'lua')
-rw-r--r--lua/denote-fzf-lua.lua259
-rw-r--r--lua/denote-fzf-lua/config.lua23
-rwxr-xr-xlua/denote-fzf-lua/scripts/fallback_search_files.sh16
-rwxr-xr-xlua/denote-fzf-lua/scripts/search_files.sh16
4 files changed, 172 insertions, 142 deletions
diff --git a/lua/denote-fzf-lua.lua b/lua/denote-fzf-lua.lua
index f545bc0..3fc4384 100644
--- a/lua/denote-fzf-lua.lua
+++ b/lua/denote-fzf-lua.lua
@@ -1,135 +1,205 @@
local M = {}
local config = require("denote-fzf-lua.config")
-local fzflua = require('fzf-lua')
+local fzflua = require("fzf-lua")
local builtin = require("fzf-lua.previewer.builtin")
---- fzf-lua custom previewer (recombines search result fields into a filename)
-M.recombine_previewer = builtin.buffer_or_file:extend()
+local FIELDS = { "path", "date", "time", "sig", "title", "keywords", "ext" }
-function M.recombine_previewer:new(o, opts, fzf_win)
- M.recombine_previewer.super.new(self, o, opts, fzf_win)
- setmetatable(self, M.recombine_previewer)
- return self
+local function list_denote_files(dir)
+ local results = {}
+ local stack = { vim.fn.expand(dir) }
+
+ while #stack > 0 do
+ local current = table.remove(stack)
+ local ok, entries = pcall(vim.fn.readdir, current)
+ if ok then
+ for _, entry in ipairs(entries) do
+ local full = current .. "/" .. entry
+ local stat = vim.loop.fs_stat(full)
+ if stat then
+ if stat.type == "directory" then
+ table.insert(stack, full)
+ elseif stat.type == "file" and entry:match("^%d%d%d%d%d%d%d%dT%d%d%d%d%d%d") then
+ table.insert(results, full)
+ end
+ end
+ end
+ end
+ end
+
+ return results
end
-function M.recombine_previewer:parse_entry(entry_str)
- local path = entry_str:match("([^:]+:%d%d:[^:]+):?")
+local function parse_denote(basename)
+ local dt = basename:sub(1, 15)
+ if not dt:match("^%d%d%d%d%d%d%d%dT%d%d%d%d%d%d$") then return nil end
+
+ local date = dt:sub(1, 4) .. "-" .. dt:sub(5, 6) .. "-" .. dt:sub(7, 8)
+ local time = dt:sub(10, 11) .. ":" .. dt:sub(12, 13) .. ":" .. dt:sub(14, 15)
+
+ local dot_idx = basename:find("%.", 16, true)
+ if not dot_idx then return nil end
+ local ext = basename:sub(dot_idx)
+ local body = basename:sub(16, dot_idx - 1)
+
+ local sig, title, keywords = "", "", ""
+ local pos = 1
+
+ if body:sub(pos, pos + 1) == "==" then
+ local sig_end = body:find("%-%-", pos + 2)
+ if sig_end then
+ sig = body:sub(pos + 2, sig_end - 1)
+ pos = sig_end
+ end
+ end
+
+ if body:sub(pos, pos + 1) == "--" then
+ local title_end = body:find("__", pos + 2)
+ if title_end then
+ title = body:sub(pos + 2, title_end - 1)
+ pos = title_end
+ else
+ title = body:sub(pos + 2)
+ pos = #body + 1
+ end
+ else
+ return nil
+ end
+
+ if pos <= #body and body:sub(pos) == "__" then
+ keywords = body:sub(pos + 2)
+ end
+
return {
- path = M.recombine_filename(path),
- line = 1,
- col = 1,
+ path = "",
+ date = date,
+ time = time,
+ sig = sig ~= "" and sig:gsub("=", " ") or ".",
+ title = title ~= "" and title:gsub("%-", " ") or ".",
+ keywords = keywords ~= "" and keywords:gsub("_", " ") or ".",
+ ext = ext,
}
end
-function M.copy_table(table)
- local new_table = {}
- for k, v in pairs(table) do
- new_table[k] = v
+local function format_denote_table(files)
+ local parsed = {}
+ for _, filepath in ipairs(files) do
+ local basename = vim.fn.fnamemodify(filepath, ":t")
+ local dirpath = vim.fn.fnamemodify(filepath, ":h") .. "/"
+ local info = parse_denote(basename)
+ if info then
+ info.path = dirpath
+ table.insert(parsed, info)
+ end
end
- return new_table
-end
----@param force_slow boolean - If true, act as if we don't have dependencies
----Check for dependencies and optional dependencies
-function M.has_prereqs(force_slow)
- if vim.fn.executable('fzf') ~= 1 then return "no" end
- if force_slow then return "partial" end
- if vim.fn.executable('fd') ~= 1 then return "partial" end
- if vim.fn.executable('sd') ~= 1 then return "partial" end
- if vim.fn.executable('qsv') ~= 1 then return "partial" end
- return "yes"
-end
+ local widths = {}
+ for i, field in ipairs(FIELDS) do
+ widths[i] = #field
+ end
+ for _, info in ipairs(parsed) do
+ for i, field in ipairs(FIELDS) do
+ widths[i] = math.max(widths[i], #info[field])
+ end
+ end
----@param force_slow boolean - If true, act as if we don't have dependencies
----Sets the full path of the appropriate search script (regular or fallback)
-function M.set_script_path(force_slow)
- local lua_file_path = debug.getinfo(1, "S").source:sub(2)
- local lua_file_dir = vim.fn.fnamemodify(lua_file_path, ":h")
- local script_path = lua_file_dir .. "/denote-fzf-lua/scripts/"
- local dependencies = M.has_prereqs(force_slow)
- if dependencies == "yes" then
- script_path = script_path .. "search_files.sh"
- elseif dependencies == "partial" then
- script_path = script_path .. "fallback_search_files.sh"
- else
- error("Missing dependency: fzf")
- return false
+ local lines = {}
+ local parts = {}
+ for i, field in ipairs(FIELDS) do
+ parts[i] = string.format("%-" .. widths[i] .. "s", field)
end
- return script_path
-end
+ table.insert(lines, table.concat(parts, " "))
----@param options table of user options
----Format the --with-nth argument for fzf (which fields are shown)
-function M.format_fzf_with_nth(options)
- local fzf_with_nth = ""
- local fields = {"path", "date", "time", "sig", "title", "keywords", "ext" }
- for i, v in ipairs(fields) do
- if options.search_fields[v] then
- fzf_with_nth = fzf_with_nth .. i .. ','
- end
+ for _, info in ipairs(parsed) do
+ parts = {}
+ for i, field in ipairs(FIELDS) do
+ parts[i] = string.format("%-" .. widths[i] .. "s", info[field])
+ end
+ table.insert(lines, table.concat(parts, " "))
end
- return fzf_with_nth:sub(1, -2)
+
+ return lines
end
----@param f string - Field from search result
----@param delim string delimiter character
----Puts a field from the search results back into Denote format (e.g. two words to --two-words)
-function M.repopulate_field(f, delim)
- if f == "." then
- return ""
- else
- return delim .. delim .. f:gsub("%s",delim)
- end
+M.reconstruct_previewer = builtin.buffer_or_file:extend()
+
+function M.reconstruct_previewer:new(o, opts, fzf_win)
+ M.reconstruct_previewer.super.new(self, o, opts, fzf_win)
+ setmetatable(self, M.reconstruct_previewer)
+ return self
end
----@param filename string chopped up string with fields separated by multiple spaces
-function M.recombine_filename(filename)
- if filename == nil then return "" end
+function M.reconstruct_previewer:parse_entry(entry_str)
+ return {
+ path = M.reconstruct_filename(entry_str),
+ line = 1,
+ col = 1,
+ }
+end
+
+local function format_field(field, delim)
+ if field == "." then return "" end
+ return delim .. delim .. field:gsub("%s", delim)
+end
+
+function M.reconstruct_filename(row)
+ if not row or row == "" then return "" end
local t = {}
- filename = filename:gsub("%s%s+", "|")
+ row = row:gsub("%s%s+", "|")
t.path, t.year, t.month, t.day, t.hour, t.min, t.sec, t.sig, t.title, t.keywords, t.ext =
- filename:match("^(.*/)|(%d%d%d%d)%-(%d%d)%-(%d%d)|(%d%d):(%d%d):(%d%d)|([^|]+)|([^|]+)|([^|]+)|(%..+)")
- t.sig = M.repopulate_field(t.sig,"=")
- t.title = M.repopulate_field(t.title,"-")
- t.keywords = M.repopulate_field(t.keywords,"_")
+ row:match("^(.*/)|(%d%d%d%d)%-(%d%d)%-(%d%d)|(%d%d):(%d%d):(%d%d)|([^|]+)|([^|]+)|([^|]+)|(%..+)")
+ if not t.path then return "" end
+ t.sig = format_field(t.sig, "=")
+ t.title = format_field(t.title, "-")
+ t.keywords = format_field(t.keywords, "_")
return t.path .. t.year .. t.month .. t.day .. "T" .. t.hour .. t.min .. t.sec .. t.sig .. t.title .. t.keywords .. t.ext
end
----@param options table of user options
----Opens a window to search filenames
+function M.format_fzf_with_nth(options)
+ local indices = {}
+ for i, field in ipairs(FIELDS) do
+ if options.search_fields[field] then
+ table.insert(indices, i)
+ end
+ end
+ return table.concat(indices, ",")
+end
+
function M.search_files(options)
- local script_path = M.set_script_path(options.force_slow_mode)
- if not script_path then return end
+ local files = list_denote_files(options.dir)
+ local lines = format_denote_table(files)
+
local opts = {}
- opts.previewer = M.recombine_previewer
- opts.fzf_opts = M.copy_table(options.fzf_lua_opts.fzf_opts)
+ opts.previewer = M.reconstruct_previewer
+ opts.fzf_opts = vim.deepcopy(options.fzf_lua_opts.fzf_opts)
opts.fzf_opts["--with-nth"] = M.format_fzf_with_nth(options)
- opts.fzf_opts['--header-lines'] = 1
- opts.fzf_opts['--delimiter'] = "\\s{2,}"
+ opts.fzf_opts["--header-lines"] = 1
+ opts.fzf_opts["--delimiter"] = "\\s{2,}"
opts.actions = {
- ['default'] = function(selected)
- local filename = M.recombine_filename(selected[1])
- vim.api.nvim_command('edit ' .. filename)
- end }
- fzflua.fzf_exec(script_path .. " " .. options.dir, opts)
+ default = function(selected)
+ local filename = M.reconstruct_filename(selected[1])
+ vim.api.nvim_command("edit " .. vim.fn.fnameescape(filename))
+ end,
+ }
+ fzflua.fzf_exec(lines, opts)
end
---- Performs a standard rg search
function M.search_contents(options)
+ if vim.fn.executable("rg") ~= 1 then
+ vim.notify("denote-fzf-lua: ripgrep is required for content search", vim.log.levels.WARN)
+ return
+ end
local opts = {}
- opts.cwd = options.dir
+ opts.cwd = options.dir
opts.previewer = "builtin"
- opts.fzf_opts = options.fzf_lua_opts.fzf_opts
- opts.actions = fzflua.defaults.actions.files
- return fzflua.fzf_live(function(q)
- local query = type(q) == "table" and table.concat(q, " ") or q or ''
- return "rg --column --color=always -i -- " .. vim.fn.shellescape(query)
+ opts.fzf_opts = options.fzf_lua_opts.fzf_opts
+ opts.actions = fzflua.defaults.actions.files
+ fzflua.fzf_live(function(query)
+ return "rg --column --color=always -i -- " .. vim.fn.shellescape(query or "")
end, opts)
end
----@param options table of user options
----Creates Neovim command :DenoteSearch
function M.load_cmd(options)
vim.api.nvim_create_user_command("DenoteSearch", function(opts)
if opts.fargs[1] == "files" then
@@ -142,12 +212,11 @@ function M.load_cmd(options)
end, {
nargs = 1,
complete = function()
- return {"files", "contents"}
+ return { "files", "contents" }
end,
})
end
----@param options? table user configuration
function M.setup(options)
options = vim.tbl_deep_extend("force", config.defaults, options or {})
fzflua.setup(options.fzf_lua_opts)
diff --git a/lua/denote-fzf-lua/config.lua b/lua/denote-fzf-lua/config.lua
index 04d3bdc..cd451e2 100644
--- a/lua/denote-fzf-lua/config.lua
+++ b/lua/denote-fzf-lua/config.lua
@@ -1,21 +1,18 @@
local M = {}
M.defaults = {
- dir = "~/notes", -- Notes directory
- force_slow_mode = false, -- Disables optional dependencies (fd, sd, qsv)
+ dir = "~/notes",
- -- Choose which fields appear in search results
search_fields = {
- path = false,
- date = true,
- time = false,
- sig = false,
- title = true,
- keywords = true,
- ext = false,
+ path = false,
+ date = true,
+ time = false,
+ sig = false,
+ title = true,
+ keywords = true,
+ ext = false,
},
- -- Settings for fzf-lua. See fzf-lua doc for full list of options
fzf_lua_opts = {
winopts = {
height = 0.85,
@@ -24,9 +21,7 @@ M.defaults = {
col = 0.50,
},
fzf_opts = {
- ['--layout'] = false,
['--reverse'] = true,
- ['--info'] = false,
['--no-info'] = true,
['--no-separator'] = true,
['--no-hscroll'] = true,
@@ -35,6 +30,4 @@ M.defaults = {
},
}
-M.options = M.defaults
-
return M
diff --git a/lua/denote-fzf-lua/scripts/fallback_search_files.sh b/lua/denote-fzf-lua/scripts/fallback_search_files.sh
deleted file mode 100755
index fd15ca2..0000000
--- a/lua/denote-fzf-lua/scripts/fallback_search_files.sh
+++ /dev/null
@@ -1,16 +0,0 @@
-#!/bin/sh
-find $1 -not -path '*/.*' |\
-sed -E "/^(.*\/)?([0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]T[0-9][0-9][0-9][0-9][0-9][0-9])(==[^_\.\-]+)?(--[^_\.]+)?(__[^\.]+)?(\.[a-z0-9]+)$/!d" |\
-sed -E "s/^(.*\/)?([0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]T[0-9][0-9][0-9][0-9][0-9][0-9])(==([^_\.\-]+))?(--([^_\.]+))?(__([^\.]+))?(\.[a-z0-9]+)$/\1,\2,\4,\6,\8,\9/" |\
-tr '\=\-_' ' ' |\
-sed -E 's/([0-9][0-9][0-9][0-9])([0-9][0-9])([0-9][0-9])T([0-9][0-9])([0-9][0-9])([0-9][0-9])/\1-\2-\3,\4:\5:\6/' |\
-sed 's/,,/,.,/g' |\
-column -t -s "," -N Path,Date,Time,Sig,Title,Keywords,Ext
-
-# 1. find everything in directory
-# 2. sed deletes every line that isn't a Denote note (can't do this with find alone because -regex can't match optional capture groups)
-# 3. sed splits the Denote filename into , delimited fields (pa
-# 4. tr replaces sig, title, keywords special characters with spaces
-# 5. sed splits the date and time nicely
-# 6. sed replaces blank fields with a period
-# 7. column pretty prints the filenames in a table
diff --git a/lua/denote-fzf-lua/scripts/search_files.sh b/lua/denote-fzf-lua/scripts/search_files.sh
deleted file mode 100755
index 2bfd271..0000000
--- a/lua/denote-fzf-lua/scripts/search_files.sh
+++ /dev/null
@@ -1,16 +0,0 @@
-#!/bin/sh
-(echo "Path,Date,Time,Sig,Title,Keywords,Ext";
-fd '^\d{8}T\d{6}(==[a-z0-9=]+)?(\-\-[a-z0-9-]+)?(__[a-z0-9_]+)?\.\w+$' $1 |\
-sd '(?P<path>\/.*\/)?(?P<datetime>\d{8}T\d{6})(==(?P<sig>[a-z0-9=]+))?(--(?P<title>[a-z0-9-]+))(__(?P<keywords>[a-z0-9_]+))?(?P<ext>\.\w+)' '$path,$datetime,$sig,$title,$keywords,$ext' |\
-tr '\=\-_' ' ' |\
-sd '(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})' '$1-$2-$3,$4:$5:$6') |\
-sd ',,' ',.,' |\
-qsv table
-
-# 1. echo the table headers
-# 2. fd only Denote files in $1 directory (and subdirectories)
-# 3. sd chops the Denote filename fields into a CSV
-# 4. tr replaces special characters in sig, title, and keywords with spaces
-# 5. sd formats the date and time as YYYY-MM-DD HH:MM:SS
-# 6. sd replaces blank fields with . (fzf can't handle blank fields)
-# 7. qsv to print the fields in tabular format