Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
__version__.py
__pycache__/
*.py[cod]
*.egg-info/
src/letsdo.egg-info
build/
dist/
src/letsdo.egg-info
.eggs/
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,33 @@ color: true
data_directory: /home/username/
```

An optional `hooks_directory` field can also be set:

```
color: true
data_directory: /home/username/
hooks_directory: /home/username/.letsdo-hooks/
```

## Hooks

Letsdo supports hooks similarly to Taskwarrior. A hook is any executable script placed in the directory defined by `hooks_directory` in `.letsdo.yaml`. If `hooks_directory` is not set, the feature is silently ignored.

Hook scripts are called with two arguments:
1. The event name: `start` when a task has just started, `stop` when a task has just stopped.
2. The full description of the task.

Example hook script (`~/.letsdo-hooks/notify`):

```sh
#!/bin/sh
event="$1"
task="$2"
notify-send "letsdo" "$event: $task"
```

All executable files in `hooks_directory` are called in alphabetical order on each event.

Let's see now the history: you can rapidly have a look at **today** and **yesterday** work done by typing:

```
Expand Down
9 changes: 9 additions & 0 deletions src/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,15 @@ def create_default_configuration(home="~"):
return yaml.dump(default_config, f)


def get_hooks_directory(home="~"):
"""Return the hooks directory path if configured, otherwise None"""
config = get_configuration(home)
hooks_dir = config.get("hooks_directory")
if hooks_dir:
return os.path.expanduser(hooks_dir)
return None


def get_task_file_path(home="~"):
"""Return the running task data file path"""
return os.path.join(get_configuration(home)["data_directory"], TASK_FILE_NAME)
Expand Down
15 changes: 11 additions & 4 deletions src/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from datetime import datetime
from app import Task, guess_task_id_from_string, work_on
from configuration import autocomplete, create_default_configuration
from hooks import run_hooks
from typing import Tuple


Expand Down Expand Up @@ -36,6 +37,7 @@ def start_task_handler(description: str, start_str: str="") -> Tuple[bool, str]:

task = Task.get_running()
if task:
run_hooks("start", task.name)
return True, f"{task.start_time}: {task.name} started"
return False, "No task running"

Expand All @@ -53,10 +55,13 @@ def edit_file_handler(filename) -> Tuple[bool, str]:

def cancel_task_handler() -> Tuple[bool, str]:
"""handles a request to cancel the current task"""
msg = Task.cancel()
if not msg:
task = Task.get_running()
if not task:
return False, "No task running, nothing to do"
return True, f"cancelled task: {msg}"
task_name = task.name
Task.cancel()
run_hooks("cancel", task_name)
return True, f"cancelled task: {task_name}"


def stop_task_handler(stop_time: str) -> Tuple[bool, str]:
Expand All @@ -65,11 +70,13 @@ def stop_task_handler(stop_time: str) -> Tuple[bool, str]:
if not task:
return False, "no task running, nothing to do"

task_name = task.name
work_time = Task.stop(stop_time)
if not work_time:
return False, "error: could not get stop time"
run_hooks("stop", task_name)
now = datetime.now().strftime("%H:%M")
msg = f"{now}: stopped task: {task.name}, after {work_time[0]}hours, {work_time[1]} minutes"
msg = f"{now}: stopped task: {task_name}, after {work_time[0]}hours, {work_time[1]} minutes"
return True, msg


Expand Down
28 changes: 28 additions & 0 deletions src/hooks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""
Hook execution support. Hooks are scripts placed in the hooks_directory
configured in .letsdo.yaml. Each executable file in that directory is called
with two arguments: the event name ("start" or "stop") and the task description.
"""
import os
import subprocess
from log import LOGGER
from configuration import get_hooks_directory


def run_hooks(event: str, task_description: str) -> None:
"""Run all executable hook scripts in the configured hooks directory."""
hooks_dir = get_hooks_directory()
if not hooks_dir:
return

if not os.path.isdir(hooks_dir):
LOGGER.warning(f"hooks_directory '{hooks_dir}' does not exist or is not a directory")
return

for entry in sorted(os.listdir(hooks_dir)):
script_path = os.path.join(hooks_dir, entry)
if os.path.isfile(script_path) and os.access(script_path, os.X_OK):
try:
subprocess.run([script_path, event, task_description], check=False)
except Exception as exc:
LOGGER.warning(f"hook '{script_path}' failed: {exc}")
Loading