pymud 0.21.0a3__py3-none-any.whl → 0.21.0a4__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.
- pymud/__init__.py +16 -16
- pymud/__main__.py +3 -3
- pymud/dialogs.py +166 -160
- pymud/extras.py +918 -942
- pymud/i18n.py +63 -42
- pymud/lang/i18n_chs.py +219 -204
- pymud/lang/i18n_eng.py +839 -42
- pymud/logger.py +167 -162
- pymud/main.py +207 -206
- pymud/modules.py +432 -431
- pymud/objects.py +1025 -1030
- pymud/pkuxkx.py +280 -262
- pymud/protocol.py +1010 -1008
- pymud/pymud.py +1286 -1292
- pymud/session.py +3419 -3391
- pymud/settings.py +196 -193
- {pymud-0.21.0a3.dist-info → pymud-0.21.0a4.dist-info}/METADATA +404 -369
- pymud-0.21.0a4.dist-info/RECORD +22 -0
- {pymud-0.21.0a3.dist-info → pymud-0.21.0a4.dist-info}/licenses/LICENSE.txt +674 -674
- pymud-0.21.0a3.dist-info/RECORD +0 -22
- {pymud-0.21.0a3.dist-info → pymud-0.21.0a4.dist-info}/WHEEL +0 -0
- {pymud-0.21.0a3.dist-info → pymud-0.21.0a4.dist-info}/entry_points.txt +0 -0
- {pymud-0.21.0a3.dist-info → pymud-0.21.0a4.dist-info}/top_level.txt +0 -0
pymud/extras.py
CHANGED
@@ -1,943 +1,919 @@
|
|
1
|
-
# External Libraries
|
2
|
-
from unicodedata import east_asian_width
|
3
|
-
from wcwidth import wcwidth
|
4
|
-
import time, re, logging
|
5
|
-
|
6
|
-
from typing import Iterable
|
7
|
-
from prompt_toolkit import ANSI
|
8
|
-
from prompt_toolkit.application import get_app
|
9
|
-
from prompt_toolkit.buffer import Buffer
|
10
|
-
from prompt_toolkit.formatted_text import to_formatted_text, fragment_list_to_text
|
11
|
-
from prompt_toolkit.formatted_text.base import OneStyleAndTextTuple
|
12
|
-
from prompt_toolkit.layout.processors import Processor, Transformation
|
13
|
-
from prompt_toolkit.application.current import get_app
|
14
|
-
from prompt_toolkit.buffer import Buffer
|
15
|
-
from prompt_toolkit.document import Document
|
16
|
-
from prompt_toolkit.data_structures import Point
|
17
|
-
from prompt_toolkit.layout.controls import UIContent
|
18
|
-
from prompt_toolkit.lexers import Lexer
|
19
|
-
from prompt_toolkit.mouse_events import MouseButton, MouseEvent, MouseEventType
|
20
|
-
from prompt_toolkit.selection import SelectionType
|
21
|
-
from prompt_toolkit.buffer import Buffer, ValidationState
|
22
|
-
|
23
|
-
from prompt_toolkit.filters import (
|
24
|
-
FilterOrBool,
|
25
|
-
)
|
26
|
-
from prompt_toolkit.formatted_text import (
|
27
|
-
StyleAndTextTuples,
|
28
|
-
to_formatted_text,
|
29
|
-
)
|
30
|
-
from prompt_toolkit.formatted_text.utils import fragment_list_to_text
|
31
|
-
from prompt_toolkit.history import InMemoryHistory
|
32
|
-
from prompt_toolkit.key_binding.key_bindings import KeyBindingsBase
|
33
|
-
from prompt_toolkit.layout.containers import (
|
34
|
-
Window,
|
35
|
-
WindowAlign,
|
36
|
-
)
|
37
|
-
from prompt_toolkit.layout.controls import (
|
38
|
-
BufferControl,
|
39
|
-
FormattedTextControl,
|
40
|
-
)
|
41
|
-
from prompt_toolkit.layout.processors import (
|
42
|
-
Processor,
|
43
|
-
TransformationInput,
|
44
|
-
Transformation
|
45
|
-
)
|
46
|
-
from prompt_toolkit.lexers import Lexer
|
47
|
-
from prompt_toolkit.mouse_events import MouseEvent, MouseEventType
|
48
|
-
from prompt_toolkit.utils import get_cwidth
|
49
|
-
from prompt_toolkit.widgets import Button, MenuContainer, MenuItem
|
50
|
-
from prompt_toolkit.widgets.base import Border
|
51
|
-
|
52
|
-
from prompt_toolkit.layout.screen import _CHAR_CACHE, Screen, WritePosition
|
53
|
-
from prompt_toolkit.layout.utils import explode_text_fragments
|
54
|
-
from prompt_toolkit.formatted_text.utils import (
|
55
|
-
fragment_list_to_text,
|
56
|
-
fragment_list_width,
|
57
|
-
)
|
58
|
-
|
59
|
-
from .settings import Settings
|
60
|
-
|
61
|
-
class MudFormatProcessor(Processor):
|
62
|
-
"在BufferControl中显示ANSI格式的处理器"
|
63
|
-
|
64
|
-
def __init__(self) -> None:
|
65
|
-
super().__init__()
|
66
|
-
self.FULL_BLOCKS = set("▂▃▅▆▇▄█")
|
67
|
-
self.SINGLE_LINES = set("┌└├┬┼┴╭╰─")
|
68
|
-
self.DOUBLE_LINES = set("╔╚╠╦╪╩═")
|
69
|
-
self.ALL_COLOR_REGX = re.compile(r"(?:\[[\d;]+m)+")
|
70
|
-
self.AVAI_COLOR_REGX = re.compile(r"(?:\[[\d;]+m)+(?!$)")
|
71
|
-
self._color_start = ""
|
72
|
-
self._color_correction = False
|
73
|
-
self._color_line_index = 0
|
74
|
-
|
75
|
-
def width_correction(self, line: str) -> str:
|
76
|
-
new_str = []
|
77
|
-
for ch in line:
|
78
|
-
new_str.append(ch)
|
79
|
-
if (east_asian_width(ch) in "FWA") and (wcwidth(ch) == 1):
|
80
|
-
if ch in self.FULL_BLOCKS:
|
81
|
-
new_str.append(ch)
|
82
|
-
elif ch in self.SINGLE_LINES:
|
83
|
-
new_str.append("─")
|
84
|
-
elif ch in self.DOUBLE_LINES:
|
85
|
-
new_str.append("═")
|
86
|
-
else:
|
87
|
-
new_str.append(' ')
|
88
|
-
|
89
|
-
return "".join(new_str)
|
90
|
-
|
91
|
-
def return_correction(self, line: str):
|
92
|
-
return line.replace("\r", "").replace("\x00", "")
|
93
|
-
|
94
|
-
def tab_correction(self, line: str):
|
95
|
-
return line.replace("\t", " " * Settings.client["tabstop"])
|
96
|
-
|
97
|
-
def line_correction(self, line: str):
|
98
|
-
# 处理\r符号(^M)
|
99
|
-
line = self.return_correction(line)
|
100
|
-
# 处理Tab(\r)符号(^I)
|
101
|
-
line = self.tab_correction(line)
|
102
|
-
|
103
|
-
# 美化(解决中文英文在Console中不对齐的问题)
|
104
|
-
if Settings.client["beautify"]:
|
105
|
-
line = self.width_correction(line)
|
106
|
-
|
107
|
-
return line
|
108
|
-
|
109
|
-
def apply_transformation(self, transformation_input: TransformationInput):
|
110
|
-
# 准备(先还原为str)
|
111
|
-
line = fragment_list_to_text(transformation_input.fragments)
|
112
|
-
|
113
|
-
# 颜色校正
|
114
|
-
thislinecolors = len(self.AVAI_COLOR_REGX.findall(line))
|
115
|
-
if thislinecolors == 0:
|
116
|
-
lineno = transformation_input.lineno - 1
|
117
|
-
while lineno > 0:
|
118
|
-
lastline = transformation_input.document.lines[lineno]
|
119
|
-
allcolors = self.ALL_COLOR_REGX.findall(lastline)
|
120
|
-
|
121
|
-
if len(allcolors) == 0:
|
122
|
-
lineno = lineno - 1
|
123
|
-
|
124
|
-
elif len(allcolors) == 1:
|
125
|
-
colors = self.AVAI_COLOR_REGX.findall(lastline)
|
126
|
-
|
127
|
-
if len(colors) == 1:
|
128
|
-
line = f"{colors[0]}{line}"
|
129
|
-
break
|
130
|
-
|
131
|
-
else:
|
132
|
-
break
|
133
|
-
|
134
|
-
else:
|
135
|
-
break
|
136
|
-
|
137
|
-
# 其他校正
|
138
|
-
line = self.line_correction(line)
|
139
|
-
|
140
|
-
# 处理ANSI标记(生成FormmatedText)
|
141
|
-
fragments = to_formatted_text(ANSI(line))
|
142
|
-
|
143
|
-
return Transformation(fragments)
|
144
|
-
|
145
|
-
class SessionBuffer(Buffer):
|
146
|
-
"继承自Buffer,为Session内容所修改,主要修改为只能在最后新增内容,并且支持分屏显示适配"
|
147
|
-
|
148
|
-
def __init__(
|
149
|
-
self,
|
150
|
-
):
|
151
|
-
super().__init__()
|
152
|
-
|
153
|
-
# 修改内容
|
154
|
-
self.__text = ""
|
155
|
-
self.__split = False
|
156
|
-
|
157
|
-
def _set_text(self, value: str) -> bool:
|
158
|
-
"""set text at current working_index. Return whether it changed."""
|
159
|
-
original_value = self.__text
|
160
|
-
self.__text = value
|
161
|
-
|
162
|
-
# Return True when this text has been changed.
|
163
|
-
if len(value) != len(original_value):
|
164
|
-
return True
|
165
|
-
elif value != original_value:
|
166
|
-
return True
|
167
|
-
return False
|
168
|
-
|
169
|
-
@property
|
170
|
-
def text(self) -> str:
|
171
|
-
return self.__text
|
172
|
-
|
173
|
-
@text.setter
|
174
|
-
def text(self, value: str) -> None:
|
175
|
-
# SessionBuffer is only appendable
|
176
|
-
|
177
|
-
if self.cursor_position > len(value):
|
178
|
-
self.cursor_position = len(value)
|
179
|
-
|
180
|
-
changed = self._set_text(value)
|
181
|
-
|
182
|
-
if changed:
|
183
|
-
self._text_changed()
|
184
|
-
self.history_search_text = None
|
185
|
-
|
186
|
-
@property
|
187
|
-
def working_index(self) -> int:
|
188
|
-
return 0
|
189
|
-
|
190
|
-
@working_index.setter
|
191
|
-
def working_index(self, value: int) -> None:
|
192
|
-
pass
|
193
|
-
|
194
|
-
def _text_changed(self) -> None:
|
195
|
-
# Remove any validation errors and complete state.
|
196
|
-
self.validation_error = None
|
197
|
-
self.validation_state = ValidationState.UNKNOWN
|
198
|
-
self.complete_state = None
|
199
|
-
self.yank_nth_arg_state = None
|
200
|
-
self.document_before_paste = None
|
201
|
-
|
202
|
-
# 添加内容时,不取消选择
|
203
|
-
#self.selection_state = None
|
204
|
-
|
205
|
-
self.suggestion = None
|
206
|
-
self.preferred_column = None
|
207
|
-
|
208
|
-
# fire 'on_text_changed' event.
|
209
|
-
self.on_text_changed.fire()
|
210
|
-
|
211
|
-
def set_document(self, value: Document, bypass_readonly: bool = False) -> None:
|
212
|
-
pass
|
213
|
-
|
214
|
-
@property
|
215
|
-
def split(self) -> bool:
|
216
|
-
return self.__split
|
217
|
-
|
218
|
-
@split.setter
|
219
|
-
def split(self, value: bool) -> None:
|
220
|
-
self.__split = value
|
221
|
-
|
222
|
-
@property
|
223
|
-
def is_returnable(self) -> bool:
|
224
|
-
return False
|
225
|
-
|
226
|
-
# End of <getters/setters>
|
227
|
-
|
228
|
-
def save_to_undo_stack(self, clear_redo_stack: bool = True) -> None:
|
229
|
-
|
230
|
-
|
231
|
-
def delete(self, count: int = 1) -> str:
|
232
|
-
|
233
|
-
|
234
|
-
def insert_text(
|
235
|
-
self,
|
236
|
-
data: str,
|
237
|
-
overwrite: bool = False,
|
238
|
-
move_cursor: bool = True,
|
239
|
-
fire_event: bool = True,
|
240
|
-
) -> None:
|
241
|
-
# 始终在最后增加内容
|
242
|
-
self.text += data
|
243
|
-
|
244
|
-
# 分隔情况下,光标保持原位置不变,否则光标始终位于最后
|
245
|
-
if not self.__split:
|
246
|
-
# 若存在选择状态,则视情保留选择
|
247
|
-
if self.selection_state:
|
248
|
-
start = self.selection_state.original_cursor_position
|
249
|
-
end = self.cursor_position
|
250
|
-
row, col = self.document.translate_index_to_position(start)
|
251
|
-
lastrow, col = self.document.translate_index_to_position(len(self.text))
|
252
|
-
self.exit_selection()
|
253
|
-
# 还没翻过半页的话,就重新选择上
|
254
|
-
if lastrow - row < get_app().output.get_size().rows // 2 - 1:
|
255
|
-
self.cursor_position = len(self.text)
|
256
|
-
self.cursor_position = start
|
257
|
-
self.start_selection
|
258
|
-
self.cursor_position = end
|
259
|
-
else:
|
260
|
-
self.cursor_position = len(self.text)
|
261
|
-
else:
|
262
|
-
self.cursor_position = len(self.text)
|
263
|
-
else:
|
264
|
-
pass
|
265
|
-
|
266
|
-
|
267
|
-
def clear_half(self):
|
268
|
-
"将Buffer前半段内容清除,并清除缓存"
|
269
|
-
remain_lines = len(self.document.lines) // 2
|
270
|
-
start = self.document.translate_row_col_to_index(remain_lines, 0)
|
271
|
-
new_text = self.text[start:]
|
272
|
-
|
273
|
-
del self.history
|
274
|
-
self.history = InMemoryHistory()
|
275
|
-
|
276
|
-
self.text = ""
|
277
|
-
self._set_text(new_text)
|
278
|
-
|
279
|
-
self._document_cache.clear()
|
280
|
-
new_doc = Document(text = new_text, cursor_position = len(new_text))
|
281
|
-
self.reset(new_doc, False)
|
282
|
-
self.__split = False
|
283
|
-
|
284
|
-
return new_doc.line_count
|
285
|
-
|
286
|
-
def undo(self) -> None:
|
287
|
-
pass
|
288
|
-
|
289
|
-
def redo(self) -> None:
|
290
|
-
pass
|
291
|
-
|
292
|
-
|
293
|
-
class SessionBufferControl(BufferControl):
|
294
|
-
def __init__(self, buffer: SessionBuffer = None, input_processors = None, include_default_input_processors: bool = True, lexer: Lexer = None, preview_search: FilterOrBool = False, focusable: FilterOrBool = True, search_buffer_control = None, menu_position = None, focus_on_click: FilterOrBool = False, key_bindings: KeyBindingsBase = None):
|
295
|
-
# 将所属Buffer类型更改为SessionBuffer
|
296
|
-
buffer = buffer or SessionBuffer()
|
297
|
-
super().__init__(buffer, input_processors, include_default_input_processors, lexer, preview_search, focusable, search_buffer_control, menu_position, focus_on_click, key_bindings)
|
298
|
-
self.buffer = buffer
|
299
|
-
|
300
|
-
|
301
|
-
def mouse_handler(self, mouse_event: MouseEvent):
|
302
|
-
"""
|
303
|
-
鼠标处理,修改内容包括:
|
304
|
-
1. 在CommandLine获得焦点的时候,鼠标对本Control也可以操作
|
305
|
-
2. 鼠标双击为选中行
|
306
|
-
"""
|
307
|
-
buffer = self.buffer
|
308
|
-
position = mouse_event.position
|
309
|
-
|
310
|
-
# Focus buffer when clicked.
|
311
|
-
cur_control = get_app().layout.current_control
|
312
|
-
cur_buffer = get_app().layout.current_buffer
|
313
|
-
# 这里时修改的内容
|
314
|
-
if (cur_control == self) or (cur_buffer and cur_buffer.name == "input"):
|
315
|
-
if self._last_get_processed_line:
|
316
|
-
processed_line = self._last_get_processed_line(position.y)
|
317
|
-
|
318
|
-
# Translate coordinates back to the cursor position of the
|
319
|
-
# original input.
|
320
|
-
xpos = processed_line.display_to_source(position.x)
|
321
|
-
index = buffer.document.translate_row_col_to_index(position.y, xpos)
|
322
|
-
|
323
|
-
# Set the cursor position.
|
324
|
-
if mouse_event.event_type == MouseEventType.MOUSE_DOWN:
|
325
|
-
buffer.exit_selection()
|
326
|
-
buffer.cursor_position = index
|
327
|
-
|
328
|
-
elif (
|
329
|
-
mouse_event.event_type == MouseEventType.MOUSE_MOVE
|
330
|
-
and mouse_event.button != MouseButton.NONE
|
331
|
-
):
|
332
|
-
# Click and drag to highlight a selection
|
333
|
-
if (
|
334
|
-
buffer.selection_state is None
|
335
|
-
and abs(buffer.cursor_position - index) > 0
|
336
|
-
):
|
337
|
-
buffer.start_selection(selection_type=SelectionType.CHARACTERS)
|
338
|
-
buffer.cursor_position = index
|
339
|
-
|
340
|
-
elif mouse_event.event_type == MouseEventType.MOUSE_UP:
|
341
|
-
# When the cursor was moved to another place, select the text.
|
342
|
-
# (The >1 is actually a small but acceptable workaround for
|
343
|
-
# selecting text in Vi navigation mode. In navigation mode,
|
344
|
-
# the cursor can never be after the text, so the cursor
|
345
|
-
# will be repositioned automatically.)
|
346
|
-
|
347
|
-
if abs(buffer.cursor_position - index) > 1:
|
348
|
-
if buffer.selection_state is None:
|
349
|
-
buffer.start_selection(
|
350
|
-
selection_type=SelectionType.CHARACTERS
|
351
|
-
)
|
352
|
-
buffer.cursor_position = index
|
353
|
-
|
354
|
-
# Select word around cursor on double click.
|
355
|
-
# Two MOUSE_UP events in a short timespan are considered a double click.
|
356
|
-
double_click = (
|
357
|
-
self._last_click_timestamp
|
358
|
-
and time.time() - self._last_click_timestamp < 0.3
|
359
|
-
)
|
360
|
-
self._last_click_timestamp = time.time()
|
361
|
-
|
362
|
-
if double_click:
|
363
|
-
start = buffer.document.translate_row_col_to_index(position.y, 0)
|
364
|
-
end = buffer.document.translate_row_col_to_index(position.y + 1, 0) - 1
|
365
|
-
buffer.cursor_position = start
|
366
|
-
buffer.start_selection(selection_type=SelectionType.LINES)
|
367
|
-
buffer.cursor_position = end
|
368
|
-
|
369
|
-
else:
|
370
|
-
# Don't handle scroll events here.
|
371
|
-
return NotImplemented
|
372
|
-
|
373
|
-
# Not focused, but focusing on click events.
|
374
|
-
else:
|
375
|
-
if (
|
376
|
-
self.focus_on_click()
|
377
|
-
and mouse_event.event_type == MouseEventType.MOUSE_UP
|
378
|
-
):
|
379
|
-
# Focus happens on mouseup. (If we did this on mousedown, the
|
380
|
-
# up event will be received at the point where this widget is
|
381
|
-
# focused and be handled anyway.)
|
382
|
-
get_app().layout.current_control = self
|
383
|
-
else:
|
384
|
-
return NotImplemented
|
385
|
-
|
386
|
-
return None
|
387
|
-
|
388
|
-
def move_cursor_down(self) -> None:
|
389
|
-
b = self.buffer
|
390
|
-
b.cursor_position += b.document.get_cursor_down_position()
|
391
|
-
|
392
|
-
def move_cursor_up(self) -> None:
|
393
|
-
b = self.buffer
|
394
|
-
b.cursor_position += b.document.get_cursor_up_position()
|
395
|
-
|
396
|
-
def move_cursor_right(self, count = 1) -> None:
|
397
|
-
b = self.buffer
|
398
|
-
b.cursor_position += count
|
399
|
-
|
400
|
-
def move_cursor_left(self, count = 1) -> None:
|
401
|
-
b = self.buffer
|
402
|
-
b.cursor_position -= count
|
403
|
-
|
404
|
-
|
405
|
-
class VSplitWindow(Window):
|
406
|
-
"修改的分块窗口,向上翻页时,下半部保持最后数据不变"
|
407
|
-
|
408
|
-
def _copy_body(
|
409
|
-
self,
|
410
|
-
ui_content: UIContent,
|
411
|
-
new_screen: Screen,
|
412
|
-
write_position: WritePosition,
|
413
|
-
move_x: int,
|
414
|
-
width: int,
|
415
|
-
vertical_scroll: int = 0,
|
416
|
-
horizontal_scroll: int = 0,
|
417
|
-
wrap_lines: bool = False,
|
418
|
-
highlight_lines: bool = False,
|
419
|
-
vertical_scroll_2: int = 0,
|
420
|
-
always_hide_cursor: bool = False,
|
421
|
-
has_focus: bool = False,
|
422
|
-
align: WindowAlign = WindowAlign.LEFT,
|
423
|
-
get_line_prefix = None,
|
424
|
-
isNotMargin = True,
|
425
|
-
):
|
426
|
-
"""
|
427
|
-
Copy the UIContent into the output screen.
|
428
|
-
Return (visible_line_to_row_col, rowcol_to_yx) tuple.
|
429
|
-
|
430
|
-
:param get_line_prefix: None or a callable that takes a line number
|
431
|
-
(int) and a wrap_count (int) and returns formatted text.
|
432
|
-
"""
|
433
|
-
xpos = write_position.xpos + move_x
|
434
|
-
ypos = write_position.ypos
|
435
|
-
line_count = ui_content.line_count
|
436
|
-
new_buffer = new_screen.data_buffer
|
437
|
-
empty_char = _CHAR_CACHE["", ""]
|
438
|
-
|
439
|
-
# Map visible line number to (row, col) of input.
|
440
|
-
# 'col' will always be zero if line wrapping is off.
|
441
|
-
visible_line_to_row_col: dict[int, tuple[int, int]] = {}
|
442
|
-
|
443
|
-
# Maps (row, col) from the input to (y, x) screen coordinates.
|
444
|
-
rowcol_to_yx: dict[tuple[int, int], tuple[int, int]] = {}
|
445
|
-
|
446
|
-
def copy_line(
|
447
|
-
line: StyleAndTextTuples,
|
448
|
-
lineno: int,
|
449
|
-
x: int,
|
450
|
-
y: int,
|
451
|
-
is_input: bool = False,
|
452
|
-
):
|
453
|
-
"""
|
454
|
-
Copy over a single line to the output screen. This can wrap over
|
455
|
-
multiple lines in the output. It will call the prefix (prompt)
|
456
|
-
function before every line.
|
457
|
-
"""
|
458
|
-
if is_input:
|
459
|
-
current_rowcol_to_yx = rowcol_to_yx
|
460
|
-
else:
|
461
|
-
current_rowcol_to_yx = {} # Throwaway dictionary.
|
462
|
-
|
463
|
-
# Draw line prefix.
|
464
|
-
if is_input and get_line_prefix:
|
465
|
-
prompt = to_formatted_text(get_line_prefix(lineno, 0))
|
466
|
-
x, y = copy_line(prompt, lineno, x, y, is_input=False)
|
467
|
-
|
468
|
-
# Scroll horizontally.
|
469
|
-
skipped = 0 # Characters skipped because of horizontal scrolling.
|
470
|
-
if horizontal_scroll and is_input:
|
471
|
-
h_scroll = horizontal_scroll
|
472
|
-
line = explode_text_fragments(line)
|
473
|
-
while h_scroll > 0 and line:
|
474
|
-
h_scroll -= get_cwidth(line[0][1])
|
475
|
-
skipped += 1
|
476
|
-
del line[:1] # Remove first character.
|
477
|
-
|
478
|
-
x -= h_scroll # When scrolling over double width character,
|
479
|
-
# this can end up being negative.
|
480
|
-
|
481
|
-
# Align this line. (Note that this doesn't work well when we use
|
482
|
-
# get_line_prefix and that function returns variable width prefixes.)
|
483
|
-
if align == WindowAlign.CENTER:
|
484
|
-
line_width = fragment_list_width(line)
|
485
|
-
if line_width < width:
|
486
|
-
x += (width - line_width) // 2
|
487
|
-
elif align == WindowAlign.RIGHT:
|
488
|
-
line_width = fragment_list_width(line)
|
489
|
-
if line_width < width:
|
490
|
-
x += width - line_width
|
491
|
-
|
492
|
-
col = 0
|
493
|
-
wrap_count = 0
|
494
|
-
for style, text, *_ in line:
|
495
|
-
new_buffer_row = new_buffer[y + ypos]
|
496
|
-
|
497
|
-
# Remember raw VT escape sequences. (E.g. FinalTerm's
|
498
|
-
# escape sequences.)
|
499
|
-
if "[ZeroWidthEscape]" in style:
|
500
|
-
new_screen.zero_width_escapes[y + ypos][x + xpos] += text
|
501
|
-
continue
|
502
|
-
|
503
|
-
for c in text:
|
504
|
-
char = _CHAR_CACHE[c, style]
|
505
|
-
char_width = char.width
|
506
|
-
|
507
|
-
# Wrap when the line width is exceeded.
|
508
|
-
if wrap_lines and x + char_width > width:
|
509
|
-
visible_line_to_row_col[y + 1] = (
|
510
|
-
lineno,
|
511
|
-
visible_line_to_row_col[y][1] + x,
|
512
|
-
)
|
513
|
-
y += 1
|
514
|
-
wrap_count += 1
|
515
|
-
x = 0
|
516
|
-
|
517
|
-
# Insert line prefix (continuation prompt).
|
518
|
-
if is_input and get_line_prefix:
|
519
|
-
prompt = to_formatted_text(
|
520
|
-
get_line_prefix(lineno, wrap_count)
|
521
|
-
)
|
522
|
-
x, y = copy_line(prompt, lineno, x, y, is_input=False)
|
523
|
-
|
524
|
-
new_buffer_row = new_buffer[y + ypos]
|
525
|
-
|
526
|
-
if y >= write_position.height:
|
527
|
-
return x, y # Break out of all for loops.
|
528
|
-
|
529
|
-
# Set character in screen and shift 'x'.
|
530
|
-
if x >= 0 and y >= 0 and x < width:
|
531
|
-
new_buffer_row[x + xpos] = char
|
532
|
-
|
533
|
-
# When we print a multi width character, make sure
|
534
|
-
# to erase the neighbours positions in the screen.
|
535
|
-
# (The empty string if different from everything,
|
536
|
-
# so next redraw this cell will repaint anyway.)
|
537
|
-
if char_width > 1:
|
538
|
-
for i in range(1, char_width):
|
539
|
-
new_buffer_row[x + xpos + i] = empty_char
|
540
|
-
|
541
|
-
# If this is a zero width characters, then it's
|
542
|
-
# probably part of a decomposed unicode character.
|
543
|
-
# See: https://en.wikipedia.org/wiki/Unicode_equivalence
|
544
|
-
# Merge it in the previous cell.
|
545
|
-
elif char_width == 0:
|
546
|
-
# Handle all character widths. If the previous
|
547
|
-
# character is a multiwidth character, then
|
548
|
-
# merge it two positions back.
|
549
|
-
for pw in [2, 1]: # Previous character width.
|
550
|
-
if (
|
551
|
-
x - pw >= 0
|
552
|
-
and new_buffer_row[x + xpos - pw].width == pw
|
553
|
-
):
|
554
|
-
prev_char = new_buffer_row[x + xpos - pw]
|
555
|
-
char2 = _CHAR_CACHE[
|
556
|
-
prev_char.char + c, prev_char.style
|
557
|
-
]
|
558
|
-
new_buffer_row[x + xpos - pw] = char2
|
559
|
-
|
560
|
-
# Keep track of write position for each character.
|
561
|
-
current_rowcol_to_yx[lineno, col + skipped] = (
|
562
|
-
y + ypos,
|
563
|
-
x + xpos,
|
564
|
-
)
|
565
|
-
|
566
|
-
col += 1
|
567
|
-
x += char_width
|
568
|
-
return x, y
|
569
|
-
|
570
|
-
# Copy content.
|
571
|
-
def copy() -> int:
|
572
|
-
y = -vertical_scroll_2
|
573
|
-
lineno = vertical_scroll
|
574
|
-
|
575
|
-
total = write_position.height
|
576
|
-
upper = (total - 1) // 2
|
577
|
-
below = total - upper - 1
|
578
|
-
|
579
|
-
if lineno + total < line_count:
|
580
|
-
if isinstance(self.content, SessionBufferControl):
|
581
|
-
b = self.content.buffer
|
582
|
-
b.split = True
|
583
|
-
|
584
|
-
while y < upper and lineno < line_count:
|
585
|
-
line = ui_content.get_line(lineno)
|
586
|
-
visible_line_to_row_col[y] = (lineno, horizontal_scroll)
|
587
|
-
x = 0
|
588
|
-
x, y = copy_line(line, lineno, x, y, is_input=True)
|
589
|
-
lineno += 1
|
590
|
-
y += 1
|
591
|
-
|
592
|
-
x = 0
|
593
|
-
x, y = copy_line([("","-"*width)], lineno, x, y, is_input=False)
|
594
|
-
y += 1
|
595
|
-
|
596
|
-
lineno = line_count - below
|
597
|
-
while y < total and lineno < line_count:
|
598
|
-
line = ui_content.get_line(lineno)
|
599
|
-
visible_line_to_row_col[y] = (lineno, horizontal_scroll)
|
600
|
-
x = 0
|
601
|
-
x, y = copy_line(line, lineno, x, y, is_input=True)
|
602
|
-
lineno += 1
|
603
|
-
y += 1
|
604
|
-
|
605
|
-
return y
|
606
|
-
|
607
|
-
else:
|
608
|
-
if isNotMargin and isinstance(self.content, SessionBufferControl):
|
609
|
-
b = self.content.buffer
|
610
|
-
b.split = False
|
611
|
-
|
612
|
-
while y < write_position.height and lineno < line_count:
|
613
|
-
# Take the next line and copy it in the real screen.
|
614
|
-
line = ui_content.get_line(lineno)
|
615
|
-
|
616
|
-
visible_line_to_row_col[y] = (lineno, horizontal_scroll)
|
617
|
-
|
618
|
-
# Copy margin and actual line.
|
619
|
-
x = 0
|
620
|
-
x, y = copy_line(line, lineno, x, y, is_input=True)
|
621
|
-
|
622
|
-
lineno += 1
|
623
|
-
y += 1
|
624
|
-
return y
|
625
|
-
|
626
|
-
|
627
|
-
|
628
|
-
copy()
|
629
|
-
|
630
|
-
def cursor_pos_to_screen_pos(row: int, col: int) -> Point:
|
631
|
-
"Translate row/col from UIContent to real Screen coordinates."
|
632
|
-
try:
|
633
|
-
y, x = rowcol_to_yx[row, col]
|
634
|
-
except KeyError:
|
635
|
-
# Normally this should never happen. (It is a bug, if it happens.)
|
636
|
-
# But to be sure, return (0, 0)
|
637
|
-
return Point(x=0, y=0)
|
638
|
-
|
639
|
-
# raise ValueError(
|
640
|
-
# 'Invalid position. row=%r col=%r, vertical_scroll=%r, '
|
641
|
-
# 'horizontal_scroll=%r, height=%r' %
|
642
|
-
# (row, col, vertical_scroll, horizontal_scroll, write_position.height))
|
643
|
-
else:
|
644
|
-
return Point(x=x, y=y)
|
645
|
-
|
646
|
-
# Set cursor and menu positions.
|
647
|
-
if ui_content.cursor_position:
|
648
|
-
screen_cursor_position = cursor_pos_to_screen_pos(
|
649
|
-
ui_content.cursor_position.y, ui_content.cursor_position.x
|
650
|
-
)
|
651
|
-
|
652
|
-
if has_focus:
|
653
|
-
new_screen.set_cursor_position(self, screen_cursor_position)
|
654
|
-
|
655
|
-
if always_hide_cursor:
|
656
|
-
new_screen.show_cursor = False
|
657
|
-
else:
|
658
|
-
new_screen.show_cursor = ui_content.show_cursor
|
659
|
-
|
660
|
-
self._highlight_digraph(new_screen)
|
661
|
-
|
662
|
-
if highlight_lines:
|
663
|
-
self._highlight_cursorlines(
|
664
|
-
new_screen,
|
665
|
-
screen_cursor_position,
|
666
|
-
xpos,
|
667
|
-
ypos,
|
668
|
-
width,
|
669
|
-
write_position.height,
|
670
|
-
)
|
671
|
-
|
672
|
-
# Draw input characters from the input processor queue.
|
673
|
-
if has_focus and ui_content.cursor_position:
|
674
|
-
self._show_key_processor_key_buffer(new_screen)
|
675
|
-
|
676
|
-
# Set menu position.
|
677
|
-
if ui_content.menu_position:
|
678
|
-
new_screen.set_menu_position(
|
679
|
-
self,
|
680
|
-
cursor_pos_to_screen_pos(
|
681
|
-
ui_content.menu_position.y, ui_content.menu_position.x
|
682
|
-
),
|
683
|
-
)
|
684
|
-
|
685
|
-
# Update output screen height.
|
686
|
-
new_screen.height = max(new_screen.height, ypos + write_position.height)
|
687
|
-
|
688
|
-
return visible_line_to_row_col, rowcol_to_yx
|
689
|
-
|
690
|
-
def _copy_margin(
|
691
|
-
self,
|
692
|
-
margin_content: UIContent,
|
693
|
-
new_screen: Screen,
|
694
|
-
write_position: WritePosition,
|
695
|
-
move_x: int,
|
696
|
-
width: int,
|
697
|
-
) -> None:
|
698
|
-
"""
|
699
|
-
Copy characters from the margin screen to the real screen.
|
700
|
-
"""
|
701
|
-
xpos = write_position.xpos + move_x
|
702
|
-
ypos = write_position.ypos
|
703
|
-
|
704
|
-
margin_write_position = WritePosition(xpos, ypos, width, write_position.height)
|
705
|
-
self._copy_body(margin_content, new_screen, margin_write_position, 0, width, isNotMargin=False)
|
706
|
-
|
707
|
-
def _scroll_down(self) -> None:
|
708
|
-
"向下滚屏,处理屏幕分隔"
|
709
|
-
info = self.render_info
|
710
|
-
|
711
|
-
if info is None:
|
712
|
-
return
|
713
|
-
|
714
|
-
if isinstance(self.content, SessionBufferControl):
|
715
|
-
b = self.content.buffer
|
716
|
-
d = b.document
|
717
|
-
|
718
|
-
b.exit_selection()
|
719
|
-
cur_line = d.cursor_position_row
|
720
|
-
|
721
|
-
# # 向下滚动时,如果存在自动折行情况,要判断本行被折成了几行,在行内时要逐行移动(此处未调试好)
|
722
|
-
# cur_col = d.cursor_position_col
|
723
|
-
# line = d.current_line
|
724
|
-
# line_width = len(line)
|
725
|
-
# line_start = d.translate_row_col_to_index(cur_line, 0)
|
726
|
-
# screen_width = info.window_width
|
727
|
-
|
728
|
-
# offset_y = cur_col // screen_width
|
729
|
-
# wraplines = math.ceil(1.0 * line_width / screen_width)
|
730
|
-
|
731
|
-
if cur_line < info.content_height:
|
732
|
-
|
733
|
-
# if offset_y < wraplines: # add
|
734
|
-
# self.content.move_cursor_right(screen_width) # add
|
735
|
-
# else: # add
|
736
|
-
|
737
|
-
self.content.move_cursor_down()
|
738
|
-
self.vertical_scroll = cur_line + 1
|
739
|
-
|
740
|
-
firstline = d.line_count - len(info.displayed_lines)
|
741
|
-
if cur_line >= firstline:
|
742
|
-
b.cursor_position = len(b.text)
|
743
|
-
|
744
|
-
def _scroll_up(self) -> None:
|
745
|
-
"向上滚屏,处理屏幕分隔"
|
746
|
-
info = self.render_info
|
747
|
-
|
748
|
-
if info is None:
|
749
|
-
return
|
750
|
-
|
751
|
-
#if info.cursor_position.y >= 1:
|
752
|
-
if isinstance(self.content, SessionBufferControl):
|
753
|
-
b = self.content.buffer
|
754
|
-
d = b.document
|
755
|
-
|
756
|
-
b.exit_selection()
|
757
|
-
cur_line = d.cursor_position_row
|
758
|
-
if cur_line > d.line_count - len(info.displayed_lines):
|
759
|
-
firstline = d.line_count - len(info.displayed_lines)
|
760
|
-
newpos = d.translate_row_col_to_index(firstline, 0)
|
761
|
-
b.cursor_position = newpos
|
762
|
-
cur_line = d.cursor_position_row
|
763
|
-
self.vertical_scroll = cur_line
|
764
|
-
|
765
|
-
elif cur_line > 0:
|
766
|
-
self.content.move_cursor_up()
|
767
|
-
self.vertical_scroll = cur_line - 1
|
768
|
-
|
769
|
-
|
770
|
-
class EasternButton(Button):
|
771
|
-
"解决增加中文等东亚全宽字符后不对齐问题"
|
772
|
-
|
773
|
-
def _get_text_fragments(self) -> StyleAndTextTuples:
|
774
|
-
# 主要改动在这里
|
775
|
-
width = self.width - (
|
776
|
-
get_cwidth(self.left_symbol) + get_cwidth(self.right_symbol)
|
777
|
-
) - (get_cwidth(self.text) - len(self.text))
|
778
|
-
|
779
|
-
|
780
|
-
text = (f"{{:^{width}}}").format(self.text)
|
781
|
-
|
782
|
-
def handler(mouse_event: MouseEvent) -> None:
|
783
|
-
if (
|
784
|
-
self.handler is not None
|
785
|
-
and mouse_event.event_type == MouseEventType.MOUSE_UP
|
786
|
-
):
|
787
|
-
self.handler()
|
788
|
-
|
789
|
-
return [
|
790
|
-
("class:button.arrow", self.left_symbol, handler),
|
791
|
-
#("[SetCursorPosition]", ""),
|
792
|
-
("class:button.text", text, handler),
|
793
|
-
("class:button.arrow", self.right_symbol, handler),
|
794
|
-
]
|
795
|
-
|
796
|
-
class EasternMenuContainer(MenuContainer):
|
797
|
-
"解决增加中文等东亚全宽字符后不对齐问题"
|
798
|
-
|
799
|
-
def _submenu(self, level: int = 0) -> Window:
|
800
|
-
def get_text_fragments() -> StyleAndTextTuples:
|
801
|
-
result: StyleAndTextTuples = []
|
802
|
-
if level < len(self.selected_menu):
|
803
|
-
menu = self._get_menu(level)
|
804
|
-
if menu.children:
|
805
|
-
result.append(("class:menu", Border.TOP_LEFT))
|
806
|
-
result.append(("class:menu", Border.HORIZONTAL * (menu.width + 4)))
|
807
|
-
result.append(("class:menu", Border.TOP_RIGHT))
|
808
|
-
result.append(("", "\n"))
|
809
|
-
try:
|
810
|
-
selected_item = self.selected_menu[level + 1]
|
811
|
-
except IndexError:
|
812
|
-
selected_item = -1
|
813
|
-
|
814
|
-
def one_item(
|
815
|
-
i: int, item: MenuItem
|
816
|
-
) -> Iterable[OneStyleAndTextTuple]:
|
817
|
-
def mouse_handler(mouse_event: MouseEvent) -> None:
|
818
|
-
if item.disabled:
|
819
|
-
# The arrow keys can't interact with menu items that are disabled.
|
820
|
-
# The mouse shouldn't be able to either.
|
821
|
-
return
|
822
|
-
hover = mouse_event.event_type == MouseEventType.MOUSE_MOVE
|
823
|
-
if (
|
824
|
-
mouse_event.event_type == MouseEventType.MOUSE_UP
|
825
|
-
or hover
|
826
|
-
):
|
827
|
-
app = get_app()
|
828
|
-
if not hover and item.handler:
|
829
|
-
app.layout.focus_last()
|
830
|
-
item.handler()
|
831
|
-
else:
|
832
|
-
self.selected_menu = self.selected_menu[
|
833
|
-
: level + 1
|
834
|
-
] + [i]
|
835
|
-
|
836
|
-
if i == selected_item:
|
837
|
-
yield ("[SetCursorPosition]", "")
|
838
|
-
style = "class:menu-bar.selected-item"
|
839
|
-
else:
|
840
|
-
style = ""
|
841
|
-
|
842
|
-
yield ("class:menu", Border.VERTICAL)
|
843
|
-
if item.text == "-":
|
844
|
-
yield (
|
845
|
-
style + "class:menu-border",
|
846
|
-
f"{Border.HORIZONTAL * (menu.width + 3)}",
|
847
|
-
mouse_handler,
|
848
|
-
)
|
849
|
-
else:
|
850
|
-
# 主要改动在这里,其他地方都未更改.
|
851
|
-
adj_width = menu.width + 3 - (get_cwidth(item.text) - len(item.text))
|
852
|
-
yield (
|
853
|
-
style,
|
854
|
-
f" {item.text}".ljust(adj_width),
|
855
|
-
mouse_handler,
|
856
|
-
)
|
857
|
-
|
858
|
-
if item.children:
|
859
|
-
yield (style, ">", mouse_handler)
|
860
|
-
else:
|
861
|
-
yield (style, " ", mouse_handler)
|
862
|
-
|
863
|
-
if i == selected_item:
|
864
|
-
yield ("[SetMenuPosition]", "")
|
865
|
-
yield ("class:menu", Border.VERTICAL)
|
866
|
-
|
867
|
-
yield ("", "\n")
|
868
|
-
|
869
|
-
for i, item in enumerate(menu.children):
|
870
|
-
result.extend(one_item(i, item))
|
871
|
-
|
872
|
-
result.append(("class:menu", Border.BOTTOM_LEFT))
|
873
|
-
result.append(("class:menu", Border.HORIZONTAL * (menu.width + 4)))
|
874
|
-
result.append(("class:menu", Border.BOTTOM_RIGHT))
|
875
|
-
return result
|
876
|
-
|
877
|
-
return Window(FormattedTextControl(get_text_fragments), style="class:menu")
|
878
|
-
|
879
|
-
|
880
|
-
|
881
|
-
class
|
882
|
-
|
883
|
-
|
884
|
-
|
885
|
-
|
886
|
-
|
887
|
-
|
888
|
-
|
889
|
-
|
890
|
-
|
891
|
-
|
892
|
-
|
893
|
-
|
894
|
-
|
895
|
-
|
896
|
-
|
897
|
-
|
898
|
-
|
899
|
-
|
900
|
-
|
901
|
-
|
902
|
-
|
903
|
-
|
904
|
-
|
905
|
-
|
906
|
-
|
907
|
-
|
908
|
-
|
909
|
-
|
910
|
-
|
911
|
-
|
912
|
-
|
913
|
-
|
914
|
-
|
915
|
-
|
916
|
-
|
917
|
-
|
918
|
-
|
919
|
-
mydict.key1 = "value1"
|
920
|
-
|
921
|
-
# 以下读访问等价
|
922
|
-
val = mydict["key1"]
|
923
|
-
val = mydict.key1
|
924
|
-
"""
|
925
|
-
|
926
|
-
def __getattr__(self, __key):
|
927
|
-
if (not __key in self.__dict__) and (not __key.startswith("__")):
|
928
|
-
return self.__getitem__(__key)
|
929
|
-
|
930
|
-
def __setattr__(self, __name: str, __value):
|
931
|
-
if __name in self.__dict__:
|
932
|
-
object.__setattr__(self, __name, __value)
|
933
|
-
else:
|
934
|
-
self.__setitem__(__name, __value)
|
935
|
-
|
936
|
-
def __getstate__(self):
|
937
|
-
return self
|
938
|
-
|
939
|
-
def __setstate__(self, state):
|
940
|
-
self.update(state)
|
941
|
-
|
942
|
-
|
1
|
+
# External Libraries
|
2
|
+
from unicodedata import east_asian_width
|
3
|
+
from wcwidth import wcwidth
|
4
|
+
import time, re, logging
|
5
|
+
|
6
|
+
from typing import Iterable, Optional, Union, Tuple
|
7
|
+
from prompt_toolkit import ANSI
|
8
|
+
from prompt_toolkit.application import get_app
|
9
|
+
from prompt_toolkit.buffer import Buffer
|
10
|
+
from prompt_toolkit.formatted_text import to_formatted_text, fragment_list_to_text
|
11
|
+
from prompt_toolkit.formatted_text.base import OneStyleAndTextTuple
|
12
|
+
from prompt_toolkit.layout.processors import Processor, Transformation
|
13
|
+
from prompt_toolkit.application.current import get_app
|
14
|
+
from prompt_toolkit.buffer import Buffer
|
15
|
+
from prompt_toolkit.document import Document
|
16
|
+
from prompt_toolkit.data_structures import Point
|
17
|
+
from prompt_toolkit.layout.controls import UIContent
|
18
|
+
from prompt_toolkit.lexers import Lexer
|
19
|
+
from prompt_toolkit.mouse_events import MouseButton, MouseEvent, MouseEventType
|
20
|
+
from prompt_toolkit.selection import SelectionType
|
21
|
+
from prompt_toolkit.buffer import Buffer, ValidationState
|
22
|
+
|
23
|
+
from prompt_toolkit.filters import (
|
24
|
+
FilterOrBool,
|
25
|
+
)
|
26
|
+
from prompt_toolkit.formatted_text import (
|
27
|
+
StyleAndTextTuples,
|
28
|
+
to_formatted_text,
|
29
|
+
)
|
30
|
+
from prompt_toolkit.formatted_text.utils import fragment_list_to_text
|
31
|
+
from prompt_toolkit.history import InMemoryHistory
|
32
|
+
from prompt_toolkit.key_binding.key_bindings import KeyBindingsBase
|
33
|
+
from prompt_toolkit.layout.containers import (
|
34
|
+
Window,
|
35
|
+
WindowAlign,
|
36
|
+
)
|
37
|
+
from prompt_toolkit.layout.controls import (
|
38
|
+
BufferControl,
|
39
|
+
FormattedTextControl,
|
40
|
+
)
|
41
|
+
from prompt_toolkit.layout.processors import (
|
42
|
+
Processor,
|
43
|
+
TransformationInput,
|
44
|
+
Transformation
|
45
|
+
)
|
46
|
+
from prompt_toolkit.lexers import Lexer
|
47
|
+
from prompt_toolkit.mouse_events import MouseEvent, MouseEventType
|
48
|
+
from prompt_toolkit.utils import get_cwidth
|
49
|
+
from prompt_toolkit.widgets import Button, MenuContainer, MenuItem
|
50
|
+
from prompt_toolkit.widgets.base import Border
|
51
|
+
|
52
|
+
from prompt_toolkit.layout.screen import _CHAR_CACHE, Screen, WritePosition
|
53
|
+
from prompt_toolkit.layout.utils import explode_text_fragments
|
54
|
+
from prompt_toolkit.formatted_text.utils import (
|
55
|
+
fragment_list_to_text,
|
56
|
+
fragment_list_width,
|
57
|
+
)
|
58
|
+
|
59
|
+
from .settings import Settings
|
60
|
+
|
61
|
+
class MudFormatProcessor(Processor):
|
62
|
+
"在BufferControl中显示ANSI格式的处理器"
|
63
|
+
|
64
|
+
def __init__(self) -> None:
|
65
|
+
super().__init__()
|
66
|
+
self.FULL_BLOCKS = set("▂▃▅▆▇▄█")
|
67
|
+
self.SINGLE_LINES = set("┌└├┬┼┴╭╰─")
|
68
|
+
self.DOUBLE_LINES = set("╔╚╠╦╪╩═")
|
69
|
+
self.ALL_COLOR_REGX = re.compile(r"(?:\[[\d;]+m)+")
|
70
|
+
self.AVAI_COLOR_REGX = re.compile(r"(?:\[[\d;]+m)+(?!$)")
|
71
|
+
self._color_start = ""
|
72
|
+
self._color_correction = False
|
73
|
+
self._color_line_index = 0
|
74
|
+
|
75
|
+
def width_correction(self, line: str) -> str:
|
76
|
+
new_str = []
|
77
|
+
for ch in line:
|
78
|
+
new_str.append(ch)
|
79
|
+
if (east_asian_width(ch) in "FWA") and (wcwidth(ch) == 1):
|
80
|
+
if ch in self.FULL_BLOCKS:
|
81
|
+
new_str.append(ch)
|
82
|
+
elif ch in self.SINGLE_LINES:
|
83
|
+
new_str.append("─")
|
84
|
+
elif ch in self.DOUBLE_LINES:
|
85
|
+
new_str.append("═")
|
86
|
+
else:
|
87
|
+
new_str.append(' ')
|
88
|
+
|
89
|
+
return "".join(new_str)
|
90
|
+
|
91
|
+
def return_correction(self, line: str):
|
92
|
+
return line.replace("\r", "").replace("\x00", "")
|
93
|
+
|
94
|
+
def tab_correction(self, line: str):
|
95
|
+
return line.replace("\t", " " * Settings.client["tabstop"])
|
96
|
+
|
97
|
+
def line_correction(self, line: str):
|
98
|
+
# 处理\r符号(^M)
|
99
|
+
line = self.return_correction(line)
|
100
|
+
# 处理Tab(\r)符号(^I)
|
101
|
+
line = self.tab_correction(line)
|
102
|
+
|
103
|
+
# 美化(解决中文英文在Console中不对齐的问题)
|
104
|
+
if Settings.client["beautify"]:
|
105
|
+
line = self.width_correction(line)
|
106
|
+
|
107
|
+
return line
|
108
|
+
|
109
|
+
def apply_transformation(self, transformation_input: TransformationInput):
|
110
|
+
# 准备(先还原为str)
|
111
|
+
line = fragment_list_to_text(transformation_input.fragments)
|
112
|
+
|
113
|
+
# 颜色校正
|
114
|
+
thislinecolors = len(self.AVAI_COLOR_REGX.findall(line))
|
115
|
+
if thislinecolors == 0:
|
116
|
+
lineno = transformation_input.lineno - 1
|
117
|
+
while lineno > 0:
|
118
|
+
lastline = transformation_input.document.lines[lineno]
|
119
|
+
allcolors = self.ALL_COLOR_REGX.findall(lastline)
|
120
|
+
|
121
|
+
if len(allcolors) == 0:
|
122
|
+
lineno = lineno - 1
|
123
|
+
|
124
|
+
elif len(allcolors) == 1:
|
125
|
+
colors = self.AVAI_COLOR_REGX.findall(lastline)
|
126
|
+
|
127
|
+
if len(colors) == 1:
|
128
|
+
line = f"{colors[0]}{line}"
|
129
|
+
break
|
130
|
+
|
131
|
+
else:
|
132
|
+
break
|
133
|
+
|
134
|
+
else:
|
135
|
+
break
|
136
|
+
|
137
|
+
# 其他校正
|
138
|
+
line = self.line_correction(line)
|
139
|
+
|
140
|
+
# 处理ANSI标记(生成FormmatedText)
|
141
|
+
fragments = to_formatted_text(ANSI(line))
|
142
|
+
|
143
|
+
return Transformation(fragments)
|
144
|
+
|
145
|
+
class SessionBuffer(Buffer):
|
146
|
+
"继承自Buffer,为Session内容所修改,主要修改为只能在最后新增内容,并且支持分屏显示适配"
|
147
|
+
|
148
|
+
def __init__(
|
149
|
+
self,
|
150
|
+
):
|
151
|
+
super().__init__()
|
152
|
+
|
153
|
+
# 修改内容
|
154
|
+
self.__text = ""
|
155
|
+
self.__split = False
|
156
|
+
|
157
|
+
def _set_text(self, value: str) -> bool:
|
158
|
+
"""set text at current working_index. Return whether it changed."""
|
159
|
+
original_value = self.__text
|
160
|
+
self.__text = value
|
161
|
+
|
162
|
+
# Return True when this text has been changed.
|
163
|
+
if len(value) != len(original_value):
|
164
|
+
return True
|
165
|
+
elif value != original_value:
|
166
|
+
return True
|
167
|
+
return False
|
168
|
+
|
169
|
+
@property
|
170
|
+
def text(self) -> str:
|
171
|
+
return self.__text
|
172
|
+
|
173
|
+
@text.setter
|
174
|
+
def text(self, value: str) -> None:
|
175
|
+
# SessionBuffer is only appendable
|
176
|
+
|
177
|
+
if self.cursor_position > len(value):
|
178
|
+
self.cursor_position = len(value)
|
179
|
+
|
180
|
+
changed = self._set_text(value)
|
181
|
+
|
182
|
+
if changed:
|
183
|
+
self._text_changed()
|
184
|
+
self.history_search_text = None
|
185
|
+
|
186
|
+
@property
|
187
|
+
def working_index(self) -> int:
|
188
|
+
return 0
|
189
|
+
|
190
|
+
@working_index.setter
|
191
|
+
def working_index(self, value: int) -> None:
|
192
|
+
pass
|
193
|
+
|
194
|
+
def _text_changed(self) -> None:
|
195
|
+
# Remove any validation errors and complete state.
|
196
|
+
self.validation_error = None
|
197
|
+
self.validation_state = ValidationState.UNKNOWN
|
198
|
+
self.complete_state = None
|
199
|
+
self.yank_nth_arg_state = None
|
200
|
+
self.document_before_paste = None
|
201
|
+
|
202
|
+
# 添加内容时,不取消选择
|
203
|
+
#self.selection_state = None
|
204
|
+
|
205
|
+
self.suggestion = None
|
206
|
+
self.preferred_column = None
|
207
|
+
|
208
|
+
# fire 'on_text_changed' event.
|
209
|
+
self.on_text_changed.fire()
|
210
|
+
|
211
|
+
def set_document(self, value: Document, bypass_readonly: bool = False) -> None:
|
212
|
+
pass
|
213
|
+
|
214
|
+
@property
|
215
|
+
def split(self) -> bool:
|
216
|
+
return self.__split
|
217
|
+
|
218
|
+
@split.setter
|
219
|
+
def split(self, value: bool) -> None:
|
220
|
+
self.__split = value
|
221
|
+
|
222
|
+
@property
|
223
|
+
def is_returnable(self) -> bool:
|
224
|
+
return False
|
225
|
+
|
226
|
+
# End of <getters/setters>
|
227
|
+
|
228
|
+
# def save_to_undo_stack(self, clear_redo_stack: bool = True) -> None:
|
229
|
+
# pass
|
230
|
+
|
231
|
+
# def delete(self, count: int = 1) -> str:
|
232
|
+
# pass
|
233
|
+
|
234
|
+
def insert_text(
|
235
|
+
self,
|
236
|
+
data: str,
|
237
|
+
overwrite: bool = False,
|
238
|
+
move_cursor: bool = True,
|
239
|
+
fire_event: bool = True,
|
240
|
+
) -> None:
|
241
|
+
# 始终在最后增加内容
|
242
|
+
self.text += data
|
243
|
+
|
244
|
+
# 分隔情况下,光标保持原位置不变,否则光标始终位于最后
|
245
|
+
if not self.__split:
|
246
|
+
# 若存在选择状态,则视情保留选择
|
247
|
+
if self.selection_state:
|
248
|
+
start = self.selection_state.original_cursor_position
|
249
|
+
end = self.cursor_position
|
250
|
+
row, col = self.document.translate_index_to_position(start)
|
251
|
+
lastrow, col = self.document.translate_index_to_position(len(self.text))
|
252
|
+
self.exit_selection()
|
253
|
+
# 还没翻过半页的话,就重新选择上
|
254
|
+
if lastrow - row < get_app().output.get_size().rows // 2 - 1:
|
255
|
+
self.cursor_position = len(self.text)
|
256
|
+
self.cursor_position = start
|
257
|
+
self.start_selection
|
258
|
+
self.cursor_position = end
|
259
|
+
else:
|
260
|
+
self.cursor_position = len(self.text)
|
261
|
+
else:
|
262
|
+
self.cursor_position = len(self.text)
|
263
|
+
else:
|
264
|
+
pass
|
265
|
+
|
266
|
+
|
267
|
+
def clear_half(self):
|
268
|
+
"将Buffer前半段内容清除,并清除缓存"
|
269
|
+
remain_lines = len(self.document.lines) // 2
|
270
|
+
start = self.document.translate_row_col_to_index(remain_lines, 0)
|
271
|
+
new_text = self.text[start:]
|
272
|
+
|
273
|
+
del self.history
|
274
|
+
self.history = InMemoryHistory()
|
275
|
+
|
276
|
+
self.text = ""
|
277
|
+
self._set_text(new_text)
|
278
|
+
|
279
|
+
self._document_cache.clear()
|
280
|
+
new_doc = Document(text = new_text, cursor_position = len(new_text))
|
281
|
+
self.reset(new_doc, False)
|
282
|
+
self.__split = False
|
283
|
+
|
284
|
+
return new_doc.line_count
|
285
|
+
|
286
|
+
def undo(self) -> None:
|
287
|
+
pass
|
288
|
+
|
289
|
+
def redo(self) -> None:
|
290
|
+
pass
|
291
|
+
|
292
|
+
|
293
|
+
class SessionBufferControl(BufferControl):
|
294
|
+
def __init__(self, buffer: Optional[SessionBuffer] = None, input_processors = None, include_default_input_processors: bool = True, lexer: Optional[Lexer] = None, preview_search: FilterOrBool = False, focusable: FilterOrBool = True, search_buffer_control = None, menu_position = None, focus_on_click: FilterOrBool = False, key_bindings: Optional[KeyBindingsBase] = None):
|
295
|
+
# 将所属Buffer类型更改为SessionBuffer
|
296
|
+
buffer = buffer or SessionBuffer()
|
297
|
+
super().__init__(buffer, input_processors, include_default_input_processors, lexer, preview_search, focusable, search_buffer_control, menu_position, focus_on_click, key_bindings)
|
298
|
+
self.buffer = buffer
|
299
|
+
|
300
|
+
|
301
|
+
def mouse_handler(self, mouse_event: MouseEvent):
|
302
|
+
"""
|
303
|
+
鼠标处理,修改内容包括:
|
304
|
+
1. 在CommandLine获得焦点的时候,鼠标对本Control也可以操作
|
305
|
+
2. 鼠标双击为选中行
|
306
|
+
"""
|
307
|
+
buffer = self.buffer
|
308
|
+
position = mouse_event.position
|
309
|
+
|
310
|
+
# Focus buffer when clicked.
|
311
|
+
cur_control = get_app().layout.current_control
|
312
|
+
cur_buffer = get_app().layout.current_buffer
|
313
|
+
# 这里时修改的内容
|
314
|
+
if (cur_control == self) or (cur_buffer and cur_buffer.name == "input"):
|
315
|
+
if self._last_get_processed_line:
|
316
|
+
processed_line = self._last_get_processed_line(position.y)
|
317
|
+
|
318
|
+
# Translate coordinates back to the cursor position of the
|
319
|
+
# original input.
|
320
|
+
xpos = processed_line.display_to_source(position.x)
|
321
|
+
index = buffer.document.translate_row_col_to_index(position.y, xpos)
|
322
|
+
|
323
|
+
# Set the cursor position.
|
324
|
+
if mouse_event.event_type == MouseEventType.MOUSE_DOWN:
|
325
|
+
buffer.exit_selection()
|
326
|
+
buffer.cursor_position = index
|
327
|
+
|
328
|
+
elif (
|
329
|
+
mouse_event.event_type == MouseEventType.MOUSE_MOVE
|
330
|
+
and mouse_event.button != MouseButton.NONE
|
331
|
+
):
|
332
|
+
# Click and drag to highlight a selection
|
333
|
+
if (
|
334
|
+
buffer.selection_state is None
|
335
|
+
and abs(buffer.cursor_position - index) > 0
|
336
|
+
):
|
337
|
+
buffer.start_selection(selection_type=SelectionType.CHARACTERS)
|
338
|
+
buffer.cursor_position = index
|
339
|
+
|
340
|
+
elif mouse_event.event_type == MouseEventType.MOUSE_UP:
|
341
|
+
# When the cursor was moved to another place, select the text.
|
342
|
+
# (The >1 is actually a small but acceptable workaround for
|
343
|
+
# selecting text in Vi navigation mode. In navigation mode,
|
344
|
+
# the cursor can never be after the text, so the cursor
|
345
|
+
# will be repositioned automatically.)
|
346
|
+
|
347
|
+
if abs(buffer.cursor_position - index) > 1:
|
348
|
+
if buffer.selection_state is None:
|
349
|
+
buffer.start_selection(
|
350
|
+
selection_type=SelectionType.CHARACTERS
|
351
|
+
)
|
352
|
+
buffer.cursor_position = index
|
353
|
+
|
354
|
+
# Select word around cursor on double click.
|
355
|
+
# Two MOUSE_UP events in a short timespan are considered a double click.
|
356
|
+
double_click = (
|
357
|
+
self._last_click_timestamp
|
358
|
+
and time.time() - self._last_click_timestamp < 0.3
|
359
|
+
)
|
360
|
+
self._last_click_timestamp = time.time()
|
361
|
+
|
362
|
+
if double_click:
|
363
|
+
start = buffer.document.translate_row_col_to_index(position.y, 0)
|
364
|
+
end = buffer.document.translate_row_col_to_index(position.y + 1, 0) - 1
|
365
|
+
buffer.cursor_position = start
|
366
|
+
buffer.start_selection(selection_type=SelectionType.LINES)
|
367
|
+
buffer.cursor_position = end
|
368
|
+
|
369
|
+
else:
|
370
|
+
# Don't handle scroll events here.
|
371
|
+
return NotImplemented
|
372
|
+
|
373
|
+
# Not focused, but focusing on click events.
|
374
|
+
else:
|
375
|
+
if (
|
376
|
+
self.focus_on_click()
|
377
|
+
and mouse_event.event_type == MouseEventType.MOUSE_UP
|
378
|
+
):
|
379
|
+
# Focus happens on mouseup. (If we did this on mousedown, the
|
380
|
+
# up event will be received at the point where this widget is
|
381
|
+
# focused and be handled anyway.)
|
382
|
+
get_app().layout.current_control = self
|
383
|
+
else:
|
384
|
+
return NotImplemented
|
385
|
+
|
386
|
+
return None
|
387
|
+
|
388
|
+
def move_cursor_down(self) -> None:
|
389
|
+
b = self.buffer
|
390
|
+
b.cursor_position += b.document.get_cursor_down_position()
|
391
|
+
|
392
|
+
def move_cursor_up(self) -> None:
|
393
|
+
b = self.buffer
|
394
|
+
b.cursor_position += b.document.get_cursor_up_position()
|
395
|
+
|
396
|
+
def move_cursor_right(self, count = 1) -> None:
|
397
|
+
b = self.buffer
|
398
|
+
b.cursor_position += count
|
399
|
+
|
400
|
+
def move_cursor_left(self, count = 1) -> None:
|
401
|
+
b = self.buffer
|
402
|
+
b.cursor_position -= count
|
403
|
+
|
404
|
+
|
405
|
+
class VSplitWindow(Window):
|
406
|
+
"修改的分块窗口,向上翻页时,下半部保持最后数据不变"
|
407
|
+
|
408
|
+
def _copy_body(
|
409
|
+
self,
|
410
|
+
ui_content: UIContent,
|
411
|
+
new_screen: Screen,
|
412
|
+
write_position: WritePosition,
|
413
|
+
move_x: int,
|
414
|
+
width: int,
|
415
|
+
vertical_scroll: int = 0,
|
416
|
+
horizontal_scroll: int = 0,
|
417
|
+
wrap_lines: bool = False,
|
418
|
+
highlight_lines: bool = False,
|
419
|
+
vertical_scroll_2: int = 0,
|
420
|
+
always_hide_cursor: bool = False,
|
421
|
+
has_focus: bool = False,
|
422
|
+
align: WindowAlign = WindowAlign.LEFT,
|
423
|
+
get_line_prefix = None,
|
424
|
+
isNotMargin = True,
|
425
|
+
):
|
426
|
+
"""
|
427
|
+
Copy the UIContent into the output screen.
|
428
|
+
Return (visible_line_to_row_col, rowcol_to_yx) tuple.
|
429
|
+
|
430
|
+
:param get_line_prefix: None or a callable that takes a line number
|
431
|
+
(int) and a wrap_count (int) and returns formatted text.
|
432
|
+
"""
|
433
|
+
xpos = write_position.xpos + move_x
|
434
|
+
ypos = write_position.ypos
|
435
|
+
line_count = ui_content.line_count
|
436
|
+
new_buffer = new_screen.data_buffer
|
437
|
+
empty_char = _CHAR_CACHE["", ""]
|
438
|
+
|
439
|
+
# Map visible line number to (row, col) of input.
|
440
|
+
# 'col' will always be zero if line wrapping is off.
|
441
|
+
visible_line_to_row_col: dict[int, tuple[int, int]] = {}
|
442
|
+
|
443
|
+
# Maps (row, col) from the input to (y, x) screen coordinates.
|
444
|
+
rowcol_to_yx: dict[tuple[int, int], tuple[int, int]] = {}
|
445
|
+
|
446
|
+
def copy_line(
|
447
|
+
line: StyleAndTextTuples,
|
448
|
+
lineno: int,
|
449
|
+
x: int,
|
450
|
+
y: int,
|
451
|
+
is_input: bool = False,
|
452
|
+
):
|
453
|
+
"""
|
454
|
+
Copy over a single line to the output screen. This can wrap over
|
455
|
+
multiple lines in the output. It will call the prefix (prompt)
|
456
|
+
function before every line.
|
457
|
+
"""
|
458
|
+
if is_input:
|
459
|
+
current_rowcol_to_yx = rowcol_to_yx
|
460
|
+
else:
|
461
|
+
current_rowcol_to_yx = {} # Throwaway dictionary.
|
462
|
+
|
463
|
+
# Draw line prefix.
|
464
|
+
if is_input and get_line_prefix:
|
465
|
+
prompt = to_formatted_text(get_line_prefix(lineno, 0))
|
466
|
+
x, y = copy_line(prompt, lineno, x, y, is_input=False)
|
467
|
+
|
468
|
+
# Scroll horizontally.
|
469
|
+
skipped = 0 # Characters skipped because of horizontal scrolling.
|
470
|
+
if horizontal_scroll and is_input:
|
471
|
+
h_scroll = horizontal_scroll
|
472
|
+
line = explode_text_fragments(line)
|
473
|
+
while h_scroll > 0 and line:
|
474
|
+
h_scroll -= get_cwidth(line[0][1])
|
475
|
+
skipped += 1
|
476
|
+
del line[:1] # Remove first character.
|
477
|
+
|
478
|
+
x -= h_scroll # When scrolling over double width character,
|
479
|
+
# this can end up being negative.
|
480
|
+
|
481
|
+
# Align this line. (Note that this doesn't work well when we use
|
482
|
+
# get_line_prefix and that function returns variable width prefixes.)
|
483
|
+
if align == WindowAlign.CENTER:
|
484
|
+
line_width = fragment_list_width(line)
|
485
|
+
if line_width < width:
|
486
|
+
x += (width - line_width) // 2
|
487
|
+
elif align == WindowAlign.RIGHT:
|
488
|
+
line_width = fragment_list_width(line)
|
489
|
+
if line_width < width:
|
490
|
+
x += width - line_width
|
491
|
+
|
492
|
+
col = 0
|
493
|
+
wrap_count = 0
|
494
|
+
for style, text, *_ in line:
|
495
|
+
new_buffer_row = new_buffer[y + ypos]
|
496
|
+
|
497
|
+
# Remember raw VT escape sequences. (E.g. FinalTerm's
|
498
|
+
# escape sequences.)
|
499
|
+
if "[ZeroWidthEscape]" in style:
|
500
|
+
new_screen.zero_width_escapes[y + ypos][x + xpos] += text
|
501
|
+
continue
|
502
|
+
|
503
|
+
for c in text:
|
504
|
+
char = _CHAR_CACHE[c, style]
|
505
|
+
char_width = char.width
|
506
|
+
|
507
|
+
# Wrap when the line width is exceeded.
|
508
|
+
if wrap_lines and x + char_width > width:
|
509
|
+
visible_line_to_row_col[y + 1] = (
|
510
|
+
lineno,
|
511
|
+
visible_line_to_row_col[y][1] + x,
|
512
|
+
)
|
513
|
+
y += 1
|
514
|
+
wrap_count += 1
|
515
|
+
x = 0
|
516
|
+
|
517
|
+
# Insert line prefix (continuation prompt).
|
518
|
+
if is_input and get_line_prefix:
|
519
|
+
prompt = to_formatted_text(
|
520
|
+
get_line_prefix(lineno, wrap_count)
|
521
|
+
)
|
522
|
+
x, y = copy_line(prompt, lineno, x, y, is_input=False)
|
523
|
+
|
524
|
+
new_buffer_row = new_buffer[y + ypos]
|
525
|
+
|
526
|
+
if y >= write_position.height:
|
527
|
+
return x, y # Break out of all for loops.
|
528
|
+
|
529
|
+
# Set character in screen and shift 'x'.
|
530
|
+
if x >= 0 and y >= 0 and x < width:
|
531
|
+
new_buffer_row[x + xpos] = char
|
532
|
+
|
533
|
+
# When we print a multi width character, make sure
|
534
|
+
# to erase the neighbours positions in the screen.
|
535
|
+
# (The empty string if different from everything,
|
536
|
+
# so next redraw this cell will repaint anyway.)
|
537
|
+
if char_width > 1:
|
538
|
+
for i in range(1, char_width):
|
539
|
+
new_buffer_row[x + xpos + i] = empty_char
|
540
|
+
|
541
|
+
# If this is a zero width characters, then it's
|
542
|
+
# probably part of a decomposed unicode character.
|
543
|
+
# See: https://en.wikipedia.org/wiki/Unicode_equivalence
|
544
|
+
# Merge it in the previous cell.
|
545
|
+
elif char_width == 0:
|
546
|
+
# Handle all character widths. If the previous
|
547
|
+
# character is a multiwidth character, then
|
548
|
+
# merge it two positions back.
|
549
|
+
for pw in [2, 1]: # Previous character width.
|
550
|
+
if (
|
551
|
+
x - pw >= 0
|
552
|
+
and new_buffer_row[x + xpos - pw].width == pw
|
553
|
+
):
|
554
|
+
prev_char = new_buffer_row[x + xpos - pw]
|
555
|
+
char2 = _CHAR_CACHE[
|
556
|
+
prev_char.char + c, prev_char.style
|
557
|
+
]
|
558
|
+
new_buffer_row[x + xpos - pw] = char2
|
559
|
+
|
560
|
+
# Keep track of write position for each character.
|
561
|
+
current_rowcol_to_yx[lineno, col + skipped] = (
|
562
|
+
y + ypos,
|
563
|
+
x + xpos,
|
564
|
+
)
|
565
|
+
|
566
|
+
col += 1
|
567
|
+
x += char_width
|
568
|
+
return x, y
|
569
|
+
|
570
|
+
# Copy content.
|
571
|
+
def copy() -> int:
|
572
|
+
y = -vertical_scroll_2
|
573
|
+
lineno = vertical_scroll
|
574
|
+
|
575
|
+
total = write_position.height
|
576
|
+
upper = (total - 1) // 2
|
577
|
+
below = total - upper - 1
|
578
|
+
|
579
|
+
if lineno + total < line_count:
|
580
|
+
if isinstance(self.content, SessionBufferControl):
|
581
|
+
b = self.content.buffer
|
582
|
+
b.split = True
|
583
|
+
|
584
|
+
while y < upper and lineno < line_count:
|
585
|
+
line = ui_content.get_line(lineno)
|
586
|
+
visible_line_to_row_col[y] = (lineno, horizontal_scroll)
|
587
|
+
x = 0
|
588
|
+
x, y = copy_line(line, lineno, x, y, is_input=True)
|
589
|
+
lineno += 1
|
590
|
+
y += 1
|
591
|
+
|
592
|
+
x = 0
|
593
|
+
x, y = copy_line([("","-"*width)], lineno, x, y, is_input=False)
|
594
|
+
y += 1
|
595
|
+
|
596
|
+
lineno = line_count - below
|
597
|
+
while y < total and lineno < line_count:
|
598
|
+
line = ui_content.get_line(lineno)
|
599
|
+
visible_line_to_row_col[y] = (lineno, horizontal_scroll)
|
600
|
+
x = 0
|
601
|
+
x, y = copy_line(line, lineno, x, y, is_input=True)
|
602
|
+
lineno += 1
|
603
|
+
y += 1
|
604
|
+
|
605
|
+
return y
|
606
|
+
|
607
|
+
else:
|
608
|
+
if isNotMargin and isinstance(self.content, SessionBufferControl):
|
609
|
+
b = self.content.buffer
|
610
|
+
b.split = False
|
611
|
+
|
612
|
+
while y < write_position.height and lineno < line_count:
|
613
|
+
# Take the next line and copy it in the real screen.
|
614
|
+
line = ui_content.get_line(lineno)
|
615
|
+
|
616
|
+
visible_line_to_row_col[y] = (lineno, horizontal_scroll)
|
617
|
+
|
618
|
+
# Copy margin and actual line.
|
619
|
+
x = 0
|
620
|
+
x, y = copy_line(line, lineno, x, y, is_input=True)
|
621
|
+
|
622
|
+
lineno += 1
|
623
|
+
y += 1
|
624
|
+
return y
|
625
|
+
|
626
|
+
|
627
|
+
|
628
|
+
copy()
|
629
|
+
|
630
|
+
def cursor_pos_to_screen_pos(row: int, col: int) -> Point:
|
631
|
+
"Translate row/col from UIContent to real Screen coordinates."
|
632
|
+
try:
|
633
|
+
y, x = rowcol_to_yx[row, col]
|
634
|
+
except KeyError:
|
635
|
+
# Normally this should never happen. (It is a bug, if it happens.)
|
636
|
+
# But to be sure, return (0, 0)
|
637
|
+
return Point(x=0, y=0)
|
638
|
+
|
639
|
+
# raise ValueError(
|
640
|
+
# 'Invalid position. row=%r col=%r, vertical_scroll=%r, '
|
641
|
+
# 'horizontal_scroll=%r, height=%r' %
|
642
|
+
# (row, col, vertical_scroll, horizontal_scroll, write_position.height))
|
643
|
+
else:
|
644
|
+
return Point(x=x, y=y)
|
645
|
+
|
646
|
+
# Set cursor and menu positions.
|
647
|
+
if ui_content.cursor_position:
|
648
|
+
screen_cursor_position = cursor_pos_to_screen_pos(
|
649
|
+
ui_content.cursor_position.y, ui_content.cursor_position.x
|
650
|
+
)
|
651
|
+
|
652
|
+
if has_focus:
|
653
|
+
new_screen.set_cursor_position(self, screen_cursor_position)
|
654
|
+
|
655
|
+
if always_hide_cursor:
|
656
|
+
new_screen.show_cursor = False
|
657
|
+
else:
|
658
|
+
new_screen.show_cursor = ui_content.show_cursor
|
659
|
+
|
660
|
+
self._highlight_digraph(new_screen)
|
661
|
+
|
662
|
+
if highlight_lines:
|
663
|
+
self._highlight_cursorlines(
|
664
|
+
new_screen,
|
665
|
+
screen_cursor_position,
|
666
|
+
xpos,
|
667
|
+
ypos,
|
668
|
+
width,
|
669
|
+
write_position.height,
|
670
|
+
)
|
671
|
+
|
672
|
+
# Draw input characters from the input processor queue.
|
673
|
+
if has_focus and ui_content.cursor_position:
|
674
|
+
self._show_key_processor_key_buffer(new_screen)
|
675
|
+
|
676
|
+
# Set menu position.
|
677
|
+
if ui_content.menu_position:
|
678
|
+
new_screen.set_menu_position(
|
679
|
+
self,
|
680
|
+
cursor_pos_to_screen_pos(
|
681
|
+
ui_content.menu_position.y, ui_content.menu_position.x
|
682
|
+
),
|
683
|
+
)
|
684
|
+
|
685
|
+
# Update output screen height.
|
686
|
+
new_screen.height = max(new_screen.height, ypos + write_position.height)
|
687
|
+
|
688
|
+
return visible_line_to_row_col, rowcol_to_yx
|
689
|
+
|
690
|
+
def _copy_margin(
|
691
|
+
self,
|
692
|
+
margin_content: UIContent,
|
693
|
+
new_screen: Screen,
|
694
|
+
write_position: WritePosition,
|
695
|
+
move_x: int,
|
696
|
+
width: int,
|
697
|
+
) -> None:
|
698
|
+
"""
|
699
|
+
Copy characters from the margin screen to the real screen.
|
700
|
+
"""
|
701
|
+
xpos = write_position.xpos + move_x
|
702
|
+
ypos = write_position.ypos
|
703
|
+
|
704
|
+
margin_write_position = WritePosition(xpos, ypos, width, write_position.height)
|
705
|
+
self._copy_body(margin_content, new_screen, margin_write_position, 0, width, isNotMargin=False)
|
706
|
+
|
707
|
+
def _scroll_down(self) -> None:
|
708
|
+
"向下滚屏,处理屏幕分隔"
|
709
|
+
info = self.render_info
|
710
|
+
|
711
|
+
if info is None:
|
712
|
+
return
|
713
|
+
|
714
|
+
if isinstance(self.content, SessionBufferControl):
|
715
|
+
b = self.content.buffer
|
716
|
+
d = b.document
|
717
|
+
|
718
|
+
b.exit_selection()
|
719
|
+
cur_line = d.cursor_position_row
|
720
|
+
|
721
|
+
# # 向下滚动时,如果存在自动折行情况,要判断本行被折成了几行,在行内时要逐行移动(此处未调试好)
|
722
|
+
# cur_col = d.cursor_position_col
|
723
|
+
# line = d.current_line
|
724
|
+
# line_width = len(line)
|
725
|
+
# line_start = d.translate_row_col_to_index(cur_line, 0)
|
726
|
+
# screen_width = info.window_width
|
727
|
+
|
728
|
+
# offset_y = cur_col // screen_width
|
729
|
+
# wraplines = math.ceil(1.0 * line_width / screen_width)
|
730
|
+
|
731
|
+
if cur_line < info.content_height:
|
732
|
+
|
733
|
+
# if offset_y < wraplines: # add
|
734
|
+
# self.content.move_cursor_right(screen_width) # add
|
735
|
+
# else: # add
|
736
|
+
|
737
|
+
self.content.move_cursor_down()
|
738
|
+
self.vertical_scroll = cur_line + 1
|
739
|
+
|
740
|
+
firstline = d.line_count - len(info.displayed_lines)
|
741
|
+
if cur_line >= firstline:
|
742
|
+
b.cursor_position = len(b.text)
|
743
|
+
|
744
|
+
def _scroll_up(self) -> None:
|
745
|
+
"向上滚屏,处理屏幕分隔"
|
746
|
+
info = self.render_info
|
747
|
+
|
748
|
+
if info is None:
|
749
|
+
return
|
750
|
+
|
751
|
+
#if info.cursor_position.y >= 1:
|
752
|
+
if isinstance(self.content, SessionBufferControl):
|
753
|
+
b = self.content.buffer
|
754
|
+
d = b.document
|
755
|
+
|
756
|
+
b.exit_selection()
|
757
|
+
cur_line = d.cursor_position_row
|
758
|
+
if cur_line > d.line_count - len(info.displayed_lines):
|
759
|
+
firstline = d.line_count - len(info.displayed_lines)
|
760
|
+
newpos = d.translate_row_col_to_index(firstline, 0)
|
761
|
+
b.cursor_position = newpos
|
762
|
+
cur_line = d.cursor_position_row
|
763
|
+
self.vertical_scroll = cur_line
|
764
|
+
|
765
|
+
elif cur_line > 0:
|
766
|
+
self.content.move_cursor_up()
|
767
|
+
self.vertical_scroll = cur_line - 1
|
768
|
+
|
769
|
+
|
770
|
+
class EasternButton(Button):
|
771
|
+
"解决增加中文等东亚全宽字符后不对齐问题"
|
772
|
+
|
773
|
+
def _get_text_fragments(self) -> StyleAndTextTuples:
|
774
|
+
# 主要改动在这里
|
775
|
+
width = self.width - (
|
776
|
+
get_cwidth(self.left_symbol) + get_cwidth(self.right_symbol)
|
777
|
+
) - (get_cwidth(self.text) - len(self.text))
|
778
|
+
|
779
|
+
|
780
|
+
text = (f"{{:^{width}}}").format(self.text)
|
781
|
+
|
782
|
+
def handler(mouse_event: MouseEvent) -> None:
|
783
|
+
if (
|
784
|
+
self.handler is not None
|
785
|
+
and mouse_event.event_type == MouseEventType.MOUSE_UP
|
786
|
+
):
|
787
|
+
self.handler()
|
788
|
+
|
789
|
+
return [
|
790
|
+
("class:button.arrow", self.left_symbol, handler),
|
791
|
+
#("[SetCursorPosition]", ""),
|
792
|
+
("class:button.text", text, handler),
|
793
|
+
("class:button.arrow", self.right_symbol, handler),
|
794
|
+
]
|
795
|
+
|
796
|
+
class EasternMenuContainer(MenuContainer):
|
797
|
+
"解决增加中文等东亚全宽字符后不对齐问题"
|
798
|
+
|
799
|
+
def _submenu(self, level: int = 0) -> Window:
|
800
|
+
def get_text_fragments() -> StyleAndTextTuples:
|
801
|
+
result: StyleAndTextTuples = []
|
802
|
+
if level < len(self.selected_menu):
|
803
|
+
menu = self._get_menu(level)
|
804
|
+
if menu.children:
|
805
|
+
result.append(("class:menu", Border.TOP_LEFT))
|
806
|
+
result.append(("class:menu", Border.HORIZONTAL * (menu.width + 4)))
|
807
|
+
result.append(("class:menu", Border.TOP_RIGHT))
|
808
|
+
result.append(("", "\n"))
|
809
|
+
try:
|
810
|
+
selected_item = self.selected_menu[level + 1]
|
811
|
+
except IndexError:
|
812
|
+
selected_item = -1
|
813
|
+
|
814
|
+
def one_item(
|
815
|
+
i: int, item: MenuItem
|
816
|
+
) -> Iterable[OneStyleAndTextTuple]:
|
817
|
+
def mouse_handler(mouse_event: MouseEvent) -> None:
|
818
|
+
if item.disabled:
|
819
|
+
# The arrow keys can't interact with menu items that are disabled.
|
820
|
+
# The mouse shouldn't be able to either.
|
821
|
+
return
|
822
|
+
hover = mouse_event.event_type == MouseEventType.MOUSE_MOVE
|
823
|
+
if (
|
824
|
+
mouse_event.event_type == MouseEventType.MOUSE_UP
|
825
|
+
or hover
|
826
|
+
):
|
827
|
+
app = get_app()
|
828
|
+
if not hover and item.handler:
|
829
|
+
app.layout.focus_last()
|
830
|
+
item.handler()
|
831
|
+
else:
|
832
|
+
self.selected_menu = self.selected_menu[
|
833
|
+
: level + 1
|
834
|
+
] + [i]
|
835
|
+
|
836
|
+
if i == selected_item:
|
837
|
+
yield ("[SetCursorPosition]", "")
|
838
|
+
style = "class:menu-bar.selected-item"
|
839
|
+
else:
|
840
|
+
style = ""
|
841
|
+
|
842
|
+
yield ("class:menu", Border.VERTICAL)
|
843
|
+
if item.text == "-":
|
844
|
+
yield (
|
845
|
+
style + "class:menu-border",
|
846
|
+
f"{Border.HORIZONTAL * (menu.width + 3)}",
|
847
|
+
mouse_handler,
|
848
|
+
)
|
849
|
+
else:
|
850
|
+
# 主要改动在这里,其他地方都未更改.
|
851
|
+
adj_width = menu.width + 3 - (get_cwidth(item.text) - len(item.text))
|
852
|
+
yield (
|
853
|
+
style,
|
854
|
+
f" {item.text}".ljust(adj_width),
|
855
|
+
mouse_handler,
|
856
|
+
)
|
857
|
+
|
858
|
+
if item.children:
|
859
|
+
yield (style, ">", mouse_handler)
|
860
|
+
else:
|
861
|
+
yield (style, " ", mouse_handler)
|
862
|
+
|
863
|
+
if i == selected_item:
|
864
|
+
yield ("[SetMenuPosition]", "")
|
865
|
+
yield ("class:menu", Border.VERTICAL)
|
866
|
+
|
867
|
+
yield ("", "\n")
|
868
|
+
|
869
|
+
for i, item in enumerate(menu.children):
|
870
|
+
result.extend(one_item(i, item))
|
871
|
+
|
872
|
+
result.append(("class:menu", Border.BOTTOM_LEFT))
|
873
|
+
result.append(("class:menu", Border.HORIZONTAL * (menu.width + 4)))
|
874
|
+
result.append(("class:menu", Border.BOTTOM_RIGHT))
|
875
|
+
return result
|
876
|
+
|
877
|
+
return Window(FormattedTextControl(get_text_fragments), style="class:menu")
|
878
|
+
|
879
|
+
|
880
|
+
|
881
|
+
class DotDict(dict):
|
882
|
+
"""
|
883
|
+
可以通过点.访问内部key-value对的字典。此类型继承自dict。
|
884
|
+
|
885
|
+
- 由于继承关系,此类型可以使用所有dict可以使用的方法
|
886
|
+
- 额外增加的点.访问方法使用示例如下
|
887
|
+
|
888
|
+
示例:
|
889
|
+
.. code:: Python
|
890
|
+
|
891
|
+
mydict = DotDict()
|
892
|
+
|
893
|
+
# 以下写内容访问等价
|
894
|
+
mydict["key1"] = "value1"
|
895
|
+
mydict.key1 = "value1"
|
896
|
+
|
897
|
+
# 以下读访问等价
|
898
|
+
val = mydict["key1"]
|
899
|
+
val = mydict.key1
|
900
|
+
"""
|
901
|
+
|
902
|
+
def __getattr__(self, __key):
|
903
|
+
if (not __key in self.__dict__) and (not __key.startswith("__")):
|
904
|
+
return self.__getitem__(__key)
|
905
|
+
|
906
|
+
def __setattr__(self, __name: str, __value):
|
907
|
+
if __name in self.__dict__:
|
908
|
+
object.__setattr__(self, __name, __value)
|
909
|
+
else:
|
910
|
+
self.__setitem__(__name, __value)
|
911
|
+
|
912
|
+
def __getstate__(self):
|
913
|
+
return self
|
914
|
+
|
915
|
+
def __setstate__(self, state):
|
916
|
+
self.update(state)
|
917
|
+
|
918
|
+
|
943
919
|
|