Merge branch 'develop' into mode-actions-1639

Conflicts:
  jrnl/controller.py
This commit is contained in:
Jonathan Wren 2023-03-25 11:56:57 -07:00
commit 15a5b143ee
12 changed files with 1558 additions and 1400 deletions

View file

@ -57,6 +57,16 @@
**Packaging:** **Packaging:**
- Update dependency cryptography to v40 [\#1710](https://github.com/jrnl-org/jrnl/pull/1710) ([renovate[bot]](https://github.com/apps/renovate))
- Update dependency poethepoet to v0.19.0 [\#1709](https://github.com/jrnl-org/jrnl/pull/1709) ([renovate[bot]](https://github.com/apps/renovate))
- Update dependency tzlocal to v4.3 [\#1708](https://github.com/jrnl-org/jrnl/pull/1708) ([renovate[bot]](https://github.com/apps/renovate))
- Update dependency tox to v4.4.7 [\#1707](https://github.com/jrnl-org/jrnl/pull/1707) ([renovate[bot]](https://github.com/apps/renovate))
- Update dependency rich to v13.3.2 [\#1706](https://github.com/jrnl-org/jrnl/pull/1706) ([renovate[bot]](https://github.com/apps/renovate))
- Update dependency pytest-xdist to v3.2.1 [\#1705](https://github.com/jrnl-org/jrnl/pull/1705) ([renovate[bot]](https://github.com/apps/renovate))
- Update dependency pytest to v7.2.2 [\#1704](https://github.com/jrnl-org/jrnl/pull/1704) ([renovate[bot]](https://github.com/apps/renovate))
- Update dependency ipdb to v0.13.13 [\#1703](https://github.com/jrnl-org/jrnl/pull/1703) ([renovate[bot]](https://github.com/apps/renovate))
- Update dependency flake8-type-checking to v2.3.1 [\#1702](https://github.com/jrnl-org/jrnl/pull/1702) ([renovate[bot]](https://github.com/apps/renovate))
- Update dependency cryptography to v39.0.2 [\#1701](https://github.com/jrnl-org/jrnl/pull/1701) ([renovate[bot]](https://github.com/apps/renovate))
- Update dependency rich to v13 [\#1654](https://github.com/jrnl-org/jrnl/pull/1654) ([renovate[bot]](https://github.com/apps/renovate)) - Update dependency rich to v13 [\#1654](https://github.com/jrnl-org/jrnl/pull/1654) ([renovate[bot]](https://github.com/apps/renovate))
## [v3.3](https://pypi.org/project/jrnl/v3.3/) (2022-10-29) ## [v3.3](https://pypi.org/project/jrnl/v3.3/) (2022-10-29)

View file

@ -74,39 +74,45 @@ jrnlimport () {
} }
``` ```
## Using templates ## Using Templates
!!! note !!! note
Templates require an [external editor](./advanced.md) be configured. Templates require an [external editor](./advanced.md) be configured.
A template is a code snippet that makes it easier to use repeated text Templates are text files that are used for creating structured journals.
each time a new journal entry is started. There are two ways you can utilize There are three ways you can use templates:
templates in your entries.
### 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 ```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 My Personal Journal
Title: Title:
Body: Body:
``` ```
The `template.txt` file could be used to create a new entry with these Then, pass the absolute or relative path to the template file as an argument, and your external
command line arguments: editor will open and have your template pre-populated.
```sh ```sh
jrnl < template.txt # Imports template.txt as the most recent entry jrnl --template /tmp/template.md
jrnl -1 --edit # Opens the most recent entry in the editor
``` ```
### 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 If you want a template by default, change the value of `template` in the [config file](./reference-config-file.md)
in your [config file](./reference-config-file.md) by changing the `template` from `false` to the template file's path, wrapped in double quotes:
setting from `false` to the template file's path in double quotes:
```sh ```sh
... ...
@ -114,9 +120,6 @@ 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 !!! tip
To read your journal entry or to verify the entry saved, you can use this 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). command: `jrnl -n 1` (Check out [Formats](./formats.md) for more options).
@ -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 ```yaml
editor: vi + -c "call cursor('.',strwidth(getline('.')))" editor: vi + -c "call cursor('.',strwidth(getline('.')))"
``` ```

View file

@ -211,6 +211,11 @@ def parse_args(args: list[str] = []) -> argparse.Namespace:
"Writing", textwrap.dedent(compose_msg).strip() "Writing", textwrap.dedent(compose_msg).strip()
) )
composing.add_argument("text", metavar="", nargs="*") 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 $XDG_DATA_HOME/jrnl/templates/",
)
read_msg = ( read_msg = (
"To find entries from your journal, use any combination of the below filters." "To find entries from your journal, use any combination of the below filters."
@ -419,7 +424,7 @@ def parse_args(args: list[str] = []) -> argparse.Namespace:
default="", default="",
help=""" help="""
Overrides default (created when first installed) config file for this command only. Overrides default (created when first installed) config file for this command only.
Examples: \n Examples: \n
\t - Use a work config file for this jrnl entry, call: \n \t - Use a work config file for this jrnl entry, call: \n
\t jrnl --config-file /home/user1/work_config.yaml \t jrnl --config-file /home/user1/work_config.yaml

View file

@ -4,6 +4,7 @@
import argparse import argparse
import logging import logging
import os import os
from pathlib import Path
from typing import Any from typing import Any
from typing import Callable from typing import Callable
@ -72,9 +73,9 @@ def save_config(config: dict, alt_config_path: str | None = None) -> None:
yaml.dump(config, f) yaml.dump(config, f)
def get_config_path() -> str: def get_config_directory() -> str:
try: try:
config_directory_path = xdg.BaseDirectory.save_config_path(XDG_RESOURCE) return xdg.BaseDirectory.save_config_path(XDG_RESOURCE)
except FileExistsError: except FileExistsError:
raise JrnlException( raise JrnlException(
Message( Message(
@ -88,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]: def get_default_config() -> dict[str, Any]:
@ -128,6 +135,15 @@ def get_default_journal_path() -> str:
return os.path.join(journal_data_path, DEFAULT_JOURNAL_NAME) 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: def scope_config(config: dict, journal_name: str) -> dict:
if journal_name not in config["journals"]: if journal_name not in config["journals"]:
return config return config

View file

@ -11,6 +11,7 @@ from jrnl import time
from jrnl.config import DEFAULT_JOURNAL_KEY from jrnl.config import DEFAULT_JOURNAL_KEY
from jrnl.config import get_config_path from jrnl.config import get_config_path
from jrnl.config import get_journal_name from jrnl.config import get_journal_name
from jrnl.config import get_templates_path
from jrnl.config import scope_config from jrnl.config import scope_config
from jrnl.editor import get_text_from_editor from jrnl.editor import get_text_from_editor
from jrnl.editor import get_text_from_stdin from jrnl.editor import get_text_from_stdin
@ -22,7 +23,7 @@ from jrnl.messages import MsgText
from jrnl.output import print_msg from jrnl.output import print_msg
from jrnl.output import print_msgs from jrnl.output import print_msgs
from jrnl.override import apply_overrides from jrnl.override import apply_overrides
from jrnl.path import expand_path from jrnl.path import absolute_path
if TYPE_CHECKING: if TYPE_CHECKING:
from argparse import Namespace from argparse import Namespace
@ -129,9 +130,78 @@ def _is_append_mode(args: "Namespace", config: dict, **kwargs) -> bool:
return append_mode return append_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 append_mode(args: "Namespace", config: dict, journal: "Journal", **kwargs) -> None: def append_mode(args: "Namespace", config: dict, journal: "Journal", **kwargs) -> None:
""" """
Gets input from the user to write to the journal 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 1. Check for input from cli
2. Check input being piped in 2. Check input being piped in
3. Open editor if configured (prepopulated with template if available) 3. Open editor if configured (prepopulated with template if available)
@ -140,8 +210,16 @@ def append_mode(args: "Namespace", config: dict, journal: "Journal", **kwargs) -
""" """
logging.debug("Append mode: starting") logging.debug("Append mode: starting")
if args.text: if args.template or config["template"]:
logging.debug("Append mode: cli text detected: %s", args.text) 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)
if raw == template_data:
logging.error("Write mode: raw text was the same as the template")
raise JrnlException(Message(MsgText.NoChangesToTemplate, MsgStyle.NORMAL))
elif args.text:
logging.debug(f"Write mode: cli text detected: {args.text}")
raw = " ".join(args.text).strip() raw = " ".join(args.text).strip()
if args.edit: if args.edit:
raw = _write_in_editor(config, raw) raw = _write_in_editor(config, raw)
@ -156,9 +234,6 @@ def append_mode(args: "Namespace", config: dict, journal: "Journal", **kwargs) -
if not raw or raw.isspace(): if not raw or raw.isspace():
logging.error("Append mode: couldn't get raw text or entry was empty") logging.error("Append mode: couldn't get raw text or entry was empty")
raise JrnlException(Message(MsgText.NoTextReceived, MsgStyle.NORMAL)) raise JrnlException(Message(MsgText.NoTextReceived, MsgStyle.NORMAL))
if config["template"] and raw == _get_editor_template(config):
logging.error("Write mode: raw text was the same as the template")
raise JrnlException(Message(MsgText.NoChangesToTemplate, MsgStyle.NORMAL))
logging.debug( logging.debug(
f"Append mode: appending raw text to journal '{args.journal_name}': {raw}" f"Append mode: appending raw text to journal '{args.journal_name}': {raw}"
@ -192,46 +267,18 @@ def search_mode(args: "Namespace", journal: "Journal", **kwargs) -> None:
_filter_journal_entries(args, journal) _filter_journal_entries(args, journal)
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"]: if config["editor"]:
logging.debug("Append mode: opening editor") logging.debug("Write mode: opening editor")
if not template: raw = get_text_from_editor(config, prepopulated_text)
template = _get_editor_template(config)
raw = get_text_from_editor(config, template)
else: else:
raw = get_text_from_stdin() raw = get_text_from_stdin()
return raw return raw
def _get_editor_template(config: dict, **kwargs) -> str:
logging.debug("Append mode: loading template for entry")
if not config["template"]: def _filter_journal_entries(args: "Namespace", journal: Journal, **kwargs) -> None:
logging.debug("Append mode: no template configured")
return ""
template_path = expand_path(config["template"])
try:
with open(template_path) as f:
template = f.read()
logging.debug("Append mode: template loaded: %s", template)
except OSError:
logging.error("Append mode: template not loaded")
raise JrnlException(
Message(
MsgText.CantReadTemplate,
MsgStyle.ERROR,
{"template": template_path},
)
)
return template
def _filter_journal_entries(args: "Namespace", journal: "Journal", **kwargs) -> None:
"""Filter journal entries in-place based upon search args""" """Filter journal entries in-place based upon search args"""
if args.on_date: if args.on_date:
args.start_date = args.end_date = args.on_date args.start_date = args.end_date = args.on_date
@ -380,6 +427,7 @@ def _change_time_search_results(
args: "Namespace", args: "Namespace",
journal: "Journal", journal: "Journal",
old_entries: list["Entry"], old_entries: list["Entry"],
no_prompt: bool = False,
**kwargs, **kwargs,
) -> None: ) -> None:
# separate entries we are not editing # separate entries we are not editing

View file

@ -105,10 +105,16 @@ class MsgText(Enum):
KeyboardInterruptMsg = "Aborted by user" KeyboardInterruptMsg = "Aborted by user"
CantReadTemplate = """ CantReadTemplateGlobalConfig = """
Unreadable template Could not read template file defined in config:
Could not read template file at: {global_template_path}
{template} """
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}" NoNamedJournal = "No '{journal_name}' journal configured\n{journals}"

2667
poetry.lock generated

File diff suppressed because it is too large Load diff

View file

@ -31,4 +31,47 @@ Feature: Using templates
| basic_onefile.yaml | | basic_onefile.yaml |
| basic_encrypted.yaml | | basic_encrypted.yaml |
| basic_folder.yaml | | basic_folder.yaml |
| basic_dayone.yaml | | basic_dayone.yaml |
Scenario Outline: --template nonexistent_file should throw an error
Given we use the config "<config_file>"
And we use the password "test" if prompted
When we run "jrnl --template this_template_does_not_exist.template"
Then we should get an error
Then the error output should contain "Unable to find a template file based on the passed arg"
Examples: configs
| config_file |
| basic_onefile.yaml |
| basic_encrypted.yaml |
| basic_folder.yaml |
| basic_dayone.yaml |
Scenario Outline: --template local_filepath should be used in new entry
Given we use the config "<config_file>"
And we use the password "test" if prompted
When we run "jrnl --template features/templates/basic.template"
Then the output should contain "No entry to save, because the template was not changed"
Examples: configs
| config_file |
| basic_onefile.yaml |
| basic_encrypted.yaml |
| basic_folder.yaml |
| basic_dayone.yaml |
Scenario Outline: --template file_in_XDG_templates_dir should be used in new entry
Given we use the config "<config_file>"
And we use the password "test" if prompted
And we copy the template "basic.template" to the default templates folder
When we run "jrnl --template basic.template"
Then the output should contain "No entry to save, because the template was not changed"
Examples: configs
| config_file |
| basic_onefile.yaml |
| basic_encrypted.yaml |
| basic_folder.yaml |
| basic_dayone.yaml |
| basic_dayone.yaml |

View file

@ -89,6 +89,7 @@ def cli_run(
mock_user_input, mock_user_input,
mock_overrides, mock_overrides,
mock_default_journal_path, mock_default_journal_path,
mock_default_templates_path,
): ):
# Check if we need more mocks # Check if we need more mocks
mock_factories.update(mock_args) mock_factories.update(mock_args)
@ -98,6 +99,7 @@ def cli_run(
mock_factories.update(mock_config_path) mock_factories.update(mock_config_path)
mock_factories.update(mock_user_input) mock_factories.update(mock_user_input)
mock_factories.update(mock_default_journal_path) mock_factories.update(mock_default_journal_path)
mock_factories.update(mock_default_templates_path)
return { return {
"status": 0, "status": 0,
@ -179,6 +181,16 @@ def mock_default_journal_path(temp_dir):
} }
@fixture
def mock_default_templates_path(temp_dir):
templates_path = Path(temp_dir.name, "templates")
return {
"get_templates_path": lambda: patch(
"jrnl.controller.get_templates_path", return_value=templates_path
),
}
@fixture @fixture
def temp_dir(): def temp_dir():
return tempfile.TemporaryDirectory() return tempfile.TemporaryDirectory()

View file

@ -125,6 +125,19 @@ def we_use_the_config(request, temp_dir, working_dir, config_file):
return config_dest return config_dest
@given(parse('we copy the template "{template_file}" to the default templates folder'), target_fixture="default_templates_path")
def we_copy_the_template(request, temp_dir, working_dir, template_file):
# Move into temp dir as cwd
os.chdir(temp_dir.name) # @todo move this step to a more universal place
# Copy template over
template_source = os.path.join(working_dir, "data", "templates", template_file)
template_dest = os.path.join(temp_dir.name, "templates", template_file)
os.makedirs(os.path.dirname(template_dest), exist_ok=True)
shutil.copy2(template_source, template_dest)
return template_dest
@given(parse('the config "{config_file}" exists'), target_fixture="config_path") @given(parse('the config "{config_file}" exists'), target_fixture="config_path")
def config_exists(config_file, temp_dir, working_dir): def config_exists(config_file, temp_dir, working_dir):
config_source = os.path.join(working_dir, "data", "configs", config_file) config_source = os.path.join(working_dir, "data", "configs", config_file)

View file

@ -25,6 +25,11 @@ def should_get_no_error(cli_run):
assert cli_run["status"] == 0, cli_run["status"] assert cli_run["status"] == 0, cli_run["status"]
@then("we should get an error")
def should_get_an_error(cli_run):
assert cli_run["status"] != 0, cli_run["status"]
@then(parse("the output should match\n{regex}")) @then(parse("the output should match\n{regex}"))
@then(parse('the output should match "{regex}"')) @then(parse('the output should match "{regex}"'))
def output_should_match(regex, cli_run): def output_should_match(regex, cli_run):

View file

@ -42,6 +42,7 @@ def expected_args(**kwargs):
"strict": False, "strict": False,
"tagged": False, "tagged": False,
"tags": False, "tags": False,
"template": None,
"text": [], "text": [],
"config_override": [], "config_override": [],
"config_file_path": "", "config_file_path": "",