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:
Apolloni 2013-06-19 03:13:19 -07:00
commit 5daf8f010a
12 changed files with 220 additions and 106 deletions

View file

@ -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

View file

@ -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.

View file

@ -1,4 +1,4 @@
jrnl jrnl [![Build Status](https://travis-ci.org/maebert/jrnl.png?branch=master)](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.

View file

@ -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,

View file

@ -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)

View file

@ -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

View file

@ -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"""

View file

@ -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

View file

@ -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
View 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)

View file

@ -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

View file

@ -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",