Commit adcebfc0 authored by Bram Schoenmakers's avatar Bram Schoenmakers

Merge branch 'column-ui' of https://github.com/bram85/topydo into column-ui

parents 3fd7c6c9 981d1af2
......@@ -8,9 +8,9 @@ python:
install:
- "python -m pip install pip --upgrade"
- "pip install ."
- "pip install urwid"
- "pip install .[columns]"
- "pip install .[ical]"
- "pip install .[prompt-toolkit]"
- "pip install .[prompt]"
- "pip install .[test]"
- "pip install pylint"
- "pip install codecov"
......
......@@ -32,6 +32,8 @@ Simply install with:
(not supported for Python 3.2).
* [prompt-toolkit][6] : For topydo's _prompt_ mode, which offers a shell-like
interface with auto-completion.
* [urwid][12] : For topydo's _columns_ mode, a TUI with columns for
your todo items.
* [arrow][8] : Used to turn dates into a human readable version.
* [backports.shutil_get_terminal_size][9] : Used to determine your terminal
window size. This function was
......@@ -60,3 +62,4 @@ Demo
[9]: https://github.com/chrippa/backports.shutil_get_terminal_size
[10]: https://dateutil.readthedocs.org/
[11]: https://github.com/testing-cabal/mock
[12]: https://github.com/urwid/urwid
......@@ -35,8 +35,8 @@ setup(
':sys_platform=="win32"': ['colorama>=0.2.5'],
':python_version=="3.2"': ['backports.shutil_get_terminal_size>=1.0.0'],
'ical': ['icalendar'],
'urwid': ['urwid >= 1.3.0'],
'prompt-toolkit': ['prompt-toolkit >= 0.53'],
'columns': ['urwid >= 1.3.0'],
'prompt': ['prompt-toolkit >= 0.53'],
'test': ['coverage', 'freezegun', 'green', ],
'test:python_version=="3.2"': ['mock'],
},
......
[column_keymap]
up = up
<Left> = prev_column
<Esc>d = delete_column
......@@ -127,5 +127,31 @@ class ConfigTest(TopydoTest):
""" No link color value. """
self.assertEqual(config("test/data/ConfigTest5.conf").link_color().color, 6)
def test_config24(self):
""" column_keymap test. """
keymap, keystates = config("test/data/ConfigTest6.conf").column_keymap()
self.assertEqual(keymap['pp'], 'postpone')
self.assertEqual(keymap['ps'], 'postpone_s')
self.assertEqual(keymap['pr'], 'pri')
self.assertEqual(keymap['pra'], 'cmd pri {} a')
self.assertIn('p', keystates)
self.assertIn('g', keystates)
self.assertIn('pp', keystates)
self.assertIn('ps', keystates)
self.assertIn('pr', keystates)
self.assertEqual(keymap['up'], 'up')
self.assertIn('u', keystates)
self.assertEqual(keymap['<Left>'], 'prev_column')
self.assertNotIn('<Lef', keystates)
self.assertEqual(keymap['<Esc>d'], 'delete_column')
self.assertNotIn('<Esc', keystates)
self.assertIn('<Esc>', keystates)
if __name__ == '__main__':
unittest.main()
# Topydo - A todo.txt client written in Python.
# Copyright (C) 2014 - 2015 Bram Schoenmakers <me@bramschoenmakers.nl>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import unittest
from test.topydo_testcase import TopydoTest
from topydo.lib.Utils import translate_key_to_config
class UtilsTest(TopydoTest):
def test_key_to_cfg(self):
ctrl_s = translate_key_to_config('ctrl s')
meta_d = translate_key_to_config('meta d')
esc = translate_key_to_config('esc')
f4 = translate_key_to_config('f4')
self.assertEqual(ctrl_s, '<C-s>')
self.assertEqual(meta_d, '<M-d>')
self.assertEqual(esc, '<Esc>')
self.assertEqual(f4, '<F4>')
if __name__ == '__main__':
unittest.main()
......@@ -65,3 +65,30 @@ append_parent_contexts = 0
;listcon = lscon
;listcontext = lscon
;listcontexts = lscon
[column_keymap]
; Keymap configuration for column-mode
gg = home
G = end
j = down
k = up
d = cmd del {}
e = cmd edit {}
u = cmd revert
x = cmd do {}
pp = postpone
ps = postpone_s
pr = pri
0 = first_column
$ = last_column
h = prev_column
l = next_column
A = append_column
I = insert_column
E = edit_column
D = delete_column
Y = copy_column
L = swap_left
R = swap_right
<Left> = prev_column
<Right> = next_column
......@@ -16,8 +16,12 @@
import configparser
import os
import re
import shlex
from itertools import accumulate
from string import ascii_lowercase
from topydo.lib.Color import Color
def home_config_path(p_filename):
......@@ -111,11 +115,40 @@ class _Config:
'listcontext': 'lscon',
'listcontexts': 'lscon',
},
'column_keymap': {
'gg': 'home',
'G': 'end',
'j': 'down',
'k': 'up',
'd': 'cmd del {}',
'e': 'cmd edit {}',
'u': 'cmd revert',
'x': 'cmd do {}',
'pp': 'postpone',
'ps': 'postpone_s',
'pr': 'pri',
'0': 'first_column',
'$': 'last_column',
'h': 'prev_column',
'l': 'next_column',
'A': 'append_column',
'I': 'insert_column',
'E': 'edit_column',
'D': 'delete_column',
'Y': 'copy_column',
'L': 'swap_left',
'R': 'swap_right',
'<Left>': 'prev_column',
'<Right>': 'next_column',
},
}
self.config = {}
self.cp = configparser.RawConfigParser()
# allow uppercase config keys
self.cp.optionxform = lambda option: option
for section in self.defaults:
self.cp.add_section(section)
......@@ -332,6 +365,29 @@ class _Config:
""" Returns the list format used by `ls` """
return self.cp.get('ls', 'list_format')
def column_keymap(self):
""" Returns keymap and keystates used in column mode """
keystates = set()
shortcuts = self.cp.items('column_keymap')
keymap_dict = dict(shortcuts)
for combo, action in shortcuts:
# add all possible prefixes to keystates
combo_as_list = re.split('(<[A-Z].+?>|.)', combo)[1::2]
if len(combo_as_list) > 1:
keystates |= set(accumulate(combo_as_list[:-1]))
if action in ['pri', 'postpone', 'postpone_s']:
keystates.add(combo)
if action == 'pri':
for c in ascii_lowercase:
keymap_dict[combo + c] = 'cmd pri {} ' + c
return (keymap_dict, keystates)
def config(p_path=None, p_overrides=None):
"""
Retrieve the config instance.
......
......@@ -94,3 +94,18 @@ def get_terminal_size(p_getter=None):
get_terminal_size.getter = inner
return get_terminal_size.getter()
def translate_key_to_config(p_key):
"""
Translates urwid key event to form understandable by topydo config parser.
"""
if len(p_key) > 1:
key = p_key.capitalize()
if key.startswith('Ctrl') or key.startswith('Meta'):
key = key[0] + '-' + key[5:]
key = '<' + key + '>'
else:
key = p_key
return key
......@@ -16,6 +16,7 @@
import urwid
class ConsoleWidget(urwid.LineBox):
def __init__(self, p_text=""):
urwid.register_signal(ConsoleWidget, ['close'])
......@@ -29,7 +30,10 @@ class ConsoleWidget(urwid.LineBox):
if p_key == 'enter' or p_key == 'q' or p_key == 'esc':
urwid.emit_signal(self, 'close')
# don't return the key, 'enter', 'escape' or 'q' are your only escape.
# don't return the key, 'enter', 'escape', 'q' or ':' are your only
# escape. ':' will reenter to the cmdline.
elif p_key == ':':
urwid.emit_signal(self, 'close', True)
def render(self, p_size, focus):
"""
......
# Topydo - A todo.txt client written in Python.
# Copyright (C) 2015 Bram Schoenmakers <me@bramschoenmakers.nl>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import urwid
class KeystateWidget(urwid.Text):
def __init__(self):
super().__init__('', align='right')
def selectable(self):
return False
......@@ -24,6 +24,7 @@ from topydo.cli.CLIApplicationBase import CLIApplicationBase
from topydo.Commands import get_subcommand
from topydo.ui.CommandLineWidget import CommandLineWidget
from topydo.ui.ConsoleWidget import ConsoleWidget
from topydo.ui.KeystateWidget import KeystateWidget
from topydo.ui.TodoListWidget import TodoListWidget
from topydo.ui.ViewWidget import ViewWidget
from topydo.ui.ColumnLayout import columns
......@@ -37,6 +38,7 @@ from topydo.lib import TodoList
COLUMN_WIDTH = 40
class UIView(View):
"""
A subclass of view holding user input data that constructed the view (i.e.
......@@ -51,6 +53,7 @@ _EDIT_COLUMN = 2
_COPY_COLUMN = 3
_INSERT_COLUMN = 4
class MainPile(urwid.Pile):
"""
This subclass of Pile doesn't change focus on cursor up/down / mouse press
......@@ -82,6 +85,7 @@ class MainPile(urwid.Pile):
if self._command_map[key] not in ('cursor up', 'cursor down'):
return key
class UIApplication(CLIApplicationBase):
def __init__(self):
super().__init__()
......@@ -93,6 +97,13 @@ class UIApplication(CLIApplicationBase):
self.columns = urwid.Columns([], dividechars=0, min_width=COLUMN_WIDTH)
self.commandline = CommandLineWidget('topydo> ')
self.keystate_widget = KeystateWidget()
self.status_line = urwid.Columns([
('weight', 1, urwid.Filler(self.commandline)),
])
self.keymap = config().column_keymap()
self._alarm = None
# console widget
self.console = ConsoleWidget()
......@@ -102,8 +113,10 @@ class UIApplication(CLIApplicationBase):
urwid.connect_signal(self.commandline, 'execute_command',
self._execute_handler)
def hide_console():
def hide_console(p_focus_commandline=False):
self._console_visible = False
if p_focus_commandline:
self._focus_commandline()
urwid.connect_signal(self.console, 'close', hide_console)
# view widget
......@@ -120,7 +133,7 @@ class UIApplication(CLIApplicationBase):
self.mainwindow = MainPile([
('weight', 1, self.columns),
(1, urwid.Filler(self.commandline)),
(1, self.status_line),
])
urwid.connect_signal(self.mainwindow, 'blur_console', hide_console)
......@@ -244,22 +257,25 @@ class UIApplication(CLIApplicationBase):
self.column_mode = _COPY_COLUMN
self._viewwidget_visible = True
def _column_action_handler(self, p_action):
dispatch = {
'first_column': self._focus_first_column,
'last_column': self._focus_last_column,
'prev_column': self._focus_previous_column,
'next_column': self._focus_next_column,
'append_column': self._append_column,
'insert_column': self._insert_column,
'edit_column': self._edit_column,
'delete_column': self._delete_column,
'copy_column': self._copy_column,
'swap_left': self._swap_column_left,
'swap_right': self._swap_column_right,
}
dispatch[p_action]()
def _handle_input(self, p_input):
dispatch = {
':': self._focus_commandline,
'0': self._focus_first_column,
'$': self._focus_last_column,
'left': self._focus_previous_column,
'h': self._focus_previous_column,
'right': self._focus_next_column,
'l': self._focus_next_column,
'A': self._append_column,
'I': self._insert_column,
'E': self._edit_column,
'D': self._delete_column,
'Y': self._copy_column,
'L': self._swap_column_left,
'R': self._swap_column_right,
}
try:
......@@ -307,11 +323,16 @@ class UIApplication(CLIApplicationBase):
before that position.
"""
todolist = TodoListWidget(p_view, p_view.data['title'])
todolist = TodoListWidget(p_view, p_view.data['title'], self.keymap)
no_output = lambda _: None
urwid.connect_signal(todolist, 'execute_command',
urwid.connect_signal(todolist, 'execute_command_silent',
lambda cmd: self._execute_handler(cmd, no_output))
urwid.connect_signal(todolist, 'execute_command', self._execute_handler)
urwid.connect_signal(todolist, 'refresh', self.mainloop.screen.clear)
urwid.connect_signal(todolist, 'add_pending_action', self._set_alarm)
urwid.connect_signal(todolist, 'remove_pending_action', self._remove_alarm)
urwid.connect_signal(todolist, 'column_action', self._column_action_handler)
urwid.connect_signal(todolist, 'show_keystate', self._print_keystate)
options = self.columns.options(
width_type='given',
......@@ -329,6 +350,19 @@ class UIApplication(CLIApplicationBase):
self.columns.focus_position = p_pos
self._blur_commandline()
def _print_keystate(self, p_keystate):
self.keystate_widget.set_text(p_keystate)
self._keystate_visible = len(p_keystate) > 0
def _set_alarm(self, p_callback):
""" Sets alarm to execute p_action specified in 0.5 sec. """
self._alarm = self.mainloop.set_alarm_in(0.5, p_callback)
def _remove_alarm(self):
""" Removes pending action alarm stored in _alarm attribute. """
self.mainloop.remove_alarm(self._alarm)
self._alarm = None
def _swap_column_left(self):
pos = self.columns.focus_position
if pos > 0:
......@@ -358,6 +392,23 @@ class UIApplication(CLIApplicationBase):
elif p_enabled == False and self._console_visible:
self.console.clear()
del contents[2]
self.mainwindow.focus_position = 0
@property
def _keystate_visible(self):
contents = self.status_line.contents
return len(contents) == 2 and isinstance(contents[1][0].original_widget,
KeystateWidget)
@_keystate_visible.setter
def _keystate_visible(self, p_enabled):
contents = self.status_line.contents
if p_enabled and len(contents) == 1:
contents.append((urwid.Filler(self.keystate_widget),
('weight', 1, True)))
elif not p_enabled and self._keystate_visible:
del contents[1]
@property
def _viewwidget_visible(self):
......
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment