From 543890e1d485c60ca8633331a99fc78161b2e1cd Mon Sep 17 00:00:00 2001 From: dabrows Date: Fri, 15 Dec 2023 15:06:35 +0100 Subject: [PATCH 01/17] Initial version, autocomplete works, leaving field and highlight of selected contact missing --- khal/ui/attendeewidget.py | 134 ++++++++++++++++++++++++++++++++++++++ khal/ui/editor.py | 19 +++--- 2 files changed, 145 insertions(+), 8 deletions(-) create mode 100644 khal/ui/attendeewidget.py diff --git a/khal/ui/attendeewidget.py b/khal/ui/attendeewidget.py new file mode 100644 index 000000000..4b63d7d78 --- /dev/null +++ b/khal/ui/attendeewidget.py @@ -0,0 +1,134 @@ +import urwid +from additional_urwid_widgets import IndicativeListBox +import subprocess +import re + +PALETTE = [("reveal_focus", "black", "light cyan", "standout"), + ("ilb_barActive_focus", "dark cyan", "light gray"), + ("ilb_barActive_offFocus", "light gray", "dark gray"), + ("ilb_barInactive_focus", "light cyan", "dark gray"), + ("ilb_barInactive_offFocus", "black", "dark gray")] + +def get_mails(): + res = subprocess.check_output(["bash", "-c", "khard email gmail | tail -n +2"]) + maildata = [re.split(r"\s{2,}", x) for x in res.decode("utf-8").split("\n")] + mails = ["%s <%s>" % (x[0], x[2]) for x in maildata if len(x) > 1] + return mails + + +class MailPopup(urwid.PopUpLauncher): + def __init__(self, widget, maillist): + self.maillist = maillist + self.widget = widget + self.popup_visible = False + self.justcompleted = False + super().__init__(widget) + + def get_current_mailpart(self): + mails = self.widget.get_edit_text().split(",") + lastmail = mails[-1].lstrip(" ") + return lastmail + + def complete_mail(self, newmail): + mails = [x.strip() for x in self.widget.get_edit_text().split(",")[:-1]] + mails += [newmail] + return ", ".join(mails) + + def get_num_mails(self): + mails = self.widget.get_edit_text().split(",") + return len(mails) + + def keypress(self, size, key): + if self.justcompleted and key not in ", ": + self.widget.keypress(size, ",") + self.widget.keypress(size, " ") + self.widget.keypress(size, key) + self.justcompleted = False + if not self.popup_visible: + self.open_pop_up() + self.popup_visible = True + + def keycallback(self, size, key): + self.widget.keypress((20,), key) + self.justcompleted = False + self.listbox.update_mails(self.get_current_mailpart()) + + def donecallback(self, text): + self.widget.set_edit_text(self.complete_mail(text)) + fulllength = len(self.widget.get_edit_text()) + self.widget.move_cursor_to_coords((fulllength,), fulllength, 0) + self.close_pop_up() + self.popup_visible = False + self.justcompleted = True + + def create_pop_up(self): + self.listbox = MailListBox(self.maillist, self.keycallback, + self.donecallback) + return urwid.WidgetWrap(self.listbox) + + def get_pop_up_parameters(self): + return {"left": 0, "top": 1, "overlay_width": 60, "overlay_height": 10} + + def render(self, size, focus=False): + return super().render(size, True) + + +class MailListItem(urwid.Text): + def render(self, size, focus=False): + return super().render(size, False) + + def selectable(self): + return True + + def keypress(self, size, key): + return key + +class MailListBox(IndicativeListBox): + + command_map = urwid.CommandMap() + own_commands = [urwid.CURSOR_DOWN, urwid.CURSOR_UP, urwid.ACTIVATE] + + def __init__(self, mails, keycallback, donecallback, **args): + self.mails = [MailListItem(x) for x in mails] + mailsBody = [urwid.AttrMap(x, None, "reveal_focus") for x in self.mails] + self.keycallback = keycallback + self.donecallback = donecallback + super().__init__(mailsBody, **args) + + def keypress(self, size, key): + cmd = self.command_map[key] + if cmd not in self.own_commands or key == " ": + self.keycallback(size, key) + elif cmd is urwid.ACTIVATE: + self.donecallback(self.get_selected_item()._original_widget.get_text()[0]) + else: + super().keypress(size, key) + + def update_mails(self, new_edit_text): + new_body = [] + for mail in self.mails: + if new_edit_text.lower() in mail.get_text()[0].lower(): + new_body += [urwid.AttrMap(mail, None, "reveal_focus")] + self.set_body(new_body) + + +class AutocompleteEdit(urwid.Edit): + def render(self, size, focus=False): + return super().render(size, True) + + +class AttendeeWidget(urwid.WidgetWrap): + def __init__(self): + self.mails = get_mails() + self.acedit = AutocompleteEdit() + self.mp = MailPopup(self.acedit, self.mails) + super().__init__(self.mp) + + +if __name__ == "__main__": + mails = get_mails() + acedit = AutocompleteEdit() + mp = MailPopup(acedit, mails) + loop = urwid.MainLoop(urwid.Filler(mp), PALETTE, pop_ups=True) + loop.run() + diff --git a/khal/ui/editor.py b/khal/ui/editor.py index b4404f409..d31da165d 100644 --- a/khal/ui/editor.py +++ b/khal/ui/editor.py @@ -43,6 +43,8 @@ button, ) +from .attendeewidget import AttendeeWidget + if TYPE_CHECKING: import khal.khalendar.event @@ -418,14 +420,15 @@ def decorate_choice(c) -> Tuple[str, str]: self.categories = urwid.AttrMap(ExtendedEdit( caption=('caption', 'Categories: '), edit_text=self.categories), 'edit', 'edit focus', ) - self.attendees = urwid.AttrMap( - ExtendedEdit( - caption=('caption', 'Attendees: '), - edit_text=self.attendees, - multiline=True - ), - 'edit', 'edit focus', - ) +# self.attendees = urwid.AttrMap( +# ExtendedEdit( +# caption=('caption', 'Attendees: '), +# edit_text=self.attendees, +# multiline=True +# ), +# 'edit', 'edit focus', +# ) + self.attendees = urwid.AttrMap(AttendeeWidget(), 'edit', 'edit focus') self.url = urwid.AttrMap(ExtendedEdit( caption=('caption', 'URL: '), edit_text=self.url), 'edit', 'edit focus', ) From e3c52675cb0ecd7d572e72b0ac10c9bf5a4cc1e4 Mon Sep 17 00:00:00 2001 From: dabrows Date: Fri, 15 Dec 2023 17:00:39 +0100 Subject: [PATCH 02/17] Added correct focus traversal and highlighting in list. --- khal/ui/attendeewidget.py | 30 +++++++++++++++++++++++++----- khal/ui/editor.py | 10 +--------- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/khal/ui/attendeewidget.py b/khal/ui/attendeewidget.py index 4b63d7d78..48ee1094a 100644 --- a/khal/ui/attendeewidget.py +++ b/khal/ui/attendeewidget.py @@ -17,6 +17,9 @@ def get_mails(): class MailPopup(urwid.PopUpLauncher): + command_map = urwid.CommandMap() + own_commands = [] + def __init__(self, widget, maillist): self.maillist = maillist self.widget = widget @@ -39,19 +42,29 @@ def get_num_mails(self): return len(mails) def keypress(self, size, key): + cmd = self.command_map[key] + if cmd is not None and cmd not in self.own_commands: + return key if self.justcompleted and key not in ", ": self.widget.keypress(size, ",") self.widget.keypress(size, " ") self.widget.keypress(size, key) self.justcompleted = False if not self.popup_visible: + # Only open the popup list if there will be at least 1 address displayed + current = self.get_current_mailpart() + if len([x for x in self.maillist if current.lower() in x.lower()]) == 0: + return self.open_pop_up() self.popup_visible = True def keycallback(self, size, key): self.widget.keypress((20,), key) self.justcompleted = False - self.listbox.update_mails(self.get_current_mailpart()) + num_candidates = self.listbox.update_mails(self.get_current_mailpart()) + if num_candidates == 0: + self.popup_visible = False + self.close_pop_up() def donecallback(self, text): self.widget.set_edit_text(self.complete_mail(text)) @@ -62,8 +75,9 @@ def donecallback(self, text): self.justcompleted = True def create_pop_up(self): + current_mailpart = self.get_current_mailpart() self.listbox = MailListBox(self.maillist, self.keycallback, - self.donecallback) + self.donecallback, current_mailpart) return urwid.WidgetWrap(self.listbox) def get_pop_up_parameters(self): @@ -88,12 +102,14 @@ class MailListBox(IndicativeListBox): command_map = urwid.CommandMap() own_commands = [urwid.CURSOR_DOWN, urwid.CURSOR_UP, urwid.ACTIVATE] - def __init__(self, mails, keycallback, donecallback, **args): + def __init__(self, mails, keycallback, donecallback, current_mailpart, **args): self.mails = [MailListItem(x) for x in mails] - mailsBody = [urwid.AttrMap(x, None, "reveal_focus") for x in self.mails] + mailsBody = [urwid.AttrMap(x, None, "list focused") for x in self.mails] self.keycallback = keycallback self.donecallback = donecallback super().__init__(mailsBody, **args) + if len(current_mailpart) != 0: + self.update_mails(current_mailpart) def keypress(self, size, key): cmd = self.command_map[key] @@ -108,8 +124,9 @@ def update_mails(self, new_edit_text): new_body = [] for mail in self.mails: if new_edit_text.lower() in mail.get_text()[0].lower(): - new_body += [urwid.AttrMap(mail, None, "reveal_focus")] + new_body += [urwid.AttrMap(mail, None, "list focused")] self.set_body(new_body) + return len(new_body) class AutocompleteEdit(urwid.Edit): @@ -124,6 +141,9 @@ def __init__(self): self.mp = MailPopup(self.acedit, self.mails) super().__init__(self.mp) + def get_attendees(self): + return self.acedit.get_edit_text() + if __name__ == "__main__": mails = get_mails() diff --git a/khal/ui/editor.py b/khal/ui/editor.py index d31da165d..b1b1d9bce 100644 --- a/khal/ui/editor.py +++ b/khal/ui/editor.py @@ -420,14 +420,6 @@ def decorate_choice(c) -> Tuple[str, str]: self.categories = urwid.AttrMap(ExtendedEdit( caption=('caption', 'Categories: '), edit_text=self.categories), 'edit', 'edit focus', ) -# self.attendees = urwid.AttrMap( -# ExtendedEdit( -# caption=('caption', 'Attendees: '), -# edit_text=self.attendees, -# multiline=True -# ), -# 'edit', 'edit focus', -# ) self.attendees = urwid.AttrMap(AttendeeWidget(), 'edit', 'edit focus') self.url = urwid.AttrMap(ExtendedEdit( caption=('caption', 'URL: '), edit_text=self.url), 'edit', 'edit focus', @@ -528,7 +520,7 @@ def update_vevent(self): self.event.update_summary(get_wrapped_text(self.summary)) self.event.update_description(get_wrapped_text(self.description)) self.event.update_location(get_wrapped_text(self.location)) - self.event.update_attendees(get_wrapped_text(self.attendees).split(',')) + self.event.update_attendees(self.attendees._original_widget.get_attendees().split(',')) self.event.update_categories(get_wrapped_text(self.categories).split(',')) self.event.update_url(get_wrapped_text(self.url)) From 2a71e613092fd77e60af32d748389946dcd15d7c Mon Sep 17 00:00:00 2001 From: dabrows Date: Fri, 15 Dec 2023 19:42:31 +0100 Subject: [PATCH 03/17] Added configuration for address provider --- khal/cli.py | 1 + khal/khalendar/event.py | 20 +++++++++++++++++--- khal/khalendar/khalendar.py | 17 +++++++++++++++++ khal/settings/khal.spec | 3 +++ khal/settings/utils.py | 1 + khal/ui/attendeewidget.py | 23 +++++++++++++---------- khal/ui/editor.py | 2 +- 7 files changed, 53 insertions(+), 14 deletions(-) diff --git a/khal/cli.py b/khal/cli.py index baf8c0090..290073e58 100644 --- a/khal/cli.py +++ b/khal/cli.py @@ -176,6 +176,7 @@ def build_collection(conf, selection): 'priority': cal['priority'], 'ctype': cal['type'], 'addresses': cal['addresses'], + 'address_adapter': cal['address_adapter'] } collection = khalendar.CalendarCollection( calendars=props, diff --git a/khal/khalendar/event.py b/khal/khalendar/event.py index 9f7339521..a6f81ae92 100644 --- a/khal/khalendar/event.py +++ b/khal/khalendar/event.py @@ -25,6 +25,7 @@ import datetime as dt import logging import os +import re from typing import Dict, List, Optional, Tuple, Type, Union import icalendar @@ -43,6 +44,17 @@ logger = logging.getLogger('khal') +class Attendee: + def __init__(self, defline): + m = re.match(r"(?P.*)\<(?P.*)\>", defline) + if m.group("name") is not None and m.group("mail") is not None: + self.cn = m.group("name").strip() + self.mail = m.group("mail").strip().lower() + else: + self.cn = None + self.mail = defline.strip().lower() + + class Event: """base Event class for representing a *recurring instance* of an Event @@ -499,7 +511,7 @@ def attendees(self) -> str: def update_attendees(self, attendees: List[str]): assert isinstance(attendees, list) - attendees = [a.strip().lower() for a in attendees if a != ""] + attendees = [Attendee(a) for a in attendees if a != ""] if len(attendees) > 0: # first check for overlaps in existing attendees. # Existing vCalAddress objects will be copied, non-existing @@ -510,11 +522,13 @@ def update_attendees(self, attendees: List[str]): for attendee in attendees: for old_attendee in old_attendees: old_email = old_attendee.lstrip("MAILTO:").lower() - if attendee == old_email: + if attendee.mail == old_email: vCalAddresses.append(old_attendee) unchanged_attendees.append(attendee) for attendee in [a for a in attendees if a not in unchanged_attendees]: - item = icalendar.prop.vCalAddress(f'MAILTO:{attendee}') + item = icalendar.prop.vCalAddress(f'MAILTO:{attendee.mail}') + if attendee.cn is not None: + item.params['CN'] = attendee.cn item.params['ROLE'] = icalendar.prop.vText('REQ-PARTICIPANT') item.params['PARTSTAT'] = icalendar.prop.vText('NEEDS-ACTION') item.params['CUTYPE'] = icalendar.prop.vText('INDIVIDUAL') diff --git a/khal/khalendar/khalendar.py b/khal/khalendar/khalendar.py index 21c85ae8d..f578bac52 100644 --- a/khal/khalendar/khalendar.py +++ b/khal/khalendar/khalendar.py @@ -30,6 +30,8 @@ import logging import os import os.path +import subprocess +import re from typing import Any, Dict, Iterable, List, Optional, Set, Tuple, Union # noqa from ..custom_types import CalendarConfiguration, EventCreationTypes, LocaleConfiguration @@ -104,9 +106,12 @@ def __init__(self, self.priority = priority self.highlight_event_days = highlight_event_days self._locale = locale + self._contacts: Dict[str, List[str]] = {} # List of mail addresses of contacts self._backend = backend.SQLiteDb(self.names, dbpath, self._locale) self._last_ctags: Dict[str, str] = {} self.update_db() + for calendar in self._calendars.keys(): + self._contacts_update(calendar) @property def writable_names(self) -> List[str]: @@ -361,12 +366,24 @@ def _needs_update(self, calendar: str, remember: bool=False) -> bool: self._last_ctags[calendar] = local_ctag return local_ctag != self._backend.get_ctag(calendar) + def _contacts_update(self, calendar: str) -> None: + adaptercommand = self._calendars[calendar].get('address_adapter') + if adaptercommand is None: + self._contacts[calendar] = [] + else: + res = subprocess.check_output(["bash", "-c", adaptercommand]) + maildata = [re.split(r"\s{2,}", x) for x in res.decode("utf-8").split("\n")] + mails = ["%s <%s>" % (x[0], x[2]) for x in maildata if len(x) > 1] + self._contacts[calendar] = mails + + def _db_update(self, calendar: str) -> None: """implements the actual db update on a per calendar base""" local_ctag = self._local_ctag(calendar) db_hrefs = {href for href, etag in self._backend.list(calendar)} storage_hrefs: Set[str] = set() bdays = self._calendars[calendar].get('ctype') == 'birthdays' + self._contacts_update(calendar) with self._backend.at_once(): for href, etag in self._storages[calendar].list(): diff --git a/khal/settings/khal.spec b/khal/settings/khal.spec index 676d51032..c44acf62d 100644 --- a/khal/settings/khal.spec +++ b/khal/settings/khal.spec @@ -76,6 +76,8 @@ type = option('calendar', 'birthdays', 'discover', default='calendar') # belongs to the user. addresses = force_list(default='') +address_adapter = string(default=None) + [sqlite] # khal stores its internal caching database here, by default this will be in the *$XDG_DATA_HOME/khal/khal.db* (this will most likely be *~/.local/share/khal/khal.db*). path = expand_db_path(default=None) @@ -231,6 +233,7 @@ default_dayevent_alarm = timedelta(default='') # 'ikhal' only) enable_mouse = boolean(default=True) +address_adapter = string(default=None) # The view section contains configuration options that effect the visual appearance # when using khal and ikhal. diff --git a/khal/settings/utils.py b/khal/settings/utils.py index 4dac92987..fff36774d 100644 --- a/khal/settings/utils.py +++ b/khal/settings/utils.py @@ -260,6 +260,7 @@ def config_checks( 'type': _get_vdir_type(vdir), 'readonly': cconfig.get('readonly', False), 'priority': 10, + 'address_adapter': cconfig.get('address_adapter', None), } unique_vdir_name = get_unique_name(vdir, config['calendars'].keys()) config['calendars'][unique_vdir_name] = vdir_config diff --git a/khal/ui/attendeewidget.py b/khal/ui/attendeewidget.py index 48ee1094a..5385cb5b6 100644 --- a/khal/ui/attendeewidget.py +++ b/khal/ui/attendeewidget.py @@ -9,12 +9,6 @@ ("ilb_barInactive_focus", "light cyan", "dark gray"), ("ilb_barInactive_offFocus", "black", "dark gray")] -def get_mails(): - res = subprocess.check_output(["bash", "-c", "khard email gmail | tail -n +2"]) - maildata = [re.split(r"\s{2,}", x) for x in res.decode("utf-8").split("\n")] - mails = ["%s <%s>" % (x[0], x[2]) for x in maildata if len(x) > 1] - return mails - class MailPopup(urwid.PopUpLauncher): command_map = urwid.CommandMap() @@ -55,14 +49,21 @@ def keypress(self, size, key): current = self.get_current_mailpart() if len([x for x in self.maillist if current.lower() in x.lower()]) == 0: return + if len(current) == 0: + return self.open_pop_up() self.popup_visible = True def keycallback(self, size, key): + cmd = self.command_map[key] + if cmd == 'menu': + self.popup_visible = False + self.close_pop_up() self.widget.keypress((20,), key) self.justcompleted = False - num_candidates = self.listbox.update_mails(self.get_current_mailpart()) - if num_candidates == 0: + cmp = self.get_current_mailpart() + num_candidates = self.listbox.update_mails(cmp) + if num_candidates == 0 or len(cmp) == 0: self.popup_visible = False self.close_pop_up() @@ -135,8 +136,10 @@ def render(self, size, focus=False): class AttendeeWidget(urwid.WidgetWrap): - def __init__(self): - self.mails = get_mails() + def __init__(self, mails): + self.mails = mails + if self.mails is None: + self.mails = [] self.acedit = AutocompleteEdit() self.mp = MailPopup(self.acedit, self.mails) super().__init__(self.mp) diff --git a/khal/ui/editor.py b/khal/ui/editor.py index b1b1d9bce..15e3670f3 100644 --- a/khal/ui/editor.py +++ b/khal/ui/editor.py @@ -420,7 +420,7 @@ def decorate_choice(c) -> Tuple[str, str]: self.categories = urwid.AttrMap(ExtendedEdit( caption=('caption', 'Categories: '), edit_text=self.categories), 'edit', 'edit focus', ) - self.attendees = urwid.AttrMap(AttendeeWidget(), 'edit', 'edit focus') + self.attendees = urwid.AttrMap(AttendeeWidget(self.collection._contacts[self.event.calendar]), 'edit', 'edit focus') self.url = urwid.AttrMap(ExtendedEdit( caption=('caption', 'URL: '), edit_text=self.url), 'edit', 'edit focus', ) From d823af469cd14131f327bf409e2f6b9db2ecebea Mon Sep 17 00:00:00 2001 From: dabrows Date: Fri, 15 Dec 2023 21:52:13 +0100 Subject: [PATCH 04/17] Added correct conversion of attendees to VCARD, other small fixes --- khal/khalendar/event.py | 17 ++++++++++++++--- khal/ui/attendeewidget.py | 19 ++++++++++++++++--- khal/ui/editor.py | 9 +++++++-- 3 files changed, 37 insertions(+), 8 deletions(-) diff --git a/khal/khalendar/event.py b/khal/khalendar/event.py index a6f81ae92..c6427277c 100644 --- a/khal/khalendar/event.py +++ b/khal/khalendar/event.py @@ -54,6 +54,18 @@ def __init__(self, defline): self.cn = None self.mail = defline.strip().lower() + @staticmethod + def format_vcard(vcard): + data = str(vcard).split(":") + if len(data) > 1: + mail = data[1] + else: + mail = str(vcard) + cn = mail + if "CN" in vcard.params: + cn = vcard.params["CN"] + return "%s <%s>" % (cn, mail) + class Event: """base Event class for representing a *recurring instance* of an Event @@ -505,9 +517,8 @@ def update_location(self, location: str) -> None: def attendees(self) -> str: addresses = self._vevents[self.ref].get('ATTENDEE', []) if not isinstance(addresses, list): - addresses = [addresses, ] - return ", ".join([address.split(':')[-1] - for address in addresses]) + return addresses + return ", ".join([Attendee.format_vcard(address) for address in addresses]) def update_attendees(self, attendees: List[str]): assert isinstance(attendees, list) diff --git a/khal/ui/attendeewidget.py b/khal/ui/attendeewidget.py index 5385cb5b6..bc06d9f45 100644 --- a/khal/ui/attendeewidget.py +++ b/khal/ui/attendeewidget.py @@ -12,7 +12,8 @@ class MailPopup(urwid.PopUpLauncher): command_map = urwid.CommandMap() - own_commands = [] + own_commands = ["cursor left", "cursor right", + "cursor max left", "cursor max right"] def __init__(self, widget, maillist): self.maillist = maillist @@ -21,6 +22,9 @@ def __init__(self, widget, maillist): self.justcompleted = False super().__init__(widget) + def change_mail_list(self, mails): + self.maillist = mails + def get_current_mailpart(self): mails = self.widget.get_edit_text().split(",") lastmail = mails[-1].lstrip(" ") @@ -37,7 +41,7 @@ def get_num_mails(self): def keypress(self, size, key): cmd = self.command_map[key] - if cmd is not None and cmd not in self.own_commands: + if cmd is not None and cmd not in self.own_commands and key != " ": return key if self.justcompleted and key not in ", ": self.widget.keypress(size, ",") @@ -136,17 +140,26 @@ def render(self, size, focus=False): class AttendeeWidget(urwid.WidgetWrap): - def __init__(self, mails): + def __init__(self, initial_attendees, mails): self.mails = mails if self.mails is None: self.mails = [] + if initial_attendees is None: + initial_attendees = "" self.acedit = AutocompleteEdit() + self.acedit.set_edit_text(initial_attendees) self.mp = MailPopup(self.acedit, self.mails) super().__init__(self.mp) def get_attendees(self): return self.acedit.get_edit_text() + def change_mail_list(self, mails): + self.mails = mails + if self.mails is None: + self.mails = [] + self.mp.change_mail_list(mails) + if __name__ == "__main__": mails = get_mails() diff --git a/khal/ui/editor.py b/khal/ui/editor.py index 15e3670f3..259ac3c33 100644 --- a/khal/ui/editor.py +++ b/khal/ui/editor.py @@ -403,7 +403,8 @@ def decorate_choice(c) -> Tuple[str, str]: self.calendar_chooser= CAttrMap(Choice( [self.collection._calendars[c] for c in self.collection.writable_names], self.collection._calendars[self.event.calendar], - decorate_choice + decorate_choice, + callback = self.account_change ), 'caption') self.description = urwid.AttrMap( @@ -420,7 +421,7 @@ def decorate_choice(c) -> Tuple[str, str]: self.categories = urwid.AttrMap(ExtendedEdit( caption=('caption', 'Categories: '), edit_text=self.categories), 'edit', 'edit focus', ) - self.attendees = urwid.AttrMap(AttendeeWidget(self.collection._contacts[self.event.calendar]), 'edit', 'edit focus') + self.attendees = urwid.AttrMap(AttendeeWidget(self.event.attendees, self.collection._contacts[self.event.calendar]), 'edit', 'edit focus') self.url = urwid.AttrMap(ExtendedEdit( caption=('caption', 'URL: '), edit_text=self.url), 'edit', 'edit focus', ) @@ -486,6 +487,10 @@ def type_change(self, allday: bool) -> None: # either there were more than one alarm or the alarm was not the default pass + def account_change(self): + newaccount = self.calendar_chooser._original_widget.active + self.attendees._original_widget.change_mail_list(self.collection._contacts[newaccount["name"]]) + @property def title(self): # Window title return f'Edit: {get_wrapped_text(self.summary)}' From d2b5df313b75e1461d244bf0f6529c033ba2ec49 Mon Sep 17 00:00:00 2001 From: dabrows Date: Fri, 15 Dec 2023 22:01:43 +0100 Subject: [PATCH 05/17] ruff formatting --- khal/khalendar/event.py | 2 +- khal/khalendar/khalendar.py | 6 +- khal/settings/utils.py | 162 +++++------ khal/ui/attendeewidget.py | 286 ++++++++++---------- khal/ui/editor.py | 524 +++++++++++++++++++++--------------- 5 files changed, 539 insertions(+), 441 deletions(-) diff --git a/khal/khalendar/event.py b/khal/khalendar/event.py index c6427277c..0b2da12fb 100644 --- a/khal/khalendar/event.py +++ b/khal/khalendar/event.py @@ -64,7 +64,7 @@ def format_vcard(vcard): cn = mail if "CN" in vcard.params: cn = vcard.params["CN"] - return "%s <%s>" % (cn, mail) + return f"{cn} <{mail}>" class Event: diff --git a/khal/khalendar/khalendar.py b/khal/khalendar/khalendar.py index f578bac52..f427a788f 100644 --- a/khal/khalendar/khalendar.py +++ b/khal/khalendar/khalendar.py @@ -30,8 +30,8 @@ import logging import os import os.path -import subprocess import re +import subprocess from typing import Any, Dict, Iterable, List, Optional, Set, Tuple, Union # noqa from ..custom_types import CalendarConfiguration, EventCreationTypes, LocaleConfiguration @@ -372,8 +372,8 @@ def _contacts_update(self, calendar: str) -> None: self._contacts[calendar] = [] else: res = subprocess.check_output(["bash", "-c", adaptercommand]) - maildata = [re.split(r"\s{2,}", x) for x in res.decode("utf-8").split("\n")] - mails = ["%s <%s>" % (x[0], x[2]) for x in maildata if len(x) > 1] + maildata = [re.split(r"\s{2,}", x) for x in res.decode("utf-8").split("\n")] + mails = [f"{x[0]} <{x[2]}>" for x in maildata if len(x) > 1] self._contacts[calendar] = mails diff --git a/khal/settings/utils.py b/khal/settings/utils.py index fff36774d..57714de86 100644 --- a/khal/settings/utils.py +++ b/khal/settings/utils.py @@ -43,7 +43,7 @@ from ..terminal import COLORS from .exceptions import InvalidSettingsError -logger = logging.getLogger('khal') +logger = logging.getLogger("khal") def is_timezone(tzstring: Optional[str]) -> dt.tzinfo: @@ -69,35 +69,36 @@ def is_timedelta(string: str) -> dt.timedelta: raise VdtValueError(f"Invalid timedelta: {string}") -def weeknumber_option(option: str) -> Union[Literal['left', 'right'], 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 :returns: 'off', 'left', 'right' or False """ option = option.lower() - if option == 'left': - return 'left' - elif option == 'right': - return 'right' - elif option in ['off', 'false', '0', 'no', 'none']: + if option == "left": + return "left" + elif option == "right": + return "right" + elif option in ["off", "false", "0", "no", "none"]: return False else: raise VdtValueError( f"Invalid value '{option}' for option 'weeknumber', must be one of " - "'off', 'left' or 'right'") + "'off', 'left' or 'right'" + ) -def monthdisplay_option(option: str) -> Literal['firstday', 'firstfullweek']: +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 """ option = option.lower() - if option == 'firstday': - return 'firstday' - elif option == 'firstfullweek': - return 'firstfullweek' + if option == "firstday": + return "firstday" + elif option == "firstfullweek": + return "firstfullweek" else: raise VdtValueError( f"Invalid value '{option}' for option 'monthdisplay', must be one " @@ -113,7 +114,7 @@ def expand_path(path: str) -> str: def expand_db_path(path: str) -> str: """expands `~` as well as variable names, defaults to $XDG_DATA_HOME""" if path is None: - path = join(xdg.BaseDirectory.xdg_data_home, 'khal', 'khal.db') + path = join(xdg.BaseDirectory.xdg_data_home, "khal", "khal.db") return expanduser(expandvars(path)) @@ -128,11 +129,16 @@ def is_color(color: str) -> str: # 3) a color name from the 16 color palette # 4) a color index from the 256 color palette # 5) an HTML-style color code - if (color in ['', 'auto'] or - color in COLORS.keys() or - (color.isdigit() and int(color) >= 0 and int(color) <= 255) or - (color.startswith('#') and (len(color) in [4, 7, 9]) and - all(c in '01234567890abcdefABCDEF' for c in color[1:]))): + if ( + color in ["", "auto"] + or color in COLORS.keys() + or (color.isdigit() and int(color) >= 0 and int(color) <= 255) + or ( + color.startswith("#") + and (len(color) in [4, 7, 9]) + and all(c in "01234567890abcdefABCDEF" for c in color[1:]) + ) + ): return color raise VdtValueError(color) @@ -141,27 +147,27 @@ def test_default_calendar(config) -> None: """test if config['default']['default_calendar'] is set to a sensible value """ - if config['default']['default_calendar'] is None: + if config["default"]["default_calendar"] is None: pass - elif config['default']['default_calendar'] not in config['calendars']: + elif config["default"]["default_calendar"] not in config["calendars"]: logger.fatal( f"in section [default] {config['default']['default_calendar']} is " "not valid for 'default_calendar', must be one of " f"{config['calendars'].keys()}" ) raise InvalidSettingsError() - elif config['calendars'][config['default']['default_calendar']]['readonly']: - logger.fatal('default_calendar may not be read_only!') + elif config["calendars"][config["default"]["default_calendar"]]["readonly"]: + logger.fatal("default_calendar may not be read_only!") raise InvalidSettingsError() def get_color_from_vdir(path: str) -> Optional[str]: try: - color = Vdir(path, '.ics').get_meta('color') + color = Vdir(path, ".ics").get_meta("color") except CollectionNotFoundError: color = None - if color is None or color == '': - logger.debug(f'Found no or empty file `color` in {path}') + if color is None or color == "": + logger.debug(f"Found no or empty file `color` in {path}") return None color = color.strip() try: @@ -175,26 +181,25 @@ def get_color_from_vdir(path: str) -> Optional[str]: def get_unique_name(path: str, names: Iterable[str]) -> str: # TODO take care of edge cases, make unique name finding less brain-dead try: - name = Vdir(path, '.ics').get_meta('displayname') + name = Vdir(path, ".ics").get_meta("displayname") except CollectionNotFoundError: - logger.fatal(f'The calendar at `{path}` is not a directory.') + logger.fatal(f"The calendar at `{path}` is not a directory.") raise - if name is None or name == '': - logger.debug(f'Found no or empty file `displayname` in {path}') + if name is None or name == "": + logger.debug(f"Found no or empty file `displayname` in {path}") name = os.path.split(path)[-1] if name in names: while name in names: - name = name + '1' + name = name + "1" return name def get_all_vdirs(expand_path: str) -> Iterable[str]: - """returns a list of paths, expanded using glob - """ + """returns a list of paths, expanded using glob""" # FIXME currently returns a list of all directories in path # we add an additional / at the end to make sure we are only getting # directories - items = glob.glob(f'{expand_path}/', recursive=True) + items = glob.glob(f"{expand_path}/", recursive=True) paths = [pathlib.Path(item) for item in sorted(items, key=len, reverse=True)] leaves = set() parents = set() @@ -211,73 +216,80 @@ def get_all_vdirs(expand_path: str) -> Iterable[str]: def get_vdir_type(_: str) -> str: # TODO implement - return 'calendar' + 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) + 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()) + 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, - _get_color_from_vdir: Callable=get_color_from_vdir, - _get_vdir_type: Callable=get_vdir_type, + _get_color_from_vdir: Callable = get_color_from_vdir, + _get_vdir_type: Callable = get_vdir_type, ) -> None: """do some tests on the config we cannot do with configobj's validator""" # TODO rename or split up, we are also expanding vdirs of type discover - if len(config['calendars'].keys()) < 1: - logger.fatal('Found no calendar section in the config file') + if len(config["calendars"].keys()) < 1: + logger.fatal("Found no calendar section in the config file") raise InvalidSettingsError() - config['sqlite']['path'] = expand_db_path(config['sqlite']['path']) - if not config['locale']['default_timezone']: - config['locale']['default_timezone'] = is_timezone( - config['locale']['default_timezone']) - if not config['locale']['local_timezone']: - config['locale']['local_timezone'] = is_timezone( - config['locale']['local_timezone']) + config["sqlite"]["path"] = expand_db_path(config["sqlite"]["path"]) + if not config["locale"]["default_timezone"]: + config["locale"]["default_timezone"] = is_timezone(config["locale"]["default_timezone"]) + if not config["locale"]["local_timezone"]: + config["locale"]["local_timezone"] = is_timezone(config["locale"]["local_timezone"]) # expand calendars with type = discover # we need a copy of config['calendars'], because we modify config in the body of the loop - for cname, cconfig in sorted(config['calendars'].items()): - if not isinstance(config['calendars'][cname], dict): - logger.fatal('Invalid config file, probably missing calendar sections') + for cname, cconfig in sorted(config["calendars"].items()): + if not isinstance(config["calendars"][cname], dict): + logger.fatal("Invalid config file, probably missing calendar sections") raise InvalidSettingsError - if config['calendars'][cname]['type'] == 'discover': + if config["calendars"][cname]["type"] == "discover": logger.debug(f"discovering calendars in {cconfig['path']}") - vdirs_discovered = get_all_vdirs(cconfig['path']) + vdirs_discovered = get_all_vdirs(cconfig["path"]) logger.debug(f"found the following vdirs: {vdirs_discovered}") for vdir in vdirs_discovered: vdir_config = { - 'path': vdir, - 'color': _get_color_from_vdir(vdir) or cconfig.get('color', None), - 'type': _get_vdir_type(vdir), - 'readonly': cconfig.get('readonly', False), - 'priority': 10, - 'address_adapter': cconfig.get('address_adapter', None), + "path": vdir, + "color": _get_color_from_vdir(vdir) or cconfig.get("color", None), + "type": _get_vdir_type(vdir), + "readonly": cconfig.get("readonly", False), + "priority": 10, + "address_adapter": cconfig.get("address_adapter", None), } - unique_vdir_name = get_unique_name(vdir, config['calendars'].keys()) - config['calendars'][unique_vdir_name] = vdir_config - config['calendars'].pop(cname) + unique_vdir_name = get_unique_name(vdir, config["calendars"].keys()) + config["calendars"][unique_vdir_name] = vdir_config + config["calendars"].pop(cname) test_default_calendar(config) - for calendar in config['calendars']: - if config['calendars'][calendar]['type'] == 'birthdays': - config['calendars'][calendar]['readonly'] = True - if config['calendars'][calendar]['color'] == 'auto': - config['calendars'][calendar]['color'] = \ - _get_color_from_vdir(config['calendars'][calendar]['path']) + for calendar in config["calendars"]: + if config["calendars"][calendar]["type"] == "birthdays": + config["calendars"][calendar]["readonly"] = True + 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]) + 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') + logger.fatal("Invalid palette entry") raise InvalidSettingsError() diff --git a/khal/ui/attendeewidget.py b/khal/ui/attendeewidget.py index bc06d9f45..f3912c1f1 100644 --- a/khal/ui/attendeewidget.py +++ b/khal/ui/attendeewidget.py @@ -1,170 +1,154 @@ import urwid from additional_urwid_widgets import IndicativeListBox -import subprocess -import re - -PALETTE = [("reveal_focus", "black", "light cyan", "standout"), - ("ilb_barActive_focus", "dark cyan", "light gray"), - ("ilb_barActive_offFocus", "light gray", "dark gray"), - ("ilb_barInactive_focus", "light cyan", "dark gray"), - ("ilb_barInactive_offFocus", "black", "dark gray")] class MailPopup(urwid.PopUpLauncher): - command_map = urwid.CommandMap() - own_commands = ["cursor left", "cursor right", - "cursor max left", "cursor max right"] - - def __init__(self, widget, maillist): - self.maillist = maillist - self.widget = widget - self.popup_visible = False - self.justcompleted = False - super().__init__(widget) - - def change_mail_list(self, mails): - self.maillist = mails - - def get_current_mailpart(self): - mails = self.widget.get_edit_text().split(",") - lastmail = mails[-1].lstrip(" ") - return lastmail - - def complete_mail(self, newmail): - mails = [x.strip() for x in self.widget.get_edit_text().split(",")[:-1]] - mails += [newmail] - return ", ".join(mails) - - def get_num_mails(self): - mails = self.widget.get_edit_text().split(",") - return len(mails) - - def keypress(self, size, key): - cmd = self.command_map[key] - if cmd is not None and cmd not in self.own_commands and key != " ": - return key - if self.justcompleted and key not in ", ": - self.widget.keypress(size, ",") - self.widget.keypress(size, " ") - self.widget.keypress(size, key) - self.justcompleted = False - if not self.popup_visible: - # Only open the popup list if there will be at least 1 address displayed - current = self.get_current_mailpart() - if len([x for x in self.maillist if current.lower() in x.lower()]) == 0: - return - if len(current) == 0: - return - self.open_pop_up() - self.popup_visible = True - - def keycallback(self, size, key): - cmd = self.command_map[key] - if cmd == 'menu': - self.popup_visible = False - self.close_pop_up() - self.widget.keypress((20,), key) - self.justcompleted = False - cmp = self.get_current_mailpart() - num_candidates = self.listbox.update_mails(cmp) - if num_candidates == 0 or len(cmp) == 0: - self.popup_visible = False - self.close_pop_up() - - def donecallback(self, text): - self.widget.set_edit_text(self.complete_mail(text)) - fulllength = len(self.widget.get_edit_text()) - self.widget.move_cursor_to_coords((fulllength,), fulllength, 0) - self.close_pop_up() - self.popup_visible = False - self.justcompleted = True - - def create_pop_up(self): - current_mailpart = self.get_current_mailpart() - self.listbox = MailListBox(self.maillist, self.keycallback, - self.donecallback, current_mailpart) - return urwid.WidgetWrap(self.listbox) - - def get_pop_up_parameters(self): - return {"left": 0, "top": 1, "overlay_width": 60, "overlay_height": 10} - - def render(self, size, focus=False): - return super().render(size, True) + command_map = urwid.CommandMap() + own_commands = ["cursor left", "cursor right", "cursor max left", "cursor max right"] + + def __init__(self, widget, maillist): + self.maillist = maillist + self.widget = widget + self.popup_visible = False + self.justcompleted = False + super().__init__(widget) + + def change_mail_list(self, mails): + self.maillist = mails + + def get_current_mailpart(self): + mails = self.widget.get_edit_text().split(",") + lastmail = mails[-1].lstrip(" ") + return lastmail + + def complete_mail(self, newmail): + mails = [x.strip() for x in self.widget.get_edit_text().split(",")[:-1]] + mails += [newmail] + return ", ".join(mails) + + def get_num_mails(self): + mails = self.widget.get_edit_text().split(",") + return len(mails) + + def keypress(self, size, key): + cmd = self.command_map[key] + if cmd is not None and cmd not in self.own_commands and key != " ": + return key + if self.justcompleted and key not in ", ": + self.widget.keypress(size, ",") + self.widget.keypress(size, " ") + self.widget.keypress(size, key) + self.justcompleted = False + if not self.popup_visible: + # Only open the popup list if there will be at least 1 address displayed + current = self.get_current_mailpart() + if len([x for x in self.maillist if current.lower() in x.lower()]) == 0: + return + if len(current) == 0: + return + self.open_pop_up() + self.popup_visible = True + + def keycallback(self, size, key): + cmd = self.command_map[key] + if cmd == "menu": + self.popup_visible = False + self.close_pop_up() + self.widget.keypress((20,), key) + self.justcompleted = False + cmp = self.get_current_mailpart() + num_candidates = self.listbox.update_mails(cmp) + if num_candidates == 0 or len(cmp) == 0: + self.popup_visible = False + self.close_pop_up() + + def donecallback(self, text): + self.widget.set_edit_text(self.complete_mail(text)) + fulllength = len(self.widget.get_edit_text()) + self.widget.move_cursor_to_coords((fulllength,), fulllength, 0) + self.close_pop_up() + self.popup_visible = False + self.justcompleted = True + + def create_pop_up(self): + current_mailpart = self.get_current_mailpart() + self.listbox = MailListBox( + self.maillist, self.keycallback, self.donecallback, current_mailpart + ) + return urwid.WidgetWrap(self.listbox) + + def get_pop_up_parameters(self): + return {"left": 0, "top": 1, "overlay_width": 60, "overlay_height": 10} + + def render(self, size, focus=False): + return super().render(size, True) class MailListItem(urwid.Text): - def render(self, size, focus=False): - return super().render(size, False) + def render(self, size, focus=False): + return super().render(size, False) - def selectable(self): - return True + def selectable(self): + return True - def keypress(self, size, key): - return key + def keypress(self, size, key): + return key -class MailListBox(IndicativeListBox): - command_map = urwid.CommandMap() - own_commands = [urwid.CURSOR_DOWN, urwid.CURSOR_UP, urwid.ACTIVATE] - - def __init__(self, mails, keycallback, donecallback, current_mailpart, **args): - self.mails = [MailListItem(x) for x in mails] - mailsBody = [urwid.AttrMap(x, None, "list focused") for x in self.mails] - self.keycallback = keycallback - self.donecallback = donecallback - super().__init__(mailsBody, **args) - if len(current_mailpart) != 0: - self.update_mails(current_mailpart) - - def keypress(self, size, key): - cmd = self.command_map[key] - if cmd not in self.own_commands or key == " ": - self.keycallback(size, key) - elif cmd is urwid.ACTIVATE: - self.donecallback(self.get_selected_item()._original_widget.get_text()[0]) - else: - super().keypress(size, key) - - def update_mails(self, new_edit_text): - new_body = [] - for mail in self.mails: - if new_edit_text.lower() in mail.get_text()[0].lower(): - new_body += [urwid.AttrMap(mail, None, "list focused")] - self.set_body(new_body) - return len(new_body) +class MailListBox(IndicativeListBox): + command_map = urwid.CommandMap() + own_commands = [urwid.CURSOR_DOWN, urwid.CURSOR_UP, urwid.ACTIVATE] + + def __init__(self, mails, keycallback, donecallback, current_mailpart, **args): + self.mails = [MailListItem(x) for x in mails] + mailsBody = [urwid.AttrMap(x, None, "list focused") for x in self.mails] + self.keycallback = keycallback + self.donecallback = donecallback + super().__init__(mailsBody, **args) + if len(current_mailpart) != 0: + self.update_mails(current_mailpart) + + def keypress(self, size, key): + cmd = self.command_map[key] + if cmd not in self.own_commands or key == " ": + self.keycallback(size, key) + elif cmd is urwid.ACTIVATE: + self.donecallback(self.get_selected_item()._original_widget.get_text()[0]) + else: + super().keypress(size, key) + + def update_mails(self, new_edit_text): + new_body = [] + for mail in self.mails: + if new_edit_text.lower() in mail.get_text()[0].lower(): + new_body += [urwid.AttrMap(mail, None, "list focused")] + self.set_body(new_body) + return len(new_body) class AutocompleteEdit(urwid.Edit): - def render(self, size, focus=False): - return super().render(size, True) + def render(self, size, focus=False): + return super().render(size, True) class AttendeeWidget(urwid.WidgetWrap): - def __init__(self, initial_attendees, mails): - self.mails = mails - if self.mails is None: - self.mails = [] - if initial_attendees is None: - initial_attendees = "" - self.acedit = AutocompleteEdit() - self.acedit.set_edit_text(initial_attendees) - self.mp = MailPopup(self.acedit, self.mails) - super().__init__(self.mp) - - def get_attendees(self): - return self.acedit.get_edit_text() - - def change_mail_list(self, mails): - self.mails = mails - if self.mails is None: - self.mails = [] - self.mp.change_mail_list(mails) - - -if __name__ == "__main__": - mails = get_mails() - acedit = AutocompleteEdit() - mp = MailPopup(acedit, mails) - loop = urwid.MainLoop(urwid.Filler(mp), PALETTE, pop_ups=True) - loop.run() + def __init__(self, initial_attendees, mails): + self.mails = mails + if self.mails is None: + self.mails = [] + if initial_attendees is None: + initial_attendees = "" + self.acedit = AutocompleteEdit() + self.acedit.set_edit_text(initial_attendees) + self.mp = MailPopup(self.acedit, self.mails) + super().__init__(self.mp) + + def get_attendees(self): + return self.acedit.get_edit_text() + + def change_mail_list(self, mails): + self.mails = mails + if self.mails is None: + self.mails = [] + self.mp.change_mail_list(mails) diff --git a/khal/ui/editor.py b/khal/ui/editor.py index 259ac3c33..70dfb05c5 100644 --- a/khal/ui/editor.py +++ b/khal/ui/editor.py @@ -25,6 +25,7 @@ import urwid from ..utils import get_weekday_occurrence, get_wrapped_text +from .attendeewidget import AttendeeWidget from .widgets import ( AlarmsEditor, CalendarWidget, @@ -43,13 +44,11 @@ button, ) -from .attendeewidget import AttendeeWidget - if TYPE_CHECKING: import khal.khalendar.event -class StartEnd: +class StartEnd: def __init__(self, startdate, starttime, enddate, endtime) -> None: """collecting some common properties""" self.startdate = startdate @@ -59,8 +58,15 @@ def __init__(self, startdate, starttime, enddate, endtime) -> None: class CalendarPopUp(urwid.PopUpLauncher): - def __init__(self, widget, on_date_change, weeknumbers: Literal['left', 'right', False]=False, - firstweekday=0, monthdisplay='firstday', keybindings=None) -> None: + 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 self._monthdisplay = monthdisplay @@ -69,7 +75,7 @@ def __init__(self, widget, on_date_change, weeknumbers: Literal['left', 'right', self.__super.__init__(widget) def keypress(self, size, key): - if key == 'enter': + if key == "enter": self.open_pop_up() else: return super().keypress(size, key) @@ -79,26 +85,31 @@ def on_change(new_date): self._get_base_widget().set_value(new_date) self._on_date_change(new_date) - on_press = {'enter': lambda _, __: self.close_pop_up(), - 'esc': lambda _, __: self.close_pop_up()} + on_press = { + "enter": lambda _, __: self.close_pop_up(), + "esc": lambda _, __: self.close_pop_up(), + } try: initial_date = self.base_widget._get_current_value() except DateConversionError: return None else: pop_up = CalendarWidget( - on_change, self._keybindings, on_press, + on_change, + self._keybindings, + on_press, firstweekday=self._firstweekday, weeknumbers=self._weeknumbers, monthdisplay=self._monthdisplay, - initial=initial_date) - pop_up = CAttrMap(pop_up, 'calendar', ' calendar focus') - pop_up = CAttrMap(urwid.LineBox(pop_up), 'calendar', 'calendar focus') + initial=initial_date, + ) + 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): - width = 31 if self._weeknumbers == 'right' else 28 - return {'left': 0, 'top': 1, 'overlay_width': width, 'overlay_height': 8} + width = 31 if self._weeknumbers == "right" else 28 + return {"left": 0, "top": 1, "overlay_width": width, "overlay_height": 8} class DateEdit(urwid.WidgetWrap): @@ -111,11 +122,11 @@ class DateEdit(urwid.WidgetWrap): def __init__( 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', + 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, ) -> None: datewidth = len(startdt.strftime(dateformat)) + 1 @@ -127,12 +138,15 @@ def __init__( EditWidget=DateWidget, validate=self._validate, edit_text=startdt.strftime(dateformat), - on_date_change=on_date_change) - wrapped = CalendarPopUp(self._edit, on_date_change, weeknumbers, - firstweekday, monthdisplay, keybindings) + on_date_change=on_date_change, + ) + wrapped = CalendarPopUp( + self._edit, on_date_change, weeknumbers, firstweekday, monthdisplay, keybindings + ) padded = CAttrMap( - urwid.Padding(wrapped, align='left', width=datewidth, left=0, right=1), - 'calendar', 'calendar focus', + urwid.Padding(wrapped, align="left", width=datewidth, left=0, right=1), + "calendar", + "calendar focus", ) super().__init__(padded) @@ -165,14 +179,15 @@ def date(self, date): class StartEndEditor(urwid.WidgetWrap): """Widget for editing start and end times (of an event).""" - def __init__(self, - start: dt.datetime, - end: dt.datetime, - conf, - on_start_date_change=lambda x: None, - on_end_date_change=lambda x: None, - on_type_change: Callable[[bool], None]=lambda _: None, - ) -> None: + def __init__( + self, + start: dt.datetime, + end: dt.datetime, + conf, + on_start_date_change=lambda x: None, + on_end_date_change=lambda x: None, + on_type_change: Callable[[bool], None] = lambda _: None, + ) -> None: """ :param on_start_date_change: a callable that gets called everytime a new start date is entered, with that new date as an argument @@ -191,12 +206,11 @@ def __init__(self, self.on_start_date_change = on_start_date_change self.on_end_date_change = on_end_date_change self.on_type_change = on_type_change - self._datewidth = len(start.strftime(self.conf['locale']['longdateformat'])) - self._timewidth = len(start.strftime(self.conf['locale']['timeformat'])) + self._datewidth = len(start.strftime(self.conf["locale"]["longdateformat"])) + self._timewidth = len(start.strftime(self.conf["locale"]["timeformat"])) # this will contain the widgets for [start|end] [date|time] self.widgets = StartEnd(None, None, None, None) - self.checkallday = urwid.CheckBox( - 'Allday', state=self.allday, on_state_change=self.toggle) + self.checkallday = urwid.CheckBox("Allday", state=self.allday, on_state_change=self.toggle) self.toggle(None, self.allday) def keypress(self, size, key): @@ -218,15 +232,15 @@ def _start_time(self): @property def localize_start(self): - if getattr(self.startdt, 'tzinfo', None) is None: - return self.conf['locale']['default_timezone'].localize + if getattr(self.startdt, "tzinfo", None) is None: + return self.conf["locale"]["default_timezone"].localize else: return self.startdt.tzinfo.localize @property def localize_end(self): - if getattr(self.enddt, 'tzinfo', None) is None: - return self.conf['locale']['default_timezone'].localize + if getattr(self.enddt, "tzinfo", None) is None: + return self.conf["locale"]["default_timezone"].localize else: return self.enddt.tzinfo.localize @@ -246,9 +260,10 @@ def _end_time(self): def _validate_start_time(self, text): try: - startval = dt.datetime.strptime(text, self.conf['locale']['timeformat']) + startval = dt.datetime.strptime(text, self.conf["locale"]["timeformat"]) self._startdt = self.localize_start( - dt.datetime.combine(self._startdt.date(), startval.time())) + dt.datetime.combine(self._startdt.date(), startval.time()) + ) except ValueError: return False else: @@ -260,7 +275,7 @@ def _start_date_change(self, date): def _validate_end_time(self, text): try: - endval = dt.datetime.strptime(text, self.conf['locale']['timeformat']) + endval = dt.datetime.strptime(text, self.conf["locale"]["timeformat"]) self._enddt = self.localize_end(dt.datetime.combine(self._enddt.date(), endval.time())) except ValueError: return False @@ -291,55 +306,74 @@ def toggle(self, checkbox, state: bool): self._enddt = self._enddt.date() self.allday = state self.widgets.startdate = DateEdit( - self._startdt, self.conf['locale']['longdateformat'], - self._start_date_change, self.conf['locale']['weeknumbers'], - self.conf['locale']['firstweekday'], - self.conf['view']['monthdisplay'], - self.conf['keybindings'], + self._startdt, + self.conf["locale"]["longdateformat"], + self._start_date_change, + self.conf["locale"]["weeknumbers"], + self.conf["locale"]["firstweekday"], + self.conf["view"]["monthdisplay"], + self.conf["keybindings"], ) self.widgets.enddate = DateEdit( - self._enddt, self.conf['locale']['longdateformat'], - self._end_date_change, self.conf['locale']['weeknumbers'], - self.conf['locale']['firstweekday'], - self.conf['view']['monthdisplay'], - self.conf['keybindings'], + self._enddt, + self.conf["locale"]["longdateformat"], + self._end_date_change, + self.conf["locale"]["weeknumbers"], + self.conf["locale"]["firstweekday"], + self.conf["view"]["monthdisplay"], + self.conf["keybindings"], ) if state is True: # allday event self.on_type_change(True) timewidth = 1 - self.widgets.starttime = urwid.Text('') - self.widgets.endtime = urwid.Text('') + self.widgets.starttime = urwid.Text("") + self.widgets.endtime = urwid.Text("") elif state is False: # datetime event self.on_type_change(False) timewidth = self._timewidth + 1 raw_start_time_widget = ValidatedEdit( - dateformat=self.conf['locale']['timeformat'], + dateformat=self.conf["locale"]["timeformat"], EditWidget=TimeWidget, validate=self._validate_start_time, - edit_text=self.startdt.strftime(self.conf['locale']['timeformat']), + edit_text=self.startdt.strftime(self.conf["locale"]["timeformat"]), ) self.widgets.starttime = urwid.Padding( - raw_start_time_widget, align='left', width=self._timewidth + 1, left=1) + raw_start_time_widget, align="left", width=self._timewidth + 1, left=1 + ) raw_end_time_widget = ValidatedEdit( - dateformat=self.conf['locale']['timeformat'], + dateformat=self.conf["locale"]["timeformat"], EditWidget=TimeWidget, validate=self._validate_end_time, - edit_text=self.enddt.strftime(self.conf['locale']['timeformat']), + edit_text=self.enddt.strftime(self.conf["locale"]["timeformat"]), ) self.widgets.endtime = urwid.Padding( - raw_end_time_widget, align='left', width=self._timewidth + 1, left=1) - - columns = NPile([ - self.checkallday, - NColumns([(5, urwid.Text('From:')), (self._datewidth, self.widgets.startdate), ( - timewidth, self.widgets.starttime)], dividechars=1), - NColumns( - [(5, urwid.Text('To:')), (self._datewidth, self.widgets.enddate), - (timewidth, self.widgets.endtime)], - dividechars=1) - ], focus_item=1) + raw_end_time_widget, align="left", width=self._timewidth + 1, left=1 + ) + + columns = NPile( + [ + self.checkallday, + NColumns( + [ + (5, urwid.Text("From:")), + (self._datewidth, self.widgets.startdate), + (timewidth, self.widgets.starttime), + ], + dividechars=1, + ), + NColumns( + [ + (5, urwid.Text("To:")), + (self._datewidth, self.widgets.enddate), + (timewidth, self.widgets.endtime), + ], + dividechars=1, + ), + ], + focus_item=1, + ) urwid.WidgetWrap.__init__(self, columns) @property @@ -357,9 +391,9 @@ class EventEditor(urwid.WidgetWrap): def __init__( self, pane, - event: 'khal.khalendar.event.Event', + event: "khal.khalendar.event.Event", save_callback=None, - always_save: bool=False, + always_save: bool = False, ) -> None: """ :param save_callback: call when saving event with new start and end @@ -382,75 +416,109 @@ def __init__( self.categories = event.categories self.url = event.url self.startendeditor = StartEndEditor( - event.start_local, event.end_local, self._conf, - self.start_datechange, self.end_datechange, + event.start_local, + event.end_local, + self._conf, + self.start_datechange, + self.end_datechange, self.type_change, ) # TODO make sure recurrence rules cannot be edited if we only # edit one instance (or this and future) (once we support that) self.recurrenceeditor = RecurrenceEditor( - self.event.recurobject, self._conf, event.start_local, + self.event.recurobject, + self._conf, + event.start_local, ) - self.summary = urwid.AttrMap(ExtendedEdit( - caption=('caption', 'Title: '), edit_text=event.summary), 'edit', 'edit focus', + self.summary = urwid.AttrMap( + ExtendedEdit(caption=("caption", "Title: "), edit_text=event.summary), + "edit", + "edit focus", ) - divider = urwid.Divider(' ') + divider = urwid.Divider(" ") 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], - decorate_choice, - callback = self.account_change - ), 'caption') + 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], + decorate_choice, + callback=self.account_change, + ), + "caption", + ) self.description = urwid.AttrMap( ExtendedEdit( - caption=('caption', 'Description: '), - edit_text=self.description, - multiline=True + caption=("caption", "Description: "), edit_text=self.description, multiline=True ), - 'edit', 'edit focus', + "edit", + "edit focus", + ) + self.location = urwid.AttrMap( + ExtendedEdit(caption=("caption", "Location: "), edit_text=self.location), + "edit", + "edit focus", ) - self.location = urwid.AttrMap(ExtendedEdit( - caption=('caption', 'Location: '), edit_text=self.location), 'edit', 'edit focus', + self.categories = urwid.AttrMap( + ExtendedEdit(caption=("caption", "Categories: "), edit_text=self.categories), + "edit", + "edit focus", ) - self.categories = urwid.AttrMap(ExtendedEdit( - caption=('caption', 'Categories: '), edit_text=self.categories), 'edit', 'edit focus', + self.attendees = urwid.AttrMap( + AttendeeWidget(self.event.attendees, self.collection._contacts[self.event.calendar]), + "edit", + "edit focus", ) - self.attendees = urwid.AttrMap(AttendeeWidget(self.event.attendees, self.collection._contacts[self.event.calendar]), 'edit', 'edit focus') - self.url = urwid.AttrMap(ExtendedEdit( - caption=('caption', 'URL: '), edit_text=self.url), 'edit', 'edit focus', + self.url = urwid.AttrMap( + ExtendedEdit(caption=("caption", "URL: "), edit_text=self.url), + "edit", + "edit focus", ) self.alarmseditor: AlarmsEditor = AlarmsEditor(self.event) - self.pile = NListBox(urwid.SimpleFocusListWalker([ - self.summary, - urwid.Columns([(13, urwid.AttrMap(urwid.Text('Calendar:'), 'caption')), - (12, self.calendar_chooser)], - ), - divider, - self.location, - self.categories, - self.description, - self.url, - divider, - self.attendees, - divider, - self.startendeditor, - self.recurrenceeditor, - divider, - self.alarmseditor, - divider, - urwid.Columns( - [(12, button('Save', on_press=self.save, padding_left=0, padding_right=0))] + self.pile = NListBox( + urwid.SimpleFocusListWalker( + [ + self.summary, + urwid.Columns( + [ + (13, urwid.AttrMap(urwid.Text("Calendar:"), "caption")), + (12, self.calendar_chooser), + ], + ), + divider, + self.location, + self.categories, + self.description, + self.url, + divider, + self.attendees, + divider, + self.startendeditor, + self.recurrenceeditor, + divider, + self.alarmseditor, + divider, + urwid.Columns( + [(12, button("Save", on_press=self.save, padding_left=0, padding_right=0))] + ), + urwid.Columns( + [ + ( + 12, + button( + "Export", on_press=self.export, padding_left=0, padding_right=0 + ), + ) + ], + ), + ] ), - urwid.Columns( - [(12, button('Export', on_press=self.export, padding_left=0, padding_right=0))], - ) - ]), outermost=True) + outermost=True, + ) self._always_save = always_save urwid.WidgetWrap.__init__(self, self.pile) @@ -467,13 +535,13 @@ def type_change(self, allday: bool) -> None: :params allday: True if the event is now an allday event, False if it isn't """ # test if self.alarmseditor exists - if not hasattr(self, 'alarmseditor'): + if not hasattr(self, "alarmseditor"): return # to make the alarms before the event, we need to set it them to # negative values - default_event_alarm = -1 * self._conf['default']['default_event_alarm'] - default_dayevent_alarm =-1 * self._conf['default']['default_dayevent_alarm'] + default_event_alarm = -1 * self._conf["default"]["default_event_alarm"] + default_dayevent_alarm = -1 * self._conf["default"]["default_dayevent_alarm"] alarms = self.alarmseditor.get_alarms() if len(alarms) == 1: timedelta = alarms[0][0] @@ -488,12 +556,14 @@ def type_change(self, allday: bool) -> None: pass def account_change(self): - newaccount = self.calendar_chooser._original_widget.active - self.attendees._original_widget.change_mail_list(self.collection._contacts[newaccount["name"]]) + newaccount = self.calendar_chooser._original_widget.active + self.attendees._original_widget.change_mail_list( + self.collection._contacts[newaccount["name"]] + ) @property def title(self): # Window title - return f'Edit: {get_wrapped_text(self.summary)}' + return f"Edit: {get_wrapped_text(self.summary)}" @classmethod def selectable(cls): @@ -525,13 +595,12 @@ def update_vevent(self): self.event.update_summary(get_wrapped_text(self.summary)) self.event.update_description(get_wrapped_text(self.description)) self.event.update_location(get_wrapped_text(self.location)) - self.event.update_attendees(self.attendees._original_widget.get_attendees().split(',')) - self.event.update_categories(get_wrapped_text(self.categories).split(',')) + self.event.update_attendees(self.attendees._original_widget.get_attendees().split(",")) + self.event.update_categories(get_wrapped_text(self.categories).split(",")) self.event.update_url(get_wrapped_text(self.url)) if self.startendeditor.changed: - self.event.update_start_end( - self.startendeditor.startdt, self.startendeditor.enddt) + self.event.update_start_end(self.startendeditor.startdt, self.startendeditor.enddt) if self.recurrenceeditor.changed: rrule = self.recurrenceeditor.active self.event.update_rrule(rrule) @@ -544,20 +613,17 @@ def export(self, button): export the event as ICS :param button: not needed, passed via the button press """ + def export_this(_, user_data): try: self.event.export_ics(user_data.get_edit_text()) except Exception as e: self.pane.window.backtrack() - self.pane.window.alert( - ('light red', - 'Failed to save event: %s' % e)) + self.pane.window.alert(("light red", "Failed to save event: %s" % e)) return self.pane.window.backtrack() - self.pane.window.alert( - ('light green', - 'Event successfuly exported')) + self.pane.window.alert(("light green", "Event successfuly exported")) overlay = urwid.Overlay( ExportDialog( @@ -566,7 +632,11 @@ def export_this(_, user_data): self.event, ), self.pane, - 'center', ('relative', 50), ('relative', 50), None) + "center", + ("relative", 50), + ("relative", 50), + None, + ) self.pane.window.open(overlay) def save(self, button): @@ -576,8 +646,7 @@ def save(self, button): :param button: not needed, passed via the button press """ if not self.startendeditor.validate(): - self.pane.window.alert( - ('light red', "Can't save: end date is before start date!")) + self.pane.window.alert(("light red", "Can't save: end date is before start date!")) return if self._always_save or self.changed is True: @@ -585,43 +654,39 @@ def save(self, button): self.event.allday = self.startendeditor.allday self.event.increment_sequence() if self.event.etag is None: # has not been saved before - self.event.calendar = self.calendar_chooser.original_widget.active['name'] + self.event.calendar = self.calendar_chooser.original_widget.active["name"] self.collection.insert(self.event) elif self.calendar_chooser.changed: - self.collection.change_collection( - self.event, - self.calendar_chooser.active['name'] - ) + self.collection.change_collection(self.event, self.calendar_chooser.active["name"]) else: self.collection.update(self.event) self._save_callback( - self.event.start_local, self.event.end_local, + self.event.start_local, + self.event.end_local, self.event.recurring or self.recurrenceeditor.changed, ) self._abort_confirmed = False self.pane.window.backtrack() 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.')) + 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 None else: self._abort_confirmed = False - if key in self.pane._conf['keybindings']['save']: + if key in self.pane._conf["keybindings"]["save"]: self.save(None) return None return super().keypress(size, key) -WEEKDAYS = ['MO', 'TU', 'WE', 'TH', 'FR', 'SA', 'SU'] # TODO use locale and respect weekdaystart +WEEKDAYS = ["MO", "TU", "WE", "TH", "FR", "SA", "SU"] # TODO use locale and respect weekdaystart class WeekDaySelector(urwid.WidgetWrap): def __init__(self, startdt, selected_days) -> None: - self._weekday_boxes = {day: urwid.CheckBox(day, state=False) for day in WEEKDAYS} weekday = startdt.weekday() self._weekday_boxes[WEEKDAYS[weekday]].state = True @@ -637,7 +702,6 @@ def days(self): class RecurrenceEditor(urwid.WidgetWrap): - def __init__(self, rrule, conf, startdt) -> None: self._conf = conf self._startdt = startdt @@ -645,7 +709,9 @@ def __init__(self, rrule, conf, startdt) -> None: self.repeat = bool(rrule) self._allow_edit = not self.repeat or self.check_understood_rrule(rrule) self.repeat_box = urwid.CheckBox( - 'Repeat: ', state=self.repeat, on_state_change=self.check_repeat, + "Repeat: ", + state=self.repeat, + on_state_change=self.check_repeat, ) if "UNTIL" in self._rrule: @@ -655,24 +721,42 @@ def __init__(self, rrule, conf, startdt) -> None: else: self._until = "Forever" - recurrence = self._rrule['freq'][0].lower() if self._rrule else "weekly" - self.recurrence_choice = CPadding(CAttrMap(Choice( - ["daily", "weekly", "monthly", "yearly"], - recurrence, - callback=self.rebuild, - ), 'popupper'), align='center', left=2, right=2) + recurrence = self._rrule["freq"][0].lower() if self._rrule else "weekly" + self.recurrence_choice = CPadding( + CAttrMap( + Choice( + ["daily", "weekly", "monthly", "yearly"], + recurrence, + callback=self.rebuild, + ), + "popupper", + ), + align="center", + left=2, + right=2, + ) self.interval_edit = PositiveIntEdit( - caption=('caption', 'every:'), - edit_text=str(self._rrule.get('INTERVAL', [1])[0]), + caption=("caption", "every:"), + edit_text=str(self._rrule.get("INTERVAL", [1])[0]), + ) + self.until_choice = CPadding( + CAttrMap( + Choice( + ["Forever", "Until", "Repetitions"], + self._until, + callback=self.rebuild, + ), + "popupper", + ), + align="center", + left=2, + right=2, ) - self.until_choice = CPadding(CAttrMap(Choice( - ["Forever", "Until", "Repetitions"], self._until, callback=self.rebuild, - ), 'popupper'), align='center', left=2, right=2) - count = str(self._rrule.get('COUNT', [1])[0]) + count = str(self._rrule.get("COUNT", [1])[0]) self.repetitions_edit = PositiveIntEdit(edit_text=count) - until = self._rrule.get('UNTIL', [None])[0] + until = self._rrule.get("UNTIL", [None])[0] if until is None and isinstance(self._startdt, dt.datetime): until = self._startdt.date() elif until is None: @@ -681,31 +765,36 @@ def __init__(self, rrule, conf, startdt) -> None: if isinstance(until, dt.datetime): until = until.date() self.until_edit = DateEdit( - until, self._conf['locale']['longdateformat'], - lambda _: None, self._conf['locale']['weeknumbers'], - self._conf['locale']['firstweekday'], - self._conf['view']['monthdisplay'], + until, + self._conf["locale"]["longdateformat"], + lambda _: None, + self._conf["locale"]["weeknumbers"], + self._conf["locale"]["firstweekday"], + self._conf["view"]["monthdisplay"], ) self._rebuild_weekday_checks() self._rebuild_monthly_choice() - self._pile = pile = NPile([urwid.Text('')]) + self._pile = pile = NPile([urwid.Text("")]) urwid.WidgetWrap.__init__(self, pile) self.rebuild() def _rebuild_monthly_choice(self): weekday, xth = get_weekday_occurrence(self._startdt) - ords = {1: 'st', 2: 'nd', 3: 'rd', 21: 'st', 22: 'nd', 23: 'rd', 31: 'st'} + ords = {1: "st", 2: "nd", 3: "rd", 21: "st", 22: "nd", 23: "rd", 31: "st"} self._xth_weekday = f"on every {xth}{ords.get(xth, 'th')} {WEEKDAYS[weekday]}" - self._xth_monthday = (f"on every {self._startdt.day}" - f"{ords.get(self._startdt.day, 'th')} of the month") + self._xth_monthday = ( + f"on every {self._startdt.day}" f"{ords.get(self._startdt.day, 'th')} of the month" + ) self.monthly_choice = Choice( - [self._xth_monthday, self._xth_weekday], self._xth_monthday, callback=self.rebuild, + [self._xth_monthday, self._xth_weekday], + self._xth_monthday, + callback=self.rebuild, ) def _rebuild_weekday_checks(self): - if self.recurrence_choice.active == 'weekly': - initial_days = self._rrule.get('BYDAY', []) + if self.recurrence_choice.active == "weekly": + initial_days = self._rrule.get("BYDAY", []) else: initial_days = [] self.weekday_checks = WeekDaySelector(self._startdt, initial_days) @@ -720,25 +809,30 @@ def update_startdt(self, startdt): def check_understood_rrule(rrule): """test if we can reproduce `rrule`.""" keys = set(rrule.keys()) - freq = rrule.get('FREQ', [None])[0] + freq = rrule.get("FREQ", [None])[0] unsupported_rrule_parts = { - 'BYSECOND', 'BYMINUTE', 'BYHOUR', 'BYYEARDAY', 'BYWEEKNO', 'BYMONTH', + "BYSECOND", + "BYMINUTE", + "BYHOUR", + "BYYEARDAY", + "BYWEEKNO", + "BYMONTH", } if keys.intersection(unsupported_rrule_parts): return False - if len(rrule.get('BYMONTHDAY', [1])) > 1: + if len(rrule.get("BYMONTHDAY", [1])) > 1: return False # we don't support negative BYMONTHDAY numbers # don't need to check whole list, we only support one monthday anyway - if rrule.get('BYMONTHDAY', [1])[0] < 1: + if rrule.get("BYMONTHDAY", [1])[0] < 1: return False - if rrule.get('BYDAY', ['1'])[0][0] == '-': + if rrule.get("BYDAY", ["1"])[0][0] == "-": return False - if rrule.get('BYSETPOS', [1])[0] != 1: + if rrule.get("BYSETPOS", [1])[0] != 1: return False - if freq not in ['DAILY', 'WEEKLY', 'MONTHLY', 'YEARLY']: + if freq not in ["DAILY", "WEEKLY", "MONTHLY", "YEARLY"]: return False - if 'BYDAY' in keys and freq == 'YEARLY': + if "BYDAY" in keys and freq == "YEARLY": return False return True @@ -752,7 +846,7 @@ def _refill_contents(self, lines): self._pile.contents.pop() except IndexError: break - [self._pile.contents.append((line, ('pack', None))) for line in lines] + [self._pile.contents.append((line, ("pack", None))) for line in lines] def rebuild(self): old_focus_y = self._pile.focus_position @@ -768,6 +862,7 @@ def _rebuild_no_edit(self): def _allow_edit(_): self._allow_edit = True self.rebuild() + lines = [ urwid.Text("We cannot reproduce this event's repetition rules."), urwid.Text("Editing the repetition rules will destroy the current rules."), @@ -781,11 +876,13 @@ def _rebuild_edit_no_repeat(self): self._refill_contents(lines) def _rebuild_edit(self): - firstline = NColumns([ - (13, self.repeat_box), - (18, self.recurrence_choice), - (13, self.interval_edit), - ]) + firstline = NColumns( + [ + (13, self.repeat_box), + (18, self.recurrence_choice), + (13, self.interval_edit), + ] + ) lines = [firstline] if self.recurrence_choice.active == "weekly": @@ -810,25 +907,25 @@ def changed(self): def rrule(self): rrule = {} - rrule['freq'] = [self.recurrence_choice.active] + rrule["freq"] = [self.recurrence_choice.active] interval = int(self.interval_edit.get_edit_text()) if interval != 1: - rrule['interval'] = [interval] - if rrule['freq'] == ['weekly'] and len(self.weekday_checks.days) > 1: - rrule['byday'] = self.weekday_checks.days - if rrule['freq'] == ['monthly'] and self.monthly_choice.active == self._xth_weekday: + rrule["interval"] = [interval] + if rrule["freq"] == ["weekly"] and len(self.weekday_checks.days) > 1: + rrule["byday"] = self.weekday_checks.days + if rrule["freq"] == ["monthly"] and self.monthly_choice.active == self._xth_weekday: weekday, occurrence = get_weekday_occurrence(self._startdt) - rrule['byday'] = [f'{occurrence}{WEEKDAYS[weekday]}'] - if self.until_choice.active == 'Until': + rrule["byday"] = [f"{occurrence}{WEEKDAYS[weekday]}"] + if self.until_choice.active == "Until": if isinstance(self._startdt, dt.datetime): - rrule['until'] = dt.datetime.combine( + rrule["until"] = dt.datetime.combine( self.until_edit.date, self._startdt.time(), ) else: - rrule['until'] = self.until_edit.date - elif self.until_choice.active == 'Repetitions': - rrule['count'] = int(self.repetitions_edit.get_edit_text()) + rrule["until"] = self.until_edit.date + elif self.until_choice.active == "Repetitions": + rrule["count"] = int(self.repetitions_edit.get_edit_text()) return rrule @property @@ -847,14 +944,19 @@ def active(self, val): class ExportDialog(urwid.WidgetWrap): def __init__(self, this_func, abort_func, event) -> None: lines = [] - lines.append(urwid.Text('Export event as ICS file')) - lines.append(urwid.Text('')) + lines.append(urwid.Text("Export event as ICS file")) + lines.append(urwid.Text("")) export_location = ExtendedEdit( - caption='Location: ', edit_text="~/%s.ics" % event.summary.strip()) + caption="Location: ", edit_text="~/%s.ics" % event.summary.strip() + ) lines.append(export_location) - lines.append(urwid.Divider(' ')) - lines.append(CAttrMap( - urwid.Button('Save', on_press=this_func, user_data=export_location), - 'button', 'button focus')) + lines.append(urwid.Divider(" ")) + 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)) From c617970d52001364a63a22ab71372072e3f13997 Mon Sep 17 00:00:00 2001 From: dabrows Date: Fri, 15 Dec 2023 22:04:47 +0100 Subject: [PATCH 06/17] Added additional_widgets dependency --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index d6a215b1c..de0c20e36 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,7 @@ dependencies = [ "configobj", "atomicwrites>=0.1.7", "tzlocal>=1.0", + "additional_urwid_widgets>=0.4.1" ] [project.optional-dependencies] proctitle = [ From ea390ecc933022356ddcb0bdffb87bd5e6411ce2 Mon Sep 17 00:00:00 2001 From: dabrows Date: Fri, 15 Dec 2023 22:50:54 +0100 Subject: [PATCH 07/17] Updated tests to match new functionality and fixed two bugs uncovered by tests --- khal/khalendar/event.py | 2 +- khal/settings/khal.spec | 8 ++++++-- tests/configs/small.conf | 1 + tests/event_test.py | 6 +++--- tests/settings_test.py | 18 ++++++++++++++++-- 5 files changed, 27 insertions(+), 8 deletions(-) diff --git a/khal/khalendar/event.py b/khal/khalendar/event.py index 0b2da12fb..d58e8edce 100644 --- a/khal/khalendar/event.py +++ b/khal/khalendar/event.py @@ -47,7 +47,7 @@ class Attendee: def __init__(self, defline): m = re.match(r"(?P.*)\<(?P.*)\>", defline) - if m.group("name") is not None and m.group("mail") is not None: + if m is not None and m.group("name") is not None and m.group("mail") is not None: self.cn = m.group("name").strip() self.mail = m.group("mail").strip().lower() else: diff --git a/khal/settings/khal.spec b/khal/settings/khal.spec index c44acf62d..3a395afb5 100644 --- a/khal/settings/khal.spec +++ b/khal/settings/khal.spec @@ -76,6 +76,12 @@ type = option('calendar', 'birthdays', 'discover', default='calendar') # belongs to the user. addresses = force_list(default='') +# The address adapter is a command that will be run using "bash -c" to get +# a list of addresses for autocompletion in the attendee field of a new event +# in interactive mode. This expects the same output as "khard email | tail -n +2" +# since it has only been developed with khard in mind - but other providers +# should be configurable to create the same output, or the parser could +# be extended (see _contacts_update in CalenderCollection in khalendar.py) address_adapter = string(default=None) [sqlite] @@ -233,8 +239,6 @@ default_dayevent_alarm = timedelta(default='') # 'ikhal' only) enable_mouse = boolean(default=True) -address_adapter = string(default=None) - # The view section contains configuration options that effect the visual appearance # when using khal and ikhal. diff --git a/tests/configs/small.conf b/tests/configs/small.conf index 37271e909..c2372349d 100644 --- a/tests/configs/small.conf +++ b/tests/configs/small.conf @@ -9,3 +9,4 @@ path = ~/.calendars/work/ readonly = True addresses = user@example.com + address_adapter = "a" diff --git a/tests/event_test.py b/tests/event_test.py index d20b90e5f..2afc465bd 100644 --- a/tests/event_test.py +++ b/tests/event_test.py @@ -539,13 +539,13 @@ def test_event_attendees(): event = Event.fromString(_get_text('event_dt_simple'), **EVENT_KWARGS) assert event.attendees == "" event.update_attendees(["this-does@not-exist.de", ]) - assert event.attendees == "this-does@not-exist.de" + assert event.attendees == "this-does@not-exist.de " assert isinstance(event._vevents[event.ref].get('ATTENDEE', []), list) assert isinstance(event._vevents[event.ref].get('ATTENDEE', [])[0], vCalAddress) assert str(event._vevents[event.ref].get('ATTENDEE', [])[0]) == "MAILTO:this-does@not-exist.de" event.update_attendees(["this-does@not-exist.de", "also-does@not-exist.de"]) - assert event.attendees == "this-does@not-exist.de, also-does@not-exist.de" + assert event.attendees == "this-does@not-exist.de , also-does@not-exist.de " assert isinstance(event._vevents[event.ref].get('ATTENDEE', []), list) assert len(event._vevents[event.ref].get('ATTENDEE', [])) == 2 @@ -561,7 +561,7 @@ def test_event_attendees(): ) event._vevents[event.ref]['ATTENDEE'] = [new_address, ] event.update_attendees(["another.mailaddress@not-exist.de", "mail.address@not-exist.de"]) - assert event.attendees == "mail.address@not-exist.de, another.mailaddress@not-exist.de" + assert event.attendees == "Real Name , another.mailaddress@not-exist.de " address = [a for a in event._vevents[event.ref].get('ATTENDEE', []) if str(a) == "MAILTO:mail.address@not-exist.de"] assert len(address) == 1 diff --git a/tests/settings_test.py b/tests/settings_test.py index a1d6799c7..406c1c8af 100644 --- a/tests/settings_test.py +++ b/tests/settings_test.py @@ -43,10 +43,12 @@ def test_simple_config(self): 'home': { 'path': os.path.expanduser('~/.calendars/home/'), 'readonly': False, 'color': None, 'priority': 10, 'type': 'calendar', 'addresses': [''], + 'address_adapter': None }, 'work': { 'path': os.path.expanduser('~/.calendars/work/'), 'readonly': False, 'color': None, 'priority': 10, 'type': 'calendar', 'addresses': [''], + 'address_adapter': None }, }, 'sqlite': {'path': os.path.expanduser('~/.local/share/khal/khal.db')}, @@ -85,10 +87,12 @@ def test_small(self): 'calendars': { 'home': {'path': os.path.expanduser('~/.calendars/home/'), 'color': 'dark green', 'readonly': False, 'priority': 20, - 'type': 'calendar', 'addresses': ['']}, + 'type': 'calendar', 'addresses': [''], + 'address_adapter': None}, 'work': {'path': os.path.expanduser('~/.calendars/work/'), 'readonly': True, 'color': None, 'priority': 10, - 'type': 'calendar', 'addresses': ['user@example.com']}}, + 'type': 'calendar', 'addresses': ['user@example.com'], + 'address_adapter': 'a'}}, 'sqlite': {'path': os.path.expanduser('~/.local/share/khal/khal.db')}, 'locale': { 'local_timezone': get_localzone(), @@ -230,6 +234,7 @@ def test_config_checks(metavdirs): 'readonly': False, 'type': 'calendar', 'priority': 10, + 'address_adapter': None, }, 'my calendar': { 'color': 'dark blue', @@ -237,6 +242,7 @@ def test_config_checks(metavdirs): 'readonly': False, 'type': 'calendar', 'priority': 10, + 'address_adapter': None, }, 'my private calendar': { 'color': '#FF00FF', @@ -244,6 +250,7 @@ def test_config_checks(metavdirs): 'readonly': False, 'type': 'calendar', 'priority': 10, + 'address_adapter': None, }, 'public1': { 'color': None, @@ -251,6 +258,7 @@ def test_config_checks(metavdirs): 'readonly': False, 'type': 'calendar', 'priority': 10, + 'address_adapter': None, }, 'public': { 'color': None, @@ -258,6 +266,7 @@ def test_config_checks(metavdirs): 'readonly': False, 'type': 'calendar', 'priority': 10, + 'address_adapter': None, }, 'work': { 'color': None, @@ -265,6 +274,7 @@ def test_config_checks(metavdirs): 'readonly': False, 'type': 'calendar', 'priority': 10, + 'address_adapter': None, }, 'cfgcolor': { 'color': 'dark blue', @@ -272,6 +282,7 @@ def test_config_checks(metavdirs): 'readonly': False, 'type': 'calendar', 'priority': 10, + 'address_adapter': None, }, 'dircolor': { 'color': 'dark blue', @@ -279,6 +290,7 @@ def test_config_checks(metavdirs): 'readonly': False, 'type': 'calendar', 'priority': 10, + 'address_adapter': None, }, 'cfgcolor_again': { 'color': 'dark blue', @@ -286,6 +298,7 @@ def test_config_checks(metavdirs): 'readonly': False, 'type': 'calendar', 'priority': 10, + 'address_adapter': None, }, 'cfgcolor_once_more': { 'color': 'dark blue', @@ -293,6 +306,7 @@ def test_config_checks(metavdirs): 'readonly': False, 'type': 'calendar', 'priority': 10, + 'address_adapter': None, }, }, From ecf6e9b9b2a9d139fc1a5f614577864da876004d Mon Sep 17 00:00:00 2001 From: dabrows Date: Fri, 15 Dec 2023 23:10:02 +0100 Subject: [PATCH 08/17] Added myself to AUTHORS --- AUTHORS.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS.txt b/AUTHORS.txt index e417da915..dc8a0247e 100644 --- a/AUTHORS.txt +++ b/AUTHORS.txt @@ -55,3 +55,4 @@ Matthew Rademaker - matthew.rademaker [at] gmail [dot] com Valentin Iovene - val [at] too [dot] gy Julian Wollrath Mattori Birnbaum - me [at] mattori [dot] com - https://mattori.com +Piotr Wojciech Dabrowski - piotr.dabrowski [at] htw-berlin [dot] de From 327863594ffee4297d14664753b8ad623ce9b2ae Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Fri, 15 Dec 2023 22:18:54 +0000 Subject: [PATCH 09/17] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- khal/ui/attendeewidget.py | 1 - tests/settings_test.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/khal/ui/attendeewidget.py b/khal/ui/attendeewidget.py index f3912c1f1..3a05b10ea 100644 --- a/khal/ui/attendeewidget.py +++ b/khal/ui/attendeewidget.py @@ -151,4 +151,3 @@ def change_mail_list(self, mails): if self.mails is None: self.mails = [] self.mp.change_mail_list(mails) - diff --git a/tests/settings_test.py b/tests/settings_test.py index 406c1c8af..097f5e47e 100644 --- a/tests/settings_test.py +++ b/tests/settings_test.py @@ -43,7 +43,7 @@ def test_simple_config(self): 'home': { 'path': os.path.expanduser('~/.calendars/home/'), 'readonly': False, 'color': None, 'priority': 10, 'type': 'calendar', 'addresses': [''], - 'address_adapter': None + 'address_adapter': None }, 'work': { 'path': os.path.expanduser('~/.calendars/work/'), 'readonly': False, From 961e3320ba12431bd1b8ca861a0ce7a4426f7763 Mon Sep 17 00:00:00 2001 From: dabrows Date: Mon, 18 Dec 2023 11:53:07 +0100 Subject: [PATCH 10/17] Fixed bug that caused crash when editing events --- khal/ui/attendeewidget.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/khal/ui/attendeewidget.py b/khal/ui/attendeewidget.py index f3912c1f1..def0dac48 100644 --- a/khal/ui/attendeewidget.py +++ b/khal/ui/attendeewidget.py @@ -152,3 +152,6 @@ def change_mail_list(self, mails): self.mails = [] self.mp.change_mail_list(mails) + def get_edit_text(self): + return self.acedit.get_edit_text() + From ed86eb232de33d18dd694d84e55f2b36b7540eef Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 18 Dec 2023 10:55:52 +0000 Subject: [PATCH 11/17] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- khal/ui/attendeewidget.py | 1 - 1 file changed, 1 deletion(-) diff --git a/khal/ui/attendeewidget.py b/khal/ui/attendeewidget.py index def0dac48..3bbd7413a 100644 --- a/khal/ui/attendeewidget.py +++ b/khal/ui/attendeewidget.py @@ -154,4 +154,3 @@ def change_mail_list(self, mails): def get_edit_text(self): return self.acedit.get_edit_text() - From 0fc8e8a25896b0aab085a8dbd90e168d7ebc1d58 Mon Sep 17 00:00:00 2001 From: dabrows Date: Mon, 18 Dec 2023 16:53:26 +0100 Subject: [PATCH 12/17] Changed default address_adapter value; Added address_adapter in [default] section as global adapter --- khal/khalendar/khalendar.py | 20 +++++++++++++++++++- khal/settings/khal.spec | 25 +++++++++++++++++++++++-- khal/ui/__init__.py | 2 ++ 3 files changed, 44 insertions(+), 3 deletions(-) diff --git a/khal/khalendar/khalendar.py b/khal/khalendar/khalendar.py index f427a788f..d95c5a3f0 100644 --- a/khal/khalendar/khalendar.py +++ b/khal/khalendar/khalendar.py @@ -32,6 +32,7 @@ import os.path import re import subprocess +from subprocess import CalledProcessError from typing import Any, Dict, Iterable, List, Optional, Set, Tuple, Union # noqa from ..custom_types import CalendarConfiguration, EventCreationTypes, LocaleConfiguration @@ -100,6 +101,7 @@ def __init__(self, self.hmethod = hmethod self.default_color = default_color + self.default_contacts = [] self.multiple = multiple self.multiple_on_overflow = multiple_on_overflow self.color = color @@ -370,12 +372,28 @@ def _contacts_update(self, calendar: str) -> None: adaptercommand = self._calendars[calendar].get('address_adapter') if adaptercommand is None: self._contacts[calendar] = [] + if adaptercommand == "default": + self._contacts[calendar] = self.default_contacts else: + self._contacts[calendar] = CalendarCollection._get_contacts(adaptercommand) + + @staticmethod + def _get_contacts(adaptercommand: str) -> List[str]: + # Check for both None and "None" because ConfigObj likes to stringify + # configuration + if adaptercommand is None or adaptercommand == "None": + return [] + try: res = subprocess.check_output(["bash", "-c", adaptercommand]) maildata = [re.split(r"\s{2,}", x) for x in res.decode("utf-8").split("\n")] mails = [f"{x[0]} <{x[2]}>" for x in maildata if len(x) > 1] - self._contacts[calendar] = mails + return mails + except CalledProcessError: + return [] + def update_default_contacts(self, command: str) -> None: + self.default_contacts.clear() + self.default_contacts += CalendarCollection._get_contacts(command) def _db_update(self, calendar: str) -> None: """implements the actual db update on a per calendar base""" diff --git a/khal/settings/khal.spec b/khal/settings/khal.spec index 3a395afb5..6736ab775 100644 --- a/khal/settings/khal.spec +++ b/khal/settings/khal.spec @@ -81,8 +81,15 @@ addresses = force_list(default='') # in interactive mode. This expects the same output as "khard email | tail -n +2" # since it has only been developed with khard in mind - but other providers # should be configurable to create the same output, or the parser could -# be extended (see _contacts_update in CalenderCollection in khalendar.py) -address_adapter = string(default=None) +# be extended (see _contacts_update in CalenderCollection in khalendar.py). +# The following options are possible: +# +# * None: No autocompletion for this calendar +# * "default": Use the autocompletion addresses from address_adapter in the +# [default] configuration section (default) +# * "": Use the output of as contacts for +# autocompletion +address_adapter = string(default="default") [sqlite] # khal stores its internal caching database here, by default this will be in the *$XDG_DATA_HOME/khal/khal.db* (this will most likely be *~/.local/share/khal/khal.db*). @@ -239,6 +246,20 @@ default_dayevent_alarm = timedelta(default='') # 'ikhal' only) enable_mouse = boolean(default=True) +# The address adapter is a command that will be run using "bash -c" to get +# a list of addresses for autocompletion in the attendee field of a new event +# in interactive mode. The address_adapter defined in this section will be used +# for auto-completion of attendees in all calendars that have their +# address_adapter set to "default". +# The following options are possible: +# +# * None: No autocompletion +# * "": Use the output of as contacts for +# autocompletion. Default: +# "khard email | tail -n +2" +address_adapter = string(default="khard email | tail -n +2") + + # The view section contains configuration options that effect the visual appearance # when using khal and ikhal. diff --git a/khal/ui/__init__.py b/khal/ui/__init__.py index 870577294..9919b0ff9 100644 --- a/khal/ui/__init__.py +++ b/khal/ui/__init__.py @@ -1065,6 +1065,8 @@ def __init__(self, collection, conf=None, title: str='', description: str='') -> self._conf = conf self.collection = collection self._deleted: Dict[int, List[str]] = {DeletionType.ALL: [], DeletionType.INSTANCES: []} + if conf is not None: + self.collection.update_default_contacts(self._conf['default']['address_adapter']) ContainerWidget = linebox[self._conf['view']['frame']] if self._conf['view']['dynamic_days']: From 2c53ebe07ee4e716f4a141615a51b1dca5072050 Mon Sep 17 00:00:00 2001 From: dabrows Date: Mon, 18 Dec 2023 16:53:36 +0100 Subject: [PATCH 13/17] Updated tests --- tests/settings_test.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/settings_test.py b/tests/settings_test.py index 097f5e47e..d5c2dc9af 100644 --- a/tests/settings_test.py +++ b/tests/settings_test.py @@ -43,12 +43,12 @@ def test_simple_config(self): 'home': { 'path': os.path.expanduser('~/.calendars/home/'), 'readonly': False, 'color': None, 'priority': 10, 'type': 'calendar', 'addresses': [''], - 'address_adapter': None + 'address_adapter': 'default' }, 'work': { 'path': os.path.expanduser('~/.calendars/work/'), 'readonly': False, 'color': None, 'priority': 10, 'type': 'calendar', 'addresses': [''], - 'address_adapter': None + 'address_adapter': 'default' }, }, 'sqlite': {'path': os.path.expanduser('~/.local/share/khal/khal.db')}, @@ -64,6 +64,7 @@ def test_simple_config(self): 'default_dayevent_alarm': dt.timedelta(0), 'show_all_days': False, 'enable_mouse': True, + 'address_adapter': 'khard email | tail -n +2', } } for key in comp_config: @@ -88,7 +89,7 @@ def test_small(self): 'home': {'path': os.path.expanduser('~/.calendars/home/'), 'color': 'dark green', 'readonly': False, 'priority': 20, 'type': 'calendar', 'addresses': [''], - 'address_adapter': None}, + 'address_adapter': 'default'}, 'work': {'path': os.path.expanduser('~/.calendars/work/'), 'readonly': True, 'color': None, 'priority': 10, 'type': 'calendar', 'addresses': ['user@example.com'], @@ -117,6 +118,7 @@ def test_small(self): 'enable_mouse': True, 'default_event_alarm': dt.timedelta(0), 'default_dayevent_alarm': dt.timedelta(0), + 'address_adapter': 'khard email | tail -n +2', } } for key in comp_config: From a223017d1ef9a1ba2a16f659582f56ac49678e4d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 18 Dec 2023 15:55:34 +0000 Subject: [PATCH 14/17] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- khal/settings/khal.spec | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/khal/settings/khal.spec b/khal/settings/khal.spec index 6736ab775..90c08c859 100644 --- a/khal/settings/khal.spec +++ b/khal/settings/khal.spec @@ -84,10 +84,10 @@ addresses = force_list(default='') # be extended (see _contacts_update in CalenderCollection in khalendar.py). # The following options are possible: # -# * None: No autocompletion for this calendar -# * "default": Use the autocompletion addresses from address_adapter in the +# * None: No autocompletion for this calendar +# * "default": Use the autocompletion addresses from address_adapter in the # [default] configuration section (default) -# * "": Use the output of as contacts for +# * "": Use the output of as contacts for # autocompletion address_adapter = string(default="default") @@ -250,11 +250,11 @@ enable_mouse = boolean(default=True) # a list of addresses for autocompletion in the attendee field of a new event # in interactive mode. The address_adapter defined in this section will be used # for auto-completion of attendees in all calendars that have their -# address_adapter set to "default". +# address_adapter set to "default". # The following options are possible: # -# * None: No autocompletion -# * "": Use the output of as contacts for +# * None: No autocompletion +# * "": Use the output of as contacts for # autocompletion. Default: # "khard email | tail -n +2" address_adapter = string(default="khard email | tail -n +2") From 70d8b86f65e020b6418af2ae80da47660ef521be Mon Sep 17 00:00:00 2001 From: dabrows Date: Mon, 18 Dec 2023 21:16:27 +0100 Subject: [PATCH 15/17] Added adherence to mypy requirements --- khal/khalendar/event.py | 8 ++++---- khal/khalendar/khalendar.py | 10 +++++----- tests/event_test.py | 6 ++++-- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/khal/khalendar/event.py b/khal/khalendar/event.py index d58e8edce..6c5c19dce 100644 --- a/khal/khalendar/event.py +++ b/khal/khalendar/event.py @@ -522,21 +522,21 @@ def attendees(self) -> str: def update_attendees(self, attendees: List[str]): assert isinstance(attendees, list) - attendees = [Attendee(a) for a in attendees if a != ""] - if len(attendees) > 0: + attendees_o : List[Attendee] = [Attendee(a) for a in attendees if a != ""] + if len(attendees_o) > 0: # first check for overlaps in existing attendees. # Existing vCalAddress objects will be copied, non-existing # vCalAddress objects will be created and appended. old_attendees = self._vevents[self.ref].get('ATTENDEE', []) unchanged_attendees = [] vCalAddresses = [] - for attendee in attendees: + for attendee in attendees_o: for old_attendee in old_attendees: old_email = old_attendee.lstrip("MAILTO:").lower() if attendee.mail == old_email: vCalAddresses.append(old_attendee) unchanged_attendees.append(attendee) - for attendee in [a for a in attendees if a not in unchanged_attendees]: + for attendee in [a for a in attendees_o if a not in unchanged_attendees]: item = icalendar.prop.vCalAddress(f'MAILTO:{attendee.mail}') if attendee.cn is not None: item.params['CN'] = attendee.cn diff --git a/khal/khalendar/khalendar.py b/khal/khalendar/khalendar.py index d95c5a3f0..c0103d21a 100644 --- a/khal/khalendar/khalendar.py +++ b/khal/khalendar/khalendar.py @@ -101,7 +101,7 @@ def __init__(self, self.hmethod = hmethod self.default_color = default_color - self.default_contacts = [] + self.default_contacts : List[str] = [] self.multiple = multiple self.multiple_on_overflow = multiple_on_overflow self.color = color @@ -112,8 +112,8 @@ def __init__(self, self._backend = backend.SQLiteDb(self.names, dbpath, self._locale) self._last_ctags: Dict[str, str] = {} self.update_db() - for calendar in self._calendars.keys(): - self._contacts_update(calendar) + for cname in self._calendars.keys(): + self._contacts_update(cname) @property def writable_names(self) -> List[str]: @@ -369,9 +369,9 @@ def _needs_update(self, calendar: str, remember: bool=False) -> bool: return local_ctag != self._backend.get_ctag(calendar) def _contacts_update(self, calendar: str) -> None: - adaptercommand = self._calendars[calendar].get('address_adapter') - if adaptercommand is None: + if self._calendars[calendar].get('address_adapter') is None: self._contacts[calendar] = [] + adaptercommand = str(self._calendars[calendar].get('address_adapter')) if adaptercommand == "default": self._contacts[calendar] = self.default_contacts else: diff --git a/tests/event_test.py b/tests/event_test.py index 2afc465bd..96baa6b2e 100644 --- a/tests/event_test.py +++ b/tests/event_test.py @@ -545,7 +545,8 @@ def test_event_attendees(): assert str(event._vevents[event.ref].get('ATTENDEE', [])[0]) == "MAILTO:this-does@not-exist.de" event.update_attendees(["this-does@not-exist.de", "also-does@not-exist.de"]) - assert event.attendees == "this-does@not-exist.de , also-does@not-exist.de " + assert event.attendees == "this-does@not-exist.de , " \ + "also-does@not-exist.de " assert isinstance(event._vevents[event.ref].get('ATTENDEE', []), list) assert len(event._vevents[event.ref].get('ATTENDEE', [])) == 2 @@ -561,7 +562,8 @@ def test_event_attendees(): ) event._vevents[event.ref]['ATTENDEE'] = [new_address, ] event.update_attendees(["another.mailaddress@not-exist.de", "mail.address@not-exist.de"]) - assert event.attendees == "Real Name , another.mailaddress@not-exist.de " + assert event.attendees == "Real Name , "\ + "another.mailaddress@not-exist.de " address = [a for a in event._vevents[event.ref].get('ATTENDEE', []) if str(a) == "MAILTO:mail.address@not-exist.de"] assert len(address) == 1 From c3a14882747965c2a8ce8eb8b27f11cb81c3bb0e Mon Sep 17 00:00:00 2001 From: Piotr Wojciech Dabrowski Date: Sun, 7 Jan 2024 19:53:04 +0100 Subject: [PATCH 16/17] Update pyproject.toml --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index de0c20e36..7793fea8d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,7 +62,6 @@ ikhal = "khal.cli:main_ikhal" [build-system] requires = ["setuptools>=64", "setuptools_scm>=8"] build-backend = "setuptools.build_meta" -requires-python = ">=3.8,<3.12" [tool.setuptools.packages] find = {} From 8d6e79271295f7a60625b18fd75be5c934d11298 Mon Sep 17 00:00:00 2001 From: Piotr Wojciech Dabrowski Date: Mon, 4 Nov 2024 10:23:18 +0100 Subject: [PATCH 17/17] Update pyproject.toml --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 7793fea8d..4c95e165d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,7 +33,7 @@ dependencies = [ "configobj", "atomicwrites>=0.1.7", "tzlocal>=1.0", - "additional_urwid_widgets>=0.4.1" + "additional_urwid_widgets>=0.4" ] [project.optional-dependencies] proctitle = [