summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorChristian Geier <geier@lostpackets.de>2023-10-27 00:02:33 +0200
committerGitHub <noreply@github.com>2023-10-27 00:02:33 +0200
commit25c5c2671908a72f82f4d2add02068374ffb3242 (patch)
tree8c70667e6015f2f569662c3c1466577af0dd2e94
parent3877aac7d11c2c4a43079b6dc22b3a7d441ae494 (diff)
parentd7265002ca47705390a1f609945b23f49278a7a9 (diff)
Merge pull request #1279 from pimutils/config_based_theming
ikhal: initial support for config based ikhal theming
-rw-r--r--CHANGELOG.rst2
-rw-r--r--khal/settings/khal.spec35
-rw-r--r--khal/settings/utils.py25
-rw-r--r--khal/ui/__init__.py171
-rw-r--r--khal/ui/colors.py25
-rw-r--r--khal/ui/editor.py73
-rw-r--r--khal/ui/widgets.py58
7 files changed, 274 insertions, 115 deletions
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 789168ec..27319007 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -14,6 +14,8 @@ not released yet
* optimization in ikhal when editing events in the far future or past
* FIX an issue in ikhal with updating the view of the event list after editing
an event
+* NEW properties of ikhal themes (dark and light) can now be overriden from the
+ config file (via the new [palette] section, check the documenation)
0.11.2
======
diff --git a/khal/settings/khal.spec b/khal/settings/khal.spec
index fb9e50f6..ec4e8eef 100644
--- a/khal/settings/khal.spec
+++ b/khal/settings/khal.spec
@@ -257,7 +257,7 @@ blank_line_before_day = boolean(default=False)
# `khal/settings/khal.spec` in the section `[default]` of the property `theme`.
#
# __ http://urwid.org/manual/displayattributes.html
-# .. _github: # https://github.com/pimutils/khal/issues
+# .. _github: https://github.com/pimutils/khal/issues
theme = option('dark', 'light', default='dark')
# Whether to show a visible frame (with *box drawing* characters) around some
@@ -324,3 +324,36 @@ multiple_on_overflow = boolean(default=False)
# actually disables highlighting for events that should use the
# default color.
default_color = color(default='')
+
+# Override ikhal's color theme with a custom palette. This is useful to style
+# certain elements of ikhal individually.
+# Palette entries take the form of `key = foreground, background, mono,
+# foreground_high, background_high` where foreground and background are used in
+# "low color mode" and foreground_high and background_high are used in "high
+# color mode" and mono if only monocolor is supported. If you don't want to set
+# a value for a certain color, use an empty string (`''`).
+# Valid entries for low color mode are listed on the `urwid website
+# <http://urwid.org/manual/displayattributes.html#standard-foreground-colors>`_. For
+# high color mode you can use any valid 24-bit color value, e.g. `'#ff0000'`.
+#
+# .. note::
+# 24-bit colors must be enclosed in single quotes to be parsed correctly,
+# otherwise the `#` will be interpreted as a comment.
+#
+# Most modern terminals should support high color mode.
+#
+# Example entry (particular ugly):
+#
+# .. highlight:: ini
+#
+# ::
+#
+# [palette]
+# header = light red, default, default, '#ff0000', default
+# edit = '', '', 'bold', '#FF00FF', '#12FF14'
+# footer = '', '', '', '#121233', '#656599'
+#
+# See the default palettes in `khal/ui/colors.py` for all available keys.
+# If you can't theme an element in ikhal, please open an issue on `github
+# <https://github.com/pimutils/khal/issues/new/choose>`_.
+[palette]
diff --git a/khal/settings/utils.py b/khal/settings/utils.py
index f01c26fd..4dac9298 100644
--- a/khal/settings/utils.py
+++ b/khal/settings/utils.py
@@ -69,11 +69,10 @@ def is_timedelta(string: str) -> dt.timedelta:
raise VdtValueError(f"Invalid timedelta: {string}")
-def weeknumber_option(option: str) -> Union[str, Literal[False]]:
+def weeknumber_option(option: str) -> Union[Literal['left', 'right'], Literal[False]]:
"""checks if *option* is a valid value
:param option: the option the user set in the config file
- :type option: str
:returns: 'off', 'left', 'right' or False
"""
option = option.lower()
@@ -89,11 +88,10 @@ def weeknumber_option(option: str) -> Union[str, Literal[False]]:
"'off', 'left' or 'right'")
-def monthdisplay_option(option: str) -> str:
+def monthdisplay_option(option: str) -> Literal['firstday', 'firstfullweek']:
"""checks if *option* is a valid value
:param option: the option the user set in the config file
- :returns: firstday, firstfullweek
"""
option = option.lower()
if option == 'firstday':
@@ -215,6 +213,17 @@ def get_vdir_type(_: str) -> str:
# TODO implement
return 'calendar'
+def validate_palette_entry(attr, definition: str) -> bool:
+ if len(definition) not in (2, 3, 5):
+ logging.error('Invalid color definition for %s: %s, must be of length, 2, 3, or 5',
+ attr, definition)
+ return False
+ if (definition[0] not in COLORS and definition[0] != '') or \
+ (definition[1] not in COLORS and definition[1] != ''):
+ logging.error('Invalid color definition for %s: %s, must be one of %s',
+ attr, definition, COLORS.keys())
+ return False
+ return True
def config_checks(
config,
@@ -263,3 +272,11 @@ def config_checks(
if config['calendars'][calendar]['color'] == 'auto':
config['calendars'][calendar]['color'] = \
_get_color_from_vdir(config['calendars'][calendar]['path'])
+
+ # check palette settings
+ valid_palette = True
+ for attr in config.get('palette', []):
+ valid_palette = valid_palette and validate_palette_entry(attr, config['palette'][attr])
+ if not valid_palette:
+ logger.fatal('Invalid palette entry')
+ raise InvalidSettingsError()
diff --git a/khal/ui/__init__.py b/khal/ui/__init__.py
index 1a93f78a..806501b4 100644
--- a/khal/ui/__init__.py
+++ b/khal/ui/__init__.py
@@ -24,7 +24,7 @@ import logging
import signal
import sys
from enum import IntEnum
-from typing import Dict, List, Optional, Tuple
+from typing import Dict, List, Literal, Optional, Tuple
import click
import urwid
@@ -35,7 +35,7 @@ from ..khalendar.exceptions import ReadOnlyCalendarError
from . import colors
from .base import Pane, Window
from .editor import EventEditor, ExportDialog
-from .widgets import CalendarWidget, NColumns, NPile, button, linebox
+from .widgets import CalendarWidget, CAttrMap, NColumns, NPile, button, linebox
from .widgets import ExtendedEdit as Edit
logger = logging.getLogger('khal')
@@ -84,13 +84,13 @@ class DateConversionError(Exception):
class SelectableText(urwid.Text):
- def selectable(self):
+ def selectable(self) -> bool:
return True
- def keypress(self, size, key):
+ def keypress(self, size: Tuple[int], key: Optional[str]) -> Optional[str]:
return key
- def get_cursor_coords(self, size):
+ def get_cursor_coords(self, size: Tuple[int]) -> Tuple[int, int]:
return 0, 0
def render(self, size, focus=False):
@@ -140,7 +140,7 @@ class DateHeader(SelectableText):
return f'{weekday}, {daystr} ({approx_delta})'
- def keypress(self, _, key: str) -> str:
+ def keypress(self, size: Tuple[int], key: Optional[str]) -> Optional[str]:
binds = self._conf['keybindings']
if key in binds['left']:
key = 'left'
@@ -219,7 +219,7 @@ class U_Event(urwid.Text):
self.set_text(mark + ' ' + text.replace('\n', newline))
- def keypress(self, _, key: str) -> str:
+ def keypress(self, size: Tuple[int], key: Optional[str]) -> Optional[str]:
binds = self._conf['keybindings']
if key in binds['left']:
key = 'left'
@@ -250,7 +250,7 @@ class EventListBox(urwid.ListBox):
self.set_focus_date_callback = set_focus_date_callback
super().__init__(*args, **kwargs)
- def keypress(self, size, key):
+ def keypress(self, size: Tuple[int], key: Optional[str]) -> Optional[str]:
return super().keypress(size, key)
@property
@@ -306,7 +306,7 @@ class DListBox(EventListBox):
self.body.ensure_date(day)
self.clean()
- def keypress(self, size, key):
+ def keypress(self, size: Tuple[int], key: Optional[str]) -> Optional[str]:
if key in self._conf['keybindings']['up']:
key = 'up'
if key in self._conf['keybindings']['down']:
@@ -610,7 +610,7 @@ class DateListBox(urwid.ListBox):
title.set_text(title.get_text()[0])
@property
- def focus_event(self):
+ def focus_event(self) -> Optional[U_Event]:
if self.body.focus == 0:
return None
else:
@@ -639,17 +639,17 @@ class EventColumn(urwid.WidgetWrap):
self.divider = urwid.Divider('─')
self.editor = False
self._last_focused_date: Optional[dt.date] = None
- self._eventshown = False
+ self._eventshown: Optional[Tuple[str, str]] = None
self.event_width = int(self.pane._conf['view']['event_view_weighting'])
self.delete_status = pane.delete_status
self.toggle_delete_all = pane.toggle_delete_all
self.toggle_delete_instance = pane.toggle_delete_instance
- self.dlistbox: DateListBox = elistbox
+ self.dlistbox: DListBox = elistbox
self.container = urwid.Pile([self.dlistbox])
urwid.WidgetWrap.__init__(self, self.container)
@property
- def focus_event(self):
+ def focus_event(self) -> Optional[U_Event]:
"""returns the event currently in focus"""
return self.dlistbox.focus_event
@@ -677,7 +677,7 @@ class EventColumn(urwid.WidgetWrap):
self._last_focused_date = date
self.dlistbox.ensure_date(date)
- def update(self, min_date, max_date, everything):
+ def update(self, min_date, max_date: dt.date, everything: bool):
"""update DateListBox
if `everything` is True, reset all displayed dates, else only those between
@@ -692,7 +692,7 @@ class EventColumn(urwid.WidgetWrap):
max_date = self.dlistbox.body.last_date
self.dlistbox.body.update_range(min_date, max_date)
- def refresh_titles(self, min_date, max_date, everything):
+ def refresh_titles(self, min_date: dt.date, max_date: dt.date, everything: bool) -> None:
"""refresh titles in DateListBoxes
if `everything` is True, reset all displayed dates, else only those between
@@ -700,7 +700,7 @@ class EventColumn(urwid.WidgetWrap):
"""
self.dlistbox.refresh_titles(min_date, max_date, everything)
- def update_date_line(self):
+ def update_date_line(self) -> None:
"""refresh titles in DateListBoxes"""
self.dlistbox.update_date_line()
@@ -776,9 +776,9 @@ class EventColumn(urwid.WidgetWrap):
ContainerWidget = linebox[self.pane._conf['view']['frame']]
new_pane = urwid.Columns([
- ('weight', 2, ContainerWidget(editor)),
- ('weight', 1, ContainerWidget(self.dlistbox))
- ], dividechars=2, focus_column=0)
+ ('weight', 2, CAttrMap(ContainerWidget(editor), 'editor', 'editor focus')),
+ ('weight', 1, CAttrMap(ContainerWidget(self.dlistbox), 'reveal focus')),
+ ], dividechars=0, focus_column=0)
new_pane.title = editor.title
def teardown(data):
@@ -859,6 +859,8 @@ class EventColumn(urwid.WidgetWrap):
# because their title is determined by X-BIRTHDAY and X-FNAME properties
# which are also copied. If the events' summary is edited it will show
# up on disk but not be displayed in khal
+ if self.focus_event is None:
+ return None
event = self.focus_event.event.duplicate()
try:
self.pane.collection.insert(event)
@@ -910,9 +912,9 @@ class EventColumn(urwid.WidgetWrap):
def selectable(self):
return True
- def keypress(self, size, key):
+ def keypress(self, size: Tuple[int], key: Optional[str]) -> Optional[str]:
prev_shown = self._eventshown
- self._eventshown = False
+ self._eventshown = None
self.clear_event_view()
if key in self._conf['keybindings']['new']:
@@ -1021,9 +1023,10 @@ class SearchDialog(urwid.WidgetWrap):
class Search(Edit):
- def keypress(self, size, key):
+ def keypress(self, size: Tuple[int], key: Optional[str]) -> Optional[str]:
if key == 'enter':
search_func(self.text)
+ return None
else:
return super().keypress(size, key)
@@ -1076,8 +1079,13 @@ class ClassicView(Pane):
toggle_delete_instance=self.toggle_delete_instance,
dynamic_days=self._conf['view']['dynamic_days'],
)
- self.eventscolumn = ContainerWidget(EventColumn(pane=self, elistbox=elistbox))
- calendar = CalendarWidget(
+ self.eventscolumn = ContainerWidget(
+ CAttrMap(EventColumn(pane=self, elistbox=elistbox),
+ 'eventcolumn',
+ 'eventcolumn focus',
+ ),
+ )
+ calendar = CAttrMap(CalendarWidget(
on_date_change=self.eventscolumn.original_widget.set_focus_date,
keybindings=self._conf['keybindings'],
on_press={key: self.new_event for key in self._conf['keybindings']['new']},
@@ -1085,7 +1093,7 @@ class ClassicView(Pane):
weeknumbers=self._conf['locale']['weeknumbers'],
monthdisplay=self._conf['view']['monthdisplay'],
get_styles=collection.get_styles
- )
+ ), 'calendar', 'calendar focus')
if self._conf['view']['dynamic_days']:
elistbox.set_focus_date_callback = calendar.set_focus_date
else:
@@ -1140,7 +1148,7 @@ class ClassicView(Pane):
event = self.collection.delete_instance(href, etag, account, rec_id)
updated_etags[event.href] = event.etag
- def keypress(self, size, key: str):
+ def keypress(self, size: Tuple[int], key: Optional[str]) -> Optional[str]:
binds = self._conf['keybindings']
if key in binds['search']:
self.search()
@@ -1203,11 +1211,19 @@ class ClassicView(Pane):
def _urwid_palette_entry(
- name: str, color: str, hmethod: str) -> Tuple[str, str, str, str, str, str]:
+ name: str, color: str, hmethod: str, color_mode: Literal['256colors', 'rgb'],
+ foreground: str = '', background: str = '',
+) -> Tuple[str, str, str, str, str, str]:
"""Create an urwid compatible palette entry.
:param name: name of the new attribute in the palette
:param color: color for the new attribute
+ :param hmethod: which highlighting mode to use, foreground or background
+ :param color_mode: which color mode we are in, if we are in 256-color mode,
+ we transform 24-bit/RGB colors to a (somewhat) matching 256-color set color
+ :param foreground: the foreground color to apply if we use background highlighting method
+ :param background: the background color to apply if we use foreground highlighting method
+
:returns: an urwid palette entry
"""
from ..terminal import COLORS
@@ -1218,9 +1234,8 @@ def _urwid_palette_entry(
# Colors from the 256 color palette need to be prefixed with h in
# urwid.
color = 'h' + color
- else:
- # 24-bit colors are not supported by urwid.
- # Convert it to some color on the 256 color palette that might resemble
+ elif color_mode == '256color':
+ # Convert to some color on the 256 color palette that might resemble
# the 24-bit color.
# First, generate the palette (indices 16-255 only). This assumes, that
# the terminal actually uses the same palette, which may or may not be
@@ -1263,34 +1278,83 @@ def _urwid_palette_entry(
# We unconditionally add the color to the high color slot. It seems to work
# in lower color terminals as well.
if hmethod in ['fg', 'foreground']:
- return (name, '', '', '', color, '')
+ return (name, '', '', '', color, background)
else:
- return (name, '', '', '', '', color)
+ return (name, '', '', '', foreground, color)
-def _add_calendar_colors(palette: List, collection: CalendarCollection) -> List:
+def _add_calendar_colors(
+ palette: List[Tuple[str, ...]],
+ collection: 'CalendarCollection',
+ color_mode: Literal['256colors', 'rgb'],
+ base: Optional[str] = None,
+ attr_template: str = 'calendar {}',
+) -> List[Tuple[str, ...]]:
"""Add the colors for the defined calendars to the palette.
+ We support setting a fixed background or foreground color that we extract
+ from a giving attribute
+
:param palette: the base palette
:param collection:
+ :param color_mode: which color mode we are in
+ :param base: the attribute to extract the background and foreground color from
+ :param attr_template: the template to use for the attribute name
:returns: the modified palette
"""
+ bg_color, fg_color = '', ''
+ for attr in palette:
+ if base and attr[0] == base:
+ if color_mode == 'rgb' and len(attr) >= 5:
+ bg_color = attr[5]
+ fg_color = attr[4]
+ else:
+ bg_color = attr[2]
+ fg_color = attr[1]
+
for cal in collection.calendars:
if cal['color'] == '':
# No color set for this calendar, use default_color instead.
color = collection.default_color
else:
color = cal['color']
- palette.append(_urwid_palette_entry('calendar ' + cal['name'], color,
- collection.hmethod))
- palette.append(_urwid_palette_entry('highlight_days_color',
- collection.color, collection.hmethod))
- palette.append(_urwid_palette_entry('highlight_days_multiple',
- collection.multiple, collection.hmethod))
+ entry = _urwid_palette_entry(
+ attr_template.format(cal['name']),
+ color,
+ collection.hmethod,
+ color_mode=color_mode,
+ foreground=fg_color,
+ background=bg_color,
+ )
+ palette.append(entry)
+
+ entry = _urwid_palette_entry(
+ 'highlight_days_color',
+ collection.color,
+ collection.hmethod,
+ color_mode=color_mode,
+ foreground=fg_color,
+ background=bg_color,
+ )
+ palette.append(entry)
+ entry = _urwid_palette_entry('highlight_days_multiple',
+ collection.multiple,
+ collection.hmethod,
+ color_mode=color_mode,
+ foreground=fg_color,
+ background=bg_color)
+ palette.append(entry)
+
return palette
-def start_pane(pane, callback, program_info='', quit_keys=None):
+def start_pane(
+ pane,
+ callback,
+ program_info='',
+ quit_keys=None,
+ color_mode: Literal['rgb', '256colors']='rgb',
+):
"""Open the user interface with the given initial pane."""
quit_keys = quit_keys or ['q']
@@ -1342,8 +1406,27 @@ def start_pane(pane, callback, program_info='', quit_keys=None):
logger.addHandler(header_handler)
frame.open(pane, callback)
+ theme = getattr(colors, pane._conf['view']['theme'])
palette = _add_calendar_colors(
- getattr(colors, pane._conf['view']['theme']), pane.collection)
+ theme, pane.collection, color_mode=color_mode,
+ base='calendar', attr_template='calendar {}',
+ )
+ palette = _add_calendar_colors(
+ palette, pane.collection, color_mode=color_mode,
+ base='popupbg', attr_template='calendar {} popup',
+ )
+
+ def merge_palettes(pallete_a, pallete_b) -> List[Tuple[str, ...]]:
+ """Merge two palettes together, with the second palette taking priority."""
+ merged = {}
+ for entry in pallete_a:
+ merged[entry[0]] = entry
+ for entry in pallete_b:
+ merged[entry[0]] = entry
+ return list(merged.values())
+
+ overwrite = [(key, *values) for key, values in pane._conf['palette'].items()]
+ palette = merge_palettes(palette, overwrite)
loop = urwid.MainLoop(
widget=frame,
palette=palette,
@@ -1376,9 +1459,11 @@ def start_pane(pane, callback, program_info='', quit_keys=None):
loop.set_alarm_in(60, check_for_updates, pane)
loop.set_alarm_in(60, check_for_updates, pane)
- # Make urwid use 256 color mode.
+
+ colors_ = 2**24 if color_mode == 'rgb' else 256
loop.screen.set_terminal_properties(
- colors=256, bright_is_bold=pane._conf['view']['bold_for_light_color'])
+ colors=colors_, bright_is_bold=pane._conf['view']['bold_for_light_color'],
+ )
def ctrl_c(signum, f):
raise urwid.ExitMainLoop()
diff --git a/khal/ui/colors.py b/khal/ui/colors.py
index f218dd93..5cffb116 100644
--- a/khal/ui/colors.py
+++ b/khal/ui/colors.py
@@ -19,17 +19,18 @@
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+from typing import Dict, List, Tuple
dark = [
('header', 'white', 'black'),
('footer', 'white', 'black'),
('line header', 'black', 'white', 'bold'),
('alt header', 'white', '', 'bold'),
- ('bright', 'dark blue', 'white', ('bold', 'standout')),
+ ('bright', 'dark blue', 'white', 'bold,standout'),
('list', 'black', 'white'),
('list focused', 'white', 'light blue', 'bold'),
('edit', 'black', 'white'),
- ('edit focused', 'white', 'light blue', 'bold'),
+ ('edit focus', 'white', 'light blue', 'bold'),
('button', 'black', 'dark cyan'),
('button focused', 'white', 'light blue', 'bold'),
@@ -44,7 +45,6 @@ dark = [
('dayname', 'light gray', ''),
('monthname', 'light gray', ''),
('weeknumber_right', 'light gray', ''),
- ('edit', 'white', 'dark blue'),
('alert', 'white', 'dark red'),
('mark', 'white', 'dark green'),
('frame', 'white', 'black'),
@@ -52,6 +52,11 @@ dark = [
('frame focus color', 'dark blue', 'black'),
('frame focus top', 'dark magenta', 'black'),
+ ('eventcolumn', '', '', ''),
+ ('eventcolumn focus', '', '', ''),
+ ('calendar', '', '', ''),
+ ('calendar focus', '', '', ''),
+
('editbx', 'light gray', 'dark blue'),
('editcp', 'black', 'light gray', 'standout'),
('popupbg', 'white', 'black', 'bold'),
@@ -63,11 +68,11 @@ light = [
('footer', 'black', 'white'),
('line header', 'black', 'white', 'bold'),
('alt header', 'black', '', 'bold'),
- ('bright', 'dark blue', 'white', ('bold', 'standout')),
+ ('bright', 'dark blue', 'white', 'bold,standout'),
('list', 'black', 'white'),
('list focused', 'white', 'light blue', 'bold'),
('edit', 'black', 'white'),
- ('edit focused', 'white', 'light blue', 'bold'),
+ ('edit focus', 'white', 'light blue', 'bold'),
('button', 'black', 'dark cyan'),
('button focused', 'white', 'light blue', 'bold'),
@@ -76,13 +81,12 @@ light = [
('today', 'black', 'light gray'),
('date header', '', 'white'),
- ('date header focused', 'white', 'dark gray', ('bold', 'standout')),
+ ('date header focused', 'white', 'dark gray', 'bold,standout'),
('date header selected', 'dark gray', 'light cyan'),
('dayname', 'dark gray', 'white'),
('monthname', 'dark gray', 'white'),
('weeknumber_right', 'dark gray', 'white'),
- ('edit', 'white', 'dark blue'),
('alert', 'white', 'dark red'),
('mark', 'white', 'dark green'),
('frame', 'dark gray', 'white'),
@@ -90,9 +94,16 @@ light = [
('frame focus color', 'dark blue', 'white'),
('frame focus top', 'dark magenta', 'white'),
+ ('eventcolumn', '', '', ''),
+ ('eventcolumn focus', '', '', ''),
+ ('calendar', '', '', ''),
+ ('calendar focus', '', '', ''),
+
('editbx', 'light gray', 'dark blue'),
('editcp', 'black', 'light gray', 'standout'),
('popupbg', 'white', 'black', 'bold'),
('popupper', 'black', 'light gray'),
('caption', 'black', '', ''),
]
+
+themes: Dict[str, List[Tuple[str, ...]]] = {'light': light, 'dark': dark}
diff --git a/khal/ui/editor.py b/khal/ui/editor.py
index fa6498f1..00a1a097 100644
--- a/khal/ui/editor.py
+++ b/khal/ui/editor.py
@@ -20,7 +20,7 @@
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import datetime as dt
-from typing import Callable, Dict, List, Literal, Optional
+from typing import TYPE_CHECKING, Callable, Dict, List, Literal, Optional, Tuple
import urwid
@@ -43,6 +43,8 @@ from .widgets import (
button,
)
+if TYPE_CHECKING:
+ import khal.khalendar.event
class StartEnd:
@@ -55,7 +57,7 @@ class StartEnd:
class CalendarPopUp(urwid.PopUpLauncher):
- def __init__(self, widget, on_date_change, weeknumbers=False,
+ def __init__(self, widget, on_date_change, weeknumbers: Literal['left', 'right', False]=False,
firstweekday=0, monthdisplay='firstday', keybindings=None) -> None:
self._on_date_change = on_date_change
self._weeknumbers = weeknumbers
@@ -88,7 +90,8 @@ class CalendarPopUp(urwid.PopUpLauncher):
weeknumbers=self._weeknumbers,
monthdisplay=self._monthdisplay,
initial=initial_date)
- pop_up = urwid.LineBox(pop_up)
+ pop_up = CAttrMap(pop_up, 'calendar', ' calendar focus')
+ pop_up = CAttrMap(urwid.LineBox(pop_up), 'calendar', 'calendar focus')
return pop_up
def get_pop_up_parameters(self):
@@ -125,10 +128,13 @@ class DateEdit(urwid.WidgetWrap):
on_date_change=on_date_change)
wrapped = CalendarPopUp(self._edit, on_date_change, weeknumbers,
firstweekday, monthdisplay, keybindings)
- padded = urwid.Padding(wrapped, align='left', width=datewidth, left=0, right=1)
+ padded = CAttrMap(
+ urwid.Padding(wrapped, align='left', width=datewidth, left=0, right=1),
+ 'calendar', 'calendar focus',
+ )
super().__init__(padded)
- def _validate(self, text):
+ def _validate(self, text: str):
try:
_date = dt.datetime.strptime(text, self._dateformat).date()
except ValueError:
@@ -157,14 +163,15 @@ class DateEdit(urwid.WidgetWrap):
class StartEndEditor(urwid.WidgetWrap):
"""Widget for editing start and end times (of an event)."""
- def __init__(self, start, end, conf,
+ def __init__(self,
+ start: dt.datetime,
+ end: dt.datetime,
+ conf,
on_start_date_change=lambda x: None,
on_end_date_change=lambda x: None,
) -> None:
"""
- :type start: datetime.datetime
- :type end: datetime.datetime
:param on_start_date_change: a callable that gets called everytime a new
start date is entered, with that new date as an argument
:param on_end_date_change: same as for on_start_date_change, just for the
@@ -172,8 +179,10 @@ class StartEndEditor(urwid.WidgetWrap):
"""
self.allday = not isinstance(start, dt.datetime)
self.conf = conf
- self._startdt, self._original_start = start, start
- self._enddt, self._original_end = end, end
+ self._startdt: dt.date = start
+ self._original_start: dt.date = start
+ self._enddt: dt.date = end
+ self._original_end: dt.date = end
self.on_start_date_change = on_start_date_change
self.on_end_date_change = on_end_date_change
self._datewidth = len(start.strftime(self.conf['locale']['longdateformat']))
@@ -256,7 +265,7 @@ class StartEndEditor(urwid.WidgetWrap):
self._enddt = self.localize_end(dt.datetime.combine(date, self._end_time))
self.on_end_date_change(date)
- def toggle(self, checkbox, state):
+ def toggle(self, checkbox, state: bool):
"""change from allday to datetime event
:param checkbox: the checkbox instance that is used for toggling, gets
@@ -264,13 +273,14 @@ class StartEndEditor(urwid.WidgetWrap):
:type checkbox: checkbox
:param state: state the event will toggle to;
True if allday event, False if datetime
- :type state: bool
"""
if self.allday is True and state is False:
self._startdt = dt.datetime.combine(self._startdt, dt.datetime.min.time())
self._enddt = dt.datetime.combine(self._enddt, dt.datetime.min.time())
elif self.allday is False and state is True:
+ assert isinstance(self._startdt, dt.datetime)
+ assert isinstance(self._enddt, dt.datetime)
self._startdt = self._startdt.date()
self._enddt = self._enddt.date()
self.allday = state
@@ -336,14 +346,18 @@ class StartEndEditor(urwid.WidgetWrap):
class EventEditor(urwid.WidgetWrap):
"""Widget that allows Editing one `Event()`"""
- def __init__(self, pane, event, save_callback=None, always_save=False) -> None:
+ def __init__(
+ self,
+ pane,
+ event: 'khal.khalendar.event.Event',
+ save_callback=None,
+ always_save: bool=False,
+ ) -> None:
"""
- :type event: khal.event.Event
:param save_callback: call when saving event with new start and end
dates and recursiveness of original and edited event as parameters
:type save_callback: callable
:param always_save: save event even if it has not changed
- :type always_save: bool
"""
self.pane = pane
self.event = event
@@ -369,13 +383,14 @@ class EventEditor(urwid.WidgetWrap):
self.event.recurobject, self._conf, event.start_local,
)
self.summary = urwid.AttrMap(ExtendedEdit(
- caption=('caption', 'Title: '), edit_text=event.summary), 'edit'
+ caption=('caption', 'Title: '), edit_text=event.summary), 'edit', 'edit focus',
)
divider = urwid.Divider(' ')
- def decorate_choice(c):
- return ('calendar ' + c['name'], c['name'])
+ def decorate_choice(c) -> Tuple[str, str]:
+ return ('calendar ' + c['name'] + ' popup', c['name'])
+
self.calendar_chooser= CAttrMap(Choice(
[self.collection._calendars[c] for c in self.collection.writable_names],
self.collection._calendars[self.event.calendar],
@@ -388,13 +403,13 @@ class EventEditor(urwid.WidgetWrap):
edit_text=self.description,
multiline=True
),
- 'edit'
+ 'edit', 'edit focus',
)
self.location = urwid.AttrMap(ExtendedEdit(
- caption=('caption', 'Location: '), edit_text=self.location), 'edit'
+ caption=('caption', 'Location: '), edit_text=self.location), 'edit', 'edit focus',
)
self.categories = urwid.AttrMap(ExtendedEdit(
- caption=('caption', 'Categories: '), edit_text=self.categories), 'edit'
+ caption=('caption', 'Categories: '), edit_text=self.categories), 'edit', 'edit focus',
)
self.attendees = urwid.AttrMap(
ExtendedEdit(
@@ -402,10 +417,10 @@ class EventEditor(urwid.WidgetWrap):
edit_text=self.attendees,
multiline=True
),
- 'edit'
+ 'edit', 'edit focus',
)
self.url = urwid.AttrMap(ExtendedEdit(
- caption=('caption', 'URL: '), edit_text=self.url), 'edit'
+ caption=('caption', 'URL: '), edit_text=self.url), 'edit', 'edit focus',
)
self.alarms = AlarmsEditor(self.event)
self.pile = NListBox(urwid.SimpleFocusListWalker([
@@ -554,17 +569,17 @@ class EventEditor(urwid.WidgetWrap):
self._abort_confirmed = False
self.pane.window.backtrack()
- def keypress(self, size, key):
+ def keypress(self, size: Tuple[int], key: str) -> Optional[str]:
if key in ['esc'] and self.changed and not self._abort_confirmed:
self.pane.window.alert(
('light red', 'Unsaved changes! Hit ESC again to discard.'))
self._abort_confirmed = True
- return
+ return None
else:
self._abort_confirmed = False
if key in self.pane._conf['keybindings']['save']:
self.save(None)
- return
+ return None
return super().keypress(size, key)
@@ -805,8 +820,8 @@ class ExportDialog(urwid.WidgetWrap):
caption='Location: ', edit_text="~/%s.ics" % event.summary.strip())
lines.append(export_location)
lines.append(urwid.Divider(' '))
- lines.append(
- urwid.Button('Save', on_press=this_func, user_data=export_location)
- )
+ lines.append(CAttrMap(
+ urwid.Button('Save', on_press=this_func, user_data=export_location),
+ 'button', 'button focus'))
content = NPile(lines)
urwid.WidgetWrap.__init__(self, urwid.LineBox(content))
diff --git a/khal/ui/widgets.py b/khal/ui/widgets.py
index 7be6413b..9f12dcc0 100644
--- a/khal/ui/widgets.py
+++ b/khal/ui/widgets.py
@@ -26,7 +26,7 @@ if they are large, into their own files
"""
import datetime as dt
import re
-from typing import Optional, Tuple
+from typing import List, Optional, Tuple
import urwid
@@ -77,7 +77,7 @@ def goto_end_of_line(text):
class ExtendedEdit(urwid.Edit):
"""A text editing widget supporting some more editing commands"""
- def keypress(self, size, key: str) -> Optional[Tuple[Tuple[int, int], str]]:
+ def keypress(self, size: Tuple[int], key: Optional[str]) -> Optional[str]:
if key == 'ctrl w':
self._delete_word()
elif key == 'ctrl u':
@@ -201,7 +201,8 @@ class TimeWidget(DateTimeWidget):
class Choice(urwid.PopUpLauncher):
def __init__(
- self, choices, active, decorate_func=None, overlay_width=32, callback=lambda: None,
+ self, choices: List[str], active: str,
+ decorate_func=None, overlay_width: int=32, callback=lambda: None,
) -> None:
self.choices = choices
self._callback = callback
@@ -211,9 +212,7 @@ class Choice(urwid.PopUpLauncher):
def create_pop_up(self):
pop_up = ChoiceList(self, callback=self._callback)
- urwid.connect_signal(
- pop_up, 'close', lambda button: self.close_pop_up(),
- )
+ urwid.connect_signal(pop_up, 'close', lambda button: self.close_pop_up())
return pop_up
def get_pop_up_parameters(self):
@@ -235,8 +234,7 @@ class Choice(urwid.PopUpLauncher):
self._active = val
self.button = urw