mirror of
https://github.com/jrnl-org/jrnl.git
synced 2025-05-21 05:28:31 +02:00
Merge branch 'develop' into v2.5
This commit is contained in:
commit
97cf65e516
75 changed files with 2578 additions and 1204 deletions
54
.build/generate_changelog.sh
Executable file
54
.build/generate_changelog.sh
Executable file
|
@ -0,0 +1,54 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
FILENAME='CHANGELOG.md'
|
||||||
|
|
||||||
|
# get the latest git tags
|
||||||
|
releases="$(git tag --sort=-creatordate | grep -Ev '(alpha|beta|rc)')"
|
||||||
|
release_latest=$(printf '%s' "$releases" | awk 'NR==1')
|
||||||
|
release_secondlatest=$(printf '%s' "$releases" | awk 'NR==2')
|
||||||
|
|
||||||
|
echo "release_latest: ${release_latest}"
|
||||||
|
echo "release_secondlatest: ${release_secondlatest}"
|
||||||
|
|
||||||
|
# delete generated line (or it will be added multiple times)
|
||||||
|
sed -i '/This Changelog was automatically generated by/d' "$FILENAME"
|
||||||
|
|
||||||
|
# delete trailing empty lines
|
||||||
|
sed -i -e :a -e '/^\n*$/{$d;N;};/\n$/ba' "$FILENAME"
|
||||||
|
|
||||||
|
# determine correct tag to go back to
|
||||||
|
if [[ $TRAVIS_TAG == $release_latest ]]; then
|
||||||
|
echo "release build"
|
||||||
|
gittag=${release_secondlatest}
|
||||||
|
elif [[ ! -z $TRAVIS_TAG ]]; then
|
||||||
|
echo "beta elease"
|
||||||
|
gittag=${release_latest}
|
||||||
|
else
|
||||||
|
echo "merge into master or develop"
|
||||||
|
gittag=${release_latest}
|
||||||
|
fi
|
||||||
|
echo "gittag: ${gittag}"
|
||||||
|
|
||||||
|
# find the line the tag starts on, and subtract 1
|
||||||
|
tagline=$(grep -n "^## \[\?$gittag\]\?" "$FILENAME" | awk '{print $1}' FS=':' | head -1)
|
||||||
|
echo "tagline: ${tagline}"
|
||||||
|
[[ ! -z $tagline ]] && sed -i "1,$(expr $tagline - 1)d" "$FILENAME"
|
||||||
|
|
||||||
|
# generate the changelog
|
||||||
|
docker run -it --rm -v "$(pwd)":/usr/local/src/your-app ferrarimarco/github-changelog-generator -t $GITHUB_TOKEN --since-tag $gittag
|
||||||
|
|
||||||
|
# Put back our link (instead of the broken one)
|
||||||
|
sed -i 's!https://pypi.org/project/jrnl/HEAD/!https://github.com/jrnl-org/jrnl/!' "$FILENAME"
|
||||||
|
|
||||||
|
BRANCH=$TRAVIS_BRANCH
|
||||||
|
if [[ $TRAVIS_BRANCH == $TRAVIS_TAG ]]; then
|
||||||
|
BRANCH='master'
|
||||||
|
fi
|
||||||
|
|
||||||
|
git config --global user.email "jrnl.bot@gmail.com"
|
||||||
|
git config --global user.name "Jrnl Bot"
|
||||||
|
git checkout $BRANCH
|
||||||
|
git add "$FILENAME"
|
||||||
|
git commit -m "Updating changelog [ci skip]"
|
||||||
|
git push https://${GITHUB_TOKEN}@github.com/jrnl-org/jrnl.git $BRANCH
|
||||||
|
|
26
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
26
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: ''
|
||||||
|
labels: ":new:, bug"
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Bug report
|
||||||
|
|
||||||
|
Hello! Thank you for reporting an issue!
|
||||||
|
If you would fill out the below points, that would make our process a whole lot easier!
|
||||||
|
|
||||||
|
* **Please tell us about your environment:**
|
||||||
|
- Jrnl version: (run `jrnl -v`)
|
||||||
|
- How you installed Jrnl
|
||||||
|
- Operating system [MacOS, Linux, Windows?]
|
||||||
|
|
||||||
|
* **What is the current behavior?**
|
||||||
|
|
||||||
|
* **Please provide the steps to reproduce and if possible a minimal demo of the problem**
|
||||||
|
|
||||||
|
* **What is the expected behavior?**
|
||||||
|
|
||||||
|
* **Other information** (e.g. detailed explanation, stacktraces, related issues, suggestions how to fix, links for us to have context, eg. stackoverflow, gitter, etc)
|
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
blank_issues_enabled: false
|
19
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
19
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea for jrnl
|
||||||
|
title: ''
|
||||||
|
labels: ":new:, enhancement"
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Feature Request
|
||||||
|
|
||||||
|
Hello! Thank you for reporting an issue!
|
||||||
|
If you would fill out the below points, that would make our process a whole lot easier!
|
||||||
|
|
||||||
|
* **What is the motivation / use case for changing the behavior?**
|
||||||
|
|
||||||
|
* **Please provide examples of the usage**
|
||||||
|
|
||||||
|
* **Other information** (e.g. detailed explanation, stacktraces, related issues, suggestions how to fix, links for us to have context, eg. stackoverflow, gitter, etc)
|
26
.github/ISSUE_TEMPLATE/support_request.md
vendored
Normal file
26
.github/ISSUE_TEMPLATE/support_request.md
vendored
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
---
|
||||||
|
name: Support Request
|
||||||
|
about: Get help with jrnl
|
||||||
|
title: ''
|
||||||
|
labels: ":new:, support"
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Support request
|
||||||
|
|
||||||
|
Hello! Thank you for reporting an issue!
|
||||||
|
If you would fill out the below points, that would make our process a whole lot easier!
|
||||||
|
|
||||||
|
* **Please tell us about your environment:**
|
||||||
|
|
||||||
|
- Jrnl version: (run `jrnl -v`)
|
||||||
|
- How you installed Jrnl
|
||||||
|
|
||||||
|
- Operating system [MacOS, Linux, Windows?]
|
||||||
|
|
||||||
|
* **What are you trying to do?**
|
||||||
|
|
||||||
|
* **What have you tried?**
|
||||||
|
|
||||||
|
* **Other information** (e.g. detailed explanation, stacktraces, related issues, suggestions how to fix, links for us to have context, eg. stackoverflow, gitter, etc)
|
19
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
19
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
|
||||||
|
# **TEMPLATE PLEASE EDIT**
|
||||||
|
*Thank you for wanting to contribute! Please fill out this description as well as look at the checklist!*
|
||||||
|
|
||||||
|
*Short block of text containing:
|
||||||
|
- Relevant changes in text form
|
||||||
|
- related issues
|
||||||
|
- Motivation (if applicable)
|
||||||
|
- Example of usage (if applicable)
|
||||||
|
- Example of changes to config files (if applicable)
|
||||||
|
*
|
||||||
|
### Checklist
|
||||||
|
- [ ] The code change is tested and works locally.
|
||||||
|
- [ ] Tests pass. Your PR cannot be merged unless tests pass
|
||||||
|
- [ ] There is no commented out code in this PR.
|
||||||
|
- [ ] Have you followed the guidelines in our Contributing document?
|
||||||
|
- [ ] Have you checked to ensure there aren't other open [Pull Requests](../pulls) for the same update/change?
|
||||||
|
- [ ] Have you added an explanation of what your changes do and why you'd like us to include them?
|
||||||
|
- [ ] Have you written new tests for your core changes, as applicable?
|
11
.github_changelog_generator
Normal file
11
.github_changelog_generator
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
project=jrnl
|
||||||
|
user=jrnl-org
|
||||||
|
base=CHANGELOG.md
|
||||||
|
issues=false
|
||||||
|
issues-wo-labels=false
|
||||||
|
include-labels=bug,enhancement,documentation,build,deprecated
|
||||||
|
release-url=https://pypi.org/project/jrnl/%s/
|
||||||
|
add-sections={ "build": { "prefix": "**Build:**", "labels": ["build"]}, "docs": { "prefix": "**Updated documentation:**", "labels": ["documentation"]}}
|
||||||
|
exclude-tags-regex=(alpha|beta|rc)
|
||||||
|
verbose=false
|
||||||
|
|
167
.travis.yml
167
.travis.yml
|
@ -1,24 +1,153 @@
|
||||||
dist: xenial # required for Python >= 3.7
|
dist: xenial # required for Python >= 3.7
|
||||||
|
os: linux
|
||||||
language: python
|
language: python
|
||||||
python: "3.7"
|
|
||||||
|
cache:
|
||||||
|
- pip
|
||||||
|
|
||||||
|
git:
|
||||||
|
depth: false
|
||||||
|
autocrlf: false
|
||||||
|
|
||||||
|
addons:
|
||||||
|
apt:
|
||||||
|
packages:
|
||||||
|
- tree
|
||||||
|
|
||||||
before_install:
|
before_install:
|
||||||
- pip install poetry
|
- date
|
||||||
|
- tree
|
||||||
|
|
||||||
install:
|
install:
|
||||||
# we run `poetry version` here to appease poetry about '0.0.0-source'
|
|
||||||
- poetry version
|
|
||||||
- poetry install
|
|
||||||
script:
|
|
||||||
- poetry run python --version
|
|
||||||
- poetry run behave
|
|
||||||
before_deploy:
|
|
||||||
- pip install poetry
|
- pip install poetry
|
||||||
- poetry config http-basic.pypi $PYPI_USER $PYPI_PASS
|
- poetry install
|
||||||
- poetry version $TRAVIS_TAG
|
- poetry run python --version
|
||||||
- poetry build
|
|
||||||
deploy:
|
script:
|
||||||
- provider: script
|
- poetry run behave
|
||||||
script: poetry publish
|
|
||||||
skip_cleanup: true
|
aliases:
|
||||||
on:
|
test_mac: &test_mac
|
||||||
branch: master
|
os: osx
|
||||||
tags: true
|
language: shell
|
||||||
|
osx_image: xcode11.2
|
||||||
|
cache:
|
||||||
|
directories:
|
||||||
|
- $HOME/.pyenv/versions
|
||||||
|
before_install:
|
||||||
|
- eval "$(pyenv init -)"
|
||||||
|
- pyenv install -s $JRNL_PYTHON_VERSION
|
||||||
|
- pyenv global $JRNL_PYTHON_VERSION
|
||||||
|
- pip install --upgrade pip
|
||||||
|
- pip --version
|
||||||
|
test_windows: &test_windows
|
||||||
|
os: windows
|
||||||
|
language: shell
|
||||||
|
cache:
|
||||||
|
directories:
|
||||||
|
- /c/Python36
|
||||||
|
- /c/Python37
|
||||||
|
- /c/Python38
|
||||||
|
before_install:
|
||||||
|
- choco install python --version $JRNL_PYTHON_VERSION
|
||||||
|
- python -m pip install --upgrade pip
|
||||||
|
- pip --version
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
fast_finish: true
|
||||||
|
allow_failures:
|
||||||
|
- python: nightly
|
||||||
|
|
||||||
|
include:
|
||||||
|
- name: Lint, via Black
|
||||||
|
python: 3.8
|
||||||
|
script:
|
||||||
|
- black --version
|
||||||
|
- black --check . --verbose --diff
|
||||||
|
|
||||||
|
# Python 3.6 Tests
|
||||||
|
- name: Python 3.6 on Linux
|
||||||
|
python: 3.6
|
||||||
|
- <<: *test_mac
|
||||||
|
name: Python 3.6 on MacOS
|
||||||
|
python: 3.6
|
||||||
|
env:
|
||||||
|
- JRNL_PYTHON_VERSION=3.6.8
|
||||||
|
- <<: *test_windows
|
||||||
|
name: Python 3.6 on Windows
|
||||||
|
python: 3.6
|
||||||
|
env:
|
||||||
|
- JRNL_PYTHON_VERSION=3.6.8
|
||||||
|
- PATH=/c/Python36:/c/Python36/Scripts:$PATH
|
||||||
|
- PYTHONIOENCODING=UTF-8
|
||||||
|
|
||||||
|
# Python 3.7 Tests
|
||||||
|
- name: Python 3.7 on Linux
|
||||||
|
python: 3.7
|
||||||
|
- <<: *test_mac
|
||||||
|
name: Python 3.7 on MacOS
|
||||||
|
python: 3.7
|
||||||
|
env:
|
||||||
|
- JRNL_PYTHON_VERSION=3.7.5
|
||||||
|
- <<: *test_windows
|
||||||
|
name: Python 3.7 on Windows
|
||||||
|
python: 3.7
|
||||||
|
env:
|
||||||
|
- JRNL_PYTHON_VERSION=3.7.5
|
||||||
|
- PATH=/c/Python37:/c/Python37/Scripts:$PATH
|
||||||
|
- PYTHONIOENCODING=UTF-8
|
||||||
|
|
||||||
|
# Python 3.8 Tests
|
||||||
|
- name: Python 3.8 on Linux
|
||||||
|
python: 3.8
|
||||||
|
- <<: *test_mac
|
||||||
|
name: Python 3.8 on MacOS
|
||||||
|
python: 3.8
|
||||||
|
env:
|
||||||
|
- JRNL_PYTHON_VERSION=3.8.0
|
||||||
|
- <<: *test_windows
|
||||||
|
name: Python 3.8 on Windows
|
||||||
|
python: 3.8
|
||||||
|
env:
|
||||||
|
- JRNL_PYTHON_VERSION=3.8.0
|
||||||
|
- PATH=/c/Python38:/c/Python38/Scripts:$PATH
|
||||||
|
- PYTHONIOENCODING=UTF-8
|
||||||
|
|
||||||
|
# ... and beyond!
|
||||||
|
- name: Python nightly on Linux
|
||||||
|
python: nightly
|
||||||
|
|
||||||
|
# Specialty tests
|
||||||
|
- name: Python 3.7 on Linux, not UTC
|
||||||
|
python: 3.7
|
||||||
|
env:
|
||||||
|
- TZ=America/Edmonton
|
||||||
|
|
||||||
|
# Changelog for Unreleased changes
|
||||||
|
- stage: Update Changelog
|
||||||
|
if: (tag IS present) OR (branch = develop AND type NOT IN (pull_request))
|
||||||
|
install:
|
||||||
|
- echo 'Skipping install'
|
||||||
|
script:
|
||||||
|
- ./.build/generate_changelog.sh
|
||||||
|
|
||||||
|
- stage: Deploy
|
||||||
|
if: tag IS present
|
||||||
|
before_deploy:
|
||||||
|
- poetry version "$TRAVIS_TAG"
|
||||||
|
- echo __version__ = \"$TRAVIS_TAG\" > jrnl/__version__.py
|
||||||
|
- poetry build
|
||||||
|
deploy:
|
||||||
|
- provider: script
|
||||||
|
script: poetry publish
|
||||||
|
skip_cleanup: true
|
||||||
|
on:
|
||||||
|
branch: master
|
||||||
|
tags: true
|
||||||
|
after_deploy:
|
||||||
|
- git config --global user.email "jrnl.bot@gmail.com"
|
||||||
|
- git config --global user.name "Jrnl Bot"
|
||||||
|
- git checkout master
|
||||||
|
- git add pyproject.toml
|
||||||
|
- git commit -m "Incrementing version to ${TRAVIS_TAG} [ci skip]"
|
||||||
|
- git push https://${GITHUB_TOKEN}@github.com/jrnl-org/jrnl.git master
|
||||||
|
|
197
CHANGELOG.md
197
CHANGELOG.md
|
@ -1,14 +1,166 @@
|
||||||
Changelog
|
# Changelog
|
||||||
=========
|
|
||||||
|
|
||||||
## 2.0
|
## [Unreleased](https://github.com/jrnl-org/jrnl/)
|
||||||
|
|
||||||
* Cryptographical backend changed from PyCrypto to cryptography.io
|
[Full Changelog](https://github.com/jrnl-org/jrnl/compare/v2.2...HEAD)
|
||||||
* Config now respects XDG conventions and may move accordingly
|
|
||||||
* Config now saved as YAML
|
|
||||||
* Config name changed from `journals.jrnl_name.journal` to `journals.jrnl_name.path`
|
|
||||||
|
|
||||||
### 1.9 (July 21, 2014)
|
**Implemented enhancements:**
|
||||||
|
|
||||||
|
- Update YAML exporter to handle Dayone format [\#773](https://github.com/jrnl-org/jrnl/pull/773) ([MinchinWeb](https://github.com/MinchinWeb))
|
||||||
|
|
||||||
|
**Fixed bugs:**
|
||||||
|
|
||||||
|
- Listing all entries in DayOne Classic journal throws IndexError [\#786](https://github.com/jrnl-org/jrnl/pull/786) ([MinchinWeb](https://github.com/MinchinWeb))
|
||||||
|
- Add UTC support for failing DayOne tests [\#785](https://github.com/jrnl-org/jrnl/pull/785) ([MinchinWeb](https://github.com/MinchinWeb))
|
||||||
|
|
||||||
|
**Build:**
|
||||||
|
|
||||||
|
- Fix crashing unicode Travis tests on Windows and fail build if Windows tests fail [\#836](https://github.com/jrnl-org/jrnl/pull/836) ([micahellison](https://github.com/micahellison))
|
||||||
|
- Remove poetry from build system in pyproject config to fix `brew install` [\#830](https://github.com/jrnl-org/jrnl/pull/830) ([wren](https://github.com/wren))
|
||||||
|
- Fix all skipped tests on Travis Windows builds by preserving newlines [\#823](https://github.com/jrnl-org/jrnl/pull/823) ([micahellison](https://github.com/micahellison))
|
||||||
|
- Automagically update the changelog you see before your very eyes! [\#806](https://github.com/jrnl-org/jrnl/pull/806) ([wren](https://github.com/wren))
|
||||||
|
|
||||||
|
**Updated documentation:**
|
||||||
|
|
||||||
|
- Updating/clarifying template explanation [\#829](https://github.com/jrnl-org/jrnl/pull/829) ([heymajor](https://github.com/heymajor))
|
||||||
|
|
||||||
|
## [v2.2](https://pypi.org/project/jrnl/v2.2/) (2020-02-01)
|
||||||
|
|
||||||
|
[Full Changelog](https://github.com/jrnl-org/jrnl/compare/v2.1.1...v2.2)
|
||||||
|
|
||||||
|
|
||||||
|
**Implemented enhancements:**
|
||||||
|
|
||||||
|
- Update YAML exporter to handle Dayone format [\#773](https://github.com/jrnl-org/jrnl/pull/773) ([MinchinWeb](https://github.com/MinchinWeb))
|
||||||
|
- Full text search \(case insensitive\) with "-contains" [\#740](https://github.com/jrnl-org/jrnl/pull/740) ([empireshades](https://github.com/empireshades))
|
||||||
|
- Reduce startup time by 55% [\#719](https://github.com/jrnl-org/jrnl/pull/719) ([maebert](https://github.com/maebert))
|
||||||
|
- Refactor password logic to prevent accidental password leakage [\#708](https://github.com/jrnl-org/jrnl/pull/708) ([pspeter](https://github.com/pspeter))
|
||||||
|
- Password confirmation [\#706](https://github.com/jrnl-org/jrnl/pull/706) ([pspeter](https://github.com/pspeter))
|
||||||
|
|
||||||
|
**Fixed bugs:**
|
||||||
|
|
||||||
|
- Close temp file before passing it to editor to prevent file locking issues in Windows [\#792](https://github.com/jrnl-org/jrnl/pull/792) ([micahellison](https://github.com/micahellison))
|
||||||
|
- Fix crash while encrypting a journal on first run without saving password [\#789](https://github.com/jrnl-org/jrnl/pull/789) ([dbxnr](https://github.com/dbxnr))
|
||||||
|
|
||||||
|
**Build:**
|
||||||
|
|
||||||
|
- Fix issue where jrnl would always out 'source' for version, fix Poetry config to build and publish properly [\#820](https://github.com/jrnl-org/jrnl/pull/820) ([wren](https://github.com/wren))
|
||||||
|
- Unpin poetry [\#808](https://github.com/jrnl-org/jrnl/pull/808) ([wren](https://github.com/wren))
|
||||||
|
- Fix all skipped tests on Travis Windows builds by preserving newlines [\#823](https://github.com/jrnl-org/jrnl/pull/823) ([micahellison](https://github.com/micahellison))
|
||||||
|
- Change PyPI auth method in build pipeline [\#807](https://github.com/jrnl-org/jrnl/pull/807) ([wren](https://github.com/wren))
|
||||||
|
- Automagically update the changelog you see before your very eyes! [\#806](https://github.com/jrnl-org/jrnl/pull/806) ([wren](https://github.com/wren))
|
||||||
|
- Update Black version and lock file to fix builds on develop branch [\#784](https://github.com/jrnl-org/jrnl/pull/784) ([wren](https://github.com/wren))
|
||||||
|
- Run black formatter on codebase for standardization [\#778](https://github.com/jrnl-org/jrnl/pull/778) ([wren](https://github.com/wren))
|
||||||
|
- Skip Broken Windows Tests [\#772](https://github.com/jrnl-org/jrnl/pull/772) ([wren](https://github.com/wren))
|
||||||
|
- Black Formatter [\#769](https://github.com/jrnl-org/jrnl/pull/769) ([MinchinWeb](https://github.com/MinchinWeb))
|
||||||
|
- Update lock file and testing suite for Python 3.8 [\#765](https://github.com/jrnl-org/jrnl/pull/765) ([wren](https://github.com/wren))
|
||||||
|
- Fix CI config to only deploy once [\#761](https://github.com/jrnl-org/jrnl/pull/761) ([wren](https://github.com/wren))
|
||||||
|
- More Travis-CI Testing [\#759](https://github.com/jrnl-org/jrnl/pull/759) ([MinchinWeb](https://github.com/MinchinWeb))
|
||||||
|
|
||||||
|
**Updated documentation:**
|
||||||
|
|
||||||
|
- Explain how fish can be configured to exclude jrnl commands from history by default [\#809](https://github.com/jrnl-org/jrnl/pull/809) ([aureooms](https://github.com/aureooms))
|
||||||
|
- Remove merge marker in recipes.md [\#782](https://github.com/jrnl-org/jrnl/pull/782) ([markphelps](https://github.com/markphelps))
|
||||||
|
- Fix merge conflict left-over [\#767](https://github.com/jrnl-org/jrnl/pull/767) ([thejspr](https://github.com/thejspr))
|
||||||
|
- Display header in docs on mobile devices [\#763](https://github.com/jrnl-org/jrnl/pull/763) ([maebert](https://github.com/maebert))
|
||||||
|
|
||||||
|
## [v2.1.1](https://pypi.org/project/jrnl/v2.1.1/) (2019-11-26)
|
||||||
|
|
||||||
|
[Full Changelog](https://github.com/jrnl-org/jrnl/compare/v2.1.post2...v2.1.1)
|
||||||
|
|
||||||
|
**Implemented enhancements:**
|
||||||
|
|
||||||
|
- Support Python 3.6+ [\#710](https://github.com/jrnl-org/jrnl/pull/710) ([pspeter](https://github.com/pspeter))
|
||||||
|
- Drop Python 2 support, add mocks in tests [\#705](https://github.com/jrnl-org/jrnl/pull/705) ([pspeter](https://github.com/pspeter))
|
||||||
|
|
||||||
|
**Fixed bugs:**
|
||||||
|
|
||||||
|
- Prevent readline usage on Windows, which was causing Active Python crashes on install [\#751](https://github.com/jrnl-org/jrnl/pull/751) ([micahellison](https://github.com/micahellison))
|
||||||
|
- Exit jrnl if no text entered into editor [\#744](https://github.com/jrnl-org/jrnl/pull/744) ([alichtman](https://github.com/alichtman))
|
||||||
|
- Fix crash when no keyring backend available [\#699](https://github.com/jrnl-org/jrnl/pull/699) ([pspeter](https://github.com/pspeter))
|
||||||
|
- Fix parsing Journals using a little-endian date format [\#694](https://github.com/jrnl-org/jrnl/pull/694) ([pspeter](https://github.com/pspeter))
|
||||||
|
|
||||||
|
**Updated documentation:**
|
||||||
|
|
||||||
|
- Update developer documentation [\#752](https://github.com/jrnl-org/jrnl/pull/752) ([micahellison](https://github.com/micahellison))
|
||||||
|
- Create templates for issues and pull requests [\#679](https://github.com/jrnl-org/jrnl/pull/679) ([C0DK](https://github.com/C0DK))
|
||||||
|
- Smaller doc fixes [\#649](https://github.com/jrnl-org/jrnl/pull/649) ([maebert](https://github.com/maebert))
|
||||||
|
- Move to mkdocs [\#611](https://github.com/jrnl-org/jrnl/pull/611) ([maebert](https://github.com/maebert))
|
||||||
|
|
||||||
|
## [v2.1.post2](https://pypi.org/project/jrnl/v2.1.post2/) (2019-11-11)
|
||||||
|
|
||||||
|
[Full Changelog](https://github.com/jrnl-org/jrnl/compare/v2.0.1...v2.1.post2)
|
||||||
|
|
||||||
|
**Fixed bugs:**
|
||||||
|
|
||||||
|
- Expand paths that use ~ to full path [\#704](https://github.com/jrnl-org/jrnl/pull/704) ([MinchinWeb](https://github.com/MinchinWeb))
|
||||||
|
|
||||||
|
**Build:**
|
||||||
|
|
||||||
|
- Separate local dev from pipeline releases [\#684](https://github.com/jrnl-org/jrnl/pull/684) ([wren](https://github.com/wren))
|
||||||
|
- Update version handling in source and travis deployments [\#683](https://github.com/jrnl-org/jrnl/pull/683) ([wren](https://github.com/wren))
|
||||||
|
- Use Poetry for dependency management and deployments [\#612](https://github.com/jrnl-org/jrnl/pull/612) ([maebert](https://github.com/maebert))
|
||||||
|
|
||||||
|
**Updated documentation:**
|
||||||
|
|
||||||
|
- Fix typos, spelling [\#734](https://github.com/jrnl-org/jrnl/pull/734) ([MinchinWeb](https://github.com/MinchinWeb))
|
||||||
|
|
||||||
|
## [v2.0.1](https://pypi.org/project/jrnl/v2.0.1/) (2019-09-26)
|
||||||
|
|
||||||
|
[Full Changelog](https://github.com/jrnl-org/jrnl/compare/v2.0.0...v2.0.1)
|
||||||
|
|
||||||
|
**Implemented enhancements:**
|
||||||
|
|
||||||
|
- Switch to hashmark Markdown headers on export \(Mk II\) [\#639](https://github.com/jrnl-org/jrnl/pull/639) ([MinchinWeb](https://github.com/MinchinWeb))
|
||||||
|
- Add '-not' flag for excluding tags from filter [\#637](https://github.com/jrnl-org/jrnl/pull/637) ([jprof](https://github.com/jprof))
|
||||||
|
- Handle KeyboardInterrupt when installing journal [\#550](https://github.com/jrnl-org/jrnl/pull/550) ([silenc3r](https://github.com/silenc3r))
|
||||||
|
|
||||||
|
**Fixed bugs:**
|
||||||
|
|
||||||
|
- Change pyYAML required version [\#660](https://github.com/jrnl-org/jrnl/pull/660) ([etnnth](https://github.com/etnnth))
|
||||||
|
|
||||||
|
**Updated documentation:**
|
||||||
|
|
||||||
|
- Fix references to Sphinx in CONTRIBUTING.md [\#655](https://github.com/jrnl-org/jrnl/pull/655) ([maebert](https://github.com/maebert))
|
||||||
|
|
||||||
|
## [v2.0.0](https://pypi.org/project/jrnl/v2.0.0/) (2019-08-24)
|
||||||
|
|
||||||
|
[Full Changelog](https://github.com/jrnl-org/jrnl/compare/1.9.8...v2.0.0)
|
||||||
|
|
||||||
|
🚨 **BREAKING CHANGES** 🚨
|
||||||
|
|
||||||
|
**Implemented enhancements:**
|
||||||
|
- Change cryptographic backend from PyCrypto to cryptography.io
|
||||||
|
- Config now respects XDG conventions and may move accordingly
|
||||||
|
- Config name changed from `journals.jrnl_name.journal` to `journals.jrnl_name.path`
|
||||||
|
|
||||||
|
**Fixed bugs:**
|
||||||
|
|
||||||
|
- Confirm that each journal can be parsed during upgrade, and abort upgrade if not [\#650](https://github.com/jrnl-org/jrnl/pull/650) ([micahellison](https://github.com/micahellison))
|
||||||
|
- Escape dates in square brackets [\#644](https://github.com/jrnl-org/jrnl/pull/644) ([wren](https://github.com/wren))
|
||||||
|
- Create encrypted journal [\#641](https://github.com/jrnl-org/jrnl/pull/641) ([gregorybodnar](https://github.com/gregorybodnar))
|
||||||
|
- Resolve issues around unreadable dates to allow markdown footnotes and prevent accidental deletion [\#623](https://github.com/jrnl-org/jrnl/pull/623) ([micahellison](https://github.com/micahellison))
|
||||||
|
- Update crypto module \#610 [\#621](https://github.com/jrnl-org/jrnl/pull/621) ([wren](https://github.com/wren))
|
||||||
|
- Fix issue \#584 YAMLLoadWarning [\#585](https://github.com/jrnl-org/jrnl/pull/585) ([wren](https://github.com/wren))
|
||||||
|
|
||||||
|
**Deprecated:**
|
||||||
|
|
||||||
|
- Deprecate Python 2 [\#624](https://github.com/jrnl-org/jrnl/pull/624) ([micahellison](https://github.com/micahellison))
|
||||||
|
- Config now saved as YAML (no more JSON)
|
||||||
|
|
||||||
|
**Build:**
|
||||||
|
|
||||||
|
- change pinned label to a super cool emoji ⭐️ [\#646](https://github.com/jrnl-org/jrnl/pull/646) ([wren](https://github.com/wren))
|
||||||
|
- Update Travis build badge and restore pypi badges [\#603](https://github.com/jrnl-org/jrnl/pull/603) ([micahellison](https://github.com/micahellison))
|
||||||
|
|
||||||
|
**Updated documentation:**
|
||||||
|
|
||||||
|
- Mention lack of Day One support and relevant history in readme [\#608](https://github.com/jrnl-org/jrnl/pull/608) ([micahellison](https://github.com/micahellison))
|
||||||
|
- Add a code of conduct file \(rather than adding to contributing\) [\#604](https://github.com/jrnl-org/jrnl/pull/604) ([wren](https://github.com/wren))
|
||||||
|
- Update docs to reflect merging jrnl-plus fork back upstream [\#601](https://github.com/jrnl-org/jrnl/pull/601) ([micahellison](https://github.com/micahellison))
|
||||||
|
- Add instructions for VS Code [\#544](https://github.com/jrnl-org/jrnl/pull/544) ([emceeaich](https://github.com/emceeaich))
|
||||||
|
|
||||||
|
## v1.9 (2014-07-21)
|
||||||
|
|
||||||
* __1.9.5__ Multi-word tags for DayOne Journals
|
* __1.9.5__ Multi-word tags for DayOne Journals
|
||||||
* __1.9.4__ Fixed: Order of journal entries in file correct after --edit'ing
|
* __1.9.4__ Fixed: Order of journal entries in file correct after --edit'ing
|
||||||
|
@ -17,7 +169,7 @@ Changelog
|
||||||
* __1.9.1__ Fixed: Dates in the future can be parsed as well.
|
* __1.9.1__ Fixed: Dates in the future can be parsed as well.
|
||||||
* __1.9.0__ Improved: Greatly improved date parsing. Also added an `-on` option for filtering
|
* __1.9.0__ Improved: Greatly improved date parsing. Also added an `-on` option for filtering
|
||||||
|
|
||||||
### 1.8 (May 22, 2014)
|
## v1.8 (2014-05-22)
|
||||||
|
|
||||||
* __1.8.7__ Fixed: -from and -to filters are inclusive (thanks to @grplyler)
|
* __1.8.7__ Fixed: -from and -to filters are inclusive (thanks to @grplyler)
|
||||||
* __1.8.6__ Improved: Tags like @C++ and @OS/2 work, too (thanks to @chaitan94)
|
* __1.8.6__ Improved: Tags like @C++ and @OS/2 work, too (thanks to @chaitan94)
|
||||||
|
@ -28,7 +180,7 @@ Changelog
|
||||||
* __1.8.1__ Minor bug fixes
|
* __1.8.1__ Minor bug fixes
|
||||||
* __1.8.0__ Official support for python 3.4
|
* __1.8.0__ Official support for python 3.4
|
||||||
|
|
||||||
### 1.7 (December 22, 2013)
|
## v1.7 (2013-12-22)
|
||||||
|
|
||||||
* __1.7.22__ Fixed an issue with writing files when exporting entries containing non-ascii characters.
|
* __1.7.22__ Fixed an issue with writing files when exporting entries containing non-ascii characters.
|
||||||
* __1.7.21__ jrnl now uses PKCS#7 padding.
|
* __1.7.21__ jrnl now uses PKCS#7 padding.
|
||||||
|
@ -54,7 +206,7 @@ Changelog
|
||||||
* __1.7.0__ Edit encrypted or DayOne journals with `jrnl --edit`.
|
* __1.7.0__ Edit encrypted or DayOne journals with `jrnl --edit`.
|
||||||
|
|
||||||
|
|
||||||
### 1.6 (November 5, 2013)
|
## v1.6 (2013-11-05)
|
||||||
|
|
||||||
* __1.6.6__ -v prints the current version, also better strings for windows users. Furthermore, jrnl/jrnl.py moved to jrnl/cli.py
|
* __1.6.6__ -v prints the current version, also better strings for windows users. Furthermore, jrnl/jrnl.py moved to jrnl/cli.py
|
||||||
* __1.6.5__ Allows composing multi-line entries on the command line or importing files
|
* __1.6.5__ Allows composing multi-line entries on the command line or importing files
|
||||||
|
@ -64,7 +216,7 @@ Changelog
|
||||||
* __1.6.1__ Attempts to fix broken config files automatically
|
* __1.6.1__ Attempts to fix broken config files automatically
|
||||||
* __1.6.0__ Passwords are now saved in the key-chain. The `password` field in `.jrnl_config` is soft-deprecated.
|
* __1.6.0__ Passwords are now saved in the key-chain. The `password` field in `.jrnl_config` is soft-deprecated.
|
||||||
|
|
||||||
### 1.5 (August 6, 2013)
|
## v1.5 (2013-08-06)
|
||||||
|
|
||||||
* __1.5.7__ The `~` in journal config paths will now expand properly to e.g. `/Users/maebert`
|
* __1.5.7__ The `~` in journal config paths will now expand properly to e.g. `/Users/maebert`
|
||||||
* __1.5.6__ Fixed: Fixed a bug where on OS X, the timezone could only be accessed on administrator accounts.
|
* __1.5.6__ Fixed: Fixed a bug where on OS X, the timezone could only be accessed on administrator accounts.
|
||||||
|
@ -75,23 +227,23 @@ Changelog
|
||||||
* __1.5.1__ Fixed: Fixed a bug introduced in 1.5.0 that caused the entire journal to be printed after composing an entry
|
* __1.5.1__ Fixed: Fixed a bug introduced in 1.5.0 that caused the entire journal to be printed after composing an entry
|
||||||
* __1.5.0__ Exporting, encrypting and displaying tags now takes your filter options into account. So you could export everything before May 2012: `jrnl -to 'may 2012' --export json`. Or encrypt all entries tagged with `@work` into a new journal: `jrnl @work --encrypt work_journal.txt`. Or display all tags of posts where Bob is also tagged: `jrnl @bob --tags`
|
* __1.5.0__ Exporting, encrypting and displaying tags now takes your filter options into account. So you could export everything before May 2012: `jrnl -to 'may 2012' --export json`. Or encrypt all entries tagged with `@work` into a new journal: `jrnl @work --encrypt work_journal.txt`. Or display all tags of posts where Bob is also tagged: `jrnl @bob --tags`
|
||||||
|
|
||||||
### 1.4 (July 22, 2013)
|
## v1.4 (2013-07-22)
|
||||||
|
|
||||||
* __1.4.2__ Fixed: Tagging works again
|
* __1.4.2__ Fixed: Tagging works again
|
||||||
* __1.4.0__ Unifies encryption between Python 2 and 3. If you have problems reading encrypted journals afterwards, first decrypt your journal with the __old__ jrnl version (install with `pip install jrnl==1.3.1`, then `jrnl --decrypt`), upgrade jrnl (`pip install jrnl --upgrade`) and encrypt it again (`jrnl --encrypt`).
|
* __1.4.0__ Unifies encryption between Python 2 and 3. If you have problems reading encrypted journals afterwards, first decrypt your journal with the __old__ jrnl version (install with `pip install jrnl==1.3.1`, then `jrnl --decrypt`), upgrade jrnl (`pip install jrnl --upgrade`) and encrypt it again (`jrnl --encrypt`).
|
||||||
|
|
||||||
### 1.3 (July 17, 2013)
|
## v1.3 (2013-07-17)
|
||||||
|
|
||||||
* __1.3.2__ Everything that is not direct output of jrnl will be written stderr to improve integration
|
* __1.3.2__ Everything that is not direct output of jrnl will be written stderr to improve integration
|
||||||
* __1.3.0__ Export to multiple files
|
* __1.3.0__ Export to multiple files
|
||||||
* __1.3.0__ Feature to export to given output file
|
* __1.3.0__ Feature to export to given output file
|
||||||
|
|
||||||
### 1.2 (July 15, 2013)
|
## v1.2 (2013-07-15)
|
||||||
|
|
||||||
* __1.2.0__ Fixed: Timezone support for DayOne
|
* __1.2.0__ Fixed: Timezone support for DayOne
|
||||||
|
|
||||||
|
|
||||||
### 1.1 (June 9, 2013)
|
## v1.1 (2013-06-09)
|
||||||
|
|
||||||
* __1.1.1__ Fixed: Unicode and Python3 issues resolved.
|
* __1.1.1__ Fixed: Unicode and Python3 issues resolved.
|
||||||
* __1.1.0__
|
* __1.1.0__
|
||||||
|
@ -99,7 +251,7 @@ Changelog
|
||||||
* Nicer error message when there is a syntactical error in your config file.
|
* Nicer error message when there is a syntactical error in your config file.
|
||||||
* Unicode support
|
* Unicode support
|
||||||
|
|
||||||
### 1.0 (March 4, 2013)
|
## v1.0 (2013-03-04)
|
||||||
|
|
||||||
* __1.0.5__ Backwards compatibility with `parsedatetime` 0.8.7
|
* __1.0.5__ Backwards compatibility with `parsedatetime` 0.8.7
|
||||||
* __1.0.4__
|
* __1.0.4__
|
||||||
|
@ -122,7 +274,7 @@ Changelog
|
||||||
* Fixed: A bug where jrnl would not add entries without timestamp
|
* Fixed: A bug where jrnl would not add entries without timestamp
|
||||||
* Fixed: Support for parsedatetime 1.x
|
* Fixed: Support for parsedatetime 1.x
|
||||||
|
|
||||||
### 0.3 (May 24, 2012)
|
## v0.3 (2012-05-24)
|
||||||
|
|
||||||
* __0.3.2__ Converts `\n` to new lines (if using directly on a command line, make sure to wrap your entry with quotes).
|
* __0.3.2__ Converts `\n` to new lines (if using directly on a command line, make sure to wrap your entry with quotes).
|
||||||
* __0.3.1__
|
* __0.3.1__
|
||||||
|
@ -135,7 +287,7 @@ Changelog
|
||||||
* Fixed: Bug where composed entry is lost when the journal file fails to load
|
* Fixed: Bug where composed entry is lost when the journal file fails to load
|
||||||
* Changed directory structure and install scripts (removing the necessity to make an alias from `jrnl` to `jrnl.py`)
|
* Changed directory structure and install scripts (removing the necessity to make an alias from `jrnl` to `jrnl.py`)
|
||||||
|
|
||||||
### 0.2 (April 16, 2012)
|
## v0.2 (2012-04-16)
|
||||||
|
|
||||||
* __0.2.4__
|
* __0.2.4__
|
||||||
* Fixed: Parsing of new lines in journal files and entries
|
* Fixed: Parsing of new lines in journal files and entries
|
||||||
|
@ -153,7 +305,7 @@ Changelog
|
||||||
* Encrypts using CBC
|
* Encrypts using CBC
|
||||||
* Fixed: `key` has been renamed to `password` in config to avoid confusion. (The key use to encrypt and decrypt a journal is the SHA256-hash of the password.)
|
* Fixed: `key` has been renamed to `password` in config to avoid confusion. (The key use to encrypt and decrypt a journal is the SHA256-hash of the password.)
|
||||||
|
|
||||||
### 0.1 (April 13, 2012)
|
## v0.1 (2012-04-13)
|
||||||
|
|
||||||
|
|
||||||
* __0.1.1__
|
* __0.1.1__
|
||||||
|
@ -166,6 +318,9 @@ Changelog
|
||||||
* Filtering by tags and dates
|
* Filtering by tags and dates
|
||||||
* Fixed: Now using dedicated classes for Journals and entries
|
* Fixed: Now using dedicated classes for Journals and entries
|
||||||
|
|
||||||
### 0.0 (March 29, 2012)
|
## v0.0 (2012-03-29)
|
||||||
|
|
||||||
* __0.0.1__ Composing entries works. That's pretty much it.
|
* __0.0.1__ Composing entries works. That's pretty much it.
|
||||||
|
|
||||||
|
|
||||||
|
\* *This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)*
|
||||||
|
|
|
@ -1,35 +1,45 @@
|
||||||
Contributing
|
# Contributing
|
||||||
============
|
|
||||||
|
|
||||||
If you use jrnl, you can totally make our day by just saying "thanks for the code." It's your chance to make a programmer happy today! If you have a moment, let us know what you use jrnl for and how; it'll help us to make it even better!
|
If you use jrnl, you can totally make our day by just saying "thanks for the code." It's your chance to make a programmer happy today! If you have a moment, let us know what you use jrnl for and how; it'll help us to make it even better!
|
||||||
|
|
||||||
|
|
||||||
Docs & Typos
|
# Table of Contents
|
||||||
------------
|
* [Docs and Typos](#docs-and-typos)
|
||||||
|
* [Bugs](#bugs)
|
||||||
|
* [Feature requests and ideas](#feature-requests-and-ideas)
|
||||||
|
* [New programmers and programmers new to python](#new-programmers-and-programmers-new-to-python)
|
||||||
|
* [Developing jrnl](#developing-jrnl)
|
||||||
|
|
||||||
|
|
||||||
|
## Docs and Typos
|
||||||
|
|
||||||
If you find a typo or a mistake in the docs, please fix it right away and send a pull request. The Right Way™ to fix the docs is to edit the `docs/*.md` files on the **master** branch. You can see the result if you run `make html` inside the project's root directory, which will open a browser that hot-reloads as you change the docs. This requires [mkdocs](https://www.mkdocs.org) to be installed. The `gh-pages` branch is automatically maintained and updates from `master`; you should never have to edit that.
|
If you find a typo or a mistake in the docs, please fix it right away and send a pull request. The Right Way™ to fix the docs is to edit the `docs/*.md` files on the **master** branch. You can see the result if you run `make html` inside the project's root directory, which will open a browser that hot-reloads as you change the docs. This requires [mkdocs](https://www.mkdocs.org) to be installed. The `gh-pages` branch is automatically maintained and updates from `master`; you should never have to edit that.
|
||||||
|
|
||||||
Bugs
|
## Bugs
|
||||||
----
|
|
||||||
|
|
||||||
Unfortunately, bugs happen. If you found one, please [open a new issue](https://github.com/jrnl-org/jrnl/issues/new) and describe it as well as possible. If you're a programmer with some time, go ahead and send us a pull request! We'll review as quickly as we can.
|
Unfortunately, bugs happen. If you found one, please [open a new issue](https://github.com/jrnl-org/jrnl/issues/new/choose) and describe it as well as possible. If you're a programmer with some time, go ahead and send us a pull request that references the issue! We'll review as quickly as we can.
|
||||||
|
|
||||||
|
## Feature requests and ideas
|
||||||
|
|
||||||
Feature requests and ideas
|
So, you have an idea for a great feature? Awesome! We'd love to hear from you! Please [open a new issue](https://github.com/jrnl-org/jrnl/issues/new/choose) and describe the goal of the feature, and any relevant use cases. We'll discuss the issue with you, and decide if it's a good fit for the project.
|
||||||
--------------------------
|
|
||||||
|
|
||||||
So, you have an idea for a great feature? Awesome! We'd love to hear from you! Please [open a new issue](https://github.com/jrnl-org/jrnl/issues) and describe the goal of the feature, and any relvant use cases. We'll discuss the issue with you, and decide if it's a good fit for the project.
|
|
||||||
|
|
||||||
When discussing new features, please keep in mind our design goals. jrnl strives to do one thing well. To us, that means:
|
When discussing new features, please keep in mind our design goals. jrnl strives to do one thing well. To us, that means:
|
||||||
|
|
||||||
* be _slim_
|
* be _slim_
|
||||||
* have a simple interface
|
* have a simple interface
|
||||||
* avoid dupicating functionality
|
* avoid duplicating functionality
|
||||||
|
|
||||||
|
## New programmers and programmers new to python
|
||||||
|
|
||||||
A short note for new programmers and programmers new to python
|
Although jrnl has grown quite a bit since its inception, the overall complexity (for an end-user program) is fairly low, and we hope you'll find the code easy enough to understand.
|
||||||
--------------------------------------------------------------
|
|
||||||
|
|
||||||
Although jrnl has grown quite a bit since its inception. The overall complexity (for an end-user program) is fairly low, and we hope you'll find the code easy enough to understand.
|
|
||||||
|
|
||||||
If you have a question, please don't hesitate to ask! Python is known for its welcoming community and openness to novice programmers, so feel free to fork the code and play around with it! If you create something you want to share with us, please create a pull request. We never expect pull requests to be perfect, idiomatic, instantly mergeable code. We can work through it together!
|
If you have a question, please don't hesitate to ask! Python is known for its welcoming community and openness to novice programmers, so feel free to fork the code and play around with it! If you create something you want to share with us, please create a pull request. We never expect pull requests to be perfect, idiomatic, instantly mergeable code. We can work through it together!
|
||||||
|
|
||||||
|
## Developing jrnl
|
||||||
|
|
||||||
|
The jrnl source uses [poetry](https://poetry.eustace.io/) for dependency management. You will need to install it to develop journal.
|
||||||
|
|
||||||
|
* To run tests: `make test` (or `poetry run behave` if on Windows)
|
||||||
|
* To run the source: `poetry install` then `poetry shell` then run `jrnl` with or without arguments as necessary
|
||||||
|
|
||||||
|
For testing, jrnl uses [behave](https://behave.readthedocs.io/).
|
||||||
|
|
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (c) 2014 Manuel Ebert
|
Copyright (c) 2014-2019 Manuel Ebert
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|
|
@ -3,15 +3,19 @@
|
||||||
## Configuration File
|
## Configuration File
|
||||||
|
|
||||||
You can configure the way jrnl behaves in a configuration file. By
|
You can configure the way jrnl behaves in a configuration file. By
|
||||||
default, this is `~/.jrnl_config`. If you have the `XDG_CONFIG_HOME`
|
default, this is `~/.config/jrnl/jrnl.yaml`. If you have the `XDG_CONFIG_HOME`
|
||||||
variable set, the configuration file will be saved as
|
variable set, the configuration file will be saved as
|
||||||
`$XDG_CONFIG_HOME/jrnl/.jrnl_config`.
|
`$XDG_CONFIG_HOME/jrnl/jrnl.yaml`.
|
||||||
|
|
||||||
!!! note
|
!!! note
|
||||||
On Windows, The configuration file is typically found at `C:\Users\[Your Username]\.jrnl_config`.
|
On Windows, the configuration file is typically found at `%USERPROFILE%\.config\jrnl\jrnl.yaml`.
|
||||||
|
|
||||||
The configuration file is a simple JSON file with the following options
|
The configuration file is a YAML file with the following options
|
||||||
and can be edited with any plain text editor.
|
and can be edited with a plain text editor.
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
Backup your config file before editing. Changes to the config file
|
||||||
|
have destructive effects on your journal!
|
||||||
|
|
||||||
- `journals`
|
- `journals`
|
||||||
paths to your journal files
|
paths to your journal files
|
||||||
|
@ -51,46 +55,16 @@ and can be edited with any plain text editor.
|
||||||
Or use the built-in prompt or an external editor to compose your
|
Or use the built-in prompt or an external editor to compose your
|
||||||
entries.
|
entries.
|
||||||
|
|
||||||
## DayOne Integration
|
|
||||||
|
|
||||||
Using your DayOne journal instead of a flat text file is dead simple --
|
|
||||||
instead of pointing to a text file, change your `.jrnl_config` to point
|
|
||||||
to your DayOne journal. This is a folder named something like
|
|
||||||
`Journal_dayone` or `Journal.dayone`, and it's located at
|
|
||||||
|
|
||||||
- `~/Library/Application Support/Day One/` by default
|
|
||||||
- `~/Dropbox/Apps/Day One/` if you're syncing with Dropbox and
|
|
||||||
- `~/Library/Mobile
|
|
||||||
Documents/5U8NS4GX82~com~dayoneapp~dayone/Documents/` if you're
|
|
||||||
syncing with iCloud.
|
|
||||||
|
|
||||||
Instead of all entries being in a single file, each entry will live in a
|
|
||||||
separate `plist` file. So your `.jrnl_config` should look like this:
|
|
||||||
|
|
||||||
``` javascript
|
|
||||||
{
|
|
||||||
...
|
|
||||||
"journals": {
|
|
||||||
"default": "~/journal.txt",
|
|
||||||
"dayone": "~/Library/Mobile Documents/5U8NS4GX82~com~dayoneapp~dayone/Documents/Journal_dayone"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Multiple journal files
|
## Multiple journal files
|
||||||
|
|
||||||
You can configure `jrnl`to use with multiple journals (eg.
|
You can configure `jrnl`to use with multiple journals (eg.
|
||||||
`private` and `work`) by defining more journals in your `.jrnl_config`,
|
`private` and `work`) by defining more journals in your `jrnl.yaml`,
|
||||||
for example:
|
for example:
|
||||||
|
|
||||||
``` javascript
|
``` yaml
|
||||||
{
|
journals:
|
||||||
...
|
default: ~\journal.txt
|
||||||
"journals": {
|
work: ~\work.txt
|
||||||
"default": "~/journal.txt",
|
|
||||||
"work": "~/work.txt"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The `default` journal gets created the first time you start `jrnl`
|
The `default` journal gets created the first time you start `jrnl`
|
||||||
|
@ -106,26 +80,22 @@ will both use `~/work.txt`, while `jrnl -n 3` will display the last
|
||||||
three entries from `~/journal.txt` (and so does `jrnl default -n 3`).
|
three entries from `~/journal.txt` (and so does `jrnl default -n 3`).
|
||||||
|
|
||||||
You can also override the default options for each individual journal.
|
You can also override the default options for each individual journal.
|
||||||
If you `.jrnl_config` looks like this:
|
If your `jrnl.yaml` looks like this:
|
||||||
|
|
||||||
``` javascript
|
``` yaml
|
||||||
{
|
encrypt: false
|
||||||
...
|
journals:
|
||||||
"encrypt": false
|
default: ~/journal.txt
|
||||||
"journals": {
|
work:
|
||||||
"default": "~/journal.txt",
|
journal: ~/work.txt
|
||||||
"work": {
|
encrypt: true
|
||||||
"journal": "~/work.txt",
|
food: ~/my_recipes.txt
|
||||||
"encrypt": true
|
|
||||||
},
|
|
||||||
"food": "~/my_recipes.txt",
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Your `default` and your `food` journals won't be encrypted, however your
|
Your `default` and your `food` journals won't be encrypted, however your
|
||||||
`work` journal will! You can override all options that are present at
|
`work` journal will! You can override all options that are present at
|
||||||
the top level of `.jrnl_config`, just make sure that at the very least
|
the top level of `jrnl.yaml`, just make sure that at the very least
|
||||||
you specify a `"journal": ...` key that points to the journal file of
|
you specify a `journal: ...` key that points to the journal file of
|
||||||
that journal.
|
that journal.
|
||||||
|
|
||||||
!!! note
|
!!! note
|
||||||
|
|
|
@ -45,9 +45,17 @@ p {
|
||||||
/* No-one likes lines that are 400 characters long. */
|
/* No-one likes lines that are 400 characters long. */
|
||||||
div.rst-content {max-width: 54em;}
|
div.rst-content {max-width: 54em;}
|
||||||
|
|
||||||
.wy-side-nav-search, .wy-nav-top, .wy-menu-vertical li.current {
|
.wy-side-nav-search, .wy-menu-vertical li.current {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
.wy-nav-top {
|
||||||
|
background-image: linear-gradient(-211deg, #95699C 0%, #604385 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wy-nav-top .fa-bars {
|
||||||
|
line-height: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
.wy-side-nav-search a.icon-home {
|
.wy-side-nav-search a.icon-home {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 250px;
|
max-width: 250px;
|
||||||
|
|
|
@ -2,9 +2,9 @@
|
||||||
|
|
||||||
## Encrypting and decrypting
|
## Encrypting and decrypting
|
||||||
|
|
||||||
If you don't choose to encrypt your file when you run
|
If you don’t choose to encrypt your file when you run
|
||||||
`jrnl` for the first time, you can encrypt
|
`jrnl` for the first time, you can encrypt
|
||||||
your existing journal file or change its password using
|
your existing journal file or change its password using this:
|
||||||
|
|
||||||
``` sh
|
``` sh
|
||||||
jrnl --encrypt
|
jrnl --encrypt
|
||||||
|
@ -18,44 +18,54 @@ replaced by the encrypted file. Conversely,
|
||||||
jrnl --decrypt
|
jrnl --decrypt
|
||||||
```
|
```
|
||||||
|
|
||||||
will replace your encrypted journal file by a Journal in plain text. You
|
will replace your encrypted journal file with a journal in plain text. You
|
||||||
can also specify a filename, ie. `jrnl --decrypt plain_text_copy.txt`,
|
can also specify a filename, i.e. `jrnl --decrypt plain_text_copy.txt`,
|
||||||
to leave your original file untouched.
|
to leave your original file untouched.
|
||||||
|
|
||||||
## Storing passwords in your keychain
|
## Storing passwords in your keychain
|
||||||
|
|
||||||
Whenever you encrypt your journal, you are asked whether you want to
|
Whenever you encrypt your journal, you are asked whether you want to
|
||||||
store the encryption password in your keychain. If you do this, you
|
store the encryption password in your keychain. If you do this, you
|
||||||
won't have to enter your password every time you want to write or read
|
won’t have to enter your password every time you want to write or read
|
||||||
your journal.
|
your journal.
|
||||||
|
|
||||||
If you don't initially store the password in the keychain but decide to
|
If you don’t initially store the password in the keychain but decide to
|
||||||
do so at a later point -- or maybe want to store it on one computer but
|
do so at a later point – or maybe want to store it on one computer but
|
||||||
not on another -- you can simply run `jrnl --encrypt` on an encrypted
|
not on another – you can simply run `jrnl --encrypt` on an encrypted
|
||||||
journal and use the same password again.
|
journal and use the same password again.
|
||||||
|
|
||||||
## A note on security
|
## A note on security
|
||||||
|
|
||||||
While jrnl follows best practises, true security is an illusion.
|
While jrnl follows best practises, true security is an illusion.
|
||||||
Specifically, jrnl will leave traces in your memory and your shell
|
Specifically, jrnl will leave traces in your memory and your shell
|
||||||
history -- it's meant to keep journals secure in transit, for example
|
history – it’s meant to keep journals secure in transit, for example
|
||||||
when storing it on an
|
when storing it on an
|
||||||
[untrusted](http://techcrunch.com/2014/04/09/condoleezza-rice-joins-dropboxs-board/)
|
[untrusted](http://techcrunch.com/2014/04/09/condoleezza-rice-joins-dropboxs-board/)
|
||||||
services such as Dropbox. If you're concerned about security, disable
|
services such as Dropbox. If you’re concerned about security, disable
|
||||||
history logging for journal in your `.bashrc`
|
history logging for journal in your `.bashrc`:
|
||||||
|
|
||||||
``` sh
|
``` sh
|
||||||
HISTIGNORE="$HISTIGNORE:jrnl *"
|
HISTIGNORE="$HISTIGNORE:jrnl *"
|
||||||
```
|
```
|
||||||
|
|
||||||
If you are using zsh instead of bash, you can get the same behaviour
|
If you are using zsh instead of bash, you can get the same behaviour by
|
||||||
adding this to your `zshrc`
|
adding this to your `zshrc`:
|
||||||
|
|
||||||
``` sh
|
``` sh
|
||||||
setopt HIST_IGNORE_SPACE
|
setopt HIST_IGNORE_SPACE
|
||||||
alias jrnl=" jrnl"
|
alias jrnl=" jrnl"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If you are using `fish` instead of `bash` or `zsh`, you can get the same behaviour by
|
||||||
|
adding this to your `fish` configuration:
|
||||||
|
|
||||||
|
``` sh
|
||||||
|
abbr jrnl " jrnl"
|
||||||
|
```
|
||||||
|
|
||||||
|
To delete existing `jrnl` commands from `fish`’s history, run
|
||||||
|
`history delete --prefix 'jrnl '`.
|
||||||
|
|
||||||
## Manual decryption
|
## Manual decryption
|
||||||
|
|
||||||
Should you ever want to decrypt your journal manually, you can do so
|
Should you ever want to decrypt your journal manually, you can do so
|
||||||
|
@ -63,8 +73,8 @@ with any program that supports the AES algorithm in CBC. The key used
|
||||||
for encryption is the SHA-256-hash of your password, the IV
|
for encryption is the SHA-256-hash of your password, the IV
|
||||||
(initialisation vector) is stored in the first 16 bytes of the encrypted
|
(initialisation vector) is stored in the first 16 bytes of the encrypted
|
||||||
file. The plain text is encoded in UTF-8 and padded according to PKCS\#7
|
file. The plain text is encoded in UTF-8 and padded according to PKCS\#7
|
||||||
before being encrypted. Here's a Python script that you can use to
|
before being encrypted. Here’s a Python script that you can use to
|
||||||
decrypt your journal
|
decrypt your journal:
|
||||||
|
|
||||||
``` python
|
``` python
|
||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
|
@ -15,27 +15,12 @@ On other platforms, install *jrnl* using pip
|
||||||
pip install jrnl
|
pip install jrnl
|
||||||
```
|
```
|
||||||
|
|
||||||
Or, if you want the option to encrypt your journal,
|
|
||||||
|
|
||||||
``` sh
|
|
||||||
pip install jrnl[encrypted]
|
|
||||||
```
|
|
||||||
|
|
||||||
to install the dependencies for encrypting journals as well.
|
|
||||||
|
|
||||||
|
|
||||||
!!! note
|
|
||||||
Installing the encryption library, `pycrypto`, requires a `gcc` compiler. For this reason, jrnl will
|
|
||||||
not install `pycrypto` unless explicitly told so like this. You can [install PyCrypto manually](https://www.dlitz.net/software/pycrypto/)
|
|
||||||
first or install it with `pip install pycrypto` if you have a `gcc` compiler.
|
|
||||||
Also note that when using zsh, the correct syntax is `pip install "jrnl[encrypted]"` (note the quotes).
|
|
||||||
|
|
||||||
The first time you run `jrnl` you will be asked where your journal file
|
The first time you run `jrnl` you will be asked where your journal file
|
||||||
should be created and whether you wish to encrypt it.
|
should be created and whether you wish to encrypt it.
|
||||||
|
|
||||||
## Quickstart
|
## Quickstart
|
||||||
|
|
||||||
to make a new entry, just type
|
To make a new entry, just type
|
||||||
|
|
||||||
``` sh
|
``` sh
|
||||||
jrnl yesterday: Called in sick. Used the time to clean the house and spent 4h on writing my book.
|
jrnl yesterday: Called in sick. Used the time to clean the house and spent 4h on writing my book.
|
||||||
|
|
|
@ -8,10 +8,6 @@ files - you can put them into a Dropbox folder for instant syncing and
|
||||||
you can be assured that your journal will still be readable in 2050,
|
you can be assured that your journal will still be readable in 2050,
|
||||||
when all your fancy iPad journal applications will long be forgotten.
|
when all your fancy iPad journal applications will long be forgotten.
|
||||||
|
|
||||||
`jrnl` also plays nice with the fabulous
|
|
||||||
[DayOne](http://dayoneapp.com) and can read and write directly from and
|
|
||||||
to DayOne Journals.
|
|
||||||
|
|
||||||
Optionally, your journal can be encrypted using the [256-bit
|
Optionally, your journal can be encrypted using the [256-bit
|
||||||
AES](http://en.wikipedia.org/wiki/Advanced_Encryption_Standard).
|
AES](http://en.wikipedia.org/wiki/Advanced_Encryption_Standard).
|
||||||
|
|
||||||
|
|
148
docs/recipes.md
148
docs/recipes.md
|
@ -7,7 +7,7 @@
|
||||||
If I want to find out how often I mentioned my flatmates Alberto and
|
If I want to find out how often I mentioned my flatmates Alberto and
|
||||||
Melo in the same entry, I run
|
Melo in the same entry, I run
|
||||||
|
|
||||||
``` sh
|
```sh
|
||||||
jrnl @alberto --tags | grep @melo
|
jrnl @alberto --tags | grep @melo
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@ each tag occurred in this filtered journal. Finally, we pipe this to
|
||||||
|
|
||||||
You can do things like
|
You can do things like
|
||||||
|
|
||||||
``` sh
|
```sh
|
||||||
jrnl @fixed -starred -n 10 -until "jan 2013" --short
|
jrnl @fixed -starred -n 10 -until "jan 2013" --short
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -33,14 +33,14 @@ January 1, 2013 that are tagged with `@fixed`.
|
||||||
|
|
||||||
How much did I write last year?
|
How much did I write last year?
|
||||||
|
|
||||||
``` sh
|
```sh
|
||||||
jrnl -from "jan 1 2013" -until "dec 31 2013" | wc -w
|
jrnl -from "jan 1 2013" -until "dec 31 2013" | wc -w
|
||||||
```
|
```
|
||||||
|
|
||||||
Will give you the number of words you wrote in 2013. How long is my
|
Will give you the number of words you wrote in 2013. How long is my
|
||||||
average entry?
|
average entry?
|
||||||
|
|
||||||
``` sh
|
```sh
|
||||||
expr $(jrnl --export text | wc -w) / $(jrnl --short | wc -l)
|
expr $(jrnl --export text | wc -w) / $(jrnl --short | wc -l)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -50,11 +50,10 @@ print exactly one line per entry).
|
||||||
|
|
||||||
### Importing older files
|
### Importing older files
|
||||||
|
|
||||||
If you want to import a file as an entry to jrnl, you can just do `jrnl
|
If you want to import a file as an entry to jrnl, you can just do `jrnl < entry.ext`. But what if you want the modification date of the file to
|
||||||
< entry.ext`. But what if you want the modification date of the file to
|
|
||||||
be the date of the entry in jrnl? Try this
|
be the date of the entry in jrnl? Try this
|
||||||
|
|
||||||
``` sh
|
```sh
|
||||||
echo `stat -f %Sm -t '%d %b %Y at %H:%M: ' entry.txt` `cat entry.txt` | jrnl
|
echo `stat -f %Sm -t '%d %b %Y at %H:%M: ' entry.txt` `cat entry.txt` | jrnl
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -63,7 +62,7 @@ then combine it with the contents of the file before piping it to jrnl.
|
||||||
If you do that often, consider creating a function in your `.bashrc` or
|
If you do that often, consider creating a function in your `.bashrc` or
|
||||||
`.bash_profile`
|
`.bash_profile`
|
||||||
|
|
||||||
``` sh
|
```sh
|
||||||
jrnlimport () {
|
jrnlimport () {
|
||||||
echo `stat -f %Sm -t '%d %b %Y at %H:%M: ' $1` `cat $1` | jrnl
|
echo `stat -f %Sm -t '%d %b %Y at %H:%M: ' $1` `cat $1` | jrnl
|
||||||
}
|
}
|
||||||
|
@ -71,19 +70,61 @@ jrnlimport () {
|
||||||
|
|
||||||
### Using templates
|
### Using templates
|
||||||
|
|
||||||
Say you always want to use the same template for creating new entries.
|
!!! note
|
||||||
If you have an [external editor](../advanced) set up, you can use this:
|
Templates require an [external editor](../advanced) be configured.
|
||||||
|
|
||||||
|
A template is a code snippet that makes it easier to enter use repeated text
|
||||||
|
each time a new journal entry is started. There are two ways you can utilize
|
||||||
|
templates in your entries.
|
||||||
|
|
||||||
|
#### 1. Command line arguments
|
||||||
|
|
||||||
|
If you had a `template.txt` file with the following contents:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
jrnl < my_template.txt
|
My Personal Journal
|
||||||
jrnl -1 --edit
|
Title:
|
||||||
|
|
||||||
|
Body:
|
||||||
```
|
```
|
||||||
|
|
||||||
Another nice solution that allows you to define individual prompts comes
|
The `template.txt` file could be used to create a new entry with these
|
||||||
from [Jacobo de
|
command line arguements:
|
||||||
Vera](https://github.com/maebert/jrnl/issues/194#issuecomment-47402869):
|
|
||||||
|
|
||||||
``` sh
|
```sh
|
||||||
|
jrnl < template.txt # Imports template.txt as the most recent entry
|
||||||
|
jrnl -1 --edit # Opens the most recent entry in the editor
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Include the template file in `jrnl.yaml`
|
||||||
|
|
||||||
|
A more efficient way to work with a template file is to declare the file
|
||||||
|
in your config file by changing the `template` setting from `false` to the
|
||||||
|
template file's path in double quotes:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
...
|
||||||
|
template: "/path/to/template.txt"
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
Changes can be saved as you continue writing the journal entry and will be
|
||||||
|
logged as a new entry in the journal you specified in the original argument.
|
||||||
|
|
||||||
|
!!! tip
|
||||||
|
To read your journal entry or to verify the entry saved, you can use this
|
||||||
|
command: `jrnl -n 1` (Check out [Import and Export](../export/#export-to-files) for more export options).
|
||||||
|
|
||||||
|
```sh
|
||||||
|
jrnl -n 1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Prompts on shell reload
|
||||||
|
|
||||||
|
If you'd like to be prompted each time you refresh your shell, you can include
|
||||||
|
this in your `.bash_profile`:
|
||||||
|
|
||||||
|
```sh
|
||||||
function log_question()
|
function log_question()
|
||||||
{
|
{
|
||||||
echo $1
|
echo $1
|
||||||
|
@ -94,22 +135,38 @@ log_question 'What did I achieve today?'
|
||||||
log_question 'What did I make progress with?'
|
log_question 'What did I make progress with?'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Whenever your shell is reloaded, you will be prompted to answer each of the
|
||||||
|
questions in the example above. Each answer will be logged as a separate
|
||||||
|
journal entry at the `default_hour` and `default_minute` listed in your
|
||||||
|
`jrnl.yaml` [config file](../advanced/#configuration-file).
|
||||||
|
|
||||||
|
### Display random entry
|
||||||
|
|
||||||
|
You can use this to select one title at random and then display the whole
|
||||||
|
entry. The invocation of `cut` needs to match the format of the timestamp.
|
||||||
|
For timestamps that have a space between data and time components, select
|
||||||
|
fields 1 and 2 as shown. For timestamps that have no whitespace, select
|
||||||
|
only field 1.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
jrnl -on "$(jrnl --short | shuf -n 1 | cut -d' ' -f1,2)"
|
||||||
|
```
|
||||||
|
|
||||||
## External editors
|
## External editors
|
||||||
|
|
||||||
To use external editors for writing and editing journal entries, set
|
Configure your preferred external editor by updating the `editor` option
|
||||||
them up in your `.jrnl_config` (see `advanced usage <advanced>` for
|
in your `jrnl.yaml` file. (See [advanced usage](../advanced) for details).
|
||||||
details). Generally, after writing an entry, you will have to save and
|
|
||||||
close the file to save the changes to jrnl.
|
!!! note
|
||||||
|
To save and log any entry edits, save and close the file.
|
||||||
|
|
||||||
### Sublime Text
|
### Sublime Text
|
||||||
|
|
||||||
To use Sublime Text, install the command line tools for Sublime Text and
|
To use Sublime Text, install the command line tools for Sublime Text and
|
||||||
configure your `.jrnl_config` like this:
|
configure your `jrnl.yaml` like this:
|
||||||
|
|
||||||
``` json
|
```yaml
|
||||||
{
|
editor: "subl -w"
|
||||||
"editor": "subl -w"
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Note the `-w` flag to make sure jrnl waits for Sublime Text to close the
|
Note the `-w` flag to make sure jrnl waits for Sublime Text to close the
|
||||||
|
@ -121,22 +178,18 @@ Similar to Sublime Text, MacVim must be started with a flag that tells
|
||||||
the the process to wait until the file is closed before passing control
|
the the process to wait until the file is closed before passing control
|
||||||
back to journal. In the case of MacVim, this is `-f`:
|
back to journal. In the case of MacVim, this is `-f`:
|
||||||
|
|
||||||
``` json
|
```yaml
|
||||||
{
|
editor: "mvim -f"
|
||||||
"editor": "mvim -f"
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### iA Writer
|
### iA Writer
|
||||||
|
|
||||||
On OS X, you can use the fabulous [iA
|
On OS X, you can use the fabulous [iA
|
||||||
Writer](http://www.iawriter.com/mac) to write entries. Configure your
|
Writer](http://www.iawriter.com/mac) to write entries. Configure your
|
||||||
`.jrnl_config` like this:
|
`jrnl.yaml` like this:
|
||||||
|
|
||||||
``` json
|
```yaml
|
||||||
{
|
editor: "open -b pro.writer.mac -Wn"
|
||||||
"editor": "open -b pro.writer.mac -Wn"
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
What does this do? `open -b ...` opens a file using the application
|
What does this do? `open -b ...` opens a file using the application
|
||||||
|
@ -148,19 +201,17 @@ If the `pro.writer.mac` bundle identifier is not found on your system,
|
||||||
you can find the right string to use by inspecting iA Writer's
|
you can find the right string to use by inspecting iA Writer's
|
||||||
`Info.plist` file in your shell:
|
`Info.plist` file in your shell:
|
||||||
|
|
||||||
``` sh
|
```sh
|
||||||
grep -A 1 CFBundleIdentifier /Applications/iA\ Writer.app/Contents/Info.plist
|
grep -A 1 CFBundleIdentifier /Applications/iA\ Writer.app/Contents/Info.plist
|
||||||
```
|
```
|
||||||
|
|
||||||
### Notepad++ on Windows
|
### Notepad++ on Windows
|
||||||
|
|
||||||
To set [Notepad++](http://notepad-plus-plus.org/) as your editor, edit
|
To set [Notepad++](http://notepad-plus-plus.org/) as your editor, edit
|
||||||
the jrnl config file (`.jrnl_config`) like this:
|
the jrnl config file (`jrnl.yaml`) like this:
|
||||||
|
|
||||||
``` json
|
```yaml
|
||||||
{
|
editor: "C:\\Program Files (x86)\\Notepad++\\notepad++.exe -multiInst -nosession"
|
||||||
"editor": "C:\\Program Files (x86)\\Notepad++\\notepad++.exe -multiInst -nosession",
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The double backslashes are needed so jrnl can read the file path
|
The double backslashes are needed so jrnl can read the file path
|
||||||
|
@ -169,12 +220,10 @@ its own Notepad++ window.
|
||||||
|
|
||||||
### Visual Studio Code
|
### Visual Studio Code
|
||||||
|
|
||||||
To set [Visual Studo Code](https://code.visualstudio.com) as your editor on Linux, edit `.jrnl_config` like this:
|
To set [Visual Studo Code](https://code.visualstudio.com) as your editor on Linux, edit `jrnl.yaml` like this:
|
||||||
|
|
||||||
```json
|
```yaml
|
||||||
{
|
editor: "/usr/bin/code --wait"
|
||||||
"editor": "/usr/bin/code --wait",
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The `--wait` argument tells VS Code to wait for files to be written out before handing back control to jrnl.
|
The `--wait` argument tells VS Code to wait for files to be written out before handing back control to jrnl.
|
||||||
|
@ -184,14 +233,13 @@ On MacOS you will need to add VS Code to your PATH. You can do that by adding:
|
||||||
```sh
|
```sh
|
||||||
export PATH="\$PATH:/Applications/Visual Studio Code.app/Contents/Resources/app/bin"
|
export PATH="\$PATH:/Applications/Visual Studio Code.app/Contents/Resources/app/bin"
|
||||||
```
|
```
|
||||||
|
|
||||||
to your `.bash_profile`, or by running the **Install 'code' command in PATH** command from the command pallet in VS Code.
|
to your `.bash_profile`, or by running the **Install 'code' command in PATH** command from the command pallet in VS Code.
|
||||||
|
|
||||||
Then you can add:
|
Then you can add:
|
||||||
|
|
||||||
```javascript
|
```yaml
|
||||||
{
|
editor: "code --wait"
|
||||||
"editor": "code --wait",
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
to ``.jrnl_config``. See also the [Visual Studio Code documentation](https://code.visualstudio.com/docs/setup/mac)
|
to `jrnl.yaml`. See also the [Visual Studio Code documentation](https://code.visualstudio.com/docs/setup/mac)
|
||||||
|
|
5
docs/theme/index.html
vendored
5
docs/theme/index.html
vendored
|
@ -82,11 +82,6 @@
|
||||||
<h3>Accessible anywhere.</h3>
|
<h3>Accessible anywhere.</h3>
|
||||||
<p>Sync your journals with Dropbox and capture your thoughts where ever you are</p>
|
<p>Sync your journals with Dropbox and capture your thoughts where ever you are</p>
|
||||||
</section>
|
</section>
|
||||||
<section>
|
|
||||||
<i class="icon dayone"></i>
|
|
||||||
<h3>DayOne compatible.</h3>
|
|
||||||
<p>Read, write and search your DayOne journal from the command line.</p>
|
|
||||||
</section>
|
|
||||||
<section>
|
<section>
|
||||||
<i class="icon github"></i>
|
<i class="icon github"></i>
|
||||||
<h3>Free & Open Source.</h3>
|
<h3>Free & Open Source.</h3>
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
# Basic Usage
|
# Basic Usage
|
||||||
|
|
||||||
`jrnl` has two modes: **composing** and **viewing**. Basically, whenever
|
`jrnl` has two modes: **composing** and **viewing**. Basically, whenever
|
||||||
you *don't* supply any arguments that start
|
you _don't_ supply any arguments that start
|
||||||
with a dash or double-dash, you're in composing mode, meaning you can
|
with a dash or double-dash, you're in composing mode, meaning you can
|
||||||
write your entry on the command line or an editor of your choice.
|
write your entry on the command line or an editor of your choice.
|
||||||
|
|
||||||
We intentionally break a convention on command line arguments: all
|
We intentionally break a convention on command line arguments: all
|
||||||
arguments starting with a *single dash*
|
arguments starting with a _single dash_
|
||||||
will *filter* your journal before viewing
|
will _filter_ your journal before viewing
|
||||||
it, and can be combined arbitrarily. Arguments with a
|
it, and can be combined arbitrarily. Arguments with a
|
||||||
*double dash* will control how your journal
|
_double dash_ will control how your journal
|
||||||
is displayed or exported and are mutually exclusive (ie. you can only
|
is displayed or exported and are mutually exclusive (ie. you can only
|
||||||
specify one way to display or export your journal at a time).
|
specify one way to display or export your journal at a time).
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ specify one way to display or export your journal at a time).
|
||||||
|
|
||||||
You can list the journals accessible by jrnl
|
You can list the journals accessible by jrnl
|
||||||
|
|
||||||
``` sh
|
```sh
|
||||||
jrnl -ls
|
jrnl -ls
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@ Composing mode is entered by either starting `jrnl` without any
|
||||||
arguments -- which will prompt you to write an entry or launch your
|
arguments -- which will prompt you to write an entry or launch your
|
||||||
editor -- or by just writing an entry on the prompt, such as
|
editor -- or by just writing an entry on the prompt, such as
|
||||||
|
|
||||||
``` sh
|
```sh
|
||||||
jrnl today at 3am: I just met Steve Buscemi in a bar! He looked funny.
|
jrnl today at 3am: I just met Steve Buscemi in a bar! He looked funny.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -44,7 +44,7 @@ jrnl today at 3am: I just met Steve Buscemi in a bar! He looked funny.
|
||||||
|
|
||||||
You can also import an entry directly from a file
|
You can also import an entry directly from a file
|
||||||
|
|
||||||
``` sh
|
```sh
|
||||||
jrnl < my_entry.txt
|
jrnl < my_entry.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -52,28 +52,28 @@ jrnl < my_entry.txt
|
||||||
|
|
||||||
Timestamps that work:
|
Timestamps that work:
|
||||||
|
|
||||||
- at 6am
|
- at 6am
|
||||||
- yesterday
|
- yesterday
|
||||||
- last monday
|
- last monday
|
||||||
- sunday at noon
|
- sunday at noon
|
||||||
- 2 march 2012
|
- 2 march 2012
|
||||||
- 7 apr
|
- 7 apr
|
||||||
- 5/20/1998 at 23:42
|
- 5/20/1998 at 23:42
|
||||||
|
|
||||||
### Starring entries
|
### Starring entries
|
||||||
|
|
||||||
To mark an entry as a favourite, simply "star" it
|
To mark an entry as a favourite, simply "star" it
|
||||||
|
|
||||||
``` sh
|
```sh
|
||||||
jrnl last sunday *: Best day of my life.
|
jrnl last sunday *: Best day of my life.
|
||||||
```
|
```
|
||||||
|
|
||||||
If you don't want to add a date (ie. your entry will be dated as now),
|
If you don't want to add a date (ie. your entry will be dated as now),
|
||||||
The following options are equivalent:
|
The following options are equivalent:
|
||||||
|
|
||||||
- `jrnl *: Best day of my life.`
|
- `jrnl *: Best day of my life.`
|
||||||
- `jrnl *Best day of my life.`
|
- `jrnl *Best day of my life.`
|
||||||
- `jrnl Best day of my life.*`
|
- `jrnl Best day of my life.*`
|
||||||
|
|
||||||
!!! note
|
!!! note
|
||||||
Just make sure that the asterisk sign is **not** surrounded by
|
Just make sure that the asterisk sign is **not** surrounded by
|
||||||
|
@ -82,21 +82,21 @@ The following options are equivalent:
|
||||||
|
|
||||||
## Viewing
|
## Viewing
|
||||||
|
|
||||||
``` sh
|
```sh
|
||||||
jrnl -n 10
|
jrnl -n 10
|
||||||
```
|
```
|
||||||
|
|
||||||
will list you the ten latest entries (if you're lazy, `jrnl -10` will do
|
will list you the ten latest entries (if you're lazy, `jrnl -10` will do
|
||||||
the same),
|
the same),
|
||||||
|
|
||||||
``` sh
|
```sh
|
||||||
jrnl -from "last year" -until march
|
jrnl -from "last year" -until march
|
||||||
```
|
```
|
||||||
|
|
||||||
everything that happened from the start of last year to the start of
|
everything that happened from the start of last year to the start of
|
||||||
last march. To only see your favourite entries, use
|
last march. To only see your favourite entries, use
|
||||||
|
|
||||||
``` sh
|
```sh
|
||||||
jrnl -starred
|
jrnl -starred
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -105,20 +105,20 @@ jrnl -starred
|
||||||
Keep track of people, projects or locations, by tagging them with an `@`
|
Keep track of people, projects or locations, by tagging them with an `@`
|
||||||
in your entries
|
in your entries
|
||||||
|
|
||||||
``` sh
|
```sh
|
||||||
jrnl Had a wonderful day on the @beach with @Tom and @Anna.
|
jrnl Had a wonderful day on the @beach with @Tom and @Anna.
|
||||||
```
|
```
|
||||||
|
|
||||||
You can filter your journal entries just like this:
|
You can filter your journal entries just like this:
|
||||||
|
|
||||||
``` sh
|
```sh
|
||||||
jrnl @pinkie @WorldDomination
|
jrnl @pinkie @WorldDomination
|
||||||
```
|
```
|
||||||
|
|
||||||
Will print all entries in which either `@pinkie` or `@WorldDomination`
|
Will print all entries in which either `@pinkie` or `@WorldDomination`
|
||||||
occurred.
|
occurred.
|
||||||
|
|
||||||
``` sh
|
```sh
|
||||||
jrnl -n 5 -and @pineapple @lubricant
|
jrnl -n 5 -and @pineapple @lubricant
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -129,16 +129,16 @@ configuration.
|
||||||
!!! note
|
!!! note
|
||||||
`jrnl @pinkie @WorldDomination` will switch to viewing mode because
|
`jrnl @pinkie @WorldDomination` will switch to viewing mode because
|
||||||
although **no** command line arguments are given, all the input strings
|
although **no** command line arguments are given, all the input strings
|
||||||
look like tags - *jrnl* will assume you want to filter by tag.
|
look like tags - _jrnl_ will assume you want to filter by tag.
|
||||||
|
|
||||||
## Editing older entries
|
## Editing older entries
|
||||||
|
|
||||||
You can edit selected entries after you wrote them. This is particularly
|
You can edit selected entries after you wrote them. This is particularly
|
||||||
useful when your journal file is encrypted or if you're using a DayOne
|
useful when your journal file is encrypted. To use this feature, you need
|
||||||
journal. To use this feature, you need to have an editor configured in
|
to have an editor configured in your journal configuration file (see
|
||||||
your journal configuration file (see `advanced usage <advanced>`)
|
`advanced usage <advanced>`)
|
||||||
|
|
||||||
``` sh
|
```sh
|
||||||
jrnl -until 1950 @texas -and @history --edit
|
jrnl -until 1950 @texas -and @history --edit
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -153,30 +153,8 @@ encrypt) your edited journal after you save and exit the editor.
|
||||||
|
|
||||||
You can also use this feature for deleting entries from your journal
|
You can also use this feature for deleting entries from your journal
|
||||||
|
|
||||||
``` sh
|
```sh
|
||||||
jrnl @girlfriend -until 'june 2012' --edit
|
jrnl @girlfriend -until 'june 2012' --edit
|
||||||
```
|
```
|
||||||
|
|
||||||
Just select all text, press delete, and everything is gone...
|
Just select all text, press delete, and everything is gone...
|
||||||
|
|
||||||
### Editing DayOne Journals
|
|
||||||
|
|
||||||
DayOne journals can be edited exactly the same way, however the output
|
|
||||||
looks a little bit different because of the way DayOne stores its
|
|
||||||
entries:
|
|
||||||
|
|
||||||
```md
|
|
||||||
# af8dbd0d43fb55458f11aad586ea2abf
|
|
||||||
2013-05-02 15:30 I told everyone I built my @robot wife for sex.
|
|
||||||
But late at night when we're alone we mostly play Battleship.
|
|
||||||
|
|
||||||
# 2391048fe24111e1983ed49a20be6f9e
|
|
||||||
2013-08-10 03:22 I had all kinds of plans in case of a @zombie attack.
|
|
||||||
I just figured I'd be on the other side.
|
|
||||||
```
|
|
||||||
|
|
||||||
The long strings starting with hash symbol are the so-called UUIDs,
|
|
||||||
unique identifiers for each entry. Don't touch them. If you do, then the
|
|
||||||
old entry would get deleted and a new one written, which means that you
|
|
||||||
could lose DayOne data that jrnl can't handle (such as as the entry's
|
|
||||||
geolocation).
|
|
||||||
|
|
29
features/contains.feature
Normal file
29
features/contains.feature
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
Feature: Contains
|
||||||
|
|
||||||
|
Scenario: Searching for a string
|
||||||
|
Given we use the config "basic.yaml"
|
||||||
|
When we run "jrnl -contains life"
|
||||||
|
Then we should get no error
|
||||||
|
and the output should be
|
||||||
|
"""
|
||||||
|
2013-06-10 15:40 Life is good.
|
||||||
|
| But I'm better.
|
||||||
|
"""
|
||||||
|
|
||||||
|
Scenario: Searching for a string within tag results
|
||||||
|
Given we use the config "tags.yaml"
|
||||||
|
When we run "jrnl @idea -contains software"
|
||||||
|
Then we should get no error
|
||||||
|
and the output should contain "software"
|
||||||
|
|
||||||
|
Scenario: Searching for a string within AND tag results
|
||||||
|
Given we use the config "tags.yaml"
|
||||||
|
When we run "jrnl -and @journal @idea -contains software"
|
||||||
|
Then we should get no error
|
||||||
|
and the output should contain "software"
|
||||||
|
|
||||||
|
Scenario: Searching for a string within NOT tag results
|
||||||
|
Given we use the config "tags.yaml"
|
||||||
|
When we run "jrnl -not @dan -contains software"
|
||||||
|
Then we should get no error
|
||||||
|
and the output should contain "software"
|
|
@ -20,6 +20,20 @@ Feature: Basic reading and writing to a journal
|
||||||
When we run "jrnl -n 1"
|
When we run "jrnl -n 1"
|
||||||
Then the output should contain "2013-07-23 09:00 A cold and stormy day."
|
Then the output should contain "2013-07-23 09:00 A cold and stormy day."
|
||||||
|
|
||||||
|
@skip_win
|
||||||
|
Scenario: Writing an empty entry from the editor
|
||||||
|
Given we use the config "editor.yaml"
|
||||||
|
When we open the editor and enter ""
|
||||||
|
Then we should see the message "[Nothing saved to file]"
|
||||||
|
|
||||||
|
Scenario: Writing an empty entry from the command line
|
||||||
|
Given we use the config "basic.yaml"
|
||||||
|
When we run "jrnl" and enter ""
|
||||||
|
Then the output should be
|
||||||
|
"""
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
Scenario: Filtering for dates
|
Scenario: Filtering for dates
|
||||||
Given we use the config "basic.yaml"
|
Given we use the config "basic.yaml"
|
||||||
When we run "jrnl -on 2013-06-10 --short"
|
When we run "jrnl -on 2013-06-10 --short"
|
||||||
|
@ -27,14 +41,6 @@ Feature: Basic reading and writing to a journal
|
||||||
When we run "jrnl -on 'june 6 2013' --short"
|
When we run "jrnl -on 'june 6 2013' --short"
|
||||||
Then the output should be "2013-06-10 15:40 Life is good."
|
Then the output should be "2013-06-10 15:40 Life is good."
|
||||||
|
|
||||||
Scenario: Emoji support
|
|
||||||
Given we use the config "basic.yaml"
|
|
||||||
When we run "jrnl 23 july 2013: 🌞 sunny day. Saw an 🐘"
|
|
||||||
Then we should see the message "Entry added"
|
|
||||||
When we run "jrnl -n 1"
|
|
||||||
Then the output should contain "🌞"
|
|
||||||
and the output should contain "🐘"
|
|
||||||
|
|
||||||
Scenario: Writing an entry at the prompt
|
Scenario: Writing an entry at the prompt
|
||||||
Given we use the config "basic.yaml"
|
Given we use the config "basic.yaml"
|
||||||
When we run "jrnl" and enter "25 jul 2013: I saw Elvis. He's alive."
|
When we run "jrnl" and enter "25 jul 2013: I saw Elvis. He's alive."
|
||||||
|
|
35
features/custom_dates.feature
Normal file
35
features/custom_dates.feature
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
Feature: Reading and writing to journal with custom date formats
|
||||||
|
|
||||||
|
Scenario: Loading a sample journal
|
||||||
|
Given we use the config "little_endian_dates.yaml"
|
||||||
|
When we run "jrnl -n 2"
|
||||||
|
Then we should get no error
|
||||||
|
And the output should be
|
||||||
|
"""
|
||||||
|
09.06.2013 15:39 My first entry.
|
||||||
|
| Everything is alright
|
||||||
|
|
||||||
|
10.06.2013 15:40 Life is good.
|
||||||
|
| But I'm better.
|
||||||
|
"""
|
||||||
|
|
||||||
|
Scenario: Writing an entry from command line
|
||||||
|
Given we use the config "little_endian_dates.yaml"
|
||||||
|
When we run "jrnl 2013-07-12: A cold and stormy day. I ate crisps on the sofa."
|
||||||
|
Then we should see the message "Entry added"
|
||||||
|
When we run "jrnl -n 1"
|
||||||
|
Then the output should contain "12.07.2013 09:00 A cold and stormy day."
|
||||||
|
|
||||||
|
Scenario: Filtering for dates
|
||||||
|
Given we use the config "little_endian_dates.yaml"
|
||||||
|
When we run "jrnl -on 2013-06-10 --short"
|
||||||
|
Then the output should be "10.06.2013 15:40 Life is good."
|
||||||
|
When we run "jrnl -on 'june 6 2013' --short"
|
||||||
|
Then the output should be "10.06.2013 15:40 Life is good."
|
||||||
|
|
||||||
|
Scenario: Writing an entry at the prompt
|
||||||
|
Given we use the config "little_endian_dates.yaml"
|
||||||
|
When we run "jrnl" and enter "2013-05-10: I saw Elvis. He's alive."
|
||||||
|
Then we should get no error
|
||||||
|
And the journal should contain "[10.05.2013 09:00] I saw Elvis."
|
||||||
|
And the journal should contain "He's alive."
|
|
@ -6,7 +6,6 @@ highlight: true
|
||||||
journals:
|
journals:
|
||||||
default: features/journals/bug153.dayone
|
default: features/journals/bug153.dayone
|
||||||
linewrap: 80
|
linewrap: 80
|
||||||
password: ''
|
|
||||||
tagsymbols: '@'
|
tagsymbols: '@'
|
||||||
template: false
|
template: false
|
||||||
timeformat: '%Y-%m-%d %H:%M'
|
timeformat: '%Y-%m-%d %H:%M'
|
||||||
|
|
12
features/data/configs/bug780.yaml
Normal file
12
features/data/configs/bug780.yaml
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
default_hour: 9
|
||||||
|
default_minute: 0
|
||||||
|
editor: ''
|
||||||
|
encrypt: false
|
||||||
|
highlight: true
|
||||||
|
journals:
|
||||||
|
default: features/journals/bug780.dayone
|
||||||
|
linewrap: 80
|
||||||
|
tagsymbols: '@'
|
||||||
|
template: false
|
||||||
|
timeformat: '%Y-%m-%d %H:%M'
|
||||||
|
indent_character: "|"
|
|
@ -7,7 +7,6 @@ highlight: true
|
||||||
journals:
|
journals:
|
||||||
default: features/journals/dayone.dayone
|
default: features/journals/dayone.dayone
|
||||||
linewrap: 80
|
linewrap: 80
|
||||||
password: ''
|
|
||||||
tagsymbols: '@'
|
tagsymbols: '@'
|
||||||
timeformat: '%Y-%m-%d %H:%M'
|
timeformat: '%Y-%m-%d %H:%M'
|
||||||
indent_character: "|"
|
indent_character: "|"
|
||||||
|
|
12
features/data/configs/editor.yaml
Normal file
12
features/data/configs/editor.yaml
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
default_hour: 9
|
||||||
|
default_minute: 0
|
||||||
|
editor: "vim"
|
||||||
|
encrypt: false
|
||||||
|
highlight: true
|
||||||
|
journals:
|
||||||
|
default: features/journals/simple.journal
|
||||||
|
linewrap: 80
|
||||||
|
tagsymbols: "@"
|
||||||
|
template: false
|
||||||
|
timeformat: "%Y-%m-%d %H:%M"
|
||||||
|
indent_character: "|"
|
|
@ -7,7 +7,6 @@ highlight: true
|
||||||
journals:
|
journals:
|
||||||
default: features/journals/empty_folder
|
default: features/journals/empty_folder
|
||||||
linewrap: 80
|
linewrap: 80
|
||||||
password: ''
|
|
||||||
tagsymbols: '@'
|
tagsymbols: '@'
|
||||||
timeformat: '%Y-%m-%d %H:%M'
|
timeformat: '%Y-%m-%d %H:%M'
|
||||||
indent_character: "|"
|
indent_character: "|"
|
||||||
|
|
|
@ -7,7 +7,6 @@ highlight: true
|
||||||
journals:
|
journals:
|
||||||
default: features/journals/encrypted.journal
|
default: features/journals/encrypted.journal
|
||||||
linewrap: 80
|
linewrap: 80
|
||||||
password: ''
|
|
||||||
tagsymbols: '@'
|
tagsymbols: '@'
|
||||||
timeformat: '%Y-%m-%d %H:%M'
|
timeformat: '%Y-%m-%d %H:%M'
|
||||||
indent_character: "|"
|
indent_character: "|"
|
||||||
|
|
12
features/data/configs/little_endian_dates.yaml
Normal file
12
features/data/configs/little_endian_dates.yaml
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
default_hour: 9
|
||||||
|
default_minute: 0
|
||||||
|
editor: ""
|
||||||
|
encrypt: false
|
||||||
|
highlight: true
|
||||||
|
journals:
|
||||||
|
default: features/journals/little_endian_dates.journal
|
||||||
|
linewrap: 80
|
||||||
|
tagsymbols: "@"
|
||||||
|
template: false
|
||||||
|
timeformat: "%d.%m.%Y %H:%M"
|
||||||
|
indent_character: "|"
|
|
@ -7,7 +7,6 @@ template: false
|
||||||
journals:
|
journals:
|
||||||
default: features/journals/markdown-headings-335.journal
|
default: features/journals/markdown-headings-335.journal
|
||||||
linewrap: 80
|
linewrap: 80
|
||||||
password: ''
|
|
||||||
tagsymbols: '@'
|
tagsymbols: '@'
|
||||||
timeformat: '%Y-%m-%d %H:%M'
|
timeformat: '%Y-%m-%d %H:%M'
|
||||||
indent_character: "|"
|
indent_character: "|"
|
||||||
|
|
|
@ -13,7 +13,6 @@ journals:
|
||||||
encrypt: true
|
encrypt: true
|
||||||
journal: features/journals/new_encrypted.journal
|
journal: features/journals/new_encrypted.journal
|
||||||
linewrap: 80
|
linewrap: 80
|
||||||
password: ''
|
|
||||||
tagsymbols: '@'
|
tagsymbols: '@'
|
||||||
timeformat: '%Y-%m-%d %H:%M'
|
timeformat: '%Y-%m-%d %H:%M'
|
||||||
indent_character: "|"
|
indent_character: "|"
|
||||||
|
|
|
@ -7,7 +7,6 @@ template: false
|
||||||
journals:
|
journals:
|
||||||
default: features/journals/tags-216.journal
|
default: features/journals/tags-216.journal
|
||||||
linewrap: 80
|
linewrap: 80
|
||||||
password: ''
|
|
||||||
tagsymbols: '@'
|
tagsymbols: '@'
|
||||||
timeformat: '%Y-%m-%d %H:%M'
|
timeformat: '%Y-%m-%d %H:%M'
|
||||||
indent_character: "|"
|
indent_character: "|"
|
||||||
|
|
|
@ -7,7 +7,6 @@ template: false
|
||||||
journals:
|
journals:
|
||||||
default: features/journals/tags-237.journal
|
default: features/journals/tags-237.journal
|
||||||
linewrap: 80
|
linewrap: 80
|
||||||
password: ''
|
|
||||||
tagsymbols: '@'
|
tagsymbols: '@'
|
||||||
timeformat: '%Y-%m-%d %H:%M'
|
timeformat: '%Y-%m-%d %H:%M'
|
||||||
indent_character: "|"
|
indent_character: "|"
|
||||||
|
|
|
@ -7,7 +7,6 @@ template: false
|
||||||
journals:
|
journals:
|
||||||
default: features/journals/tags.journal
|
default: features/journals/tags.journal
|
||||||
linewrap: 80
|
linewrap: 80
|
||||||
password: ''
|
|
||||||
tagsymbols: '@'
|
tagsymbols: '@'
|
||||||
timeformat: '%Y-%m-%d %H:%M'
|
timeformat: '%Y-%m-%d %H:%M'
|
||||||
indent_character: "|"
|
indent_character: "|"
|
||||||
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"default_hour": 9,
|
||||||
|
"timeformat": "%d.%m.%Y %H:%M",
|
||||||
|
"linewrap": 80,
|
||||||
|
"encrypt": false,
|
||||||
|
"editor": "",
|
||||||
|
"default_minute": 0,
|
||||||
|
"highlight": true,
|
||||||
|
"journals": {"default": "features/journals/simple_jrnl-1-9-5_little_endian_dates.journal"},
|
||||||
|
"tagsymbols": "@"
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>Activity</key>
|
||||||
|
<string>Stationary</string>
|
||||||
|
<key>Creation Date</key>
|
||||||
|
<date>2019-12-30T21:28:54Z</date>
|
||||||
|
<key>Entry Text</key>
|
||||||
|
<string></string>
|
||||||
|
<key>Starred</key>
|
||||||
|
<false />
|
||||||
|
<key>UUID</key>
|
||||||
|
<string>48A25033B34047C591160A4480197D8B</string>
|
||||||
|
<key>Creator</key>
|
||||||
|
<dict>
|
||||||
|
<key>Device Agent</key>
|
||||||
|
<string>PC</string>
|
||||||
|
<key>Generation Date</key>
|
||||||
|
<date>2019-12-30T21:28:54Z</date>
|
||||||
|
<key>Host Name</key>
|
||||||
|
<string>LE-TREPORT</string>
|
||||||
|
<key>OS Agent</key>
|
||||||
|
<string>Microsoft Windows/10 Home</string>
|
||||||
|
<key>Software Agent</key>
|
||||||
|
<string>Journaley/2.1</string>
|
||||||
|
</dict>
|
||||||
|
<key>Tags</key>
|
||||||
|
<array>
|
||||||
|
<string>i_have_no_body</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
5
features/data/journals/little_endian_dates.journal
Normal file
5
features/data/journals/little_endian_dates.journal
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
[09.06.2013 15:39] My first entry.
|
||||||
|
Everything is alright
|
||||||
|
|
||||||
|
[10.06.2013 15:40] Life is good.
|
||||||
|
But I'm better.
|
|
@ -0,0 +1,13 @@
|
||||||
|
10.06.2010 15:00 A life without chocolate is like a bad analogy.
|
||||||
|
|
||||||
|
10.06.2013 15:40 He said "[this] is the best time to be alive".
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent malesuada
|
||||||
|
quis est ac dignissim. Aliquam dignissim rutrum pretium. Phasellus pellentesque
|
||||||
|
augue et venenatis facilisis.
|
||||||
|
|
||||||
|
[03.08.2019 12:55] Some chat log or something
|
||||||
|
|
||||||
|
Suspendisse potenti. Sed dignissim sed nisl eu consequat. Aenean ante ex,
|
||||||
|
elementum ut interdum et, mattis eget lacus. In commodo nulla nec tellus
|
||||||
|
placerat, sed ultricies metus bibendum. Duis eget venenatis erat. In at dolor
|
||||||
|
dui.
|
65
features/dayone.feature
Normal file
65
features/dayone.feature
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
Feature: Dayone specific implementation details.
|
||||||
|
|
||||||
|
Scenario: Loading a DayOne Journal
|
||||||
|
Given we use the config "dayone.yaml"
|
||||||
|
When we run "jrnl -from 'feb 2013'"
|
||||||
|
Then we should get no error
|
||||||
|
and the output should be
|
||||||
|
"""
|
||||||
|
2013-05-17 11:39 This entry has tags!
|
||||||
|
|
||||||
|
2013-06-17 20:38 This entry has a location.
|
||||||
|
|
||||||
|
2013-07-17 11:38 This entry is starred!
|
||||||
|
"""
|
||||||
|
|
||||||
|
# broken still
|
||||||
|
@skip
|
||||||
|
Scenario: Entries without timezone information will be interpreted as in the current timezone
|
||||||
|
Given we use the config "dayone.yaml"
|
||||||
|
When we run "jrnl -until 'feb 2013'"
|
||||||
|
Then we should get no error
|
||||||
|
and the output should contain "2013-01-17T18:37Z" in the local time
|
||||||
|
|
||||||
|
Scenario: Writing into Dayone
|
||||||
|
Given we use the config "dayone.yaml"
|
||||||
|
When we run "jrnl 01 may 1979: Being born hurts."
|
||||||
|
and we run "jrnl -until 1980"
|
||||||
|
Then the output should be
|
||||||
|
"""
|
||||||
|
1979-05-01 09:00 Being born hurts.
|
||||||
|
"""
|
||||||
|
|
||||||
|
Scenario: Loading tags from a DayOne Journal
|
||||||
|
Given we use the config "dayone.yaml"
|
||||||
|
When we run "jrnl --tags"
|
||||||
|
Then the output should be
|
||||||
|
"""
|
||||||
|
@work : 1
|
||||||
|
@play : 1
|
||||||
|
"""
|
||||||
|
|
||||||
|
Scenario: Saving tags from a DayOne Journal
|
||||||
|
Given we use the config "dayone.yaml"
|
||||||
|
When we run "jrnl A hard day at @work"
|
||||||
|
and we run "jrnl --tags"
|
||||||
|
Then the output should be
|
||||||
|
"""
|
||||||
|
@work : 2
|
||||||
|
@play : 1
|
||||||
|
"""
|
||||||
|
|
||||||
|
Scenario: Filtering by tags from a DayOne Journal
|
||||||
|
Given we use the config "dayone.yaml"
|
||||||
|
When we run "jrnl @work"
|
||||||
|
Then the output should be
|
||||||
|
"""
|
||||||
|
2013-05-17 11:39 This entry has tags!
|
||||||
|
"""
|
||||||
|
|
||||||
|
Scenario: Exporting dayone to json
|
||||||
|
Given we use the config "dayone.yaml"
|
||||||
|
When we run "jrnl --export json"
|
||||||
|
Then we should get no error
|
||||||
|
and the output should be parsable as json
|
||||||
|
and the json output should contain entries.0.uuid = "4BB1F46946AD439996C9B59DE7C4DDC1"
|
31
features/dayone_regressions.feature
Normal file
31
features/dayone_regressions.feature
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
Feature: Zapped Dayone bugs stay dead!
|
||||||
|
|
||||||
|
# fails when system time is UTC (as on Travis-CI)
|
||||||
|
@skip
|
||||||
|
Scenario: DayOne tag searching should work with tags containing a mixture of upper and lower case.
|
||||||
|
# https://github.com/jrnl-org/jrnl/issues/354
|
||||||
|
Given we use the config "dayone.yaml"
|
||||||
|
When we run "jrnl @plAy"
|
||||||
|
Then the output should contain
|
||||||
|
"""
|
||||||
|
2013-05-17 11:39 This entry has tags!
|
||||||
|
"""
|
||||||
|
|
||||||
|
# fails when system time is UTC (as on Travis-CI)
|
||||||
|
@skip
|
||||||
|
Scenario: Title with an embedded period on DayOne journal
|
||||||
|
Given we use the config "dayone.yaml"
|
||||||
|
When we run "jrnl 04-24-2014: "Ran 6.2 miles today in 1:02:03. I'm feeling sore because I forgot to stretch.""
|
||||||
|
Then we should see the message "Entry added"
|
||||||
|
When we run "jrnl -1"
|
||||||
|
Then the output should be
|
||||||
|
"""
|
||||||
|
2014-04-24 09:00 Ran 6.2 miles today in 1:02:03.
|
||||||
|
| I'm feeling sore because I forgot to stretch.
|
||||||
|
"""
|
||||||
|
|
||||||
|
Scenario: Opening an folder that's not a DayOne folder gives a nice error message
|
||||||
|
Given we use the config "empty_folder.yaml"
|
||||||
|
When we run "jrnl Herro"
|
||||||
|
Then we should get an error
|
||||||
|
Then we should see the message "is a directory, but doesn't seem to be a DayOne journal either"
|
|
@ -2,30 +2,56 @@
|
||||||
Scenario: Loading an encrypted journal
|
Scenario: Loading an encrypted journal
|
||||||
Given we use the config "encrypted.yaml"
|
Given we use the config "encrypted.yaml"
|
||||||
When we run "jrnl -n 1" and enter "bad doggie no biscuit"
|
When we run "jrnl -n 1" and enter "bad doggie no biscuit"
|
||||||
Then we should see the message "Password"
|
Then the output should contain "Password"
|
||||||
and the output should contain "2013-06-10 15:40 Life is good"
|
And the output should contain "2013-06-10 15:40 Life is good"
|
||||||
|
|
||||||
Scenario: Decrypting a journal
|
Scenario: Decrypting a journal
|
||||||
Given we use the config "encrypted.yaml"
|
Given we use the config "encrypted.yaml"
|
||||||
When we run "jrnl --decrypt" and enter "bad doggie no biscuit"
|
When we run "jrnl --decrypt" and enter "bad doggie no biscuit"
|
||||||
Then the config for journal "default" should have "encrypt" set to "bool:False"
|
Then the config for journal "default" should have "encrypt" set to "bool:False"
|
||||||
Then we should see the message "Journal decrypted"
|
Then we should see the message "Journal decrypted"
|
||||||
and the journal should have 2 entries
|
And the journal should have 2 entries
|
||||||
|
|
||||||
Scenario: Encrypting a journal
|
Scenario: Encrypting a journal
|
||||||
Given we use the config "basic.yaml"
|
Given we use the config "basic.yaml"
|
||||||
When we run "jrnl --encrypt" and enter "swordfish"
|
When we run "jrnl --encrypt" and enter
|
||||||
|
"""
|
||||||
|
swordfish
|
||||||
|
swordfish
|
||||||
|
n
|
||||||
|
"""
|
||||||
Then we should see the message "Journal encrypted"
|
Then we should see the message "Journal encrypted"
|
||||||
and the config for journal "default" should have "encrypt" set to "bool:True"
|
And the config for journal "default" should have "encrypt" set to "bool:True"
|
||||||
When we run "jrnl -n 1" and enter "swordfish"
|
When we run "jrnl -n 1" and enter "swordfish"
|
||||||
Then we should see the message "Password"
|
Then the output should contain "Password"
|
||||||
and the output should contain "2013-06-10 15:40 Life is good"
|
And the output should contain "2013-06-10 15:40 Life is good"
|
||||||
|
|
||||||
|
Scenario: Mistyping your password
|
||||||
|
Given we use the config "basic.yaml"
|
||||||
|
When we run "jrnl --encrypt" and enter
|
||||||
|
"""
|
||||||
|
swordfish
|
||||||
|
sordfish
|
||||||
|
swordfish
|
||||||
|
swordfish
|
||||||
|
n
|
||||||
|
"""
|
||||||
|
Then we should see the message "Passwords did not match"
|
||||||
|
And we should see the message "Journal encrypted"
|
||||||
|
And the config for journal "default" should have "encrypt" set to "bool:True"
|
||||||
|
When we run "jrnl -n 1" and enter "swordfish"
|
||||||
|
Then the output should contain "Password"
|
||||||
|
And the output should contain "2013-06-10 15:40 Life is good"
|
||||||
|
|
||||||
Scenario: Storing a password in Keychain
|
Scenario: Storing a password in Keychain
|
||||||
Given we use the config "multiple.yaml"
|
Given we use the config "multiple.yaml"
|
||||||
When we run "jrnl simple --encrypt" and enter "sabertooth"
|
When we run "jrnl simple --encrypt" and enter
|
||||||
|
"""
|
||||||
|
sabertooth
|
||||||
|
sabertooth
|
||||||
|
y
|
||||||
|
"""
|
||||||
When we set the keychain password of "simple" to "sabertooth"
|
When we set the keychain password of "simple" to "sabertooth"
|
||||||
Then the config for journal "simple" should have "encrypt" set to "bool:True"
|
Then the config for journal "simple" should have "encrypt" set to "bool:True"
|
||||||
When we run "jrnl simple -n 1"
|
When we run "jrnl simple -n 1"
|
||||||
Then we should not see the message "Password"
|
Then the output should contain "2013-06-10 15:40 Life is good"
|
||||||
and the output should contain "2013-06-10 15:40 Life is good"
|
|
||||||
|
|
|
@ -1,25 +1,28 @@
|
||||||
from behave import *
|
|
||||||
import shutil
|
import shutil
|
||||||
import os
|
import os
|
||||||
import jrnl
|
import sys
|
||||||
try:
|
|
||||||
from io import StringIO
|
|
||||||
except ImportError:
|
def before_feature(context, feature):
|
||||||
from cStringIO import StringIO
|
# add "skip" tag
|
||||||
|
# https://stackoverflow.com/a/42721605/4276230
|
||||||
|
if "skip" in feature.tags:
|
||||||
|
feature.skip("Marked with @skip")
|
||||||
|
return
|
||||||
|
|
||||||
|
if "skip_win" in feature.tags and "win32" in sys.platform:
|
||||||
|
feature.skip("Skipping on Windows")
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
def before_scenario(context, scenario):
|
def before_scenario(context, scenario):
|
||||||
"""Before each scenario, backup all config and journal test data."""
|
"""Before each scenario, backup all config and journal test data."""
|
||||||
context.messages = StringIO()
|
|
||||||
jrnl.util.STDERR = context.messages
|
|
||||||
jrnl.util.TEST = True
|
|
||||||
|
|
||||||
# Clean up in case something went wrong
|
# Clean up in case something went wrong
|
||||||
for folder in ("configs", "journals"):
|
for folder in ("configs", "journals"):
|
||||||
working_dir = os.path.join("features", folder)
|
working_dir = os.path.join("features", folder)
|
||||||
if os.path.exists(working_dir):
|
if os.path.exists(working_dir):
|
||||||
shutil.rmtree(working_dir)
|
shutil.rmtree(working_dir)
|
||||||
|
|
||||||
|
|
||||||
for folder in ("configs", "journals"):
|
for folder in ("configs", "journals"):
|
||||||
original = os.path.join("features", "data", folder)
|
original = os.path.join("features", "data", folder)
|
||||||
working_dir = os.path.join("features", folder)
|
working_dir = os.path.join("features", folder)
|
||||||
|
@ -32,10 +35,19 @@ def before_scenario(context, scenario):
|
||||||
else:
|
else:
|
||||||
shutil.copy2(source, working_dir)
|
shutil.copy2(source, working_dir)
|
||||||
|
|
||||||
|
# add "skip" tag
|
||||||
|
# https://stackoverflow.com/a/42721605/4276230
|
||||||
|
if "skip" in scenario.effective_tags:
|
||||||
|
scenario.skip("Marked with @skip")
|
||||||
|
return
|
||||||
|
|
||||||
|
if "skip_win" in scenario.effective_tags and "win32" in sys.platform:
|
||||||
|
scenario.skip("Skipping on Windows")
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
def after_scenario(context, scenario):
|
def after_scenario(context, scenario):
|
||||||
"""After each scenario, restore all test data and remove working_dirs."""
|
"""After each scenario, restore all test data and remove working_dirs."""
|
||||||
context.messages.close()
|
|
||||||
context.messages = None
|
|
||||||
for folder in ("configs", "journals"):
|
for folder in ("configs", "journals"):
|
||||||
working_dir = os.path.join("features", folder)
|
working_dir = os.path.join("features", folder)
|
||||||
if os.path.exists(working_dir):
|
if os.path.exists(working_dir):
|
||||||
|
|
|
@ -4,21 +4,20 @@ Feature: Exporting a Journal
|
||||||
Given we use the config "tags.yaml"
|
Given we use the config "tags.yaml"
|
||||||
When we run "jrnl --export json"
|
When we run "jrnl --export json"
|
||||||
Then we should get no error
|
Then we should get no error
|
||||||
and the output should be parsable as json
|
And the output should be parsable as json
|
||||||
and "entries" in the json output should have 2 elements
|
And "entries" in the json output should have 2 elements
|
||||||
and "tags" in the json output should contain "@idea"
|
And "tags" in the json output should contain "@idea"
|
||||||
and "tags" in the json output should contain "@journal"
|
And "tags" in the json output should contain "@journal"
|
||||||
and "tags" in the json output should contain "@dan"
|
And "tags" in the json output should contain "@dan"
|
||||||
|
|
||||||
Scenario: Exporting using filters should only export parts of the journal
|
Scenario: Exporting using filters should only export parts of the journal
|
||||||
Given we use the config "tags.yaml"
|
Given we use the config "tags.yaml"
|
||||||
When we run "jrnl -until 'may 2013' --export json"
|
When we run "jrnl -until 'may 2013' --export json"
|
||||||
# Then we should get no error
|
|
||||||
Then the output should be parsable as json
|
Then the output should be parsable as json
|
||||||
and "entries" in the json output should have 1 element
|
And "entries" in the json output should have 1 element
|
||||||
and "tags" in the json output should contain "@idea"
|
And "tags" in the json output should contain "@idea"
|
||||||
and "tags" in the json output should contain "@journal"
|
And "tags" in the json output should contain "@journal"
|
||||||
and "tags" in the json output should not contain "@dan"
|
And "tags" in the json output should not contain "@dan"
|
||||||
|
|
||||||
Scenario: Exporting using custom templates
|
Scenario: Exporting using custom templates
|
||||||
Given we use the config "basic.yaml"
|
Given we use the config "basic.yaml"
|
||||||
|
@ -83,3 +82,57 @@ Feature: Exporting a Journal
|
||||||
More stuff
|
More stuff
|
||||||
more stuff again
|
more stuff again
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
Scenario: Exporting to XML
|
||||||
|
Given we use the config "tags.yaml"
|
||||||
|
When we run "jrnl --export xml"
|
||||||
|
Then the output should be a valid XML string
|
||||||
|
And "entries" node in the xml output should have 2 elements
|
||||||
|
And "tags" in the xml output should contain ["@idea", "@journal", "@dan"]
|
||||||
|
|
||||||
|
Scenario: Exporting tags
|
||||||
|
Given we use the config "tags.yaml"
|
||||||
|
When we run "jrnl --export tags"
|
||||||
|
Then the output should be
|
||||||
|
"""
|
||||||
|
@idea : 2
|
||||||
|
@journal : 1
|
||||||
|
@dan : 1
|
||||||
|
"""
|
||||||
|
|
||||||
|
Scenario: Exporting fancy
|
||||||
|
Given we use the config "tags.yaml"
|
||||||
|
When we run "jrnl --export fancy"
|
||||||
|
Then the output should be
|
||||||
|
"""
|
||||||
|
┎──────────────────────────────────────────────────────────────╮2013-04-09 15:39
|
||||||
|
┃ I have an @idea: ╘═══════════════╕
|
||||||
|
┠╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
|
||||||
|
┃ (1) write a command line @journal software │
|
||||||
|
┃ (2) ??? │
|
||||||
|
┃ (3) PROFIT! │
|
||||||
|
┖──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
┎──────────────────────────────────────────────────────────────╮2013-06-10 15:40
|
||||||
|
┃ I met with @dan. ╘═══════════════╕
|
||||||
|
┠╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
|
||||||
|
┃ As alway's he shared his latest @idea on how to rule the world with me. │
|
||||||
|
┃ inst │
|
||||||
|
┖──────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
"""
|
||||||
|
|
||||||
|
Scenario: Export to yaml
|
||||||
|
Given we use the config "tags.yaml"
|
||||||
|
And we created a directory named "exported_journal"
|
||||||
|
When we run "jrnl --export yaml -o exported_journal"
|
||||||
|
Then "exported_journal" should contain the files ["2013-04-09_i-have-an-idea.md", "2013-06-10_i-met-with-dan.md"]
|
||||||
|
And the content of exported yaml "exported_journal/2013-04-09_i-have-an-idea.md" should be
|
||||||
|
"""
|
||||||
|
title: I have an @idea:
|
||||||
|
date: 2013-04-09 15:39
|
||||||
|
stared: False
|
||||||
|
tags: idea, journal
|
||||||
|
|
||||||
|
(1) write a command line @journal software
|
||||||
|
(2) ???
|
||||||
|
(3) PROFIT!
|
||||||
|
"""
|
||||||
|
|
|
@ -42,5 +42,10 @@ Feature: Multiple journals
|
||||||
|
|
||||||
Scenario: Don't crash if no file exists for a configured encrypted journal
|
Scenario: Don't crash if no file exists for a configured encrypted journal
|
||||||
Given we use the config "multiple.yaml"
|
Given we use the config "multiple.yaml"
|
||||||
When we run "jrnl new_encrypted Adding first entry" and enter "these three eyes"
|
When we run "jrnl new_encrypted Adding first entry" and enter
|
||||||
Then we should see the message "Journal 'new_encrypted' created"
|
"""
|
||||||
|
these three eyes
|
||||||
|
these three eyes
|
||||||
|
n
|
||||||
|
"""
|
||||||
|
Then we should see the message "Encrypted journal 'new_encrypted' created"
|
||||||
|
|
|
@ -15,13 +15,6 @@ Feature: Zapped bugs should stay dead.
|
||||||
Then we should see the message "Entry added"
|
Then we should see the message "Entry added"
|
||||||
and the journal should contain "[2013-11-30 15:42] Project Started."
|
and the journal should contain "[2013-11-30 15:42] Project Started."
|
||||||
|
|
||||||
Scenario: Date in the future should be parsed correctly
|
|
||||||
# https://github.com/maebert/jrnl/issues/185
|
|
||||||
Given we use the config "basic.yaml"
|
|
||||||
When we run "jrnl 26/06/2019: Planet? Earth. Year? 2019."
|
|
||||||
Then we should see the message "Entry added"
|
|
||||||
and the journal should contain "[2019-06-26 09:00] Planet?"
|
|
||||||
|
|
||||||
Scenario: Loading entry with ambiguous time stamp
|
Scenario: Loading entry with ambiguous time stamp
|
||||||
#https://github.com/maebert/jrnl/issues/153
|
#https://github.com/maebert/jrnl/issues/153
|
||||||
Given we use the config "bug153.yaml"
|
Given we use the config "bug153.yaml"
|
||||||
|
@ -32,6 +25,19 @@ Feature: Zapped bugs should stay dead.
|
||||||
2013-10-27 03:27 Some text.
|
2013-10-27 03:27 Some text.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
Scenario: Date in the future should be parsed correctly
|
||||||
|
# https://github.com/maebert/jrnl/issues/185
|
||||||
|
Given we use the config "basic.yaml"
|
||||||
|
When we run "jrnl 26/06/2019: Planet? Earth. Year? 2019."
|
||||||
|
Then we should see the message "Entry added"
|
||||||
|
and the journal should contain "[2019-06-26 09:00] Planet?"
|
||||||
|
|
||||||
|
Scenario: Empty DayOne entry bodies should not error
|
||||||
|
# https://github.com/jrnl-org/jrnl/issues/780
|
||||||
|
Given we use the config "bug780.yaml"
|
||||||
|
When we run "jrnl --short"
|
||||||
|
Then we should get no error
|
||||||
|
|
||||||
Scenario: Title with an embedded period.
|
Scenario: Title with an embedded period.
|
||||||
Given we use the config "basic.yaml"
|
Given we use the config "basic.yaml"
|
||||||
When we run "jrnl 04-24-2014: Created a new website - empty.com. Hope to get a lot of traffic."
|
When we run "jrnl 04-24-2014: Created a new website - empty.com. Hope to get a lot of traffic."
|
||||||
|
@ -62,3 +68,57 @@ Feature: Zapped bugs should stay dead.
|
||||||
Then the output should contain "I'm going to activate the machine."
|
Then the output should contain "I'm going to activate the machine."
|
||||||
Then the output should contain "I've crossed so many timelines. Is there any going back?"
|
Then the output should contain "I've crossed so many timelines. Is there any going back?"
|
||||||
|
|
||||||
|
Scenario: Viewing today's entries does not print the entire journal
|
||||||
|
# https://github.com/jrnl-org/jrnl/issues/741
|
||||||
|
Given we use the config "basic.yaml"
|
||||||
|
When we run "jrnl -on today"
|
||||||
|
Then the output should not contain "Life is good"
|
||||||
|
Then the output should not contain "But I'm better."
|
||||||
|
|
||||||
|
Scenario: Create entry using day of the week as entry date.
|
||||||
|
Given we use the config "basic.yaml"
|
||||||
|
When we run "jrnl monday: This is an entry on a Monday."
|
||||||
|
Then we should see the message "Entry added"
|
||||||
|
When we run "jrnl -1"
|
||||||
|
Then the output should contain "monday at 9am" in the local time
|
||||||
|
Then the output should contain "This is an entry on a Monday."
|
||||||
|
|
||||||
|
Scenario: Create entry using day of the week abbreviations as entry date.
|
||||||
|
Given we use the config "basic.yaml"
|
||||||
|
When we run "jrnl fri: This is an entry on a Friday."
|
||||||
|
Then we should see the message "Entry added"
|
||||||
|
When we run "jrnl -1"
|
||||||
|
Then the output should contain "friday at 9am" in the local time
|
||||||
|
|
||||||
|
Scenario: Displaying entries using -on today should display entries created today.
|
||||||
|
Given we use the config "basic.yaml"
|
||||||
|
When we run "jrnl today: Adding an entry right now."
|
||||||
|
Then we should see the message "Entry added"
|
||||||
|
When we run "jrnl -on today"
|
||||||
|
Then the output should contain "Adding an entry right now."
|
||||||
|
|
||||||
|
Scenario: Displaying entries using -from day should display correct entries
|
||||||
|
Given we use the config "basic.yaml"
|
||||||
|
When we run "jrnl yesterday: This thing happened yesterday"
|
||||||
|
Then we should see the message "Entry added"
|
||||||
|
When we run "jrnl today at 11:59pm: Adding an entry right now."
|
||||||
|
Then we should see the message "Entry added"
|
||||||
|
When we run "jrnl tomorrow: A future entry."
|
||||||
|
Then we should see the message "Entry added"
|
||||||
|
When we run "jrnl -from today"
|
||||||
|
Then the output should contain "Adding an entry right now."
|
||||||
|
Then the output should contain "A future entry."
|
||||||
|
Then the output should not contain "This thing happened yesterday"
|
||||||
|
|
||||||
|
Scenario: Displaying entries using -from and -to day should display correct entries
|
||||||
|
Given we use the config "basic.yaml"
|
||||||
|
When we run "jrnl yesterday: This thing happened yesterday"
|
||||||
|
Then we should see the message "Entry added"
|
||||||
|
When we run "jrnl today at 11:59pm: Adding an entry right now."
|
||||||
|
Then we should see the message "Entry added"
|
||||||
|
When we run "jrnl tomorrow: A future entry."
|
||||||
|
Then we should see the message "Entry added"
|
||||||
|
When we run "jrnl -from yesterday -to today"
|
||||||
|
Then the output should contain "This thing happened yesterday"
|
||||||
|
Then the output should contain "Adding an entry right now."
|
||||||
|
Then the output should not contain "A future entry."
|
||||||
|
|
|
@ -1,19 +1,30 @@
|
||||||
from __future__ import unicode_literals
|
from unittest.mock import patch
|
||||||
from __future__ import absolute_import
|
|
||||||
|
|
||||||
from behave import given, when, then
|
from behave import given, when, then
|
||||||
from jrnl import cli, install, Journal, util, plugins
|
from jrnl import cli, install, Journal, util, plugins
|
||||||
from jrnl import __version__
|
from jrnl import __version__
|
||||||
from dateutil import parser as date_parser
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
|
try:
|
||||||
|
import parsedatetime.parsedatetime_consts as pdt
|
||||||
|
except ImportError:
|
||||||
|
import parsedatetime as pdt
|
||||||
|
import time
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
import yaml
|
import yaml
|
||||||
import keyring
|
import keyring
|
||||||
|
import tzlocal
|
||||||
|
import shlex
|
||||||
|
import sys
|
||||||
|
|
||||||
|
consts = pdt.Constants(usePyICU=False)
|
||||||
|
consts.DOWParseStyle = -1 # Prefers past weekdays
|
||||||
|
CALENDAR = pdt.Calendar(consts)
|
||||||
|
|
||||||
|
|
||||||
class TestKeyring(keyring.backend.KeyringBackend):
|
class TestKeyring(keyring.backend.KeyringBackend):
|
||||||
"""A test keyring that just stores its valies in a hash"""
|
"""A test keyring that just stores its values in a hash"""
|
||||||
|
|
||||||
priority = 1
|
priority = 1
|
||||||
keys = defaultdict(dict)
|
keys = defaultdict(dict)
|
||||||
|
@ -24,42 +35,38 @@ class TestKeyring(keyring.backend.KeyringBackend):
|
||||||
def get_password(self, servicename, username):
|
def get_password(self, servicename, username):
|
||||||
return self.keys[servicename].get(username)
|
return self.keys[servicename].get(username)
|
||||||
|
|
||||||
def delete_password(self, servicename, username, password):
|
def delete_password(self, servicename, username):
|
||||||
self.keys[servicename][username] = None
|
self.keys[servicename][username] = None
|
||||||
|
|
||||||
|
|
||||||
# set the keyring for keyring lib
|
# set the keyring for keyring lib
|
||||||
keyring.set_keyring(TestKeyring())
|
keyring.set_keyring(TestKeyring())
|
||||||
|
|
||||||
|
|
||||||
try:
|
|
||||||
from io import StringIO
|
|
||||||
except ImportError:
|
|
||||||
from cStringIO import StringIO
|
|
||||||
import tzlocal
|
|
||||||
import shlex
|
|
||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
def ushlex(command):
|
def ushlex(command):
|
||||||
if sys.version_info[0] == 3:
|
if sys.version_info[0] == 3:
|
||||||
return shlex.split(command)
|
return shlex.split(command)
|
||||||
return map(lambda s: s.decode('UTF8'), shlex.split(command.encode('utf8')))
|
return map(lambda s: s.decode("UTF8"), shlex.split(command.encode("utf8")))
|
||||||
|
|
||||||
|
|
||||||
def read_journal(journal_name="default"):
|
def read_journal(journal_name="default"):
|
||||||
config = util.load_config(install.CONFIG_FILE_PATH)
|
config = util.load_config(install.CONFIG_FILE_PATH)
|
||||||
with open(config['journals'][journal_name]) as journal_file:
|
with open(config["journals"][journal_name]) as journal_file:
|
||||||
journal = journal_file.read()
|
journal = journal_file.read()
|
||||||
return journal
|
return journal
|
||||||
|
|
||||||
|
|
||||||
def open_journal(journal_name="default"):
|
def open_journal(journal_name="default"):
|
||||||
config = util.load_config(install.CONFIG_FILE_PATH)
|
config = util.load_config(install.CONFIG_FILE_PATH)
|
||||||
journal_conf = config['journals'][journal_name]
|
journal_conf = config["journals"][journal_name]
|
||||||
if type(journal_conf) is dict: # We can override the default config on a by-journal basis
|
|
||||||
|
if type(journal_conf) is dict:
|
||||||
|
# We can override the default config on a by-journal basis
|
||||||
config.update(journal_conf)
|
config.update(journal_conf)
|
||||||
else: # But also just give them a string to point to the journal file
|
else:
|
||||||
config['journal'] = journal_conf
|
# But also just give them a string to point to the journal file
|
||||||
|
config["journal"] = journal_conf
|
||||||
|
|
||||||
return Journal.open_journal(journal_name, config)
|
return Journal.open_journal(journal_name, config)
|
||||||
|
|
||||||
|
|
||||||
|
@ -69,22 +76,80 @@ def set_config(context, config_file):
|
||||||
install.CONFIG_FILE_PATH = os.path.abspath(full_path)
|
install.CONFIG_FILE_PATH = os.path.abspath(full_path)
|
||||||
if config_file.endswith("yaml"):
|
if config_file.endswith("yaml"):
|
||||||
# Add jrnl version to file for 2.x journals
|
# Add jrnl version to file for 2.x journals
|
||||||
with open(install.CONFIG_FILE_PATH, 'a') as cf:
|
with open(install.CONFIG_FILE_PATH, "a") as cf:
|
||||||
cf.write("version: {}".format(__version__))
|
cf.write("version: {}".format(__version__))
|
||||||
|
|
||||||
|
|
||||||
|
@when('we open the editor and enter ""')
|
||||||
|
@when('we open the editor and enter "{text}"')
|
||||||
|
def open_editor_and_enter(context, text=""):
|
||||||
|
text = text or context.text
|
||||||
|
|
||||||
|
def _mock_editor_function(command):
|
||||||
|
tmpfile = command[-1]
|
||||||
|
with open(tmpfile, "w+") as f:
|
||||||
|
if text is not None:
|
||||||
|
f.write(text)
|
||||||
|
else:
|
||||||
|
f.write("")
|
||||||
|
|
||||||
|
return tmpfile
|
||||||
|
|
||||||
|
with patch("subprocess.call", side_effect=_mock_editor_function):
|
||||||
|
run(context, "jrnl")
|
||||||
|
|
||||||
|
|
||||||
|
def _mock_getpass(inputs):
|
||||||
|
def prompt_return(prompt="Password: "):
|
||||||
|
print(prompt)
|
||||||
|
return next(inputs)
|
||||||
|
|
||||||
|
return prompt_return
|
||||||
|
|
||||||
|
|
||||||
|
def _mock_input(inputs):
|
||||||
|
def prompt_return(prompt=""):
|
||||||
|
val = next(inputs)
|
||||||
|
print(prompt, val)
|
||||||
|
return val
|
||||||
|
|
||||||
|
return prompt_return
|
||||||
|
|
||||||
|
|
||||||
@when('we run "{command}" and enter')
|
@when('we run "{command}" and enter')
|
||||||
|
@when('we run "{command}" and enter ""')
|
||||||
@when('we run "{command}" and enter "{inputs}"')
|
@when('we run "{command}" and enter "{inputs}"')
|
||||||
def run_with_input(context, command, inputs=None):
|
def run_with_input(context, command, inputs=""):
|
||||||
text = inputs or context.text
|
# create an iterator through all inputs. These inputs will be fed one by one
|
||||||
|
# to the mocked calls for 'input()', 'util.getpass()' and 'sys.stdin.read()'
|
||||||
|
if context.text:
|
||||||
|
text = iter(context.text.split("\n"))
|
||||||
|
else:
|
||||||
|
text = iter([inputs])
|
||||||
|
|
||||||
args = ushlex(command)[1:]
|
args = ushlex(command)[1:]
|
||||||
buffer = StringIO(text.strip())
|
|
||||||
util.STDIN = buffer
|
# fmt: off
|
||||||
try:
|
# see: https://github.com/psf/black/issues/557
|
||||||
cli.run(args or [])
|
with patch("builtins.input", side_effect=_mock_input(text)) as mock_input, \
|
||||||
context.exit_status = 0
|
patch("getpass.getpass", side_effect=_mock_getpass(text)) as mock_getpass, \
|
||||||
except SystemExit as e:
|
patch("sys.stdin.read", side_effect=text) as mock_read:
|
||||||
context.exit_status = e.code
|
|
||||||
|
try:
|
||||||
|
cli.run(args or [])
|
||||||
|
context.exit_status = 0
|
||||||
|
except SystemExit as e:
|
||||||
|
context.exit_status = e.code
|
||||||
|
|
||||||
|
# at least one of the mocked input methods got called
|
||||||
|
assert mock_input.called or mock_getpass.called or mock_read.called
|
||||||
|
# all inputs were used
|
||||||
|
try:
|
||||||
|
next(text)
|
||||||
|
assert False, "Not all inputs were consumed"
|
||||||
|
except StopIteration:
|
||||||
|
pass
|
||||||
|
# fmt: on
|
||||||
|
|
||||||
|
|
||||||
@when('we run "{command}"')
|
@when('we run "{command}"')
|
||||||
|
@ -106,112 +171,66 @@ def load_template(context, filename):
|
||||||
|
|
||||||
@when('we set the keychain password of "{journal}" to "{password}"')
|
@when('we set the keychain password of "{journal}" to "{password}"')
|
||||||
def set_keychain(context, journal, password):
|
def set_keychain(context, journal, password):
|
||||||
keyring.set_password('jrnl', journal, password)
|
keyring.set_password("jrnl", journal, password)
|
||||||
|
|
||||||
|
|
||||||
@then('we should get an error')
|
@then("we should get an error")
|
||||||
def has_error(context):
|
def has_error(context):
|
||||||
assert context.exit_status != 0, context.exit_status
|
assert context.exit_status != 0, context.exit_status
|
||||||
|
|
||||||
|
|
||||||
@then('we should get no error')
|
@then("we should get no error")
|
||||||
def no_error(context):
|
def no_error(context):
|
||||||
assert context.exit_status is 0, context.exit_status
|
assert context.exit_status == 0, context.exit_status
|
||||||
|
|
||||||
|
|
||||||
@then('the output should be parsable as json')
|
@then("the output should be")
|
||||||
def check_output_json(context):
|
|
||||||
out = context.stdout_capture.getvalue()
|
|
||||||
assert json.loads(out), out
|
|
||||||
|
|
||||||
|
|
||||||
@then('"{field}" in the json output should have {number:d} elements')
|
|
||||||
@then('"{field}" in the json output should have 1 element')
|
|
||||||
def check_output_field(context, field, number=1):
|
|
||||||
out = context.stdout_capture.getvalue()
|
|
||||||
out_json = json.loads(out)
|
|
||||||
assert field in out_json, [field, out_json]
|
|
||||||
assert len(out_json[field]) == number, len(out_json[field])
|
|
||||||
|
|
||||||
|
|
||||||
@then('"{field}" in the json output should not contain "{key}"')
|
|
||||||
def check_output_field_not_key(context, field, key):
|
|
||||||
out = context.stdout_capture.getvalue()
|
|
||||||
out_json = json.loads(out)
|
|
||||||
assert field in out_json
|
|
||||||
assert key not in out_json[field]
|
|
||||||
|
|
||||||
|
|
||||||
@then('"{field}" in the json output should contain "{key}"')
|
|
||||||
def check_output_field_key(context, field, key):
|
|
||||||
out = context.stdout_capture.getvalue()
|
|
||||||
out_json = json.loads(out)
|
|
||||||
assert field in out_json
|
|
||||||
assert key in out_json[field]
|
|
||||||
|
|
||||||
|
|
||||||
@then('the json output should contain {path} = "{value}"')
|
|
||||||
def check_json_output_path(context, path, value):
|
|
||||||
""" E.g.
|
|
||||||
the json output should contain entries.0.title = "hello"
|
|
||||||
"""
|
|
||||||
out = context.stdout_capture.getvalue()
|
|
||||||
struct = json.loads(out)
|
|
||||||
|
|
||||||
for node in path.split('.'):
|
|
||||||
try:
|
|
||||||
struct = struct[int(node)]
|
|
||||||
except ValueError:
|
|
||||||
struct = struct[node]
|
|
||||||
assert struct == value, struct
|
|
||||||
|
|
||||||
|
|
||||||
@then('the output should be')
|
|
||||||
@then('the output should be "{text}"')
|
@then('the output should be "{text}"')
|
||||||
def check_output(context, text=None):
|
def check_output(context, text=None):
|
||||||
text = (text or context.text).strip().splitlines()
|
text = (text or context.text).strip().splitlines()
|
||||||
out = context.stdout_capture.getvalue().strip().splitlines()
|
out = context.stdout_capture.getvalue().strip().splitlines()
|
||||||
assert len(text) == len(out), "Output has {} lines (expected: {})".format(len(out), len(text))
|
assert len(text) == len(out), "Output has {} lines (expected: {})".format(
|
||||||
|
len(out), len(text)
|
||||||
|
)
|
||||||
for line_text, line_out in zip(text, out):
|
for line_text, line_out in zip(text, out):
|
||||||
assert line_text.strip() == line_out.strip(), [line_text.strip(), line_out.strip()]
|
assert line_text.strip() == line_out.strip(), [
|
||||||
|
line_text.strip(),
|
||||||
|
line_out.strip(),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
@then('the output should contain "{text}" in the local time')
|
@then('the output should contain "{text}" in the local time')
|
||||||
def check_output_time_inline(context, text):
|
def check_output_time_inline(context, text):
|
||||||
out = context.stdout_capture.getvalue()
|
out = context.stdout_capture.getvalue()
|
||||||
local_tz = tzlocal.get_localzone()
|
local_tz = tzlocal.get_localzone()
|
||||||
utc_time = date_parser.parse(text)
|
date, flag = CALENDAR.parse(text)
|
||||||
local_date = utc_time.astimezone(local_tz).strftime("%Y-%m-%d %H:%M")
|
output_date = time.strftime("%Y-%m-%d %H:%M", date)
|
||||||
assert local_date in out, local_date
|
assert output_date in out, output_date
|
||||||
|
|
||||||
|
|
||||||
@then('the output should contain')
|
@then("the output should contain")
|
||||||
@then('the output should contain "{text}"')
|
@then('the output should contain "{text}"')
|
||||||
def check_output_inline(context, text=None):
|
def check_output_inline(context, text=None):
|
||||||
text = text or context.text
|
text = text or context.text
|
||||||
out = context.stdout_capture.getvalue()
|
out = context.stdout_capture.getvalue()
|
||||||
if isinstance(out, bytes):
|
|
||||||
out = out.decode('utf-8')
|
|
||||||
assert text in out, text
|
assert text in out, text
|
||||||
|
|
||||||
|
|
||||||
@then('the output should not contain "{text}"')
|
@then('the output should not contain "{text}"')
|
||||||
def check_output_not_inline(context, text):
|
def check_output_not_inline(context, text):
|
||||||
out = context.stdout_capture.getvalue()
|
out = context.stdout_capture.getvalue()
|
||||||
if isinstance(out, bytes):
|
|
||||||
out = out.decode('utf-8')
|
|
||||||
assert text not in out
|
assert text not in out
|
||||||
|
|
||||||
|
|
||||||
@then('we should see the message "{text}"')
|
@then('we should see the message "{text}"')
|
||||||
def check_message(context, text):
|
def check_message(context, text):
|
||||||
out = context.messages.getvalue()
|
out = context.stderr_capture.getvalue()
|
||||||
assert text in out, [text, out]
|
assert text in out, [text, out]
|
||||||
|
|
||||||
|
|
||||||
@then('we should not see the message "{text}"')
|
@then('we should not see the message "{text}"')
|
||||||
def check_not_message(context, text):
|
def check_not_message(context, text):
|
||||||
out = context.messages.getvalue()
|
out = context.stderr_capture.getvalue()
|
||||||
assert text not in out, [text, out]
|
assert text not in out, [text, out]
|
||||||
|
|
||||||
|
|
||||||
|
@ -226,7 +245,7 @@ def check_journal_content(context, text, journal_name="default"):
|
||||||
def journal_doesnt_exist(context, journal_name="default"):
|
def journal_doesnt_exist(context, journal_name="default"):
|
||||||
with open(install.CONFIG_FILE_PATH) as config_file:
|
with open(install.CONFIG_FILE_PATH) as config_file:
|
||||||
config = yaml.load(config_file, Loader=yaml.FullLoader)
|
config = yaml.load(config_file, Loader=yaml.FullLoader)
|
||||||
journal_path = config['journals'][journal_name]
|
journal_path = config["journals"][journal_name]
|
||||||
assert not os.path.exists(journal_path)
|
assert not os.path.exists(journal_path)
|
||||||
|
|
||||||
|
|
||||||
|
@ -234,11 +253,7 @@ def journal_doesnt_exist(context, journal_name="default"):
|
||||||
@then('the config for journal "{journal}" should have "{key}" set to "{value}"')
|
@then('the config for journal "{journal}" should have "{key}" set to "{value}"')
|
||||||
def config_var(context, key, value, journal=None):
|
def config_var(context, key, value, journal=None):
|
||||||
t, value = value.split(":")
|
t, value = value.split(":")
|
||||||
value = {
|
value = {"bool": lambda v: v.lower() == "true", "int": int, "str": str}[t](value)
|
||||||
"bool": lambda v: v.lower() == "true",
|
|
||||||
"int": int,
|
|
||||||
"str": str
|
|
||||||
}[t](value)
|
|
||||||
config = util.load_config(install.CONFIG_FILE_PATH)
|
config = util.load_config(install.CONFIG_FILE_PATH)
|
||||||
if journal:
|
if journal:
|
||||||
config = config["journals"][journal]
|
config = config["journals"][journal]
|
||||||
|
@ -246,8 +261,8 @@ def config_var(context, key, value, journal=None):
|
||||||
assert config[key] == value
|
assert config[key] == value
|
||||||
|
|
||||||
|
|
||||||
@then('the journal should have {number:d} entries')
|
@then("the journal should have {number:d} entries")
|
||||||
@then('the journal should have {number:d} entry')
|
@then("the journal should have {number:d} entry")
|
||||||
@then('journal "{journal_name}" should have {number:d} entries')
|
@then('journal "{journal_name}" should have {number:d} entries')
|
||||||
@then('journal "{journal_name}" should have {number:d} entry')
|
@then('journal "{journal_name}" should have {number:d} entry')
|
||||||
def check_journal_entries(context, number, journal_name="default"):
|
def check_journal_entries(context, number, journal_name="default"):
|
||||||
|
@ -264,6 +279,6 @@ def list_journal_directory(context, journal="default"):
|
||||||
for file in f:
|
for file in f:
|
||||||
print(os.path.join(root,file))
|
print(os.path.join(root,file))
|
||||||
|
|
||||||
@then('fail')
|
@then("fail")
|
||||||
def debug_fail(context):
|
def debug_fail(context):
|
||||||
assert False
|
assert False
|
||||||
|
|
124
features/steps/export_steps.py
Normal file
124
features/steps/export_steps.py
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
from xml.etree import ElementTree
|
||||||
|
|
||||||
|
from behave import then, given
|
||||||
|
|
||||||
|
|
||||||
|
@then("the output should be parsable as json")
|
||||||
|
def check_output_json(context):
|
||||||
|
out = context.stdout_capture.getvalue()
|
||||||
|
assert json.loads(out), out
|
||||||
|
|
||||||
|
|
||||||
|
@then('"{field}" in the json output should have {number:d} elements')
|
||||||
|
@then('"{field}" in the json output should have 1 element')
|
||||||
|
def check_output_field(context, field, number=1):
|
||||||
|
out = context.stdout_capture.getvalue()
|
||||||
|
out_json = json.loads(out)
|
||||||
|
assert field in out_json, [field, out_json]
|
||||||
|
assert len(out_json[field]) == number, len(out_json[field])
|
||||||
|
|
||||||
|
|
||||||
|
@then('"{field}" in the json output should not contain "{key}"')
|
||||||
|
def check_output_field_not_key(context, field, key):
|
||||||
|
out = context.stdout_capture.getvalue()
|
||||||
|
out_json = json.loads(out)
|
||||||
|
assert field in out_json
|
||||||
|
assert key not in out_json[field]
|
||||||
|
|
||||||
|
|
||||||
|
@then('"{field}" in the json output should contain "{key}"')
|
||||||
|
def check_output_field_key(context, field, key):
|
||||||
|
out = context.stdout_capture.getvalue()
|
||||||
|
out_json = json.loads(out)
|
||||||
|
assert field in out_json
|
||||||
|
assert key in out_json[field]
|
||||||
|
|
||||||
|
|
||||||
|
@then('the json output should contain {path} = "{value}"')
|
||||||
|
def check_json_output_path(context, path, value):
|
||||||
|
""" E.g.
|
||||||
|
the json output should contain entries.0.title = "hello"
|
||||||
|
"""
|
||||||
|
out = context.stdout_capture.getvalue()
|
||||||
|
struct = json.loads(out)
|
||||||
|
|
||||||
|
for node in path.split("."):
|
||||||
|
try:
|
||||||
|
struct = struct[int(node)]
|
||||||
|
except ValueError:
|
||||||
|
struct = struct[node]
|
||||||
|
assert struct == value, struct
|
||||||
|
|
||||||
|
|
||||||
|
@then("the output should be a valid XML string")
|
||||||
|
def assert_valid_xml_string(context):
|
||||||
|
output = context.stdout_capture.getvalue()
|
||||||
|
xml_tree = ElementTree.fromstring(output)
|
||||||
|
assert xml_tree, output
|
||||||
|
|
||||||
|
|
||||||
|
@then('"entries" node in the xml output should have {number:d} elements')
|
||||||
|
def assert_xml_output_entries_count(context, number):
|
||||||
|
output = context.stdout_capture.getvalue()
|
||||||
|
xml_tree = ElementTree.fromstring(output)
|
||||||
|
|
||||||
|
xml_tags = (node.tag for node in xml_tree)
|
||||||
|
assert "entries" in xml_tags, str(list(xml_tags))
|
||||||
|
|
||||||
|
actual_entry_count = len(xml_tree.find("entries"))
|
||||||
|
assert actual_entry_count == number, actual_entry_count
|
||||||
|
|
||||||
|
|
||||||
|
@then('"tags" in the xml output should contain {expected_tags_json_list}')
|
||||||
|
def assert_xml_output_tags(context, expected_tags_json_list):
|
||||||
|
output = context.stdout_capture.getvalue()
|
||||||
|
xml_tree = ElementTree.fromstring(output)
|
||||||
|
|
||||||
|
xml_tags = (node.tag for node in xml_tree)
|
||||||
|
assert "tags" in xml_tags, str(list(xml_tags))
|
||||||
|
|
||||||
|
expected_tags = json.loads(expected_tags_json_list)
|
||||||
|
actual_tags = set(t.attrib["name"] for t in xml_tree.find("tags"))
|
||||||
|
assert actual_tags == set(expected_tags), [actual_tags, set(expected_tags)]
|
||||||
|
|
||||||
|
|
||||||
|
@given('we created a directory named "{dir_name}"')
|
||||||
|
def create_directory(context, dir_name):
|
||||||
|
if os.path.exists(dir_name):
|
||||||
|
shutil.rmtree(dir_name)
|
||||||
|
os.mkdir(dir_name)
|
||||||
|
|
||||||
|
|
||||||
|
@then('"{dir_name}" should contain the files {expected_files_json_list}')
|
||||||
|
def assert_dir_contains_files(context, dir_name, expected_files_json_list):
|
||||||
|
actual_files = os.listdir(dir_name)
|
||||||
|
expected_files = json.loads(expected_files_json_list)
|
||||||
|
assert actual_files == expected_files, [actual_files, expected_files]
|
||||||
|
|
||||||
|
|
||||||
|
@then('the content of exported yaml "{file_path}" should be')
|
||||||
|
def assert_exported_yaml_file_content(context, file_path):
|
||||||
|
expected_content = context.text.strip().splitlines()
|
||||||
|
|
||||||
|
with open(file_path, "r") as f:
|
||||||
|
actual_content = f.read().strip().splitlines()
|
||||||
|
|
||||||
|
for actual_line, expected_line in zip(actual_content, expected_content):
|
||||||
|
if actual_line.startswith("tags: ") and expected_line.startswith("tags: "):
|
||||||
|
assert_equal_tags_ignoring_order(actual_line, expected_line)
|
||||||
|
else:
|
||||||
|
assert actual_line.strip() == expected_line.strip(), [
|
||||||
|
actual_line.strip(),
|
||||||
|
expected_line.strip(),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def assert_equal_tags_ignoring_order(actual_line, expected_line):
|
||||||
|
actual_tags = set(tag.strip() for tag in actual_line[len("tags: ") :].split(","))
|
||||||
|
expected_tags = set(
|
||||||
|
tag.strip() for tag in expected_line[len("tags: ") :].split(",")
|
||||||
|
)
|
||||||
|
assert actual_tags == expected_tags, [actual_tags, expected_tags]
|
|
@ -19,5 +19,16 @@ Feature: Upgrading Journals from 1.x.x to 2.x.x
|
||||||
bad doggie no biscuit
|
bad doggie no biscuit
|
||||||
bad doggie no biscuit
|
bad doggie no biscuit
|
||||||
"""
|
"""
|
||||||
Then we should see the message "Password"
|
Then the output should contain "Password"
|
||||||
and the output should contain "2013-06-10 15:40 Life is good"
|
and the output should contain "2013-06-10 15:40 Life is good"
|
||||||
|
|
||||||
|
Scenario: Upgrade and parse journals with little endian date format
|
||||||
|
Given we use the config "upgrade_from_195_little_endian_dates.json"
|
||||||
|
When we run "jrnl -9" and enter "Y"
|
||||||
|
Then the output should contain
|
||||||
|
"""
|
||||||
|
10.06.2010 15:00 A life without chocolate is like a bad analogy.
|
||||||
|
|
||||||
|
10.06.2013 15:40 He said "[this] is the best time to be alive".
|
||||||
|
"""
|
||||||
|
Then the journal should have 2 entries
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# encoding: utf-8
|
|
||||||
|
|
||||||
from __future__ import absolute_import, unicode_literals
|
|
||||||
from . import Entry
|
from . import Entry
|
||||||
from . import Journal
|
from . import Journal
|
||||||
from . import time as jrnl_time
|
from . import time as jrnl_time
|
||||||
|
@ -21,36 +19,55 @@ class DayOne(Journal.Journal):
|
||||||
"""A special Journal handling DayOne files"""
|
"""A special Journal handling DayOne files"""
|
||||||
|
|
||||||
# InvalidFileException was added to plistlib in Python3.4
|
# InvalidFileException was added to plistlib in Python3.4
|
||||||
PLIST_EXCEPTIONS = (ExpatError, plistlib.InvalidFileException) if hasattr(plistlib, "InvalidFileException") else ExpatError
|
PLIST_EXCEPTIONS = (
|
||||||
|
(ExpatError, plistlib.InvalidFileException)
|
||||||
|
if hasattr(plistlib, "InvalidFileException")
|
||||||
|
else ExpatError
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
self.entries = []
|
self.entries = []
|
||||||
self._deleted_entries = []
|
self._deleted_entries = []
|
||||||
super(DayOne, self).__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
def open(self):
|
def open(self):
|
||||||
filenames = [os.path.join(self.config['journal'], "entries", f) for f in os.listdir(os.path.join(self.config['journal'], "entries"))]
|
filenames = [
|
||||||
|
os.path.join(self.config["journal"], "entries", f)
|
||||||
|
for f in os.listdir(os.path.join(self.config["journal"], "entries"))
|
||||||
|
]
|
||||||
filenames = []
|
filenames = []
|
||||||
for root, dirnames, f in os.walk(self.config['journal']):
|
for root, dirnames, f in os.walk(self.config["journal"]):
|
||||||
for filename in fnmatch.filter(f, '*.doentry'):
|
for filename in fnmatch.filter(f, "*.doentry"):
|
||||||
filenames.append(os.path.join(root, filename))
|
filenames.append(os.path.join(root, filename))
|
||||||
self.entries = []
|
self.entries = []
|
||||||
for filename in filenames:
|
for filename in filenames:
|
||||||
with open(filename, 'rb') as plist_entry:
|
with open(filename, "rb") as plist_entry:
|
||||||
try:
|
try:
|
||||||
dict_entry = plistlib.readPlist(plist_entry)
|
dict_entry = plistlib.readPlist(plist_entry)
|
||||||
except self.PLIST_EXCEPTIONS:
|
except self.PLIST_EXCEPTIONS:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
timezone = pytz.timezone(dict_entry['Time Zone'])
|
timezone = pytz.timezone(dict_entry["Time Zone"])
|
||||||
except (KeyError, pytz.exceptions.UnknownTimeZoneError):
|
except (KeyError, pytz.exceptions.UnknownTimeZoneError):
|
||||||
timezone = tzlocal.get_localzone()
|
timezone = tzlocal.get_localzone()
|
||||||
date = dict_entry['Creation Date']
|
date = dict_entry["Creation Date"]
|
||||||
date = date + timezone.utcoffset(date, is_dst=False)
|
# convert the date to UTC rather than keep messing with
|
||||||
entry = Entry.Entry(self, date, text=dict_entry['Entry Text'], starred=dict_entry["Starred"])
|
# timezones
|
||||||
|
if timezone.zone != "UTC":
|
||||||
|
date = date + timezone.utcoffset(date, is_dst=False)
|
||||||
|
|
||||||
|
entry = Entry.Entry(
|
||||||
|
self,
|
||||||
|
date,
|
||||||
|
text=dict_entry["Entry Text"],
|
||||||
|
starred=dict_entry["Starred"],
|
||||||
|
)
|
||||||
entry.uuid = dict_entry["UUID"]
|
entry.uuid = dict_entry["UUID"]
|
||||||
entry._tags = [self.config['tagsymbols'][0] + tag.lower() for tag in dict_entry.get("Tags", [])]
|
entry._tags = [
|
||||||
|
self.config["tagsymbols"][0] + tag.lower()
|
||||||
|
for tag in dict_entry.get("Tags", [])
|
||||||
|
]
|
||||||
|
|
||||||
self.entries.append(entry)
|
self.entries.append(entry)
|
||||||
self.sort()
|
self.sort()
|
||||||
|
@ -60,30 +77,39 @@ class DayOne(Journal.Journal):
|
||||||
"""Writes only the entries that have been modified into plist files."""
|
"""Writes only the entries that have been modified into plist files."""
|
||||||
for entry in self.entries:
|
for entry in self.entries:
|
||||||
if entry.modified:
|
if entry.modified:
|
||||||
utc_time = datetime.utcfromtimestamp(time.mktime(entry.date.timetuple()))
|
utc_time = datetime.utcfromtimestamp(
|
||||||
|
time.mktime(entry.date.timetuple())
|
||||||
|
)
|
||||||
|
|
||||||
if not hasattr(entry, "uuid"):
|
if not hasattr(entry, "uuid"):
|
||||||
entry.uuid = uuid.uuid1().hex
|
entry.uuid = uuid.uuid1().hex
|
||||||
|
|
||||||
filename = os.path.join(self.config['journal'], "entries", entry.uuid.upper() + ".doentry")
|
filename = os.path.join(
|
||||||
|
self.config["journal"], "entries", entry.uuid.upper() + ".doentry"
|
||||||
|
)
|
||||||
|
|
||||||
entry_plist = {
|
entry_plist = {
|
||||||
'Creation Date': utc_time,
|
"Creation Date": utc_time,
|
||||||
'Starred': entry.starred if hasattr(entry, 'starred') else False,
|
"Starred": entry.starred if hasattr(entry, "starred") else False,
|
||||||
'Entry Text': entry.title + "\n" + entry.body,
|
"Entry Text": entry.title + "\n" + entry.body,
|
||||||
'Time Zone': str(tzlocal.get_localzone()),
|
"Time Zone": str(tzlocal.get_localzone()),
|
||||||
'UUID': entry.uuid.upper(),
|
"UUID": entry.uuid.upper(),
|
||||||
'Tags': [tag.strip(self.config['tagsymbols']).replace("_", " ") for tag in entry.tags]
|
"Tags": [
|
||||||
|
tag.strip(self.config["tagsymbols"]).replace("_", " ")
|
||||||
|
for tag in entry.tags
|
||||||
|
],
|
||||||
}
|
}
|
||||||
plistlib.writePlist(entry_plist, filename)
|
plistlib.writePlist(entry_plist, filename)
|
||||||
for entry in self._deleted_entries:
|
for entry in self._deleted_entries:
|
||||||
filename = os.path.join(self.config['journal'], "entries", entry.uuid + ".doentry")
|
filename = os.path.join(
|
||||||
|
self.config["journal"], "entries", entry.uuid + ".doentry"
|
||||||
|
)
|
||||||
os.remove(filename)
|
os.remove(filename)
|
||||||
|
|
||||||
def editable_str(self):
|
def editable_str(self):
|
||||||
"""Turns the journal into a string of entries that can be edited
|
"""Turns the journal into a string of entries that can be edited
|
||||||
manually and later be parsed with eslf.parse_editable_str."""
|
manually and later be parsed with eslf.parse_editable_str."""
|
||||||
return "\n".join(["# {0}\n{1}".format(e.uuid, e.__unicode__()) for e in self.entries])
|
return "\n".join([f"# {e.uuid}\n{str(e)}" for e in self.entries])
|
||||||
|
|
||||||
def parse_editable_str(self, edited):
|
def parse_editable_str(self, edited):
|
||||||
"""Parses the output of self.editable_str and updates its entries."""
|
"""Parses the output of self.editable_str and updates its entries."""
|
||||||
|
@ -107,7 +133,7 @@ class DayOne(Journal.Journal):
|
||||||
current_entry.modified = False
|
current_entry.modified = False
|
||||||
current_entry.uuid = m.group(1).lower()
|
current_entry.uuid = m.group(1).lower()
|
||||||
else:
|
else:
|
||||||
date_blob_re = re.compile("^\[[^\\]]+\] ")
|
date_blob_re = re.compile("^\\[[^\\]]+\\] ")
|
||||||
date_blob = date_blob_re.findall(line)
|
date_blob = date_blob_re.findall(line)
|
||||||
if date_blob:
|
if date_blob:
|
||||||
date_blob = date_blob[0]
|
date_blob = date_blob[0]
|
||||||
|
@ -115,7 +141,7 @@ class DayOne(Journal.Journal):
|
||||||
if line.endswith("*"):
|
if line.endswith("*"):
|
||||||
current_entry.starred = True
|
current_entry.starred = True
|
||||||
line = line[:-1]
|
line = line[:-1]
|
||||||
current_entry.title = line[len(date_blob) - 1:]
|
current_entry.title = line[len(date_blob) - 1 :]
|
||||||
current_entry.date = new_date
|
current_entry.date = new_date
|
||||||
elif current_entry:
|
elif current_entry:
|
||||||
current_entry.body += line + "\n"
|
current_entry.body += line + "\n"
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
from . import Journal, util
|
from . import util
|
||||||
|
from .Journal import Journal, LegacyJournal
|
||||||
from cryptography.fernet import Fernet, InvalidToken
|
from cryptography.fernet import Fernet, InvalidToken
|
||||||
from cryptography.hazmat.primitives import hashes, padding
|
from cryptography.hazmat.primitives import hashes, padding
|
||||||
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||||
|
@ -8,120 +9,124 @@ from cryptography.hazmat.backends import default_backend
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import base64
|
import base64
|
||||||
import getpass
|
|
||||||
import logging
|
import logging
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger()
|
log = logging.getLogger()
|
||||||
|
|
||||||
|
|
||||||
def make_key(password):
|
def make_key(password):
|
||||||
password = util.bytes(password)
|
password = password.encode("utf-8")
|
||||||
kdf = PBKDF2HMAC(
|
kdf = PBKDF2HMAC(
|
||||||
algorithm=hashes.SHA256(),
|
algorithm=hashes.SHA256(),
|
||||||
length=32,
|
length=32,
|
||||||
# Salt is hard-coded
|
# Salt is hard-coded
|
||||||
salt=b'\xf2\xd5q\x0e\xc1\x8d.\xde\xdc\x8e6t\x89\x04\xce\xf8',
|
salt=b"\xf2\xd5q\x0e\xc1\x8d.\xde\xdc\x8e6t\x89\x04\xce\xf8",
|
||||||
iterations=100000,
|
iterations=100000,
|
||||||
backend=default_backend()
|
backend=default_backend(),
|
||||||
)
|
)
|
||||||
key = kdf.derive(password)
|
key = kdf.derive(password)
|
||||||
return base64.urlsafe_b64encode(key)
|
return base64.urlsafe_b64encode(key)
|
||||||
|
|
||||||
|
|
||||||
class EncryptedJournal(Journal.Journal):
|
class EncryptedJournal(Journal):
|
||||||
def __init__(self, name='default', **kwargs):
|
def __init__(self, name="default", **kwargs):
|
||||||
super(EncryptedJournal, self).__init__(name, **kwargs)
|
super().__init__(name, **kwargs)
|
||||||
self.config['encrypt'] = True
|
self.config["encrypt"] = True
|
||||||
|
self.password = None
|
||||||
|
|
||||||
def open(self, filename=None):
|
def open(self, filename=None):
|
||||||
"""Opens the journal file defined in the config and parses it into a list of Entries.
|
"""Opens the journal file defined in the config and parses it into a list of Entries.
|
||||||
Entries have the form (date, title, body)."""
|
Entries have the form (date, title, body)."""
|
||||||
filename = filename or self.config['journal']
|
filename = filename or self.config["journal"]
|
||||||
|
|
||||||
if not os.path.exists(filename):
|
if not os.path.exists(filename):
|
||||||
password = util.getpass("Enter password for new journal: ")
|
self.create_file(filename)
|
||||||
if password:
|
self.password = util.create_password(self.name)
|
||||||
if util.yesno("Do you want to store the password in your keychain?", default=True):
|
print(
|
||||||
util.set_keychain(self.name, password)
|
f"Encrypted journal '{self.name}' created at {filename}",
|
||||||
else:
|
file=sys.stderr,
|
||||||
util.set_keychain(self.name, None)
|
)
|
||||||
self.config['password'] = password
|
|
||||||
text = ""
|
text = self._load(filename)
|
||||||
self._store(filename, text)
|
|
||||||
util.prompt("[Journal '{0}' created at {1}]".format(self.name, filename))
|
|
||||||
else:
|
|
||||||
util.prompt("No password supplied for encrypted journal")
|
|
||||||
sys.exit(1)
|
|
||||||
else:
|
|
||||||
text = self._load(filename)
|
|
||||||
self.entries = self._parse(text)
|
self.entries = self._parse(text)
|
||||||
self.sort()
|
self.sort()
|
||||||
log.debug("opened %s with %d entries", self.__class__.__name__, len(self))
|
log.debug("opened %s with %d entries", self.__class__.__name__, len(self))
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
def _load(self, filename):
|
||||||
def _load(self, filename, password=None):
|
|
||||||
"""Loads an encrypted journal from a file and tries to decrypt it.
|
"""Loads an encrypted journal from a file and tries to decrypt it.
|
||||||
If password is not provided, will look for password in the keychain
|
If password is not provided, will look for password in the keychain
|
||||||
and otherwise ask the user to enter a password up to three times.
|
and otherwise ask the user to enter a password up to three times.
|
||||||
If the password is provided but wrong (or corrupt), this will simply
|
If the password is provided but wrong (or corrupt), this will simply
|
||||||
return None."""
|
return None."""
|
||||||
with open(filename, 'rb') as f:
|
with open(filename, "rb") as f:
|
||||||
journal_encrypted = f.read()
|
journal_encrypted = f.read()
|
||||||
|
|
||||||
def validate_password(password):
|
def decrypt_journal(password):
|
||||||
key = make_key(password)
|
key = make_key(password)
|
||||||
try:
|
try:
|
||||||
plain = Fernet(key).decrypt(journal_encrypted).decode('utf-8')
|
plain = Fernet(key).decrypt(journal_encrypted).decode("utf-8")
|
||||||
self.config['password'] = password
|
self.password = password
|
||||||
return plain
|
return plain
|
||||||
except (InvalidToken, IndexError):
|
except (InvalidToken, IndexError):
|
||||||
return None
|
return None
|
||||||
if password:
|
|
||||||
return validate_password(password)
|
if self.password:
|
||||||
return util.get_password(keychain=self.name, validator=validate_password)
|
return decrypt_journal(self.password)
|
||||||
|
|
||||||
|
return util.decrypt_content(keychain=self.name, decrypt_func=decrypt_journal)
|
||||||
|
|
||||||
def _store(self, filename, text):
|
def _store(self, filename, text):
|
||||||
key = make_key(self.config['password'])
|
key = make_key(self.password)
|
||||||
journal = Fernet(key).encrypt(text.encode('utf-8'))
|
journal = Fernet(key).encrypt(text.encode("utf-8"))
|
||||||
with open(filename, 'wb') as f:
|
with open(filename, "wb") as f:
|
||||||
f.write(journal)
|
f.write(journal)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _create(cls, filename, password):
|
def from_journal(cls, other: Journal):
|
||||||
key = make_key(password)
|
new_journal = super().from_journal(other)
|
||||||
dummy = Fernet(key).encrypt(b"")
|
new_journal.password = (
|
||||||
with open(filename, 'wb') as f:
|
other.password
|
||||||
f.write(dummy)
|
if hasattr(other, "password")
|
||||||
|
else util.create_password(other.name)
|
||||||
|
)
|
||||||
|
return new_journal
|
||||||
|
|
||||||
|
|
||||||
class LegacyEncryptedJournal(Journal.LegacyJournal):
|
class LegacyEncryptedJournal(LegacyJournal):
|
||||||
"""Legacy class to support opening journals encrypted with the jrnl 1.x
|
"""Legacy class to support opening journals encrypted with the jrnl 1.x
|
||||||
standard. You'll not be able to save these journals anymore."""
|
standard. You'll not be able to save these journals anymore."""
|
||||||
def __init__(self, name='default', **kwargs):
|
|
||||||
super(LegacyEncryptedJournal, self).__init__(name, **kwargs)
|
|
||||||
self.config['encrypt'] = True
|
|
||||||
|
|
||||||
def _load(self, filename, password=None):
|
def __init__(self, name="default", **kwargs):
|
||||||
with open(filename, 'rb') as f:
|
super().__init__(name, **kwargs)
|
||||||
|
self.config["encrypt"] = True
|
||||||
|
self.password = None
|
||||||
|
|
||||||
|
def _load(self, filename):
|
||||||
|
with open(filename, "rb") as f:
|
||||||
journal_encrypted = f.read()
|
journal_encrypted = f.read()
|
||||||
iv, cipher = journal_encrypted[:16], journal_encrypted[16:]
|
iv, cipher = journal_encrypted[:16], journal_encrypted[16:]
|
||||||
|
|
||||||
def validate_password(password):
|
def decrypt_journal(password):
|
||||||
decryption_key = hashlib.sha256(password.encode('utf-8')).digest()
|
decryption_key = hashlib.sha256(password.encode("utf-8")).digest()
|
||||||
decryptor = Cipher(algorithms.AES(decryption_key), modes.CBC(iv), default_backend()).decryptor()
|
decryptor = Cipher(
|
||||||
|
algorithms.AES(decryption_key), modes.CBC(iv), default_backend()
|
||||||
|
).decryptor()
|
||||||
try:
|
try:
|
||||||
plain_padded = decryptor.update(cipher) + decryptor.finalize()
|
plain_padded = decryptor.update(cipher) + decryptor.finalize()
|
||||||
self.config['password'] = password
|
self.password = password
|
||||||
if plain_padded[-1] in (" ", 32):
|
if plain_padded[-1] in (" ", 32):
|
||||||
# Ancient versions of jrnl. Do not judge me.
|
# Ancient versions of jrnl. Do not judge me.
|
||||||
return plain_padded.decode('utf-8').rstrip(" ")
|
return plain_padded.decode("utf-8").rstrip(" ")
|
||||||
else:
|
else:
|
||||||
unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder()
|
unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder()
|
||||||
plain = unpadder.update(plain_padded) + unpadder.finalize()
|
plain = unpadder.update(plain_padded) + unpadder.finalize()
|
||||||
return plain.decode('utf-8')
|
return plain.decode("utf-8")
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return None
|
return None
|
||||||
if password:
|
|
||||||
return validate_password(password)
|
if self.password:
|
||||||
return util.get_password(keychain=self.name, validator=validate_password)
|
return decrypt_journal(self.password)
|
||||||
|
return util.decrypt_content(keychain=self.name, decrypt_func=decrypt_journal)
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# encoding: utf-8
|
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
import re
|
import re
|
||||||
import textwrap
|
import textwrap
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
@ -24,7 +22,7 @@ class Entry:
|
||||||
def _parse_text(self):
|
def _parse_text(self):
|
||||||
raw_text = self.text
|
raw_text = self.text
|
||||||
lines = raw_text.splitlines()
|
lines = raw_text.splitlines()
|
||||||
if lines[0].strip().endswith("*"):
|
if lines and lines[0].strip().endswith("*"):
|
||||||
self.starred = True
|
self.starred = True
|
||||||
raw_text = lines[0].strip("\n *") + "\n" + "\n".join(lines[1:])
|
raw_text = lines[0].strip("\n *") + "\n" + "\n".join(lines[1:])
|
||||||
self._title, self._body = split_title(raw_text)
|
self._title, self._body = split_title(raw_text)
|
||||||
|
@ -51,72 +49,84 @@ class Entry:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def tag_regex(tagsymbols):
|
def tag_regex(tagsymbols):
|
||||||
pattern = r'(?u)(?:^|\s)([{tags}][-+*#/\w]+)'.format(tags=tagsymbols)
|
pattern = fr"(?u)(?:^|\s)([{tagsymbols}][-+*#/\w]+)"
|
||||||
return re.compile(pattern, re.UNICODE)
|
return re.compile(pattern)
|
||||||
|
|
||||||
def _parse_tags(self):
|
def _parse_tags(self):
|
||||||
tagsymbols = self.journal.config['tagsymbols']
|
tagsymbols = self.journal.config["tagsymbols"]
|
||||||
return set(tag.lower() for tag in re.findall(Entry.tag_regex(tagsymbols), self.text))
|
return {
|
||||||
|
tag.lower() for tag in re.findall(Entry.tag_regex(tagsymbols), self.text)
|
||||||
|
}
|
||||||
|
|
||||||
def __unicode__(self):
|
def __str__(self):
|
||||||
"""Returns a string representation of the entry to be written into a journal file."""
|
"""Returns a string representation of the entry to be written into a journal file."""
|
||||||
date_str = self.date.strftime(self.journal.config['timeformat'])
|
date_str = self.date.strftime(self.journal.config["timeformat"])
|
||||||
title = "[{}] {}".format(date_str, self.title.rstrip("\n "))
|
title = "[{}] {}".format(date_str, self.title.rstrip("\n "))
|
||||||
if self.starred:
|
if self.starred:
|
||||||
title += " *"
|
title += " *"
|
||||||
return "{title}{sep}{body}\n".format(
|
return "{title}{sep}{body}\n".format(
|
||||||
title=title,
|
title=title,
|
||||||
sep="\n" if self.body.rstrip("\n ") else "",
|
sep="\n" if self.body.rstrip("\n ") else "",
|
||||||
body=self.body.rstrip("\n ")
|
body=self.body.rstrip("\n "),
|
||||||
)
|
)
|
||||||
|
|
||||||
def pprint(self, short=False):
|
def pprint(self, short=False):
|
||||||
"""Returns a pretty-printed version of the entry.
|
"""Returns a pretty-printed version of the entry.
|
||||||
If short is true, only print the title."""
|
If short is true, only print the title."""
|
||||||
date_str = self.date.strftime(self.journal.config['timeformat'])
|
date_str = self.date.strftime(self.journal.config["timeformat"])
|
||||||
if self.journal.config['indent_character']:
|
if self.journal.config["indent_character"]:
|
||||||
indent = self.journal.config['indent_character'].rstrip() + " "
|
indent = self.journal.config["indent_character"].rstrip() + " "
|
||||||
else:
|
else:
|
||||||
indent = ""
|
indent = ""
|
||||||
if not short and self.journal.config['linewrap']:
|
if not short and self.journal.config["linewrap"]:
|
||||||
title = textwrap.fill(date_str + " " + self.title, self.journal.config['linewrap'])
|
title = textwrap.fill(
|
||||||
body = "\n".join([
|
date_str + " " + self.title, self.journal.config["linewrap"]
|
||||||
textwrap.fill(
|
)
|
||||||
line,
|
body = "\n".join(
|
||||||
self.journal.config['linewrap'],
|
[
|
||||||
initial_indent=indent,
|
textwrap.fill(
|
||||||
subsequent_indent=indent,
|
line,
|
||||||
drop_whitespace=True) or indent
|
self.journal.config["linewrap"],
|
||||||
for line in self.body.rstrip(" \n").splitlines()
|
initial_indent=indent,
|
||||||
])
|
subsequent_indent=indent,
|
||||||
|
drop_whitespace=True,
|
||||||
|
)
|
||||||
|
or indent
|
||||||
|
for line in self.body.rstrip(" \n").splitlines()
|
||||||
|
]
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
title = date_str + " " + self.title.rstrip("\n ")
|
title = date_str + " " + self.title.rstrip("\n ")
|
||||||
body = self.body.rstrip("\n ")
|
body = self.body.rstrip("\n ")
|
||||||
|
|
||||||
# Suppress bodies that are just blanks and new lines.
|
# Suppress bodies that are just blanks and new lines.
|
||||||
has_body = len(self.body) > 20 or not all(char in (" ", "\n") for char in self.body)
|
has_body = len(self.body) > 20 or not all(
|
||||||
|
char in (" ", "\n") for char in self.body
|
||||||
|
)
|
||||||
|
|
||||||
if short:
|
if short:
|
||||||
return title
|
return title
|
||||||
else:
|
else:
|
||||||
return "{title}{sep}{body}\n".format(
|
return "{title}{sep}{body}\n".format(
|
||||||
title=title,
|
title=title, sep="\n" if has_body else "", body=body if has_body else ""
|
||||||
sep="\n" if has_body else "",
|
|
||||||
body=body if has_body else "",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<Entry '{0}' on {1}>".format(self.title.strip(), self.date.strftime("%Y-%m-%d %H:%M"))
|
return "<Entry '{}' on {}>".format(
|
||||||
|
self.title.strip(), self.date.strftime("%Y-%m-%d %H:%M")
|
||||||
|
)
|
||||||
|
|
||||||
def __hash__(self):
|
def __hash__(self):
|
||||||
return hash(self.__repr__())
|
return hash(self.__repr__())
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
if not isinstance(other, Entry) \
|
if (
|
||||||
or self.title.strip() != other.title.strip() \
|
not isinstance(other, Entry)
|
||||||
or self.body.rstrip() != other.body.rstrip() \
|
or self.title.strip() != other.title.strip()
|
||||||
or self.date != other.date \
|
or self.body.rstrip() != other.body.rstrip()
|
||||||
or self.starred != other.starred:
|
or self.date != other.date
|
||||||
|
or self.starred != other.starred
|
||||||
|
):
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
178
jrnl/Journal.py
178
jrnl/Journal.py
|
@ -1,13 +1,10 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# encoding: utf-8
|
|
||||||
|
|
||||||
from __future__ import absolute_import, unicode_literals
|
|
||||||
from . import Entry
|
from . import Entry
|
||||||
from . import util
|
from . import util
|
||||||
from . import time
|
from . import time
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import codecs
|
|
||||||
import re
|
import re
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import logging
|
import logging
|
||||||
|
@ -15,7 +12,7 @@ import logging
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Tag(object):
|
class Tag:
|
||||||
def __init__(self, name, count=0):
|
def __init__(self, name, count=0):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.count = count
|
self.count = count
|
||||||
|
@ -24,26 +21,27 @@ class Tag(object):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<Tag '{}'>".format(self.name)
|
return f"<Tag '{self.name}'>"
|
||||||
|
|
||||||
|
|
||||||
class Journal(object):
|
class Journal:
|
||||||
def __init__(self, name='default', **kwargs):
|
def __init__(self, name="default", **kwargs):
|
||||||
self.config = {
|
self.config = {
|
||||||
'journal': "journal.txt",
|
"journal": "journal.txt",
|
||||||
'encrypt': False,
|
"encrypt": False,
|
||||||
'default_hour': 9,
|
"default_hour": 9,
|
||||||
'default_minute': 0,
|
"default_minute": 0,
|
||||||
'timeformat': "%Y-%m-%d %H:%M",
|
"timeformat": "%Y-%m-%d %H:%M",
|
||||||
'tagsymbols': '@',
|
"tagsymbols": "@",
|
||||||
'highlight': True,
|
"highlight": True,
|
||||||
'linewrap': 80,
|
"linewrap": 80,
|
||||||
'indent_character': '|',
|
"indent_character": "|",
|
||||||
}
|
}
|
||||||
self.config.update(kwargs)
|
self.config.update(kwargs)
|
||||||
# Set up date parser
|
# Set up date parser
|
||||||
self.search_tags = None # Store tags we're highlighting
|
self.search_tags = None # Store tags we're highlighting
|
||||||
self.name = name
|
self.name = name
|
||||||
|
self.entries = []
|
||||||
|
|
||||||
def __len__(self):
|
def __len__(self):
|
||||||
"""Returns the number of entries"""
|
"""Returns the number of entries"""
|
||||||
|
@ -59,21 +57,28 @@ class Journal(object):
|
||||||
another journal object"""
|
another journal object"""
|
||||||
new_journal = cls(other.name, **other.config)
|
new_journal = cls(other.name, **other.config)
|
||||||
new_journal.entries = other.entries
|
new_journal.entries = other.entries
|
||||||
log.debug("Imported %d entries from %s to %s", len(new_journal), other.__class__.__name__, cls.__name__)
|
log.debug(
|
||||||
|
"Imported %d entries from %s to %s",
|
||||||
|
len(new_journal),
|
||||||
|
other.__class__.__name__,
|
||||||
|
cls.__name__,
|
||||||
|
)
|
||||||
return new_journal
|
return new_journal
|
||||||
|
|
||||||
def import_(self, other_journal_txt):
|
def import_(self, other_journal_txt):
|
||||||
self.entries = list(frozenset(self.entries) | frozenset(self._parse(other_journal_txt)))
|
self.entries = list(
|
||||||
|
frozenset(self.entries) | frozenset(self._parse(other_journal_txt))
|
||||||
|
)
|
||||||
self.sort()
|
self.sort()
|
||||||
|
|
||||||
def open(self, filename=None):
|
def open(self, filename=None):
|
||||||
"""Opens the journal file defined in the config and parses it into a list of Entries.
|
"""Opens the journal file defined in the config and parses it into a list of Entries.
|
||||||
Entries have the form (date, title, body)."""
|
Entries have the form (date, title, body)."""
|
||||||
filename = filename or self.config['journal']
|
filename = filename or self.config["journal"]
|
||||||
|
|
||||||
if not os.path.exists(filename):
|
if not os.path.exists(filename):
|
||||||
util.prompt("[Journal '{0}' created at {1}]".format(self.name, filename))
|
self.create_file(filename)
|
||||||
self._create(filename)
|
print(f"[Journal '{self.name}' created at {filename}]", file=sys.stderr)
|
||||||
|
|
||||||
text = self._load(filename)
|
text = self._load(filename)
|
||||||
self.entries = self._parse(text)
|
self.entries = self._parse(text)
|
||||||
|
@ -83,7 +88,7 @@ class Journal(object):
|
||||||
|
|
||||||
def write(self, filename=None):
|
def write(self, filename=None):
|
||||||
"""Dumps the journal into the config file, overwriting it"""
|
"""Dumps the journal into the config file, overwriting it"""
|
||||||
filename = filename or self.config['journal']
|
filename = filename or self.config["journal"]
|
||||||
text = self._to_text()
|
text = self._to_text()
|
||||||
self._store(filename, text)
|
self._store(filename, text)
|
||||||
|
|
||||||
|
@ -95,8 +100,13 @@ class Journal(object):
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def create_file(filename):
|
||||||
|
with open(filename, "w"):
|
||||||
|
pass
|
||||||
|
|
||||||
def _to_text(self):
|
def _to_text(self):
|
||||||
return "\n".join([e.__unicode__() for e in self.entries])
|
return "\n".join([str(e) for e in self.entries])
|
||||||
|
|
||||||
def _load(self, filename):
|
def _load(self, filename):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
@ -104,10 +114,6 @@ class Journal(object):
|
||||||
def _store(self, filename, text):
|
def _store(self, filename, text):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _create(cls, filename):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def _parse(self, journal_txt):
|
def _parse(self, journal_txt):
|
||||||
"""Parses a journal that's stored in a string and returns a list of entries"""
|
"""Parses a journal that's stored in a string and returns a list of entries"""
|
||||||
|
|
||||||
|
@ -118,14 +124,19 @@ class Journal(object):
|
||||||
# Initialise our current entry
|
# Initialise our current entry
|
||||||
entries = []
|
entries = []
|
||||||
|
|
||||||
date_blob_re = re.compile("(?:^|\n)\[([^\\]]+)\] ")
|
date_blob_re = re.compile("(?:^|\n)\\[([^\\]]+)\\] ")
|
||||||
last_entry_pos = 0
|
last_entry_pos = 0
|
||||||
for match in date_blob_re.finditer(journal_txt):
|
for match in date_blob_re.finditer(journal_txt):
|
||||||
date_blob = match.groups()[0]
|
date_blob = match.groups()[0]
|
||||||
new_date = time.parse(date_blob)
|
try:
|
||||||
|
new_date = datetime.strptime(date_blob, self.config["timeformat"])
|
||||||
|
except ValueError:
|
||||||
|
# Passing in a date that had brackets around it
|
||||||
|
new_date = time.parse(date_blob, bracketed=True)
|
||||||
|
|
||||||
if new_date:
|
if new_date:
|
||||||
if entries:
|
if entries:
|
||||||
entries[-1].text = journal_txt[last_entry_pos:match.start()]
|
entries[-1].text = journal_txt[last_entry_pos : match.start()]
|
||||||
last_entry_pos = match.end()
|
last_entry_pos = match.end()
|
||||||
entries.append(Entry.Entry(self, date=new_date))
|
entries.append(Entry.Entry(self, date=new_date))
|
||||||
|
|
||||||
|
@ -140,30 +151,28 @@ class Journal(object):
|
||||||
entry._parse_text()
|
entry._parse_text()
|
||||||
return entries
|
return entries
|
||||||
|
|
||||||
def __unicode__(self):
|
|
||||||
return self.pprint()
|
|
||||||
|
|
||||||
def pprint(self, short=False):
|
def pprint(self, short=False):
|
||||||
"""Prettyprints the journal's entries"""
|
"""Prettyprints the journal's entries"""
|
||||||
sep = "\n"
|
sep = "\n"
|
||||||
pp = sep.join([e.pprint(short=short) for e in self.entries])
|
pp = sep.join([e.pprint(short=short) for e in self.entries])
|
||||||
if self.config['highlight']: # highlight tags
|
if self.config["highlight"]: # highlight tags
|
||||||
if self.search_tags:
|
if self.search_tags:
|
||||||
for tag in self.search_tags:
|
for tag in self.search_tags:
|
||||||
tagre = re.compile(re.escape(tag), re.IGNORECASE)
|
tagre = re.compile(re.escape(tag), re.IGNORECASE)
|
||||||
pp = re.sub(tagre,
|
pp = re.sub(tagre, lambda match: util.colorize(match.group(0)), pp)
|
||||||
lambda match: util.colorize(match.group(0)),
|
|
||||||
pp, re.UNICODE)
|
|
||||||
else:
|
else:
|
||||||
pp = re.sub(
|
pp = re.sub(
|
||||||
Entry.Entry.tag_regex(self.config['tagsymbols']),
|
Entry.Entry.tag_regex(self.config["tagsymbols"]),
|
||||||
lambda match: util.colorize(match.group(0)),
|
lambda match: util.colorize(match.group(0)),
|
||||||
pp
|
pp,
|
||||||
)
|
)
|
||||||
return pp
|
return pp
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.pprint()
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<Journal with {0} entries>".format(len(self.entries))
|
return f"<Journal with {len(self.entries)} entries>"
|
||||||
|
|
||||||
def sort(self):
|
def sort(self):
|
||||||
"""Sorts the Journal's entries by date"""
|
"""Sorts the Journal's entries by date"""
|
||||||
|
@ -179,14 +188,22 @@ class Journal(object):
|
||||||
"""Returns a set of tuples (count, tag) for all tags present in the journal."""
|
"""Returns a set of tuples (count, tag) for all tags present in the journal."""
|
||||||
# Astute reader: should the following line leave you as puzzled as me the first time
|
# Astute reader: should the following line leave you as puzzled as me the first time
|
||||||
# I came across this construction, worry not and embrace the ensuing moment of enlightment.
|
# I came across this construction, worry not and embrace the ensuing moment of enlightment.
|
||||||
tags = [tag
|
tags = [tag for entry in self.entries for tag in set(entry.tags)]
|
||||||
for entry in self.entries
|
|
||||||
for tag in set(entry.tags)]
|
|
||||||
# To be read: [for entry in journal.entries: for tag in set(entry.tags): tag]
|
# To be read: [for entry in journal.entries: for tag in set(entry.tags): tag]
|
||||||
tag_counts = set([(tags.count(tag), tag) for tag in tags])
|
tag_counts = {(tags.count(tag), tag) for tag in tags}
|
||||||
return [Tag(tag, count=count) for count, tag in sorted(tag_counts)]
|
return [Tag(tag, count=count) for count, tag in sorted(tag_counts)]
|
||||||
|
|
||||||
def filter(self, tags=[], start_date=None, end_date=None, starred=False, strict=False, short=False, exclude=[]):
|
def filter(
|
||||||
|
self,
|
||||||
|
tags=[],
|
||||||
|
start_date=None,
|
||||||
|
end_date=None,
|
||||||
|
starred=False,
|
||||||
|
strict=False,
|
||||||
|
short=False,
|
||||||
|
contains=None,
|
||||||
|
exclude=[],
|
||||||
|
):
|
||||||
"""Removes all entries from the journal that don't match the filter.
|
"""Removes all entries from the journal that don't match the filter.
|
||||||
|
|
||||||
tags is a list of tags, each being a string that starts with one of the
|
tags is a list of tags, each being a string that starts with one of the
|
||||||
|
@ -200,21 +217,32 @@ class Journal(object):
|
||||||
|
|
||||||
exclude is a list of the tags which should not appear in the results.
|
exclude is a list of the tags which should not appear in the results.
|
||||||
entry is kept if any tag is present, unless they appear in exclude."""
|
entry is kept if any tag is present, unless they appear in exclude."""
|
||||||
self.search_tags = set([tag.lower() for tag in tags])
|
self.search_tags = {tag.lower() for tag in tags}
|
||||||
excluded_tags = set([tag.lower() for tag in exclude])
|
excluded_tags = {tag.lower() for tag in exclude}
|
||||||
end_date = time.parse(end_date, inclusive=True)
|
end_date = time.parse(end_date, inclusive=True)
|
||||||
start_date = time.parse(start_date)
|
start_date = time.parse(start_date)
|
||||||
|
|
||||||
# If strict mode is on, all tags have to be present in entry
|
# If strict mode is on, all tags have to be present in entry
|
||||||
tagged = self.search_tags.issubset if strict else self.search_tags.intersection
|
tagged = self.search_tags.issubset if strict else self.search_tags.intersection
|
||||||
excluded = lambda tags: len([tag for tag in tags if tag in excluded_tags]) > 0
|
excluded = lambda tags: len([tag for tag in tags if tag in excluded_tags]) > 0
|
||||||
|
if contains:
|
||||||
|
contains_lower = contains.casefold()
|
||||||
|
|
||||||
result = [
|
result = [
|
||||||
entry for entry in self.entries
|
entry
|
||||||
|
for entry in self.entries
|
||||||
if (not tags or tagged(entry.tags))
|
if (not tags or tagged(entry.tags))
|
||||||
and (not starred or entry.starred)
|
and (not starred or entry.starred)
|
||||||
and (not start_date or entry.date >= start_date)
|
and (not start_date or entry.date >= start_date)
|
||||||
and (not end_date or entry.date <= end_date)
|
and (not end_date or entry.date <= end_date)
|
||||||
and (not exclude or not excluded(entry.tags))
|
and (not exclude or not excluded(entry.tags))
|
||||||
|
and (
|
||||||
|
not contains
|
||||||
|
or (
|
||||||
|
contains_lower in entry.title.casefold()
|
||||||
|
or contains_lower in entry.body.casefold()
|
||||||
|
)
|
||||||
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
self.entries = result
|
self.entries = result
|
||||||
|
@ -223,11 +251,11 @@ class Journal(object):
|
||||||
"""Constructs a new entry from some raw text input.
|
"""Constructs a new entry from some raw text input.
|
||||||
If a date is given, it will parse and use this, otherwise scan for a date in the input first."""
|
If a date is given, it will parse and use this, otherwise scan for a date in the input first."""
|
||||||
|
|
||||||
raw = raw.replace('\\n ', '\n').replace('\\n', '\n')
|
raw = raw.replace("\\n ", "\n").replace("\\n", "\n")
|
||||||
starred = False
|
starred = False
|
||||||
# Split raw text into title and body
|
# Split raw text into title and body
|
||||||
sep = re.search("\n|[\?!.]+ +\n?", raw)
|
sep = re.search(r"\n|[?!.]+ +\n?", raw)
|
||||||
first_line = raw[:sep.end()].strip() if sep else raw
|
first_line = raw[: sep.end()].strip() if sep else raw
|
||||||
starred = False
|
starred = False
|
||||||
|
|
||||||
if not date:
|
if not date:
|
||||||
|
@ -235,12 +263,12 @@ class Journal(object):
|
||||||
if colon_pos > 0:
|
if colon_pos > 0:
|
||||||
date = time.parse(
|
date = time.parse(
|
||||||
raw[:colon_pos],
|
raw[:colon_pos],
|
||||||
default_hour=self.config['default_hour'],
|
default_hour=self.config["default_hour"],
|
||||||
default_minute=self.config['default_minute']
|
default_minute=self.config["default_minute"],
|
||||||
)
|
)
|
||||||
if date: # Parsed successfully, strip that from the raw text
|
if date: # Parsed successfully, strip that from the raw text
|
||||||
starred = raw[:colon_pos].strip().endswith("*")
|
starred = raw[:colon_pos].strip().endswith("*")
|
||||||
raw = raw[colon_pos + 1:].strip()
|
raw = raw[colon_pos + 1 :].strip()
|
||||||
starred = starred or first_line.startswith("*") or first_line.endswith("*")
|
starred = starred or first_line.startswith("*") or first_line.endswith("*")
|
||||||
if not date: # Still nothing? Meh, just live in the moment.
|
if not date: # Still nothing? Meh, just live in the moment.
|
||||||
date = time.parse("now")
|
date = time.parse("now")
|
||||||
|
@ -254,7 +282,7 @@ class Journal(object):
|
||||||
def editable_str(self):
|
def editable_str(self):
|
||||||
"""Turns the journal into a string of entries that can be edited
|
"""Turns the journal into a string of entries that can be edited
|
||||||
manually and later be parsed with eslf.parse_editable_str."""
|
manually and later be parsed with eslf.parse_editable_str."""
|
||||||
return "\n".join([e.__unicode__() for e in self.entries])
|
return "\n".join([str(e) for e in self.entries])
|
||||||
|
|
||||||
def parse_editable_str(self, edited):
|
def parse_editable_str(self, edited):
|
||||||
"""Parses the output of self.editable_str and updates it's entries."""
|
"""Parses the output of self.editable_str and updates it's entries."""
|
||||||
|
@ -268,17 +296,12 @@ class Journal(object):
|
||||||
|
|
||||||
|
|
||||||
class PlainJournal(Journal):
|
class PlainJournal(Journal):
|
||||||
@classmethod
|
|
||||||
def _create(cls, filename):
|
|
||||||
with codecs.open(filename, "a", "utf-8"):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _load(self, filename):
|
def _load(self, filename):
|
||||||
with codecs.open(filename, "r", "utf-8") as f:
|
with open(filename, "r", encoding="utf-8") as f:
|
||||||
return f.read()
|
return f.read()
|
||||||
|
|
||||||
def _store(self, filename, text):
|
def _store(self, filename, text):
|
||||||
with codecs.open(filename, 'w', "utf-8") as f:
|
with open(filename, "w", encoding="utf-8") as f:
|
||||||
f.write(text)
|
f.write(text)
|
||||||
|
|
||||||
|
|
||||||
|
@ -286,25 +309,28 @@ class LegacyJournal(Journal):
|
||||||
"""Legacy class to support opening journals formatted with the jrnl 1.x
|
"""Legacy class to support opening journals formatted with the jrnl 1.x
|
||||||
standard. Main difference here is that in 1.x, timestamps were not cuddled
|
standard. Main difference here is that in 1.x, timestamps were not cuddled
|
||||||
by square brackets. You'll not be able to save these journals anymore."""
|
by square brackets. You'll not be able to save these journals anymore."""
|
||||||
|
|
||||||
def _load(self, filename):
|
def _load(self, filename):
|
||||||
with codecs.open(filename, "r", "utf-8") as f:
|
with open(filename, "r", encoding="utf-8") as f:
|
||||||
return f.read()
|
return f.read()
|
||||||
|
|
||||||
def _parse(self, journal_txt):
|
def _parse(self, journal_txt):
|
||||||
"""Parses a journal that's stored in a string and returns a list of entries"""
|
"""Parses a journal that's stored in a string and returns a list of entries"""
|
||||||
# Entries start with a line that looks like 'date title' - let's figure out how
|
# Entries start with a line that looks like 'date title' - let's figure out how
|
||||||
# long the date will be by constructing one
|
# long the date will be by constructing one
|
||||||
date_length = len(datetime.today().strftime(self.config['timeformat']))
|
date_length = len(datetime.today().strftime(self.config["timeformat"]))
|
||||||
|
|
||||||
# Initialise our current entry
|
# Initialise our current entry
|
||||||
entries = []
|
entries = []
|
||||||
current_entry = None
|
current_entry = None
|
||||||
new_date_format_regex = re.compile(r'(^\[[^\]]+\].*?$)')
|
new_date_format_regex = re.compile(r"(^\[[^\]]+\].*?$)")
|
||||||
for line in journal_txt.splitlines():
|
for line in journal_txt.splitlines():
|
||||||
line = line.rstrip()
|
line = line.rstrip()
|
||||||
try:
|
try:
|
||||||
# try to parse line as date => new entry begins
|
# try to parse line as date => new entry begins
|
||||||
new_date = datetime.strptime(line[:date_length], self.config['timeformat'])
|
new_date = datetime.strptime(
|
||||||
|
line[:date_length], self.config["timeformat"]
|
||||||
|
)
|
||||||
|
|
||||||
# parsing successful => save old entry and create new one
|
# parsing successful => save old entry and create new one
|
||||||
if new_date and current_entry:
|
if new_date and current_entry:
|
||||||
|
@ -316,14 +342,16 @@ class LegacyJournal(Journal):
|
||||||
else:
|
else:
|
||||||
starred = False
|
starred = False
|
||||||
|
|
||||||
current_entry = Entry.Entry(self, date=new_date, text=line[date_length + 1:], starred=starred)
|
current_entry = Entry.Entry(
|
||||||
|
self, date=new_date, text=line[date_length + 1 :], starred=starred
|
||||||
|
)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
# Happens when we can't parse the start of the line as an date.
|
# Happens when we can't parse the start of the line as an date.
|
||||||
# In this case, just append line to our body (after some
|
# In this case, just append line to our body (after some
|
||||||
# escaping for the new format).
|
# escaping for the new format).
|
||||||
line = new_date_format_regex.sub(r' \1', line)
|
line = new_date_format_regex.sub(r" \1", line)
|
||||||
if current_entry:
|
if current_entry:
|
||||||
current_entry.text += line + u"\n"
|
current_entry.text += line + "\n"
|
||||||
|
|
||||||
# Append last entry
|
# Append last entry
|
||||||
if current_entry:
|
if current_entry:
|
||||||
|
@ -340,22 +368,26 @@ def open_journal(name, config, legacy=False):
|
||||||
backwards compatibility with jrnl 1.x
|
backwards compatibility with jrnl 1.x
|
||||||
"""
|
"""
|
||||||
config = config.copy()
|
config = config.copy()
|
||||||
config['journal'] = os.path.expanduser(os.path.expandvars(config['journal']))
|
config["journal"] = os.path.expanduser(os.path.expandvars(config["journal"]))
|
||||||
|
|
||||||
if os.path.isdir(config['journal']):
|
if os.path.isdir(config["journal"]):
|
||||||
if config['journal'].strip("/").endswith(".dayone") or "entries" in os.listdir(config['journal']):
|
if config["journal"].strip("/").endswith(".dayone") or "entries" in os.listdir(
|
||||||
|
config["journal"]
|
||||||
|
):
|
||||||
from . import DayOneJournal
|
from . import DayOneJournal
|
||||||
|
|
||||||
return DayOneJournal.DayOne(**config).open()
|
return DayOneJournal.DayOne(**config).open()
|
||||||
else:
|
else:
|
||||||
from . import FolderJournal
|
from . import FolderJournal
|
||||||
return FolderJournal.Folder(**config).open()
|
return FolderJournal.Folder(**config).open()
|
||||||
|
|
||||||
if not config['encrypt']:
|
if not config["encrypt"]:
|
||||||
if legacy:
|
if legacy:
|
||||||
return LegacyJournal(name, **config).open()
|
return LegacyJournal(name, **config).open()
|
||||||
return PlainJournal(name, **config).open()
|
return PlainJournal(name, **config).open()
|
||||||
else:
|
else:
|
||||||
from . import EncryptedJournal
|
from . import EncryptedJournal
|
||||||
|
|
||||||
if legacy:
|
if legacy:
|
||||||
return EncryptedJournal.LegacyEncryptedJournal(name, **config).open()
|
return EncryptedJournal.LegacyEncryptedJournal(name, **config).open()
|
||||||
return EncryptedJournal.EncryptedJournal(name, **config).open()
|
return EncryptedJournal.EncryptedJournal(name, **config).open()
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# encoding: utf-8
|
|
||||||
|
|
||||||
import pkg_resources
|
import os
|
||||||
|
|
||||||
dist = pkg_resources.get_distribution('jrnl')
|
|
||||||
__title__ = dist.project_name
|
|
||||||
__version__ = dist.version
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
from .__version__ import __version__
|
||||||
|
except ImportError:
|
||||||
|
__version__ = "source"
|
||||||
|
__title__ = "jrnl"
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# encoding: utf-8
|
|
||||||
from __future__ import absolute_import, unicode_literals
|
|
||||||
from . import cli
|
from . import cli
|
||||||
|
|
||||||
|
|
||||||
|
|
1
jrnl/__version__.py
Normal file
1
jrnl/__version__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
__version__ = "v2.2-beta"
|
357
jrnl/cli.py
357
jrnl/cli.py
|
@ -1,5 +1,4 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# encoding: utf-8
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
jrnl
|
jrnl
|
||||||
|
@ -7,9 +6,8 @@
|
||||||
license: MIT, see LICENSE for more details.
|
license: MIT, see LICENSE for more details.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
from .Journal import PlainJournal, open_journal
|
||||||
from __future__ import absolute_import
|
from .EncryptedJournal import EncryptedJournal
|
||||||
from . import Journal
|
|
||||||
from . import util
|
from . import util
|
||||||
from . import install
|
from . import install
|
||||||
from . import plugins
|
from . import plugins
|
||||||
|
@ -25,32 +23,155 @@ logging.getLogger("keyring.backend").setLevel(logging.ERROR)
|
||||||
|
|
||||||
def parse_args(args=None):
|
def parse_args(args=None):
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument('-v', '--version', dest='version', action="store_true", help="prints version information and exits")
|
parser.add_argument(
|
||||||
parser.add_argument('-ls', dest='ls', action="store_true", help="displays accessible journals")
|
"-v",
|
||||||
parser.add_argument('-d', '--debug', dest='debug', action='store_true', help='execute in debug mode')
|
"--version",
|
||||||
|
dest="version",
|
||||||
|
action="store_true",
|
||||||
|
help="prints version information and exits",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-ls", dest="ls", action="store_true", help="displays accessible journals"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-d", "--debug", dest="debug", action="store_true", help="execute in debug mode"
|
||||||
|
)
|
||||||
|
|
||||||
composing = parser.add_argument_group('Composing', 'To write an entry simply write it on the command line, e.g. "jrnl yesterday at 1pm: Went to the gym."')
|
composing = parser.add_argument_group(
|
||||||
composing.add_argument('text', metavar='', nargs="*")
|
"Composing",
|
||||||
|
'To write an entry simply write it on the command line, e.g. "jrnl yesterday at 1pm: Went to the gym."',
|
||||||
|
)
|
||||||
|
composing.add_argument("text", metavar="", nargs="*")
|
||||||
|
|
||||||
reading = parser.add_argument_group('Reading', 'Specifying either of these parameters will display posts of your journal')
|
reading = parser.add_argument_group(
|
||||||
reading.add_argument('-from', dest='start_date', metavar="DATE", help='View entries after this date')
|
"Reading",
|
||||||
reading.add_argument('-until', '-to', dest='end_date', metavar="DATE", help='View entries before this date')
|
"Specifying either of these parameters will display posts of your journal",
|
||||||
reading.add_argument('-on', dest='on_date', metavar="DATE", help='View entries on this date')
|
)
|
||||||
reading.add_argument('-and', dest='strict', action="store_true", help='Filter by tags using AND (default: OR)')
|
reading.add_argument(
|
||||||
reading.add_argument('-starred', dest='starred', action="store_true", help='Show only starred entries')
|
"-from", dest="start_date", metavar="DATE", help="View entries after this date"
|
||||||
reading.add_argument('-n', dest='limit', default=None, metavar="N", help="Shows the last n entries matching the filter. '-n 3' and '-3' have the same effect.", nargs="?", type=int)
|
)
|
||||||
reading.add_argument('-not', dest='excluded', nargs='+', default=[], metavar="E", help="Exclude entries with these tags")
|
reading.add_argument(
|
||||||
|
"-until",
|
||||||
|
"-to",
|
||||||
|
dest="end_date",
|
||||||
|
metavar="DATE",
|
||||||
|
help="View entries before this date",
|
||||||
|
)
|
||||||
|
reading.add_argument(
|
||||||
|
"-contains", dest="contains", help="View entries containing a specific string"
|
||||||
|
)
|
||||||
|
reading.add_argument(
|
||||||
|
"-on", dest="on_date", metavar="DATE", help="View entries on this date"
|
||||||
|
)
|
||||||
|
reading.add_argument(
|
||||||
|
"-and",
|
||||||
|
dest="strict",
|
||||||
|
action="store_true",
|
||||||
|
help="Filter by tags using AND (default: OR)",
|
||||||
|
)
|
||||||
|
reading.add_argument(
|
||||||
|
"-starred",
|
||||||
|
dest="starred",
|
||||||
|
action="store_true",
|
||||||
|
help="Show only starred entries",
|
||||||
|
)
|
||||||
|
reading.add_argument(
|
||||||
|
"-n",
|
||||||
|
dest="limit",
|
||||||
|
default=None,
|
||||||
|
metavar="N",
|
||||||
|
help="Shows the last n entries matching the filter. '-n 3' and '-3' have the same effect.",
|
||||||
|
nargs="?",
|
||||||
|
type=int,
|
||||||
|
)
|
||||||
|
reading.add_argument(
|
||||||
|
"-not",
|
||||||
|
dest="excluded",
|
||||||
|
nargs="+",
|
||||||
|
default=[],
|
||||||
|
metavar="E",
|
||||||
|
help="Exclude entries with these tags",
|
||||||
|
)
|
||||||
|
|
||||||
exporting = parser.add_argument_group('Export / Import', 'Options for transmogrifying your journal')
|
exporting = parser.add_argument_group(
|
||||||
exporting.add_argument('-s', '--short', dest='short', action="store_true", help='Show only titles or line containing the search tags')
|
"Export / Import", "Options for transmogrifying your journal"
|
||||||
exporting.add_argument('--tags', dest='tags', action="store_true", help='Returns a list of all tags and number of occurences')
|
)
|
||||||
exporting.add_argument('--export', metavar='TYPE', dest='export', choices=plugins.EXPORT_FORMATS, help='Export your journal. TYPE can be {}.'.format(plugins.util.oxford_list(plugins.EXPORT_FORMATS)), default=False, const=None)
|
exporting.add_argument(
|
||||||
exporting.add_argument('-o', metavar='OUTPUT', dest='output', help='Optionally specifies output file when using --export. If OUTPUT is a directory, exports each entry into an individual file instead.', default=False, const=None)
|
"-s",
|
||||||
exporting.add_argument('--import', metavar='TYPE', dest='import_', choices=plugins.IMPORT_FORMATS, help='Import entries into your journal. TYPE can be {}, and it defaults to jrnl if nothing else is specified.'.format(plugins.util.oxford_list(plugins.IMPORT_FORMATS)), default=False, const='jrnl', nargs='?')
|
"--short",
|
||||||
exporting.add_argument('-i', metavar='INPUT', dest='input', help='Optionally specifies input file when using --import.', default=False, const=None)
|
dest="short",
|
||||||
exporting.add_argument('--encrypt', metavar='FILENAME', dest='encrypt', help='Encrypts your existing journal with a new password', nargs='?', default=False, const=None)
|
action="store_true",
|
||||||
exporting.add_argument('--decrypt', metavar='FILENAME', dest='decrypt', help='Decrypts your journal and stores it in plain text', nargs='?', default=False, const=None)
|
help="Show only titles or line containing the search tags",
|
||||||
exporting.add_argument('--edit', dest='edit', help='Opens your editor to edit the selected entries.', action="store_true")
|
)
|
||||||
|
exporting.add_argument(
|
||||||
|
"--tags",
|
||||||
|
dest="tags",
|
||||||
|
action="store_true",
|
||||||
|
help="Returns a list of all tags and number of occurences",
|
||||||
|
)
|
||||||
|
exporting.add_argument(
|
||||||
|
"--export",
|
||||||
|
metavar="TYPE",
|
||||||
|
dest="export",
|
||||||
|
choices=plugins.EXPORT_FORMATS,
|
||||||
|
help="Export your journal. TYPE can be {}.".format(
|
||||||
|
plugins.util.oxford_list(plugins.EXPORT_FORMATS)
|
||||||
|
),
|
||||||
|
default=False,
|
||||||
|
const=None,
|
||||||
|
)
|
||||||
|
exporting.add_argument(
|
||||||
|
"-o",
|
||||||
|
metavar="OUTPUT",
|
||||||
|
dest="output",
|
||||||
|
help="Optionally specifies output file when using --export. If OUTPUT is a directory, exports each entry into an individual file instead.",
|
||||||
|
default=False,
|
||||||
|
const=None,
|
||||||
|
)
|
||||||
|
exporting.add_argument(
|
||||||
|
"--import",
|
||||||
|
metavar="TYPE",
|
||||||
|
dest="import_",
|
||||||
|
choices=plugins.IMPORT_FORMATS,
|
||||||
|
help="Import entries into your journal. TYPE can be {}, and it defaults to jrnl if nothing else is specified.".format(
|
||||||
|
plugins.util.oxford_list(plugins.IMPORT_FORMATS)
|
||||||
|
),
|
||||||
|
default=False,
|
||||||
|
const="jrnl",
|
||||||
|
nargs="?",
|
||||||
|
)
|
||||||
|
exporting.add_argument(
|
||||||
|
"-i",
|
||||||
|
metavar="INPUT",
|
||||||
|
dest="input",
|
||||||
|
help="Optionally specifies input file when using --import.",
|
||||||
|
default=False,
|
||||||
|
const=None,
|
||||||
|
)
|
||||||
|
exporting.add_argument(
|
||||||
|
"--encrypt",
|
||||||
|
metavar="FILENAME",
|
||||||
|
dest="encrypt",
|
||||||
|
help="Encrypts your existing journal with a new password",
|
||||||
|
nargs="?",
|
||||||
|
default=False,
|
||||||
|
const=None,
|
||||||
|
)
|
||||||
|
exporting.add_argument(
|
||||||
|
"--decrypt",
|
||||||
|
metavar="FILENAME",
|
||||||
|
dest="decrypt",
|
||||||
|
help="Decrypts your journal and stores it in plain text",
|
||||||
|
nargs="?",
|
||||||
|
default=False,
|
||||||
|
const=None,
|
||||||
|
)
|
||||||
|
exporting.add_argument(
|
||||||
|
"--edit",
|
||||||
|
dest="edit",
|
||||||
|
help="Opens your editor to edit the selected entries.",
|
||||||
|
action="store_true",
|
||||||
|
)
|
||||||
|
|
||||||
return parser.parse_args(args)
|
return parser.parse_args(args)
|
||||||
|
|
||||||
|
@ -64,13 +185,30 @@ def guess_mode(args, config):
|
||||||
compose = False
|
compose = False
|
||||||
export = False
|
export = False
|
||||||
import_ = True
|
import_ = True
|
||||||
elif args.decrypt is not False or args.encrypt is not False or args.export is not False or any((args.short, args.tags, args.edit)):
|
elif (
|
||||||
|
args.decrypt is not False
|
||||||
|
or args.encrypt is not False
|
||||||
|
or args.export is not False
|
||||||
|
or any((args.short, args.tags, args.edit))
|
||||||
|
):
|
||||||
compose = False
|
compose = False
|
||||||
export = True
|
export = True
|
||||||
elif any((args.start_date, args.end_date, args.on_date, args.limit, args.strict, args.starred)):
|
elif any(
|
||||||
|
(
|
||||||
|
args.start_date,
|
||||||
|
args.end_date,
|
||||||
|
args.on_date,
|
||||||
|
args.limit,
|
||||||
|
args.strict,
|
||||||
|
args.starred,
|
||||||
|
args.contains,
|
||||||
|
)
|
||||||
|
):
|
||||||
# Any sign of displaying stuff?
|
# Any sign of displaying stuff?
|
||||||
compose = False
|
compose = False
|
||||||
elif args.text and all(word[0] in config['tagsymbols'] for word in " ".join(args.text).split()):
|
elif args.text and all(
|
||||||
|
word[0] in config["tagsymbols"] for word in " ".join(args.text).split()
|
||||||
|
):
|
||||||
# No date and only tags?
|
# No date and only tags?
|
||||||
compose = False
|
compose = False
|
||||||
|
|
||||||
|
@ -79,38 +217,37 @@ def guess_mode(args, config):
|
||||||
|
|
||||||
def encrypt(journal, filename=None):
|
def encrypt(journal, filename=None):
|
||||||
""" Encrypt into new file. If filename is not set, we encrypt the journal file itself. """
|
""" Encrypt into new file. If filename is not set, we encrypt the journal file itself. """
|
||||||
from . import EncryptedJournal
|
journal.config["encrypt"] = True
|
||||||
|
|
||||||
journal.config['password'] = util.getpass("Enter new password: ")
|
new_journal = EncryptedJournal.from_journal(journal)
|
||||||
journal.config['encrypt'] = True
|
|
||||||
|
|
||||||
new_journal = EncryptedJournal.EncryptedJournal(None, **journal.config)
|
|
||||||
new_journal.entries = journal.entries
|
|
||||||
new_journal.write(filename)
|
new_journal.write(filename)
|
||||||
|
|
||||||
if util.yesno("Do you want to store the password in your keychain?", default=True):
|
print(
|
||||||
util.set_keychain(journal.name, journal.config['password'])
|
"Journal encrypted to {}.".format(filename or new_journal.config["journal"]),
|
||||||
|
file=sys.stderr,
|
||||||
util.prompt("Journal encrypted to {0}.".format(filename or new_journal.config['journal']))
|
)
|
||||||
|
|
||||||
|
|
||||||
def decrypt(journal, filename=None):
|
def decrypt(journal, filename=None):
|
||||||
""" Decrypts into new file. If filename is not set, we encrypt the journal file itself. """
|
""" Decrypts into new file. If filename is not set, we encrypt the journal file itself. """
|
||||||
journal.config['encrypt'] = False
|
journal.config["encrypt"] = False
|
||||||
journal.config['password'] = ""
|
|
||||||
|
|
||||||
new_journal = Journal.PlainJournal(filename, **journal.config)
|
new_journal = PlainJournal.from_journal(journal)
|
||||||
new_journal.entries = journal.entries
|
|
||||||
new_journal.write(filename)
|
new_journal.write(filename)
|
||||||
util.prompt("Journal decrypted to {0}.".format(filename or new_journal.config['journal']))
|
print(
|
||||||
|
"Journal decrypted to {}.".format(filename or new_journal.config["journal"]),
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def list_journals(config):
|
def list_journals(config):
|
||||||
"""List the journals specified in the configuration file"""
|
"""List the journals specified in the configuration file"""
|
||||||
result = "Journals defined in {}\n".format(install.CONFIG_FILE_PATH)
|
result = f"Journals defined in {install.CONFIG_FILE_PATH}\n"
|
||||||
ml = min(max(len(k) for k in config['journals']), 20)
|
ml = min(max(len(k) for k in config["journals"]), 20)
|
||||||
for journal, cfg in config['journals'].items():
|
for journal, cfg in config["journals"].items():
|
||||||
result += " * {:{}} -> {}\n".format(journal, ml, cfg['journal'] if isinstance(cfg, dict) else cfg)
|
result += " * {:{}} -> {}\n".format(
|
||||||
|
journal, ml, cfg["journal"] if isinstance(cfg, dict) else cfg
|
||||||
|
)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@ -118,11 +255,11 @@ def update_config(config, new_config, scope, force_local=False):
|
||||||
"""Updates a config dict with new values - either global if scope is None
|
"""Updates a config dict with new values - either global if scope is None
|
||||||
or config['journals'][scope] is just a string pointing to a journal file,
|
or config['journals'][scope] is just a string pointing to a journal file,
|
||||||
or within the scope"""
|
or within the scope"""
|
||||||
if scope and type(config['journals'][scope]) is dict: # Update to journal specific
|
if scope and type(config["journals"][scope]) is dict: # Update to journal specific
|
||||||
config['journals'][scope].update(new_config)
|
config["journals"][scope].update(new_config)
|
||||||
elif scope and force_local: # Convert to dict
|
elif scope and force_local: # Convert to dict
|
||||||
config['journals'][scope] = {"journal": config['journals'][scope]}
|
config["journals"][scope] = {"journal": config["journals"][scope]}
|
||||||
config['journals'][scope].update(new_config)
|
config["journals"][scope].update(new_config)
|
||||||
else:
|
else:
|
||||||
config.update(new_config)
|
config.update(new_config)
|
||||||
|
|
||||||
|
@ -130,28 +267,29 @@ def update_config(config, new_config, scope, force_local=False):
|
||||||
def configure_logger(debug=False):
|
def configure_logger(debug=False):
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.DEBUG if debug else logging.INFO,
|
level=logging.DEBUG if debug else logging.INFO,
|
||||||
format='%(levelname)-8s %(name)-12s %(message)s'
|
format="%(levelname)-8s %(name)-12s %(message)s",
|
||||||
)
|
)
|
||||||
logging.getLogger('parsedatetime').setLevel(logging.INFO) # disable parsedatetime debug logging
|
logging.getLogger("parsedatetime").setLevel(
|
||||||
|
logging.INFO
|
||||||
|
) # disable parsedatetime debug logging
|
||||||
|
|
||||||
|
|
||||||
def run(manual_args=None):
|
def run(manual_args=None):
|
||||||
args = parse_args(manual_args)
|
args = parse_args(manual_args)
|
||||||
configure_logger(args.debug)
|
configure_logger(args.debug)
|
||||||
args.text = [p.decode('utf-8') if util.PY2 and not isinstance(p, unicode) else p for p in args.text]
|
|
||||||
if args.version:
|
if args.version:
|
||||||
version_str = "{0} version {1}".format(jrnl.__title__, jrnl.__version__)
|
version_str = f"{jrnl.__title__} version {jrnl.__version__}"
|
||||||
print(util.py2encode(version_str))
|
print(version_str)
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
config = install.load_or_install_jrnl()
|
config = install.load_or_install_jrnl()
|
||||||
except UserAbort as err:
|
except UserAbort as err:
|
||||||
util.prompt("\n{}".format(err))
|
print(f"\n{err}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
if args.ls:
|
if args.ls:
|
||||||
util.prnt(list_journals(config))
|
print(list_journals(config))
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
log.debug('Using configuration "%s"', config)
|
log.debug('Using configuration "%s"', config)
|
||||||
|
@ -159,13 +297,14 @@ def run(manual_args=None):
|
||||||
|
|
||||||
# If the first textual argument points to a journal file,
|
# If the first textual argument points to a journal file,
|
||||||
# use this!
|
# use this!
|
||||||
journal_name = args.text[0] if (args.text and args.text[0] in config['journals']) else 'default'
|
|
||||||
|
|
||||||
if journal_name is not 'default':
|
journal_name = install.DEFAULT_JOURNAL_KEY
|
||||||
|
if args.text and args.text[0] in config["journals"]:
|
||||||
|
journal_name = args.text[0]
|
||||||
args.text = args.text[1:]
|
args.text = args.text[1:]
|
||||||
elif "default" not in config['journals']:
|
elif install.DEFAULT_JOURNAL_KEY not in config["journals"]:
|
||||||
util.prompt("No default journal configured.")
|
print("No default journal configured.", file=sys.stderr)
|
||||||
util.prompt(list_journals(config))
|
print(list_journals(config), file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
config = util.scope_config(config, journal_name)
|
config = util.scope_config(config, journal_name)
|
||||||
|
@ -175,7 +314,7 @@ def run(manual_args=None):
|
||||||
try:
|
try:
|
||||||
args.limit = int(args.text[0].lstrip("-"))
|
args.limit = int(args.text[0].lstrip("-"))
|
||||||
args.text = args.text[1:]
|
args.text = args.text[1:]
|
||||||
except:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
log.debug('Using journal "%s"', journal_name)
|
log.debug('Using journal "%s"', journal_name)
|
||||||
|
@ -190,32 +329,39 @@ def run(manual_args=None):
|
||||||
if mode_compose and not args.text:
|
if mode_compose and not args.text:
|
||||||
if not sys.stdin.isatty():
|
if not sys.stdin.isatty():
|
||||||
# Piping data into jrnl
|
# Piping data into jrnl
|
||||||
raw = util.py23_read()
|
raw = sys.stdin.read()
|
||||||
elif config['editor']:
|
elif config["editor"]:
|
||||||
template = ""
|
template = ""
|
||||||
if config['template']:
|
if config["template"]:
|
||||||
try:
|
try:
|
||||||
template = open(config['template']).read()
|
template = open(config["template"]).read()
|
||||||
except:
|
except OSError:
|
||||||
util.prompt("[Could not read template at '']".format(config['template']))
|
print(
|
||||||
|
f"[Could not read template at '{config['template']}']",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
raw = util.get_text_from_editor(config, template)
|
raw = util.get_text_from_editor(config, template)
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
raw = util.py23_read("[Compose Entry; " + _exit_multiline_code + " to finish writing]\n")
|
print(
|
||||||
|
"[Compose Entry; " + _exit_multiline_code + " to finish writing]\n",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
raw = sys.stdin.read()
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
util.prompt("[Entry NOT saved to journal.]")
|
print("[Entry NOT saved to journal.]", file=sys.stderr)
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
if raw:
|
if raw:
|
||||||
args.text = [raw]
|
args.text = [raw]
|
||||||
else:
|
else:
|
||||||
mode_compose = False
|
sys.exit()
|
||||||
|
|
||||||
# This is where we finally open the journal!
|
# This is where we finally open the journal!
|
||||||
try:
|
try:
|
||||||
journal = Journal.open_journal(journal_name, config)
|
journal = open_journal(journal_name, config)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
util.prompt("[Interrupted while opening journal]".format(journal_name))
|
print(f"[Interrupted while opening journal]", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# Import mode
|
# Import mode
|
||||||
|
@ -225,35 +371,37 @@ def run(manual_args=None):
|
||||||
# Writing mode
|
# Writing mode
|
||||||
elif mode_compose:
|
elif mode_compose:
|
||||||
raw = " ".join(args.text).strip()
|
raw = " ".join(args.text).strip()
|
||||||
if util.PY2 and type(raw) is not unicode:
|
|
||||||
raw = raw.decode(sys.getfilesystemencoding())
|
|
||||||
log.debug('Appending raw line "%s" to journal "%s"', raw, journal_name)
|
log.debug('Appending raw line "%s" to journal "%s"', raw, journal_name)
|
||||||
journal.new_entry(raw)
|
journal.new_entry(raw)
|
||||||
util.prompt("[Entry added to {0} journal]".format(journal_name))
|
print(f"[Entry added to {journal_name} journal]", file=sys.stderr)
|
||||||
journal.write()
|
journal.write()
|
||||||
|
|
||||||
if not mode_compose:
|
if not mode_compose:
|
||||||
old_entries = journal.entries
|
old_entries = journal.entries
|
||||||
if args.on_date:
|
if args.on_date:
|
||||||
args.start_date = args.end_date = args.on_date
|
args.start_date = args.end_date = args.on_date
|
||||||
journal.filter(tags=args.text,
|
journal.filter(
|
||||||
start_date=args.start_date, end_date=args.end_date,
|
tags=args.text,
|
||||||
strict=args.strict,
|
start_date=args.start_date,
|
||||||
short=args.short,
|
end_date=args.end_date,
|
||||||
starred=args.starred,
|
strict=args.strict,
|
||||||
exclude=args.excluded)
|
short=args.short,
|
||||||
|
starred=args.starred,
|
||||||
|
exclude=args.excluded,
|
||||||
|
contains=args.contains,
|
||||||
|
)
|
||||||
journal.limit(args.limit)
|
journal.limit(args.limit)
|
||||||
|
|
||||||
# Reading mode
|
# Reading mode
|
||||||
if not mode_compose and not mode_export and not mode_import:
|
if not mode_compose and not mode_export and not mode_import:
|
||||||
print(util.py2encode(journal.pprint()))
|
print(journal.pprint())
|
||||||
|
|
||||||
# Various export modes
|
# Various export modes
|
||||||
elif args.short:
|
elif args.short:
|
||||||
print(util.py2encode(journal.pprint(short=True)))
|
print(journal.pprint(short=True))
|
||||||
|
|
||||||
elif args.tags:
|
elif args.tags:
|
||||||
print(util.py2encode(plugins.get_exporter("tags").export(journal)))
|
print(plugins.get_exporter("tags").export(journal))
|
||||||
|
|
||||||
elif args.export is not False:
|
elif args.export is not False:
|
||||||
exporter = plugins.get_exporter(args.export)
|
exporter = plugins.get_exporter(args.export)
|
||||||
|
@ -263,19 +411,28 @@ def run(manual_args=None):
|
||||||
encrypt(journal, filename=args.encrypt)
|
encrypt(journal, filename=args.encrypt)
|
||||||
# Not encrypting to a separate file: update config!
|
# Not encrypting to a separate file: update config!
|
||||||
if not args.encrypt:
|
if not args.encrypt:
|
||||||
update_config(original_config, {"encrypt": True}, journal_name, force_local=True)
|
update_config(
|
||||||
|
original_config, {"encrypt": True}, journal_name, force_local=True
|
||||||
|
)
|
||||||
install.save_config(original_config)
|
install.save_config(original_config)
|
||||||
|
|
||||||
elif args.decrypt is not False:
|
elif args.decrypt is not False:
|
||||||
decrypt(journal, filename=args.decrypt)
|
decrypt(journal, filename=args.decrypt)
|
||||||
# Not decrypting to a separate file: update config!
|
# Not decrypting to a separate file: update config!
|
||||||
if not args.decrypt:
|
if not args.decrypt:
|
||||||
update_config(original_config, {"encrypt": False}, journal_name, force_local=True)
|
update_config(
|
||||||
|
original_config, {"encrypt": False}, journal_name, force_local=True
|
||||||
|
)
|
||||||
install.save_config(original_config)
|
install.save_config(original_config)
|
||||||
|
|
||||||
elif args.edit:
|
elif args.edit:
|
||||||
if not config['editor']:
|
if not config["editor"]:
|
||||||
util.prompt("[{1}ERROR{2}: You need to specify an editor in {0} to use the --edit function.]".format(install.CONFIG_FILE_PATH, ERROR_COLOR, RESET_COLOR))
|
print(
|
||||||
|
"[{1}ERROR{2}: You need to specify an editor in {0} to use the --edit function.]".format(
|
||||||
|
install.CONFIG_FILE_PATH, ERROR_COLOR, RESET_COLOR
|
||||||
|
),
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
other_entries = [e for e in old_entries if e not in journal.entries]
|
other_entries = [e for e in old_entries if e not in journal.entries]
|
||||||
# Edit
|
# Edit
|
||||||
|
@ -286,11 +443,19 @@ def run(manual_args=None):
|
||||||
num_edited = len([e for e in journal.entries if e.modified])
|
num_edited = len([e for e in journal.entries if e.modified])
|
||||||
prompts = []
|
prompts = []
|
||||||
if num_deleted:
|
if num_deleted:
|
||||||
prompts.append("{0} {1} deleted".format(num_deleted, "entry" if num_deleted == 1 else "entries"))
|
prompts.append(
|
||||||
|
"{} {} deleted".format(
|
||||||
|
num_deleted, "entry" if num_deleted == 1 else "entries"
|
||||||
|
)
|
||||||
|
)
|
||||||
if num_edited:
|
if num_edited:
|
||||||
prompts.append("{0} {1} modified".format(num_edited, "entry" if num_deleted == 1 else "entries"))
|
prompts.append(
|
||||||
|
"{} {} modified".format(
|
||||||
|
num_edited, "entry" if num_deleted == 1 else "entries"
|
||||||
|
)
|
||||||
|
)
|
||||||
if prompts:
|
if prompts:
|
||||||
util.prompt("[{0}]".format(", ".join(prompts).capitalize()))
|
print("[{}]".format(", ".join(prompts).capitalize()), file=sys.stderr)
|
||||||
journal.entries += other_entries
|
journal.entries += other_entries
|
||||||
journal.sort()
|
journal.sort()
|
||||||
journal.write()
|
journal.write()
|
||||||
|
|
|
@ -1,66 +0,0 @@
|
||||||
#!/usr/bin/env python
|
|
||||||
# encoding: utf-8
|
|
||||||
|
|
||||||
from __future__ import absolute_import, unicode_literals
|
|
||||||
from .util import ERROR_COLOR, RESET_COLOR
|
|
||||||
from .util import slugify, u
|
|
||||||
from .template import Template
|
|
||||||
import os
|
|
||||||
import codecs
|
|
||||||
|
|
||||||
|
|
||||||
class Exporter(object):
|
|
||||||
"""This Exporter can convert entries and journals into text files."""
|
|
||||||
def __init__(self, format):
|
|
||||||
with open("jrnl/templates/" + format + ".template") as f:
|
|
||||||
front_matter, body = f.read().strip("-\n").split("---", 2)
|
|
||||||
self.template = Template(body)
|
|
||||||
|
|
||||||
def export_entry(self, entry):
|
|
||||||
"""Returns a unicode representation of a single entry."""
|
|
||||||
return entry.__unicode__()
|
|
||||||
|
|
||||||
def _get_vars(self, journal):
|
|
||||||
return {
|
|
||||||
'journal': journal,
|
|
||||||
'entries': journal.entries,
|
|
||||||
'tags': journal.tags
|
|
||||||
}
|
|
||||||
|
|
||||||
def export_journal(self, journal):
|
|
||||||
"""Returns a unicode representation of an entire journal."""
|
|
||||||
return self.template.render_block("journal", **self._get_vars(journal))
|
|
||||||
|
|
||||||
def write_file(self, journal, path):
|
|
||||||
"""Exports a journal into a single file."""
|
|
||||||
try:
|
|
||||||
with codecs.open(path, "w", "utf-8") as f:
|
|
||||||
f.write(self.export_journal(journal))
|
|
||||||
return "[Journal exported to {0}]".format(path)
|
|
||||||
except IOError as e:
|
|
||||||
return "[{2}ERROR{3}: {0} {1}]".format(e.filename, e.strerror, ERROR_COLOR, RESET_COLOR)
|
|
||||||
|
|
||||||
def make_filename(self, entry):
|
|
||||||
return entry.date.strftime("%Y-%m-%d_{0}.{1}".format(slugify(u(entry.title)), self.extension))
|
|
||||||
|
|
||||||
def write_files(self, journal, path):
|
|
||||||
"""Exports a journal into individual files for each entry."""
|
|
||||||
for entry in journal.entries:
|
|
||||||
try:
|
|
||||||
full_path = os.path.join(path, self.make_filename(entry))
|
|
||||||
with codecs.open(full_path, "w", "utf-8") as f:
|
|
||||||
f.write(self.export_entry(entry))
|
|
||||||
except IOError as e:
|
|
||||||
return "[{2}ERROR{3}: {0} {1}]".format(e.filename, e.strerror, ERROR_COLOR, RESET_COLOR)
|
|
||||||
return "[Journal exported to {0}]".format(path)
|
|
||||||
|
|
||||||
def export(self, journal, format="text", output=None):
|
|
||||||
"""Exports to individual files if output is an existing path, or into
|
|
||||||
a single file if output is a file name, or returns the exporter's
|
|
||||||
representation as unicode if output is None."""
|
|
||||||
if output and os.path.isdir(output): # multiple files
|
|
||||||
return self.write_files(journal, output)
|
|
||||||
elif output: # single file
|
|
||||||
return self.write_file(journal, output)
|
|
||||||
else:
|
|
||||||
return self.export_journal(journal)
|
|
135
jrnl/install.py
135
jrnl/install.py
|
@ -1,8 +1,5 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# encoding: utf-8
|
|
||||||
|
|
||||||
from __future__ import absolute_import
|
|
||||||
import readline
|
|
||||||
import glob
|
import glob
|
||||||
import getpass
|
import getpass
|
||||||
import os
|
import os
|
||||||
|
@ -17,11 +14,16 @@ import yaml
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
DEFAULT_CONFIG_NAME = 'jrnl.yaml'
|
if "win32" not in sys.platform:
|
||||||
DEFAULT_JOURNAL_NAME = 'journal.txt'
|
# readline is not included in Windows Active Python
|
||||||
XDG_RESOURCE = 'jrnl'
|
import readline
|
||||||
|
|
||||||
USER_HOME = os.path.expanduser('~')
|
DEFAULT_CONFIG_NAME = "jrnl.yaml"
|
||||||
|
DEFAULT_JOURNAL_NAME = "journal.txt"
|
||||||
|
DEFAULT_JOURNAL_KEY = "default"
|
||||||
|
XDG_RESOURCE = "jrnl"
|
||||||
|
|
||||||
|
USER_HOME = os.path.expanduser("~")
|
||||||
|
|
||||||
CONFIG_PATH = xdg.BaseDirectory.save_config_path(XDG_RESOURCE) or USER_HOME
|
CONFIG_PATH = xdg.BaseDirectory.save_config_path(XDG_RESOURCE) or USER_HOME
|
||||||
CONFIG_FILE_PATH = os.path.join(CONFIG_PATH, DEFAULT_CONFIG_NAME)
|
CONFIG_FILE_PATH = os.path.join(CONFIG_PATH, DEFAULT_CONFIG_NAME)
|
||||||
|
@ -42,21 +44,20 @@ def module_exists(module_name):
|
||||||
else:
|
else:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
default_config = {
|
default_config = {
|
||||||
'version': __version__,
|
"version": __version__,
|
||||||
'journals': {
|
"journals": {DEFAULT_JOURNAL_KEY: JOURNAL_FILE_PATH},
|
||||||
"default": JOURNAL_FILE_PATH
|
"editor": os.getenv("VISUAL") or os.getenv("EDITOR") or "",
|
||||||
},
|
"encrypt": False,
|
||||||
'editor': os.getenv('VISUAL') or os.getenv('EDITOR') or "",
|
"template": False,
|
||||||
'encrypt': False,
|
"default_hour": 9,
|
||||||
'template': False,
|
"default_minute": 0,
|
||||||
'default_hour': 9,
|
"timeformat": "%Y-%m-%d %H:%M",
|
||||||
'default_minute': 0,
|
"tagsymbols": "@",
|
||||||
'timeformat': "%Y-%m-%d %H:%M",
|
"highlight": True,
|
||||||
'tagsymbols': '@',
|
"linewrap": 79,
|
||||||
'highlight': True,
|
"indent_character": "|",
|
||||||
'linewrap': 79,
|
|
||||||
'indent_character': '|',
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -69,13 +70,18 @@ def upgrade_config(config):
|
||||||
for key in missing_keys:
|
for key in missing_keys:
|
||||||
config[key] = default_config[key]
|
config[key] = default_config[key]
|
||||||
save_config(config)
|
save_config(config)
|
||||||
print("[Configuration updated to newest version at {}]".format(CONFIG_FILE_PATH))
|
print(
|
||||||
|
f"[Configuration updated to newest version at {CONFIG_FILE_PATH}]",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def save_config(config):
|
def save_config(config):
|
||||||
config['version'] = __version__
|
config["version"] = __version__
|
||||||
with open(CONFIG_FILE_PATH, 'w') as f:
|
with open(CONFIG_FILE_PATH, "w") as f:
|
||||||
yaml.safe_dump(config, f, encoding='utf-8', allow_unicode=True, default_flow_style=False)
|
yaml.safe_dump(
|
||||||
|
config, f, encoding="utf-8", allow_unicode=True, default_flow_style=False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def load_or_install_jrnl():
|
def load_or_install_jrnl():
|
||||||
|
@ -83,25 +89,35 @@ def load_or_install_jrnl():
|
||||||
If jrnl is already installed, loads and returns a config object.
|
If jrnl is already installed, loads and returns a config object.
|
||||||
Else, perform various prompts to install jrnl.
|
Else, perform various prompts to install jrnl.
|
||||||
"""
|
"""
|
||||||
config_path = CONFIG_FILE_PATH if os.path.exists(CONFIG_FILE_PATH) else CONFIG_FILE_PATH_FALLBACK
|
config_path = (
|
||||||
|
CONFIG_FILE_PATH
|
||||||
|
if os.path.exists(CONFIG_FILE_PATH)
|
||||||
|
else CONFIG_FILE_PATH_FALLBACK
|
||||||
|
)
|
||||||
if os.path.exists(config_path):
|
if os.path.exists(config_path):
|
||||||
log.debug('Reading configuration from file %s', config_path)
|
log.debug("Reading configuration from file %s", config_path)
|
||||||
config = util.load_config(config_path)
|
config = util.load_config(config_path)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
upgrade.upgrade_jrnl_if_necessary(config_path)
|
upgrade.upgrade_jrnl_if_necessary(config_path)
|
||||||
except upgrade.UpgradeValidationException:
|
except upgrade.UpgradeValidationException:
|
||||||
util.prompt("Aborting upgrade.")
|
print("Aborting upgrade.", file=sys.stderr)
|
||||||
util.prompt("Please tell us about this problem at the following URL:")
|
print(
|
||||||
util.prompt("https://github.com/jrnl-org/jrnl/issues/new?title=UpgradeValidationException")
|
"Please tell us about this problem at the following URL:",
|
||||||
util.prompt("Exiting.")
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
"https://github.com/jrnl-org/jrnl/issues/new?title=UpgradeValidationException",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
print("Exiting.", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
upgrade_config(config)
|
upgrade_config(config)
|
||||||
|
|
||||||
return config
|
return config
|
||||||
else:
|
else:
|
||||||
log.debug('Configuration file not found, installing jrnl...')
|
log.debug("Configuration file not found, installing jrnl...")
|
||||||
try:
|
try:
|
||||||
config = install()
|
config = install()
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
|
@ -110,41 +126,40 @@ def load_or_install_jrnl():
|
||||||
|
|
||||||
|
|
||||||
def install():
|
def install():
|
||||||
def autocomplete(text, state):
|
if "win32" not in sys.platform:
|
||||||
expansions = glob.glob(os.path.expanduser(os.path.expandvars(text)) + '*')
|
readline.set_completer_delims(" \t\n;")
|
||||||
expansions = [e + "/" if os.path.isdir(e) else e for e in expansions]
|
readline.parse_and_bind("tab: complete")
|
||||||
expansions.append(None)
|
readline.set_completer(autocomplete)
|
||||||
return expansions[state]
|
|
||||||
readline.set_completer_delims(' \t\n;')
|
|
||||||
readline.parse_and_bind("tab: complete")
|
|
||||||
readline.set_completer(autocomplete)
|
|
||||||
|
|
||||||
# Where to create the journal?
|
# Where to create the journal?
|
||||||
path_query = 'Path to your journal file (leave blank for {}): '.format(JOURNAL_FILE_PATH)
|
path_query = f"Path to your journal file (leave blank for {JOURNAL_FILE_PATH}): "
|
||||||
journal_path = util.py23_input(path_query).strip() or JOURNAL_FILE_PATH
|
journal_path = input(path_query).strip() or JOURNAL_FILE_PATH
|
||||||
default_config['journals']['default'] = os.path.expanduser(os.path.expandvars(journal_path))
|
default_config["journals"][DEFAULT_JOURNAL_KEY] = os.path.expanduser(
|
||||||
|
os.path.expandvars(journal_path)
|
||||||
|
)
|
||||||
|
|
||||||
path = os.path.split(default_config['journals']['default'])[0] # If the folder doesn't exist, create it
|
# If the folder doesn't exist, create it
|
||||||
|
path = os.path.split(default_config["journals"][DEFAULT_JOURNAL_KEY])[0]
|
||||||
try:
|
try:
|
||||||
os.makedirs(path)
|
os.makedirs(path)
|
||||||
except OSError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Encrypt it?
|
# Encrypt it?
|
||||||
password = getpass.getpass("Enter password for journal (leave blank for no encryption): ")
|
encrypt = util.yesno(
|
||||||
if password:
|
"Do you want to encrypt your journal? You can always change this later",
|
||||||
default_config['encrypt'] = True
|
default=False,
|
||||||
if util.yesno("Do you want to store the password in your keychain?", default=True):
|
)
|
||||||
util.set_keychain("default", password)
|
if encrypt:
|
||||||
else:
|
default_config["encrypt"] = True
|
||||||
util.set_keychain("default", None)
|
print("Journal will be encrypted.", file=sys.stderr)
|
||||||
EncryptedJournal._create(default_config['journals']['default'], password)
|
|
||||||
print("Journal will be encrypted.")
|
|
||||||
else:
|
|
||||||
PlainJournal._create(default_config['journals']['default'])
|
|
||||||
|
|
||||||
config = default_config
|
save_config(default_config)
|
||||||
save_config(config)
|
return default_config
|
||||||
if password:
|
|
||||||
config['password'] = password
|
|
||||||
return config
|
def autocomplete(text, state):
|
||||||
|
expansions = glob.glob(os.path.expanduser(os.path.expandvars(text)) + "*")
|
||||||
|
expansions = [e + "/" if os.path.isdir(e) else e for e in expansions]
|
||||||
|
expansions.append(None)
|
||||||
|
return expansions[state]
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# encoding: utf-8
|
# encoding: utf-8
|
||||||
|
|
||||||
from __future__ import absolute_import, unicode_literals
|
|
||||||
|
|
||||||
from .text_exporter import TextExporter
|
from .text_exporter import TextExporter
|
||||||
from .jrnl_importer import JRNLImporter
|
from .jrnl_importer import JRNLImporter
|
||||||
from .json_exporter import JSONExporter
|
from .json_exporter import JSONExporter
|
||||||
|
@ -11,16 +9,26 @@ from .tag_exporter import TagExporter
|
||||||
from .xml_exporter import XMLExporter
|
from .xml_exporter import XMLExporter
|
||||||
from .yaml_exporter import YAMLExporter
|
from .yaml_exporter import YAMLExporter
|
||||||
from .template_exporter import __all__ as template_exporters
|
from .template_exporter import __all__ as template_exporters
|
||||||
|
from .fancy_exporter import FancyExporter
|
||||||
|
|
||||||
__exporters =[JSONExporter, MarkdownExporter, TagExporter, TextExporter, XMLExporter, YAMLExporter] + template_exporters
|
__exporters = [
|
||||||
__importers =[JRNLImporter]
|
JSONExporter,
|
||||||
|
MarkdownExporter,
|
||||||
|
TagExporter,
|
||||||
|
TextExporter,
|
||||||
|
XMLExporter,
|
||||||
|
YAMLExporter,
|
||||||
|
FancyExporter,
|
||||||
|
] + template_exporters
|
||||||
|
__importers = [JRNLImporter]
|
||||||
|
|
||||||
__exporter_types = dict([(name, plugin) for plugin in __exporters for name in plugin.names])
|
__exporter_types = {name: plugin for plugin in __exporters for name in plugin.names}
|
||||||
__importer_types = dict([(name, plugin) for plugin in __importers for name in plugin.names])
|
__importer_types = {name: plugin for plugin in __importers for name in plugin.names}
|
||||||
|
|
||||||
EXPORT_FORMATS = sorted(__exporter_types.keys())
|
EXPORT_FORMATS = sorted(__exporter_types.keys())
|
||||||
IMPORT_FORMATS = sorted(__importer_types.keys())
|
IMPORT_FORMATS = sorted(__importer_types.keys())
|
||||||
|
|
||||||
|
|
||||||
def get_exporter(format):
|
def get_exporter(format):
|
||||||
for exporter in __exporters:
|
for exporter in __exporters:
|
||||||
if hasattr(exporter, "names") and format in exporter.names:
|
if hasattr(exporter, "names") and format in exporter.names:
|
||||||
|
|
74
jrnl/plugins/fancy_exporter.py
Normal file
74
jrnl/plugins/fancy_exporter.py
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# encoding: utf-8
|
||||||
|
|
||||||
|
from __future__ import absolute_import, unicode_literals, print_function
|
||||||
|
from .text_exporter import TextExporter
|
||||||
|
from textwrap import TextWrapper
|
||||||
|
|
||||||
|
|
||||||
|
class FancyExporter(TextExporter):
|
||||||
|
"""This Exporter can convert entries and journals into text with unicode box drawing characters."""
|
||||||
|
|
||||||
|
names = ["fancy", "boxed"]
|
||||||
|
extension = "txt"
|
||||||
|
|
||||||
|
border_a = "┎"
|
||||||
|
border_b = "─"
|
||||||
|
border_c = "╮"
|
||||||
|
border_d = "╘"
|
||||||
|
border_e = "═"
|
||||||
|
border_f = "╕"
|
||||||
|
border_g = "┃"
|
||||||
|
border_h = "│"
|
||||||
|
border_i = "┠"
|
||||||
|
border_j = "╌"
|
||||||
|
border_k = "┤"
|
||||||
|
border_l = "┖"
|
||||||
|
border_m = "┘"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def export_entry(cls, entry):
|
||||||
|
"""Returns a fancy unicode representation of a single entry."""
|
||||||
|
date_str = entry.date.strftime(entry.journal.config["timeformat"])
|
||||||
|
linewrap = entry.journal.config["linewrap"] or 78
|
||||||
|
initial_linewrap = linewrap - len(date_str) - 2
|
||||||
|
body_linewrap = linewrap - 2
|
||||||
|
card = [
|
||||||
|
cls.border_a + cls.border_b * (initial_linewrap) + cls.border_c + date_str
|
||||||
|
]
|
||||||
|
w = TextWrapper(
|
||||||
|
width=initial_linewrap,
|
||||||
|
initial_indent=cls.border_g + " ",
|
||||||
|
subsequent_indent=cls.border_g + " ",
|
||||||
|
)
|
||||||
|
title_lines = w.wrap(entry.title)
|
||||||
|
card.append(
|
||||||
|
title_lines[0].ljust(initial_linewrap + 1)
|
||||||
|
+ cls.border_d
|
||||||
|
+ cls.border_e * (len(date_str) - 1)
|
||||||
|
+ cls.border_f
|
||||||
|
)
|
||||||
|
w.width = body_linewrap
|
||||||
|
if len(title_lines) > 1:
|
||||||
|
for line in w.wrap(
|
||||||
|
" ".join(
|
||||||
|
[
|
||||||
|
title_line[len(w.subsequent_indent) :]
|
||||||
|
for title_line in title_lines[1:]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
):
|
||||||
|
card.append(line.ljust(body_linewrap + 1) + cls.border_h)
|
||||||
|
if entry.body:
|
||||||
|
card.append(cls.border_i + cls.border_j * body_linewrap + cls.border_k)
|
||||||
|
for line in entry.body.splitlines():
|
||||||
|
body_lines = w.wrap(line) or [cls.border_g]
|
||||||
|
for body_line in body_lines:
|
||||||
|
card.append(body_line.ljust(body_linewrap + 1) + cls.border_h)
|
||||||
|
card.append(cls.border_l + cls.border_b * body_linewrap + cls.border_m)
|
||||||
|
return "\n".join(card)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def export_journal(cls, journal):
|
||||||
|
"""Returns a unicode representation of an entire journal."""
|
||||||
|
return "\n".join(cls.export_entry(entry) for entry in journal)
|
|
@ -1,13 +1,13 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# encoding: utf-8
|
# encoding: utf-8
|
||||||
|
|
||||||
from __future__ import absolute_import, unicode_literals
|
|
||||||
import codecs
|
|
||||||
import sys
|
import sys
|
||||||
from .. import util
|
from .. import util
|
||||||
|
|
||||||
class JRNLImporter(object):
|
|
||||||
|
class JRNLImporter:
|
||||||
"""This plugin imports entries from other jrnl files."""
|
"""This plugin imports entries from other jrnl files."""
|
||||||
|
|
||||||
names = ["jrnl"]
|
names = ["jrnl"]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -17,15 +17,18 @@ class JRNLImporter(object):
|
||||||
old_cnt = len(journal.entries)
|
old_cnt = len(journal.entries)
|
||||||
old_entries = journal.entries
|
old_entries = journal.entries
|
||||||
if input:
|
if input:
|
||||||
with codecs.open(input, "r", "utf-8") as f:
|
with open(input, "r", encoding="utf-8") as f:
|
||||||
other_journal_txt = f.read()
|
other_journal_txt = f.read()
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
other_journal_txt = util.py23_read()
|
other_journal_txt = sys.stdin.read()
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
util.prompt("[Entries NOT imported into journal.]")
|
print("[Entries NOT imported into journal.]", file=sys.stderr)
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
journal.import_(other_journal_txt)
|
journal.import_(other_journal_txt)
|
||||||
new_cnt = len(journal.entries)
|
new_cnt = len(journal.entries)
|
||||||
util.prompt("[{0} imported to {1} journal]".format(new_cnt - old_cnt, journal.name))
|
print(
|
||||||
|
"[{} imported to {} journal]".format(new_cnt - old_cnt, journal.name),
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
journal.write()
|
journal.write()
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# encoding: utf-8
|
# encoding: utf-8
|
||||||
|
|
||||||
from __future__ import absolute_import, unicode_literals
|
|
||||||
from .text_exporter import TextExporter
|
from .text_exporter import TextExporter
|
||||||
import json
|
import json
|
||||||
from .util import get_tags_count
|
from .util import get_tags_count
|
||||||
|
@ -9,20 +8,21 @@ from .util import get_tags_count
|
||||||
|
|
||||||
class JSONExporter(TextExporter):
|
class JSONExporter(TextExporter):
|
||||||
"""This Exporter can convert entries and journals into json."""
|
"""This Exporter can convert entries and journals into json."""
|
||||||
|
|
||||||
names = ["json"]
|
names = ["json"]
|
||||||
extension = "json"
|
extension = "json"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def entry_to_dict(cls, entry):
|
def entry_to_dict(cls, entry):
|
||||||
entry_dict = {
|
entry_dict = {
|
||||||
'title': entry.title,
|
"title": entry.title,
|
||||||
'body': entry.body,
|
"body": entry.body,
|
||||||
'date': entry.date.strftime("%Y-%m-%d"),
|
"date": entry.date.strftime("%Y-%m-%d"),
|
||||||
'time': entry.date.strftime("%H:%M"),
|
"time": entry.date.strftime("%H:%M"),
|
||||||
'starred': entry.starred
|
"starred": entry.starred,
|
||||||
}
|
}
|
||||||
if hasattr(entry, "uuid"):
|
if hasattr(entry, "uuid"):
|
||||||
entry_dict['uuid'] = entry.uuid
|
entry_dict["uuid"] = entry.uuid
|
||||||
return entry_dict
|
return entry_dict
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -35,7 +35,7 @@ class JSONExporter(TextExporter):
|
||||||
"""Returns a json representation of an entire journal."""
|
"""Returns a json representation of an entire journal."""
|
||||||
tags = get_tags_count(journal)
|
tags = get_tags_count(journal)
|
||||||
result = {
|
result = {
|
||||||
"tags": dict((tag, count) for count, tag in tags),
|
"tags": {tag: count for count, tag in tags},
|
||||||
"entries": [cls.entry_to_dict(e) for e in journal.entries]
|
"entries": [cls.entry_to_dict(e) for e in journal.entries],
|
||||||
}
|
}
|
||||||
return json.dumps(result, indent=2)
|
return json.dumps(result, indent=2)
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# encoding: utf-8
|
# encoding: utf-8
|
||||||
|
|
||||||
from __future__ import absolute_import, unicode_literals, print_function
|
|
||||||
from .text_exporter import TextExporter
|
from .text_exporter import TextExporter
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
@ -11,24 +10,25 @@ from ..util import WARNING_COLOR, RESET_COLOR
|
||||||
|
|
||||||
class MarkdownExporter(TextExporter):
|
class MarkdownExporter(TextExporter):
|
||||||
"""This Exporter can convert entries and journals into Markdown."""
|
"""This Exporter can convert entries and journals into Markdown."""
|
||||||
|
|
||||||
names = ["md", "markdown"]
|
names = ["md", "markdown"]
|
||||||
extension = "md"
|
extension = "md"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def export_entry(cls, entry, to_multifile=True):
|
def export_entry(cls, entry, to_multifile=True):
|
||||||
"""Returns a markdown representation of a single entry."""
|
"""Returns a markdown representation of a single entry."""
|
||||||
date_str = entry.date.strftime(entry.journal.config['timeformat'])
|
date_str = entry.date.strftime(entry.journal.config["timeformat"])
|
||||||
body_wrapper = "\n" if entry.body else ""
|
body_wrapper = "\n" if entry.body else ""
|
||||||
body = body_wrapper + entry.body
|
body = body_wrapper + entry.body
|
||||||
|
|
||||||
if to_multifile is True:
|
if to_multifile is True:
|
||||||
heading = '#'
|
heading = "#"
|
||||||
else:
|
else:
|
||||||
heading = '###'
|
heading = "###"
|
||||||
|
|
||||||
'''Increase heading levels in body text'''
|
"""Increase heading levels in body text"""
|
||||||
newbody = ''
|
newbody = ""
|
||||||
previous_line = ''
|
previous_line = ""
|
||||||
warn_on_heading_level = False
|
warn_on_heading_level = False
|
||||||
for line in body.splitlines(True):
|
for line in body.splitlines(True):
|
||||||
if re.match(r"^#+ ", line):
|
if re.match(r"^#+ ", line):
|
||||||
|
@ -36,30 +36,32 @@ class MarkdownExporter(TextExporter):
|
||||||
newbody = newbody + previous_line + heading + line
|
newbody = newbody + previous_line + heading + line
|
||||||
if re.match(r"^#######+ ", heading + line):
|
if re.match(r"^#######+ ", heading + line):
|
||||||
warn_on_heading_level = True
|
warn_on_heading_level = True
|
||||||
line = ''
|
line = ""
|
||||||
elif re.match(r"^=+$", line.rstrip()) and not re.match(r"^$", previous_line.strip()):
|
elif re.match(r"^=+$", line.rstrip()) and not re.match(
|
||||||
|
r"^$", previous_line.strip()
|
||||||
|
):
|
||||||
"""Setext style H1"""
|
"""Setext style H1"""
|
||||||
newbody = newbody + heading + "# " + previous_line
|
newbody = newbody + heading + "# " + previous_line
|
||||||
line = ''
|
line = ""
|
||||||
elif re.match(r"^-+$", line.rstrip()) and not re.match(r"^$", previous_line.strip()):
|
elif re.match(r"^-+$", line.rstrip()) and not re.match(
|
||||||
|
r"^$", previous_line.strip()
|
||||||
|
):
|
||||||
"""Setext style H2"""
|
"""Setext style H2"""
|
||||||
newbody = newbody + heading + "## " + previous_line
|
newbody = newbody + heading + "## " + previous_line
|
||||||
line = ''
|
line = ""
|
||||||
else:
|
else:
|
||||||
newbody = newbody + previous_line
|
newbody = newbody + previous_line
|
||||||
previous_line = line
|
previous_line = line
|
||||||
newbody = newbody + previous_line # add very last line
|
newbody = newbody + previous_line # add very last line
|
||||||
|
|
||||||
if warn_on_heading_level is True:
|
if warn_on_heading_level is True:
|
||||||
print("{}WARNING{}: Headings increased past H6 on export - {} {}".format(WARNING_COLOR, RESET_COLOR, date_str, entry.title), file=sys.stderr)
|
print(
|
||||||
|
f"{WARNING_COLOR}WARNING{RESET_COLOR}: "
|
||||||
|
f"Headings increased past H6 on export - {date_str} {entry.title}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
|
||||||
return "{md} {date} {title}\n{body} {space}".format(
|
return f"{heading} {date_str} {entry.title}\n{newbody} "
|
||||||
md=heading,
|
|
||||||
date=date_str,
|
|
||||||
title=entry.title,
|
|
||||||
body=newbody,
|
|
||||||
space=""
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def export_journal(cls, journal):
|
def export_journal(cls, journal):
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# encoding: utf-8
|
# encoding: utf-8
|
||||||
|
|
||||||
from __future__ import absolute_import, unicode_literals
|
|
||||||
from .text_exporter import TextExporter
|
from .text_exporter import TextExporter
|
||||||
from .util import get_tags_count
|
from .util import get_tags_count
|
||||||
|
|
||||||
|
|
||||||
class TagExporter(TextExporter):
|
class TagExporter(TextExporter):
|
||||||
"""This Exporter can lists the tags for entries and journals, exported as a plain text file."""
|
"""This Exporter can lists the tags for entries and journals, exported as a plain text file."""
|
||||||
|
|
||||||
names = ["tags"]
|
names = ["tags"]
|
||||||
extension = "tags"
|
extension = "tags"
|
||||||
|
|
||||||
|
@ -22,9 +22,11 @@ class TagExporter(TextExporter):
|
||||||
tag_counts = get_tags_count(journal)
|
tag_counts = get_tags_count(journal)
|
||||||
result = ""
|
result = ""
|
||||||
if not tag_counts:
|
if not tag_counts:
|
||||||
return '[No tags found in journal.]'
|
return "[No tags found in journal.]"
|
||||||
elif min(tag_counts)[0] == 0:
|
elif min(tag_counts)[0] == 0:
|
||||||
tag_counts = filter(lambda x: x[0] > 1, tag_counts)
|
tag_counts = filter(lambda x: x[0] > 1, tag_counts)
|
||||||
result += '[Removed tags that appear only once.]\n'
|
result += "[Removed tags that appear only once.]\n"
|
||||||
result += "\n".join("{0:20} : {1}".format(tag, n) for n, tag in sorted(tag_counts, reverse=True))
|
result += "\n".join(
|
||||||
|
"{:20} : {}".format(tag, n) for n, tag in sorted(tag_counts, reverse=True)
|
||||||
|
)
|
||||||
return result
|
return result
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import re
|
import re
|
||||||
import asteval
|
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
VAR_RE = r"[_a-zA-Z][a-zA-Z0-9_]*"
|
VAR_RE = r"[_a-zA-Z][a-zA-Z0-9_]*"
|
||||||
|
@ -7,13 +6,15 @@ EXPRESSION_RE = r"[\[\]():.a-zA-Z0-9_]*"
|
||||||
PRINT_RE = r"{{ *(.+?) *}}"
|
PRINT_RE = r"{{ *(.+?) *}}"
|
||||||
START_BLOCK_RE = r"{% *(if|for) +(.+?) *%}"
|
START_BLOCK_RE = r"{% *(if|for) +(.+?) *%}"
|
||||||
END_BLOCK_RE = r"{% *end(for|if) *%}"
|
END_BLOCK_RE = r"{% *end(for|if) *%}"
|
||||||
FOR_RE = r"{{% *for +({varname}) +in +([^%]+) *%}}".format(varname=VAR_RE, expression=EXPRESSION_RE)
|
FOR_RE = r"{{% *for +({varname}) +in +([^%]+) *%}}".format(
|
||||||
|
varname=VAR_RE, expression=EXPRESSION_RE
|
||||||
|
)
|
||||||
IF_RE = r"{% *if +(.+?) *%}"
|
IF_RE = r"{% *if +(.+?) *%}"
|
||||||
BLOCK_RE = r"{% *block +(.+?) *%}((?:.|\n)+?){% *endblock *%}"
|
BLOCK_RE = r"{% *block +(.+?) *%}((?:.|\n)+?){% *endblock *%}"
|
||||||
INCLUDE_RE = r"{% *include +(.+?) *%}"
|
INCLUDE_RE = r"{% *include +(.+?) *%}"
|
||||||
|
|
||||||
|
|
||||||
class Template(object):
|
class Template:
|
||||||
def __init__(self, template):
|
def __init__(self, template):
|
||||||
self.template = template
|
self.template = template
|
||||||
self.clean_template = None
|
self.clean_template = None
|
||||||
|
@ -39,9 +40,11 @@ class Template(object):
|
||||||
return self._expand(self.blocks[block], **vars)
|
return self._expand(self.blocks[block], **vars)
|
||||||
|
|
||||||
def _eval_context(self, vars):
|
def _eval_context(self, vars):
|
||||||
|
import asteval
|
||||||
|
|
||||||
e = asteval.Interpreter(use_numpy=False, writer=None)
|
e = asteval.Interpreter(use_numpy=False, writer=None)
|
||||||
e.symtable.update(vars)
|
e.symtable.update(vars)
|
||||||
e.symtable['__last_iteration'] = vars.get("__last_iteration", False)
|
e.symtable["__last_iteration"] = vars.get("__last_iteration", False)
|
||||||
return e
|
return e
|
||||||
|
|
||||||
def _get_blocks(self):
|
def _get_blocks(self):
|
||||||
|
@ -49,12 +52,19 @@ class Template(object):
|
||||||
name, contents = match.groups()
|
name, contents = match.groups()
|
||||||
self.blocks[name] = self._strip_single_nl(contents)
|
self.blocks[name] = self._strip_single_nl(contents)
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
self.clean_template = re.sub(BLOCK_RE, s, self.template, flags=re.MULTILINE)
|
self.clean_template = re.sub(BLOCK_RE, s, self.template, flags=re.MULTILINE)
|
||||||
|
|
||||||
def _expand(self, template, **vars):
|
def _expand(self, template, **vars):
|
||||||
stack = sorted(
|
stack = sorted(
|
||||||
[(m.start(), 1, m.groups()[0]) for m in re.finditer(START_BLOCK_RE, template)] +
|
[
|
||||||
[(m.end(), -1, m.groups()[0]) for m in re.finditer(END_BLOCK_RE, template)]
|
(m.start(), 1, m.groups()[0])
|
||||||
|
for m in re.finditer(START_BLOCK_RE, template)
|
||||||
|
]
|
||||||
|
+ [
|
||||||
|
(m.end(), -1, m.groups()[0])
|
||||||
|
for m in re.finditer(END_BLOCK_RE, template)
|
||||||
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
last_nesting, nesting = 0, 0
|
last_nesting, nesting = 0, 0
|
||||||
|
@ -80,19 +90,23 @@ class Template(object):
|
||||||
start = pos
|
start = pos
|
||||||
last_nesting = nesting
|
last_nesting = nesting
|
||||||
|
|
||||||
result += self._expand_vars(template[stack[-1][0]:], **vars)
|
result += self._expand_vars(template[stack[-1][0] :], **vars)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def _expand_vars(self, template, **vars):
|
def _expand_vars(self, template, **vars):
|
||||||
safe_eval = self._eval_context(vars)
|
safe_eval = self._eval_context(vars)
|
||||||
expanded = re.sub(INCLUDE_RE, lambda m: self.render_block(m.groups()[0], **vars), template)
|
expanded = re.sub(
|
||||||
|
INCLUDE_RE, lambda m: self.render_block(m.groups()[0], **vars), template
|
||||||
|
)
|
||||||
return re.sub(PRINT_RE, lambda m: str(safe_eval(m.groups()[0])), expanded)
|
return re.sub(PRINT_RE, lambda m: str(safe_eval(m.groups()[0])), expanded)
|
||||||
|
|
||||||
def _expand_cond(self, template, **vars):
|
def _expand_cond(self, template, **vars):
|
||||||
start_block = re.search(IF_RE, template, re.M)
|
start_block = re.search(IF_RE, template, re.M)
|
||||||
end_block = list(re.finditer(END_BLOCK_RE, template, re.M))[-1]
|
end_block = list(re.finditer(END_BLOCK_RE, template, re.M))[-1]
|
||||||
expression = start_block.groups()[0]
|
expression = start_block.groups()[0]
|
||||||
sub_template = self._strip_single_nl(template[start_block.end():end_block.start()])
|
sub_template = self._strip_single_nl(
|
||||||
|
template[start_block.end() : end_block.start()]
|
||||||
|
)
|
||||||
|
|
||||||
safe_eval = self._eval_context(vars)
|
safe_eval = self._eval_context(vars)
|
||||||
if safe_eval(expression):
|
if safe_eval(expression):
|
||||||
|
@ -110,15 +124,17 @@ class Template(object):
|
||||||
start_block = re.search(FOR_RE, template, re.M)
|
start_block = re.search(FOR_RE, template, re.M)
|
||||||
end_block = list(re.finditer(END_BLOCK_RE, template, re.M))[-1]
|
end_block = list(re.finditer(END_BLOCK_RE, template, re.M))[-1]
|
||||||
var_name, iterator = start_block.groups()
|
var_name, iterator = start_block.groups()
|
||||||
sub_template = self._strip_single_nl(template[start_block.end():end_block.start()], strip_r=False)
|
sub_template = self._strip_single_nl(
|
||||||
|
template[start_block.end() : end_block.start()], strip_r=False
|
||||||
|
)
|
||||||
|
|
||||||
safe_eval = self._eval_context(vars)
|
safe_eval = self._eval_context(vars)
|
||||||
|
|
||||||
result = ''
|
result = ""
|
||||||
items = safe_eval(iterator)
|
items = safe_eval(iterator)
|
||||||
for idx, var in enumerate(items):
|
for idx, var in enumerate(items):
|
||||||
vars[var_name] = var
|
vars[var_name] = var
|
||||||
vars['__last_iteration'] = idx == len(items) - 1
|
vars["__last_iteration"] = idx == len(items) - 1
|
||||||
result += self._expand(sub_template, **vars)
|
result += self._expand(sub_template, **vars)
|
||||||
del vars[var_name]
|
del vars[var_name]
|
||||||
return self._strip_single_nl(result)
|
return self._strip_single_nl(result)
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# encoding: utf-8
|
# encoding: utf-8
|
||||||
|
|
||||||
from __future__ import absolute_import, unicode_literals
|
|
||||||
|
|
||||||
from .text_exporter import TextExporter
|
from .text_exporter import TextExporter
|
||||||
from .template import Template
|
from .template import Template
|
||||||
import os
|
import os
|
||||||
|
@ -14,21 +12,14 @@ class GenericTemplateExporter(TextExporter):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def export_entry(cls, entry):
|
def export_entry(cls, entry):
|
||||||
"""Returns a unicode representation of a single entry."""
|
"""Returns a string representation of a single entry."""
|
||||||
vars = {
|
vars = {"entry": entry, "tags": entry.tags}
|
||||||
'entry': entry,
|
|
||||||
'tags': entry.tags
|
|
||||||
}
|
|
||||||
return cls.template.render_block("entry", **vars)
|
return cls.template.render_block("entry", **vars)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def export_journal(cls, journal):
|
def export_journal(cls, journal):
|
||||||
"""Returns a unicode representation of an entire journal."""
|
"""Returns a string representation of an entire journal."""
|
||||||
vars = {
|
vars = {"journal": journal, "entries": journal.entries, "tags": journal.tags}
|
||||||
'journal': journal,
|
|
||||||
'entries': journal.entries,
|
|
||||||
'tags': journal.tags
|
|
||||||
}
|
|
||||||
return cls.template.render_block("journal", **vars)
|
return cls.template.render_block("journal", **vars)
|
||||||
|
|
||||||
|
|
||||||
|
@ -36,11 +27,12 @@ def __exporter_from_file(template_file):
|
||||||
"""Create a template class from a file"""
|
"""Create a template class from a file"""
|
||||||
name = os.path.basename(template_file).replace(".template", "")
|
name = os.path.basename(template_file).replace(".template", "")
|
||||||
template = Template.from_file(template_file)
|
template = Template.from_file(template_file)
|
||||||
return type(str("{}Exporter".format(name.title())), (GenericTemplateExporter, ), {
|
return type(
|
||||||
"names": [name],
|
str(f"{name.title()}Exporter"),
|
||||||
"extension": template.extension,
|
(GenericTemplateExporter,),
|
||||||
"template": template
|
{"names": [name], "extension": template.extension, "template": template},
|
||||||
})
|
)
|
||||||
|
|
||||||
|
|
||||||
__all__ = []
|
__all__ = []
|
||||||
|
|
||||||
|
|
|
@ -1,41 +1,42 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# encoding: utf-8
|
# encoding: utf-8
|
||||||
|
|
||||||
from __future__ import absolute_import, unicode_literals
|
from ..util import slugify
|
||||||
import codecs
|
|
||||||
from ..util import u, slugify
|
|
||||||
import os
|
import os
|
||||||
from ..util import ERROR_COLOR, RESET_COLOR
|
from ..util import ERROR_COLOR, RESET_COLOR
|
||||||
|
|
||||||
|
|
||||||
class TextExporter(object):
|
class TextExporter:
|
||||||
"""This Exporter can convert entries and journals into text files."""
|
"""This Exporter can convert entries and journals into text files."""
|
||||||
|
|
||||||
names = ["text", "txt"]
|
names = ["text", "txt"]
|
||||||
extension = "txt"
|
extension = "txt"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def export_entry(cls, entry):
|
def export_entry(cls, entry):
|
||||||
"""Returns a unicode representation of a single entry."""
|
"""Returns a string representation of a single entry."""
|
||||||
return entry.__unicode__()
|
return str(entry)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def export_journal(cls, journal):
|
def export_journal(cls, journal):
|
||||||
"""Returns a unicode representation of an entire journal."""
|
"""Returns a string representation of an entire journal."""
|
||||||
return "\n".join(cls.export_entry(entry) for entry in journal)
|
return "\n".join(cls.export_entry(entry) for entry in journal)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def write_file(cls, journal, path):
|
def write_file(cls, journal, path):
|
||||||
"""Exports a journal into a single file."""
|
"""Exports a journal into a single file."""
|
||||||
try:
|
try:
|
||||||
with codecs.open(path, "w", "utf-8") as f:
|
with open(path, "w", encoding="utf-8") as f:
|
||||||
f.write(cls.export_journal(journal))
|
f.write(cls.export_journal(journal))
|
||||||
return "[Journal exported to {0}]".format(path)
|
return f"[Journal exported to {path}]"
|
||||||
except IOError as e:
|
except IOError as e:
|
||||||
return "[{2}ERROR{3}: {0} {1}]".format(e.filename, e.strerror, ERROR_COLOR, RESET_COLOR)
|
return f"[{ERROR_COLOR}ERROR{RESET_COLOR}: {e.filename} {e.strerror}]"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def make_filename(cls, entry):
|
def make_filename(cls, entry):
|
||||||
return entry.date.strftime("%Y-%m-%d_{0}.{1}".format(slugify(u(entry.title)), cls.extension))
|
return entry.date.strftime(
|
||||||
|
"%Y-%m-%d_{}.{}".format(slugify(str(entry.title)), cls.extension)
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def write_files(cls, journal, path):
|
def write_files(cls, journal, path):
|
||||||
|
@ -43,20 +44,22 @@ class TextExporter(object):
|
||||||
for entry in journal.entries:
|
for entry in journal.entries:
|
||||||
try:
|
try:
|
||||||
full_path = os.path.join(path, cls.make_filename(entry))
|
full_path = os.path.join(path, cls.make_filename(entry))
|
||||||
with codecs.open(full_path, "w", "utf-8") as f:
|
with open(full_path, "w", encoding="utf-8") as f:
|
||||||
f.write(cls.export_entry(entry))
|
f.write(cls.export_entry(entry))
|
||||||
except IOError as e:
|
except IOError as e:
|
||||||
return "[{2}ERROR{3}: {0} {1}]".format(e.filename, e.strerror, ERROR_COLOR, RESET_COLOR)
|
return "[{2}ERROR{3}: {0} {1}]".format(
|
||||||
return "[Journal exported to {0}]".format(path)
|
e.filename, e.strerror, ERROR_COLOR, RESET_COLOR
|
||||||
|
)
|
||||||
|
return "[Journal exported to {}]".format(path)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def export(cls, journal, output=None):
|
def export(cls, journal, output=None):
|
||||||
"""Exports to individual files if output is an existing path, or into
|
"""Exports to individual files if output is an existing path, or into
|
||||||
a single file if output is a file name, or returns the exporter's
|
a single file if output is a file name, or returns the exporter's
|
||||||
representation as unicode if output is None."""
|
representation as string if output is None."""
|
||||||
if output and os.path.isdir(output): # multiple files
|
if output and os.path.isdir(output): # multiple files
|
||||||
return cls.write_files(journal, output)
|
return cls.write_files(journal, output)
|
||||||
elif output: # single file
|
elif output: # single file
|
||||||
return cls.write_file(journal, output)
|
return cls.write_file(journal, output)
|
||||||
else:
|
else:
|
||||||
return cls.export_journal(journal)
|
return cls.export_journal(journal)
|
||||||
|
|
|
@ -6,11 +6,9 @@ def get_tags_count(journal):
|
||||||
"""Returns a set of tuples (count, tag) for all tags present in the journal."""
|
"""Returns a set of tuples (count, tag) for all tags present in the journal."""
|
||||||
# Astute reader: should the following line leave you as puzzled as me the first time
|
# Astute reader: should the following line leave you as puzzled as me the first time
|
||||||
# I came across this construction, worry not and embrace the ensuing moment of enlightment.
|
# I came across this construction, worry not and embrace the ensuing moment of enlightment.
|
||||||
tags = [tag
|
tags = [tag for entry in journal.entries for tag in set(entry.tags)]
|
||||||
for entry in journal.entries
|
|
||||||
for tag in set(entry.tags)]
|
|
||||||
# To be read: [for entry in journal.entries: for tag in set(entry.tags): tag]
|
# To be read: [for entry in journal.entries: for tag in set(entry.tags): tag]
|
||||||
tag_counts = set([(tags.count(tag), tag) for tag in tags])
|
tag_counts = {(tags.count(tag), tag) for tag in tags}
|
||||||
return tag_counts
|
return tag_counts
|
||||||
|
|
||||||
|
|
||||||
|
@ -24,4 +22,4 @@ def oxford_list(lst):
|
||||||
elif len(lst) == 2:
|
elif len(lst) == 2:
|
||||||
return lst[0] + " or " + lst[1]
|
return lst[0] + " or " + lst[1]
|
||||||
else:
|
else:
|
||||||
return ', '.join(lst[:-1]) + ", or " + lst[-1]
|
return ", ".join(lst[:-1]) + ", or " + lst[-1]
|
||||||
|
|
|
@ -1,15 +1,14 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# encoding: utf-8
|
# encoding: utf-8
|
||||||
|
|
||||||
from __future__ import absolute_import, unicode_literals
|
|
||||||
from .json_exporter import JSONExporter
|
from .json_exporter import JSONExporter
|
||||||
from .util import get_tags_count
|
from .util import get_tags_count
|
||||||
from ..util import u
|
|
||||||
from xml.dom import minidom
|
from xml.dom import minidom
|
||||||
|
|
||||||
|
|
||||||
class XMLExporter(JSONExporter):
|
class XMLExporter(JSONExporter):
|
||||||
"""This Exporter can convert entries and journals into XML."""
|
"""This Exporter can convert entries and journals into XML."""
|
||||||
|
|
||||||
names = ["xml"]
|
names = ["xml"]
|
||||||
extension = "xml"
|
extension = "xml"
|
||||||
|
|
||||||
|
@ -17,10 +16,10 @@ class XMLExporter(JSONExporter):
|
||||||
def export_entry(cls, entry, doc=None):
|
def export_entry(cls, entry, doc=None):
|
||||||
"""Returns an XML representation of a single entry."""
|
"""Returns an XML representation of a single entry."""
|
||||||
doc_el = doc or minidom.Document()
|
doc_el = doc or minidom.Document()
|
||||||
entry_el = doc_el.createElement('entry')
|
entry_el = doc_el.createElement("entry")
|
||||||
for key, value in cls.entry_to_dict(entry).items():
|
for key, value in cls.entry_to_dict(entry).items():
|
||||||
elem = doc_el.createElement(key)
|
elem = doc_el.createElement(key)
|
||||||
elem.appendChild(doc_el.createTextNode(u(value)))
|
elem.appendChild(doc_el.createTextNode(value))
|
||||||
entry_el.appendChild(elem)
|
entry_el.appendChild(elem)
|
||||||
if not doc:
|
if not doc:
|
||||||
doc_el.appendChild(entry_el)
|
doc_el.appendChild(entry_el)
|
||||||
|
@ -30,11 +29,11 @@ class XMLExporter(JSONExporter):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def entry_to_xml(cls, entry, doc):
|
def entry_to_xml(cls, entry, doc):
|
||||||
entry_el = doc.createElement('entry')
|
entry_el = doc.createElement("entry")
|
||||||
entry_el.setAttribute('date', entry.date.isoformat())
|
entry_el.setAttribute("date", entry.date.isoformat())
|
||||||
if hasattr(entry, "uuid"):
|
if hasattr(entry, "uuid"):
|
||||||
entry_el.setAttribute('uuid', u(entry.uuid))
|
entry_el.setAttribute("uuid", entry.uuid)
|
||||||
entry_el.setAttribute('starred', u(entry.starred))
|
entry_el.setAttribute("starred", entry.starred)
|
||||||
entry_el.appendChild(doc.createTextNode(entry.fulltext))
|
entry_el.appendChild(doc.createTextNode(entry.fulltext))
|
||||||
return entry_el
|
return entry_el
|
||||||
|
|
||||||
|
@ -43,13 +42,13 @@ class XMLExporter(JSONExporter):
|
||||||
"""Returns an XML representation of an entire journal."""
|
"""Returns an XML representation of an entire journal."""
|
||||||
tags = get_tags_count(journal)
|
tags = get_tags_count(journal)
|
||||||
doc = minidom.Document()
|
doc = minidom.Document()
|
||||||
xml = doc.createElement('journal')
|
xml = doc.createElement("journal")
|
||||||
tags_el = doc.createElement('tags')
|
tags_el = doc.createElement("tags")
|
||||||
entries_el = doc.createElement('entries')
|
entries_el = doc.createElement("entries")
|
||||||
for count, tag in tags:
|
for count, tag in tags:
|
||||||
tag_el = doc.createElement('tag')
|
tag_el = doc.createElement("tag")
|
||||||
tag_el.setAttribute('name', tag)
|
tag_el.setAttribute("name", tag)
|
||||||
count_node = doc.createTextNode(u(count))
|
count_node = doc.createTextNode(str(count))
|
||||||
tag_el.appendChild(count_node)
|
tag_el.appendChild(count_node)
|
||||||
tags_el.appendChild(tag_el)
|
tags_el.appendChild(tag_el)
|
||||||
for entry in journal.entries:
|
for entry in journal.entries:
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# encoding: utf-8
|
# encoding: utf-8
|
||||||
|
|
||||||
from __future__ import absolute_import, unicode_literals, print_function
|
|
||||||
from .text_exporter import TextExporter
|
from .text_exporter import TextExporter
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
@ -11,6 +10,7 @@ from ..util import WARNING_COLOR, ERROR_COLOR, RESET_COLOR
|
||||||
|
|
||||||
class YAMLExporter(TextExporter):
|
class YAMLExporter(TextExporter):
|
||||||
"""This Exporter can convert entries and journals into Markdown formatted text with YAML front matter."""
|
"""This Exporter can convert entries and journals into Markdown formatted text with YAML front matter."""
|
||||||
|
|
||||||
names = ["yaml"]
|
names = ["yaml"]
|
||||||
extension = "md"
|
extension = "md"
|
||||||
|
|
||||||
|
@ -18,67 +18,109 @@ class YAMLExporter(TextExporter):
|
||||||
def export_entry(cls, entry, to_multifile=True):
|
def export_entry(cls, entry, to_multifile=True):
|
||||||
"""Returns a markdown representation of a single entry, with YAML front matter."""
|
"""Returns a markdown representation of a single entry, with YAML front matter."""
|
||||||
if to_multifile is False:
|
if to_multifile is False:
|
||||||
print("{}ERROR{}: YAML export must be to individual files. Please specify a directory to export to.".format("\033[31m", "\033[0m", file=sys.stderr))
|
print(
|
||||||
|
"{}ERROR{}: YAML export must be to individual files. Please \
|
||||||
|
specify a directory to export to.".format(
|
||||||
|
ERROR_COLOR, RESET_COLOR, file=sys.stderr
|
||||||
|
)
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
date_str = entry.date.strftime(entry.journal.config['timeformat'])
|
date_str = entry.date.strftime(entry.journal.config["timeformat"])
|
||||||
body_wrapper = "\n" if entry.body else ""
|
body_wrapper = "\n" if entry.body else ""
|
||||||
body = body_wrapper + entry.body
|
body = body_wrapper + entry.body
|
||||||
|
|
||||||
tagsymbols = entry.journal.config['tagsymbols']
|
tagsymbols = entry.journal.config["tagsymbols"]
|
||||||
# see also Entry.Entry.rag_regex
|
# see also Entry.Entry.rag_regex
|
||||||
multi_tag_regex = re.compile(r'(?u)^\s*([{tags}][-+*#/\w]+\s*)+$'.format(tags=tagsymbols), re.UNICODE)
|
multi_tag_regex = re.compile(fr"(?u)^\s*([{tagsymbols}][-+*#/\w]+\s*)+$")
|
||||||
|
|
||||||
'''Increase heading levels in body text'''
|
"""Increase heading levels in body text"""
|
||||||
newbody = ''
|
newbody = ""
|
||||||
heading = '#'
|
heading = "#"
|
||||||
previous_line = ''
|
previous_line = ""
|
||||||
warn_on_heading_level = False
|
warn_on_heading_level = False
|
||||||
for line in entry.body.splitlines(True):
|
for line in body.splitlines(True):
|
||||||
if re.match(r"^#+ ", line):
|
if re.match(r"^#+ ", line):
|
||||||
"""ATX style headings"""
|
"""ATX style headings"""
|
||||||
newbody = newbody + previous_line + heading + line
|
newbody = newbody + previous_line + heading + line
|
||||||
if re.match(r"^#######+ ", heading + line):
|
if re.match(r"^#######+ ", heading + line):
|
||||||
warn_on_heading_level = True
|
warn_on_heading_level = True
|
||||||
line = ''
|
line = ""
|
||||||
elif re.match(r"^=+$", line.rstrip()) and not re.match(r"^$", previous_line.strip()):
|
elif re.match(r"^=+$", line.rstrip()) and not re.match(
|
||||||
|
r"^$", previous_line.strip()
|
||||||
|
):
|
||||||
"""Setext style H1"""
|
"""Setext style H1"""
|
||||||
newbody = newbody + heading + "# " + previous_line
|
newbody = newbody + heading + "# " + previous_line
|
||||||
line = ''
|
line = ""
|
||||||
elif re.match(r"^-+$", line.rstrip()) and not re.match(r"^$", previous_line.strip()):
|
elif re.match(r"^-+$", line.rstrip()) and not re.match(
|
||||||
|
r"^$", previous_line.strip()
|
||||||
|
):
|
||||||
"""Setext style H2"""
|
"""Setext style H2"""
|
||||||
newbody = newbody + heading + "## " + previous_line
|
newbody = newbody + heading + "## " + previous_line
|
||||||
line = ''
|
line = ""
|
||||||
elif multi_tag_regex.match(line):
|
elif multi_tag_regex.match(line):
|
||||||
"""Tag only lines"""
|
"""Tag only lines"""
|
||||||
line = ''
|
line = ""
|
||||||
else:
|
else:
|
||||||
newbody = newbody + previous_line
|
newbody = newbody + previous_line
|
||||||
previous_line = line
|
previous_line = line
|
||||||
newbody = newbody + previous_line # add very last line
|
newbody = newbody + previous_line # add very last line
|
||||||
|
|
||||||
if warn_on_heading_level is True:
|
if warn_on_heading_level is True:
|
||||||
print("{}WARNING{}: Headings increased past H6 on export - {} {}".format(WARNING_COLOR, RESET_COLOR, date_str, entry.title), file=sys.stderr)
|
print(
|
||||||
|
"{}WARNING{}: Headings increased past H6 on export - {} {}".format(
|
||||||
|
WARNING_COLOR, RESET_COLOR, date_str, entry.title
|
||||||
|
),
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
|
||||||
dayone_attributes = ''
|
dayone_attributes = ""
|
||||||
if hasattr(entry, "uuid"):
|
if hasattr(entry, "uuid"):
|
||||||
dayone_attributes += 'uuid: ' + entry.uuid + '\n'
|
dayone_attributes += "uuid: " + entry.uuid + "\n"
|
||||||
# TODO: copy over pictures, if present
|
if (
|
||||||
# source directory is entry.journal.config['journal']
|
hasattr(entry, "creator_device_agent")
|
||||||
# output directory is...?
|
or hasattr(entry, "creator_generation_date")
|
||||||
|
or hasattr(entry, "creator_host_name")
|
||||||
|
or hasattr(entry, "creator_os_agent")
|
||||||
|
or hasattr(entry, "creator_software_agent")
|
||||||
|
):
|
||||||
|
dayone_attributes += "creator:\n"
|
||||||
|
if hasattr(entry, "creator_device_agent"):
|
||||||
|
dayone_attributes += f" device agent: {entry.creator_device_agent}\n"
|
||||||
|
if hasattr(entry, "creator_generation_date"):
|
||||||
|
dayone_attributes += " generation date: {}\n".format(
|
||||||
|
str(entry.creator_generation_date)
|
||||||
|
)
|
||||||
|
if hasattr(entry, "creator_host_name"):
|
||||||
|
dayone_attributes += f" host name: {entry.creator_host_name}\n"
|
||||||
|
if hasattr(entry, "creator_os_agent"):
|
||||||
|
dayone_attributes += f" os agent: {entry.creator_os_agent}\n"
|
||||||
|
if hasattr(entry, "creator_software_agent"):
|
||||||
|
dayone_attributes += (
|
||||||
|
f" software agent: {entry.creator_software_agent}\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
# TODO: copy over pictures, if present
|
||||||
|
# source directory is entry.journal.config['journal']
|
||||||
|
# output directory is...?
|
||||||
|
|
||||||
return "title: {title}\ndate: {date}\nstared: {stared}\ntags: {tags}\n{dayone} {body} {space}".format(
|
return "title: {title}\ndate: {date}\nstared: {stared}\ntags: {tags}\n{dayone} {body} {space}".format(
|
||||||
date = date_str,
|
date=date_str,
|
||||||
title = entry.title,
|
title=entry.title,
|
||||||
stared = entry.starred,
|
stared=entry.starred,
|
||||||
tags = ', '.join([tag[1:] for tag in entry.tags]),
|
tags=", ".join([tag[1:] for tag in entry.tags]),
|
||||||
dayone = dayone_attributes,
|
dayone=dayone_attributes,
|
||||||
body = newbody,
|
body=newbody,
|
||||||
space=""
|
space="",
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def export_journal(cls, journal):
|
def export_journal(cls, journal):
|
||||||
"""Returns an error, as YAML export requires a directory as a target."""
|
"""Returns an error, as YAML export requires a directory as a target."""
|
||||||
print("{}ERROR{}: YAML export must be to individual files. Please specify a directory to export to.".format(ERROR_COLOR, RESET_COLOR), file=sys.stderr)
|
print(
|
||||||
|
"{}ERROR{}: YAML export must be to individual files. Please specify a directory to export to.".format(
|
||||||
|
ERROR_COLOR, RESET_COLOR
|
||||||
|
),
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
25
jrnl/time.py
25
jrnl/time.py
|
@ -1,7 +1,10 @@
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from dateutil.parser import parse as dateparse
|
from dateutil.parser import parse as dateparse
|
||||||
try: import parsedatetime.parsedatetime_consts as pdt
|
|
||||||
except ImportError: import parsedatetime as pdt
|
try:
|
||||||
|
import parsedatetime.parsedatetime_consts as pdt
|
||||||
|
except ImportError:
|
||||||
|
import parsedatetime as pdt
|
||||||
|
|
||||||
FAKE_YEAR = 9999
|
FAKE_YEAR = 9999
|
||||||
DEFAULT_FUTURE = datetime(FAKE_YEAR, 12, 31, 23, 59, 59)
|
DEFAULT_FUTURE = datetime(FAKE_YEAR, 12, 31, 23, 59, 59)
|
||||||
|
@ -12,15 +15,18 @@ consts.DOWParseStyle = -1 # "Monday" will be either today or the last Monday
|
||||||
CALENDAR = pdt.Calendar(consts)
|
CALENDAR = pdt.Calendar(consts)
|
||||||
|
|
||||||
|
|
||||||
def parse(date_str, inclusive=False, default_hour=None, default_minute=None):
|
def parse(
|
||||||
|
date_str, inclusive=False, default_hour=None, default_minute=None, bracketed=False
|
||||||
|
):
|
||||||
"""Parses a string containing a fuzzy date and returns a datetime.datetime object"""
|
"""Parses a string containing a fuzzy date and returns a datetime.datetime object"""
|
||||||
if not date_str:
|
if not date_str:
|
||||||
return None
|
return None
|
||||||
elif isinstance(date_str, datetime):
|
elif isinstance(date_str, datetime):
|
||||||
return date_str
|
return date_str
|
||||||
|
|
||||||
# Don't try to parse anything with 6 or less characters. It's probably a markdown footnote
|
# Don't try to parse anything with 6 or less characters and was parsed from the existing journal.
|
||||||
if len(date_str) <= 6:
|
# It's probably a markdown footnote
|
||||||
|
if len(date_str) <= 6 and bracketed:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
default_date = DEFAULT_FUTURE if inclusive else DEFAULT_PAST
|
default_date = DEFAULT_FUTURE if inclusive else DEFAULT_PAST
|
||||||
|
@ -36,7 +42,7 @@ def parse(date_str, inclusive=False, default_hour=None, default_minute=None):
|
||||||
flag = 1 if date.hour == date.minute == 0 else 2
|
flag = 1 if date.hour == date.minute == 0 else 2
|
||||||
date = date.timetuple()
|
date = date.timetuple()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if e.args[0] == 'day is out of range for month':
|
if e.args[0] == "day is out of range for month":
|
||||||
y, m, d, H, M, S = default_date.timetuple()[:6]
|
y, m, d, H, M, S = default_date.timetuple()[:6]
|
||||||
default_date = datetime(y, m, d - 1, H, M, S)
|
default_date = datetime(y, m, d - 1, H, M, S)
|
||||||
else:
|
else:
|
||||||
|
@ -52,7 +58,12 @@ def parse(date_str, inclusive=False, default_hour=None, default_minute=None):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if flag is 1: # Date found, but no time. Use the default time.
|
if flag is 1: # Date found, but no time. Use the default time.
|
||||||
date = datetime(*date[:3], hour=default_hour or 0, minute=default_minute or 0)
|
date = datetime(
|
||||||
|
*date[:3],
|
||||||
|
hour=23 if inclusive else default_hour or 0,
|
||||||
|
minute=59 if inclusive else default_minute or 0,
|
||||||
|
second=59 if inclusive else 0
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
date = datetime(*date[:6])
|
date = datetime(*date[:6])
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from __future__ import absolute_import, unicode_literals
|
import sys
|
||||||
|
|
||||||
from . import __version__
|
from . import __version__
|
||||||
from . import Journal
|
from . import Journal
|
||||||
|
@ -6,27 +6,27 @@ from . import util
|
||||||
from .EncryptedJournal import EncryptedJournal
|
from .EncryptedJournal import EncryptedJournal
|
||||||
from .util import UserAbort
|
from .util import UserAbort
|
||||||
import os
|
import os
|
||||||
import codecs
|
|
||||||
|
|
||||||
|
|
||||||
def backup(filename, binary=False):
|
def backup(filename, binary=False):
|
||||||
util.prompt(" Created a backup at {}.backup".format(filename))
|
print(f" Created a backup at {filename}.backup", file=sys.stderr)
|
||||||
filename = os.path.expanduser(os.path.expandvars(filename))
|
filename = os.path.expanduser(os.path.expandvars(filename))
|
||||||
with open(filename, 'rb' if binary else 'r') as original:
|
with open(filename, "rb" if binary else "r") as original:
|
||||||
contents = original.read()
|
contents = original.read()
|
||||||
with open(filename + ".backup", 'wb' if binary else 'w') as backup:
|
with open(filename + ".backup", "wb" if binary else "w") as backup:
|
||||||
backup.write(contents)
|
backup.write(contents)
|
||||||
|
|
||||||
|
|
||||||
def upgrade_jrnl_if_necessary(config_path):
|
def upgrade_jrnl_if_necessary(config_path):
|
||||||
with codecs.open(config_path, "r", "utf-8") as f:
|
with open(config_path, "r", encoding="utf-8") as f:
|
||||||
config_file = f.read()
|
config_file = f.read()
|
||||||
if not config_file.strip().startswith("{"):
|
if not config_file.strip().startswith("{"):
|
||||||
return
|
return
|
||||||
|
|
||||||
config = util.load_config(config_path)
|
config = util.load_config(config_path)
|
||||||
|
|
||||||
util.prompt("""Welcome to jrnl {}.
|
print(
|
||||||
|
f"""Welcome to jrnl {__version__}.
|
||||||
|
|
||||||
It looks like you've been using an older version of jrnl until now. That's
|
It looks like you've been using an older version of jrnl until now. That's
|
||||||
okay - jrnl will now upgrade your configuration and journal files. Afterwards
|
okay - jrnl will now upgrade your configuration and journal files. Afterwards
|
||||||
|
@ -40,20 +40,24 @@ you can enjoy all of the great new features that come with jrnl 2:
|
||||||
Please note that jrnl 1.x is NOT forward compatible with this version of jrnl.
|
Please note that jrnl 1.x is NOT forward compatible with this version of jrnl.
|
||||||
If you choose to proceed, you will not be able to use your journals with
|
If you choose to proceed, you will not be able to use your journals with
|
||||||
older versions of jrnl anymore.
|
older versions of jrnl anymore.
|
||||||
""".format(__version__))
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
encrypted_journals = {}
|
encrypted_journals = {}
|
||||||
plain_journals = {}
|
plain_journals = {}
|
||||||
other_journals = {}
|
other_journals = {}
|
||||||
all_journals = []
|
all_journals = []
|
||||||
|
|
||||||
for journal_name, journal_conf in config['journals'].items():
|
for journal_name, journal_conf in config["journals"].items():
|
||||||
if isinstance(journal_conf, dict):
|
if isinstance(journal_conf, dict):
|
||||||
path = journal_conf.get("journal")
|
path = journal_conf.get("journal")
|
||||||
encrypt = journal_conf.get("encrypt")
|
encrypt = journal_conf.get("encrypt")
|
||||||
else:
|
else:
|
||||||
encrypt = config.get('encrypt')
|
encrypt = config.get("encrypt")
|
||||||
path = journal_conf
|
path = journal_conf
|
||||||
|
|
||||||
|
path = os.path.expanduser(path)
|
||||||
|
|
||||||
if encrypt:
|
if encrypt:
|
||||||
encrypted_journals[journal_name] = path
|
encrypted_journals[journal_name] = path
|
||||||
elif os.path.isdir(path):
|
elif os.path.isdir(path):
|
||||||
|
@ -61,21 +65,36 @@ older versions of jrnl anymore.
|
||||||
else:
|
else:
|
||||||
plain_journals[journal_name] = path
|
plain_journals[journal_name] = path
|
||||||
|
|
||||||
longest_journal_name = max([len(journal) for journal in config['journals']])
|
longest_journal_name = max([len(journal) for journal in config["journals"]])
|
||||||
if encrypted_journals:
|
if encrypted_journals:
|
||||||
util.prompt("\nFollowing encrypted journals will be upgraded to jrnl {}:".format(__version__))
|
print(
|
||||||
|
f"\nFollowing encrypted journals will be upgraded to jrnl {__version__}:",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
for journal, path in encrypted_journals.items():
|
for journal, path in encrypted_journals.items():
|
||||||
util.prompt(" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name))
|
print(
|
||||||
|
" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name),
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
|
||||||
if plain_journals:
|
if plain_journals:
|
||||||
util.prompt("\nFollowing plain text journals will upgraded to jrnl {}:".format(__version__))
|
print(
|
||||||
|
f"\nFollowing plain text journals will upgraded to jrnl {__version__}:",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
for journal, path in plain_journals.items():
|
for journal, path in plain_journals.items():
|
||||||
util.prompt(" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name))
|
print(
|
||||||
|
" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name),
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
|
||||||
if other_journals:
|
if other_journals:
|
||||||
util.prompt("\nFollowing journals will be not be touched:")
|
print("\nFollowing journals will be not be touched:", file=sys.stderr)
|
||||||
for journal, path in other_journals.items():
|
for journal, path in other_journals.items():
|
||||||
util.prompt(" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name))
|
print(
|
||||||
|
" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name),
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
cont = util.yesno("\nContinue upgrading jrnl?", default=False)
|
cont = util.yesno("\nContinue upgrading jrnl?", default=False)
|
||||||
|
@ -85,23 +104,37 @@ older versions of jrnl anymore.
|
||||||
raise UserAbort("jrnl NOT upgraded, exiting.")
|
raise UserAbort("jrnl NOT upgraded, exiting.")
|
||||||
|
|
||||||
for journal_name, path in encrypted_journals.items():
|
for journal_name, path in encrypted_journals.items():
|
||||||
util.prompt("\nUpgrading encrypted '{}' journal stored in {}...".format(journal_name, path))
|
print(
|
||||||
|
f"\nUpgrading encrypted '{journal_name}' journal stored in {path}...",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
backup(path, binary=True)
|
backup(path, binary=True)
|
||||||
old_journal = Journal.open_journal(journal_name, util.scope_config(config, journal_name), legacy=True)
|
old_journal = Journal.open_journal(
|
||||||
|
journal_name, util.scope_config(config, journal_name), legacy=True
|
||||||
|
)
|
||||||
all_journals.append(EncryptedJournal.from_journal(old_journal))
|
all_journals.append(EncryptedJournal.from_journal(old_journal))
|
||||||
|
|
||||||
for journal_name, path in plain_journals.items():
|
for journal_name, path in plain_journals.items():
|
||||||
util.prompt("\nUpgrading plain text '{}' journal stored in {}...".format(journal_name, path))
|
print(
|
||||||
|
f"\nUpgrading plain text '{journal_name}' journal stored in {path}...",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
backup(path)
|
backup(path)
|
||||||
old_journal = Journal.open_journal(journal_name, util.scope_config(config, journal_name), legacy=True)
|
old_journal = Journal.open_journal(
|
||||||
|
journal_name, util.scope_config(config, journal_name), legacy=True
|
||||||
|
)
|
||||||
all_journals.append(Journal.PlainJournal.from_journal(old_journal))
|
all_journals.append(Journal.PlainJournal.from_journal(old_journal))
|
||||||
|
|
||||||
# loop through lists to validate
|
# loop through lists to validate
|
||||||
failed_journals = [j for j in all_journals if not j.validate_parsing()]
|
failed_journals = [j for j in all_journals if not j.validate_parsing()]
|
||||||
|
|
||||||
if len(failed_journals) > 0:
|
if len(failed_journals) > 0:
|
||||||
util.prompt("\nThe following journal{} failed to upgrade:\n{}".format(
|
print(
|
||||||
's' if len(failed_journals) > 1 else '', "\n".join(j.name for j in failed_journals))
|
"\nThe following journal{} failed to upgrade:\n{}".format(
|
||||||
|
"s" if len(failed_journals) > 1 else "",
|
||||||
|
"\n".join(j.name for j in failed_journals),
|
||||||
|
),
|
||||||
|
file=sys.stderr,
|
||||||
)
|
)
|
||||||
|
|
||||||
raise UpgradeValidationException
|
raise UpgradeValidationException
|
||||||
|
@ -110,11 +143,13 @@ older versions of jrnl anymore.
|
||||||
for j in all_journals:
|
for j in all_journals:
|
||||||
j.write()
|
j.write()
|
||||||
|
|
||||||
util.prompt("\nUpgrading config...")
|
print("\nUpgrading config...", file=sys.stderr)
|
||||||
backup(config_path)
|
backup(config_path)
|
||||||
|
|
||||||
util.prompt("\nWe're all done here and you can start enjoying jrnl 2.".format(config_path))
|
print("\nWe're all done here and you can start enjoying jrnl 2.", file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
class UpgradeValidationException(Exception):
|
class UpgradeValidationException(Exception):
|
||||||
"""Raised when the contents of an upgraded journal do not match the old journal"""
|
"""Raised when the contents of an upgraded journal do not match the old journal"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
189
jrnl/util.py
189
jrnl/util.py
|
@ -1,42 +1,32 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# encoding: utf-8
|
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
from __future__ import absolute_import
|
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
import getpass as gp
|
import getpass as gp
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
if "win32" in sys.platform:
|
if "win32" in sys.platform:
|
||||||
import colorama
|
import colorama
|
||||||
|
|
||||||
colorama.init()
|
colorama.init()
|
||||||
import re
|
import re
|
||||||
import tempfile
|
import tempfile
|
||||||
import subprocess
|
import subprocess
|
||||||
import codecs
|
|
||||||
import unicodedata
|
import unicodedata
|
||||||
import shlex
|
import shlex
|
||||||
import logging
|
import logging
|
||||||
|
from typing import Optional, Callable
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
PY3 = sys.version_info[0] == 3
|
|
||||||
PY2 = sys.version_info[0] == 2
|
|
||||||
STDIN = sys.stdin
|
|
||||||
STDERR = sys.stderr
|
|
||||||
STDOUT = sys.stdout
|
|
||||||
TEST = False
|
|
||||||
__cached_tz = None
|
|
||||||
|
|
||||||
WARNING_COLOR = "\033[33m"
|
WARNING_COLOR = "\033[33m"
|
||||||
ERROR_COLOR = "\033[31m"
|
ERROR_COLOR = "\033[31m"
|
||||||
RESET_COLOR = "\033[0m"
|
RESET_COLOR = "\033[0m"
|
||||||
|
|
||||||
# Based on Segtok by Florian Leitner
|
# Based on Segtok by Florian Leitner
|
||||||
# https://github.com/fnl/segtok
|
# https://github.com/fnl/segtok
|
||||||
SENTENCE_SPLITTER = re.compile(r"""
|
SENTENCE_SPLITTER = re.compile(
|
||||||
|
r"""
|
||||||
( # A sentence ends at one of two sequences:
|
( # A sentence ends at one of two sequences:
|
||||||
[.!?\u203C\u203D\u2047\u2048\u2049\u3002\uFE52\uFE57\uFF01\uFF0E\uFF1F\uFF61] # Either, a sequence starting with a sentence terminal,
|
[.!?\u203C\u203D\u2047\u2048\u2049\u3002\uFE52\uFE57\uFF01\uFF0E\uFF1F\uFF61] # Either, a sequence starting with a sentence terminal,
|
||||||
[\'\u2019\"\u201D]? # an optional right quote,
|
[\'\u2019\"\u201D]? # an optional right quote,
|
||||||
|
@ -44,101 +34,85 @@ SENTENCE_SPLITTER = re.compile(r"""
|
||||||
\s+ # a sequence of required spaces.
|
\s+ # a sequence of required spaces.
|
||||||
| # Otherwise,
|
| # Otherwise,
|
||||||
\n # a sentence also terminates newlines.
|
\n # a sentence also terminates newlines.
|
||||||
)""", re.UNICODE | re.VERBOSE)
|
)""",
|
||||||
|
re.VERBOSE,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class UserAbort(Exception):
|
class UserAbort(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def getpass(prompt="Password: "):
|
def create_password(
|
||||||
if not TEST:
|
journal_name: str, prompt: str = "Enter password for new journal: "
|
||||||
return gp.getpass(bytes(prompt))
|
) -> str:
|
||||||
|
while True:
|
||||||
|
pw = gp.getpass(prompt)
|
||||||
|
if not pw:
|
||||||
|
print("Password can't be an empty string!", file=sys.stderr)
|
||||||
|
continue
|
||||||
|
elif pw == gp.getpass("Enter password again: "):
|
||||||
|
break
|
||||||
|
|
||||||
|
print("Passwords did not match, please try again", file=sys.stderr)
|
||||||
|
|
||||||
|
if yesno("Do you want to store the password in your keychain?", default=True):
|
||||||
|
set_keychain(journal_name, pw)
|
||||||
else:
|
else:
|
||||||
return py23_input(prompt)
|
set_keychain(journal_name, None)
|
||||||
|
|
||||||
|
return pw
|
||||||
|
|
||||||
|
|
||||||
def get_password(validator, keychain=None, max_attempts=3):
|
def decrypt_content(
|
||||||
|
decrypt_func: Callable[[str], Optional[str]],
|
||||||
|
keychain: str = None,
|
||||||
|
max_attempts: int = 3,
|
||||||
|
) -> str:
|
||||||
pwd_from_keychain = keychain and get_keychain(keychain)
|
pwd_from_keychain = keychain and get_keychain(keychain)
|
||||||
password = pwd_from_keychain or getpass()
|
password = pwd_from_keychain or gp.getpass()
|
||||||
result = validator(password)
|
result = decrypt_func(password)
|
||||||
# Password is bad:
|
# Password is bad:
|
||||||
if result is None and pwd_from_keychain:
|
if result is None and pwd_from_keychain:
|
||||||
set_keychain(keychain, None)
|
set_keychain(keychain, None)
|
||||||
attempt = 1
|
attempt = 1
|
||||||
while result is None and attempt < max_attempts:
|
while result is None and attempt < max_attempts:
|
||||||
prompt("Wrong password, try again.")
|
print("Wrong password, try again.", file=sys.stderr)
|
||||||
password = getpass()
|
password = gp.getpass()
|
||||||
result = validator(password)
|
result = decrypt_func(password)
|
||||||
attempt += 1
|
attempt += 1
|
||||||
if result is not None:
|
if result is not None:
|
||||||
return result
|
return result
|
||||||
else:
|
else:
|
||||||
prompt("Extremely wrong password.")
|
print("Extremely wrong password.", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
def get_keychain(journal_name):
|
def get_keychain(journal_name):
|
||||||
import keyring
|
import keyring
|
||||||
return keyring.get_password('jrnl', journal_name)
|
|
||||||
|
try:
|
||||||
|
return keyring.get_password("jrnl", journal_name)
|
||||||
|
except RuntimeError:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def set_keychain(journal_name, password):
|
def set_keychain(journal_name, password):
|
||||||
import keyring
|
import keyring
|
||||||
|
|
||||||
if password is None:
|
if password is None:
|
||||||
try:
|
try:
|
||||||
keyring.delete_password('jrnl', journal_name)
|
keyring.delete_password("jrnl", journal_name)
|
||||||
except:
|
except keyring.errors.PasswordDeleteError:
|
||||||
pass
|
pass
|
||||||
elif not TEST:
|
else:
|
||||||
keyring.set_password('jrnl', journal_name, password)
|
keyring.set_password("jrnl", journal_name, password)
|
||||||
|
|
||||||
|
|
||||||
def u(s):
|
|
||||||
"""Mock unicode function for python 2 and 3 compatibility."""
|
|
||||||
if not isinstance(s, str):
|
|
||||||
s = str(s)
|
|
||||||
return s if PY3 or type(s) is unicode else s.decode("utf-8")
|
|
||||||
|
|
||||||
|
|
||||||
def py2encode(s):
|
|
||||||
"""Encodes to UTF-8 in Python 2 but not in Python 3."""
|
|
||||||
return s.encode("utf-8") if PY2 and type(s) is unicode else s
|
|
||||||
|
|
||||||
|
|
||||||
def bytes(s):
|
|
||||||
"""Returns bytes, no matter what."""
|
|
||||||
if PY3:
|
|
||||||
return s.encode("utf-8") if type(s) is not bytes else s
|
|
||||||
return s.encode("utf-8") if type(s) is unicode else s
|
|
||||||
|
|
||||||
|
|
||||||
def prnt(s):
|
|
||||||
"""Encode and print a string"""
|
|
||||||
STDOUT.write(u(s + "\n"))
|
|
||||||
|
|
||||||
|
|
||||||
def prompt(msg):
|
|
||||||
"""Prints a message to the std err stream defined in util."""
|
|
||||||
if not msg.endswith("\n"):
|
|
||||||
msg += "\n"
|
|
||||||
STDERR.write(u(msg))
|
|
||||||
|
|
||||||
|
|
||||||
def py23_input(msg=""):
|
|
||||||
prompt(msg)
|
|
||||||
return STDIN.readline().strip()
|
|
||||||
|
|
||||||
|
|
||||||
def py23_read(msg=""):
|
|
||||||
print(msg)
|
|
||||||
return STDIN.read()
|
|
||||||
|
|
||||||
|
|
||||||
def yesno(prompt, default=True):
|
def yesno(prompt, default=True):
|
||||||
prompt = prompt.strip() + (" [Y/n]" if default else " [y/N]")
|
prompt = f"{prompt.strip()} {'[Y/n]' if default else '[y/N]'} "
|
||||||
raw = py23_input(prompt)
|
response = input(prompt)
|
||||||
return {'y': True, 'n': False}.get(raw.lower(), default)
|
return {"y": True, "n": False}.get(response.lower(), default)
|
||||||
|
|
||||||
|
|
||||||
def load_config(config_path):
|
def load_config(config_path):
|
||||||
|
@ -149,66 +123,61 @@ def load_config(config_path):
|
||||||
|
|
||||||
|
|
||||||
def scope_config(config, journal_name):
|
def scope_config(config, journal_name):
|
||||||
if journal_name not in config['journals']:
|
if journal_name not in config["journals"]:
|
||||||
return config
|
return config
|
||||||
config = config.copy()
|
config = config.copy()
|
||||||
journal_conf = config['journals'].get(journal_name)
|
journal_conf = config["journals"].get(journal_name)
|
||||||
if type(journal_conf) is dict: # We can override the default config on a by-journal basis
|
if (
|
||||||
log.debug('Updating configuration with specific journal overrides %s', journal_conf)
|
type(journal_conf) is dict
|
||||||
|
): # We can override the default config on a by-journal basis
|
||||||
|
log.debug(
|
||||||
|
"Updating configuration with specific journal overrides %s", journal_conf
|
||||||
|
)
|
||||||
config.update(journal_conf)
|
config.update(journal_conf)
|
||||||
else: # But also just give them a string to point to the journal file
|
else: # But also just give them a string to point to the journal file
|
||||||
config['journal'] = journal_conf
|
config["journal"] = journal_conf
|
||||||
config.pop('journals')
|
config.pop("journals")
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
||||||
def get_text_from_editor(config, template=""):
|
def get_text_from_editor(config, template=""):
|
||||||
filehandle, tmpfile = tempfile.mkstemp(prefix="jrnl", text=True, suffix=".txt")
|
filehandle, tmpfile = tempfile.mkstemp(prefix="jrnl", text=True, suffix=".txt")
|
||||||
with codecs.open(tmpfile, 'w', "utf-8") as f:
|
os.close(filehandle)
|
||||||
|
|
||||||
|
with open(tmpfile, "w", encoding="utf-8") as f:
|
||||||
if template:
|
if template:
|
||||||
f.write(template)
|
f.write(template)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
subprocess.call(shlex.split(config['editor'], posix="win" not in sys.platform) + [tmpfile])
|
subprocess.call(
|
||||||
|
shlex.split(config["editor"], posix="win" not in sys.platform) + [tmpfile]
|
||||||
|
)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
subprocess.call(config['editor'] + [tmpfile])
|
subprocess.call(config["editor"] + [tmpfile])
|
||||||
with codecs.open(tmpfile, "r", "utf-8") as f:
|
|
||||||
|
with open(tmpfile, "r", encoding="utf-8") as f:
|
||||||
raw = f.read()
|
raw = f.read()
|
||||||
os.close(filehandle)
|
|
||||||
os.remove(tmpfile)
|
os.remove(tmpfile)
|
||||||
|
|
||||||
if not raw:
|
if not raw:
|
||||||
prompt('[Nothing saved to file]')
|
print("[Nothing saved to file]", file=sys.stderr)
|
||||||
|
|
||||||
return raw
|
return raw
|
||||||
|
|
||||||
|
|
||||||
def colorize(string):
|
def colorize(string):
|
||||||
"""Returns the string wrapped in cyan ANSI escape"""
|
"""Returns the string wrapped in cyan ANSI escape"""
|
||||||
return u"\033[36m{}\033[39m".format(string)
|
return f"\033[36m{string}\033[39m"
|
||||||
|
|
||||||
|
|
||||||
def slugify(string):
|
def slugify(string):
|
||||||
"""Slugifies a string.
|
"""Slugifies a string.
|
||||||
Based on public domain code from https://github.com/zacharyvoase/slugify
|
Based on public domain code from https://github.com/zacharyvoase/slugify
|
||||||
and ported to deal with all kinds of python 2 and 3 strings
|
|
||||||
"""
|
"""
|
||||||
string = u(string)
|
normalized_string = str(unicodedata.normalize("NFKD", string))
|
||||||
ascii_string = str(unicodedata.normalize('NFKD', string).encode('ascii', 'ignore'))
|
no_punctuation = re.sub(r"[^\w\s-]", "", normalized_string).strip().lower()
|
||||||
if PY3:
|
slug = re.sub(r"[-\s]+", "-", no_punctuation)
|
||||||
ascii_string = ascii_string[1:] # removed the leading 'b'
|
return slug
|
||||||
no_punctuation = re.sub(r'[^\w\s-]', '', ascii_string).strip().lower()
|
|
||||||
slug = re.sub(r'[-\s]+', '-', no_punctuation)
|
|
||||||
return u(slug)
|
|
||||||
|
|
||||||
|
|
||||||
def int2byte(i):
|
|
||||||
"""Converts an integer to a byte.
|
|
||||||
This is equivalent to chr() in Python 2 and bytes((i,)) in Python 3."""
|
|
||||||
return chr(i) if PY2 else bytes((i,))
|
|
||||||
|
|
||||||
|
|
||||||
def byte2int(b):
|
|
||||||
"""Converts a byte to an integer.
|
|
||||||
This is equivalent to ord(bs[0]) on Python 2 and bs[0] on Python 3."""
|
|
||||||
return ord(b)if PY2 else b
|
|
||||||
|
|
||||||
|
|
||||||
def split_title(text):
|
def split_title(text):
|
||||||
|
@ -216,4 +185,4 @@ def split_title(text):
|
||||||
punkt = SENTENCE_SPLITTER.search(text)
|
punkt = SENTENCE_SPLITTER.search(text)
|
||||||
if not punkt:
|
if not punkt:
|
||||||
return text, ""
|
return text, ""
|
||||||
return text[:punkt.end()].strip(), text[punkt.end():].strip()
|
return text[: punkt.end()].strip(), text[punkt.end() :].strip()
|
||||||
|
|
180
poetry.lock
generated
180
poetry.lock
generated
|
@ -6,25 +6,13 @@ optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
version = "1.4.3"
|
version = "1.4.3"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
category = "main"
|
|
||||||
description = "Fast ASN.1 parser and serializer with definitions for private keys, public keys, certificates, CRL, OCSP, CMS, PKCS#3, PKCS#7, PKCS#8, PKCS#12, PKCS#5, X.509 and TSP"
|
|
||||||
name = "asn1crypto"
|
|
||||||
optional = false
|
|
||||||
python-versions = "*"
|
|
||||||
version = "0.24.0"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
category = "main"
|
category = "main"
|
||||||
description = "Safe, minimalistic evaluator of python expression using ast module"
|
description = "Safe, minimalistic evaluator of python expression using ast module"
|
||||||
name = "asteval"
|
name = "asteval"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
python-versions = ">=3.5"
|
||||||
version = "0.9.14"
|
version = "0.9.18"
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
numpy = "*"
|
|
||||||
six = "*"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
category = "dev"
|
category = "dev"
|
||||||
|
@ -32,7 +20,7 @@ description = "Classes Without Boilerplate"
|
||||||
name = "attrs"
|
name = "attrs"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||||
version = "19.1.0"
|
version = "19.3.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
category = "dev"
|
category = "dev"
|
||||||
|
@ -53,13 +41,16 @@ description = "The uncompromising code formatter."
|
||||||
name = "black"
|
name = "black"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.6"
|
python-versions = ">=3.6"
|
||||||
version = "18.9b0"
|
version = "19.10b0"
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
appdirs = "*"
|
appdirs = "*"
|
||||||
attrs = ">=17.4.0"
|
attrs = ">=18.1.0"
|
||||||
click = ">=6.5"
|
click = ">=6.5"
|
||||||
|
pathspec = ">=0.6,<1"
|
||||||
|
regex = "*"
|
||||||
toml = ">=0.9.4"
|
toml = ">=0.9.4"
|
||||||
|
typed-ast = ">=1.4.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
category = "main"
|
category = "main"
|
||||||
|
@ -67,7 +58,7 @@ description = "Foreign Function Interface for Python calling C code."
|
||||||
name = "cffi"
|
name = "cffi"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
version = "1.12.3"
|
version = "1.13.2"
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
pycparser = "*"
|
pycparser = "*"
|
||||||
|
@ -86,8 +77,8 @@ description = "Cross-platform colored terminal text."
|
||||||
marker = "sys_platform == \"win32\""
|
marker = "sys_platform == \"win32\""
|
||||||
name = "colorama"
|
name = "colorama"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||||
version = "0.4.1"
|
version = "0.4.3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
category = "main"
|
category = "main"
|
||||||
|
@ -95,15 +86,14 @@ description = "cryptography is a package which provides cryptographic recipes an
|
||||||
name = "cryptography"
|
name = "cryptography"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*"
|
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*"
|
||||||
version = "2.7"
|
version = "2.8"
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
asn1crypto = ">=0.21.0"
|
|
||||||
cffi = ">=1.8,<1.11.3 || >1.11.3"
|
cffi = ">=1.8,<1.11.3 || >1.11.3"
|
||||||
six = ">=1.4.1"
|
six = ">=1.4.1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
category = "main"
|
category = "dev"
|
||||||
description = "Discover and load entry points from installed packages."
|
description = "Discover and load entry points from installed packages."
|
||||||
name = "entrypoints"
|
name = "entrypoints"
|
||||||
optional = false
|
optional = false
|
||||||
|
@ -116,7 +106,7 @@ description = "the modular source code checker: pep8, pyflakes and co"
|
||||||
name = "flake8"
|
name = "flake8"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||||
version = "3.7.8"
|
version = "3.7.9"
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
entrypoints = ">=0.3.0,<0.4.0"
|
entrypoints = ">=0.3.0,<0.4.0"
|
||||||
|
@ -126,11 +116,15 @@ pyflakes = ">=2.1.0,<2.2.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
category = "main"
|
category = "main"
|
||||||
description = "Clean single-source support for Python 3 and 2"
|
description = "Read metadata from Python packages"
|
||||||
name = "future"
|
marker = "python_version < \"3.8\""
|
||||||
|
name = "importlib-metadata"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
|
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
|
||||||
version = "0.17.1"
|
version = "1.3.0"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
zipp = ">=0.5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
category = "main"
|
category = "main"
|
||||||
|
@ -139,15 +133,15 @@ marker = "sys_platform == \"linux\""
|
||||||
name = "jeepney"
|
name = "jeepney"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.5"
|
python-versions = ">=3.5"
|
||||||
version = "0.4"
|
version = "0.4.2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
category = "dev"
|
category = "dev"
|
||||||
description = "A small but fast and easy to use stand-alone template engine written in pure python."
|
description = "A very fast and expressive template engine."
|
||||||
name = "jinja2"
|
name = "jinja2"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
version = "2.10.1"
|
version = "2.10.3"
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
MarkupSafe = ">=0.23"
|
MarkupSafe = ">=0.23"
|
||||||
|
@ -158,13 +152,16 @@ description = "Store and access your passwords safely."
|
||||||
name = "keyring"
|
name = "keyring"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.5"
|
python-versions = ">=3.5"
|
||||||
version = "19.0.2"
|
version = "19.3.0"
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
entrypoints = "*"
|
|
||||||
pywin32-ctypes = "<0.1.0 || >0.1.0,<0.1.1 || >0.1.1"
|
pywin32-ctypes = "<0.1.0 || >0.1.0,<0.1.1 || >0.1.1"
|
||||||
secretstorage = "*"
|
secretstorage = "*"
|
||||||
|
|
||||||
|
[package.dependencies.importlib-metadata]
|
||||||
|
python = "<3.8"
|
||||||
|
version = "*"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
category = "dev"
|
category = "dev"
|
||||||
description = "Python LiveReload is an awesome tool for web developers"
|
description = "Python LiveReload is an awesome tool for web developers"
|
||||||
|
@ -222,11 +219,12 @@ tornado = ">=5.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
category = "main"
|
category = "main"
|
||||||
description = "NumPy is the fundamental package for array computing with Python."
|
description = "More routines for operating on iterables, beyond itertools"
|
||||||
name = "numpy"
|
marker = "python_version < \"3.8\""
|
||||||
|
name = "more-itertools"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*"
|
python-versions = ">=3.5"
|
||||||
version = "1.16.4"
|
version = "8.0.2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
category = "dev"
|
category = "dev"
|
||||||
|
@ -234,7 +232,7 @@ description = "parse() is the opposite of format()"
|
||||||
name = "parse"
|
name = "parse"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
version = "1.12.0"
|
version = "1.14.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
category = "dev"
|
category = "dev"
|
||||||
|
@ -242,10 +240,10 @@ description = "Simplifies to build parse types based on the parse module"
|
||||||
name = "parse-type"
|
name = "parse-type"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=2.6, !=3.0.*, !=3.1.*"
|
python-versions = ">=2.6, !=3.0.*, !=3.1.*"
|
||||||
version = "0.4.2"
|
version = "0.5.2"
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
parse = ">=1.8"
|
parse = ">=1.8.4"
|
||||||
six = ">=1.11"
|
six = ">=1.11"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -254,10 +252,7 @@ description = "Parse human-readable date/time text."
|
||||||
name = "parsedatetime"
|
name = "parsedatetime"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
version = "2.4"
|
version = "2.5"
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
future = "*"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
category = "main"
|
category = "main"
|
||||||
|
@ -265,7 +260,15 @@ description = "comprehensive password hashing framework supporting over 30 schem
|
||||||
name = "passlib"
|
name = "passlib"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
version = "1.7.1"
|
version = "1.7.2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
category = "dev"
|
||||||
|
description = "Utility library for gitignore style pattern matching of file paths."
|
||||||
|
name = "pathspec"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||||
|
version = "0.7.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
category = "dev"
|
category = "dev"
|
||||||
|
@ -296,8 +299,8 @@ category = "main"
|
||||||
description = "Extensions to the standard Python datetime module"
|
description = "Extensions to the standard Python datetime module"
|
||||||
name = "python-dateutil"
|
name = "python-dateutil"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
|
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
|
||||||
version = "2.8.0"
|
version = "2.8.1"
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
six = ">=1.5"
|
six = ">=1.5"
|
||||||
|
@ -308,7 +311,7 @@ description = "World timezone definitions, modern and historical"
|
||||||
name = "pytz"
|
name = "pytz"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
version = "2019.1"
|
version = "2019.3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
category = "main"
|
category = "main"
|
||||||
|
@ -333,7 +336,15 @@ description = "YAML parser and emitter for Python"
|
||||||
name = "pyyaml"
|
name = "pyyaml"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||||
version = "5.1.2"
|
version = "5.2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
category = "dev"
|
||||||
|
description = "Alternative regular expression module, to replace re."
|
||||||
|
name = "regex"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
version = "2019.12.20"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
category = "main"
|
category = "main"
|
||||||
|
@ -354,7 +365,7 @@ description = "Python 2 and 3 compatibility utilities"
|
||||||
name = "six"
|
name = "six"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=2.6, !=3.0.*, !=3.1.*"
|
python-versions = ">=2.6, !=3.0.*, !=3.1.*"
|
||||||
version = "1.12.0"
|
version = "1.13.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
category = "dev"
|
category = "dev"
|
||||||
|
@ -372,6 +383,14 @@ optional = false
|
||||||
python-versions = ">= 3.5"
|
python-versions = ">= 3.5"
|
||||||
version = "6.0.3"
|
version = "6.0.3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
category = "dev"
|
||||||
|
description = "a fork of Python 2 and 3 ast modules with type comment support"
|
||||||
|
name = "typed-ast"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
version = "1.4.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
category = "main"
|
category = "main"
|
||||||
description = "tzinfo object for the local timezone"
|
description = "tzinfo object for the local timezone"
|
||||||
|
@ -383,47 +402,62 @@ version = "1.5.1"
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
pytz = "*"
|
pytz = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
category = "main"
|
||||||
|
description = "Backport of pathlib-compatible object wrapper for zip files"
|
||||||
|
marker = "python_version < \"3.8\""
|
||||||
|
name = "zipp"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7"
|
||||||
|
version = "0.6.0"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
more-itertools = "*"
|
||||||
|
|
||||||
[metadata]
|
[metadata]
|
||||||
content-hash = "9896cf59c7552b6ad95219ee5555c7445a3fab39c2e4f4c6f3d991a36635e44b"
|
content-hash = "98e23837423d5d8621f14cbe592d209ef98e1926b7a3f94e0f88bb6be908aae8"
|
||||||
python-versions = "^3.7"
|
python-versions = ">=3.6.0, <3.9.0"
|
||||||
|
|
||||||
[metadata.hashes]
|
[metadata.hashes]
|
||||||
appdirs = ["9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92", "d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"]
|
appdirs = ["9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92", "d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"]
|
||||||
asn1crypto = ["2f1adbb7546ed199e3c90ef23ec95c5cf3585bac7d11fb7eb562a3fe89c64e87", "9d5c20441baf0cb60a4ac34cc447c6c189024b6b4c6cd7877034f4965c464e49"]
|
asteval = ["5d64e18b8a72c2c7ae8f9b70d1f80b68bbcaa98c1c0d7047c35489d03209bc86"]
|
||||||
asteval = ["7c81fee6707a7a28e8beae891b858535a7e61f9ce275a0a4cf5f428fbc934cb8"]
|
attrs = ["08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c", "f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"]
|
||||||
attrs = ["69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79", "f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399"]
|
|
||||||
behave = ["b9662327aa53294c1351b0a9c369093ccec1d21026f050c3bd9b3e5cccf81a86", "ebda1a6c9e5bfe95c5f9f0a2794e01c7098b3dde86c10a95d8621c5907ff6f1c"]
|
behave = ["b9662327aa53294c1351b0a9c369093ccec1d21026f050c3bd9b3e5cccf81a86", "ebda1a6c9e5bfe95c5f9f0a2794e01c7098b3dde86c10a95d8621c5907ff6f1c"]
|
||||||
black = ["817243426042db1d36617910df579a54f1afd659adb96fc5032fcf4b36209739", "e030a9a28f542debc08acceb273f228ac422798e5215ba2a791a6ddeaaca22a5"]
|
black = ["1b30e59be925fafc1ee4565e5e08abef6b03fe455102883820fe5ee2e4734e0b", "c2edb73a08e9e0e6f65a0e6af18b059b8b1cdd5bef997d7a0b181df93dc81539"]
|
||||||
cffi = ["041c81822e9f84b1d9c401182e174996f0bae9991f33725d059b771744290774", "046ef9a22f5d3eed06334d01b1e836977eeef500d9b78e9ef693f9380ad0b83d", "066bc4c7895c91812eff46f4b1c285220947d4aa46fa0a2651ff85f2afae9c90", "066c7ff148ae33040c01058662d6752fd73fbc8e64787229ea8498c7d7f4041b", "2444d0c61f03dcd26dbf7600cf64354376ee579acad77aef459e34efcb438c63", "300832850b8f7967e278870c5d51e3819b9aad8f0a2c8dbe39ab11f119237f45", "34c77afe85b6b9e967bd8154e3855e847b70ca42043db6ad17f26899a3df1b25", "46de5fa00f7ac09f020729148ff632819649b3e05a007d286242c4882f7b1dc3", "4aa8ee7ba27c472d429b980c51e714a24f47ca296d53f4d7868075b175866f4b", "4d0004eb4351e35ed950c14c11e734182591465a33e960a4ab5e8d4f04d72647", "4e3d3f31a1e202b0f5a35ba3bc4eb41e2fc2b11c1eff38b362de710bcffb5016", "50bec6d35e6b1aaeb17f7c4e2b9374ebf95a8975d57863546fa83e8d31bdb8c4", "55cad9a6df1e2a1d62063f79d0881a414a906a6962bc160ac968cc03ed3efcfb", "5662ad4e4e84f1eaa8efce5da695c5d2e229c563f9d5ce5b0113f71321bcf753", "59b4dc008f98fc6ee2bb4fd7fc786a8d70000d058c2bbe2698275bc53a8d3fa7", "73e1ffefe05e4ccd7bcea61af76f36077b914f92b76f95ccf00b0c1b9186f3f9", "a1f0fd46eba2d71ce1589f7e50a9e2ffaeb739fb2c11e8192aa2b45d5f6cc41f", "a2e85dc204556657661051ff4bab75a84e968669765c8a2cd425918699c3d0e8", "a5457d47dfff24882a21492e5815f891c0ca35fefae8aa742c6c263dac16ef1f", "a8dccd61d52a8dae4a825cdbb7735da530179fea472903eb871a5513b5abbfdc", "ae61af521ed676cf16ae94f30fe202781a38d7178b6b4ab622e4eec8cefaff42", "b012a5edb48288f77a63dba0840c92d0504aa215612da4541b7b42d849bc83a3", "d2c5cfa536227f57f97c92ac30c8109688ace8fa4ac086d19d0af47d134e2909", "d42b5796e20aacc9d15e66befb7a345454eef794fdb0737d1af593447c6c8f45", "dee54f5d30d775f525894d67b1495625dd9322945e7fee00731952e0368ff42d", "e070535507bd6aa07124258171be2ee8dfc19119c28ca94c9dfb7efd23564512", "e1ff2748c84d97b065cc95429814cdba39bcbd77c9c85c89344b317dc0d9cbff", "ed851c75d1e0e043cbf5ca9a8e1b13c4c90f3fbd863dacb01c0808e2b5204201"]
|
cffi = ["0b49274afc941c626b605fb59b59c3485c17dc776dc3cc7cc14aca74cc19cc42", "0e3ea92942cb1168e38c05c1d56b0527ce31f1a370f6117f1d490b8dcd6b3a04", "135f69aecbf4517d5b3d6429207b2dff49c876be724ac0c8bf8e1ea99df3d7e5", "19db0cdd6e516f13329cba4903368bff9bb5a9331d3410b1b448daaadc495e54", "2781e9ad0e9d47173c0093321bb5435a9dfae0ed6a762aabafa13108f5f7b2ba", "291f7c42e21d72144bb1c1b2e825ec60f46d0a7468f5346841860454c7aa8f57", "2c5e309ec482556397cb21ede0350c5e82f0eb2621de04b2633588d118da4396", "2e9c80a8c3344a92cb04661115898a9129c074f7ab82011ef4b612f645939f12", "32a262e2b90ffcfdd97c7a5e24a6012a43c61f1f5a57789ad80af1d26c6acd97", "3c9fff570f13480b201e9ab69453108f6d98244a7f495e91b6c654a47486ba43", "415bdc7ca8c1c634a6d7163d43fb0ea885a07e9618a64bda407e04b04333b7db", "42194f54c11abc8583417a7cf4eaff544ce0de8187abaf5d29029c91b1725ad3", "4424e42199e86b21fc4db83bd76909a6fc2a2aefb352cb5414833c030f6ed71b", "4a43c91840bda5f55249413037b7a9b79c90b1184ed504883b72c4df70778579", "599a1e8ff057ac530c9ad1778293c665cb81a791421f46922d80a86473c13346", "5c4fae4e9cdd18c82ba3a134be256e98dc0596af1e7285a3d2602c97dcfa5159", "5ecfa867dea6fabe2a58f03ac9186ea64da1386af2159196da51c4904e11d652", "62f2578358d3a92e4ab2d830cd1c2049c9c0d0e6d3c58322993cc341bdeac22e", "6471a82d5abea994e38d2c2abc77164b4f7fbaaf80261cb98394d5793f11b12a", "6d4f18483d040e18546108eb13b1dfa1000a089bcf8529e30346116ea6240506", "71a608532ab3bd26223c8d841dde43f3516aa5d2bf37b50ac410bb5e99053e8f", "74a1d8c85fb6ff0b30fbfa8ad0ac23cd601a138f7509dc617ebc65ef305bb98d", "7b93a885bb13073afb0aa73ad82059a4c41f4b7d8eb8368980448b52d4c7dc2c", "7d4751da932caaec419d514eaa4215eaf14b612cff66398dd51129ac22680b20", "7f627141a26b551bdebbc4855c1157feeef18241b4b8366ed22a5c7d672ef858", "8169cf44dd8f9071b2b9248c35fc35e8677451c52f795daa2bb4643f32a540bc", "aa00d66c0fab27373ae44ae26a66a9e43ff2a678bf63a9c7c1a9a4d61172827a", "ccb032fda0873254380aa2bfad2582aedc2959186cce61e3a17abc1a55ff89c3", "d754f39e0d1603b5b24a7f8484b22d2904fa551fe865fd0d4c3332f078d20d4e", "d75c461e20e29afc0aee7172a0950157c704ff0dd51613506bd7d82b718e7410", "dcd65317dd15bc0451f3e01c80da2216a31916bdcffd6221ca1202d96584aa25", "e570d3ab32e2c2861c4ebe6ffcad6a8abf9347432a37608fe1fbd157b3f0036b", "fd43a88e045cf992ed09fa724b5315b790525f2676883a6ea64e3263bae6549d"]
|
||||||
click = ["2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", "5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"]
|
click = ["2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", "5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"]
|
||||||
colorama = ["05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d", "f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48"]
|
colorama = ["7d73d2a99753107a36ac6b455ee49046802e59d9d076ef8e47b61499fa29afff", "e96da0d330793e2cb9485e9ddfd918d456036c7149416295932478192f4436a1"]
|
||||||
cryptography = ["24b61e5fcb506424d3ec4e18bca995833839bf13c59fc43e530e488f28d46b8c", "25dd1581a183e9e7a806fe0543f485103232f940fcfc301db65e630512cce643", "3452bba7c21c69f2df772762be0066c7ed5dc65df494a1d53a58b683a83e1216", "41a0be220dd1ed9e998f5891948306eb8c812b512dc398e5a01846d855050799", "5751d8a11b956fbfa314f6553d186b94aa70fdb03d8a4d4f1c82dcacf0cbe28a", "5f61c7d749048fa6e3322258b4263463bfccefecb0dd731b6561cb617a1d9bb9", "72e24c521fa2106f19623a3851e9f89ddfdeb9ac63871c7643790f872a305dfc", "7b97ae6ef5cba2e3bb14256625423413d5ce8d1abb91d4f29b6d1a081da765f8", "961e886d8a3590fd2c723cf07be14e2a91cf53c25f02435c04d39e90780e3b53", "96d8473848e984184b6728e2c9d391482008646276c3ff084a1bd89e15ff53a1", "ae536da50c7ad1e002c3eee101871d93abdc90d9c5f651818450a0d3af718609", "b0db0cecf396033abb4a93c95d1602f268b3a68bb0a9cc06a7cff587bb9a7292", "cfee9164954c186b191b91d4193989ca994703b2fff406f71cf454a2d3c7327e", "e6347742ac8f35ded4a46ff835c60e68c22a536a8ae5c4422966d06946b6d4c6", "f27d93f0139a3c056172ebb5d4f9056e770fdf0206c2f422ff2ebbad142e09ed", "f57b76e46a58b63d1c6375017f4564a28f19a5ca912691fd2e4261b3414b618d"]
|
cryptography = ["02079a6addc7b5140ba0825f542c0869ff4df9a69c360e339ecead5baefa843c", "1df22371fbf2004c6f64e927668734070a8953362cd8370ddd336774d6743595", "369d2346db5934345787451504853ad9d342d7f721ae82d098083e1f49a582ad", "3cda1f0ed8747339bbdf71b9f38ca74c7b592f24f65cdb3ab3765e4b02871651", "44ff04138935882fef7c686878e1c8fd80a723161ad6a98da31e14b7553170c2", "4b1030728872c59687badcca1e225a9103440e467c17d6d1730ab3d2d64bfeff", "58363dbd966afb4f89b3b11dfb8ff200058fbc3b947507675c19ceb46104b48d", "6ec280fb24d27e3d97aa731e16207d58bd8ae94ef6eab97249a2afe4ba643d42", "7270a6c29199adc1297776937a05b59720e8a782531f1f122f2eb8467f9aab4d", "73fd30c57fa2d0a1d7a49c561c40c2f79c7d6c374cc7750e9ac7c99176f6428e", "7f09806ed4fbea8f51585231ba742b58cbcfbfe823ea197d8c89a5e433c7e912", "90df0cc93e1f8d2fba8365fb59a858f51a11a394d64dbf3ef844f783844cc793", "971221ed40f058f5662a604bd1ae6e4521d84e6cad0b7b170564cc34169c8f13", "a518c153a2b5ed6b8cc03f7ae79d5ffad7315ad4569b2d5333a13c38d64bd8d7", "b0de590a8b0979649ebeef8bb9f54394d3a41f66c5584fff4220901739b6b2f0", "b43f53f29816ba1db8525f006fa6f49292e9b029554b3eb56a189a70f2a40879", "d31402aad60ed889c7e57934a03477b572a03af7794fa8fb1780f21ea8f6551f", "de96157ec73458a7f14e3d26f17f8128c959084931e8997b9e655a39c8fde9f9", "df6b4dca2e11865e6cfbfb708e800efb18370f5a46fd601d3755bc7f85b3a8a2", "ecadccc7ba52193963c0475ac9f6fa28ac01e01349a2ca48509667ef41ffd2cf", "fb81c17e0ebe3358486cd8cc3ad78adbae58af12fc2bf2bc0bb84e8090fa5ce8"]
|
||||||
entrypoints = ["589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19", "c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"]
|
entrypoints = ["589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19", "c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451"]
|
||||||
flake8 = ["19241c1cbc971b9962473e4438a2ca19749a7dd002dd1a946eaba171b4114548", "8e9dfa3cecb2400b3738a42c54c3043e821682b9c840b0448c0503f781130696"]
|
flake8 = ["45681a117ecc81e870cbf1262835ae4af5e7a8b08e40b944a8a6e6b895914cfb", "49356e766643ad15072a789a20915d3c91dc89fd313ccd71802303fd67e4deca"]
|
||||||
future = ["67045236dcfd6816dc439556d009594abf643e5eb48992e36beac09c2ca659b8"]
|
importlib-metadata = ["073a852570f92da5f744a3472af1b61e28e9f78ccf0c9117658dc32b15de7b45", "d95141fbfa7ef2ec65cfd945e2af7e5a6ddbd7c8d9a25e66ff3be8e3daf9f60f"]
|
||||||
jeepney = ["6089412a5de162c04747f0220f6b2223b8ba660acd041e52a76426ca550e3c70", "f6f8b1428403b4afad04b6b82f9ab9fc426c253d7504c9031c41712a2c01dc74"]
|
jeepney = ["0ba6d8c597e9bef1ebd18aaec595f942a264e25c1a48f164d46120eacaa2e9bb", "6f45dce1125cf6c58a1c88123d3831f36a789f9204fbad3172eac15f8ccd08d0"]
|
||||||
jinja2 = ["065c4f02ebe7f7cf559e49ee5a95fb800a9e4528727aec6f24402a5374c65013", "14dd6caf1527abb21f08f86c784eac40853ba93edb79552aa1e4b8aef1b61c7b"]
|
jinja2 = ["74320bb91f31270f9551d46522e33af46a80c3d619f4a4bf42b3164d30b5911f", "9fe95f19286cfefaa917656583d020be14e7859c6b0252588391e47db34527de"]
|
||||||
keyring = ["1b74595f7439e4581a11d4f9a12790ac34addce64ca389c86272ff465f5e0b90", "afbfe7bc9bdba69d25c551b0c738adde533d87e0b51ad6bbe332cbea19ad8476"]
|
keyring = ["9b80469783d3f6106bce1d389c6b8b20c8d4d739943b1b8cd0ddc2a45d065f9d", "ee3d35b7f1ac3cb69e9a1e4323534649d3ab2fea402738a77e4250c152970fed"]
|
||||||
livereload = ["78d55f2c268a8823ba499305dcac64e28ddeb9a92571e12d543cd304faf5817b", "89254f78d7529d7ea0a3417d224c34287ebfe266b05e67e51facaf82c27f0f66"]
|
livereload = ["78d55f2c268a8823ba499305dcac64e28ddeb9a92571e12d543cd304faf5817b", "89254f78d7529d7ea0a3417d224c34287ebfe266b05e67e51facaf82c27f0f66"]
|
||||||
markdown = ["2e50876bcdd74517e7b71f3e7a76102050edec255b3983403f1a63e7c8a41e7a", "56a46ac655704b91e5b7e6326ce43d5ef72411376588afa1dd90e881b83c7e8c"]
|
markdown = ["2e50876bcdd74517e7b71f3e7a76102050edec255b3983403f1a63e7c8a41e7a", "56a46ac655704b91e5b7e6326ce43d5ef72411376588afa1dd90e881b83c7e8c"]
|
||||||
markupsafe = ["00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", "09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", "09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", "1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", "24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", "29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", "43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", "46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", "500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", "535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", "62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", "6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", "717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", "79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", "7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", "88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", "8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", "98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", "9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", "9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", "ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", "b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", "b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", "b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", "ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", "c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", "cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", "e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"]
|
markupsafe = ["00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", "09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", "09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", "1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", "24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", "29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", "43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", "46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", "500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", "535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", "62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", "6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", "717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", "79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", "7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", "88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", "8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", "98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", "9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", "9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", "ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", "b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", "b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", "b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", "ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", "c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", "cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", "e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7"]
|
||||||
mccabe = ["ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", "dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"]
|
mccabe = ["ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", "dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"]
|
||||||
mkdocs = ["17d34329aad75d5de604b9ed4e31df3a4d235afefdc46ce7b1964fddb2e1e939", "8cc8b38325456b9e942c981a209eaeb1e9f3f77b493ad755bfef889b9c8d356a"]
|
mkdocs = ["17d34329aad75d5de604b9ed4e31df3a4d235afefdc46ce7b1964fddb2e1e939", "8cc8b38325456b9e942c981a209eaeb1e9f3f77b493ad755bfef889b9c8d356a"]
|
||||||
numpy = ["0778076e764e146d3078b17c24c4d89e0ecd4ac5401beff8e1c87879043a0633", "141c7102f20abe6cf0d54c4ced8d565b86df4d3077ba2343b61a6db996cefec7", "14270a1ee8917d11e7753fb54fc7ffd1934f4d529235beec0b275e2ccf00333b", "27e11c7a8ec9d5838bc59f809bfa86efc8a4fd02e58960fa9c49d998e14332d5", "2a04dda79606f3d2f760384c38ccd3d5b9bb79d4c8126b67aff5eb09a253763e", "3c26010c1b51e1224a3ca6b8df807de6e95128b0908c7e34f190e7775455b0ca", "52c40f1a4262c896420c6ea1c6fda62cf67070e3947e3307f5562bd783a90336", "6e4f8d9e8aa79321657079b9ac03f3cf3fd067bf31c1cca4f56d49543f4356a5", "7242be12a58fec245ee9734e625964b97cf7e3f2f7d016603f9e56660ce479c7", "7dc253b542bfd4b4eb88d9dbae4ca079e7bf2e2afd819ee18891a43db66c60c7", "94f5bd885f67bbb25c82d80184abbf7ce4f6c3c3a41fbaa4182f034bba803e69", "a89e188daa119ffa0d03ce5123dee3f8ffd5115c896c2a9d4f0dbb3d8b95bfa3", "ad3399da9b0ca36e2f24de72f67ab2854a62e623274607e37e0ce5f5d5fa9166", "b0348be89275fd1d4c44ffa39530c41a21062f52299b1e3ee7d1c61f060044b8", "b5554368e4ede1856121b0dfa35ce71768102e4aa55e526cb8de7f374ff78722", "cbddc56b2502d3f87fda4f98d948eb5b11f36ff3902e17cb6cc44727f2200525", "d79f18f41751725c56eceab2a886f021d70fd70a6188fd386e29a045945ffc10", "dc2ca26a19ab32dc475dbad9dfe723d3a64c835f4c23f625c2b6566ca32b9f29", "dd9bcd4f294eb0633bb33d1a74febdd2b9018b8b8ed325f861fffcd2c7660bb8", "e8baab1bc7c9152715844f1faca6744f2416929de10d7639ed49555a85549f52", "ec31fe12668af687b99acf1567399632a7c47b0e17cfb9ae47c098644ef36797", "f12b4f7e2d8f9da3141564e6737d79016fe5336cc92de6814eba579744f65b0a", "f58ac38d5ca045a377b3b377c84df8175ab992c970a53332fa8ac2373df44ff7"]
|
more-itertools = ["b84b238cce0d9adad5ed87e745778d20a3f8487d0f0cb8b8a586816c7496458d", "c833ef592a0324bcc6a60e48440da07645063c453880c9477ceb22490aec1564"]
|
||||||
parse = ["1b68657434d371e5156048ca4a0c5aea5afc6ca59a2fea4dd1a575354f617142"]
|
parse = ["95a4f4469e37c57b5e924629ac99926f28bee7da59515dc5b8078c4c3e779249"]
|
||||||
parse-type = ["6e906a66f340252e4c324914a60d417d33a4bea01292ea9bbf68b4fc123be8c9", "f596bdc75d3dd93036fbfe3d04127da9f6df0c26c36e01e76da85adef4336b3c"]
|
parse-type = ["089a471b06327103865dfec2dd844230c3c658a4a1b5b4c8b6c16c8f77577f9e", "7f690b18d35048c15438d6d0571f9045cffbec5907e0b1ccf006f889e3a38c0b"]
|
||||||
parsedatetime = ["3d817c58fb9570d1eec1dd46fa9448cd644eeed4fb612684b02dfda3a79cb84b", "9ee3529454bf35c40a77115f5a596771e59e1aee8c53306f346c461b8e913094"]
|
parsedatetime = ["3b835fc54e472c17ef447be37458b400e3fefdf14bb1ffdedb5d2c853acf4ba1", "d2e9ddb1e463de871d32088a3f3cea3dc8282b1b2800e081bd0ef86900451667"]
|
||||||
passlib = ["3d948f64138c25633613f303bcc471126eae67c04d5e3f6b7b8ce6242f8653e0", "43526aea08fa32c6b6dbbbe9963c4c767285b78147b7437597f992812f69d280"]
|
passlib = ["68c35c98a7968850e17f1b6892720764cc7eed0ef2b7cb3116a89a28e43fe177", "8d666cef936198bc2ab47ee9b0410c94adf2ba798e5a84bf220be079ae7ab6a8"]
|
||||||
|
pathspec = ["163b0632d4e31cef212976cf57b43d9fd6b0bac6e67c26015d611a647d5e7424", "562aa70af2e0d434367d9790ad37aed893de47f1693e4201fd1d3dca15d19b96"]
|
||||||
pycodestyle = ["95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", "e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c"]
|
pycodestyle = ["95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", "e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c"]
|
||||||
pycparser = ["a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3"]
|
pycparser = ["a988718abfad80b6b157acce7bf130a30876d27603738ac39f140993246b25b3"]
|
||||||
pyflakes = ["17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0", "d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2"]
|
pyflakes = ["17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0", "d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2"]
|
||||||
python-dateutil = ["7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb", "c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e"]
|
python-dateutil = ["73ebfe9dbf22e832286dafa60473e4cd239f8592f699aa5adaf10050e6e1823c", "75bb3f31ea686f1197762692a9ee6a7550b59fc6ca3a1f4b5d7e32fb98e2da2a"]
|
||||||
pytz = ["303879e36b721603cc54604edcac9d20401bdbe31e1e4fdee5b9f98d5d31dfda", "d747dd3d23d77ef44c6a3526e274af6efeb0a6f1afd5a69ba4d5be4098c8e141"]
|
pytz = ["1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d", "b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be"]
|
||||||
pywin32-ctypes = ["24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942", "9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"]
|
pywin32-ctypes = ["24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942", "9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"]
|
||||||
pyxdg = ["1948ff8e2db02156c0cccd2529b43c0cff56ebaa71f5f021bbd755bc1419190e", "fe2928d3f532ed32b39c32a482b54136fe766d19936afc96c8f00645f9da1a06"]
|
pyxdg = ["1948ff8e2db02156c0cccd2529b43c0cff56ebaa71f5f021bbd755bc1419190e", "fe2928d3f532ed32b39c32a482b54136fe766d19936afc96c8f00645f9da1a06"]
|
||||||
pyyaml = ["0113bc0ec2ad727182326b61326afa3d1d8280ae1122493553fd6f4397f33df9", "01adf0b6c6f61bd11af6e10ca52b7d4057dd0be0343eb9283c878cf3af56aee4", "5124373960b0b3f4aa7df1707e63e9f109b5263eca5976c66e08b1c552d4eaf8", "5ca4f10adbddae56d824b2c09668e91219bb178a1eee1faa56af6f99f11bf696", "7907be34ffa3c5a32b60b95f4d95ea25361c951383a894fec31be7252b2b6f34", "7ec9b2a4ed5cad025c2278a1e6a19c011c80a3caaac804fd2d329e9cc2c287c9", "87ae4c829bb25b9fe99cf71fbb2140c448f534e24c998cc60f39ae4f94396a73", "9de9919becc9cc2ff03637872a440195ac4241c80536632fffeb6a1e25a74299", "a5a85b10e450c66b49f98846937e8cfca1db3127a9d5d1e31ca45c3d0bef4c5b", "b0997827b4f6a7c286c01c5f60384d218dca4ed7d9efa945c3e1aa623d5709ae", "b631ef96d3222e62861443cc89d6563ba3eeb816eeb96b2629345ab795e53681", "bf47c0607522fdbca6c9e817a6e81b08491de50f3766a7a0e6a5be7905961b41", "f81025eddd0327c7d4cfe9b62cf33190e1e736cc6e97502b3ec425f574b3e7a8"]
|
pyyaml = ["0e7f69397d53155e55d10ff68fdfb2cf630a35e6daf65cf0bdeaf04f127c09dc", "2e9f0b7c5914367b0916c3c104a024bb68f269a486b9d04a2e8ac6f6597b7803", "35ace9b4147848cafac3db142795ee42deebe9d0dad885ce643928e88daebdcc", "38a4f0d114101c58c0f3a88aeaa44d63efd588845c5a2df5290b73db8f246d15", "483eb6a33b671408c8529106df3707270bfacb2447bf8ad856a4b4f57f6e3075", "4b6be5edb9f6bb73680f5bf4ee08ff25416d1400fbd4535fe0069b2994da07cd", "7f38e35c00e160db592091751d385cd7b3046d6d51f578b29943225178257b31", "8100c896ecb361794d8bfdb9c11fce618c7cf83d624d73d5ab38aef3bc82d43f", "c0ee8eca2c582d29c3c2ec6e2c4f703d1b7f1fb10bc72317355a746057e7346c", "e4c015484ff0ff197564917b4b4246ca03f411b9bd7f16e02a2f586eb48b6d04", "ebc4ed52dcc93eeebeae5cf5deb2ae4347b3a81c3fa12b0b8c976544829396a4"]
|
||||||
|
regex = ["032fdcc03406e1a6485ec09b826eac78732943840c4b29e503b789716f051d8d", "0e6cf1e747f383f52a0964452658c04300a9a01e8a89c55ea22813931b580aa8", "106e25a841921d8259dcef2a42786caae35bc750fb996f830065b3dfaa67b77e", "1768cf42a78a11dae63152685e7a1d90af7a8d71d2d4f6d2387edea53a9e0588", "27d1bd20d334f50b7ef078eba0f0756a640fd25f5f1708d3b5bed18a5d6bced9", "29b20f66f2e044aafba86ecf10a84e611b4667643c42baa004247f5dfef4f90b", "4850c78b53acf664a6578bba0e9ebeaf2807bb476c14ec7e0f936f2015133cae", "57eacd38a5ec40ed7b19a968a9d01c0d977bda55664210be713e750dd7b33540", "724eb24b92fc5fdc1501a1b4df44a68b9c1dda171c8ef8736799e903fb100f63", "77ae8d926f38700432807ba293d768ba9e7652df0cbe76df2843b12f80f68885", "78b3712ec529b2a71731fbb10b907b54d9c53a17ca589b42a578bc1e9a2c82ea", "7bbbdbada3078dc360d4692a9b28479f569db7fc7f304b668787afc9feb38ec8", "8d9ef7f6c403e35e73b7fc3cde9f6decdc43b1cb2ff8d058c53b9084bfcb553e", "a83049eb717ae828ced9cf607845929efcb086a001fc8af93ff15c50012a5716", "adc35d38952e688535980ae2109cad3a109520033642e759f987cf47fe278aa1", "c29a77ad4463f71a506515d9ec3a899ed026b4b015bf43245c919ff36275444b", "cfd31b3300fefa5eecb2fe596c6dee1b91b3a05ece9d5cfd2631afebf6c6fadd", "d3ee0b035816e0520fac928de31b6572106f0d75597f6fa3206969a02baba06f", "d508875793efdf6bab3d47850df8f40d4040ae9928d9d80864c1768d6aeaf8e3", "ef0b828a7e22e58e06a1cceddba7b4665c6af8afeb22a0d8083001330572c147", "faad39fdbe2c2ccda9846cd21581063086330efafa47d87afea4073a08128656"]
|
||||||
secretstorage = ["20c797ae48a4419f66f8d28fc221623f11fc45b6828f96bdb1ad9990acb59f92", "7a119fb52a88e398dbb22a4b3eb39b779bfbace7e4153b7bc6e5954d86282a8a"]
|
secretstorage = ["20c797ae48a4419f66f8d28fc221623f11fc45b6828f96bdb1ad9990acb59f92", "7a119fb52a88e398dbb22a4b3eb39b779bfbace7e4153b7bc6e5954d86282a8a"]
|
||||||
six = ["3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", "d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"]
|
six = ["1f1b7d42e254082a9db6279deae68afb421ceba6158efa6131de7b3003ee93fd", "30f610279e8b2578cab6db20741130331735c781b56053c59c4076da27f06b66"]
|
||||||
toml = ["229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", "235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e", "f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"]
|
toml = ["229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", "235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e", "f1db651f9657708513243e61e6cc67d101a39bad662eaa9b5546f789338e07a3"]
|
||||||
tornado = ["349884248c36801afa19e342a77cc4458caca694b0eda633f5878e458a44cb2c", "398e0d35e086ba38a0427c3b37f4337327231942e731edaa6e9fd1865bbd6f60", "4e73ef678b1a859f0cb29e1d895526a20ea64b5ffd510a2307b5998c7df24281", "559bce3d31484b665259f50cd94c5c28b961b09315ccd838f284687245f416e5", "abbe53a39734ef4aba061fca54e30c6b4639d3e1f59653f0da37a0003de148c7", "c845db36ba616912074c5b1ee897f8e0124df269468f25e4fe21fe72f6edd7a9", "c9399267c926a4e7c418baa5cbe91c7d1cf362d505a1ef898fde44a07c9dd8a5"]
|
tornado = ["349884248c36801afa19e342a77cc4458caca694b0eda633f5878e458a44cb2c", "398e0d35e086ba38a0427c3b37f4337327231942e731edaa6e9fd1865bbd6f60", "4e73ef678b1a859f0cb29e1d895526a20ea64b5ffd510a2307b5998c7df24281", "559bce3d31484b665259f50cd94c5c28b961b09315ccd838f284687245f416e5", "abbe53a39734ef4aba061fca54e30c6b4639d3e1f59653f0da37a0003de148c7", "c845db36ba616912074c5b1ee897f8e0124df269468f25e4fe21fe72f6edd7a9", "c9399267c926a4e7c418baa5cbe91c7d1cf362d505a1ef898fde44a07c9dd8a5"]
|
||||||
|
typed-ast = ["1170afa46a3799e18b4c977777ce137bb53c7485379d9706af8a59f2ea1aa161", "18511a0b3e7922276346bcb47e2ef9f38fb90fd31cb9223eed42c85d1312344e", "262c247a82d005e43b5b7f69aff746370538e176131c32dda9cb0f324d27141e", "2b907eb046d049bcd9892e3076c7a6456c93a25bebfe554e931620c90e6a25b0", "354c16e5babd09f5cb0ee000d54cfa38401d8b8891eefa878ac772f827181a3c", "48e5b1e71f25cfdef98b013263a88d7145879fbb2d5185f2a0c79fa7ebbeae47", "4e0b70c6fc4d010f8107726af5fd37921b666f5b31d9331f0bd24ad9a088e631", "630968c5cdee51a11c05a30453f8cd65e0cc1d2ad0d9192819df9978984529f4", "66480f95b8167c9c5c5c87f32cf437d585937970f3fc24386f313a4c97b44e34", "71211d26ffd12d63a83e079ff258ac9d56a1376a25bc80b1cdcdf601b855b90b", "7954560051331d003b4e2b3eb822d9dd2e376fa4f6d98fee32f452f52dd6ebb2", "838997f4310012cf2e1ad3803bce2f3402e9ffb71ded61b5ee22617b3a7f6b6e", "95bd11af7eafc16e829af2d3df510cecfd4387f6453355188342c3e79a2ec87a", "bc6c7d3fa1325a0c6613512a093bc2a2a15aeec350451cbdf9e1d4bffe3e3233", "cc34a6f5b426748a507dd5d1de4c1978f2eb5626d51326e43280941206c209e1", "d755f03c1e4a51e9b24d899561fec4ccaf51f210d52abdf8c07ee2849b212a36", "d7c45933b1bdfaf9f36c579671fec15d25b06c8398f113dab64c18ed1adda01d", "d896919306dd0aa22d0132f62a1b78d11aaf4c9fc5b3410d3c666b818191630a", "fdc1c9bbf79510b76408840e009ed65958feba92a88833cdceecff93ae8fff66", "ffde2fbfad571af120fcbfbbc61c72469e72f550d676c3342492a9dfdefb8f12"]
|
||||||
tzlocal = ["4ebeb848845ac898da6519b9b31879cf13b6626f7184c496037b818e238f2c4e"]
|
tzlocal = ["4ebeb848845ac898da6519b9b31879cf13b6626f7184c496037b818e238f2c4e"]
|
||||||
|
zipp = ["3718b1cbcd963c7d4c5511a8240812904164b7f381b647143a89d3b98f9bcd8e", "f06903e9f1f43b12d371004b4ac7b06ab39a44adc747266928ae6debfa7b3335"]
|
||||||
|
|
|
@ -1,19 +1,22 @@
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "jrnl"
|
name = "jrnl"
|
||||||
version = "0.0.0-source"
|
version = "v2.2.1-beta2"
|
||||||
description = "Collect your thoughts and notes without leaving the command line."
|
description = "Collect your thoughts and notes without leaving the command line."
|
||||||
authors = [
|
authors = [
|
||||||
"Manuel Ebert <manuel@1450.me>",
|
"Manuel Ebert <manuel@1450.me>",
|
||||||
"Jonathan Wren <jonathan@nowandwren.com>",
|
"Jonathan Wren <jonathan@nowandwren.com>",
|
||||||
"Micah Ellison <micahellison@gmail.com>"
|
"Micah Ellison <micahellison@gmail.com>"
|
||||||
]
|
]
|
||||||
|
maintainers = [
|
||||||
|
"Jonathan Wren and Micah Ellison <jrnl-sh@googlegroups.com>",
|
||||||
|
]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
homepage = "https://jrnl.sh"
|
homepage = "https://jrnl.sh"
|
||||||
repository = "https://github.com/jrnl-org/jrnl"
|
repository = "https://github.com/jrnl-org/jrnl"
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = "^3.7"
|
python = ">=3.6.0, <3.9.0"
|
||||||
pyxdg = "^0.26.0"
|
pyxdg = "^0.26.0"
|
||||||
cryptography = "^2.7"
|
cryptography = "^2.7"
|
||||||
passlib = "^1.7"
|
passlib = "^1.7"
|
||||||
|
@ -30,12 +33,8 @@ pyyaml = "^5.1"
|
||||||
behave = "^1.2"
|
behave = "^1.2"
|
||||||
mkdocs = "^1.0"
|
mkdocs = "^1.0"
|
||||||
flake8 = "^3.7"
|
flake8 = "^3.7"
|
||||||
black = {version = "^18.3-alpha.0",allows-prereleases = true}
|
black = {version = "^19.10b0",allow-prereleases = true}
|
||||||
|
|
||||||
[tool.poetry.scripts]
|
[tool.poetry.scripts]
|
||||||
jrnl = 'jrnl.cli:run'
|
jrnl = 'jrnl.cli:run'
|
||||||
|
|
||||||
[build-system]
|
|
||||||
requires = ["poetry>=0.12"]
|
|
||||||
build-backend = "poetry.masonry.api"
|
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue