Merge branch 'master' of gitea.hptrow.me:pt/setup_env
This commit is contained in:
commit
8432a87a1e
10
CLAUDE.md
10
CLAUDE.md
@ -59,7 +59,9 @@ Symlink-based deployment. Editing `~/.<file>` directly edits the repo file. `git
|
|||||||
- `dotfiles/.bashrc` — main shell config
|
- `dotfiles/.bashrc` — main shell config
|
||||||
- `dotfiles/.vimrc`, `.gitconfig`, `.tmux.conf`, `.psqlrc`, `.pspgconf`
|
- `dotfiles/.vimrc`, `.gitconfig`, `.tmux.conf`, `.psqlrc`, `.pspgconf`
|
||||||
- `dotfiles/bin/td` — time-tracking CLI
|
- `dotfiles/bin/td` — time-tracking CLI
|
||||||
- `dotfiles/nvim/td.lua`, `td_mappings.lua` — nvim lua modules
|
- `dotfiles/nvim/td.lua`, `td_mappings.lua` — nvim time-tracking integration
|
||||||
|
- `dotfiles/nvim/rename_term.lua` — vault-wide find-and-replace with confirmation popup (`<leader>rn`)
|
||||||
|
- `dotfiles/nvim/telescope_tasks.lua` — vault-wide task pickers (`<leader>tl` open tasks, `<leader>tL` priority tasks)
|
||||||
|
|
||||||
**Gitignored** (machine-specific, auto-bootstrapped from examples):
|
**Gitignored** (machine-specific, auto-bootstrapped from examples):
|
||||||
- `dotfiles/.bashrc_local` — secrets and credentials
|
- `dotfiles/.bashrc_local` — secrets and credentials
|
||||||
@ -69,7 +71,11 @@ Symlink-based deployment. Editing `~/.<file>` directly edits the repo file. `git
|
|||||||
|
|
||||||
The nvim config lives in a separate repo at `git@gitea.hptrow.me:pt/nvchad.git` (`customize` branch), deployed to `~/.config/nvim`. `install_nvchad.sh` manages it.
|
The nvim config lives in a separate repo at `git@gitea.hptrow.me:pt/nvchad.git` (`customize` branch), deployed to `~/.config/nvim`. `install_nvchad.sh` manages it.
|
||||||
|
|
||||||
`dotfiles/nvim/td.lua` and `td_mappings.lua` are deployed as symlinks into `~/.config/nvim/lua/` by `deploy_nvim`. The nvim repo's `mappings.lua` loads them via `require "td_mappings"`. The nvim repo does not track these files — setup_env owns them.
|
**Ownership boundary — important:**
|
||||||
|
- **nvchad repo owns `mappings.lua`** — this is the keymap entry point. It lives in `~/.config/nvim/lua/mappings.lua` and is tracked by the nvchad git repo. Edit it there, push from `~/.config/nvim`.
|
||||||
|
- **setup_env owns feature modules** — `td.lua`, `td_mappings.lua`, `rename_term.lua`, `telescope_tasks.lua`. These are deployed as symlinks into `~/.config/nvim/lua/` by `deploy_nvim`. The nvchad repo does NOT track them. `mappings.lua` loads them via `require`.
|
||||||
|
|
||||||
|
Do NOT put `mappings.lua` in `dotfiles/nvim/`. It will conflict with the nvchad repo's tracked copy on every `git pull` during sync, producing `.backup` files on all machines.
|
||||||
|
|
||||||
**Dependency**: `deploy_nvim` must run after `install_nvchad.sh` has created `~/.config/nvim/lua/`. On a fresh machine this is handled automatically by `setup_env.sh` ordering. On existing machines `sync.sh` handles it.
|
**Dependency**: `deploy_nvim` must run after `install_nvchad.sh` has created `~/.config/nvim/lua/`. On a fresh machine this is handled automatically by `setup_env.sh` ordering. On existing machines `sync.sh` handles it.
|
||||||
|
|
||||||
|
|||||||
@ -129,14 +129,14 @@ xmspa() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
alias gs='git status -s'
|
alias gs='git status -s'
|
||||||
alias ga='git status --untracked-files=all -s | fzf -m | awk "{print \$2}" | xargs git add '
|
alias ga='git status --untracked-files=all -s | fzf -m | while IFS= read -r line; do git add "${line:3}"; done'
|
||||||
alias gx='git status --untracked-files=all -s | fzf -m | awk "{print \$2}" | xargs git checkout '
|
alias gx='git status --untracked-files=all -s | fzf -m | while IFS= read -r line; do git checkout "${line:3}"; done'
|
||||||
alias td='rg "\- \[[^(x|~)]\]"'
|
alias td='rg "\- \[[^(x|~)]\]"'
|
||||||
alias tdp='rg "\- \[[^(x|~)]\].*(🔼|⏫)"'
|
alias tdp='rg "\- \[[^(x|~)]\].*(🔼|⏫)"'
|
||||||
alias tdtp='rg "\- \[[^(x|~)]\].*⏫"'
|
alias tdtp='rg "\- \[[^(x|~)]\].*⏫"'
|
||||||
# alias tdo='rg "\- \[[^x]\]" | fzf | xargs nvim'
|
# alias tdo='rg "\- \[[^x]\]" | fzf | xargs nvim'
|
||||||
alias tdo='rg "\- \[[^(x|~)]\]" --line-number | fzf | awk -F: "{print \$1, \"+\"\$2}" | xargs -r nvim'
|
alias tdo='rg "\- \[[^(x|~)]\]" --line-number | fzf | { IFS=: read -r file line rest && nvim "$file" "+$line"; }'
|
||||||
alias tdop='rg "\- \[[^(x|~)]\].*(🔼|⏫)" --line-number | fzf | awk -F: "{print \$1, \"+\"\$2}" | xargs -r nvim'
|
alias tdop='rg "\- \[[^(x|~)]\].*(🔼|⏫)" --line-number | fzf | { IFS=: read -r file line rest && nvim "$file" "+$line"; }'
|
||||||
|
|
||||||
# time-track markdown todos — see `command td --help`. Log at $TD_LOG (default ./time.csv)
|
# time-track markdown todos — see `command td --help`. Log at $TD_LOG (default ./time.csv)
|
||||||
# `command td` bypasses the `td` alias (which is rg for finding todos).
|
# `command td` bypasses the `td` alias (which is rg for finding todos).
|
||||||
@ -144,6 +144,7 @@ tstart() { command td start "$@"; }
|
|||||||
tstop() { command td stop; }
|
tstop() { command td stop; }
|
||||||
treport() { command td report "$@"; }
|
treport() { command td report "$@"; }
|
||||||
tweek() { command td week "$@"; }
|
tweek() { command td week "$@"; }
|
||||||
|
tweekmd() { command td weekmd "$@"; }
|
||||||
|
|
||||||
alias gr='git reset HEAD'
|
alias gr='git reset HEAD'
|
||||||
alias gc='git commit -v'
|
alias gc='git commit -v'
|
||||||
@ -178,6 +179,50 @@ xnspa() {
|
|||||||
-f "$file" | vd -
|
-f "$file" | vd -
|
||||||
}
|
}
|
||||||
|
|
||||||
|
xndb2() {
|
||||||
|
local file candidates
|
||||||
|
|
||||||
|
candidates=$(
|
||||||
|
lsof +D ~/.local/state/nvim/swap/ 2>/dev/null | grep -v 'lsof:' |
|
||||||
|
grep -o "/swap/.*" |
|
||||||
|
sed 's|^/swap/||' |
|
||||||
|
tr "%" "/" |
|
||||||
|
sed -E 's/\.sw[op]$//' |
|
||||||
|
grep '\.db2\.sql$'
|
||||||
|
)
|
||||||
|
|
||||||
|
if [[ -z "$candidates" ]]; then
|
||||||
|
echo "xndb2: no open .db2.sql nvim buffers found" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
file=$(echo "$candidates" | fzf) || return
|
||||||
|
|
||||||
|
[[ -z "$file" ]] && return
|
||||||
|
|
||||||
|
echo "xndb2: running $file" >&2
|
||||||
|
|
||||||
|
[[ -f ~/.jrqrc ]] && source ~/.jrqrc
|
||||||
|
|
||||||
|
if [[ -z "$JR_URL_DB2" || -z "$JR_USER_DB2" || -z "$JR_PASS_DB2" ]]; then
|
||||||
|
echo "xndb2: DB2 credentials not set in ~/.jrqrc" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local tmp
|
||||||
|
tmp=$(mktemp /tmp/xndb2_XXXXXX.csv)
|
||||||
|
|
||||||
|
/opt/jrunner/bin/jrunner \
|
||||||
|
-scu "$JR_URL_DB2" \
|
||||||
|
-scn "$JR_USER_DB2" \
|
||||||
|
-scp "$JR_PASS_DB2" \
|
||||||
|
-sq "$file" \
|
||||||
|
-f csv > "$tmp"
|
||||||
|
|
||||||
|
vd "$tmp"
|
||||||
|
rm -f "$tmp"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# Add an "alert" alias for long running commands. Use like so:
|
# Add an "alert" alias for long running commands. Use like so:
|
||||||
# sleep 10; alert
|
# sleep 10; alert
|
||||||
|
|||||||
@ -212,6 +212,57 @@ def cmd_week(args):
|
|||||||
print()
|
print()
|
||||||
print(f"Totals: completed {n_done} created {n_new} time {_fmt_dur(total_secs)}")
|
print(f"Totals: completed {n_done} created {n_new} time {_fmt_dur(total_secs)}")
|
||||||
|
|
||||||
|
def _lookup_line(file, tid):
|
||||||
|
if not file or not os.path.exists(file):
|
||||||
|
return ""
|
||||||
|
try:
|
||||||
|
with open(file) as f:
|
||||||
|
for line in f:
|
||||||
|
if f"^{tid}" in line:
|
||||||
|
return line.rstrip()
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def cmd_weekmd(args):
|
||||||
|
since = args.since or _default_since()
|
||||||
|
events = list(_scan_git(since))
|
||||||
|
totals = _tid_totals()
|
||||||
|
if not events:
|
||||||
|
print(f"no task events since {since}"); return
|
||||||
|
by_day = defaultdict(list)
|
||||||
|
for ts, kind, tid, file, desc in events:
|
||||||
|
by_day[ts[:10]].append((ts, kind, tid, file, desc))
|
||||||
|
marker = {"done": "[x]", "new": "[ ]", "done+new": "[+]", "reopen": "[o]"}
|
||||||
|
n_done = n_new = total_secs = 0
|
||||||
|
counted_tids = set()
|
||||||
|
rows = []
|
||||||
|
for day in sorted(by_day):
|
||||||
|
dt = datetime.fromisoformat(day)
|
||||||
|
day_label = dt.strftime("%a %Y-%m-%d")
|
||||||
|
first = True
|
||||||
|
for ts, kind, tid, file, desc in by_day[day]:
|
||||||
|
t = totals.get(tid, 0)
|
||||||
|
raw = _lookup_line(file, tid) or desc
|
||||||
|
line = re.sub(r"^\s*-\s*\[.\]\s*", "", raw)
|
||||||
|
line = re.sub(r"\s*\^\S+.*$", "", line).strip()
|
||||||
|
rows.append((day_label if first else "", marker.get(kind, "?"), tid, _fmt_dur(t) if t else "", file, line))
|
||||||
|
first = False
|
||||||
|
if kind in ("done", "done+new"): n_done += 1
|
||||||
|
if kind in ("new", "done+new"): n_new += 1
|
||||||
|
if tid not in counted_tids:
|
||||||
|
counted_tids.add(tid); total_secs += t
|
||||||
|
print(f"# Week since {since}\n")
|
||||||
|
print("| Day | | TID | Time | File | Task |")
|
||||||
|
print("|-----|---|-----|------|------|------|")
|
||||||
|
for day_label, mk, tid, dur, file, line in rows:
|
||||||
|
dl = day_label.replace("|", "\\|")
|
||||||
|
fn = file.replace("|", "\\|")
|
||||||
|
ln = line.replace("|", "\\|")
|
||||||
|
print(f"| {dl} | {mk} | {tid} | {dur} | {fn} | {ln} |")
|
||||||
|
print()
|
||||||
|
print(f"**Totals:** completed {n_done} · created {n_new} · time {_fmt_dur(total_secs)}")
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
p = argparse.ArgumentParser(prog="td", description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
p = argparse.ArgumentParser(prog="td", description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||||
sp = p.add_subparsers(dest="cmd", required=True)
|
sp = p.add_subparsers(dest="cmd", required=True)
|
||||||
@ -221,6 +272,7 @@ def main():
|
|||||||
sp.add_parser("current").set_defaults(func=cmd_current)
|
sp.add_parser("current").set_defaults(func=cmd_current)
|
||||||
sp.add_parser("tidgen").set_defaults(func=cmd_tidgen)
|
sp.add_parser("tidgen").set_defaults(func=cmd_tidgen)
|
||||||
s = sp.add_parser("week"); s.add_argument("--since", default=None); s.set_defaults(func=cmd_week)
|
s = sp.add_parser("week"); s.add_argument("--since", default=None); s.set_defaults(func=cmd_week)
|
||||||
|
s = sp.add_parser("weekmd"); s.add_argument("--since", default=None); s.set_defaults(func=cmd_weekmd)
|
||||||
args = p.parse_args()
|
args = p.parse_args()
|
||||||
args.func(args)
|
args.func(args)
|
||||||
|
|
||||||
|
|||||||
169
dotfiles/nvim/rename_term.lua
Normal file
169
dotfiles/nvim/rename_term.lua
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
-- rename_term.lua
|
||||||
|
-- Vault-wide find-and-replace with a confirmation popup.
|
||||||
|
-- Usage: require("rename_term").run()
|
||||||
|
-- Keybinding: <leader>rn
|
||||||
|
|
||||||
|
local M = {}
|
||||||
|
|
||||||
|
local function get_vault_root()
|
||||||
|
-- prefer obsidian vault root, fall back to cwd
|
||||||
|
local ok, obs = pcall(require, "obsidian")
|
||||||
|
if ok and obs.get_client then
|
||||||
|
local client = obs.get_client()
|
||||||
|
if client and client.dir then
|
||||||
|
return tostring(client.dir)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return vim.fn.getcwd()
|
||||||
|
end
|
||||||
|
|
||||||
|
local function show_popup(lines, on_confirm)
|
||||||
|
local width = math.min(80, vim.o.columns - 4)
|
||||||
|
local height = math.min(#lines + 4, vim.o.lines - 6)
|
||||||
|
local row = math.floor((vim.o.lines - height) / 2)
|
||||||
|
local col = math.floor((vim.o.columns - width) / 2)
|
||||||
|
|
||||||
|
local buf = vim.api.nvim_create_buf(false, true)
|
||||||
|
vim.api.nvim_buf_set_option(buf, "bufhidden", "wipe")
|
||||||
|
|
||||||
|
local display = { "Affected files — press y to apply, n/q to cancel:", "" }
|
||||||
|
for _, l in ipairs(lines) do
|
||||||
|
table.insert(display, " " .. l)
|
||||||
|
end
|
||||||
|
table.insert(display, "")
|
||||||
|
table.insert(display, string.format(" %d file(s) will be modified.", #lines))
|
||||||
|
|
||||||
|
vim.api.nvim_buf_set_lines(buf, 0, -1, false, display)
|
||||||
|
vim.api.nvim_buf_set_option(buf, "modifiable", false)
|
||||||
|
|
||||||
|
local win = vim.api.nvim_open_win(buf, true, {
|
||||||
|
relative = "editor",
|
||||||
|
width = width,
|
||||||
|
height = height,
|
||||||
|
row = row,
|
||||||
|
col = col,
|
||||||
|
style = "minimal",
|
||||||
|
border = "rounded",
|
||||||
|
})
|
||||||
|
|
||||||
|
local function close()
|
||||||
|
if vim.api.nvim_win_is_valid(win) then
|
||||||
|
vim.api.nvim_win_close(win, true)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local opts = { buffer = buf, nowait = true, silent = true }
|
||||||
|
|
||||||
|
vim.keymap.set("n", "y", function()
|
||||||
|
close()
|
||||||
|
on_confirm()
|
||||||
|
end, opts)
|
||||||
|
|
||||||
|
vim.keymap.set("n", "n", close, opts)
|
||||||
|
vim.keymap.set("n", "q", close, opts)
|
||||||
|
vim.keymap.set("n", "<Esc>", close, opts)
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.run(search, replace)
|
||||||
|
local vault = get_vault_root()
|
||||||
|
|
||||||
|
-- prompt if not passed as args
|
||||||
|
if not search or search == "" then
|
||||||
|
search = vim.fn.input("Search: ")
|
||||||
|
if search == "" then
|
||||||
|
print("rename_term: search term is empty, aborting.")
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if not replace or replace == "" then
|
||||||
|
replace = vim.fn.input("Replace: ")
|
||||||
|
-- allow replacing with empty string intentionally
|
||||||
|
end
|
||||||
|
|
||||||
|
-- find affected files (content)
|
||||||
|
local raw = vim.fn.systemlist(
|
||||||
|
string.format("grep -rl --include='*.md' -F %q %q", search, vault)
|
||||||
|
)
|
||||||
|
|
||||||
|
-- find affected file names
|
||||||
|
local named = vim.fn.systemlist(
|
||||||
|
string.format("find %q -name %q", vault, "*" .. search .. "*")
|
||||||
|
)
|
||||||
|
|
||||||
|
-- deduplicate
|
||||||
|
local seen = {}
|
||||||
|
local content_files = {}
|
||||||
|
for _, f in ipairs(raw) do
|
||||||
|
if not seen[f] then
|
||||||
|
seen[f] = true
|
||||||
|
table.insert(content_files, f)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if #content_files == 0 and #named == 0 then
|
||||||
|
vim.notify('rename_term: no matches for "' .. search .. '"', vim.log.levels.WARN)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
-- build display list
|
||||||
|
local display = {}
|
||||||
|
for _, f in ipairs(content_files) do
|
||||||
|
table.insert(display, vim.fn.fnamemodify(f, ":~:.") .. " [content]")
|
||||||
|
end
|
||||||
|
for _, f in ipairs(named) do
|
||||||
|
table.insert(display, vim.fn.fnamemodify(f, ":~:.") .. " [filename]")
|
||||||
|
end
|
||||||
|
|
||||||
|
show_popup(display, function()
|
||||||
|
local errors = {}
|
||||||
|
|
||||||
|
-- 1. replace content in all matched files
|
||||||
|
for _, f in ipairs(content_files) do
|
||||||
|
-- escape for sed: & / \ need escaping
|
||||||
|
local function sed_escape(s)
|
||||||
|
return s:gsub("([&/\\])", "\\%1")
|
||||||
|
end
|
||||||
|
local cmd = string.format(
|
||||||
|
"sed -i 's/%s/%s/g' %q",
|
||||||
|
sed_escape(search), sed_escape(replace), f
|
||||||
|
)
|
||||||
|
local result = vim.fn.system(cmd)
|
||||||
|
if vim.v.shell_error ~= 0 then
|
||||||
|
table.insert(errors, "content: " .. f .. " — " .. result)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 2. rename files whose names contain the search term
|
||||||
|
for _, f in ipairs(named) do
|
||||||
|
local dir = vim.fn.fnamemodify(f, ":h")
|
||||||
|
local base = vim.fn.fnamemodify(f, ":t")
|
||||||
|
local newbase = base:gsub(vim.pesc(search), replace)
|
||||||
|
local newpath = dir .. "/" .. newbase
|
||||||
|
if newbase ~= base then
|
||||||
|
local ok, err = os.rename(f, newpath)
|
||||||
|
if not ok then
|
||||||
|
table.insert(errors, "rename: " .. f .. " — " .. (err or "unknown"))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- reload any open buffers whose files were touched
|
||||||
|
for _, f in ipairs(content_files) do
|
||||||
|
local bufnr = vim.fn.bufnr(f)
|
||||||
|
if bufnr ~= -1 and vim.api.nvim_buf_is_loaded(bufnr) then
|
||||||
|
vim.api.nvim_buf_call(bufnr, function() vim.cmd("edit!") end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
if #errors > 0 then
|
||||||
|
vim.notify("rename_term errors:\n" .. table.concat(errors, "\n"), vim.log.levels.ERROR)
|
||||||
|
else
|
||||||
|
vim.notify(string.format(
|
||||||
|
'rename_term: replaced "%s" → "%s" in %d file(s), renamed %d file(s).',
|
||||||
|
search, replace, #content_files, #named
|
||||||
|
), vim.log.levels.INFO)
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
return M
|
||||||
38
dotfiles/nvim/telescope_tasks.lua
Normal file
38
dotfiles/nvim/telescope_tasks.lua
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
local telescope = require('telescope.builtin')
|
||||||
|
|
||||||
|
local M = {}
|
||||||
|
|
||||||
|
local function vault_root()
|
||||||
|
local path = vim.fn.expand('%:p:h')
|
||||||
|
local home = vim.fn.expand('~')
|
||||||
|
while path ~= home and path ~= '/' do
|
||||||
|
if vim.fn.filereadable(path .. '/time.csv') == 1
|
||||||
|
or vim.fn.isdirectory(path .. '/.obsidian') == 1 then
|
||||||
|
return path
|
||||||
|
end
|
||||||
|
path = vim.fn.fnamemodify(path, ':h')
|
||||||
|
end
|
||||||
|
return vim.fn.getcwd()
|
||||||
|
end
|
||||||
|
|
||||||
|
-- all open tasks vault-wide (equivalent to tdo shell alias)
|
||||||
|
function M.open_tasks()
|
||||||
|
telescope.grep_string({
|
||||||
|
prompt_title = "Open Tasks",
|
||||||
|
search = "\\- \\[[^x~]\\]",
|
||||||
|
use_regex = true,
|
||||||
|
cwd = vault_root(),
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
-- priority open tasks vault-wide (equivalent to tdop shell alias)
|
||||||
|
function M.priority_tasks()
|
||||||
|
telescope.grep_string({
|
||||||
|
prompt_title = "Priority Tasks",
|
||||||
|
search = "\\- \\[[^x~]\\].*(🔼|⏫)",
|
||||||
|
use_regex = true,
|
||||||
|
cwd = vault_root(),
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
return M
|
||||||
Loading…
Reference in New Issue
Block a user