mirror of
https://github.com/jrnl-org/jrnl.git
synced 2025-05-10 16:48:31 +02:00
Merge pull request #1 from maebert/master
i think i'm re-syncing all the updates from maebrt:master back to alapolloni:master.
This commit is contained in:
commit
5daf8f010a
12 changed files with 220 additions and 106 deletions
|
@ -1,7 +1,13 @@
|
||||||
language: python
|
language: python
|
||||||
python:
|
python:
|
||||||
|
- "2.6"
|
||||||
- "2.7"
|
- "2.7"
|
||||||
# - "3.2"
|
- "3.2"
|
||||||
|
- "3.3"
|
||||||
install: "pip install -r requirements.txt --use-mirrors"
|
install: "pip install -r requirements.txt --use-mirrors"
|
||||||
# command to run tests
|
# command to run tests
|
||||||
script: nosetests
|
script: nosetests
|
||||||
|
matrix:
|
||||||
|
allow_failures: # python 3 support for travis is shaky....
|
||||||
|
- python: 3.2
|
||||||
|
- python: 3.3
|
||||||
|
|
31
CHANGELOG.md
31
CHANGELOG.md
|
@ -1,7 +1,32 @@
|
||||||
Changelog
|
Changelog
|
||||||
=========
|
=========
|
||||||
|
|
||||||
### 1.0.1 (March 12, 2013)
|
#### 1.1.0
|
||||||
|
|
||||||
|
* [New] JSON export exports tags as well.
|
||||||
|
* [Improved] Nicer error message when there is a syntactical error in your config file.
|
||||||
|
* [Improved] Unicode support
|
||||||
|
|
||||||
|
#### 1.0.5
|
||||||
|
|
||||||
|
* [Improved] Backwards compatibility with `parsedatetime` 0.8.7
|
||||||
|
|
||||||
|
#### 1.0.4
|
||||||
|
|
||||||
|
* [Improved] Python 2.6 compatibility
|
||||||
|
* [Improved] Better utf-8 support
|
||||||
|
* [New] Python 3 compatibility
|
||||||
|
* [New] Respects the `XDG_CONFIG_HOME` environment variable for storing your configuration file (Thanks [evaryont](https://github.com/evaryont))
|
||||||
|
|
||||||
|
#### 1.0.3 (April 17, 2013)
|
||||||
|
|
||||||
|
* [Improved] 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
|
* [Fixed] Requires parsedatetime 1.1.2 or newer
|
||||||
|
|
||||||
|
@ -13,11 +38,11 @@ Changelog
|
||||||
* [Fixed] A bug where jrnl would not add entries without timestamp
|
* [Fixed] A bug where jrnl would not add entries without timestamp
|
||||||
* [Fixed] Support for parsedatetime 1.x
|
* [Fixed] Support for parsedatetime 1.x
|
||||||
|
|
||||||
### 0.3.2 (July 5, 2012)
|
#### 0.3.2 (July 5, 2012)
|
||||||
|
|
||||||
* [Improved] Converts `\n` to new lines (if using directly on a command line, make sure to wrap your entry with quotes).
|
* [Improved] Converts `\n` to new lines (if using directly on a command line, make sure to wrap your entry with quotes).
|
||||||
|
|
||||||
### 0.3.1 (June 16, 2012)
|
#### 0.3.1 (June 16, 2012)
|
||||||
|
|
||||||
* [Improved] Supports deleting of last entry.
|
* [Improved] Supports deleting of last entry.
|
||||||
* [Fixed] Fixes a bug where --encrypt or --decrypt without a target file would not work.
|
* [Fixed] Fixes a bug where --encrypt or --decrypt without a target file would not work.
|
||||||
|
|
21
README.md
21
README.md
|
@ -1,4 +1,4 @@
|
||||||
jrnl
|
jrnl [](https://travis-ci.org/maebert/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* 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.
|
||||||
|
@ -33,7 +33,11 @@ Install _jrnl_ using pip:
|
||||||
|
|
||||||
pip install jrnl
|
pip install jrnl
|
||||||
|
|
||||||
Alternatively, install manually by cloning the repository:
|
Or, if you want the option to encrypt your journal,
|
||||||
|
|
||||||
|
pip install jrnl[encrypted]
|
||||||
|
|
||||||
|
To install `pycrypto` as well (Note: this requires a `gcc` compiler. You can also [install PyCyrypto manually](https://www.dlitz.net/software/pycrypto/) first)). Alternatively, install _jrnl_ manually by cloning the repository:
|
||||||
|
|
||||||
git clone git://github.com/maebert/jrnl.git
|
git clone git://github.com/maebert/jrnl.git
|
||||||
cd jrnl
|
cd jrnl
|
||||||
|
@ -44,7 +48,7 @@ The first time you run `jrnl` you will be asked where your journal file should b
|
||||||
Usage
|
Usage
|
||||||
-----
|
-----
|
||||||
|
|
||||||
_jrnl_ has to modes: __composing__ and __viewing__.
|
_jrnl_ has two modes: __composing__ and __viewing__.
|
||||||
|
|
||||||
### Viewing:
|
### Viewing:
|
||||||
|
|
||||||
|
@ -104,7 +108,7 @@ With
|
||||||
|
|
||||||
jrnl --tags
|
jrnl --tags
|
||||||
|
|
||||||
you'll get a list of all tags you used in your journal, sorted by most frequent. Tags occuring several times in the same entry are only counted as one.
|
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.
|
||||||
|
|
||||||
### JSON export
|
### JSON export
|
||||||
|
|
||||||
|
@ -137,7 +141,7 @@ will replace your encrypted journal file by a Journal in plain text. You can als
|
||||||
Advanced usages
|
Advanced usages
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
The first time launched, _jrnl_ will create a file called `.jrnl_config` in your home directory.
|
The first time launched, _jrnl_ will create a file configuration file at `~/.jrnl_config` or, if the `XDG_CONFIG_HOME` environment variable is set, `$XDG_CONFIG_HOME/jrnl`.
|
||||||
|
|
||||||
### .jrnl_config
|
### .jrnl_config
|
||||||
|
|
||||||
|
@ -150,7 +154,7 @@ The configuration file is a simple JSON file with the following options.
|
||||||
- `tagsymbols`: Symbols to be interpreted as tags. (__See note below__)
|
- `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
|
- `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
|
- `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` and you have [clint](http://www.nicosphere.net/clint-command-line-library-for-python/) installed, tags will be highlighted in cyan.
|
- `highlight`: if `true`, tags will be highlighted in cyan.
|
||||||
- `linewrap`: controls the width of the output. Set to `0` or `false` if you don't want to wrap long lines.
|
- `linewrap`: controls the width of the output. Set to `0` or `false` if you don't want to wrap long lines.
|
||||||
|
|
||||||
> __Note on `tagsymbols`:__ 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
|
> __Note on `tagsymbols`:__ 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
|
||||||
|
@ -168,7 +172,7 @@ The configuration file is a simple JSON file with the following options.
|
||||||
Using your DayOne journal instead of a flat text file is dead simple - instead of pointing to a text file, set the `"journal"` key in your `.jrnl_conf` to point to your DayOne journal. This is a folder ending with `.dayone`, and it's located at
|
Using your DayOne journal instead of a flat text file is dead simple - instead of pointing to a text file, set the `"journal"` key in your `.jrnl_conf` to point to your DayOne journal. This is a folder ending with `.dayone`, and it's located at
|
||||||
|
|
||||||
* `~/Library/Application Support/Day One/` by default
|
* `~/Library/Application Support/Day One/` by default
|
||||||
* `~/Dropbox/Apps/Day One/` if you're syncing with Dropbox and
|
* `~/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.
|
* `~/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. You can also star entries when you write them:
|
Instead of all entries being in a single file, each entry will live in a separate `plist` file. You can also star entries when you write them:
|
||||||
|
@ -222,4 +226,5 @@ Known Issues
|
||||||
------------
|
------------
|
||||||
|
|
||||||
- The Windows shell prior to Windows 7 has issues with unicode encoding. If you want to use non-ascii characters, change the codepage with `chcp 1252` before using `jrnl` (Thanks to Yves Pouplard for solving this!)
|
- The Windows shell prior to Windows 7 has issues with unicode encoding. If you want to use non-ascii characters, change the codepage with `chcp 1252` before using `jrnl` (Thanks to Yves Pouplard for solving this!)
|
||||||
- _jrnl_ relies on the `Crypto` package to encrypt journals, which has some known problems with installing within virtual environments.
|
- _jrnl_ relies on the `PyCrypto` package to encrypt journals, which has some known problems with installing on Windows and within virtual environments. If you have trouble installing __jrnl__, [install PyCyrypto manually](https://www.dlitz.net/software/pycrypto/) first.
|
||||||
|
|
||||||
|
|
|
@ -15,16 +15,16 @@ class Entry:
|
||||||
|
|
||||||
def parse_tags(self):
|
def parse_tags(self):
|
||||||
fulltext = " ".join([self.title, self.body]).lower()
|
fulltext = " ".join([self.title, self.body]).lower()
|
||||||
tags = re.findall(r"([%s]\w+)" % self.journal.config['tagsymbols'], fulltext)
|
tags = re.findall(ur'([{}]\w+)'.format(self.journal.config['tagsymbols']), fulltext, re.UNICODE)
|
||||||
self.tags = set(tags)
|
self.tags = set(tags)
|
||||||
|
|
||||||
def __str__(self):
|
def __unicode__(self):
|
||||||
"""Returns a string representation of the entry to be written into a journal file."""
|
"""Returns a string representation of the entry to be written into a journal file."""
|
||||||
date_str = self.date.strftime(self.journal.config['timeformat'])
|
date_str = self.date.strftime(self.journal.config['timeformat'])
|
||||||
title = date_str + " " + self.title
|
title = date_str + " " + self.title
|
||||||
body = self.body.strip()
|
body = self.body.strip()
|
||||||
|
|
||||||
return "{title}{sep}{body}\n".format(
|
return u"{title}{sep}{body}\n".format(
|
||||||
title=title,
|
title=title,
|
||||||
sep="\n" if self.body else "",
|
sep="\n" if self.body else "",
|
||||||
body=body
|
body=body
|
||||||
|
@ -50,7 +50,7 @@ class Entry:
|
||||||
# Suppress bodies that are just blanks and new lines.
|
# Suppress bodies that are just blanks and new lines.
|
||||||
has_body = len(self.body) > 20 or not all(char in (" ", "\n") for char in self.body)
|
has_body = len(self.body) > 20 or not all(char in (" ", "\n") for char in self.body)
|
||||||
|
|
||||||
return "{title}{sep}{body}\n".format(
|
return u"{title}{sep}{body}\n".format(
|
||||||
title=title,
|
title=title,
|
||||||
sep="\n" if has_body else "",
|
sep="\n" if has_body else "",
|
||||||
body=body if has_body else "",
|
body=body if has_body else "",
|
||||||
|
@ -74,7 +74,7 @@ class Entry:
|
||||||
space = "\n"
|
space = "\n"
|
||||||
md_head = "###"
|
md_head = "###"
|
||||||
|
|
||||||
return "{md} {date}, {title} {body} {space}".format(
|
return u"{md} {date}, {title} {body} {space}".format(
|
||||||
md=md_head,
|
md=md_head,
|
||||||
date=date_str,
|
date=date_str,
|
||||||
title=self.title,
|
title=self.title,
|
||||||
|
|
|
@ -1,30 +1,38 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# encoding: utf-8
|
# encoding: utf-8
|
||||||
|
|
||||||
from Entry import Entry
|
try: from . import Entry
|
||||||
|
except (SystemError, ValueError): import Entry
|
||||||
|
import codecs
|
||||||
import os
|
import os
|
||||||
import parsedatetime.parsedatetime as pdt
|
try: import parsedatetime.parsedatetime_consts as pdt
|
||||||
|
except ImportError: import parsedatetime.parsedatetime as pdt
|
||||||
import re
|
import re
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import time
|
import time
|
||||||
try: import simplejson as json
|
try: import simplejson as json
|
||||||
except ImportError: import json
|
except ImportError: import json
|
||||||
import sys
|
import sys
|
||||||
import readline, glob
|
import glob
|
||||||
try:
|
try:
|
||||||
from Crypto.Cipher import AES
|
from Crypto.Cipher import AES
|
||||||
from Crypto.Random import random, atfork
|
from Crypto.Random import random, atfork
|
||||||
|
crypto_installed = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
pass
|
crypto_installed = False
|
||||||
|
if "win32" in sys.platform: import pyreadline as readline
|
||||||
|
else: import readline
|
||||||
import hashlib
|
import hashlib
|
||||||
import getpass
|
import getpass
|
||||||
try:
|
try:
|
||||||
import clint
|
import colorama
|
||||||
|
colorama.init()
|
||||||
except ImportError:
|
except ImportError:
|
||||||
clint = None
|
colorama = None
|
||||||
import plistlib
|
import plistlib
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
class Journal(object):
|
class Journal(object):
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
self.config = {
|
self.config = {
|
||||||
|
@ -41,23 +49,25 @@ class Journal(object):
|
||||||
self.config.update(kwargs)
|
self.config.update(kwargs)
|
||||||
|
|
||||||
# Set up date parser
|
# Set up date parser
|
||||||
consts = pdt.Constants()
|
consts = pdt.Constants(usePyICU=False)
|
||||||
consts.DOWParseStyle = -1 # "Monday" will be either today or the last Monday
|
consts.DOWParseStyle = -1 # "Monday" will be either today or the last Monday
|
||||||
self.dateparse = pdt.Calendar(consts)
|
self.dateparse = pdt.Calendar(consts)
|
||||||
self.key = None # used to decrypt and encrypt the journal
|
self.key = None # used to decrypt and encrypt the journal
|
||||||
|
|
||||||
journal_txt = self.open()
|
journal_txt = self.open()
|
||||||
self.entries = self.parse(journal_txt)
|
self.entries = self.parse(journal_txt)
|
||||||
self.sort()
|
self.sort()
|
||||||
|
|
||||||
def _colorize(self, string, color='red'):
|
def _colorize(self, string):
|
||||||
if clint:
|
if colorama:
|
||||||
return str(clint.textui.colored.ColoredString(color.upper(), string))
|
return colorama.Fore.CYAN + string + colorama.Fore.RESET
|
||||||
else:
|
else:
|
||||||
return string
|
return string
|
||||||
|
|
||||||
def _decrypt(self, cipher):
|
def _decrypt(self, cipher):
|
||||||
"""Decrypts a cipher string using self.key as the key and the first 16 byte of the cipher as the IV"""
|
"""Decrypts a cipher string using self.key as the key and the first 16 byte of the cipher as the IV"""
|
||||||
|
if not crypto_installed:
|
||||||
|
sys.exit("Error: PyCrypto is not installed.")
|
||||||
if not cipher:
|
if not cipher:
|
||||||
return ""
|
return ""
|
||||||
crypto = AES.new(self.key, AES.MODE_CBC, cipher[:16])
|
crypto = AES.new(self.key, AES.MODE_CBC, cipher[:16])
|
||||||
|
@ -66,35 +76,37 @@ class Journal(object):
|
||||||
except ValueError:
|
except ValueError:
|
||||||
print("ERROR: Your journal file seems to be corrupted. You do have a backup, don't you?")
|
print("ERROR: Your journal file seems to be corrupted. You do have a backup, don't you?")
|
||||||
sys.exit(-1)
|
sys.exit(-1)
|
||||||
if plain[-1] != " ": # Journals are always padded
|
if plain[-1] != " ": # Journals are always padded
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
return plain
|
return plain
|
||||||
|
|
||||||
def _encrypt(self, plain):
|
def _encrypt(self, plain):
|
||||||
"""Encrypt a plaintext string using self.key as the key"""
|
"""Encrypt a plaintext string using self.key as the key"""
|
||||||
atfork() # A seed for PyCrypto
|
if not crypto_installed:
|
||||||
|
sys.exit("Error: PyCrypto is not installed.")
|
||||||
|
atfork() # A seed for PyCrypto
|
||||||
iv = ''.join(chr(random.randint(0, 0xFF)) for i in range(16))
|
iv = ''.join(chr(random.randint(0, 0xFF)) for i in range(16))
|
||||||
crypto = AES.new(self.key, AES.MODE_CBC, iv)
|
crypto = AES.new(self.key, AES.MODE_CBC, iv)
|
||||||
if len(plain) % 16 != 0:
|
if len(plain) % 16 != 0:
|
||||||
plain += " " * (16 - len(plain) % 16)
|
plain += " " * (16 - len(plain) % 16)
|
||||||
else: # Always pad so we can detect properly decrypted files :)
|
else: # Always pad so we can detect properly decrypted files :)
|
||||||
plain += " " * 16
|
plain += " " * 16
|
||||||
return iv + crypto.encrypt(plain)
|
return iv + crypto.encrypt(plain)
|
||||||
|
|
||||||
def make_key(self, prompt="Password: "):
|
def make_key(self, prompt="Password: "):
|
||||||
"""Creates an encryption key from the default password or prompts for a new password."""
|
"""Creates an encryption key from the default password or prompts for a new password."""
|
||||||
password = self.config['password'] or getpass.getpass(prompt)
|
password = self.config['password'] or getpass.getpass(prompt)
|
||||||
self.key = hashlib.sha256(password).digest()
|
self.key = hashlib.sha256(password.encode('utf-8')).digest()
|
||||||
|
|
||||||
def open(self, filename=None):
|
def open(self, filename=None):
|
||||||
"""Opens the journal file defined in the config and parses it into a list of Entries.
|
"""Opens the journal file defined in the config and parses it into a list of Entries.
|
||||||
Entries have the form (date, title, body)."""
|
Entries have the form (date, title, body)."""
|
||||||
filename = filename or self.config['journal']
|
filename = filename or self.config['journal']
|
||||||
journal = None
|
journal = None
|
||||||
with open(filename) as f:
|
|
||||||
journal = f.read()
|
|
||||||
if self.config['encrypt']:
|
if self.config['encrypt']:
|
||||||
|
with open(filename, "rb") as f:
|
||||||
|
journal = f.read()
|
||||||
decrypted = None
|
decrypted = None
|
||||||
attempts = 0
|
attempts = 0
|
||||||
while decrypted is None:
|
while decrypted is None:
|
||||||
|
@ -102,13 +114,16 @@ class Journal(object):
|
||||||
decrypted = self._decrypt(journal)
|
decrypted = self._decrypt(journal)
|
||||||
if decrypted is None:
|
if decrypted is None:
|
||||||
attempts += 1
|
attempts += 1
|
||||||
self.config['password'] = None # This password doesn't work.
|
self.config['password'] = None # This password doesn't work.
|
||||||
if attempts < 3:
|
if attempts < 3:
|
||||||
print("Wrong password, try again.")
|
print("Wrong password, try again.")
|
||||||
else:
|
else:
|
||||||
print("Extremely wrong password.")
|
print("Extremely wrong password.")
|
||||||
sys.exit(-1)
|
sys.exit(-1)
|
||||||
journal = decrypted
|
journal = decrypted
|
||||||
|
else:
|
||||||
|
with codecs.open(filename, "r", "utf-8") as f:
|
||||||
|
journal = f.read()
|
||||||
return journal
|
return journal
|
||||||
|
|
||||||
def parse(self, journal):
|
def parse(self, journal):
|
||||||
|
@ -130,7 +145,7 @@ class Journal(object):
|
||||||
# parsing successfull => save old entry and create new one
|
# parsing successfull => save old entry and create new one
|
||||||
if new_date and current_entry:
|
if new_date and current_entry:
|
||||||
entries.append(current_entry)
|
entries.append(current_entry)
|
||||||
current_entry = Entry(self, date=new_date, title=line[date_length+1:])
|
current_entry = Entry.Entry(self, date=new_date, title=line[date_length+1:])
|
||||||
except ValueError:
|
except ValueError:
|
||||||
# Happens when we can't parse the start of the line as an date.
|
# Happens when we can't parse the start of the line as an date.
|
||||||
# In this case, just append line to our body.
|
# In this case, just append line to our body.
|
||||||
|
@ -143,33 +158,36 @@ class Journal(object):
|
||||||
entry.parse_tags()
|
entry.parse_tags()
|
||||||
return entries
|
return entries
|
||||||
|
|
||||||
def __str__(self):
|
def __unicode__(self):
|
||||||
"""Prettyprints the journal's entries"""
|
"""Prettyprints the journal's entries"""
|
||||||
sep = "\n"
|
sep = "\n"
|
||||||
pp = sep.join([e.pprint() for e in self.entries])
|
pp = sep.join([e.pprint() for e in self.entries])
|
||||||
if self.config['highlight']: # highlight tags
|
if self.config['highlight']: # highlight tags
|
||||||
if self.search_tags:
|
if self.search_tags:
|
||||||
for tag in self.search_tags:
|
for tag in self.search_tags:
|
||||||
tagre = re.compile(re.escape(tag), re.IGNORECASE)
|
tagre = re.compile(re.escape(tag), re.IGNORECASE)
|
||||||
pp = re.sub(tagre,
|
pp = re.sub(tagre,
|
||||||
lambda match: self._colorize(match.group(0), 'cyan'),
|
lambda match: self._colorize(match.group(0)),
|
||||||
pp)
|
pp, re.UNICODE)
|
||||||
else:
|
else:
|
||||||
pp = re.sub(r"([%s]\w+)" % self.config['tagsymbols'],
|
pp = re.sub(ur"(?u)([{}]\w+)".format(self.config['tagsymbols']),
|
||||||
lambda match: self._colorize(match.group(0), 'cyan'),
|
lambda match: self._colorize(match.group(0)),
|
||||||
pp)
|
pp)
|
||||||
return pp
|
return pp
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<Journal with %d entries>" % len(self.entries)
|
return "<Journal with %d entries>" % len(self.entries)
|
||||||
|
|
||||||
def write(self, filename = None):
|
def write(self, filename=None):
|
||||||
"""Dumps the journal into the config file, overwriting it"""
|
"""Dumps the journal into the config file, overwriting it"""
|
||||||
filename = filename or self.config['journal']
|
filename = filename or self.config['journal']
|
||||||
journal = "\n".join([str(e) for e in self.entries])
|
journal = "\n".join([unicode(e) for e in self.entries])
|
||||||
if self.config['encrypt']:
|
if self.config['encrypt']:
|
||||||
journal = self._encrypt(journal)
|
journal = self._encrypt(journal)
|
||||||
with open(filename, 'w') as journal_file:
|
with open(filename, 'wb') as journal_file:
|
||||||
|
journal_file.write(journal)
|
||||||
|
else:
|
||||||
|
with codecs.open(filename, 'w', "utf-8") as journal_file:
|
||||||
journal_file.write(journal)
|
journal_file.write(journal)
|
||||||
|
|
||||||
def sort(self):
|
def sort(self):
|
||||||
|
@ -227,10 +245,10 @@ class Journal(object):
|
||||||
|
|
||||||
date, flag = self.dateparse.parse(date)
|
date, flag = self.dateparse.parse(date)
|
||||||
|
|
||||||
if not flag: # Oops, unparsable.
|
if not flag: # Oops, unparsable.
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if flag is 1: # Date found, but no time. Use the default time.
|
if flag is 1: # Date found, but no time. Use the default time.
|
||||||
date = datetime(*date[:3], hour=self.config['default_hour'], minute=self.config['default_minute'])
|
date = datetime(*date[:3], hour=self.config['default_hour'], minute=self.config['default_minute'])
|
||||||
else:
|
else:
|
||||||
date = datetime(*date[:6])
|
date = datetime(*date[:6])
|
||||||
|
@ -252,7 +270,7 @@ class Journal(object):
|
||||||
|
|
||||||
# Split raw text into title and body
|
# Split raw text into title and body
|
||||||
title_end = len(raw)
|
title_end = len(raw)
|
||||||
for separator in ["\n",". ","? ","! "]:
|
for separator in ["\n", ". ", "? ", "! "]:
|
||||||
sep_pos = raw.find(separator)
|
sep_pos = raw.find(separator)
|
||||||
if 1 < sep_pos < title_end:
|
if 1 < sep_pos < title_end:
|
||||||
title_end = sep_pos
|
title_end = sep_pos
|
||||||
|
@ -261,17 +279,18 @@ class Journal(object):
|
||||||
if not date:
|
if not date:
|
||||||
if title.find(":") > 0:
|
if title.find(":") > 0:
|
||||||
date = self.parse_date(title[:title.find(":")])
|
date = self.parse_date(title[:title.find(":")])
|
||||||
if date: # Parsed successfully, strip that from the raw text
|
if date: # Parsed successfully, strip that from the raw text
|
||||||
title = title[title.find(":")+1:].strip()
|
title = title[title.find(":")+1:].strip()
|
||||||
if not date: # Still nothing? Meh, just live in the moment.
|
if not date: # Still nothing? Meh, just live in the moment.
|
||||||
date = self.parse_date("now")
|
date = self.parse_date("now")
|
||||||
|
|
||||||
entry = Entry(self, date, title, body)
|
entry = Entry.Entry(self, date, title, body)
|
||||||
self.entries.append(entry)
|
self.entries.append(entry)
|
||||||
if sort:
|
if sort:
|
||||||
self.sort()
|
self.sort()
|
||||||
return entry
|
return entry
|
||||||
|
|
||||||
|
|
||||||
class DayOne(Journal):
|
class DayOne(Journal):
|
||||||
"""A special Journal handling DayOne files"""
|
"""A special Journal handling DayOne files"""
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
|
@ -298,7 +317,6 @@ class DayOne(Journal):
|
||||||
# we're returning the obvious.
|
# we're returning the obvious.
|
||||||
return self.entries
|
return self.entries
|
||||||
|
|
||||||
|
|
||||||
def write(self):
|
def write(self):
|
||||||
"""Writes only the entries that have been modified into plist files."""
|
"""Writes only the entries that have been modified into plist files."""
|
||||||
for entry in self.entries:
|
for entry in self.entries:
|
||||||
|
@ -315,4 +333,3 @@ class DayOne(Journal):
|
||||||
'UUID': new_uuid
|
'UUID': new_uuid
|
||||||
}
|
}
|
||||||
plistlib.writePlist(entry_plist, filename)
|
plistlib.writePlist(entry_plist, filename)
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,17 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# encoding: utf-8
|
# encoding: utf-8
|
||||||
|
|
||||||
from Journal import Journal
|
|
||||||
from jrnl import cli
|
"""
|
||||||
|
jrnl is a simple journal application for your command line.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__title__ = 'jrnl'
|
||||||
|
__version__ = '1.1.0'
|
||||||
|
__author__ = 'Manuel Ebert'
|
||||||
|
__license__ = 'MIT License'
|
||||||
|
__copyright__ = 'Copyright 2013 Manuel Ebert'
|
||||||
|
|
||||||
|
from . import Journal
|
||||||
|
from . import jrnl
|
||||||
|
from .jrnl import cli
|
||||||
|
|
|
@ -4,9 +4,38 @@
|
||||||
try: import simplejson as json
|
try: import simplejson as json
|
||||||
except ImportError: import json
|
except ImportError: import json
|
||||||
|
|
||||||
|
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 to_tag_list(journal):
|
||||||
|
"""Prints a list of all tags and the number of occurances."""
|
||||||
|
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(u"{0:20} : {1}".format(tag, n) for n, tag in sorted(tag_counts, reverse=False))
|
||||||
|
return result
|
||||||
|
|
||||||
def to_json(journal):
|
def to_json(journal):
|
||||||
"""Returns a JSON representation of the Journal."""
|
"""Returns a JSON representation of the Journal."""
|
||||||
return json.dumps([e.to_dict() for e in journal.entries], indent=2)
|
tags = get_tags_count(journal)
|
||||||
|
result = {
|
||||||
|
"tags": dict((tag, count) for count, tag in tags),
|
||||||
|
"entries": [e.to_dict() for e in journal.entries]
|
||||||
|
}
|
||||||
|
return json.dumps(result, indent=2)
|
||||||
|
|
||||||
def to_md(journal):
|
def to_md(journal):
|
||||||
"""Returns a markdown representation of the Journal"""
|
"""Returns a markdown representation of the Journal"""
|
||||||
|
|
|
@ -6,6 +6,9 @@ import getpass
|
||||||
try: import simplejson as json
|
try: import simplejson as json
|
||||||
except ImportError: import json
|
except ImportError: import json
|
||||||
import os
|
import os
|
||||||
|
try: from . import util
|
||||||
|
except (SystemError, ValueError): import util
|
||||||
|
|
||||||
|
|
||||||
def module_exists(module_name):
|
def module_exists(module_name):
|
||||||
"""Checks if a module exists and can be imported"""
|
"""Checks if a module exists and can be imported"""
|
||||||
|
@ -62,7 +65,7 @@ def install_jrnl(config_path='~/.jrnl_config'):
|
||||||
|
|
||||||
# Where to create the journal?
|
# Where to create the journal?
|
||||||
path_query = 'Path to your journal file (leave blank for ~/journal.txt): '
|
path_query = 'Path to your journal file (leave blank for ~/journal.txt): '
|
||||||
journal_path = raw_input(path_query).strip() or os.path.expanduser('~/journal.txt')
|
journal_path = util.py23_input(path_query).strip() or os.path.expanduser('~/journal.txt')
|
||||||
default_config['journals']['default'] = os.path.expanduser(journal_path)
|
default_config['journals']['default'] = os.path.expanduser(journal_path)
|
||||||
|
|
||||||
# Encrypt it?
|
# Encrypt it?
|
||||||
|
@ -77,8 +80,8 @@ def install_jrnl(config_path='~/.jrnl_config'):
|
||||||
print("PyCrypto not found. To encrypt your journal, install the PyCrypto package from http://www.pycrypto.org and run 'jrnl --encrypt'. For now, your journal will be stored in plain text.")
|
print("PyCrypto not found. To encrypt your journal, install the PyCrypto package from http://www.pycrypto.org and run 'jrnl --encrypt'. For now, your journal will be stored in plain text.")
|
||||||
|
|
||||||
# Use highlighting:
|
# Use highlighting:
|
||||||
if module_exists("clint"):
|
if not module_exists("colorama"):
|
||||||
print("clint not found. To turn on highlighting, install clint and set highlight to true in your .jrnl_conf.")
|
print("colorama not found. To turn on highlighting, install colorama and set highlight to true in your .jrnl_conf.")
|
||||||
default_config['highlight'] = False
|
default_config['highlight'] = False
|
||||||
|
|
||||||
open(default_config['journals']['default'], 'a').close() # Touch to make sure it's there
|
open(default_config['journals']['default'], 'a').close() # Touch to make sure it's there
|
||||||
|
@ -91,4 +94,4 @@ def install_jrnl(config_path='~/.jrnl_config'):
|
||||||
config['password'] = password
|
config['password'] = password
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
|
63
jrnl/jrnl.py
63
jrnl/jrnl.py
|
@ -7,9 +7,16 @@
|
||||||
license: MIT, see LICENSE for more details.
|
license: MIT, see LICENSE for more details.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import Journal
|
try:
|
||||||
import exporters
|
from . import Journal
|
||||||
import install
|
from . import util
|
||||||
|
from . import exporters
|
||||||
|
from . import install
|
||||||
|
except (SystemError, ValueError):
|
||||||
|
import Journal
|
||||||
|
import util
|
||||||
|
import exporters
|
||||||
|
import install
|
||||||
import os
|
import os
|
||||||
import tempfile
|
import tempfile
|
||||||
import subprocess
|
import subprocess
|
||||||
|
@ -18,13 +25,8 @@ import sys
|
||||||
try: import simplejson as json
|
try: import simplejson as json
|
||||||
except ImportError: import json
|
except ImportError: import json
|
||||||
|
|
||||||
|
xdg_config = os.environ.get('XDG_CONFIG_HOME')
|
||||||
__title__ = 'jrnl'
|
CONFIG_PATH = os.path.join(xdg_config, "jrnl") if xdg_config else os.path.expanduser('~/.jrnl_config')
|
||||||
__version__ = '1.0.0-rc1'
|
|
||||||
__author__ = 'Manuel Ebert, Stephan Gabler'
|
|
||||||
__license__ = 'MIT'
|
|
||||||
|
|
||||||
CONFIG_PATH = os.path.expanduser('~/.jrnl_config')
|
|
||||||
PYCRYPTO = install.module_exists("Crypto")
|
PYCRYPTO = install.module_exists("Crypto")
|
||||||
|
|
||||||
def parse_args():
|
def parse_args():
|
||||||
|
@ -87,36 +89,19 @@ def encrypt(journal, filename=None):
|
||||||
journal.make_key(prompt="Enter new password:")
|
journal.make_key(prompt="Enter new password:")
|
||||||
journal.config['encrypt'] = True
|
journal.config['encrypt'] = True
|
||||||
journal.write(filename)
|
journal.write(filename)
|
||||||
print("Journal encrypted to {}.".format(filename or journal.config['journal']))
|
print("Journal encrypted to {0}.".format(filename or journal.config['journal']))
|
||||||
|
|
||||||
def decrypt(journal, filename=None):
|
def decrypt(journal, filename=None):
|
||||||
""" Decrypts into new file. If filename is not set, we encrypt the journal file itself. """
|
""" Decrypts into new file. If filename is not set, we encrypt the journal file itself. """
|
||||||
journal.config['encrypt'] = False
|
journal.config['encrypt'] = False
|
||||||
journal.config['password'] = ""
|
journal.config['password'] = ""
|
||||||
journal.write(filename)
|
journal.write(filename)
|
||||||
print("Journal decrypted to {}.".format(filename or journal.config['journal']))
|
print("Journal decrypted to {0}.".format(filename or journal.config['journal']))
|
||||||
|
|
||||||
def print_tags(journal):
|
|
||||||
"""Prints a list of all tags and the number of occurances."""
|
|
||||||
# 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 = {(tags.count(tag), tag) for tag in tags}
|
|
||||||
if min(tag_counts)[0] == 0:
|
|
||||||
tag_counts = filter(lambda x: x[0] > 1, tag_counts)
|
|
||||||
print('[Removed tags that appear only once.]')
|
|
||||||
for n, tag in sorted(tag_counts, reverse=False):
|
|
||||||
print("{:20} : {}".format(tag, n))
|
|
||||||
|
|
||||||
|
|
||||||
def touch_journal(filename):
|
def touch_journal(filename):
|
||||||
"""If filename does not exist, touch the file"""
|
"""If filename does not exist, touch the file"""
|
||||||
if not os.path.exists(filename):
|
if not os.path.exists(filename):
|
||||||
print("[Journal created at {}]".format(filename))
|
print("[Journal created at {0}]".format(filename))
|
||||||
open(filename, 'a').close()
|
open(filename, 'a').close()
|
||||||
|
|
||||||
def update_config(config, new_config, scope):
|
def update_config(config, new_config, scope):
|
||||||
|
@ -134,7 +119,12 @@ def cli():
|
||||||
config = install.install_jrnl(CONFIG_PATH)
|
config = install.install_jrnl(CONFIG_PATH)
|
||||||
else:
|
else:
|
||||||
with open(CONFIG_PATH) as f:
|
with open(CONFIG_PATH) as f:
|
||||||
config = json.load(f)
|
try:
|
||||||
|
config = json.load(f)
|
||||||
|
except ValueError as e:
|
||||||
|
print("[There seems to be something wrong with your jrnl config at {}: {}]".format(CONFIG_PATH, e.message))
|
||||||
|
print("[Entry was NOT added to your journal]")
|
||||||
|
sys.exit(-1)
|
||||||
install.update_config(config, config_path=CONFIG_PATH)
|
install.update_config(config, config_path=CONFIG_PATH)
|
||||||
|
|
||||||
original_config = config.copy()
|
original_config = config.copy()
|
||||||
|
@ -172,7 +162,7 @@ def cli():
|
||||||
if config['editor']:
|
if config['editor']:
|
||||||
raw = get_text_from_editor(config)
|
raw = get_text_from_editor(config)
|
||||||
else:
|
else:
|
||||||
raw = raw_input("[Compose Entry] ")
|
raw = util.py23_input("[Compose Entry] ")
|
||||||
if raw:
|
if raw:
|
||||||
args.text = [raw]
|
args.text = [raw]
|
||||||
else:
|
else:
|
||||||
|
@ -181,9 +171,10 @@ def cli():
|
||||||
# Writing mode
|
# Writing mode
|
||||||
if mode_compose:
|
if mode_compose:
|
||||||
raw = " ".join(args.text).strip()
|
raw = " ".join(args.text).strip()
|
||||||
entry = journal.new_entry(raw, args.date)
|
unicode_raw = raw.decode(sys.getfilesystemencoding())
|
||||||
|
entry = journal.new_entry(unicode_raw, args.date)
|
||||||
entry.starred = args.star
|
entry.starred = args.star
|
||||||
print("[Entry added to {} journal]").format(journal_name)
|
print("[Entry added to {0} journal]".format(journal_name))
|
||||||
journal.write()
|
journal.write()
|
||||||
|
|
||||||
# Reading mode
|
# Reading mode
|
||||||
|
@ -193,11 +184,11 @@ def cli():
|
||||||
strict=args.strict,
|
strict=args.strict,
|
||||||
short=args.short)
|
short=args.short)
|
||||||
journal.limit(args.limit)
|
journal.limit(args.limit)
|
||||||
print(journal)
|
print(unicode(journal))
|
||||||
|
|
||||||
# Various export modes
|
# Various export modes
|
||||||
elif args.tags:
|
elif args.tags:
|
||||||
print_tags(journal)
|
print(exporters.to_tag_list(journal))
|
||||||
|
|
||||||
elif args.json: # export to json
|
elif args.json: # export to json
|
||||||
print(exporters.to_json(journal))
|
print(exporters.to_json(journal))
|
||||||
|
|
10
jrnl/util.py
Normal file
10
jrnl/util.py
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
#!/usr/bin/env python
|
||||||
|
# encoding: utf-8
|
||||||
|
import sys
|
||||||
|
|
||||||
|
def py23_input(msg):
|
||||||
|
if sys.version_info[0] == 3:
|
||||||
|
try: return input(msg)
|
||||||
|
except SyntaxError: return ""
|
||||||
|
else:
|
||||||
|
return raw_input(msg)
|
|
@ -1,2 +1,4 @@
|
||||||
clint >= 0.3.1
|
parsedatetime >= 1.1.2
|
||||||
parsedatetime == 1.1.2
|
colorama >= 0.2.5
|
||||||
|
pycrypto >= 2.6
|
||||||
|
argparse==1.2.1
|
||||||
|
|
28
setup.py
28
setup.py
|
@ -43,23 +43,37 @@ except ImportError:
|
||||||
from distutils.core import setup
|
from distutils.core import setup
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import re
|
||||||
|
|
||||||
if sys.argv[-1] == 'publish':
|
if sys.argv[-1] == 'publish':
|
||||||
os.system("python setup.py bdist-egg upload")
|
|
||||||
os.system("python setup.py sdist upload")
|
os.system("python setup.py sdist upload")
|
||||||
sys.exit()
|
sys.exit()
|
||||||
|
|
||||||
base_dir = os.path.dirname(os.path.abspath(__file__))
|
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
|
def get_version(filename="jrnl/__init__.py"):
|
||||||
|
with open(os.path.join(base_dir, filename)) as initfile:
|
||||||
|
for line in initfile.readlines():
|
||||||
|
m = re.match("__version__ *= *['\"](.*)['\"]", line)
|
||||||
|
if m:
|
||||||
|
return m.group(1)
|
||||||
|
|
||||||
|
conditional_dependencies = {
|
||||||
|
"pyreadline>=2.0": "win32" in sys.platform,
|
||||||
|
"argparse==1.2.1": sys.version.startswith("2.6")
|
||||||
|
}
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name = "jrnl",
|
name = "jrnl",
|
||||||
version = "1.0.1",
|
version = get_version(),
|
||||||
description = "A command line journal application that stores your journal in a plain text file",
|
description = "A command line journal application that stores your journal in a plain text file",
|
||||||
packages = ['jrnl'],
|
packages = ['jrnl'],
|
||||||
install_requires = ["parsedatetime >= 1.1.2"],
|
install_requires = [
|
||||||
|
"parsedatetime>=1.1.2",
|
||||||
|
"colorama>=0.2.5"
|
||||||
|
] + [p for p, cond in conditional_dependencies.items() if cond],
|
||||||
extras_require = {
|
extras_require = {
|
||||||
'encryption': ["pycrypto"],
|
"encrypted": "pycrypto>=2.6"
|
||||||
'highlight': ["clint"]
|
|
||||||
},
|
},
|
||||||
long_description=__doc__,
|
long_description=__doc__,
|
||||||
entry_points={
|
entry_points={
|
||||||
|
@ -68,7 +82,7 @@ setup(
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
classifiers=[
|
classifiers=[
|
||||||
'Development Status :: 3 - Alpha',
|
'Development Status :: 5 - Production/Stable',
|
||||||
'Environment :: Console',
|
'Environment :: Console',
|
||||||
'Intended Audience :: End Users/Desktop',
|
'Intended Audience :: End Users/Desktop',
|
||||||
'License :: OSI Approved :: MIT License',
|
'License :: OSI Approved :: MIT License',
|
||||||
|
@ -81,7 +95,7 @@ setup(
|
||||||
],
|
],
|
||||||
# metadata for upload to PyPI
|
# metadata for upload to PyPI
|
||||||
author = "Manuel Ebert",
|
author = "Manuel Ebert",
|
||||||
author_email = "manuel@herelabs.com",
|
author_email = "manuel@hey.com",
|
||||||
license = "MIT License",
|
license = "MIT License",
|
||||||
keywords = "journal todo todo.txt jrnl".split(),
|
keywords = "journal todo todo.txt jrnl".split(),
|
||||||
url = "http://maebert.github.com/jrnl",
|
url = "http://maebert.github.com/jrnl",
|
||||||
|
|
Loading…
Add table
Reference in a new issue