changed todo, pass_autofill and bash aliases use nmcli
This commit is contained in:
4
.bashrc
4
.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
|
||||
|
||||
@@ -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
|
||||
|
||||
179
.local/bin/todo
179
.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<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()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user