[Python] Urwid example with input validation per keypress on a datetime entry?

Discussão em 'Python' iniciado por Stack, Fevereiro 24, 2025.

  1. Stack

    Stack Membro Participativo

    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:

    1. 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.
    2. Has a (customizable) shortcut for help, like ctrl+h, which opens a help screen a bit like this example.
    3. allows moving to the next box using either the enter button
    4. Allows selecting the auto-completed suggestion using the tab button
    5. 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).
    6. 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. [​IMG]

    Continue reading...

Compartilhe esta Página