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.
- urwid/__init__.py +333 -0
- urwid/canvas.py +1413 -0
- urwid/command_map.py +137 -0
- urwid/container.py +59 -0
- urwid/decoration.py +65 -0
- urwid/display/__init__.py +97 -0
- urwid/display/_posix_raw_display.py +413 -0
- urwid/display/_raw_display_base.py +914 -0
- urwid/display/_web.css +12 -0
- urwid/display/_web.js +462 -0
- urwid/display/_win32.py +171 -0
- urwid/display/_win32_raw_display.py +269 -0
- urwid/display/common.py +1219 -0
- urwid/display/curses.py +690 -0
- urwid/display/escape.py +624 -0
- urwid/display/html_fragment.py +251 -0
- urwid/display/lcd.py +518 -0
- urwid/display/raw.py +37 -0
- urwid/display/web.py +636 -0
- urwid/event_loop/__init__.py +55 -0
- urwid/event_loop/abstract_loop.py +175 -0
- urwid/event_loop/asyncio_loop.py +231 -0
- urwid/event_loop/glib_loop.py +294 -0
- urwid/event_loop/main_loop.py +721 -0
- urwid/event_loop/select_loop.py +230 -0
- urwid/event_loop/tornado_loop.py +206 -0
- urwid/event_loop/trio_loop.py +302 -0
- urwid/event_loop/twisted_loop.py +269 -0
- urwid/event_loop/zmq_loop.py +275 -0
- urwid/font.py +695 -0
- urwid/graphics.py +96 -0
- urwid/highlight.css +19 -0
- urwid/listbox.py +1899 -0
- urwid/monitored_list.py +522 -0
- urwid/numedit.py +376 -0
- urwid/signals.py +330 -0
- urwid/split_repr.py +130 -0
- urwid/str_util.py +358 -0
- urwid/text_layout.py +632 -0
- urwid/treetools.py +515 -0
- urwid/util.py +557 -0
- urwid/version.py +16 -0
- urwid/vterm.py +1806 -0
- urwid/widget/__init__.py +181 -0
- urwid/widget/attr_map.py +161 -0
- urwid/widget/attr_wrap.py +140 -0
- urwid/widget/bar_graph.py +649 -0
- urwid/widget/big_text.py +77 -0
- urwid/widget/box_adapter.py +126 -0
- urwid/widget/columns.py +1145 -0
- urwid/widget/constants.py +574 -0
- urwid/widget/container.py +227 -0
- urwid/widget/divider.py +110 -0
- urwid/widget/edit.py +718 -0
- urwid/widget/filler.py +403 -0
- urwid/widget/frame.py +539 -0
- urwid/widget/grid_flow.py +539 -0
- urwid/widget/line_box.py +194 -0
- urwid/widget/overlay.py +829 -0
- urwid/widget/padding.py +597 -0
- urwid/widget/pile.py +971 -0
- urwid/widget/popup.py +170 -0
- urwid/widget/progress_bar.py +141 -0
- urwid/widget/scrollable.py +597 -0
- urwid/widget/solid_fill.py +44 -0
- urwid/widget/text.py +354 -0
- urwid/widget/widget.py +852 -0
- urwid/widget/widget_decoration.py +166 -0
- urwid/widget/wimp.py +792 -0
- urwid/wimp.py +23 -0
- urwid-2.6.0.post0.dist-info/COPYING +504 -0
- urwid-2.6.0.post0.dist-info/METADATA +332 -0
- urwid-2.6.0.post0.dist-info/RECORD +75 -0
- urwid-2.6.0.post0.dist-info/WHEEL +5 -0
- 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
|