mirror of
https://github.com/jrnl-org/jrnl.git
synced 2025-05-20 13:08:31 +02:00
Merge branch 'master' into fix-custom-dates
This commit is contained in:
commit
25c0feaab6
45 changed files with 451 additions and 527 deletions
26
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
26
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
## Bug report
|
||||
|
||||
Hello! Thank you for reporting an issue!
|
||||
If you would fill out the below points, that would make our process a whole lot easier!
|
||||
|
||||
* **Please tell us about your environment:**
|
||||
- Jrnl version: (run `jrnl -v`)
|
||||
- How you installed Jrnl
|
||||
- Operating system [MacOS, Linux, Windows?]
|
||||
|
||||
* **What is the current behavior?**
|
||||
|
||||
* **Please provide the steps to reproduce and if possible a minimal demo of the problem**
|
||||
|
||||
* **What is the expected behavior?**
|
||||
|
||||
* **Other information** (e.g. detailed explanation, stacktraces, related issues, suggestions how to fix, links for us to have context, eg. stackoverflow, gitter, etc)
|
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
1
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
blank_issues_enabled: false
|
19
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
19
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
|
@ -0,0 +1,19 @@
|
|||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for jrnl
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
## Feature Request
|
||||
|
||||
Hello! Thank you for reporting an issue!
|
||||
If you would fill out the below points, that would make our process a whole lot easier!
|
||||
|
||||
* **What is the motivation / use case for changing the behavior?**
|
||||
|
||||
* **Please provide examples of the usage**
|
||||
|
||||
* **Other information** (e.g. detailed explanation, stacktraces, related issues, suggestions how to fix, links for us to have context, eg. stackoverflow, gitter, etc)
|
27
.github/ISSUE_TEMPLATE/support_request.md
vendored
Normal file
27
.github/ISSUE_TEMPLATE/support_request.md
vendored
Normal file
|
@ -0,0 +1,27 @@
|
|||
---
|
||||
name: Support Request
|
||||
about: Get help with jrnl
|
||||
title: ''
|
||||
labels: support
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
## Support request
|
||||
|
||||
Hello! Thank you for reporting an issue!
|
||||
If you would fill out the below points, that would make our process a whole lot easier!
|
||||
|
||||
* **Please tell us about your environment:**
|
||||
|
||||
- Jrnl version: (run `jrnl -v`)
|
||||
- How you installed Jrnl
|
||||
|
||||
- Operating system [MacOS, Linux, Windows?]
|
||||
|
||||
* **What are you trying to do?**
|
||||
|
||||
* **What have you tried?**
|
||||
|
||||
* **Other information** (e.g. detailed explanation, stacktraces, related issues, suggestions how to fix, links for us to have context, eg. stackoverflow, gitter, etc)
|
||||
|
19
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
19
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
|
@ -0,0 +1,19 @@
|
|||
|
||||
# **TEMPLATE PLEASE EDIT**
|
||||
*Thank you for wanting to contribute! Please fill out this description as well as look at the checklist!*
|
||||
|
||||
*Short block of text containing:
|
||||
- Relevant changes in text form
|
||||
- related issues
|
||||
- Motivation (if applicable)
|
||||
- Example of usage (if applicable)
|
||||
- Example of changes to config files (if applicable)
|
||||
*
|
||||
### Checklist
|
||||
- [ ] The code change is tested and works locally.
|
||||
- [ ] Tests pass. Your PR cannot be merged unless tests pass
|
||||
- [ ] There is no commented out code in this PR.
|
||||
- [ ] Have you followed the guidelines in our Contributing document?
|
||||
- [ ] Have you checked to ensure there aren't other open [Pull Requests](../pulls) for the same update/change?
|
||||
- [ ] Have you added an explanation of what your changes do and why you'd like us to include them?
|
||||
- [ ] Have you written new tests for your core changes, as applicable?
|
12
.travis.yml
12
.travis.yml
|
@ -1,8 +1,10 @@
|
|||
dist: xenial # required for Python >= 3.7
|
||||
language: python
|
||||
python: "3.7"
|
||||
git:
|
||||
depth: false
|
||||
before_install:
|
||||
- pip install poetry
|
||||
- pip install poetry~=0.12.17
|
||||
install:
|
||||
# we run `poetry version` here to appease poetry about '0.0.0-source'
|
||||
- poetry version
|
||||
|
@ -11,7 +13,6 @@ 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
|
||||
|
@ -22,3 +23,10 @@ deploy:
|
|||
on:
|
||||
branch: master
|
||||
tags: true
|
||||
after_deploy:
|
||||
- git config --global user.email "jrnl.bot@gmail.com"
|
||||
- git config --global user.name "Jrnl Bot"
|
||||
- git checkout master
|
||||
- git add pyproject.toml
|
||||
- git commit -m "Incrementing version to ${TRAVIS_TAG}"
|
||||
- git push https://${GITHUB_TOKEN}@github.com/jrnl-org/jrnl.git master
|
||||
|
|
|
@ -3,7 +3,7 @@ Changelog
|
|||
|
||||
## 2.0
|
||||
|
||||
* Cryptographical backend changed from PyCrypto to cryptography.io
|
||||
* Cryptographic 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`
|
||||
|
|
|
@ -18,13 +18,13 @@ Unfortunately, bugs happen. If you found one, please [open a new issue](https://
|
|||
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.
|
||||
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 relevant 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
|
||||
* avoid duplicating functionality
|
||||
|
||||
|
||||
A short note for new programmers and programmers new to python
|
||||
|
|
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014 Manuel Ebert
|
||||
Copyright (c) 2014-2019 Manuel Ebert
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
|
|
@ -3,15 +3,19 @@
|
|||
## 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`
|
||||
default, this is `~/.config/jrnl/jrnl.yaml`. If you have the `XDG_CONFIG_HOME`
|
||||
variable set, the configuration file will be saved as
|
||||
`$XDG_CONFIG_HOME/jrnl/.jrnl_config`.
|
||||
`$XDG_CONFIG_HOME/jrnl/jrnl.yaml`.
|
||||
|
||||
!!! note
|
||||
On Windows, The configuration file is typically found at `C:\Users\[Your Username]\.jrnl_config`.
|
||||
On Windows, the configuration file is typically found at `%USERPROFILE%\.config\jrnl\jrnl.yaml`.
|
||||
|
||||
The configuration file is a simple JSON file with the following options
|
||||
and can be edited with any plain text editor.
|
||||
The configuration file is a YAML file with the following options
|
||||
and can be edited with a plain text editor.
|
||||
|
||||
!!! note
|
||||
Backup your config file before editing. Changes to the config file
|
||||
have destructive effects on your journal!
|
||||
|
||||
- `journals`
|
||||
paths to your journal files
|
||||
|
@ -51,46 +55,16 @@ and can be edited with any plain text editor.
|
|||
Or use the built-in prompt or an external editor to compose your
|
||||
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`,
|
||||
`private` and `work`) by defining more journals in your `jrnl.yaml`,
|
||||
for example:
|
||||
|
||||
``` javascript
|
||||
{
|
||||
...
|
||||
"journals": {
|
||||
"default": "~/journal.txt",
|
||||
"work": "~/work.txt"
|
||||
}
|
||||
}
|
||||
``` yaml
|
||||
journals:
|
||||
default: ~\journal.txt
|
||||
work: ~\work.txt
|
||||
```
|
||||
|
||||
The `default` journal gets created the first time you start `jrnl`
|
||||
|
@ -106,26 +80,22 @@ will both use `~/work.txt`, while `jrnl -n 3` will display the last
|
|||
three entries from `~/journal.txt` (and so does `jrnl default -n 3`).
|
||||
|
||||
You can also override the default options for each individual journal.
|
||||
If you `.jrnl_config` looks like this:
|
||||
If your `jrnl.yaml` looks like this:
|
||||
|
||||
``` javascript
|
||||
{
|
||||
...
|
||||
"encrypt": false
|
||||
"journals": {
|
||||
"default": "~/journal.txt",
|
||||
"work": {
|
||||
"journal": "~/work.txt",
|
||||
"encrypt": true
|
||||
},
|
||||
"food": "~/my_recipes.txt",
|
||||
}
|
||||
``` yaml
|
||||
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
|
||||
the top level of `jrnl.yaml`, just make sure that at the very least
|
||||
you specify a `journal: ...` key that points to the journal file of
|
||||
that journal.
|
||||
|
||||
!!! note
|
||||
|
|
|
@ -2,9 +2,9 @@
|
|||
|
||||
## Encrypting and decrypting
|
||||
|
||||
If you don't choose to encrypt your file when you run
|
||||
If you don’t choose to encrypt your file when you run
|
||||
`jrnl` for the first time, you can encrypt
|
||||
your existing journal file or change its password using
|
||||
your existing journal file or change its password using this:
|
||||
|
||||
``` sh
|
||||
jrnl --encrypt
|
||||
|
@ -18,44 +18,50 @@ replaced by the encrypted file. Conversely,
|
|||
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`,
|
||||
will replace your encrypted journal file with a journal in plain text. You
|
||||
can also specify a filename, i.e. `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
|
||||
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
|
||||
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
|
||||
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`
|
||||
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`
|
||||
If you are using zsh instead of bash, you can get the same behaviour by
|
||||
adding this to your `zshrc`:
|
||||
|
||||
``` sh
|
||||
setopt HIST_IGNORE_SPACE
|
||||
alias jrnl=" jrnl"
|
||||
```
|
||||
|
||||
The fish shell does not support automatically preventing logging like
|
||||
this. To prevent `jrnl` commands being logged by fish, you must make
|
||||
sure to type a space before every `jrnl` command you enter. To delete
|
||||
existing `jrnl` commands from fish’s history, run
|
||||
`history delete --prefix 'jrnl '`.
|
||||
|
||||
## Manual decryption
|
||||
|
||||
Should you ever want to decrypt your journal manually, you can do so
|
||||
|
@ -63,8 +69,8 @@ with any program that supports the AES algorithm in CBC. The key used
|
|||
for encryption is the SHA-256-hash of your password, the IV
|
||||
(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
|
||||
before being encrypted. Here’s a Python script that you can use to
|
||||
decrypt your journal:
|
||||
|
||||
``` python
|
||||
#!/usr/bin/env python3
|
||||
|
|
|
@ -15,27 +15,12 @@ On other platforms, install *jrnl* using pip
|
|||
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
|
||||
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.
|
||||
|
|
|
@ -8,10 +8,6 @@ files - you can put them into a Dropbox folder for instant syncing and
|
|||
you can be assured that your journal will still be readable in 2050,
|
||||
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).
|
||||
|
||||
|
|
|
@ -50,8 +50,7 @@ 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
|
||||
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
|
||||
|
@ -94,22 +93,32 @@ log_question 'What did I achieve today?'
|
|||
log_question 'What did I make progress with?'
|
||||
```
|
||||
|
||||
### Display random entry
|
||||
|
||||
You can use this to select one title at random and then display the whole
|
||||
entry. The invocation of `cut` needs to match the format of the timestamp.
|
||||
For timestamps that have a space between data and time components, select
|
||||
fields 1 and 2 as shown. For timestamps that have no whitespace, select
|
||||
only field 1.
|
||||
|
||||
```sh
|
||||
jrnl -on "$(jrnl --short | shuf -n 1 | cut -d' ' -f1,2)"
|
||||
```
|
||||
|
||||
## External editors
|
||||
|
||||
To use external editors for writing and editing journal entries, set
|
||||
them up in your `.jrnl_config` (see `advanced usage <advanced>` for
|
||||
them up in your `jrnl.yaml` (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:
|
||||
configure your `jrnl.yaml` like this:
|
||||
|
||||
``` json
|
||||
{
|
||||
"editor": "subl -w"
|
||||
}
|
||||
```yaml
|
||||
editor: "subl -w"
|
||||
```
|
||||
|
||||
Note the `-w` flag to make sure jrnl waits for Sublime Text to close the
|
||||
|
@ -121,22 +130,20 @@ 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"
|
||||
}
|
||||
<<<<<<< HEAD
|
||||
|
||||
```yaml
|
||||
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:
|
||||
`jrnl.yaml` like this:
|
||||
|
||||
``` json
|
||||
{
|
||||
"editor": "open -b pro.writer.mac -Wn"
|
||||
}
|
||||
```yaml
|
||||
editor: "open -b pro.writer.mac -Wn"
|
||||
```
|
||||
|
||||
What does this do? `open -b ...` opens a file using the application
|
||||
|
@ -155,12 +162,10 @@ 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:
|
||||
the jrnl config file (`jrnl.yaml`) like this:
|
||||
|
||||
``` json
|
||||
{
|
||||
"editor": "C:\\Program Files (x86)\\Notepad++\\notepad++.exe -multiInst -nosession",
|
||||
}
|
||||
```yaml
|
||||
editor: "C:\\Program Files (x86)\\Notepad++\\notepad++.exe -multiInst -nosession"
|
||||
```
|
||||
|
||||
The double backslashes are needed so jrnl can read the file path
|
||||
|
@ -169,12 +174,10 @@ 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:
|
||||
To set [Visual Studo Code](https://code.visualstudio.com) as your editor on Linux, edit `jrnl.yaml` like this:
|
||||
|
||||
```json
|
||||
{
|
||||
"editor": "/usr/bin/code --wait",
|
||||
}
|
||||
```yaml
|
||||
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.
|
||||
|
@ -184,14 +187,13 @@ 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",
|
||||
}
|
||||
```yaml
|
||||
editor: "code --wait"
|
||||
```
|
||||
|
||||
to ``.jrnl_config``. See also the [Visual Studio Code documentation](https://code.visualstudio.com/docs/setup/mac)
|
||||
to `jrnl.yaml`. See also the [Visual Studio Code documentation](https://code.visualstudio.com/docs/setup/mac)
|
||||
|
|
5
docs/theme/index.html
vendored
5
docs/theme/index.html
vendored
|
@ -82,11 +82,6 @@
|
|||
<h3>Accessible anywhere.</h3>
|
||||
<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>
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
# Basic Usage
|
||||
|
||||
`jrnl` has two modes: **composing** and **viewing**. Basically, whenever
|
||||
you *don't* supply any arguments that start
|
||||
you _don't_ supply any arguments that start
|
||||
with a dash or double-dash, you're in composing mode, meaning you can
|
||||
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
|
||||
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
|
||||
_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).
|
||||
|
||||
|
@ -129,14 +129,14 @@ 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.
|
||||
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>`)
|
||||
useful when your journal file is encrypted. 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
|
||||
|
@ -158,25 +158,3 @@ 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).
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
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"
|
||||
Then the output should contain "Password"
|
||||
and the output should contain "2013-06-10 15:40 Life is good"
|
||||
|
||||
Scenario: Decrypting a journal
|
||||
|
@ -14,16 +14,16 @@
|
|||
|
||||
Scenario: Encrypting a journal
|
||||
Given we use the config "basic.yaml"
|
||||
When we run "jrnl --encrypt" and enter "swordfish"
|
||||
When we run "jrnl --encrypt" and enter "swordfish" and "n"
|
||||
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"
|
||||
Then the output should contain "Password"
|
||||
and the output should contain "2013-06-10 15:40 Life is good"
|
||||
|
||||
Scenario: Storing a password in Keychain
|
||||
Given we use the config "multiple.yaml"
|
||||
When we run "jrnl simple --encrypt" and enter "sabertooth"
|
||||
When we run "jrnl simple --encrypt" and enter "sabertooth" and "y"
|
||||
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"
|
||||
|
|
|
@ -1,25 +1,15 @@
|
|||
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)
|
||||
|
@ -32,10 +22,9 @@ def before_scenario(context, scenario):
|
|||
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):
|
||||
|
|
|
@ -42,5 +42,5 @@ Feature: Multiple journals
|
|||
|
||||
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"
|
||||
When we run "jrnl new_encrypted Adding first entry" and enter "these three eyes" and "y"
|
||||
Then we should see the message "Journal 'new_encrypted' created"
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
from unittest.mock import patch
|
||||
|
||||
from behave import given, when, then
|
||||
from jrnl import cli, install, Journal, util, plugins
|
||||
|
@ -10,10 +9,13 @@ import os
|
|||
import json
|
||||
import yaml
|
||||
import keyring
|
||||
import tzlocal
|
||||
import shlex
|
||||
import sys
|
||||
|
||||
|
||||
class TestKeyring(keyring.backend.KeyringBackend):
|
||||
"""A test keyring that just stores its valies in a hash"""
|
||||
"""A test keyring that just stores its values in a hash"""
|
||||
|
||||
priority = 1
|
||||
keys = defaultdict(dict)
|
||||
|
@ -31,15 +33,6 @@ class TestKeyring(keyring.backend.KeyringBackend):
|
|||
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)
|
||||
|
@ -73,19 +66,42 @@ def set_config(context, config_file):
|
|||
cf.write("version: {}".format(__version__))
|
||||
|
||||
|
||||
def _mock_getpass(inputs):
|
||||
def prompt_return(prompt="Password: "):
|
||||
print(prompt)
|
||||
return next(inputs)
|
||||
return prompt_return
|
||||
|
||||
|
||||
def _mock_input(inputs):
|
||||
def prompt_return(prompt=""):
|
||||
val = next(inputs)
|
||||
print(prompt, val)
|
||||
return val
|
||||
return prompt_return
|
||||
|
||||
|
||||
@when('we run "{command}" and enter')
|
||||
@when('we run "{command}" and enter "{inputs}"')
|
||||
def run_with_input(context, command, inputs=None):
|
||||
text = inputs or context.text
|
||||
@when('we run "{command}" and enter "{inputs1}"')
|
||||
@when('we run "{command}" and enter "{inputs1}" and "{inputs2}"')
|
||||
def run_with_input(context, command, inputs1="", inputs2=""):
|
||||
# create an iterator through all inputs. These inputs will be fed one by one
|
||||
# to the mocked calls for 'input()', 'util.getpass()' and 'sys.stdin.read()'
|
||||
text = iter((inputs1, inputs2)) if inputs1 else iter(context.text.split("\n"))
|
||||
args = ushlex(command)[1:]
|
||||
buffer = StringIO(text.strip())
|
||||
util.STDIN = buffer
|
||||
with patch("builtins.input", side_effect=_mock_input(text)) as mock_input:
|
||||
with patch("jrnl.util.getpass", side_effect=_mock_getpass(text)) as mock_getpass:
|
||||
with patch("sys.stdin.read", side_effect=text) as mock_read:
|
||||
try:
|
||||
cli.run(args or [])
|
||||
context.exit_status = 0
|
||||
except SystemExit as e:
|
||||
context.exit_status = e.code
|
||||
|
||||
# assert at least one of the mocked input methods got called
|
||||
assert mock_input.called or mock_getpass.called or mock_read.called
|
||||
|
||||
|
||||
|
||||
@when('we run "{command}"')
|
||||
def run(context, command):
|
||||
|
@ -190,28 +206,24 @@ def check_output_time_inline(context, 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()
|
||||
out = context.stderr_capture.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()
|
||||
out = context.stderr_capture.getvalue()
|
||||
assert text not in out, [text, out]
|
||||
|
||||
|
||||
|
|
|
@ -19,8 +19,8 @@ Feature: Upgrading Journals from 1.x.x to 2.x.x
|
|||
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"
|
||||
Then the output should contain "Password"
|
||||
and the output should contain "2013-06-10 15:40 Life is good"
|
||||
|
||||
Scenario: Upgrade and parse journals with little endian date format
|
||||
Given we use the config "upgrade_from_195_little_endian_dates.json"
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
#!/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
|
||||
|
@ -26,7 +24,7 @@ class DayOne(Journal.Journal):
|
|||
def __init__(self, **kwargs):
|
||||
self.entries = []
|
||||
self._deleted_entries = []
|
||||
super(DayOne, self).__init__(**kwargs)
|
||||
super().__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"))]
|
||||
|
@ -83,7 +81,7 @@ class DayOne(Journal.Journal):
|
|||
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])
|
||||
return "\n".join([f"# {e.uuid}\n{str(e)}" for e in self.entries])
|
||||
|
||||
def parse_editable_str(self, edited):
|
||||
"""Parses the output of self.editable_str and updates its entries."""
|
||||
|
@ -107,7 +105,7 @@ class DayOne(Journal.Journal):
|
|||
current_entry.modified = False
|
||||
current_entry.uuid = m.group(1).lower()
|
||||
else:
|
||||
date_blob_re = re.compile("^\[[^\\]]+\] ")
|
||||
date_blob_re = re.compile("^\\[[^\\]]+\\] ")
|
||||
date_blob = date_blob_re.findall(line)
|
||||
if date_blob:
|
||||
date_blob = date_blob[0]
|
||||
|
|
|
@ -8,14 +8,13 @@ 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)
|
||||
password = password.encode("utf-8")
|
||||
kdf = PBKDF2HMAC(
|
||||
algorithm=hashes.SHA256(),
|
||||
length=32,
|
||||
|
@ -30,7 +29,7 @@ def make_key(password):
|
|||
|
||||
class EncryptedJournal(Journal.Journal):
|
||||
def __init__(self, name='default', **kwargs):
|
||||
super(EncryptedJournal, self).__init__(name, **kwargs)
|
||||
super().__init__(name, **kwargs)
|
||||
self.config['encrypt'] = True
|
||||
|
||||
def open(self, filename=None):
|
||||
|
@ -48,9 +47,9 @@ class EncryptedJournal(Journal.Journal):
|
|||
self.config['password'] = password
|
||||
text = ""
|
||||
self._store(filename, text)
|
||||
util.prompt("[Journal '{0}' created at {1}]".format(self.name, filename))
|
||||
print(f"[Journal '{self.name}' created at {filename}]", file=sys.stderr)
|
||||
else:
|
||||
util.prompt("No password supplied for encrypted journal")
|
||||
print("No password supplied for encrypted journal", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
else:
|
||||
text = self._load(filename)
|
||||
|
@ -59,7 +58,6 @@ class EncryptedJournal(Journal.Journal):
|
|||
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
|
||||
|
@ -99,7 +97,7 @@ 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)
|
||||
super().__init__(name, **kwargs)
|
||||
self.config['encrypt'] = True
|
||||
|
||||
def _load(self, filename, password=None):
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
#!/usr/bin/env python
|
||||
# encoding: utf-8
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import re
|
||||
import textwrap
|
||||
from datetime import datetime
|
||||
|
@ -51,14 +49,14 @@ class Entry:
|
|||
|
||||
@staticmethod
|
||||
def tag_regex(tagsymbols):
|
||||
pattern = r'(?u)(?:^|\s)([{tags}][-+*#/\w]+)'.format(tags=tagsymbols)
|
||||
return re.compile(pattern, re.UNICODE)
|
||||
pattern = fr'(?u)(?:^|\s)([{tagsymbols}][-+*#/\w]+)'
|
||||
return re.compile(pattern)
|
||||
|
||||
def _parse_tags(self):
|
||||
tagsymbols = self.journal.config['tagsymbols']
|
||||
return set(tag.lower() for tag in re.findall(Entry.tag_regex(tagsymbols), self.text))
|
||||
return {tag.lower() for tag in re.findall(Entry.tag_regex(tagsymbols), self.text)}
|
||||
|
||||
def __unicode__(self):
|
||||
def __str__(self):
|
||||
"""Returns a string representation of the entry to be written into a journal file."""
|
||||
date_str = self.date.strftime(self.journal.config['timeformat'])
|
||||
title = "[{}] {}".format(date_str, self.title.rstrip("\n "))
|
||||
|
@ -106,7 +104,7 @@ class Entry:
|
|||
)
|
||||
|
||||
def __repr__(self):
|
||||
return "<Entry '{0}' on {1}>".format(self.title.strip(), self.date.strftime("%Y-%m-%d %H:%M"))
|
||||
return "<Entry '{}' on {}>".format(self.title.strip(), self.date.strftime("%Y-%m-%d %H:%M"))
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.__repr__())
|
||||
|
|
|
@ -1,13 +1,10 @@
|
|||
#!/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
|
||||
|
@ -15,7 +12,7 @@ import logging
|
|||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Tag(object):
|
||||
class Tag:
|
||||
def __init__(self, name, count=0):
|
||||
self.name = name
|
||||
self.count = count
|
||||
|
@ -24,10 +21,10 @@ class Tag(object):
|
|||
return self.name
|
||||
|
||||
def __repr__(self):
|
||||
return "<Tag '{}'>".format(self.name)
|
||||
return f"<Tag '{self.name}'>"
|
||||
|
||||
|
||||
class Journal(object):
|
||||
class Journal:
|
||||
def __init__(self, name='default', **kwargs):
|
||||
self.config = {
|
||||
'journal': "journal.txt",
|
||||
|
@ -72,7 +69,7 @@ class Journal(object):
|
|||
filename = filename or self.config['journal']
|
||||
|
||||
if not os.path.exists(filename):
|
||||
util.prompt("[Journal '{0}' created at {1}]".format(self.name, filename))
|
||||
print(f"[Journal '{self.name}' created at {filename}]", file=sys.stderr)
|
||||
self._create(filename)
|
||||
|
||||
text = self._load(filename)
|
||||
|
@ -96,7 +93,7 @@ class Journal(object):
|
|||
return True
|
||||
|
||||
def _to_text(self):
|
||||
return "\n".join([e.__unicode__() for e in self.entries])
|
||||
return "\n".join([str(e) for e in self.entries])
|
||||
|
||||
def _load(self, filename):
|
||||
raise NotImplementedError
|
||||
|
@ -118,7 +115,7 @@ class Journal(object):
|
|||
# Initialise our current entry
|
||||
entries = []
|
||||
|
||||
date_blob_re = re.compile("(?:^|\n)\[([^\\]]+)\] ")
|
||||
date_blob_re = re.compile("(?:^|\n)\\[([^\\]]+)\\] ")
|
||||
last_entry_pos = 0
|
||||
for match in date_blob_re.finditer(journal_txt):
|
||||
date_blob = match.groups()[0]
|
||||
|
@ -144,9 +141,6 @@ class Journal(object):
|
|||
entry._parse_text()
|
||||
return entries
|
||||
|
||||
def __unicode__(self):
|
||||
return self.pprint()
|
||||
|
||||
def pprint(self, short=False):
|
||||
"""Prettyprints the journal's entries"""
|
||||
sep = "\n"
|
||||
|
@ -157,7 +151,7 @@ class Journal(object):
|
|||
tagre = re.compile(re.escape(tag), re.IGNORECASE)
|
||||
pp = re.sub(tagre,
|
||||
lambda match: util.colorize(match.group(0)),
|
||||
pp, re.UNICODE)
|
||||
pp)
|
||||
else:
|
||||
pp = re.sub(
|
||||
Entry.Entry.tag_regex(self.config['tagsymbols']),
|
||||
|
@ -166,8 +160,11 @@ class Journal(object):
|
|||
)
|
||||
return pp
|
||||
|
||||
def __str__(self):
|
||||
return self.pprint()
|
||||
|
||||
def __repr__(self):
|
||||
return "<Journal with {0} entries>".format(len(self.entries))
|
||||
return f"<Journal with {len(self.entries)} entries>"
|
||||
|
||||
def sort(self):
|
||||
"""Sorts the Journal's entries by date"""
|
||||
|
@ -187,7 +184,7 @@ class Journal(object):
|
|||
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])
|
||||
tag_counts = {(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=[]):
|
||||
|
@ -204,8 +201,8 @@ class Journal(object):
|
|||
|
||||
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])
|
||||
self.search_tags = {tag.lower() for tag in tags}
|
||||
excluded_tags = {tag.lower() for tag in exclude}
|
||||
end_date = time.parse(end_date, inclusive=True)
|
||||
start_date = time.parse(start_date)
|
||||
|
||||
|
@ -230,7 +227,7 @@ class Journal(object):
|
|||
raw = raw.replace('\\n ', '\n').replace('\\n', '\n')
|
||||
starred = False
|
||||
# Split raw text into title and body
|
||||
sep = re.search("\n|[\?!.]+ +\n?", raw)
|
||||
sep = re.search(r"\n|[?!.]+ +\n?", raw)
|
||||
first_line = raw[:sep.end()].strip() if sep else raw
|
||||
starred = False
|
||||
|
||||
|
@ -258,7 +255,7 @@ class Journal(object):
|
|||
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])
|
||||
return "\n".join([str(e) for e in self.entries])
|
||||
|
||||
def parse_editable_str(self, edited):
|
||||
"""Parses the output of self.editable_str and updates it's entries."""
|
||||
|
@ -274,15 +271,15 @@ class Journal(object):
|
|||
class PlainJournal(Journal):
|
||||
@classmethod
|
||||
def _create(cls, filename):
|
||||
with codecs.open(filename, "a", "utf-8"):
|
||||
with open(filename, "a", encoding="utf-8"):
|
||||
pass
|
||||
|
||||
def _load(self, filename):
|
||||
with codecs.open(filename, "r", "utf-8") as f:
|
||||
with open(filename, "r", encoding="utf-8") as f:
|
||||
return f.read()
|
||||
|
||||
def _store(self, filename, text):
|
||||
with codecs.open(filename, 'w', "utf-8") as f:
|
||||
with open(filename, 'w', encoding="utf-8") as f:
|
||||
f.write(text)
|
||||
|
||||
|
||||
|
@ -291,7 +288,7 @@ class LegacyJournal(Journal):
|
|||
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:
|
||||
with open(filename, "r", encoding="utf-8") as f:
|
||||
return f.read()
|
||||
|
||||
def _parse(self, journal_txt):
|
||||
|
@ -327,7 +324,7 @@ class LegacyJournal(Journal):
|
|||
# escaping for the new format).
|
||||
line = new_date_format_regex.sub(r' \1', line)
|
||||
if current_entry:
|
||||
current_entry.text += line + u"\n"
|
||||
current_entry.text += line + "\n"
|
||||
|
||||
# Append last entry
|
||||
if current_entry:
|
||||
|
@ -351,8 +348,9 @@ def open_journal(name, config, legacy=False):
|
|||
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'])
|
||||
print(
|
||||
f"[Error: {config['journal']} is a directory, but doesn't seem to be a DayOne journal either.",
|
||||
file=sys.stderr
|
||||
)
|
||||
|
||||
sys.exit(1)
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
#!/usr/bin/env python
|
||||
# encoding: utf-8
|
||||
|
||||
import pkg_resources
|
||||
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
#!/usr/bin/env python
|
||||
# encoding: utf-8
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
from . import cli
|
||||
|
||||
|
||||
|
|
58
jrnl/cli.py
58
jrnl/cli.py
|
@ -1,5 +1,4 @@
|
|||
#!/usr/bin/env python
|
||||
# encoding: utf-8
|
||||
|
||||
"""
|
||||
jrnl
|
||||
|
@ -7,8 +6,6 @@
|
|||
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
|
||||
|
@ -91,7 +88,7 @@ def encrypt(journal, filename=None):
|
|||
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']))
|
||||
print("Journal encrypted to {}.".format(filename or new_journal.config['journal']), file=sys.stderr)
|
||||
|
||||
|
||||
def decrypt(journal, filename=None):
|
||||
|
@ -102,12 +99,12 @@ def decrypt(journal, filename=None):
|
|||
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']))
|
||||
print("Journal decrypted to {}.".format(filename or new_journal.config['journal']), file=sys.stderr)
|
||||
|
||||
|
||||
def list_journals(config):
|
||||
"""List the journals specified in the configuration file"""
|
||||
result = "Journals defined in {}\n".format(install.CONFIG_FILE_PATH)
|
||||
result = f"Journals defined in {install.CONFIG_FILE_PATH}\n"
|
||||
ml = min(max(len(k) for k in config['journals']), 20)
|
||||
for journal, cfg in config['journals'].items():
|
||||
result += " * {:{}} -> {}\n".format(journal, ml, cfg['journal'] if isinstance(cfg, dict) else cfg)
|
||||
|
@ -138,20 +135,19 @@ def configure_logger(debug=False):
|
|||
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))
|
||||
version_str = f"{jrnl.__title__} version {jrnl.__version__}"
|
||||
print(version_str)
|
||||
sys.exit(0)
|
||||
|
||||
try:
|
||||
config = install.load_or_install_jrnl()
|
||||
except UserAbort as err:
|
||||
util.prompt("\n{}".format(err))
|
||||
print(f"\n{err}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if args.ls:
|
||||
util.prnt(list_journals(config))
|
||||
print(list_journals(config))
|
||||
sys.exit(0)
|
||||
|
||||
log.debug('Using configuration "%s"', config)
|
||||
|
@ -161,11 +157,11 @@ def run(manual_args=None):
|
|||
# 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':
|
||||
if journal_name != 'default':
|
||||
args.text = args.text[1:]
|
||||
elif "default" not in config['journals']:
|
||||
util.prompt("No default journal configured.")
|
||||
util.prompt(list_journals(config))
|
||||
print("No default journal configured.", file=sys.stderr)
|
||||
print(list_journals(config), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
config = util.scope_config(config, journal_name)
|
||||
|
@ -175,7 +171,7 @@ def run(manual_args=None):
|
|||
try:
|
||||
args.limit = int(args.text[0].lstrip("-"))
|
||||
args.text = args.text[1:]
|
||||
except:
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
log.debug('Using journal "%s"', journal_name)
|
||||
|
@ -190,21 +186,22 @@ def run(manual_args=None):
|
|||
if mode_compose and not args.text:
|
||||
if not sys.stdin.isatty():
|
||||
# Piping data into jrnl
|
||||
raw = util.py23_read()
|
||||
raw = sys.stdin.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']))
|
||||
except OSError:
|
||||
print(f"[Could not read template at '{config['template']}']", file=sys.stderr)
|
||||
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")
|
||||
print("[Compose Entry; " + _exit_multiline_code + " to finish writing]\n", file=sys.stderr)
|
||||
raw = sys.stdin.read()
|
||||
except KeyboardInterrupt:
|
||||
util.prompt("[Entry NOT saved to journal.]")
|
||||
print("[Entry NOT saved to journal.]", file=sys.stderr)
|
||||
sys.exit(0)
|
||||
if raw:
|
||||
args.text = [raw]
|
||||
|
@ -215,7 +212,7 @@ def run(manual_args=None):
|
|||
try:
|
||||
journal = Journal.open_journal(journal_name, config)
|
||||
except KeyboardInterrupt:
|
||||
util.prompt("[Interrupted while opening journal]".format(journal_name))
|
||||
print(f"[Interrupted while opening journal]", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Import mode
|
||||
|
@ -225,11 +222,9 @@ def run(manual_args=None):
|
|||
# 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))
|
||||
print(f"[Entry added to {journal_name} journal]", file=sys.stderr)
|
||||
journal.write()
|
||||
|
||||
if not mode_compose:
|
||||
|
@ -246,14 +241,14 @@ def run(manual_args=None):
|
|||
|
||||
# Reading mode
|
||||
if not mode_compose and not mode_export and not mode_import:
|
||||
print(util.py2encode(journal.pprint()))
|
||||
print(journal.pprint())
|
||||
|
||||
# Various export modes
|
||||
elif args.short:
|
||||
print(util.py2encode(journal.pprint(short=True)))
|
||||
print(journal.pprint(short=True))
|
||||
|
||||
elif args.tags:
|
||||
print(util.py2encode(plugins.get_exporter("tags").export(journal)))
|
||||
print(plugins.get_exporter("tags").export(journal))
|
||||
|
||||
elif args.export is not False:
|
||||
exporter = plugins.get_exporter(args.export)
|
||||
|
@ -275,7 +270,8 @@ def run(manual_args=None):
|
|||
|
||||
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))
|
||||
print("[{1}ERROR{2}: You need to specify an editor in {0} to use the --edit function.]"
|
||||
.format(install.CONFIG_FILE_PATH, ERROR_COLOR, RESET_COLOR), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
other_entries = [e for e in old_entries if e not in journal.entries]
|
||||
# Edit
|
||||
|
@ -286,11 +282,11 @@ def run(manual_args=None):
|
|||
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"))
|
||||
prompts.append("{} {} 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"))
|
||||
prompts.append("{} {} modified".format(num_edited, "entry" if num_deleted == 1 else "entries"))
|
||||
if prompts:
|
||||
util.prompt("[{0}]".format(", ".join(prompts).capitalize()))
|
||||
print("[{}]".format(", ".join(prompts).capitalize()), file=sys.stderr)
|
||||
journal.entries += other_entries
|
||||
journal.sort()
|
||||
journal.write()
|
||||
|
|
|
@ -1,15 +1,12 @@
|
|||
#!/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
|
||||
from .util import slugify
|
||||
from .plugins.template import Template
|
||||
import os
|
||||
import codecs
|
||||
|
||||
|
||||
class Exporter(object):
|
||||
class Exporter:
|
||||
"""This Exporter can convert entries and journals into text files."""
|
||||
def __init__(self, format):
|
||||
with open("jrnl/templates/" + format + ".template") as f:
|
||||
|
@ -17,8 +14,8 @@ class Exporter(object):
|
|||
self.template = Template(body)
|
||||
|
||||
def export_entry(self, entry):
|
||||
"""Returns a unicode representation of a single entry."""
|
||||
return entry.__unicode__()
|
||||
"""Returns a string representation of a single entry."""
|
||||
return str(entry)
|
||||
|
||||
def _get_vars(self, journal):
|
||||
return {
|
||||
|
@ -28,36 +25,36 @@ class Exporter(object):
|
|||
}
|
||||
|
||||
def export_journal(self, journal):
|
||||
"""Returns a unicode representation of an entire journal."""
|
||||
"""Returns a string 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:
|
||||
with open(path, "w", encoding="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)
|
||||
return f"[Journal exported to {path}]"
|
||||
except OSError as e:
|
||||
return f"[{ERROR_COLOR}ERROR{RESET_COLOR}: {e.filename} {e.strerror}]"
|
||||
|
||||
def make_filename(self, entry):
|
||||
return entry.date.strftime("%Y-%m-%d_{0}.{1}".format(slugify(u(entry.title)), self.extension))
|
||||
return entry.date.strftime("%Y-%m-%d_{}.{}".format(slugify(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:
|
||||
with open(full_path, "w", encoding="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)
|
||||
except OSError as e:
|
||||
return f"[{ERROR_COLOR}ERROR{RESET_COLOR}: {e.filename} {e.strerror}]"
|
||||
return f"[Journal exported to {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."""
|
||||
representation as string if output is None."""
|
||||
if output and os.path.isdir(output): # multiple files
|
||||
return self.write_files(journal, output)
|
||||
elif output: # single file
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
#!/usr/bin/env python
|
||||
# encoding: utf-8
|
||||
|
||||
from __future__ import absolute_import
|
||||
import readline
|
||||
import glob
|
||||
import getpass
|
||||
|
@ -69,7 +67,7 @@ def upgrade_config(config):
|
|||
for key in missing_keys:
|
||||
config[key] = default_config[key]
|
||||
save_config(config)
|
||||
print("[Configuration updated to newest version at {}]".format(CONFIG_FILE_PATH))
|
||||
print(f"[Configuration updated to newest version at {CONFIG_FILE_PATH}]", file=sys.stderr)
|
||||
|
||||
|
||||
def save_config(config):
|
||||
|
@ -91,10 +89,10 @@ def load_or_install_jrnl():
|
|||
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.")
|
||||
print("Aborting upgrade.", file=sys.stderr)
|
||||
print("Please tell us about this problem at the following URL:", file=sys.stderr)
|
||||
print("https://github.com/jrnl-org/jrnl/issues/new?title=UpgradeValidationException", file=sys.stderr)
|
||||
print("Exiting.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
upgrade_config(config)
|
||||
|
@ -120,8 +118,8 @@ def install():
|
|||
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
|
||||
path_query = f'Path to your journal file (leave blank for {JOURNAL_FILE_PATH}): '
|
||||
journal_path = input(path_query).strip() or JOURNAL_FILE_PATH
|
||||
default_config['journals']['default'] = os.path.expanduser(os.path.expandvars(journal_path))
|
||||
|
||||
path = os.path.split(default_config['journals']['default'])[0] # If the folder doesn't exist, create it
|
||||
|
@ -139,7 +137,7 @@ def install():
|
|||
else:
|
||||
util.set_keychain("default", None)
|
||||
EncryptedJournal._create(default_config['journals']['default'], password)
|
||||
print("Journal will be encrypted.")
|
||||
print("Journal will be encrypted.", file=sys.stderr)
|
||||
else:
|
||||
PlainJournal._create(default_config['journals']['default'])
|
||||
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
#!/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
|
||||
|
@ -15,8 +13,8 @@ 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])
|
||||
__exporter_types = {name: plugin for plugin in __exporters for name in plugin.names}
|
||||
__importer_types = {name: plugin for plugin in __importers for name in plugin.names}
|
||||
|
||||
EXPORT_FORMATS = sorted(__exporter_types.keys())
|
||||
IMPORT_FORMATS = sorted(__importer_types.keys())
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
#!/usr/bin/env python
|
||||
# encoding: utf-8
|
||||
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
import codecs
|
||||
import sys
|
||||
from .. import util
|
||||
|
||||
class JRNLImporter(object):
|
||||
class JRNLImporter:
|
||||
"""This plugin imports entries from other jrnl files."""
|
||||
names = ["jrnl"]
|
||||
|
||||
|
@ -17,15 +15,15 @@ class JRNLImporter(object):
|
|||
old_cnt = len(journal.entries)
|
||||
old_entries = journal.entries
|
||||
if input:
|
||||
with codecs.open(input, "r", "utf-8") as f:
|
||||
with open(input, "r", encoding="utf-8") as f:
|
||||
other_journal_txt = f.read()
|
||||
else:
|
||||
try:
|
||||
other_journal_txt = util.py23_read()
|
||||
other_journal_txt = sys.stdin.read()
|
||||
except KeyboardInterrupt:
|
||||
util.prompt("[Entries NOT imported into journal.]")
|
||||
print("[Entries NOT imported into journal.]", file=sys.stderr)
|
||||
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))
|
||||
print("[{} imported to {} journal]".format(new_cnt - old_cnt, journal.name), file=sys.stderr)
|
||||
journal.write()
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
#!/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
|
||||
|
@ -35,7 +34,7 @@ class JSONExporter(TextExporter):
|
|||
"""Returns a json representation of an entire journal."""
|
||||
tags = get_tags_count(journal)
|
||||
result = {
|
||||
"tags": dict((tag, count) for count, tag in tags),
|
||||
"tags": {tag: count for count, tag in tags},
|
||||
"entries": [cls.entry_to_dict(e) for e in journal.entries]
|
||||
}
|
||||
return json.dumps(result, indent=2)
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
#!/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
|
||||
|
@ -51,15 +50,11 @@ class MarkdownExporter(TextExporter):
|
|||
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)
|
||||
print(f"{WARNING_COLOR}WARNING{RESET_COLOR}: "
|
||||
f"Headings increased past H6 on export - {date_str} {entry.title}",
|
||||
file=sys.stderr)
|
||||
|
||||
return "{md} {date} {title}\n{body} {space}".format(
|
||||
md=heading,
|
||||
date=date_str,
|
||||
title=entry.title,
|
||||
body=newbody,
|
||||
space=""
|
||||
)
|
||||
return f"{heading} {date_str} {entry.title}\n{newbody} "
|
||||
|
||||
@classmethod
|
||||
def export_journal(cls, journal):
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
#!/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
|
||||
|
||||
|
@ -26,5 +25,5 @@ class TagExporter(TextExporter):
|
|||
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))
|
||||
result += "\n".join("{:20} : {}".format(tag, n) for n, tag in sorted(tag_counts, reverse=True))
|
||||
return result
|
||||
|
|
|
@ -13,7 +13,7 @@ BLOCK_RE = r"{% *block +(.+?) *%}((?:.|\n)+?){% *endblock *%}"
|
|||
INCLUDE_RE = r"{% *include +(.+?) *%}"
|
||||
|
||||
|
||||
class Template(object):
|
||||
class Template:
|
||||
def __init__(self, template):
|
||||
self.template = template
|
||||
self.clean_template = None
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
#!/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
|
||||
|
@ -14,7 +12,7 @@ class GenericTemplateExporter(TextExporter):
|
|||
|
||||
@classmethod
|
||||
def export_entry(cls, entry):
|
||||
"""Returns a unicode representation of a single entry."""
|
||||
"""Returns a string representation of a single entry."""
|
||||
vars = {
|
||||
'entry': entry,
|
||||
'tags': entry.tags
|
||||
|
@ -23,7 +21,7 @@ class GenericTemplateExporter(TextExporter):
|
|||
|
||||
@classmethod
|
||||
def export_journal(cls, journal):
|
||||
"""Returns a unicode representation of an entire journal."""
|
||||
"""Returns a string representation of an entire journal."""
|
||||
vars = {
|
||||
'journal': journal,
|
||||
'entries': journal.entries,
|
||||
|
@ -36,7 +34,7 @@ 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, ), {
|
||||
return type(str(f"{name.title()}Exporter"), (GenericTemplateExporter, ), {
|
||||
"names": [name],
|
||||
"extension": template.extension,
|
||||
"template": template
|
||||
|
|
|
@ -1,41 +1,39 @@
|
|||
#!/usr/bin/env python
|
||||
# encoding: utf-8
|
||||
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
import codecs
|
||||
from ..util import u, slugify
|
||||
from ..util import slugify
|
||||
import os
|
||||
from ..util import ERROR_COLOR, RESET_COLOR
|
||||
|
||||
|
||||
class TextExporter(object):
|
||||
class TextExporter:
|
||||
"""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__()
|
||||
"""Returns a string representation of a single entry."""
|
||||
return str(entry)
|
||||
|
||||
@classmethod
|
||||
def export_journal(cls, journal):
|
||||
"""Returns a unicode representation of an entire journal."""
|
||||
"""Returns a string representation of an entire journal."""
|
||||
return "\n".join(cls.export_entry(entry) for entry in journal)
|
||||
|
||||
@classmethod
|
||||
def write_file(cls, journal, path):
|
||||
"""Exports a journal into a single file."""
|
||||
try:
|
||||
with codecs.open(path, "w", "utf-8") as f:
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
f.write(cls.export_journal(journal))
|
||||
return "[Journal exported to {0}]".format(path)
|
||||
return f"[Journal exported to {path}]"
|
||||
except IOError as e:
|
||||
return "[{2}ERROR{3}: {0} {1}]".format(e.filename, e.strerror, ERROR_COLOR, RESET_COLOR)
|
||||
return f"[{ERROR_COLOR}ERROR{RESET_COLOR}: {e.filename} {e.strerror}]"
|
||||
|
||||
@classmethod
|
||||
def make_filename(cls, entry):
|
||||
return entry.date.strftime("%Y-%m-%d_{0}.{1}".format(slugify(u(entry.title)), cls.extension))
|
||||
return entry.date.strftime("%Y-%m-%d_{}.{}".format(slugify(str(entry.title)), cls.extension))
|
||||
|
||||
@classmethod
|
||||
def write_files(cls, journal, path):
|
||||
|
@ -43,17 +41,17 @@ class TextExporter(object):
|
|||
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:
|
||||
with open(full_path, "w", encoding="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)
|
||||
return "[Journal exported to {}]".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."""
|
||||
representation as string if output is None."""
|
||||
if output and os.path.isdir(output): # multiple files
|
||||
return cls.write_files(journal, output)
|
||||
elif output: # single file
|
||||
|
|
|
@ -10,7 +10,7 @@ def get_tags_count(journal):
|
|||
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])
|
||||
tag_counts = {(tags.count(tag), tag) for tag in tags}
|
||||
return tag_counts
|
||||
|
||||
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
#!/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
|
||||
|
||||
|
||||
|
@ -20,7 +18,7 @@ class XMLExporter(JSONExporter):
|
|||
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)))
|
||||
elem.appendChild(doc_el.createTextNode(value))
|
||||
entry_el.appendChild(elem)
|
||||
if not doc:
|
||||
doc_el.appendChild(entry_el)
|
||||
|
@ -33,8 +31,8 @@ class XMLExporter(JSONExporter):
|
|||
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.setAttribute('uuid', entry.uuid)
|
||||
entry_el.setAttribute('starred', entry.starred)
|
||||
entry_el.appendChild(doc.createTextNode(entry.fulltext))
|
||||
return entry_el
|
||||
|
||||
|
@ -49,7 +47,7 @@ class XMLExporter(JSONExporter):
|
|||
for count, tag in tags:
|
||||
tag_el = doc.createElement('tag')
|
||||
tag_el.setAttribute('name', tag)
|
||||
count_node = doc.createTextNode(u(count))
|
||||
count_node = doc.createTextNode(str(count))
|
||||
tag_el.appendChild(count_node)
|
||||
tags_el.appendChild(tag_el)
|
||||
for entry in journal.entries:
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
#!/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
|
||||
|
@ -18,7 +17,8 @@ class YAMLExporter(TextExporter):
|
|||
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))
|
||||
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'])
|
||||
|
@ -27,7 +27,7 @@ class YAMLExporter(TextExporter):
|
|||
|
||||
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)
|
||||
multi_tag_regex = re.compile(r'(?u)^\s*([{tags}][-+*#/\w]+\s*)+$'.format(tags=tagsymbols))
|
||||
|
||||
'''Increase heading levels in body text'''
|
||||
newbody = ''
|
||||
|
|
|
@ -51,7 +51,7 @@ def parse(date_str, inclusive=False, default_hour=None, default_minute=None):
|
|||
except TypeError:
|
||||
return None
|
||||
|
||||
if flag is 1: # Date found, but no time. Use the default time.
|
||||
if flag == 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])
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from __future__ import absolute_import, unicode_literals
|
||||
import sys
|
||||
|
||||
from . import __version__
|
||||
from . import Journal
|
||||
|
@ -6,11 +6,10 @@ 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))
|
||||
print(f" Created a backup at {filename}.backup", file=sys.stderr)
|
||||
filename = os.path.expanduser(os.path.expandvars(filename))
|
||||
with open(filename, 'rb' if binary else 'r') as original:
|
||||
contents = original.read()
|
||||
|
@ -19,14 +18,14 @@ def backup(filename, binary=False):
|
|||
|
||||
|
||||
def upgrade_jrnl_if_necessary(config_path):
|
||||
with codecs.open(config_path, "r", "utf-8") as f:
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
config_file = f.read()
|
||||
if not config_file.strip().startswith("{"):
|
||||
return
|
||||
|
||||
config = util.load_config(config_path)
|
||||
|
||||
util.prompt("""Welcome to jrnl {}.
|
||||
print("""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
|
||||
|
@ -54,6 +53,8 @@ older versions of jrnl anymore.
|
|||
encrypt = config.get('encrypt')
|
||||
path = journal_conf
|
||||
|
||||
path = os.path.expanduser(path)
|
||||
|
||||
if encrypt:
|
||||
encrypted_journals[journal_name] = path
|
||||
elif os.path.isdir(path):
|
||||
|
@ -63,19 +64,19 @@ older versions of jrnl anymore.
|
|||
|
||||
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__))
|
||||
print(f"\nFollowing encrypted journals will be upgraded to jrnl {__version__}:", file=sys.stderr)
|
||||
for journal, path in encrypted_journals.items():
|
||||
util.prompt(" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name))
|
||||
print(" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name), file=sys.stderr)
|
||||
|
||||
if plain_journals:
|
||||
util.prompt("\nFollowing plain text journals will upgraded to jrnl {}:".format(__version__))
|
||||
print(f"\nFollowing plain text journals will upgraded to jrnl {__version__}:", file=sys.stderr)
|
||||
for journal, path in plain_journals.items():
|
||||
util.prompt(" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name))
|
||||
print(" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name), file=sys.stderr)
|
||||
|
||||
if other_journals:
|
||||
util.prompt("\nFollowing journals will be not be touched:")
|
||||
print("\nFollowing journals will be not be touched:", file=sys.stderr)
|
||||
for journal, path in other_journals.items():
|
||||
util.prompt(" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name))
|
||||
print(" {:{pad}} -> {}".format(journal, path, pad=longest_journal_name), file=sys.stderr)
|
||||
|
||||
try:
|
||||
cont = util.yesno("\nContinue upgrading jrnl?", default=False)
|
||||
|
@ -85,13 +86,13 @@ older versions of jrnl anymore.
|
|||
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))
|
||||
print(f"\nUpgrading encrypted '{journal_name}' journal stored in {path}...", file=sys.stderr)
|
||||
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))
|
||||
print(f"\nUpgrading plain text '{journal_name}' journal stored in {path}...", file=sys.stderr)
|
||||
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))
|
||||
|
@ -100,8 +101,9 @@ older versions of jrnl anymore.
|
|||
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))
|
||||
print("\nThe following journal{} failed to upgrade:\n{}".format(
|
||||
's' if len(failed_journals) > 1 else '', "\n".join(j.name for j in failed_journals)),
|
||||
file=sys.stderr
|
||||
)
|
||||
|
||||
raise UpgradeValidationException
|
||||
|
@ -110,10 +112,11 @@ older versions of jrnl anymore.
|
|||
for j in all_journals:
|
||||
j.write()
|
||||
|
||||
util.prompt("\nUpgrading config...")
|
||||
print("\nUpgrading config...", file=sys.stderr)
|
||||
backup(config_path)
|
||||
|
||||
util.prompt("\nWe're all done here and you can start enjoying jrnl 2.".format(config_path))
|
||||
print("\nWe're all done here and you can start enjoying jrnl 2.", file=sys.stderr)
|
||||
|
||||
|
||||
class UpgradeValidationException(Exception):
|
||||
"""Raised when the contents of an upgraded journal do not match the old journal"""
|
||||
|
|
109
jrnl/util.py
109
jrnl/util.py
|
@ -1,8 +1,4 @@
|
|||
#!/usr/bin/env python
|
||||
# encoding: utf-8
|
||||
|
||||
from __future__ import unicode_literals
|
||||
from __future__ import absolute_import
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
@ -14,22 +10,12 @@ if "win32" in sys.platform:
|
|||
import re
|
||||
import tempfile
|
||||
import subprocess
|
||||
import codecs
|
||||
import unicodedata
|
||||
import shlex
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
PY3 = sys.version_info[0] == 3
|
||||
PY2 = sys.version_info[0] == 2
|
||||
STDIN = sys.stdin
|
||||
STDERR = sys.stderr
|
||||
STDOUT = sys.stdout
|
||||
TEST = False
|
||||
__cached_tz = None
|
||||
|
||||
WARNING_COLOR = "\033[33m"
|
||||
ERROR_COLOR = "\033[31m"
|
||||
RESET_COLOR = "\033[0m"
|
||||
|
@ -44,18 +30,14 @@ SENTENCE_SPLITTER = re.compile(r"""
|
|||
\s+ # a sequence of required spaces.
|
||||
| # Otherwise,
|
||||
\n # a sentence also terminates newlines.
|
||||
)""", re.UNICODE | re.VERBOSE)
|
||||
)""", re.VERBOSE)
|
||||
|
||||
|
||||
class UserAbort(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def getpass(prompt="Password: "):
|
||||
if not TEST:
|
||||
return gp.getpass(bytes(prompt))
|
||||
else:
|
||||
return py23_input(prompt)
|
||||
getpass = gp.getpass
|
||||
|
||||
|
||||
def get_password(validator, keychain=None, max_attempts=3):
|
||||
|
@ -67,14 +49,14 @@ def get_password(validator, keychain=None, max_attempts=3):
|
|||
set_keychain(keychain, None)
|
||||
attempt = 1
|
||||
while result is None and attempt < max_attempts:
|
||||
prompt("Wrong password, try again.")
|
||||
password = getpass()
|
||||
print("Wrong password, try again.", file=sys.stderr)
|
||||
password = gp.getpass()
|
||||
result = validator(password)
|
||||
attempt += 1
|
||||
if result is not None:
|
||||
return result
|
||||
else:
|
||||
prompt("Extremely wrong password.")
|
||||
print("Extremely wrong password.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
|
@ -88,57 +70,16 @@ def set_keychain(journal_name, password):
|
|||
if password is None:
|
||||
try:
|
||||
keyring.delete_password('jrnl', journal_name)
|
||||
except:
|
||||
except RuntimeError:
|
||||
pass
|
||||
elif not TEST:
|
||||
else:
|
||||
keyring.set_password('jrnl', journal_name, password)
|
||||
|
||||
|
||||
def u(s):
|
||||
"""Mock unicode function for python 2 and 3 compatibility."""
|
||||
if not isinstance(s, str):
|
||||
s = str(s)
|
||||
return s if PY3 or type(s) is unicode else s.decode("utf-8")
|
||||
|
||||
|
||||
def py2encode(s):
|
||||
"""Encodes to UTF-8 in Python 2 but not in Python 3."""
|
||||
return s.encode("utf-8") if PY2 and type(s) is unicode else s
|
||||
|
||||
|
||||
def bytes(s):
|
||||
"""Returns bytes, no matter what."""
|
||||
if PY3:
|
||||
return s.encode("utf-8") if type(s) is not bytes else s
|
||||
return s.encode("utf-8") if type(s) is unicode else s
|
||||
|
||||
|
||||
def prnt(s):
|
||||
"""Encode and print a string"""
|
||||
STDOUT.write(u(s + "\n"))
|
||||
|
||||
|
||||
def prompt(msg):
|
||||
"""Prints a message to the std err stream defined in util."""
|
||||
if not msg.endswith("\n"):
|
||||
msg += "\n"
|
||||
STDERR.write(u(msg))
|
||||
|
||||
|
||||
def py23_input(msg=""):
|
||||
prompt(msg)
|
||||
return STDIN.readline().strip()
|
||||
|
||||
|
||||
def py23_read(msg=""):
|
||||
print(msg)
|
||||
return STDIN.read()
|
||||
|
||||
|
||||
def yesno(prompt, default=True):
|
||||
prompt = prompt.strip() + (" [Y/n]" if default else " [y/N]")
|
||||
raw = py23_input(prompt)
|
||||
return {'y': True, 'n': False}.get(raw.lower(), default)
|
||||
prompt = f"{prompt.strip()} {'[Y/n]' if default else '[y/N]'} "
|
||||
response = input(prompt)
|
||||
return {"y": True, "n": False}.get(response.lower(), default)
|
||||
|
||||
|
||||
def load_config(config_path):
|
||||
|
@ -164,51 +105,35 @@ def scope_config(config, journal_name):
|
|||
|
||||
def get_text_from_editor(config, template=""):
|
||||
filehandle, tmpfile = tempfile.mkstemp(prefix="jrnl", text=True, suffix=".txt")
|
||||
with codecs.open(tmpfile, 'w', "utf-8") as f:
|
||||
with open(tmpfile, 'w', encoding="utf-8") as f:
|
||||
if template:
|
||||
f.write(template)
|
||||
try:
|
||||
subprocess.call(shlex.split(config['editor'], posix="win" not in sys.platform) + [tmpfile])
|
||||
except AttributeError:
|
||||
subprocess.call(config['editor'] + [tmpfile])
|
||||
with codecs.open(tmpfile, "r", "utf-8") as f:
|
||||
with open(tmpfile, "r", encoding="utf-8") as f:
|
||||
raw = f.read()
|
||||
os.close(filehandle)
|
||||
os.remove(tmpfile)
|
||||
if not raw:
|
||||
prompt('[Nothing saved to file]')
|
||||
print('[Nothing saved to file]', file=sys.stderr)
|
||||
return raw
|
||||
|
||||
|
||||
def colorize(string):
|
||||
"""Returns the string wrapped in cyan ANSI escape"""
|
||||
return u"\033[36m{}\033[39m".format(string)
|
||||
return f"\033[36m{string}\033[39m"
|
||||
|
||||
|
||||
def slugify(string):
|
||||
"""Slugifies a string.
|
||||
Based on public domain code from https://github.com/zacharyvoase/slugify
|
||||
and ported to deal with all kinds of python 2 and 3 strings
|
||||
"""
|
||||
string = u(string)
|
||||
ascii_string = str(unicodedata.normalize('NFKD', string).encode('ascii', 'ignore'))
|
||||
if PY3:
|
||||
ascii_string = ascii_string[1:] # removed the leading 'b'
|
||||
no_punctuation = re.sub(r'[^\w\s-]', '', ascii_string).strip().lower()
|
||||
normalized_string = str(unicodedata.normalize('NFKD', string))
|
||||
no_punctuation = re.sub(r'[^\w\s-]', '', normalized_string).strip().lower()
|
||||
slug = re.sub(r'[-\s]+', '-', no_punctuation)
|
||||
return u(slug)
|
||||
|
||||
|
||||
def int2byte(i):
|
||||
"""Converts an integer to a byte.
|
||||
This is equivalent to chr() in Python 2 and bytes((i,)) in Python 3."""
|
||||
return chr(i) if PY2 else bytes((i,))
|
||||
|
||||
|
||||
def byte2int(b):
|
||||
"""Converts a byte to an integer.
|
||||
This is equivalent to ord(bs[0]) on Python 2 and bs[0] on Python 3."""
|
||||
return ord(b)if PY2 else b
|
||||
return slug
|
||||
|
||||
|
||||
def split_title(text):
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[tool.poetry]
|
||||
name = "jrnl"
|
||||
version = "0.0.0-source"
|
||||
version = "v2.1.post2"
|
||||
description = "Collect your thoughts and notes without leaving the command line."
|
||||
authors = [
|
||||
"Manuel Ebert <manuel@1450.me>",
|
||||
|
|
Loading…
Add table
Reference in a new issue