Context Suppose one would like two, modular, input boxes in which one can enter a date in format yyyy-mm-dd and optionally the time in format: HH-MM, like: Please enter the date in format YYYY-MM-DD, (and optionally the time in format HH-MM )` <highlighed input box when selected> That: Directly shows a warning/red text (perhaps at the top right of the screen), that says: invalid character entered: "_" if the user tries to type an invalid character, like a. Has a (customizable) shortcut for help, like ctrl+h, which opens a help screen a bit like this example. allows moving to the next box using either the enter button Allows selecting the auto-completed suggestion using the tab button Allows selecting the digits using up and down arrows, and going to the next/previous digit using the left, right arrow, whilst only showing valid options, e.g. if february as month is selected, it only allows selecting 1 to 28/29 (instead of showing 1 to 30/31). Highlights the text box as red if the user selected an invalid date. E.g. selecting 2024-02-29 and then going down on the date one year, e.g. 2023-02-29. (Because 2023 is not a leap year). Question How could one build that in urwid? Attempt I am experiencing some difficulties in implementing the features 1, 2, 3, 4 and 6 in the MWE below: import urwid import datetime import calendar class DateTimeEdit(urwid.Edit): # ... (the DateTimeEdit class from the previous response) ... def __init__(self, caption, date_only=False, **kwargs): super().__init__(caption, **kwargs) self.date_only = date_only self.error_text = urwid.Text("") self.help_text = urwid.Text("") self.date_parts = [4, 2, 2] # year, month, day self.time_parts = [2, 2] # hour, minute self.current_part = 0 self.date_values = [None, None, None] self.time_values = [None, None] self.date_separator = "-" self.time_separator = ":" def valid_char(self, ch): if ch.isdigit() or ch == self.date_separator or (not self.date_only and ch == self.time_separator): return True else: self.error_text.set_text(f"invalid character entered: '{ch}'") return False def keypress(self, size, key): if key == 'ctrl h': self.show_help() return None if key == 'enter': return 'enter' # Signal to move to the next box if key == 'tab': # Implement suggestion selection here return None if key == 'left': self.move_to_previous_part() return None if key == 'right': self.move_to_next_part() return None if key == 'up' or key == 'down': self.adjust_value(key) return None result = super().keypress(size, key) if result: self.error_text.set_text("") # Clear error on valid input self.update_values() return result def update_values(self): text = self.get_edit_text() if self.date_only: parts = text.split(self.date_separator) for i, part in enumerate(parts): if part: try: self.date_values = int(part) except ValueError: self.date_values = None else: date_time_parts = text.split(" ") if len(date_time_parts) > 0: date_parts = date_time_parts[0].split(self.date_separator) for i, part in enumerate(date_parts): if part: try: self.date_values = int(part) except ValueError: self.date_values = None if len(date_time_parts) > 1: time_parts = date_time_parts[1].split(self.time_separator) for i, part in enumerate(time_parts): if part: try: self.time_values = int(part) except ValueError: self.time_values = None def move_to_next_part(self): if self.date_only: if self.current_part < len(self.date_parts) - 1: self.current_part += 1 else: if self.current_part < len(self.date_parts) + len(self.time_parts) - 1: self.current_part += 1 self.update_cursor() def move_to_previous_part(self): if self.current_part > 0: self.current_part -= 1 self.update_cursor() def update_cursor(self): # Calculate cursor position based on current_part cursor_pos = 0 if self.date_only: for i in range(self.current_part): cursor_pos += self.date_parts + 1 else: if self.current_part < len(self.date_parts): for i in range(self.current_part): cursor_pos += self.date_parts + 1 else: cursor_pos = sum(self.date_parts) + 1 for i in range(self.current_part - len(self.date_parts)): cursor_pos += self.time_parts + 1 self.set_edit_pos(cursor_pos) def adjust_value(self, direction): if self.date_only: if self.current_part == 0: self.adjust_year(direction) elif self.current_part == 1: self.adjust_month(direction) elif self.current_part == 2: self.adjust_day(direction) else: if self.current_part == 0: self.adjust_year(direction) elif self.current_part == 1: self.adjust_month(direction) elif self.current_part == 2: self.adjust_day(direction) elif self.current_part == 3: self.adjust_hour(direction) elif self.current_part == 4: self.adjust_minute(direction) self.update_text() def adjust_year(self, direction): if self.date_values[0] is None: self.date_values[0] = 2024 # Default year if direction == 'up': self.date_values[0] += 1 elif direction == 'down': self.date_values[0] -= 1 def adjust_month(self, direction): if self.date_values[1] is None: self.date_values[1] = 1 if direction == 'up': self.date_values[1] = (self.date_values[1] % 12) + 1 elif direction == 'down': self.date_values[1] = (self.date_values[1] - 2) % 12 + 1 def adjust_day(self, direction): if self.date_values[2] is None: self.date_values[2] = 1 max_days = self.get_max_days() if direction == 'up': self.date_values[2] = (self.date_values[2] % max_days) + 1 elif direction == 'down': self.date_values[2] = (self.date_values[2] - 2) % max_days + 1 def adjust_hour(self, direction): if self.time_values[0] is None: self.time_values[0] = 0 if direction == 'up': self.time_values[0] = (self.time_values[0] + 1) % 24 elif direction == 'down': self.time_values[0] = (self.time_values[0] - 1) % 24 def adjust_minute(self, direction): if self.time_values[1] is None: self.time_values[1] = 0 if direction == 'up': self.time_values[1] = (self.time_values[1] + 1) % 60 elif direction == 'down': self.time_values[1] = (self.time_values[1] - 1) % 60 def get_max_days(self): if self.date_values[0] is None or self.date_values[1] is None: return 31 try: _, max_days = calendar.monthrange(self.date_values[0], self.date_values[1]) return max_days except ValueError: return 31 def update_text(self): date_str = self.date_separator.join(map(lambda x: str(x).zfill(2) if x is not None else "00", self.date_values)) if self.date_only: self.set_edit_text(date_str) else: time_str = self.time_separator.join(map(lambda x: str(x).zfill(2) if x is not None else "00", self.time_values)) self.set_edit_text(date_str + " " + time_str) import urwid import datetime import calendar # (DateTimeEdit class code from previous responses) def main(): date_edit = DateTimeEdit("Date (YYYY-MM-DD): ", date_only=True) datetime_edit = DateTimeEdit("Date & Time (YYYY-MM-DD HH:MM): ") error_display = urwid.Text("") date_edit.error_text = error_display datetime_edit.error_text = error_display pile = urwid.Pile([ urwid.Text("Enter Date and/or Time:"), date_edit, datetime_edit, error_display, ]) fill = urwid.Filler(pile, 'top') loop = urwid.MainLoop(fill) loop.run() if __name__ == "__main__": main() Output Below is an example output that contains an invalid date that is not corrected (first 2024-02-29 is entered, and then the year is moved down 1. Continue reading...