tour.py

  1#!/usr/bin/env python
  2#
  3# Urwid tour.  It slices, it dices..
  4#    Copyright (C) 2004-2011  Ian Ward
  5#
  6#    This library is free software; you can redistribute it and/or
  7#    modify it under the terms of the GNU Lesser General Public
  8#    License as published by the Free Software Foundation; either
  9#    version 2.1 of the License, or (at your option) any later version.
 10#
 11#    This library is distributed in the hope that it will be useful,
 12#    but WITHOUT ANY WARRANTY; without even the implied warranty of
 13#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 14#    Lesser General Public License for more details.
 15#
 16#    You should have received a copy of the GNU Lesser General Public
 17#    License along with this library; if not, write to the Free Software
 18#    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 19#
 20# Urwid web site: https://urwid.org/
 21
 22"""Urwid tour.
 23
 24Shows many of the standard widget types and features.
 25"""
 26
 27from __future__ import annotations
 28
 29import urwid
 30from urwid.command_map import Command
 31
 32PORTS = range(1, 65536)
 33PRIVILEGED_PORTS = range(1, 1024)
 34COLOURS = ("black", "brown", "dark blue", "dark cyan", "dark green", "dark red", "light blue", "light gray")
 35
 36
 37class ReadlineEdit(urwid.Edit):
 38    """Edit that takes the readline editing keys, from its own copy of the shared command map."""
 39
 40    _command_map = urwid.command_map.copy()
 41    _command_map.bind_readline()
 42
 43
 44def main() -> None:
 45    """Build and run the tour of standard widget types and features."""
 46    text_header = (
 47        "Welcome to the urwid tour!  UP / DOWN / PAGE UP / PAGE DOWN scroll.  "
 48        "TAB / SHIFT TAB move between fields.  F8 exits."
 49    )
 50    text_intro: list[str | tuple[str, str]] = [
 51        ("important", "Text"),
 52        (
 53            " widgets are the most common in "
 54            "any urwid program.  This Text widget was created "
 55            "without setting the wrap or align mode, so it "
 56            "defaults to left alignment with wrapping on space "
 57            "characters.  "
 58        ),
 59        ("important", "Change the window width"),
 60        " to see how the widgets on this page react.  This Text widget is wrapped with a ",
 61        ("important", "Padding"),
 62        " widget to keep it indented on the left and right.",
 63    ]
 64    text_right = "This Text widget is right aligned.  Wrapped words stay to the right as well. "
 65    text_center = "This one is center aligned."
 66    text_clip = (
 67        "Text widgets may be clipped instead of wrapped.\n"
 68        "Extra text is discarded instead of wrapped to the next line. "
 69        "65-> 70-> 75-> 80-> 85-> 90-> 95-> 100>\n"
 70        "Newlines embedded in the string are still respected."
 71    )
 72    text_right_clip = (
 73        "This is a right aligned and clipped Text widget.\n"
 74        "<100 <-95 <-90 <-85 <-80 <-75 <-70 <-65             "
 75        "Text will be cut off at the left of this widget."
 76    )
 77    text_center_clip = "Center aligned and clipped widgets will have text cut off both sides."
 78    text_ellipsis = (
 79        "Text can be clipped using the ellipsis character (…)\n"
 80        "Extra text is discarded and a … mark is shown."
 81        "50-> 55-> 60-> 65-> 70-> 75-> 80-> 85-> 90-> 95-> 100>\n"
 82    )
 83    text_any = (
 84        "The 'any' wrap mode will wrap on any character.  This "
 85        "mode will not collapse space characters at the end of the "
 86        "line but it still honors embedded newline characters.\n"
 87        "Like this one."
 88    )
 89    text_padding = (
 90        "Padding widgets have many options.  This "
 91        "is a standard Text widget wrapped with a Padding widget "
 92        "with the alignment set to relative 20% and with its width "
 93        "fixed at 40."
 94    )
 95    text_divider: list[str | tuple[str, str]] = [
 96        "The ",
 97        ("important", "Divider"),
 98        " widget repeats the same character across the whole line.  It can also add blank lines above and below.",
 99    ]
100    text_edit: list[str | tuple[str, str]] = [
101        "The ",
102        ("important", "Edit"),
103        (
104            " widget is a simple text editing widget.  It supports cursor "
105            "movement and tries to maintain the current column when focus "
106            "moves to another edit widget.  It wraps and aligns the same "
107            "way as Text widgets."
108        ),
109    ]
110    text_edit_cap1 = ("editcp", "This is a caption.  Edit here: ")
111    text_edit_text1 = "editable stuff"
112    text_edit_cap2 = ("editcp", "This one supports newlines: ")
113    text_edit_text2 = (
114        "line one starts them all\n"
115        "== line 2 == with some more text to edit.. words.. whee..\n"
116        "LINE III, the line to end lines one and two, unless you "
117        "change something."
118    )
119    text_edit_cap3 = ("editcp", "This one is clipped, try editing past the edge: ")
120    text_edit_text3 = "add some text here -> -> -> ...."
121    text_edit_cap4 = ("editcp", "This one takes at most 8 characters: ")
122    text_edit_text4 = "12345"
123    text_edit_cap5 = ("editcp", "This one shows a hint while empty and checks the port: ")
124    text_edit_cap6 = ("editcp", "This one completes a colour with TAB: ")
125    text_edit_cap7 = ("editcp", "This one takes readline keys, such as ctrl a, ctrl w, meta b and ctrl _ to undo: ")
126    text_edit_text7 = "ls /usr/local/share"
127    text_edit_alignments = "Different Alignments:"
128    text_edit_left = "left aligned (default)"
129    text_edit_center = "center aligned"
130    text_edit_right = "right aligned"
131    text_intedit: tuple[str, list[str | tuple[str, str]]] = (
132        "editcp",
133        [("important", "IntEdit"), " allows only numbers: "],
134    )
135    text_edit_padding = ("editcp", "Edit widget within a Padding widget ")
136    text_columns1: list[str | tuple[str, str]] = [
137        ("important", "Columns"),
138        (
139            " are used to share horizontal screen space.  "
140            "This one splits the space into two parts with "
141            "three characters between each column.  The "
142            "contents of each column is a single widget."
143        ),
144    ]
145    text_columns2: list[str | tuple[str, str]] = [
146        "When you need to put more than one widget into a column you can use a ",
147        ("important", "Pile"),
148        " to combine two or more widgets.",
149    ]
150    text_col_columns = "Columns may be placed inside other columns."
151    text_col_21 = "Col 2.1"
152    text_col_22 = "Col 2.2"
153    text_col_23 = "Col 2.3"
154    text_column_widths = (
155        "Columns may also have uneven relative "
156        "weights or fixed widths.  Use a minimum width so that "
157        "columns don't become too small."
158    )
159    text_weight = "Weight %d"
160    text_fixed_9 = "<Fixed 9>"  # should be 9 columns wide
161    text_fixed_14 = "<--Fixed 14-->"  # should be 14 columns wide
162    text_edit_col_cap1 = ("editcp", "Edit widget within Columns")
163    text_edit_col_text1 = "here's\nsome\ninfo"
164    text_edit_col_cap2 = ("editcp", "and within Pile ")
165    text_edit_col_text2 = "more"
166    text_edit_col_cap3 = ("editcp", "another ")
167    text_edit_col_text3 = "still more"
168    text_gridflow: list[str | tuple[str, str]] = [
169        "A ",
170        ("important", "GridFlow"),
171        (
172            " widget "
173            "may be used to display a list of flow widgets with equal "
174            "widths.  Widgets that don't fit on the first line will "
175            "flow to the next.  This is useful for small widgets that "
176            "you want to keep together such as "
177        ),
178        ("important", "Button"),
179        ", ",
180        ("important", "CheckBox"),
181        " and ",
182        ("important", "RadioButton"),
183        " widgets.  The ",
184        ("important", "GridFlow"),
185        " of buttons below sets ",
186        ("important", "unfocused_focus_attr"),
187        ", so its focus button stays marked while the focus is elsewhere.",
188    ]
189    text_button_list = ["Yes", "No", "Perhaps", "Certainly", "Partially", "Tuesdays Only", "Help"]
190    text_cb_list = ["Wax", "Wash", "Buff", "Clear Coat", "Dry", "Racing Stripe"]
191    text_rb_list = ["Morning", "Afternoon", "Evening", "Weekend"]
192    text_table: list[str | tuple[str, str]] = [
193        "A ",
194        ("important", "Table"),
195        (
196            " widget lines up rows of cells between a fixed header and footer: every column is as wide as its "
197            "widest cell, a number, or a share of the rest.  This one shows at most four rows at a time, with a "
198            "scrollbar while they do not all fit, a footer row of totals, and keeps the focused row marked while the "
199            "focus is elsewhere."
200        ),
201    ]
202    table_rows = [
203        ("urwid", "1.2M", "console user interface library"),
204        ("table.py", "34K", "the widget shown here"),
205        ("tour.py", "18K", "this program"),
206        ("palette", "1", "a single entry"),
207        ("canvas.py", "52K", "what widgets render to"),
208        ("\u4e2d\u6587", "2", "wide characters line up too"),
209    ]
210    text_drop_down: list[str | tuple[str, str]] = [
211        "A ",
212        ("important", "DropDown"),
213        (
214            " widget shows the selected option.  ENTER, SPACE or a mouse click "
215            "opens a list of all the options below it, or above it when there is no room "
216            "below; ESC or a click outside closes the list without a change.  A long list scrolls.  "
217            "The list is a pop-up, so the MainLoop is created with pop_ups=True."
218        ),
219    ]
220    text_drop_down_cap1 = "Color:"
221    text_drop_down_list1 = ["Red", "Green", "Blue", "Cyan", "Magenta", "Yellow"]
222    text_drop_down_cap2 = "Month:"
223    text_drop_down_list2 = [
224        "January",
225        "February",
226        "March",
227        "April",
228        "May",
229        "June",
230        "July",
231        "August",
232        "September",
233        "October",
234        "November",
235        "December",
236    ]
237    text_tabs: list[str | tuple[str, str]] = [
238        "A ",
239        ("important", "Tabs"),
240        (
241            " widget shows one row of tab labels over the body of the selected tab.  "
242            "META RIGHT / META LEFT or a mouse click switch tabs.  UP from the body moves to the label row, "
243            "where LEFT / RIGHT and ENTER pick a tab and DOWN goes back.  "
244            "Make the window narrow to see the label row scroll."
245        ),
246    ]
247    text_listbox: list[str | tuple[str, str]] = [
248        "All these widgets have been displayed with the help of a ",
249        ("important", "ListBox"),
250        " widget.  ListBox widgets handle scrolling and changing focus.  A ",
251        ("important", "Frame"),
252        " widget is used to keep the instructions at the top of the screen.",
253    ]
254
255    def button_press(button: urwid.Button) -> None:
256        frame.footer = urwid.AttrMap(urwid.Text(["Pressed: ", button.get_label()]), "header")
257
258    radio_button_group: list[urwid.RadioButton] = []
259
260    drop_down_options = (text_drop_down_list1, text_drop_down_list2)
261    drop_down_selected = urwid.Text("")
262
263    def show_drop_down_selection(changed: urwid.DropDown | None = None, new_index: int = 0) -> None:
264        # 'change' is sent before changed.selected moves, so its new option is read from new_index.
265        markup: list[str | tuple[str, str]] = ["Selected: "]
266        for number, (drop_down, options) in enumerate(zip(drop_downs, drop_down_options, strict=True)):
267            index = new_index if drop_down is changed else drop_down.selected
268            if number:
269                markup.append(", ")
270            markup.append(("important", options[index]))
271        drop_down_selected.set_text(markup)
272
273    drop_downs = [
274        urwid.DropDown(drop_down_options[0], attr_map="buttn", focus_map="buttnf"),
275        urwid.DropDown(drop_down_options[1], selected=5, max_visible_items=5, attr_map="buttn", focus_map="buttnf"),
276    ]
277    for drop_down in drop_downs:
278        urwid.connect_signal(drop_down, "change", show_drop_down_selection)
279    show_drop_down_selection()
280    # One width for both, so the two drop-downs line up.
281    drop_down_width = max(drop_down.pack()[0] for drop_down in drop_downs)
282
283    blank = urwid.Divider()
284    listbox_content: list[urwid.widget.AbstractFlowWidget] = [
285        blank,
286        urwid.Padding(urwid.Text(text_intro), left=2, right=2, min_width=20),
287        blank,
288        urwid.Text(text_right, align=urwid.RIGHT),
289        blank,
290        urwid.Text(text_center, align=urwid.CENTER),
291        blank,
292        urwid.Text(text_clip, wrap=urwid.CLIP),
293        blank,
294        urwid.Text(text_right_clip, align=urwid.RIGHT, wrap=urwid.CLIP),
295        blank,
296        urwid.Text(text_center_clip, align=urwid.CENTER, wrap=urwid.CLIP),
297        blank,
298        urwid.Text(text_ellipsis, wrap=urwid.ELLIPSIS),
299        blank,
300        urwid.Text(text_any, wrap=urwid.ANY),
301        blank,
302        urwid.Padding(urwid.Text(text_padding), (urwid.RELATIVE, 20), 40),
303        blank,
304        urwid.AttrMap(urwid.Divider("=", 1), "bright"),
305        urwid.Padding(urwid.Text(text_divider), left=2, right=2, min_width=20),
306        urwid.AttrMap(urwid.Divider("-", 0, 1), "bright"),
307        blank,
308        urwid.Padding(urwid.Text(text_edit), left=2, right=2, min_width=20),
309        blank,
310        urwid.AttrMap(urwid.Edit(text_edit_cap1, text_edit_text1), "editbx", "editfc"),
311        blank,
312        urwid.AttrMap(urwid.Edit(text_edit_cap2, text_edit_text2, multiline=True), "editbx", "editfc"),
313        blank,
314        urwid.AttrMap(urwid.Edit(text_edit_cap3, text_edit_text3, wrap=urwid.CLIP), "editbx", "editfc"),
315        blank,
316        urwid.AttrMap(urwid.Edit(text_edit_cap4, text_edit_text4, max_length=8), "editbx", "editfc"),
317        blank,
318        urwid.AttrMap(
319            urwid.Edit(
320                text_edit_cap5,
321                placeholder=("edithint", "e.g. 8080"),
322                validator=check_port,
323                validation_attr={urwid.Severity.WARNING: "editwarn", urwid.Severity.ERROR: "editerr"},
324                validation_focus_attr={urwid.Severity.WARNING: "editwarnf", urwid.Severity.ERROR: "editerrf"},
325            ),
326            "editbx",
327            "editfc",
328        ),
329        blank,
330        urwid.AttrMap(
331            urwid.Edit(text_edit_cap6, text_completer=complete_colour),
332            "editbx",
333            "editfc",
334        ),
335        blank,
336        urwid.AttrMap(ReadlineEdit(text_edit_cap7, text_edit_text7, undo_limit=50, kill_buffer=[]), "editbx", "editfc"),
337        blank,
338        urwid.Text(text_edit_alignments),
339        urwid.AttrMap(urwid.Edit("", text_edit_left, align=urwid.LEFT), "editbx", "editfc"),
340        urwid.AttrMap(urwid.Edit("", text_edit_center, align=urwid.CENTER), "editbx", "editfc"),
341        urwid.AttrMap(urwid.Edit("", text_edit_right, align=urwid.RIGHT), "editbx", "editfc"),
342        blank,
343        urwid.AttrMap(urwid.IntEdit(text_intedit, 123), "editbx", "editfc"),
344        blank,
345        urwid.Padding(urwid.AttrMap(urwid.Edit(text_edit_padding, ""), "editbx", "editfc"), left=10, width=50),
346        blank,
347        blank,
348        urwid.AttrMap(
349            urwid.Columns(
350                [
351                    urwid.Divider("."),
352                    urwid.Divider(","),
353                    urwid.Divider("."),
354                ]
355            ),
356            "bright",
357        ),
358        blank,
359        urwid.Columns(
360            [
361                urwid.Padding(urwid.Text(text_columns1), left=2, right=0, min_width=20),
362                urwid.Pile([urwid.Divider("~"), urwid.Text(text_columns2), urwid.Divider("_")]),
363            ],
364            3,
365        ),
366        blank,
367        blank,
368        urwid.Columns(
369            [
370                urwid.Text(text_col_columns),
371                urwid.Columns(
372                    [
373                        urwid.Text(text_col_21),
374                        urwid.Text(text_col_22),
375                        urwid.Text(text_col_23),
376                    ],
377                    1,
378                ),
379            ],
380            2,
381        ),
382        blank,
383        urwid.Padding(urwid.Text(text_column_widths), left=2, right=2, min_width=20),
384        blank,
385        urwid.Columns(
386            [
387                urwid.AttrMap(urwid.Text(text_weight % 1), "reverse"),
388                (urwid.WEIGHT, 2, urwid.Text(text_weight % 2)),
389                (urwid.WEIGHT, 3, urwid.AttrMap(urwid.Text(text_weight % 3), "reverse")),
390                (urwid.WEIGHT, 4, urwid.Text(text_weight % 4)),
391                (urwid.WEIGHT, 5, urwid.AttrMap(urwid.Text(text_weight % 5), "reverse")),
392                (urwid.WEIGHT, 6, urwid.Text(text_weight % 6)),
393            ],
394            0,
395            min_width=8,
396        ),
397        blank,
398        urwid.Columns(
399            [
400                (urwid.WEIGHT, 2, urwid.AttrMap(urwid.Text(text_weight % 2), "reverse")),
401                (9, urwid.Text(text_fixed_9)),
402                (urwid.WEIGHT, 3, urwid.AttrMap(urwid.Text(text_weight % 3), "reverse")),
403                (14, urwid.Text(text_fixed_14)),
404            ],
405            0,
406            min_width=8,
407        ),
408        blank,
409        urwid.Columns(
410            [
411                urwid.AttrMap(urwid.Edit(text_edit_col_cap1, text_edit_col_text1, multiline=True), "editbx", "editfc"),
412                urwid.Pile(
413                    [
414                        urwid.AttrMap(urwid.Edit(text_edit_col_cap2, text_edit_col_text2), "editbx", "editfc"),
415                        blank,
416                        urwid.AttrMap(urwid.Edit(text_edit_col_cap3, text_edit_col_text3), "editbx", "editfc"),
417                    ],
418                    select_navigation=True,
419                ),
420            ],
421            1,
422            select_navigation=True,
423        ),
424        blank,
425        urwid.AttrMap(
426            urwid.Columns(
427                [
428                    urwid.Divider("'"),
429                    urwid.Divider('"'),
430                    urwid.Divider("~"),
431                    urwid.Divider('"'),
432                    urwid.Divider("'"),
433                ]
434            ),
435            "bright",
436        ),
437        blank,
438        blank,
439        urwid.Padding(urwid.Text(text_gridflow), left=2, right=2, min_width=20),
440        blank,
441        urwid.Padding(
442            urwid.GridFlow(
443                [urwid.AttrMap(urwid.Button(txt, button_press), "buttn", "buttnf") for txt in text_button_list],
444                13,
445                3,
446                1,
447                urwid.LEFT,
448                select_navigation=True,
449                unfocused_focus_attr={"buttn": "buttnl"},
450            ),
451            left=4,
452            right=3,
453            min_width=13,
454        ),
455        blank,
456        urwid.Padding(
457            urwid.GridFlow(
458                [urwid.AttrMap(urwid.CheckBox(txt), "buttn", "buttnf") for txt in text_cb_list],
459                10,
460                3,
461                1,
462                urwid.LEFT,
463                select_navigation=True,
464            ),
465            left=4,
466            right=3,
467            min_width=10,
468        ),
469        blank,
470        urwid.Padding(
471            urwid.GridFlow(
472                [urwid.AttrMap(urwid.RadioButton(radio_button_group, txt), "buttn", "buttnf") for txt in text_rb_list],
473                13,
474                3,
475                1,
476                urwid.LEFT,
477                select_navigation=True,
478            ),
479            left=4,
480            right=3,
481            min_width=13,
482        ),
483        blank,
484        blank,
485        urwid.Padding(urwid.Text(text_table), left=2, right=2, min_width=20),
486        blank,
487        urwid.Padding(
488            urwid.Table(
489                [
490                    urwid.TableColumn("Name"),
491                    urwid.TableColumn("Size", align=urwid.RIGHT, attr="bright"),
492                    urwid.TableColumn("Note", width=(urwid.WEIGHT, 1), wrap=urwid.ELLIPSIS),
493                ],
494                table_rows,
495                header_attr="important",
496                footer=["6 entries", "1.3M"],
497                footer_attr="important",
498                focus_attr={None: "buttnf", "bright": "buttnf"},
499                unfocused_focus_attr={None: "buttnl", "bright": "buttnl"},
500                max_rows=4,
501                scrollbar=None,
502                scrollbar_thumb_char=urwid.ScrollBar.Symbols.DARK_SHADE,
503                scrollbar_trough_char=urwid.ScrollBar.Symbols.LITE_SHADE,
504            ),
505            left=4,
506            right=3,
507            min_width=20,
508        ),
509        blank,
510        blank,
511        urwid.Padding(urwid.Text(text_drop_down), left=2, right=2, min_width=20),
512        blank,
513        urwid.Padding(
514            urwid.Columns(
515                [
516                    (7, urwid.Text(text_drop_down_cap1)),
517                    (drop_down_width, urwid.AttrMap(drop_downs[0], "buttn", "buttnf")),
518                ]
519            ),
520            left=4,
521            right=3,
522            min_width=20,
523        ),
524        urwid.Padding(
525            urwid.Columns(
526                [
527                    (7, urwid.Text(text_drop_down_cap2)),
528                    (drop_down_width, urwid.AttrMap(drop_downs[1], "buttn", "buttnf")),
529                ]
530            ),
531            left=4,
532            right=3,
533            min_width=20,
534        ),
535        blank,
536        urwid.Padding(drop_down_selected, left=4, right=3, min_width=20),
537        blank,
538        blank,
539        urwid.Padding(urwid.Text(text_tabs), left=2, right=2, min_width=20),
540        blank,
541        urwid.Padding(
542            urwid.Tabs(
543                [
544                    ("Overview", urwid.Text("Each tab has its own body; only the selected one is drawn.")),
545                    ("Name", urwid.AttrMap(urwid.Edit("Name: "), "editbx", "editfc")),
546                    ("Options", urwid.Pile([urwid.CheckBox("Verbose"), urwid.CheckBox("Dry run")])),
547                ],
548                bar_selectable=True,
549                underline=(urwid.Tabs.Symbols.LINE_LIGHT, urwid.Tabs.Symbols.LINE_HEAVY),
550                attr_map="tab",
551                selected_map="tab selected",
552                focus_map="buttnf",
553            ),
554            left=4,
555            right=3,
556            min_width=20,
557        ),
558        blank,
559        blank,
560        urwid.Padding(urwid.Text(text_listbox), left=2, right=2, min_width=20),
561        blank,
562        blank,
563    ]
564
565    header = urwid.AttrMap(urwid.Text(text_header), "header")
566    # select_navigation lets TAB / SHIFT TAB visit every edit, button and check box in order
567    listbox = urwid.ListBox(urwid.SimpleListWalker(listbox_content), select_navigation=True)
568    scrollable = urwid.ScrollBar(
569        listbox,
570        trough_char=urwid.ScrollBar.Symbols.LITE_SHADE,
571        end_markers=(urwid.ScrollBar.Symbols.ARROW_TRIANGLE_UP, urwid.ScrollBar.Symbols.ARROW_TRIANGLE_DOWN),
572        scroll_by_focus=True,
573    )
574    frame: urwid.Frame[
575        urwid.AttrMap[urwid.ScrollBar[urwid.ListBox[int]]],
576        urwid.AttrMap[urwid.Text] | None,
577        urwid.AttrMap[urwid.Text] | None,
578    ] = urwid.Frame(
579        urwid.AttrMap(scrollable, "body"),
580        header=header,
581    )
582
583    palette: list[tuple[str, str, str] | tuple[str, str, str, str]] = [
584        ("body", "black", "light gray", "standout"),
585        ("reverse", "light gray", "black"),
586        ("header", "white", "dark red", "bold"),
587        ("important", "dark blue", "light gray", "standout,underline"),
588        ("editfc", "white", "dark blue", "bold"),
589        ("editbx", "light gray", "dark blue"),
590        ("editcp", "black", "light gray", "standout"),
591        ("edithint", "light cyan", "dark blue"),
592        ("editwarn", "yellow", "dark blue"),
593        ("editwarnf", "yellow", "dark blue", "bold"),
594        ("editerr", "light red", "dark blue"),
595        ("editerrf", "light red", "dark blue", "bold"),
596        ("bright", "dark gray", "light gray", "bold,standout"),
597        ("buttn", "black", "dark cyan"),
598        ("buttnf", "white", "dark blue", "bold"),
599        ("buttnl", "white", "dark cyan", "bold"),
600        ("tab", "dark gray", "light gray"),
601        ("tab selected", "dark blue", "light gray", "bold"),
602    ]
603
604    # use appropriate Screen class
605    if urwid.display.web.is_web_request():
606        screen = urwid.display.web.Screen()
607    else:
608        screen = urwid.display.raw.Screen()
609
610    def unhandled(key: str | tuple[str, int, int, int]) -> None:
611        if key == "f8":
612            raise urwid.ExitMainLoop
613
614    urwid.command_map.bind(Command.NEXT_TAB, "meta right")
615    urwid.command_map.bind(Command.PREVIOUS_TAB, "meta left")
616    # pop_ups=True adds the PopUpTarget that shows the open DropDown lists.
617    urwid.MainLoop(frame, palette, screen, unhandled_input=unhandled, pop_ups=True).run()
618
619
620def check_port(text: str) -> urwid.ValidationMessage | str | None:
621    """Return what is wrong with *text* as a TCP port number, or ``None`` for an empty or usable one."""
622    if not text:
623        return None
624    if not text.isdecimal() or int(text) not in PORTS:
625        return "not a port number"
626    if int(text) in PRIVILEGED_PORTS:
627        return urwid.ValidationMessage(urwid.Severity.WARNING, "a privileged port")
628    return None
629
630
631def complete_colour(text: str, pos: int) -> list[tuple[str, int]]:
632    """Return the colours that start with the text before the cursor, none for an empty field."""
633    prefix = text[:pos]
634    return [(name, len(name)) for name in COLOURS if name.startswith(prefix)] if prefix else []
635
636
637def setup() -> None:
638    """Run the tour as a web request when applicable, otherwise as a standalone program."""
639    urwid.display.web.set_preferences("Urwid Tour")
640    # try to handle short web requests quickly
641    if urwid.display.web.handle_short_request():
642        return
643
644    main()
645
646
647if __name__ == "__main__" or urwid.display.web.is_web_request():
648    setup()