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
|
||||
python:
|
||||
- "2.6"
|
||||
- "2.7"
|
||||
# - "3.2"
|
||||
- "3.2"
|
||||
- "3.3"
|
||||
install: "pip install -r requirements.txt --use-mirrors"
|
||||
# command to run tests
|
||||
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
|
||||
=========
|
||||
|
||||
### 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
|
||||
|
||||
|
@ -13,11 +38,11 @@ Changelog
|
|||
* [Fixed] A bug where jrnl would not add entries without timestamp
|
||||
* [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).
|
||||
|
||||
### 0.3.1 (June 16, 2012)
|
||||
#### 0.3.1 (June 16, 2012)
|
||||
|
||||
* [Improved] Supports deleting of last entry.
|
||||
* [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.
|
||||
|
@ -33,7 +33,11 @@ Install _jrnl_ using pip:
|
|||
|
||||
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
|
||||
cd jrnl
|
||||
|
@ -44,7 +48,7 @@ The first time you run `jrnl` you will be asked where your journal file should b
|
|||
Usage
|
||||
-----
|
||||
|
||||
_jrnl_ has to modes: __composing__ and __viewing__.
|
||||
_jrnl_ has two modes: __composing__ and __viewing__.
|
||||
|
||||
### Viewing:
|
||||
|
||||
|
@ -104,7 +108,7 @@ With
|
|||
|
||||
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
|
||||
|
||||
|
@ -137,7 +141,7 @@ will replace your encrypted journal file by a Journal in plain text. You can als
|
|||
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
|
||||
|
||||
|
@ -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__)
|
||||
- `default_hour` and `default_minute`: if you supply a date, such as `last thursday`, but no specific time, the entry will be created at this time
|
||||
- `timeformat`: how to format the timestamps in your journal, see the [python docs](http://docs.python.org/library/time.html#time.strftime) for reference
|
||||
- `highlight`: if `true` 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.
|
||||
|
||||
> __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
|
||||
|
||||
* `~/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.
|
||||
|
||||
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!)
|
||||
- _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):
|
||||
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)
|
||||
|
||||
def __str__(self):
|
||||
def __unicode__(self):
|
||||
"""Returns a string representation of the entry to be written into a journal file."""
|
||||
date_str = self.date.strftime(self.journal.config['timeformat'])
|
||||
title = date_str + " " + self.title
|
||||
body = self.body.strip()
|
||||
|
||||
return "{title}{sep}{body}\n".format(
|
||||
return u"{title}{sep}{body}\n".format(
|
||||
title=title,
|
||||
sep="\n" if self.body else "",
|
||||
body=body
|
||||
|
@ -50,7 +50,7 @@ class Entry:
|
|||
# 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)
|
||||
|
||||
return "{title}{sep}{body}\n".format(
|
||||
return u"{title}{sep}{body}\n".format(
|
||||
title=title,
|
||||
sep="\n" if has_body else "",
|
||||
body=body if has_body else "",
|
||||
|
@ -74,7 +74,7 @@ class Entry:
|
|||
space = "\n"
|
||||
md_head = "###"
|
||||
|
||||
return "{md} {date}, {title} {body} {space}".format(
|
||||
return u"{md} {date}, {title} {body} {space}".format(
|
||||
md=md_head,
|
||||
date=date_str,
|
||||
title=self.title,
|
||||
|
|
|
@ -1,30 +1,38 @@
|
|||
#!/usr/bin/env python
|
||||
# encoding: utf-8
|
||||
|
||||
from Entry import Entry
|
||||
try: from . import Entry
|
||||
except (SystemError, ValueError): import Entry
|
||||
import codecs
|
||||
import os
|
||||
import parsedatetime.parsedatetime as pdt
|
||||
try: import parsedatetime.parsedatetime_consts as pdt
|
||||
except ImportError: import parsedatetime.parsedatetime as pdt
|
||||
import re
|
||||
from datetime import datetime
|
||||
import time
|
||||
try: import simplejson as json
|
||||
except ImportError: import json
|
||||
import sys
|
||||
import readline, glob
|
||||
import glob
|
||||
try:
|
||||
from Crypto.Cipher import AES
|
||||
from Crypto.Random import random, atfork
|
||||
crypto_installed = True
|
||||
except ImportError:
|
||||
pass
|
||||
crypto_installed = False
|
||||
if "win32" in sys.platform: import pyreadline as readline
|
||||
else: import readline
|
||||
import hashlib
|
||||
import getpass
|
||||
try:
|
||||
import clint
|
||||
import colorama
|
||||
colorama.init()
|
||||
except ImportError:
|
||||
clint = None
|
||||
colorama = None
|
||||
import plistlib
|
||||
import uuid
|
||||
|
||||
|
||||
class Journal(object):
|
||||
def __init__(self, **kwargs):
|
||||
self.config = {
|
||||
|
@ -41,23 +49,25 @@ class Journal(object):
|
|||
self.config.update(kwargs)
|
||||
|
||||
# Set up date parser
|
||||
consts = pdt.Constants()
|
||||
consts.DOWParseStyle = -1 # "Monday" will be either today or the last Monday
|
||||
consts = pdt.Constants(usePyICU=False)
|
||||
consts.DOWParseStyle = -1 # "Monday" will be either today or the last Monday
|
||||
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()
|
||||
self.entries = self.parse(journal_txt)
|
||||
self.sort()
|
||||
|
||||
def _colorize(self, string, color='red'):
|
||||
if clint:
|
||||
return str(clint.textui.colored.ColoredString(color.upper(), string))
|
||||
def _colorize(self, string):
|
||||
if colorama:
|
||||
return colorama.Fore.CYAN + string + colorama.Fore.RESET
|
||||
else:
|
||||
return string
|
||||
|
||||
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"""
|
||||
if not crypto_installed:
|
||||
sys.exit("Error: PyCrypto is not installed.")
|
||||
if not cipher:
|
||||
return ""
|
||||
crypto = AES.new(self.key, AES.MODE_CBC, cipher[:16])
|
||||
|
@ -66,35 +76,37 @@ class Journal(object):
|
|||
except ValueError:
|
||||
print("ERROR: Your journal file seems to be corrupted. You do have a backup, don't you?")
|
||||
sys.exit(-1)
|
||||
if plain[-1] != " ": # Journals are always padded
|
||||
if plain[-1] != " ": # Journals are always padded
|
||||
return None
|
||||
else:
|
||||
return plain
|
||||
|
||||
def _encrypt(self, plain):
|
||||
"""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))
|
||||
crypto = AES.new(self.key, AES.MODE_CBC, iv)
|
||||
if len(plain) % 16 != 0:
|
||||
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
|
||||
return iv + crypto.encrypt(plain)
|
||||
|
||||
def make_key(self, prompt="Password: "):
|
||||
"""Creates an encryption key from the default password or prompts for a new password."""
|
||||
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):
|
||||
"""Opens the journal file defined in the config and parses it into a list of Entries.
|
||||
Entries have the form (date, title, body)."""
|
||||
filename = filename or self.config['journal']
|
||||
journal = None
|
||||
with open(filename) as f:
|
||||
journal = f.read()
|
||||
if self.config['encrypt']:
|
||||
with open(filename, "rb") as f:
|
||||
journal = f.read()
|
||||
decrypted = None
|
||||
attempts = 0
|
||||
while decrypted is None:
|
||||
|
@ -102,13 +114,16 @@ class Journal(object):
|
|||
decrypted = self._decrypt(journal)
|
||||
if decrypted is None:
|
||||
attempts += 1
|
||||
self.config['password'] = None # This password doesn't work.
|
||||
self.config['password'] = None # This password doesn't work.
|
||||
if attempts < 3:
|
||||
print("Wrong password, try again.")
|
||||
else:
|
||||
print("Extremely wrong password.")
|
||||
sys.exit(-1)
|
||||
journal = decrypted
|
||||
else:
|
||||
with codecs.open(filename, "r", "utf-8") as f:
|
||||
journal = f.read()
|
||||
return journal
|
||||
|
||||
def parse(self, journal):
|
||||
|
@ -130,7 +145,7 @@ class Journal(object):
|
|||
# parsing successfull => save old entry and create new one
|
||||
if new_date and 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:
|
||||
# Happens when we can't parse the start of the line as an date.
|
||||
# In this case, just append line to our body.
|
||||
|
@ -143,33 +158,36 @@ class Journal(object):
|
|||
entry.parse_tags()
|
||||
return entries
|
||||
|
||||
def __str__(self):
|
||||
def __unicode__(self):
|
||||
"""Prettyprints the journal's entries"""
|
||||
sep = "\n"
|
||||
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:
|
||||
for tag in self.search_tags:
|
||||
tagre = re.compile(re.escape(tag), re.IGNORECASE)
|
||||
pp = re.sub(tagre,
|
||||
lambda match: self._colorize(match.group(0), 'cyan'),
|
||||
pp)
|
||||
lambda match: self._colorize(match.group(0)),
|
||||
pp, re.UNICODE)
|
||||
else:
|
||||
pp = re.sub(r"([%s]\w+)" % self.config['tagsymbols'],
|
||||
lambda match: self._colorize(match.group(0), 'cyan'),
|
||||
pp = re.sub(ur"(?u)([{}]\w+)".format(self.config['tagsymbols']),
|
||||
lambda match: self._colorize(match.group(0)),
|
||||
pp)
|
||||
return pp
|
||||
|
||||
def __repr__(self):
|
||||
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"""
|
||||
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']:
|
||||
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)
|
||||
|
||||
def sort(self):
|
||||
|
@ -227,10 +245,10 @@ class Journal(object):
|
|||
|
||||
date, flag = self.dateparse.parse(date)
|
||||
|
||||
if not flag: # Oops, unparsable.
|
||||
if not flag: # Oops, unparsable.
|
||||
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'])
|
||||
else:
|
||||
date = datetime(*date[:6])
|
||||
|
@ -252,7 +270,7 @@ class Journal(object):
|
|||
|
||||
# Split raw text into title and body
|
||||
title_end = len(raw)
|
||||
for separator in ["\n",". ","? ","! "]:
|
||||
for separator in ["\n", ". ", "? ", "! "]:
|
||||
sep_pos = raw.find(separator)
|
||||
if 1 < sep_pos < title_end:
|
||||
title_end = sep_pos
|
||||
|
@ -261,17 +279,18 @@ class Journal(object):
|
|||
if not date:
|
||||
if title.find(":") > 0:
|
||||
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()
|
||||
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")
|
||||
|
||||
entry = Entry(self, date, title, body)
|
||||
entry = Entry.Entry(self, date, title, body)
|
||||
self.entries.append(entry)
|
||||
if sort:
|
||||
self.sort()
|
||||
return entry
|
||||
|
||||
|
||||
class DayOne(Journal):
|
||||
"""A special Journal handling DayOne files"""
|
||||
def __init__(self, **kwargs):
|
||||
|
@ -298,7 +317,6 @@ class DayOne(Journal):
|
|||
# we're returning the obvious.
|
||||
return self.entries
|
||||
|
||||
|
||||
def write(self):
|
||||
"""Writes only the entries that have been modified into plist files."""
|
||||
for entry in self.entries:
|
||||
|
@ -315,4 +333,3 @@ class DayOne(Journal):
|
|||
'UUID': new_uuid
|
||||
}
|
||||
plistlib.writePlist(entry_plist, filename)
|
||||
|
||||
|
|
|
@ -1,5 +1,17 @@
|
|||
#!/usr/bin/env python
|
||||
# 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
|
||||
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):
|
||||
"""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):
|
||||
"""Returns a markdown representation of the Journal"""
|
||||
|
|
|
@ -6,6 +6,9 @@ import getpass
|
|||
try: import simplejson as json
|
||||
except ImportError: import json
|
||||
import os
|
||||
try: from . import util
|
||||
except (SystemError, ValueError): import util
|
||||
|
||||
|
||||
def module_exists(module_name):
|
||||
"""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?
|
||||
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)
|
||||
|
||||
# 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.")
|
||||
|
||||
# Use highlighting:
|
||||
if module_exists("clint"):
|
||||
print("clint not found. To turn on highlighting, install clint and set highlight to true in your .jrnl_conf.")
|
||||
if not module_exists("colorama"):
|
||||
print("colorama not found. To turn on highlighting, install colorama and set highlight to true in your .jrnl_conf.")
|
||||
default_config['highlight'] = False
|
||||
|
||||
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
|
||||
return config
|
||||
|
||||
|
||||
|
||||
|
|
63
jrnl/jrnl.py
63
jrnl/jrnl.py
|
@ -7,9 +7,16 @@
|
|||
license: MIT, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
import Journal
|
||||
import exporters
|
||||
import install
|
||||
try:
|
||||
from . import Journal
|
||||
from . import util
|
||||
from . import exporters
|
||||
from . import install
|
||||
except (SystemError, ValueError):
|
||||
import Journal
|
||||
import util
|
||||
import exporters
|
||||
import install
|
||||
import os
|
||||
import tempfile
|
||||
import subprocess
|
||||
|
@ -18,13 +25,8 @@ import sys
|
|||
try: import simplejson as json
|
||||
except ImportError: import json
|
||||
|
||||
|
||||
__title__ = 'jrnl'
|
||||
__version__ = '1.0.0-rc1'
|
||||
__author__ = 'Manuel Ebert, Stephan Gabler'
|
||||
__license__ = 'MIT'
|
||||
|
||||
CONFIG_PATH = os.path.expanduser('~/.jrnl_config')
|
||||
xdg_config = os.environ.get('XDG_CONFIG_HOME')
|
||||
CONFIG_PATH = os.path.join(xdg_config, "jrnl") if xdg_config else os.path.expanduser('~/.jrnl_config')
|
||||
PYCRYPTO = install.module_exists("Crypto")
|
||||
|
||||
def parse_args():
|
||||
|
@ -87,36 +89,19 @@ def encrypt(journal, filename=None):
|
|||
journal.make_key(prompt="Enter new password:")
|
||||
journal.config['encrypt'] = True
|
||||
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):
|
||||
""" Decrypts into new file. If filename is not set, we encrypt the journal file itself. """
|
||||
journal.config['encrypt'] = False
|
||||
journal.config['password'] = ""
|
||||
journal.write(filename)
|
||||
print("Journal decrypted to {}.".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))
|
||||
|
||||
print("Journal decrypted to {0}.".format(filename or journal.config['journal']))
|
||||
|
||||
def touch_journal(filename):
|
||||
"""If filename does not exist, touch the file"""
|
||||
if not os.path.exists(filename):
|
||||
print("[Journal created at {}]".format(filename))
|
||||
print("[Journal created at {0}]".format(filename))
|
||||
open(filename, 'a').close()
|
||||
|
||||
def update_config(config, new_config, scope):
|
||||
|
@ -134,7 +119,12 @@ def cli():
|
|||
config = install.install_jrnl(CONFIG_PATH)
|
||||
else:
|
||||
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)
|
||||
|
||||
original_config = config.copy()
|
||||
|
@ -172,7 +162,7 @@ def cli():
|
|||
if config['editor']:
|
||||
raw = get_text_from_editor(config)
|
||||
else:
|
||||
raw = raw_input("[Compose Entry] ")
|
||||
raw = util.py23_input("[Compose Entry] ")
|
||||
if raw:
|
||||
args.text = [raw]
|
||||
else:
|
||||
|
@ -181,9 +171,10 @@ def cli():
|
|||
# Writing mode
|
||||
if mode_compose:
|
||||
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
|
||||
print("[Entry added to {} journal]").format(journal_name)
|
||||
print("[Entry added to {0} journal]".format(journal_name))
|
||||
journal.write()
|
||||
|
||||
# Reading mode
|
||||
|
@ -193,11 +184,11 @@ def cli():
|
|||
strict=args.strict,
|
||||
short=args.short)
|
||||
journal.limit(args.limit)
|
||||
print(journal)
|
||||
print(unicode(journal))
|
||||
|
||||
# Various export modes
|
||||
elif args.tags:
|
||||
print_tags(journal)
|
||||
print(exporters.to_tag_list(journal))
|
||||
|
||||
elif args.json: # export to json
|
||||
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
|
||||
import os
|
||||
import sys
|
||||
import re
|
||||
|
||||
if sys.argv[-1] == 'publish':
|
||||
os.system("python setup.py bdist-egg upload")
|
||||
os.system("python setup.py sdist upload")
|
||||
sys.exit()
|
||||
|
||||
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(
|
||||
name = "jrnl",
|
||||
version = "1.0.1",
|
||||
version = get_version(),
|
||||
description = "A command line journal application that stores your journal in a plain text file",
|
||||
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 = {
|
||||
'encryption': ["pycrypto"],
|
||||
'highlight': ["clint"]
|
||||
"encrypted": "pycrypto>=2.6"
|
||||
},
|
||||
long_description=__doc__,
|
||||
entry_points={
|
||||
|
@ -68,7 +82,7 @@ setup(
|
|||
],
|
||||
},
|
||||
classifiers=[
|
||||
'Development Status :: 3 - Alpha',
|
||||
'Development Status :: 5 - Production/Stable',
|
||||
'Environment :: Console',
|
||||
'Intended Audience :: End Users/Desktop',
|
||||
'License :: OSI Approved :: MIT License',
|
||||
|
@ -81,7 +95,7 @@ setup(
|
|||
],
|
||||
# metadata for upload to PyPI
|
||||
author = "Manuel Ebert",
|
||||
author_email = "manuel@herelabs.com",
|
||||
author_email = "manuel@hey.com",
|
||||
license = "MIT License",
|
||||
keywords = "journal todo todo.txt jrnl".split(),
|
||||
url = "http://maebert.github.com/jrnl",
|
||||
|
|
Loading…
Add table
Reference in a new issue