pymud 0.21.0a5__py3-none-any.whl → 0.21.2a1__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/extras.py +461 -449
- pymud/lang/i18n_chs.py +3 -0
- pymud/lang/i18n_eng.py +7 -3
- pymud/pymud.py +44 -50
- pymud/session.py +180 -44
- pymud/settings.py +1 -1
- {pymud-0.21.0a5.dist-info → pymud-0.21.2a1.dist-info}/METADATA +71 -27
- {pymud-0.21.0a5.dist-info → pymud-0.21.2a1.dist-info}/RECORD +12 -12
- {pymud-0.21.0a5.dist-info → pymud-0.21.2a1.dist-info}/WHEEL +1 -1
- {pymud-0.21.0a5.dist-info → pymud-0.21.2a1.dist-info}/entry_points.txt +0 -0
- {pymud-0.21.0a5.dist-info → pymud-0.21.2a1.dist-info}/licenses/LICENSE.txt +0 -0
- {pymud-0.21.0a5.dist-info → pymud-0.21.2a1.dist-info}/top_level.txt +0 -0
pymud/extras.py
CHANGED
@@ -1,24 +1,27 @@
|
|
1
1
|
# External Libraries
|
2
2
|
from unicodedata import east_asian_width
|
3
3
|
from wcwidth import wcwidth
|
4
|
+
from dataclasses import dataclass
|
4
5
|
import time, re, logging
|
5
6
|
|
6
|
-
from typing import Iterable, Optional, Union, Tuple
|
7
|
+
from typing import Iterable, NamedTuple, Optional, Union, Tuple
|
7
8
|
from prompt_toolkit import ANSI
|
8
9
|
from prompt_toolkit.application import get_app
|
9
10
|
from prompt_toolkit.buffer import Buffer
|
10
11
|
from prompt_toolkit.formatted_text import to_formatted_text, fragment_list_to_text
|
11
12
|
from prompt_toolkit.formatted_text.base import OneStyleAndTextTuple
|
13
|
+
from prompt_toolkit.layout.controls import UIContent, UIControl
|
12
14
|
from prompt_toolkit.layout.processors import Processor, Transformation
|
13
15
|
from prompt_toolkit.application.current import get_app
|
14
16
|
from prompt_toolkit.buffer import Buffer
|
15
17
|
from prompt_toolkit.document import Document
|
16
18
|
from prompt_toolkit.data_structures import Point
|
17
|
-
from prompt_toolkit.layout.controls import UIContent
|
19
|
+
from prompt_toolkit.layout.controls import UIContent, FormattedTextControl
|
18
20
|
from prompt_toolkit.lexers import Lexer
|
19
21
|
from prompt_toolkit.mouse_events import MouseButton, MouseEvent, MouseEventType
|
20
22
|
from prompt_toolkit.selection import SelectionType
|
21
23
|
from prompt_toolkit.buffer import Buffer, ValidationState
|
24
|
+
from prompt_toolkit.utils import Event
|
22
25
|
|
23
26
|
from prompt_toolkit.filters import (
|
24
27
|
FilterOrBool,
|
@@ -35,7 +38,7 @@ from prompt_toolkit.layout.containers import (
|
|
35
38
|
WindowAlign,
|
36
39
|
)
|
37
40
|
from prompt_toolkit.layout.controls import (
|
38
|
-
|
41
|
+
|
39
42
|
FormattedTextControl,
|
40
43
|
)
|
41
44
|
from prompt_toolkit.layout.processors import (
|
@@ -58,350 +61,6 @@ from prompt_toolkit.formatted_text.utils import (
|
|
58
61
|
|
59
62
|
from .settings import Settings
|
60
63
|
|
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
64
|
class VSplitWindow(Window):
|
406
65
|
"修改的分块窗口,向上翻页时,下半部保持最后数据不变"
|
407
66
|
|
@@ -576,54 +235,78 @@ class VSplitWindow(Window):
|
|
576
235
|
upper = (total - 1) // 2
|
577
236
|
below = total - upper - 1
|
578
237
|
|
579
|
-
if
|
580
|
-
|
581
|
-
|
582
|
-
|
583
|
-
|
584
|
-
|
585
|
-
|
586
|
-
|
587
|
-
|
588
|
-
|
589
|
-
|
590
|
-
|
591
|
-
|
592
|
-
|
593
|
-
|
594
|
-
|
595
|
-
|
596
|
-
|
597
|
-
|
598
|
-
|
599
|
-
|
600
|
-
|
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
|
238
|
+
#if isNotMargin:
|
239
|
+
if isinstance(self.content, SessionBufferControl):
|
240
|
+
b = self.content.buffer
|
241
|
+
if not b:
|
242
|
+
return y
|
243
|
+
|
244
|
+
line_count = b.lineCount
|
245
|
+
start_lineno = b.start_lineno
|
246
|
+
if start_lineno < 0:
|
247
|
+
# no split window
|
248
|
+
if line_count < total:
|
249
|
+
# 内容行数小于屏幕行数
|
250
|
+
lineno = 0
|
251
|
+
|
252
|
+
while y < total and lineno < line_count:
|
253
|
+
# Take the next line and copy it in the real screen.
|
254
|
+
line = ui_content.get_line(lineno)
|
255
|
+
visible_line_to_row_col[y] = (lineno, horizontal_scroll)
|
256
|
+
x = 0
|
257
|
+
x, y = copy_line(line, lineno, x, y, is_input=True)
|
258
|
+
lineno += 1
|
259
|
+
y += 1
|
611
260
|
|
612
|
-
|
613
|
-
|
614
|
-
line = ui_content.get_line(lineno)
|
261
|
+
else:
|
262
|
+
# 若内容行数大于屏幕行数,则倒序复制,确保即使有自动折行时,最后一行也保持在屏幕最底部
|
615
263
|
|
616
|
-
|
264
|
+
y = total
|
265
|
+
lineno = line_count
|
617
266
|
|
618
|
-
|
619
|
-
|
620
|
-
|
267
|
+
while y >= 0 and lineno >= 0:
|
268
|
+
lineno -= 1
|
269
|
+
# Take the next line and copy it in the real screen.
|
270
|
+
display_lines = ui_content.get_height_for_line(lineno, width, None)
|
271
|
+
y -= display_lines
|
272
|
+
line = ui_content.get_line(lineno)
|
273
|
+
visible_line_to_row_col[y] = (lineno, horizontal_scroll)
|
274
|
+
copy_line(line, lineno, 0, y, is_input=True)
|
621
275
|
|
622
|
-
|
623
|
-
|
624
|
-
|
625
|
-
|
626
|
-
|
276
|
+
|
277
|
+
else:
|
278
|
+
# 有split window
|
279
|
+
# 复制上半部分,正序复制,确保即使有自动折行时,第一行也保持在屏幕最顶部
|
280
|
+
lineno = start_lineno
|
281
|
+
while y < upper and lineno < line_count:
|
282
|
+
line = ui_content.get_line(lineno)
|
283
|
+
visible_line_to_row_col[y] = (lineno, horizontal_scroll)
|
284
|
+
x = 0
|
285
|
+
x, y = copy_line(line, lineno, x, y, is_input=True)
|
286
|
+
lineno += 1
|
287
|
+
y += 1
|
288
|
+
|
289
|
+
# x = 0
|
290
|
+
# x, y = copy_line([("","-"*width)], lineno, x, y, is_input=False)
|
291
|
+
# y += 1
|
292
|
+
|
293
|
+
# 复制下半部分,倒序复制,确保即使有自动折行时,最后一行也保持在屏幕最底部
|
294
|
+
y = total
|
295
|
+
lineno = line_count
|
296
|
+
|
297
|
+
while y >= below and lineno >= 0:
|
298
|
+
lineno -= 1
|
299
|
+
# Take the next line and copy it in the real screen.
|
300
|
+
display_lines = ui_content.get_height_for_line(lineno, width, None)
|
301
|
+
y -= display_lines
|
302
|
+
line = ui_content.get_line(lineno)
|
303
|
+
visible_line_to_row_col[y] = (lineno, horizontal_scroll)
|
304
|
+
copy_line(line, lineno, 0, y, is_input=True)
|
305
|
+
|
306
|
+
# 最后复制分割线,若上下有由于折行额外占用的内容,都用分割线给覆盖掉
|
307
|
+
copy_line([("","-"*width)], -1, 0, upper + 1, is_input=False)
|
308
|
+
|
309
|
+
return y
|
627
310
|
|
628
311
|
copy()
|
629
312
|
|
@@ -687,23 +370,6 @@ class VSplitWindow(Window):
|
|
687
370
|
|
688
371
|
return visible_line_to_row_col, rowcol_to_yx
|
689
372
|
|
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
373
|
def _scroll_down(self) -> None:
|
708
374
|
"向下滚屏,处理屏幕分隔"
|
709
375
|
info = self.render_info
|
@@ -713,33 +379,13 @@ class VSplitWindow(Window):
|
|
713
379
|
|
714
380
|
if isinstance(self.content, SessionBufferControl):
|
715
381
|
b = self.content.buffer
|
716
|
-
|
717
|
-
|
718
|
-
b.
|
719
|
-
|
720
|
-
|
721
|
-
|
722
|
-
|
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)
|
382
|
+
if not b:
|
383
|
+
return
|
384
|
+
start_lineno = b.start_lineno
|
385
|
+
if (start_lineno >= 0) and (start_lineno < b.lineCount - len(info.displayed_lines)):
|
386
|
+
b.start_lineno = b.start_lineno + 1
|
387
|
+
else:
|
388
|
+
b.start_lineno = -1
|
743
389
|
|
744
390
|
def _scroll_up(self) -> None:
|
745
391
|
"向上滚屏,处理屏幕分隔"
|
@@ -748,23 +394,19 @@ class VSplitWindow(Window):
|
|
748
394
|
if info is None:
|
749
395
|
return
|
750
396
|
|
751
|
-
#if info.cursor_position.y >= 1:
|
752
397
|
if isinstance(self.content, SessionBufferControl):
|
753
398
|
b = self.content.buffer
|
754
|
-
|
755
|
-
|
756
|
-
b.
|
757
|
-
|
758
|
-
|
759
|
-
|
760
|
-
|
761
|
-
b.
|
762
|
-
cur_line = d.cursor_position_row
|
763
|
-
self.vertical_scroll = cur_line
|
399
|
+
if not b:
|
400
|
+
return
|
401
|
+
start_lineno = b.start_lineno
|
402
|
+
if start_lineno > 0:
|
403
|
+
b.start_lineno = b.start_lineno - 1
|
404
|
+
|
405
|
+
elif start_lineno == 0:
|
406
|
+
b.start_lineno = 0
|
764
407
|
|
765
|
-
|
766
|
-
|
767
|
-
self.vertical_scroll = cur_line - 1
|
408
|
+
else:
|
409
|
+
b.start_lineno = b.lineCount - len(info.displayed_lines) - 1
|
768
410
|
|
769
411
|
|
770
412
|
class EasternButton(Button):
|
@@ -876,7 +518,377 @@ class EasternMenuContainer(MenuContainer):
|
|
876
518
|
|
877
519
|
return Window(FormattedTextControl(get_text_fragments), style="class:menu")
|
878
520
|
|
521
|
+
@dataclass
|
522
|
+
class SessionSelectionState:
|
523
|
+
start_row: int = -1
|
524
|
+
end_row: int = -1
|
525
|
+
start_col: int = -1
|
526
|
+
end_col: int = -1
|
527
|
+
def is_valid(self):
|
528
|
+
if self.start_row >= 0 and self.end_row >= 0 and self.start_col >= 0 and self.end_col >= 0:
|
529
|
+
if (self.start_row == self.end_row) and (self.start_col == self.end_col):
|
530
|
+
return False
|
531
|
+
elif self.start_row > self.end_row:
|
532
|
+
srow, scol = self.end_row, self.end_col
|
533
|
+
erow, ecol = self.start_row, self.start_col
|
534
|
+
self.start_row, self.end_row = srow, erow
|
535
|
+
self.start_col, self.end_col = scol, ecol
|
536
|
+
elif self.start_row == self.end_row and self.start_col > self.end_col:
|
537
|
+
scol, ecol = self.end_col, self.start_col
|
538
|
+
self.start_col, self.end_col = scol, ecol
|
539
|
+
|
540
|
+
return True
|
541
|
+
|
542
|
+
return False
|
543
|
+
|
544
|
+
@property
|
545
|
+
def rows(self):
|
546
|
+
if self.is_valid():
|
547
|
+
return self.end_row - self.start_row + 1
|
548
|
+
else:
|
549
|
+
return 0
|
550
|
+
|
551
|
+
|
552
|
+
class SessionBuffer:
|
553
|
+
def __init__(
|
554
|
+
self,
|
555
|
+
name,
|
556
|
+
newline = "\n",
|
557
|
+
max_buffered_lines = 10000,
|
558
|
+
) -> None:
|
559
|
+
|
560
|
+
self.name = name
|
561
|
+
self.lines = []
|
562
|
+
self.newline = newline
|
563
|
+
self.isnewline = True
|
564
|
+
self.max_buffered_lines = max_buffered_lines
|
565
|
+
self.selection = SessionSelectionState(-1, -1, -1, -1)
|
566
|
+
self.start_lineno = -1
|
567
|
+
|
568
|
+
def append(self, line: str):
|
569
|
+
"""
|
570
|
+
追加文本到缓冲区。
|
571
|
+
当文本以换行符结尾时,会自动添加到缓冲区。
|
572
|
+
当文本不以换行符结尾时,会自动添加到上一行。
|
573
|
+
"""
|
574
|
+
newline_after_append = False
|
575
|
+
if line.endswith(self.newline):
|
576
|
+
line = line.rstrip(self.newline)
|
577
|
+
newline_after_append = True
|
578
|
+
if not self.newline in line:
|
579
|
+
if self.isnewline:
|
580
|
+
self.lines.append(line)
|
581
|
+
else:
|
582
|
+
self.lines[-1] += line
|
583
|
+
|
584
|
+
else:
|
585
|
+
lines = line.split(self.newline)
|
586
|
+
if self.isnewline:
|
587
|
+
self.lines.extend(lines)
|
588
|
+
else:
|
589
|
+
self.lines[-1] += lines[0]
|
590
|
+
self.lines.extend(lines[1:])
|
591
|
+
|
592
|
+
self.isnewline = newline_after_append
|
593
|
+
|
594
|
+
## limit buffered lines
|
595
|
+
if len(self.lines) > self.max_buffered_lines:
|
596
|
+
diff = self.max_buffered_lines - len(self.lines)
|
597
|
+
del self.lines[:diff]
|
598
|
+
## adjust selection
|
599
|
+
if self.selection.start_row >= 0:
|
600
|
+
self.selection.start_row -= diff
|
601
|
+
self.selection.end_row -= diff
|
602
|
+
|
603
|
+
get_app().invalidate()
|
604
|
+
|
605
|
+
def clear(self):
|
606
|
+
self.lines.clear()
|
607
|
+
self.selection = SessionSelectionState(-1, -1, -1, -1)
|
608
|
+
|
609
|
+
get_app().invalidate()
|
879
610
|
|
611
|
+
def loadfile(self, filename, encoding = 'utf-8', errors = 'ignore'):
|
612
|
+
with open(filename, 'r', encoding = encoding, errors = errors) as fp:
|
613
|
+
lines = fp.readlines()
|
614
|
+
self.clear()
|
615
|
+
self.lines.extend(lines)
|
616
|
+
|
617
|
+
get_app().invalidate()
|
618
|
+
|
619
|
+
@property
|
620
|
+
def lineCount(self):
|
621
|
+
return len(self.lines)
|
622
|
+
|
623
|
+
def getLine(self, lineno):
|
624
|
+
if lineno < 0 or lineno >= len(self.lines):
|
625
|
+
return ""
|
626
|
+
return self.lines[lineno]
|
627
|
+
|
628
|
+
# 获取指定某行到某行的内容。当start未设置时,从首行开始。当end未设置时,到最后一行结束。
|
629
|
+
# 注意判断首位顺序逻辑,以及给定参数是否越界
|
630
|
+
def getLines(self, start = None, end = None):
|
631
|
+
if start is None:
|
632
|
+
start = 0
|
633
|
+
if end is None:
|
634
|
+
end = len(self.lines) - 1
|
635
|
+
if start < 0:
|
636
|
+
start = 0
|
637
|
+
if end >= len(self.lines):
|
638
|
+
end = len(self.lines) - 1
|
639
|
+
if start > end:
|
640
|
+
return []
|
641
|
+
return self.lines[start:end+1]
|
642
|
+
|
643
|
+
def selection_range_at_line(self, lineno):
|
644
|
+
if self.selection.is_valid():
|
645
|
+
if self.selection.rows > 1:
|
646
|
+
if lineno == self.selection.start_row:
|
647
|
+
return (self.selection.start_col, len(self.lines[lineno]) - 1)
|
648
|
+
elif lineno == self.selection.end_row:
|
649
|
+
return (0, self.selection.end_col)
|
650
|
+
elif lineno > self.selection.start_row and lineno < self.selection.end_row:
|
651
|
+
return (0, len(self.lines[lineno]) - 1)
|
652
|
+
|
653
|
+
elif self.selection.rows == 1:
|
654
|
+
if lineno == self.selection.start_row:
|
655
|
+
return (self.selection.start_col, self.selection.end_col)
|
656
|
+
|
657
|
+
return None
|
658
|
+
|
659
|
+
def exit_selection(self):
|
660
|
+
self.selection = SessionSelectionState(-1, -1, -1, -1)
|
661
|
+
|
662
|
+
def nosplit(self):
|
663
|
+
self.start_lineno = -1
|
664
|
+
get_app().invalidate()
|
665
|
+
|
666
|
+
class SessionBufferControl(UIControl):
|
667
|
+
def __init__(self, buffer: Optional[SessionBuffer]) -> None:
|
668
|
+
self.buffer = buffer
|
669
|
+
|
670
|
+
# 为MUD显示进行校正的处理,包括对齐校正,换行颜色校正等
|
671
|
+
self.FULL_BLOCKS = set("▂▃▅▆▇▄█")
|
672
|
+
self.SINGLE_LINES = set("┌└├┬┼┴╭╰─")
|
673
|
+
self.DOUBLE_LINES = set("╔╚╠╦╪╩═")
|
674
|
+
self.ALL_COLOR_REGX = re.compile(r"(?:\[[\d;]+m)+")
|
675
|
+
self.AVAI_COLOR_REGX = re.compile(r"(?:\[[\d;]+m)+(?!$)")
|
676
|
+
self._color_start = ""
|
677
|
+
self._color_correction = False
|
678
|
+
self._color_line_index = 0
|
679
|
+
|
680
|
+
self._last_click_timestamp = 0
|
681
|
+
|
682
|
+
def reset(self) -> None:
|
683
|
+
# Default reset. (Doesn't have to be implemented.)
|
684
|
+
pass
|
685
|
+
|
686
|
+
def preferred_width(self, max_available_width: int) -> int | None:
|
687
|
+
return None
|
688
|
+
|
689
|
+
def is_focusable(self) -> bool:
|
690
|
+
"""
|
691
|
+
Tell whether this user control is focusable.
|
692
|
+
"""
|
693
|
+
return False
|
694
|
+
|
695
|
+
|
696
|
+
def width_correction(self, line: str) -> str:
|
697
|
+
new_str = []
|
698
|
+
for ch in line:
|
699
|
+
new_str.append(ch)
|
700
|
+
if (east_asian_width(ch) in "FWA") and (wcwidth(ch) == 1):
|
701
|
+
if ch in self.FULL_BLOCKS:
|
702
|
+
new_str.append(ch)
|
703
|
+
elif ch in self.SINGLE_LINES:
|
704
|
+
new_str.append("─")
|
705
|
+
elif ch in self.DOUBLE_LINES:
|
706
|
+
new_str.append("═")
|
707
|
+
else:
|
708
|
+
new_str.append(' ')
|
709
|
+
|
710
|
+
return "".join(new_str)
|
711
|
+
|
712
|
+
def return_correction(self, line: str):
|
713
|
+
return line.replace("\r", "").replace("\x00", "")
|
714
|
+
|
715
|
+
def tab_correction(self, line: str):
|
716
|
+
return line.replace("\t", " " * Settings.client["tabstop"])
|
717
|
+
|
718
|
+
def line_correction(self, line: str):
|
719
|
+
# 处理\r符号(^M)
|
720
|
+
line = self.return_correction(line)
|
721
|
+
# 处理Tab(\r)符号(^I)
|
722
|
+
line = self.tab_correction(line)
|
723
|
+
|
724
|
+
# 美化(解决中文英文在Console中不对齐的问题)
|
725
|
+
if Settings.client["beautify"]:
|
726
|
+
line = self.width_correction(line)
|
727
|
+
|
728
|
+
return line
|
729
|
+
|
730
|
+
def create_content(self, width: int, height: int) -> UIContent:
|
731
|
+
"""
|
732
|
+
Generate the content for this user control.
|
733
|
+
|
734
|
+
Returns a :class:`.UIContent` instance.
|
735
|
+
"""
|
736
|
+
buffer = self.buffer
|
737
|
+
if not buffer:
|
738
|
+
return UIContent(
|
739
|
+
get_line = lambda i: [],
|
740
|
+
line_count = 0,
|
741
|
+
cursor_position = None
|
742
|
+
)
|
743
|
+
|
744
|
+
def get_line(i: int) -> StyleAndTextTuples:
|
745
|
+
line = buffer.getLine(i)
|
746
|
+
# 颜色校正
|
747
|
+
thislinecolors = len(self.AVAI_COLOR_REGX.findall(line))
|
748
|
+
if thislinecolors == 0:
|
749
|
+
lineno = i - 1
|
750
|
+
while lineno >= 0:
|
751
|
+
lastline = buffer.getLine(lineno)
|
752
|
+
allcolors = self.ALL_COLOR_REGX.findall(lastline)
|
753
|
+
|
754
|
+
if len(allcolors) == 0:
|
755
|
+
lineno = lineno - 1
|
756
|
+
|
757
|
+
elif len(allcolors) == 1:
|
758
|
+
colors = self.AVAI_COLOR_REGX.findall(lastline)
|
759
|
+
|
760
|
+
if len(colors) == 1:
|
761
|
+
line = f"{colors[0]}{line}"
|
762
|
+
break
|
763
|
+
|
764
|
+
else:
|
765
|
+
break
|
766
|
+
|
767
|
+
else:
|
768
|
+
break
|
769
|
+
|
770
|
+
|
771
|
+
# 其他校正
|
772
|
+
line = self.line_correction(line)
|
773
|
+
|
774
|
+
# 处理ANSI标记(生成FormmatedText)
|
775
|
+
fragments = to_formatted_text(ANSI(line))
|
776
|
+
|
777
|
+
# 选择内容标识
|
778
|
+
selected_fragment = " class:selected "
|
779
|
+
|
780
|
+
# In case of selection, highlight all matches.
|
781
|
+
selection_at_line = buffer.selection_range_at_line(i)
|
782
|
+
|
783
|
+
if selection_at_line:
|
784
|
+
from_, to = selection_at_line
|
785
|
+
# from_ = source_to_display(from_)
|
786
|
+
# to = source_to_display(to)
|
787
|
+
|
788
|
+
fragments = explode_text_fragments(fragments)
|
789
|
+
|
790
|
+
if from_ == 0 and to == 0 and len(fragments) == 0:
|
791
|
+
# When this is an empty line, insert a space in order to
|
792
|
+
# visualize the selection.
|
793
|
+
return [(selected_fragment, " ")]
|
794
|
+
else:
|
795
|
+
for i in range(from_, to):
|
796
|
+
if i < len(fragments):
|
797
|
+
old_fragment, old_text, *_ = fragments[i]
|
798
|
+
fragments[i] = (old_fragment + selected_fragment, old_text)
|
799
|
+
elif i == len(fragments):
|
800
|
+
fragments.append((selected_fragment, " "))
|
801
|
+
|
802
|
+
return fragments
|
803
|
+
|
804
|
+
content = UIContent(
|
805
|
+
get_line = get_line,
|
806
|
+
line_count = buffer.lineCount,
|
807
|
+
cursor_position = None
|
808
|
+
)
|
809
|
+
|
810
|
+
return content
|
811
|
+
|
812
|
+
def mouse_handler(self, mouse_event: MouseEvent):
|
813
|
+
"""
|
814
|
+
Handle mouse events.
|
815
|
+
|
816
|
+
When `NotImplemented` is returned, it means that the given event is not
|
817
|
+
handled by the `UIControl` itself. The `Window` or key bindings can
|
818
|
+
decide to handle this event as scrolling or changing focus.
|
819
|
+
|
820
|
+
:param mouse_event: `MouseEvent` instance.
|
821
|
+
"""
|
822
|
+
"""
|
823
|
+
鼠标处理,修改内容包括:
|
824
|
+
1. 在CommandLine获得焦点的时候,鼠标对本Control也可以操作
|
825
|
+
2. 鼠标双击为选中行
|
826
|
+
"""
|
827
|
+
buffer = self.buffer
|
828
|
+
position = mouse_event.position
|
829
|
+
|
830
|
+
# Focus buffer when clicked.
|
831
|
+
cur_control = get_app().layout.current_control
|
832
|
+
cur_buffer = get_app().layout.current_buffer
|
833
|
+
# 这里是修改的内容
|
834
|
+
if (cur_control == self) or (cur_buffer and cur_buffer.name == "input"):
|
835
|
+
|
836
|
+
if buffer:
|
837
|
+
# Set the selection position.
|
838
|
+
if mouse_event.event_type == MouseEventType.MOUSE_DOWN:
|
839
|
+
buffer.exit_selection()
|
840
|
+
buffer.selection.start_row = position.y
|
841
|
+
buffer.selection.start_col = position.x
|
842
|
+
|
843
|
+
elif (
|
844
|
+
mouse_event.event_type == MouseEventType.MOUSE_MOVE
|
845
|
+
and mouse_event.button == MouseButton.LEFT
|
846
|
+
):
|
847
|
+
# Click and drag to highlight a selection
|
848
|
+
if buffer.selection.start_row >= 0:
|
849
|
+
buffer.selection.end_row = position.y
|
850
|
+
buffer.selection.end_col = position.x
|
851
|
+
|
852
|
+
elif mouse_event.event_type == MouseEventType.MOUSE_UP:
|
853
|
+
# When the cursor was moved to another place, select the text.
|
854
|
+
# (The >1 is actually a small but acceptable workaround for
|
855
|
+
# selecting text in Vi navigation mode. In navigation mode,
|
856
|
+
# the cursor can never be after the text, so the cursor
|
857
|
+
# will be repositioned automatically.)
|
858
|
+
|
859
|
+
if buffer.selection.start_row >= 0:
|
860
|
+
buffer.selection.end_row = position.y
|
861
|
+
buffer.selection.end_col = position.x
|
862
|
+
|
863
|
+
if buffer.selection.start_row == buffer.selection.end_row and buffer.selection.start_col == buffer.selection.end_col:
|
864
|
+
buffer.selection = SessionSelectionState(-1, -1, -1, -1)
|
865
|
+
|
866
|
+
|
867
|
+
# Select word around cursor on double click.
|
868
|
+
# Two MOUSE_UP events in a short timespan are considered a double click.
|
869
|
+
double_click = (
|
870
|
+
self._last_click_timestamp
|
871
|
+
and time.time() - self._last_click_timestamp < 0.3
|
872
|
+
)
|
873
|
+
self._last_click_timestamp = time.time()
|
874
|
+
|
875
|
+
if double_click:
|
876
|
+
buffer.selection.start_row = position.y
|
877
|
+
buffer.selection.start_col = 0
|
878
|
+
buffer.selection.end_row = position.y
|
879
|
+
buffer.selection.end_col = len(buffer.getLine(position.y))
|
880
|
+
|
881
|
+
get_app().layout.focus("input")
|
882
|
+
|
883
|
+
else:
|
884
|
+
# Don't handle scroll events here.
|
885
|
+
return NotImplemented
|
886
|
+
|
887
|
+
# Not focused, but focusing on click events.
|
888
|
+
else:
|
889
|
+
return NotImplemented
|
890
|
+
|
891
|
+
return None
|
880
892
|
|
881
893
|
class DotDict(dict):
|
882
894
|
"""
|