add td time-tracking script and deploy_bin installer
`td` is a Python CLI for tracking time on markdown todos identified by ^tid block-refs, writing to a CSV at $TD_LOG (default ./time.csv). Shell wrappers tstart/tstop/treport in .bashrc wrap it. setup_env.sh gains deploy_bin() which symlinks every file in dotfiles/bin/ into ~/.local/bin/, mirroring how deploy_configs handles dotfiles. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
0c1f15e31a
commit
f71458a34f
@ -102,21 +102,21 @@ alias opg="lsof 2>/dev/null +D . | grep 'pg.*swp$' | awk '{print \$9}' | sed 's/
|
|||||||
alias osw="lsof 2>/dev/null +D . | awk '\$NF ~ /swp$/ {print \$9}' | sed 's/\.swp//g' | sed 's/\/\./\//g'"
|
alias osw="lsof 2>/dev/null +D . | awk '\$NF ~ /swp$/ {print \$9}' | sed 's/\.swp//g' | sed 's/\/\./\//g'"
|
||||||
alias xpg="lsof 2>/dev/null +D . | grep 'pg.*swp$' | awk '{print \$9}' | sed 's/\.swp//g' | sed 's/\/\./\//g' | xargs -r $PG -f"
|
alias xpg="lsof 2>/dev/null +D . | grep 'pg.*swp$' | awk '{print \$9}' | sed 's/\.swp//g' | sed 's/\/\./\//g' | xargs -r $PG -f"
|
||||||
alias xsw="lsof 2>/dev/null +D . | grep '.*swp$' | awk '{print \$9}' | sed 's/\.swp//g' | sed 's/\/\./\//g' | xargs -r $PG -f"
|
alias xsw="lsof 2>/dev/null +D . | grep '.*swp$' | awk '{print \$9}' | sed 's/\.swp//g' | sed 's/\/\./\//g' | xargs -r $PG -f"
|
||||||
alias ons='lsof +D ~/.local/state/nvim/swap/ | grep -o "/swap/.*" | cut -c 7- | tr "%" "/" | sed "s/\\.swp$//" | grep "$(pwd)"'
|
alias ons='lsof +D ~/.local/state/nvim/swap/ 2>/dev/null | grep -v "lsof:" | grep -o "/swap/.*" | cut -c 7- | tr "%" "/" | sed "s/\\.swp$//" | grep "$(pwd)"'
|
||||||
alias xns='lsof +D ~/.local/state/nvim/swap/ | grep -o "/swap/.*" | cut -c 7- | tr "%" "/" | sed "s/\\.swp$//" | sed "s/\\.swo$//" | grep "$(pwd | sed "s|^//|/|")" | fzf | xargs -I % $PG -f %'
|
alias xns='lsof +D ~/.local/state/nvim/swap/ 2>/dev/null | grep -v "lsof:" | grep -o "/swap/.*" | cut -c 7- | tr "%" "/" | sed "s/\\.swp$//" | sed "s/\\.swo$//" | grep "$(pwd | sed "s|^//|/|")" | fzf | xargs -I % $PG -f %'
|
||||||
alias xnsp='lsof +D ~/.local/state/nvim/swap/ | grep -o "/swap/.*" | cut -c 7- | tr "%" "/" | sed "s/\\.swp$//" | sed "s/\\.swo$//" | grep "$(pwd | sed "s|^//|/|")" | fzf | xargs -I % $PG -f % | pspg'
|
alias xnsp='lsof +D ~/.local/state/nvim/swap/ 2>/dev/null | grep -v "lsof:" | grep -o "/swap/.*" | cut -c 7- | tr "%" "/" | sed "s/\\.swp$//" | sed "s/\\.swo$//" | grep "$(pwd | sed "s|^//|/|")" | fzf | xargs -I % $PG -f % | pspg'
|
||||||
# alias xnspa='lsof +D ~/.local/state/nvim/swap/ | grep -o "/swap/.*" | cut -c 7- | tr "%" "/" | sed "s/\\.swp$//" | sed "s/\\.swo$//" | grep "$(pwd | sed "s|^//|/|")" | fzf | xargs -I % $PG -f % --csv | vd - > /dev/null 2>&1'
|
# alias xnspa='lsof +D ~/.local/state/nvim/swap/ | grep -o "/swap/.*" | cut -c 7- | tr "%" "/" | sed "s/\\.swp$//" | sed "s/\\.swo$//" | grep "$(pwd | sed "s|^//|/|")" | fzf | xargs -I % $PG -f % --csv | vd - > /dev/null 2>&1'
|
||||||
#alias xnsp='lsof +D ~/.local/state/nvim/swap/ | grep -o "/swap/.*" | cut -c 7- | tr "%" "/" | sed "s/\\.swp$//" | grep "$(pwd)" | fzf | xargs -I % $PG -f % | pspg'
|
#alias xnsp='lsof +D ~/.local/state/nvim/swap/ | grep -o "/swap/.*" | cut -c 7- | tr "%" "/" | sed "s/\\.swp$//" | grep "$(pwd)" | fzf | xargs -I % $PG -f % | pspg'
|
||||||
alias mns='fzf | xargs -I {} sqlcmd -U Pricing -S mid-sql02 -C -i {} | pspg'
|
alias mns='fzf | xargs -I {} sqlcmd -U Pricing -S mid-sql02 -C -i {} | pspg'
|
||||||
alias xmsp='lsof +D ~/.local/state/nvim/swap/ | grep -o "/swap/.*" | cut -c 7- | tr "%" "/" | sed "s/\\.swp$//" | grep "$(pwd | sed "s|^//|/|")" | fzf | xargs -I % $MS -i % | pspg'
|
alias xmsp='lsof +D ~/.local/state/nvim/swap/ 2>/dev/null | grep -v "lsof:" | grep -o "/swap/.*" | cut -c 7- | tr "%" "/" | sed "s/\\.swp$//" | grep "$(pwd | sed "s|^//|/|")" | fzf | xargs -I % $MS -i % | pspg'
|
||||||
alias xms='lsof +D ~/.local/state/nvim/swap/ | grep -o "/swap/.*" | cut -c 7- | tr "%" "/" | sed "s/\\.swp$//" | grep "$(pwd | sed "s|^//|/|")" | fzf | xargs -I % $MSW -i % '
|
alias xms='lsof +D ~/.local/state/nvim/swap/ 2>/dev/null | grep -v "lsof:" | grep -o "/swap/.*" | cut -c 7- | tr "%" "/" | sed "s/\\.swp$//" | grep "$(pwd | sed "s|^//|/|")" | fzf | xargs -I % $MSW -i % '
|
||||||
# alias xmspa='lsof +D ~/.local/state/nvim/swap/ | grep -o "/swap/.*" | cut -c 7- | tr "%" "/" | sed "s/\\.swp$//" | grep "$(pwd | sed "s|^//|/|")" | fzf | xargs -I % $MSC -i % | vd -f csv -'
|
# alias xmspa='lsof +D ~/.local/state/nvim/swap/ | grep -o "/swap/.*" | cut -c 7- | tr "%" "/" | sed "s/\\.swp$//" | grep "$(pwd | sed "s|^//|/|")" | fzf | xargs -I % $MSC -i % | vd -f csv -'
|
||||||
# alias xmspa='selected_file=$(lsof +D ~/.local/state/nvim/swap/ | grep -o "/swap/.*" | cut -c 7- | tr "%" "/" | sed "s/\\.swp$//" | grep "$(pwd)" | fzf) && [ -n "$selected_file" ] && $MSC -i "$selected_file" | vd -f csv -'
|
# alias xmspa='selected_file=$(lsof +D ~/.local/state/nvim/swap/ | grep -o "/swap/.*" | cut -c 7- | tr "%" "/" | sed "s/\\.swp$//" | grep "$(pwd)" | fzf) && [ -n "$selected_file" ] && $MSC -i "$selected_file" | vd -f csv -'
|
||||||
xmspa() {
|
xmspa() {
|
||||||
local file
|
local file
|
||||||
|
|
||||||
file=$(
|
file=$(
|
||||||
lsof +D ~/.local/state/nvim/swap/ 2>/dev/null |
|
lsof +D ~/.local/state/nvim/swap/ 2>/dev/null | grep -v 'lsof:' |
|
||||||
grep -o "/swap/.*" |
|
grep -o "/swap/.*" |
|
||||||
sed 's|^/swap/||' |
|
sed 's|^/swap/||' |
|
||||||
tr "%" "/" |
|
tr "%" "/" |
|
||||||
@ -143,6 +143,13 @@ 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 | awk -F: "{print \$1, \"+\"\$2}" | xargs -r nvim'
|
||||||
alias tdop='rg "\- \[[^(x|~)]\].*(🔼|⏫)" --line-number | fzf | awk -F: "{print \$1, \"+\"\$2}" | xargs -r nvim'
|
alias tdop='rg "\- \[[^(x|~)]\].*(🔼|⏫)" --line-number | fzf | awk -F: "{print \$1, \"+\"\$2}" | xargs -r nvim'
|
||||||
|
|
||||||
|
# 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).
|
||||||
|
tstart() { command td start "$@"; }
|
||||||
|
tstop() { command td stop; }
|
||||||
|
treport() { command td report "$@"; }
|
||||||
|
|
||||||
alias gr='git reset HEAD'
|
alias gr='git reset HEAD'
|
||||||
alias gc='git commit -v'
|
alias gc='git commit -v'
|
||||||
alias gd='git difftool'
|
alias gd='git difftool'
|
||||||
@ -155,7 +162,7 @@ xnspa() {
|
|||||||
local file
|
local file
|
||||||
|
|
||||||
file=$(
|
file=$(
|
||||||
lsof +D ~/.local/state/nvim/swap/ 2>/dev/null |
|
lsof +D ~/.local/state/nvim/swap/ 2>/dev/null | grep -v 'lsof:' |
|
||||||
grep -o "/swap/.*" |
|
grep -o "/swap/.*" |
|
||||||
sed 's|^/swap/||' |
|
sed 's|^/swap/||' |
|
||||||
tr "%" "/" |
|
tr "%" "/" |
|
||||||
@ -218,3 +225,6 @@ bind 'set bell-style none'
|
|||||||
[ -f ~/setup_env/dotfiles/.bashrc_local ] && source ~/setup_env/dotfiles/.bashrc_local
|
[ -f ~/setup_env/dotfiles/.bashrc_local ] && source ~/setup_env/dotfiles/.bashrc_local
|
||||||
export PATH=$PATH:~/lua-language-server/bin
|
export PATH=$PATH:~/lua-language-server/bin
|
||||||
[ -f "$HOME/.cargo/env" ] && . "$HOME/.cargo/env"
|
[ -f "$HOME/.cargo/env" ] && . "$HOME/.cargo/env"
|
||||||
|
|
||||||
|
# opencode
|
||||||
|
export PATH=/home/ptrowbridge/.opencode/bin:$PATH
|
||||||
|
|||||||
128
dotfiles/bin/td
Executable file
128
dotfiles/bin/td
Executable file
@ -0,0 +1,128 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""td — time-track markdown todos identified by ^tid block-refs.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
td start <tid> [--file PATH] [--desc TEXT]
|
||||||
|
td stop
|
||||||
|
td report [FILTER] # FILTER matches tid or file substring
|
||||||
|
td current # print the currently-running tid (or nothing)
|
||||||
|
td tidgen # print a new unique tid
|
||||||
|
|
||||||
|
Log location: $TD_LOG (default ./time.csv)
|
||||||
|
"""
|
||||||
|
import argparse, csv, os, re, subprocess, sys
|
||||||
|
from datetime import datetime
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
LOG = os.environ.get("TD_LOG", "./time.csv")
|
||||||
|
FIELDS = ["started_at", "stopped_at", "tid", "file", "description"]
|
||||||
|
|
||||||
|
def now():
|
||||||
|
return datetime.now().isoformat(timespec="seconds")
|
||||||
|
|
||||||
|
def read_rows():
|
||||||
|
if not os.path.exists(LOG):
|
||||||
|
return []
|
||||||
|
with open(LOG, newline="") as f:
|
||||||
|
return list(csv.DictReader(f))
|
||||||
|
|
||||||
|
def write_rows(rows):
|
||||||
|
with open(LOG, "w", newline="") as f:
|
||||||
|
w = csv.DictWriter(f, fieldnames=FIELDS)
|
||||||
|
w.writeheader()
|
||||||
|
w.writerows(rows)
|
||||||
|
|
||||||
|
def lookup_task(tid):
|
||||||
|
try:
|
||||||
|
out = subprocess.check_output(
|
||||||
|
["rg", "--no-heading", "--with-filename", rf"\^{tid}\b", "."],
|
||||||
|
text=True, stderr=subprocess.DEVNULL,
|
||||||
|
)
|
||||||
|
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||||
|
return "", ""
|
||||||
|
first = out.splitlines()[0] if out else ""
|
||||||
|
if not first:
|
||||||
|
return "", ""
|
||||||
|
path, _, line = first.partition(":")
|
||||||
|
file = path.removeprefix("./")
|
||||||
|
desc = re.sub(r"^\s*-\s*\[.\]\s*", "", line)
|
||||||
|
desc = re.sub(r"\s*\^\S+\s*$", "", desc).strip()
|
||||||
|
return file, desc
|
||||||
|
|
||||||
|
def cmd_start(args):
|
||||||
|
rows = read_rows()
|
||||||
|
ts = now()
|
||||||
|
stopped = [r["tid"] for r in rows if not r["stopped_at"]]
|
||||||
|
for r in rows:
|
||||||
|
if not r["stopped_at"]:
|
||||||
|
r["stopped_at"] = ts
|
||||||
|
file, desc = args.file or "", args.desc or ""
|
||||||
|
if not (file or desc):
|
||||||
|
file, desc = lookup_task(args.tid)
|
||||||
|
if not file:
|
||||||
|
print(f"warning: ^{args.tid} not found in {os.getcwd()} — logging with empty file/desc", file=sys.stderr)
|
||||||
|
rows.append({"started_at": ts, "stopped_at": "", "tid": args.tid, "file": file, "description": desc})
|
||||||
|
write_rows(rows)
|
||||||
|
if stopped:
|
||||||
|
print(f" stopped {', '.join(stopped)} first")
|
||||||
|
snippet = f" ({file}: {desc[:50]})" if desc else ""
|
||||||
|
print(f"started {args.tid}{snippet}")
|
||||||
|
|
||||||
|
def cmd_stop(args):
|
||||||
|
rows = read_rows()
|
||||||
|
if not rows:
|
||||||
|
print(f"no log at {LOG}"); sys.exit(1)
|
||||||
|
ts = now()
|
||||||
|
stopped = []
|
||||||
|
for r in rows:
|
||||||
|
if not r["stopped_at"]:
|
||||||
|
r["stopped_at"] = ts
|
||||||
|
stopped.append(r["tid"])
|
||||||
|
if not stopped:
|
||||||
|
print("nothing running"); sys.exit(1)
|
||||||
|
write_rows(rows)
|
||||||
|
print(f"stopped {', '.join(stopped)}")
|
||||||
|
|
||||||
|
def cmd_report(args):
|
||||||
|
rows = read_rows()
|
||||||
|
if not rows:
|
||||||
|
print(f"no log at {LOG}"); sys.exit(1)
|
||||||
|
totals, files, running = defaultdict(int), {}, []
|
||||||
|
for r in rows:
|
||||||
|
if args.filter and args.filter not in r["tid"] and args.filter not in r["file"]:
|
||||||
|
continue
|
||||||
|
start = datetime.fromisoformat(r["started_at"])
|
||||||
|
files[r["tid"]] = r["file"]
|
||||||
|
if r["stopped_at"]:
|
||||||
|
stop = datetime.fromisoformat(r["stopped_at"])
|
||||||
|
totals[r["tid"]] += int((stop - start).total_seconds())
|
||||||
|
else:
|
||||||
|
running.append((r["tid"], start, r["file"], r["description"]))
|
||||||
|
for tid in sorted(totals):
|
||||||
|
t = totals[tid]
|
||||||
|
h, m = t // 3600, (t % 3600) // 60
|
||||||
|
print(f"{tid:<24} {h:>3}h{m:02d}m {files.get(tid,'')}")
|
||||||
|
for tid, start, file, desc in running:
|
||||||
|
print(f"{tid:<24} RUNNING since {start.strftime('%H:%M')} {file}: {desc[:40]}")
|
||||||
|
|
||||||
|
def cmd_current(args):
|
||||||
|
for r in read_rows():
|
||||||
|
if not r["stopped_at"]:
|
||||||
|
print(r["tid"]); return
|
||||||
|
|
||||||
|
def cmd_tidgen(args):
|
||||||
|
print(f"tid-{datetime.now().strftime('%Y%m%d-%H%M%S')}")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
p = argparse.ArgumentParser(prog="td", description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||||
|
sp = p.add_subparsers(dest="cmd", required=True)
|
||||||
|
s = sp.add_parser("start"); s.add_argument("tid"); s.add_argument("--file", default=""); s.add_argument("--desc", default=""); s.set_defaults(func=cmd_start)
|
||||||
|
sp.add_parser("stop").set_defaults(func=cmd_stop)
|
||||||
|
s = sp.add_parser("report"); s.add_argument("filter", nargs="?"); s.set_defaults(func=cmd_report)
|
||||||
|
sp.add_parser("current").set_defaults(func=cmd_current)
|
||||||
|
sp.add_parser("tidgen").set_defaults(func=cmd_tidgen)
|
||||||
|
args = p.parse_args()
|
||||||
|
args.func(args)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
13
setup_env.sh
13
setup_env.sh
@ -65,6 +65,18 @@ deploy_configs() {
|
|||||||
source ~/.bashrc
|
source ~/.bashrc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Deploy executable scripts from dotfiles/bin/ into ~/.local/bin/
|
||||||
|
deploy_bin() {
|
||||||
|
echo "Deploying scripts to ~/.local/bin/ ..."
|
||||||
|
local src_dir="$(pwd)/dotfiles/bin"
|
||||||
|
[[ -d "$src_dir" ]] || return 0
|
||||||
|
mkdir -p ~/.local/bin
|
||||||
|
for script in "$src_dir"/*; do
|
||||||
|
[[ -f "$script" ]] || continue
|
||||||
|
create_symlink "$script" ~/.local/bin/"$(basename "$script")"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
# Main script
|
# Main script
|
||||||
main() {
|
main() {
|
||||||
install_packages
|
install_packages
|
||||||
@ -72,6 +84,7 @@ main() {
|
|||||||
install_vundle
|
install_vundle
|
||||||
install_git_bash_prompt
|
install_git_bash_prompt
|
||||||
deploy_configs
|
deploy_configs
|
||||||
|
deploy_bin
|
||||||
echo "Setup complete! Please restart your shell or run 'source ~/.bashrc' for changes to take effect."
|
echo "Setup complete! Please restart your shell or run 'source ~/.bashrc' for changes to take effect."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user