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()