From ce61e9d0c867a634c8a066619992cc6dabf3ac35 Mon Sep 17 00:00:00 2001 From: Opal Date: Wed, 7 May 2025 16:33:00 -0700 Subject: [PATCH] changed todo, pass_autofill and bash aliases use nmcli --- .bashrc | 4 +- .local/bin/pass_autofill | 6 +- .local/bin/todo | 179 ++++++++++++++++++++++++--------------- 3 files changed, 118 insertions(+), 71 deletions(-) diff --git a/.bashrc b/.bashrc index 86d9a9c..64ebab2 100755 --- a/.bashrc +++ b/.bashrc @@ -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 diff --git a/.local/bin/pass_autofill b/.local/bin/pass_autofill index 78bc17c..eba7f81 100755 --- a/.local/bin/pass_autofill +++ b/.local/bin/pass_autofill @@ -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 diff --git a/.local/bin/todo b/.local/bin/todo index 8c547fa..4a166a6 100755 --- a/.local/bin/todo +++ b/.local/bin/todo @@ -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\d{4}-\d{2}-\d{2})\)\s*)?' # optional done with date - r'(?:\[#(?P[ABC])\]\s*)?' # optional priority - r'\((?P\d{4}-\d{2}-\d{2})\)\s*' # created date - r'(?P.+)$' # description+tags + r'^(?:x \((?P\d{4}-\d{2}-\d{2})\)\s*)?' + r'(?:\[#(?P[ABC])\]\s*)?' + r'\((?P\d{4}-\d{2}-\d{2})\)\s*' + r'(?P.+)$' ) -@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() {