Compare commits
4 commits
a088fcbe76
...
0d2e68a03d
| Author | SHA1 | Date | |
|---|---|---|---|
| 0d2e68a03d | |||
| c164be29d3 | |||
| b20d56a007 | |||
| fccfb85026 |
1 changed files with 128 additions and 86 deletions
212
topen.py
212
topen.py
|
|
@ -12,7 +12,7 @@ You can make use of the open editor and utility functions to find and edit
|
|||
notes, either filling in the required configuration manually or passing around
|
||||
a TConf configuration object containing them all. If choosing the latter, you can
|
||||
read the configuration in part from a `taskrc` file using the utility function
|
||||
`parse_conf()`.
|
||||
`parse_rc()`.
|
||||
|
||||
"""
|
||||
|
||||
|
|
@ -21,7 +21,6 @@ import configparser
|
|||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from collections import namedtuple
|
||||
from dataclasses import asdict, dataclass
|
||||
from pathlib import Path
|
||||
from typing import Any, Self
|
||||
|
|
@ -31,7 +30,7 @@ from tasklib import Task, TaskWarrior
|
|||
NON_EXISTENT_PATH = Path("%%%%I_DONT_EXIST_%%%%")
|
||||
|
||||
|
||||
def main():
|
||||
def main(cfg: "TConf | None" = None):
|
||||
"""Runs the cli interface.
|
||||
|
||||
First sets up the correct options, with overrides in the following order:
|
||||
|
|
@ -44,10 +43,8 @@ def main():
|
|||
|
||||
If the task does not yet have a note annotation it also adds it automatically.
|
||||
"""
|
||||
opts_override = {"task_rc": TConf(0).task_rc} | parse_env() | parse_cli()
|
||||
conf_file = _real_path(opts_override["task_rc"])
|
||||
opts: dict = parse_conf(conf_file) | opts_override
|
||||
cfg = TConf.from_dict(opts)
|
||||
if not cfg:
|
||||
cfg = build_config()
|
||||
|
||||
if not cfg.task_id:
|
||||
_ = sys.stderr.write("Please provide task ID as argument.\n")
|
||||
|
|
@ -112,6 +109,83 @@ def add_annotation_if_missing(task: Task, annotation_content: str) -> None:
|
|||
_ = whisper(f"Added annotation: {annotation_content}")
|
||||
|
||||
|
||||
@dataclass()
|
||||
class Opt:
|
||||
"""Assembled metadata for a single configuration option."""
|
||||
|
||||
cli: tuple[str, ...] | None
|
||||
env: str | None
|
||||
rc: str | None
|
||||
default: Any = None
|
||||
metavar: str | None = None
|
||||
cast: type = str
|
||||
help_text: str = ""
|
||||
|
||||
|
||||
OPTIONS: dict[str, Opt] = {
|
||||
"task_id": Opt(None, None, None, default=None),
|
||||
"task_rc": Opt(
|
||||
("--task-rc",),
|
||||
"TASKRC",
|
||||
None, # taskrc has no key for this
|
||||
default=Path("~/.taskrc"),
|
||||
metavar="FILE",
|
||||
cast=Path,
|
||||
help_text="Location of taskwarrior config file",
|
||||
),
|
||||
"task_data": Opt(
|
||||
("--task-data",),
|
||||
"TASKDATA",
|
||||
"data.location",
|
||||
default=Path("~/.task"),
|
||||
metavar="DIR",
|
||||
cast=Path,
|
||||
help_text="Location of taskwarrior data directory",
|
||||
),
|
||||
"notes_dir": Opt(
|
||||
("-d", "--notes-dir"),
|
||||
"TOPEN_NOTES_DIR",
|
||||
"notes.dir",
|
||||
default=None, # resolved later in TConf.__post_init__
|
||||
metavar="DIR",
|
||||
cast=Path,
|
||||
help_text="Location of topen notes files",
|
||||
),
|
||||
"notes_ext": Opt(
|
||||
("--extension",),
|
||||
"TOPEN_NOTES_EXT",
|
||||
"notes.ext",
|
||||
default="md",
|
||||
metavar="EXT",
|
||||
help_text="Extension of note files",
|
||||
),
|
||||
"notes_annot": Opt(
|
||||
("--annotation",),
|
||||
"TOPEN_NOTES_ANNOT",
|
||||
"notes.annot",
|
||||
default="Note",
|
||||
metavar="NOTE",
|
||||
help_text="Annotation content to set within taskwarrior",
|
||||
),
|
||||
"notes_editor": Opt(
|
||||
("--editor",),
|
||||
"TOPEN_NOTES_EDITOR",
|
||||
"notes.editor",
|
||||
default=os.getenv("EDITOR") or os.getenv("VISUAL") or "nano",
|
||||
metavar="CMD",
|
||||
help_text="Program to open note files with",
|
||||
),
|
||||
"notes_quiet": Opt(
|
||||
("--quiet",),
|
||||
"TOPEN_NOTES_QUIET",
|
||||
"notes.quiet",
|
||||
default=False,
|
||||
cast=bool,
|
||||
help_text="Silence any verbose displayed information",
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@dataclass()
|
||||
class TConf:
|
||||
"""Topen Configuration
|
||||
|
|
@ -171,6 +245,23 @@ class TConf:
|
|||
return cls(**d)
|
||||
|
||||
|
||||
def build_config() -> TConf:
|
||||
"""Return final configuration object."""
|
||||
cli = parse_cli()
|
||||
env = parse_env()
|
||||
rc_path = Path(
|
||||
cli.get("task_rc") or env.get("task_rc") or TConf(0).task_rc
|
||||
).expanduser()
|
||||
rc = parse_rc(rc_path) if rc_path.exists() else {}
|
||||
|
||||
# we use the 'parsed' XDG-included taskrc locations for defaults
|
||||
defaults = {k: opt.default for k, opt in OPTIONS.items()}
|
||||
defaults["task_rc"] = rc_path
|
||||
|
||||
merged = defaults | rc | env | cli # later wins
|
||||
return TConf.from_dict({k: v for k, v in merged.items() if v is not None})
|
||||
|
||||
|
||||
def parse_cli() -> dict:
|
||||
"""Parse cli options and arguments.
|
||||
|
||||
|
|
@ -189,93 +280,50 @@ you view the task.
|
|||
_ = parser.add_argument(
|
||||
"id", help="The id/uuid of the taskwarrior task for which we edit notes"
|
||||
)
|
||||
_ = parser.add_argument(
|
||||
"-d",
|
||||
"--notes-dir",
|
||||
metavar="DIR",
|
||||
help="Location of topen notes files",
|
||||
)
|
||||
_ = parser.add_argument(
|
||||
"--quiet",
|
||||
action="store_true",
|
||||
help="Silence any verbose displayed information",
|
||||
)
|
||||
_ = parser.add_argument(
|
||||
"--extension", metavar="EXT", help="Extension of note files"
|
||||
)
|
||||
_ = parser.add_argument(
|
||||
"--annotation",
|
||||
metavar="NOTE",
|
||||
help="Annotation content to set within taskwarrior",
|
||||
)
|
||||
_ = parser.add_argument(
|
||||
"--editor", metavar="CMD", help="Program to open note files with"
|
||||
)
|
||||
_ = parser.add_argument(
|
||||
"--task-rc", metavar="FILE", help="Location of taskwarrior config file"
|
||||
)
|
||||
_ = parser.add_argument(
|
||||
"--task-data", metavar="DIR", help="Location of taskwarrior data directory"
|
||||
)
|
||||
|
||||
p = parser.parse_args()
|
||||
return _filtered_dict(
|
||||
{
|
||||
"task_id": p.id,
|
||||
"task_rc": p.task_rc,
|
||||
"task_data": p.task_data,
|
||||
"notes_dir": p.notes_dir,
|
||||
"notes_ext": p.extension,
|
||||
"notes_annot": p.annotation,
|
||||
"notes_editor": p.editor,
|
||||
"notes_quiet": p.quiet,
|
||||
}
|
||||
for key, opt in OPTIONS.items():
|
||||
if opt.cli is None:
|
||||
continue
|
||||
parser.add_argument(
|
||||
*opt.cli,
|
||||
dest=key,
|
||||
metavar=opt.metavar,
|
||||
help=opt.help_text,
|
||||
default=None,
|
||||
)
|
||||
args = parser.parse_args()
|
||||
cli_vals = {k: v for k, v in vars(args).items() if v is not None}
|
||||
cli_vals["task_id"] = cli_vals.pop("id")
|
||||
return cli_vals
|
||||
|
||||
|
||||
def parse_env() -> dict:
|
||||
def parse_env() -> dict[str, Any]:
|
||||
"""Parse environment variable options.
|
||||
|
||||
Returns them as a simple dict object.
|
||||
"""
|
||||
return _filtered_dict(
|
||||
{
|
||||
"task_rc": os.getenv("TASKRC"),
|
||||
"task_data": os.getenv("TASKDATA"),
|
||||
"notes_dir": os.getenv("TOPEN_NOTES_DIR"),
|
||||
"notes_ext": os.getenv("TOPEN_NOTES_EXT"),
|
||||
"notes_annot": os.getenv("TOPEN_NOTES_ANNOT"),
|
||||
"notes_editor": os.getenv("TOPEN_NOTES_EDITOR"),
|
||||
"notes_quiet": os.getenv("TOPEN_NOTES_QUIET"),
|
||||
}
|
||||
)
|
||||
out: dict[str, Any] = {}
|
||||
for key, opt in OPTIONS.items():
|
||||
if opt.env and (val := os.getenv(opt.env)) is not None:
|
||||
out[key] = opt.cast(val)
|
||||
return out
|
||||
|
||||
|
||||
def parse_conf(conf_file: Path) -> dict:
|
||||
def parse_rc(rc_path: Path) -> dict:
|
||||
"""Parse taskrc configuration file options.
|
||||
|
||||
Returns them as a simple dict object.
|
||||
Uses dot.annotation for options just like taskwarrior settings.
|
||||
"""
|
||||
c = configparser.ConfigParser(allow_unnamed_section=True, allow_no_value=True)
|
||||
with open(conf_file.expanduser()) as f:
|
||||
c.read_string("[GENERAL]\n" + f.read())
|
||||
cfg = configparser.ConfigParser(allow_unnamed_section=True, allow_no_value=True)
|
||||
with rc_path.expanduser().open() as fr:
|
||||
cfg.read_string("[GENERAL]\n" + fr.read())
|
||||
|
||||
ConfTrans = namedtuple("ParsedToTConf", ["name", "tconf_name"])
|
||||
return _filtered_dict(
|
||||
{
|
||||
opt.tconf_name: c.get("GENERAL", opt.name)
|
||||
for opt in [
|
||||
ConfTrans("data.location", "task_data"),
|
||||
ConfTrans("notes.dir", "notes_dir"),
|
||||
ConfTrans("notes.ext", "notes_ext"),
|
||||
ConfTrans("notes.annot", "notes_annot"),
|
||||
ConfTrans("notes.editor", "notes_editor"),
|
||||
ConfTrans("notes.quiet", "notes_quiet"),
|
||||
]
|
||||
if c.has_option("GENERAL", opt.name)
|
||||
}
|
||||
)
|
||||
out: dict[str, Any] = {}
|
||||
for key, opt in OPTIONS.items():
|
||||
if opt.rc and cfg.has_option("GENERAL", opt.rc):
|
||||
raw = cfg.get("GENERAL", opt.rc)
|
||||
out[key] = opt.cast(raw)
|
||||
return out
|
||||
|
||||
|
||||
IS_QUIET = False
|
||||
|
|
@ -290,11 +338,5 @@ def _real_path(p: Path | str) -> Path:
|
|||
return Path(os.path.expandvars(p)).expanduser()
|
||||
|
||||
|
||||
# A None-filtered dict which only contains
|
||||
# keys which have a value.
|
||||
def _filtered_dict(d: dict) -> dict:
|
||||
return {k: v for (k, v) in d.items() if v}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue