├── .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 | [![Testing](https://github.com/jrnl-org/jrnl/workflows/Testing/badge.svg)](https://github.com/jrnl-org/jrnl/actions?query=workflow%3ATesting) 14 | [![Downloads](https://pepy.tech/badge/jrnl)](https://pepy.tech/project/jrnl) 15 | [![Version](http://img.shields.io/pypi/v/jrnl.svg?style=flat)](https://pypi.python.org/pypi/jrnl/) 16 | [![Homebrew](https://img.shields.io/homebrew/v/jrnl?style=flat-square)](https://formulae.brew.sh/formula/jrnl) 17 | [![Gitter](https://img.shields.io/gitter/room/jrnl-org/jrnl)](https://gitter.im/jrnl-org/jrnl) 18 | [![Changelog](https://img.shields.io/badge/changelog-on%20github-green)](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 | 3 | 4 | favicon 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /docs_theme/img/logo_white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | logo_white 5 | Created with Sketch. 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------