urwid 2.6.0.post0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of urwid might be problematic. Click here for more details.

Files changed (75) hide show
  1. urwid/__init__.py +333 -0
  2. urwid/canvas.py +1413 -0
  3. urwid/command_map.py +137 -0
  4. urwid/container.py +59 -0
  5. urwid/decoration.py +65 -0
  6. urwid/display/__init__.py +97 -0
  7. urwid/display/_posix_raw_display.py +413 -0
  8. urwid/display/_raw_display_base.py +914 -0
  9. urwid/display/_web.css +12 -0
  10. urwid/display/_web.js +462 -0
  11. urwid/display/_win32.py +171 -0
  12. urwid/display/_win32_raw_display.py +269 -0
  13. urwid/display/common.py +1219 -0
  14. urwid/display/curses.py +690 -0
  15. urwid/display/escape.py +624 -0
  16. urwid/display/html_fragment.py +251 -0
  17. urwid/display/lcd.py +518 -0
  18. urwid/display/raw.py +37 -0
  19. urwid/display/web.py +636 -0
  20. urwid/event_loop/__init__.py +55 -0
  21. urwid/event_loop/abstract_loop.py +175 -0
  22. urwid/event_loop/asyncio_loop.py +231 -0
  23. urwid/event_loop/glib_loop.py +294 -0
  24. urwid/event_loop/main_loop.py +721 -0
  25. urwid/event_loop/select_loop.py +230 -0
  26. urwid/event_loop/tornado_loop.py +206 -0
  27. urwid/event_loop/trio_loop.py +302 -0
  28. urwid/event_loop/twisted_loop.py +269 -0
  29. urwid/event_loop/zmq_loop.py +275 -0
  30. urwid/font.py +695 -0
  31. urwid/graphics.py +96 -0
  32. urwid/highlight.css +19 -0
  33. urwid/listbox.py +1899 -0
  34. urwid/monitored_list.py +522 -0
  35. urwid/numedit.py +376 -0
  36. urwid/signals.py +330 -0
  37. urwid/split_repr.py +130 -0
  38. urwid/str_util.py +358 -0
  39. urwid/text_layout.py +632 -0
  40. urwid/treetools.py +515 -0
  41. urwid/util.py +557 -0
  42. urwid/version.py +16 -0
  43. urwid/vterm.py +1806 -0
  44. urwid/widget/__init__.py +181 -0
  45. urwid/widget/attr_map.py +161 -0
  46. urwid/widget/attr_wrap.py +140 -0
  47. urwid/widget/bar_graph.py +649 -0
  48. urwid/widget/big_text.py +77 -0
  49. urwid/widget/box_adapter.py +126 -0
  50. urwid/widget/columns.py +1145 -0
  51. urwid/widget/constants.py +574 -0
  52. urwid/widget/container.py +227 -0
  53. urwid/widget/divider.py +110 -0
  54. urwid/widget/edit.py +718 -0
  55. urwid/widget/filler.py +403 -0
  56. urwid/widget/frame.py +539 -0
  57. urwid/widget/grid_flow.py +539 -0
  58. urwid/widget/line_box.py +194 -0
  59. urwid/widget/overlay.py +829 -0
  60. urwid/widget/padding.py +597 -0
  61. urwid/widget/pile.py +971 -0
  62. urwid/widget/popup.py +170 -0
  63. urwid/widget/progress_bar.py +141 -0
  64. urwid/widget/scrollable.py +597 -0
  65. urwid/widget/solid_fill.py +44 -0
  66. urwid/widget/text.py +354 -0
  67. urwid/widget/widget.py +852 -0
  68. urwid/widget/widget_decoration.py +166 -0
  69. urwid/widget/wimp.py +792 -0
  70. urwid/wimp.py +23 -0
  71. urwid-2.6.0.post0.dist-info/COPYING +504 -0
  72. urwid-2.6.0.post0.dist-info/METADATA +332 -0
  73. urwid-2.6.0.post0.dist-info/RECORD +75 -0
  74. urwid-2.6.0.post0.dist-info/WHEEL +5 -0
  75. urwid-2.6.0.post0.dist-info/top_level.txt +1 -0
urwid/display/web.py ADDED
@@ -0,0 +1,636 @@
1
+ # Urwid web (CGI/Asynchronous Javascript) display module
2
+ # Copyright (C) 2004-2007 Ian Ward
3
+ #
4
+ # This library is free software; you can redistribute it and/or
5
+ # modify it under the terms of the GNU Lesser General Public
6
+ # License as published by the Free Software Foundation; either
7
+ # version 2.1 of the License, or (at your option) any later version.
8
+ #
9
+ # This library is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12
+ # Lesser General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU Lesser General Public
15
+ # License along with this library; if not, write to the Free Software
16
+ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17
+ #
18
+ # Urwid web site: https://urwid.org/
19
+
20
+
21
+ """
22
+ Urwid web application display module
23
+ """
24
+ from __future__ import annotations
25
+
26
+ import dataclasses
27
+ import glob
28
+ import html
29
+ import os
30
+ import pathlib
31
+ import random
32
+ import selectors
33
+ import signal
34
+ import socket
35
+ import string
36
+ import sys
37
+ import tempfile
38
+ import typing
39
+ from contextlib import suppress
40
+
41
+ from urwid.str_util import calc_text_pos, calc_width, move_next_char
42
+ from urwid.util import StoppingContext
43
+
44
+ from .common import BaseScreen
45
+
46
+ if typing.TYPE_CHECKING:
47
+ from typing_extensions import Literal
48
+
49
+ from urwid import Canvas
50
+
51
+ TEMP_DIR = tempfile.gettempdir()
52
+ CURRENT_DIR = pathlib.Path(__file__).parent
53
+
54
+ _js_code = CURRENT_DIR.joinpath("_web.js").read_text("utf-8")
55
+
56
+ ALARM_DELAY = 60
57
+ POLL_CONNECT = 3
58
+ MAX_COLS = 200
59
+ MAX_ROWS = 100
60
+ MAX_READ = 4096
61
+ BUF_SZ = 16384
62
+
63
+ _code_colours = {
64
+ "black": "0",
65
+ "dark red": "1",
66
+ "dark green": "2",
67
+ "brown": "3",
68
+ "dark blue": "4",
69
+ "dark magenta": "5",
70
+ "dark cyan": "6",
71
+ "light gray": "7",
72
+ "dark gray": "8",
73
+ "light red": "9",
74
+ "light green": "A",
75
+ "yellow": "B",
76
+ "light blue": "C",
77
+ "light magenta": "D",
78
+ "light cyan": "E",
79
+ "white": "F",
80
+ }
81
+
82
+ # replace control characters with ?'s
83
+ _trans_table = "?" * 32 + "".join([chr(x) for x in range(32, 256)])
84
+
85
+ _css_style = CURRENT_DIR.joinpath("_web.css").read_text("utf-8")
86
+
87
+ # HTML Initial Page
88
+ _html_page = [
89
+ """<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
90
+ "http://www.w3.org/TR/html4/loose.dtd">
91
+ <html>
92
+ <head>
93
+ <title>Urwid Web Display - """,
94
+ """</title>
95
+ <style type="text/css">
96
+ """
97
+ + _css_style
98
+ + r"""
99
+ </style>
100
+ </head>
101
+ <body id="body" onload="load_web_display()">
102
+ <div style="position:absolute; visibility:hidden;">
103
+ <br id="br"\>
104
+ <pre>The quick brown fox jumps over the lazy dog.<span id="testchar">X</span>
105
+ <span id="testchar2">Y</span></pre>
106
+ </div>
107
+ Urwid Web Display - <b>""",
108
+ """</b> -
109
+ Status: <span id="status">Set up</span>
110
+ <script type="text/javascript">
111
+ //<![CDATA[
112
+ """
113
+ + _js_code
114
+ + """
115
+ //]]>
116
+ </script>
117
+ <pre id="text"></pre>
118
+ </body>
119
+ </html>
120
+ """,
121
+ ]
122
+
123
+
124
+ class Screen(BaseScreen):
125
+ def __init__(self):
126
+ super().__init__()
127
+ self.palette = {}
128
+ self.has_color = True
129
+ self._started = False
130
+
131
+ @property
132
+ def started(self):
133
+ return self._started
134
+
135
+ def register_palette(self, palette):
136
+ """Register a list of palette entries.
137
+
138
+ palette -- list of (name, foreground, background) or
139
+ (name, same_as_other_name) palette entries.
140
+
141
+ calls self.register_palette_entry for each item in l
142
+ """
143
+
144
+ for item in palette:
145
+ if len(item) in {3, 4}:
146
+ self.register_palette_entry(*item)
147
+ continue
148
+ if len(item) != 2:
149
+ raise ValueError(f"Invalid register_palette usage: {item!r}")
150
+ name, like_name = item
151
+ if like_name not in self.palette:
152
+ raise KeyError(f"palette entry '{like_name}' doesn't exist")
153
+ self.palette[name] = self.palette[like_name]
154
+
155
+ def register_palette_entry(
156
+ self,
157
+ name: str | None,
158
+ foreground: str,
159
+ background: str,
160
+ mono: str | None = None,
161
+ foreground_high: str | None = None,
162
+ background_high: str | None = None,
163
+ ) -> None:
164
+ """Register a single palette entry.
165
+
166
+ name -- new entry/attribute name
167
+ foreground -- foreground colour
168
+ background -- background colour
169
+ mono -- monochrome terminal attribute
170
+
171
+ See curses_display.register_palette_entry for more info.
172
+ """
173
+ if foreground == "default":
174
+ foreground = "black"
175
+ if background == "default":
176
+ background = "light gray"
177
+ self.palette[name] = (foreground, background, mono)
178
+
179
+ def set_mouse_tracking(self, enable: bool = True) -> None:
180
+ """Not yet implemented"""
181
+
182
+ def tty_signal_keys(self, *args, **vargs):
183
+ """Do nothing."""
184
+
185
+ def start(self) -> StoppingContext:
186
+ """
187
+ This function reads the initial screen size, generates a
188
+ unique id and handles cleanup when fn exits.
189
+
190
+ web_display.set_preferences(..) must be called before calling
191
+ this function for the preferences to take effect
192
+ """
193
+ if self._started:
194
+ return StoppingContext(self)
195
+
196
+ client_init = sys.stdin.read(50)
197
+ if not client_init.startswith("window resize "):
198
+ raise ValueError(client_init)
199
+ _ignore1, _ignore2, x, y = client_init.split(" ", 3)
200
+ x = int(x)
201
+ y = int(y)
202
+ self._set_screen_size(x, y)
203
+ self.last_screen = {}
204
+ self.last_screen_width = 0
205
+
206
+ self.update_method = os.environ["HTTP_X_URWID_METHOD"]
207
+ if self.update_method not in {"multipart", "polling"}:
208
+ raise ValueError(self.update_method)
209
+
210
+ if self.update_method == "polling" and not _prefs.allow_polling:
211
+ sys.stdout.write("Status: 403 Forbidden\r\n\r\n")
212
+ sys.exit(0)
213
+
214
+ clients = glob.glob(os.path.join(_prefs.pipe_dir, "urwid*.in"))
215
+ if len(clients) >= _prefs.max_clients:
216
+ sys.stdout.write("Status: 503 Sever Busy\r\n\r\n")
217
+ sys.exit(0)
218
+
219
+ urwid_id = f"{random.randrange(10 ** 9):09d}{random.randrange(10 ** 9):09d}" # noqa: S311
220
+ self.pipe_name = os.path.join(_prefs.pipe_dir, f"urwid{urwid_id}")
221
+ os.mkfifo(f"{self.pipe_name}.in", 0o600)
222
+ signal.signal(signal.SIGTERM, self._cleanup_pipe)
223
+
224
+ self.input_fd = os.open(f"{self.pipe_name}.in", os.O_NONBLOCK | os.O_RDONLY)
225
+ self.input_tail = ""
226
+ self.content_head = (
227
+ "Content-type: "
228
+ "multipart/x-mixed-replace;boundary=ZZ\r\n"
229
+ "X-Urwid-ID: " + urwid_id + "\r\n"
230
+ "\r\n\r\n"
231
+ "--ZZ\r\n"
232
+ )
233
+ if self.update_method == "polling":
234
+ self.content_head = f"Content-type: text/plain\r\nX-Urwid-ID: {urwid_id}\r\n\r\n\r\n"
235
+
236
+ signal.signal(signal.SIGALRM, self._handle_alarm)
237
+ signal.alarm(ALARM_DELAY)
238
+ self._started = True
239
+
240
+ return StoppingContext(self)
241
+
242
+ def stop(self):
243
+ """
244
+ Restore settings and clean up.
245
+ """
246
+ if not self._started:
247
+ return
248
+
249
+ # XXX which exceptions does this actually raise? EnvironmentError?
250
+ with suppress(Exception):
251
+ self._close_connection()
252
+ signal.signal(signal.SIGTERM, signal.SIG_DFL)
253
+ self._cleanup_pipe()
254
+ self._started = False
255
+
256
+ def set_input_timeouts(self, *args):
257
+ pass
258
+
259
+ def _close_connection(self):
260
+ if self.update_method == "polling child":
261
+ self.server_socket.settimeout(0)
262
+ sock, _addr = self.server_socket.accept()
263
+ sock.sendall(b"Z")
264
+ sock.close()
265
+
266
+ if self.update_method == "multipart":
267
+ sys.stdout.write("\r\nZ\r\n--ZZ--\r\n")
268
+ sys.stdout.flush()
269
+
270
+ def _cleanup_pipe(self, *args):
271
+ if not self.pipe_name:
272
+ return
273
+ # XXX which exceptions does this actually raise? EnvironmentError?
274
+ with suppress(Exception):
275
+ os.remove(f"{self.pipe_name}.in")
276
+ os.remove(f"{self.pipe_name}.update")
277
+
278
+ def _set_screen_size(self, cols, rows):
279
+ """Set the screen size (within max size)."""
280
+
281
+ cols = min(cols, MAX_COLS)
282
+ rows = min(rows, MAX_ROWS)
283
+ self.screen_size = cols, rows
284
+
285
+ def draw_screen(self, size: tuple[int, int], canvas: Canvas):
286
+ """Send a screen update to the client."""
287
+
288
+ (cols, rows) = size
289
+
290
+ if cols != self.last_screen_width:
291
+ self.last_screen = {}
292
+
293
+ sendq = [self.content_head]
294
+
295
+ if self.update_method == "polling":
296
+ send = sendq.append
297
+ elif self.update_method == "polling child":
298
+ signal.alarm(0)
299
+ try:
300
+ s, _addr = self.server_socket.accept()
301
+ except socket.timeout:
302
+ sys.exit(0)
303
+ send = s.sendall
304
+ else:
305
+ signal.alarm(0)
306
+ send = sendq.append
307
+ send("\r\n")
308
+ self.content_head = ""
309
+
310
+ if canvas.rows() != rows:
311
+ raise ValueError(rows)
312
+
313
+ if canvas.cursor is not None:
314
+ cx, cy = canvas.cursor
315
+ else:
316
+ cx = cy = None
317
+
318
+ new_screen = {}
319
+
320
+ y = -1
321
+ for row in canvas.content():
322
+ y += 1
323
+ l_row = row.copy()
324
+
325
+ line = []
326
+
327
+ sig = tuple(l_row)
328
+ if y == cy:
329
+ sig = (*sig, cx)
330
+ new_screen[sig] = [*new_screen.get(sig, []), y]
331
+ old_line_numbers = self.last_screen.get(sig, None)
332
+ if old_line_numbers is not None:
333
+ if y in old_line_numbers:
334
+ old_line = y
335
+ else:
336
+ old_line = old_line_numbers[0]
337
+ send(f"<{old_line:d}\n")
338
+ continue
339
+
340
+ col = 0
341
+ for a, _cs, run in l_row:
342
+ t_run = run.translate(_trans_table)
343
+ if a is None:
344
+ fg, bg, _mono = "black", "light gray", None
345
+ else:
346
+ fg, bg, _mono = self.palette[a]
347
+ if y == cy and col <= cx:
348
+ run_width = calc_width(t_run, 0, len(t_run))
349
+ if col + run_width > cx:
350
+ line.append(code_span(t_run, fg, bg, cx - col))
351
+ else:
352
+ line.append(code_span(t_run, fg, bg))
353
+ col += run_width
354
+ else:
355
+ line.append(code_span(t_run, fg, bg))
356
+
357
+ send(f"{''.join(line)}\n")
358
+ self.last_screen = new_screen
359
+ self.last_screen_width = cols
360
+
361
+ if self.update_method == "polling":
362
+ sys.stdout.write("".join(sendq))
363
+ sys.stdout.flush()
364
+ sys.stdout.close()
365
+ self._fork_child()
366
+ elif self.update_method == "polling child":
367
+ s.close()
368
+ else: # update_method == "multipart"
369
+ send("\r\n--ZZ\r\n")
370
+ sys.stdout.write("".join(sendq))
371
+ sys.stdout.flush()
372
+
373
+ signal.alarm(ALARM_DELAY)
374
+
375
+ def clear(self):
376
+ """
377
+ Force the screen to be completely repainted on the next
378
+ call to draw_screen().
379
+
380
+ (does nothing for web_display)
381
+ """
382
+
383
+ def _fork_child(self):
384
+ """
385
+ Fork a child to run CGI disconnected for polling update method.
386
+ Force parent process to exit.
387
+ """
388
+ daemonize(f"{self.pipe_name}.err")
389
+ self.input_fd = os.open(f"{self.pipe_name}.in", os.O_NONBLOCK | os.O_RDONLY)
390
+ self.update_method = "polling child"
391
+ s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
392
+ s.bind(f"{self.pipe_name}.update")
393
+ s.listen(1)
394
+ s.settimeout(POLL_CONNECT)
395
+ self.server_socket = s
396
+
397
+ def _handle_alarm(self, sig, frame):
398
+ if self.update_method not in {"multipart", "polling child"}:
399
+ raise ValueError(self.update_method)
400
+ if self.update_method == "polling child":
401
+ # send empty update
402
+ try:
403
+ s, _addr = self.server_socket.accept()
404
+ s.close()
405
+ except socket.timeout:
406
+ sys.exit(0)
407
+ else:
408
+ # send empty update
409
+ sys.stdout.write("\r\n\r\n--ZZ\r\n")
410
+ sys.stdout.flush()
411
+ signal.alarm(ALARM_DELAY)
412
+
413
+ def get_cols_rows(self):
414
+ """Return the screen size."""
415
+ return self.screen_size
416
+
417
+ @typing.overload
418
+ def get_input(self, raw_keys: Literal[False]) -> list[str]: ...
419
+
420
+ @typing.overload
421
+ def get_input(self, raw_keys: Literal[True]) -> tuple[list[str], list[int]]: ...
422
+
423
+ def get_input(self, raw_keys: bool = False) -> list[str] | tuple[list[str], list[int]]:
424
+ """Return pending input as a list."""
425
+ pending_input = []
426
+ resized = False
427
+ with selectors.DefaultSelector() as selector:
428
+ selector.register(self.input_fd, selectors.EVENT_READ)
429
+
430
+ iready = [event.fd for event, _ in selector.select(0.5)]
431
+
432
+ if not iready:
433
+ if raw_keys:
434
+ return [], []
435
+ return []
436
+
437
+ keydata = os.read(self.input_fd, MAX_READ)
438
+ os.close(self.input_fd)
439
+ self.input_fd = os.open(f"{self.pipe_name}.in", os.O_NONBLOCK | os.O_RDONLY)
440
+ # sys.stderr.write( repr((keydata,self.input_tail))+"\n" )
441
+ keys = keydata.split("\n")
442
+ keys[0] = self.input_tail + keys[0]
443
+ self.input_tail = keys[-1]
444
+
445
+ for k in keys[:-1]:
446
+ if k.startswith("window resize "):
447
+ _ign1, _ign2, x, y = k.split(" ", 3)
448
+ x = int(x)
449
+ y = int(y)
450
+ self._set_screen_size(x, y)
451
+ resized = True
452
+ else:
453
+ pending_input.append(k)
454
+ if resized:
455
+ pending_input.append("window resize")
456
+
457
+ if raw_keys:
458
+ return pending_input, []
459
+ return pending_input
460
+
461
+
462
+ def code_span(s, fg, bg, cursor=-1):
463
+ code_fg = _code_colours[fg]
464
+ code_bg = _code_colours[bg]
465
+
466
+ if cursor >= 0:
467
+ c_off, _ign = calc_text_pos(s, 0, len(s), cursor)
468
+ c2_off = move_next_char(s, c_off, len(s))
469
+
470
+ return (
471
+ code_fg
472
+ + code_bg
473
+ + s[:c_off]
474
+ + "\n"
475
+ + code_bg
476
+ + code_fg
477
+ + s[c_off:c2_off]
478
+ + "\n"
479
+ + code_fg
480
+ + code_bg
481
+ + s[c2_off:]
482
+ + "\n"
483
+ )
484
+
485
+ return f"{code_fg + code_bg + s}\n"
486
+
487
+
488
+ def html_escape(text):
489
+ """Escape text so that it will be displayed safely within HTML"""
490
+ return html.escape(text)
491
+
492
+
493
+ def is_web_request():
494
+ """
495
+ Return True if this is a CGI web request.
496
+ """
497
+ return "REQUEST_METHOD" in os.environ
498
+
499
+
500
+ def handle_short_request():
501
+ """
502
+ Handle short requests such as passing keystrokes to the application
503
+ or sending the initial html page. If returns True, then this
504
+ function recognised and handled a short request, and the calling
505
+ script should immediately exit.
506
+
507
+ web_display.set_preferences(..) should be called before calling this
508
+ function for the preferences to take effect
509
+ """
510
+ if not is_web_request():
511
+ return False
512
+
513
+ if os.environ["REQUEST_METHOD"] == "GET":
514
+ # Initial request, send the HTML and javascript.
515
+ sys.stdout.write("Content-type: text/html\r\n\r\n" + html_escape(_prefs.app_name).join(_html_page))
516
+ return True
517
+
518
+ if os.environ["REQUEST_METHOD"] != "POST":
519
+ # Don't know what to do with head requests etc.
520
+ return False
521
+
522
+ if "HTTP_X_URWID_ID" not in os.environ:
523
+ # If no urwid id, then the application should be started.
524
+ return False
525
+
526
+ urwid_id = os.environ["HTTP_X_URWID_ID"]
527
+ if len(urwid_id) > 20:
528
+ # invalid. handle by ignoring
529
+ # assert 0, "urwid id too long!"
530
+ sys.stdout.write("Status: 414 URI Too Long\r\n\r\n")
531
+ return True
532
+ for c in urwid_id:
533
+ if c not in string.digits:
534
+ # invald. handle by ignoring
535
+ # assert 0, "invalid chars in id!"
536
+ sys.stdout.write("Status: 403 Forbidden\r\n\r\n")
537
+ return True
538
+
539
+ if os.environ.get("HTTP_X_URWID_METHOD", None) == "polling":
540
+ # this is a screen update request
541
+ s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
542
+ try:
543
+ s.connect(os.path.join(_prefs.pipe_dir, f"urwid{urwid_id}.update"))
544
+ data = f"Content-type: text/plain\r\n\r\n{s.recv(BUF_SZ)}"
545
+ while data:
546
+ sys.stdout.write(data)
547
+ data = s.recv(BUF_SZ)
548
+ except OSError:
549
+ sys.stdout.write("Status: 404 Not Found\r\n\r\n")
550
+ return True
551
+ return True
552
+
553
+ # this is a keyboard input request
554
+ try:
555
+ fd = os.open((os.path.join(_prefs.pipe_dir, f"urwid{urwid_id}.in")), os.O_WRONLY)
556
+ except OSError:
557
+ sys.stdout.write("Status: 404 Not Found\r\n\r\n")
558
+ return True
559
+
560
+ # FIXME: use the correct encoding based on the request
561
+ keydata = sys.stdin.read(MAX_READ)
562
+ os.write(fd, keydata.encode("ascii"))
563
+ os.close(fd)
564
+ sys.stdout.write("Content-type: text/plain\r\n\r\n")
565
+
566
+ return True
567
+
568
+
569
+ @dataclasses.dataclass
570
+ class _Preferences:
571
+ app_name: str = "Unnamed Application"
572
+ pipe_dir: str = TEMP_DIR
573
+ allow_polling: bool = True
574
+ max_clients: int = 20
575
+
576
+
577
+ _prefs = _Preferences()
578
+
579
+
580
+ def set_preferences(
581
+ app_name: str,
582
+ pipe_dir: str = TEMP_DIR,
583
+ allow_polling: bool = True,
584
+ max_clients: int = 20,
585
+ ) -> None:
586
+ """
587
+ Set web_display preferences.
588
+
589
+ app_name -- application name to appear in html interface
590
+ pipe_dir -- directory for input pipes, daemon update sockets
591
+ and daemon error logs
592
+ allow_polling -- allow creation of daemon processes for
593
+ browsers without multipart support
594
+ max_clients -- maximum concurrent client connections. This
595
+ pool is shared by all urwid applications
596
+ using the same pipe_dir
597
+ """
598
+ _prefs.app_name = app_name
599
+ _prefs.pipe_dir = pipe_dir
600
+ _prefs.allow_polling = allow_polling
601
+ _prefs.max_clients = max_clients
602
+
603
+
604
+ class ErrorLog:
605
+ def __init__(self, errfile: str | pathlib.PurePath) -> None:
606
+ self.errfile = errfile
607
+
608
+ def write(self, err: str) -> None:
609
+ with open(self.errfile, "a", encoding="utf-8") as f:
610
+ f.write(err)
611
+
612
+
613
+ def daemonize(errfile):
614
+ """
615
+ Detach process and become a daemon.
616
+ """
617
+ pid = os.fork()
618
+ if pid:
619
+ os._exit(0)
620
+
621
+ os.setsid()
622
+ signal.signal(signal.SIGHUP, signal.SIG_IGN)
623
+ os.umask(0)
624
+
625
+ pid = os.fork()
626
+ if pid:
627
+ os._exit(0)
628
+
629
+ os.chdir("/")
630
+ for fd in range(0, 20):
631
+ with suppress(OSError):
632
+ os.close(fd)
633
+
634
+ sys.stdin = open("/dev/null", encoding="utf-8") # noqa: SIM115 # pylint: disable=consider-using-with
635
+ sys.stdout = open("/dev/null", "w", encoding="utf-8") # noqa: SIM115 # pylint: disable=consider-using-with
636
+ sys.stderr = ErrorLog(errfile)
@@ -0,0 +1,55 @@
1
+ """Package with EventLoop implementations for urwid."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+
7
+ from .abstract_loop import EventLoop, ExitMainLoop
8
+ from .asyncio_loop import AsyncioEventLoop
9
+ from .main_loop import MainLoop
10
+ from .select_loop import SelectEventLoop
11
+
12
+ __all__ = (
13
+ "AsyncioEventLoop",
14
+ "EventLoop",
15
+ "ExitMainLoop",
16
+ "MainLoop",
17
+ "SelectEventLoop",
18
+ )
19
+
20
+ try:
21
+ from .twisted_loop import TwistedEventLoop
22
+
23
+ __all__ += ("TwistedEventLoop",)
24
+ except ImportError:
25
+ pass
26
+
27
+ try:
28
+ from .tornado_loop import TornadoEventLoop
29
+
30
+ __all__ += ("TornadoEventLoop",)
31
+ except ImportError:
32
+ pass
33
+
34
+ try:
35
+ from .glib_loop import GLibEventLoop
36
+
37
+ __all__ += ("GLibEventLoop",)
38
+ except ImportError:
39
+ pass
40
+
41
+ try:
42
+ from .trio_loop import TrioEventLoop
43
+
44
+ __all__ += ("TrioEventLoop",)
45
+ except ImportError:
46
+ pass
47
+
48
+ if sys.platform != "win32":
49
+ # ZMQEventLoop cause interpreter crash on windows
50
+ try:
51
+ from .zmq_loop import ZMQEventLoop
52
+
53
+ __all__ += ("ZMQEventLoop",)
54
+ except ImportError:
55
+ pass