Deploy jrnl-org/jrnl to github.com/jrnl-org/jrnl.git:gh-pages
17
.github/stale.yml
vendored
Normal file
|
@ -0,0 +1,17 @@
|
|||
# Number of days of inactivity before an issue becomes stale
|
||||
daysUntilStale: 60
|
||||
# Number of days of inactivity before a stale issue is closed
|
||||
daysUntilClose: 7
|
||||
# Issues with these labels will never be considered stale
|
||||
exemptLabels:
|
||||
- ':star:'
|
||||
- security
|
||||
# Label to use when marking an issue as stale
|
||||
staleLabel: stale
|
||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
recent activity. It will be closed if no further activity occurs. Thank you
|
||||
for your contributions.
|
||||
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||
closeComment: false
|
54
.gitignore
vendored
Normal file
|
@ -0,0 +1,54 @@
|
|||
*.py[cod]
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
.python-version
|
||||
|
||||
# Packages
|
||||
*.egg
|
||||
*.egg-info
|
||||
dist
|
||||
build
|
||||
eggs
|
||||
parts
|
||||
bin
|
||||
var
|
||||
sdist
|
||||
develop-eggs
|
||||
.installed.cfg
|
||||
lib
|
||||
lib64
|
||||
.python-version
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
.DS_Store
|
||||
.travis-solo
|
||||
Icon
|
||||
|
||||
# Documentation
|
||||
_build
|
||||
_sources
|
||||
_static
|
||||
objects.inv
|
||||
searchindex.js
|
||||
|
||||
# MS Visual Studio (PyTools)
|
||||
obj
|
||||
*.pyproj
|
||||
*.sln
|
||||
*.suo
|
||||
|
||||
# virtaulenv
|
||||
env/
|
||||
env*/
|
||||
|
||||
# PyCharm Project files
|
||||
.idea/
|
||||
|
||||
# export testing director
|
||||
exp/
|
||||
|
||||
_extras/
|
||||
*.sublime-*
|
||||
site/
|
29
.travis.yml
Normal file
|
@ -0,0 +1,29 @@
|
|||
dist: xenial # required for Python >= 3.7
|
||||
language: python
|
||||
python: "3.7"
|
||||
before_install:
|
||||
- pip install poetry
|
||||
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
|
||||
- poetry config http-basic.pypi $PYPI_USER $PYPI_PASS
|
||||
- poetry version $TRAVIS_TAG
|
||||
- poetry build
|
||||
deploy:
|
||||
- provider: script
|
||||
script: poetry publish
|
||||
skip_cleanup: true
|
||||
on:
|
||||
branch: master
|
||||
tags: true
|
||||
- provider: pages
|
||||
skip_cleanup: true
|
||||
github_token: $GITHUB_TOKEN
|
||||
on:
|
||||
branch: master
|
171
CHANGELOG.md
Normal file
|
@ -0,0 +1,171 @@
|
|||
Changelog
|
||||
=========
|
||||
|
||||
## 2.0
|
||||
|
||||
* Cryptographical backend changed from PyCrypto to cryptography.io
|
||||
* 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)
|
||||
|
||||
* __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.3__ Fixed: Tags at the beginning of lines
|
||||
* __1.9.2__ Fixed: Tag search ignores email-addresses (thanks to @mjhoffman65)
|
||||
* __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.8 (May 22, 2014)
|
||||
|
||||
* __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.5__ Fixed: file names when exporting to individual files contain full year (thanks to @jdevera)
|
||||
* __1.8.4__ Improved: using external editors (thanks to @chrissexton)
|
||||
* __1.8.3__ Fixed: export to text files and improves help (thanks to @igniteflow and @mpe)
|
||||
* __1.8.2__ Better integration with environment variables (thanks to @ajaam and @matze)
|
||||
* __1.8.1__ Minor bug fixes
|
||||
* __1.8.0__ Official support for python 3.4
|
||||
|
||||
### 1.7 (December 22, 2013)
|
||||
|
||||
* __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.20__ Minor fixes when parsing DayOne journals
|
||||
* __1.7.19__ Creates full path to journal during installation if it doesn't exist yet
|
||||
* __1.7.18__ Small update to parsing regex
|
||||
* __1.7.17__ Fixes writing new lines between entries
|
||||
* __1.7.16__ Even more unicode fixes!
|
||||
* __1.7.15__ More unicode fixes
|
||||
* __1.7.14__ Fix for trailing whitespaces (eg. when writing markdown code block)
|
||||
* __1.7.13__ Fix for UTF-8 in DayOne journals
|
||||
* __1.7.12__ Fixes a bug where filtering by tags didn't work for DayOne journals
|
||||
* __1.7.11__ `-ls` will list all available journals (Thanks @jtan189)
|
||||
* __1.7.10__ Supports `-3` as a shortcut for `-n 3` and updates to tzlocal 1.1
|
||||
* __1.7.9__ Fix a logic bug so that jrnl -h and jrnl -v are possible even if jrnl not configured yet.
|
||||
* __1.7.8__ Upgrade to parsedatetime 1.2
|
||||
* __1.7.7__ Cleaned up imports, better unicode support
|
||||
* __1.7.6__ Python 3 port for slugify
|
||||
* __1.7.5__ Colorama is only needed on Windows. Smaller fixes
|
||||
* __1.7.3__ Touches temporary files before opening them to allow more external editors.
|
||||
* __1.7.2__ Dateutil added to requirements.
|
||||
* __1.7.1__ Fixes issues with parsing time information in entries.
|
||||
* __1.7.0__ Edit encrypted or DayOne journals with `jrnl --edit`.
|
||||
|
||||
|
||||
### 1.6 (November 5, 2013)
|
||||
|
||||
* __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.4__ Fixed a bug that caused creating encrypted journals to fail
|
||||
* __1.6.3__ New, pretty, _useful_ documentation!
|
||||
* __1.6.2__ Starring entries now works for plain-text journals too!
|
||||
* __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.5 (August 6, 2013)
|
||||
|
||||
* __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.5__ Fixed: Detects DayOne journals stored in `~/Library/Mobile Data` as well.
|
||||
* __1.5.4__ DayOne journals can now handle tags
|
||||
* __1.5.3__ Fixed: DayOne integration with older DayOne Journals
|
||||
* __1.5.2__ Soft-deprecated `-to` for filtering by time and introduces `-until` instead.
|
||||
* __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.4 (July 22, 2013)
|
||||
|
||||
* __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.3 (July 17, 2013)
|
||||
|
||||
* __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__ Feature to export to given output file
|
||||
|
||||
### 1.2 (July 15, 2013)
|
||||
|
||||
* __1.2.0__ Fixed: Timezone support for DayOne
|
||||
|
||||
|
||||
### 1.1 (June 9, 2013)
|
||||
|
||||
* __1.1.1__ Fixed: Unicode and Python3 issues resolved.
|
||||
* __1.1.0__
|
||||
* JSON export exports tags as well.
|
||||
* Nicer error message when there is a syntactical error in your config file.
|
||||
* Unicode support
|
||||
|
||||
### 1.0 (March 4, 2013)
|
||||
|
||||
* __1.0.5__ Backwards compatibility with `parsedatetime` 0.8.7
|
||||
* __1.0.4__
|
||||
* Python 2.6 compatibility
|
||||
* Better utf-8 support
|
||||
* Python 3 compatibility
|
||||
* Respects the `XDG_CONFIG_HOME` environment variable for storing your configuration file (Thanks [evaryont](https://github.com/evaryont))
|
||||
|
||||
* __1.0.3__
|
||||
* Removed clint in favour of colorama
|
||||
* Fixed: Fixed a bug where showing tags failed when no tags are defined.
|
||||
* Fixed: Improvements to config parsing (Thanks [alapolloni](https://github.com/alapolloni))
|
||||
* Fixed: Fixes readline support on Windows
|
||||
* Fixed: Smaller fixes and typos
|
||||
* __1.0.1__ (March 12, 2013) Fixed: Requires parsedatetime 1.1.2 or newer
|
||||
* __1.0.0__
|
||||
* Integrates seamlessly with DayOne
|
||||
* Each journal can have individual settings
|
||||
* Fixed: A bug where jrnl would not go into compose mode
|
||||
* Fixed: A bug where jrnl would not add entries without timestamp
|
||||
* Fixed: Support for parsedatetime 1.x
|
||||
|
||||
### 0.3 (May 24, 2012)
|
||||
|
||||
* __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__
|
||||
* Supports deleting of last entry.
|
||||
* Fixed: Fixes a bug where --encrypt or --decrypt without a target file would not work.
|
||||
* Supports a config option for setting word wrap.
|
||||
* Supports multiple journal files.
|
||||
* __0.3.0__
|
||||
* Fixed: Dates such as "May 3" will now be interpreted as being in the past if the current day is at least 28 days in the future
|
||||
* 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`)
|
||||
|
||||
### 0.2 (April 16, 2012)
|
||||
|
||||
* __0.2.4__
|
||||
* Fixed: Parsing of new lines in journal files and entries
|
||||
* Adds support for encrypting and decrypting into new files
|
||||
* __0.2.3__
|
||||
* Adds a `-short` option that will only display the titles of entries (or, when filtering by tags, the context of the tag)
|
||||
* Adds tag export
|
||||
* Adds coloured highlight of tags (by default, highlights all tags - when filtering by tags, only highlights search tags)
|
||||
* `.jrnl_config` will get automatically updated when updating jrnl to a new version
|
||||
* __0.2.2__
|
||||
* Adds --encrypt and --decrypt to encrypt / decrypt existing journal files
|
||||
* Adds markdown export (kudos to dedan)
|
||||
* __0.2.1__ Submitted to [PyPi](http://pypi.python.org/pypi/jrnl/0.2.1).
|
||||
* __0.2.0__
|
||||
* 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.)
|
||||
|
||||
### 0.1 (April 13, 2012)
|
||||
|
||||
|
||||
* __0.1.1__
|
||||
* Fixed: Removed unnecessary print commands
|
||||
* Created the documentation
|
||||
* __0.1.0__
|
||||
* Supports encrypted journals using AES encryption
|
||||
* Support external editors for composing entries
|
||||
* __0.0.2__
|
||||
* Filtering by tags and dates
|
||||
* Fixed: Now using dedicated classes for Journals and entries
|
||||
|
||||
### 0.0 (March 29, 2012)
|
||||
|
||||
* __0.0.1__ Composing entries works. That's pretty much it.
|
66
CODE_OF_CONDUCT.md
Normal file
|
@ -0,0 +1,66 @@
|
|||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as contributors and maintainers
|
||||
pledge to making participation in our project and our community a harassment-free experience for
|
||||
everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity
|
||||
and expression, level of experience, education, socio-economic status, nationality, personal
|
||||
appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment include:
|
||||
|
||||
* Using welcoming and inclusive language Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism Focusing on what is best for the community Showing
|
||||
* empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks Public or private
|
||||
* harassment Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable behavior and are
|
||||
expected to take appropriate and fair corrective action in response to any instances of unacceptable
|
||||
behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits,
|
||||
code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or
|
||||
to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate,
|
||||
threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all project spaces, and it also applies when an individual is
|
||||
representing the project or its community in public spaces. Examples of representing a project or
|
||||
community include using an official project e-mail address, posting via an official social media
|
||||
account, or acting as an appointed representative at an online or offline event. Representation of a
|
||||
project may be further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting
|
||||
the project team. All complaints will be reviewed and investigated and will result in a response
|
||||
that is deemed necessary and appropriate to the circumstances. The project team is obligated to
|
||||
maintain confidentiality with regard to the reporter of an incident. Further details of specific
|
||||
enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face
|
||||
temporary or permanent repercussions as determined by other members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at
|
||||
https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq
|
35
CONTRIBUTING.md
Normal file
|
@ -0,0 +1,35 @@
|
|||
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!
|
||||
|
||||
|
||||
Docs & 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.
|
||||
|
||||
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.
|
||||
|
||||
|
||||
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) 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:
|
||||
|
||||
* be _slim_
|
||||
* have a simple interface
|
||||
* avoid dupicating functionality
|
||||
|
||||
|
||||
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.
|
||||
|
||||
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!
|
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014 Manuel Ebert
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
29
Makefile
Normal file
|
@ -0,0 +1,29 @@
|
|||
# A Makefile for commands I run frequently:
|
||||
|
||||
clean:
|
||||
rm -rf dist
|
||||
rm -rf _static
|
||||
rm -rf jrnl.egg-info
|
||||
rm -rf _build
|
||||
rm -rf _sources
|
||||
rm -rf _static
|
||||
rm -rf site/
|
||||
rm -f *.html
|
||||
|
||||
html:
|
||||
poetry run mkdocs serve
|
||||
|
||||
format: ## check style with flake8
|
||||
poetry run black features jrnl
|
||||
|
||||
lint: ## check style with flake8
|
||||
poetry run flake8 jrnl features --ignore E501
|
||||
|
||||
test: ## Run behave tests
|
||||
poetry run behave
|
||||
|
||||
build:
|
||||
poetry build
|
||||
|
||||
install: clean ## install the package to the active Python's site-packages
|
||||
poetry install
|
41
README.md
Normal file
|
@ -0,0 +1,41 @@
|
|||
jrnl [](https://travis-ci.com/jrnl-org/jrnl) [](https://pepy.tech/project/jrnl) [](https://pypi.python.org/pypi/jrnl/)
|
||||
====
|
||||
|
||||
_To get help, [submit an issue](https://github.com/jrnl-org/jrnl/issues/new) on Github._
|
||||
|
||||
*jrnl* is a simple journal application for your command line. Journals are stored as human readable plain text 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, when all your fancy iPad journal applications will long be forgotten.
|
||||
|
||||
Optionally, your journal can be encrypted using the [256-bit AES](http://en.wikipedia.org/wiki/Advanced_Encryption_Standard).
|
||||
|
||||
### Why keep a journal?
|
||||
|
||||
Journals aren't just for people who have too much time on their summer vacation. A journal helps you to keep track of the things you get done and how you did them. Your imagination may be limitless, but your memory isn't. For personal use, make it a good habit to write at least 20 words a day. Just to reflect what made this day special, why you haven't wasted it. For professional use, consider a text-based journal to be the perfect complement to your GTD todo list - a documentation of what and how you've done it.
|
||||
|
||||
In a Nutshell
|
||||
-------------
|
||||
|
||||
to make a new entry, just type
|
||||
|
||||
jrnl yesterday: Called in sick. Used the time to clean the house and spent 4h on writing my book.
|
||||
|
||||
and hit return. `yesterday:` will be interpreted as a timestamp. Everything until the first sentence mark (`.?!`) will be interpreted as the title, the rest as the body. In your journal file, the result will look like this:
|
||||
|
||||
2012-03-29 09:00 Called in sick.
|
||||
Used the time to clean the house and spent 4h on writing my book.
|
||||
|
||||
If you just call `jrnl`, you will be prompted to compose your entry - but you can also configure _jrnl_ to use your external editor.
|
||||
|
||||
Known Issues
|
||||
------------
|
||||
jrnl used to support integration with Day One, but no longer supports it since Day One 2 was released with a different backend. [See the GitHub issue for more information](https://github.com/jrnl-org/jrnl/issues/409).
|
||||
|
||||
Authors
|
||||
-------
|
||||
Current maintainers:
|
||||
|
||||
* Jonathan Wren ([wren](https://github.com/wren))
|
||||
* Micah Ellison ([micahellison](https://github.com/micahellison))
|
||||
|
||||
Original maintainer:
|
||||
|
||||
* Manuel Ebert ([maebert](https://github.com/maebert))
|
2
docs/CNAME
Normal file
|
@ -0,0 +1,2 @@
|
|||
jrnl.sh
|
||||
|
145
docs/advanced.md
Normal file
|
@ -0,0 +1,145 @@
|
|||
# Advanced Usage
|
||||
|
||||
## Configuration File
|
||||
|
||||
You can configure the way jrnl behaves in a configuration file. By
|
||||
default, this is `~/.jrnl_config`. If you have the `XDG_CONFIG_HOME`
|
||||
variable set, the configuration file will be saved as
|
||||
`$XDG_CONFIG_HOME/jrnl/.jrnl_config`.
|
||||
|
||||
!!! note
|
||||
On Windows, The configuration file is typically found at `C:\Users\[Your Username]\.jrnl_config`.
|
||||
|
||||
The configuration file is a simple JSON file with the following options
|
||||
and can be edited with any plain text editor.
|
||||
|
||||
- `journals`
|
||||
paths to your journal files
|
||||
- `editor`
|
||||
if set, executes this command to launch an external editor for
|
||||
writing your entries, e.g. `vim`. Some editors require special
|
||||
options to work properly, see `FAQ <recipes>` for details.
|
||||
- `encrypt`
|
||||
if `true`, encrypts your journal using AES.
|
||||
- `tagsymbols`
|
||||
Symbols to be interpreted as tags. (See note below)
|
||||
- `default_hour` and `default_minute`
|
||||
if you supply a date, such as `last thursday`, but no specific
|
||||
time, the entry will be created at this time
|
||||
- `timeformat`
|
||||
how to format the timestamps in your journal, see the [python docs](http://docs.python.org/library/time.html#time.strftime) for reference
|
||||
- `highlight`
|
||||
if `true`, tags will be highlighted in cyan.
|
||||
- `linewrap`
|
||||
controls the width of the output. Set to `false` if you don't
|
||||
want to wrap long lines.
|
||||
|
||||
!!! note
|
||||
Although it seems intuitive to use the `#`
|
||||
character for tags, there's a drawback: on most shells, this is
|
||||
interpreted as a meta-character starting a comment. This means that if
|
||||
you type
|
||||
|
||||
> `jrnl Implemented endless scrolling on the #frontend of our website.`
|
||||
|
||||
your bash will chop off everything after the `#` before passing it to
|
||||
`jrnl`. To avoid this, wrap your input into quotation marks like
|
||||
this:
|
||||
|
||||
> `jrnl "Implemented endless scrolling on the #frontend of our website."`
|
||||
|
||||
Or use the built-in prompt or an external editor to compose your
|
||||
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
|
||||
|
||||
You can configure `jrnl`to use with multiple journals (eg.
|
||||
`private` and `work`) by defining more journals in your `.jrnl_config`,
|
||||
for example:
|
||||
|
||||
``` javascript
|
||||
{
|
||||
...
|
||||
"journals": {
|
||||
"default": "~/journal.txt",
|
||||
"work": "~/work.txt"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The `default` journal gets created the first time you start `jrnl`
|
||||
Now you can access the `work` journal by using `jrnl work` instead of
|
||||
`jrnl`, eg.
|
||||
|
||||
``` sh
|
||||
jrnl work at 10am: Meeting with @Steve
|
||||
jrnl work -n 3
|
||||
```
|
||||
|
||||
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`).
|
||||
|
||||
You can also override the default options for each individual journal.
|
||||
If you `.jrnl_config` looks like this:
|
||||
|
||||
``` javascript
|
||||
{
|
||||
...
|
||||
"encrypt": false
|
||||
"journals": {
|
||||
"default": "~/journal.txt",
|
||||
"work": {
|
||||
"journal": "~/work.txt",
|
||||
"encrypt": true
|
||||
},
|
||||
"food": "~/my_recipes.txt",
|
||||
}
|
||||
```
|
||||
|
||||
Your `default` and your `food` journals won't be encrypted, however your
|
||||
`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
|
||||
you specify a `"journal": ...` key that points to the journal file of
|
||||
that journal.
|
||||
|
||||
!!! note
|
||||
Changing `encrypt` to a different value will not encrypt or decrypt your
|
||||
journal file, it merely says whether or not your journal
|
||||
is encrypted. Hence manually changing
|
||||
this option will most likely result in your journal file being
|
||||
impossible to load.
|
||||
|
||||
## Known Issues
|
||||
|
||||
### Unicode on Windows
|
||||
|
||||
The Windows shell prior to Windows 7 has issues with unicode encoding.
|
||||
To use non-ascii characters, first tweak Python to recognize the encoding by adding `'cp65001': 'utf_8'`, to `Lib/encoding/aliases.py`. Then, change the codepage with `chcp 1252` before using `jrnl`.
|
||||
|
||||
(Related issue: [#486](https://github.com/jrnl-org/jrnl/issues/486))
|
79
docs/assets/highlight.css
Normal file
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
|
||||
Atom One Dark With support for ReasonML by Gidi Morris, based off work by Daniel Gamage
|
||||
|
||||
Original One Dark Syntax theme from https://github.com/atom/one-dark-syntax
|
||||
|
||||
*/
|
||||
.hljs {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
padding: 0.5em;
|
||||
line-height: 1.3em;
|
||||
color: #e2e8f2;
|
||||
background: #383e49;
|
||||
border-radius: 5px;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.3rem;
|
||||
}
|
||||
.hljs-keyword, .hljs-operator {
|
||||
color: #F92672;
|
||||
}
|
||||
.hljs-pattern-match {
|
||||
color: #F92672;
|
||||
}
|
||||
.hljs-pattern-match .hljs-constructor {
|
||||
color: #61aeee;
|
||||
}
|
||||
.hljs-function {
|
||||
color: #61aeee;
|
||||
}
|
||||
.hljs-function .hljs-params {
|
||||
color: #A6E22E;
|
||||
}
|
||||
.hljs-function .hljs-params .hljs-typing {
|
||||
color: #FD971F;
|
||||
}
|
||||
.hljs-module-access .hljs-module {
|
||||
color: #7e57c2;
|
||||
}
|
||||
.hljs-constructor {
|
||||
color: #e2b93d;
|
||||
}
|
||||
.hljs-constructor .hljs-string {
|
||||
color: #9CCC65;
|
||||
}
|
||||
.hljs-comment, .hljs-quote {
|
||||
color: #b18eb1;
|
||||
font-style: italic;
|
||||
}
|
||||
.hljs-doctag, .hljs-formula {
|
||||
color: #c678dd;
|
||||
}
|
||||
.hljs-section, .hljs-name, .hljs-selector-tag, .hljs-deletion, .hljs-subst {
|
||||
color: #e06c75;
|
||||
}
|
||||
.hljs-literal {
|
||||
color: #56b6c2;
|
||||
}
|
||||
.hljs-string, .hljs-regexp, .hljs-addition, .hljs-attribute, .hljs-meta-string {
|
||||
color: #98c379;
|
||||
}
|
||||
.hljs-built_in, .hljs-class .hljs-title {
|
||||
color: #e6c07b;
|
||||
}
|
||||
.hljs-attr, .hljs-variable, .hljs-template-variable, .hljs-type, .hljs-selector-class, .hljs-selector-attr, .hljs-selector-pseudo, .hljs-number {
|
||||
color: #d19a66;
|
||||
}
|
||||
.hljs-symbol, .hljs-bullet, .hljs-link, .hljs-meta, .hljs-selector-id, .hljs-title {
|
||||
color: #61aeee;
|
||||
}
|
||||
.hljs-emphasis {
|
||||
font-style: italic;
|
||||
}
|
||||
.hljs-strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
.hljs-link {
|
||||
text-decoration: underline;
|
||||
}
|
196
docs/assets/theme.css
Normal file
|
@ -0,0 +1,196 @@
|
|||
|
||||
/* ------------------------------------------------------------ */
|
||||
/* Overrides for jrnl theme */
|
||||
/* ------------------------------------------------------------ */
|
||||
|
||||
:root {
|
||||
--sidebar: #604385;
|
||||
--sidebar-dark: #604385;
|
||||
--off-white: rgba(255,255,255,.7);
|
||||
}
|
||||
|
||||
body.wy-body-for-nav, section.wy-nav-content-wrap {
|
||||
background-color: rgb(252,252,252);
|
||||
}
|
||||
|
||||
pre {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
margin: 1em -1em;
|
||||
}
|
||||
|
||||
pre code {
|
||||
padding: 1em 1.5em !important;
|
||||
}
|
||||
|
||||
code {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
h1,h2 ,h3, h4, h5, h6 {
|
||||
font-family: "Open Sans", "Helvetica Neue", Helvetica, sans-serif;
|
||||
font-weight: 600;
|
||||
margin-top: 2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
p, td, tr, div, li {
|
||||
font-family: "Open Sans", "Helvetica Neue", Helvetica, sans-serif;
|
||||
font-weight: 00;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 1em 0em;
|
||||
}
|
||||
|
||||
/* No-one likes lines that are 400 characters long. */
|
||||
div.rst-content {max-width: 54em;}
|
||||
|
||||
.wy-side-nav-search, .wy-nav-top, .wy-menu-vertical li.current {
|
||||
background-color: transparent;
|
||||
}
|
||||
.wy-side-nav-search a.icon-home {
|
||||
width: 100%;
|
||||
max-width: 250px;
|
||||
background-size: 100%;
|
||||
}
|
||||
|
||||
a.icon-home:before {
|
||||
display: block;
|
||||
width: 84px;
|
||||
height: 70px;
|
||||
content: "";
|
||||
background: url(../img/logo_white.svg) center center no-repeat;
|
||||
margin: 10px auto;
|
||||
}
|
||||
|
||||
.wy-menu-vertical a, .wy-menu-vertical li ul li a {color: var(--off-white) !important; }
|
||||
|
||||
.wy-menu-vertical a:hover, .wy-menu-vertical li.current a:hover { background-color: transparent !important; color: white !important;}
|
||||
.wy-menu-vertical li.on a, .wy-menu-vertical li.current>a {
|
||||
background: transparent; !important;
|
||||
color: white !important;
|
||||
border: none;
|
||||
position: relative;
|
||||
font-weight: 700 !important;
|
||||
border-right: none !important;
|
||||
}
|
||||
|
||||
.wy-menu-vertical li.on a, .wy-menu-vertical li.current a {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.wy-menu-vertical li.on a, .wy-menu-vertical li.current>a:after {
|
||||
display: block;
|
||||
position: absolute;
|
||||
right: 0em;
|
||||
top: 0;
|
||||
z-index: 999 !important;
|
||||
content: "";
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-top: 1em solid transparent;
|
||||
border-bottom: 1em solid transparent;
|
||||
border-right: 1em solid white;
|
||||
}
|
||||
|
||||
.rst-versions, .rst-versions .rst-current-version { display: none; }
|
||||
.wy-menu-vertical span {
|
||||
color: white !important;
|
||||
font-size: 1.2em;
|
||||
font-weight: 300 !important;
|
||||
}
|
||||
.wy-menu-vertical li a {color: var(--off-white) !important; font-weight: 300 !important;}
|
||||
|
||||
|
||||
.wy-nav-side {
|
||||
background-image: linear-gradient(211deg, #95699C 0%, #604385 100%);
|
||||
font-weight: 300 !important;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
|
||||
footer {display: none;}
|
||||
|
||||
.wy-side-nav-search input[type=text], form .search-query {
|
||||
background-color: rgba(0,0,0,.1) !important;
|
||||
border: 1px solid rgba(255,255,255,.3);
|
||||
box-shadow: none;
|
||||
margin-bottom: 1em;
|
||||
color: white !important;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.wy-side-nav-search input[type=text]::placeholder, form .search-query::placeholder {
|
||||
color: var(--off-white) !important;
|
||||
}
|
||||
|
||||
.toctree-l2 a:first-child {display: none;}
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
/* Logo: ; */
|
||||
/* ------------------------------------------------------------ */
|
||||
|
||||
.logo {
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
vertical-align: middle;
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
/* Code blocks in callouts */
|
||||
/* ------------------------------------------------------------ */
|
||||
|
||||
div.admonition {
|
||||
border-radius: 5px;
|
||||
margin: 1em -1em;
|
||||
}
|
||||
div.admonition p.admonition-title {
|
||||
border-top-left-radius: 5px;
|
||||
border-top-right-radius: 5px;
|
||||
}
|
||||
div.admonition > p {
|
||||
padding: 0em .5em;
|
||||
}
|
||||
|
||||
|
||||
div.admonition div.highlight {
|
||||
background: none !important;
|
||||
}
|
||||
div.admonition div.highlight pre {
|
||||
background-color: rgba(255,255,255,.5) !important;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------ */
|
||||
/* Fancy ordered lists. */
|
||||
/* ------------------------------------------------------------ */
|
||||
|
||||
ol {
|
||||
counter-reset:li;
|
||||
margin-left: 0px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ol li {
|
||||
list-style: none !important;
|
||||
margin-bottom: 1.5em;
|
||||
margin-left: 3em !important;
|
||||
}
|
||||
|
||||
ol > li:before {
|
||||
content:counter(li);
|
||||
counter-increment:li;
|
||||
background-color: var(--sidebar);
|
||||
border-radius: 50%;
|
||||
display: block;
|
||||
float: left;
|
||||
margin-left: -3em;
|
||||
margin-top: -.3em;
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
color: var(--sidebar-dark);
|
||||
text-align: center;
|
||||
line-height: 2em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
93
docs/encryption.md
Normal file
|
@ -0,0 +1,93 @@
|
|||
# Encryption
|
||||
|
||||
## Encrypting and decrypting
|
||||
|
||||
If you don't choose to encrypt your file when you run
|
||||
`jrnl` for the first time, you can encrypt
|
||||
your existing journal file or change its password using
|
||||
|
||||
``` sh
|
||||
jrnl --encrypt
|
||||
```
|
||||
|
||||
If it is already encrypted, you will first be asked for the current
|
||||
password. You can then enter a new password and your plain journal will
|
||||
replaced by the encrypted file. Conversely,
|
||||
|
||||
``` sh
|
||||
jrnl --decrypt
|
||||
```
|
||||
|
||||
will replace your encrypted journal file by a Journal in plain text. You
|
||||
can also specify a filename, ie. `jrnl --decrypt plain_text_copy.txt`,
|
||||
to leave your original file untouched.
|
||||
|
||||
## Storing passwords in your keychain
|
||||
|
||||
Whenever you encrypt your journal, you are asked whether you want to
|
||||
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
|
||||
your journal.
|
||||
|
||||
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
|
||||
not on another -- you can simply run `jrnl --encrypt` on an encrypted
|
||||
journal and use the same password again.
|
||||
|
||||
## A note on security
|
||||
|
||||
While jrnl follows best practises, true security is an illusion.
|
||||
Specifically, jrnl will leave traces in your memory and your shell
|
||||
history -- it's meant to keep journals secure in transit, for example
|
||||
when storing it on an
|
||||
[untrusted](http://techcrunch.com/2014/04/09/condoleezza-rice-joins-dropboxs-board/)
|
||||
services such as Dropbox. If you're concerned about security, disable
|
||||
history logging for journal in your `.bashrc`
|
||||
|
||||
``` sh
|
||||
HISTIGNORE="$HISTIGNORE:jrnl *"
|
||||
```
|
||||
|
||||
If you are using zsh instead of bash, you can get the same behaviour
|
||||
adding this to your `zshrc`
|
||||
|
||||
``` sh
|
||||
setopt HIST_IGNORE_SPACE
|
||||
alias jrnl=" jrnl"
|
||||
```
|
||||
|
||||
## Manual decryption
|
||||
|
||||
Should you ever want to decrypt your journal manually, you can do so
|
||||
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
|
||||
(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
|
||||
before being encrypted. Here's a Python script that you can use to
|
||||
decrypt your journal
|
||||
|
||||
``` python
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
from Crypto.Cipher import AES
|
||||
import getpass
|
||||
import hashlib
|
||||
import sys
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("filepath", help="journal file to decrypt")
|
||||
args = parser.parse_args()
|
||||
|
||||
pwd = getpass.getpass()
|
||||
key = hashlib.sha256(pwd.encode('utf-8')).digest()
|
||||
|
||||
with open(args.filepath, 'rb') as f:
|
||||
ciphertext = f.read()
|
||||
|
||||
crypto = AES.new(key, AES.MODE_CBC, ciphertext[:16])
|
||||
plain = crypto.decrypt(ciphertext[16:])
|
||||
plain = plain.strip(plain[-1:])
|
||||
plain = plain.decode("utf-8")
|
||||
print(plain)
|
||||
```
|
76
docs/export.md
Normal file
|
@ -0,0 +1,76 @@
|
|||
# Import and Export
|
||||
|
||||
## Tag export
|
||||
|
||||
With
|
||||
|
||||
``` sh
|
||||
jrnl --tags
|
||||
```
|
||||
|
||||
you'll get a list of all tags you used in your journal, sorted by most
|
||||
frequent. Tags occurring several times in the same entry are only
|
||||
counted as one.
|
||||
|
||||
## List of all entries
|
||||
|
||||
``` sh
|
||||
jrnl --short
|
||||
```
|
||||
|
||||
Will only display the date and title of each entry.
|
||||
|
||||
## JSON export
|
||||
|
||||
Can do
|
||||
|
||||
``` sh
|
||||
jrnl --export json
|
||||
```
|
||||
|
||||
Why not create a [beautiful timeline](http://timeline.verite.co/) of
|
||||
your journal?
|
||||
|
||||
## Markdown export
|
||||
|
||||
Use
|
||||
|
||||
``` sh
|
||||
jrnl --export markdown
|
||||
```
|
||||
|
||||
Markdown is a simple markup language that is human readable and can be
|
||||
used to be rendered to other formats (html, pdf). This README for
|
||||
example is formatted in markdown and github makes it look nice.
|
||||
|
||||
## Text export
|
||||
|
||||
``` sh
|
||||
jrnl --export text
|
||||
```
|
||||
|
||||
Pretty-prints your entire journal.
|
||||
|
||||
## Export to files
|
||||
|
||||
You can specify the output file of your exported journal using the
|
||||
`-o` argument
|
||||
|
||||
``` sh
|
||||
jrnl --export md -o journal.md
|
||||
```
|
||||
|
||||
The above command will generate a file named `journal.md`. If the`-o` argument is a directory, jrnl will export each entry into an individual file
|
||||
|
||||
``` sh
|
||||
jrnl --export json -o my_entries/
|
||||
```
|
||||
|
||||
The contents of `my\_entries/` will then look like this:
|
||||
|
||||
``` output
|
||||
my_entries/
|
||||
|- 2013_06_03_a-beautiful-day.json
|
||||
|- 2013_06_07_dinner-with-gabriel.json
|
||||
|- ...
|
||||
```
|
55
docs/installation.md
Normal file
|
@ -0,0 +1,55 @@
|
|||
# Getting started
|
||||
|
||||
## Installation
|
||||
|
||||
On OS X, the easiest way to install *jrnl* is using
|
||||
[Homebrew](http://brew.sh/)
|
||||
|
||||
``` sh
|
||||
brew install jrnl
|
||||
```
|
||||
|
||||
On other platforms, install *jrnl* using pip
|
||||
|
||||
``` sh
|
||||
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
|
||||
should be created and whether you wish to encrypt it.
|
||||
|
||||
## Quickstart
|
||||
|
||||
to make a new entry, just type
|
||||
|
||||
``` sh
|
||||
jrnl yesterday: Called in sick. Used the time to clean the house and spent 4h on writing my book.
|
||||
```
|
||||
|
||||
and hit return. `yesterday:` will be interpreted as a time stamp.
|
||||
Everything until the first sentence mark (`.?!:`) will be interpreted as
|
||||
the title, the rest as the body. In your journal file, the result will
|
||||
look like this:
|
||||
|
||||
``` output
|
||||
2012-03-29 09:00 Called in sick.
|
||||
Used the time to clean the house and spent 4h on writing my book.
|
||||
```
|
||||
|
||||
If you just call `jrnl`, you will be prompted to compose your entry -
|
||||
but you can also configure *jrnl* to use your external editor.
|
31
docs/overview.md
Normal file
|
@ -0,0 +1,31 @@
|
|||
# Overview
|
||||
|
||||
## What is jrnl?
|
||||
|
||||
`jrnl` is a simple journal application for
|
||||
your command line. Journals are stored as human readable plain text
|
||||
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,
|
||||
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
|
||||
AES](http://en.wikipedia.org/wiki/Advanced_Encryption_Standard).
|
||||
|
||||
## Why keep a journal?
|
||||
|
||||
Journals aren't just for people who have too much
|
||||
time on their summer vacation. A journal helps you to keep track of the
|
||||
things you get done and how you did them. Your imagination may be
|
||||
limitless, but your memory isn't.
|
||||
|
||||
For personal use, make it a good habit to write at least 20 words a day.
|
||||
Just to reflect what made this day special, why you haven't wasted it.
|
||||
|
||||
For professional use, consider a text-based journal to be the perfect
|
||||
complement to your GTD todo list - a documentation of what and how
|
||||
you've done it. Or use it as a quick way to keep a change log. Or use it
|
||||
to keep a lab book.
|
197
docs/recipes.md
Normal file
|
@ -0,0 +1,197 @@
|
|||
# FAQ
|
||||
|
||||
## Recipes
|
||||
|
||||
### Co-occurrence of tags
|
||||
|
||||
If I want to find out how often I mentioned my flatmates Alberto and
|
||||
Melo in the same entry, I run
|
||||
|
||||
``` sh
|
||||
jrnl @alberto --tags | grep @melo
|
||||
```
|
||||
|
||||
And will get something like `@melo: 9`, meaning there are 9 entries
|
||||
where both `@alberto` and `@melo` are tagged. How does this work? First,
|
||||
`jrnl @alberto` will filter the journal to only entries containing the
|
||||
tag `@alberto`, and then the `--tags` option will print out how often
|
||||
each tag occurred in this filtered journal. Finally, we pipe this to
|
||||
`grep` which will only display the line containing `@melo`.
|
||||
|
||||
### Combining filters
|
||||
|
||||
You can do things like
|
||||
|
||||
``` sh
|
||||
jrnl @fixed -starred -n 10 -until "jan 2013" --short
|
||||
```
|
||||
|
||||
To get a short summary of the 10 most recent, favourited entries before
|
||||
January 1, 2013 that are tagged with `@fixed`.
|
||||
|
||||
### Statistics
|
||||
|
||||
How much did I write last year?
|
||||
|
||||
``` sh
|
||||
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
|
||||
average entry?
|
||||
|
||||
``` sh
|
||||
expr $(jrnl --export text | wc -w) / $(jrnl --short | wc -l)
|
||||
```
|
||||
|
||||
This will first get the total number of words in the journal and divide
|
||||
it by the number of entries (this works because `jrnl --short` will
|
||||
print exactly one line per entry).
|
||||
|
||||
### Importing older files
|
||||
|
||||
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
|
||||
be the date of the entry in jrnl? Try this
|
||||
|
||||
``` sh
|
||||
echo `stat -f %Sm -t '%d %b %Y at %H:%M: ' entry.txt` `cat entry.txt` | jrnl
|
||||
```
|
||||
|
||||
The first part will format the modification date of `entry.txt`, and
|
||||
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
|
||||
`.bash_profile`
|
||||
|
||||
``` sh
|
||||
jrnlimport () {
|
||||
echo `stat -f %Sm -t '%d %b %Y at %H:%M: ' $1` `cat $1` | jrnl
|
||||
}
|
||||
```
|
||||
|
||||
### Using templates
|
||||
|
||||
Say you always want to use the same template for creating new entries.
|
||||
If you have an [external editor](../advanced) set up, you can use this:
|
||||
|
||||
```sh
|
||||
jrnl < my_template.txt
|
||||
jrnl -1 --edit
|
||||
```
|
||||
|
||||
Another nice solution that allows you to define individual prompts comes
|
||||
from [Jacobo de
|
||||
Vera](https://github.com/maebert/jrnl/issues/194#issuecomment-47402869):
|
||||
|
||||
``` sh
|
||||
function log_question()
|
||||
{
|
||||
echo $1
|
||||
read
|
||||
jrnl today: ${1}. $REPLY
|
||||
}
|
||||
log_question 'What did I achieve today?'
|
||||
log_question 'What did I make progress with?'
|
||||
```
|
||||
|
||||
## External editors
|
||||
|
||||
To use external editors for writing and editing journal entries, set
|
||||
them up in your `.jrnl_config` (see `advanced usage <advanced>` for
|
||||
details). Generally, after writing an entry, you will have to save and
|
||||
close the file to save the changes to jrnl.
|
||||
|
||||
### Sublime Text
|
||||
|
||||
To use Sublime Text, install the command line tools for Sublime Text and
|
||||
configure your `.jrnl_config` like this:
|
||||
|
||||
``` json
|
||||
{
|
||||
"editor": "subl -w"
|
||||
}
|
||||
```
|
||||
|
||||
Note the `-w` flag to make sure jrnl waits for Sublime Text to close the
|
||||
file before writing into the journal.
|
||||
|
||||
### MacVim
|
||||
|
||||
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
|
||||
back to journal. In the case of MacVim, this is `-f`:
|
||||
|
||||
``` json
|
||||
{
|
||||
"editor": "mvim -f"
|
||||
}
|
||||
```
|
||||
|
||||
### iA Writer
|
||||
|
||||
On OS X, you can use the fabulous [iA
|
||||
Writer](http://www.iawriter.com/mac) to write entries. Configure your
|
||||
`.jrnl_config` like this:
|
||||
|
||||
``` json
|
||||
{
|
||||
"editor": "open -b pro.writer.mac -Wn"
|
||||
}
|
||||
```
|
||||
|
||||
What does this do? `open -b ...` opens a file using the application
|
||||
identified by the bundle identifier (a unique string for every app out
|
||||
there). `-Wn` tells the application to wait until it's closed before
|
||||
passing back control, and to use a new instance of the application.
|
||||
|
||||
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
|
||||
`Info.plist` file in your shell:
|
||||
|
||||
``` sh
|
||||
grep -A 1 CFBundleIdentifier /Applications/iA\ Writer.app/Contents/Info.plist
|
||||
```
|
||||
|
||||
### Notepad++ on Windows
|
||||
|
||||
To set [Notepad++](http://notepad-plus-plus.org/) as your editor, edit
|
||||
the jrnl config file (`.jrnl_config`) like this:
|
||||
|
||||
``` json
|
||||
{
|
||||
"editor": "C:\\Program Files (x86)\\Notepad++\\notepad++.exe -multiInst -nosession",
|
||||
}
|
||||
```
|
||||
|
||||
The double backslashes are needed so jrnl can read the file path
|
||||
correctly. The `-multiInst -nosession` options will cause jrnl to open
|
||||
its own Notepad++ window.
|
||||
|
||||
### Visual Studio Code
|
||||
|
||||
To set [Visual Studo Code](https://code.visualstudio.com) as your editor on Linux, edit `.jrnl_config` like this:
|
||||
|
||||
```json
|
||||
{
|
||||
"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.
|
||||
|
||||
On MacOS you will need to add VS Code to your PATH. You can do that by adding:
|
||||
|
||||
```sh
|
||||
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.
|
||||
|
||||
Then you can add:
|
||||
|
||||
```javascript
|
||||
{
|
||||
"editor": "code --wait",
|
||||
}
|
||||
```
|
||||
|
||||
to ``.jrnl_config``. See also the [Visual Studio Code documentation](https://code.visualstudio.com/docs/setup/mac)
|
BIN
docs/theme/img/banner_og.png
vendored
Normal file
After Width: | Height: | Size: 224 KiB |
BIN
docs/theme/img/banner_twitter.png
vendored
Normal file
After Width: | Height: | Size: 175 KiB |
BIN
docs/theme/img/favicon.ico
vendored
Normal file
After Width: | Height: | Size: 17 KiB |
15
docs/theme/img/favicon.svg
vendored
Normal file
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="64px" height="64px" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 55 (78076) - https://sketchapp.com -->
|
||||
<title>favicon</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs>
|
||||
<linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-1">
|
||||
<stop stop-color="#986B9D" offset="0%"></stop>
|
||||
<stop stop-color="#664C8B" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g id="favicon" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<path d="M47,9.75231557 L47,37 L42,32 L37,37 L37,7 L25,7 C17.8202983,7 12,12.8202983 12,20 L12,44 C12,51.1797017 17.8202983,57 25,57 L32,57 L32,7 L34,7 L34,57 L39,57 C46.1797017,57 52,51.1797017 52,44 L52,20 C52,15.8373552 50.0435392,12.1316662 47,9.75231557 Z M47.097641,7.37132944 C51.2495133,10.039075 54,14.6983322 54,20 L54,44 C54,49.3016678 51.2495133,53.960925 47.097641,56.6286706 C52.7817736,55.2392133 57,50.1122164 57,44 L57,20 C57,13.8877836 52.7817736,8.76078667 47.097641,7.37132944 Z M16.902359,56.6286706 C12.7504867,53.960925 10,49.3016678 10,44 L10,20 C10,14.6983322 12.7504867,10.039075 16.902359,7.37132944 C11.2182264,8.76078667 7,13.8877836 7,20 L7,44 C7,50.1122164 11.2182264,55.2392133 16.902359,56.6286706 Z M52.097641,7.37132944 C56.2495133,10.039075 59,14.6983322 59,20 L59,44 C59,49.3016678 56.2495133,53.960925 52.097641,56.6286706 C57.7817736,55.2392133 62,50.1122164 62,44 L62,20 C62,13.8877836 57.7817736,8.76078667 52.097641,7.37132944 Z M11.902359,56.6286706 C7.7504867,53.960925 5,49.3016678 5,44 L5,20 C5,14.6983322 7.7504867,10.039075 11.902359,7.37132944 C6.21822638,8.76078667 2,13.8877836 2,20 L2,44 C2,50.1122164 6.21822638,55.2392133 11.902359,56.6286706 Z M15,5 L49,5 C57.2842712,5 64,11.7157288 64,20 L64,44 C64,52.2842712 57.2842712,59 49,59 L15,59 C6.71572875,59 0,52.2842712 0,44 L0,20 C2.0539126e-15,11.7157288 6.71572875,5 15,5 Z" id="Combined-Shape" fill="url(#linearGradient-1)" fill-rule="nonzero"></path>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.1 KiB |
10
docs/theme/img/jrnl_white.svg
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="90px" height="98px" viewBox="0 0 90 98" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 55 (78076) - https://sketchapp.com -->
|
||||
<title>jrnl_white</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<g id="jrnl_white" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<path d="M63,5.34786368 L63,43 L58,38 L53,43 L53,5 L30,5 C22.8202983,5 17,10.8202983 17,18 L17,58 C17,65.1797017 22.8202983,71 30,71 L45,71 L45,5 L47,5 L47,71 L60,71 C67.1797017,71 73,65.1797017 73,58 L73,18 C73,11.8528367 68.7334,6.70219128 63,5.34786368 Z M67.6693542,5.10618565 C72.0593056,7.72299627 75,12.5180801 75,18 L75,58 C75,63.4819199 72.0593056,68.2770037 67.6693542,70.8938143 C74.060623,70.0745197 79,64.6140865 79,58 L79,18 C79,11.3859135 74.060623,5.92548026 67.6693542,5.10618565 Z M22.3306458,70.8938143 C17.9406944,68.2770037 15,63.4819199 15,58 L15,18 C15,12.5180801 17.9406944,7.72299627 22.3306458,5.10618565 C15.939377,5.92548026 11,11.3859135 11,18 L11,58 C11,64.6140865 15.939377,70.0745197 22.3306458,70.8938143 Z M73.6693542,5.10618565 C78.0593056,7.72299627 81,12.5180801 81,18 L81,58 C81,63.4819199 78.0593056,68.2770037 73.6693542,70.8938143 C80.060623,70.0745197 85,64.6140865 85,58 L85,18 C85,11.3859135 80.060623,5.92548026 73.6693542,5.10618565 Z M16.3306458,70.8938143 C11.9406944,68.2770037 9,63.4819199 9,58 L9,18 C9,12.5180801 11.9406944,7.72299627 16.3306458,5.10618565 C9.93937703,5.92548026 5,11.3859135 5,18 L5,58 C5,64.6140865 9.93937703,70.0745197 16.3306458,70.8938143 Z M18,3 L72,3 C80.2842712,3 87,9.71572875 87,18 L87,58 C87,66.2842712 80.2842712,73 72,73 L18,73 C9.71572875,73 3,66.2842712 3,58 L3,18 C3,9.71572875 9.71572875,3 18,3 Z" id="Combined-Shape" fill="#FFFFFF" fill-rule="nonzero"></path>
|
||||
<path d="M24.22,95.092 C24.0293324,95.092 23.7616684,95.0736668 23.417,95.037 C23.0723316,95.0003332 22.7680013,94.9306672 22.504,94.828 L22.856,92.65 C23.1786683,92.7526672 23.5673311,92.804 24.022,92.804 C24.6380031,92.804 25.0706654,92.6243351 25.32,92.265 C25.5693346,91.9056649 25.694,91.3666703 25.694,90.648 L25.694,82.642 L28.356,82.285 L28.356,90.692 C28.356,92.1880075 28.0003369,93.2953297 27.289,94.014 C26.5776631,94.7326703 25.5546733,95.092 24.22,95.092 Z M41.4,84.892 C41.1799989,84.8186663 40.8756686,84.7416671 40.487,84.661 C40.0983314,84.5803329 39.6473359,84.54 39.134,84.54 C38.8406652,84.54 38.5290017,84.569333 38.199,84.628 C37.8689984,84.686667 37.6380007,84.7379998 37.506,84.782 L37.506,94 L34.844,94 L34.844,83.044 C35.3573359,82.8533324 35.9989962,82.6736675 36.769,82.505 C37.5390039,82.3363325 38.3933286,82.252 39.332,82.252 C39.5080009,82.252 39.7133322,82.2629999 39.948,82.285 C40.1826678,82.3070001 40.4173322,82.3363332 40.652,82.373 C40.8866678,82.4096669 41.1139989,82.4536664 41.334,82.505 C41.5540011,82.5563336 41.7299993,82.6039998 41.862,82.648 L41.4,84.892 Z M46.81,82.868 C47.3233359,82.7213326 47.9906626,82.5820007 48.812,82.45 C49.6333374,82.3179993 50.5426617,82.252 51.54,82.252 C52.4786714,82.252 53.2633302,82.3803321 53.894,82.637 C54.5246698,82.893668 55.0269981,83.2529977 55.401,83.715 C55.7750019,84.1770023 56.0389992,84.7343301 56.193,85.387 C56.3470008,86.0396699 56.424,86.7546628 56.424,87.532 L56.424,94 L53.762,94 L53.762,87.95 C53.762,87.3339969 53.7216671,86.8096688 53.641,86.377 C53.5603329,85.9443312 53.4283342,85.5923347 53.245,85.321 C53.0616658,85.0496653 52.8123349,84.8516673 52.497,84.727 C52.1816651,84.6023327 51.7966689,84.54 51.342,84.54 C51.004665,84.54 50.6526685,84.5619998 50.286,84.606 C49.9193315,84.6500002 49.6480009,84.6866665 49.472,84.716 L49.472,94 L46.81,94 L46.81,82.868 Z M66.784,94.22 C65.991996,94.2053333 65.3356693,94.1173341 64.815,93.956 C64.2943307,93.7946659 63.8800015,93.5636682 63.572,93.263 C63.2639985,92.9623318 63.0476673,92.5883356 62.923,92.141 C62.7983327,91.6936644 62.736,91.1840029 62.736,90.612 L62.736,82.692 L65.398,82.252 L65.398,90.106 C65.398,90.4286683 65.4236664,90.6999989 65.475,90.92 C65.5263336,91.1400011 65.6179993,91.3269992 65.75,91.481 C65.8820007,91.6350008 66.0616655,91.7523329 66.289,91.833 C66.5163345,91.9136671 66.8059982,91.9759998 67.158,92.02 L66.784,94.22 Z" id="jrnl" fill="#FFFFFF" fill-rule="nonzero"></path>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 4.2 KiB |
16
docs/theme/img/logo.svg
vendored
Normal file
|
@ -0,0 +1,16 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="90px" height="98px" viewBox="0 0 90 98" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 55 (78076) - https://sketchapp.com -->
|
||||
<title>logo</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs>
|
||||
<linearGradient x1="50%" y1="0%" x2="50%" y2="100%" id="linearGradient-1">
|
||||
<stop stop-color="#986B9D" offset="0%"></stop>
|
||||
<stop stop-color="#664C8B" offset="100%"></stop>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g id="logo" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<path d="M63,5.34786368 L63,43 L58,38 L53,43 L53,5 L30,5 C22.8202983,5 17,10.8202983 17,18 L17,58 C17,65.1797017 22.8202983,71 30,71 L45,71 L45,5 L47,5 L47,71 L60,71 C67.1797017,71 73,65.1797017 73,58 L73,18 C73,11.8528367 68.7334,6.70219128 63,5.34786368 Z M67.6693542,5.10618565 C72.0593056,7.72299627 75,12.5180801 75,18 L75,58 C75,63.4819199 72.0593056,68.2770037 67.6693542,70.8938143 C74.060623,70.0745197 79,64.6140865 79,58 L79,18 C79,11.3859135 74.060623,5.92548026 67.6693542,5.10618565 Z M22.3306458,70.8938143 C17.9406944,68.2770037 15,63.4819199 15,58 L15,18 C15,12.5180801 17.9406944,7.72299627 22.3306458,5.10618565 C15.939377,5.92548026 11,11.3859135 11,18 L11,58 C11,64.6140865 15.939377,70.0745197 22.3306458,70.8938143 Z M73.6693542,5.10618565 C78.0593056,7.72299627 81,12.5180801 81,18 L81,58 C81,63.4819199 78.0593056,68.2770037 73.6693542,70.8938143 C80.060623,70.0745197 85,64.6140865 85,58 L85,18 C85,11.3859135 80.060623,5.92548026 73.6693542,5.10618565 Z M16.3306458,70.8938143 C11.9406944,68.2770037 9,63.4819199 9,58 L9,18 C9,12.5180801 11.9406944,7.72299627 16.3306458,5.10618565 C9.93937703,5.92548026 5,11.3859135 5,18 L5,58 C5,64.6140865 9.93937703,70.0745197 16.3306458,70.8938143 Z M18,3 L72,3 C80.2842712,3 87,9.71572875 87,18 L87,58 C87,66.2842712 80.2842712,73 72,73 L18,73 C9.71572875,73 3,66.2842712 3,58 L3,18 C3,9.71572875 9.71572875,3 18,3 Z" id="Combined-Shape" fill="url(#linearGradient-1)" fill-rule="nonzero"></path>
|
||||
<path d="M24.22,95.092 C24.0293324,95.092 23.7616684,95.0736668 23.417,95.037 C23.0723316,95.0003332 22.7680013,94.9306672 22.504,94.828 L22.856,92.65 C23.1786683,92.7526672 23.5673311,92.804 24.022,92.804 C24.6380031,92.804 25.0706654,92.6243351 25.32,92.265 C25.5693346,91.9056649 25.694,91.3666703 25.694,90.648 L25.694,82.642 L28.356,82.285 L28.356,90.692 C28.356,92.1880075 28.0003369,93.2953297 27.289,94.014 C26.5776631,94.7326703 25.5546733,95.092 24.22,95.092 Z M41.4,84.892 C41.1799989,84.8186663 40.8756686,84.7416671 40.487,84.661 C40.0983314,84.5803329 39.6473359,84.54 39.134,84.54 C38.8406652,84.54 38.5290017,84.569333 38.199,84.628 C37.8689984,84.686667 37.6380007,84.7379998 37.506,84.782 L37.506,94 L34.844,94 L34.844,83.044 C35.3573359,82.8533324 35.9989962,82.6736675 36.769,82.505 C37.5390039,82.3363325 38.3933286,82.252 39.332,82.252 C39.5080009,82.252 39.7133322,82.2629999 39.948,82.285 C40.1826678,82.3070001 40.4173322,82.3363332 40.652,82.373 C40.8866678,82.4096669 41.1139989,82.4536664 41.334,82.505 C41.5540011,82.5563336 41.7299993,82.6039998 41.862,82.648 L41.4,84.892 Z M46.81,82.868 C47.3233359,82.7213326 47.9906626,82.5820007 48.812,82.45 C49.6333374,82.3179993 50.5426617,82.252 51.54,82.252 C52.4786714,82.252 53.2633302,82.3803321 53.894,82.637 C54.5246698,82.893668 55.0269981,83.2529977 55.401,83.715 C55.7750019,84.1770023 56.0389992,84.7343301 56.193,85.387 C56.3470008,86.0396699 56.424,86.7546628 56.424,87.532 L56.424,94 L53.762,94 L53.762,87.95 C53.762,87.3339969 53.7216671,86.8096688 53.641,86.377 C53.5603329,85.9443312 53.4283342,85.5923347 53.245,85.321 C53.0616658,85.0496653 52.8123349,84.8516673 52.497,84.727 C52.1816651,84.6023327 51.7966689,84.54 51.342,84.54 C51.004665,84.54 50.6526685,84.5619998 50.286,84.606 C49.9193315,84.6500002 49.6480009,84.6866665 49.472,84.716 L49.472,94 L46.81,94 L46.81,82.868 Z M66.784,94.22 C65.991996,94.2053333 65.3356693,94.1173341 64.815,93.956 C64.2943307,93.7946659 63.8800015,93.5636682 63.572,93.263 C63.2639985,92.9623318 63.0476673,92.5883356 62.923,92.141 C62.7983327,91.6936644 62.736,91.1840029 62.736,90.612 L62.736,82.692 L65.398,82.252 L65.398,90.106 C65.398,90.4286683 65.4236664,90.6999989 65.475,90.92 C65.5263336,91.1400011 65.6179993,91.3269992 65.75,91.481 C65.8820007,91.6350008 66.0616655,91.7523329 66.289,91.833 C66.5163345,91.9136671 66.8059982,91.9759998 67.158,92.02 L66.784,94.22 Z" id="jrnl" fill="#674C8B" fill-rule="nonzero"></path>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 4.5 KiB |
9
docs/theme/img/logo_white.svg
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="84px" height="70px" viewBox="0 0 84 70" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 55 (78076) - https://sketchapp.com -->
|
||||
<title>logo_white</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<g id="logo_white" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<path d="M60,2.34786368 L60,40 L55,35 L50,40 L50,2 L27,2 C19.8202983,2 14,7.82029825 14,15 L14,55 C14,62.1797017 19.8202983,68 27,68 L42,68 L42,2 L44,2 L44,68 L57,68 C64.1797017,68 70,62.1797017 70,55 L70,15 C70,8.85283673 65.7334,3.70219128 60,2.34786368 Z M64.6693542,2.10618565 C69.0593056,4.72299627 72,9.51808015 72,15 L72,55 C72,60.4819199 69.0593056,65.2770037 64.6693542,67.8938143 C71.060623,67.0745197 76,61.6140865 76,55 L76,15 C76,8.38591347 71.060623,2.92548026 64.6693542,2.10618565 Z M19.3306458,67.8938143 C14.9406944,65.2770037 12,60.4819199 12,55 L12,15 C12,9.51808015 14.9406944,4.72299627 19.3306458,2.10618565 C12.939377,2.92548026 8,8.38591347 8,15 L8,55 C8,61.6140865 12.939377,67.0745197 19.3306458,67.8938143 Z M70.6693542,2.10618565 C75.0593056,4.72299627 78,9.51808015 78,15 L78,55 C78,60.4819199 75.0593056,65.2770037 70.6693542,67.8938143 C77.060623,67.0745197 82,61.6140865 82,55 L82,15 C82,8.38591347 77.060623,2.92548026 70.6693542,2.10618565 Z M13.3306458,67.8938143 C8.94069436,65.2770037 6,60.4819199 6,55 L6,15 C6,9.51808015 8.94069436,4.72299627 13.3306458,2.10618565 C6.93937703,2.92548026 2,8.38591347 2,15 L2,55 C2,61.6140865 6.93937703,67.0745197 13.3306458,67.8938143 Z M15,0 L69,0 C77.2842712,-1.33226763e-15 84,6.71572875 84,15 L84,55 C84,63.2842712 77.2842712,70 69,70 L15,70 C6.71572875,70 2.66453526e-15,63.2842712 0,55 L0,15 C-8.8817842e-16,6.71572875 6.71572875,1.33226763e-15 15,0 Z" id="Combined-Shape" fill="#FFFFFF" fill-rule="nonzero"></path>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.9 KiB |
18
docs/theme/img/sprites.svg
vendored
Normal file
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="400px" height="160px" viewBox="0 0 400 160" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 55 (78076) - https://sketchapp.com -->
|
||||
<title>sprites</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<g id="sprites" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<path d="M200.001666,7.10542736e-15 C186.746448,7.10542736e-15 176.000667,10.745781 176.000667,24.0009999 C176.000667,28.0531687 177.014042,31.8666609 178.791449,35.2174671 L162.102754,51.9021622 L162.117422,51.9141627 C160.812034,53.1968828 160,54.9729568 160,56.9463723 C160,60.8452014 163.160132,64.0039997 167.057627,64.0039997 C169.028376,64.0039997 170.805783,63.1919658 172.08717,61.8865781 L172.08317,61.882578 L188.765198,45.2018831 C192.117338,46.984624 195.937497,48.0019997 199.999,48.0019997 C213.254219,48.0019997 224,37.2562187 224,24.0009999 C224.002666,10.7471144 213.256885,7.10542736e-15 200.001666,7.10542736e-15 Z M169.589733,59.386474 C168.945706,60.0545018 168.053669,60.4731859 167.057627,60.4731859 C165.108213,60.4731859 163.528147,58.8957869 163.528147,56.9450389 C163.528147,55.9489974 163.948164,55.0596271 164.614859,54.4129335 L164.597525,54.3969328 L180.730197,38.2642607 C182.139589,40.1656733 183.819659,41.8457432 185.718405,43.2604689 L169.589733,59.386474 Z M200.001666,44.0044998 C188.957206,44.0044998 180.000833,35.04546 180.000833,24.0036666 C180.000833,12.9592065 188.957206,4.00283342 200.001666,4.00283342 C211.04346,4.00283342 220.0025,12.9592065 220.0025,24.0036666 C220.0025,35.0467933 211.044793,44.0044998 200.001666,44.0044998 Z M200.001666,10.00175 C200.551023,10.00175 201.001708,10.4497687 201.001708,11.0017917 C201.001708,11.5524813 200.551023,12.0018333 200.001666,12.0018333 C193.37339,12.0018333 188.001166,17.3753905 188.001166,24.0023332 C188.001166,24.5543562 187.553148,25.0023749 187.001125,25.0023749 C186.449102,25.0023749 186.001083,24.5543562 186.001083,24.0023332 C186.001083,16.2686777 192.268011,10.00175 200.001666,10.00175 Z" id="path10" fill="#684688" fill-rule="nonzero"></path>
|
||||
<path d="M42,26 L42,18 C42,8.05733333 33.9426667,0 24,0 C14.0573333,0 6,8.05733333 6,18 L6,26 C2.68533333,26 0,28.6853333 0,32 L0,38 L0,40 L0,44 L0,46 C0,55.9426667 8.05733333,64 18,64 L30,64 C39.9426667,64 48,55.9426667 48,46 L48,44 L48,40 L48,38 L48,32 C48,28.684 45.312,26 42,26 Z M10,18 C10,10.268 16.268,4 24,4 C31.732,4 38,10.268 38,18 L38,26 L34,26 L34,18.004 C34,12.48 29.524,8.004 24,8.004 C18.476,8.004 14,12.48 14,18.004 L14,26 L10,26 L10,18 Z M32,18 L32,18.0066667 L32,26 L16,26 L16,18.004 L16,18 C16,13.5813333 19.5813333,10 24,10 C28.4173333,10 32,13.5826667 32,18 Z M44,38 L44,40 L44,44 L44,46 C44,53.7173333 37.7173333,60 30,60 L18,60 C10.2826667,60 4,53.7173333 4,46 L4,44 L4,40 L4,38 L4,32 C4,30.896 4.896,30 6,30 C7.33466667,30 8.66533333,30 10,30 L38,30 C39.332,30 40.664,30 42,30 C43.1026667,30 44,30.896 44,32 L44,38 Z M24,38 C26.208,38 28,39.7906667 28,42 C28,43.2173333 27.344,45.536 26.668,47.364 C26.1213333,48.84 25.564,49.996 24,49.996 C22.5626667,49.996 21.8786667,48.828 21.3333333,47.344 C20.6666667,45.52 20,43.2133333 20,42 C20,39.7906667 21.792,38 24,38 Z" id="path12" fill="#684688" fill-rule="nonzero"></path>
|
||||
<path d="M56,110.174879 L37.4495142,102.225364 L44.8646072,96.9285223 C41.0009854,92.8572051 35.5547484,90.2992712 29.5,90.2992712 C19.2306123,90.2992712 10.6738014,97.6001925 8.72073517,107.291434 L3.77369603,105.243629 C6.58790907,93.6333303 17.023696,85 29.5,85 C37.336557,85 44.3508342,88.4239389 49.2055642,93.8300944 L56,88.9753644 L56,110.174879 Z M14.1353928,126.072692 C17.9965854,130.142795 23.4452516,132.700729 29.5,132.700729 C39.8082547,132.700729 48.3893574,125.343936 50.2974837,115.594395 L55.2396645,117.709002 C52.4412412,129.342378 41.9945229,138 29.5,138 C21.663443,138 14.6467366,134.576061 9.79443579,129.169906 L3,134.024636 L3,112.823907 L21.5504858,120.773421 L14.1353928,126.072692 Z" id="path6" fill="#684688" fill-rule="nonzero"></path>
|
||||
<path d="M110.666667,136 L110.666667,88 L114.666667,88 L114.666667,136 L124,136 C129.154658,136 133.333333,131.821324 133.333333,126.666667 L133.333333,97.3333333 C133.333333,94.1616732 131.751312,91.3595139 129.333333,89.672919 L129.333333,109.333333 L124,105.333333 L118.666667,109.333333 L118.666667,88 L121.333333,88 L100,88 C94.8453423,88 90.6666667,92.1786757 90.6666667,97.3333333 L90.6666667,126.666667 C90.6666667,131.821324 94.8453423,136 100,136 L110.666667,136 Z M134.130346,88.6638236 C136.127154,90.9949079 137.333333,94.0232615 137.333333,97.3333333 L137.333333,126.666667 C137.333333,129.976739 136.127154,133.005092 134.130346,135.336176 C137.570042,133.9607 140,130.597457 140,126.666667 L140,97.3333333 C140,93.4025432 137.570042,90.0393001 134.130346,88.6638236 Z M89.8696538,135.336176 C87.872846,133.005092 86.6666667,129.976739 86.6666667,126.666667 L86.6666667,97.3333333 C86.6666667,94.0232615 87.872846,90.9949079 89.8696538,88.6638236 C86.4299578,90.0393001 84,93.4025432 84,97.3333333 L84,126.666667 C84,130.597457 86.4299578,133.9607 89.8696538,135.336176 Z M93.3333333,84 L130.666667,84 C138.030463,84 144,89.9695367 144,97.3333333 L144,126.666667 C144,134.030463 138.030463,140 130.666667,140 L93.3333333,140 C85.9695367,140 80,134.030463 80,126.666667 L80,97.3333333 C80,89.9695367 85.9695367,84 93.3333333,84 Z" id="Combined-Shape" fill="#684688" fill-rule="nonzero"></path>
|
||||
<path d="M363.625,2 L363.625,13.625 C363.625,13.625 332.463,13.711 331.708333,47.375 C332.168333,42.2843333 335.715667,22.375 363.625,22.375 L363.625,31.0833333 L384,16.5416667 L363.625,2 Z M328,8.54166667 C323.568,8.54166667 320,12.1096667 320,16.5416667 L320,52.5416667 C320,56.9736667 323.568,60.5416667 328,60.5416667 L369.333333,60.5416667 C373.765333,60.5416667 377.333333,56.9736667 377.333333,52.5416667 L377.333333,29.875 L373.333333,32.5416667 L373.333333,52.5416667 C373.333333,54.7576667 371.549333,56.5416667 369.333333,56.5416667 L328,56.5416667 C325.784,56.5416667 324,54.7576667 324,52.5416667 L324,16.5416667 C324,14.3256667 325.784,12.5416667 328,12.5416667 L344,12.5416667 L353.333333,8.54166667 L328,8.54166667 Z" id="path8" fill="#684688" fill-rule="nonzero"></path>
|
||||
<path d="M272,13 C272.552,13 273,13.448 273,14 C273,14.552 272.549333,15 272,15 C261.345333,15 252,20.608 252,27 C252,27.552 251.552,28 251,28 C250.448,28 250,27.552 250,27 C250,19.412 260.074667,13 272,13 Z M272,3 C254.326667,3 240,13.7453333 240,27 C240,35.2546667 245.557333,42.532 254.016,46.852 C254.016,46.9053333 254,46.9413333 254,47 C254,50.5853333 251.322667,54.4466667 250.144,56.472 C250.146667,56.472 250.148,56.472 250.148,56.472 C250.053333,56.692 250,56.9333333 250,57.188 C250,58.188 250.809333,59 251.812,59 C252,59 252.330667,58.9506667 252.321333,58.972 C258.572,57.948 264.46,52.2053333 265.828,50.5426667 C267.826667,50.836 269.886667,51 272,51 C289.670667,51 304,40.2546667 304,27 C304,13.7453333 289.672,3 272,3 Z M272,47 C270.165333,47 268.284,46.86 266.408,46.5866667 C266.213333,46.5546667 266.02,46.544 265.828,46.544 C264.64,46.544 263.502667,47.072 262.736,48.004 C261.88,49.0466667 259.444,51.372 256.566667,53.0826667 C257.346667,51.292 257.956,49.2866667 257.998667,47.2186667 C258.010667,47.0906667 258.016,46.96 258.016,46.8506667 C258.016,45.3466667 257.174667,43.972 255.836,43.2893333 C248.424,39.504 244,33.4146667 244,27 C244,15.972 256.56,7 272,7 C287.436,7 300,15.972 300,27 C300,38.028 287.438667,47 272,47 Z" id="path16" fill="#684688" fill-rule="nonzero"></path>
|
||||
<path d="M192,80 C174.326667,80 160,94.3266667 160,112 C160,129.673333 174.326667,144 192,144 C209.673333,144 224,129.673333 224,112 C224,94.3266667 209.673333,80 192,80 Z M192,84 C207.463973,84 220,96.536024 220,112 C220,124.684311 211.56628,135.391332 200,138.833333 L200,137.666667 L200,135.375 L200,132.875 C200,130.353667 199.104333,128.479667 197.375,127.291667 C198.459,127.187667 199.469667,127.061667 200.375,126.875 C201.280333,126.688333 202.239333,126.436667 203.25,126.083333 C204.260667,125.73 205.155667,125.281 205.958333,124.791667 C206.761,124.302333 207.553,123.656333 208.291667,122.875 C209.030333,122.093667 209.625,121.239333 210.125,120.25 C210.625,119.260667 211.041333,118.052 211.333333,116.666667 C211.625333,115.281333 211.75,113.75 211.75,112.083333 C211.75,108.854 210.729,106.105333 208.625,103.833333 C209.583667,101.333333 209.458333,98.604 208.291667,95.6666667 L207.5,95.5833333 C206.958667,95.5206667 205.988667,95.7703333 204.583333,96.2916667 C203.178,96.813 201.625667,97.6453333 199.875,98.8333333 C197.396333,98.1453333 194.791667,97.8333333 192.125,97.8333333 C189.437,97.8333333 186.895667,98.1466667 184.458333,98.8333333 C183.354333,98.0826667 182.322667,97.4906667 181.333333,97 C180.344,96.5106667 179.551667,96.1773333 178.958333,96 C178.365,95.8226667 177.802333,95.708 177.291667,95.6666667 C176.781,95.6253333 176.437,95.6036667 176.291667,95.625 C176.146333,95.6463333 176.061333,95.687 176,95.7083333 C174.833333,98.667 174.708,101.354667 175.666667,103.833333 C173.562667,106.104 172.5,108.854 172.5,112.083333 C172.5,113.75 172.666333,115.281333 172.958333,116.666667 C173.250333,118.052 173.625,119.260667 174.125,120.25 C174.625,121.239333 175.26,122.093667 176,122.875 C176.74,123.656333 177.532,124.301 178.333333,124.791667 C179.134667,125.282333 180.031,125.73 181.041667,126.083333 C182.052333,126.436667 182.678,126.688333 183.583333,126.875 C184.488667,127.061667 185.500667,127.229333 186.583333,127.333333 C184.875333,128.5 184,130.333667 184,132.875 L184,135.125 L184,137.75 L184,138.833333 C172.433733,135.391332 164,124.684311 164,112 C164,96.536024 176.53604,84 192,84 Z" id="path22" fill="#684688" fill-rule="nonzero"></path>
|
||||
<path d="M100,1.0658141e-14 C99.0049467,1.0658141e-14 98.0752267,0.350804013 97.3333333,0.958333333 C96.59144,1.56586265 96,2.50111463 96,3.625 L96,5.33333333 L94.6666667,5.33333333 L94.6666667,6.66666667 L87.3333333,6.66666667 C87.264,6.66305333 87.1943867,6.66305333 87.125,6.66666667 C86.1491067,6.76895307 85.32808,7.6854408 85.3333333,8.66666667 L85.3333333,9.33333333 L83.3333333,9.33333333 C82.28616,9.333438 81.33344,10.2861644 81.3333333,11.3333333 L81.3333333,12.125 C80.5629867,12.3974076 80.0044,13.1829169 80,14 L80,18 C80.0001067,19.0471693 80.9528267,19.9998947 82,20 L85.3333333,20 L85.3333333,53.3333333 L83.6666667,54.9166667 C83.05688,55.268736 82.6604,55.962572 82.6666667,56.6666667 L82.6666667,60.6666667 C82.6667733,61.713836 83.6194933,62.6665613 84.6666667,62.6666667 L95.2083333,62.6666667 L104.666667,62.6666667 L104.875,62.6666667 L115.333333,62.6666667 C116.380507,62.6665613 117.333227,61.713836 117.333333,60.6666667 L117.333333,56.6666667 C117.34,55.962572 116.94312,55.268736 116.333333,54.9166667 L114.666667,53.3333333 L114.666667,20 L118,20 C119.047173,19.9998947 119.999893,19.0471693 120,18 L120,14 C119.996,13.1829169 119.437013,12.3974076 118.666667,12.125 L118.666667,11.3333333 C118.66656,10.2861644 117.71384,9.333438 116.666667,9.33333333 L114.666667,9.33333333 L114.666667,8.66666667 C114.66656,7.61949773 113.71384,6.66677133 112.666667,6.66666667 L105.333333,6.66666667 L105.333333,5.33333333 L104,5.33333333 L104,3.625 C104,2.50111463 103.40856,1.56586269 102.666667,0.958333333 C101.924773,0.350803973 100.995053,1.0658141e-14 100,1.0658141e-14 Z M100,2.66666667 C100.274467,2.66666667 100.684267,2.8172396 100.958333,3.04166667 C101.2324,3.26609373 101.333333,3.4793628 101.333333,3.625 L101.333333,5.33333333 L98.6666667,5.33333333 L98.6666667,3.625 C98.6666667,3.4793628 98.7676,3.2660936 99.0416667,3.04166667 C99.3157333,2.81723973 99.7255333,2.66666667 100,2.66666667 Z M88,10.6666667 L112,10.6666667 L112,12 L114.666667,12 C114.596267,12.9827864 114.819853,13.6843969 116,13.4166667 L116,17.3333333 L110.666667,17.3333333 L89.3333333,17.3333333 L84,17.3333333 L84,13.5416667 C85.09188,13.6062785 85.25424,12.8356391 85.3333333,12 L88,12 L88,10.6666667 Z M89.3333333,20 L110.666667,20 L110.666667,56 L113.333333,57.8333333 L113.333333,58.6666667 L86.6666667,58.6666667 L86.6666667,57.8333333 L89.3333333,56 L89.3333333,20 Z M92,22.6666667 L92,28 L94.6666667,28 L94.6666667,22.6666667 L92,22.6666667 Z M96,22.6666667 L96,28 L98.6666667,28 L98.6666667,22.6666667 L96,22.6666667 Z M101.333333,22.6666667 L101.333333,28 L104,28 L104,22.6666667 L101.333333,22.6666667 Z M105.333333,22.6666667 L105.333333,28 L108,28 L108,22.6666667 L105.333333,22.6666667 Z M92,29.3333333 L92,34.6666667 L94.6666667,34.6666667 L94.6666667,29.3333333 L92,29.3333333 Z M96,29.3333333 L96,34.6666667 L98.6666667,34.6666667 L98.6666667,29.3333333 L96,29.3333333 Z M101.333333,29.3333333 L101.333333,34.6666667 L104,34.6666667 L104,29.3333333 L101.333333,29.3333333 Z M105.333333,29.3333333 L105.333333,34.6666667 L108,34.6666667 L108,29.3333333 L105.333333,29.3333333 Z M103.166667,40.8333333 C102.15268,40.8333333 101.333333,41.6526813 101.333333,42.6666667 C101.333333,43.680652 102.15268,44.5 103.166667,44.5 C104.180653,44.5 105,43.680652 105,42.6666667 C105,41.6526813 104.180653,40.8333333 103.166667,40.8333333 Z" id="path3858" fill="#684688" fill-rule="nonzero"></path>
|
||||
<path d="M247.471598,80 L243.471598,115.791667 L247.429931,116.208333 L251.013264,84 L291.929931,84 L295.513264,116.208333 L299.471598,115.791667 L295.471598,80 L247.471598,80 Z M255.471598,88 L255.471598,92 L287.471598,92 L287.471598,88 L255.471598,88 Z M255.471598,96 L255.471598,100 L287.471598,100 L287.471598,96 L255.471598,96 Z M255.471598,104 L255.471598,108 L287.471598,108 L287.471598,104 L255.471598,104 Z M255.471598,112 L255.471598,116 L287.471598,116 L287.471598,112 L255.471598,112 Z M241.471598,120 C240.371598,120 239.748598,120.872667 240.096598,121.916667 L246.846598,142.083333 C247.194598,143.127333 248.371598,144 249.471598,144 L293.471598,144 C294.571598,144 295.748598,143.127333 296.096598,142.083333 L302.846598,121.916667 C303.195931,120.872667 302.571598,120 301.471598,120 L241.471598,120 Z M263.471598,124 L279.471598,124 L279.471598,128 L263.471598,128 L263.471598,124 Z" id="path20" fill="#684688" fill-rule="nonzero"></path>
|
||||
<path d="M384,92.1508074 C381.644441,93.1945738 379.116959,93.9024443 376.461373,94.2184709 C379.172907,92.5947297 381.248518,90.0192484 382.232259,86.9597924 C379.688777,88.4634811 376.881243,89.5553522 373.889761,90.1472486 C371.494254,87.5917412 368.086876,86 364.307523,86 C357.056897,86 351.177986,91.8789116 351.177986,99.1255639 C351.177986,100.15333 351.29396,101.157149 351.51796,102.117046 C340.608034,101.569071 330.933874,96.3420823 324.459092,88.399481 C323.327273,90.3351705 322.683403,92.5906513 322.683403,94.9982892 C322.683403,99.5534862 325.002989,103.572735 328.522367,105.924216 C326.370729,105.852268 324.347092,105.260371 322.575481,104.276527 L322.575481,104.440501 C322.575481,110.799308 327.102652,116.106298 333.105485,117.314143 C332.005667,117.610091 330.845926,117.774065 329.646133,117.774065 C328.798341,117.774065 327.978471,117.690091 327.1746,117.530091 C328.846237,122.749132 333.693408,126.54438 339.436372,126.648432 C334.945175,130.167811 329.282211,132.259422 323.131404,132.259422 C322.071637,132.259422 321.027766,132.195422 320,132.079448 C325.810938,135.8108 332.709668,137.986386 340.124268,137.986386 C364.275732,137.986386 377.477323,117.982065 377.477323,100.633331 L377.433296,98.9336682 C380.012752,97.0938743 382.244285,94.7823411 384,92.1508074 Z" id="Path" fill="#FFFFFF" fill-rule="nonzero"></path>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 15 KiB |
307
docs/theme/index.css
vendored
Normal file
|
@ -0,0 +1,307 @@
|
|||
article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}audio,canvas,video{display:inline-block}audio:not([controls]){display:none;height:0}[hidden]{display:none}html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}a:focus{outline:thin dotted}a:active,a:hover{outline:0}h1{font-size:2em;margin:.67em 0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:bold}dfn{font-style:italic}hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0}mark{background:#ff0;color:#000}code,kbd,pre,samp{font-family:monospace,serif;font-size:1em}pre{white-space:pre-wrap}q{quotes:"\201C" "\201D" "\2018" "\2019"}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:0}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}legend{border:0;padding:0}button,input,select,textarea{font-family:inherit;font-size:100%;margin:0}button,input{line-height:normal}button,select{text-transform:none}button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0}input[type="search"]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}textarea{overflow:auto;vertical-align:top}table{border-collapse:collapse;border-spacing:0}
|
||||
|
||||
body
|
||||
{
|
||||
background-color: #FAFDFE;
|
||||
background-color: #f7f8f9;
|
||||
font-family: "Open Sans", "Helvetica Neue", sans-serif;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.icon {
|
||||
background-image: url("img/sprites.svg");
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: inline-block;
|
||||
font-size: 40px;
|
||||
background-size: 200px 80px;
|
||||
}
|
||||
|
||||
h3 { font-weight: 400; }
|
||||
|
||||
.icon.secure {
|
||||
background-position: 0em 0em;
|
||||
}
|
||||
|
||||
.icon.future {
|
||||
background-position: -1em 0em;
|
||||
}
|
||||
|
||||
.icon.search {
|
||||
background-position: -2em 0em;
|
||||
}
|
||||
|
||||
.icon.nli {
|
||||
background-position: -3em 0em;
|
||||
}
|
||||
|
||||
.icon.share {
|
||||
background-position: 0em -1em;
|
||||
}
|
||||
|
||||
.icon.sync {
|
||||
background-position: 0em -1em;
|
||||
}
|
||||
|
||||
.icon.dayone {
|
||||
background-position: -1em -1em;
|
||||
}
|
||||
|
||||
.icon.github {
|
||||
background-position: -2em -1em;
|
||||
}
|
||||
|
||||
.icon.folders {
|
||||
background-position: -3em -1em;
|
||||
}
|
||||
|
||||
.icon.twitter {
|
||||
background-position: -4em -1em;
|
||||
}
|
||||
|
||||
header {
|
||||
background-image: linear-gradient(211deg, #95699C 0%, #604385 100%);
|
||||
color: white;
|
||||
border: 0px solid transparent;
|
||||
display: relative;
|
||||
padding-top: 150px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#terminal {
|
||||
background: #1B1C2E;
|
||||
max-width: 520px;
|
||||
box-shadow: 0 -2px 16px 0 rgba(0,0,0,0.35);
|
||||
border-radius: 6px 6px 0 0;
|
||||
min-height: 100px;
|
||||
margin: 0px auto;
|
||||
position: relative;
|
||||
/*transform: translateY(40px);*/
|
||||
color: #f7f8f9;
|
||||
font-family: "Monaco", "Courier New";
|
||||
font-size: 12pt;
|
||||
padding: 45px 20px 0px 20px;
|
||||
line-height: 165%;
|
||||
}
|
||||
|
||||
#terminal b {
|
||||
font-weight: normal;
|
||||
color: #C2CDD9;
|
||||
}
|
||||
|
||||
#terminal i {
|
||||
font-style: normal;
|
||||
color: #BB97BA;
|
||||
}
|
||||
|
||||
#terminal:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
left: 15px;
|
||||
display: inline-block;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
border-radius: 50%;
|
||||
background: #3B3B4A;
|
||||
box-shadow: 25px 0 0 #3B3B4A, 50px 0 0 #3B3B4A;
|
||||
}
|
||||
|
||||
#typed:before {
|
||||
content: "$ ";
|
||||
color: #A879A7;
|
||||
}
|
||||
|
||||
#twitter {
|
||||
display: block;
|
||||
position: absolute;
|
||||
text-decoration: none;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
border: 1px solid white;
|
||||
padding: 5px 10px;
|
||||
color: white;
|
||||
border-radius: 3px;
|
||||
opacity: .7;
|
||||
}
|
||||
|
||||
#twitter .icon {
|
||||
transform: scale(.5);
|
||||
vertical-align: -18%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#twitter:hover,
|
||||
#twitter:active {
|
||||
opacity: 1;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#title {
|
||||
max-width: 630px;
|
||||
margin: 0 auto;
|
||||
padding: 0px 20px;
|
||||
}
|
||||
|
||||
#prompt {
|
||||
max-width: 700px;
|
||||
margin: 100px auto 0px auto;
|
||||
padding: 0px 20px;
|
||||
}
|
||||
|
||||
header img {
|
||||
float: left;
|
||||
margin-right: 30px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: white;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
nav {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
nav a#twitter-nav {
|
||||
display: none;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #684688;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
nav a {
|
||||
font-size: 14pt;
|
||||
line-height: 40pt;
|
||||
margin: 0 40px;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #A3629F;
|
||||
}
|
||||
|
||||
nav a.cta {
|
||||
display: inline-block;
|
||||
color: white;
|
||||
background-image: linear-gradient(259deg, #A3629F 0%, #604385 100%);
|
||||
box-shadow: 0 2px 8px 0 rgba(0,0,0,0.25);
|
||||
border-radius: 50px;
|
||||
padding: 0px 30pt;
|
||||
white-space: nowrap;
|
||||
transition: all .1s ease;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
nav a.cta:hover {
|
||||
text-decoration: none;
|
||||
background-image: linear-gradient(259deg, #AE57A8 0%, #68419C 100%);
|
||||
box-shadow: 0 4px 16px 0 rgba(0,0,0,0.25);
|
||||
color: #f7f8f9;
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 50px 0 0 0;
|
||||
}
|
||||
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
margin: 0 auto;
|
||||
max-width: 920px;
|
||||
flex-wrap: wrap;
|
||||
padding: 20px 20px;
|
||||
justify-content: space-between;
|
||||
|
||||
}
|
||||
|
||||
.flex section {
|
||||
/*margin: 20px;*/
|
||||
margin-top: 40px;
|
||||
width: 32%;
|
||||
}
|
||||
|
||||
.flex section:first-child {
|
||||
margin-left: 0px;
|
||||
}
|
||||
.flex section:last-child {
|
||||
margin-right: 0px;
|
||||
}
|
||||
|
||||
|
||||
.flex section i {
|
||||
float: left;
|
||||
left: 0;
|
||||
display: block;
|
||||
margin: 0px auto 10px auto;
|
||||
}
|
||||
|
||||
.flex section h3 {
|
||||
margin-top: 0;
|
||||
font-size: 14pt;
|
||||
color: #684688;
|
||||
margin-bottom: .5em;
|
||||
font-weight: 300;
|
||||
margin-left: 40px;
|
||||
}
|
||||
|
||||
.flex section p {
|
||||
padding-left: 40px;
|
||||
color: #888;
|
||||
font-size: 12pt;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
footer {
|
||||
max-width: 700px;
|
||||
margin: 20px auto;
|
||||
padding: 0 20px 20px 20px;
|
||||
font-size: 10pt;
|
||||
opacity: .5;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 680px) {
|
||||
.flex {
|
||||
display: block;
|
||||
padding: 0;
|
||||
}
|
||||
.flex section {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 20px;
|
||||
margin: 0;
|
||||
width: calc(100% - 40px);
|
||||
}
|
||||
|
||||
nav a,
|
||||
nav a#twitter-nav {
|
||||
display: inline-block;
|
||||
margin: 0px 10px;
|
||||
}
|
||||
|
||||
nav a.cta {
|
||||
display: block;
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
header #twitter {
|
||||
display: none;
|
||||
}
|
||||
|
||||
header #logo {
|
||||
display: block;
|
||||
float: none;
|
||||
margin: 0px auto;
|
||||
}
|
||||
|
||||
header #title br {
|
||||
display: none;
|
||||
}
|
||||
|
||||
}
|
126
docs/theme/index.html
vendored
Executable file
|
@ -0,0 +1,126 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>jrnl - The Command Line Journal</title>
|
||||
<meta name="description" content="Collect your thoughts and notes without leaving the command line.">
|
||||
<meta name="image" content="https://jrnl.sh/img/banner.png">
|
||||
<meta itemprop="name" content="jrnl - The Command Line Journal">
|
||||
<meta itemprop="description" content="Collect your thoughts and notes without leaving the command line.">
|
||||
<meta itemprop="image" content="https://jrnl.sh/img/banner_og.png">
|
||||
<meta name="twitter:card" content="summary">
|
||||
<meta name="twitter:title" content="jrnl - The Command Line Journal">
|
||||
<meta name="twitter:description" content="Collect your thoughts and notes without leaving the command line.">
|
||||
<meta name="twitter:creator" content="jrnl">
|
||||
<meta name="twitter:image:src" content="https://jrnl.sh/img/banner_twitter.png">
|
||||
<meta name="og:title" content="jrnl - The Command Line Journal">
|
||||
<meta name="og:description" content="Collect your thoughts and notes without leaving the command line.">
|
||||
<meta name="og:image" content="https://jrnl.sh/img/banner_og.png">
|
||||
<meta name="og:url" content="https://jrnl.sh">
|
||||
<meta name="og:site_name" content="jrnl - The Command Line Journal">
|
||||
<meta name="og:type" content="website">
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<link href='http://fonts.googleapis.com/css?family=Open+Sans:300,400' rel='stylesheet' type='text/css'>
|
||||
<link rel="stylesheet" href="index.css">
|
||||
<link rel="shortcut icon" href="img/favicon.ico">
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
"@context": "https://schema.org",
|
||||
"@type": "SoftwareApplication",
|
||||
"applicationCategory": "https://schema.org/LifestyleApplication",
|
||||
"name": "jrnl",
|
||||
"description": "Collect your thoughts and notes without leaving the command line.",
|
||||
"operatingSystem": ["macOS", "Windows", "Linux"],
|
||||
"thumbnailUrl": "https://jrnl.sh/img/banner_og.png",
|
||||
"installUrl": "https://jrnl.sh/installation",
|
||||
"softwareVersion": "2.0.0rc2"
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header>
|
||||
<aside>
|
||||
<a id="twitter" href="https://twitter.com/intent/tweet?text=Write+your+memoirs+on+the+command+line.+Like+a+boss.+%23jrnl&url=http%3A%2F%2Fjrnl.sh&via=maebert"><i class="icon twitter"></i>Tell your friends</a>
|
||||
</aside>
|
||||
<div id="title">
|
||||
<img id="logo" src="img/jrnl_white.svg" width="90px" height="98px" title="jrnl" />
|
||||
<h1>Collect your thoughts and notes <br />without leaving the command line.</h1>
|
||||
</div>
|
||||
<div id="prompt">
|
||||
<div id="terminal">
|
||||
<div id="typed"></div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<main>
|
||||
<nav>
|
||||
<a href="overview">Documentation</a>
|
||||
<a href="http://github.com/jrnl-org/jrnl" title="View on Github">Fork me on GitHub</a>
|
||||
<a id="twitter-nav" href="https://twitter.com/intent/tweet?text=Write+your+memoirs+on+the+command+line.+Like+a+boss.+%23jrnl&url=http%3A%2F%2Fjrnl.sh&via=maebert">Tell your friends on twitter</a>
|
||||
<a href="installation" class="cta">Download</a>
|
||||
</nav>
|
||||
<div class="flex">
|
||||
<section>
|
||||
<i class="icon nli"></i>
|
||||
<h3>Human friendly.</h3>
|
||||
<p>jrnl has a natural-language interface so you don't have to remember cryptic shortcuts when you're writing down your thoughts.</p>
|
||||
</section>
|
||||
<section>
|
||||
<i class="icon future"></i>
|
||||
<h3>Future-proof.</h3>
|
||||
<p>Your journals are stored in plain-text files that will still be readable in 50 years when all your fancy iPad apps will have gone the way of the Dodo.</p>
|
||||
</section>
|
||||
<section>
|
||||
<i class="icon secure"></i>
|
||||
<h3>Secure.</h3>
|
||||
<p>Encrypt your journals with the industry-strength AES encryption. The NSA won't be able to read your dirty secrets.</p>
|
||||
</section>
|
||||
<section>
|
||||
<i class="icon sync"></i>
|
||||
<h3>Accessible anywhere.</h3>
|
||||
<p>Sync your journals with Dropbox and capture your thoughts where ever you are</p>
|
||||
</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>
|
||||
<i class="icon github"></i>
|
||||
<h3>Free & Open Source.</h3>
|
||||
<p>jrnl is made by a bunch of really friendly and remarkably attractive people. Maybe even <a href="https://www.github.com/jrnl-org/jrnl" title="Fork jrnl on GitHub">you</a>?</p>
|
||||
</section>
|
||||
<section>
|
||||
<i class="icon folders"></i>
|
||||
<h3>For work and play.</h3>
|
||||
<p>Effortlessly access several journals for all parts of your life.</p>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
<footer>
|
||||
jrnl is made with love by <a rel="author" href="http://www.1450.me">Manuel Ebert</a> and <a href="https://github.com/jrnl-org/jrnl/graphs/contributors" title="Contributors">many other fabulous people</a>. If you need help, <a href="https://github.com/jrnl-org/jrnl/issues/new" title="Open a new issue on Github">submit an issue</a> on Github.
|
||||
</footer>
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/typed.js/2.0.10/typed.min.js"></script>
|
||||
<script>
|
||||
new Typed("#typed", {
|
||||
strings: [
|
||||
"jrnl today: Started writing my memoirs. On the command line. Like a boss.",
|
||||
"jrnl yesterday 2pm: used jrnl to keep track of accomplished tasks. The done.txt for my todo.txt",
|
||||
"jrnl <b>-from</b> 2009 <b>-until</b> may<br /><i>`(Displays all entries from January 2009 to last may)`</i>",
|
||||
"jrnl A day on the beach with @beth and @frank. Taggidy-tag-tag.",
|
||||
"jrnl <b>--tags</b><br /><i>`@idea 7<br />@beth 5</i>`",
|
||||
"jrnl <b>--export</b> json<br /><i>`(Exports your entire journal to json)</i>`",
|
||||
"jrnl <b>--encrypt</b><br /><i>`(256 bit AES encryption. Crack this, NSA)</i>`"
|
||||
],
|
||||
typeSpeed: 35,
|
||||
backSpeed: 15,
|
||||
backDelay: 2000,
|
||||
loop: true,
|
||||
showCursor: false
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
1
docs/theme/index.js
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
var typed2 =
|
182
docs/usage.md
Normal file
|
@ -0,0 +1,182 @@
|
|||
# Basic Usage
|
||||
|
||||
`jrnl` has two modes: **composing** and **viewing**. Basically, whenever
|
||||
you *don't* supply any arguments that start
|
||||
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.
|
||||
|
||||
We intentionally break a convention on command line arguments: all
|
||||
arguments starting with a *single dash*
|
||||
will *filter* your journal before viewing
|
||||
it, and can be combined arbitrarily. Arguments with a
|
||||
*double dash* will control how your journal
|
||||
is displayed or exported and are mutually exclusive (ie. you can only
|
||||
specify one way to display or export your journal at a time).
|
||||
|
||||
## Listing Journals
|
||||
|
||||
You can list the journals accessible by jrnl
|
||||
|
||||
``` sh
|
||||
jrnl -ls
|
||||
```
|
||||
|
||||
The journals displayed correspond to those specified in the jrnl
|
||||
configuration file.
|
||||
|
||||
## Composing Entries
|
||||
|
||||
Composing mode is entered by either starting `jrnl` without any
|
||||
arguments -- which will prompt you to write an entry or launch your
|
||||
editor -- or by just writing an entry on the prompt, such as
|
||||
|
||||
``` sh
|
||||
jrnl today at 3am: I just met Steve Buscemi in a bar! He looked funny.
|
||||
```
|
||||
|
||||
!!! note
|
||||
Most shell contains a certain number of reserved characters, such as `#`
|
||||
and `*`. Unbalanced quotes, parenthesis, and so on will also get into
|
||||
the way of your editing.
|
||||
For writing longer entries, just enter `jrnl`
|
||||
and hit `return`. Only then enter the text of your journal entry.
|
||||
Alternatively, `use an external editor <advanced>`).
|
||||
|
||||
You can also import an entry directly from a file
|
||||
|
||||
``` sh
|
||||
jrnl < my_entry.txt
|
||||
```
|
||||
|
||||
### Smart timestamps
|
||||
|
||||
Timestamps that work:
|
||||
|
||||
- at 6am
|
||||
- yesterday
|
||||
- last monday
|
||||
- sunday at noon
|
||||
- 2 march 2012
|
||||
- 7 apr
|
||||
- 5/20/1998 at 23:42
|
||||
|
||||
### Starring entries
|
||||
|
||||
To mark an entry as a favourite, simply "star" it
|
||||
|
||||
``` sh
|
||||
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),
|
||||
The following options are equivalent:
|
||||
|
||||
- `jrnl *: Best day of my life.`
|
||||
- `jrnl *Best day of my life.`
|
||||
- `jrnl Best day of my life.*`
|
||||
|
||||
!!! note
|
||||
Just make sure that the asterisk sign is **not** surrounded by
|
||||
whitespaces, e.g. `jrnl Best day of my life! *` will **not** work (the
|
||||
reason being that the `*` sign has a special meaning on most shells).
|
||||
|
||||
## Viewing
|
||||
|
||||
``` sh
|
||||
jrnl -n 10
|
||||
```
|
||||
|
||||
will list you the ten latest entries (if you're lazy, `jrnl -10` will do
|
||||
the same),
|
||||
|
||||
``` sh
|
||||
jrnl -from "last year" -until march
|
||||
```
|
||||
|
||||
everything that happened from the start of last year to the start of
|
||||
last march. To only see your favourite entries, use
|
||||
|
||||
``` sh
|
||||
jrnl -starred
|
||||
```
|
||||
|
||||
## Using Tags
|
||||
|
||||
Keep track of people, projects or locations, by tagging them with an `@`
|
||||
in your entries
|
||||
|
||||
``` sh
|
||||
jrnl Had a wonderful day on the @beach with @Tom and @Anna.
|
||||
```
|
||||
|
||||
You can filter your journal entries just like this:
|
||||
|
||||
``` sh
|
||||
jrnl @pinkie @WorldDomination
|
||||
```
|
||||
|
||||
Will print all entries in which either `@pinkie` or `@WorldDomination`
|
||||
occurred.
|
||||
|
||||
``` sh
|
||||
jrnl -n 5 -and @pineapple @lubricant
|
||||
```
|
||||
|
||||
the last five entries containing both `@pineapple` **and** `@lubricant`.
|
||||
You can change which symbols you'd like to use for tagging in the
|
||||
configuration.
|
||||
|
||||
!!! note
|
||||
`jrnl @pinkie @WorldDomination` will switch to viewing mode because
|
||||
although **no** command line arguments are given, all the input strings
|
||||
look like tags - *jrnl* will assume you want to filter by tag.
|
||||
|
||||
## Editing older entries
|
||||
|
||||
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
|
||||
journal. To use this feature, you need to have an editor configured in
|
||||
your journal configuration file (see `advanced usage <advanced>`)
|
||||
|
||||
``` sh
|
||||
jrnl -until 1950 @texas -and @history --edit
|
||||
```
|
||||
|
||||
Will open your editor with all entries tagged with `@texas` and
|
||||
`@history` before 1950. You can make any changes to them you want; after
|
||||
you save the file and close the editor, your journal will be updated.
|
||||
|
||||
Of course, if you are using multiple journals, you can also edit e.g.
|
||||
the latest entry of your work journal with `jrnl work -n 1 --edit`. In
|
||||
any case, this will bring up your editor and save (and, if applicable,
|
||||
encrypt) your edited journal after you save and exit the editor.
|
||||
|
||||
You can also use this feature for deleting entries from your journal
|
||||
|
||||
``` sh
|
||||
jrnl @girlfriend -until 'june 2012' --edit
|
||||
```
|
||||
|
||||
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).
|
60
features/core.feature
Normal file
|
@ -0,0 +1,60 @@
|
|||
Feature: Basic reading and writing to a journal
|
||||
|
||||
Scenario: Loading a sample journal
|
||||
Given we use the config "basic.yaml"
|
||||
When we run "jrnl -n 2"
|
||||
Then we should get no error
|
||||
and the output should be
|
||||
"""
|
||||
2013-06-09 15:39 My first entry.
|
||||
| Everything is alright
|
||||
|
||||
2013-06-10 15:40 Life is good.
|
||||
| But I'm better.
|
||||
"""
|
||||
|
||||
Scenario: Writing an entry from command line
|
||||
Given we use the config "basic.yaml"
|
||||
When we run "jrnl 23 july 2013: 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 "2013-07-23 09:00 A cold and stormy day."
|
||||
|
||||
Scenario: Filtering for dates
|
||||
Given we use the config "basic.yaml"
|
||||
When we run "jrnl -on 2013-06-10 --short"
|
||||
Then the output should be "2013-06-10 15:40 Life is good."
|
||||
When we run "jrnl -on 'june 6 2013' --short"
|
||||
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
|
||||
Given we use the config "basic.yaml"
|
||||
When we run "jrnl" and enter "25 jul 2013: I saw Elvis. He's alive."
|
||||
Then we should get no error
|
||||
and the journal should contain "[2013-07-25 09:00] I saw Elvis."
|
||||
and the journal should contain "He's alive."
|
||||
|
||||
Scenario: Displaying the version number
|
||||
Given we use the config "basic.yaml"
|
||||
When we run "jrnl -v"
|
||||
Then we should get no error
|
||||
Then the output should contain "version"
|
||||
|
||||
Scenario: --short displays the short version of entries (only the title)
|
||||
Given we use the config "basic.yaml"
|
||||
When we run "jrnl -on 2013-06-10 --short"
|
||||
Then the output should be "2013-06-10 15:40 Life is good."
|
||||
|
||||
Scenario: -s displays the short version of entries (only the title)
|
||||
Given we use the config "basic.yaml"
|
||||
When we run "jrnl -on 2013-06-10 -s"
|
||||
Then the output should be "2013-06-10 15:40 Life is good."
|
||||
|
12
features/data/configs/basic.yaml
Normal file
|
@ -0,0 +1,12 @@
|
|||
default_hour: 9
|
||||
default_minute: 0
|
||||
editor: ""
|
||||
encrypt: false
|
||||
highlight: true
|
||||
journals:
|
||||
default: features/journals/simple.journal
|
||||
linewrap: 80
|
||||
tagsymbols: "@"
|
||||
template: false
|
||||
timeformat: "%Y-%m-%d %H:%M"
|
||||
indent_character: "|"
|
12
features/data/configs/brackets.yaml
Normal file
|
@ -0,0 +1,12 @@
|
|||
default_hour: 9
|
||||
default_minute: 0
|
||||
editor: ""
|
||||
encrypt: false
|
||||
highlight: true
|
||||
journals:
|
||||
default: features/journals/brackets.journal
|
||||
linewrap: 80
|
||||
tagsymbols: "@"
|
||||
template: false
|
||||
timeformat: "%Y-%m-%d %H:%M"
|
||||
indent_character: "|"
|
13
features/data/configs/bug153.yaml
Normal file
|
@ -0,0 +1,13 @@
|
|||
default_hour: 9
|
||||
default_minute: 0
|
||||
editor: ''
|
||||
encrypt: false
|
||||
highlight: true
|
||||
journals:
|
||||
default: features/journals/bug153.dayone
|
||||
linewrap: 80
|
||||
password: ''
|
||||
tagsymbols: '@'
|
||||
template: false
|
||||
timeformat: '%Y-%m-%d %H:%M'
|
||||
indent_character: "|"
|
13
features/data/configs/bug343.yaml
Normal file
|
@ -0,0 +1,13 @@
|
|||
default_hour: 9
|
||||
default_minute: 0
|
||||
editor: ''
|
||||
template: false
|
||||
encrypt: false
|
||||
highlight: true
|
||||
journals:
|
||||
simple: features/journals/simple.journal
|
||||
work: features/journals/work.journal
|
||||
linewrap: 80
|
||||
tagsymbols: '@'
|
||||
timeformat: '%Y-%m-%d %H:%M'
|
||||
indent_character: "|"
|
13
features/data/configs/dayone.yaml
Normal file
|
@ -0,0 +1,13 @@
|
|||
default_hour: 9
|
||||
default_minute: 0
|
||||
editor: ''
|
||||
template: false
|
||||
encrypt: false
|
||||
highlight: true
|
||||
journals:
|
||||
default: features/journals/dayone.dayone
|
||||
linewrap: 80
|
||||
password: ''
|
||||
tagsymbols: '@'
|
||||
timeformat: '%Y-%m-%d %H:%M'
|
||||
indent_character: "|"
|
13
features/data/configs/empty_folder.yaml
Normal file
|
@ -0,0 +1,13 @@
|
|||
default_hour: 9
|
||||
default_minute: 0
|
||||
editor: ''
|
||||
template: false
|
||||
encrypt: false
|
||||
highlight: true
|
||||
journals:
|
||||
default: features/journals/empty_folder
|
||||
linewrap: 80
|
||||
password: ''
|
||||
tagsymbols: '@'
|
||||
timeformat: '%Y-%m-%d %H:%M'
|
||||
indent_character: "|"
|
13
features/data/configs/encrypted.yaml
Normal file
|
@ -0,0 +1,13 @@
|
|||
default_hour: 9
|
||||
default_minute: 0
|
||||
editor: ''
|
||||
encrypt: true
|
||||
template: false
|
||||
highlight: true
|
||||
journals:
|
||||
default: features/journals/encrypted.journal
|
||||
linewrap: 80
|
||||
password: ''
|
||||
tagsymbols: '@'
|
||||
timeformat: '%Y-%m-%d %H:%M'
|
||||
indent_character: "|"
|
13
features/data/configs/encrypted_old.json
Normal file
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"default_hour": 9,
|
||||
"default_minute": 0,
|
||||
"editor": "",
|
||||
"encrypt": true,
|
||||
"highlight": true,
|
||||
"journals": {
|
||||
"default": "features/journals/encrypted_jrnl-1-9-5.journal"
|
||||
},
|
||||
"linewrap": 80,
|
||||
"tagsymbols": "@",
|
||||
"timeformat": "%Y-%m-%d %H:%M"
|
||||
}
|
11
features/data/configs/encrypted_old.yaml
Normal file
|
@ -0,0 +1,11 @@
|
|||
default_hour: 9
|
||||
default_minute: 0
|
||||
editor: ''
|
||||
encrypt: true
|
||||
highlight: true
|
||||
journals:
|
||||
default: features/journals/encrypted_jrnl1-9-5.journal
|
||||
linewrap: 80
|
||||
tagsymbols: '@'
|
||||
timeformat: '%Y-%m-%d %H:%M'
|
||||
indent_character: "|"
|
13
features/data/configs/markdown-headings-335.yaml
Normal file
|
@ -0,0 +1,13 @@
|
|||
default_hour: 9
|
||||
default_minute: 0
|
||||
editor: ''
|
||||
encrypt: false
|
||||
highlight: true
|
||||
template: false
|
||||
journals:
|
||||
default: features/journals/markdown-headings-335.journal
|
||||
linewrap: 80
|
||||
password: ''
|
||||
tagsymbols: '@'
|
||||
timeformat: '%Y-%m-%d %H:%M'
|
||||
indent_character: "|"
|
12
features/data/configs/mostlyreadabledates.yaml
Normal file
|
@ -0,0 +1,12 @@
|
|||
default_hour: 9
|
||||
default_minute: 0
|
||||
editor: ""
|
||||
encrypt: false
|
||||
highlight: true
|
||||
journals:
|
||||
default: features/journals/mostlyreadabledates.journal
|
||||
linewrap: 80
|
||||
tagsymbols: "@"
|
||||
template: false
|
||||
timeformat: "%Y-%m-%d %H:%M"
|
||||
indent_character: "|"
|
19
features/data/configs/multiple.yaml
Normal file
|
@ -0,0 +1,19 @@
|
|||
default_hour: 9
|
||||
default_minute: 0
|
||||
editor: ''
|
||||
encrypt: false
|
||||
highlight: true
|
||||
template: false
|
||||
journals:
|
||||
default: features/journals/simple.journal
|
||||
ideas: features/journals/nothing.journal
|
||||
simple: features/journals/simple.journal
|
||||
work: features/journals/work.journal
|
||||
new_encrypted:
|
||||
encrypt: true
|
||||
journal: features/journals/new_encrypted.journal
|
||||
linewrap: 80
|
||||
password: ''
|
||||
tagsymbols: '@'
|
||||
timeformat: '%Y-%m-%d %H:%M'
|
||||
indent_character: "|"
|
13
features/data/configs/tags-216.yaml
Normal file
|
@ -0,0 +1,13 @@
|
|||
default_hour: 9
|
||||
default_minute: 0
|
||||
editor: ''
|
||||
encrypt: false
|
||||
highlight: true
|
||||
template: false
|
||||
journals:
|
||||
default: features/journals/tags-216.journal
|
||||
linewrap: 80
|
||||
password: ''
|
||||
tagsymbols: '@'
|
||||
timeformat: '%Y-%m-%d %H:%M'
|
||||
indent_character: "|"
|
13
features/data/configs/tags-237.yaml
Normal file
|
@ -0,0 +1,13 @@
|
|||
default_hour: 9
|
||||
default_minute: 0
|
||||
editor: ''
|
||||
encrypt: false
|
||||
highlight: true
|
||||
template: false
|
||||
journals:
|
||||
default: features/journals/tags-237.journal
|
||||
linewrap: 80
|
||||
password: ''
|
||||
tagsymbols: '@'
|
||||
timeformat: '%Y-%m-%d %H:%M'
|
||||
indent_character: "|"
|
13
features/data/configs/tags.yaml
Normal file
|
@ -0,0 +1,13 @@
|
|||
default_hour: 9
|
||||
default_minute: 0
|
||||
editor: ''
|
||||
encrypt: false
|
||||
highlight: true
|
||||
template: false
|
||||
journals:
|
||||
default: features/journals/tags.journal
|
||||
linewrap: 80
|
||||
password: ''
|
||||
tagsymbols: '@'
|
||||
timeformat: '%Y-%m-%d %H:%M'
|
||||
indent_character: "|"
|
12
features/data/configs/unreadabledates.yaml
Normal file
|
@ -0,0 +1,12 @@
|
|||
default_hour: 9
|
||||
default_minute: 0
|
||||
editor: ""
|
||||
encrypt: false
|
||||
highlight: true
|
||||
journals:
|
||||
default: features/journals/unreadabledates.journal
|
||||
linewrap: 80
|
||||
tagsymbols: "@"
|
||||
template: false
|
||||
timeformat: "%Y-%m-%d %H:%M"
|
||||
indent_character: "|"
|
11
features/data/configs/upgrade_from_195.json
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"default_hour": 9,
|
||||
"timeformat": "%Y-%m-%d %H:%M",
|
||||
"linewrap": 80,
|
||||
"encrypt": false,
|
||||
"editor": "",
|
||||
"default_minute": 0,
|
||||
"highlight": true,
|
||||
"journals": {"default": "features/journals/simple_jrnl-1-9-5.journal"},
|
||||
"tagsymbols": "@"
|
||||
}
|
2
features/data/journals/brackets.journal
Normal file
|
@ -0,0 +1,2 @@
|
|||
[2019-07-08 05:42] Entry subject
|
||||
[1] line starting with 1
|
|
@ -0,0 +1,56 @@
|
|||
<?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>Creation Date</key>
|
||||
<date>2013-10-27T02:27:27Z</date>
|
||||
<key>Creator</key>
|
||||
<dict>
|
||||
<key>Device Agent</key>
|
||||
<string>iPhone/iPhone3,1</string>
|
||||
<key>Generation Date</key>
|
||||
<date>2013-10-27T07:02:27Z</date>
|
||||
<key>Host Name</key>
|
||||
<string>omrt104001</string>
|
||||
<key>OS Agent</key>
|
||||
<string>iOS/7.0.3</string>
|
||||
<key>Software Agent</key>
|
||||
<string>Day One (iOS)/1.11.4</string>
|
||||
</dict>
|
||||
<key>Entry Text</key>
|
||||
<string>Some text.</string>
|
||||
<key>Location</key>
|
||||
<dict>
|
||||
<key>Administrative Area</key>
|
||||
<string>Östergötlands län</string>
|
||||
<key>Country</key>
|
||||
<string>Sverige</string>
|
||||
<key>Latitude</key>
|
||||
<real>58.383400000000000</real>
|
||||
<key>Locality</key>
|
||||
<string>City</string>
|
||||
<key>Longitude</key>
|
||||
<real>15.577170000000000</real>
|
||||
<key>Place Name</key>
|
||||
<string>Street</string>
|
||||
</dict>
|
||||
<key>Starred</key>
|
||||
<false/>
|
||||
<key>Time Zone</key>
|
||||
<string>Europe/Stockholm</string>
|
||||
<key>UUID</key>
|
||||
<string>B40EE704E15846DE8D45C44118A4D511</string>
|
||||
<key>Weather</key>
|
||||
<dict>
|
||||
<key>Celsius</key>
|
||||
<string>12</string>
|
||||
<key>Description</key>
|
||||
<string>Clear</string>
|
||||
<key>Fahrenheit</key>
|
||||
<string>54</string>
|
||||
<key>IconName</key>
|
||||
<string>sunnyn.png</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
|
@ -0,0 +1,52 @@
|
|||
<dict>
|
||||
<key>Creation Date</key>
|
||||
<date>2013-10-27T02:27:27Z</date>
|
||||
<key>Creator</key>
|
||||
<dict>
|
||||
<key>Device Agent</key>
|
||||
<string>iPhone/iPhone3,1</string>
|
||||
<key>Generation Date</key>
|
||||
<date>2013-10-27T07:02:27Z</date>
|
||||
<key>Host Name</key>
|
||||
<string>omrt104001</string>
|
||||
<key>OS Agent</key>
|
||||
<string>iOS/7.0.3</string>
|
||||
<key>Software Agent</key>
|
||||
<string>Day One (iOS)/1.11.4</string>
|
||||
</dict>
|
||||
<key>Entry Text</key>
|
||||
<string>This is not a valid plist.</string>
|
||||
<key>Location</key>
|
||||
<dict>
|
||||
<key>Administrative Area</key>
|
||||
<string>Östergötlands län</string>
|
||||
<key>Country</key>
|
||||
<string>Sverige</string>
|
||||
<key>Latitude</key>
|
||||
<real>58.383400000000000</real>
|
||||
<key>Locality</key>
|
||||
<string>City</string>
|
||||
<key>Longitude</key>
|
||||
<real>15.577170000000000</real>
|
||||
<key>Place Name</key>
|
||||
<string>Street</string>
|
||||
</dict>
|
||||
<key>Starred</key>
|
||||
<false/>
|
||||
<key>Time Zone</key>
|
||||
<string>Europe/Stockholm</string>
|
||||
<key>UUID</key>
|
||||
<string>B40EE704E15846DE8D45C44118A4D511</string>
|
||||
<key>Weather</key>
|
||||
<dict>
|
||||
<key>Celsius</key>
|
||||
<string>12</string>
|
||||
<key>Description</key>
|
||||
<string>Clear</string>
|
||||
<key>Fahrenheit</key>
|
||||
<string>54</string>
|
||||
<key>IconName</key>
|
||||
<string>sunnyn.png</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
|
@ -0,0 +1,34 @@
|
|||
<?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>Creation Date</key>
|
||||
<date>2013-05-17T18:39:20Z</date>
|
||||
<key>Creator</key>
|
||||
<dict>
|
||||
<key>Device Agent</key>
|
||||
<string>Macintosh/MacBookAir5,2</string>
|
||||
<key>Generation Date</key>
|
||||
<date>2013-08-17T18:39:20Z</date>
|
||||
<key>Host Name</key>
|
||||
<string>Egeria</string>
|
||||
<key>OS Agent</key>
|
||||
<string>Mac OS X/10.8.4</string>
|
||||
<key>Software Agent</key>
|
||||
<string>Day One (Mac)/1.8</string>
|
||||
</dict>
|
||||
<key>Entry Text</key>
|
||||
<string>This entry has tags!</string>
|
||||
<key>Starred</key>
|
||||
<false/>
|
||||
<key>Tags</key>
|
||||
<array>
|
||||
<string>work</string>
|
||||
<string>PLaY</string>
|
||||
</array>
|
||||
<key>Time Zone</key>
|
||||
<string>America/Los_Angeles</string>
|
||||
<key>UUID</key>
|
||||
<string>044F3747A38546168B572C2E3F217FA2</string>
|
||||
</dict>
|
||||
</plist>
|
|
@ -0,0 +1,46 @@
|
|||
<?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>Creation Date</key>
|
||||
<date>2013-06-17T18:38:29Z</date>
|
||||
<key>Creator</key>
|
||||
<dict>
|
||||
<key>Device Agent</key>
|
||||
<string>Macintosh/MacBookAir5,2</string>
|
||||
<key>Generation Date</key>
|
||||
<date>2013-08-17T18:38:29Z</date>
|
||||
<key>Host Name</key>
|
||||
<string>Egeria</string>
|
||||
<key>OS Agent</key>
|
||||
<string>Mac OS X/10.8.4</string>
|
||||
<key>Software Agent</key>
|
||||
<string>Day One (Mac)/1.8</string>
|
||||
</dict>
|
||||
<key>Entry Text</key>
|
||||
<string>This entry has a location.</string>
|
||||
<key>Location</key>
|
||||
<dict>
|
||||
<key>Administrative Area</key>
|
||||
<string>California</string>
|
||||
<key>Country</key>
|
||||
<string>Germany</string>
|
||||
<key>Latitude</key>
|
||||
<real>52.4979764</real>
|
||||
<key>Locality</key>
|
||||
<string>Berlin</string>
|
||||
<key>Longitude</key>
|
||||
<real>13.2404758</real>
|
||||
<key>Place Name</key>
|
||||
<string>Abandoned Spy Tower</string>
|
||||
</dict>
|
||||
<key>Starred</key>
|
||||
<false/>
|
||||
<key>Tags</key>
|
||||
<array/>
|
||||
<key>Time Zone</key>
|
||||
<string>Europe/Berlin</string>
|
||||
<key>UUID</key>
|
||||
<string>0BDDD6CDA43C4A9AA2681517CC35AD9D</string>
|
||||
</dict>
|
||||
</plist>
|
|
@ -0,0 +1,31 @@
|
|||
<?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>Creation Date</key>
|
||||
<date>2013-07-17T18:38:08Z</date>
|
||||
<key>Creator</key>
|
||||
<dict>
|
||||
<key>Device Agent</key>
|
||||
<string>Macintosh/MacBookAir5,2</string>
|
||||
<key>Generation Date</key>
|
||||
<date>2013-08-17T18:38:08Z</date>
|
||||
<key>Host Name</key>
|
||||
<string>Egeria</string>
|
||||
<key>OS Agent</key>
|
||||
<string>Mac OS X/10.8.4</string>
|
||||
<key>Software Agent</key>
|
||||
<string>Day One (Mac)/1.8</string>
|
||||
</dict>
|
||||
<key>Entry Text</key>
|
||||
<string>This entry is starred!</string>
|
||||
<key>Starred</key>
|
||||
<true/>
|
||||
<key>Tags</key>
|
||||
<array/>
|
||||
<key>Time Zone</key>
|
||||
<string>America/Los_Angeles</string>
|
||||
<key>UUID</key>
|
||||
<string>422BC895507944A291E6FC44FC6B8BFC</string>
|
||||
</dict>
|
||||
</plist>
|
|
@ -0,0 +1,29 @@
|
|||
<?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>Creation Date</key>
|
||||
<date>2013-01-17T18:37:50Z</date>
|
||||
<key>Creator</key>
|
||||
<dict>
|
||||
<key>Device Agent</key>
|
||||
<string>Macintosh/MacBookAir5,2</string>
|
||||
<key>Generation Date</key>
|
||||
<date>2013-08-17T18:37:50Z</date>
|
||||
<key>Host Name</key>
|
||||
<string>Egeria</string>
|
||||
<key>OS Agent</key>
|
||||
<string>Mac OS X/10.8.4</string>
|
||||
<key>Software Agent</key>
|
||||
<string>Day One (Mac)/1.8</string>
|
||||
</dict>
|
||||
<key>Entry Text</key>
|
||||
<string>This is a DayOne entry without Timezone.</string>
|
||||
<key>Starred</key>
|
||||
<false/>
|
||||
<key>Tags</key>
|
||||
<array/>
|
||||
<key>UUID</key>
|
||||
<string>4BB1F46946AD439996C9B59DE7C4DDC1</string>
|
||||
</dict>
|
||||
</plist>
|
1
features/data/journals/empty_folder/empty.txt
Normal file
|
@ -0,0 +1 @@
|
|||
Nothing to see here
|
1
features/data/journals/encrypted.journal
Normal file
|
@ -0,0 +1 @@
|
|||
gAAAAABVIHB7tnwKExG7aC5ZbAbBL9SG2oY2GENeoOJ22i1PZigOvCYvrQN3kpsu0KGr7ay5K-_46R5YFlqJvtQ8anPH2FSITsaZy-l5Lz_5quw3rmzhLwAR1tc0icgtR4MEpXEdsuQ7cyb12Xq-JLDrnATs0id5Vow9Ri_tE7Xe4BXgXaySn3aRPwWKoninVxVPVvETY3MXHSUEXV9OZ-pH5kYBLGYbLA==
|
BIN
features/data/journals/encrypted_jrnl-1-9-5.journal
Normal file
42
features/data/journals/markdown-headings-335.journal
Normal file
|
@ -0,0 +1,42 @@
|
|||
[2015-04-14 13:23] Heading Test
|
||||
|
||||
H1-1
|
||||
=
|
||||
|
||||
H1-2
|
||||
===
|
||||
|
||||
H1-3
|
||||
============================
|
||||
|
||||
H2-1
|
||||
-
|
||||
|
||||
H2-2
|
||||
---
|
||||
|
||||
H2-3
|
||||
----------------------------------
|
||||
|
||||
Horizontal Rules (ignore)
|
||||
|
||||
---
|
||||
|
||||
===
|
||||
|
||||
# ATX H1
|
||||
|
||||
## ATX H2
|
||||
|
||||
### ATX H3
|
||||
|
||||
#### ATX H4
|
||||
|
||||
##### ATX H5
|
||||
|
||||
###### ATX H6
|
||||
|
||||
Stuff
|
||||
|
||||
More stuff
|
||||
more stuff again
|
8
features/data/journals/mostlyreadabledates.journal
Normal file
|
@ -0,0 +1,8 @@
|
|||
[2019-07-18 14:23] Entry subject
|
||||
Time machines are possible. I know, because I've built one in my garage.
|
||||
|
||||
[2019-07-19 14:23] Entry subject
|
||||
I'm going to activate the machine. Nobody knows what comes next after this. Or before this?
|
||||
|
||||
[2019-07 14:23] Entry subject
|
||||
I've crossed so many timelines. Is there any going back?
|
5
features/data/journals/simple.journal
Normal file
|
@ -0,0 +1,5 @@
|
|||
[2013-06-09 15:39] My first entry.
|
||||
Everything is alright
|
||||
|
||||
[2013-06-10 15:40] Life is good.
|
||||
But I'm better.
|
13
features/data/journals/simple_jrnl-1-9-5.journal
Normal file
|
@ -0,0 +1,13 @@
|
|||
2010-06-10 15:00 A life without chocolate is like a bad analogy.
|
||||
|
||||
2013-06-10 15:40 He said "[this] is the best time to be alive".
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Praesent malesuada
|
||||
quis est ac dignissim. Aliquam dignissim rutrum pretium. Phasellus pellentesque
|
||||
augue et venenatis facilisis.
|
||||
|
||||
[2019-08-03 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.
|
2
features/data/journals/tags-216.journal
Normal file
|
@ -0,0 +1,2 @@
|
|||
[2013-06-10 15:40] I programmed for @OS/2.
|
||||
Almost makes me want to go back to @C++, though. (Still better than @C#).
|
3
features/data/journals/tags-237.journal
Normal file
|
@ -0,0 +1,3 @@
|
|||
[2014-07-22 11:11] This entry has an email.
|
||||
@Newline tag should show as a tag.
|
||||
Kyla's @email is kyla@clevelandunderdog.org and Guinness's is guinness@fortheloveofpits.org.
|
8
features/data/journals/tags.journal
Normal file
|
@ -0,0 +1,8 @@
|
|||
[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
|
5
features/data/journals/unreadabledates.journal
Normal file
|
@ -0,0 +1,5 @@
|
|||
[ashasd7zdskhz7asdkjasd] Entry subject
|
||||
I've lost track of time.
|
||||
|
||||
[sadfhakjsdf88sdf7sdff] Entry subject
|
||||
Time has no meaning.
|
0
features/data/journals/work.journal
Normal file
19
features/data/templates/sample.template
Normal file
|
@ -0,0 +1,19 @@
|
|||
---
|
||||
extension: txt
|
||||
---
|
||||
|
||||
{% block journal %}
|
||||
{% for entry in entries %}
|
||||
{% include entry %}
|
||||
{% endfor %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block entry %}
|
||||
{{ entry.title }}
|
||||
{{ "-" * len(entry.title) }}
|
||||
|
||||
{{ entry.body }}
|
||||
|
||||
{% endblock %}
|
||||
`
|
31
features/encryption.feature
Normal file
|
@ -0,0 +1,31 @@
|
|||
Feature: Encrypted journals
|
||||
Scenario: Loading an encrypted journal
|
||||
Given we use the config "encrypted.yaml"
|
||||
When we run "jrnl -n 1" and enter "bad doggie no biscuit"
|
||||
Then we should see the message "Password"
|
||||
and the output should contain "2013-06-10 15:40 Life is good"
|
||||
|
||||
Scenario: Decrypting a journal
|
||||
Given we use the config "encrypted.yaml"
|
||||
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 we should see the message "Journal decrypted"
|
||||
and the journal should have 2 entries
|
||||
|
||||
Scenario: Encrypting a journal
|
||||
Given we use the config "basic.yaml"
|
||||
When we run "jrnl --encrypt" and enter "swordfish"
|
||||
Then 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 we should see the message "Password"
|
||||
and the output should contain "2013-06-10 15:40 Life is good"
|
||||
|
||||
Scenario: Storing a password in Keychain
|
||||
Given we use the config "multiple.yaml"
|
||||
When we run "jrnl simple --encrypt" and enter "sabertooth"
|
||||
When we set the keychain password of "simple" to "sabertooth"
|
||||
Then the config for journal "simple" should have "encrypt" set to "bool:True"
|
||||
When we run "jrnl simple -n 1"
|
||||
Then we should not see the message "Password"
|
||||
and the output should contain "2013-06-10 15:40 Life is good"
|
42
features/environment.py
Normal file
|
@ -0,0 +1,42 @@
|
|||
from behave import *
|
||||
import shutil
|
||||
import os
|
||||
import jrnl
|
||||
try:
|
||||
from io import StringIO
|
||||
except ImportError:
|
||||
from cStringIO import StringIO
|
||||
|
||||
def before_scenario(context, scenario):
|
||||
"""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
|
||||
for folder in ("configs", "journals"):
|
||||
working_dir = os.path.join("features", folder)
|
||||
if os.path.exists(working_dir):
|
||||
shutil.rmtree(working_dir)
|
||||
|
||||
|
||||
for folder in ("configs", "journals"):
|
||||
original = os.path.join("features", "data", folder)
|
||||
working_dir = os.path.join("features", folder)
|
||||
if not os.path.exists(working_dir):
|
||||
os.mkdir(working_dir)
|
||||
for filename in os.listdir(original):
|
||||
source = os.path.join(original, filename)
|
||||
if os.path.isdir(source):
|
||||
shutil.copytree(source, os.path.join(working_dir, filename))
|
||||
else:
|
||||
shutil.copy2(source, working_dir)
|
||||
|
||||
def after_scenario(context, scenario):
|
||||
"""After each scenario, restore all test data and remove working_dirs."""
|
||||
context.messages.close()
|
||||
context.messages = None
|
||||
for folder in ("configs", "journals"):
|
||||
working_dir = os.path.join("features", folder)
|
||||
if os.path.exists(working_dir):
|
||||
shutil.rmtree(working_dir)
|
85
features/exporting.feature
Normal file
|
@ -0,0 +1,85 @@
|
|||
Feature: Exporting a Journal
|
||||
|
||||
Scenario: Exporting to json
|
||||
Given we use the config "tags.yaml"
|
||||
When we run "jrnl --export json"
|
||||
Then we should get no error
|
||||
and the output should be parsable as json
|
||||
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 "@journal"
|
||||
and "tags" in the json output should contain "@dan"
|
||||
|
||||
Scenario: Exporting using filters should only export parts of the journal
|
||||
Given we use the config "tags.yaml"
|
||||
When we run "jrnl -until 'may 2013' --export json"
|
||||
# Then we should get no error
|
||||
Then the output should be parsable as json
|
||||
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 "@journal"
|
||||
and "tags" in the json output should not contain "@dan"
|
||||
|
||||
Scenario: Exporting using custom templates
|
||||
Given we use the config "basic.yaml"
|
||||
Given we load template "sample.template"
|
||||
When we run "jrnl --export sample"
|
||||
Then the output should be
|
||||
"""
|
||||
My first entry.
|
||||
---------------
|
||||
|
||||
Everything is alright
|
||||
|
||||
Life is good.
|
||||
-------------
|
||||
|
||||
But I'm better.
|
||||
"""
|
||||
|
||||
Scenario: Increasing Headings on Markdown export
|
||||
Given we use the config "markdown-headings-335.yaml"
|
||||
When we run "jrnl --export markdown"
|
||||
Then the output should be
|
||||
"""
|
||||
# 2015
|
||||
|
||||
## April
|
||||
|
||||
### 2015-04-14 13:23 Heading Test
|
||||
|
||||
#### H1-1
|
||||
|
||||
#### H1-2
|
||||
|
||||
#### H1-3
|
||||
|
||||
##### H2-1
|
||||
|
||||
##### H2-2
|
||||
|
||||
##### H2-3
|
||||
|
||||
Horizontal Rules (ignore)
|
||||
|
||||
---
|
||||
|
||||
===
|
||||
|
||||
#### ATX H1
|
||||
|
||||
##### ATX H2
|
||||
|
||||
###### ATX H3
|
||||
|
||||
####### ATX H4
|
||||
|
||||
######## ATX H5
|
||||
|
||||
######### ATX H6
|
||||
|
||||
Stuff
|
||||
|
||||
More stuff
|
||||
more stuff again
|
||||
"""
|
46
features/multiple_journals.feature
Normal file
|
@ -0,0 +1,46 @@
|
|||
Feature: Multiple journals
|
||||
|
||||
Scenario: Loading a config with two journals
|
||||
Given we use the config "multiple.yaml"
|
||||
Then journal "default" should have 2 entries
|
||||
and journal "work" should have 0 entries
|
||||
|
||||
Scenario: Write to default config by default
|
||||
Given we use the config "multiple.yaml"
|
||||
When we run "jrnl this goes to default"
|
||||
Then journal "default" should have 3 entries
|
||||
and journal "work" should have 0 entries
|
||||
|
||||
Scenario: Write to specified journal
|
||||
Given we use the config "multiple.yaml"
|
||||
When we run "jrnl work a long day in the office"
|
||||
Then journal "default" should have 2 entries
|
||||
and journal "work" should have 1 entry
|
||||
|
||||
Scenario: Tell user which journal was used
|
||||
Given we use the config "multiple.yaml"
|
||||
When we run "jrnl work a long day in the office"
|
||||
Then we should see the message "Entry added to work journal"
|
||||
|
||||
Scenario: Write to specified journal with a timestamp
|
||||
Given we use the config "multiple.yaml"
|
||||
When we run "jrnl work 23 july 2012: a long day in the office"
|
||||
Then journal "default" should have 2 entries
|
||||
and journal "work" should have 1 entry
|
||||
and journal "work" should contain "2012-07-23"
|
||||
|
||||
Scenario: Create new journals as required
|
||||
Given we use the config "multiple.yaml"
|
||||
Then journal "ideas" should not exist
|
||||
When we run "jrnl ideas 23 july 2012: sell my junk on ebay and make lots of money"
|
||||
Then journal "ideas" should have 1 entry
|
||||
|
||||
Scenario: Don't crash if no default journal is specified
|
||||
Given we use the config "bug343.yaml"
|
||||
When we run "jrnl a long day in the office"
|
||||
Then we should see the message "No default journal configured"
|
||||
|
||||
Scenario: Don't crash if no file exists for a configured encrypted journal
|
||||
Given we use the config "multiple.yaml"
|
||||
When we run "jrnl new_encrypted Adding first entry" and enter "these three eyes"
|
||||
Then we should see the message "Journal 'new_encrypted' created"
|
64
features/regression.feature
Normal file
|
@ -0,0 +1,64 @@
|
|||
Feature: Zapped bugs should stay dead.
|
||||
|
||||
Scenario: Writing an entry does not print the entire journal
|
||||
# https://github.com/maebert/jrnl/issues/87
|
||||
Given we use the config "basic.yaml"
|
||||
When we run "jrnl 23 july 2013: 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 not contain "Life is good"
|
||||
|
||||
Scenario: Date with time should be parsed correctly
|
||||
# https://github.com/maebert/jrnl/issues/117
|
||||
Given we use the config "basic.yaml"
|
||||
When we run "jrnl 2013-11-30 15:42: Project Started."
|
||||
Then we should see the message "Entry added"
|
||||
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
|
||||
#https://github.com/maebert/jrnl/issues/153
|
||||
Given we use the config "bug153.yaml"
|
||||
When we run "jrnl -1"
|
||||
Then we should get no error
|
||||
and the output should be
|
||||
"""
|
||||
2013-10-27 03:27 Some text.
|
||||
"""
|
||||
|
||||
Scenario: Title with an embedded period.
|
||||
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."
|
||||
Then we should see the message "Entry added"
|
||||
When we run "jrnl -1"
|
||||
Then the output should be
|
||||
"""
|
||||
2014-04-24 09:00 Created a new website - empty.com.
|
||||
| Hope to get a lot of traffic.
|
||||
"""
|
||||
|
||||
Scenario: Integers in square brackets should not be read as dates
|
||||
Given we use the config "brackets.yaml"
|
||||
When we run "jrnl -1"
|
||||
Then the output should contain "[1] line starting with 1"
|
||||
|
||||
Scenario: Journals with unreadable dates should still be viewable
|
||||
Given we use the config "unreadabledates.yaml"
|
||||
When we run "jrnl -2"
|
||||
Then the output should contain "I've lost track of time."
|
||||
Then the output should contain "Time has no meaning."
|
||||
|
||||
Scenario: Journals with readable dates AND unreadable dates should still contain all data.
|
||||
Given we use the config "mostlyreadabledates.yaml"
|
||||
When we run "jrnl -3"
|
||||
Then the output should contain "Time machines are possible."
|
||||
When we run "jrnl -1"
|
||||
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?"
|
||||
|
20
features/starring.feature
Normal file
|
@ -0,0 +1,20 @@
|
|||
Feature: Starring entries
|
||||
|
||||
Scenario: Starring an entry will mark it in the journal file
|
||||
Given we use the config "basic.yaml"
|
||||
When we run "jrnl 20 july 2013 *: Best day of my life!"
|
||||
Then we should see the message "Entry added"
|
||||
and the journal should contain "[2013-07-20 09:00] Best day of my life! *"
|
||||
|
||||
Scenario: Filtering by starred entries
|
||||
Given we use the config "basic.yaml"
|
||||
When we run "jrnl -starred"
|
||||
Then the output should be
|
||||
"""
|
||||
"""
|
||||
When we run "jrnl 20 july 2013 *: Best day of my life!"
|
||||
When we run "jrnl -starred"
|
||||
Then the output should be
|
||||
"""
|
||||
2013-07-20 09:00 Best day of my life!
|
||||
"""
|
260
features/steps/core.py
Normal file
|
@ -0,0 +1,260 @@
|
|||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
|
||||
from behave import given, when, then
|
||||
from jrnl import cli, install, Journal, util, plugins
|
||||
from jrnl import __version__
|
||||
from dateutil import parser as date_parser
|
||||
from collections import defaultdict
|
||||
import os
|
||||
import json
|
||||
import yaml
|
||||
import keyring
|
||||
|
||||
|
||||
class TestKeyring(keyring.backend.KeyringBackend):
|
||||
"""A test keyring that just stores its valies in a hash"""
|
||||
|
||||
priority = 1
|
||||
keys = defaultdict(dict)
|
||||
|
||||
def set_password(self, servicename, username, password):
|
||||
self.keys[servicename][username] = password
|
||||
|
||||
def get_password(self, servicename, username):
|
||||
return self.keys[servicename].get(username)
|
||||
|
||||
def delete_password(self, servicename, username, password):
|
||||
self.keys[servicename][username] = None
|
||||
|
||||
# set the keyring for keyring lib
|
||||
keyring.set_keyring(TestKeyring())
|
||||
|
||||
|
||||
try:
|
||||
from io import StringIO
|
||||
except ImportError:
|
||||
from cStringIO import StringIO
|
||||
import tzlocal
|
||||
import shlex
|
||||
import sys
|
||||
|
||||
|
||||
def ushlex(command):
|
||||
if sys.version_info[0] == 3:
|
||||
return shlex.split(command)
|
||||
return map(lambda s: s.decode('UTF8'), shlex.split(command.encode('utf8')))
|
||||
|
||||
|
||||
def read_journal(journal_name="default"):
|
||||
config = util.load_config(install.CONFIG_FILE_PATH)
|
||||
with open(config['journals'][journal_name]) as journal_file:
|
||||
journal = journal_file.read()
|
||||
return journal
|
||||
|
||||
|
||||
def open_journal(journal_name="default"):
|
||||
config = util.load_config(install.CONFIG_FILE_PATH)
|
||||
journal_conf = config['journals'][journal_name]
|
||||
if type(journal_conf) is dict: # We can override the default config on a by-journal basis
|
||||
config.update(journal_conf)
|
||||
else: # But also just give them a string to point to the journal file
|
||||
config['journal'] = journal_conf
|
||||
return Journal.open_journal(journal_name, config)
|
||||
|
||||
|
||||
@given('we use the config "{config_file}"')
|
||||
def set_config(context, config_file):
|
||||
full_path = os.path.join("features/configs", config_file)
|
||||
install.CONFIG_FILE_PATH = os.path.abspath(full_path)
|
||||
if config_file.endswith("yaml"):
|
||||
# Add jrnl version to file for 2.x journals
|
||||
with open(install.CONFIG_FILE_PATH, 'a') as cf:
|
||||
cf.write("version: {}".format(__version__))
|
||||
|
||||
|
||||
@when('we run "{command}" and enter')
|
||||
@when('we run "{command}" and enter "{inputs}"')
|
||||
def run_with_input(context, command, inputs=None):
|
||||
text = inputs or context.text
|
||||
args = ushlex(command)[1:]
|
||||
buffer = StringIO(text.strip())
|
||||
util.STDIN = buffer
|
||||
try:
|
||||
cli.run(args or [])
|
||||
context.exit_status = 0
|
||||
except SystemExit as e:
|
||||
context.exit_status = e.code
|
||||
|
||||
|
||||
@when('we run "{command}"')
|
||||
def run(context, command):
|
||||
args = ushlex(command)[1:]
|
||||
try:
|
||||
cli.run(args or None)
|
||||
context.exit_status = 0
|
||||
except SystemExit as e:
|
||||
context.exit_status = e.code
|
||||
|
||||
|
||||
@given('we load template "{filename}"')
|
||||
def load_template(context, filename):
|
||||
full_path = os.path.join("features/data/templates", filename)
|
||||
exporter = plugins.template_exporter.__exporter_from_file(full_path)
|
||||
plugins.__exporter_types[exporter.names[0]] = exporter
|
||||
|
||||
|
||||
@when('we set the keychain password of "{journal}" to "{password}"')
|
||||
def set_keychain(context, journal, password):
|
||||
keyring.set_password('jrnl', journal, password)
|
||||
|
||||
|
||||
@then('we should get an error')
|
||||
def has_error(context):
|
||||
assert context.exit_status != 0, context.exit_status
|
||||
|
||||
|
||||
@then('we should get no error')
|
||||
def no_error(context):
|
||||
assert context.exit_status is 0, context.exit_status
|
||||
|
||||
|
||||
@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')
|
||||
@then('the output should be "{text}"')
|
||||
def check_output(context, text=None):
|
||||
text = (text or context.text).strip().splitlines()
|
||||
out = context.stdout_capture.getvalue().strip().splitlines()
|
||||
assert len(text) == len(out), "Output has {} lines (expected: {})".format(len(out), len(text))
|
||||
for line_text, line_out in zip(text, out):
|
||||
assert line_text.strip() == line_out.strip(), [line_text.strip(), line_out.strip()]
|
||||
|
||||
|
||||
@then('the output should contain "{text}" in the local time')
|
||||
def check_output_time_inline(context, text):
|
||||
out = context.stdout_capture.getvalue()
|
||||
local_tz = tzlocal.get_localzone()
|
||||
utc_time = date_parser.parse(text)
|
||||
local_date = utc_time.astimezone(local_tz).strftime("%Y-%m-%d %H:%M")
|
||||
assert local_date in out, local_date
|
||||
|
||||
|
||||
@then('the output should contain')
|
||||
@then('the output should contain "{text}"')
|
||||
def check_output_inline(context, text=None):
|
||||
text = text or context.text
|
||||
out = context.stdout_capture.getvalue()
|
||||
if isinstance(out, bytes):
|
||||
out = out.decode('utf-8')
|
||||
assert text in out, text
|
||||
|
||||
|
||||
@then('the output should not contain "{text}"')
|
||||
def check_output_not_inline(context, text):
|
||||
out = context.stdout_capture.getvalue()
|
||||
if isinstance(out, bytes):
|
||||
out = out.decode('utf-8')
|
||||
assert text not in out
|
||||
|
||||
|
||||
@then('we should see the message "{text}"')
|
||||
def check_message(context, text):
|
||||
out = context.messages.getvalue()
|
||||
assert text in out, [text, out]
|
||||
|
||||
|
||||
@then('we should not see the message "{text}"')
|
||||
def check_not_message(context, text):
|
||||
out = context.messages.getvalue()
|
||||
assert text not in out, [text, out]
|
||||
|
||||
|
||||
@then('the journal should contain "{text}"')
|
||||
@then('journal "{journal_name}" should contain "{text}"')
|
||||
def check_journal_content(context, text, journal_name="default"):
|
||||
journal = read_journal(journal_name)
|
||||
assert text in journal, journal
|
||||
|
||||
|
||||
@then('journal "{journal_name}" should not exist')
|
||||
def journal_doesnt_exist(context, journal_name="default"):
|
||||
with open(install.CONFIG_FILE_PATH) as config_file:
|
||||
config = yaml.load(config_file, Loader=yaml.FullLoader)
|
||||
journal_path = config['journals'][journal_name]
|
||||
assert not os.path.exists(journal_path)
|
||||
|
||||
|
||||
@then('the config 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):
|
||||
t, value = value.split(":")
|
||||
value = {
|
||||
"bool": lambda v: v.lower() == "true",
|
||||
"int": int,
|
||||
"str": str
|
||||
}[t](value)
|
||||
config = util.load_config(install.CONFIG_FILE_PATH)
|
||||
if journal:
|
||||
config = config["journals"][journal]
|
||||
assert key in config
|
||||
assert config[key] == value
|
||||
|
||||
|
||||
@then('the journal should have {number:d} entries')
|
||||
@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} entry')
|
||||
def check_journal_entries(context, number, journal_name="default"):
|
||||
journal = open_journal(journal_name)
|
||||
assert len(journal.entries) == number
|
||||
|
||||
|
||||
@then('fail')
|
||||
def debug_fail(context):
|
||||
assert False
|
84
features/tagging.feature
Normal file
|
@ -0,0 +1,84 @@
|
|||
Feature: Tagging
|
||||
|
||||
Scenario: Displaying tags
|
||||
Given we use the config "tags.yaml"
|
||||
When we run "jrnl --tags"
|
||||
Then we should get no error
|
||||
and the output should be
|
||||
"""
|
||||
@idea : 2
|
||||
@journal : 1
|
||||
@dan : 1
|
||||
"""
|
||||
|
||||
Scenario: Filtering journals should also filter tags
|
||||
Given we use the config "tags.yaml"
|
||||
When we run "jrnl -from 'may 2013' --tags"
|
||||
Then we should get no error
|
||||
and the output should be
|
||||
"""
|
||||
@idea : 1
|
||||
@dan : 1
|
||||
"""
|
||||
|
||||
Scenario: Tags should allow certain special characters
|
||||
Given we use the config "tags-216.yaml"
|
||||
When we run "jrnl --tags"
|
||||
Then we should get no error
|
||||
and the output should be
|
||||
"""
|
||||
@os/2 : 1
|
||||
@c++ : 1
|
||||
@c# : 1
|
||||
"""
|
||||
Scenario: An email should not be a tag
|
||||
Given we use the config "tags-237.yaml"
|
||||
When we run "jrnl --tags"
|
||||
Then we should get no error
|
||||
and the output should be
|
||||
"""
|
||||
@newline : 1
|
||||
@email : 1
|
||||
"""
|
||||
|
||||
Scenario: Entry cans start and end with tags
|
||||
Given we use the config "basic.yaml"
|
||||
When we run "jrnl today: @foo came over, we went to a @bar"
|
||||
When we run "jrnl --tags"
|
||||
Then the output should be
|
||||
"""
|
||||
@foo : 1
|
||||
@bar : 1
|
||||
"""
|
||||
|
||||
Scenario: Excluding a tag should filter it
|
||||
Given we use the config "basic.yaml"
|
||||
When we run "jrnl today: @foo came over, we went to a bar"
|
||||
When we run "jrnl I have decided I did not enjoy that @bar"
|
||||
When we run "jrnl --tags -not @bar"
|
||||
Then the output should be
|
||||
"""
|
||||
@foo : 1
|
||||
"""
|
||||
|
||||
Scenario: Excluding a tag should filter an entry, even if an unfiltered tag is in that entry
|
||||
Given we use the config "basic.yaml"
|
||||
When we run "jrnl today: I do @not think this will show up @thought"
|
||||
When we run "jrnl today: I think this will show up @thought"
|
||||
When we run "jrnl --tags -not @not"
|
||||
Then the output should be
|
||||
"""
|
||||
@thought : 1
|
||||
"""
|
||||
|
||||
Scenario: Excluding multiple tags should filter them
|
||||
Given we use the config "basic.yaml"
|
||||
When we run "jrnl today: I do @not think this will show up @thought"
|
||||
When we run "jrnl today: I think this will show up @thought"
|
||||
When we run "jrnl today: This should @never show up @thought"
|
||||
When we run "jrnl today: What a nice day for filtering @thought"
|
||||
When we run "jrnl --tags -not @not @never"
|
||||
Then the output should be
|
||||
"""
|
||||
@thought : 2
|
||||
"""
|
23
features/upgrade.feature
Normal file
|
@ -0,0 +1,23 @@
|
|||
Feature: Upgrading Journals from 1.x.x to 2.x.x
|
||||
|
||||
Scenario: Upgrade and parse journals with square brackets
|
||||
Given we use the config "upgrade_from_195.json"
|
||||
When we run "jrnl -9" and enter "Y"
|
||||
Then the output should contain
|
||||
"""
|
||||
2010-06-10 15:00 A life without chocolate is like a bad analogy.
|
||||
|
||||
2013-06-10 15:40 He said "[this] is the best time to be alive".
|
||||
"""
|
||||
Then the journal should have 2 entries
|
||||
|
||||
Scenario: Upgrading a journal encrypted with jrnl 1.x
|
||||
Given we use the config "encrypted_old.json"
|
||||
When we run "jrnl -n 1" and enter
|
||||
"""
|
||||
Y
|
||||
bad doggie no biscuit
|
||||
bad doggie no biscuit
|
||||
"""
|
||||
Then we should see the message "Password"
|
||||
and the output should contain "2013-06-10 15:40 Life is good"
|
146
jrnl/DayOneJournal.py
Normal file
|
@ -0,0 +1,146 @@
|
|||
#!/usr/bin/env python
|
||||
# encoding: utf-8
|
||||
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
from . import Entry
|
||||
from . import Journal
|
||||
from . import time as jrnl_time
|
||||
import os
|
||||
import re
|
||||
from datetime import datetime
|
||||
import time
|
||||
import fnmatch
|
||||
import plistlib
|
||||
import pytz
|
||||
import uuid
|
||||
import tzlocal
|
||||
from xml.parsers.expat import ExpatError
|
||||
|
||||
|
||||
class DayOne(Journal.Journal):
|
||||
"""A special Journal handling DayOne files"""
|
||||
|
||||
# InvalidFileException was added to plistlib in Python3.4
|
||||
PLIST_EXCEPTIONS = (ExpatError, plistlib.InvalidFileException) if hasattr(plistlib, "InvalidFileException") else ExpatError
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.entries = []
|
||||
self._deleted_entries = []
|
||||
super(DayOne, self).__init__(**kwargs)
|
||||
|
||||
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 = []
|
||||
for root, dirnames, f in os.walk(self.config['journal']):
|
||||
for filename in fnmatch.filter(f, '*.doentry'):
|
||||
filenames.append(os.path.join(root, filename))
|
||||
self.entries = []
|
||||
for filename in filenames:
|
||||
with open(filename, 'rb') as plist_entry:
|
||||
try:
|
||||
dict_entry = plistlib.readPlist(plist_entry)
|
||||
except self.PLIST_EXCEPTIONS:
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
timezone = pytz.timezone(dict_entry['Time Zone'])
|
||||
except (KeyError, pytz.exceptions.UnknownTimeZoneError):
|
||||
timezone = tzlocal.get_localzone()
|
||||
date = dict_entry['Creation Date']
|
||||
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._tags = [self.config['tagsymbols'][0] + tag.lower() for tag in dict_entry.get("Tags", [])]
|
||||
|
||||
self.entries.append(entry)
|
||||
self.sort()
|
||||
return self
|
||||
|
||||
def write(self):
|
||||
"""Writes only the entries that have been modified into plist files."""
|
||||
for entry in self.entries:
|
||||
if entry.modified:
|
||||
utc_time = datetime.utcfromtimestamp(time.mktime(entry.date.timetuple()))
|
||||
|
||||
if not hasattr(entry, "uuid"):
|
||||
entry.uuid = uuid.uuid1().hex
|
||||
|
||||
filename = os.path.join(self.config['journal'], "entries", entry.uuid.upper() + ".doentry")
|
||||
|
||||
entry_plist = {
|
||||
'Creation Date': utc_time,
|
||||
'Starred': entry.starred if hasattr(entry, 'starred') else False,
|
||||
'Entry Text': entry.title + "\n" + entry.body,
|
||||
'Time Zone': str(tzlocal.get_localzone()),
|
||||
'UUID': entry.uuid.upper(),
|
||||
'Tags': [tag.strip(self.config['tagsymbols']).replace("_", " ") for tag in entry.tags]
|
||||
}
|
||||
plistlib.writePlist(entry_plist, filename)
|
||||
for entry in self._deleted_entries:
|
||||
filename = os.path.join(self.config['journal'], "entries", entry.uuid + ".doentry")
|
||||
os.remove(filename)
|
||||
|
||||
def editable_str(self):
|
||||
"""Turns the journal into a string of entries that can be edited
|
||||
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])
|
||||
|
||||
def parse_editable_str(self, edited):
|
||||
"""Parses the output of self.editable_str and updates its entries."""
|
||||
# Method: create a new list of entries from the edited text, then match
|
||||
# UUIDs of the new entries against self.entries, updating the entries
|
||||
# if the edited entries differ, and deleting entries from self.entries
|
||||
# if they don't show up in the edited entries anymore.
|
||||
|
||||
# Initialise our current entry
|
||||
entries = []
|
||||
current_entry = None
|
||||
|
||||
for line in edited.splitlines():
|
||||
# try to parse line as UUID => new entry begins
|
||||
line = line.rstrip()
|
||||
m = re.match("# *([a-f0-9]+) *$", line.lower())
|
||||
if m:
|
||||
if current_entry:
|
||||
entries.append(current_entry)
|
||||
current_entry = Entry.Entry(self)
|
||||
current_entry.modified = False
|
||||
current_entry.uuid = m.group(1).lower()
|
||||
else:
|
||||
date_blob_re = re.compile("^\[[^\\]]+\] ")
|
||||
date_blob = date_blob_re.findall(line)
|
||||
if date_blob:
|
||||
date_blob = date_blob[0]
|
||||
new_date = jrnl_time.parse(date_blob.strip(" []"))
|
||||
if line.endswith("*"):
|
||||
current_entry.starred = True
|
||||
line = line[:-1]
|
||||
current_entry.title = line[len(date_blob) - 1:]
|
||||
current_entry.date = new_date
|
||||
elif current_entry:
|
||||
current_entry.body += line + "\n"
|
||||
|
||||
# Append last entry
|
||||
if current_entry:
|
||||
entries.append(current_entry)
|
||||
|
||||
# Now, update our current entries if they changed
|
||||
for entry in entries:
|
||||
entry._parse_text()
|
||||
matched_entries = [e for e in self.entries if e.uuid.lower() == entry.uuid]
|
||||
if matched_entries:
|
||||
# This entry is an existing entry
|
||||
match = matched_entries[0]
|
||||
if match != entry:
|
||||
self.entries.remove(match)
|
||||
entry.modified = True
|
||||
self.entries.append(entry)
|
||||
else:
|
||||
# This entry seems to be new... save it.
|
||||
entry.modified = True
|
||||
self.entries.append(entry)
|
||||
# Remove deleted entries
|
||||
edited_uuids = [e.uuid for e in entries]
|
||||
self._deleted_entries = [e for e in self.entries if e.uuid not in edited_uuids]
|
||||
self.entries[:] = [e for e in self.entries if e.uuid in edited_uuids]
|
||||
return entries
|
127
jrnl/EncryptedJournal.py
Normal file
|
@ -0,0 +1,127 @@
|
|||
from . import Journal, util
|
||||
from cryptography.fernet import Fernet, InvalidToken
|
||||
from cryptography.hazmat.primitives import hashes, padding
|
||||
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||
import hashlib
|
||||
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
import sys
|
||||
import os
|
||||
import base64
|
||||
import getpass
|
||||
import logging
|
||||
|
||||
log = logging.getLogger()
|
||||
|
||||
|
||||
def make_key(password):
|
||||
password = util.bytes(password)
|
||||
kdf = PBKDF2HMAC(
|
||||
algorithm=hashes.SHA256(),
|
||||
length=32,
|
||||
# Salt is hard-coded
|
||||
salt=b'\xf2\xd5q\x0e\xc1\x8d.\xde\xdc\x8e6t\x89\x04\xce\xf8',
|
||||
iterations=100000,
|
||||
backend=default_backend()
|
||||
)
|
||||
key = kdf.derive(password)
|
||||
return base64.urlsafe_b64encode(key)
|
||||
|
||||
|
||||
class EncryptedJournal(Journal.Journal):
|
||||
def __init__(self, name='default', **kwargs):
|
||||
super(EncryptedJournal, self).__init__(name, **kwargs)
|
||||
self.config['encrypt'] = True
|
||||
|
||||
def open(self, filename=None):
|
||||
"""Opens the journal file defined in the config and parses it into a list of Entries.
|
||||
Entries have the form (date, title, body)."""
|
||||
filename = filename or self.config['journal']
|
||||
|
||||
if not os.path.exists(filename):
|
||||
password = util.getpass("Enter password for new journal: ")
|
||||
if password:
|
||||
if util.yesno("Do you want to store the password in your keychain?", default=True):
|
||||
util.set_keychain(self.name, password)
|
||||
else:
|
||||
util.set_keychain(self.name, None)
|
||||
self.config['password'] = password
|
||||
text = ""
|
||||
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.sort()
|
||||
log.debug("opened %s with %d entries", self.__class__.__name__, len(self))
|
||||
return self
|
||||
|
||||
|
||||
def _load(self, filename, password=None):
|
||||
"""Loads an encrypted journal from a file and tries to decrypt it.
|
||||
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.
|
||||
If the password is provided but wrong (or corrupt), this will simply
|
||||
return None."""
|
||||
with open(filename, 'rb') as f:
|
||||
journal_encrypted = f.read()
|
||||
|
||||
def validate_password(password):
|
||||
key = make_key(password)
|
||||
try:
|
||||
plain = Fernet(key).decrypt(journal_encrypted).decode('utf-8')
|
||||
self.config['password'] = password
|
||||
return plain
|
||||
except (InvalidToken, IndexError):
|
||||
return None
|
||||
if password:
|
||||
return validate_password(password)
|
||||
return util.get_password(keychain=self.name, validator=validate_password)
|
||||
|
||||
def _store(self, filename, text):
|
||||
key = make_key(self.config['password'])
|
||||
journal = Fernet(key).encrypt(text.encode('utf-8'))
|
||||
with open(filename, 'wb') as f:
|
||||
f.write(journal)
|
||||
|
||||
@classmethod
|
||||
def _create(cls, filename, password):
|
||||
key = make_key(password)
|
||||
dummy = Fernet(key).encrypt(b"")
|
||||
with open(filename, 'wb') as f:
|
||||
f.write(dummy)
|
||||
|
||||
|
||||
class LegacyEncryptedJournal(Journal.LegacyJournal):
|
||||
"""Legacy class to support opening journals encrypted with the jrnl 1.x
|
||||
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):
|
||||
with open(filename, 'rb') as f:
|
||||
journal_encrypted = f.read()
|
||||
iv, cipher = journal_encrypted[:16], journal_encrypted[16:]
|
||||
|
||||
def validate_password(password):
|
||||
decryption_key = hashlib.sha256(password.encode('utf-8')).digest()
|
||||
decryptor = Cipher(algorithms.AES(decryption_key), modes.CBC(iv), default_backend()).decryptor()
|
||||
try:
|
||||
plain_padded = decryptor.update(cipher) + decryptor.finalize()
|
||||
self.config['password'] = password
|
||||
if plain_padded[-1] in (" ", 32):
|
||||
# Ancient versions of jrnl. Do not judge me.
|
||||
return plain_padded.decode('utf-8').rstrip(" ")
|
||||
else:
|
||||
unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder()
|
||||
plain = unpadder.update(plain_padded) + unpadder.finalize()
|
||||
return plain.decode('utf-8')
|
||||
except ValueError:
|
||||
return None
|
||||
if password:
|
||||
return validate_password(password)
|
||||
return util.get_password(keychain=self.name, validator=validate_password)
|
124
jrnl/Entry.py
Executable file
|
@ -0,0 +1,124 @@
|
|||
#!/usr/bin/env python
|
||||
# encoding: utf-8
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import re
|
||||
import textwrap
|
||||
from datetime import datetime
|
||||
from .util import split_title
|
||||
|
||||
|
||||
class Entry:
|
||||
def __init__(self, journal, date=None, text="", starred=False):
|
||||
self.journal = journal # Reference to journal mainly to access its config
|
||||
self.date = date or datetime.now()
|
||||
self.text = text
|
||||
self._title = self._body = self._tags = None
|
||||
self.starred = starred
|
||||
self.modified = False
|
||||
|
||||
@property
|
||||
def fulltext(self):
|
||||
return self.title + " " + self.body
|
||||
|
||||
def _parse_text(self):
|
||||
raw_text = self.text
|
||||
lines = raw_text.splitlines()
|
||||
if lines[0].strip().endswith("*"):
|
||||
self.starred = True
|
||||
raw_text = lines[0].strip("\n *") + "\n" + "\n".join(lines[1:])
|
||||
self._title, self._body = split_title(raw_text)
|
||||
if self._tags is None:
|
||||
self._tags = list(self._parse_tags())
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
if self._title is None:
|
||||
self._parse_text()
|
||||
return self._title
|
||||
|
||||
@property
|
||||
def body(self):
|
||||
if self._body is None:
|
||||
self._parse_text()
|
||||
return self._body
|
||||
|
||||
@property
|
||||
def tags(self):
|
||||
if self._tags is None:
|
||||
self._parse_text()
|
||||
return self._tags
|
||||
|
||||
@staticmethod
|
||||
def tag_regex(tagsymbols):
|
||||
pattern = r'(?u)(?:^|\s)([{tags}][-+*#/\w]+)'.format(tags=tagsymbols)
|
||||
return re.compile(pattern, re.UNICODE)
|
||||
|
||||
def _parse_tags(self):
|
||||
tagsymbols = self.journal.config['tagsymbols']
|
||||
return set(tag.lower() for tag in re.findall(Entry.tag_regex(tagsymbols), self.text))
|
||||
|
||||
def __unicode__(self):
|
||||
"""Returns a string representation of the entry to be written into a journal file."""
|
||||
date_str = self.date.strftime(self.journal.config['timeformat'])
|
||||
title = "[{}] {}".format(date_str, self.title.rstrip("\n "))
|
||||
if self.starred:
|
||||
title += " *"
|
||||
return "{title}{sep}{body}\n".format(
|
||||
title=title,
|
||||
sep="\n" if self.body.rstrip("\n ") else "",
|
||||
body=self.body.rstrip("\n ")
|
||||
)
|
||||
|
||||
def pprint(self, short=False):
|
||||
"""Returns a pretty-printed version of the entry.
|
||||
If short is true, only print the title."""
|
||||
date_str = self.date.strftime(self.journal.config['timeformat'])
|
||||
if self.journal.config['indent_character']:
|
||||
indent = self.journal.config['indent_character'].rstrip() + " "
|
||||
else:
|
||||
indent = ""
|
||||
if not short and self.journal.config['linewrap']:
|
||||
title = textwrap.fill(date_str + " " + self.title, self.journal.config['linewrap'])
|
||||
body = "\n".join([
|
||||
textwrap.fill(
|
||||
line,
|
||||
self.journal.config['linewrap'],
|
||||
initial_indent=indent,
|
||||
subsequent_indent=indent,
|
||||
drop_whitespace=True) or indent
|
||||
for line in self.body.rstrip(" \n").splitlines()
|
||||
])
|
||||
else:
|
||||
title = date_str + " " + self.title.rstrip("\n ")
|
||||
body = self.body.rstrip("\n ")
|
||||
|
||||
# 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)
|
||||
|
||||
if short:
|
||||
return title
|
||||
else:
|
||||
return "{title}{sep}{body}\n".format(
|
||||
title=title,
|
||||
sep="\n" if has_body else "",
|
||||
body=body if has_body else "",
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return "<Entry '{0}' on {1}>".format(self.title.strip(), self.date.strftime("%Y-%m-%d %H:%M"))
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.__repr__())
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, Entry) \
|
||||
or self.title.strip() != other.title.strip() \
|
||||
or self.body.rstrip() != other.body.rstrip() \
|
||||
or self.date != other.date \
|
||||
or self.starred != other.starred:
|
||||
return False
|
||||
return True
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
364
jrnl/Journal.py
Normal file
|
@ -0,0 +1,364 @@
|
|||
#!/usr/bin/env python
|
||||
# encoding: utf-8
|
||||
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
from . import Entry
|
||||
from . import util
|
||||
from . import time
|
||||
import os
|
||||
import sys
|
||||
import codecs
|
||||
import re
|
||||
from datetime import datetime
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Tag(object):
|
||||
def __init__(self, name, count=0):
|
||||
self.name = name
|
||||
self.count = count
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def __repr__(self):
|
||||
return "<Tag '{}'>".format(self.name)
|
||||
|
||||
|
||||
class Journal(object):
|
||||
def __init__(self, name='default', **kwargs):
|
||||
self.config = {
|
||||
'journal': "journal.txt",
|
||||
'encrypt': False,
|
||||
'default_hour': 9,
|
||||
'default_minute': 0,
|
||||
'timeformat': "%Y-%m-%d %H:%M",
|
||||
'tagsymbols': '@',
|
||||
'highlight': True,
|
||||
'linewrap': 80,
|
||||
'indent_character': '|',
|
||||
}
|
||||
self.config.update(kwargs)
|
||||
# Set up date parser
|
||||
self.search_tags = None # Store tags we're highlighting
|
||||
self.name = name
|
||||
|
||||
def __len__(self):
|
||||
"""Returns the number of entries"""
|
||||
return len(self.entries)
|
||||
|
||||
def __iter__(self):
|
||||
"""Iterates over the journal's entries."""
|
||||
return (entry for entry in self.entries)
|
||||
|
||||
@classmethod
|
||||
def from_journal(cls, other):
|
||||
"""Creates a new journal by copying configuration and entries from
|
||||
another journal object"""
|
||||
new_journal = cls(other.name, **other.config)
|
||||
new_journal.entries = other.entries
|
||||
log.debug("Imported %d entries from %s to %s", len(new_journal), other.__class__.__name__, cls.__name__)
|
||||
return new_journal
|
||||
|
||||
def import_(self, other_journal_txt):
|
||||
self.entries = list(frozenset(self.entries) | frozenset(self._parse(other_journal_txt)))
|
||||
self.sort()
|
||||
|
||||
def open(self, filename=None):
|
||||
"""Opens the journal file defined in the config and parses it into a list of Entries.
|
||||
Entries have the form (date, title, body)."""
|
||||
filename = filename or self.config['journal']
|
||||
|
||||
if not os.path.exists(filename):
|
||||
util.prompt("[Journal '{0}' created at {1}]".format(self.name, filename))
|
||||
self._create(filename)
|
||||
|
||||
text = self._load(filename)
|
||||
self.entries = self._parse(text)
|
||||
self.sort()
|
||||
log.debug("opened %s with %d entries", self.__class__.__name__, len(self))
|
||||
return self
|
||||
|
||||
def write(self, filename=None):
|
||||
"""Dumps the journal into the config file, overwriting it"""
|
||||
filename = filename or self.config['journal']
|
||||
text = self._to_text()
|
||||
self._store(filename, text)
|
||||
|
||||
def validate_parsing(self):
|
||||
"""Confirms that the jrnl is still parsed correctly after being dumped to text."""
|
||||
new_entries = self._parse(self._to_text())
|
||||
for i, entry in enumerate(self.entries):
|
||||
if entry != new_entries[i]:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _to_text(self):
|
||||
return "\n".join([e.__unicode__() for e in self.entries])
|
||||
|
||||
def _load(self, filename):
|
||||
raise NotImplementedError
|
||||
|
||||
def _store(self, filename, text):
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
def _create(cls, filename):
|
||||
raise NotImplementedError
|
||||
|
||||
def _parse(self, journal_txt):
|
||||
"""Parses a journal that's stored in a string and returns a list of entries"""
|
||||
|
||||
# Return empty array if the journal is blank
|
||||
if not journal_txt:
|
||||
return []
|
||||
|
||||
# Initialise our current entry
|
||||
entries = []
|
||||
|
||||
date_blob_re = re.compile("(?:^|\n)\[([^\\]]+)\] ")
|
||||
last_entry_pos = 0
|
||||
for match in date_blob_re.finditer(journal_txt):
|
||||
date_blob = match.groups()[0]
|
||||
new_date = time.parse(date_blob)
|
||||
if new_date:
|
||||
if entries:
|
||||
entries[-1].text = journal_txt[last_entry_pos:match.start()]
|
||||
last_entry_pos = match.end()
|
||||
entries.append(Entry.Entry(self, date=new_date))
|
||||
|
||||
# If no entries were found, treat all the existing text as an entry made now
|
||||
if not entries:
|
||||
entries.append(Entry.Entry(self, date=time.parse("now")))
|
||||
|
||||
# Fill in the text of the last entry
|
||||
entries[-1].text = journal_txt[last_entry_pos:]
|
||||
|
||||
for entry in entries:
|
||||
entry._parse_text()
|
||||
return entries
|
||||
|
||||
def __unicode__(self):
|
||||
return self.pprint()
|
||||
|
||||
def pprint(self, short=False):
|
||||
"""Prettyprints the journal's entries"""
|
||||
sep = "\n"
|
||||
pp = sep.join([e.pprint(short=short) for e in self.entries])
|
||||
if self.config['highlight']: # highlight tags
|
||||
if self.search_tags:
|
||||
for tag in self.search_tags:
|
||||
tagre = re.compile(re.escape(tag), re.IGNORECASE)
|
||||
pp = re.sub(tagre,
|
||||
lambda match: util.colorize(match.group(0)),
|
||||
pp, re.UNICODE)
|
||||
else:
|
||||
pp = re.sub(
|
||||
Entry.Entry.tag_regex(self.config['tagsymbols']),
|
||||
lambda match: util.colorize(match.group(0)),
|
||||
pp
|
||||
)
|
||||
return pp
|
||||
|
||||
def __repr__(self):
|
||||
return "<Journal with {0} entries>".format(len(self.entries))
|
||||
|
||||
def sort(self):
|
||||
"""Sorts the Journal's entries by date"""
|
||||
self.entries = sorted(self.entries, key=lambda entry: entry.date)
|
||||
|
||||
def limit(self, n=None):
|
||||
"""Removes all but the last n entries"""
|
||||
if n:
|
||||
self.entries = self.entries[-n:]
|
||||
|
||||
@property
|
||||
def tags(self):
|
||||
"""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
|
||||
# I came across this construction, worry not and embrace the ensuing moment of enlightment.
|
||||
tags = [tag
|
||||
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]
|
||||
tag_counts = set([(tags.count(tag), tag) for tag in tags])
|
||||
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=[]):
|
||||
"""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
|
||||
tag symbols defined in the config, e.g. ["@John", "#WorldDomination"].
|
||||
|
||||
start_date and end_date define a timespan by which to filter.
|
||||
|
||||
starred limits journal to starred entries
|
||||
|
||||
If strict is True, all tags must be present in an entry. If false, the
|
||||
|
||||
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."""
|
||||
self.search_tags = set([tag.lower() for tag in tags])
|
||||
excluded_tags = set([tag.lower() for tag in exclude])
|
||||
end_date = time.parse(end_date, inclusive=True)
|
||||
start_date = time.parse(start_date)
|
||||
|
||||
# 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
|
||||
excluded = lambda tags: len([tag for tag in tags if tag in excluded_tags]) > 0
|
||||
result = [
|
||||
entry for entry in self.entries
|
||||
if (not tags or tagged(entry.tags))
|
||||
and (not starred or entry.starred)
|
||||
and (not start_date or entry.date >= start_date)
|
||||
and (not end_date or entry.date <= end_date)
|
||||
and (not exclude or not excluded(entry.tags))
|
||||
]
|
||||
|
||||
self.entries = result
|
||||
|
||||
def new_entry(self, raw, date=None, sort=True):
|
||||
"""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."""
|
||||
|
||||
raw = raw.replace('\\n ', '\n').replace('\\n', '\n')
|
||||
starred = False
|
||||
# Split raw text into title and body
|
||||
sep = re.search("\n|[\?!.]+ +\n?", raw)
|
||||
first_line = raw[:sep.end()].strip() if sep else raw
|
||||
starred = False
|
||||
|
||||
if not date:
|
||||
colon_pos = first_line.find(": ")
|
||||
if colon_pos > 0:
|
||||
date = time.parse(
|
||||
raw[:colon_pos],
|
||||
default_hour=self.config['default_hour'],
|
||||
default_minute=self.config['default_minute']
|
||||
)
|
||||
if date: # Parsed successfully, strip that from the raw text
|
||||
starred = raw[:colon_pos].strip().endswith("*")
|
||||
raw = raw[colon_pos + 1:].strip()
|
||||
starred = starred or first_line.startswith("*") or first_line.endswith("*")
|
||||
if not date: # Still nothing? Meh, just live in the moment.
|
||||
date = time.parse("now")
|
||||
entry = Entry.Entry(self, date, raw, starred=starred)
|
||||
entry.modified = True
|
||||
self.entries.append(entry)
|
||||
if sort:
|
||||
self.sort()
|
||||
return entry
|
||||
|
||||
def editable_str(self):
|
||||
"""Turns the journal into a string of entries that can be edited
|
||||
manually and later be parsed with eslf.parse_editable_str."""
|
||||
return "\n".join([e.__unicode__() for e in self.entries])
|
||||
|
||||
def parse_editable_str(self, edited):
|
||||
"""Parses the output of self.editable_str and updates it's entries."""
|
||||
mod_entries = self._parse(edited)
|
||||
# Match those entries that can be found in self.entries and set
|
||||
# these to modified, so we can get a count of how many entries got
|
||||
# modified and how many got deleted later.
|
||||
for entry in mod_entries:
|
||||
entry.modified = not any(entry == old_entry for old_entry in self.entries)
|
||||
self.entries = mod_entries
|
||||
|
||||
|
||||
class PlainJournal(Journal):
|
||||
@classmethod
|
||||
def _create(cls, filename):
|
||||
with codecs.open(filename, "a", "utf-8"):
|
||||
pass
|
||||
|
||||
def _load(self, filename):
|
||||
with codecs.open(filename, "r", "utf-8") as f:
|
||||
return f.read()
|
||||
|
||||
def _store(self, filename, text):
|
||||
with codecs.open(filename, 'w', "utf-8") as f:
|
||||
f.write(text)
|
||||
|
||||
|
||||
class LegacyJournal(Journal):
|
||||
"""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
|
||||
by square brackets. You'll not be able to save these journals anymore."""
|
||||
def _load(self, filename):
|
||||
with codecs.open(filename, "r", "utf-8") as f:
|
||||
return f.read()
|
||||
|
||||
def _parse(self, journal_txt):
|
||||
"""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
|
||||
# long the date will be by constructing one
|
||||
date_length = len(datetime.today().strftime(self.config['timeformat']))
|
||||
|
||||
# Initialise our current entry
|
||||
entries = []
|
||||
current_entry = None
|
||||
new_date_format_regex = re.compile(r'(^\[[^\]]+\].*?$)')
|
||||
for line in journal_txt.splitlines():
|
||||
line = line.rstrip()
|
||||
try:
|
||||
# try to parse line as date => new entry begins
|
||||
new_date = datetime.strptime(line[:date_length], self.config['timeformat'])
|
||||
|
||||
# parsing successful => save old entry and create new one
|
||||
if new_date and current_entry:
|
||||
entries.append(current_entry)
|
||||
|
||||
if line.endswith("*"):
|
||||
starred = True
|
||||
line = line[:-1]
|
||||
else:
|
||||
starred = False
|
||||
|
||||
current_entry = Entry.Entry(self, date=new_date, text=line[date_length + 1:], starred=starred)
|
||||
except ValueError:
|
||||
# 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
|
||||
# escaping for the new format).
|
||||
line = new_date_format_regex.sub(r' \1', line)
|
||||
if current_entry:
|
||||
current_entry.text += line + u"\n"
|
||||
|
||||
# Append last entry
|
||||
if current_entry:
|
||||
entries.append(current_entry)
|
||||
for entry in entries:
|
||||
entry._parse_text()
|
||||
return entries
|
||||
|
||||
|
||||
def open_journal(name, config, legacy=False):
|
||||
"""
|
||||
Creates a normal, encrypted or DayOne journal based on the passed config.
|
||||
If legacy is True, it will open Journals with legacy classes build for
|
||||
backwards compatibility with jrnl 1.x
|
||||
"""
|
||||
config = config.copy()
|
||||
config['journal'] = os.path.expanduser(os.path.expandvars(config['journal']))
|
||||
|
||||
if os.path.isdir(config['journal']):
|
||||
if config['journal'].strip("/").endswith(".dayone") or "entries" in os.listdir(config['journal']):
|
||||
from . import DayOneJournal
|
||||
return DayOneJournal.DayOne(**config).open()
|
||||
else:
|
||||
util.prompt(
|
||||
u"[Error: {0} is a directory, but doesn't seem to be a DayOne journal either.".format(config['journal'])
|
||||
)
|
||||
|
||||
sys.exit(1)
|
||||
|
||||
if not config['encrypt']:
|
||||
if legacy:
|
||||
return LegacyJournal(name, **config).open()
|
||||
return PlainJournal(name, **config).open()
|
||||
else:
|
||||
from . import EncryptedJournal
|
||||
if legacy:
|
||||
return EncryptedJournal.LegacyEncryptedJournal(name, **config).open()
|
||||
return EncryptedJournal.EncryptedJournal(name, **config).open()
|
9
jrnl/__init__.py
Normal file
|
@ -0,0 +1,9 @@
|
|||
#!/usr/bin/env python
|
||||
# encoding: utf-8
|
||||
|
||||
import pkg_resources
|
||||
|
||||
dist = pkg_resources.get_distribution('jrnl')
|
||||
__title__ = dist.project_name
|
||||
__version__ = dist.version
|
||||
|
8
jrnl/__main__.py
Normal file
|
@ -0,0 +1,8 @@
|
|||
#!/usr/bin/env python
|
||||
# encoding: utf-8
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
from . import cli
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
cli.run()
|
296
jrnl/cli.py
Normal file
|
@ -0,0 +1,296 @@
|
|||
#!/usr/bin/env python
|
||||
# encoding: utf-8
|
||||
|
||||
"""
|
||||
jrnl
|
||||
|
||||
license: MIT, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
from . import Journal
|
||||
from . import util
|
||||
from . import install
|
||||
from . import plugins
|
||||
from .util import ERROR_COLOR, RESET_COLOR, UserAbort
|
||||
import jrnl
|
||||
import argparse
|
||||
import sys
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
logging.getLogger("keyring.backend").setLevel(logging.ERROR)
|
||||
|
||||
|
||||
def parse_args(args=None):
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('-v', '--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.add_argument('text', metavar='', nargs="*")
|
||||
|
||||
reading = parser.add_argument_group('Reading', 'Specifying either of these parameters will display posts of your journal')
|
||||
reading.add_argument('-from', dest='start_date', metavar="DATE", help='View entries after this date')
|
||||
reading.add_argument('-until', '-to', dest='end_date', metavar="DATE", help='View entries before this date')
|
||||
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.add_argument('-s', '--short', dest='short', action="store_true", help='Show only titles or line containing the search tags')
|
||||
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)
|
||||
|
||||
|
||||
def guess_mode(args, config):
|
||||
"""Guesses the mode (compose, read or export) from the given arguments"""
|
||||
compose = True
|
||||
export = False
|
||||
import_ = False
|
||||
if args.import_ is not False:
|
||||
compose = False
|
||||
export = False
|
||||
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)):
|
||||
compose = False
|
||||
export = True
|
||||
elif any((args.start_date, args.end_date, args.on_date, args.limit, args.strict, args.starred)):
|
||||
# Any sign of displaying stuff?
|
||||
compose = False
|
||||
elif args.text and all(word[0] in config['tagsymbols'] for word in " ".join(args.text).split()):
|
||||
# No date and only tags?
|
||||
compose = False
|
||||
|
||||
return compose, export, import_
|
||||
|
||||
|
||||
def encrypt(journal, filename=None):
|
||||
""" Encrypt into new file. If filename is not set, we encrypt the journal file itself. """
|
||||
from . import EncryptedJournal
|
||||
|
||||
journal.config['password'] = util.getpass("Enter new password: ")
|
||||
journal.config['encrypt'] = True
|
||||
|
||||
new_journal = EncryptedJournal.EncryptedJournal(None, **journal.config)
|
||||
new_journal.entries = journal.entries
|
||||
new_journal.write(filename)
|
||||
|
||||
if util.yesno("Do you want to store the password in your keychain?", default=True):
|
||||
util.set_keychain(journal.name, journal.config['password'])
|
||||
|
||||
util.prompt("Journal encrypted to {0}.".format(filename or new_journal.config['journal']))
|
||||
|
||||
|
||||
def decrypt(journal, filename=None):
|
||||
""" Decrypts into new file. If filename is not set, we encrypt the journal file itself. """
|
||||
journal.config['encrypt'] = False
|
||||
journal.config['password'] = ""
|
||||
|
||||
new_journal = Journal.PlainJournal(filename, **journal.config)
|
||||
new_journal.entries = journal.entries
|
||||
new_journal.write(filename)
|
||||
util.prompt("Journal decrypted to {0}.".format(filename or new_journal.config['journal']))
|
||||
|
||||
|
||||
def list_journals(config):
|
||||
"""List the journals specified in the configuration file"""
|
||||
result = "Journals defined in {}\n".format(install.CONFIG_FILE_PATH)
|
||||
ml = min(max(len(k) for k in config['journals']), 20)
|
||||
for journal, cfg in config['journals'].items():
|
||||
result += " * {:{}} -> {}\n".format(journal, ml, cfg['journal'] if isinstance(cfg, dict) else cfg)
|
||||
return result
|
||||
|
||||
|
||||
def update_config(config, new_config, scope, force_local=False):
|
||||
"""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 within the scope"""
|
||||
if scope and type(config['journals'][scope]) is dict: # Update to journal specific
|
||||
config['journals'][scope].update(new_config)
|
||||
elif scope and force_local: # Convert to dict
|
||||
config['journals'][scope] = {"journal": config['journals'][scope]}
|
||||
config['journals'][scope].update(new_config)
|
||||
else:
|
||||
config.update(new_config)
|
||||
|
||||
|
||||
def configure_logger(debug=False):
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG if debug else logging.INFO,
|
||||
format='%(levelname)-8s %(name)-12s %(message)s'
|
||||
)
|
||||
logging.getLogger('parsedatetime').setLevel(logging.INFO) # disable parsedatetime debug logging
|
||||
|
||||
|
||||
def run(manual_args=None):
|
||||
args = parse_args(manual_args)
|
||||
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:
|
||||
version_str = "{0} version {1}".format(jrnl.__title__, jrnl.__version__)
|
||||
print(util.py2encode(version_str))
|
||||
sys.exit(0)
|
||||
|
||||
try:
|
||||
config = install.load_or_install_jrnl()
|
||||
except UserAbort as err:
|
||||
util.prompt("\n{}".format(err))
|
||||
sys.exit(1)
|
||||
|
||||
if args.ls:
|
||||
util.prnt(list_journals(config))
|
||||
sys.exit(0)
|
||||
|
||||
log.debug('Using configuration "%s"', config)
|
||||
original_config = config.copy()
|
||||
|
||||
# If the first textual argument points to a journal file,
|
||||
# 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':
|
||||
args.text = args.text[1:]
|
||||
elif "default" not in config['journals']:
|
||||
util.prompt("No default journal configured.")
|
||||
util.prompt(list_journals(config))
|
||||
sys.exit(1)
|
||||
|
||||
config = util.scope_config(config, journal_name)
|
||||
|
||||
# If the first remaining argument looks like e.g. '-3', interpret that as a limiter
|
||||
if not args.limit and args.text and args.text[0].startswith("-"):
|
||||
try:
|
||||
args.limit = int(args.text[0].lstrip("-"))
|
||||
args.text = args.text[1:]
|
||||
except:
|
||||
pass
|
||||
|
||||
log.debug('Using journal "%s"', journal_name)
|
||||
mode_compose, mode_export, mode_import = guess_mode(args, config)
|
||||
|
||||
# How to quit writing?
|
||||
if "win32" in sys.platform:
|
||||
_exit_multiline_code = "on a blank line, press Ctrl+Z and then Enter"
|
||||
else:
|
||||
_exit_multiline_code = "press Ctrl+D"
|
||||
|
||||
if mode_compose and not args.text:
|
||||
if not sys.stdin.isatty():
|
||||
# Piping data into jrnl
|
||||
raw = util.py23_read()
|
||||
elif config['editor']:
|
||||
template = ""
|
||||
if config['template']:
|
||||
try:
|
||||
template = open(config['template']).read()
|
||||
except:
|
||||
util.prompt("[Could not read template at '']".format(config['template']))
|
||||
sys.exit(1)
|
||||
raw = util.get_text_from_editor(config, template)
|
||||
else:
|
||||
try:
|
||||
raw = util.py23_read("[Compose Entry; " + _exit_multiline_code + " to finish writing]\n")
|
||||
except KeyboardInterrupt:
|
||||
util.prompt("[Entry NOT saved to journal.]")
|
||||
sys.exit(0)
|
||||
if raw:
|
||||
args.text = [raw]
|
||||
else:
|
||||
mode_compose = False
|
||||
|
||||
# This is where we finally open the journal!
|
||||
try:
|
||||
journal = Journal.open_journal(journal_name, config)
|
||||
except KeyboardInterrupt:
|
||||
util.prompt("[Interrupted while opening journal]".format(journal_name))
|
||||
sys.exit(1)
|
||||
|
||||
# Import mode
|
||||
if mode_import:
|
||||
plugins.get_importer(args.import_).import_(journal, args.input)
|
||||
|
||||
# Writing mode
|
||||
elif mode_compose:
|
||||
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)
|
||||
journal.new_entry(raw)
|
||||
util.prompt("[Entry added to {0} journal]".format(journal_name))
|
||||
journal.write()
|
||||
|
||||
if not mode_compose:
|
||||
old_entries = journal.entries
|
||||
if args.on_date:
|
||||
args.start_date = args.end_date = args.on_date
|
||||
journal.filter(tags=args.text,
|
||||
start_date=args.start_date, end_date=args.end_date,
|
||||
strict=args.strict,
|
||||
short=args.short,
|
||||
starred=args.starred,
|
||||
exclude=args.excluded)
|
||||
journal.limit(args.limit)
|
||||
|
||||
# Reading mode
|
||||
if not mode_compose and not mode_export and not mode_import:
|
||||
print(util.py2encode(journal.pprint()))
|
||||
|
||||
# Various export modes
|
||||
elif args.short:
|
||||
print(util.py2encode(journal.pprint(short=True)))
|
||||
|
||||
elif args.tags:
|
||||
print(util.py2encode(plugins.get_exporter("tags").export(journal)))
|
||||
|
||||
elif args.export is not False:
|
||||
exporter = plugins.get_exporter(args.export)
|
||||
print(exporter.export(journal, args.output))
|
||||
|
||||
elif args.encrypt is not False:
|
||||
encrypt(journal, filename=args.encrypt)
|
||||
# Not encrypting to a separate file: update config!
|
||||
if not args.encrypt:
|
||||
update_config(original_config, {"encrypt": True}, journal_name, force_local=True)
|
||||
install.save_config(original_config)
|
||||
|
||||
elif args.decrypt is not False:
|
||||
decrypt(journal, filename=args.decrypt)
|
||||
# Not decrypting to a separate file: update config!
|
||||
if not args.decrypt:
|
||||
update_config(original_config, {"encrypt": False}, journal_name, force_local=True)
|
||||
install.save_config(original_config)
|
||||
|
||||
elif args.edit:
|
||||
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))
|
||||
sys.exit(1)
|
||||
other_entries = [e for e in old_entries if e not in journal.entries]
|
||||
# Edit
|
||||
old_num_entries = len(journal)
|
||||
edited = util.get_text_from_editor(config, journal.editable_str())
|
||||
journal.parse_editable_str(edited)
|
||||
num_deleted = old_num_entries - len(journal)
|
||||
num_edited = len([e for e in journal.entries if e.modified])
|
||||
prompts = []
|
||||
if num_deleted:
|
||||
prompts.append("{0} {1} deleted".format(num_deleted, "entry" if num_deleted == 1 else "entries"))
|
||||
if num_edited:
|
||||
prompts.append("{0} {1} modified".format(num_edited, "entry" if num_deleted == 1 else "entries"))
|
||||
if prompts:
|
||||
util.prompt("[{0}]".format(", ".join(prompts).capitalize()))
|
||||
journal.entries += other_entries
|
||||
journal.sort()
|
||||
journal.write()
|
66
jrnl/export.py
Normal file
|
@ -0,0 +1,66 @@
|
|||
#!/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)
|
150
jrnl/install.py
Normal file
|
@ -0,0 +1,150 @@
|
|||
#!/usr/bin/env python
|
||||
# encoding: utf-8
|
||||
|
||||
from __future__ import absolute_import
|
||||
import readline
|
||||
import glob
|
||||
import getpass
|
||||
import os
|
||||
import xdg.BaseDirectory
|
||||
from . import util
|
||||
from . import upgrade
|
||||
from . import __version__
|
||||
from .Journal import PlainJournal
|
||||
from .EncryptedJournal import EncryptedJournal
|
||||
from .util import UserAbort
|
||||
import yaml
|
||||
import logging
|
||||
import sys
|
||||
|
||||
DEFAULT_CONFIG_NAME = 'jrnl.yaml'
|
||||
DEFAULT_JOURNAL_NAME = 'journal.txt'
|
||||
XDG_RESOURCE = 'jrnl'
|
||||
|
||||
USER_HOME = os.path.expanduser('~')
|
||||
|
||||
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_FALLBACK = os.path.join(USER_HOME, ".jrnl_config")
|
||||
|
||||
JOURNAL_PATH = xdg.BaseDirectory.save_data_path(XDG_RESOURCE) or USER_HOME
|
||||
JOURNAL_FILE_PATH = os.path.join(JOURNAL_PATH, DEFAULT_JOURNAL_NAME)
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def module_exists(module_name):
|
||||
"""Checks if a module exists and can be imported"""
|
||||
try:
|
||||
__import__(module_name)
|
||||
except ImportError:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
default_config = {
|
||||
'version': __version__,
|
||||
'journals': {
|
||||
"default": JOURNAL_FILE_PATH
|
||||
},
|
||||
'editor': os.getenv('VISUAL') or os.getenv('EDITOR') or "",
|
||||
'encrypt': False,
|
||||
'template': False,
|
||||
'default_hour': 9,
|
||||
'default_minute': 0,
|
||||
'timeformat': "%Y-%m-%d %H:%M",
|
||||
'tagsymbols': '@',
|
||||
'highlight': True,
|
||||
'linewrap': 79,
|
||||
'indent_character': '|',
|
||||
}
|
||||
|
||||
|
||||
def upgrade_config(config):
|
||||
"""Checks if there are keys missing in a given config dict, and if so, updates the config file accordingly.
|
||||
This essentially automatically ports jrnl installations if new config parameters are introduced in later
|
||||
versions."""
|
||||
missing_keys = set(default_config).difference(config)
|
||||
if missing_keys:
|
||||
for key in missing_keys:
|
||||
config[key] = default_config[key]
|
||||
save_config(config)
|
||||
print("[Configuration updated to newest version at {}]".format(CONFIG_FILE_PATH))
|
||||
|
||||
|
||||
def save_config(config):
|
||||
config['version'] = __version__
|
||||
with open(CONFIG_FILE_PATH, 'w') as f:
|
||||
yaml.safe_dump(config, f, encoding='utf-8', allow_unicode=True, default_flow_style=False)
|
||||
|
||||
|
||||
def load_or_install_jrnl():
|
||||
"""
|
||||
If jrnl is already installed, loads and returns a config object.
|
||||
Else, perform various prompts to install jrnl.
|
||||
"""
|
||||
config_path = CONFIG_FILE_PATH if os.path.exists(CONFIG_FILE_PATH) else CONFIG_FILE_PATH_FALLBACK
|
||||
if os.path.exists(config_path):
|
||||
log.debug('Reading configuration from file %s', config_path)
|
||||
config = util.load_config(config_path)
|
||||
|
||||
try:
|
||||
upgrade.upgrade_jrnl_if_necessary(config_path)
|
||||
except upgrade.UpgradeValidationException:
|
||||
util.prompt("Aborting upgrade.")
|
||||
util.prompt("Please tell us about this problem at the following URL:")
|
||||
util.prompt("https://github.com/jrnl-org/jrnl/issues/new?title=UpgradeValidationException")
|
||||
util.prompt("Exiting.")
|
||||
sys.exit(1)
|
||||
|
||||
upgrade_config(config)
|
||||
|
||||
return config
|
||||
else:
|
||||
log.debug('Configuration file not found, installing jrnl...')
|
||||
try:
|
||||
config = install()
|
||||
except KeyboardInterrupt:
|
||||
raise UserAbort("Installation aborted")
|
||||
return config
|
||||
|
||||
|
||||
def install():
|
||||
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]
|
||||
readline.set_completer_delims(' \t\n;')
|
||||
readline.parse_and_bind("tab: complete")
|
||||
readline.set_completer(autocomplete)
|
||||
|
||||
# Where to create the journal?
|
||||
path_query = 'Path to your journal file (leave blank for {}): '.format(JOURNAL_FILE_PATH)
|
||||
journal_path = util.py23_input(path_query).strip() or JOURNAL_FILE_PATH
|
||||
default_config['journals']['default'] = 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
|
||||
try:
|
||||
os.makedirs(path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
# Encrypt it?
|
||||
password = getpass.getpass("Enter password for journal (leave blank for no encryption): ")
|
||||
if password:
|
||||
default_config['encrypt'] = True
|
||||
if util.yesno("Do you want to store the password in your keychain?", default=True):
|
||||
util.set_keychain("default", password)
|
||||
else:
|
||||
util.set_keychain("default", None)
|
||||
EncryptedJournal._create(default_config['journals']['default'], password)
|
||||
print("Journal will be encrypted.")
|
||||
else:
|
||||
PlainJournal._create(default_config['journals']['default'])
|
||||
|
||||
config = default_config
|
||||
save_config(config)
|
||||
if password:
|
||||
config['password'] = password
|
||||
return config
|
35
jrnl/plugins/__init__.py
Normal file
|
@ -0,0 +1,35 @@
|
|||
#!/usr/bin/env python
|
||||
# encoding: utf-8
|
||||
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from .text_exporter import TextExporter
|
||||
from .jrnl_importer import JRNLImporter
|
||||
from .json_exporter import JSONExporter
|
||||
from .markdown_exporter import MarkdownExporter
|
||||
from .tag_exporter import TagExporter
|
||||
from .xml_exporter import XMLExporter
|
||||
from .yaml_exporter import YAMLExporter
|
||||
from .template_exporter import __all__ as template_exporters
|
||||
|
||||
__exporters =[JSONExporter, MarkdownExporter, TagExporter, TextExporter, XMLExporter, YAMLExporter] + template_exporters
|
||||
__importers =[JRNLImporter]
|
||||
|
||||
__exporter_types = dict([(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])
|
||||
|
||||
EXPORT_FORMATS = sorted(__exporter_types.keys())
|
||||
IMPORT_FORMATS = sorted(__importer_types.keys())
|
||||
|
||||
def get_exporter(format):
|
||||
for exporter in __exporters:
|
||||
if hasattr(exporter, "names") and format in exporter.names:
|
||||
return exporter
|
||||
return None
|
||||
|
||||
|
||||
def get_importer(format):
|
||||
for importer in __importers:
|
||||
if hasattr(importer, "names") and format in importer.names:
|
||||
return importer
|
||||
return None
|
31
jrnl/plugins/jrnl_importer.py
Normal file
|
@ -0,0 +1,31 @@
|
|||
#!/usr/bin/env python
|
||||
# encoding: utf-8
|
||||
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
import codecs
|
||||
import sys
|
||||
from .. import util
|
||||
|
||||
class JRNLImporter(object):
|
||||
"""This plugin imports entries from other jrnl files."""
|
||||
names = ["jrnl"]
|
||||
|
||||
@staticmethod
|
||||
def import_(journal, input=None):
|
||||
"""Imports from an existing file if input is specified, and
|
||||
standard input otherwise."""
|
||||
old_cnt = len(journal.entries)
|
||||
old_entries = journal.entries
|
||||
if input:
|
||||
with codecs.open(input, "r", "utf-8") as f:
|
||||
other_journal_txt = f.read()
|
||||
else:
|
||||
try:
|
||||
other_journal_txt = util.py23_read()
|
||||
except KeyboardInterrupt:
|
||||
util.prompt("[Entries NOT imported into journal.]")
|
||||
sys.exit(0)
|
||||
journal.import_(other_journal_txt)
|
||||
new_cnt = len(journal.entries)
|
||||
util.prompt("[{0} imported to {1} journal]".format(new_cnt - old_cnt, journal.name))
|
||||
journal.write()
|
41
jrnl/plugins/json_exporter.py
Normal file
|
@ -0,0 +1,41 @@
|
|||
#!/usr/bin/env python
|
||||
# encoding: utf-8
|
||||
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
from .text_exporter import TextExporter
|
||||
import json
|
||||
from .util import get_tags_count
|
||||
|
||||
|
||||
class JSONExporter(TextExporter):
|
||||
"""This Exporter can convert entries and journals into json."""
|
||||
names = ["json"]
|
||||
extension = "json"
|
||||
|
||||
@classmethod
|
||||
def entry_to_dict(cls, entry):
|
||||
entry_dict = {
|
||||
'title': entry.title,
|
||||
'body': entry.body,
|
||||
'date': entry.date.strftime("%Y-%m-%d"),
|
||||
'time': entry.date.strftime("%H:%M"),
|
||||
'starred': entry.starred
|
||||
}
|
||||
if hasattr(entry, "uuid"):
|
||||
entry_dict['uuid'] = entry.uuid
|
||||
return entry_dict
|
||||
|
||||
@classmethod
|
||||
def export_entry(cls, entry):
|
||||
"""Returns a json representation of a single entry."""
|
||||
return json.dumps(cls.entry_to_dict(entry), indent=2) + "\n"
|
||||
|
||||
@classmethod
|
||||
def export_journal(cls, journal):
|
||||
"""Returns a json representation of an entire journal."""
|
||||
tags = get_tags_count(journal)
|
||||
result = {
|
||||
"tags": dict((tag, count) for count, tag in tags),
|
||||
"entries": [cls.entry_to_dict(e) for e in journal.entries]
|
||||
}
|
||||
return json.dumps(result, indent=2)
|
80
jrnl/plugins/markdown_exporter.py
Normal file
|
@ -0,0 +1,80 @@
|
|||
#!/usr/bin/env python
|
||||
# encoding: utf-8
|
||||
|
||||
from __future__ import absolute_import, unicode_literals, print_function
|
||||
from .text_exporter import TextExporter
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from ..util import WARNING_COLOR, RESET_COLOR
|
||||
|
||||
|
||||
class MarkdownExporter(TextExporter):
|
||||
"""This Exporter can convert entries and journals into Markdown."""
|
||||
names = ["md", "markdown"]
|
||||
extension = "md"
|
||||
|
||||
@classmethod
|
||||
def export_entry(cls, entry, to_multifile=True):
|
||||
"""Returns a markdown representation of a single entry."""
|
||||
date_str = entry.date.strftime(entry.journal.config['timeformat'])
|
||||
body_wrapper = "\n" if entry.body else ""
|
||||
body = body_wrapper + entry.body
|
||||
|
||||
if to_multifile is True:
|
||||
heading = '#'
|
||||
else:
|
||||
heading = '###'
|
||||
|
||||
'''Increase heading levels in body text'''
|
||||
newbody = ''
|
||||
previous_line = ''
|
||||
warn_on_heading_level = False
|
||||
for line in body.splitlines(True):
|
||||
if re.match(r"^#+ ", line):
|
||||
"""ATX style headings"""
|
||||
newbody = newbody + previous_line + heading + line
|
||||
if re.match(r"^#######+ ", heading + line):
|
||||
warn_on_heading_level = True
|
||||
line = ''
|
||||
elif re.match(r"^=+$", line.rstrip()) and not re.match(r"^$", previous_line.strip()):
|
||||
"""Setext style H1"""
|
||||
newbody = newbody + heading + "# " + previous_line
|
||||
line = ''
|
||||
elif re.match(r"^-+$", line.rstrip()) and not re.match(r"^$", previous_line.strip()):
|
||||
"""Setext style H2"""
|
||||
newbody = newbody + heading + "## " + previous_line
|
||||
line = ''
|
||||
else:
|
||||
newbody = newbody + previous_line
|
||||
previous_line = line
|
||||
newbody = newbody + previous_line # add very last line
|
||||
|
||||
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)
|
||||
|
||||
return "{md} {date} {title}\n{body} {space}".format(
|
||||
md=heading,
|
||||
date=date_str,
|
||||
title=entry.title,
|
||||
body=newbody,
|
||||
space=""
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def export_journal(cls, journal):
|
||||
"""Returns a Markdown representation of an entire journal."""
|
||||
out = []
|
||||
year, month = -1, -1
|
||||
for e in journal.entries:
|
||||
if not e.date.year == year:
|
||||
year = e.date.year
|
||||
out.append("# " + str(year))
|
||||
out.append("")
|
||||
if not e.date.month == month:
|
||||
month = e.date.month
|
||||
out.append("## " + e.date.strftime("%B"))
|
||||
out.append("")
|
||||
out.append(cls.export_entry(e, False))
|
||||
result = "\n".join(out)
|
||||
return result
|
30
jrnl/plugins/tag_exporter.py
Normal file
|
@ -0,0 +1,30 @@
|
|||
#!/usr/bin/env python
|
||||
# encoding: utf-8
|
||||
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
from .text_exporter import TextExporter
|
||||
from .util import get_tags_count
|
||||
|
||||
|
||||
class TagExporter(TextExporter):
|
||||
"""This Exporter can lists the tags for entries and journals, exported as a plain text file."""
|
||||
names = ["tags"]
|
||||
extension = "tags"
|
||||
|
||||
@classmethod
|
||||
def export_entry(cls, entry):
|
||||
"""Returns a list of tags for a single entry."""
|
||||
return ", ".join(entry.tags)
|
||||
|
||||
@classmethod
|
||||
def export_journal(cls, journal):
|
||||
"""Returns a list of tags and their frequency for an entire journal."""
|
||||
tag_counts = get_tags_count(journal)
|
||||
result = ""
|
||||
if not tag_counts:
|
||||
return '[No tags found in journal.]'
|
||||
elif min(tag_counts)[0] == 0:
|
||||
tag_counts = filter(lambda x: x[0] > 1, tag_counts)
|
||||
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))
|
||||
return result
|
124
jrnl/plugins/template.py
Normal file
|
@ -0,0 +1,124 @@
|
|||
import re
|
||||
import asteval
|
||||
import yaml
|
||||
|
||||
VAR_RE = r"[_a-zA-Z][a-zA-Z0-9_]*"
|
||||
EXPRESSION_RE = r"[\[\]():.a-zA-Z0-9_]*"
|
||||
PRINT_RE = r"{{ *(.+?) *}}"
|
||||
START_BLOCK_RE = r"{% *(if|for) +(.+?) *%}"
|
||||
END_BLOCK_RE = r"{% *end(for|if) *%}"
|
||||
FOR_RE = r"{{% *for +({varname}) +in +([^%]+) *%}}".format(varname=VAR_RE, expression=EXPRESSION_RE)
|
||||
IF_RE = r"{% *if +(.+?) *%}"
|
||||
BLOCK_RE = r"{% *block +(.+?) *%}((?:.|\n)+?){% *endblock *%}"
|
||||
INCLUDE_RE = r"{% *include +(.+?) *%}"
|
||||
|
||||
|
||||
class Template(object):
|
||||
def __init__(self, template):
|
||||
self.template = template
|
||||
self.clean_template = None
|
||||
self.blocks = {}
|
||||
|
||||
@classmethod
|
||||
def from_file(cls, filename):
|
||||
with open(filename) as f:
|
||||
front_matter, body = f.read().strip("-\n").split("---", 2)
|
||||
front_matter = yaml.load(front_matter, Loader=yaml.FullLoader)
|
||||
template = cls(body)
|
||||
template.__dict__.update(front_matter)
|
||||
return template
|
||||
|
||||
def render(self, **vars):
|
||||
if self.clean_template is None:
|
||||
self._get_blocks()
|
||||
return self._expand(self.clean_template, **vars)
|
||||
|
||||
def render_block(self, block, **vars):
|
||||
if self.clean_template is None:
|
||||
self._get_blocks()
|
||||
return self._expand(self.blocks[block], **vars)
|
||||
|
||||
def _eval_context(self, vars):
|
||||
e = asteval.Interpreter(use_numpy=False, writer=None)
|
||||
e.symtable.update(vars)
|
||||
e.symtable['__last_iteration'] = vars.get("__last_iteration", False)
|
||||
return e
|
||||
|
||||
def _get_blocks(self):
|
||||
def s(match):
|
||||
name, contents = match.groups()
|
||||
self.blocks[name] = self._strip_single_nl(contents)
|
||||
return ""
|
||||
self.clean_template = re.sub(BLOCK_RE, s, self.template, flags=re.MULTILINE)
|
||||
|
||||
def _expand(self, template, **vars):
|
||||
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)]
|
||||
)
|
||||
|
||||
last_nesting, nesting = 0, 0
|
||||
start = 0
|
||||
result = ""
|
||||
block_type = None
|
||||
if not stack:
|
||||
return self._expand_vars(template, **vars)
|
||||
|
||||
for pos, indent, typ in stack:
|
||||
nesting += indent
|
||||
if nesting == 1 and last_nesting == 0:
|
||||
block_type = typ
|
||||
result += self._expand_vars(template[start:pos], **vars)
|
||||
start = pos
|
||||
elif nesting == 0 and last_nesting == 1:
|
||||
if block_type == "if":
|
||||
result += self._expand_cond(template[start:pos], **vars)
|
||||
elif block_type == "for":
|
||||
result += self._expand_loops(template[start:pos], **vars)
|
||||
elif block_type == "block":
|
||||
result += self._save_block(template[start:pos], **vars)
|
||||
start = pos
|
||||
last_nesting = nesting
|
||||
|
||||
result += self._expand_vars(template[stack[-1][0]:], **vars)
|
||||
return result
|
||||
|
||||
def _expand_vars(self, template, **vars):
|
||||
safe_eval = self._eval_context(vars)
|
||||
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)
|
||||
|
||||
def _expand_cond(self, template, **vars):
|
||||
start_block = re.search(IF_RE, template, re.M)
|
||||
end_block = list(re.finditer(END_BLOCK_RE, template, re.M))[-1]
|
||||
expression = start_block.groups()[0]
|
||||
sub_template = self._strip_single_nl(template[start_block.end():end_block.start()])
|
||||
|
||||
safe_eval = self._eval_context(vars)
|
||||
if safe_eval(expression):
|
||||
return self._expand(sub_template)
|
||||
return ""
|
||||
|
||||
def _strip_single_nl(self, template, strip_r=True):
|
||||
if template[0] == "\n":
|
||||
template = template[1:]
|
||||
if strip_r and template[-1] == "\n":
|
||||
template = template[:-1]
|
||||
return template
|
||||
|
||||
def _expand_loops(self, template, **vars):
|
||||
start_block = re.search(FOR_RE, template, re.M)
|
||||
end_block = list(re.finditer(END_BLOCK_RE, template, re.M))[-1]
|
||||
var_name, iterator = start_block.groups()
|
||||
sub_template = self._strip_single_nl(template[start_block.end():end_block.start()], strip_r=False)
|
||||
|
||||
safe_eval = self._eval_context(vars)
|
||||
|
||||
result = ''
|
||||
items = safe_eval(iterator)
|
||||
for idx, var in enumerate(items):
|
||||
vars[var_name] = var
|
||||
vars['__last_iteration'] = idx == len(items) - 1
|
||||
result += self._expand(sub_template, **vars)
|
||||
del vars[var_name]
|
||||
return self._strip_single_nl(result)
|
49
jrnl/plugins/template_exporter.py
Normal file
|
@ -0,0 +1,49 @@
|
|||
#!/usr/bin/env python
|
||||
# encoding: utf-8
|
||||
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from .text_exporter import TextExporter
|
||||
from .template import Template
|
||||
import os
|
||||
from glob import glob
|
||||
|
||||
|
||||
class GenericTemplateExporter(TextExporter):
|
||||
"""This Exporter can convert entries and journals into text files."""
|
||||
|
||||
@classmethod
|
||||
def export_entry(cls, entry):
|
||||
"""Returns a unicode representation of a single entry."""
|
||||
vars = {
|
||||
'entry': entry,
|
||||
'tags': entry.tags
|
||||
}
|
||||
return cls.template.render_block("entry", **vars)
|
||||
|
||||
@classmethod
|
||||
def export_journal(cls, journal):
|
||||
"""Returns a unicode representation of an entire journal."""
|
||||
vars = {
|
||||
'journal': journal,
|
||||
'entries': journal.entries,
|
||||
'tags': journal.tags
|
||||
}
|
||||
return cls.template.render_block("journal", **vars)
|
||||
|
||||
|
||||
def __exporter_from_file(template_file):
|
||||
"""Create a template class from a file"""
|
||||
name = os.path.basename(template_file).replace(".template", "")
|
||||
template = Template.from_file(template_file)
|
||||
return type(str("{}Exporter".format(name.title())), (GenericTemplateExporter, ), {
|
||||
"names": [name],
|
||||
"extension": template.extension,
|
||||
"template": template
|
||||
})
|
||||
|
||||
__all__ = []
|
||||
|
||||
# Factory pattern to create Exporter classes for all available templates
|
||||
for template_file in glob("jrnl/templates/*.template"):
|
||||
__all__.append(__exporter_from_file(template_file))
|
62
jrnl/plugins/text_exporter.py
Normal file
|
@ -0,0 +1,62 @@
|
|||
#!/usr/bin/env python
|
||||
# encoding: utf-8
|
||||
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
import codecs
|
||||
from ..util import u, slugify
|
||||
import os
|
||||
from ..util import ERROR_COLOR, RESET_COLOR
|
||||
|
||||
|
||||
class TextExporter(object):
|
||||
"""This Exporter can convert entries and journals into text files."""
|
||||
names = ["text", "txt"]
|
||||
extension = "txt"
|
||||
|
||||
@classmethod
|
||||
def export_entry(cls, entry):
|
||||
"""Returns a unicode representation of a single entry."""
|
||||
return entry.__unicode__()
|
||||
|
||||
@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)
|
||||
|
||||
@classmethod
|
||||
def write_file(cls, journal, path):
|
||||
"""Exports a journal into a single file."""
|
||||
try:
|
||||
with codecs.open(path, "w", "utf-8") as f:
|
||||
f.write(cls.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)
|
||||
|
||||
@classmethod
|
||||
def make_filename(cls, entry):
|
||||
return entry.date.strftime("%Y-%m-%d_{0}.{1}".format(slugify(u(entry.title)), cls.extension))
|
||||
|
||||
@classmethod
|
||||
def write_files(cls, journal, path):
|
||||
"""Exports a journal into individual files for each entry."""
|
||||
for entry in journal.entries:
|
||||
try:
|
||||
full_path = os.path.join(path, cls.make_filename(entry))
|
||||
with codecs.open(full_path, "w", "utf-8") as f:
|
||||
f.write(cls.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)
|
||||
|
||||
@classmethod
|
||||
def export(cls, journal, 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 cls.write_files(journal, output)
|
||||
elif output: # single file
|
||||
return cls.write_file(journal, output)
|
||||
else:
|
||||
return cls.export_journal(journal)
|
27
jrnl/plugins/util.py
Normal file
|
@ -0,0 +1,27 @@
|
|||
#!/usr/bin/env python
|
||||
# encoding: utf-8
|
||||
|
||||
|
||||
def get_tags_count(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
|
||||
# I came across this construction, worry not and embrace the ensuing moment of enlightment.
|
||||
tags = [tag
|
||||
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]
|
||||
tag_counts = set([(tags.count(tag), tag) for tag in tags])
|
||||
return tag_counts
|
||||
|
||||
|
||||
def oxford_list(lst):
|
||||
"""Return Human-readable list of things obeying the object comma)"""
|
||||
lst = sorted(lst)
|
||||
if not lst:
|
||||
return "(nothing)"
|
||||
elif len(lst) == 1:
|
||||
return lst[0]
|
||||
elif len(lst) == 2:
|
||||
return lst[0] + " or " + lst[1]
|
||||
else:
|
||||
return ', '.join(lst[:-1]) + ", or " + lst[-1]
|
60
jrnl/plugins/xml_exporter.py
Normal file
|
@ -0,0 +1,60 @@
|
|||
#!/usr/bin/env python
|
||||
# encoding: utf-8
|
||||
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
from .json_exporter import JSONExporter
|
||||
from .util import get_tags_count
|
||||
from ..util import u
|
||||
from xml.dom import minidom
|
||||
|
||||
|
||||
class XMLExporter(JSONExporter):
|
||||
"""This Exporter can convert entries and journals into XML."""
|
||||
names = ["xml"]
|
||||
extension = "xml"
|
||||
|
||||
@classmethod
|
||||
def export_entry(cls, entry, doc=None):
|
||||
"""Returns an XML representation of a single entry."""
|
||||
doc_el = doc or minidom.Document()
|
||||
entry_el = doc_el.createElement('entry')
|
||||
for key, value in cls.entry_to_dict(entry).items():
|
||||
elem = doc_el.createElement(key)
|
||||
elem.appendChild(doc_el.createTextNode(u(value)))
|
||||
entry_el.appendChild(elem)
|
||||
if not doc:
|
||||
doc_el.appendChild(entry_el)
|
||||
return doc_el.toprettyxml()
|
||||
else:
|
||||
return entry_el
|
||||
|
||||
@classmethod
|
||||
def entry_to_xml(cls, entry, doc):
|
||||
entry_el = doc.createElement('entry')
|
||||
entry_el.setAttribute('date', entry.date.isoformat())
|
||||
if hasattr(entry, "uuid"):
|
||||
entry_el.setAttribute('uuid', u(entry.uuid))
|
||||
entry_el.setAttribute('starred', u(entry.starred))
|
||||
entry_el.appendChild(doc.createTextNode(entry.fulltext))
|
||||
return entry_el
|
||||
|
||||
@classmethod
|
||||
def export_journal(cls, journal):
|
||||
"""Returns an XML representation of an entire journal."""
|
||||
tags = get_tags_count(journal)
|
||||
doc = minidom.Document()
|
||||
xml = doc.createElement('journal')
|
||||
tags_el = doc.createElement('tags')
|
||||
entries_el = doc.createElement('entries')
|
||||
for count, tag in tags:
|
||||
tag_el = doc.createElement('tag')
|
||||
tag_el.setAttribute('name', tag)
|
||||
count_node = doc.createTextNode(u(count))
|
||||
tag_el.appendChild(count_node)
|
||||
tags_el.appendChild(tag_el)
|
||||
for entry in journal.entries:
|
||||
entries_el.appendChild(cls.entry_to_xml(entry, doc))
|
||||
xml.appendChild(entries_el)
|
||||
xml.appendChild(tags_el)
|
||||
doc.appendChild(xml)
|
||||
return doc.toprettyxml()
|
84
jrnl/plugins/yaml_exporter.py
Normal file
|
@ -0,0 +1,84 @@
|
|||
#!/usr/bin/env python
|
||||
# encoding: utf-8
|
||||
|
||||
from __future__ import absolute_import, unicode_literals, print_function
|
||||
from .text_exporter import TextExporter
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from ..util import WARNING_COLOR, ERROR_COLOR, RESET_COLOR
|
||||
|
||||
|
||||
class YAMLExporter(TextExporter):
|
||||
"""This Exporter can convert entries and journals into Markdown formatted text with YAML front matter."""
|
||||
names = ["yaml"]
|
||||
extension = "md"
|
||||
|
||||
@classmethod
|
||||
def export_entry(cls, entry, to_multifile=True):
|
||||
"""Returns a markdown representation of a single entry, with YAML front matter."""
|
||||
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))
|
||||
return
|
||||
|
||||
date_str = entry.date.strftime(entry.journal.config['timeformat'])
|
||||
body_wrapper = "\n" if entry.body else ""
|
||||
body = body_wrapper + entry.body
|
||||
|
||||
tagsymbols = entry.journal.config['tagsymbols']
|
||||
# see also Entry.Entry.rag_regex
|
||||
multi_tag_regex = re.compile(r'(?u)^\s*([{tags}][-+*#/\w]+\s*)+$'.format(tags=tagsymbols), re.UNICODE)
|
||||
|
||||
'''Increase heading levels in body text'''
|
||||
newbody = ''
|
||||
heading = '#'
|
||||
previous_line = ''
|
||||
warn_on_heading_level = False
|
||||
for line in entry.body.splitlines(True):
|
||||
if re.match(r"^#+ ", line):
|
||||
"""ATX style headings"""
|
||||
newbody = newbody + previous_line + heading + line
|
||||
if re.match(r"^#######+ ", heading + line):
|
||||
warn_on_heading_level = True
|
||||
line = ''
|
||||
elif re.match(r"^=+$", line.rstrip()) and not re.match(r"^$", previous_line.strip()):
|
||||
"""Setext style H1"""
|
||||
newbody = newbody + heading + "# " + previous_line
|
||||
line = ''
|
||||
elif re.match(r"^-+$", line.rstrip()) and not re.match(r"^$", previous_line.strip()):
|
||||
"""Setext style H2"""
|
||||
newbody = newbody + heading + "## " + previous_line
|
||||
line = ''
|
||||
elif multi_tag_regex.match(line):
|
||||
"""Tag only lines"""
|
||||
line = ''
|
||||
else:
|
||||
newbody = newbody + previous_line
|
||||
previous_line = line
|
||||
newbody = newbody + previous_line # add very last line
|
||||
|
||||
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)
|
||||
|
||||
dayone_attributes = ''
|
||||
if hasattr(entry, "uuid"):
|
||||
dayone_attributes += 'uuid: ' + entry.uuid + '\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(
|
||||
date = date_str,
|
||||
title = entry.title,
|
||||
stared = entry.starred,
|
||||
tags = ', '.join([tag[1:] for tag in entry.tags]),
|
||||
dayone = dayone_attributes,
|
||||
body = newbody,
|
||||
space=""
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def export_journal(cls, journal):
|
||||
"""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)
|
||||
return
|
18
jrnl/templates/sample.template
Normal file
|
@ -0,0 +1,18 @@
|
|||
---
|
||||
extension: txt
|
||||
---
|
||||
|
||||
{% block journal %}
|
||||
{% for entry in entries %}
|
||||
{% include entry %}
|
||||
{% endfor %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block entry %}
|
||||
{{ entry.title }}
|
||||
{{ "-" * len(entry.title) }}
|
||||
|
||||
{{ entry.body }}
|
||||
|
||||
{% endblock %}
|
65
jrnl/time.py
Normal file
|
@ -0,0 +1,65 @@
|
|||
from datetime import datetime
|
||||
from dateutil.parser import parse as dateparse
|
||||
try: import parsedatetime.parsedatetime_consts as pdt
|
||||
except ImportError: import parsedatetime as pdt
|
||||
|
||||
FAKE_YEAR = 9999
|
||||
DEFAULT_FUTURE = datetime(FAKE_YEAR, 12, 31, 23, 59, 59)
|
||||
DEFAULT_PAST = datetime(FAKE_YEAR, 1, 1, 0, 0)
|
||||
|
||||
consts = pdt.Constants(usePyICU=False)
|
||||
consts.DOWParseStyle = -1 # "Monday" will be either today or the last Monday
|
||||
CALENDAR = pdt.Calendar(consts)
|
||||
|
||||
|
||||
def parse(date_str, inclusive=False, default_hour=None, default_minute=None):
|
||||
"""Parses a string containing a fuzzy date and returns a datetime.datetime object"""
|
||||
if not date_str:
|
||||
return None
|
||||
elif isinstance(date_str, datetime):
|
||||
return date_str
|
||||
|
||||
# Don't try to parse anything with 6 or less characters. It's probably a markdown footnote
|
||||
if len(date_str) <= 6:
|
||||
return None
|
||||
|
||||
default_date = DEFAULT_FUTURE if inclusive else DEFAULT_PAST
|
||||
date = None
|
||||
year_present = False
|
||||
while not date:
|
||||
try:
|
||||
date = dateparse(date_str, default=default_date)
|
||||
if date.year == FAKE_YEAR:
|
||||
date = datetime(datetime.now().year, date.timetuple()[1:6])
|
||||
else:
|
||||
year_present = True
|
||||
flag = 1 if date.hour == date.minute == 0 else 2
|
||||
date = date.timetuple()
|
||||
except Exception as e:
|
||||
if e.args[0] == 'day is out of range for month':
|
||||
y, m, d, H, M, S = default_date.timetuple()[:6]
|
||||
default_date = datetime(y, m, d - 1, H, M, S)
|
||||
else:
|
||||
date, flag = CALENDAR.parse(date_str)
|
||||
|
||||
if not flag: # Oops, unparsable.
|
||||
try: # Try and parse this as a single year
|
||||
year = int(date_str)
|
||||
return datetime(year, 1, 1)
|
||||
except ValueError:
|
||||
return None
|
||||
except TypeError:
|
||||
return None
|
||||
|
||||
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)
|
||||
else:
|
||||
date = datetime(*date[:6])
|
||||
|
||||
# Ugly heuristic: if the date is more than 4 weeks in the future, we got the year wrong.
|
||||
# Rather then this, we would like to see parsedatetime patched so we can tell it to prefer
|
||||
# past dates
|
||||
dt = datetime.now() - date
|
||||
if dt.days < -28 and not year_present:
|
||||
date = date.replace(date.year - 1)
|
||||
return date
|
120
jrnl/upgrade.py
Normal file
|
@ -0,0 +1,120 @@
|
|||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from . import __version__
|
||||
from . import Journal
|
||||
from . import util
|
||||
from .EncryptedJournal import EncryptedJournal
|
||||
from .util import UserAbort
|
||||
import os
|
||||
import codecs
|
||||
|
||||
|
||||
def backup(filename, binary=False):
|
||||
util.prompt(" Created a backup at {}.backup".format(filename))
|
||||
filename = os.path.expanduser(os.path.expandvars(filename))
|
||||
with open(filename, 'rb' if binary else 'r') as original:
|
||||
contents = original.read()
|
||||
with open(filename + ".backup", 'wb' if binary else 'w') as backup:
|
||||
backup.write(contents)
|
||||
|
||||
|
||||
def upgrade_jrnl_if_necessary(config_path):
|
||||
with codecs.open(config_path, "r", "utf-8") as f:
|
||||
config_file = f.read()
|
||||
if not config_file.strip().startswith("{"):
|
||||
return
|
||||
|
||||
config = util.load_config(config_path)
|
||||
|
||||
util.prompt("""Welcome to jrnl {}.
|
||||
|
||||
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
|
||||
you can enjoy all of the great new features that come with jrnl 2:
|
||||
|
||||
- Support for storing your journal in multiple files
|
||||
- Faster reading and writing for large journals
|
||||
- New encryption back-end that makes installing jrnl much easier
|
||||
- Tons of bug fixes
|
||||
|
||||
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
|
||||
older versions of jrnl anymore.
|
||||
""".format(__version__))
|
||||
encrypted_journals = {}
|
||||
plain_journals = {}
|
||||
other_journals = {}
|
||||
all_journals = []
|
||||
|
||||
for journal_name, journal_conf in config['journals'].items():
|
||||
if isinstance(journal_conf, dict):
|
||||
path = journal_conf.get("journal")
|
||||
encrypt = journal_conf.get("encrypt")
|
||||
else:
|
||||
encrypt = config.get('encrypt')
|
||||
path = journal_conf
|
||||
|
||||
if encrypt:
|
||||
encrypted_journals[journal_name] = path
|
||||
elif os.path.isdir(path):
|
||||
other_journals[journal_name] = path
|
||||
else:
|
||||
plain_journals[journal_name] = path
|
||||
|
||||
longest_journal_name = max([len(journal) for journal in config['journals']])
|
||||
if encrypted_journals:
|
||||
util.prompt("\nFollowing encrypted journals will be upgraded to jrnl {}:".format(__version__))
|
||||
for journal, path in encrypted_journals.items():
|
||||
util.prompt(" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name))
|
||||
|
||||
if plain_journals:
|
||||
util.prompt("\nFollowing plain text journals will upgraded to jrnl {}:".format(__version__))
|
||||
for journal, path in plain_journals.items():
|
||||
util.prompt(" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name))
|
||||
|
||||
if other_journals:
|
||||
util.prompt("\nFollowing journals will be not be touched:")
|
||||
for journal, path in other_journals.items():
|
||||
util.prompt(" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name))
|
||||
|
||||
try:
|
||||
cont = util.yesno("\nContinue upgrading jrnl?", default=False)
|
||||
if not cont:
|
||||
raise KeyboardInterrupt
|
||||
except KeyboardInterrupt:
|
||||
raise UserAbort("jrnl NOT upgraded, exiting.")
|
||||
|
||||
for journal_name, path in encrypted_journals.items():
|
||||
util.prompt("\nUpgrading encrypted '{}' journal stored in {}...".format(journal_name, path))
|
||||
backup(path, binary=True)
|
||||
old_journal = Journal.open_journal(journal_name, util.scope_config(config, journal_name), legacy=True)
|
||||
all_journals.append(EncryptedJournal.from_journal(old_journal))
|
||||
|
||||
for journal_name, path in plain_journals.items():
|
||||
util.prompt("\nUpgrading plain text '{}' journal stored in {}...".format(journal_name, path))
|
||||
backup(path)
|
||||
old_journal = Journal.open_journal(journal_name, util.scope_config(config, journal_name), legacy=True)
|
||||
all_journals.append(Journal.PlainJournal.from_journal(old_journal))
|
||||
|
||||
# loop through lists to validate
|
||||
failed_journals = [j for j in all_journals if not j.validate_parsing()]
|
||||
|
||||
if len(failed_journals) > 0:
|
||||
util.prompt("\nThe following journal{} failed to upgrade:\n{}".format(
|
||||
's' if len(failed_journals) > 1 else '', "\n".join(j.name for j in failed_journals))
|
||||
)
|
||||
|
||||
raise UpgradeValidationException
|
||||
|
||||
# write all journals - or - don't
|
||||
for j in all_journals:
|
||||
j.write()
|
||||
|
||||
util.prompt("\nUpgrading config...")
|
||||
backup(config_path)
|
||||
|
||||
util.prompt("\nWe're all done here and you can start enjoying jrnl 2.".format(config_path))
|
||||
|
||||
class UpgradeValidationException(Exception):
|
||||
"""Raised when the contents of an upgraded journal do not match the old journal"""
|
||||
pass
|