euporie 2.3.2__py3-none-any.whl → 2.4.1__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.
- euporie/console/__main__.py +3 -1
- euporie/console/app.py +6 -4
- euporie/console/tabs/console.py +34 -9
- euporie/core/__init__.py +6 -1
- euporie/core/__main__.py +1 -1
- euporie/core/app.py +79 -109
- euporie/core/border.py +44 -14
- euporie/core/comm/base.py +5 -4
- euporie/core/comm/ipywidgets.py +11 -11
- euporie/core/comm/registry.py +12 -6
- euporie/core/commands.py +30 -23
- euporie/core/completion.py +1 -4
- euporie/core/config.py +15 -5
- euporie/core/convert/{base.py → core.py} +117 -53
- euporie/core/convert/formats/ansi.py +46 -25
- euporie/core/convert/formats/base64.py +3 -3
- euporie/core/convert/formats/common.py +38 -13
- euporie/core/convert/formats/formatted_text.py +54 -12
- euporie/core/convert/formats/html.py +5 -5
- euporie/core/convert/formats/jpeg.py +1 -1
- euporie/core/convert/formats/markdown.py +4 -4
- euporie/core/convert/formats/pdf.py +1 -1
- euporie/core/convert/formats/pil.py +5 -3
- euporie/core/convert/formats/png.py +7 -6
- euporie/core/convert/formats/rich.py +4 -3
- euporie/core/convert/formats/sixel.py +5 -5
- euporie/core/convert/utils.py +1 -1
- euporie/core/current.py +11 -5
- euporie/core/formatted_text/ansi.py +4 -8
- euporie/core/formatted_text/html.py +1630 -856
- euporie/core/formatted_text/markdown.py +177 -166
- euporie/core/formatted_text/table.py +20 -14
- euporie/core/formatted_text/utils.py +21 -10
- euporie/core/io.py +14 -14
- euporie/core/kernel.py +48 -37
- euporie/core/key_binding/bindings/micro.py +5 -1
- euporie/core/key_binding/bindings/mouse.py +2 -2
- euporie/core/keys.py +3 -0
- euporie/core/launch.py +5 -2
- euporie/core/lexers.py +13 -2
- euporie/core/log.py +135 -139
- euporie/core/margins.py +32 -14
- euporie/core/path.py +273 -0
- euporie/core/processors.py +35 -0
- euporie/core/renderer.py +21 -5
- euporie/core/style.py +34 -19
- euporie/core/tabs/base.py +101 -17
- euporie/core/tabs/notebook.py +72 -30
- euporie/core/terminal.py +56 -48
- euporie/core/utils.py +12 -16
- euporie/core/widgets/cell.py +6 -5
- euporie/core/widgets/cell_outputs.py +2 -2
- euporie/core/widgets/decor.py +74 -82
- euporie/core/widgets/dialog.py +132 -28
- euporie/core/widgets/display.py +76 -24
- euporie/core/widgets/file_browser.py +87 -31
- euporie/core/widgets/formatted_text_area.py +1 -3
- euporie/core/widgets/forms.py +79 -40
- euporie/core/widgets/inputs.py +23 -13
- euporie/core/widgets/layout.py +4 -3
- euporie/core/widgets/menu.py +368 -216
- euporie/core/widgets/page.py +99 -58
- euporie/core/widgets/pager.py +1 -1
- euporie/core/widgets/palette.py +30 -27
- euporie/core/widgets/search_bar.py +38 -25
- euporie/core/widgets/status_bar.py +103 -5
- euporie/data/desktop/euporie-console.desktop +7 -0
- euporie/data/desktop/euporie-notebook.desktop +7 -0
- euporie/hub/__main__.py +3 -1
- euporie/hub/app.py +9 -7
- euporie/notebook/__main__.py +3 -1
- euporie/notebook/app.py +7 -30
- euporie/notebook/tabs/__init__.py +7 -3
- euporie/notebook/tabs/display.py +18 -9
- euporie/notebook/tabs/edit.py +106 -23
- euporie/notebook/tabs/json.py +73 -0
- euporie/notebook/tabs/log.py +18 -8
- euporie/notebook/tabs/notebook.py +60 -41
- euporie/preview/__main__.py +3 -1
- euporie/preview/app.py +2 -1
- euporie/preview/tabs/notebook.py +23 -10
- euporie/web/tabs/web.py +149 -0
- euporie/web/widgets/webview.py +563 -0
- euporie-2.4.1.data/data/share/applications/euporie-console.desktop +7 -0
- euporie-2.4.1.data/data/share/applications/euporie-notebook.desktop +7 -0
- {euporie-2.3.2.dist-info → euporie-2.4.1.dist-info}/METADATA +6 -5
- euporie-2.4.1.dist-info/RECORD +129 -0
- {euporie-2.3.2.dist-info → euporie-2.4.1.dist-info}/WHEEL +1 -1
- euporie/core/url.py +0 -64
- euporie-2.3.2.dist-info/RECORD +0 -122
- {euporie-2.3.2.dist-info → euporie-2.4.1.dist-info}/entry_points.txt +0 -0
- {euporie-2.3.2.dist-info → euporie-2.4.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,563 @@
|
|
1
|
+
"""Defines a web-view control."""
|
2
|
+
|
3
|
+
from __future__ import annotations
|
4
|
+
|
5
|
+
import logging
|
6
|
+
from typing import TYPE_CHECKING, cast
|
7
|
+
|
8
|
+
from prompt_toolkit.application.current import get_app
|
9
|
+
from prompt_toolkit.cache import FastDictCache
|
10
|
+
from prompt_toolkit.data_structures import Point
|
11
|
+
from prompt_toolkit.eventloop.utils import run_in_executor_with_context
|
12
|
+
from prompt_toolkit.filters import Condition
|
13
|
+
from prompt_toolkit.formatted_text.utils import split_lines
|
14
|
+
from prompt_toolkit.layout.containers import Window
|
15
|
+
from prompt_toolkit.layout.controls import UIContent, UIControl
|
16
|
+
from prompt_toolkit.mouse_events import MouseButton, MouseEvent, MouseEventType
|
17
|
+
from prompt_toolkit.utils import Event
|
18
|
+
from upath import UPath
|
19
|
+
|
20
|
+
from euporie.core.commands import add_cmd
|
21
|
+
from euporie.core.formatted_text.html import HTML, Node
|
22
|
+
from euporie.core.formatted_text.utils import max_line_width, paste
|
23
|
+
from euporie.core.key_binding.registry import (
|
24
|
+
load_registered_bindings,
|
25
|
+
register_bindings,
|
26
|
+
)
|
27
|
+
from euporie.core.path import parse_path
|
28
|
+
|
29
|
+
if TYPE_CHECKING:
|
30
|
+
from pathlib import Path
|
31
|
+
from typing import Any, Callable, Iterable
|
32
|
+
|
33
|
+
from prompt_toolkit.formatted_text.base import StyleAndTextTuples
|
34
|
+
from prompt_toolkit.key_binding.key_bindings import (
|
35
|
+
KeyBindingsBase,
|
36
|
+
NotImplementedOrNone,
|
37
|
+
)
|
38
|
+
from prompt_toolkit.layout.controls import GetLinePrefixCallable
|
39
|
+
from prompt_toolkit.layout.mouse_handlers import MouseHandler
|
40
|
+
|
41
|
+
|
42
|
+
log = logging.getLogger(__name__)
|
43
|
+
|
44
|
+
|
45
|
+
@Condition
|
46
|
+
def webview_has_focus() -> bool:
|
47
|
+
"""Determine if there is a currently focused webview."""
|
48
|
+
return isinstance(get_app().layout.current_control, WebViewControl)
|
49
|
+
|
50
|
+
|
51
|
+
class WebViewControl(UIControl):
|
52
|
+
"""Web view displays.
|
53
|
+
|
54
|
+
A control which displays rendered HTML content.
|
55
|
+
"""
|
56
|
+
|
57
|
+
_window: Window
|
58
|
+
|
59
|
+
def __init__(
|
60
|
+
self,
|
61
|
+
url: str | Path,
|
62
|
+
link_handler: Callable | None = None,
|
63
|
+
) -> None:
|
64
|
+
"""Create a new web-view control instance."""
|
65
|
+
self._cursor_position = Point(0, 0)
|
66
|
+
self.loading = False
|
67
|
+
self.resizing = False
|
68
|
+
self.fragments: StyleAndTextTuples = []
|
69
|
+
self.width = 0
|
70
|
+
self.height = 0
|
71
|
+
self.url: Path | None = None
|
72
|
+
self.status = ""
|
73
|
+
self.link_handler = link_handler or self.load_url
|
74
|
+
|
75
|
+
self.rendered = Event(self)
|
76
|
+
self.on_cursor_position_changed = Event(self)
|
77
|
+
|
78
|
+
self.prev_stack: list[Path] = []
|
79
|
+
self.next_stack: list[Path] = []
|
80
|
+
|
81
|
+
# self.cursor_processor = CursorProcessor(
|
82
|
+
# lambda: self.cursor_position, style="fg:red"
|
83
|
+
# )
|
84
|
+
|
85
|
+
self.key_bindings = load_registered_bindings(
|
86
|
+
"euporie.web.widgets.webview.WebViewControl"
|
87
|
+
)
|
88
|
+
|
89
|
+
self._dom_cache: FastDictCache[tuple[Path], HTML] = FastDictCache(
|
90
|
+
get_value=self.get_dom, size=100
|
91
|
+
)
|
92
|
+
self._fragment_cache: FastDictCache[
|
93
|
+
tuple[HTML, int, int], StyleAndTextTuples
|
94
|
+
] = FastDictCache(get_value=self.get_fragments, size=100_000)
|
95
|
+
self._content_cache: FastDictCache = FastDictCache(self.get_content, size=1_000)
|
96
|
+
|
97
|
+
self.load_url(url)
|
98
|
+
|
99
|
+
# Start a new event loop in a thread
|
100
|
+
self.thread = None
|
101
|
+
|
102
|
+
@property
|
103
|
+
def window(self) -> Window:
|
104
|
+
"""Get the control's window."""
|
105
|
+
try:
|
106
|
+
return self._window
|
107
|
+
except AttributeError:
|
108
|
+
for window in get_app().layout.find_all_windows():
|
109
|
+
if window.content == self:
|
110
|
+
self._window = window
|
111
|
+
return window
|
112
|
+
return Window()
|
113
|
+
|
114
|
+
@property
|
115
|
+
def cursor_position(self) -> Point:
|
116
|
+
"""Get the cursor position."""
|
117
|
+
return self._cursor_position
|
118
|
+
|
119
|
+
@cursor_position.setter
|
120
|
+
def cursor_position(self, value: Point) -> None:
|
121
|
+
"""Set the cursor position."""
|
122
|
+
changed = self._cursor_position != value
|
123
|
+
self._cursor_position = value
|
124
|
+
if changed:
|
125
|
+
self.on_cursor_position_changed.fire()
|
126
|
+
|
127
|
+
def get_dom(self, url: Path) -> HTML:
|
128
|
+
"""Load a HTML page as renderable formatted text."""
|
129
|
+
return HTML(
|
130
|
+
markup=url.read_text(),
|
131
|
+
base=url,
|
132
|
+
mouse_handler=self._node_mouse_handler,
|
133
|
+
paste_fixed=False,
|
134
|
+
)
|
135
|
+
|
136
|
+
@property
|
137
|
+
def title(self) -> str:
|
138
|
+
"""Return the title of the current HTML page."""
|
139
|
+
if url := self.url:
|
140
|
+
dom = self._dom_cache.get((url,))
|
141
|
+
if dom is not None:
|
142
|
+
return dom.title
|
143
|
+
return ""
|
144
|
+
|
145
|
+
def get_fragments(self, dom: HTML, width: int, height: int) -> StyleAndTextTuples:
|
146
|
+
"""Render a HTML page as lines of formatted text."""
|
147
|
+
return dom.render(width, height)
|
148
|
+
|
149
|
+
def load_url(self, url: str | Path, **kwargs: Any) -> None:
|
150
|
+
"""Load a new URL."""
|
151
|
+
save_to_history = kwargs.get("save_to_history", True)
|
152
|
+
# Trigger "loading" view
|
153
|
+
self.loading = True
|
154
|
+
get_app().invalidate()
|
155
|
+
# Update navigation history
|
156
|
+
if self.url and save_to_history:
|
157
|
+
self.prev_stack.append(self.url)
|
158
|
+
self.next_stack.clear()
|
159
|
+
# Update url
|
160
|
+
self.url = UPath(url)
|
161
|
+
# Reset rendering
|
162
|
+
self.rendered.fire()
|
163
|
+
get_app().invalidate()
|
164
|
+
|
165
|
+
def nav_prev(self) -> None:
|
166
|
+
"""Navigate forwards through the browser history."""
|
167
|
+
if self.url and self.prev_stack:
|
168
|
+
self.next_stack.append(self.url)
|
169
|
+
self.load_url(self.prev_stack.pop(), save_to_history=False)
|
170
|
+
|
171
|
+
def nav_next(self) -> None:
|
172
|
+
"""Navigate backwards through the browser history."""
|
173
|
+
if self.url and self.next_stack:
|
174
|
+
self.prev_stack.append(self.url)
|
175
|
+
self.load_url(self.next_stack.pop(), save_to_history=False)
|
176
|
+
|
177
|
+
def render(self) -> None:
|
178
|
+
"""Render the HTML DOM in a thread."""
|
179
|
+
|
180
|
+
def _render() -> None:
|
181
|
+
assert self.url is not None
|
182
|
+
# Potentiall redirect url
|
183
|
+
self.url = parse_path(self.url)
|
184
|
+
dom = self._dom_cache[self.url,]
|
185
|
+
self.fragments = self._fragment_cache[dom, self.width, self.height]
|
186
|
+
self.loading = False
|
187
|
+
self.resizing = False
|
188
|
+
# Scroll to the top
|
189
|
+
self.window.vertical_scroll = 0
|
190
|
+
self.cursor_position = Point(0, 0)
|
191
|
+
self.rendered.fire()
|
192
|
+
get_app().invalidate()
|
193
|
+
|
194
|
+
if self.url:
|
195
|
+
run_in_executor_with_context(_render)
|
196
|
+
|
197
|
+
def reset(self) -> None:
|
198
|
+
"""Reset the state of the control."""
|
199
|
+
|
200
|
+
def preferred_width(self, max_available_width: int) -> int | None:
|
201
|
+
"""Calculate and return the preferred width of the control."""
|
202
|
+
return None
|
203
|
+
|
204
|
+
def preferred_height(
|
205
|
+
self,
|
206
|
+
width: int,
|
207
|
+
max_available_height: int,
|
208
|
+
wrap_lines: bool,
|
209
|
+
get_line_prefix: GetLinePrefixCallable | None,
|
210
|
+
) -> int | None:
|
211
|
+
"""Calculate and return the preferred height of the control."""
|
212
|
+
return None
|
213
|
+
|
214
|
+
def is_focusable(self) -> bool:
|
215
|
+
"""Tell whether this user control is focusable."""
|
216
|
+
return True
|
217
|
+
|
218
|
+
def get_content(
|
219
|
+
self,
|
220
|
+
url: Path,
|
221
|
+
loading: bool,
|
222
|
+
resizing: bool,
|
223
|
+
width: int,
|
224
|
+
# height: int,
|
225
|
+
cursor_position: Point,
|
226
|
+
) -> UIContent:
|
227
|
+
"""Create a cacheable UIContent."""
|
228
|
+
if self.loading:
|
229
|
+
lines = [
|
230
|
+
cast("StyleAndTextTuples", []),
|
231
|
+
cast(
|
232
|
+
"StyleAndTextTuples",
|
233
|
+
[("", " " * ((width - 8) // 2)), ("class:loading", "Loading…")],
|
234
|
+
),
|
235
|
+
]
|
236
|
+
else:
|
237
|
+
lines = list(split_lines(self.fragments))
|
238
|
+
|
239
|
+
def get_line(i: int) -> StyleAndTextTuples:
|
240
|
+
try:
|
241
|
+
line = lines[i]
|
242
|
+
except IndexError:
|
243
|
+
return []
|
244
|
+
|
245
|
+
# Paste fixed elements
|
246
|
+
if (dom := self._dom_cache[url,]).fixed:
|
247
|
+
visible_line = max(0, i - self.window.vertical_scroll)
|
248
|
+
fixed_lines = list(split_lines(dom.fixed_mask))
|
249
|
+
if visible_line < len(fixed_lines):
|
250
|
+
line = paste(
|
251
|
+
fixed_lines[visible_line], line, 0, 0, transparent=True
|
252
|
+
)
|
253
|
+
|
254
|
+
# Apply processors
|
255
|
+
# merged_processor = self.cursor_processor
|
256
|
+
# line = lines[i]
|
257
|
+
# transformation = merged_processor.apply_transformation(
|
258
|
+
# TransformationInput(
|
259
|
+
# buffer_control=self, document=Document(), lineno=i, source_to_display=lambda i: i, fragments=line, width=width, height=height,
|
260
|
+
# )
|
261
|
+
# )
|
262
|
+
# return transformation.fragments
|
263
|
+
|
264
|
+
return line
|
265
|
+
|
266
|
+
return UIContent(
|
267
|
+
get_line=get_line,
|
268
|
+
line_count=len(lines),
|
269
|
+
cursor_position=self.cursor_position,
|
270
|
+
show_cursor=False,
|
271
|
+
)
|
272
|
+
|
273
|
+
def create_content(self, width: int, height: int) -> UIContent:
|
274
|
+
"""Generate the content for this user control.
|
275
|
+
|
276
|
+
Returns:
|
277
|
+
A :class:`.UIContent` instance.
|
278
|
+
"""
|
279
|
+
# Trigger a re-render if things have changed
|
280
|
+
if self.loading:
|
281
|
+
self.render()
|
282
|
+
if width != self.width: # or height != self.height:
|
283
|
+
self.resizing = True
|
284
|
+
self.width = width
|
285
|
+
self.height = height
|
286
|
+
self.render()
|
287
|
+
|
288
|
+
return self._content_cache[
|
289
|
+
self.url,
|
290
|
+
self.loading,
|
291
|
+
self.resizing,
|
292
|
+
width,
|
293
|
+
# height,
|
294
|
+
self.cursor_position,
|
295
|
+
]
|
296
|
+
|
297
|
+
def _node_mouse_handler(
|
298
|
+
self, node: Node, mouse_event: MouseEvent
|
299
|
+
) -> NotImplementedOrNone:
|
300
|
+
"""Handle click events."""
|
301
|
+
if url := node.attrs.get("_link_path"):
|
302
|
+
# TODO - Check for #anchor links and scroll accordingly
|
303
|
+
if (
|
304
|
+
mouse_event.button == MouseButton.LEFT
|
305
|
+
and mouse_event.event_type == MouseEventType.MOUSE_UP
|
306
|
+
):
|
307
|
+
self.link_handler(url, save_to_history=True, new_tab=False)
|
308
|
+
return None
|
309
|
+
elif (
|
310
|
+
mouse_event.button == MouseButton.MIDDLE
|
311
|
+
and mouse_event.event_type == MouseEventType.MOUSE_UP
|
312
|
+
):
|
313
|
+
self.link_handler(url, save_to_history=False, new_tab=True)
|
314
|
+
return None
|
315
|
+
elif mouse_event.event_type == MouseEventType.MOUSE_MOVE:
|
316
|
+
self.status = str(url)
|
317
|
+
return None
|
318
|
+
return NotImplemented
|
319
|
+
|
320
|
+
def mouse_handler(self, mouse_event: MouseEvent) -> NotImplementedOrNone:
|
321
|
+
"""Handle mouse events.
|
322
|
+
|
323
|
+
When `NotImplemented` is returned, it means that the given event is not
|
324
|
+
handled by the `UIControl` itself. The `Window` or key bindings can
|
325
|
+
decide to handle this event as scrolling or changing focus.
|
326
|
+
|
327
|
+
Args:
|
328
|
+
mouse_event: `MouseEvent` instance.
|
329
|
+
|
330
|
+
Returns:
|
331
|
+
NotImplemented if the UI does not need to be updates, None if it does
|
332
|
+
"""
|
333
|
+
# Focus on mouse down
|
334
|
+
if mouse_event.event_type == MouseEventType.MOUSE_DOWN:
|
335
|
+
get_app().layout.focus(self)
|
336
|
+
return None
|
337
|
+
# if mouse_event.event_type == MouseEventType.MOUSE_MOVE:
|
338
|
+
# self.cursor_position = mouse_event.position
|
339
|
+
|
340
|
+
# if mouse_event.event_type == MouseEventType.MOUSE_UP:
|
341
|
+
handler: MouseHandler | None = None
|
342
|
+
try:
|
343
|
+
content = self._content_cache[
|
344
|
+
self.url,
|
345
|
+
self.loading,
|
346
|
+
self.resizing,
|
347
|
+
self.width,
|
348
|
+
# self.height,
|
349
|
+
self.cursor_position,
|
350
|
+
]
|
351
|
+
line = content.get_line(mouse_event.position.y)
|
352
|
+
except IndexError:
|
353
|
+
return NotImplemented
|
354
|
+
else:
|
355
|
+
# Find position in the fragment list.
|
356
|
+
xpos = mouse_event.position.x
|
357
|
+
# Find mouse handler for this character.
|
358
|
+
count = 0
|
359
|
+
for item in line:
|
360
|
+
count += len(item[1])
|
361
|
+
if count > xpos:
|
362
|
+
if len(item) >= 3:
|
363
|
+
handler = item[2]
|
364
|
+
if callable(handler):
|
365
|
+
return handler(mouse_event)
|
366
|
+
else:
|
367
|
+
break
|
368
|
+
|
369
|
+
if callable(handler):
|
370
|
+
return handler
|
371
|
+
|
372
|
+
self.status = ""
|
373
|
+
|
374
|
+
return NotImplemented
|
375
|
+
|
376
|
+
@property
|
377
|
+
def content_width(self) -> int:
|
378
|
+
"""Return the width of the content."""
|
379
|
+
# return max(fragment_list_width(line) for line in self.lines)
|
380
|
+
return max_line_width(self.fragments)
|
381
|
+
|
382
|
+
def move_cursor_down(self) -> None:
|
383
|
+
"""Move the cursor down one line."""
|
384
|
+
x, y = self.cursor_position
|
385
|
+
self.cursor_position = Point(x=x, y=y + 1)
|
386
|
+
|
387
|
+
def move_cursor_up(self) -> None:
|
388
|
+
"""Move the cursor up one line."""
|
389
|
+
x, y = self.cursor_position
|
390
|
+
self.cursor_position = Point(x=x, y=max(0, y - 1))
|
391
|
+
|
392
|
+
def move_cursor_left(self) -> None:
|
393
|
+
"""Move the cursor down one line."""
|
394
|
+
x, y = self.cursor_position
|
395
|
+
self.cursor_position = Point(x=max(0, x - 1), y=y)
|
396
|
+
|
397
|
+
def move_cursor_right(self) -> None:
|
398
|
+
"""Move the cursor up one line."""
|
399
|
+
x, y = self.cursor_position
|
400
|
+
self.cursor_position = Point(x=x + 1, y=y)
|
401
|
+
|
402
|
+
def get_key_bindings(self) -> KeyBindingsBase | None:
|
403
|
+
"""Return key bindings that are specific for this user control.
|
404
|
+
|
405
|
+
Returns:
|
406
|
+
A :class:`.KeyBindings` object if some key bindings are specified, or
|
407
|
+
`None` otherwise.
|
408
|
+
"""
|
409
|
+
return self.key_bindings
|
410
|
+
|
411
|
+
def get_invalidate_events(self) -> Iterable[Event[object]]:
|
412
|
+
"""Return a list of `Event` objects, which can be a generator.
|
413
|
+
|
414
|
+
the application collects all these events, in order to bind redraw
|
415
|
+
handlers to these events.
|
416
|
+
"""
|
417
|
+
yield self.rendered
|
418
|
+
yield self.on_cursor_position_changed
|
419
|
+
|
420
|
+
# ################################### Commands ####################################
|
421
|
+
|
422
|
+
@staticmethod
|
423
|
+
@add_cmd(filter=webview_has_focus)
|
424
|
+
def _webview_nav_prev() -> None:
|
425
|
+
"""Navigate backwards in the browser history."""
|
426
|
+
from euporie.web.widgets.webview import WebViewControl
|
427
|
+
|
428
|
+
current_control = get_app().layout.current_control
|
429
|
+
if isinstance(current_control, WebViewControl):
|
430
|
+
current_control.nav_prev()
|
431
|
+
|
432
|
+
@staticmethod
|
433
|
+
@add_cmd(filter=webview_has_focus)
|
434
|
+
def _webview_nav_next() -> None:
|
435
|
+
"""Navigate forwards in the browser history."""
|
436
|
+
from euporie.web.widgets.webview import WebViewControl
|
437
|
+
|
438
|
+
current_control = get_app().layout.current_control
|
439
|
+
if isinstance(current_control, WebViewControl):
|
440
|
+
current_control.nav_next()
|
441
|
+
|
442
|
+
@staticmethod
|
443
|
+
@add_cmd(filter=webview_has_focus)
|
444
|
+
def _scroll_webview_left() -> None:
|
445
|
+
"""Scroll the display up one line."""
|
446
|
+
from euporie.core.widgets.display import DisplayWindow
|
447
|
+
|
448
|
+
window = get_app().layout.current_window
|
449
|
+
assert isinstance(window, DisplayWindow)
|
450
|
+
window._scroll_left()
|
451
|
+
|
452
|
+
@staticmethod
|
453
|
+
@add_cmd(filter=webview_has_focus)
|
454
|
+
def _scroll_webview_right() -> None:
|
455
|
+
"""Scroll the display down one line."""
|
456
|
+
from euporie.core.widgets.display import DisplayWindow
|
457
|
+
|
458
|
+
window = get_app().layout.current_window
|
459
|
+
assert isinstance(window, DisplayWindow)
|
460
|
+
window._scroll_right()
|
461
|
+
|
462
|
+
@staticmethod
|
463
|
+
@add_cmd(filter=webview_has_focus)
|
464
|
+
def _scroll_webview_up() -> None:
|
465
|
+
"""Scroll the display up one line."""
|
466
|
+
get_app().layout.current_window._scroll_up()
|
467
|
+
|
468
|
+
@staticmethod
|
469
|
+
@add_cmd(filter=webview_has_focus)
|
470
|
+
def _scroll_webview_down() -> None:
|
471
|
+
"""Scroll the display down one line."""
|
472
|
+
get_app().layout.current_window._scroll_down()
|
473
|
+
|
474
|
+
@staticmethod
|
475
|
+
@add_cmd(filter=webview_has_focus)
|
476
|
+
def _page_up_webview() -> None:
|
477
|
+
"""Scroll the display up one page."""
|
478
|
+
window = get_app().layout.current_window
|
479
|
+
if window.render_info is not None:
|
480
|
+
for _ in range(window.render_info.window_height):
|
481
|
+
window._scroll_up()
|
482
|
+
|
483
|
+
@staticmethod
|
484
|
+
@add_cmd(filter=webview_has_focus)
|
485
|
+
def _page_down_webview() -> None:
|
486
|
+
"""Scroll the display down one page."""
|
487
|
+
window = get_app().layout.current_window
|
488
|
+
if window.render_info is not None:
|
489
|
+
for _ in range(window.render_info.window_height):
|
490
|
+
window._scroll_down()
|
491
|
+
|
492
|
+
@staticmethod
|
493
|
+
@add_cmd(filter=webview_has_focus)
|
494
|
+
def _go_to_start_of_webview() -> None:
|
495
|
+
"""Scroll the display to the top."""
|
496
|
+
from euporie.web.widgets.webview import WebViewControl
|
497
|
+
|
498
|
+
current_control = get_app().layout.current_control
|
499
|
+
if isinstance(current_control, WebViewControl):
|
500
|
+
current_control.cursor_position = Point(0, 0)
|
501
|
+
|
502
|
+
@staticmethod
|
503
|
+
@add_cmd(filter=webview_has_focus)
|
504
|
+
def _go_to_end_of_webview() -> None:
|
505
|
+
"""Scroll the display down one page."""
|
506
|
+
from euporie.web.widgets.webview import WebViewControl
|
507
|
+
|
508
|
+
layout = get_app().layout
|
509
|
+
current_control = layout.current_control
|
510
|
+
window = layout.current_window
|
511
|
+
if (
|
512
|
+
isinstance(current_control, WebViewControl)
|
513
|
+
and window.render_info is not None
|
514
|
+
):
|
515
|
+
current_control.cursor_position = Point(
|
516
|
+
0, window.render_info.ui_content.line_count - 1
|
517
|
+
)
|
518
|
+
|
519
|
+
# ################################# Key Bindings ##################################
|
520
|
+
|
521
|
+
register_bindings(
|
522
|
+
{
|
523
|
+
"euporie.web.widgets.webview.WebViewControl": {
|
524
|
+
"scroll-webview-left": "left",
|
525
|
+
"scroll-webview-right": "right",
|
526
|
+
"scroll-webview-up": ["up", "k"],
|
527
|
+
"scroll-webview-down": ["down", "j"],
|
528
|
+
"page-up-webview": "pageup",
|
529
|
+
"page-down-webview": "pagedown",
|
530
|
+
"go-to-start-of-webview": "home",
|
531
|
+
"go-to-end-of-webview": "end",
|
532
|
+
"webview-nav-prev": ("escape", "left"),
|
533
|
+
"webview-nav-next": ("escape", "right"),
|
534
|
+
}
|
535
|
+
}
|
536
|
+
)
|
537
|
+
|
538
|
+
|
539
|
+
if __name__ == "__main__":
|
540
|
+
import sys
|
541
|
+
|
542
|
+
from prompt_toolkit.application.application import Application
|
543
|
+
from prompt_toolkit.key_binding.key_bindings import KeyBindings
|
544
|
+
from prompt_toolkit.layout.layout import Layout
|
545
|
+
from prompt_toolkit.output.color_depth import ColorDepth
|
546
|
+
from prompt_toolkit.styles.style import Style
|
547
|
+
|
548
|
+
from euporie.core.style import HTML_STYLE
|
549
|
+
from euporie.core.widgets.display import DisplayWindow
|
550
|
+
from euporie.web.widgets.webview import WebViewControl # noqa F811
|
551
|
+
|
552
|
+
kb = KeyBindings()
|
553
|
+
kb.add("q")(lambda event: event.app.exit())
|
554
|
+
layout = Layout(container=DisplayWindow(WebViewControl(UPath(sys.argv[-1]))))
|
555
|
+
app: Application = Application(
|
556
|
+
layout=layout,
|
557
|
+
key_bindings=kb,
|
558
|
+
full_screen=True,
|
559
|
+
style=Style(HTML_STYLE),
|
560
|
+
mouse_support=True,
|
561
|
+
color_depth=ColorDepth.DEPTH_24_BIT,
|
562
|
+
)
|
563
|
+
app.run()
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: euporie
|
3
|
-
Version: 2.
|
3
|
+
Version: 2.4.1
|
4
4
|
Summary: Euporie is a suite of terminal applications for interacting with Jupyter kernels
|
5
5
|
Project-URL: Documentation, https://euporie.readthedocs.io/en/latest
|
6
6
|
Project-URL: Issues, https://github.com/joouha/euporie/issues
|
@@ -22,7 +22,6 @@ Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
22
22
|
Classifier: Topic :: Scientific/Engineering
|
23
23
|
Requires-Python: >=3.8
|
24
24
|
Requires-Dist: aenum~=3.1
|
25
|
-
Requires-Dist: appdirs~=1.4
|
26
25
|
Requires-Dist: fastjsonschema~=2.15
|
27
26
|
Requires-Dist: flatlatex~=0.15
|
28
27
|
Requires-Dist: fsspec[http]>=2022.8.0
|
@@ -33,21 +32,23 @@ Requires-Dist: markdown-it-py~=2.1.0
|
|
33
32
|
Requires-Dist: mdit-py-plugins~=0.3.0
|
34
33
|
Requires-Dist: nbformat~=5.0
|
35
34
|
Requires-Dist: pillow~=9.0
|
35
|
+
Requires-Dist: platformdirs~=3.5
|
36
36
|
Requires-Dist: prompt-toolkit~=3.0.36
|
37
37
|
Requires-Dist: pygments~=2.11
|
38
38
|
Requires-Dist: pyperclip~=1.8
|
39
39
|
Requires-Dist: sixelcrop~=0.1.3
|
40
40
|
Requires-Dist: timg~=1.1
|
41
|
-
Requires-Dist: typing-extensions~=4.
|
42
|
-
Requires-Dist: universal-pathlib~=0.0.
|
41
|
+
Requires-Dist: typing-extensions~=4.5
|
42
|
+
Requires-Dist: universal-pathlib~=0.0.23
|
43
43
|
Provides-Extra: chafa
|
44
44
|
Requires-Dist: chafa-py>=1.0.2; extra == 'chafa'
|
45
45
|
Provides-Extra: format
|
46
46
|
Requires-Dist: black>=19.3.b0; extra == 'format'
|
47
47
|
Requires-Dist: isort~=5.10.1; extra == 'format'
|
48
|
-
Requires-Dist: ssort~=0.11.6; extra == 'format'
|
49
48
|
Provides-Extra: hub
|
50
49
|
Requires-Dist: asyncssh~=2.10.1; extra == 'hub'
|
50
|
+
Provides-Extra: jupytext
|
51
|
+
Requires-Dist: jupytext>=1.14.0; extra == 'jupytext'
|
51
52
|
Description-Content-Type: text/x-rst
|
52
53
|
|
53
54
|
|logo|
|