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
alias wgup='sudo wg-quick up /etc/wireguard/wg0.conf'
alias wgdown='sudo wg-quick down /etc/wireguard/wg0.conf'
alias wgup='nmcli connection up wg0'
alias wgdown='nmcli connection down wg0'
#######################################################
# SPECIAL FUNCTIONS

View File

@@ -16,10 +16,10 @@ if [ -n "$entry" ]; then
# Type username, press Tab, then password, then Enter
wtype "$username"
sleep 0.1
sleep 0.2
wtype -k Tab
sleep 0.1
sleep 0.2
wtype "$password"
sleep 0.1
sleep 0.2
wtype -k Return
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.
"""
import sys
import os
import argparse
import re
import configparser
import urllib.request
import urllib.parse
import urllib.error
import json
from dataclasses import dataclass
from datetime import datetime, date
from pathlib import Path
from typing import List, Optional
from rich.console import Console
from rich.table import Table
@@ -56,13 +54,11 @@ def load_config():
cfg = configparser.ConfigParser()
cfg.optionxform = str
config_path = Path.home() / '.config' / 'todo' / 'config.ini'
# Initialize defaults
for section, opts in DEFAULT_CONFIG.items():
if not cfg.has_section(section):
cfg[section] = {}
for k, v in opts.items():
cfg[section].setdefault(k, str(v))
# Read user overrides
if config_path.exists():
cfg.read(config_path)
return {
@@ -90,75 +86,70 @@ console = Console()
# -------------------------------------------------------------------
LINE_RE = re.compile(
r'^(?:x \((?P<completed>\d{4}-\d{2}-\d{2})\)\s*)?' # optional done with date
r'(?:\[#(?P<priority>[ABC])\]\s*)?' # optional priority
r'\((?P<created>\d{4}-\d{2}-\d{2})\)\s*' # created date
r'(?P<description>.+)$' # description+tags
r'^(?:x \((?P<completed>\d{4}-\d{2}-\d{2})\)\s*)?'
r'(?:\[#(?P<priority>[ABC])\]\s*)?'
r'\((?P<created>\d{4}-\d{2}-\d{2})\)\s*'
r'(?P<description>.+)$'
)
@dataclass
class Task:
completed: Optional[date]
priority: str
created: date
description: str
tags: List[str]
def __init__(self, completed, priority, created, description, tags):
self.completed = completed
self.priority = priority
self.created = created
self.description = description
self.tags = tags
@property
def done(self) -> bool:
def done(self):
return self.completed is not None
@classmethod
def from_line(cls, line: str) -> Optional['Task']:
def from_line(cls, line):
m = LINE_RE.match(line)
if not m:
return None
gd = m.groupdict()
# Parse dates
completed = None
if gd.get('completed'):
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()
parts = gd['description'].split()
tags = [w for w in parts if w.startswith('@')]
desc = ' '.join(w for w in parts if not w.startswith('@'))
return cls(completed=completed,
priority=prio,
created=created,
description=desc,
tags=tags)
return cls(completed, prio, created, desc, tags)
def to_line(self) -> str:
def to_line(self):
parts = []
if self.done:
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(self.description)
if self.tags:
parts.append(' ' + ' '.join(self.tags))
return ''.join(parts)
def mark_done(self) -> 'Task':
return Task(completed=date.today(),
priority=self.priority,
created=self.created,
description=self.description,
tags=self.tags)
def mark_done(self):
return Task(date.today(), self.priority, self.created, self.description, self.tags)
# -------------------------------------------------------------------
# I/O
# -------------------------------------------------------------------
def read_tasks() -> List[Task]:
def read_tasks():
if not TODO_FILE_PATH.exists():
return []
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: List[Task]):
def write_tasks(tasks):
TODO_FILE_PATH.parent.mkdir(parents=True, exist_ok=True)
lines = [t.to_line() for t in tasks]
TODO_FILE_PATH.write_text("\n".join(lines) + "\n")
@@ -167,10 +158,8 @@ def write_tasks(tasks: List[Task]):
# Display & Sorting
# -------------------------------------------------------------------
def display_tasks(tasks: List[Task], show_all: bool, sort_by: Optional[str]):
# filter
def display_tasks(tasks, show_all, sort_by):
display = tasks if show_all else [t for t in tasks if not t.done]
# sort
if sort_by == 'created':
display.sort(key=lambda t: t.created)
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]
display.sort(key=lambda t: tag in t.tags, reverse=True)
# columns order
cols = ['#','Created','Pri','Description','Tags']
if show_all:
cols = ['#','Created','Completed','Pri','Done','Description','Tags']
else:
cols = ['#','Created','Pri','Description','Tags']
cols.insert(2, 'Completed')
cols.insert(4, 'Done')
table = Table(title='TODOs (all)' if show_all else 'TODOs')
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):
pri_lbl = PRIORITY_LABELS.get(t.priority, t.priority)
pri_cell= Text(pri_lbl, style=PRIORITY_STYLE_MAP.get(t.priority,''))
tag_cell= Text()
pri_cell = Text(pri_lbl, style=PRIORITY_STYLE_MAP.get(t.priority,''))
tag_cell = Text()
for tag in t.tags:
tag_cell.append(tag, TAG_COLOR_MAP.get(tag,''))
tag_cell.append(' ')
# cells
row = [str(idx),
t.created.strftime(DATE_FORMAT)]
row = [str(idx), t.created.strftime(DATE_FORMAT)]
if show_all:
comp = t.completed.strftime(DATE_FORMAT) if t.completed else ''
row.append(comp)
@@ -216,10 +202,10 @@ def display_tasks(tasks: List[Task], show_all: bool, sort_by: Optional[str]):
# -------------------------------------------------------------------
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):
display_tasks(read_tasks(), show_all=True, sort_by=args.sort)
display_tasks(read_tasks(), True, args.sort)
def cmd_add(args):
if args.text:
@@ -231,15 +217,18 @@ def cmd_add(args):
console.print('[yellow]No tag; defaulting to @misc.[/]')
entry += ' @misc'
t = Task.from_line(entry)
tasks = read_tasks() + ([t] if t else [])
write_tasks(tasks)
console.print(f'[green]Added:[/] {t.description if t else entry}')
tasks = read_tasks()
if t:
tasks.append(t)
write_tasks(tasks)
console.print(f'[green]Added:[/] {t.description}')
else:
desc = Prompt.ask('Task description')
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')
t = Task(completed=None, priority=prio, created=date.today(), description=desc, tags=[tag])
tasks = read_tasks() + [t]
t = Task(None, prio, date.today(), desc, [tag])
tasks = read_tasks()
tasks.append(t)
write_tasks(tasks)
console.print(f'[green]Added:[/] {desc}')
@@ -263,8 +252,8 @@ def cmd_donei(args):
if not active:
console.print('[yellow]No active tasks.[/]')
return
for disp,i in enumerate(active, start=1):
console.print(f"{disp}: {i[1].description}")
for disp,(i,t) in enumerate(active, start=1):
console.print(f"{disp}: {t.description}")
choice = IntPrompt.ask('Complete task #')
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)
with ARCHIVE_FILE_PATH.open('a') as f:
for t in done:
f.write(t.to_line() + '\n')
write_tasks([t for t in tasks if not t.done])
f.write(t.to_line() + "\n")
remaining = [t for t in tasks if not t.done]
write_tasks(remaining)
console.print(f'[green]Archived {len(done)}[/]')
def cmd_tags(args):
@@ -286,14 +276,74 @@ def cmd_tags(args):
if not tasks:
console.print(f'[yellow]No active tasks with tag {args.tag}[/]')
else:
display_tasks(tasks, show_all=False, sort_by=None)
display_tasks(tasks, False, None)
def cmd_edit(args):
editor = os.getenv('EDITOR','nano')
os.execvp(editor, [editor, str(TODO_FILE_PATH)])
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
@@ -307,7 +357,7 @@ def main():
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.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')
@@ -325,10 +375,7 @@ def main():
sub.add_parser('edit', help='Open todo file in editor')
p_jira = sub.add_parser('jira', help='Import JIRA issues')
p_jira.add_argument('--project', required=True)
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)
p_jira.add_argument('--project', default='IS', help='JIRA project key (defaults to IS)')
args = parser.parse_args()
{