Skip to content

Commit

Permalink
Merge pull request #98 from geier/click
Browse files Browse the repository at this point in the history
Port CLI to click
  • Loading branch information
untitaker committed Sep 11, 2014
2 parents 280839c + 5a83282 commit 58baa0d
Show file tree
Hide file tree
Showing 7 changed files with 311 additions and 101 deletions.
241 changes: 151 additions & 90 deletions khal/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,82 +20,74 @@
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
"""khal
Usage:
khal calendar [-vc CONF] [ (-a CAL ... | -d CAL ... ) ] [--days=N| --events=N] [DATE ...]
khal agenda [-vc CONF] [ (-a CAL ... | -d CAL ... ) ] [--days=N| --events=N] [DATE ...]
khal interactive [-vc CONF] [ (-a CAL ... | -d CAL ... ) ]
khal new [-vc CONF] [-a cal] DESCRIPTION...
khal printcalendars [-vc CONF]
khal [-vc CONF] [ (-a CAL ... | -d CAL ... ) ] [DATE ...]
khal (-h | --help)
khal --version
Options:
-h --help Show this help.
--version Print version information.
-a CAL Use this calendars (can be used several times)
-d CAL Do not use this calendar (can be used several times)
-v Be extra verbose.
-c CONF Use this config file.
"""
import logging
import signal
import StringIO
import sys

try:
from setproctitle import setproctitle
except ImportError:
setproctitle = lambda x: None

from docopt import docopt
import click

from khal import controllers
from khal import khalendar
from khal import __version__, __productname__
from khal import __version__
from khal.log import logger
from khal.settings import get_config

try:
from StringIO import StringIO
except ImportError:
from io import StringIO

def capture_user_interruption():
"""
Tries to hide to the user the ugly python backtraces generated by
pressing Ctrl-C.
"""
signal.signal(signal.SIGINT, lambda x, y: sys.exit(0))

days_option = click.option('--days', default=None, type=int)
events_option = click.option('--events', default=None, type=int)
dates_arg = click.argument('dates', nargs=-1)
time_args = lambda f: dates_arg(events_option(days_option(f)))

def main_khal():
capture_user_interruption()

# setting the process title so it looks nicer in ps
# shows up as 'khal' under linux and as 'python: khal (python2.7)'
# under FreeBSD, which is still nicer than the default
setproctitle('khal')
def _calendar_select_callback(ctx, option, calendars):
if not calendars:
return
if 'calendar_selection' in ctx.obj:
raise click.UsageError('Can\'t use both -a and -d.')
if not isinstance(calendars, tuple):
calendars = (calendars,)

arguments = docopt(__doc__, version=__productname__ + ' ' + __version__,
options_first=False)
mode = option.name
selection = ctx.obj['calendar_selection'] = set()

if arguments['-v']:
logger.setLevel(logging.DEBUG)
logger.debug('this is {} version {}'.format(__productname__, __version__))
if mode == 'include_calendar':
selection.update(calendars)
elif mode == 'exclude_calendar':
selection.update(ctx.obj['conf']['calendars'].keys())
for value in calendars:
calendars.remove(value)
else:
raise ValueError(mode)

conf = get_config(arguments['-c'])

out = StringIO.StringIO()
conf.write(out)
logger.debug('using config:')
logger.debug(out.getvalue())
def calendar_selector(f):
a = click.option('--include-calendar', '-a', multiple=True, metavar='CAL',
expose_value=False, callback=_calendar_select_callback,
help=('Include the given calendar. Can be specified '
'multiple times.'))
d = click.option('--exclude-calendar', '-d', multiple=True, metavar='CAL',
expose_value=False, callback=_calendar_select_callback,
help=('Exclude the given calendar. Can be specified '
'multiple times.'))

collection = khalendar.CalendarCollection()
return d(a(f))


def build_collection(ctx):
conf = ctx.obj['conf']
collection = khalendar.CalendarCollection()
selection = ctx.obj.get('calendar_selection', None)
for name, cal in conf['calendars'].items():
if (name in arguments['-a'] and arguments['-d'] == list()) or \
(name not in arguments['-d'] and arguments['-a'] == list()):
if selection is None or name in ctx.obj['calendar_selection']:
collection.append(khalendar.Calendar(
name=name,
dbpath=conf['sqlite']['path'],
Expand All @@ -106,43 +98,112 @@ def main_khal():
local_tz=conf['locale']['local_timezone'],
default_tz=conf['locale']['default_timezone']
))

collection._default_calendar_name = conf['default']['default_calendar']
commands = ['agenda', 'calendar', 'new', 'interactive', 'printcalendars']

if not any([arguments[com] for com in commands]):
arguments = docopt(__doc__,
version=__productname__ + ' ' + __version__,
argv=[conf['default']['default_command']] + sys.argv[1:])

days = int(arguments['--days']) if arguments['--days'] else None
events = int(arguments['--events']) if arguments['--events'] else None

if arguments['calendar']:
controllers.Calendar(collection,
date=arguments['DATE'],
firstweekday=conf['locale']['firstweekday'],
encoding=conf['locale']['encoding'],
dateformat=conf['locale']['dateformat'],
longdateformat=conf['locale']['longdateformat'],
days=days,
events = events)
elif arguments['agenda']:
controllers.Agenda(collection,
date=arguments['DATE'],
firstweekday=conf['locale']['firstweekday'],
encoding=conf['locale']['encoding'],
dateformat=conf['locale']['dateformat'],
longdateformat=conf['locale']['longdateformat'],
days=days,
events=events)
elif arguments['new']:
controllers.NewFromString(collection, conf, arguments['DESCRIPTION'])
elif arguments['interactive']:
controllers.Interactive(collection, conf)
elif arguments['printcalendars']:
print('\n'.join(collection.names))


def main_ikhal():
sys.argv = [sys.argv[0], 'interactive'] + sys.argv[1:]
main_khal()
return collection


def _get_cli():
@click.group(invoke_without_command=True)
@click.option('--config', '-c', default=None, metavar='PATH',
help='The config file to use.')
@click.option('--verbose', '-v', is_flag=True,
help='Output debugging information.')
@click.version_option(version=__version__)
@click.pass_context
def cli(ctx, config, verbose):
# setting the process title so it looks nicer in ps
# shows up as 'khal' under linux and as 'python: khal (python2.7)'
# under FreeBSD, which is still nicer than the default
setproctitle('khal')

if verbose:
logger.setLevel(logging.DEBUG)
else:
logger.setLevel(logging.INFO)

if ctx.obj is None:
ctx.obj = {}

ctx.obj['conf'] = conf = get_config(config)

out = StringIO()
conf.write(out)
logger.debug('Using config:')
logger.debug(out.getvalue())

if conf is None:
raise click.UsageError('Invalid config file, exiting.')

if not ctx.invoked_subcommand:
command = conf['default']['default_command']
if command:
ctx.invoke(cli.commands[command])
else:
click.echo(ctx.get_help())
ctx.exit(1)

@cli.command()
@time_args
@calendar_selector
@click.pass_context
def calendar(ctx, days, events, dates):
controllers.Calendar(
build_collection(ctx),
date=dates,
firstweekday=ctx.obj['conf']['locale']['firstweekday'],
encoding=ctx.obj['conf']['locale']['encoding'],
dateformat=ctx.obj['conf']['locale']['dateformat'],
longdateformat=ctx.obj['conf']['locale']['longdateformat'],
days=days,
events=events
)

@cli.command()
@time_args
@calendar_selector
@click.pass_context
def agenda(ctx, days, events, dates):
controllers.Agenda(
build_collection(ctx),
date=dates,
firstweekday=ctx.obj['conf']['locale']['firstweekday'],
encoding=ctx.obj['conf']['locale']['encoding'],
dateformat=ctx.obj['conf']['locale']['dateformat'],
longdateformat=ctx.obj['conf']['locale']['longdateformat'],
days=days,
events=events,
)

@cli.command()
@click.option('--include-calendar', '-a', help=('The calendar to use.'),
expose_value=False, callback=_calendar_select_callback)
@click.argument('description', nargs=-1)
@click.pass_context
def new(ctx, description):
controllers.NewFromString(
build_collection(ctx),
ctx.obj['conf'],
list(description)
)

@cli.command()
@calendar_selector
@click.pass_context
def interactive(ctx):
controllers.Interactive(build_collection(ctx), ctx.obj['conf'])

@cli.command()
@calendar_selector
@click.pass_context
def printcalendars(ctx):
click.echo('\n'.join(build_collection(ctx).names))

return cli


main_khal = _get_cli()


def main_ikhal(args=sys.argv[1:]):
main_khal(['interactive'] + args)
8 changes: 5 additions & 3 deletions khal/controllers.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#

from click import echo

import datetime
import logging
import sys
Expand Down Expand Up @@ -112,7 +114,7 @@ def get_agenda(collection, dateformat, longdateformat, dates=[],
event_column.extend([colored(d, event.color) for d in desc])

if event_column == []:
event_column = [bstring('No Events')]
event_column = [bstring('No events')]
return event_column


Expand All @@ -129,7 +131,7 @@ def __init__(self, collection, date=[], firstweekday=0, encoding='utf-8',
firstweekday=firstweekday)

rows = merge_columns(calendar_column, event_column)
print('\n'.join(rows).encode(encoding))
echo('\n'.join(rows).encode(encoding))


class Agenda(object):
Expand All @@ -139,7 +141,7 @@ def __init__(self, collection, date=None, firstweekday=0, encoding='utf-8',
term_width, _ = get_terminal_size()
event_column = get_agenda(collection, dates=date, width=term_width,
**kwargs)
print('\n'.join(event_column).encode(encoding))
echo('\n'.join(event_column).encode(encoding))


class NewFromString(object):
Expand Down
7 changes: 1 addition & 6 deletions khal/khalendar/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,6 @@ class SQLiteDb(object):
and of parameters named "accountS" should be an iterable like list()
"""

_calendars = []

def __init__(self, calendar, db_path, local_tz, default_tz,
color=None, readonly=False, unicode_symbols=True,
debug=False):
Expand All @@ -119,10 +117,7 @@ def __init__(self, calendar, db_path, local_tz, default_tz,
self.debug = debug
self._create_default_tables()
self._check_table_version()

if not self.calendar in self._calendars or db_path == ':memory:':
self.create_account_table()
self._calendars.append(self.calendar)
self.create_account_table()

def __del__(self):
self.conn.close()
Expand Down
33 changes: 32 additions & 1 deletion khal/log.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,40 @@
import logging
import sys

import click

from khal import __productname__

stdout_handler = logging.StreamHandler(sys.stdout)

class ColorFormatter(logging.Formatter):
colors = {
'error': dict(fg='red'),
'exception': dict(fg='red'),
'critical': dict(fg='red'),
'debug': dict(fg='blue'),
'warning': dict(fg='yellow')
}

def format(self, record):
if not record.exc_info:
level = record.levelname.lower()
if level in self.colors:
prefix = click.style('{}: '.format(level),
**self.colors[level])
record.msg = '\n'.join(prefix + x
for x in str(record.msg).splitlines())

return logging.Formatter.format(self, record)


class ClickStream(object):
def write(self, string):
click.echo(string, file=sys.stderr, nl=False)


stdout_handler = logging.StreamHandler(ClickStream())
stdout_handler.formatter = ColorFormatter()

logger = logging.getLogger(__productname__)
logger.setLevel(logging.INFO)
logger.addHandler(stdout_handler)
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def write_version():


requirements = [
'docopt',
'click>=3.2',
'icalendar',
'urwid',
'pyxdg',
Expand Down
2 changes: 2 additions & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# coding: utf-8
# vim: set ts=4 sw=4 expandtab sts=4:
Loading

0 comments on commit 58baa0d

Please sign in to comment.