Repository: pimutils/khal

Issue:
100% CPU load if allday event is moved some years in ikhal
Hi,

I am running khal 0.10.3 and I noticed an odd bug.
I note down birthdays in ikhal as allday events with yearly repetition. Whenever I happen to know the exact year of birth I set it as the starting date otherwise I just take the current year.

Now if I try to edit a birthday and set it from 2021 and set the start to 1989 ikhal freezes and the process and takes up 100% CPU time. 

I presume this happens because some calculation on the event length is done, which for a moment becomes very long. But this happens before I can set back the end date so I cant do anything to work around this.

Discussion:

Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 970695a..789168e 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -7,6 +7,13 @@ Package maintainers and users who have to manually update their installation
 may want to subscribe to `GitHub's tag feed
 <https://github.com/geier/khal/tags.atom>`_.
 
+0.11.3
+======
+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
 
 0.11.2
 ======
diff --git a/khal/controllers.py b/khal/controllers.py
index 64f43e6..cb78930 100644
--- a/khal/controllers.py
+++ b/khal/controllers.py
@@ -33,7 +33,12 @@ import pytz
 from click import confirm, echo, prompt, style
 
 from khal import __productname__, __version__, calendar_display, parse_datetime, utils
-from khal.custom_types import EventCreationTypes, LocaleConfiguration
+from khal.custom_types import (
+    EventCreationTypes,
+    LocaleConfiguration,
+    MonthDisplayType,
+    WeekNumbersType,
+)
 from khal.exceptions import DateTimeParseError, FatalError
 from khal.khalendar import CalendarCollection
 from khal.khalendar.event import Event
@@ -48,7 +53,7 @@ from .terminal import merge_columns
 logger = logging.getLogger('khal')
 
 
-def format_day(day, format_string: str, locale, attributes=None):
+def format_day(day: dt.date, format_string: str, locale, attributes=None):
     if attributes is None:
         attributes = {}
 
@@ -68,23 +73,28 @@ def format_day(day, format_string: str, locale, attributes=None):
         raise KeyError("cannot format day with: %s" % format_string)
 
 
-def calendar(collection, agenda_format=None, notstarted=False, once=False, daterange=None,
-             day_format=None,
-             locale=None,
-             conf=None,
-             firstweekday=0,
-             weeknumber=False,
-             monthdisplay='firstday',
-             hmethod='fg',
-             default_color='',
-             multiple='',
-             multiple_on_overflow=False,
-             color='',
-             highlight_event_days=0,
-             full=False,
-             bold_for_light_color=True,
-             env=None,
-             ):
+def calendar(
+    collection: CalendarCollection,
+    agenda_format=None,
+    notstarted: bool=False,
+    once=False,
+    daterange=None,
+    day_format=None,
+    locale=None,
+    conf=None,
+    firstweekday: int=0,
+    weeknumber: WeekNumbersType=False,
+    monthdisplay: MonthDisplayType='firstday',
+    hmethod: str='fg',
+    default_color: str='',
+    multiple='',
+    multiple_on_overflow: bool=False,
+    color='',
+    highlight_event_days=0,
+    full=False,
+    bold_for_light_color: bool=True,
+    env=None,
+    ):
     term_width, _ = get_terminal_size()
     lwidth = 27 if conf['locale']['weeknumbers'] == 'right' else 25
     rwidth = term_width - lwidth - 4
@@ -163,7 +173,7 @@ def get_events_between(
     agenda_format: str,
     notstarted: bool,
     env: dict,
-    width,
+    width: Optional[int],
     seen,
     original_start: dt.datetime,
 ) -> List[str]:
@@ -230,7 +240,7 @@ def khal_list(
     day_format: Optional[str]=None,
     once=False,
     notstarted: bool = False,
-    width: bool = False,
+    width: Optional[int] = None,
     env=None,
     datepoint=None,
 ):
diff --git a/khal/custom_types.py b/khal/custom_types.py
index 80f88d7..9700007 100644
--- a/khal/custom_types.py
+++ b/khal/custom_types.py
@@ -1,6 +1,6 @@
 import datetime as dt
 import os
-from typing import List, Optional, Protocol, Tuple, TypedDict, Union
+from typing import List, Literal, Optional, Protocol, Tuple, TypedDict, Union
 
 import pytz
 
@@ -77,3 +77,6 @@ class EventCreationTypes(TypedDict):
 
 
 PathLike = Union[str, os.PathLike]
+
+WeekNumbersType = Literal['left', 'right', False]
+MonthDisplayType = Literal['firstday', 'firstfullweek']
diff --git a/khal/icalendar.py b/khal/icalendar.py
index 5ae1787..cfe9811 100644
--- a/khal/icalendar.py
+++ b/khal/icalendar.py
@@ -38,13 +38,12 @@ from .utils import generate_random_uid, localize_strip_tz, to_unix_time
 logger = logging.getLogger('khal')
 
 
-def split_ics(ics: str, random_uid: bool=False, default_timezone=None):
+def split_ics(ics: str, random_uid: bool=False, default_timezone=None) -> List:
     """split an ics string into several according to VEVENT's UIDs
 
     and sort the right VTIMEZONEs accordingly
     ignores all other ics components
     :param random_uid: assign random uids to all events
-    :rtype list:
     """
     cal = cal_from_ics(ics)
     tzs = {}
@@ -217,7 +216,10 @@ def ics_from_list(
     return calendar.to_ical().decode('utf-8')
 
 
-def expand(vevent, href=''):
+def expand(
+    vevent: icalendar.Event,
+    href: str='',
+) -> Optional[List[Tuple[dt.datetime, dt.datetime]]]:
     """
     Constructs a list of start and end dates for all recurring instances of the
     event defined in vevent.
@@ -229,12 +231,9 @@ def expand(vevent, href=''):
     the function still returns a tuple of start and end (date)times.
 
     :param vevent: vevent to be expanded
-    :type vevent: icalendar.cal.Event
     :param href: the href of the vevent, used for more informative logging and
                  nothing else
-    :type href: str
     :returns: list of start and end (date)times of the expanded event
-    :rtype: list(tuple(datetime, datetime))
     """
     # we do this now and than never care about the "real" end time again
     if 'DURATION' in vevent:
@@ -249,7 +248,7 @@ def expand(vevent, href=''):
     events_tz = getattr(vevent['DTSTART'].dt, 'tzinfo', None)
     allday = not isinstance(vevent['DTSTART'].dt, dt.datetime)
 
-    def sanitize_datetime(date):
+    def sanitize_datetime(date: dt.date) -> dt.date:
         if allday and isinstance(date, dt.datetime):
             date = date.date()
         if events_tz is not None:
@@ -274,20 +273,24 @@ def expand(vevent, href=''):
             ignoretz=True,
         )
 
-        if rrule._until is None:
+        # telling mypy, that _until exists
+        # we are very sure (TM) that rrulestr always returns a rrule, not a
+        # rruleset (which wouldn't have a _until attribute)
+        if rrule._until is None:  # type: ignore
             # rrule really doesn't like to calculate all recurrences until
             # eternity, so we only do it until 2037, because a) I'm not sure
             # if python can deal with larger datetime values yet and b) pytz
             # doesn't know any larger transition times
-            rrule._until = dt.datetime(2037, 12, 31)
+            rrule._until = dt.datetime(2037, 12, 31)  # type: ignore
         else:
             if events_tz and 'Z' in rrule_param.to_ical().decode():
-                rrule._until = pytz.UTC.localize(
-                    rrule._until).astimezone(events_tz).replace(tzinfo=None)
+                assert isinstance(rrule._until, dt.datetime)  # type: ignore
+                rrule._until = pytz.UTC.localize(  # type: ignore
+                    rrule._until).astimezone(events_tz).replace(tzinfo=None)  # type: ignore
 
             # rrule._until and dtstart could be dt.date or dt.datetime. They
             # need to be the same for comparison
-            testuntil = rrule._until
+            testuntil = rrule._until  # type: ignore
             if (type(dtstart) == dt.date and type(testuntil) == dt.datetime):
                 testuntil = testuntil.date()
             teststart = dtstart
@@ -298,15 +301,15 @@ def expand(vevent, href=''):
                 logger.warning(
                     f'{href}: Unsupported recurrence. UNTIL is before DTSTART.\n'
                     'This event will not be available in khal.')
-                return False
+                return None
 
         if rrule.count() == 0:
             logger.warning(
                 f'{href}: Recurrence defined but will never occur.\n'
                 'This event will not be available in khal.')
-            return False
+            return None
 
-        rrule = map(sanitize_datetime, rrule)
+        rrule = map(sanitize_datetime, rrule)  # type: ignore
 
         logger.debug(f'calculating recurrence dates for {href}, this might take some time.')
 
@@ -362,24 +365,24 @@ def assert_only_one_uid(cal: icalendar.Calendar):
         return True
 
 
-def sanitize(vevent, default_timezone, href='', calendar=''):
+def sanitize(
+    vevent: icalendar.Event,
+    default_timezone: pytz.BaseTzInfo,
+    href: str='',
+    calendar: str='',
+) -> icalendar.Event:
     """
     clean up vevents we do not understand
 
     :param vevent: the vevent that needs to be cleaned
-    :type vevent: icalendar.cal.Event
     :param default_timezone: timezone to apply to start and/or end dates which
          were supposed to be localized but which timezone was not understood
          by icalendar
-    :type timezone: pytz.timezone
     :param href: used for logging to inform user which .ics files are
         problematic
-    :type href: str
     :param calendar: used for logging to inform user which .ics files are
         problematic
-    :type calendar: str
     :returns: clean vevent
-    :rtype: icalendar.cal.Event
     """
     # convert localized datetimes with timezone information we don't
     # understand to the default timezone
diff --git a/khal/khalendar/backend.py b/khal/khalendar/backend.py
index 25b3b29..4ef898d 100644
--- a/khal/khalendar/backend.py
+++ b/khal/khalendar/backend.py
@@ -28,7 +28,7 @@ import logging
 import sqlite3
 from enum import IntEnum
 from os import makedirs, path
-from typing import Any, Dict, Iterable, Iterator, List, Optional, Tuple, Union
+from typing import Any, Iterable, Iterator, List, Optional, Tuple, Union
 
 import icalendar
 import icalendar.cal
@@ -36,7 +36,7 @@ import pytz
 from dateutil import parser
 
 from .. import utils
-from ..custom_types import EventTuple
+from ..custom_types import EventTuple, LocaleConfiguration
 from ..icalendar import assert_only_one_uid, cal_from_ics
 from ..icalendar import expand as expand_vevent
 from ..icalendar import sanitize as sanitize_vevent
@@ -75,7 +75,7 @@ class SQLiteDb:
     def __init__(self,
                  calendars: Iterable[str],
                  db_path: Optional[str],
-                 locale: Dict[str, str],
+                 locale: LocaleConfiguration,
                  ) -> None:
         assert db_path is not None
         self.calendars: List[str] = list(calendars)
diff --git a/khal/khalendar/khalendar.py b/khal/khalendar/khalendar.py
index 5814c25..fc22f28 100644
--- a/khal/khalendar/khalendar.py
+++ b/khal/khalendar/khalendar.py
@@ -32,7 +32,7 @@ import os
 import os.path
 from typing import Any, Dict, Iterable, List, Optional, Set, Tuple, Union  # noqa
 
-from ..custom_types import CalendarConfiguration, EventCreationTypes
+from ..custom_types import CalendarConfiguration, EventCreationTypes, LocaleConfiguration
 from ..icalendar import new_vevent
 from . import backend
 from .event import Event
@@ -69,10 +69,10 @@ class CalendarCollection:
                  color: str='',
                  priority: int=10,
                  highlight_event_days: bool=False,
-                 locale: Optional[Dict[str, Any]]=None,
+                 locale: Optional[LocaleConfiguration]=None,
                  dbpath: Optional[str]=None,
                  ) -> None:
-        locale = locale or {}
+        assert locale
         assert dbpath is not None
         assert calendars is not None
 
diff --git a/khal/parse_datetime.py b/khal/parse_datetime.py
index 34b3784..c3915b2 100644
--- a/khal/parse_datetime.py
+++ b/khal/parse_datetime.py
@@ -287,8 +287,6 @@ def guesstimedeltafstr(delta_string: str) -> dt.timedelta:
     """parses a timedelta from a string
 
     :param delta_string: string encoding time-delta, e.g. '1h 15m'
-    :type delta_string: str
-    :rtype: datetime.timedelta
     """
 
     tups = re.split(r'(-?\d+)', delta_string)
@@ -330,7 +328,6 @@ def guessrangefstr(daterange: Union[str, List[str]],
     """parses a range string
 
     :param daterange: date1 [date2 | timedelta]
-    :type daterange: str or list
     :param locale:
     :returns: start and end of the date(time) range  and if
         this is an all-day time range or not,
diff --git a/khal/ui/__init__.py b/khal/ui/__init__.py
index 3643e5b..5f5df56 100644
--- a/khal/ui/__init__.py
+++ b/khal/ui/__init__.py
@@ -23,12 +23,14 @@ import datetime as dt
 import logging
 import signal
 import sys
-from typing import Optional
+from enum import IntEnum
+from typing import Dict, List, Optional, Tuple
 
 import click
 import urwid
 
 from .. import utils
+from ..khalendar import CalendarCollection
 from ..khalendar.exceptions import ReadOnlyCalendarError
 from . import colors
 from .base import Pane, Window
@@ -72,8 +74,9 @@ logger = logging.getLogger('khal')
 #   │ └─────────────────┘  └──────────────────────────────────┘ │
 #   └───────────────────────────────────────────────────────────┘
 
-ALL = 1
-INSTANCES = 2
+class DeletionType(IntEnum):
+    ALL = 0
+    INSTANCES = 1
 
 
 class DateConversionError(Exception):
@@ -99,11 +102,10 @@ class SelectableText(urwid.Text):
 
 
 class DateHeader(SelectableText):
-    def __init__(self, day, dateformat, conf):
+    def __init__(self, day: dt.date, dateformat: str, conf):
         """
-        :type day: datetime.date
-        :type dateformat: format to print `day` in
-        :type dateformat: str
+        :param day: the date that is represented by this DateHeader instance
+        :param dateformat: format to print `day` in
         """
         self._day = day
         self._dateformat = dateformat
@@ -111,21 +113,18 @@ class DateHeader(SelectableText):
         super().__init__('', wrap='clip')
         self.update_date_line()
 
-    def update_date_line(self):
+    def update_date_line(self) -> None:
         """update self, so that the timedelta is accurate
 
         to be called after a date change
         """
         self.set_text(self.relative_day(self._day, self._dateformat))
 
-    def relative_day(self, day, dtformat):
+    def relative_day(self, day: dt.date, dtformat: str) -> str:
         """convert day into a string with its weekday and relative distance to today
 
         :param day: day to be converted
-        :type: day: datetime.day
         :param dtformat: the format day is to be printed in, passed to strftime
-        :type dtformat: str
-        :rtype: str
         """
 
         weekday = day.strftime('%A')
@@ -145,7 +144,7 @@ class DateHeader(SelectableText):
             day=daystr,
         )
 
-    def keypress(self, _, key):
+    def keypress(self, _, key: str) -> str:
         binds = self._conf['keybindings']
         if key in binds['left']:
             key = 'left'
@@ -177,7 +176,7 @@ class U_Event(urwid.Text):
         super().__init__('', wrap='clip')
         self.set_title()
 
-    def get_cursor_coords(self, size):
+    def get_cursor_coords(self, size) -> Tuple[int, int]:
         return 0, 0
 
     def render(self, size, focus=False):
@@ -188,20 +187,24 @@ class U_Event(urwid.Text):
         return canv
 
     @classmethod
-    def selectable(cls):
+    def selectable(cls) -> bool:
         return True
 
     @property
-    def uid(self):
+    def uid(self) -> str:
         return self.event.calendar + '\n' + \
             str(self.event.href) + '\n' + str(self.event.etag)
 
     @property
-    def recuid(self):
+    def recuid(self) -> Tuple[str, str]:
         return (self.uid, self.event.recurrence_id)
 
-    def set_title(self, mark=' '):
-        mark = {ALL: 'D', INSTANCES: 'd', False: ''}[self.delete_status(self.recuid)]
+    def set_title(self, mark: str=' ') -> None:
+        mark = {
+            DeletionType.ALL: 'D',
+            DeletionType.INSTANCES: 'd',
+            None: '',
+        }[self.delete_status(self.recuid)]
         if self.relative:
             format_ = self._conf['view']['agenda_event_format']
         else:
@@ -220,7 +223,7 @@ class U_Event(urwid.Text):
 
         self.set_text(mark + ' ' + text.replace('\n', newline))
 
-    def keypress(self, _, key):
+    def keypress(self, _, key: str) -> str:
         binds = self._conf['keybindings']
         if key in binds['left']:
             key = 'left'
@@ -241,8 +244,8 @@ class EventListBox(urwid.ListBox):
             delete_status, toggle_delete_instance, toggle_delete_all,
             set_focus_date_callback=None,
             **kwargs):
-        self._init = True
-        self.parent = parent
+        self._init: bool = True
+        self.parent: 'ClassicView' = parent
         self.delete_status = delete_status
         self.toggle_delete_instance = toggle_delete_instance
         self.toggle_delete_all = toggle_delete_all
@@ -255,7 +258,7 @@ class EventListBox(urwid.ListBox):
         return super().keypress(size, key)
 
     @property
-    def focus_event(self):
+    def focus_event(self) -> Optional[U_Event]:
         if self.focus is None:
             return None
         else:
@@ -292,17 +295,20 @@ class DListBox(EventListBox):
     def clean(self):
         """reset event most recently in focus"""
         if self._old_focus is not None:
-            self.body[self._old_focus].body[0].set_attr_map({None: 'date'})
+            try:
+                self.body[self._old_focus].body[0].set_attr_map({None: 'date'})
+            except IndexError:
+                # after reseting the EventList, the old focus might not exist
+                pass
 
-    def ensure_date(self, day):
+    def ensure_date(self, day: dt.date) -> None:
         """ensure an entry for `day` exists and bring it into focus"""
         try:
             self._old_focus = self.focus_position
         except IndexError:
             pass
-        rval = self.body.ensure_date(day)
+        self.body.ensure_date(day)
         self.clean()
-        return rval
 
     def keypress(self, size, key):
         if key in self._conf['keybindings']['up']:
@@ -336,7 +342,7 @@ class DListBox(EventListBox):
         return self.body.focus_event
 
     @property
-    def current_date(self):
+    def current_date(self) -> dt.date:
         return self.body.current_day
 
     def refresh_titles(self, start, end, recurring):
@@ -362,12 +368,21 @@ class DayWalker(urwid.SimpleFocusListWalker):
         super().__init__([])
         self.ensure_date(this_date)
 
-    def ensure_date(self, day):
+
+    def reset(self):
+        """delete all events contained in this DayWalker"""
+        self.clear()
+        self._last_day = None
+        self._first_day = None
+
+
+    def ensure_date(self, day: dt.date) -> None:
         """make sure a DateListBox for `day` exists, update it and bring it into focus"""
         # TODO this function gets called twice on every date change, not necessary but
         # isn't very costly either
+        if self.days_to_next_already_loaded(day) > 200:  # arbitrary number
+            self.reset()
         item_no = None
-
         if len(self) == 0:
             pile = self._get_events(day)
             self.append(pile)
@@ -387,21 +402,30 @@ class DayWalker(urwid.SimpleFocusListWalker):
         self[item_no].set_selected_date(day)
         self.set_focus(item_no)
 
+    def days_to_next_already_loaded(self, day: dt.date) -> int:
+        """return number of days until `day` is already loaded into the CalendarWidget"""
+        if len(self) == 0:
+            return 0
+        elif self[0].date <= day <= self[-1].date:
+            return 0
+        elif day <= self[0].date:
+            return (self[0].date - day).days
+        elif self[-1].date <= day:
+            return (day - self[-1].date).days
+        else:
+            raise ValueError("This should not happen")
+
     def update_events_ondate(self, day):
         """refresh the contents of the day's DateListBox"""
         offset = (day - self[0].date).days
         assert self[offset].date == day
         self[offset] = self._get_events(day)
 
-    def refresh_titles(self, start, end, everything):
+    def refresh_titles(self, start: dt.date, end: dt.date, everything: bool):
         """refresh events' titles
 
         if `everything` is True, reset all titles, otherwise only
         those between `start` and `end`
-
-        :type start: datetime.date
-        :type end: datetime.date
-        :type bool: bool
         """
         start = start.date() if isinstance(start, dt.datetime) else start
         end = end.date() if isinstance(end, dt.datetime) else end
@@ -418,12 +442,8 @@ class DayWalker(urwid.SimpleFocusListWalker):
         for index in range(offset, offset + length + 1):
             self[index].refresh_titles()
 
-    def update_range(self, start, end, everything=False):
-        """refresh contents of all days between start and end (inclusive)
-
-        :type start: datetime.date
-        :type end: datetime.date
-        """
+    def update_range(self, start: dt.date, end: dt.date, everything: bool=False):
+        """refresh contents of all days between start and end (inclusive)"""
         start = start.date() if isinstance(start, dt.datetime) else start
         end = end.date() if isinstance(end, dt.datetime) else end
 
@@ -467,11 +487,8 @@ class DayWalker(urwid.SimpleFocusListWalker):
         pile = self._get_events(self._first_day)
         self.insert(0, pile)
 
-    def _get_events(self, day):
-        """get all events on day, return a DateListBox of `U_Event()`s
-
-        :type day: datetime.date
-        """
+    def _get_events(self, day: dt.date) -> urwid.Widget:
+        """get all events on day, return a DateListBox of `U_Event()`s """
         event_list = []
         date_header = DateHeader(
             day=day,
@@ -490,23 +507,30 @@ class DayWalker(urwid.SimpleFocusListWalker):
             (len(event_list) + 1) if self.events else 1
         )
 
-    def selectable(self):
+    def selectable(self) -> bool:
         """mark this widget as selectable"""
         return True
 
     @property
-    def focus_event(self):
+    def focus_event(self) -> U_Event:
         return self[self.focus].original_widget.focus_event
 
     @property
-    def current_day(self):
+    def current_day(self) -> dt.date:
         return self[self.focus].original_widget.date
 
+    @property
+    def first_date(self) -> dt.date:
+        return self[0].original_widget.date
+
+    @property
+    def last_date(self) -> dt.date:
+        return self[-1].original_widget.date
 
 class StaticDayWalker(DayWalker):
     """Only show events for a fixed number of days."""
 
-    def ensure_date(self, day):
+    def ensure_date(self, day: dt.date) -> None:
         """make sure a DateListBox for `day` exists, update it and bring it into focus"""
         # TODO cache events for each day and update as needed
         num_days = max(1, self._conf['default']['timedelta'].days)
@@ -523,25 +547,18 @@ class StaticDayWalker(DayWalker):
         """refresh the contents of the day's DateListBox"""
         self[0] = self._get_events(day)
 
-    def refresh_titles(self, start, end, everything):
+    def refresh_titles(self, start: dt.date, end: dt.date, everything: bool) -> None:
         """refresh events' titles
 
         if `everything` is True, reset all titles, otherwise only
         those between `start` and `end`
-
-        :type start: datetime.date
-        :type end: datetime.date
-        :type bool: bool
         """
+        # TODO: why are we not using the arguments?
         for one in self:
             one.refresh_titles()
 
-    def update_range(self, start, end, everything=False):
-        """refresh contents of all days between start and end (inclusive)
-
-        :type start: datetime.date
-        :type end: datetime.date
-        """
+    def update_range(self, start: dt.date, end: dt.date, everything: bool=False):
+        """refresh contents of all days between start and end (inclusive)"""
         start = start.date() if isinstance(start, dt.datetime) else start
         end = end.date() if isinstance(end, dt.datetime) else end
 
@@ -558,8 +575,9 @@ class StaticDayWalker(DayWalker):
 
 
 class DateListBox(urwid.ListBox):
-    """A ListBox container for a SimpleFocusListWalker, that contains one day
-    worth of events"""
+    """A ListBox container containing all events for one specific date
+    used with a SimpleFocusListWalker
+    """
 
     selected_date = None
 
@@ -584,11 +602,10 @@ class DateListBox(urwid.ListBox):
     def reset_style(self):
         self.body[0].set_attr_map({None: 'date header'})
 
-    def set_selected_date(self, day):
+    def set_selected_date(self, day: dt.date) -> None:
         """Mark `day` as selected
 
         :param day: day to mark as selected
-        :type day: datetime.date
         """
         DateListBox.selected_date = day
         # we need to touch the title's content to make sure
@@ -625,13 +642,13 @@ class EventColumn(urwid.WidgetWrap):
         self._conf = pane._conf
         self.divider = urwid.Divider('─')
         self.editor = False
-        self._current_date = None
+        self._last_focused_date: Optional[dt.date] = None
         self._eventshown = False
         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 = elistbox
+        self.dlistbox: DateListBox = elistbox
         self.container = urwid.Pile([self.dlistbox])
         urwid.WidgetWrap.__init__(self, self.container)
 
@@ -656,12 +673,12 @@ class EventColumn(urwid.WidgetWrap):
         self.focus_date = date
 
     @property
-    def focus_date(self):
-        return self._current_date
+    def focus_date(self) -> dt.date:
+        return self.dlistbox.current_date
 
     @focus_date.setter
-    def focus_date(self, date):
-        self._current_date = date
+    def focus_date(self, date: dt.date) -> None:
+        self._last_focused_date = date
         self.dlistbox.ensure_date(date)
 
     def update(self, min_date, max_date, everything):
@@ -674,6 +691,9 @@ class EventColumn(urwid.WidgetWrap):
             min_date = self.pane.calendar.base_widget.walker.earliest_date
             max_date = self.pane.calendar.base_widget.walker.latest_date
         self.pane.base_widget.calendar.base_widget.reset_styles_range(min_date, max_date)
+        if everything:
+            min_date = self.dlistbox.body.first_date
+            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):
@@ -688,13 +708,12 @@ class EventColumn(urwid.WidgetWrap):
         """refresh titles in DateListBoxes"""
         self.dlistbox.update_date_line()
 
-    def edit(self, event, always_save=False, external_edit=False):
+    def edit(self, event, always_save: bool=False, external_edit: bool=False) -> None:
         """create an EventEditor and display it
 
         :param event: event to edit
         :type event: khal.event.Event
         :param always_save: even save the event if it hasn't changed
-        :type always_save: bool
         """
         if event.readonly:
             self.pane.window.alert(
@@ -710,12 +729,10 @@ class EventColumn(urwid.WidgetWrap):
         else:
             original_end = event.end_local
 
-        def update_colors(new_start, new_end, everything=False):
+        def update_colors(new_start: dt.date, new_end: dt.date, everything: bool=False):
             """reset colors in the calendar widget and dates in DayWalker
             between min(new_start, original_start)
 
-            :type new_start: datetime.date
-            :type new_end: datetime.date
             :param everything: set to True if event is a recurring one, than everything
                   gets reseted
             """
@@ -817,9 +834,9 @@ class EventColumn(urwid.WidgetWrap):
             return
         status = self.delete_status(event.recuid)
         refresh = True
-        if status == ALL:
+        if status == DeletionType.ALL:
             self.toggle_delete_all(event.recuid)
-        elif status == INSTANCES:
+        elif status == DeletionType.INSTANCES:
             self.toggle_delete_instance(event.recuid)
         elif event.event.recurring:
             # FIXME if in search results, original pane is used for overlay, not search results
@@ -840,7 +857,7 @@ class EventColumn(urwid.WidgetWrap):
                 event.event.start_local, event.event.end_local, event.event.recurring)
             event.set_title()  # if we are in search results, refresh_titles doesn't work properly
 
-    def duplicate(self):
+    def duplicate(self) -> None:
         """duplicate the event in focus"""
         # TODO copying from birthday calendars is currently problematic
         # because their title is determined by X-BIRTHDAY and X-FNAME properties
@@ -864,11 +881,11 @@ class EventColumn(urwid.WidgetWrap):
         except IndexError:
             pass
 
-    def new(self, date: dt.date, end: Optional[dt.date]=None):
+    def new(self, date: dt.date, end: Optional[dt.date]=None) -> None:
         """create a new event on `date` at the next full hour and edit it
 
         :param date: default date for new event
-        :param end: optional, date the event ends on (inclusive)
+        :param end: date the event ends on (inclusive)
         """
         dtstart: dt.date
         dtend: dt.date
@@ -1039,13 +1056,13 @@ class ClassicView(Pane):
     on the right
     """
 
-    def __init__(self, collection, conf=None, title='', description=''):
+    def __init__(self, collection, conf=None, title: str='', description: str=''):
         self.init = True
         # Will be set when opening the view inside a Window
         self.window = None
         self._conf = conf
         self.collection = collection
-        self._deleted = {ALL: [], INSTANCES: []}
+        self._deleted: Dict[int, List[str]] = {DeletionType.ALL: [], DeletionType.INSTANCES: []}
 
         ContainerWidget = linebox[self._conf['view']['frame']]
         if self._conf['view']['dynamic_days']:
@@ -1089,26 +1106,26 @@ class ClassicView(Pane):
         )
         Pane.__init__(self, columns, title=title, description=description)
 
-    def delete_status(self, uid):
-        if uid[0] in self._deleted[ALL]:
-            return ALL
-        elif uid in self._deleted[INSTANCES]:
-            return INSTANCES
+    def delete_status(self, uid: str) -> Optional[DeletionType]:
+        if uid[0] in self._deleted[DeletionType.ALL]:
+            return DeletionType.ALL
+        elif uid in self._deleted[DeletionType.INSTANCES]:
+            return DeletionType.INSTANCES
         else:
-            return False
+            return None
 
-    def toggle_delete_all(self, recuid):
+    def toggle_delete_all(self, recuid: Tuple[str, str]) -> None:
         uid, _ = recuid
-        if uid in self._deleted[ALL]:
-            self._deleted[ALL].remove(uid)
+        if uid in self._deleted[DeletionType.ALL]:
+            self._deleted[DeletionType.ALL].remove(uid)
         else:
-            self._deleted[ALL].append(uid)
+            self._deleted[DeletionType.ALL].append(uid)
 
-    def toggle_delete_instance(self, uid):
-        if uid in self._deleted[INSTANCES]:
-            self._deleted[INSTANCES].remove(uid)
+    def toggle_delete_instance(self, uid: str) -> None:
+        if uid in self._deleted[DeletionType.INSTANCES]:
+            self._deleted[DeletionType.INSTANCES].remove(uid)
         else:
-            self._deleted[INSTANCES].append(uid)
+            self._deleted[DeletionType.INSTANCES].append(uid)
 
     def cleanup(self, data):
         """delete all events marked for deletion"""
@@ -1118,16 +1135,16 @@ class ClassicView(Pane):
         # We therefore keep track of the etags of the events we already
         # deleted.
         updated_etags = {}
-        for part in self._deleted[ALL]:
+        for part in self._deleted[DeletionType.ALL]:
             account, href, etag = part.split('\n', 2)
             self.collection.delete(href, etag, account)
-        for part, rec_id in self._deleted[INSTANCES]:
+        for part, rec_id in self._deleted[DeletionType.INSTANCES]:
             account, href, etag = part.split('\n', 2)
             etag = updated_etags.get(href) or etag
             event = self.collection.delete_instance(href, etag, account, rec_id)
             updated_etags[event.href] = event.etag
 
-    def keypress(self, size, key):
+    def keypress(self, size, key: str):
         binds = self._conf['keybindings']
         if key in binds['search']:
             self.search()
@@ -1143,7 +1160,7 @@ class ClassicView(Pane):
             height=None)
         self.window.open(overlay)
 
-    def _search(self, search_term):
+    def _search(self, search_term: str):
         """search for events matching `search_term"""
         self.window.backtrack()
         events = sorted(self.collection.search(search_term))
@@ -1188,15 +1205,13 @@ class ClassicView(Pane):
         self.eventscolumn.original_widget.new(date, end)
 
 
-def _urwid_palette_entry(name, color, hmethod):
+def _urwid_palette_entry(
+    name: str, color: str, hmethod: str) -> Tuple[str, str, str, str, str, str]:
     """Create an urwid compatible palette entry.
 
     :param name: name of the new attribute in the palette
-    :type name: string
     :param color: color for the new attribute
-    :type color: string
     :returns: an urwid palette entry
-    :rtype: tuple
     """
     from ..terminal import COLORS
     if color == '' or color in COLORS or color is None:
@@ -1256,15 +1271,12 @@ def _urwid_palette_entry(name, color, hmethod):
         return (name, '', '', '', '', color)
 
 
-def _add_calendar_colors(palette, collection):
+def _add_calendar_colors(palette: List, collection: CalendarCollection) -> List:
     """Add the colors for the defined calendars to the palette.
 
     :param palette: the base palette
-    :type palette: list
     :param collection:
-    :type collection: CalendarCollection
     :returns: the modified palette
-    :rtype: list
     """
     for cal in collection.calendars:
         if cal['color'] == '':
diff --git a/khal/ui/calendarwidget.py b/khal/ui/calendarwidget.py
index da2df44..87c7435 100644
--- a/khal/ui/calendarwidget.py
+++ b/khal/ui/calendarwidget.py
@@ -26,22 +26,29 @@ if anything doesn't work as expected, please open an issue for khal
 
 import calendar
 import datetime as dt
-from collections import defaultdict
 from locale import LC_ALL, LC_TIME, getlocale, setlocale
+from typing import Any, Callable, Dict, List, Literal, Optional, Tuple, TypedDict, Union
 
 import urwid
 
 from khal.utils import get_month_abbr_len
 
+
+# Some custom types
+class MarkType(TypedDict):
+    date: dt.date
+    pos: Tuple[int, int]
+OnPressType = Dict[str, Callable[[dt.date, Optional[dt.date]], Optional[str]]]
+GetStylesSignature = Callable[[dt.date, bool], Optional[Union[str, Tuple[str, str]]]]
+
+
 setlocale(LC_ALL, '')
 
 
-def getweeknumber(day):
+def getweeknumber(day: dt.date) -> int:
     """return iso week number for datetime.date object
     :param day: date
-    :type day: datetime.date()
     :return: weeknumber
-    :rtype: int
     """
     return dt.date.isocalendar(day)[1]
 
@@ -50,20 +57,20 @@ class DatePart(urwid.Text):
 
     """used in the Date widget (single digit)"""
 
-    def __init__(self, digit):
+    def __init__(self, digit: str):
         super().__init__(digit)
 
     @classmethod
-    def selectable(cls):
+    def selectable(cls: type) -> bool:
         return True
 
-    def keypress(self, _, key):
+    def keypress(self, size: Tuple[int], key: str) -> str:
         return key
 
-    def get_cursor_coords(self, size):
+    def get_cursor_coords(self, size: Tuple[int]) -> Tuple[int, int]:
         return 1, 0
 
-    def render(self, size, focus=False):
+    def render(self, size: Tuple[int], focus: bool=False) -> urwid.Canvas:
         canv = super().render(size, focus)
         if focus:
             canv = urwid.CompositeCanvas(canv)
@@ -75,7 +82,7 @@ class Date(urwid.WidgetWrap):
 
     """used in the main calendar for dates (a number)"""
 
-    def __init__(self, date, get_styles=None):
+    def __init__(self, date: dt.date, get_styles: GetStylesSignature):
         dstr = str(date.day).rjust(2)
         self.halves = [urwid.AttrMap(DatePart(dstr[:1]), None, None),
                        urwid.AttrMap(DatePart(dstr[1:]), None, None)]
@@ -83,7 +90,7 @@ class Date(urwid.WidgetWrap):
         self._get_styles = get_styles
         super().__init__(urwid.Columns(self.halves))
 
-    def set_styles(self, styles):
+    def set_styles(self, styles: Union[None, str, Tuple[str, str]]) -> None:
         """If single string, sets the same style for both halves, if two
         strings, sets different style for each half.
         """
@@ -98,21 +105,21 @@ class Date(urwid.WidgetWrap):
             self.halves[0].set_focus_map({None: styles})
             self.halves[1].set_focus_map({None: styles})
 
-    def reset_styles(self, focus=False):
+    def reset_styles(self, focus: bool=False) -> None:
         self.set_styles(self._get_styles(self.date, focus))
 
     @property
-    def marked(self):
+    def marked(self) -> bool:
         if 'mark' in [self.halves[0].attr_map[None], self.halves[1].attr_map[None]]:
             return True
         else:
             return False
 
     @classmethod
-    def selectable(cls):
+    def selectable(cls) -> bool:
         return True
 
-    def keypress(self, _, key):
+    def keypress(self, _: Any, key: str) -> str:
         return key
 
 
@@ -129,24 +136,34 @@ class DateCColumns(urwid.Columns):
     # TODO only call on_date_change when we change our date ourselves,
     # not if it gets changed by an (external) call to set_focus_date()
 
-    def __init__(self, widget_list, on_date_change, on_press, keybindings,
-                 get_styles=None, **kwargs):
+    def __init__(self,
+                 widget_list,
+                 on_date_change: Callable[[dt.date], None],
+                 on_press: OnPressType,
+                 keybindings: Dict[str, List[str]],
+                 get_styles: GetStylesSignature,
+                 **kwargs):
         self.on_date_change = on_date_change
         self.on_press = on_press
         self.keybindings = keybindings
         self.get_styles = get_styles
-        self._init = True
+        self._init: bool = True
         super().__init__(widget_list, **kwargs)
 
-    def __repr__(self):
+    def __repr__(self) -> str:
         return f'<DateCColumns from {self[1].date} to {self[7].date}>'
 
-    def _clear_cursor(self):
-        old_pos = self.focus_position
+    def _clear_cursor(self) -> None:
+        old_pos: int = self.focus_position
         self.contents[old_pos][0].set_styles(
             self.get_styles(self.contents[old_pos][0].date, False))
 
-    @urwid.Columns.focus_position.setter
+    @property
+    def focus_position(self) -> int:
+        """returns the current focus position"""
+        return urwid.Columns.focus_position.fget(self)
+
+    @focus_position.setter
     def focus_position(self, position: int) -> None:
         """calls on_date_change before calling super()._set_focus_position"""
         # do not call when building up the interface, lots of potentially
@@ -160,14 +177,14 @@ class DateCColumns(urwid.Columns):
             self.on_date_change(self.contents[position][0].date)
         urwid.Columns.focus_position.fset(self, position)
 
-    def set_focus_date(self, a_date):
+    def set_focus_date(self, a_date: dt.date) -> None:
         for num, day in enumerate(self.contents[1:8], 1):
             if day[0].date == a_date:
                 self._set_focus_position(num)
                 return None
         raise ValueError('%s not found in this week' % a_date)
 
-    def get_date_column(self, a_date):
+    def get_date_column(self, a_date: dt.date) -> int:
         """return the column `a_date` is in, raises ValueError if `a_date`
            cannot be found
         """
@@ -176,7 +193,7 @@ class DateCColumns(urwid.Columns):
                 return num
         raise ValueError('%s not found in this week' % a_date)
 
-    def keypress(self, size, key):
+    def keypress(self, size: Tuple[int], key: str) -> str:
         """only leave calendar area on pressing 'tab' or 'enter'"""
 
         if key in self.keybindings['left']:
@@ -202,7 +219,7 @@ class DateCColumns(urwid.Columns):
             self.focus_position = 7
             exit_row = True
             key = 'up'
-        elif key in self.keybindings['view']:  # XXX make this more generic
+        elif key in self.keybindings['view']:  # TODO make this more generic
             self.focus_position = old_pos
             key = 'right'
         elif key in ['up', 'down']:
@@ -213,23 +230,30 @@ class DateCColumns(urwid.Columns):
 
         return key
 
-
 class CListBox(urwid.ListBox):
     """our custom version of ListBox containing a CalendarWalker instance
 
     it should contain a `CalendarWalker` instance which it autoextends on
     rendering, if needed """
 
-    def __init__(self, walker):
-        self._init = True
+    def __init__(self, walker: 'CalendarWalker'):
+        self._init: bool = True
         self.keybindings = walker.keybindings
         self.on_press = walker.on_press
-        self._marked = False
-        self._pos_old = False
-
+        self._marked: Optional[MarkType] = None
+        self._pos_old: Optional[Tuple[int, int]] = None
+        self.body: 'CalendarWalker'
         super().__init__(walker)
 
-    def render(self, size, focus=False):
+    @property
+    def focus_position(self) -> int:
+        return super().focus_position
+
+    @focus_position.setter
+    def focus_position(self, position: int) -> None:
+        super().set_focus(position)
+
+    def render(self, size: Tuple[int], focus: bool=False) -> urwid.Canvas:
         while 'bottom' in self.ends_visible(size):
             self.body._autoextend()
         if self._init:
@@ -246,28 +270,32 @@ class CListBox(urwid.ListBox):
                 self.focus.get_styles(self.body.focus_date, False))
         return super().mouse_event(*args)
 
-    def _date(self, row, column):
+    def _date(self, row: int, column: int) -> dt.date:
         """return the date at row `row` and  column `column`"""
         return self.body[row].contents[column][0].date
 
-    def _unmark_one(self, row, column):
+    def _unmark_one(self, row: int, column: int) -> None:
         """remove attribute *mark* from the date at row `row` and column `column`
         returning it to the attributes defined by self._get_color()
         """
         self.body[row].contents[column][0].reset_styles()
 
-    def _mark_one(self, row, column):
+    def _mark_one(self, row: int, column: int) -> None:
         """set attribute *mark* on the date at row `row` and column `column`"""
         self.body[row].contents[column][0].set_styles('mark')
 
-    def _mark(self, a_date=None):
+    def _mark(self, a_date: Optional[dt.date]=None) -> None:
         """make sure everything between the marked entry and `a_date`
         is visually marked, and nothing else"""
 
+        assert self._marked is not None
+
         if a_date is None:
             a_date = self.body.focus_date
 
-        def toggle(row, column):
+        def toggle(row: int, column: int) -> None:
+            """toggle the mark attribute on the date at row `row` and column
+            `column`"""
             if self.body[row].contents[column][0].marked:
                 self._mark_one(row, column)
             else:
@@ -291,25 +319,28 @@ class CListBox(urwid.ListBox):
             toggle(self.focus_position, self.focus.focus_col)
         self._pos_old = self.focus_position, self.focus.focus_col
 
-    def _unmark_all(self):
-        start = min(self._marked['pos'][0], self.focus_position, self._pos_old[0])
-        end = max(self._marked['pos'][0], self.focus_position, self._pos_old[0]) + 1
-        for row in range(start, end):
-            for col in range(1, 8):
-                self._unmark_one(row, col)
-
-    def set_focus_date(self, a_day):
+    def _unmark_all(self) -> None:
+        """remove attribute *mark* from all dates"""
+        if self._marked and self._pos_old:
+            start = min(self._marked['pos'][0], self.focus_position, self._pos_old[0])
+            end = max(self._marked['pos'][0], self.focus_position, self._pos_old[0]) + 1
+            for row in range(start, end):
+                for col in range(1, 8):
+                    self._unmark_one(row, col)
+
+    def set_focus_date(self, a_day: dt.date) -> None:
+        """set focus to the date `a_day`"""
         self.focus.focus.set_styles(self.focus.get_styles(self.body.focus_date, False))
         if self._marked:
             self._unmark_all()
             self._mark(a_day)
         self.body.set_focus_date(a_day)
 
-    def keypress(self, size, key):
+    def keypress(self, size: bool, key: str) -> Optional[str]:
         if key in self.keybindings['mark'] + ['esc'] and self._marked:
             self._unmark_all()
-            self._marked = False
-            return
+            self._marked = None
+            return None
         if key in self.keybindings['mark']:
             self._marked = {'date': self.body.focus_date,
                             'pos': (self.focus_position, self.focus.focus_col)}
@@ -319,7 +350,6 @@ class CListBox(urwid.ListBox):
                             'pos': (self.focus_position, self.focus.focus_col)}
             self.focus.focus_col = col
             self.focus_position = row
-
         if key in self.on_press:
             if self._marked:
                 start = min(self.body.focus_date, self._marked['date'])
@@ -342,11 +372,16 @@ class CListBox(urwid.ListBox):
 
 
 class CalendarWalker(urwid.SimpleFocusListWalker):
-    def __init__(self, on_date_change, on_press, keybindings, firstweekday=0,
-                 weeknumbers=False, monthdisplay='firstday', get_styles=None,
-                 initial=None):
-        if initial is None:
-            initial = dt.date.today()
+    def __init__(self,
+                 on_date_change: Callable[[dt.date], None],
+                 on_press: Dict[str, Callable[[dt.date, Optional[dt.date]], Optional[str]]],
+                 keybindings: Dict[str, List[str]],
+                 get_styles: GetStylesSignature,
+                 firstweekday: int = 0,
+                 weeknumbers: Literal['left', 'right', False]=False,
+                 monthdisplay: Literal['firstday', 'firstfullweek']='firstday',
+                 initial: Optional[dt.date]=None,
+                 ) -> None:
         self.firstweekday = firstweekday
         self.weeknumbers = weeknumbers
         self.monthdisplay = monthdisplay
@@ -354,46 +389,60 @@ class CalendarWalker(urwid.SimpleFocusListWalker):
         self.on_press = on_press
         self.keybindings = keybindings
         self.get_styles = get_styles
+        self.reset(initial)
+
+    def reset(self, initial: Optional[dt.date]=None) -> None:
+        if initial is None:
+            initial = dt.date.today()
         weeks = self._construct_month(initial.year, initial.month)
         urwid.SimpleFocusListWalker.__init__(self, weeks)
 
-    def set_focus(self, position):
+    def set_focus(self, position: int) -> None:
         """set focus by item number"""
         while position >= len(self) - 1:
             self._autoextend()
         while position <= 0:
             no_additional_weeks = self._autoprepend()
             position += no_additional_weeks
-        return urwid.SimpleFocusListWalker.set_focus(self, position)
+        urwid.SimpleFocusListWalker.set_focus(self, position)
+
+    def days_to_next_already_loaded(self, day: dt.date) -> int:
+        """return the number of weeks from the focus to the next week that is already loaded"""
+        if len(self) == 0:
+            return 0
+        elif self.earliest_date <= day <= self.latest_date:
+            return 0
+        elif day <= self.earliest_date:
+            return (self.earliest_date - day).days
+        elif self.latest_date <= day:
+            return (day - self.latest_date).days
+        else:
+            raise ValueError("This should not happen")
 
     @property
-    def focus_date(self):
-        """return the date the focus is currently set to
-
-        :rtype: datetime.date
-        """
+    def focus_date(self) -> dt.date:
+        """return the date the focus is currently set to"""
         return self[self.focus].focus.date
 
-    def set_focus_date(self, a_day):
-        """set the focus to `a_day`
-
-        :type: a_day: datetime.date
-        """
+    def set_focus_date(self, a_day: dt.date) -> None:
+        """set the focus to `a_day`"""
+        if self.days_to_next_already_loaded(a_day) > 200:   # arbitrary number
+            self.reset(a_day)
         row, column = self.get_date_pos(a_day)
         self.set_focus(row)
         self[self.focus]._set_focus_position(column)
 
     @property
-    def earliest_date(self):
+    def earliest_date(self) -> dt.date:
         """return earliest day that is already loaded into the CalendarWidget"""
         return self[0][1].date
 
     @property
-    def latest_date(self):
+    def latest_date(self) -> dt.date:
         """return latest day that is already loaded into the CalendarWidget"""
         return self[-1][7].date
 
-    def reset_styles_range(self, min_date, max_date):
+    def reset_styles_range(self, min_date: dt.date, max_date: dt.date) -> None:
         """reset styles for all (displayed) dates between min_date and max_date"""
         minr, minc = self.get_date_pos(max(min_date, self.earliest_date))
         maxr, maxc = self.get_date_pos(min(max_date, self.latest_date))
@@ -404,12 +453,8 @@ class CalendarWalker(urwid.SimpleFocusListWalker):
                 focus = ((row, column) == focus_pos)
                 self[row][column].reset_styles(focus)
 
-    def get_date_pos(self, a_day):
-        """get row and column where `a_day` is located
-
-        :type: a_day: datetime.date
-        :rtype: tuple(int, int)
-        """
+    def get_date_pos(self, a_day: dt.date) -> Tuple[int, int]:
+        """get row and column where `a_day` is located"""
         # rough estimate of difference in lines, i.e. full weeks, we might be
         # off by as much as one week though
         week_diff = int((self.focus_date - a_day).days / 7)
@@ -429,12 +474,12 @@ class CalendarWalker(urwid.SimpleFocusListWalker):
                     self._autoextend()
                 column = self[row].get_date_column(a_day)
                 return row, column
-            except ValueError:
+            except (ValueError, IndexError):
                 pass
         # we didn't find the date we were looking for...
         raise ValueError('something is wrong')
 
-    def _autoextend(self):
+    def _autoextend(self) -> None:
         """appends the next month"""
         date_last_month = self[-1][1].date  # a date from the last month
         last_month = date_last_month.month
@@ -444,11 +489,10 @@ class CalendarWalker(urwid.SimpleFocusListWalker):
         weeks = self._construct_month(year, month, clean_first_row=True)
         self.extend(weeks)
 
-    def _autoprepend(self):
+    def _autoprepend(self) -> int:
         """prepends the previous month
 
         :returns: number of weeks prepended
-        :rtype: int
         """
         try:
             date_first_month = self[0][-1].date  # a date from the first month
@@ -469,16 +513,14 @@ class CalendarWalker(urwid.SimpleFocusListWalker):
             self.insert(0, one)
         return len(weeks)
 
-    def _construct_week(self, week):
+    def _construct_week(self, week: List[dt.date]) -> DateCColumns:
         """
         constructs a CColumns week from a week of datetime.date objects. Also
         prepends the month name if the first day of the month is included in
         that week.
 
         :param week: list of datetime.date objects
-        :returns: the week as an CColumns object and True or False depending on
-                  if today is in this week
-        :rtype: tuple(urwid.CColumns, bool)
+        :returns: the week as an CColumns object
         """
         if self.monthdisplay == 'firstday' and 1 in (day.day for day in week):
             month_name = calendar.month_abbr[week[-1].month].ljust(4)
@@ -493,6 +535,7 @@ class CalendarWalker(urwid.SimpleFocusListWalker):
             month_name = '    '
             attr = None
 
+        this_week: List[Tuple[int, Union[Date, urwid.AttrMap]]]
         this_week = [(get_month_abbr_len(), urwid.AttrMap(urwid.Text(month_name), attr))]
         for _number, day in enumerate(week):
             new_date = Date(day, self.get_styles)
@@ -511,64 +554,65 @@ class CalendarWalker(urwid.SimpleFocusListWalker):
         return week
 
     def _construct_month(self,
-                         year=dt.date.today().year,
-                         month=dt.date.today().month,
-                         clean_first_row=False,
-                         clean_last_row=False):
+                         year: int=dt.date.today().year,
+                         month: int=dt.date.today().month,
+                         clean_first_row: bool=False,
+                         clean_last_row: bool=False,
+                         ) -> List[DateCColumns]:
         """construct one month of DateCColumns
 
         :param year: the year this month is set in
-        :type year: int
         :param month: the number of the month to be constructed
-        :type month: int (1-12)
-        :param clean_first_row: makes sure that the first element returned is
-                                completely in `month` and not partly in the one
-                                before (which might lead to that line occurring
-                                twice
-        :type clean_first_row: bool
-        :param clean_last_row: makes sure that the last element returned is
-                               completely in `month` and not partly in the one
-                               after (which might lead to that line occurring
-                               twice
-        :type clean_last_row: bool
+        :param clean_first_row: if set, makes sure that the first element
+           returned is completely in `month` and not partly in the one before
+           (which might lead to that line occurring twice
+        :param clean_last_row: if set, makes sure that the last element returned
+            is completely in `month` and not partly in the one after (which
+            might lead to that line occurring twice)
         :returns: list of DateCColumns and the number of the list element which
                   contains today (or None if it isn't in there)
-        :rtype: tuple(list(dateCColumns, int or None))
         """
 
-        plain_weeks = calendar.Calendar(
-            self.firstweekday).monthdatescalendar(year, month)
+        plain_weeks = calendar.Calendar(self.firstweekday).monthdatescalendar(year, month)
         weeks = []
         for _number, week in enumerate(plain_weeks):
-            week = self._construct_week(week)
-            weeks.append(week)
+            weeks.append(self._construct_week(week))
         if clean_first_row and weeks[0][1].date.month != weeks[0][7].date.month:
             return weeks[1:]
-        elif clean_last_row and \
-                weeks[-1][1].date.month != weeks[-1][7].date.month:
+        elif clean_last_row and weeks[-1][1].date.month != weeks[-1][7].date.month:
             return weeks[:-1]
         else:
             return weeks
 
 
 class CalendarWidget(urwid.WidgetWrap):
-    def __init__(self, on_date_change, keybindings, on_press, firstweekday=0,
-                 weeknumbers=False, monthdisplay='firstday', get_styles=None, initial=None):
-        """
+    def __init__(self,
+                 on_date_change: Callable[[dt.date], None],
+                 keybindings: Dict[str, List[str]],
+                 on_press: Optional[OnPressType]=None,
+                 firstweekday: int=0,
+                 weeknumbers: Literal['left', 'right', False]=False,
+                 monthdisplay: Literal['firstday', 'firstfullweek']='firstday',
+                 get_styles: Optional[GetStylesSignature]=None,
+                 initial: Optional[dt.date]=None,
+                 ):
+        """A calendar widget that can be used in urwid applications
+
         :param on_date_change: a function that is called every time the selected
-            date is changed with the newly selected date as a first (and only
-            argument)
-        :type on_date_change: function
+            date is changed with the newly selected date as an argument
         :param keybindings: bind keys to specific functionionality, keys are
             the available commands, values are lists of keys that should be
             bound to those commands. See below for the defaults.
             Available commands:
-                'left', 'right', 'up', 'down': move cursor in direction
+                'left', 'right', 'up', 'down': move cursor in that direction
                 'today': refocus on today
                 'mark': toggles selection mode
-        :type keybindings: dict
+                'other': toggles between selecting the earlier and the later end
+                         of a selection
+                'view': returns the key `right` to the widget containing the
+                        CalendarWidget
         :param on_press: dictonary of functions that are called when the key is
-            pressed and is not already bound to one of the internal functionions
+            pressed and is not already bound to one of the internal functions
             via `keybindings`. These functions must accept two arguments, in
             normal mode the first argument is the currently selected date
             (datetime.date) and the second is `None`. When a date range is
@@ -576,33 +620,43 @@ class CalendarWidget(urwid.WidgetWrap):
             is the later date. The function's return values are interpreted as
             pressed keys, which are handed to the widget containing the
             CalendarWidget.
-        :type on_press: dict
+        :param firstweekday: the first day of the week, 0 for Monday, 6 for
+        :param weeknumbers: display weeknumbers on the left or right side of
+           the calendar.
+        :param monthdisplay: display the month name in the row of the 1st of the
+           month or in the first row that only contains days of the current month.
+        :param get_styles: a function that returns a list of styles for a given date
+        :param initial: the date that is selected when the widget is first rendered
         """
         if initial is None:
             self._initial = dt.date.today()
         else:
             self._initial = initial
 
-        default_keybindings = {
+        if on_press is None:
+            on_press = {}
+
+        default_keybindings: Dict[str, List[str]] = {
             'left': ['left'], 'down': ['down'], 'right': ['right'], 'up': ['up'],
             'today': ['t'],
             'view': [],
             'mark': ['v'],
+            'other': ['%'],
         }
-        on_press = defaultdict(lambda: lambda x: x, on_press)
+
         default_keybindings.update(keybindings)
         calendar.setfirstweekday(firstweekday)
 
         try:
-            mylocale = '.'.join(getlocale(LC_TIME))
+            mylocale: str = '.'.join(getlocale(LC_TIME))  # type: ignore
         except TypeError:  # language code and encoding may be None
             mylocale = 'C'
 
-        _calendar = calendar.LocaleTextCalendar(firstweekday, mylocale)
+        _calendar = calendar.LocaleTextCalendar(firstweekday, mylocale)  # type: ignore
         weekheader = _calendar.formatweekheader(2)
         dnames = weekheader.split(' ')
 
-        def _get_styles(date, focus):
+        def _get_styles(date: dt.date, focus: bool) -> Optional[str]:
             if focus:
                 if date == dt.date.today():
                     return 'today focus'
@@ -619,36 +673,39 @@ class CalendarWidget(urwid.WidgetWrap):
         if weeknumbers == 'right':
             dnames.append('#w')
         month_names_length = get_month_abbr_len()
-        dnames = urwid.Columns(
+        cnames = urwid.Columns(
             [(month_names_length, urwid.Text(' ' * month_names_length))] +
             [(2, urwid.AttrMap(urwid.Text(name), 'dayname')) for name in dnames],
             dividechars=1)
         self.walker = CalendarWalker(
-            on_date_change, on_press, default_keybindings, firstweekday,
-            weeknumbers, monthdisplay,
-            get_styles, initial=self._initial)
+            on_date_change=on_date_change,
+            on_press=on_press,
+            keybindings=default_keybindings,
+            firstweekday=firstweekday,
+            weeknumbers=weeknumbers,
+            monthdisplay=monthdisplay,
+            get_styles=get_styles,
+            initial=self._initial,
+        )
         self.box = CListBox(self.walker)
-        frame = urwid.Frame(self.box, header=dnames)
+        frame = urwid.Frame(self.box, header=cnames)
         urwid.WidgetWrap.__init__(self, frame)
         self.set_focus_date(self._initial)
 
-    def focus_today(self):
+    def focus_today(self) -> None:
         self.set_focus_date(dt.date.today())
 
-    def reset_styles_range(self, min_date, max_date):
+    def reset_styles_range(self, min_date: dt.date, max_date: dt.date) -> None:
         self.walker.reset_styles_range(min_date, max_date)
 
     @classmethod
-    def selectable(cls):
+    def selectable(cls) -> bool:
         return True
 
     @property
-    def focus_date(self):
+    def focus_date(self) -> dt.date:
         return self.walker.focus_date
 
-    def set_focus_date(self, a_day):
-        """set the focus to `a_day`
-
-        :type a_day: datetime.date
-        """
+    def set_focus_date(self, a_day: dt.date) -> None:
+        """set the focus to `a_day`"""
         self.box.set_focus_date(a_day)
diff --git a/khal/ui/editor.py b/khal/ui/editor.py
index 72ad105..1063a4d 100644
--- a/khal/ui/editor.py
+++ b/khal/ui/editor.py
@@ -20,6 +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
 
 import urwid
 
@@ -103,10 +104,14 @@ class DateEdit(urwid.WidgetWrap):
     """
 
     def __init__(
-            self, startdt=None, dateformat='%Y-%m-%d',
-            on_date_change=lambda _: None,
-            weeknumbers=False, firstweekday=0, monthdisplay='firstday',
-            keybindings=None,
+        self,
+        startdt: dt.date,
+        dateformat: str='%Y-%m-%d',
+        on_date_change: Callable=lambda _: None,
+        weeknumbers: Literal['left', 'right', False]=False,
+        firstweekday: int=0,
+        monthdisplay: Literal['firstday', 'firstfullweek']='firstday',
+        keybindings: Optional[Dict[str, List[str]]] = None,
     ):
         datewidth = len(startdt.strftime(dateformat)) + 1
         self._dateformat = dateformat
@@ -751,6 +756,8 @@ class RecurrenceEditor(urwid.WidgetWrap):
 
     @property
     def changed(self):
+        # TODO this often gives false positives which leads to redraws of all
+        # events shown
         return self._rrule != self.rrule()  # TODO do this properly
 
     def rrule(self):
diff --git a/khal/ui/widgets.py b/khal/ui/widgets.py
index 1fe62fd..3d3a8fc 100644
--- a/khal/ui/widgets.py
+++ b/khal/ui/widgets.py
@@ -26,6 +26,7 @@ if they are large, into their own files
 """
 import datetime as dt
 import re
+from typing import Optional, Tuple
 
 import urwid
 
@@ -76,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):
+    def keypress(self, size, key: str) -> Optional[Tuple[Tuple[int, int], str]]:
         if key == 'ctrl w':
             self._delete_word()
         elif key == 'ctrl u':
@@ -121,7 +122,7 @@ class ExtendedEdit(urwid.Edit):
 
 class DateTimeWidget(ExtendedEdit):
 
-    def __init__(self, dateformat, on_date_change=lambda x: None, **kwargs):
+    def __init__(self, dateformat: str, on_date_change=lambda x: None, **kwargs):
         self.dateformat = dateformat
         self.on_date_change = on_date_change
         super().__init__(wrap='any', **kwargs)
@@ -165,11 +166,8 @@ class DateTimeWidget(ExtendedEdit):
         except DateConversionError:
             pass
 
-    def set_value(self, new_date):
-        """set a new value for this widget
-
-        :type new_date: datetime.date
-        """
+    def set_value(self, new_date: dt.date):
+        """set a new value for this widget"""
         self.set_edit_text(new_date.strftime(self.dateformat))
 
 


Test patch:
diff --git a/tests/khalendar_utils_test.py b/tests/khalendar_utils_test.py
index c7cd123..b9904f6 100644
--- a/tests/khalendar_utils_test.py
+++ b/tests/khalendar_utils_test.py
@@ -690,14 +690,14 @@ class TestSpecial:
         vevent = _get_vevent(_get_text('event_dt_rrule_until_before_start'))
         dtstart = icalendar_helpers.expand(vevent, berlin)
         # TODO test for logging message
-        assert dtstart is False
+        assert dtstart is None
 
     def test_event_invalid_rrule(self):
         """test handling if an event with RRULE will never occur"""
         vevent = _get_vevent(_get_text('event_rrule_no_occurence'))
         dtstart = icalendar_helpers.expand(vevent, berlin)
         # TODO test for logging message
-        assert dtstart is False
+        assert dtstart is None
 
 
 simple_rdate = """BEGIN:VEVENT
diff --git a/tests/ui/tests_walker.py b/tests/ui/test_walker.py
similarity index 100%
rename from tests/ui/tests_walker.py
rename to tests/ui/test_walker.py
diff --git a/tests/utils.py b/tests/utils.py
index 8758295..cfcf636 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -4,6 +4,7 @@ from typing import Dict, Tuple
 import icalendar
 import pytz
 
+from khal.custom_types import LocaleConfiguration
 from khal.khalendar import CalendarCollection
 from khal.khalendar.vdir import Vdir
 
@@ -26,7 +27,7 @@ GMTPLUS3 = pytz.timezone('Etc/GMT+3')
 BOGOTA = pytz.timezone('America/Bogota')
 
 
-LOCALE_BERLIN = {
+LOCALE_BERLIN: LocaleConfiguration = {
     'default_timezone': BERLIN,
     'local_timezone': BERLIN,
     'dateformat': '%d.%m.',
@@ -39,7 +40,7 @@ LOCALE_BERLIN = {
     'weeknumbers': False,
 }
 
-LOCALE_NEW_YORK = {
+LOCALE_NEW_YORK: LocaleConfiguration = {
     'default_timezone': NEW_YORK,
     'local_timezone': NEW_YORK,
     'timeformat': '%H:%M',