Skip to content

Commit

Permalink
use click for args, allow custom extensions
Browse files Browse the repository at this point in the history
  • Loading branch information
purarue committed Oct 11, 2023
1 parent 80dafab commit 4586416
Show file tree
Hide file tree
Showing 7 changed files with 138 additions and 93 deletions.
72 changes: 54 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
## calcurse-load

Personal hooks/scripts for calcurse. This integrates [`calcurse`](https://github.com/lfos/calcurse) with Google Calendar, and [`todo.txt`](http://todotxt.org/).
Hooks/scripts for loading data data for calcurse. This integrates [`calcurse`](https://github.com/lfos/calcurse) with Google Calendar, and [`todo.txt`](http://todotxt.org/).

- pre-load:
- Looks at the locally indexed Google Calendar JSON dump, adds events as `calcurse` appointments; adds summary/HTML links as appointment notes.
Expand Down Expand Up @@ -35,7 +33,11 @@ alias calcurse='calcurse --datadir "$CALCURSE_DIR" --confdir ~/.config/calcurse

In addition to that, this maintains a data directory in `$XDG_DATA_HOME/calcurse_load`, where it stores data for `gcal_index`.

---
## About

If you wanted to disable one of the `todotxt` or `gcal` extensions, you could remove or rename the corresponding scripts in the `hooks` directory.

## gcal pre-load

The `gcal` calcurse hook tries to read any `gcal_index`-created JSON files in the `$XDG_DATA_HOME/calcurse_load/gcal/` directory. If there's description/extra information for events from Google Calendar, this attaches corresponding notes to each calcurse event. Specifically, it:

Expand All @@ -44,19 +46,7 @@ The `gcal` calcurse hook tries to read any `gcal_index`-created JSON files in th
- Generates Google Calendar events from the JSON
- Adds the newly created events and writes back to the appointments file.

---

The `post-save` `todotxt` hook converts the `calcurse` todos back to `todotxt` todos, and updates the `todotxt` file if any todos were added. A `todo.txt` is searched for in one of the common locations:

- `$TODOTXT_FILE`
- `$TODO_DIR/todo.txt`
- `$XDG_CONFIG/todo/todo.txt`
- `~/.config/todo/todo.txt`
- `~/.todo/todo.txt`

If you wanted to disable one of the `todotxt` or `gcal` extension, you could remove or rename the corresponding scripts in the `hooks` directory.

### Google Calendar Update Process
### gcal update example

`gcal_index` saves an index of Google Calendar events for a Google Account locally as a JSON file.

Expand Down Expand Up @@ -88,6 +78,16 @@ Prints the JSON dump to STDOUT; example:

For an example script one might put under cron, see [`example_update_google_cal`](./example_update_google_cal)

## todotxt

The `pre-load`/`post-save` `todotxt` hook converts the `calcurse` todos back to `todotxt` todos, and updates the `todotxt` file if any todos were added. A `todo.txt` is searched for in one of the common locations:

- `$TODOTXT_FILE`
- `$TODO_DIR/todo.txt`
- `$XDG_CONFIG/todo/todo.txt`
- `~/.config/todo/todo.txt`
- `~/.todo/todo.txt`

### Todo.txt Priority Conversion

| Todo.txt | Calcurse |
Expand Down Expand Up @@ -116,4 +116,40 @@ required arguments:
--post-save Execute the postsave action for the extension
```

If you want to use this for other purposes; I defined a `Extension` base class in `calcurse_load.ext.abstract`, you'd just have to add a subclass in a file there, and then add it to the dictionary in `calcurse_load.ext`
If you want to use this for other purposes; there is a `Extension` base class in `calcurse_load.ext.abstract`.

To load a custom extension, you can point this at the fully
qualified name of the extension class. For example, if you have a module
called `my_custom_calcurse` installed into your python environment, and
inside that module you have a class called `MyCustomExtension`, you can
load that extension by passing `my_custom_calcurse.MyCustomExtension` to
the `--pre-load` or `--post-save` options.

For example to use it with the gcal extension, you could provide the fully qualified path:

```
python3 -m calcurse_load --pre-load calcurse_load.ext.gcal.gcal_ext
```

This should also work with relative paths, so for example you could put that extension in a `myextension.py` file in your calcurse configuration directory:

```
.
├── gcal.enabled
├── myextension.py
├── post-save
├── pre-load
└── todotxt.enabled
1 directory, 5 files
```

Then, at the top of your `pre-load`/`post-save`, just be sure to change the directory to the current one, like:

```
#!/bin/sh
cd "$(dirname "$0")" || exit 1
```

And then you could define your `CustomExtension` in `myextension.py`, and use it like `python3 -m calcurse_load --pre-load myextension.CustomExtension`
109 changes: 55 additions & 54 deletions calcurse_load/__main__.py
Original file line number Diff line number Diff line change
@@ -1,64 +1,65 @@
import sys
import argparse
from typing import Sequence
from functools import lru_cache

from .ext import EXTENSIONS
import click

from .ext.all import EXTENSION_NAMES, get_extension
from .ext.abstract import Extension
from .calcurse import get_configuration

from typing import Tuple, List, Callable

CHOICES = list(EXTENSION_NAMES)
CHOICES.append("custom.module.name.Extension")

config = get_configuration()


@lru_cache(maxsize=None)
def _load_extension(name: str) -> Extension:
if name in CHOICES:
return get_extension(name)(config=config)
else:
import importlib

def parse_args() -> Tuple[argparse.Namespace, List[str]]:
parser = argparse.ArgumentParser(
description="Load extra data into calcurse",
# manually write out usage
usage="calcurse_load (--pre-load|--post-save) ({})...".format(
"|".join(EXTENSIONS.keys())
),
)
required_args = parser.add_argument_group("required options")
hook_choice = required_args.add_mutually_exclusive_group(required=True)
hook_choice.add_argument(
"--pre-load",
help="Execute the preload action for the extension",
action="store_true",
default=False,
)
hook_choice.add_argument(
"--post-save",
help="Execute the postsave action for the extension",
action="store_true",
default=False,
)
extensions = []
args, extra = parser.parse_known_args()
for arg in map(str.lower, extra):
if arg not in EXTENSIONS:
print(
f"Unexpected argument: {arg}, currently loaded extensions: '{','.join(EXTENSIONS)}'"
)
else:
extensions.append(arg)
return args, extensions
module_name, class_name = name.rsplit(".", 1)
module = importlib.import_module(module_name)
extclass = getattr(module, class_name)
assert issubclass(extclass, Extension)
ext = extclass(config=config)
assert isinstance(ext, Extension)
return ext


def cli() -> None:
args, extensions = parse_args()
configuration = get_configuration()
if len(extensions) == 0:
print(
"No extensions passed!\nPossible extensions:\n{}".format(
"\n".join(EXTENSIONS)
),
file=sys.stderr,
)
sys.exit(1)
for ext in extensions:
ext_class: Callable = EXTENSIONS[ext] # type: ignore[type-arg]
load_hook = ext_class(config=configuration)
if args.pre_load:
load_hook.pre_load()
else:
load_hook.post_save()
@click.command()
@click.option(
"--pre-load",
help="Execute the preload action for the extension",
metavar="|".join(CHOICES),
multiple=True,
type=click.UNPROCESSED,
callback=lambda ctx, param, value: [_load_extension(v) for v in value],
)
@click.option(
"--post-save",
help="Execute the postsave action for the extension",
metavar="|".join(CHOICES),
multiple=True,
type=click.UNPROCESSED,
callback=lambda ctx, param, value: [_load_extension(v) for v in value],
)
def cli(
pre_load: Sequence[Extension],
post_save: Sequence[Extension],
) -> None:
"""
A CLI for loading data for calcurse
"""
for ext in pre_load:
click.echo(f"Loading {ext}")
ext.pre_load()
for ext in post_save:
click.echo(f"Saving {ext}")
ext.post_save()


if __name__ == "__main__":
Expand Down
15 changes: 0 additions & 15 deletions calcurse_load/calcurse.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,18 +31,3 @@ def get_configuration() -> Configuration:
calcurse_dir=calcurse_dir,
calcurse_load_dir=calcurse_load_dir,
)


# so that eval "$(calcurse_load --shell)" can be called,
# to get the config values from the hooks
def eval_shell_configuration() -> str:
conf = get_configuration()
return "\n".join(
[
f'{envvar}="{val}"'
for envvar, val in zip(
("CALCURSE_DIR", "CALCURSE_LOAD_DIR"),
(conf.calcurse_dir, conf.calcurse_load_dir),
)
]
)
4 changes: 0 additions & 4 deletions calcurse_load/ext/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +0,0 @@
from .gcal import gcal_ext
from .todotxt import todotxt_ext

EXTENSIONS = {"gcal": gcal_ext, "todotxt": todotxt_ext}
13 changes: 11 additions & 2 deletions calcurse_load/ext/abstract.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
import typing
from abc import ABC, abstractmethod

if typing.TYPE_CHECKING:
from ..calcurse import Configuration


class Extension(ABC):
def __init__(self, config) -> None: # type: ignore[no-untyped-def]
self.config = config
def __init__(self, config: "Configuration") -> None: # type: ignore[no-untyped-def]
self.config: Configuration = config

def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.config})"

__str__ = __repr__

@abstractmethod
def pre_load(self) -> None:
Expand Down
17 changes: 17 additions & 0 deletions calcurse_load/ext/all.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from typing import Type
from .abstract import Extension

EXTENSION_NAMES = {"gcal", "todotxt"}


def get_extension(name: str) -> Type[Extension]:
if name == "gcal":
from .gcal import gcal_ext

return gcal_ext
elif name == "todotxt":
from .todotxt import todotxt_ext

return todotxt_ext
else:
raise ValueError(f"Unknown extension: {name}")
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ exclude =
tests*
include =
calcurse_load
calcurse_load.*
gcal_index

[options.extras_require]
Expand Down

0 comments on commit 4586416

Please sign in to comment.