changed todo, pass_autofill and bash aliases use nmcli

This commit is contained in:
2025-05-07 16:33:00 -07:00
parent aa7ea36ff7
commit ce61e9d0c8
3 changed files with 118 additions and 71 deletions

View File

@@ -157,8 +157,8 @@ alias wo='vim ~/sync/workout/workout.txt'
####################################################### #######################################################
# Wireguard # Wireguard
alias wgup='sudo wg-quick up /etc/wireguard/wg0.conf' alias wgup='nmcli connection up wg0'
alias wgdown='sudo wg-quick down /etc/wireguard/wg0.conf' alias wgdown='nmcli connection down wg0'
####################################################### #######################################################
# SPECIAL FUNCTIONS # SPECIAL FUNCTIONS

View File

@@ -16,10 +16,10 @@ if [ -n "$entry" ]; then
# Type username, press Tab, then password, then Enter # Type username, press Tab, then password, then Enter
wtype "$username" wtype "$username"
sleep 0.1 sleep 0.2
wtype -k Tab wtype -k Tab
sleep 0.1 sleep 0.2
wtype "$password" wtype "$password"
sleep 0.1 sleep 0.2
wtype -k Return wtype -k Return
fi fi

View File

@@ -6,19 +6,17 @@ A minimalist CLI todo application with priorities, tags, sorting, JIRA import,
and completion-date tracking. Uses INI config at ~/.config/todo/config.ini. and completion-date tracking. Uses INI config at ~/.config/todo/config.ini.
""" """
import sys
import os import os
import argparse import argparse
import re import re
import configparser import configparser
import urllib.request import urllib.request
import urllib.parse import urllib.parse
import urllib.error
import json import json
from dataclasses import dataclass
from datetime import datetime, date from datetime import datetime, date
from pathlib import Path from pathlib import Path
from typing import List, Optional
from rich.console import Console from rich.console import Console
from rich.table import Table from rich.table import Table
@@ -56,13 +54,11 @@ def load_config():
cfg = configparser.ConfigParser() cfg = configparser.ConfigParser()
cfg.optionxform = str cfg.optionxform = str
config_path = Path.home() / '.config' / 'todo' / 'config.ini' config_path = Path.home() / '.config' / 'todo' / 'config.ini'
# Initialize defaults
for section, opts in DEFAULT_CONFIG.items(): for section, opts in DEFAULT_CONFIG.items():
if not cfg.has_section(section): if not cfg.has_section(section):
cfg[section] = {} cfg[section] = {}
for k, v in opts.items(): for k, v in opts.items():
cfg[section].setdefault(k, str(v)) cfg[section].setdefault(k, str(v))
# Read user overrides
if config_path.exists(): if config_path.exists():
cfg.read(config_path) cfg.read(config_path)
return { return {
@@ -90,75 +86,70 @@ console = Console()
# ------------------------------------------------------------------- # -------------------------------------------------------------------
LINE_RE = re.compile( LINE_RE = re.compile(
r'^(?:x \((?P<completed>\d{4}-\d{2}-\d{2})\)\s*)?' # optional done with date r'^(?:x \((?P<completed>\d{4}-\d{2}-\d{2})\)\s*)?'
r'(?:\[#(?P<priority>[ABC])\]\s*)?' # optional priority r'(?:\[#(?P<priority>[ABC])\]\s*)?'
r'\((?P<created>\d{4}-\d{2}-\d{2})\)\s*' # created date r'\((?P<created>\d{4}-\d{2}-\d{2})\)\s*'
r'(?P<description>.+)$' # description+tags r'(?P<description>.+)$'
) )
@dataclass
class Task: class Task:
completed: Optional[date] def __init__(self, completed, priority, created, description, tags):
priority: str self.completed = completed
created: date self.priority = priority
description: str self.created = created
tags: List[str] self.description = description
self.tags = tags
@property @property
def done(self) -> bool: def done(self):
return self.completed is not None return self.completed is not None
@classmethod @classmethod
def from_line(cls, line: str) -> Optional['Task']: def from_line(cls, line):
m = LINE_RE.match(line) m = LINE_RE.match(line)
if not m: if not m:
return None return None
gd = m.groupdict() gd = m.groupdict()
# Parse dates
completed = None completed = None
if gd.get('completed'): if gd.get('completed'):
completed = datetime.strptime(gd['completed'], DATE_FORMAT).date() completed = datetime.strptime(gd['completed'], DATE_FORMAT).date()
prio = gd['priority'] or DEFAULT_PRIORITY prio = gd.get('priority') or DEFAULT_PRIORITY
created = datetime.strptime(gd['created'], DATE_FORMAT).date() created = datetime.strptime(gd['created'], DATE_FORMAT).date()
parts = gd['description'].split() parts = gd['description'].split()
tags = [w for w in parts if w.startswith('@')] tags = [w for w in parts if w.startswith('@')]
desc = ' '.join(w for w in parts if not w.startswith('@')) desc = ' '.join(w for w in parts if not w.startswith('@'))
return cls(completed=completed, return cls(completed, prio, created, desc, tags)
priority=prio,
created=created,
description=desc,
tags=tags)
def to_line(self) -> str: def to_line(self):
parts = [] parts = []
if self.done: if self.done:
parts.append(f"x ({self.completed.strftime(DATE_FORMAT)}) ") parts.append(f"x ({self.completed.strftime(DATE_FORMAT)}) ")
parts.append(f"[#%s] " % self.priority) parts.append(f"[#{self.priority}] ")
parts.append(f"({self.created.strftime(DATE_FORMAT)}) ") parts.append(f"({self.created.strftime(DATE_FORMAT)}) ")
parts.append(self.description) parts.append(self.description)
if self.tags: if self.tags:
parts.append(' ' + ' '.join(self.tags)) parts.append(' ' + ' '.join(self.tags))
return ''.join(parts) return ''.join(parts)
def mark_done(self) -> 'Task': def mark_done(self):
return Task(completed=date.today(), return Task(date.today(), self.priority, self.created, self.description, self.tags)
priority=self.priority,
created=self.created,
description=self.description,
tags=self.tags)
# ------------------------------------------------------------------- # -------------------------------------------------------------------
# I/O # I/O
# ------------------------------------------------------------------- # -------------------------------------------------------------------
def read_tasks() -> List[Task]: def read_tasks():
if not TODO_FILE_PATH.exists(): if not TODO_FILE_PATH.exists():
return [] return []
lines = TODO_FILE_PATH.read_text().splitlines() lines = TODO_FILE_PATH.read_text().splitlines()
return [t for t in (Task.from_line(l) for l in lines) if t] tasks = []
for l in lines:
t = Task.from_line(l)
if t:
tasks.append(t)
return tasks
def write_tasks(tasks):
def write_tasks(tasks: List[Task]):
TODO_FILE_PATH.parent.mkdir(parents=True, exist_ok=True) TODO_FILE_PATH.parent.mkdir(parents=True, exist_ok=True)
lines = [t.to_line() for t in tasks] lines = [t.to_line() for t in tasks]
TODO_FILE_PATH.write_text("\n".join(lines) + "\n") TODO_FILE_PATH.write_text("\n".join(lines) + "\n")
@@ -167,10 +158,8 @@ def write_tasks(tasks: List[Task]):
# Display & Sorting # Display & Sorting
# ------------------------------------------------------------------- # -------------------------------------------------------------------
def display_tasks(tasks: List[Task], show_all: bool, sort_by: Optional[str]): def display_tasks(tasks, show_all, sort_by):
# filter
display = tasks if show_all else [t for t in tasks if not t.done] display = tasks if show_all else [t for t in tasks if not t.done]
# sort
if sort_by == 'created': if sort_by == 'created':
display.sort(key=lambda t: t.created) display.sort(key=lambda t: t.created)
elif sort_by == 'priority': elif sort_by == 'priority':
@@ -179,11 +168,10 @@ def display_tasks(tasks: List[Task], show_all: bool, sort_by: Optional[str]):
tag = sort_by.split(':',1)[1] tag = sort_by.split(':',1)[1]
display.sort(key=lambda t: tag in t.tags, reverse=True) display.sort(key=lambda t: tag in t.tags, reverse=True)
# columns order cols = ['#','Created','Pri','Description','Tags']
if show_all: if show_all:
cols = ['#','Created','Completed','Pri','Done','Description','Tags'] cols.insert(2, 'Completed')
else: cols.insert(4, 'Done')
cols = ['#','Created','Pri','Description','Tags']
table = Table(title='TODOs (all)' if show_all else 'TODOs') table = Table(title='TODOs (all)' if show_all else 'TODOs')
for c in cols: for c in cols:
@@ -191,14 +179,12 @@ def display_tasks(tasks: List[Task], show_all: bool, sort_by: Optional[str]):
for idx, t in enumerate(display, start=1): for idx, t in enumerate(display, start=1):
pri_lbl = PRIORITY_LABELS.get(t.priority, t.priority) pri_lbl = PRIORITY_LABELS.get(t.priority, t.priority)
pri_cell= Text(pri_lbl, style=PRIORITY_STYLE_MAP.get(t.priority,'')) pri_cell = Text(pri_lbl, style=PRIORITY_STYLE_MAP.get(t.priority,''))
tag_cell= Text() tag_cell = Text()
for tag in t.tags: for tag in t.tags:
tag_cell.append(tag, TAG_COLOR_MAP.get(tag,'')) tag_cell.append(tag, TAG_COLOR_MAP.get(tag,''))
tag_cell.append(' ') tag_cell.append(' ')
# cells row = [str(idx), t.created.strftime(DATE_FORMAT)]
row = [str(idx),
t.created.strftime(DATE_FORMAT)]
if show_all: if show_all:
comp = t.completed.strftime(DATE_FORMAT) if t.completed else '' comp = t.completed.strftime(DATE_FORMAT) if t.completed else ''
row.append(comp) row.append(comp)
@@ -216,10 +202,10 @@ def display_tasks(tasks: List[Task], show_all: bool, sort_by: Optional[str]):
# ------------------------------------------------------------------- # -------------------------------------------------------------------
def cmd_ls(args): def cmd_ls(args):
display_tasks(read_tasks(), show_all=False, sort_by=args.sort) display_tasks(read_tasks(), False, args.sort)
def cmd_lsa(args): def cmd_lsa(args):
display_tasks(read_tasks(), show_all=True, sort_by=args.sort) display_tasks(read_tasks(), True, args.sort)
def cmd_add(args): def cmd_add(args):
if args.text: if args.text:
@@ -231,15 +217,18 @@ def cmd_add(args):
console.print('[yellow]No tag; defaulting to @misc.[/]') console.print('[yellow]No tag; defaulting to @misc.[/]')
entry += ' @misc' entry += ' @misc'
t = Task.from_line(entry) t = Task.from_line(entry)
tasks = read_tasks() + ([t] if t else []) tasks = read_tasks()
write_tasks(tasks) if t:
console.print(f'[green]Added:[/] {t.description if t else entry}') tasks.append(t)
write_tasks(tasks)
console.print(f'[green]Added:[/] {t.description}')
else: else:
desc = Prompt.ask('Task description') desc = Prompt.ask('Task description')
prio = Prompt.ask('Priority (A=High,B=Med,C=Low)', choices=['A','B','C'], default=DEFAULT_PRIORITY) prio = Prompt.ask('Priority (A=High,B=Med,C=Low)', choices=['A','B','C'], default=DEFAULT_PRIORITY)
tag = Prompt.ask('Tag (e.g. @work,@me,@jira)', default='@misc') tag = Prompt.ask('Tag (e.g. @work,@me,@jira)', default='@misc')
t = Task(completed=None, priority=prio, created=date.today(), description=desc, tags=[tag]) t = Task(None, prio, date.today(), desc, [tag])
tasks = read_tasks() + [t] tasks = read_tasks()
tasks.append(t)
write_tasks(tasks) write_tasks(tasks)
console.print(f'[green]Added:[/] {desc}') console.print(f'[green]Added:[/] {desc}')
@@ -263,8 +252,8 @@ def cmd_donei(args):
if not active: if not active:
console.print('[yellow]No active tasks.[/]') console.print('[yellow]No active tasks.[/]')
return return
for disp,i in enumerate(active, start=1): for disp,(i,t) in enumerate(active, start=1):
console.print(f"{disp}: {i[1].description}") console.print(f"{disp}: {t.description}")
choice = IntPrompt.ask('Complete task #') choice = IntPrompt.ask('Complete task #')
cmd_done(argparse.Namespace(number=str(choice))) cmd_done(argparse.Namespace(number=str(choice)))
@@ -277,8 +266,9 @@ def cmd_archive(args):
ARCHIVE_FILE_PATH.parent.mkdir(parents=True, exist_ok=True) ARCHIVE_FILE_PATH.parent.mkdir(parents=True, exist_ok=True)
with ARCHIVE_FILE_PATH.open('a') as f: with ARCHIVE_FILE_PATH.open('a') as f:
for t in done: for t in done:
f.write(t.to_line() + '\n') f.write(t.to_line() + "\n")
write_tasks([t for t in tasks if not t.done]) remaining = [t for t in tasks if not t.done]
write_tasks(remaining)
console.print(f'[green]Archived {len(done)}[/]') console.print(f'[green]Archived {len(done)}[/]')
def cmd_tags(args): def cmd_tags(args):
@@ -286,14 +276,74 @@ def cmd_tags(args):
if not tasks: if not tasks:
console.print(f'[yellow]No active tasks with tag {args.tag}[/]') console.print(f'[yellow]No active tasks with tag {args.tag}[/]')
else: else:
display_tasks(tasks, show_all=False, sort_by=None) display_tasks(tasks, False, None)
def cmd_edit(args): def cmd_edit(args):
editor = os.getenv('EDITOR','nano') editor = os.getenv('EDITOR','nano')
os.execvp(editor, [editor, str(TODO_FILE_PATH)]) os.execvp(editor, [editor, str(TODO_FILE_PATH)])
def cmd_jira(args): def cmd_jira(args):
console.print('[italic]JIRA import not yet implemented[/]') jira_token = os.getenv('JIRA_TOKEN')
if not jira_token:
console.print('[red]Missing JIRA_TOKEN environment variable[/]')
return
base_url = 'https://jira.atg-corp.com'
jql = (
f'project="{args.project}" '
f'AND assignee=currentUser() '
f'AND status in ("Open","In Progress") '
f'ORDER BY created DESC'
)
url = f'{base_url}/rest/api/2/search?jql={urllib.parse.quote(jql)}'
console.print(f'[cyan]Requesting URL:[/] {url}')
req = urllib.request.Request(url)
req.add_header('Authorization', f'Bearer {jira_token}')
req.add_header('Accept', 'application/json')
req.add_header('Content-Type', 'application/json')
try:
with urllib.request.urlopen(req) as resp:
data = json.load(resp)
except urllib.error.HTTPError as e:
body = e.read().decode(errors='ignore')
console.print(f'[red]HTTP Error {e.code}: {e.reason}[/]')
console.print('[yellow]Response body:[/]')
console.print(body)
return
except Exception as e:
console.print(f'[red]Failed to fetch JIRA issues:[/] {e}')
return
issues = data.get('issues', [])
if not issues:
console.print('[yellow]No JIRA issues found.[/]')
return
tasks = read_tasks()
added = 0
for issue in issues:
key = issue['key']
summary = issue['fields']['summary']
date_str = issue['fields']['created'][:10]
desc = f'[{key}] {summary}'
# skip duplicates by key
if any(t.description.startswith(f'[{key}]') for t in tasks):
continue
t = Task(
None,
'B',
datetime.strptime(date_str, DATE_FORMAT).date(),
desc,
['@jira']
)
tasks.append(t)
added += 1
write_tasks(tasks)
console.print(f'[green]Imported {added} JIRA tasks from {args.project} assigned to you.[/]')
# ------------------------------------------------------------------- # -------------------------------------------------------------------
# CLI dispatch # CLI dispatch
@@ -307,7 +357,7 @@ def main():
p_ls.add_argument('--sort', choices=['created','priority','tag:@jira','tag:@work','tag:@me']) p_ls.add_argument('--sort', choices=['created','priority','tag:@jira','tag:@work','tag:@me'])
p_lsa = sub.add_parser('lsa', help='List all tasks') p_lsa = sub.add_parser('lsa', help='List all tasks')
p_lsa.add_argument('--sort', choices=p_ls.get_default('sort') and p_ls.choices or None) p_lsa.add_argument('--sort', choices=['created','priority','tag:@jira','tag:@work','tag:@me'])
sub.add_parser('archive', help='Archive completed tasks') sub.add_parser('archive', help='Archive completed tasks')
@@ -325,10 +375,7 @@ def main():
sub.add_parser('edit', help='Open todo file in editor') sub.add_parser('edit', help='Open todo file in editor')
p_jira = sub.add_parser('jira', help='Import JIRA issues') p_jira = sub.add_parser('jira', help='Import JIRA issues')
p_jira.add_argument('--project', required=True) p_jira.add_argument('--project', default='IS', help='JIRA project key (defaults to IS)')
p_jira.add_argument('--status','-s',action='append',default=['Open','In Progress'])
p_jira.add_argument('--assignee','-a',default='currentUser()')
p_jira.add_argument('--board','-b',type=int)
args = parser.parse_args() args = parser.parse_args()
{ {