optimzied obsidian

This commit is contained in:
cavelazquez8 2025-02-21 22:34:43 -08:00
parent b046fce3d1
commit c03096290d
1 changed files with 261 additions and 284 deletions

451
init.lua
View File

@ -190,28 +190,49 @@ vim.keymap.set('n', '<C-j>', '<C-w><C-j>', { desc = 'Move focus to the lower win
vim.keymap.set('n', '<C-k>', '<C-w><C-k>', { desc = 'Move focus to the upper window' }) vim.keymap.set('n', '<C-k>', '<C-w><C-k>', { desc = 'Move focus to the upper window' })
vim.api.nvim_set_keymap('c', '%%', "<C-R>=expand('%:h').'/'<CR>", { noremap = true, silent = true }) vim.api.nvim_set_keymap('c', '%%', "<C-R>=expand('%:h').'/'<CR>", { noremap = true, silent = true })
-- Obsidian Integration
-- Configuration
local config_path = vim.fn.expand '~/.config/nvim/obsidian_vaults.json'
local vaults = {}
-- Utility functions -- Obsidian Integration (Optimized)
local function is_executable(cmd) local M = {}
--[[ Module Design:
1. Encapsulated configuration
2. Separated concerns with dedicated modules
3. Strict local scoping
4. Error handling consistency
5. Documentation-ready structure
6. Reduced code duplication
]]
-- Configuration Module --
local Config = {
path = vim.fn.expand '~/.config/nvim/obsidian_vaults.json',
vaults = {},
default_vault_name = 'default',
temp_dir_suffix = '_temp_preview',
}
-- Utility Module --
local Utils = {}
function Utils.is_executable(cmd)
return vim.fn.executable(cmd) == 1 return vim.fn.executable(cmd) == 1
end end
local function ensure_dir_exists(path) function Utils.ensure_dir(path)
return vim.fn.mkdir(path, 'p') return vim.fn.mkdir(path, 'p') == 1
end end
local function safe_path(path) function Utils.safe_path(path)
return path:gsub([[\]], [[/]]):gsub('/$', '') .. '/' return (path:gsub([[\]], [[/]]):gsub('/$', '') .. '/')
end end
-- URL encode a string using either Python or Lua function Utils.notify(msg, level)
local function url_encode(str) vim.notify(msg, level or vim.log.levels.INFO)
if is_executable 'python3' then end
local handle = io.popen(string.format('python3 -c "import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1]))" %q', str))
function Utils.url_encode(str)
if Utils.is_executable 'python3' then
local handle = io.popen(string.format('python3 -c "import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1]))" %q 2>/dev/null', str))
if handle then if handle then
local result = handle:read '*a' local result = handle:read '*a'
handle:close() handle:close()
@ -219,268 +240,245 @@ local function url_encode(str)
end end
end end
-- Fallback to basic Lua URL encoding
return str:gsub('[^%w%-%.%_%~]', function(c) return str:gsub('[^%w%-%.%_%~]', function(c)
return string.format('%%%02X', string.byte(c)) return string.format('%%%02X', string.byte(c))
end) end)
end end
-- Vault management functions -- If vim.pesc is not defined, define it to escape Lua patterns.
local function save_vault_configs() if not vim.pesc then
local f = io.open(config_path, 'w') vim.pesc = function(str)
if not f then return str:gsub('([^%w])', '%%%1')
vim.notify('Failed to save vault configurations', vim.log.levels.ERROR)
return false
end end
local success, encoded = pcall(vim.json.encode, vaults)
if not success then
f:close()
vim.notify('Failed to encode vault configurations', vim.log.levels.ERROR)
return false
end
f:write(encoded)
f:close()
return true
end end
local function load_vault_configs() -- Vault Manager Module --
local f = io.open(config_path, 'r') local VaultManager = {}
if f then
local content = f:read '*all' function VaultManager.load()
f:close() local file = io.open(Config.path, 'r')
if not file then
return VaultManager.create_default()
end
local success, parsed = pcall(vim.json.decode, file:read '*a')
file:close()
local success, parsed = pcall(vim.json.decode, content)
if success and type(parsed) == 'table' then if success and type(parsed) == 'table' then
vaults = parsed Config.vaults = parsed
return true return true
end end
end
-- Initialize with default vault if none exists return VaultManager.create_default()
if #vaults == 0 then end
local default_path = vim.fn.expand '~/obsidian-vault/'
ensure_dir_exists(default_path) function VaultManager.create_default()
vaults = { { local default_path = Utils.safe_path(vim.fn.expand '~/obsidian-vault/')
name = 'default', if Utils.ensure_dir(default_path) then
Config.vaults = { {
name = Config.default_vault_name,
path = default_path, path = default_path,
} } } }
save_vault_configs() return VaultManager.save()
end end
return false return false
end end
-- Find which vault contains a given filepath function VaultManager.save()
local function find_containing_vault(filepath) local file = io.open(Config.path, 'w')
filepath = safe_path(filepath) if not file then
for _, vault in ipairs(vaults) do Utils.notify('Failed to save vault config', vim.log.levels.ERROR)
local vault_path = safe_path(vault.path) return false
if filepath:find('^' .. vim.pesc(vault_path)) then end
return vault, filepath:gsub('^' .. vim.pesc(vault_path), '')
local success, encoded = pcall(vim.json.encode, Config.vaults)
if not success then
file:close()
Utils.notify('Config serialization failed', vim.log.levels.ERROR)
return false
end
file:write(encoded)
file:close()
return true
end
function VaultManager.find_containing(filepath)
local safe_path = Utils.safe_path(filepath)
for _, vault in ipairs(Config.vaults) do
local vault_path = Utils.safe_path(vault.path)
if safe_path:find('^' .. vim.pesc(vault_path)) then
return vault, safe_path:gsub('^' .. vim.pesc(vault_path), '')
end end
end end
return nil, nil return nil, nil
end end
-- Obsidian interaction functions -- Obsidian Core --
local function open_in_obsidian() local Obsidian = {}
function Obsidian.open()
local filepath = vim.fn.expand '%:p' local filepath = vim.fn.expand '%:p'
if filepath == '' then if filepath == '' then
vim.notify('No file to open!', vim.log.levels.WARN) return Utils.notify('No file to open', vim.log.levels.WARN)
return
end end
if not filepath:match '%.md$' then if not filepath:match '%.md$' then
vim.notify('Not a markdown file', vim.log.levels.WARN) return Utils.notify('Not a markdown file', vim.log.levels.WARN)
return
end end
local containing_vault, relative_path = find_containing_vault(filepath) local vault, rel_path = VaultManager.find_containing(filepath)
if vault then
if containing_vault then Obsidian.open_vault_file(vault, rel_path)
local encoded_path = url_encode(relative_path)
local uri = string.format('obsidian://open?vault=%s&file=%s', containing_vault.name, encoded_path)
vim.notify(string.format("Opening in vault '%s': %s", containing_vault.name, relative_path))
vim.fn.jobstart({ 'xdg-open', uri }, {
detach = true,
on_exit = function(_, code)
if code ~= 0 then
vim.notify('Failed to open Obsidian', vim.log.levels.ERROR)
end
end,
})
else else
-- Handle external files Obsidian.open_external_file(filepath)
local default_vault = vaults[1]
if not default_vault then
vim.notify('No default vault configured', vim.log.levels.ERROR)
return
end
local temp_dir = safe_path(default_vault.path .. '_temp_preview')
ensure_dir_exists(temp_dir)
local file_basename = vim.fn.fnamemodify(filepath, ':t')
local temp_link_path = temp_dir .. file_basename
os.remove(temp_link_path)
if not vim.loop.fs_symlink(filepath, temp_link_path) then
vim.notify('Failed to create symlink', vim.log.levels.ERROR)
return
end
-- Register cleanup
vim.api.nvim_create_autocmd({ 'BufDelete', 'BufWipeout' }, {
buffer = vim.api.nvim_get_current_buf(),
callback = function()
os.remove(temp_link_path)
end,
})
local encoded_path = url_encode('_temp_preview/' .. file_basename)
local uri = string.format('obsidian://open?vault=%s&file=%s', default_vault.name, encoded_path)
vim.fn.jobstart({ 'xdg-open', uri }, {
detach = true,
on_exit = function(_, code)
if code ~= 0 then
vim.notify('Failed to open Obsidian', vim.log.levels.ERROR)
end
end,
})
end end
end end
local function close_obsidian(detach) function Obsidian.open_vault_file(vault, rel_path)
detach = detach or false local encoded = Utils.url_encode(rel_path)
if is_executable 'pkill' then local uri = ('obsidian://open?vault=%s&file=%s'):format(vault.name, encoded)
Utils.notify(("Opening in '%s': %s"):format(vault.name, rel_path))
Obsidian.execute({ 'xdg-open', uri }, true)
end
function Obsidian.open_external_file(filepath)
local default_vault = Config.vaults[1]
if not default_vault then
return Utils.notify('No default vault', vim.log.levels.ERROR)
end
local temp_dir = Utils.safe_path(default_vault.path .. Config.temp_dir_suffix)
if not Utils.ensure_dir(temp_dir) then
return
end
local basename = vim.fn.fnamemodify(filepath, ':t')
local symlink = temp_dir .. basename
os.remove(symlink)
if not vim.loop.fs_symlink(filepath, symlink) then
return Utils.notify('Symlink failed', vim.log.levels.ERROR)
end
vim.api.nvim_create_autocmd({ 'BufDelete', 'BufWipeout' }, {
buffer = vim.api.nvim_get_current_buf(),
callback = function()
os.remove(symlink)
end,
})
local encoded = Utils.url_encode(Config.temp_dir_suffix .. '/' .. basename)
local uri = ('obsidian://open?vault=%s&file=%s'):format(default_vault.name, encoded)
Obsidian.execute({ 'xdg-open', uri }, true)
end
function Obsidian.close(detach)
if not Utils.is_executable 'pkill' then
return
end
local opts = { local opts = {
on_exit = function(_, code) detach = detach or false,
on_exit = not detach and function(_, code)
if code == 0 then if code == 0 then
vim.notify 'Closed Obsidian' Utils.notify 'Obsidian closed'
end end
end, end,
} }
if detach then Obsidian.execute({ 'pkill', '-9', '-f', 'obsidian' }, opts.detach, opts.on_exit)
opts.detach = true
opts.on_exit = nil -- No notifications when detached
end
-- Force kill with SIGKILL (9) for immediate termination
vim.fn.jobstart({ 'pkill', '-9', '-f', 'obsidian' }, opts)
end
end end
-- Public functions for commands function Obsidian.execute(command, detach, callback)
function add_vault(name, path) vim.fn.jobstart(command, {
if type(name) ~= 'string' or type(path) ~= 'string' then detach = detach,
vim.notify('Invalid vault name or path', vim.log.levels.ERROR) on_exit = callback or function(_, code)
return false if code ~= 0 then
Utils.notify('Command failed', vim.log.levels.ERROR)
end end
end,
})
end
path = safe_path(vim.fn.expand(path)) -- Public API --
function M.add_vault(name, path)
path = Utils.safe_path(vim.fn.expand(path))
-- Check for duplicates for _, vault in ipairs(Config.vaults) do
for _, vault in ipairs(vaults) do
if vault.name == name then if vault.name == name then
vim.notify('Vault name already exists', vim.log.levels.WARN) return Utils.notify('Vault name exists', vim.log.levels.WARN)
return false
end end
if safe_path(vault.path) == path then if Utils.safe_path(vault.path) == path then
vim.notify('Vault path already exists', vim.log.levels.WARN) return Utils.notify('Vault path exists', vim.log.levels.WARN)
return false
end end
end end
-- Ensure directory exists if not Utils.ensure_dir(path) then
if ensure_dir_exists(path) ~= 1 then return Utils.notify('Directory creation failed', vim.log.levels.ERROR)
vim.notify('Failed to create vault directory', vim.log.levels.ERROR)
return false
end end
table.insert(vaults, { name = name, path = path }) table.insert(Config.vaults, { name = name, path = path })
if save_vault_configs() then return VaultManager.save() and Utils.notify(('Added vault: %s'):format(name))
vim.notify(string.format('Added vault: %s at %s', name, path))
return true
end
return false
end end
function remove_vault(name) function M.remove_vault(name)
for i, vault in ipairs(vaults) do for i, vault in ipairs(Config.vaults) do
if vault.name == name then if vault.name == name then
table.remove(vaults, i) table.remove(Config.vaults, i)
if save_vault_configs() then return VaultManager.save() and Utils.notify(('Removed vault: %s'):format(name))
vim.notify(string.format('Removed vault: %s', name))
return true
end
break
end end
end end
vim.notify('Vault not found', vim.log.levels.WARN) Utils.notify('Vault not found', vim.log.levels.WARN)
return false
end end
function list_vaults() function M.list_vaults()
if #vaults == 0 then local lines = { 'Configured Vaults:' }
vim.notify('No vaults configured', vim.log.levels.INFO) for _, vault in ipairs(Config.vaults) do
return table.insert(lines, ('- %s: %s'):format(vault.name, vault.path))
end end
Utils.notify(table.concat(lines, '\n'))
local lines = { 'Configured Obsidian vaults:' }
for _, vault in ipairs(vaults) do
table.insert(lines, string.format('- %s: %s', vault.name, vault.path))
end
vim.notify(table.concat(lines, '\n'))
end end
-- Load configurations -- Initialization --
load_vault_configs() VaultManager.load()
-- Create commands -- Command Setup --
vim.api.nvim_create_user_command('OpenInObsidian', open_in_obsidian, {}) vim.api.nvim_create_user_command('OpenInObsidian', Obsidian.open, {})
vim.api.nvim_create_user_command('CloseObsidian', function() vim.api.nvim_create_user_command('CloseObsidian', function()
close_obsidian(false) Obsidian.close()
end, {}) end, {})
vim.api.nvim_create_user_command('ListObsidianVaults', list_vaults, {}) vim.api.nvim_create_user_command('ListObsidianVaults', M.list_vaults, {})
vim.api.nvim_create_user_command('AddObsidianVault', function(opts) vim.api.nvim_create_user_command('AddObsidianVault', function(opts)
local args = vim.split(opts.args, '%s+', { trimempty = true }) local args = vim.split(opts.args, '%s+', { trimempty = true })
if #args ~= 2 then if #args == 2 then
vim.notify('Usage: AddObsidianVault <name> <path>', vim.log.levels.WARN) M.add_vault(args[1], args[2])
return
end end
add_vault(args[1], args[2])
end, { nargs = '+' }) end, { nargs = '+' })
vim.api.nvim_create_user_command('RemoveObsidianVault', function(opts) vim.api.nvim_create_user_command('RemoveObsidianVault', function(opts)
remove_vault(opts.args) M.remove_vault(opts.args)
end, { nargs = 1 }) end, { nargs = 1 })
-- Set up keymaps -- Keymaps --
local ok, wk = pcall(require, 'which-key') local wk_ok, wk = pcall(require, 'which-key')
if ok then local keymaps = {
wk.register {
['<leader>o'] = { ['<leader>o'] = {
name = 'Obsidian', name = 'Obsidian',
o = { open_in_obsidian, 'Open in Obsidian' }, o = { Obsidian.open, 'Open' },
c = { c = {
function() function()
close_obsidian(false) Obsidian.close()
end, end,
'Close Obsidian', 'Close',
}, },
l = { list_vaults, 'List Vaults' }, l = { M.list_vaults, 'List' },
a = { a = {
function() function()
vim.ui.input({ prompt = 'Vault name: ' }, function(name) vim.ui.input({ prompt = 'Vault name: ' }, function(name)
if name then if name then
vim.ui.input({ prompt = 'Vault path: ' }, function(path) vim.ui.input({ prompt = 'Path: ' }, function(path)
if path then if path then
add_vault(name, path) M.add_vault(name, path)
end end
end) end)
end end
@ -490,68 +488,47 @@ if ok then
}, },
r = { r = {
function() function()
vim.ui.input({ prompt = 'Vault name to remove: ' }, function(name) vim.ui.input({ prompt = 'Vault to remove: ' }, function(name)
if name then if name then
remove_vault(name) M.remove_vault(name)
end end
end) end)
end, end,
'Remove Vault', 'Remove Vault',
}, },
}, },
} }
if wk_ok then
wk.register(keymaps)
else else
local opts = { noremap = true, silent = true } for lhs, rhs in pairs(keymaps['<leader>o']) do
vim.keymap.set('n', '<leader>oo', open_in_obsidian, opts) if type(rhs) == 'table' then
vim.keymap.set('n', '<leader>oc', function() vim.keymap.set('n', '<leader>o' .. lhs, rhs[1], { desc = rhs[2] })
close_obsidian(false)
end, opts)
vim.keymap.set('n', '<leader>ol', list_vaults, opts)
vim.keymap.set('n', '<leader>oa', function()
vim.ui.input({ prompt = 'Vault name: ' }, function(name)
if name then
vim.ui.input({ prompt = 'Vault path: ' }, function(path)
if path then
add_vault(name, path)
end end
end)
end end
end)
end, opts)
vim.keymap.set('n', '<leader>or', function()
vim.ui.input({ prompt = 'Vault name to remove: ' }, function(name)
if name then
remove_vault(name)
end
end)
end, opts)
end end
-- Set up autocommands -- Autocommands --
vim.api.nvim_create_augroup('obsidian_integration', { clear = true }) vim.api.nvim_create_augroup('ObsidianIntegration', { clear = true })
-- Enable autoread
vim.o.autoread = true
vim.api.nvim_create_autocmd({ 'FocusGained', 'BufEnter' }, { vim.api.nvim_create_autocmd({ 'FocusGained', 'BufEnter' }, {
group = 'obsidian_integration', group = 'ObsidianIntegration',
callback = function() callback = vim.schedule_wrap(function()
vim.cmd 'checktime' vim.cmd 'checktime'
end),
})
vim.api.nvim_create_autocmd('VimLeavePre', {
group = 'ObsidianIntegration',
callback = function()
for _, vault in ipairs(Config.vaults) do
os.execute(('rm -rf %q'):format(vault.path .. Config.temp_dir_suffix))
end
Obsidian.close(true)
end, end,
}) })
-- Cleanup on exit
vim.api.nvim_create_autocmd('VimLeavePre', {
group = 'obsidian_integration',
callback = function()
-- Clean up temp directories
for _, vault in ipairs(vaults) do
local temp_dir = safe_path(vault.path .. '_temp_preview')
os.execute(string.format('rm -rf %q', temp_dir))
end
-- Force close Obsidian immediately using detached process
close_obsidian(true)
end,
})
-- [[ Basic Autocommands ]] -- [[ Basic Autocommands ]]
-- See `:help lua-guide-autocommands` -- See `:help lua-guide-autocommands`