Skip to content

Commit

Permalink
Add ability to pass template path with --template
Browse files Browse the repository at this point in the history
Update jrnl/args.py
  • Loading branch information
alichtman committed Mar 13, 2023
1 parent 17c987c commit c08301a
Show file tree
Hide file tree
Showing 9 changed files with 184 additions and 64 deletions.
38 changes: 20 additions & 18 deletions docs/tips-and-tricks.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,49 +74,52 @@ jrnlimport () {
}
```

## Using templates
## Using Templates

!!! note
Templates require an [external editor](./advanced.md) be configured.

A template is a code snippet that makes it easier to use repeated text
each time a new journal entry is started. There are two ways you can utilize
templates in your entries.
Templates are text files that are used for creating structured journals.
There are three ways you can use templates:

### 1. Command line arguments
### 1. Use the `--template` command line argument and the default $XDG_DATA_HOME/jrnl/templates directory

If you had a `template.txt` file with the following contents:
`$XDG_DATA_HOME/jrnl/templates` is created by default to store your templates! Create a template (like `default.md`) in this directory and pass `--template FILE_IN_DIR`.

```sh
jrnl --template default.md
```

### 2. Use the `--template` command line argument with a local / absolute path

You can create a template file with any text. Here is an example:

```sh
# /tmp/template.txt
My Personal Journal
Title:

Body:
```

The `template.txt` file could be used to create a new entry with these
command line arguments:
Then, pass the absolute or relative path to the template file as an argument, and your external
editor will open and have your template pre-populated.

```sh
jrnl < template.txt # Imports template.txt as the most recent entry
jrnl -1 --edit # Opens the most recent entry in the editor
jrnl --template /tmp/template.md
```

### 2. Include the template file in `jrnl.yaml`
### 3. Set a default template file in `jrnl.yaml`

A more efficient way to work with a template file is to declare the file
in your [config file](./reference-config-file.md) by changing the `template`
setting from `false` to the template file's path in double quotes:
If you want a template by default, change the value of `template` in the [config file](./reference-config-file.md)
from `false` to the template file's path, wrapped in double quotes:

```sh
...
template: "/path/to/template.txt"
...
```

Changes can be saved as you continue writing the journal entry and will be
logged as a new entry in the journal you specified in the original argument.

!!! tip
To read your journal entry or to verify the entry saved, you can use this
command: `jrnl -n 1` (Check out [Formats](./formats.md) for more options).
Expand Down Expand Up @@ -219,4 +222,3 @@ To cause vi to jump to the end of the last line of the entry you edit, in your c
```yaml
editor: vi + -c "call cursor('.',strwidth(getline('.')))"
```

7 changes: 6 additions & 1 deletion jrnl/args.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,11 @@ def parse_args(args: list[str] = []) -> argparse.Namespace:
"Writing", textwrap.dedent(compose_msg).strip()
)
composing.add_argument("text", metavar="", nargs="*")
composing.add_argument(
"--template",
dest="template",
help="Path to template file. Can be a local path, absolute path, or a path relative to $JRNL_TEMPLATE_DIR",
)

read_msg = (
"To find entries from your journal, use any combination of the below filters."
Expand Down Expand Up @@ -372,7 +377,7 @@ def parse_args(args: list[str] = []) -> argparse.Namespace:
default="",
help="""
Overrides default (created when first installed) config file for this command only.
Examples: \n
\t - Use a work config file for this jrnl entry, call: \n
\t jrnl --config-file /home/user1/work_config.yaml
Expand Down
23 changes: 19 additions & 4 deletions jrnl/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import argparse
import logging
import os
from pathlib import Path
from typing import Any
from typing import Callable

Expand Down Expand Up @@ -34,7 +35,6 @@


def make_yaml_valid_dict(input: list) -> dict:

"""
Convert a two-element list of configuration key-value pair into a flat dict.
Expand Down Expand Up @@ -73,9 +73,9 @@ def save_config(config: dict, alt_config_path: str | None = None) -> None:
yaml.dump(config, f)


def get_config_path() -> str:
def get_config_directory() -> str:
try:
config_directory_path = xdg.BaseDirectory.save_config_path(XDG_RESOURCE)
return xdg.BaseDirectory.save_config_path(XDG_RESOURCE)
except FileExistsError:
raise JrnlException(
Message(
Expand All @@ -89,7 +89,13 @@ def get_config_path() -> str:
),
)

return os.path.join(config_directory_path or home_dir(), DEFAULT_CONFIG_NAME)

def get_config_path() -> Path:
try:
config_directory_path = get_config_directory()
except JrnlException:
return Path(home_dir(), DEFAULT_CONFIG_NAME)
return Path(config_directory_path, DEFAULT_CONFIG_NAME)


def get_default_config() -> dict[str, Any]:
Expand Down Expand Up @@ -120,6 +126,15 @@ def get_default_journal_path() -> str:
return os.path.join(journal_data_path, DEFAULT_JOURNAL_NAME)


def get_templates_path() -> Path:
# jrnl_xdg_resource_path is created by save_data_path if it does not exist
jrnl_xdg_resource_path = Path(xdg.BaseDirectory.save_data_path(XDG_RESOURCE))
jrnl_templates_path = jrnl_xdg_resource_path / "templates"
# Create the directory if needed.
jrnl_templates_path.mkdir(exist_ok=True)
return jrnl_templates_path


def scope_config(config: dict, journal_name: str) -> dict:
if journal_name not in config["journals"]:
return config
Expand Down
117 changes: 82 additions & 35 deletions jrnl/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from jrnl.config import DEFAULT_JOURNAL_KEY
from jrnl.config import get_config_path
from jrnl.config import get_journal_name
from jrnl.config import get_templates_path
from jrnl.config import scope_config
from jrnl.editor import get_text_from_editor
from jrnl.editor import get_text_from_stdin
Expand All @@ -23,7 +24,7 @@
from jrnl.output import print_msg
from jrnl.output import print_msgs
from jrnl.override import apply_overrides
from jrnl.path import expand_path
from jrnl.path import absolute_path

if TYPE_CHECKING:
from argparse import Namespace
Expand Down Expand Up @@ -120,9 +121,78 @@ def _is_write_mode(args: "Namespace", config: dict, **kwargs) -> bool:
return write_mode


def _read_template_file(template_arg: str, template_path_from_config: str) -> str:
"""
This function is called when either a template file is passed with --template, or config.template is set.
The processing logic is:
If --template was not used: Load the global template file.
If --template was used:
* Check $XDG_DATA_HOME/jrnl/templates/template_arg.
* Check template_arg as an absolute / relative path.
If a file is found, its contents are returned as a string.
If not, a JrnlException is raised.
"""
logging.debug(
"Write mode: Either a template arg was passed, or the global config is set."
)

# If filename is unset, we are in this flow due to a global template being configured
if not template_arg:
logging.debug("Write mode: Global template configuration detected.")
global_template_path = absolute_path(template_path_from_config)
try:
with open(global_template_path, encoding="utf-8") as f:
template_data = f.read()
return template_data
except FileNotFoundError:
raise JrnlException(
Message(
MsgText.CantReadTemplateGlobalConfig,
MsgStyle.ERROR,
{
"global_template_path": global_template_path,
},
)
)
else: # A template CLI arg was passed.
logging.debug("Trying to load template from $XDG_DATA_HOME/jrnl/templates/")
jrnl_template_dir = get_templates_path()
logging.debug(f"Write mode: jrnl templates directory: {jrnl_template_dir}")
template_path = jrnl_template_dir / template_arg
try:
with open(template_path, encoding="utf-8") as f:
template_data = f.read()
return template_data
except FileNotFoundError:
logging.debug(
f"Couldn't open {template_path}. Treating --template argument like a local / abs path."
)
pass

normalized_template_arg_filepath = absolute_path(template_arg)
try:
with open(normalized_template_arg_filepath, encoding="utf-8") as f:
template_data = f.read()
return template_data
except FileNotFoundError:
raise JrnlException(
Message(
MsgText.CantReadTemplateCLIArg,
MsgStyle.ERROR,
{
"normalized_template_arg_filepath": normalized_template_arg_filepath,
"jrnl_template_dir": template_path,
},
)
)


def write_mode(args: "Namespace", config: dict, journal: Journal, **kwargs) -> None:
"""
Gets input from the user to write to the journal
0. Check for a template passed as an argument, or in the global config
1. Check for input from cli
2. Check input being piped in
3. Open editor if configured (prepopulated with template if available)
Expand All @@ -131,8 +201,14 @@ def write_mode(args: "Namespace", config: dict, journal: Journal, **kwargs) -> N
"""
logging.debug("Write mode: starting")

if args.text:
logging.debug("Write mode: cli text detected: %s", args.text)
if args.template or config["template"]:
logging.debug(f"Write mode: template CLI arg detected: {args.template}")
# Read template file and pass as raw text into the composer
template_data = _read_template_file(args.template, config["template"])
raw = _write_in_editor(config, template_data)

elif args.text:
logging.debug(f"Write mode: cli text detected: {args.text}")
raw = " ".join(args.text).strip()
if args.edit:
raw = _write_in_editor(config, raw)
Expand Down Expand Up @@ -218,45 +294,16 @@ def search_mode(args: "Namespace", journal: Journal, **kwargs) -> None:
_display_search_results(**kwargs)


def _write_in_editor(config: dict, template: str | None = None) -> str:
def _write_in_editor(config: dict, prepopulated_text: str | None = None) -> str:
if config["editor"]:
logging.debug("Write mode: opening editor")
if not template:
template = _get_editor_template(config)
raw = get_text_from_editor(config, template)

raw = get_text_from_editor(config, prepopulated_text)
else:
raw = get_text_from_stdin()

return raw


def _get_editor_template(config: dict, **kwargs) -> str:
logging.debug("Write mode: loading template for entry")

if not config["template"]:
logging.debug("Write mode: no template configured")
return ""

template_path = expand_path(config["template"])

try:
with open(template_path) as f:
template = f.read()
logging.debug("Write mode: template loaded: %s", template)
except OSError:
logging.error("Write mode: template not loaded")
raise JrnlException(
Message(
MsgText.CantReadTemplate,
MsgStyle.ERROR,
{"template": template_path},
)
)

return template


def _has_search_args(args: "Namespace") -> bool:
return any(
(
Expand Down Expand Up @@ -426,7 +473,7 @@ def _change_time_search_results(
journal: Journal,
old_entries: list["Entry"],
no_prompt: bool = False,
**kwargs
**kwargs,
) -> None:
# separate entries we are not editing
other_entries = _other_entries(journal, old_entries)
Expand Down
3 changes: 2 additions & 1 deletion jrnl/journals/Journal.py
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,8 @@ def ask_action(entry):

def new_entry(self, raw: str, date=None, sort: bool = True) -> Entry:
"""Constructs a new entry from some raw text input.
If a date is given, it will parse and use this, otherwise scan for a date in the input first."""
If a date is given, it will parse and use this, otherwise scan for a date in the input first.
"""

raw = raw.replace("\\n ", "\n").replace("\\n", "\n")
# Split raw text into title and body
Expand Down
14 changes: 10 additions & 4 deletions jrnl/messages/MsgText.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,16 @@ def __str__(self) -> str:

KeyboardInterruptMsg = "Aborted by user"

CantReadTemplate = """
Unreadable template
Could not read template file at:
{template}
CantReadTemplateGlobalConfig = """
Could not read template file defined in config:
{global_template_path}
"""

CantReadTemplateCLIArg = """
Unable to find a template file based on the passed arg, and no global template was detected.
The following filepaths were checked:
jrnl XDG Template Directory : {jrnl_template_dir}
Local Filepath : {normalized_template_arg_filepath}
"""

NoNamedJournal = "No '{journal_name}' journal configured\n{journals}"
Expand Down
2 changes: 1 addition & 1 deletion jrnl/override.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
if TYPE_CHECKING:
from argparse import Namespace


# import logging
def apply_overrides(args: "Namespace", base_config: dict) -> dict:
"""Unpack CLI provided overrides into the configuration tree.
Expand All @@ -26,7 +27,6 @@ def apply_overrides(args: "Namespace", base_config: dict) -> dict:

cfg_with_overrides = base_config.copy()
for pairs in overrides:

pairs = make_yaml_valid_dict(pairs)
key_as_dots, override_value = _get_key_and_value_from_pair(pairs)
keys = _convert_dots_to_list(key_as_dots)
Expand Down
Loading

0 comments on commit c08301a

Please sign in to comment.