├── .build
└── allow_all_python_version.py
├── .gitattributes
├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── bug_report.yaml
│ ├── config.yml
│ ├── documentation.yaml
│ ├── feature_request.yaml
│ └── support_request.yaml
├── PULL_REQUEST_TEMPLATE.md
├── actionlint-matcher.json
├── actions
│ └── run_tests
│ │ └── action.yaml
├── lock.yml
├── renovate.json
├── stale.yml
└── workflows
│ ├── changelog.yaml
│ ├── docs.yaml
│ ├── release.yaml
│ ├── testing_pipelines.yaml
│ ├── testing_prs.yaml
│ └── testing_schedule.yaml
├── .gitignore
├── .readthedocs.yaml
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE.md
├── README.md
├── SECURITY.md
├── docs
├── advanced.md
├── contributing.md
├── encryption.md
├── external-editors.md
├── formats.md
├── installation.md
├── journal-types.md
├── overview.md
├── privacy-and-security.md
├── reference-command-line.md
├── reference-config-file.md
├── tips-and-tricks.md
└── usage.md
├── docs_theme
├── assets
│ ├── colors.css
│ ├── highlight.css
│ ├── index.css
│ ├── readme-header.png
│ └── theme.css
├── breadcrumbs.html
├── img
│ ├── banner_og.png
│ ├── banner_twitter.png
│ ├── favicon.ico
│ ├── favicon.svg
│ ├── jrnl_white.svg
│ ├── logo.svg
│ ├── logo_white.svg
│ └── sprites.svg
├── index.html
├── index.js
├── main.html
├── requirements.txt
└── search.html
├── issue_template.md
├── jrnl
├── __init__.py
├── __main__.py
├── __version__.py
├── args.py
├── color.py
├── commands.py
├── config.py
├── controller.py
├── editor.py
├── encryption
│ ├── BaseEncryption.py
│ ├── BaseKeyEncryption.py
│ ├── BasePasswordEncryption.py
│ ├── Jrnlv1Encryption.py
│ ├── Jrnlv2Encryption.py
│ ├── NoEncryption.py
│ └── __init__.py
├── exception.py
├── install.py
├── journals
│ ├── DayOneJournal.py
│ ├── Entry.py
│ ├── FolderJournal.py
│ ├── Journal.py
│ └── __init__.py
├── keyring.py
├── main.py
├── messages
│ ├── Message.py
│ ├── MsgStyle.py
│ ├── MsgText.py
│ └── __init__.py
├── os_compat.py
├── output.py
├── override.py
├── path.py
├── plugins
│ ├── __init__.py
│ ├── calendar_heatmap_exporter.py
│ ├── dates_exporter.py
│ ├── fancy_exporter.py
│ ├── jrnl_importer.py
│ ├── json_exporter.py
│ ├── markdown_exporter.py
│ ├── tag_exporter.py
│ ├── text_exporter.py
│ ├── util.py
│ ├── xml_exporter.py
│ └── yaml_exporter.py
├── prompt.py
├── templates
│ └── sample.template
├── time.py
└── upgrade.py
├── mkdocs.yml
├── package-lock.json
├── package.json
├── poetry.lock
├── pyproject.toml
├── tasks.py
└── tests
├── __init__.py
├── bdd
├── features
│ ├── actions.feature
│ ├── build.feature
│ ├── change_time.feature
│ ├── config_file.feature
│ ├── core.feature
│ ├── datetime.feature
│ ├── delete.feature
│ ├── encrypt.feature
│ ├── file_storage.feature
│ ├── format.feature
│ ├── import.feature
│ ├── install.feature
│ ├── multiple_journals.feature
│ ├── override.feature
│ ├── password.feature
│ ├── search.feature
│ ├── star.feature
│ ├── tag.feature
│ ├── template.feature
│ ├── upgrade.feature
│ └── write.feature
└── test_features.py
├── conftest.py
├── data
├── configs
│ ├── basic_dayone.yaml
│ ├── basic_encrypted.yaml
│ ├── basic_folder.yaml
│ ├── basic_onefile.yaml
│ ├── brackets.yaml
│ ├── bug153.yaml
│ ├── bug780.yaml
│ ├── dayone.yaml
│ ├── dayone_empty.yaml
│ ├── deletion.yaml
│ ├── deletion_filters.yaml
│ ├── duplicate_keys.yaml
│ ├── editor-args.yaml
│ ├── editor.yaml
│ ├── editor_empty_folder.yaml
│ ├── editor_encrypted.yaml
│ ├── editor_markdown_extension.yaml
│ ├── empty_file.yaml
│ ├── empty_folder.yaml
│ ├── encrypted.yaml
│ ├── encrypted_old.json
│ ├── encrypted_old.yaml
│ ├── format_md.yaml
│ ├── format_text.yaml
│ ├── invalid_color.yaml
│ ├── linewrap_auto.yaml
│ ├── little_endian_dates.yaml
│ ├── markdown-headings-335.yaml
│ ├── missing_directory.yaml
│ ├── missing_journal.yaml
│ ├── mostlyreadabledates.yaml
│ ├── multiline-tags.yaml
│ ├── multiline.yaml
│ ├── multiple.yaml
│ ├── no_colors.yaml
│ ├── no_default_journal.yaml
│ ├── simple.yaml
│ ├── tags-216.yaml
│ ├── tags-237.yaml
│ ├── tags.yaml
│ ├── unreadabledates.yaml
│ ├── upgrade_from_195.json
│ ├── upgrade_from_195_little_endian_dates.json
│ ├── upgrade_from_195_with_missing_encrypted_journal.json
│ └── upgrade_from_195_with_missing_journal.json
├── journals
│ ├── basic_dayone.dayone
│ │ └── entries
│ │ │ ├── D04D335AFED711EABA18FAFFC2100C3D.doentry
│ │ │ ├── FC8A86CAFED711EA8892FAFFC2100C3D.doentry
│ │ │ └── FD8ABC8EFED711EABC35FAFFC2100C3D.doentry
│ ├── basic_encrypted.journal
│ ├── basic_folder
│ │ ├── 2020
│ │ │ ├── 08
│ │ │ │ ├── 29.txt
│ │ │ │ └── 31.txt
│ │ │ ├── 09
│ │ │ │ ├── 24.txt
│ │ │ │ └── should-be-ignored.txt
│ │ │ └── should-be-ignored.txt
│ │ └── should-be-ignored.txt
│ ├── basic_onefile.journal
│ ├── brackets.journal
│ ├── bug153.dayone
│ │ └── entries
│ │ │ ├── B40EE704E15846DE8D45C44118A4D511.doentry
│ │ │ └── B40EE704E15846DE8D45C44118A4D512.doentry
│ ├── bug780.dayone
│ │ └── entries
│ │ │ └── 48A25033B34047C591160A4480197D8B.doentry
│ ├── dayone.dayone
│ │ └── entries
│ │ │ ├── 044F3747A38546168B572C2E3F217FA2.doentry
│ │ │ ├── 0BDDD6CDA43C4A9AA2681517CC35AD9D.doentry
│ │ │ ├── 422BC895507944A291E6FC44FC6B8BFC.doentry
│ │ │ └── 4BB1F46946AD439996C9B59DE7C4DDC1.doentry
│ ├── dayone_empty.dayone
│ │ └── entries
│ │ │ └── empty.txt
│ ├── deletion.journal
│ ├── deletion_filters.journal
│ ├── empty_folder
│ │ └── empty
│ ├── encrypted.journal
│ ├── encrypted_jrnl-1-9-5.journal
│ ├── little_endian_dates.journal
│ ├── markdown-headings-335.journal
│ ├── mostlyreadabledates.journal
│ ├── multiline-tags.journal
│ ├── multiline.journal
│ ├── simple.journal
│ ├── simple_jrnl-1-9-5.journal
│ ├── simple_jrnl-1-9-5_little_endian_dates.journal
│ ├── tags-216.journal
│ ├── tags-237.journal
│ ├── tags.journal
│ ├── unreadabledates.journal
│ └── work.journal
└── templates
│ ├── basic.template
│ └── sample.template
├── lib
├── fixtures.py
├── given_steps.py
├── helpers.py
├── then_steps.py
├── type_builders.py
└── when_steps.py
└── unit
├── test_color.py
├── test_config_file.py
├── test_controller.py
├── test_editor.py
├── test_export.py
├── test_install.py
├── test_journals_folder_journal.py
├── test_os_compat.py
├── test_output.py
├── test_override.py
├── test_parse_args.py
├── test_path.py
└── test_time.py
/.build/allow_all_python_version.py:
--------------------------------------------------------------------------------
1 | import toml
2 |
3 | pyproject = toml.load("pyproject.toml")
4 |
5 | pyproject["tool"]["poetry"]["dependencies"]["python"] = "*"
6 |
7 | with open("pyproject.toml", "w") as toml_file:
8 | toml.dump(pyproject, toml_file)
9 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.journal text eol=lf
2 | *.feature text eol=lf
3 | poetry.lock text eol=lf
4 | pyrpoject.toml text eol=lf
5 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4 | patreon: # Replace with a single Patreon username
5 | open_collective: jrnl
6 | ko_fi: # Replace with a single Ko-fi username
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
13 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.yaml:
--------------------------------------------------------------------------------
1 | name: Bug Report
2 | description: Create a report to help us improve
3 | title: "Bug Report"
4 | labels: [ ":new:", "bug" ]
5 | body:
6 | - type: markdown
7 | attributes:
8 | value: |
9 | # Bug Report
10 | Hello, and thank you for reporting an issue!
11 |
12 | Please fill out the points below, as it will make our process much easier.
13 |
14 | - type: textarea
15 | id: diagnostic
16 | attributes:
17 | label: Diagnostic output
18 | description: Run `jrnl --diagnostic` and paste the output below
19 | placeholder: Paste output here
20 | validations:
21 | required: true
22 |
23 | - type: textarea
24 | id: current-behavior
25 | attributes:
26 | label: Current Behavior
27 | description: Please put a short description of what is currently happening.
28 | placeholder: Tell us what is happening
29 | validations:
30 | required: true
31 |
32 | - type: textarea
33 | id: expected-behavior
34 | attributes:
35 | label: Expected Behavior
36 | description: Please write a short description of what you would expect to happen
37 | placeholder: Tell us what should be happening
38 | validations:
39 | required: true
40 |
41 | - type: textarea
42 | id: repro-steps
43 | attributes:
44 | label: Repro Steps
45 | description: |
46 | Provide the steps to reproduce the problem.
47 |
48 | Please be as precise as possible, since more info will let us help you faster.
49 | placeholder: Repro steps
50 | validations:
51 | required: true
52 |
53 | - type: textarea
54 | id: debug-output
55 | attributes:
56 | label: Debug output
57 | description: |
58 | Please provide the output of your command with the `--debug` flag on.
59 | placeholder: "example: `jrnl --debug lorem ipsum`"
60 | validations:
61 | required: true
62 |
63 | - type: textarea
64 | id: other-info
65 | attributes:
66 | label: Other Information
67 | description: >
68 | Is there anything else we should know?
69 |
70 | (e.g. more detailed explanation, stacktraces, related
71 | issues, suggestions how to fix, links for us to have context, eg.
72 | stackoverflow, gitter, etc)
73 | validations:
74 | required: false
75 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/documentation.yaml:
--------------------------------------------------------------------------------
1 | name: Documentation Change
2 | description: Request or report any updates to our documentation (https://jrnl.sh)
3 | title: Documentation Change
4 | labels: [ ":new:", "documentation" ]
5 | body:
6 | - type: markdown
7 | attributes:
8 | value: |
9 | # Documentation Change
10 | Hello, and thank you for reporting an issue!
11 |
12 | Please fill out the points below, as it will make our process much easier.
13 |
14 | - type: textarea
15 | id: affected-pages
16 | attributes:
17 | label: Affected Page(s)
18 | description: >
19 | Please tell us which page, or pages, from the documentation site
20 | (https://jrnl.sh) are affected in this issue
21 | placeholder: "example: https://jrnl.sh/en/stable/overview"
22 | validations:
23 | required: true
24 |
25 | - type: textarea
26 | id: what-could-be-better
27 | attributes:
28 | label: What Could Be Better?
29 | description: >
30 | Please write a short description of what you hope can be clarified or
31 | further explained.
32 | validations:
33 | required: true
34 |
35 | - type: textarea
36 | id: other-info
37 | attributes:
38 | label: Other Information
39 | description: Is there anything else we should know that might be helpful?
40 | validations:
41 | required: false
42 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.yaml:
--------------------------------------------------------------------------------
1 | name: Feature Request
2 | description: Suggest an idea for jrnl
3 | title: "Feature Report"
4 | labels: [ ":new:", "enhancement" ]
5 | body:
6 | - type: markdown
7 | attributes:
8 | value: |
9 | # Feature Request
10 | Hello, and thank you for reporting an issue!
11 |
12 | Please fill out the points below, as it will make our process much easier.
13 |
14 | - type: textarea
15 | id: user-case
16 | attributes:
17 | label: Use Case/Motivation
18 | description: What is the motivation / use case for changing the behavior?
19 | placeholder: Tell us about your idea
20 | validations:
21 | required: true
22 |
23 | - type: textarea
24 | id: example-usage
25 | attributes:
26 | label: Example Usage
27 | description: Please provide examples of the usage you would like to see.
28 | placeholder: e.g `jrnl --new-flag="super cool new feature"`
29 | validations:
30 | required: true
31 |
32 | - type: textarea
33 | id: other-info
34 | attributes:
35 | label: Other Information
36 | description: >
37 | Is there anything else we should know?
38 |
39 | (e.g. more detailed explanation, stacktraces, related
40 | issues, suggestions how to fix, links for us to have context, eg.
41 | stackoverflow, gitter, etc)
42 | validations:
43 | required: false
44 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/support_request.yaml:
--------------------------------------------------------------------------------
1 | name: Support Request
2 | description: Get help with jrnl
3 | title: Support Request
4 | labels: [ ":new:", "support" ]
5 | body:
6 | - type: markdown
7 | attributes:
8 | value: |
9 | # Support Request
10 | Hello, and thank you for reporting an issue!
11 |
12 | Please fill out the points below, as it will make our process much easier.
13 |
14 | - type: textarea
15 | id: diagnostic
16 | attributes:
17 | label: Diagnostic output
18 | description: Run `jrnl --diagnostic` and paste the output below
19 | placeholder: Paste output here
20 | validations:
21 | required: true
22 |
23 | - type: textarea
24 | id: current-behavior
25 | attributes:
26 | label: What are you trying to do?
27 | description: Please put a short description of what is happening.
28 | placeholder: Tell us what is happening
29 | validations:
30 | required: true
31 |
32 | - type: textarea
33 | id: tried
34 | attributes:
35 | label: What have you tried?
36 | description: >
37 | Have you tried anything to fix the problem? This can help give us more
38 | information to help you with.
39 | placeholder: Tell us what should be happening
40 | validations:
41 | required: true
42 |
43 | - type: textarea
44 | id: other-info
45 | attributes:
46 | label: Other Information
47 | description: >
48 | Is there anything else we should know?
49 |
50 | (e.g. more detailed explanation, stacktraces, related
51 | issues, suggestions how to fix, links for us to have context, eg.
52 | stackoverflow, gitter, etc)
53 | validations:
54 | required: false
55 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
12 |
13 | ### Checklist
14 |
15 | - [ ] I have read the [contributing doc](https://github.com/jrnl-org/jrnl/blob/develop/docs/contributing.md).
16 | - [ ] I have included a link to the relevant issue number.
17 | - [ ] I have checked to ensure there aren't other open [pull requests](../pulls)
18 | for the same issue.
19 | - [ ] I have written new tests for these changes, as needed.
20 |
25 |
--------------------------------------------------------------------------------
/.github/actionlint-matcher.json:
--------------------------------------------------------------------------------
1 | {
2 | "problemMatcher": [
3 | {
4 | "owner": "actionlint",
5 | "pattern": [
6 | {
7 | "regexp": "^(?:\\x1b\\[\\d+m)?(.+?)(?:\\x1b\\[\\d+m)*:(?:\\x1b\\[\\d+m)*(\\d+)(?:\\x1b\\[\\d+m)*:(?:\\x1b\\[\\d+m)*(\\d+)(?:\\x1b\\[\\d+m)*: (?:\\x1b\\[\\d+m)*(.+?)(?:\\x1b\\[\\d+m)* \\[(.+?)\\]$",
8 | "file": 1,
9 | "line": 2,
10 | "column": 3,
11 | "message": 4,
12 | "code": 5
13 | }
14 | ]
15 | }
16 | ]
17 | }
18 |
--------------------------------------------------------------------------------
/.github/actions/run_tests/action.yaml:
--------------------------------------------------------------------------------
1 | name: run jrnl tests
2 | description: Runs all jrnl tests on multiple platforms
3 | inputs:
4 | cache-string:
5 | description: 'Cache string secret. Change to bust the cache'
6 | required: true
7 | runs:
8 | using: "composite"
9 | steps:
10 | - run: git config --global core.autocrlf false
11 | shell: bash
12 |
13 | - name: Set up Python ${{ matrix.python-version }}
14 | uses: actions/setup-python@v5
15 | with:
16 | python-version: ${{ matrix.python-version }}
17 | allow-prereleases: true
18 |
19 | - name: Capture full Python version in env
20 | run: echo "PYTHON_FULL_VERSION=$(python --version)" >> $GITHUB_ENV
21 | shell: bash
22 |
23 | - name: poetry cache # Change CACHE_STRING secret to bust the cache
24 | uses: actions/cache@v4
25 | with:
26 | path: .venv
27 | key: ${{ runner.os }}-${{ hashFiles('poetry.lock') }}-${{ env.PYTHON_FULL_VERSION }}-${{ inputs.cache-string }}
28 |
29 | - name: Install dependencies
30 | run: |
31 | echo '::group::poetry'
32 | pip --disable-pip-version-check install poetry
33 | poetry config --local virtualenvs.in-project true
34 | echo '::endgroup::'
35 |
36 | echo '::group::Other dependencies'
37 | poetry sync
38 | echo '::endgroup::'
39 |
40 | echo 'DEPS_INSTALLED=true' >> $GITHUB_ENV
41 | shell: bash
42 |
43 | - name: Linting & Testing
44 | if: ${{ env.DEPS_INSTALLED == 'true' }}
45 | run: poetry run poe test
46 | shell: bash
47 |
--------------------------------------------------------------------------------
/.github/lock.yml:
--------------------------------------------------------------------------------
1 | # Configuration for Lock Threads - https://github.com/dessant/lock-threads-app
2 |
3 | # Number of days of inactivity before a closed issue or pull request is locked
4 | daysUntilLock: 90
5 |
6 | # Skip issues and pull requests created before a given timestamp. Timestamp must
7 | # follow ISO 8601 (`YYYY-MM-DD`). Set to `false` to disable
8 | skipCreatedBefore: false
9 |
10 | # Issues and pull requests with these labels will be ignored. Set to `[]` to disable
11 | exemptLabels: []
12 |
13 | # Label to add before locking, such as `outdated`. Set to `false` to disable
14 | lockLabel: ':lock:'
15 |
16 | # Comment to post before locking. Set to `false` to disable
17 | lockComment: >
18 | This thread has been automatically locked since there has not been
19 | any recent activity after it was closed. Please open a new issue for
20 | related bugs. You can link back here from your new issue to continue
21 | the conversation.
22 |
23 | # Assign `resolved` as the reason for locking. Set to `false` to disable
24 | setLockReason: true
25 |
26 | # Limit to only `issues` or `pulls`
27 | # only: issues
28 |
29 | # Optionally, specify configuration settings just for `issues` or `pulls`
30 | # issues:
31 | # exemptLabels:
32 | # - help-wanted
33 | # lockLabel: outdated
34 |
35 | # pulls:
36 | # daysUntilLock: 30
37 |
38 | # Repository to extend settings from
39 | # _extends: repo
40 |
--------------------------------------------------------------------------------
/.github/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": [
4 | "config:base"
5 | ],
6 | "schedule": [ "at any time" ],
7 | "prConcurrentLimit": 10,
8 | "prHourlyLimit": 10,
9 | "reviewers": [
10 | "wren",
11 | "micahellison"
12 | ],
13 | "labels": [ "packaging" ]
14 | }
15 |
--------------------------------------------------------------------------------
/.github/stale.yml:
--------------------------------------------------------------------------------
1 | # Number of days of inactivity before an issue becomes stale
2 | daysUntilStale: 60
3 | # Number of days of inactivity before a stale issue is closed
4 | daysUntilClose: 7
5 | # Issues with these labels will never be considered stale
6 | exemptLabels:
7 | - ':pushpin:'
8 | - critical
9 | # Label to use when marking an issue as stale
10 | staleLabel: stale
11 | # Comment to post when marking an issue as stale. Set to `false` to disable
12 | markComment: >
13 | This issue has been automatically marked as stale because it has not had
14 | recent activity. It will be closed if no further activity occurs. Thank you
15 | for your contributions.
16 | # Comment to post when closing a stale issue. Set to `false` to disable
17 | closeComment: false
18 |
--------------------------------------------------------------------------------
/.github/workflows/docs.yaml:
--------------------------------------------------------------------------------
1 | # Copyright © 2012-2023 jrnl contributors
2 | # License: https://www.gnu.org/licenses/gpl-3.0.html
3 |
4 | name: Docs
5 |
6 | on:
7 | push:
8 | branches: [ develop, release ]
9 | paths:
10 | - 'docs/**'
11 | - 'docs_theme/**'
12 | - 'mkdocs.yml'
13 | - 'readthedocs.yml'
14 | - '.github/workflows/docs.yaml'
15 | - 'tasks.py'
16 | - 'pyproject.toml'
17 | pull_request:
18 | branches: [ develop ]
19 | paths:
20 | - 'docs/**'
21 | - 'docs_theme/**'
22 | - 'mkdocs.yml'
23 | - 'readthedocs.yml'
24 | - '.github/workflows/docs.yaml'
25 | - 'tasks.py'
26 | - 'pyproject.toml'
27 |
28 | jobs:
29 | accessibility:
30 | if: contains(toJson(github.event.commits), '[ci skip]') == false
31 | runs-on: ubuntu-latest
32 | strategy:
33 | fail-fast: true
34 | matrix:
35 | python-version: [ '3.11' ]
36 | os: [ ubuntu-latest ]
37 |
38 | steps:
39 | - uses: actions/checkout@v4
40 |
41 | - name: Set up Python
42 | uses: actions/setup-python@v5
43 | with:
44 | python-version: ${{ matrix.python-version }}
45 |
46 | - name: Setup Node.js environment
47 | uses: actions/setup-node@main
48 |
49 | - name: Capture full Python version in env
50 | run: echo "PYTHON_FULL_VERSION=$(python --version)" >> "$GITHUB_ENV"
51 |
52 | - name: poetry cache
53 | uses: actions/cache@v4
54 | with:
55 | path: .venv
56 | key: ${{ runner.os }}-${{ hashFiles('poetry.lock') }}-${{ env.PYTHON_FULL_VERSION }}-${{ secrets.CACHE_STRING }}
57 |
58 | - name: npm cache
59 | uses: actions/cache@v4
60 | with:
61 | path: node_modules
62 | key: ${{ runner.os }}-pa11y-v3
63 |
64 | - name: Install dependencies
65 | run: |
66 | pip install poetry
67 | poetry config --local virtualenvs.in-project true
68 | poetry sync --no-root
69 | npm install
70 | echo "node_modules/.bin" >> "$GITHUB_PATH"
71 |
72 | - name: Start docs server
73 | run: poetry run poe docs-run &
74 |
75 | - name: Accessibility testing (Pa11y)
76 | run: poetry run poe docs-check
77 |
--------------------------------------------------------------------------------
/.github/workflows/release.yaml:
--------------------------------------------------------------------------------
1 | # Copyright © 2012-2023 jrnl contributors
2 | # License: https://www.gnu.org/licenses/gpl-3.0.html
3 |
4 | name: Release
5 | on:
6 | workflow_dispatch:
7 | inputs:
8 | version:
9 | description: 'Version (e.g. v2.5, v2.5.1-beta, v2.6-beta2)'
10 | type: string
11 | required: true
12 | include_repo_version:
13 | description: 'Update version in repo?'
14 | type: boolean
15 | required: true
16 | default: true
17 | include_pypi:
18 | description: 'Publish to PyPI?'
19 | type: boolean
20 | required: true
21 | default: true
22 |
23 | jobs:
24 | validate:
25 | name: "Validate version string"
26 | runs-on: ubuntu-latest
27 | steps:
28 | - name: Validate version
29 | run: |
30 | JRNL_VERSION="${{ github.event.inputs.version }}"
31 | echo "::debug::version: $JRNL_VERSION"
32 | if [[ ! $JRNL_VERSION =~ ^v[0-9]+(\.[0-9]+){1,2}(-(alpha|beta)([0-9]+)?)?$ ]]; then
33 | echo
34 | echo "::error::Bad version"
35 | echo
36 | echo "Version string should match pattern above."
37 | echo "Here are some examples of valid version numbers:"
38 | echo
39 | echo " v2.5"
40 | echo " v2.5-alpha"
41 | echo " v2.5-beta"
42 | echo " v2.5.1"
43 | echo " v2.5.1-alpha"
44 | echo " v2.5.1-beta"
45 | exit 1
46 | fi
47 |
48 | release_pypi:
49 | needs: validate
50 | name: "Release to PyPI"
51 | runs-on: ubuntu-latest
52 | outputs:
53 | pypi_version: ${{ steps.pypi-version-getter.outputs.pypi_version }}
54 | env:
55 | HOME_REPO: ${{ secrets.HOME_REPO }}
56 | steps:
57 | - name: Get version
58 | run: |
59 | JRNL_VERSION="${{ github.event.inputs.version }}"
60 | echo "::debug::version: $JRNL_VERSION"
61 | echo "JRNL_VERSION=$JRNL_VERSION" >> "$GITHUB_ENV"
62 |
63 | - name: Set up Python
64 | uses: actions/setup-python@v5
65 | with:
66 | python-version: '3.13'
67 |
68 | - name: Checkout repo
69 | uses: actions/checkout@v4
70 | with:
71 | token: ${{ secrets.JRNL_BOT_TOKEN }}
72 |
73 | - name: Config git user
74 | run: |
75 | git config --global user.name "${{ secrets.JRNL_BOT_NAME }}"
76 | git config --global user.email "${{ secrets.JRNL_BOT_EMAIL }}"
77 |
78 | - name: Install dependencies
79 | run: pip install poetry
80 |
81 | - name: Update version in files
82 | if: ${{ github.event.inputs.include_repo_version == 'true' }}
83 | run: |
84 | poetry version "$JRNL_VERSION"
85 | echo "__version__ = \"$JRNL_VERSION\"" > jrnl/__version__.py
86 |
87 | - name: Commit updated files
88 | if: ${{ github.event.inputs.include_repo_version == 'true' && github.repository == env.HOME_REPO }}
89 | run: |
90 | git add pyproject.toml jrnl/__version__.py
91 | git commit -m "Increment version to ${JRNL_VERSION}"
92 | git tag -a -m "$JRNL_VERSION" "$JRNL_VERSION"
93 | git push
94 | git push --tags
95 |
96 | - name: Build
97 | run: poetry build
98 |
99 | - name: Deploy to PyPI
100 | if: ${{ github.event.inputs.include_pypi == 'true' && github.repository == env.HOME_REPO }}
101 | env:
102 | POETRY_PYPI_TOKEN_PYPI: ${{ secrets.PYPI_TOKEN }}
103 | run: poetry publish
104 |
105 | - name: Get PyPI version
106 | id: pypi-version-getter
107 | run: |
108 | pypi_version="$(find dist/jrnl-*.tar.gz | sed -r 's!dist/jrnl-(.*)\.tar\.gz!\1!')"
109 | echo "pypi_version=$pypi_version" >> "$GITHUB_OUTPUT"
110 |
--------------------------------------------------------------------------------
/.github/workflows/testing_pipelines.yaml:
--------------------------------------------------------------------------------
1 | # Copyright © 2012-2023 jrnl contributors
2 | # License: https://www.gnu.org/licenses/gpl-3.0.html
3 |
4 | name: Testing Pipeline Files
5 |
6 | on:
7 | push:
8 | branches: [ develop, release ]
9 | paths:
10 | - '.github/workflows/**'
11 | - '.github/actions/**'
12 | pull_request:
13 | branches: [ develop ]
14 | paths:
15 | - '.github/workflows/**'
16 | - '.github/actions/**'
17 | schedule:
18 | - cron: '0 0 * * SAT'
19 |
20 | jobs:
21 | test:
22 | if: >
23 | ! contains(github.event.head_commit.message, '[ci skip]')
24 | runs-on: ${{ matrix.os }}
25 | strategy:
26 | fail-fast: false
27 | matrix:
28 | os: [ ubuntu-latest ]
29 | steps:
30 | - run: git config --global core.autocrlf false
31 | - uses: actions/checkout@v4
32 | - name: Check workflow files
33 | uses: docker://rhysd/actionlint:latest
34 | with:
35 | args: -color
36 |
--------------------------------------------------------------------------------
/.github/workflows/testing_prs.yaml:
--------------------------------------------------------------------------------
1 | # Copyright © 2012-2023 jrnl contributors
2 | # License: https://www.gnu.org/licenses/gpl-3.0.html
3 |
4 | name: Testing
5 |
6 | on:
7 | push:
8 | branches: [ develop, release ]
9 | paths:
10 | - 'jrnl/**'
11 | - 'features/**'
12 | - 'tests/**'
13 | - 'poetry.lock'
14 | - 'pyproject.toml'
15 | - '.github/workflows/testing_prs.yaml'
16 | - 'tasks.py'
17 | pull_request:
18 | branches: [ develop ]
19 | paths:
20 | - 'jrnl/**'
21 | - 'features/**'
22 | - 'tests/**'
23 | - 'poetry.lock'
24 | - 'pyproject.toml'
25 | - '.github/workflows/testing_prs.yaml'
26 | - 'tasks.py'
27 |
28 | defaults:
29 | run:
30 | shell: bash # needed to prevent Windows from using PowerShell
31 |
32 | jobs:
33 | test:
34 | if: >
35 | ! contains(github.event.head_commit.message, '[ci skip]')
36 | runs-on: ${{ matrix.os }}
37 | strategy:
38 | fail-fast: false
39 | matrix:
40 | python-version: [ '3.10', '3.11', '3.12', '3.13' ]
41 | os: [ ubuntu-latest, macos-latest, windows-latest ]
42 | steps:
43 | - run: git config --global core.autocrlf false
44 | - uses: actions/checkout@v4
45 | - name: Run tests
46 | uses: ./.github/actions/run_tests
47 | with:
48 | cache-string: ${{ secrets.CACHE_STRING }}
49 |
--------------------------------------------------------------------------------
/.github/workflows/testing_schedule.yaml:
--------------------------------------------------------------------------------
1 | # Copyright © 2012-2023 jrnl contributors
2 | # License: https://www.gnu.org/licenses/gpl-3.0.html
3 |
4 | name: Testing
5 |
6 | on:
7 | schedule:
8 | - cron: '0 0 * * SAT'
9 |
10 | defaults:
11 | run:
12 | shell: bash # needed to prevent Windows from using PowerShell
13 |
14 | jobs:
15 | test_all:
16 | runs-on: ${{ matrix.os }}
17 | strategy:
18 | fail-fast: false
19 | matrix:
20 | python-version: [ '3.10', '3.11', '3.12', '3.13' ]
21 | os: [ ubuntu-latest, macos-latest, windows-latest ]
22 | steps:
23 | - run: git config --global core.autocrlf false
24 | - uses: actions/checkout@v4
25 | - name: Run tests
26 | uses: ./.github/actions/run_tests
27 | with:
28 | cache-string: ${{ secrets.CACHE_STRING }}
29 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled DLL and Shared Library files
2 | *.py[cod]
3 | *.so
4 |
5 | # Packages
6 | *.egg
7 | *.egg-info/
8 | .installed.cfg
9 | bin/
10 | build/
11 | develop-eggs/
12 | dist/
13 | eggs/
14 | lib64/
15 | parts/
16 | sdist/
17 | .tox/
18 | var/
19 | node_modules/
20 | __pycache__/
21 | .pytest_cache/
22 | .flakeheaven_cache/
23 |
24 | # Versioning
25 | .python-version
26 | .tool-versions
27 |
28 | # Installer logs
29 | .DS_Store
30 | .travis-solo
31 | Icon
32 | pip-log.txt
33 |
34 | # Documentation
35 | _build
36 | _sources
37 | _static
38 | coverage.xml
39 | exp/
40 | objects.inv
41 | searchindex.js
42 |
43 | # virtualenv
44 | .venv*/
45 | env/
46 | env*/
47 | venv*/
48 |
49 | # Editor and IDE specific files
50 | # Since contributors may want to user a variety of development tools it is
51 | # recommended that editor specific file types be ignored globally by each
52 | # contributor via a global gitignore. Instructions for setting up a global
53 | # ignore file can be found here:
54 | # https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
55 | # (Configuring ignored files for all repositories on your computer)
56 | # Examples of such files are:
57 | # MS Visual Studio (PyTools)
58 | # obj
59 | # *.suo
60 | # PyCharm
61 | # .idea/
62 | # VS Code
63 | # .vscode/settings.json
--------------------------------------------------------------------------------
/.readthedocs.yaml:
--------------------------------------------------------------------------------
1 | # readthedocs.yml
2 | # Read the Docs configuration file
3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
4 |
5 | # Required
6 | version: 2
7 |
8 | # Set the OS
9 | build:
10 | os: ubuntu-22.04
11 | tools:
12 | python: "3"
13 |
14 |
15 | # Build documentation in the docs/ directory
16 | mkdocs:
17 | configuration: mkdocs.yml
18 | fail_on_warning: false
19 |
20 | # Optionally build your docs in additional formats such as PDF
21 | formats:
22 | - pdf
23 | - epub
24 |
25 | # Optionally set the version of Python and requirements required to build your docs
26 | python:
27 | install:
28 | - requirements: docs_theme/requirements.txt
29 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 |
5 |
6 | # Contributing
7 |
8 | See "[Contributing](docs/contributing.md)" in the `docs` directory.
9 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | jrnl
13 | [](https://github.com/jrnl-org/jrnl/actions?query=workflow%3ATesting)
14 | [](https://pepy.tech/project/jrnl)
15 | [](https://pypi.python.org/pypi/jrnl/)
16 | [](https://formulae.brew.sh/formula/jrnl)
17 | [](https://gitter.im/jrnl-org/jrnl)
18 | [](https://github.com/jrnl-org/jrnl/blob/develop/CHANGELOG.md)
19 | ====
20 |
21 | _To get help, [submit an issue](https://github.com/jrnl-org/jrnl/issues/new/choose) on
22 | GitHub._
23 |
24 | `jrnl` is a simple journal application for the command line.
25 |
26 | You can use it to easily create, search, and view journal entries. Journals are
27 | stored as human-readable plain text, and can also be encrypted using [AES
28 | encryption](http://en.wikipedia.org/wiki/Advanced_Encryption_Standard).
29 |
30 | ## In a Nutshell
31 |
32 | To make a new entry, just enter
33 |
34 | ``` sh
35 | jrnl yesterday: Called in sick. Used the time to clean the house and write my
36 | book.
37 | ```
38 |
39 | `yesterday:` is interpreted by `jrnl` as a timestamp. Everything until the
40 | first sentence ending (either `.`, `?`, or `!`) is interpreted as the title, and
41 | the rest as the body. In your journal file, the result will look like this:
42 |
43 | [2012-03-29 09:00] Called in sick.
44 | Used the time to clean the house and write my book.
45 |
46 | If you just call `jrnl`, you will be prompted to compose your entry - but you
47 | can also configure _jrnl_ to use your external editor.
48 |
49 | For more information, please read the
50 | [documentation](https://jrnl.sh).
51 |
52 | ## Contributors
53 |
54 | ### Maintainers
55 |
56 | Our maintainers help keep the lights on for the project:
57 |
58 | * Jonathan Wren ([wren](https://github.com/wren))
59 | * Micah Ellison ([micahellison](https://github.com/micahellison))
60 |
61 | Please thank them if you like `jrnl`!
62 |
63 | ### Code Contributors
64 |
65 | This project is made with love by the many fabulous people who have contributed.
66 | `jrnl` couldn't exist without each and every one of you!
67 |
68 |
71 |
72 | If you'd also like to help make `jrnl` better, please see our [contributing
73 | documentation](docs/contributing.md).
74 |
75 | ### Financial Backers
76 |
77 | Another way show support is through direct financial contributions. These funds
78 | go to covering our costs, and are a quick way to show your appreciation for
79 | `jrnl`.
80 |
81 | [Become a financial contributor](https://opencollective.com/jrnl/contribute)
82 | and help us sustain our community.
83 |
84 |
86 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security
2 |
3 | If you've discovered a potential security issue in jrnl, please contact the maintainers at [maintainers@jrnl.sh](mailto:maintainers@jrnl.sh).
4 |
5 | You can also feel free to [open an issue](https://github.com/jrnl-org/jrnl/issues/new/choose) (but please don't disclose the vulnerability) in case the email goes to spam.
6 |
7 | You can find [known privacy and security issues in our documentation](https://jrnl.sh/en/stable/privacy-and-security/).
8 |
--------------------------------------------------------------------------------
/docs/advanced.md:
--------------------------------------------------------------------------------
1 |
5 |
6 | # Advanced Usage
7 |
8 | ## Configuration File
9 |
10 | `jrnl` has a wide variety of options that can be customized through the config file,
11 | including templates, formats, multiple journals, and more. See
12 | the [configuration file reference](./reference-config-file.md) for details
13 | or read on for some common use cases.
14 |
15 | ### Multiple journal files
16 |
17 | You can configure `jrnl`to use with multiple journals (eg.
18 | `private` and `work`) by defining more journals in your [config file](./reference-config-file.md),
19 | for example:
20 |
21 | ``` yaml
22 | journals:
23 | default: ~/journal.txt
24 | work: ~/work.txt
25 | ```
26 |
27 | The `default` journal gets created the first time you start `jrnl`
28 | Now you can access the `work` journal by using `jrnl work` instead of
29 | `jrnl`, eg.
30 |
31 | ``` sh
32 | jrnl work at 10am: Meeting with @Steve
33 | jrnl work -n 3
34 | ```
35 |
36 | will both use `~/work.txt`, while `jrnl -n 3` will display the last
37 | three entries from `~/journal.txt` (and so does `jrnl default -n 3`).
38 |
39 | You can also override the default options for each individual journal.
40 | If your `jrnl.yaml` looks like this:
41 |
42 | ``` yaml
43 | encrypt: false
44 | journals:
45 | default: ~/journal.txt
46 | work:
47 | journal: ~/work.txt
48 | encrypt: true
49 | food: ~/my_recipes.txt
50 | ```
51 |
52 | Your `default` and your `food` journals won't be encrypted, however your
53 | `work` journal will!
54 |
55 | You can override all options that are present at
56 | the top level of `jrnl.yaml`, just make sure that at the very least
57 | you specify a `journal: ...` key that points to the journal file of
58 | that journal.
59 |
60 | Consider the following example configuration
61 |
62 | ``` yaml
63 | editor: vi -c startinsert
64 | journals:
65 | default: ~/journal.txt
66 | work:
67 | journal: ~/work.txt
68 | encrypt: true
69 | display_format: json
70 | editor: code -rw
71 | food:
72 | display_format: markdown
73 | journal: ~/recipes.txt
74 | ```
75 |
76 | The `work` journal is encrypted, prints to `json` by default, and is edited using an existing window of VSCode. Similarly, the `food` journal prints to markdown by default, but uses all the other defaults.
77 |
78 | ### Modifying Configurations from the Command line
79 |
80 | You can override a configuration field for the current instance of `jrnl` using `--config-override CONFIG_KEY CONFIG_VALUE` where `CONFIG_KEY` is a valid configuration field, specified in dot notation and `CONFIG_VALUE` is the (valid) desired override value. The dot notation can be used to change config keys within other keys, such as `colors.title` for the `title` key within the `colors` key.
81 |
82 | You can specify multiple overrides as multiple calls to `--config-override`.
83 |
84 | !!! note
85 | These overrides allow you to modify ***any*** field of your jrnl configuration. We trust that you know what you are doing.
86 |
87 | #### Examples:
88 |
89 | ``` sh
90 | # Create an entry using the `stdin` prompt, for rapid logging
91 | jrnl --config-override editor ""
92 |
93 | # Populate a project's log
94 | jrnl --config-override journals.todo "$(git rev-parse --show-toplevel)/todo.txt" todo find my towel
95 |
96 | # Pass multiple overrides
97 | jrnl --config-override display_format fancy --config-override linewrap 20 \
98 | --config-override colors.title green
99 | ```
100 |
101 | ### Using an alternate config
102 |
103 | You can specify an alternate configuration file for the current instance of `jrnl` using `--config-file CONFIG_FILE_PATH` where
104 | `CONFIG_FILE_PATH` is a path to an alternate `jrnl` configuration file.
105 |
106 | #### Examples:
107 |
108 | ``` sh
109 | # Use personalised configuration file for personal journal entries
110 | jrnl --config-file ~/foo/jrnl/personal-config.yaml
111 |
112 | # Use alternate configuration file for work-related entries
113 | jrnl --config-file ~/foo/jrnl/work-config.yaml
114 |
115 | # Use default configuration file (created on first run)
116 | jrnl
117 | ```
118 |
--------------------------------------------------------------------------------
/docs/installation.md:
--------------------------------------------------------------------------------
1 |
5 |
6 | # Getting started
7 |
8 | ## Installation
9 |
10 | The easiest way to install `jrnl` is using
11 | [pipx](https://pipx.pypa.io/stable/installation/)
12 | with [Python](https://www.python.org/) 3.10+:
13 |
14 | ``` sh
15 | pipx install jrnl
16 | ```
17 |
18 | !!! tip
19 | Do not use `sudo` while installing `jrnl`. This may lead to path issues.
20 |
21 | The first time you run `jrnl` you will be asked where your journal file
22 | should be created and whether you wish to encrypt it.
23 |
24 | ## Quickstart
25 |
26 | To make a new entry, just type
27 |
28 | ``` text
29 | jrnl yesterday: Called in sick. Used the time to clean, and spent 4h on writing my book.
30 | ```
31 |
32 | and hit return. `yesterday:` will be interpreted as a time stamp.
33 | Everything until the first sentence mark (`.?!:`) will be interpreted as
34 | the title, the rest as the body. In your journal file, the result will
35 | look like this:
36 |
37 | ``` output
38 | 2012-03-29 09:00 Called in sick.
39 | Used the time to clean the house and spent 4h on writing my book.
40 | ```
41 |
42 | If you just call `jrnl`, you will be prompted to compose your entry -
43 | but you can also [configure](advanced.md) *jrnl* to use your external editor.
44 |
--------------------------------------------------------------------------------
/docs/journal-types.md:
--------------------------------------------------------------------------------
1 |
5 |
6 | # Journal Types
7 | `jrnl` can store your journal in a few different ways:
8 |
9 | - a single text file (encrypted or otherwise)
10 | - a folder structure organized by date containing unencrypted text files
11 | - the DayOne Classic format
12 |
13 | There is no need to specify what type of journal you'd like to use. Instead,
14 | `jrnl` will automatically detect the journal type based on whether you're
15 | referencing a file or a folder in your [config file](advanced.md),
16 | and if it's a folder, whether or not DayOne Classic content exists in it.
17 |
18 | ## Single File
19 | The single file format is the most flexible, as it can be [encrypted](encryption.md).
20 | To use it, enter any path that is a file or does not already exist. You can
21 | use any extension. `jrnl` will automatically create the file when you save
22 | your first entry.
23 |
24 | ## Folder
25 | The folder journal format organizes your entries into subfolders for the year
26 | and month and `.txt` files for each day. If there are multiple entries in a day,
27 | they all appear in the same `.txt` file.
28 |
29 | The directory tree structure is in this format: `YYYY/MM/DD.txt`. For instance, if
30 | you have an entry on May 5th, 2021 in a folder journal at `~/folderjournal`, it will
31 | be located in: `~/folderjournal/2021/05/05.txt`
32 |
33 | !!! note
34 | Creating a new folder journal can be done in two ways:
35 |
36 | * Create a folder with the name of the journal before running `jrnl`. Otherwise, when you run `jrnl` for the first time, it will assume that you are creating a single file journal instead, and it will create a file at that path.
37 | * Create a new journal in your [config_file](advanced.md) and end the path with a ``/`` (on a POSIX system like Linux or MacOSX) or a ``\`` (on a Windows system). The folder will be created automatically if it doesn't exist.
38 |
39 | !!! note
40 | Folder journals can't be encrypted.
41 |
42 | ## Day One Classic
43 | `jrnl` supports the original data format used by DayOne. It's similar to the folder
44 | journal format, except it's identified by either of these characteristics:
45 |
46 | * the folder has a `.dayone` extension
47 | * the folder has a subfolder named `entries`
48 |
49 | This is not to be confused with the DayOne 2.0 format, [which is very different](https://help.dayoneapp.com/en/articles/1187337-day-one-classic-is-retired).
50 |
51 | !!! note
52 | DayOne Classic journals can't be encrypted.
53 |
54 | ## Changing your journal type
55 | You can't simply modify a journal's configuration to change its type. Instead,
56 | define a new journal as the type you'd like, and use
57 | [piping](https://en.wikipedia.org/wiki/Redirection_(computing)#Piping)
58 | to export your old journal as `txt` to an import command on your new journal.
59 |
60 | For instance, if you have a `projects` journal you would like to import into
61 | a `new` journal, you would run the following after setting up the configuration
62 | for your `new` journal:
63 | ```
64 | jrnl projects --format txt | jrnl new --import
65 | ```
66 |
--------------------------------------------------------------------------------
/docs/overview.md:
--------------------------------------------------------------------------------
1 |
5 |
6 | # Overview
7 |
8 | `jrnl` is a simple journal application for the command line.
9 |
10 | You can use it to easily create, search, and view journal entries. Journals are
11 | stored as human-readable plain text, and can also be encrypted using [AES
12 | encryption](http://en.wikipedia.org/wiki/Advanced_Encryption_Standard).
13 |
14 | `jrnl` has most of the features you need, and few of the ones you don't.
15 |
16 | ## Plain Text
17 |
18 | `jrnl` stores each journal in plain text. You can store `jrnl` files anywhere,
19 | including in shared folders to keep them synchronized between devices. Journal
20 | files are compact (thousands of entries take up less than 1 MiB) and can be read
21 | by almost any electronic device, now and for the foreseeable future.
22 |
23 | ## Tags
24 |
25 | To make it easier to find entries later, `jrnl` includes support for inline tags
26 | (the default tag symbol is `@`). You can find and filter entries by using tags
27 | along with other search criteria.
28 |
29 | ## Support for Multiple Journals
30 |
31 | `jrnl` includes support for the creation of multiple journals, each of which
32 | can be stored as a single file or as a set of files. Entries are automatically
33 | timestamped in a human-readable format that makes it easy to view multiple
34 | entries at a time. `jrnl` can easily find the entries you want so that you can
35 | read them or edit them.
36 |
37 | ## Support for External Editors
38 |
39 | `jrnl` plays nicely with your favorite text editor. You may prefer to write
40 | journal entries in an editor. Or you may want to make changes that require a
41 | more comprehensive application. `jrnl` can filter specific entries and pass them
42 | to the [external editor](./external-editors.md) of your choice.
43 |
44 | ## Encryption
45 |
46 | `jrnl` includes support for [AES
47 | encryption](http://en.wikipedia.org/wiki/Advanced_Encryption_Standard). See the
48 | [encryption page](./encryption.md) for more information.
49 |
50 | ## Import and Export
51 |
52 | `jrnl` makes it easy to import entries from other sources. Existing entries can
53 | be exported in a variety of [formats](./formats.md).
54 |
55 | ## Multi-Platform Support
56 |
57 | `jrnl` is compatible with most operating systems. You can [download](./installation.md) it using one
58 | of a variety of package managers, or you can build from source.
59 |
60 | ## Open-Source
61 |
62 | `jrnl` is written in [Python](https://www.python.org) and maintained by a
63 | [friendly community](https://github.com/jrnl-org/jrnl) of open-source software
64 | enthusiasts.
65 |
--------------------------------------------------------------------------------
/docs_theme/assets/colors.css:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright © 2012-2023 jrnl contributors
3 | License: https://www.gnu.org/licenses/gpl-3.0.html
4 | */
5 |
6 | :root {
7 | /* For dark bg */
8 | --white: #fcfcfc;
9 | --off-white: #f4f0ff;
10 | --purple: #7e57c2;
11 | --light-purple: #cf93e6;
12 | --blue: #61aeee;
13 | --green: #a6e22e;
14 | --orange: #fd971f;
15 | --red: #eb5567;
16 | --pink: #d57699;
17 | --yellow: #e2b93d;
18 |
19 | /* For light bg */
20 | --black: #404040;
21 | --teal: #2a8068;
22 | --dark-blue: #356eb7;
23 | --mid-purple: #846392;
24 | --bright-purple: #af27ad;
25 | --dark-purple: #604385;
26 | --darkest-purple: #251A32;
27 | --grey: #3b3b4a;
28 |
29 | --black-shadow: #0000001A;
30 | --blacker-shadow: #00000059;
31 |
32 | /* Special cases */
33 | --terminal: #1b1c2e;
34 | }
35 |
--------------------------------------------------------------------------------
/docs_theme/assets/highlight.css:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright © 2012-2023 jrnl contributors
3 | License: https://www.gnu.org/licenses/gpl-3.0.html
4 |
5 | Atom One Dark With support for ReasonML by Gidi Morris, based off work by
6 | Daniel Gamage
7 |
8 | Original One Dark Syntax theme from https://github.com/atom/one-dark-syntax
9 | */
10 |
11 | .hljs {
12 | display: block;
13 | overflow-x: auto;
14 | padding: 0.5em;
15 | line-height: 1.3em;
16 | color: var(--off-white);
17 | background: #383e49;
18 | border-radius: 5px;
19 | font-size: 0.9rem;
20 | line-height: 1.3rem;
21 | }
22 | .hljs-keyword,
23 | .hljs-operator {
24 | color: var(--pink);
25 | }
26 | .hljs-pattern-match {
27 | color: var(--pink);
28 | }
29 | .hljs-pattern-match .hljs-constructor {
30 | color: var(--blue);
31 | }
32 | .hljs-function {
33 | color: var(--blue);
34 | }
35 | .hljs-function .hljs-params {
36 | color: var(--green);
37 | }
38 | .hljs-function .hljs-params .hljs-typing {
39 | color: var(--orange);
40 | }
41 | .hljs-module-access .hljs-module {
42 | color: var(--purple);
43 | }
44 | .hljs-constructor {
45 | color: var(--yellow);
46 | }
47 | .hljs-constructor .hljs-string {
48 | color: var(--green);
49 | }
50 | .hljs-comment,
51 | .hljs-quote {
52 | color: var(--light-purple);
53 | font-style: italic;
54 | }
55 | .hljs-doctag,
56 | .hljs-formula {
57 | color: var(--purple);
58 | }
59 | .hljs-section,
60 | .hljs-name,
61 | .hljs-selector-tag,
62 | .hljs-deletion,
63 | .hljs-subst {
64 | color: var(--yellow);
65 | }
66 | .hljs-literal {
67 | color: var(--blue);
68 | }
69 | .hljs-string,
70 | .hljs-regexp,
71 | .hljs-addition,
72 | .hljs-attribute,
73 | .hljs-meta-string {
74 | color: var(--green);
75 | }
76 | .hljs-built_in,
77 | .hljs-class .hljs-title {
78 | color: var(--orange);
79 | }
80 | .hljs-attr,
81 | .hljs-variable,
82 | .hljs-template-variable,
83 | .hljs-type,
84 | .hljs-selector-class,
85 | .hljs-selector-attr,
86 | .hljs-selector-pseudo,
87 | .hljs-number {
88 | color: var(--orange);
89 | }
90 |
91 | .rst-content a tt,
92 | .rst-content a tt,
93 | .rst-content a code {
94 | color: var(--blue);
95 | }
96 |
97 | .hljs-number,
98 | .hljs-literal,
99 | .hljs-variable,
100 | .hljs-template-variable,
101 | .hljs-tag .hljs-attr {
102 | color: var(--blue);
103 | }
104 |
105 | .hljs-tag {
106 | color: var(--pink)
107 | }
108 |
109 | .hljs-symbol,
110 | .hljs-bullet,
111 | .hljs-link,
112 | .hljs-meta,
113 | .hljs-selector-id,
114 | .hljs-title {
115 | color: var(--blue);
116 | }
117 | .hljs-emphasis {
118 | font-style: italic;
119 | }
120 | .hljs-strong {
121 | font-weight: bold;
122 | }
123 | .hljs-link {
124 | text-decoration: underline;
125 | }
126 |
127 | .rst-content .note .admonition-title {
128 | background: var(--dark-blue);
129 | }
130 |
131 | .rst-content .note.admonition {
132 | background: var(--light-blue);
133 | }
134 |
135 | .rst-content .tip .admonition-title {
136 | background: var(--teal);
137 | }
138 |
139 | .rst-content .tip .admonition {
140 | background: var(--light-blue);
141 | }
142 |
143 | /* hack to bypass a11y issue with conflicting highlight.css files */
144 | code.language-xml span.hljs-meta span.hljs-string {
145 | color: var(--green) !important;
146 | }
147 |
148 |
--------------------------------------------------------------------------------
/docs_theme/assets/readme-header.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jrnl-org/jrnl/7923e815f7560b5a11cf4c49008c2e10f907e39e/docs_theme/assets/readme-header.png
--------------------------------------------------------------------------------
/docs_theme/breadcrumbs.html:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
37 | {%- if config.theme.prev_next_buttons_location|lower in ['top', 'both']
38 | and page and (page.next_page or page.previous_page) %}
39 |
47 | {%- endif %}
48 |
49 |
50 |
--------------------------------------------------------------------------------
/docs_theme/img/banner_og.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jrnl-org/jrnl/7923e815f7560b5a11cf4c49008c2e10f907e39e/docs_theme/img/banner_og.png
--------------------------------------------------------------------------------
/docs_theme/img/banner_twitter.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jrnl-org/jrnl/7923e815f7560b5a11cf4c49008c2e10f907e39e/docs_theme/img/banner_twitter.png
--------------------------------------------------------------------------------
/docs_theme/img/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jrnl-org/jrnl/7923e815f7560b5a11cf4c49008c2e10f907e39e/docs_theme/img/favicon.ico
--------------------------------------------------------------------------------
/docs_theme/img/favicon.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs_theme/img/logo_white.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs_theme/index.js:
--------------------------------------------------------------------------------
1 | var typed2 =
2 |
--------------------------------------------------------------------------------
/docs_theme/main.html:
--------------------------------------------------------------------------------
1 |
5 |
6 | {% extends "base.html" %}
7 |
8 | {%- block search_button %}
9 | {% if 'search' in config['plugins'] %}
10 |
11 |
15 |
16 | {% endif %}
17 | {%- endblock %}
18 |
--------------------------------------------------------------------------------
/docs_theme/requirements.txt:
--------------------------------------------------------------------------------
1 | mkdocs>=1.4
2 | jinja2==3.1.6
3 |
--------------------------------------------------------------------------------
/docs_theme/search.html:
--------------------------------------------------------------------------------
1 |
5 |
6 | {% extends "main.html" %}
7 |
8 | {% block content %}
9 |
10 |
11 |
26 |
27 |
28 | Results
29 |
30 |
31 | Searching...
32 |
33 |
34 | {% endblock %}
35 |
--------------------------------------------------------------------------------
/issue_template.md:
--------------------------------------------------------------------------------
1 | # Stop
2 |
3 | Please don't file a blank issue.
4 |
5 | Fill out one of the templates from the link below and we'll be better able to
6 | help you.
7 |
8 | https://github.com/jrnl-org/jrnl/issues/new/choose
9 |
--------------------------------------------------------------------------------
/jrnl/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2012-2023 jrnl contributors
2 | # License: https://www.gnu.org/licenses/gpl-3.0.html
3 |
4 | try:
5 | from jrnl.__version__ import __version__
6 | except ImportError:
7 | __version__ = "source"
8 | __title__ = "jrnl"
9 |
--------------------------------------------------------------------------------
/jrnl/__main__.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2012-2023 jrnl contributors
2 | # License: https://www.gnu.org/licenses/gpl-3.0.html
3 |
4 | import sys
5 |
6 | from jrnl.main import run
7 |
8 | if __name__ == "__main__":
9 | sys.exit(run())
10 |
--------------------------------------------------------------------------------
/jrnl/__version__.py:
--------------------------------------------------------------------------------
1 | __version__ = "v4.2.1"
2 |
--------------------------------------------------------------------------------
/jrnl/color.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2012-2023 jrnl contributors
2 | # License: https://www.gnu.org/licenses/gpl-3.0.html
3 |
4 | import re
5 | from string import punctuation
6 | from string import whitespace
7 | from typing import TYPE_CHECKING
8 |
9 | import colorama
10 |
11 | from jrnl.os_compat import on_windows
12 |
13 | if TYPE_CHECKING:
14 | from jrnl.journals import Entry
15 |
16 | if on_windows():
17 | colorama.init()
18 |
19 |
20 | def colorize(string: str, color: str, bold: bool = False) -> str:
21 | """Returns the string colored with colorama.Fore.color. If the color set by
22 | the user is "NONE" or the color doesn't exist in the colorama.Fore attributes,
23 | it returns the string without any modification."""
24 | color_escape = getattr(colorama.Fore, color.upper(), None)
25 | if not color_escape:
26 | return string
27 | elif not bold:
28 | return color_escape + string + colorama.Fore.RESET
29 | else:
30 | return colorama.Style.BRIGHT + color_escape + string + colorama.Style.RESET_ALL
31 |
32 |
33 | def highlight_tags_with_background_color(
34 | entry: "Entry", text: str, color: str, is_title: bool = False
35 | ) -> str:
36 | """
37 | Takes a string and colorizes the tags in it based upon the config value for
38 | color.tags, while colorizing the rest of the text based on `color`.
39 | :param entry: Entry object, for access to journal config
40 | :param text: Text to be colorized
41 | :param color: Color for non-tag text, passed to colorize()
42 | :param is_title: Boolean flag indicating if the text is a title or not
43 | :return: Colorized str
44 | """
45 |
46 | def colorized_text_generator(fragments):
47 | """Efficiently generate colorized tags / text from text fragments.
48 | Taken from @shobrook. Thanks, buddy :)
49 | :param fragments: List of strings representing parts of entry (tag or word).
50 | :rtype: List of tuples
51 | :returns [(colorized_str, original_str)]"""
52 | for part in fragments:
53 | if part and part[0] not in config["tagsymbols"]:
54 | yield colorize(part, color, bold=is_title), part
55 | elif part:
56 | yield colorize(part, config["colors"]["tags"], bold=True), part
57 |
58 | config = entry.journal.config
59 | if config["highlight"]: # highlight tags
60 | text_fragments = re.split(entry.tag_regex(config["tagsymbols"]), text)
61 |
62 | # Colorizing tags inside of other blocks of text
63 | final_text = ""
64 | previous_piece = ""
65 | for colorized_piece, piece in colorized_text_generator(text_fragments):
66 | # If this piece is entirely punctuation or whitespace or the start
67 | # of a line or the previous piece was a tag or this piece is a tag,
68 | # then add it to the final text without a leading space.
69 | if (
70 | all(char in punctuation + whitespace for char in piece)
71 | or previous_piece.endswith("\n")
72 | or (previous_piece and previous_piece[0] in config["tagsymbols"])
73 | or piece[0] in config["tagsymbols"]
74 | ):
75 | final_text += colorized_piece
76 | else:
77 | # Otherwise add a leading space and then append the piece.
78 | final_text += " " + colorized_piece
79 |
80 | previous_piece = piece
81 | return final_text.lstrip()
82 | else:
83 | return text
84 |
--------------------------------------------------------------------------------
/jrnl/editor.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2012-2023 jrnl contributors
2 | # License: https://www.gnu.org/licenses/gpl-3.0.html
3 |
4 | import logging
5 | import os
6 | import subprocess
7 | import sys
8 | import tempfile
9 | from pathlib import Path
10 |
11 | from jrnl.exception import JrnlException
12 | from jrnl.messages import Message
13 | from jrnl.messages import MsgStyle
14 | from jrnl.messages import MsgText
15 | from jrnl.os_compat import on_windows
16 | from jrnl.os_compat import split_args
17 | from jrnl.output import print_msg
18 | from jrnl.path import absolute_path
19 | from jrnl.path import get_templates_path
20 |
21 |
22 | def get_text_from_editor(config: dict, template: str = "") -> str:
23 | suffix = ".jrnl"
24 | if config["template"]:
25 | template_filename = Path(config["template"]).name
26 | suffix = "-" + template_filename
27 | filehandle, tmpfile = tempfile.mkstemp(prefix="jrnl", text=True, suffix=suffix)
28 | os.close(filehandle)
29 |
30 | with open(tmpfile, "w", encoding="utf-8") as f:
31 | if template:
32 | f.write(template)
33 |
34 | try:
35 | subprocess.call(split_args(config["editor"]) + [tmpfile])
36 | except FileNotFoundError:
37 | raise JrnlException(
38 | Message(
39 | MsgText.EditorMisconfigured,
40 | MsgStyle.ERROR,
41 | {"editor_key": config["editor"]},
42 | )
43 | )
44 |
45 | with open(tmpfile, "r", encoding="utf-8") as f:
46 | raw = f.read()
47 | os.remove(tmpfile)
48 |
49 | if not raw:
50 | raise JrnlException(Message(MsgText.NoTextReceived, MsgStyle.NORMAL))
51 |
52 | return raw
53 |
54 |
55 | def get_text_from_stdin() -> str:
56 | print_msg(
57 | Message(
58 | MsgText.WritingEntryStart,
59 | MsgStyle.TITLE,
60 | {
61 | "how_to_quit": (
62 | MsgText.HowToQuitWindows if on_windows() else MsgText.HowToQuitLinux
63 | )
64 | },
65 | )
66 | )
67 |
68 | try:
69 | raw = sys.stdin.read()
70 | except KeyboardInterrupt:
71 | logging.error("Append mode: keyboard interrupt")
72 | raise JrnlException(
73 | Message(MsgText.KeyboardInterruptMsg, MsgStyle.ERROR_ON_NEW_LINE),
74 | Message(MsgText.JournalNotSaved, MsgStyle.WARNING),
75 | )
76 |
77 | return raw
78 |
79 |
80 | def get_template_path(template_path: str, jrnl_template_dir: str) -> str:
81 | actual_template_path = os.path.join(jrnl_template_dir, template_path)
82 | if not os.path.exists(actual_template_path):
83 | logging.debug(
84 | f"Couldn't open {actual_template_path}. "
85 | "Treating template path like a local / abs path."
86 | )
87 | actual_template_path = absolute_path(template_path)
88 |
89 | return actual_template_path
90 |
91 |
92 | def read_template_file(template_path: str) -> str:
93 | """
94 | Reads the template file given a template path in this order:
95 |
96 | * Check $XDG_DATA_HOME/jrnl/templates/template_path.
97 | * Check template_arg as an absolute / relative path.
98 |
99 | If a file is found, its contents are returned as a string.
100 | If not, a JrnlException is raised.
101 | """
102 |
103 | jrnl_template_dir = get_templates_path()
104 | actual_template_path = get_template_path(template_path, jrnl_template_dir)
105 |
106 | try:
107 | with open(actual_template_path, encoding="utf-8") as f:
108 | template_data = f.read()
109 | return template_data
110 | except FileNotFoundError:
111 | raise JrnlException(
112 | Message(
113 | MsgText.CantReadTemplate,
114 | MsgStyle.ERROR,
115 | {
116 | "template_path": template_path,
117 | "actual_template_path": actual_template_path,
118 | "jrnl_template_dir": str(jrnl_template_dir) + os.sep,
119 | },
120 | )
121 | )
122 |
--------------------------------------------------------------------------------
/jrnl/encryption/BaseEncryption.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2012-2023 jrnl contributors
2 | # License: https://www.gnu.org/licenses/gpl-3.0.html
3 |
4 | import logging
5 | from abc import ABC
6 | from abc import abstractmethod
7 |
8 | from jrnl.exception import JrnlException
9 | from jrnl.messages import Message
10 | from jrnl.messages import MsgStyle
11 | from jrnl.messages import MsgText
12 |
13 |
14 | class BaseEncryption(ABC):
15 | def __init__(self, journal_name: str, config: dict):
16 | logging.debug("start")
17 | self._encoding: str = "utf-8"
18 | self._journal_name: str = journal_name
19 | self._config: dict = config
20 |
21 | def clear(self) -> None:
22 | pass
23 |
24 | def encrypt(self, text: str) -> bytes:
25 | logging.debug("encrypting")
26 | return self._encrypt(text)
27 |
28 | def decrypt(self, text: bytes) -> str:
29 | logging.debug("decrypting")
30 | if (result := self._decrypt(text)) is None:
31 | raise JrnlException(
32 | Message(MsgText.DecryptionFailedGeneric, MsgStyle.ERROR)
33 | )
34 |
35 | return result
36 |
37 | @abstractmethod
38 | def _encrypt(self, text: str) -> bytes:
39 | """
40 | This is needed because self.decrypt might need
41 | to perform actions (e.g. prompt for password)
42 | before actually encrypting.
43 | """
44 | pass
45 |
46 | @abstractmethod
47 | def _decrypt(self, text: bytes) -> str | None:
48 | """
49 | This is needed because self.decrypt might need
50 | to perform actions (e.g. prompt for password)
51 | before actually decrypting.
52 | """
53 | pass
54 |
--------------------------------------------------------------------------------
/jrnl/encryption/BaseKeyEncryption.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2012-2023 jrnl contributors
2 | # License: https://www.gnu.org/licenses/gpl-3.0.html
3 |
4 | from .BaseEncryption import BaseEncryption
5 |
6 |
7 | class BaseKeyEncryption(BaseEncryption):
8 | pass
9 |
--------------------------------------------------------------------------------
/jrnl/encryption/BasePasswordEncryption.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2012-2023 jrnl contributors
2 | # License: https://www.gnu.org/licenses/gpl-3.0.html
3 |
4 | import logging
5 |
6 | from jrnl.encryption.BaseEncryption import BaseEncryption
7 | from jrnl.exception import JrnlException
8 | from jrnl.keyring import get_keyring_password
9 | from jrnl.messages import Message
10 | from jrnl.messages import MsgStyle
11 | from jrnl.messages import MsgText
12 | from jrnl.prompt import create_password
13 | from jrnl.prompt import prompt_password
14 |
15 |
16 | class BasePasswordEncryption(BaseEncryption):
17 | def __init__(self, *args, **kwargs) -> None:
18 | super().__init__(*args, **kwargs)
19 | logging.debug("start")
20 | self._attempts: int = 0
21 | self._max_attempts: int = 3
22 | self._password: str = ""
23 | self._check_keyring: bool = True
24 |
25 | @property
26 | def check_keyring(self) -> bool:
27 | return self._check_keyring
28 |
29 | @check_keyring.setter
30 | def check_keyring(self, value: bool) -> None:
31 | self._check_keyring = value
32 |
33 | @property
34 | def password(self) -> str | None:
35 | return self._password
36 |
37 | @password.setter
38 | def password(self, value: str) -> None:
39 | self._password = value
40 |
41 | def clear(self):
42 | self.password = None
43 | self.check_keyring = False
44 |
45 | def encrypt(self, text: str) -> bytes:
46 | logging.debug("encrypting")
47 | if not self.password:
48 | if self.check_keyring and (
49 | keyring_pw := get_keyring_password(self._journal_name)
50 | ):
51 | self.password = keyring_pw
52 |
53 | if not self.password:
54 | self.password = create_password(self._journal_name)
55 |
56 | return self._encrypt(text)
57 |
58 | def decrypt(self, text: bytes) -> str:
59 | logging.debug("decrypting")
60 | if not self.password:
61 | if self.check_keyring and (
62 | keyring_pw := get_keyring_password(self._journal_name)
63 | ):
64 | self.password = keyring_pw
65 |
66 | if not self.password:
67 | self._prompt_password()
68 |
69 | while (result := self._decrypt(text)) is None:
70 | self._prompt_password()
71 |
72 | return result
73 |
74 | def _prompt_password(self) -> None:
75 | if self._attempts >= self._max_attempts:
76 | raise JrnlException(
77 | Message(MsgText.PasswordMaxTriesExceeded, MsgStyle.ERROR)
78 | )
79 |
80 | first_try = self._attempts == 0
81 | self.password = prompt_password(first_try=first_try)
82 | self._attempts += 1
83 |
--------------------------------------------------------------------------------
/jrnl/encryption/Jrnlv1Encryption.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2012-2023 jrnl contributors
2 | # License: https://www.gnu.org/licenses/gpl-3.0.html
3 |
4 | import hashlib
5 | import logging
6 |
7 | from cryptography.hazmat.backends import default_backend
8 | from cryptography.hazmat.primitives import padding
9 | from cryptography.hazmat.primitives.ciphers import Cipher
10 | from cryptography.hazmat.primitives.ciphers import algorithms
11 | from cryptography.hazmat.primitives.ciphers import modes
12 |
13 | from jrnl.encryption.BasePasswordEncryption import BasePasswordEncryption
14 |
15 |
16 | class Jrnlv1Encryption(BasePasswordEncryption):
17 | def __init__(self, *args, **kwargs) -> None:
18 | super().__init__(*args, **kwargs)
19 | logging.debug("start")
20 |
21 | def _encrypt(self, _: str) -> bytes:
22 | raise NotImplementedError
23 |
24 | def _decrypt(self, text: bytes) -> str | None:
25 | logging.debug("decrypting")
26 | iv, cipher = text[:16], text[16:]
27 | password = self._password or ""
28 | decryption_key = hashlib.sha256(password.encode(self._encoding)).digest()
29 | decryptor = Cipher(
30 | algorithms.AES(decryption_key), modes.CBC(iv), default_backend()
31 | ).decryptor()
32 | try:
33 | plain_padded = decryptor.update(cipher) + decryptor.finalize()
34 | if plain_padded[-1] in (" ", 32):
35 | # Ancient versions of jrnl. Do not judge me.
36 | return plain_padded.decode(self._encoding).rstrip(" ")
37 | else:
38 | unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder()
39 | plain = unpadder.update(plain_padded) + unpadder.finalize()
40 | return plain.decode(self._encoding)
41 | except ValueError:
42 | return None
43 |
--------------------------------------------------------------------------------
/jrnl/encryption/Jrnlv2Encryption.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2012-2023 jrnl contributors
2 | # License: https://www.gnu.org/licenses/gpl-3.0.html
3 |
4 | import base64
5 | import logging
6 |
7 | from cryptography.fernet import Fernet
8 | from cryptography.fernet import InvalidToken
9 | from cryptography.hazmat.backends import default_backend
10 | from cryptography.hazmat.primitives import hashes
11 | from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
12 |
13 | from .BasePasswordEncryption import BasePasswordEncryption
14 |
15 |
16 | class Jrnlv2Encryption(BasePasswordEncryption):
17 | def __init__(self, *args, **kwargs) -> None:
18 | # Salt is hard-coded
19 | self._salt: bytes = b"\xf2\xd5q\x0e\xc1\x8d.\xde\xdc\x8e6t\x89\x04\xce\xf8"
20 | self._key: bytes = b""
21 |
22 | super().__init__(*args, **kwargs)
23 | logging.debug("start")
24 |
25 | @property
26 | def password(self):
27 | return self._password
28 |
29 | @password.setter
30 | def password(self, value: str | None):
31 | self._password = value
32 | self._make_key()
33 |
34 | def _make_key(self) -> None:
35 | if self._password is None:
36 | # Password was removed after being set
37 | self._key = None
38 | return
39 | password = self.password.encode(self._encoding)
40 | kdf = PBKDF2HMAC(
41 | algorithm=hashes.SHA256(),
42 | length=32,
43 | salt=self._salt,
44 | iterations=100_000,
45 | backend=default_backend(),
46 | )
47 | key = kdf.derive(password)
48 | self._key = base64.urlsafe_b64encode(key)
49 |
50 | def _encrypt(self, text: str) -> bytes:
51 | logging.debug("encrypting")
52 | return Fernet(self._key).encrypt(text.encode(self._encoding))
53 |
54 | def _decrypt(self, text: bytes) -> str | None:
55 | logging.debug("decrypting")
56 | try:
57 | return Fernet(self._key).decrypt(text).decode(self._encoding)
58 | except (InvalidToken, IndexError):
59 | return None
60 |
--------------------------------------------------------------------------------
/jrnl/encryption/NoEncryption.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2012-2023 jrnl contributors
2 | # License: https://www.gnu.org/licenses/gpl-3.0.html
3 |
4 | import logging
5 |
6 | from jrnl.encryption.BaseEncryption import BaseEncryption
7 |
8 |
9 | class NoEncryption(BaseEncryption):
10 | def __init__(self, *args, **kwargs):
11 | super().__init__(*args, **kwargs)
12 | logging.debug("start")
13 |
14 | def _encrypt(self, text: str) -> bytes:
15 | logging.debug("encrypting")
16 | return text.encode(self._encoding)
17 |
18 | def _decrypt(self, text: bytes) -> str:
19 | logging.debug("decrypting")
20 | return text.decode(self._encoding)
21 |
--------------------------------------------------------------------------------
/jrnl/encryption/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2012-2023 jrnl contributors
2 | # License: https://www.gnu.org/licenses/gpl-3.0.html
3 |
4 | from enum import Enum
5 | from importlib import import_module
6 | from typing import TYPE_CHECKING
7 | from typing import Type
8 |
9 | if TYPE_CHECKING:
10 | from .BaseEncryption import BaseEncryption
11 |
12 |
13 | class EncryptionMethods(str, Enum):
14 | def __str__(self) -> str:
15 | return self.value
16 |
17 | NONE = "NoEncryption"
18 | JRNLV1 = "Jrnlv1Encryption"
19 | JRNLV2 = "Jrnlv2Encryption"
20 |
21 |
22 | def determine_encryption_method(config: str | bool) -> Type["BaseEncryption"]:
23 | ENCRYPTION_METHODS = {
24 | True: EncryptionMethods.JRNLV2, # the default
25 | False: EncryptionMethods.NONE,
26 | "jrnlv1": EncryptionMethods.JRNLV1,
27 | "jrnlv2": EncryptionMethods.JRNLV2,
28 | }
29 |
30 | key = config
31 | if isinstance(config, str):
32 | key = config.lower()
33 |
34 | my_class = ENCRYPTION_METHODS[key]
35 |
36 | return getattr(import_module(f"jrnl.encryption.{my_class}"), my_class)
37 |
--------------------------------------------------------------------------------
/jrnl/exception.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2012-2023 jrnl contributors
2 | # License: https://www.gnu.org/licenses/gpl-3.0.html
3 |
4 | from typing import TYPE_CHECKING
5 |
6 | from jrnl.output import print_msg
7 |
8 | if TYPE_CHECKING:
9 | from jrnl.messages import Message
10 | from jrnl.messages import MsgText
11 |
12 |
13 | class JrnlException(Exception):
14 | """Common exceptions raised by jrnl."""
15 |
16 | def __init__(self, *messages: "Message"):
17 | self.messages = messages
18 |
19 | def print(self) -> None:
20 | for msg in self.messages:
21 | print_msg(msg)
22 |
23 | def has_message_text(self, message_text: "MsgText"):
24 | return any([m.text == message_text for m in self.messages])
25 |
--------------------------------------------------------------------------------
/jrnl/journals/__init__.py:
--------------------------------------------------------------------------------
1 | from .DayOneJournal import DayOne
2 | from .Entry import Entry
3 | from .FolderJournal import Folder
4 | from .Journal import Journal
5 | from .Journal import open_journal
6 |
--------------------------------------------------------------------------------
/jrnl/keyring.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2012-2023 jrnl contributors
2 | # License: https://www.gnu.org/licenses/gpl-3.0.html
3 |
4 | import keyring
5 |
6 | from jrnl.messages import Message
7 | from jrnl.messages import MsgStyle
8 | from jrnl.messages import MsgText
9 | from jrnl.output import print_msg
10 |
11 |
12 | def get_keyring_password(journal_name: str = "default") -> str | None:
13 | try:
14 | return keyring.get_password("jrnl", journal_name)
15 | except keyring.errors.KeyringError as e:
16 | if not isinstance(e, keyring.errors.NoKeyringError):
17 | print_msg(Message(MsgText.KeyringRetrievalFailure, MsgStyle.ERROR))
18 | return None
19 |
20 |
21 | def set_keyring_password(password: str, journal_name: str = "default") -> None:
22 | try:
23 | return keyring.set_password("jrnl", journal_name, password)
24 | except keyring.errors.KeyringError as e:
25 | if isinstance(e, keyring.errors.NoKeyringError):
26 | msg = Message(MsgText.KeyringBackendNotFound, MsgStyle.WARNING)
27 | else:
28 | msg = Message(MsgText.KeyringRetrievalFailure, MsgStyle.ERROR)
29 | print_msg(msg)
30 |
--------------------------------------------------------------------------------
/jrnl/main.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2012-2023 jrnl contributors
2 | # License: https://www.gnu.org/licenses/gpl-3.0.html
3 |
4 | import logging
5 | import sys
6 | import traceback
7 |
8 | from rich.logging import RichHandler
9 |
10 | from jrnl import controller
11 | from jrnl.args import parse_args
12 | from jrnl.exception import JrnlException
13 | from jrnl.messages import Message
14 | from jrnl.messages import MsgStyle
15 | from jrnl.messages import MsgText
16 | from jrnl.output import print_msg
17 |
18 |
19 | def configure_logger(debug: bool = False) -> None:
20 | if not debug:
21 | logging.disable()
22 | return
23 |
24 | logging.basicConfig(
25 | level=logging.DEBUG,
26 | datefmt="[%X]",
27 | format="%(message)s",
28 | handlers=[RichHandler()],
29 | )
30 | logging.getLogger("parsedatetime").setLevel(logging.INFO)
31 | logging.getLogger("keyring.backend").setLevel(logging.ERROR)
32 | logging.debug("Logging start")
33 |
34 |
35 | def run(manual_args: list[str] | None = None) -> int:
36 | try:
37 | if manual_args is None:
38 | manual_args = sys.argv[1:]
39 |
40 | args = parse_args(manual_args)
41 | configure_logger(args.debug)
42 | logging.debug("Parsed args:\n%s", args)
43 |
44 | status_code = controller.run(args)
45 |
46 | except JrnlException as e:
47 | status_code = 1
48 | e.print()
49 |
50 | except KeyboardInterrupt:
51 | status_code = 1
52 |
53 | print_msg(
54 | Message(
55 | MsgText.KeyboardInterruptMsg,
56 | MsgStyle.ERROR_ON_NEW_LINE,
57 | )
58 | )
59 |
60 | except Exception as e:
61 | # uncaught exception
62 | status_code = 1
63 | debug = False
64 | try:
65 | if args.debug: # type: ignore
66 | debug = True
67 | except NameError:
68 | # This should only happen when the exception
69 | # happened before the args were parsed
70 | if "--debug" in sys.argv:
71 | debug = True
72 |
73 | if debug:
74 | from rich.console import Console
75 |
76 | traceback.print_tb(sys.exc_info()[2])
77 | Console(stderr=True).print_exception(extra_lines=1)
78 |
79 | print_msg(
80 | Message(
81 | MsgText.UncaughtException,
82 | MsgStyle.ERROR,
83 | {"name": type(e).__name__, "exception": e},
84 | )
85 | )
86 |
87 | # This should be the only exit point
88 | return status_code
89 |
--------------------------------------------------------------------------------
/jrnl/messages/Message.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2012-2023 jrnl contributors
2 | # License: https://www.gnu.org/licenses/gpl-3.0.html
3 |
4 | from typing import TYPE_CHECKING
5 | from typing import Mapping
6 | from typing import NamedTuple
7 |
8 | from jrnl.messages.MsgStyle import MsgStyle
9 |
10 | if TYPE_CHECKING:
11 | from jrnl.messages.MsgText import MsgText
12 |
13 |
14 | class Message(NamedTuple):
15 | text: "MsgText"
16 | style: MsgStyle = MsgStyle.NORMAL
17 | params: Mapping = {}
18 |
--------------------------------------------------------------------------------
/jrnl/messages/MsgStyle.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2012-2023 jrnl contributors
2 | # License: https://www.gnu.org/licenses/gpl-3.0.html
3 |
4 | from enum import Enum
5 | from typing import Callable
6 | from typing import NamedTuple
7 |
8 | from rich import box
9 | from rich.panel import Panel
10 |
11 | from jrnl.messages.MsgText import MsgText
12 |
13 |
14 | class MsgStyle(Enum):
15 | class _Color(NamedTuple):
16 | """
17 | String representing a standard color to display
18 | see: https://rich.readthedocs.io/en/stable/appendix/colors.html
19 | """
20 |
21 | color: str
22 |
23 | class _Decoration(Enum):
24 | NONE = {
25 | "callback": lambda x, **_: x,
26 | "args": {},
27 | }
28 | BOX = {
29 | "callback": Panel,
30 | "args": {
31 | "expand": False,
32 | "padding": (0, 2),
33 | "title_align": "left",
34 | "box": box.HEAVY,
35 | },
36 | }
37 |
38 | @property
39 | def callback(self) -> Callable:
40 | return self.value["callback"]
41 |
42 | @property
43 | def args(self) -> dict:
44 | return self.value["args"]
45 |
46 | PROMPT = {
47 | "decoration": _Decoration.NONE,
48 | "color": _Color("white"),
49 | "append_space": True,
50 | }
51 | TITLE = {
52 | "decoration": _Decoration.BOX,
53 | "color": _Color("cyan"),
54 | }
55 | NORMAL = {
56 | "decoration": _Decoration.BOX,
57 | "color": _Color("white"),
58 | }
59 | WARNING = {
60 | "decoration": _Decoration.BOX,
61 | "color": _Color("yellow"),
62 | }
63 | ERROR = {
64 | "decoration": _Decoration.BOX,
65 | "color": _Color("red"),
66 | "box_title": str(MsgText.Error),
67 | }
68 | ERROR_ON_NEW_LINE = {
69 | "decoration": _Decoration.BOX,
70 | "color": _Color("red"),
71 | "prepend_newline": True,
72 | "box_title": str(MsgText.Error),
73 | }
74 |
75 | @property
76 | def decoration(self) -> _Decoration:
77 | return self.value["decoration"]
78 |
79 | @property
80 | def color(self) -> _Color:
81 | return self.value["color"].color
82 |
83 | @property
84 | def prepend_newline(self) -> bool:
85 | return self.value.get("prepend_newline", False)
86 |
87 | @property
88 | def append_space(self) -> bool:
89 | return self.value.get("append_space", False)
90 |
91 | @property
92 | def box_title(self) -> MsgText:
93 | return self.value.get("box_title")
94 |
--------------------------------------------------------------------------------
/jrnl/messages/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2012-2023 jrnl contributors
2 | # License: https://www.gnu.org/licenses/gpl-3.0.html
3 |
4 | from jrnl.messages import Message
5 | from jrnl.messages import MsgStyle
6 | from jrnl.messages import MsgText
7 |
8 | Message = Message.Message
9 | MsgStyle = MsgStyle.MsgStyle
10 | MsgText = MsgText.MsgText
11 |
--------------------------------------------------------------------------------
/jrnl/os_compat.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2012-2023 jrnl contributors
2 | # License: https://www.gnu.org/licenses/gpl-3.0.html
3 |
4 | import shlex
5 | from sys import platform
6 |
7 |
8 | def on_windows() -> bool:
9 | return "win32" in platform
10 |
11 |
12 | def on_posix() -> bool:
13 | return not on_windows()
14 |
15 |
16 | def split_args(args: str) -> list[str]:
17 | """Split arguments and add escape characters as appropriate for the OS"""
18 | return shlex.split(args, posix=on_posix())
19 |
--------------------------------------------------------------------------------
/jrnl/override.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2012-2023 jrnl contributors
2 | # License: https://www.gnu.org/licenses/gpl-3.0.html
3 |
4 | from typing import TYPE_CHECKING
5 |
6 | from jrnl.config import make_yaml_valid_dict
7 | from jrnl.config import update_config
8 |
9 | if TYPE_CHECKING:
10 | from argparse import Namespace
11 |
12 |
13 | # import logging
14 | def apply_overrides(args: "Namespace", base_config: dict) -> dict:
15 | """Unpack CLI provided overrides into the configuration tree.
16 |
17 | :param overrides: List of configuration key-value pairs collected from the CLI
18 | :type overrides: list
19 | :param base_config: Configuration Loaded from the saved YAML
20 | :type base_config: dict
21 | :return: Configuration to be used during runtime with the overrides applied
22 | :rtype: dict
23 | """
24 | overrides = vars(args).get("config_override")
25 | if not overrides:
26 | return base_config
27 |
28 | cfg_with_overrides = base_config.copy()
29 | for pairs in overrides:
30 | pairs = make_yaml_valid_dict(pairs)
31 | key_as_dots, override_value = _get_key_and_value_from_pair(pairs)
32 | keys = _convert_dots_to_list(key_as_dots)
33 | cfg_with_overrides = _recursively_apply(
34 | cfg_with_overrides, keys, override_value
35 | )
36 |
37 | update_config(base_config, cfg_with_overrides, None)
38 | return base_config
39 |
40 |
41 | def _get_key_and_value_from_pair(pairs: dict) -> tuple:
42 | key_as_dots, override_value = list(pairs.items())[0]
43 | return key_as_dots, override_value
44 |
45 |
46 | def _convert_dots_to_list(key_as_dots: str) -> list[str]:
47 | keys = key_as_dots.split(".")
48 | keys = [k for k in keys if k != ""] # remove empty elements
49 | return keys
50 |
51 |
52 | def _recursively_apply(tree: dict, nodes: list, override_value) -> dict:
53 | """Recurse through configuration and apply overrides at the leaf of the config tree
54 |
55 | Credit to iJames on SO: https://stackoverflow.com/a/47276490 for algorithm
56 |
57 | Args:
58 | config (dict): Configuration to modify
59 | nodes (list): Vector of override keys; the length of the vector indicates tree
60 | depth
61 | override_value (str): Runtime override passed from the command-line
62 | """
63 | key = nodes[0]
64 | if len(nodes) == 1:
65 | tree[key] = override_value
66 | else:
67 | next_key = nodes[1:]
68 | next_node = _get_config_node(tree, key)
69 | _recursively_apply(next_node, next_key, override_value)
70 |
71 | return tree
72 |
73 |
74 | def _get_config_node(config: dict, key: str):
75 | if key in config:
76 | pass
77 | else:
78 | config[key] = None
79 | return config[key]
80 |
--------------------------------------------------------------------------------
/jrnl/path.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2012-2023 jrnl contributors
2 | # License: https://www.gnu.org/licenses/gpl-3.0.html
3 |
4 | import os.path
5 | from pathlib import Path
6 |
7 | import xdg.BaseDirectory
8 |
9 | from jrnl.exception import JrnlException
10 | from jrnl.messages import Message
11 | from jrnl.messages import MsgStyle
12 | from jrnl.messages import MsgText
13 |
14 | # Constants
15 | XDG_RESOURCE = "jrnl"
16 | DEFAULT_CONFIG_NAME = "jrnl.yaml"
17 | DEFAULT_JOURNAL_NAME = "journal.txt"
18 |
19 |
20 | def home_dir() -> str:
21 | return os.path.expanduser("~")
22 |
23 |
24 | def expand_path(path: str) -> str:
25 | return os.path.expanduser(os.path.expandvars(path))
26 |
27 |
28 | def absolute_path(path: str) -> str:
29 | return os.path.abspath(expand_path(path))
30 |
31 |
32 | def get_default_journal_path() -> str:
33 | journal_data_path = xdg.BaseDirectory.save_data_path(XDG_RESOURCE) or home_dir()
34 | return os.path.join(journal_data_path, DEFAULT_JOURNAL_NAME)
35 |
36 |
37 | def get_templates_path() -> str:
38 | """
39 | Get the path to the XDG templates directory. Creates the directory if it
40 | doesn't exist.
41 | """
42 | # jrnl_xdg_resource_path is created by save_data_path if it does not exist
43 | jrnl_xdg_resource_path = Path(xdg.BaseDirectory.save_data_path(XDG_RESOURCE))
44 | jrnl_templates_path = jrnl_xdg_resource_path / "templates"
45 | # Create the directory if needed.
46 | jrnl_templates_path.mkdir(exist_ok=True)
47 | return str(jrnl_templates_path)
48 |
49 |
50 | def get_config_directory() -> str:
51 | try:
52 | return xdg.BaseDirectory.save_config_path(XDG_RESOURCE)
53 | except FileExistsError:
54 | raise JrnlException(
55 | Message(
56 | MsgText.ConfigDirectoryIsFile,
57 | MsgStyle.ERROR,
58 | {
59 | "config_directory_path": os.path.join(
60 | xdg.BaseDirectory.xdg_config_home, XDG_RESOURCE
61 | )
62 | },
63 | ),
64 | )
65 |
66 |
67 | def get_config_path() -> str:
68 | try:
69 | config_directory_path = get_config_directory()
70 | except JrnlException:
71 | return os.path.join(home_dir(), DEFAULT_CONFIG_NAME)
72 | return os.path.join(config_directory_path, DEFAULT_CONFIG_NAME)
73 |
--------------------------------------------------------------------------------
/jrnl/plugins/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2012-2023 jrnl contributors
2 | # License: https://www.gnu.org/licenses/gpl-3.0.html
3 |
4 | from typing import Type
5 |
6 | from jrnl.plugins.calendar_heatmap_exporter import CalendarHeatmapExporter
7 | from jrnl.plugins.dates_exporter import DatesExporter
8 | from jrnl.plugins.fancy_exporter import FancyExporter
9 | from jrnl.plugins.jrnl_importer import JRNLImporter
10 | from jrnl.plugins.json_exporter import JSONExporter
11 | from jrnl.plugins.markdown_exporter import MarkdownExporter
12 | from jrnl.plugins.tag_exporter import TagExporter
13 | from jrnl.plugins.text_exporter import TextExporter
14 | from jrnl.plugins.xml_exporter import XMLExporter
15 | from jrnl.plugins.yaml_exporter import YAMLExporter
16 |
17 | __exporters = [
18 | CalendarHeatmapExporter,
19 | DatesExporter,
20 | FancyExporter,
21 | JSONExporter,
22 | MarkdownExporter,
23 | TagExporter,
24 | TextExporter,
25 | XMLExporter,
26 | YAMLExporter,
27 | ]
28 | __importers = [JRNLImporter]
29 |
30 | __exporter_types = {name: plugin for plugin in __exporters for name in plugin.names}
31 | __exporter_types["pretty"] = None
32 | __exporter_types["short"] = None
33 | __importer_types = {name: plugin for plugin in __importers for name in plugin.names}
34 |
35 | EXPORT_FORMATS = sorted(__exporter_types.keys())
36 | IMPORT_FORMATS = sorted(__importer_types.keys())
37 |
38 |
39 | def get_exporter(format: str) -> Type[TextExporter] | None:
40 | for exporter in __exporters:
41 | if hasattr(exporter, "names") and format in exporter.names:
42 | return exporter
43 | return None
44 |
45 |
46 | def get_importer(format: str) -> Type[JRNLImporter] | None:
47 | for importer in __importers:
48 | if hasattr(importer, "names") and format in importer.names:
49 | return importer
50 | return None
51 |
--------------------------------------------------------------------------------
/jrnl/plugins/dates_exporter.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2012-2023 jrnl contributors
2 | # License: https://www.gnu.org/licenses/gpl-3.0.html
3 |
4 | from typing import TYPE_CHECKING
5 |
6 | from jrnl.plugins.text_exporter import TextExporter
7 | from jrnl.plugins.util import get_journal_frequency_one_level
8 |
9 | if TYPE_CHECKING:
10 | from jrnl.journals import Entry
11 | from jrnl.journals import Journal
12 |
13 |
14 | class DatesExporter(TextExporter):
15 | """This Exporter lists dates and their respective counts, for heatingmapping etc."""
16 |
17 | names = ["dates"]
18 | extension = "dates"
19 |
20 | @classmethod
21 | def export_entry(cls, entry: "Entry"):
22 | raise NotImplementedError
23 |
24 | @classmethod
25 | def export_journal(cls, journal: "Journal") -> str:
26 | """Returns dates and their frequencies for an entire journal."""
27 | date_counts = get_journal_frequency_one_level(journal)
28 | result = "\n".join(f"{date}, {count}" for date, count in date_counts.items())
29 | return result
30 |
--------------------------------------------------------------------------------
/jrnl/plugins/jrnl_importer.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2012-2023 jrnl contributors
2 | # License: https://www.gnu.org/licenses/gpl-3.0.html
3 |
4 | import sys
5 | from typing import TYPE_CHECKING
6 |
7 | from jrnl.exception import JrnlException
8 | from jrnl.messages import Message
9 | from jrnl.messages import MsgStyle
10 | from jrnl.messages import MsgText
11 | from jrnl.output import print_msg
12 |
13 | if TYPE_CHECKING:
14 | from jrnl.journals import Journal
15 |
16 |
17 | class JRNLImporter:
18 | """This plugin imports entries from other jrnl files."""
19 |
20 | names = ["jrnl"]
21 |
22 | @staticmethod
23 | def import_(journal: "Journal", input: str | None = None) -> None:
24 | """Imports from an existing file if input is specified, and
25 | standard input otherwise."""
26 | old_cnt = len(journal.entries)
27 | if input:
28 | with open(input, "r", encoding="utf-8") as f:
29 | other_journal_txt = f.read()
30 | else:
31 | try:
32 | other_journal_txt = sys.stdin.read()
33 | except KeyboardInterrupt:
34 | raise JrnlException(
35 | Message(MsgText.KeyboardInterruptMsg, MsgStyle.ERROR_ON_NEW_LINE),
36 | Message(MsgText.ImportAborted, MsgStyle.WARNING),
37 | )
38 |
39 | journal.import_(other_journal_txt)
40 | new_cnt = len(journal.entries)
41 | journal.write()
42 | print_msg(
43 | Message(
44 | MsgText.ImportSummary,
45 | MsgStyle.NORMAL,
46 | {
47 | "count": new_cnt - old_cnt,
48 | "journal_name": journal.name,
49 | },
50 | )
51 | )
52 |
--------------------------------------------------------------------------------
/jrnl/plugins/json_exporter.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2012-2023 jrnl contributors
2 | # License: https://www.gnu.org/licenses/gpl-3.0.html
3 |
4 | import json
5 | from typing import TYPE_CHECKING
6 |
7 | from jrnl.plugins.text_exporter import TextExporter
8 | from jrnl.plugins.util import get_tags_count
9 |
10 | if TYPE_CHECKING:
11 | from jrnl.journals import Entry
12 | from jrnl.journals import Journal
13 |
14 |
15 | class JSONExporter(TextExporter):
16 | """This Exporter can convert entries and journals into json."""
17 |
18 | names = ["json"]
19 | extension = "json"
20 |
21 | @classmethod
22 | def entry_to_dict(cls, entry: "Entry") -> dict:
23 | entry_dict = {
24 | "title": entry.title,
25 | "body": entry.body,
26 | "date": entry.date.strftime("%Y-%m-%d"),
27 | "time": entry.date.strftime("%H:%M"),
28 | "tags": entry.tags,
29 | "starred": entry.starred,
30 | }
31 | if hasattr(entry, "uuid"):
32 | entry_dict["uuid"] = entry.uuid
33 | if (
34 | hasattr(entry, "creator_device_agent")
35 | or hasattr(entry, "creator_generation_date")
36 | or hasattr(entry, "creator_host_name")
37 | or hasattr(entry, "creator_os_agent")
38 | or hasattr(entry, "creator_software_agent")
39 | ):
40 | entry_dict["creator"] = {}
41 | if hasattr(entry, "creator_device_agent"):
42 | entry_dict["creator"]["device_agent"] = entry.creator_device_agent
43 | if hasattr(entry, "creator_generation_date"):
44 | entry_dict["creator"]["generation_date"] = str(
45 | entry.creator_generation_date
46 | )
47 | if hasattr(entry, "creator_host_name"):
48 | entry_dict["creator"]["host_name"] = entry.creator_host_name
49 | if hasattr(entry, "creator_os_agent"):
50 | entry_dict["creator"]["os_agent"] = entry.creator_os_agent
51 | if hasattr(entry, "creator_software_agent"):
52 | entry_dict["creator"]["software_agent"] = entry.creator_software_agent
53 |
54 | return entry_dict
55 |
56 | @classmethod
57 | def export_entry(cls, entry: "Entry") -> str:
58 | """Returns a json representation of a single entry."""
59 | return json.dumps(cls.entry_to_dict(entry), indent=2) + "\n"
60 |
61 | @classmethod
62 | def export_journal(cls, journal: "Journal") -> str:
63 | """Returns a json representation of an entire journal."""
64 | tags = get_tags_count(journal)
65 | result = {
66 | "tags": {tag: count for count, tag in tags},
67 | "entries": [cls.entry_to_dict(e) for e in journal.entries],
68 | }
69 | return json.dumps(result, indent=2)
70 |
--------------------------------------------------------------------------------
/jrnl/plugins/markdown_exporter.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2012-2023 jrnl contributors
2 | # License: https://www.gnu.org/licenses/gpl-3.0.html
3 |
4 | import os
5 | import re
6 | from typing import TYPE_CHECKING
7 |
8 | from jrnl.messages import Message
9 | from jrnl.messages import MsgStyle
10 | from jrnl.messages import MsgText
11 | from jrnl.output import print_msg
12 | from jrnl.plugins.text_exporter import TextExporter
13 |
14 | if TYPE_CHECKING:
15 | from jrnl.journals import Entry
16 | from jrnl.journals import Journal
17 |
18 |
19 | class MarkdownExporter(TextExporter):
20 | """This Exporter can convert entries and journals into Markdown."""
21 |
22 | names = ["md", "markdown"]
23 | extension = "md"
24 |
25 | @classmethod
26 | def export_entry(cls, entry: "Entry", to_multifile: bool = True) -> str:
27 | """Returns a markdown representation of a single entry."""
28 | date_str = entry.date.strftime(entry.journal.config["timeformat"])
29 | body_wrapper = "\n" if entry.body else ""
30 | body = body_wrapper + entry.body
31 |
32 | if to_multifile is True:
33 | heading = "#"
34 | else:
35 | heading = "###"
36 |
37 | """Increase heading levels in body text"""
38 | newbody = ""
39 | previous_line = ""
40 | warn_on_heading_level = False
41 | for line in body.splitlines(True):
42 | if re.match(r"^#+ ", line):
43 | """ATX style headings"""
44 | newbody = newbody + previous_line + heading + line
45 | if re.match(r"^#######+ ", heading + line):
46 | warn_on_heading_level = True
47 | line = ""
48 | elif re.match(r"^=+$", line.rstrip()) and not re.match(
49 | r"^$", previous_line.strip()
50 | ):
51 | """Setext style H1"""
52 | newbody = newbody + heading + "# " + previous_line
53 | line = ""
54 | elif re.match(r"^-+$", line.rstrip()) and not re.match(
55 | r"^$", previous_line.strip()
56 | ):
57 | """Setext style H2"""
58 | newbody = newbody + heading + "## " + previous_line
59 | line = ""
60 | else:
61 | newbody = newbody + previous_line
62 | previous_line = line
63 | newbody = newbody + previous_line # add very last line
64 |
65 | # make sure the export ends with a blank line
66 | if previous_line not in ["\r", "\n", "\r\n", "\n\r"]:
67 | newbody = newbody + os.linesep
68 |
69 | if warn_on_heading_level is True:
70 | print_msg(
71 | Message(
72 | MsgText.HeadingsPastH6,
73 | MsgStyle.WARNING,
74 | {"date": date_str, "title": entry.title},
75 | )
76 | )
77 |
78 | return f"{heading} {date_str} {entry.title}\n{newbody} "
79 |
80 | @classmethod
81 | def export_journal(cls, journal: "Journal") -> str:
82 | """Returns a Markdown representation of an entire journal."""
83 | out = []
84 | year, month = -1, -1
85 | for e in journal.entries:
86 | if e.date.year != year:
87 | year = e.date.year
88 | out.append("# " + str(year))
89 | out.append("")
90 | if e.date.month != month:
91 | month = e.date.month
92 | out.append("## " + e.date.strftime("%B"))
93 | out.append("")
94 | out.append(cls.export_entry(e, False))
95 | result = "\n".join(out)
96 | return result
97 |
--------------------------------------------------------------------------------
/jrnl/plugins/tag_exporter.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2012-2023 jrnl contributors
2 | # License: https://www.gnu.org/licenses/gpl-3.0.html
3 |
4 | from typing import TYPE_CHECKING
5 |
6 | from jrnl.plugins.text_exporter import TextExporter
7 | from jrnl.plugins.util import get_tags_count
8 |
9 | if TYPE_CHECKING:
10 | from jrnl.journals import Entry
11 | from jrnl.journals import Journal
12 |
13 |
14 | class TagExporter(TextExporter):
15 | """This Exporter lists the tags for entries and journals."""
16 |
17 | names = ["tags"]
18 | extension = "tags"
19 |
20 | @classmethod
21 | def export_entry(cls, entry: "Entry") -> str:
22 | """Returns a list of tags for a single entry."""
23 | return ", ".join(entry.tags)
24 |
25 | @classmethod
26 | def export_journal(cls, journal: "Journal") -> str:
27 | """Returns a list of tags and their frequency for an entire journal."""
28 | tag_counts = get_tags_count(journal)
29 | result = ""
30 | if not tag_counts:
31 | return "[No tags found in journal.]"
32 | elif min(tag_counts)[0] == 0:
33 | tag_counts = filter(lambda x: x[0] > 1, tag_counts)
34 | result += "[Removed tags that appear only once.]\n"
35 | result += "\n".join(
36 | "{:20} : {}".format(tag, n) for n, tag in sorted(tag_counts, reverse=True)
37 | )
38 | return result
39 |
--------------------------------------------------------------------------------
/jrnl/plugins/util.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2012-2023 jrnl contributors
2 | # License: https://www.gnu.org/licenses/gpl-3.0.html
3 |
4 | from collections import Counter
5 | from typing import TYPE_CHECKING
6 |
7 | if TYPE_CHECKING:
8 | from jrnl.journals import Journal
9 |
10 |
11 | class NestedDict(dict):
12 | """https://stackoverflow.com/a/74873621/8740440"""
13 |
14 | def __missing__(self, x):
15 | self[x] = NestedDict()
16 | return self[x]
17 |
18 |
19 | def get_tags_count(journal: "Journal") -> set[tuple[int, str]]:
20 | """Returns a set of tuples (count, tag) for all tags present in the journal."""
21 | # Astute reader: should the following line leave you as puzzled as me the first time
22 | # I came across this construction, worry not and embrace the ensuing moment of
23 | # enlightment.
24 | tags = [tag for entry in journal.entries for tag in set(entry.tags)]
25 | # To be read: [for entry in journal.entries: for tag in set(entry.tags): tag]
26 | tag_counts = {(tags.count(tag), tag) for tag in tags}
27 | return tag_counts
28 |
29 |
30 | def oxford_list(lst: list) -> str:
31 | """Return Human-readable list of things obeying the object comma)"""
32 | lst = sorted(lst)
33 | if not lst:
34 | return "(nothing)"
35 | elif len(lst) == 1:
36 | return lst[0]
37 | elif len(lst) == 2:
38 | return lst[0] + " or " + lst[1]
39 | else:
40 | return ", ".join(lst[:-1]) + ", or " + lst[-1]
41 |
42 |
43 | def get_journal_frequency_nested(journal: "Journal") -> NestedDict:
44 | """Returns a NestedDict of the form {year: {month: {day: count}}}"""
45 | journal_frequency = NestedDict()
46 | for entry in journal.entries:
47 | date = entry.date.date()
48 | if date.day in journal_frequency[date.year][date.month]:
49 | journal_frequency[date.year][date.month][date.day] += 1
50 | else:
51 | journal_frequency[date.year][date.month][date.day] = 1
52 |
53 | return journal_frequency
54 |
55 |
56 | def get_journal_frequency_one_level(journal: "Journal") -> Counter:
57 | """Returns a Counter of the form {date (YYYY-MM-DD): count}"""
58 | date_counts = Counter()
59 | for entry in journal.entries:
60 | # entry.date.date() gets date without time
61 | date = str(entry.date.date())
62 | date_counts[date] += 1
63 | return date_counts
64 |
--------------------------------------------------------------------------------
/jrnl/plugins/xml_exporter.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2012-2023 jrnl contributors
2 | # License: https://www.gnu.org/licenses/gpl-3.0.html
3 |
4 | from typing import TYPE_CHECKING
5 | from xml.dom import minidom
6 |
7 | from jrnl.plugins.json_exporter import JSONExporter
8 | from jrnl.plugins.util import get_tags_count
9 |
10 | if TYPE_CHECKING:
11 | from jrnl.journals import Entry
12 | from jrnl.journals import Journal
13 |
14 |
15 | class XMLExporter(JSONExporter):
16 | """This Exporter can convert entries and journals into XML."""
17 |
18 | names = ["xml"]
19 | extension = "xml"
20 |
21 | @classmethod
22 | def export_entry(
23 | cls, entry: "Entry", doc: minidom.Document | None = None
24 | ) -> minidom.Element | str:
25 | """Returns an XML representation of a single entry."""
26 | doc_el = doc or minidom.Document()
27 | entry_el = doc_el.createElement("entry")
28 | for key, value in cls.entry_to_dict(entry).items():
29 | elem = doc_el.createElement(key)
30 | elem.appendChild(doc_el.createTextNode(value))
31 | entry_el.appendChild(elem)
32 | if not doc:
33 | doc_el.appendChild(entry_el)
34 | return doc_el.toprettyxml()
35 | else:
36 | return entry_el
37 |
38 | @classmethod
39 | def entry_to_xml(cls, entry: "Entry", doc: minidom.Document) -> minidom.Element:
40 | entry_el = doc.createElement("entry")
41 | entry_el.setAttribute("date", entry.date.isoformat())
42 | if hasattr(entry, "uuid"):
43 | entry_el.setAttribute("uuid", entry.uuid)
44 | entry_el.setAttribute("starred", entry.starred)
45 | tags = entry.tags
46 | for tag in tags:
47 | tag_el = doc.createElement("tag")
48 | tag_el.setAttribute("name", tag)
49 | entry_el.appendChild(tag_el)
50 | entry_el.appendChild(doc.createTextNode(entry.fulltext))
51 | return entry_el
52 |
53 | @classmethod
54 | def export_journal(cls, journal: "Journal") -> str:
55 | """Returns an XML representation of an entire journal."""
56 | tags = get_tags_count(journal)
57 | doc = minidom.Document()
58 | xml = doc.createElement("journal")
59 | tags_el = doc.createElement("tags")
60 | entries_el = doc.createElement("entries")
61 | for count, tag in tags:
62 | tag_el = doc.createElement("tag")
63 | tag_el.setAttribute("name", tag)
64 | count_node = doc.createTextNode(str(count))
65 | tag_el.appendChild(count_node)
66 | tags_el.appendChild(tag_el)
67 | for entry in journal.entries:
68 | entries_el.appendChild(cls.entry_to_xml(entry, doc))
69 | xml.appendChild(entries_el)
70 | xml.appendChild(tags_el)
71 | doc.appendChild(xml)
72 | return doc.toprettyxml()
73 |
--------------------------------------------------------------------------------
/jrnl/prompt.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2012-2023 jrnl contributors
2 | # License: https://www.gnu.org/licenses/gpl-3.0.html
3 |
4 | from jrnl.messages import Message
5 | from jrnl.messages import MsgStyle
6 | from jrnl.messages import MsgText
7 | from jrnl.output import print_msg
8 | from jrnl.output import print_msgs
9 |
10 |
11 | def create_password(journal_name: str) -> str:
12 | kwargs = {
13 | "get_input": True,
14 | "hide_input": True,
15 | }
16 | while True:
17 | pw = print_msg(
18 | Message(
19 | MsgText.PasswordFirstEntry,
20 | MsgStyle.PROMPT,
21 | params={"journal_name": journal_name},
22 | ),
23 | **kwargs
24 | )
25 |
26 | if not pw:
27 | print_msg(Message(MsgText.PasswordCanNotBeEmpty, MsgStyle.WARNING))
28 | continue
29 |
30 | elif pw == print_msg(
31 | Message(MsgText.PasswordConfirmEntry, MsgStyle.PROMPT), **kwargs
32 | ):
33 | break
34 |
35 | print_msg(Message(MsgText.PasswordDidNotMatch, MsgStyle.ERROR))
36 |
37 | if yesno(Message(MsgText.PasswordStoreInKeychain), default=True):
38 | from jrnl.keyring import set_keyring_password
39 |
40 | set_keyring_password(pw, journal_name)
41 |
42 | return pw
43 |
44 |
45 | def prompt_password(first_try: bool = True) -> str:
46 | if not first_try:
47 | print_msg(Message(MsgText.WrongPasswordTryAgain, MsgStyle.WARNING))
48 |
49 | return (
50 | print_msg(
51 | Message(MsgText.Password, MsgStyle.PROMPT),
52 | get_input=True,
53 | hide_input=True,
54 | )
55 | or ""
56 | )
57 |
58 |
59 | def yesno(prompt: Message | str, default: bool = True) -> bool:
60 | response = print_msgs(
61 | [
62 | prompt,
63 | Message(
64 | MsgText.YesOrNoPromptDefaultYes
65 | if default
66 | else MsgText.YesOrNoPromptDefaultNo
67 | ),
68 | ],
69 | style=MsgStyle.PROMPT,
70 | delimiter=" ",
71 | get_input=True,
72 | )
73 |
74 | answers = {
75 | str(MsgText.OneCharacterYes): True,
76 | str(MsgText.OneCharacterNo): False,
77 | }
78 |
79 | # Does using `lower()` work in all languages?
80 | return answers.get(str(response).lower().strip(), default)
81 |
--------------------------------------------------------------------------------
/jrnl/templates/sample.template:
--------------------------------------------------------------------------------
1 | ---
2 | extension: txt
3 | ---
4 |
5 | {% block journal %}
6 | {% for entry in entries %}
7 | {% include entry %}
8 | {% endfor %}
9 |
10 | {% endblock %}
11 |
12 | {% block entry %}
13 | {{ entry.title }}
14 | {{ "-" * len(entry.title) }}
15 |
16 | {{ entry.body }}
17 |
18 | {% endblock %}
19 |
--------------------------------------------------------------------------------
/jrnl/time.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2012-2023 jrnl contributors
2 | # License: https://www.gnu.org/licenses/gpl-3.0.html
3 |
4 | import datetime
5 |
6 | FAKE_YEAR = 9999
7 | DEFAULT_FUTURE = datetime.datetime(FAKE_YEAR, 12, 31, 23, 59, 59)
8 | DEFAULT_PAST = datetime.datetime(FAKE_YEAR, 1, 1, 0, 0)
9 |
10 |
11 | def __get_pdt_calendar():
12 | import parsedatetime as pdt
13 |
14 | consts = pdt.Constants(usePyICU=False)
15 | consts.DOWParseStyle = -1 # "Monday" will be either today or the last Monday
16 | calendar = pdt.Calendar(consts, version=pdt.VERSION_CONTEXT_STYLE)
17 |
18 | return calendar
19 |
20 |
21 | def parse(
22 | date_str: str | datetime.datetime,
23 | inclusive: bool = False,
24 | default_hour: int | None = None,
25 | default_minute: int | None = None,
26 | bracketed: bool = False,
27 | ) -> datetime.datetime | None:
28 | """Parses a string containing a fuzzy date and returns a datetime.datetime object"""
29 | if not date_str:
30 | return None
31 | elif isinstance(date_str, datetime.datetime):
32 | return date_str
33 |
34 | # Don't try to parse anything with 6 or fewer characters and was parsed from the
35 | # existing journal. It's probably a markdown footnote
36 | if len(date_str) <= 6 and bracketed:
37 | return None
38 |
39 | default_date = DEFAULT_FUTURE if inclusive else DEFAULT_PAST
40 | date = None
41 | year_present = False
42 |
43 | hasTime = False
44 | hasDate = False
45 |
46 | while not date:
47 | try:
48 | from dateutil.parser import parse as dateparse
49 |
50 | date = dateparse(date_str, default=default_date)
51 | if date.year == FAKE_YEAR:
52 | date = datetime.datetime(
53 | datetime.datetime.now().year, date.timetuple()[1:6]
54 | )
55 | else:
56 | year_present = True
57 | hasTime = not (date.hour == date.minute == 0)
58 | hasDate = True
59 | date = date.timetuple()
60 | except Exception as e:
61 | if e.args[0] == "day is out of range for month":
62 | y, m, d, H, M, S = default_date.timetuple()[:6]
63 | default_date = datetime.datetime(y, m, d - 1, H, M, S)
64 | else:
65 | calendar = __get_pdt_calendar()
66 | date, parse_context = calendar.parse(date_str)
67 | hasTime = parse_context.hasTime
68 | hasDate = parse_context.hasDate
69 |
70 | if not hasDate and not hasTime:
71 | try: # Try and parse this as a single year
72 | year = int(date_str)
73 | return datetime.datetime(year, 1, 1)
74 | except ValueError:
75 | return None
76 | except TypeError:
77 | return None
78 |
79 | if hasDate and not hasTime:
80 | date = datetime.datetime( # Use the default time
81 | *date[:3],
82 | hour=23 if inclusive else default_hour or 0,
83 | minute=59 if inclusive else default_minute or 0,
84 | second=59 if inclusive else 0
85 | )
86 | else:
87 | date = datetime.datetime(*date[:6])
88 |
89 | # Ugly heuristic: if the date is more than 4 weeks in the future, we got the year
90 | # wrong. Rather than this, we would like to see parsedatetime patched so we can
91 | # tell it to prefer past dates
92 | dt = datetime.datetime.now() - date
93 | if dt.days < -28 and not year_present:
94 | date = date.replace(date.year - 1)
95 | return date
96 |
97 |
98 | def is_valid_date(year: int, month: int, day: int) -> bool:
99 | try:
100 | datetime.datetime(year, month, day)
101 | return True
102 | except ValueError:
103 | return False
104 |
--------------------------------------------------------------------------------
/mkdocs.yml:
--------------------------------------------------------------------------------
1 | site_name: jrnl
2 | site_url: https://jrnl.sh
3 | theme:
4 | name: readthedocs
5 | custom_dir: docs_theme
6 | static_templates:
7 | - index.html
8 | watch:
9 | - docs
10 | - docs_theme
11 | extra_css:
12 | - https://fonts.googleapis.com/css?family=Open+Sans:300,600
13 | - assets/colors.css
14 | - assets/theme.css
15 | - assets/highlight.css
16 | markdown_extensions:
17 | - admonition
18 | repo_url: https://github.com/jrnl-org/jrnl/
19 | edit_uri: edit/develop/docs/
20 | site_author: jrnl contributors
21 | site_description: Collect your thoughts and notes without leaving the command line.
22 | nav:
23 | - Overview: overview.md
24 | - 'User Guide':
25 | - Quickstart: installation.md
26 | - Basic Usage: usage.md
27 | - Encryption: encryption.md
28 | - Journal Types: journal-types.md
29 | - Privacy and Security: privacy-and-security.md
30 | - Formats: formats.md
31 | - Advanced Usage: advanced.md
32 | - 'External Editors': external-editors.md
33 | - 'Tips and Tricks': tips-and-tricks.md
34 | - 'Reference':
35 | - Command Line: reference-command-line.md
36 | - Configuration File: reference-config-file.md
37 | - 'Contributing':
38 | - Contributing to jrnl: contributing.md
39 |
40 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "devDependencies": {
3 | "pa11y-ci": "3.1.0"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/tasks.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2012-2023 jrnl contributors
2 | # License: https://www.gnu.org/licenses/gpl-3.0.html
3 |
4 | import json
5 | import os
6 | import pathlib
7 | import subprocess
8 |
9 | import requests
10 | import xmltodict
11 |
12 | DOCS_URL = "http://127.0.0.1:8000"
13 | SITEMAP_FILENAME = "sitemap.xml"
14 | CONFIG_FILENAME = "config.json"
15 |
16 |
17 | def delete_files(files):
18 | for file in files:
19 | pathlib.Path(file).unlink(missing_ok=True)
20 |
21 |
22 | def run_shell(command):
23 | # Required to run NPM commands in Windows and *nix
24 | subprocess.run(command, check=True, shell=True)
25 |
26 |
27 | def generate_sitemap():
28 | sitemap = requests.get(f"{DOCS_URL}/{SITEMAP_FILENAME}")
29 | with open(SITEMAP_FILENAME, "wb") as f:
30 | f.write(sitemap.content)
31 |
32 |
33 | def generate_pa11y_config_from_sitemap():
34 | with open(SITEMAP_FILENAME) as f:
35 | xml_sitemap = xmltodict.parse(f.read())
36 |
37 | urls = [
38 | f"{DOCS_URL}/",
39 | f"{DOCS_URL}/search.html?q=jrnl",
40 | ]
41 | urls += [url["loc"] for url in xml_sitemap["urlset"]["url"]]
42 |
43 | with open(CONFIG_FILENAME, "w") as f:
44 | f.write(
45 | json.dumps(
46 | {
47 | "defaults": {"chromeLaunchConfig": {"args": ["--no-sandbox"]}},
48 | "urls": urls,
49 | }
50 | )
51 | )
52 |
53 |
54 | def output_file(file):
55 | if not os.getenv("CI", False):
56 | return
57 |
58 | print(f"::group::{file}")
59 |
60 | with open(file) as f:
61 | print(f.read())
62 |
63 | print("::endgroup::")
64 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jrnl-org/jrnl/7923e815f7560b5a11cf4c49008c2e10f907e39e/tests/__init__.py
--------------------------------------------------------------------------------
/tests/bdd/features/build.feature:
--------------------------------------------------------------------------------
1 | # Copyright © 2012-2023 jrnl contributors
2 | # License: https://www.gnu.org/licenses/gpl-3.0.html
3 |
4 | Feature: Build process
5 |
6 | Scenario: Version numbers should stay in sync
7 | Given we use the config "simple.yaml"
8 | When we run "jrnl --version"
9 | Then we should get no error
10 | And the output should contain pyproject.toml version
11 |
--------------------------------------------------------------------------------
/tests/bdd/features/core.feature:
--------------------------------------------------------------------------------
1 | # Copyright © 2012-2023 jrnl contributors
2 | # License: https://www.gnu.org/licenses/gpl-3.0.html
3 |
4 | Feature: Functionality of jrnl outside of actually handling journals
5 |
6 | Scenario: Displaying the version number
7 | Given we use the config "simple.yaml"
8 | When we run "jrnl --version"
9 | Then we should get no error
10 | Then the output should match "^jrnl v\d+\.\d+(\.\d+)?(-(alpha|beta)\d*)?"
11 |
12 | Scenario: Running the diagnostic command
13 | Given we use the config "simple.yaml"
14 | When we run "jrnl --diagnostic"
15 | Then the output should contain "jrnl"
16 | And the output should contain "Python"
17 | And the output should contain "OS"
18 |
19 | @todo
20 | Scenario: Listing available journals
21 |
22 |
--------------------------------------------------------------------------------
/tests/bdd/features/file_storage.feature:
--------------------------------------------------------------------------------
1 | # Copyright © 2012-2023 jrnl contributors
2 | # License: https://www.gnu.org/licenses/gpl-3.0.html
3 |
4 | Feature: Journals iteracting with the file system in a way that users can see
5 |
6 | Scenario: Adding entries to a Folder journal should generate date files
7 | Given we use the config "empty_folder.yaml"
8 | When we run "jrnl 23 July 2013: Testing folder journal."
9 | Then we should get no error
10 | And the journal directory should contain
11 | """
12 | 2013/07/23.txt
13 | """
14 |
15 | Scenario: Adding multiple entries to a Folder journal should generate multiple date files
16 | Given we use the config "empty_folder.yaml"
17 | When we run "jrnl 23 July 2013: Testing folder journal."
18 | And we run "jrnl 3/7/2014: Second entry of journal."
19 | Then we should get no error
20 | And the journal directory should contain
21 | """
22 | 2013/07/23.txt
23 | """
24 |
25 | Scenario: If the journal and its parent directory don't exist, they should be created
26 | Given we use the config "missing_directory.yaml"
27 | Then the journal should not exist
28 | When we run "jrnl This is a new entry in my journal"
29 | Then the journal should exist
30 | When we run "jrnl -99 --short"
31 | Then the output should contain "This is a new entry in my journal"
32 |
33 | Scenario: If the journal file doesn't exist, then it should be created
34 | Given we use the config "missing_journal.yaml"
35 | Then the journal should not exist
36 | When we run "jrnl This is a new entry in my journal"
37 | Then the journal should exist
38 | When we run "jrnl -99 --short"
39 | Then the output should contain "This is a new entry in my journal"
40 |
41 | @on_posix
42 | Scenario: If the directory for a Folder journal ending in a slash ('/') doesn't exist, then it should be created
43 | Given we use the config "missing_directory.yaml"
44 | Then the journal "endslash" directory should not exist
45 | When we run "jrnl endslash This is a new entry in my journal"
46 | Then the journal "endslash" directory should exist
47 | When we run "jrnl endslash -1"
48 | Then the output should contain "This is a new entry in my journal"
49 |
50 | @on_win
51 | Scenario: If the directory for a Folder journal ending in a backslash ('\') doesn't exist, then it should be created
52 | Given we use the config "missing_directory.yaml"
53 | Then the journal "endbackslash" directory should not exist
54 | When we run "jrnl endbackslash This is a new entry in my journal"
55 | Then the journal "endbackslash" directory should exist
56 | When we run "jrnl endbackslash -1"
57 | Then the output should contain "This is a new entry in my journal"
58 |
59 | Scenario: Creating journal with relative path should update to absolute path
60 | Given we use no config
61 | When we run "jrnl hello world" and enter
62 | """
63 | test.txt
64 | n
65 | \n
66 | """
67 | Then the output should contain "Journal 'default' created"
68 | When we change directory to "subfolder"
69 | And we run "jrnl -n 1"
70 | Then the output should contain "hello world"
71 |
72 | Scenario: the temporary filename suffix should default to ".jrnl"
73 | Given we use the config "editor.yaml"
74 | When we run "jrnl --edit"
75 | Then the editor should have been called
76 | Then the editor filename should end with ".jrnl"
77 |
78 | Scenario: the temporary filename suffix should be "-{template_filename}"
79 | Given we use the config "editor_markdown_extension.yaml"
80 | When we run "jrnl --edit"
81 | Then the editor filename should end with "-extension.md"
82 |
--------------------------------------------------------------------------------
/tests/bdd/features/star.feature:
--------------------------------------------------------------------------------
1 | # Copyright © 2012-2023 jrnl contributors
2 | # License: https://www.gnu.org/licenses/gpl-3.0.html
3 |
4 | Feature: Starring entries
5 |
6 | Scenario Outline: Starring an entry will mark it in the journal file
7 | Given we use the config ""
8 | When we run "jrnl 20 july 2013 *: Best day of my life!"
9 | Then we should get no error
10 | When we run "jrnl -on 2013-07-20 -starred"
11 | Then the output should contain "2013-07-20 09:00 Best day of my life!"
12 |
13 | Examples: configs
14 | | config_file |
15 | | simple.yaml |
16 | | empty_folder.yaml |
17 | | dayone.yaml |
18 |
19 | Scenario Outline: Filtering by starred entries will show only starred entries
20 | Given we use the config ""
21 | When we run "jrnl -starred"
22 | Then the output should be empty
23 | When we run "jrnl 20 july 2013 *: Best day of my life!"
24 | When we run "jrnl -starred"
25 | Then the output should be "2013-07-20 09:00 Best day of my life!"
26 |
27 | Examples: configs
28 | | config_file |
29 | | simple.yaml |
30 | | empty_folder.yaml |
31 | | dayone_empty.yaml |
32 |
33 | Scenario: Starring an entry will mark it in an encrypted journal
34 | Given we use the config "encrypted.yaml"
35 | And we use the password "bad doggie no biscuit" if prompted
36 | When we run "jrnl 20 july 2013 *: Best day of my life!"
37 | Then we should get no error
38 | When we run "jrnl -on 2013-07-20 -starred" and enter "bad doggie no biscuit"
39 | Then the output should contain "2013-07-20 09:00 Best day of my life!"
40 |
--------------------------------------------------------------------------------
/tests/bdd/features/tag.feature:
--------------------------------------------------------------------------------
1 | # Copyright © 2012-2023 jrnl contributors
2 | # License: https://www.gnu.org/licenses/gpl-3.0.html
3 |
4 | Feature: Tagging
5 | # See search.feature for tag-related searches
6 | # And format.feature for tag-related output
7 |
8 | Scenario Outline: Tags should allow certain special characters such as /, +, #
9 | Given we use the config ""
10 | When we run "jrnl 2020-09-26: This is an entry about @os/2 and @c++ and @c#"
11 | When we run "jrnl --tags -on 2020-09-26"
12 | Then we should get no error
13 | And the output should be
14 | """
15 | @os/2 : 1
16 | @c++ : 1
17 | @c# : 1
18 | """
19 |
20 | Examples: configs
21 | | config_file |
22 | | basic_onefile.yaml |
23 | | basic_folder.yaml |
24 | | basic_dayone.yaml |
25 |
26 | Scenario Outline: Emails addresses should not be parsed as tags
27 | Given we use the config ""
28 | When we run "jrnl 2020-09-26: The email address test@example.com does not seem to work for me"
29 | When we run "jrnl 2020-09-26: The email address test@example.org also does not work for me"
30 | When we run "jrnl 2020-09-26: I tried test@example.org and test@example.edu too"
31 | When we run "jrnl --tags -on 2020-09-26"
32 | Then we should get no error
33 | And the output should be "[No tags found in journal.]"
34 |
35 | Examples: configs
36 | | config_file |
37 | | basic_onefile.yaml |
38 | | basic_folder.yaml |
39 | | basic_dayone.yaml |
40 |
41 | Scenario Outline: Entry can start and end with tags
42 | Given we use the config ""
43 | When we run "jrnl 2020-09-26: @foo came over, we went to a @bar"
44 | When we run "jrnl --tags -on 2020-09-26"
45 | Then the output should be
46 | """
47 | @foo : 1
48 | @bar : 1
49 | """
50 |
51 | Examples: configs
52 | | config_file |
53 | | basic_onefile.yaml |
54 | | basic_folder.yaml |
55 | | basic_dayone.yaml |
56 |
--------------------------------------------------------------------------------
/tests/bdd/features/template.feature:
--------------------------------------------------------------------------------
1 | # Copyright © 2012-2023 jrnl contributors
2 | # License: https://www.gnu.org/licenses/gpl-3.0.html
3 |
4 | Feature: Using templates
5 | Scenario Outline: Template contents should be used in new entry
6 | Given we use the config ""
7 | And we use the password "test" if prompted
8 | And we append to the editor if opened
9 | """
10 | This is an addition to a templated entry
11 | """
12 | When we run "jrnl --config-override template features/templates/basic.template"
13 | And we run "jrnl -1"
14 | Then the output should contain "This text is in the basic template"
15 | Then the output should contain "This is an addition to a templated entry"
16 |
17 | Examples: configs
18 | | config_file |
19 | | basic_onefile.yaml |
20 | | basic_encrypted.yaml |
21 | | basic_folder.yaml |
22 | | basic_dayone.yaml |
23 |
24 | Scenario Outline: Templated entry should not be saved if template is unchanged
25 | Given we use the config ""
26 | And we use the password "test" if prompted
27 | When we run "jrnl --config-override template features/templates/basic.template"
28 | Then the output should contain "No entry to save, because the template was not changed"
29 |
30 | Examples: configs
31 | | config_file |
32 | | basic_onefile.yaml |
33 | | basic_encrypted.yaml |
34 | | basic_folder.yaml |
35 | | basic_dayone.yaml |
36 |
37 | Scenario Outline: --template nonexistent_file should throw an error
38 | Given we use the config ""
39 | And we use the password "test" if prompted
40 | When we run "jrnl --template this_template_does_not_exist.template"
41 | Then we should get an error
42 | Then the error output should contain "Unable to find a template file"
43 |
44 | Examples: configs
45 | | config_file |
46 | | basic_onefile.yaml |
47 | | basic_encrypted.yaml |
48 | | basic_folder.yaml |
49 | | basic_dayone.yaml |
50 |
51 | Scenario Outline: --template local_filepath should be used in new entry
52 | Given we use the config ""
53 | And we use the password "test" if prompted
54 | When we run "jrnl --template features/templates/basic.template"
55 | Then the output should contain "No entry to save, because the template was not changed"
56 |
57 | Examples: configs
58 | | config_file |
59 | | basic_onefile.yaml |
60 | | basic_encrypted.yaml |
61 | | basic_folder.yaml |
62 | | basic_dayone.yaml |
63 |
64 | Scenario Outline: --template file_in_XDG_templates_dir should be used in new entry
65 | Given we use the config ""
66 | And we use the password "test" if prompted
67 | And we copy the template "basic.template" to the default templates folder
68 | When we run "jrnl --template basic.template"
69 | Then the output should contain "No entry to save, because the template was not changed"
70 |
71 |
72 | Examples: configs
73 | | config_file |
74 | | basic_onefile.yaml |
75 | | basic_encrypted.yaml |
76 | | basic_folder.yaml |
77 | | basic_dayone.yaml |
78 | | basic_dayone.yaml |
79 |
--------------------------------------------------------------------------------
/tests/bdd/features/upgrade.feature:
--------------------------------------------------------------------------------
1 | # Copyright © 2012-2023 jrnl contributors
2 | # License: https://www.gnu.org/licenses/gpl-3.0.html
3 |
4 | Feature: Upgrading Journals from 1.x.x to 2.x.x
5 |
6 | Scenario: Upgrade and parse journals with square brackets
7 | Given we use the config "upgrade_from_195.json"
8 | When we run "jrnl -9" and enter "Y"
9 | When we run "jrnl -99 --short"
10 | Then the output should be
11 | """
12 | 2010-06-10 15:00 A life without chocolate is like a bad analogy.
13 | 2013-06-10 15:40 He said "[this] is the best time to be alive".Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent malesuada
14 | """
15 | And the output should contain
16 | """
17 | 2010-06-10 15:00 A life without chocolate is like a bad analogy.
18 | """
19 | And the output should contain
20 | """
21 | 2013-06-10 15:40 He said "[this] is the best time to be alive".
22 | """
23 |
24 | Scenario: Upgrading a journal encrypted with jrnl 1.x
25 | Given we use the config "encrypted_old.json"
26 | When we run "jrnl -n 1" and enter
27 | """
28 | Y
29 | bad doggie no biscuit
30 | bad doggie no biscuit
31 | """
32 | Then we should be prompted for a password
33 | And the output should contain "2013-06-10 15:40 Life is good"
34 |
35 | Scenario: Upgrading a config without colors to colors
36 | Given we use the config "no_colors.yaml"
37 | When we run "jrnl -n 1"
38 | Then the config should contain
39 | """
40 | colors:
41 | date: none
42 | title: none
43 | body: none
44 | tags: none
45 | """
46 |
47 | Scenario: Upgrade and parse journals with little endian date format
48 | Given we use the config "upgrade_from_195_little_endian_dates.json"
49 | When we run "jrnl -9 --short" and enter "Y"
50 | Then the output should contain
51 | """
52 | 10.06.2010 15:00 A life without chocolate is like a bad analogy.
53 | 10.06.2013 15:40 He said "[this] is the best time to be alive".
54 | """
55 |
56 | Scenario: Upgrade with missing journal
57 | Given we use the config "upgrade_from_195_with_missing_journal.json"
58 | When we run "jrnl --list" and enter "Y"
59 | Then the output should contain "features/journals/missing.journal does not exist"
60 | And we should get no error
61 |
62 | Scenario: Upgrade with missing encrypted journal
63 | Given we use the config "upgrade_from_195_with_missing_encrypted_journal.json"
64 | When we run "jrnl --list" and enter
65 | """
66 | Y
67 | bad doggie no biscuit
68 | """
69 | Then the output should contain "features/journals/missing.journal does not exist"
70 | And the output should contain "We're all done"
71 | And we should get no error
72 |
--------------------------------------------------------------------------------
/tests/bdd/test_features.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2012-2023 jrnl contributors
2 | # License: https://www.gnu.org/licenses/gpl-3.0.html
3 |
4 | from pytest_bdd import scenarios
5 |
6 | scenarios("features/actions.feature")
7 | scenarios("features/build.feature")
8 | scenarios("features/config_file.feature")
9 | scenarios("features/core.feature")
10 | scenarios("features/datetime.feature")
11 | scenarios("features/delete.feature")
12 | scenarios("features/change_time.feature")
13 | scenarios("features/encrypt.feature")
14 | scenarios("features/file_storage.feature")
15 | scenarios("features/format.feature")
16 | scenarios("features/import.feature")
17 | scenarios("features/install.feature")
18 | scenarios("features/multiple_journals.feature")
19 | scenarios("features/override.feature")
20 | scenarios("features/password.feature")
21 | scenarios("features/search.feature")
22 | scenarios("features/star.feature")
23 | scenarios("features/tag.feature")
24 | scenarios("features/template.feature")
25 | scenarios("features/upgrade.feature")
26 | scenarios("features/write.feature")
27 |
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2012-2023 jrnl contributors
2 | # License: https://www.gnu.org/licenses/gpl-3.0.html
3 |
4 | from pytest import mark
5 | from pytest import skip
6 |
7 | from jrnl.os_compat import on_posix
8 | from jrnl.os_compat import on_windows
9 |
10 | pytest_plugins = [
11 | "tests.lib.fixtures",
12 | "tests.lib.given_steps",
13 | "tests.lib.when_steps",
14 | "tests.lib.then_steps",
15 | ]
16 |
17 |
18 | def pytest_bdd_apply_tag(tag, function):
19 | # skip markers
20 | if tag == "skip_win":
21 | marker = mark.skipif(on_windows(), reason="Skip test on Windows")
22 | elif tag == "skip_posix":
23 | marker = mark.skipif(on_posix(), reason="Skip test on Mac/Linux")
24 |
25 | # only on OS markers
26 | elif tag == "on_win":
27 | marker = mark.skipif(not on_windows(), reason="Skip test not on Windows")
28 | elif tag == "on_posix":
29 | marker = mark.skipif(not on_posix(), reason="Skip test not on Mac/Linux")
30 | else:
31 | # Fall back to pytest-bdd's default behavior
32 | return None
33 |
34 | marker(function)
35 | return True
36 |
37 |
38 | def pytest_runtest_setup(item):
39 | markers = [mark.name for mark in item.iter_markers()]
40 |
41 | on_win = on_windows()
42 | on_nix = on_posix()
43 |
44 | if "skip_win" in markers and on_win:
45 | skip("Skip test on Windows")
46 |
47 | if "skip_posix" in markers and on_nix:
48 | skip("Skip test on Mac/Linux")
49 |
50 | if "on_win" in markers and not on_win:
51 | skip("Skip test not on Windows")
52 |
53 | if "on_posix" in markers and not on_nix:
54 | skip("Skip test not on Mac/Linux")
55 |
--------------------------------------------------------------------------------
/tests/data/configs/basic_dayone.yaml:
--------------------------------------------------------------------------------
1 | colors:
2 | date: none
3 | title: none
4 | body: none
5 | tags: none
6 | default_hour: 9
7 | default_minute: 0
8 | editor: noop
9 | encrypt: false
10 | highlight: true
11 | journals:
12 | default: features/journals/basic_dayone.dayone
13 | linewrap: 80
14 | tagsymbols: "@"
15 | template: false
16 | timeformat: "%Y-%m-%d %H:%M"
17 | indent_character: "|"
18 |
--------------------------------------------------------------------------------
/tests/data/configs/basic_encrypted.yaml:
--------------------------------------------------------------------------------
1 | colors:
2 | date: none
3 | title: none
4 | body: none
5 | tags: none
6 | default_hour: 9
7 | default_minute: 0
8 | editor: noop
9 | encrypt: true
10 | highlight: true
11 | journals:
12 | default: features/journals/basic_encrypted.journal
13 | linewrap: 80
14 | tagsymbols: "@"
15 | template: false
16 | timeformat: "%Y-%m-%d %H:%M"
17 | indent_character: "|"
18 |
--------------------------------------------------------------------------------
/tests/data/configs/basic_folder.yaml:
--------------------------------------------------------------------------------
1 | colors:
2 | date: none
3 | title: none
4 | body: none
5 | tags: none
6 | default_hour: 9
7 | default_minute: 0
8 | editor: noop
9 | encrypt: false
10 | highlight: true
11 | journals:
12 | default: features/journals/basic_folder
13 | linewrap: 80
14 | tagsymbols: "@"
15 | template: false
16 | timeformat: "%Y-%m-%d %H:%M"
17 | indent_character: "|"
18 |
--------------------------------------------------------------------------------
/tests/data/configs/basic_onefile.yaml:
--------------------------------------------------------------------------------
1 | colors:
2 | date: none
3 | title: none
4 | body: none
5 | tags: none
6 | default_hour: 9
7 | default_minute: 0
8 | editor: noop
9 | encrypt: false
10 | highlight: true
11 | journals:
12 | default: features/journals/basic_onefile.journal
13 | linewrap: 80
14 | tagsymbols: "@"
15 | template: false
16 | timeformat: "%Y-%m-%d %H:%M"
17 | indent_character: "|"
18 |
--------------------------------------------------------------------------------
/tests/data/configs/brackets.yaml:
--------------------------------------------------------------------------------
1 | default_hour: 9
2 | default_minute: 0
3 | editor: ""
4 | encrypt: false
5 | highlight: true
6 | journals:
7 | default: features/journals/brackets.journal
8 | linewrap: 80
9 | tagsymbols: "@"
10 | template: false
11 | timeformat: "%Y-%m-%d %H:%M"
12 | indent_character: "|"
13 |
--------------------------------------------------------------------------------
/tests/data/configs/bug153.yaml:
--------------------------------------------------------------------------------
1 | default_hour: 9
2 | default_minute: 0
3 | editor: ''
4 | encrypt: false
5 | highlight: true
6 | journals:
7 | default: features/journals/bug153.dayone
8 | linewrap: 80
9 | tagsymbols: '@'
10 | template: false
11 | timeformat: '%Y-%m-%d %H:%M'
12 | indent_character: "|"
13 | colors:
14 | date: none
15 | title: none
16 | body: none
17 | tags: none
18 |
--------------------------------------------------------------------------------
/tests/data/configs/bug780.yaml:
--------------------------------------------------------------------------------
1 | default_hour: 9
2 | default_minute: 0
3 | editor: ''
4 | encrypt: false
5 | highlight: true
6 | journals:
7 | default: features/journals/bug780.dayone
8 | linewrap: 80
9 | tagsymbols: '@'
10 | template: false
11 | timeformat: '%Y-%m-%d %H:%M'
12 | indent_character: "|"
13 |
--------------------------------------------------------------------------------
/tests/data/configs/dayone.yaml:
--------------------------------------------------------------------------------
1 | default_hour: 9
2 | default_minute: 0
3 | editor: noop
4 | template: false
5 | encrypt: false
6 | highlight: true
7 | journals:
8 | default: features/journals/dayone.dayone
9 | linewrap: 80
10 | tagsymbols: '@'
11 | timeformat: '%Y-%m-%d %H:%M'
12 | indent_character: "|"
13 |
--------------------------------------------------------------------------------
/tests/data/configs/dayone_empty.yaml:
--------------------------------------------------------------------------------
1 | default_hour: 9
2 | default_minute: 0
3 | editor: noop
4 | template: false
5 | encrypt: false
6 | highlight: true
7 | journals:
8 | default: features/journals/dayone_empty.dayone
9 | linewrap: 80
10 | tagsymbols: '@'
11 | timeformat: '%Y-%m-%d %H:%M'
12 | indent_character: "|"
13 | colors:
14 | date: none
15 | title: none
16 | body: none
17 | tags: none
18 |
--------------------------------------------------------------------------------
/tests/data/configs/deletion.yaml:
--------------------------------------------------------------------------------
1 | default_hour: 9
2 | default_minute: 0
3 | editor: ""
4 | encrypt: false
5 | highlight: true
6 | journals:
7 | default: features/journals/deletion.journal
8 | linewrap: 80
9 | tagsymbols: "@"
10 | template: false
11 | timeformat: "%Y-%m-%d %H:%M"
12 | indent_character: "|"
13 |
--------------------------------------------------------------------------------
/tests/data/configs/deletion_filters.yaml:
--------------------------------------------------------------------------------
1 | default_hour: 9
2 | default_minute: 0
3 | editor: ""
4 | encrypt: false
5 | highlight: true
6 | journals:
7 | default: features/journals/deletion_filters.journal
8 | linewrap: 80
9 | tagsymbols: "@"
10 | template: false
11 | timeformat: "%Y-%m-%d %H:%M"
12 | indent_character: "|"
13 |
--------------------------------------------------------------------------------
/tests/data/configs/duplicate_keys.yaml:
--------------------------------------------------------------------------------
1 | default_hour: 9
2 | default_minute: 0
3 | editor: ''
4 | encrypt: false
5 | highlight: true
6 | template: false
7 | template: false
8 | journals:
9 | default:
10 | encrypt: false
11 | journal: features/journals/simple.journal
12 | journal: features/journals/simple.journal
13 | ideas:
14 | encrypt: false
15 | journal: features/journals/does-not-exist.journal
16 | simple:
17 | encrypt: false
18 | journal: features/journals/simple.journal
19 | encrypt: false
20 | work:
21 | encrypt: false
22 | journal: features/journals/work.journal
23 | linewrap: 80
24 | tagsymbols: '@'
25 | editor: nano
26 | timeformat: '%Y-%m-%d %H:%M'
27 | indent_character: "|"
28 |
--------------------------------------------------------------------------------
/tests/data/configs/editor-args.yaml:
--------------------------------------------------------------------------------
1 | default_hour: 9
2 | default_minute: 0
3 | editor: vim -f -c 'setf markdown'
4 | encrypt: false
5 | highlight: true
6 | journals:
7 | default: features/journals/simple.journal
8 | linewrap: 80
9 | tagsymbols: "@"
10 | template: false
11 | timeformat: "%Y-%m-%d %H:%M"
12 | indent_character: "|"
13 |
--------------------------------------------------------------------------------
/tests/data/configs/editor.yaml:
--------------------------------------------------------------------------------
1 | default_hour: 9
2 | default_minute: 0
3 | editor: "vim"
4 | encrypt: false
5 | highlight: true
6 | journals:
7 | default: features/journals/simple.journal
8 | linewrap: 80
9 | tagsymbols: "@"
10 | template: false
11 | timeformat: "%Y-%m-%d %H:%M"
12 | indent_character: "|"
13 |
--------------------------------------------------------------------------------
/tests/data/configs/editor_empty_folder.yaml:
--------------------------------------------------------------------------------
1 | default_hour: 9
2 | default_minute: 0
3 | editor: 'vim'
4 | template: false
5 | encrypt: false
6 | highlight: true
7 | journals:
8 | default: features/journals/empty_folder
9 | linewrap: 80
10 | tagsymbols: '@'
11 | timeformat: '%Y-%m-%d %H:%M'
12 | indent_character: "|"
13 |
--------------------------------------------------------------------------------
/tests/data/configs/editor_encrypted.yaml:
--------------------------------------------------------------------------------
1 | colors:
2 | body: green
3 | date: blue
4 | tags: none
5 | title: yellow
6 | default_hour: 9
7 | default_minute: 0
8 | editor: "vim"
9 | encrypt: true
10 | template: false
11 | highlight: true
12 | journals:
13 | default: features/journals/encrypted.journal
14 | linewrap: 80
15 | tagsymbols: '@'
16 | timeformat: '%Y-%m-%d %H:%M'
17 | indent_character: "|"
18 |
--------------------------------------------------------------------------------
/tests/data/configs/editor_markdown_extension.yaml:
--------------------------------------------------------------------------------
1 | default_hour: 9
2 | default_minute: 0
3 | encrypt: false
4 | highlight: true
5 | editor: "vim"
6 | journals:
7 | default: features/journals/editor_markdown_extension.journal
8 | linewrap: 80
9 | tagsymbols: "@"
10 | template: features/templates/extension.md
11 | timeformat: "%Y-%m-%d %H:%M"
12 | indent_character: "|"
13 | colors:
14 | date: none
15 | title: none
16 | body: none
17 | tags: none
18 |
--------------------------------------------------------------------------------
/tests/data/configs/empty_file.yaml:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jrnl-org/jrnl/7923e815f7560b5a11cf4c49008c2e10f907e39e/tests/data/configs/empty_file.yaml
--------------------------------------------------------------------------------
/tests/data/configs/empty_folder.yaml:
--------------------------------------------------------------------------------
1 | default_hour: 9
2 | default_minute: 0
3 | editor: ''
4 | template: false
5 | encrypt: false
6 | highlight: true
7 | journals:
8 | default: features/journals/empty_folder
9 | linewrap: 80
10 | tagsymbols: '@'
11 | timeformat: '%Y-%m-%d %H:%M'
12 | indent_character: "|"
13 |
--------------------------------------------------------------------------------
/tests/data/configs/encrypted.yaml:
--------------------------------------------------------------------------------
1 | default_hour: 9
2 | default_minute: 0
3 | editor: ''
4 | encrypt: true
5 | template: false
6 | highlight: true
7 | journals:
8 | default: features/journals/encrypted.journal
9 | linewrap: 80
10 | tagsymbols: '@'
11 | timeformat: '%Y-%m-%d %H:%M'
12 | indent_character: "|"
13 |
--------------------------------------------------------------------------------
/tests/data/configs/encrypted_old.json:
--------------------------------------------------------------------------------
1 | {
2 | "default_hour": 9,
3 | "default_minute": 0,
4 | "editor": "",
5 | "encrypt": true,
6 | "highlight": true,
7 | "journals": {
8 | "default": "features/journals/encrypted_jrnl-1-9-5.journal"
9 | },
10 | "linewrap": 80,
11 | "tagsymbols": "@",
12 | "timeformat": "%Y-%m-%d %H:%M"
13 | }
14 |
--------------------------------------------------------------------------------
/tests/data/configs/encrypted_old.yaml:
--------------------------------------------------------------------------------
1 | default_hour: 9
2 | default_minute: 0
3 | editor: ''
4 | encrypt: true
5 | highlight: true
6 | journals:
7 | default: features/journals/encrypted_jrnl1-9-5.journal
8 | linewrap: 80
9 | tagsymbols: '@'
10 | timeformat: '%Y-%m-%d %H:%M'
11 | indent_character: "|"
12 |
--------------------------------------------------------------------------------
/tests/data/configs/format_md.yaml:
--------------------------------------------------------------------------------
1 | colors:
2 | body: none
3 | date: none
4 | tags: none
5 | title: none
6 | default_hour: 9
7 | default_minute: 0
8 | display_format: markdown
9 | editor: ''
10 | encrypt: false
11 | highlight: true
12 | indent_character: '|'
13 | journals:
14 | default: features/journals/simple.journal
15 | linewrap: 80
16 | tagsymbols: '@'
17 | template: false
18 | timeformat: '%Y-%m-%d %H:%M'
19 | version: v2.4.5
20 |
--------------------------------------------------------------------------------
/tests/data/configs/format_text.yaml:
--------------------------------------------------------------------------------
1 | colors:
2 | body: none
3 | date: none
4 | tags: none
5 | title: none
6 | default_hour: 9
7 | default_minute: 0
8 | display_format: text
9 | editor: ''
10 | encrypt: false
11 | highlight: true
12 | indent_character: '|'
13 | journals:
14 | default: features/journals/simple.journal
15 | linewrap: 80
16 | tagsymbols: '@'
17 | template: false
18 | timeformat: '%Y-%m-%d %H:%M'
19 | version: v2.4.5
20 |
--------------------------------------------------------------------------------
/tests/data/configs/invalid_color.yaml:
--------------------------------------------------------------------------------
1 | default_hour: 9
2 | default_minute: 0
3 | editor: ""
4 | encrypt: false
5 | highlight: true
6 | journals:
7 | default: features/journals/simple.journal
8 | linewrap: 80
9 | tagsymbols: "@"
10 | template: false
11 | timeformat: "%Y-%m-%d %H:%M"
12 | indent_character: "|"
13 | colors:
14 | date: not-a-color
15 | title: also-not-a-color
16 | body: still-no-color
17 | tags: me-too
18 |
--------------------------------------------------------------------------------
/tests/data/configs/linewrap_auto.yaml:
--------------------------------------------------------------------------------
1 | default_hour: 9
2 | default_minute: 0
3 | editor: ""
4 | encrypt: false
5 | highlight: true
6 | journals:
7 | default: features/journals/simple.journal
8 | linewrap: auto
9 | tagsymbols: "@"
10 | template: false
11 | timeformat: "%Y-%m-%d %H:%M"
12 | indent_character: "|"
13 | colors:
14 | date: none
15 | title: none
16 | body: none
17 | tags: none
18 |
--------------------------------------------------------------------------------
/tests/data/configs/little_endian_dates.yaml:
--------------------------------------------------------------------------------
1 | default_hour: 9
2 | default_minute: 0
3 | editor: ""
4 | encrypt: false
5 | highlight: true
6 | journals:
7 | default: features/journals/little_endian_dates.journal
8 | linewrap: 80
9 | tagsymbols: "@"
10 | template: false
11 | timeformat: "%d.%m.%Y %H:%M"
12 | indent_character: "|"
13 |
--------------------------------------------------------------------------------
/tests/data/configs/markdown-headings-335.yaml:
--------------------------------------------------------------------------------
1 | default_hour: 9
2 | default_minute: 0
3 | editor: ''
4 | encrypt: false
5 | highlight: true
6 | template: false
7 | journals:
8 | default: features/journals/markdown-headings-335.journal
9 | linewrap: 80
10 | tagsymbols: '@'
11 | timeformat: '%Y-%m-%d %H:%M'
12 | indent_character: "|"
13 | colors:
14 | date: none
15 | title: none
16 | body: none
17 | tags: none
18 |
--------------------------------------------------------------------------------
/tests/data/configs/missing_directory.yaml:
--------------------------------------------------------------------------------
1 | default_hour: 9
2 | default_minute: 0
3 | editor: ""
4 | encrypt: false
5 | highlight: true
6 | journals:
7 | default: features/journals/missing_directory/simple.journal
8 | endslash: features/journals/missing_folder/
9 | endbackslash: features\journals\missing_folder\
10 | linewrap: 80
11 | tagsymbols: "@"
12 | template: false
13 | timeformat: "%Y-%m-%d %H:%M"
14 | indent_character: "|"
15 | colors:
16 | date: none
17 | title: none
18 | body: none
19 | tags: none
20 |
--------------------------------------------------------------------------------
/tests/data/configs/missing_journal.yaml:
--------------------------------------------------------------------------------
1 | default_hour: 9
2 | default_minute: 0
3 | editor: ""
4 | encrypt: false
5 | highlight: true
6 | journals:
7 | default: features/journals/missing.journal
8 | linewrap: 80
9 | tagsymbols: "@"
10 | template: false
11 | timeformat: "%Y-%m-%d %H:%M"
12 | indent_character: "|"
13 | colors:
14 | date: none
15 | title: none
16 | body: none
17 | tags: none
18 |
--------------------------------------------------------------------------------
/tests/data/configs/mostlyreadabledates.yaml:
--------------------------------------------------------------------------------
1 | default_hour: 9
2 | default_minute: 0
3 | editor: ""
4 | encrypt: false
5 | highlight: true
6 | journals:
7 | default: features/journals/mostlyreadabledates.journal
8 | linewrap: 80
9 | tagsymbols: "@"
10 | template: false
11 | timeformat: "%Y-%m-%d %H:%M"
12 | indent_character: "|"
13 |
--------------------------------------------------------------------------------
/tests/data/configs/multiline-tags.yaml:
--------------------------------------------------------------------------------
1 | default_hour: 9
2 | default_minute: 0
3 | editor: ""
4 | encrypt: false
5 | highlight: true
6 | journals:
7 | default: features/journals/multiline-tags.journal
8 | linewrap: 80
9 | tagsymbols: "@"
10 | template: false
11 | timeformat: "%Y-%m-%d %H:%M"
12 | indent_character: "|"
13 | colors:
14 | date: none
15 | title: none
16 | body: none
17 | tags: none
18 |
--------------------------------------------------------------------------------
/tests/data/configs/multiline.yaml:
--------------------------------------------------------------------------------
1 | default_hour: 9
2 | default_minute: 0
3 | editor: ""
4 | encrypt: false
5 | highlight: true
6 | journals:
7 | default: features/journals/multiline.journal
8 | linewrap: 80
9 | tagsymbols: "@"
10 | template: false
11 | timeformat: "%Y-%m-%d %H:%M"
12 | indent_character: "|"
13 | colors:
14 | date: none
15 | title: none
16 | body: none
17 | tags: none
18 |
--------------------------------------------------------------------------------
/tests/data/configs/multiple.yaml:
--------------------------------------------------------------------------------
1 | default_hour: 9
2 | default_minute: 0
3 | editor: ''
4 | encrypt: false
5 | highlight: true
6 | template: false
7 | journals:
8 | default: features/journals/simple.journal
9 | ideas: features/journals/does-not-exist.journal
10 | simple: features/journals/simple.journal
11 | work: features/journals/work.journal
12 | new_encrypted:
13 | encrypt: true
14 | journal: features/journals/new_encrypted.journal
15 | ✨: features/journals/simple.journal
16 | linewrap: 80
17 | tagsymbols: '@'
18 | timeformat: '%Y-%m-%d %H:%M'
19 | indent_character: "|"
20 |
--------------------------------------------------------------------------------
/tests/data/configs/no_colors.yaml:
--------------------------------------------------------------------------------
1 | default_hour: 9
2 | default_minute: 0
3 | editor: ""
4 | encrypt: false
5 | highlight: true
6 | journals:
7 | default: features/journals/simple.journal
8 | linewrap: 80
9 | tagsymbols: "@"
10 | template: false
11 | timeformat: "%Y-%m-%d %H:%M"
12 | indent_character: "|"
13 |
--------------------------------------------------------------------------------
/tests/data/configs/no_default_journal.yaml:
--------------------------------------------------------------------------------
1 | default_hour: 9
2 | default_minute: 0
3 | editor: ''
4 | template: false
5 | encrypt: false
6 | highlight: true
7 | journals:
8 | simple: features/journals/simple.journal
9 | work: features/journals/work.journal
10 | linewrap: 80
11 | tagsymbols: '@'
12 | timeformat: '%Y-%m-%d %H:%M'
13 | indent_character: "|"
14 |
--------------------------------------------------------------------------------
/tests/data/configs/simple.yaml:
--------------------------------------------------------------------------------
1 | default_hour: 9
2 | default_minute: 0
3 | editor: ""
4 | encrypt: false
5 | highlight: true
6 | journals:
7 | default: features/journals/simple.journal
8 | linewrap: 80
9 | tagsymbols: "@"
10 | template: false
11 | timeformat: "%Y-%m-%d %H:%M"
12 | indent_character: "|"
13 | colors:
14 | date: none
15 | title: none
16 | body: none
17 | tags: none
18 |
--------------------------------------------------------------------------------
/tests/data/configs/tags-216.yaml:
--------------------------------------------------------------------------------
1 | default_hour: 9
2 | default_minute: 0
3 | editor: ''
4 | encrypt: false
5 | highlight: true
6 | template: false
7 | journals:
8 | default: features/journals/tags-216.journal
9 | linewrap: 80
10 | tagsymbols: '@'
11 | timeformat: '%Y-%m-%d %H:%M'
12 | indent_character: "|"
13 | colors:
14 | date: none
15 | title: none
16 | body: none
17 | tags: none
18 |
--------------------------------------------------------------------------------
/tests/data/configs/tags-237.yaml:
--------------------------------------------------------------------------------
1 | default_hour: 9
2 | default_minute: 0
3 | editor: ''
4 | encrypt: false
5 | highlight: true
6 | template: false
7 | journals:
8 | default: features/journals/tags-237.journal
9 | linewrap: 80
10 | tagsymbols: '@'
11 | timeformat: '%Y-%m-%d %H:%M'
12 | indent_character: "|"
13 | colors:
14 | date: none
15 | title: none
16 | body: none
17 | tags: none
18 |
--------------------------------------------------------------------------------
/tests/data/configs/tags.yaml:
--------------------------------------------------------------------------------
1 | default_hour: 9
2 | default_minute: 0
3 | editor: ''
4 | encrypt: false
5 | highlight: true
6 | template: false
7 | journals:
8 | default: features/journals/tags.journal
9 | linewrap: 80
10 | tagsymbols: '@'
11 | timeformat: '%Y-%m-%d %H:%M'
12 | indent_character: "|"
13 | colors:
14 | date: none
15 | title: none
16 | body: none
17 | tags: none
18 |
--------------------------------------------------------------------------------
/tests/data/configs/unreadabledates.yaml:
--------------------------------------------------------------------------------
1 | default_hour: 9
2 | default_minute: 0
3 | editor: ""
4 | encrypt: false
5 | highlight: true
6 | journals:
7 | default: features/journals/unreadabledates.journal
8 | linewrap: 80
9 | tagsymbols: "@"
10 | template: false
11 | timeformat: "%Y-%m-%d %H:%M"
12 | indent_character: "|"
13 | colors:
14 | date: none
15 | title: none
16 | body: none
17 | tags: none
18 |
--------------------------------------------------------------------------------
/tests/data/configs/upgrade_from_195.json:
--------------------------------------------------------------------------------
1 | {
2 | "default_hour": 9,
3 | "timeformat": "%Y-%m-%d %H:%M",
4 | "linewrap": 80,
5 | "encrypt": false,
6 | "editor": "",
7 | "default_minute": 0,
8 | "highlight": true,
9 | "journals": {"default": "features/journals/simple_jrnl-1-9-5.journal"},
10 | "tagsymbols": "@"
11 | }
12 |
--------------------------------------------------------------------------------
/tests/data/configs/upgrade_from_195_little_endian_dates.json:
--------------------------------------------------------------------------------
1 | {
2 | "default_hour": 9,
3 | "timeformat": "%d.%m.%Y %H:%M",
4 | "linewrap": 80,
5 | "encrypt": false,
6 | "editor": "",
7 | "default_minute": 0,
8 | "highlight": true,
9 | "journals": {"default": "features/journals/simple_jrnl-1-9-5_little_endian_dates.journal"},
10 | "tagsymbols": "@"
11 | }
12 |
--------------------------------------------------------------------------------
/tests/data/configs/upgrade_from_195_with_missing_encrypted_journal.json:
--------------------------------------------------------------------------------
1 | {
2 | "default_hour": 9,
3 | "timeformat": "%Y-%m-%d %H:%M",
4 | "linewrap": 80,
5 | "encrypt": true,
6 | "editor": "",
7 | "default_minute": 0,
8 | "highlight": true,
9 | "journals": {"default": "features/journals/encrypted_jrnl-1-9-5.journal", "missing": "features/journals/missing.journal"},
10 | "tagsymbols": "@"
11 | }
12 |
--------------------------------------------------------------------------------
/tests/data/configs/upgrade_from_195_with_missing_journal.json:
--------------------------------------------------------------------------------
1 | {
2 | "default_hour": 9,
3 | "timeformat": "%Y-%m-%d %H:%M",
4 | "linewrap": 80,
5 | "encrypt": false,
6 | "editor": "",
7 | "default_minute": 0,
8 | "highlight": true,
9 | "journals": {"default": "features/journals/simple_jrnl-1-9-5.journal", "missing": "features/journals/missing.journal"},
10 | "tagsymbols": "@"
11 | }
12 |
--------------------------------------------------------------------------------
/tests/data/journals/basic_dayone.dayone/entries/D04D335AFED711EABA18FAFFC2100C3D.doentry:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Creation Date
6 | 2020-08-29T18:11:00Z
7 | Starred
8 |
9 | Entry Text
10 | Entry the first.
11 | Lorem @ipsum dolor sit amet, consectetur adipiscing elit. Praesent malesuada
12 | quis est ac dignissim. Aliquam dignissim rutrum pretium. Phasellus pellentesque
13 | augue et venenatis facilisis. Suspendisse potenti. Sed dignissim sed nisl eu
14 | consequat. Aenean ante ex, elementum ut interdum et, mattis eget lacus. In
15 | commodo nulla nec tellus placerat, sed ultricies metus bibendum. Duis eget
16 | venenatis erat. In at dolor dui. @tagone and maybe also @tagtwo.
17 |
18 | Curabitur accumsan nunc ac neque tristique, eleifend faucibus justo
19 | ullamcorper. Suspendisse at mattis nunc. Nullam eget lacinia urna. Suspendisse
20 | potenti. Ut urna est, venenatis sed ante in, ultrices congue mi. Maecenas eget
21 | molestie metus. Mauris porttitor dui ornare gravida porta. Quisque sed lectus
22 | hendrerit, lacinia ante eget, vulputate ante. Aliquam vitae erat non felis
23 | feugiat sagittis. Phasellus quis arcu fringilla, mattis ligula id, vestibulum
24 | urna. Vivamus facilisis leo a mi tincidunt condimentum. Donec eu euismod enim.
25 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam eu ligula eget
26 | velit scelerisque fringilla. Phasellus pharetra justo et nulla fringilla, ac
27 | porta sapien accumsan. Class aptent taciti sociosqu ad litora torquent per
28 | conubia nostra, per inceptos himenaeos.
29 | Time Zone
30 | America/Los_Angeles
31 | UUID
32 | D04D335AFED711EABA18FAFFC2100C3D
33 | Tags
34 |
35 | ipsum
36 | tagone
37 | tagtwo
38 |
39 | Creator
40 |
41 | Device Agent
42 |
43 | Generation Date
44 | 2020-09-25T02:35:45Z
45 | Host Name
46 | iris.lan
47 | OS Agent
48 | Darwin/19.3.0
49 | Software Agent
50 | jrnl/v2.4.5
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/tests/data/journals/basic_dayone.dayone/entries/FC8A86CAFED711EA8892FAFFC2100C3D.doentry:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Creation Date
6 | 2020-08-31T21:32:00Z
7 | Starred
8 |
9 | Entry Text
10 | A second entry in what I hope to be a long series.
11 | Sed sit amet metus et sapien feugiat elementum. Aliquam bibendum lobortis leo
12 | vitae tempus. Donec eleifend nec mi non volutpat. Lorem ipsum dolor sit amet,
13 | consectetur adipiscing elit. Praesent ut sodales libero. Maecenas nisl lorem,
14 | vestibulum in tempus sit amet, fermentum ut arcu. Donec vel vestibulum lectus,
15 | eget pretium enim. Maecenas diam nunc, imperdiet vitae pharetra sed, pretium id
16 | lectus. Donec eu metus et turpis tempor tristique ac non ex. In tellus arcu,
17 | egestas at efficitur et, ultrices vel est. Sed commodo et nibh non elementum.
18 | Mauris tempus vitae neque vel viverra. @tagtwo all by its lonesome.
19 |
20 | Nulla mattis elementum magna, viverra pretium dui fermentum et. Cras vel
21 | vestibulum odio. Quisque sit amet turpis et urna finibus maximus. Interdum et
22 | malesuada fames ac ante ipsum primis in faucibus. Fusce porttitor iaculis sem,
23 | non dictum ipsum varius nec. Nulla eu erat at risus gravida blandit non vel
24 | ante. Nam egestas ipsum leo, eu ultricies ipsum tincidunt vel. Morbi a commodo
25 | eros.
26 |
27 | Nullam dictum, nisl ac varius tempus, ex tortor fermentum nisl, non
28 | tempus dolor neque a lorem. Suspendisse a faucibus ex, vel ornare tortor.
29 | Maecenas tincidunt id felis quis semper. Pellentesque enim libero, fermentum
30 | quis metus id, rhoncus euismod magna. Nulla finibus velit eu purus bibendum
31 | interdum. Integer id justo dui. Integer eu tellus in turpis bibendum blandit.
32 | Quisque auctor lacinia consectetur.
33 | Time Zone
34 | America/Los_Angeles
35 | UUID
36 | FC8A86CAFED711EA8892FAFFC2100C3D
37 | Tags
38 |
39 | tagtwo
40 |
41 | Creator
42 |
43 | Device Agent
44 |
45 | Generation Date
46 | 2020-09-25T02:36:59Z
47 | Host Name
48 | iris.lan
49 | OS Agent
50 | Darwin/19.3.0
51 | Software Agent
52 | jrnl/v2.4.5
53 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/tests/data/journals/basic_dayone.dayone/entries/FD8ABC8EFED711EABC35FAFFC2100C3D.doentry:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Creation Date
6 | 2020-09-24T16:14:00Z
7 | Starred
8 |
9 | Entry Text
10 | The third entry finally after weeks without writing.
11 | I'm so excited about emojis. 💯 🎶 💩
12 |
13 | Donec semper pellentesque iaculis. Nullam cursus et justo sit amet venenatis.
14 | Vivamus tempus ex dictum metus vehicula gravida. Aliquam sed sem dolor. Nulla
15 | eget ultrices purus. Quisque at nunc at quam pharetra consectetur vitae quis
16 | dolor. Fusce ultricies purus eu est feugiat, quis scelerisque nibh malesuada.
17 | Quisque egestas semper nibh in hendrerit. Nam finibus ex in mi mattis
18 | vulputate. Sed mauris urna, consectetur in justo eu, volutpat accumsan justo.
19 | Phasellus aliquam lacus placerat convallis vestibulum. Curabitur maximus at
20 | ante eget fringilla. @tagthree and also @tagone
21 | Time Zone
22 | America/Los_Angeles
23 | UUID
24 | FD8ABC8EFED711EABC35FAFFC2100C3D
25 | Tags
26 |
27 | tagthree
28 | tagone
29 |
30 | Creator
31 |
32 | Device Agent
33 |
34 | Generation Date
35 | 2020-09-25T02:37:01Z
36 | Host Name
37 | iris.lan
38 | OS Agent
39 | Darwin/19.3.0
40 | Software Agent
41 | jrnl/v2.4.5
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/tests/data/journals/basic_folder/2020/08/29.txt:
--------------------------------------------------------------------------------
1 | [2020-08-29 11:11:00 AM] Entry the first.
2 | Lorem @ipsum dolor sit amet, consectetur adipiscing elit. Praesent malesuada
3 | quis est ac dignissim. Aliquam dignissim rutrum pretium. Phasellus pellentesque
4 | augue et venenatis facilisis. Suspendisse potenti. Sed dignissim sed nisl eu
5 | consequat. Aenean ante ex, elementum ut interdum et, mattis eget lacus. In
6 | commodo nulla nec tellus placerat, sed ultricies metus bibendum. Duis eget
7 | venenatis erat. In at dolor dui. @tagone and maybe also @tagtwo.
8 |
9 | Curabitur accumsan nunc ac neque tristique, eleifend faucibus justo
10 | ullamcorper. Suspendisse at mattis nunc. Nullam eget lacinia urna. Suspendisse
11 | potenti. Ut urna est, venenatis sed ante in, ultrices congue mi. Maecenas eget
12 | molestie metus. Mauris porttitor dui ornare gravida porta. Quisque sed lectus
13 | hendrerit, lacinia ante eget, vulputate ante. Aliquam vitae erat non felis
14 | feugiat sagittis. Phasellus quis arcu fringilla, mattis ligula id, vestibulum
15 | urna. Vivamus facilisis leo a mi tincidunt condimentum. Donec eu euismod enim.
16 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam eu ligula eget
17 | velit scelerisque fringilla. Phasellus pharetra justo et nulla fringilla, ac
18 | porta sapien accumsan. Class aptent taciti sociosqu ad litora torquent per
19 | conubia nostra, per inceptos himenaeos.
20 |
--------------------------------------------------------------------------------
/tests/data/journals/basic_folder/2020/08/31.txt:
--------------------------------------------------------------------------------
1 | [2020-08-31 02:32:00 PM] A second entry in what I hope to be a long series. *
2 | Sed sit amet metus et sapien feugiat elementum. Aliquam bibendum lobortis leo
3 | vitae tempus. Donec eleifend nec mi non volutpat. Lorem ipsum dolor sit amet,
4 | consectetur adipiscing elit. Praesent ut sodales libero. Maecenas nisl lorem,
5 | vestibulum in tempus sit amet, fermentum ut arcu. Donec vel vestibulum lectus,
6 | eget pretium enim. Maecenas diam nunc, imperdiet vitae pharetra sed, pretium id
7 | lectus. Donec eu metus et turpis tempor tristique ac non ex. In tellus arcu,
8 | egestas at efficitur et, ultrices vel est. Sed commodo et nibh non elementum.
9 | Mauris tempus vitae neque vel viverra. @tagtwo all by its lonesome.
10 |
11 | Nulla mattis elementum magna, viverra pretium dui fermentum et. Cras vel
12 | vestibulum odio. Quisque sit amet turpis et urna finibus maximus. Interdum et
13 | malesuada fames ac ante ipsum primis in faucibus. Fusce porttitor iaculis sem,
14 | non dictum ipsum varius nec. Nulla eu erat at risus gravida blandit non vel
15 | ante. Nam egestas ipsum leo, eu ultricies ipsum tincidunt vel. Morbi a commodo
16 | eros.
17 |
18 | Nullam dictum, nisl ac varius tempus, ex tortor fermentum nisl, non
19 | tempus dolor neque a lorem. Suspendisse a faucibus ex, vel ornare tortor.
20 | Maecenas tincidunt id felis quis semper. Pellentesque enim libero, fermentum
21 | quis metus id, rhoncus euismod magna. Nulla finibus velit eu purus bibendum
22 | interdum. Integer id justo dui. Integer eu tellus in turpis bibendum blandit.
23 | Quisque auctor lacinia consectetur.
24 |
--------------------------------------------------------------------------------
/tests/data/journals/basic_folder/2020/09/24.txt:
--------------------------------------------------------------------------------
1 | [2020-09-24 09:14:00 AM] The third entry finally after weeks without writing.
2 | I'm so excited about emojis. 💯 🎶 💩
3 |
4 | Donec semper pellentesque iaculis. Nullam cursus et justo sit amet venenatis.
5 | Vivamus tempus ex dictum metus vehicula gravida. Aliquam sed sem dolor. Nulla
6 | eget ultrices purus. Quisque at nunc at quam pharetra consectetur vitae quis
7 | dolor. Fusce ultricies purus eu est feugiat, quis scelerisque nibh malesuada.
8 | Quisque egestas semper nibh in hendrerit. Nam finibus ex in mi mattis
9 | vulputate. Sed mauris urna, consectetur in justo eu, volutpat accumsan justo.
10 | Phasellus aliquam lacus placerat convallis vestibulum. Curabitur maximus at
11 | ante eget fringilla. @tagthree and also @tagone
12 |
--------------------------------------------------------------------------------
/tests/data/journals/basic_folder/2020/09/should-be-ignored.txt:
--------------------------------------------------------------------------------
1 | [2022-03-02 9:25:00 AM] This file should be ignored (month)
2 | This text file is in a folder journal's month directory ("2020/09"), but it's not in the file name format used by jrnl for folder journal entries, so it should be ignored.
3 |
4 | This file should not ever appear in a test.
--------------------------------------------------------------------------------
/tests/data/journals/basic_folder/2020/should-be-ignored.txt:
--------------------------------------------------------------------------------
1 | [2022-03-02 9:25:00 AM] This file should be ignored (year)
2 | This text file is in a folder journal's year directory ("2020"), but it's not in the file name format used by jrnl for folder journal entries, so it should be ignored.
3 |
4 | This file should not ever appear in a test.
--------------------------------------------------------------------------------
/tests/data/journals/basic_folder/should-be-ignored.txt:
--------------------------------------------------------------------------------
1 | [2022-03-02 9:25:00 AM] This file should be ignored (root)
2 | This text file is in a folder journal's root directory, but it's not in the file name format used by jrnl for folder journal entries, so it should be ignored.
3 |
4 | This file should not ever appear in a test.
--------------------------------------------------------------------------------
/tests/data/journals/basic_onefile.journal:
--------------------------------------------------------------------------------
1 | [2020-08-29 11:11] Entry the first.
2 |
3 | Lorem @ipsum dolor sit amet, consectetur adipiscing elit. Praesent malesuada
4 | quis est ac dignissim. Aliquam dignissim rutrum pretium. Phasellus pellentesque
5 | augue et venenatis facilisis. Suspendisse potenti. Sed dignissim sed nisl eu
6 | consequat. Aenean ante ex, elementum ut interdum et, mattis eget lacus. In
7 | commodo nulla nec tellus placerat, sed ultricies metus bibendum. Duis eget
8 | venenatis erat. In at dolor dui. @tagone and maybe also @tagtwo.
9 |
10 | Curabitur accumsan nunc ac neque tristique, eleifend faucibus justo
11 | ullamcorper. Suspendisse at mattis nunc. Nullam eget lacinia urna. Suspendisse
12 | potenti. Ut urna est, venenatis sed ante in, ultrices congue mi. Maecenas eget
13 | molestie metus. Mauris porttitor dui ornare gravida porta. Quisque sed lectus
14 | hendrerit, lacinia ante eget, vulputate ante. Aliquam vitae erat non felis
15 | feugiat sagittis. Phasellus quis arcu fringilla, mattis ligula id, vestibulum
16 | urna. Vivamus facilisis leo a mi tincidunt condimentum. Donec eu euismod enim.
17 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam eu ligula eget
18 | velit scelerisque fringilla. Phasellus pharetra justo et nulla fringilla, ac
19 | porta sapien accumsan. Class aptent taciti sociosqu ad litora torquent per
20 | conubia nostra, per inceptos himenaeos.
21 |
22 | [2020-08-31 14:32] A second entry in what I hope to be a long series. *
23 |
24 | Sed sit amet metus et sapien feugiat elementum. Aliquam bibendum lobortis leo
25 | vitae tempus. Donec eleifend nec mi non volutpat. Lorem ipsum dolor sit amet,
26 | consectetur adipiscing elit. Praesent ut sodales libero. Maecenas nisl lorem,
27 | vestibulum in tempus sit amet, fermentum ut arcu. Donec vel vestibulum lectus,
28 | eget pretium enim. Maecenas diam nunc, imperdiet vitae pharetra sed, pretium id
29 | lectus. Donec eu metus et turpis tempor tristique ac non ex. In tellus arcu,
30 | egestas at efficitur et, ultrices vel est. Sed commodo et nibh non elementum.
31 | Mauris tempus vitae neque vel viverra. @tagtwo all by its lonesome.
32 |
33 | Nulla mattis elementum magna, viverra pretium dui fermentum et. Cras vel
34 | vestibulum odio. Quisque sit amet turpis et urna finibus maximus. Interdum et
35 | malesuada fames ac ante ipsum primis in faucibus. Fusce porttitor iaculis sem,
36 | non dictum ipsum varius nec. Nulla eu erat at risus gravida blandit non vel
37 | ante. Nam egestas ipsum leo, eu ultricies ipsum tincidunt vel. Morbi a commodo
38 | eros.
39 |
40 | Nullam dictum, nisl ac varius tempus, ex tortor fermentum nisl, non
41 | tempus dolor neque a lorem. Suspendisse a faucibus ex, vel ornare tortor.
42 | Maecenas tincidunt id felis quis semper. Pellentesque enim libero, fermentum
43 | quis metus id, rhoncus euismod magna. Nulla finibus velit eu purus bibendum
44 | interdum. Integer id justo dui. Integer eu tellus in turpis bibendum blandit.
45 | Quisque auctor lacinia consectetur.
46 |
47 | [2020-09-24 09:14] The third entry finally after weeks without writing.
48 |
49 | I'm so excited about emojis. 💯 🎶 💩
50 |
51 | Donec semper pellentesque iaculis. Nullam cursus et justo sit amet venenatis.
52 | Vivamus tempus ex dictum metus vehicula gravida. Aliquam sed sem dolor. Nulla
53 | eget ultrices purus. Quisque at nunc at quam pharetra consectetur vitae quis
54 | dolor. Fusce ultricies purus eu est feugiat, quis scelerisque nibh malesuada.
55 | Quisque egestas semper nibh in hendrerit. Nam finibus ex in mi mattis
56 | vulputate. Sed mauris urna, consectetur in justo eu, volutpat accumsan justo.
57 | Phasellus aliquam lacus placerat convallis vestibulum. Curabitur maximus at
58 | ante eget fringilla. @tagthree and also @tagone
59 |
--------------------------------------------------------------------------------
/tests/data/journals/brackets.journal:
--------------------------------------------------------------------------------
1 | [2019-07-08 05:42] Entry subject
2 | [1] line starting with 1
3 |
--------------------------------------------------------------------------------
/tests/data/journals/bug153.dayone/entries/B40EE704E15846DE8D45C44118A4D511.doentry:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 | Creation Date
7 | 2013-10-27T02:27:27Z
8 | Creator
9 |
10 | Device Agent
11 | iPhone/iPhone3,1
12 | Generation Date
13 | 2013-10-27T07:02:27Z
14 | Host Name
15 | omrt104001
16 | OS Agent
17 | iOS/7.0.3
18 | Software Agent
19 | Day One (iOS)/1.11.4
20 |
21 | Entry Text
22 | Some text.
23 | Location
24 |
25 | Administrative Area
26 | Östergötlands län
27 | Country
28 | Sverige
29 | Latitude
30 | 58.383400000000000
31 | Locality
32 | City
33 | Longitude
34 | 15.577170000000000
35 | Place Name
36 | Street
37 |
38 | Starred
39 |
40 | Time Zone
41 | Europe/Stockholm
42 | UUID
43 | B40EE704E15846DE8D45C44118A4D511
44 | Weather
45 |
46 | Celsius
47 | 12
48 | Description
49 | Clear
50 | Fahrenheit
51 | 54
52 | IconName
53 | sunnyn.png
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/tests/data/journals/bug153.dayone/entries/B40EE704E15846DE8D45C44118A4D512.doentry:
--------------------------------------------------------------------------------
1 |
2 | Creation Date
3 | 2013-10-27T02:27:27Z
4 | Creator
5 |
6 | Device Agent
7 | iPhone/iPhone3,1
8 | Generation Date
9 | 2013-10-27T07:02:27Z
10 | Host Name
11 | omrt104001
12 | OS Agent
13 | iOS/7.0.3
14 | Software Agent
15 | Day One (iOS)/1.11.4
16 |
17 | Entry Text
18 | This is not a valid plist.
19 | Location
20 |
21 | Administrative Area
22 | Östergötlands län
23 | Country
24 | Sverige
25 | Latitude
26 | 58.383400000000000
27 | Locality
28 | City
29 | Longitude
30 | 15.577170000000000
31 | Place Name
32 | Street
33 |
34 | Starred
35 |
36 | Time Zone
37 | Europe/Stockholm
38 | UUID
39 | B40EE704E15846DE8D45C44118A4D511
40 | Weather
41 |
42 | Celsius
43 | 12
44 | Description
45 | Clear
46 | Fahrenheit
47 | 54
48 | IconName
49 | sunnyn.png
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/tests/data/journals/bug780.dayone/entries/48A25033B34047C591160A4480197D8B.doentry:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Activity
6 | Stationary
7 | Creation Date
8 | 2019-12-30T21:28:54Z
9 | Entry Text
10 |
11 | Starred
12 |
13 | UUID
14 | 48A25033B34047C591160A4480197D8B
15 | Creator
16 |
17 | Device Agent
18 | PC
19 | Generation Date
20 | 2019-12-30T21:28:54Z
21 | Host Name
22 | LE-TREPORT
23 | OS Agent
24 | Microsoft Windows/10 Home
25 | Software Agent
26 | Journaley/2.1
27 |
28 | Tags
29 |
30 | i_have_no_body
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/tests/data/journals/dayone.dayone/entries/044F3747A38546168B572C2E3F217FA2.doentry:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Creation Date
6 | 2013-05-17T18:39:20Z
7 | Creator
8 |
9 | Device Agent
10 | Macintosh/MacBookAir5,2
11 | Generation Date
12 | 2013-08-17T18:39:20Z
13 | Host Name
14 | Egeria
15 | OS Agent
16 | Mac OS X/10.8.4
17 | Software Agent
18 | Day One (Mac)/1.8
19 |
20 | Entry Text
21 | This entry has tags!
22 | Starred
23 |
24 | Tags
25 |
26 | work
27 | PLaY
28 |
29 | Time Zone
30 | America/Los_Angeles
31 | UUID
32 | 044F3747A38546168B572C2E3F217FA2
33 |
34 |
35 |
--------------------------------------------------------------------------------
/tests/data/journals/dayone.dayone/entries/0BDDD6CDA43C4A9AA2681517CC35AD9D.doentry:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Creation Date
6 | 2013-06-17T18:38:29Z
7 | Creator
8 |
9 | Device Agent
10 | Macintosh/MacBookAir5,2
11 | Generation Date
12 | 2013-08-17T18:38:29Z
13 | Host Name
14 | Egeria
15 | OS Agent
16 | Mac OS X/10.8.4
17 | Software Agent
18 | Day One (Mac)/1.8
19 |
20 | Entry Text
21 | This entry has a location.
22 | Location
23 |
24 | Administrative Area
25 | California
26 | Country
27 | Germany
28 | Latitude
29 | 52.4979764
30 | Locality
31 | Berlin
32 | Longitude
33 | 13.2404758
34 | Place Name
35 | Abandoned Spy Tower
36 |
37 | Starred
38 |
39 | Tags
40 |
41 | Time Zone
42 | Europe/Berlin
43 | UUID
44 | 0BDDD6CDA43C4A9AA2681517CC35AD9D
45 |
46 |
47 |
--------------------------------------------------------------------------------
/tests/data/journals/dayone.dayone/entries/422BC895507944A291E6FC44FC6B8BFC.doentry:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Creation Date
6 | 2013-07-17T18:38:08Z
7 | Creator
8 |
9 | Device Agent
10 | Macintosh/MacBookAir5,2
11 | Generation Date
12 | 2013-08-17T18:38:08Z
13 | Host Name
14 | Egeria
15 | OS Agent
16 | Mac OS X/10.8.4
17 | Software Agent
18 | Day One (Mac)/1.8
19 |
20 | Entry Text
21 | This entry is starred!
22 | Starred
23 |
24 | Tags
25 |
26 | Time Zone
27 | America/Los_Angeles
28 | UUID
29 | 422BC895507944A291E6FC44FC6B8BFC
30 |
31 |
32 |
--------------------------------------------------------------------------------
/tests/data/journals/dayone.dayone/entries/4BB1F46946AD439996C9B59DE7C4DDC1.doentry:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Creation Date
6 | 2013-01-17T18:37:50Z
7 | Creator
8 |
9 | Device Agent
10 | Macintosh/MacBookAir5,2
11 | Generation Date
12 | 2013-08-17T18:37:50Z
13 | Host Name
14 | Egeria
15 | OS Agent
16 | Mac OS X/10.8.4
17 | Software Agent
18 | Day One (Mac)/1.8
19 |
20 | Entry Text
21 | This is a DayOne entry without Timezone.
22 | Starred
23 |
24 | Tags
25 |
26 | UUID
27 | 4BB1F46946AD439996C9B59DE7C4DDC1
28 |
29 |
30 |
--------------------------------------------------------------------------------
/tests/data/journals/dayone_empty.dayone/entries/empty.txt:
--------------------------------------------------------------------------------
1 | This file exists to preserve the directory structure, but should be ignored by jrnl.
2 |
--------------------------------------------------------------------------------
/tests/data/journals/deletion.journal:
--------------------------------------------------------------------------------
1 | [2019-10-29 11:11] First entry.
2 |
3 | [2019-10-29 11:11] Second entry.
4 |
5 | [2019-10-29 11:13] Third entry.
--------------------------------------------------------------------------------
/tests/data/journals/deletion_filters.journal:
--------------------------------------------------------------------------------
1 | [2019-10-01 08:00] It's just another day in October.
2 | Not much to write about.
3 |
4 | [2020-01-01 08:00] Happy New Year!
5 | So this is the New Year. @holidays
6 |
7 | [2020-03-01 08:00] It's just another day in March.
8 | A stick, a stone, it's the end of the road.
9 |
10 | [2020-05-01 09:00] Happy May Day!
11 | @holidays @springtime Several holidays fall on this date.
12 |
13 | [2020-05-02 12:10] Writing tests. *
14 | @springtime They will help prevent bugs.
15 |
--------------------------------------------------------------------------------
/tests/data/journals/empty_folder/empty:
--------------------------------------------------------------------------------
1 | Nothing to see here
2 |
--------------------------------------------------------------------------------
/tests/data/journals/encrypted.journal:
--------------------------------------------------------------------------------
1 | gAAAAABVIHB7tnwKExG7aC5ZbAbBL9SG2oY2GENeoOJ22i1PZigOvCYvrQN3kpsu0KGr7ay5K-_46R5YFlqJvtQ8anPH2FSITsaZy-l5Lz_5quw3rmzhLwAR1tc0icgtR4MEpXEdsuQ7cyb12Xq-JLDrnATs0id5Vow9Ri_tE7Xe4BXgXaySn3aRPwWKoninVxVPVvETY3MXHSUEXV9OZ-pH5kYBLGYbLA==
2 |
--------------------------------------------------------------------------------
/tests/data/journals/encrypted_jrnl-1-9-5.journal:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jrnl-org/jrnl/7923e815f7560b5a11cf4c49008c2e10f907e39e/tests/data/journals/encrypted_jrnl-1-9-5.journal
--------------------------------------------------------------------------------
/tests/data/journals/little_endian_dates.journal:
--------------------------------------------------------------------------------
1 | [09.06.2013 15:39] My first entry.
2 | Everything is alright
3 |
4 | [10.07.2013 15:40] Life is good.
5 | But I'm better.
6 |
--------------------------------------------------------------------------------
/tests/data/journals/markdown-headings-335.journal:
--------------------------------------------------------------------------------
1 | [2015-04-14 13:23] Heading Test
2 |
3 | H1-1
4 | =
5 |
6 | H1-2
7 | ===
8 |
9 | H1-3
10 | ============================
11 |
12 | H2-1
13 | -
14 |
15 | H2-2
16 | ---
17 |
18 | H2-3
19 | ----------------------------------
20 |
21 | Horizontal Rules (ignore)
22 |
23 | ---
24 |
25 | ===
26 |
27 | # ATX H1
28 |
29 | ## ATX H2
30 |
31 | ### ATX H3
32 |
33 | #### ATX H4
34 |
35 | ##### ATX H5
36 |
37 | ###### ATX H6
38 |
39 | Stuff
40 |
41 | More stuff
42 | more stuff again
43 |
--------------------------------------------------------------------------------
/tests/data/journals/mostlyreadabledates.journal:
--------------------------------------------------------------------------------
1 | [2019-07-18 14:23] The first entry
2 | Time machines are possible. I know, because I've built one in my garage.
3 |
4 | [2019-07-19 14:23] The second entry
5 | I'm going to activate the machine. Nobody knows what comes next after this. Or before this?
6 |
7 | [2019-07 14:23] The third entry
8 | I've crossed so many timelines. Is there any going back?
9 |
--------------------------------------------------------------------------------
/tests/data/journals/multiline-tags.journal:
--------------------------------------------------------------------------------
1 | [2013-06-09 15:39] Multiple @line entry with @tags.
2 | Tag with @punctuation. afterwards
3 | @TagOnLineAloneWithOutPunctuation
4 | @TagOnLineAloneWithPunctuation.
5 | Text before @tag. And After.
6 | @hi. Hello
7 | hi Hello
--------------------------------------------------------------------------------
/tests/data/journals/multiline.journal:
--------------------------------------------------------------------------------
1 | [2013-06-09 15:39] Multiple line entry.
2 | This is the first line.
3 | This line doesn't have any ending punctuation
4 |
5 | There is a blank line above this.
6 |
--------------------------------------------------------------------------------
/tests/data/journals/simple.journal:
--------------------------------------------------------------------------------
1 | [2013-06-09 15:39] My first entry.
2 | Everything is alright
3 |
4 | [2013-06-10 15:40] Life is good.
5 | But I'm better.
6 |
--------------------------------------------------------------------------------
/tests/data/journals/simple_jrnl-1-9-5.journal:
--------------------------------------------------------------------------------
1 | 2010-06-10 15:00 A life without chocolate is like a bad analogy.
2 |
3 | 2013-06-10 15:40 He said "[this] is the best time to be alive".
4 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent malesuada
5 | quis est ac dignissim. Aliquam dignissim rutrum pretium. Phasellus pellentesque
6 | augue et venenatis facilisis.
7 |
8 | [2019-08-03 12:55] Some chat log or something
9 |
10 | Suspendisse potenti. Sed dignissim sed nisl eu consequat. Aenean ante ex,
11 | elementum ut interdum et, mattis eget lacus. In commodo nulla nec tellus
12 | placerat, sed ultricies metus bibendum. Duis eget venenatis erat. In at dolor
13 | dui.
14 |
--------------------------------------------------------------------------------
/tests/data/journals/simple_jrnl-1-9-5_little_endian_dates.journal:
--------------------------------------------------------------------------------
1 | 10.06.2010 15:00 A life without chocolate is like a bad analogy.
2 |
3 | 10.06.2013 15:40 He said "[this] is the best time to be alive".
4 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent malesuada
5 | quis est ac dignissim. Aliquam dignissim rutrum pretium. Phasellus pellentesque
6 | augue et venenatis facilisis.
7 |
8 | [03.08.2019 12:55] Some chat log or something
9 |
10 | Suspendisse potenti. Sed dignissim sed nisl eu consequat. Aenean ante ex,
11 | elementum ut interdum et, mattis eget lacus. In commodo nulla nec tellus
12 | placerat, sed ultricies metus bibendum. Duis eget venenatis erat. In at dolor
13 | dui.
14 |
--------------------------------------------------------------------------------
/tests/data/journals/tags-216.journal:
--------------------------------------------------------------------------------
1 | [2013-06-10 15:40] I programmed for @OS/2.
2 | Almost makes me want to go back to @C++, though. (Still better than @C#).
3 |
--------------------------------------------------------------------------------
/tests/data/journals/tags-237.journal:
--------------------------------------------------------------------------------
1 | [2014-07-22 11:11] This entry has an email.
2 | @Newline tag should show as a tag.
3 | Kyla's @email is kyla@clevelandunderdog.org and Guinness's is guinness@fortheloveofpits.org.
4 |
--------------------------------------------------------------------------------
/tests/data/journals/tags.journal:
--------------------------------------------------------------------------------
1 | [2013-04-09 15:39] I have an @idea:
2 | (1) write a command line @journal software
3 | (2) ???
4 | (3) PROFIT!
5 |
6 | [2013-06-10 15:40] I met with @dan.
7 | As alway's he shared his latest @idea on how to rule the world with me.
8 | inst
9 |
--------------------------------------------------------------------------------
/tests/data/journals/unreadabledates.journal:
--------------------------------------------------------------------------------
1 | [ashasd7zdskhz7asdkjasd] Entry subject
2 | I've lost track of time.
3 |
4 | [sadfhakjsdf88sdf7sdff] Entry subject
5 | Time has no meaning.
6 |
--------------------------------------------------------------------------------
/tests/data/journals/work.journal:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jrnl-org/jrnl/7923e815f7560b5a11cf4c49008c2e10f907e39e/tests/data/journals/work.journal
--------------------------------------------------------------------------------
/tests/data/templates/basic.template:
--------------------------------------------------------------------------------
1 | This text is in the basic template
2 |
--------------------------------------------------------------------------------
/tests/data/templates/sample.template:
--------------------------------------------------------------------------------
1 | ---
2 | extension: txt
3 | ---
4 |
5 | {% block journal %}
6 | {% for entry in entries %}
7 | {% include entry %}
8 | {% endfor %}
9 |
10 | {% endblock %}
11 |
12 | {% block entry %}
13 | {{ entry.title }}
14 | {{ "-" * len(entry.title) }}
15 |
16 | {{ entry.body }}
17 |
18 | {% endblock %}
19 | `
20 |
--------------------------------------------------------------------------------
/tests/lib/helpers.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2012-2023 jrnl contributors
2 | # License: https://www.gnu.org/licenses/gpl-3.0.html
3 |
4 | import functools
5 | import os
6 |
7 |
8 | def does_directory_contain_files(file_list, directory_path):
9 | if not os.path.isdir(directory_path):
10 | return False
11 |
12 | for file in file_list.split("\n"):
13 | fullpath = directory_path + "/" + file
14 | if not os.path.isfile(fullpath):
15 | return False
16 |
17 | return True
18 |
19 |
20 | def does_directory_contain_n_files(directory_path, number):
21 | count = 0
22 | if not os.path.isdir(directory_path):
23 | return False
24 |
25 | files = [
26 | f
27 | for f in os.listdir(directory_path)
28 | if os.path.isfile(os.path.join(directory_path, f))
29 | ]
30 | count = len(files)
31 |
32 | return int(number) == count
33 |
34 |
35 | def assert_equal_tags_ignoring_order(
36 | actual_line, expected_line, actual_content, expected_content
37 | ):
38 | actual_tags = set(tag.strip() for tag in actual_line[len("tags: ") :].split(","))
39 | expected_tags = set(
40 | tag.strip() for tag in expected_line[len("tags: ") :].split(",")
41 | )
42 | assert actual_tags == expected_tags, [
43 | [actual_tags, expected_tags],
44 | [expected_content, actual_content],
45 | ]
46 |
47 |
48 | # @see: https://stackoverflow.com/a/65782539/569146
49 | def get_nested_val(dictionary, path, *default):
50 | try:
51 | return functools.reduce(lambda x, y: x[y], path.split("."), dictionary)
52 | except KeyError:
53 | if default:
54 | return default[0]
55 | raise
56 |
57 |
58 | # @see: https://stackoverflow.com/a/41599695/569146
59 | def spy_wrapper(wrapped_function):
60 | from unittest import mock
61 |
62 | mock = mock.MagicMock()
63 |
64 | def wrapper(self, *args, **kwargs):
65 | mock(*args, **kwargs)
66 | return wrapped_function(self, *args, **kwargs)
67 |
68 | wrapper.mock = mock
69 | return wrapper
70 |
71 |
72 | def get_fixture(request, name, default=None):
73 | try:
74 | return request.getfixturevalue(name)
75 | except LookupError:
76 | return default
77 |
--------------------------------------------------------------------------------
/tests/lib/type_builders.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2012-2023 jrnl contributors
2 | # License: https://www.gnu.org/licenses/gpl-3.0.html
3 |
4 | from parse_type import TypeBuilder
5 |
6 | should_choice = TypeBuilder.make_enum(
7 | {
8 | "should": True,
9 | "should not": False,
10 | }
11 | )
12 |
--------------------------------------------------------------------------------
/tests/lib/when_steps.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2012-2023 jrnl contributors
2 | # License: https://www.gnu.org/licenses/gpl-3.0.html
3 |
4 | import os
5 | from contextlib import ExitStack
6 |
7 | from pytest_bdd import when
8 | from pytest_bdd.parsers import parse
9 | from pytest_bdd.parsers import re
10 |
11 | # This is an undocumented and unsupported function:
12 | # https://github.com/pytest-dev/pytest-bdd/issues/684
13 | try:
14 | from pytest_bdd.compat import inject_fixture # pytest_bdd 7.1.2 and later
15 | except ImportError:
16 | from pytest_bdd.steps import inject_fixture # pytest_bdd 7.1.1 and earlier
17 |
18 | from jrnl.main import run
19 |
20 |
21 | @when(parse('we change directory to "{directory_name}"'))
22 | def when_we_change_directory(directory_name):
23 | if not os.path.isdir(directory_name):
24 | os.mkdir(directory_name)
25 |
26 | os.chdir(directory_name)
27 |
28 |
29 | # These variables are used in the `@when(re(...))` section below
30 | command = '(?P[^"]*)'
31 | input_method = "(?Penter|pipe|type)"
32 | all_input = '("(?P[^"]*)")'
33 | # Note: A line with only a raw newline r'\n' is treated as
34 | # an empty line of input internally for testing purposes.
35 |
36 |
37 | @when(re(f'we run "jrnl {command}" and {input_method}'))
38 | def we_run_jrnl_docstring(capsys, keyring, request, command, input_method, docstring):
39 | we_run_jrnl(capsys, keyring, request, command, input_method, docstring)
40 |
41 |
42 | @when(re(f'we run "jrnl ?{command}" and {input_method} {all_input}'))
43 | @when(re(f'we run "jrnl {command}"(?! and)'))
44 | @when('we run "jrnl"')
45 | def we_run_jrnl(capsys, keyring, request, command, input_method, all_input):
46 | from keyring import set_keyring
47 |
48 | set_keyring(keyring)
49 |
50 | # fixture injection (pytest-bdd >=6.0)
51 | inject_fixture(request, "command", command)
52 | inject_fixture(request, "input_method", input_method)
53 | inject_fixture(request, "all_input", all_input)
54 |
55 | cli_run = request.getfixturevalue("cli_run")
56 |
57 | with ExitStack() as stack:
58 | mocks = cli_run["mocks"]
59 | factories = cli_run["mock_factories"]
60 |
61 | for id in factories:
62 | mocks[id] = stack.enter_context(factories[id]())
63 |
64 | try:
65 | cli_run["status"] = run() or 0
66 | except StopIteration:
67 | # This happens when input is expected, but don't have any input left
68 | pass
69 | except SystemExit as e:
70 | cli_run["status"] = e.code
71 |
72 | captured = capsys.readouterr()
73 |
74 | cli_run["stdout"] = captured.out
75 | cli_run["stderr"] = captured.err
76 |
--------------------------------------------------------------------------------
/tests/unit/test_color.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2012-2023 jrnl contributors
2 | # License: https://www.gnu.org/licenses/gpl-3.0.html
3 |
4 | import pytest
5 | from colorama import Fore
6 | from colorama import Style
7 |
8 | from jrnl.color import colorize
9 |
10 |
11 | @pytest.fixture()
12 | def data_fixture():
13 | string = "Zwei peanuts walked into a bar"
14 | yield string
15 |
16 |
17 | def test_colorize(data_fixture):
18 | string = data_fixture
19 | colorized_string = colorize(string, "BLUE", True)
20 |
21 | assert colorized_string == Style.BRIGHT + Fore.BLUE + string + Style.RESET_ALL
22 |
--------------------------------------------------------------------------------
/tests/unit/test_config_file.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2012-2023 jrnl contributors
2 | # License: https://www.gnu.org/licenses/gpl-3.0.html
3 |
4 | import os
5 |
6 | import pytest
7 |
8 | from jrnl.exception import JrnlException
9 | from jrnl.install import find_alt_config
10 |
11 |
12 | def test_find_alt_config(request):
13 | work_config_path = os.path.join(
14 | request.fspath.dirname, "..", "data", "configs", "basic_onefile.yaml"
15 | )
16 | found_alt_config = find_alt_config(work_config_path)
17 | assert found_alt_config == work_config_path
18 |
19 |
20 | def test_find_alt_config_not_exist(request):
21 | bad_config_path = os.path.join(
22 | request.fspath.dirname, "..", "data", "configs", "does-not-exist.yaml"
23 | )
24 | with pytest.raises(JrnlException) as ex:
25 | found_alt_config = find_alt_config(bad_config_path)
26 | assert found_alt_config is not None
27 | assert isinstance(ex.value, JrnlException)
28 |
--------------------------------------------------------------------------------
/tests/unit/test_controller.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2012-2023 jrnl contributors
2 | # License: https://www.gnu.org/licenses/gpl-3.0.html
3 |
4 | import random
5 | import string
6 | from unittest import mock
7 |
8 | import pytest
9 |
10 | import jrnl
11 | from jrnl.args import parse_args
12 | from jrnl.controller import _display_search_results
13 |
14 |
15 | @pytest.fixture
16 | def random_string():
17 | return "".join(random.choices(string.ascii_uppercase + string.digits, k=25))
18 |
19 |
20 | @pytest.mark.parametrize("export_format", ["pretty", "short"])
21 | def test_display_search_results_pretty_short(export_format):
22 | mock_args = parse_args(["--format", export_format])
23 |
24 | test_journal = jrnl.journals.Journal()
25 | test_journal.new_entry("asdf")
26 |
27 | test_journal.pprint = mock.Mock()
28 |
29 | _display_search_results(mock_args, test_journal)
30 |
31 | test_journal.pprint.assert_called_once()
32 |
33 |
34 | @pytest.mark.parametrize(
35 | "export_format", ["markdown", "json", "xml", "yaml", "fancy", "dates"]
36 | )
37 | @mock.patch("jrnl.plugins.get_exporter")
38 | @mock.patch("builtins.print")
39 | def test_display_search_results_builtin_plugins(
40 | mock_print, mock_exporter, export_format, random_string
41 | ):
42 | test_filename = random_string
43 | mock_args = parse_args(["--format", export_format, "--file", test_filename])
44 |
45 | test_journal = jrnl.journals.Journal()
46 | test_journal.new_entry("asdf")
47 |
48 | mock_export = mock.Mock()
49 | mock_exporter.return_value.export = mock_export
50 |
51 | _display_search_results(mock_args, test_journal)
52 |
53 | mock_exporter.assert_called_once_with(export_format)
54 | mock_export.assert_called_once_with(test_journal, test_filename)
55 | mock_print.assert_called_once_with(mock_export.return_value)
56 |
--------------------------------------------------------------------------------
/tests/unit/test_editor.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2012-2023 jrnl contributors
2 | # License: https://www.gnu.org/licenses/gpl-3.0.html
3 |
4 | import os
5 | from unittest.mock import mock_open
6 | from unittest.mock import patch
7 |
8 | import pytest
9 |
10 | from jrnl.editor import get_template_path
11 | from jrnl.editor import read_template_file
12 | from jrnl.exception import JrnlException
13 |
14 |
15 | @patch(
16 | "os.getcwd", side_effect="/"
17 | ) # prevent failures in CI if current directory has been deleted
18 | @patch("builtins.open", side_effect=FileNotFoundError())
19 | def test_read_template_file_with_no_file_raises_exception(mock_open, mock_getcwd):
20 | with pytest.raises(JrnlException) as ex:
21 | read_template_file("invalid_file.txt")
22 | assert isinstance(ex.value, JrnlException)
23 |
24 |
25 | @patch(
26 | "os.getcwd", side_effect="/"
27 | ) # prevent failures in CI if current directory has been deleted
28 | @patch("builtins.open", new_callable=mock_open, read_data="template text")
29 | def test_read_template_file_with_valid_file_returns_text(mock_file, mock_getcwd):
30 | assert read_template_file("valid_file.txt") == "template text"
31 |
32 |
33 | def test_get_template_path_when_exists_returns_correct_path():
34 | with patch("os.path.exists", return_value=True):
35 | output = get_template_path("template", "templatepath")
36 |
37 | assert output == os.path.join("templatepath", "template")
38 |
39 |
40 | @patch("jrnl.editor.absolute_path")
41 | def test_get_template_path_when_doesnt_exist_returns_correct_path(mock_absolute_paths):
42 | with patch("os.path.exists", return_value=False):
43 | output = get_template_path("template", "templatepath")
44 |
45 | assert output == mock_absolute_paths.return_value
46 |
--------------------------------------------------------------------------------
/tests/unit/test_export.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2012-2023 jrnl contributors
2 | # License: https://www.gnu.org/licenses/gpl-3.0.html
3 |
4 | from unittest import mock
5 |
6 | import pytest
7 |
8 | from jrnl.exception import JrnlException
9 | from jrnl.plugins.fancy_exporter import check_provided_linewrap_viability
10 | from jrnl.plugins.yaml_exporter import YAMLExporter
11 |
12 |
13 | @pytest.fixture()
14 | def datestr():
15 | yield "2020-10-20 16:59"
16 |
17 |
18 | def build_card_header(datestr):
19 | top_left_corner = "┎─╮"
20 | content = top_left_corner + datestr
21 | return content
22 |
23 |
24 | class TestFancy:
25 | def test_too_small_linewrap(self, datestr):
26 | journal = "test_journal"
27 | content = build_card_header(datestr)
28 |
29 | total_linewrap = 12
30 |
31 | with pytest.raises(JrnlException):
32 | check_provided_linewrap_viability(total_linewrap, [content], journal)
33 |
34 |
35 | class TestYaml:
36 | @mock.patch("builtins.open")
37 | def test_export_to_nonexisting_folder(self, mock_open):
38 | with pytest.raises(JrnlException):
39 | YAMLExporter.write_file("journal", "non-existing-path")
40 | mock_open.assert_not_called()
41 |
--------------------------------------------------------------------------------
/tests/unit/test_install.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2012-2023 jrnl contributors
2 | # License: https://www.gnu.org/licenses/gpl-3.0.html
3 |
4 | import sys
5 | from unittest import mock
6 |
7 | import pytest
8 |
9 |
10 | @pytest.mark.filterwarnings(
11 | "ignore:.*imp module is deprecated.*"
12 | ) # ansiwrap spits out an unrelated warning
13 | def test_initialize_autocomplete_runs_without_readline():
14 | from jrnl import install
15 |
16 | with mock.patch.dict(sys.modules, {"readline": None}):
17 | install._initialize_autocomplete() # should not throw exception
18 |
--------------------------------------------------------------------------------
/tests/unit/test_journals_folder_journal.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2012-2023 jrnl contributors
2 | # License: https://www.gnu.org/licenses/gpl-3.0.html
3 |
4 | import pathlib
5 | from unittest import mock
6 |
7 | import pytest
8 |
9 | from jrnl.journals.FolderJournal import Folder
10 |
11 |
12 | @pytest.mark.parametrize(
13 | "inputs_and_outputs",
14 | [
15 | [
16 | "/2020/01",
17 | ["02.txt", "03.txt", "31.txt"],
18 | ["/2020/01/02.txt", "/2020/01/03.txt", "/2020/01/31.txt"],
19 | ],
20 | [
21 | "/2020/02", # leap year
22 | ["02.txt", "03.txt", "28.txt", "29.txt", "31.txt", "39.txt"],
23 | [
24 | "/2020/02/02.txt",
25 | "/2020/02/03.txt",
26 | "/2020/02/28.txt",
27 | "/2020/02/29.txt",
28 | ],
29 | ],
30 | [
31 | "/2100/02", # not a leap year
32 | ["01.txt", "28.txt", "29.txt", "39.txt"],
33 | ["/2100/02/01.txt", "/2100/02/28.txt"],
34 | ],
35 | [
36 | "/2023/04",
37 | ["29.txt", "30.txt", "31.txt", "39.txt"],
38 | ["/2023/04/29.txt", "/2023/04/30.txt"],
39 | ],
40 | ],
41 | )
42 | def test_get_day_files_expected_filtering(inputs_and_outputs):
43 | year_month_path, glob_filenames, expected_output = inputs_and_outputs
44 |
45 | year_month_path = pathlib.Path(year_month_path)
46 |
47 | glob_files = map(lambda x: year_month_path / x, glob_filenames)
48 | expected_output = list(map(lambda x: str(pathlib.PurePath(x)), expected_output))
49 |
50 | with (
51 | mock.patch("pathlib.Path.glob", return_value=glob_files),
52 | mock.patch.object(pathlib.Path, "is_file", return_value=True),
53 | ):
54 | actual_output = list(Folder._get_day_files(year_month_path))
55 | actual_output.sort()
56 |
57 | expected_output.sort()
58 |
59 | assert actual_output == expected_output
60 |
--------------------------------------------------------------------------------
/tests/unit/test_os_compat.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2012-2023 jrnl contributors
2 | # License: https://www.gnu.org/licenses/gpl-3.0.html
3 |
4 | from unittest import mock
5 |
6 | import pytest
7 |
8 | from jrnl.os_compat import on_posix
9 | from jrnl.os_compat import on_windows
10 | from jrnl.os_compat import split_args
11 |
12 |
13 | @pytest.mark.parametrize(
14 | "systems",
15 | [
16 | ["linux", False],
17 | ["win32", True],
18 | ["cygwin", False],
19 | ["msys", False],
20 | ["darwin", False],
21 | ["os2", False],
22 | ["os2emx", False],
23 | ["riscos", False],
24 | ["atheos", False],
25 | ["freebsd7", False],
26 | ["freebsd8", False],
27 | ["freebsdN", False],
28 | ["openbsd6", False],
29 | ],
30 | )
31 | def test_on_windows(systems):
32 | osname, expected_on_windows = systems[0], systems[1]
33 | with mock.patch("jrnl.os_compat.platform", osname):
34 | assert on_windows() == expected_on_windows
35 |
36 |
37 | @pytest.mark.parametrize(
38 | "systems",
39 | [
40 | ["linux", True],
41 | ["win32", False],
42 | ["cygwin", True],
43 | ["msys", True],
44 | ["darwin", True],
45 | ["os2", True],
46 | ["os2emx", True],
47 | ["riscos", True],
48 | ["atheos", True],
49 | ["freebsd7", True],
50 | ["freebsd8", True],
51 | ["freebsdN", True],
52 | ["openbsd6", True],
53 | ],
54 | )
55 | def test_on_posix(systems):
56 | osname, expected_on_posix = systems[0], systems[1]
57 | with mock.patch("jrnl.os_compat.platform", osname):
58 | assert on_posix() == expected_on_posix
59 |
60 |
61 | @pytest.mark.parametrize(
62 | "args",
63 | [
64 | ["notepad", ["notepad"]],
65 | ["subl -w", ["subl", "-w"]],
66 | [
67 | '"C:\\Program Files\\Sublime Text 3\\subl.exe" -w',
68 | ['"C:\\Program Files\\Sublime Text 3\\subl.exe"', "-w"],
69 | ],
70 | ],
71 | )
72 | def test_split_args_on_windows(args):
73 | input_arguments, expected_split_args = args[0], args[1]
74 | with mock.patch("jrnl.os_compat.on_windows", lambda: True):
75 | assert split_args(input_arguments) == expected_split_args
76 |
77 |
78 | @pytest.mark.parametrize(
79 | "args",
80 | [
81 | ["vim", ["vim"]],
82 | [
83 | 'vim -f +Goyo +Limelight "+set spell linebreak"',
84 | ["vim", "-f", "+Goyo", "+Limelight", '"+set spell linebreak"'],
85 | ],
86 | ],
87 | )
88 | def test_split_args_on_not_windows(args):
89 | input_arguments, expected_split_args = args[0], args[1]
90 | with mock.patch("jrnl.os_compat.on_windows", lambda: True):
91 | assert split_args(input_arguments) == expected_split_args
92 |
--------------------------------------------------------------------------------
/tests/unit/test_output.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2012-2023 jrnl contributors
2 | # License: https://www.gnu.org/licenses/gpl-3.0.html
3 |
4 | from unittest.mock import Mock
5 | from unittest.mock import patch
6 |
7 | from jrnl.messages import Message
8 | from jrnl.output import print_msg
9 |
10 |
11 | @patch("jrnl.output.print_msgs")
12 | def test_print_msg_calls_print_msgs_as_list_with_style(print_msgs):
13 | test_msg = Mock(Message)
14 | print_msg(test_msg)
15 | print_msgs.assert_called_once_with([test_msg], style=test_msg.style)
16 |
17 |
18 | @patch("jrnl.output.print_msgs")
19 | def test_print_msg_calls_print_msgs_with_kwargs(print_msgs):
20 | test_msg = Mock(Message)
21 | kwargs = {
22 | "delimter": "test delimiter 🤡",
23 | "get_input": True,
24 | "hide_input": True,
25 | "some_rando_arg": "💩",
26 | }
27 | print_msg(test_msg, **kwargs)
28 | print_msgs.assert_called_once_with([test_msg], style=test_msg.style, **kwargs)
29 |
--------------------------------------------------------------------------------
/tests/unit/test_override.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2012-2023 jrnl contributors
2 | # License: https://www.gnu.org/licenses/gpl-3.0.html
3 |
4 | from argparse import Namespace
5 |
6 | import pytest
7 |
8 | from jrnl.override import _convert_dots_to_list
9 | from jrnl.override import _get_config_node
10 | from jrnl.override import _get_key_and_value_from_pair
11 | from jrnl.override import _recursively_apply
12 | from jrnl.override import apply_overrides
13 |
14 |
15 | @pytest.fixture()
16 | def minimal_config():
17 | cfg = {
18 | "colors": {"body": "red", "date": "green"},
19 | "default": "/tmp/journal.jrnl",
20 | "editor": "vim",
21 | "journals": {"default": "/tmp/journals/journal.jrnl"},
22 | }
23 | return cfg
24 |
25 |
26 | def expected_args(overrides):
27 | default_args = {
28 | "contains": None,
29 | "debug": False,
30 | "delete": False,
31 | "edit": False,
32 | "end_date": None,
33 | "today_in_history": False,
34 | "month": None,
35 | "day": None,
36 | "year": None,
37 | "excluded": [],
38 | "export": False,
39 | "filename": None,
40 | "limit": None,
41 | "on_date": None,
42 | "preconfig_cmd": None,
43 | "postconfig_cmd": None,
44 | "short": False,
45 | "starred": False,
46 | "start_date": None,
47 | "strict": False,
48 | "tags": False,
49 | "text": [],
50 | "config_override": [],
51 | }
52 | return Namespace(**{**default_args, **overrides})
53 |
54 |
55 | def test_apply_override(minimal_config):
56 | overrides = {"config_override": [["editor", "nano"]]}
57 | apply_overrides(expected_args(overrides), minimal_config)
58 | assert minimal_config["editor"] == "nano"
59 |
60 |
61 | def test_override_dot_notation(minimal_config):
62 | overrides = {"config_override": [["colors.body", "blue"]]}
63 | apply_overrides(expected_args(overrides), minimal_config)
64 | assert minimal_config["colors"] == {"body": "blue", "date": "green"}
65 |
66 |
67 | def test_multiple_overrides(minimal_config):
68 | overrides = {
69 | "config_override": [
70 | ["colors.title", "magenta"],
71 | ["editor", "nano"],
72 | ["journals.burner", "/tmp/journals/burner.jrnl"],
73 | ]
74 | }
75 |
76 | actual = apply_overrides(expected_args(overrides), minimal_config)
77 | assert actual["editor"] == "nano"
78 | assert actual["colors"]["title"] == "magenta"
79 | assert "burner" in actual["journals"]
80 | assert actual["journals"]["burner"] == "/tmp/journals/burner.jrnl"
81 |
82 |
83 | def test_recursively_apply():
84 | cfg = {"colors": {"body": "red", "title": "green"}}
85 | cfg = _recursively_apply(cfg, ["colors", "body"], "blue")
86 | assert cfg["colors"]["body"] == "blue"
87 |
88 |
89 | def test_get_config_node(minimal_config):
90 | assert len(minimal_config.keys()) == 4
91 | assert _get_config_node(minimal_config, "editor") == "vim"
92 | assert _get_config_node(minimal_config, "display_format") is None
93 |
94 |
95 | def test_get_kv_from_pair():
96 | pair = {"ab.cde": "fgh"}
97 | k, v = _get_key_and_value_from_pair(pair)
98 | assert k == "ab.cde"
99 | assert v == "fgh"
100 |
101 |
102 | class TestDotNotationToList:
103 | def test_unpack_dots_to_list(self):
104 | keys = "a.b.c.d.e.f"
105 | keys_list = _convert_dots_to_list(keys)
106 | assert len(keys_list) == 6
107 |
108 | def test_sequential_delimiters(self):
109 | k = "g.r..h.v"
110 | k_l = _convert_dots_to_list(k)
111 | assert len(k_l) == 4
112 |
--------------------------------------------------------------------------------
/tests/unit/test_path.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2012-2023 jrnl contributors
2 | # License: https://www.gnu.org/licenses/gpl-3.0.html
3 |
4 | import random
5 | import string
6 | from os import getenv
7 | from unittest.mock import patch
8 |
9 | import pytest
10 |
11 | from jrnl.path import absolute_path
12 | from jrnl.path import expand_path
13 | from jrnl.path import home_dir
14 |
15 |
16 | @pytest.fixture
17 | def home_dir_str(monkeypatch):
18 | username = "username"
19 | monkeypatch.setenv("USERPROFILE", username) # for windows
20 | monkeypatch.setenv("HOME", username) # for *nix
21 | return username
22 |
23 |
24 | @pytest.fixture
25 | def random_test_var(monkeypatch):
26 | name = f"JRNL_TEST_{''.join(random.sample(string.ascii_uppercase, 10))}"
27 | val = "".join(random.sample(string.ascii_lowercase, 25))
28 | monkeypatch.setenv(name, val)
29 | return (name, val)
30 |
31 |
32 | def test_home_dir(home_dir_str):
33 | assert home_dir() == home_dir_str
34 |
35 |
36 | @pytest.mark.on_posix
37 | @pytest.mark.parametrize(
38 | "path",
39 | ["~"],
40 | )
41 | def test_expand_path_actually_expands_mac_linux(path):
42 | # makes sure that path isn't being returns as-is
43 | assert expand_path(path) != path
44 |
45 |
46 | @pytest.mark.on_win
47 | @pytest.mark.parametrize(
48 | "path",
49 | ["~", "%USERPROFILE%"],
50 | )
51 | def test_expand_path_actually_expands_windows(path):
52 | # makes sure that path isn't being returns as-is
53 | assert expand_path(path) != path
54 |
55 |
56 | @pytest.mark.on_posix
57 | @pytest.mark.parametrize(
58 | "paths",
59 | [
60 | ["~", "HOME"],
61 | ],
62 | )
63 | def test_expand_path_expands_into_correct_value_mac_linux(paths):
64 | input_path, expected_path = paths[0], paths[1]
65 | assert expand_path(input_path) == getenv(expected_path)
66 |
67 |
68 | @pytest.mark.on_win
69 | @pytest.mark.parametrize(
70 | "paths",
71 | [
72 | ["~", "USERPROFILE"],
73 | ["%USERPROFILE%", "USERPROFILE"],
74 | ],
75 | )
76 | def test_expand_path_expands_into_correct_value_windows(paths):
77 | input_path, expected_path = paths[0], paths[1]
78 | assert expand_path(input_path) == getenv(expected_path)
79 |
80 |
81 | @pytest.mark.on_posix
82 | @pytest.mark.parametrize("_", range(25))
83 | def test_expand_path_expands_into_random_env_value_mac_linux(_, random_test_var):
84 | var_name, var_value = random_test_var[0], random_test_var[1]
85 | assert expand_path(var_name) == var_name
86 | assert expand_path(f"${var_name}") == var_value # mac & linux
87 | assert expand_path(f"${var_name}") == getenv(var_name)
88 |
89 |
90 | @pytest.mark.on_win
91 | @pytest.mark.parametrize("_", range(25))
92 | def test_expand_path_expands_into_random_env_value_windows(_, random_test_var):
93 | var_name, var_value = random_test_var[0], random_test_var[1]
94 | assert expand_path(var_name) == var_name
95 | assert expand_path(f"%{var_name}%") == var_value # windows
96 | assert expand_path(f"%{var_name}%") == getenv(var_name)
97 |
98 |
99 | @patch("jrnl.path.expand_path")
100 | @patch("os.path.abspath")
101 | def test_absolute_path(mock_abspath, mock_expand_path):
102 | test_val = "test_value"
103 |
104 | assert absolute_path(test_val) == mock_abspath.return_value
105 | mock_expand_path.assert_called_with(test_val)
106 | mock_abspath.assert_called_with(mock_expand_path.return_value)
107 |
--------------------------------------------------------------------------------
/tests/unit/test_time.py:
--------------------------------------------------------------------------------
1 | # Copyright © 2012-2023 jrnl contributors
2 | # License: https://www.gnu.org/licenses/gpl-3.0.html
3 |
4 | import datetime
5 |
6 | import pytest
7 |
8 | from jrnl import time
9 |
10 |
11 | def test_default_hour_is_added():
12 | assert time.parse(
13 | "2020-06-20", inclusive=False, default_hour=9, default_minute=0, bracketed=False
14 | ) == datetime.datetime(2020, 6, 20, 9)
15 |
16 |
17 | def test_default_minute_is_added():
18 | assert time.parse(
19 | "2020-06-20",
20 | inclusive=False,
21 | default_hour=0,
22 | default_minute=30,
23 | bracketed=False,
24 | ) == datetime.datetime(2020, 6, 20, 0, 30)
25 |
26 |
27 | @pytest.mark.parametrize(
28 | "inputs",
29 | [
30 | [2000, 2, 29, True],
31 | [2023, 1, 0, False],
32 | [2023, 1, 1, True],
33 | [2023, 4, 31, False],
34 | [2023, 12, 31, True],
35 | [2023, 12, 32, False],
36 | [2023, 13, 1, False],
37 | [2100, 2, 27, True],
38 | [2100, 2, 28, True],
39 | [2100, 2, 29, False],
40 | ],
41 | )
42 | def test_is_valid_date(inputs):
43 | year, month, day, expected_result = inputs
44 | assert time.is_valid_date(year, month, day) == expected_result
45 |
--------------------------------------------------------------------------------