pymud 0.20.2a5__py3-none-any.whl → 0.20.4__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 CHANGED
@@ -1,1047 +1,1047 @@
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
- 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: 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
- # def create_content(
301
- # self, width: int, height: int, preview_search: bool = False
302
- # ) -> UIContent:
303
- # """
304
- # Create a UIContent.
305
- # """
306
- # buffer = self.buffer
307
-
308
- # # Trigger history loading of the buffer. We do this during the
309
- # # rendering of the UI here, because it needs to happen when an
310
- # # `Application` with its event loop is running. During the rendering of
311
- # # the buffer control is the earliest place we can achieve this, where
312
- # # we're sure the right event loop is active, and don't require user
313
- # # interaction (like in a key binding).
314
- # buffer.load_history_if_not_yet_loaded()
315
-
316
- # # Get the document to be shown. If we are currently searching (the
317
- # # search buffer has focus, and the preview_search filter is enabled),
318
- # # then use the search document, which has possibly a different
319
- # # text/cursor position.)
320
- # search_control = self.search_buffer_control
321
- # preview_now = preview_search or bool(
322
- # # Only if this feature is enabled.
323
- # self.preview_search()
324
- # and
325
- # # And something was typed in the associated search field.
326
- # search_control
327
- # and search_control.buffer.text
328
- # and
329
- # # And we are searching in this control. (Many controls can point to
330
- # # the same search field, like in Pyvim.)
331
- # get_app().layout.search_target_buffer_control == self
332
- # )
333
-
334
- # if preview_now and search_control is not None:
335
- # ss = self.search_state
336
-
337
- # document = buffer.document_for_search(
338
- # SearchState(
339
- # text=search_control.buffer.text,
340
- # direction=ss.direction,
341
- # ignore_case=ss.ignore_case,
342
- # )
343
- # )
344
- # else:
345
- # document = buffer.document
346
-
347
- # get_processed_line = self._create_get_processed_line_func(
348
- # document, width, height
349
- # )
350
- # self._last_get_processed_line = get_processed_line
351
-
352
- # def translate_rowcol(row: int, col: int) -> Point:
353
- # "Return the content column for this coordinate."
354
- # return Point(x=get_processed_line(row).source_to_display(col), y=row)
355
-
356
- # def get_line(i: int) -> StyleAndTextTuples:
357
- # "Return the fragments for a given line number."
358
- # fragments = get_processed_line(i).fragments
359
-
360
- # # Add a space at the end, because that is a possible cursor
361
- # # position. (When inserting after the input.) We should do this on
362
- # # all the lines, not just the line containing the cursor. (Because
363
- # # otherwise, line wrapping/scrolling could change when moving the
364
- # # cursor around.)
365
- # fragments = fragments + [("", " ")]
366
- # return fragments
367
-
368
- # content = UIContent(
369
- # get_line=get_line,
370
- # line_count=document.line_count,
371
- # cursor_position=translate_rowcol(
372
- # document.cursor_position_row, document.cursor_position_col
373
- # ),
374
- # )
375
-
376
- # # If there is an auto completion going on, use that start point for a
377
- # # pop-up menu position. (But only when this buffer has the focus --
378
- # # there is only one place for a menu, determined by the focused buffer.)
379
- # if get_app().layout.current_control == self:
380
- # menu_position = self.menu_position() if self.menu_position else None
381
- # if menu_position is not None:
382
- # assert isinstance(menu_position, int)
383
- # menu_row, menu_col = buffer.document.translate_index_to_position(
384
- # menu_position
385
- # )
386
- # content.menu_position = translate_rowcol(menu_row, menu_col)
387
- # elif buffer.complete_state:
388
- # # Position for completion menu.
389
- # # Note: We use 'min', because the original cursor position could be
390
- # # behind the input string when the actual completion is for
391
- # # some reason shorter than the text we had before. (A completion
392
- # # can change and shorten the input.)
393
- # menu_row, menu_col = buffer.document.translate_index_to_position(
394
- # min(
395
- # buffer.cursor_position,
396
- # buffer.complete_state.original_document.cursor_position,
397
- # )
398
- # )
399
- # content.menu_position = translate_rowcol(menu_row, menu_col)
400
- # else:
401
- # content.menu_position = None
402
-
403
- # return content
404
-
405
- def mouse_handler(self, mouse_event: MouseEvent):
406
- """
407
- 鼠标处理,修改内容包括:
408
- 1. 在CommandLine获得焦点的时候,鼠标对本Control也可以操作
409
- 2. 鼠标双击为选中行
410
- """
411
- buffer = self.buffer
412
- position = mouse_event.position
413
-
414
- # Focus buffer when clicked.
415
- cur_control = get_app().layout.current_control
416
- cur_buffer = get_app().layout.current_buffer
417
- # 这里时修改的内容
418
- if (cur_control == self) or (cur_buffer and cur_buffer.name == "input"):
419
- if self._last_get_processed_line:
420
- processed_line = self._last_get_processed_line(position.y)
421
-
422
- # Translate coordinates back to the cursor position of the
423
- # original input.
424
- xpos = processed_line.display_to_source(position.x)
425
- index = buffer.document.translate_row_col_to_index(position.y, xpos)
426
-
427
- # Set the cursor position.
428
- if mouse_event.event_type == MouseEventType.MOUSE_DOWN:
429
- buffer.exit_selection()
430
- buffer.cursor_position = index
431
-
432
- elif (
433
- mouse_event.event_type == MouseEventType.MOUSE_MOVE
434
- and mouse_event.button != MouseButton.NONE
435
- ):
436
- # Click and drag to highlight a selection
437
- if (
438
- buffer.selection_state is None
439
- and abs(buffer.cursor_position - index) > 0
440
- ):
441
- buffer.start_selection(selection_type=SelectionType.CHARACTERS)
442
- buffer.cursor_position = index
443
-
444
- elif mouse_event.event_type == MouseEventType.MOUSE_UP:
445
- # When the cursor was moved to another place, select the text.
446
- # (The >1 is actually a small but acceptable workaround for
447
- # selecting text in Vi navigation mode. In navigation mode,
448
- # the cursor can never be after the text, so the cursor
449
- # will be repositioned automatically.)
450
-
451
- if abs(buffer.cursor_position - index) > 1:
452
- if buffer.selection_state is None:
453
- buffer.start_selection(
454
- selection_type=SelectionType.CHARACTERS
455
- )
456
- buffer.cursor_position = index
457
-
458
- # Select word around cursor on double click.
459
- # Two MOUSE_UP events in a short timespan are considered a double click.
460
- double_click = (
461
- self._last_click_timestamp
462
- and time.time() - self._last_click_timestamp < 0.3
463
- )
464
- self._last_click_timestamp = time.time()
465
-
466
- if double_click:
467
- start = buffer.document.translate_row_col_to_index(position.y, 0)
468
- end = buffer.document.translate_row_col_to_index(position.y + 1, 0) - 1
469
- buffer.cursor_position = start
470
- buffer.start_selection(selection_type=SelectionType.LINES)
471
- buffer.cursor_position = end
472
-
473
- else:
474
- # Don't handle scroll events here.
475
- return NotImplemented
476
-
477
- # Not focused, but focusing on click events.
478
- else:
479
- if (
480
- self.focus_on_click()
481
- and mouse_event.event_type == MouseEventType.MOUSE_UP
482
- ):
483
- # Focus happens on mouseup. (If we did this on mousedown, the
484
- # up event will be received at the point where this widget is
485
- # focused and be handled anyway.)
486
- get_app().layout.current_control = self
487
- else:
488
- return NotImplemented
489
-
490
- return None
491
-
492
- def move_cursor_down(self) -> None:
493
- b = self.buffer
494
- b.cursor_position += b.document.get_cursor_down_position()
495
-
496
- def move_cursor_up(self) -> None:
497
- b = self.buffer
498
- b.cursor_position += b.document.get_cursor_up_position()
499
-
500
- def move_cursor_right(self, count = 1) -> None:
501
- b = self.buffer
502
- b.cursor_position += count
503
-
504
- def move_cursor_left(self, count = 1) -> None:
505
- b = self.buffer
506
- b.cursor_position -= count
507
-
508
-
509
- class VSplitWindow(Window):
510
- "修改的分块窗口,向上翻页时,下半部保持最后数据不变"
511
-
512
- def _copy_body(
513
- self,
514
- ui_content: UIContent,
515
- new_screen: Screen,
516
- write_position: WritePosition,
517
- move_x: int,
518
- width: int,
519
- vertical_scroll: int = 0,
520
- horizontal_scroll: int = 0,
521
- wrap_lines: bool = False,
522
- highlight_lines: bool = False,
523
- vertical_scroll_2: int = 0,
524
- always_hide_cursor: bool = False,
525
- has_focus: bool = False,
526
- align: WindowAlign = WindowAlign.LEFT,
527
- get_line_prefix = None,
528
- isNotMargin = True,
529
- ):
530
- """
531
- Copy the UIContent into the output screen.
532
- Return (visible_line_to_row_col, rowcol_to_yx) tuple.
533
-
534
- :param get_line_prefix: None or a callable that takes a line number
535
- (int) and a wrap_count (int) and returns formatted text.
536
- """
537
- xpos = write_position.xpos + move_x
538
- ypos = write_position.ypos
539
- line_count = ui_content.line_count
540
- new_buffer = new_screen.data_buffer
541
- empty_char = _CHAR_CACHE["", ""]
542
-
543
- # Map visible line number to (row, col) of input.
544
- # 'col' will always be zero if line wrapping is off.
545
- visible_line_to_row_col: dict[int, tuple[int, int]] = {}
546
-
547
- # Maps (row, col) from the input to (y, x) screen coordinates.
548
- rowcol_to_yx: dict[tuple[int, int], tuple[int, int]] = {}
549
-
550
- def copy_line(
551
- line: StyleAndTextTuples,
552
- lineno: int,
553
- x: int,
554
- y: int,
555
- is_input: bool = False,
556
- ):
557
- """
558
- Copy over a single line to the output screen. This can wrap over
559
- multiple lines in the output. It will call the prefix (prompt)
560
- function before every line.
561
- """
562
- if is_input:
563
- current_rowcol_to_yx = rowcol_to_yx
564
- else:
565
- current_rowcol_to_yx = {} # Throwaway dictionary.
566
-
567
- # Draw line prefix.
568
- if is_input and get_line_prefix:
569
- prompt = to_formatted_text(get_line_prefix(lineno, 0))
570
- x, y = copy_line(prompt, lineno, x, y, is_input=False)
571
-
572
- # Scroll horizontally.
573
- skipped = 0 # Characters skipped because of horizontal scrolling.
574
- if horizontal_scroll and is_input:
575
- h_scroll = horizontal_scroll
576
- line = explode_text_fragments(line)
577
- while h_scroll > 0 and line:
578
- h_scroll -= get_cwidth(line[0][1])
579
- skipped += 1
580
- del line[:1] # Remove first character.
581
-
582
- x -= h_scroll # When scrolling over double width character,
583
- # this can end up being negative.
584
-
585
- # Align this line. (Note that this doesn't work well when we use
586
- # get_line_prefix and that function returns variable width prefixes.)
587
- if align == WindowAlign.CENTER:
588
- line_width = fragment_list_width(line)
589
- if line_width < width:
590
- x += (width - line_width) // 2
591
- elif align == WindowAlign.RIGHT:
592
- line_width = fragment_list_width(line)
593
- if line_width < width:
594
- x += width - line_width
595
-
596
- col = 0
597
- wrap_count = 0
598
- for style, text, *_ in line:
599
- new_buffer_row = new_buffer[y + ypos]
600
-
601
- # Remember raw VT escape sequences. (E.g. FinalTerm's
602
- # escape sequences.)
603
- if "[ZeroWidthEscape]" in style:
604
- new_screen.zero_width_escapes[y + ypos][x + xpos] += text
605
- continue
606
-
607
- for c in text:
608
- char = _CHAR_CACHE[c, style]
609
- char_width = char.width
610
-
611
- # Wrap when the line width is exceeded.
612
- if wrap_lines and x + char_width > width:
613
- visible_line_to_row_col[y + 1] = (
614
- lineno,
615
- visible_line_to_row_col[y][1] + x,
616
- )
617
- y += 1
618
- wrap_count += 1
619
- x = 0
620
-
621
- # Insert line prefix (continuation prompt).
622
- if is_input and get_line_prefix:
623
- prompt = to_formatted_text(
624
- get_line_prefix(lineno, wrap_count)
625
- )
626
- x, y = copy_line(prompt, lineno, x, y, is_input=False)
627
-
628
- new_buffer_row = new_buffer[y + ypos]
629
-
630
- if y >= write_position.height:
631
- return x, y # Break out of all for loops.
632
-
633
- # Set character in screen and shift 'x'.
634
- if x >= 0 and y >= 0 and x < width:
635
- new_buffer_row[x + xpos] = char
636
-
637
- # When we print a multi width character, make sure
638
- # to erase the neighbours positions in the screen.
639
- # (The empty string if different from everything,
640
- # so next redraw this cell will repaint anyway.)
641
- if char_width > 1:
642
- for i in range(1, char_width):
643
- new_buffer_row[x + xpos + i] = empty_char
644
-
645
- # If this is a zero width characters, then it's
646
- # probably part of a decomposed unicode character.
647
- # See: https://en.wikipedia.org/wiki/Unicode_equivalence
648
- # Merge it in the previous cell.
649
- elif char_width == 0:
650
- # Handle all character widths. If the previous
651
- # character is a multiwidth character, then
652
- # merge it two positions back.
653
- for pw in [2, 1]: # Previous character width.
654
- if (
655
- x - pw >= 0
656
- and new_buffer_row[x + xpos - pw].width == pw
657
- ):
658
- prev_char = new_buffer_row[x + xpos - pw]
659
- char2 = _CHAR_CACHE[
660
- prev_char.char + c, prev_char.style
661
- ]
662
- new_buffer_row[x + xpos - pw] = char2
663
-
664
- # Keep track of write position for each character.
665
- current_rowcol_to_yx[lineno, col + skipped] = (
666
- y + ypos,
667
- x + xpos,
668
- )
669
-
670
- col += 1
671
- x += char_width
672
- return x, y
673
-
674
- # Copy content.
675
- def copy() -> int:
676
- y = -vertical_scroll_2
677
- lineno = vertical_scroll
678
-
679
- total = write_position.height
680
- upper = (total - 1) // 2
681
- below = total - upper - 1
682
-
683
- if lineno + total < line_count:
684
- if isinstance(self.content, SessionBufferControl):
685
- b = self.content.buffer
686
- b.split = True
687
-
688
- while y < upper and lineno < line_count:
689
- line = ui_content.get_line(lineno)
690
- visible_line_to_row_col[y] = (lineno, horizontal_scroll)
691
- x = 0
692
- x, y = copy_line(line, lineno, x, y, is_input=True)
693
- lineno += 1
694
- y += 1
695
-
696
- x = 0
697
- x, y = copy_line([("","-"*width)], lineno, x, y, is_input=False)
698
- y += 1
699
-
700
- lineno = line_count - below
701
- while y < total and lineno < line_count:
702
- line = ui_content.get_line(lineno)
703
- visible_line_to_row_col[y] = (lineno, horizontal_scroll)
704
- x = 0
705
- x, y = copy_line(line, lineno, x, y, is_input=True)
706
- lineno += 1
707
- y += 1
708
-
709
- return y
710
-
711
- else:
712
- if isNotMargin and isinstance(self.content, SessionBufferControl):
713
- b = self.content.buffer
714
- b.split = False
715
-
716
- while y < write_position.height and lineno < line_count:
717
- # Take the next line and copy it in the real screen.
718
- line = ui_content.get_line(lineno)
719
-
720
- visible_line_to_row_col[y] = (lineno, horizontal_scroll)
721
-
722
- # Copy margin and actual line.
723
- x = 0
724
- x, y = copy_line(line, lineno, x, y, is_input=True)
725
-
726
- lineno += 1
727
- y += 1
728
- return y
729
-
730
-
731
-
732
- copy()
733
-
734
- def cursor_pos_to_screen_pos(row: int, col: int) -> Point:
735
- "Translate row/col from UIContent to real Screen coordinates."
736
- try:
737
- y, x = rowcol_to_yx[row, col]
738
- except KeyError:
739
- # Normally this should never happen. (It is a bug, if it happens.)
740
- # But to be sure, return (0, 0)
741
- return Point(x=0, y=0)
742
-
743
- # raise ValueError(
744
- # 'Invalid position. row=%r col=%r, vertical_scroll=%r, '
745
- # 'horizontal_scroll=%r, height=%r' %
746
- # (row, col, vertical_scroll, horizontal_scroll, write_position.height))
747
- else:
748
- return Point(x=x, y=y)
749
-
750
- # Set cursor and menu positions.
751
- if ui_content.cursor_position:
752
- screen_cursor_position = cursor_pos_to_screen_pos(
753
- ui_content.cursor_position.y, ui_content.cursor_position.x
754
- )
755
-
756
- if has_focus:
757
- new_screen.set_cursor_position(self, screen_cursor_position)
758
-
759
- if always_hide_cursor:
760
- new_screen.show_cursor = False
761
- else:
762
- new_screen.show_cursor = ui_content.show_cursor
763
-
764
- self._highlight_digraph(new_screen)
765
-
766
- if highlight_lines:
767
- self._highlight_cursorlines(
768
- new_screen,
769
- screen_cursor_position,
770
- xpos,
771
- ypos,
772
- width,
773
- write_position.height,
774
- )
775
-
776
- # Draw input characters from the input processor queue.
777
- if has_focus and ui_content.cursor_position:
778
- self._show_key_processor_key_buffer(new_screen)
779
-
780
- # Set menu position.
781
- if ui_content.menu_position:
782
- new_screen.set_menu_position(
783
- self,
784
- cursor_pos_to_screen_pos(
785
- ui_content.menu_position.y, ui_content.menu_position.x
786
- ),
787
- )
788
-
789
- # Update output screen height.
790
- new_screen.height = max(new_screen.height, ypos + write_position.height)
791
-
792
- return visible_line_to_row_col, rowcol_to_yx
793
-
794
- def _copy_margin(
795
- self,
796
- margin_content: UIContent,
797
- new_screen: Screen,
798
- write_position: WritePosition,
799
- move_x: int,
800
- width: int,
801
- ) -> None:
802
- """
803
- Copy characters from the margin screen to the real screen.
804
- """
805
- xpos = write_position.xpos + move_x
806
- ypos = write_position.ypos
807
-
808
- margin_write_position = WritePosition(xpos, ypos, width, write_position.height)
809
- self._copy_body(margin_content, new_screen, margin_write_position, 0, width, isNotMargin=False)
810
-
811
- def _scroll_down(self) -> None:
812
- "向下滚屏,处理屏幕分隔"
813
- info = self.render_info
814
-
815
- if info is None:
816
- return
817
-
818
- if isinstance(self.content, SessionBufferControl):
819
- b = self.content.buffer
820
- d = b.document
821
-
822
- b.exit_selection()
823
- cur_line = d.cursor_position_row
824
-
825
- # # 向下滚动时,如果存在自动折行情况,要判断本行被折成了几行,在行内时要逐行移动(此处未调试好)
826
- # cur_col = d.cursor_position_col
827
- # line = d.current_line
828
- # line_width = len(line)
829
- # line_start = d.translate_row_col_to_index(cur_line, 0)
830
- # screen_width = info.window_width
831
-
832
- # offset_y = cur_col // screen_width
833
- # wraplines = math.ceil(1.0 * line_width / screen_width)
834
-
835
- if cur_line < info.content_height:
836
-
837
- # if offset_y < wraplines: # add
838
- # self.content.move_cursor_right(screen_width) # add
839
- # else: # add
840
-
841
- self.content.move_cursor_down()
842
- self.vertical_scroll = cur_line + 1
843
-
844
- firstline = d.line_count - len(info.displayed_lines)
845
- if cur_line >= firstline:
846
- b.cursor_position = len(b.text)
847
-
848
- def _scroll_up(self) -> None:
849
- "向上滚屏,处理屏幕分隔"
850
- info = self.render_info
851
-
852
- if info is None:
853
- return
854
-
855
- #if info.cursor_position.y >= 1:
856
- if isinstance(self.content, SessionBufferControl):
857
- b = self.content.buffer
858
- d = b.document
859
-
860
- b.exit_selection()
861
- cur_line = d.cursor_position_row
862
- if cur_line > d.line_count - len(info.displayed_lines):
863
- firstline = d.line_count - len(info.displayed_lines)
864
- newpos = d.translate_row_col_to_index(firstline, 0)
865
- b.cursor_position = newpos
866
- cur_line = d.cursor_position_row
867
- self.vertical_scroll = cur_line
868
-
869
- elif cur_line > 0:
870
- self.content.move_cursor_up()
871
- self.vertical_scroll = cur_line - 1
872
-
873
-
874
- class EasternButton(Button):
875
- "解决增加中文等东亚全宽字符后不对齐问题"
876
-
877
- def _get_text_fragments(self) -> StyleAndTextTuples:
878
- # 主要改动在这里
879
- width = self.width - (
880
- get_cwidth(self.left_symbol) + get_cwidth(self.right_symbol)
881
- ) - (get_cwidth(self.text) - len(self.text))
882
-
883
-
884
- text = (f"{{:^{width}}}").format(self.text)
885
-
886
- def handler(mouse_event: MouseEvent) -> None:
887
- if (
888
- self.handler is not None
889
- and mouse_event.event_type == MouseEventType.MOUSE_UP
890
- ):
891
- self.handler()
892
-
893
- return [
894
- ("class:button.arrow", self.left_symbol, handler),
895
- #("[SetCursorPosition]", ""),
896
- ("class:button.text", text, handler),
897
- ("class:button.arrow", self.right_symbol, handler),
898
- ]
899
-
900
- class EasternMenuContainer(MenuContainer):
901
- "解决增加中文等东亚全宽字符后不对齐问题"
902
-
903
- def _submenu(self, level: int = 0) -> Window:
904
- def get_text_fragments() -> StyleAndTextTuples:
905
- result: StyleAndTextTuples = []
906
- if level < len(self.selected_menu):
907
- menu = self._get_menu(level)
908
- if menu.children:
909
- result.append(("class:menu", Border.TOP_LEFT))
910
- result.append(("class:menu", Border.HORIZONTAL * (menu.width + 4)))
911
- result.append(("class:menu", Border.TOP_RIGHT))
912
- result.append(("", "\n"))
913
- try:
914
- selected_item = self.selected_menu[level + 1]
915
- except IndexError:
916
- selected_item = -1
917
-
918
- def one_item(
919
- i: int, item: MenuItem
920
- ) -> Iterable[OneStyleAndTextTuple]:
921
- def mouse_handler(mouse_event: MouseEvent) -> None:
922
- if item.disabled:
923
- # The arrow keys can't interact with menu items that are disabled.
924
- # The mouse shouldn't be able to either.
925
- return
926
- hover = mouse_event.event_type == MouseEventType.MOUSE_MOVE
927
- if (
928
- mouse_event.event_type == MouseEventType.MOUSE_UP
929
- or hover
930
- ):
931
- app = get_app()
932
- if not hover and item.handler:
933
- app.layout.focus_last()
934
- item.handler()
935
- else:
936
- self.selected_menu = self.selected_menu[
937
- : level + 1
938
- ] + [i]
939
-
940
- if i == selected_item:
941
- yield ("[SetCursorPosition]", "")
942
- style = "class:menu-bar.selected-item"
943
- else:
944
- style = ""
945
-
946
- yield ("class:menu", Border.VERTICAL)
947
- if item.text == "-":
948
- yield (
949
- style + "class:menu-border",
950
- f"{Border.HORIZONTAL * (menu.width + 3)}",
951
- mouse_handler,
952
- )
953
- else:
954
- # 主要改动在这里,其他地方都未更改.
955
- adj_width = menu.width + 3 - (get_cwidth(item.text) - len(item.text))
956
- yield (
957
- style,
958
- f" {item.text}".ljust(adj_width),
959
- mouse_handler,
960
- )
961
-
962
- if item.children:
963
- yield (style, ">", mouse_handler)
964
- else:
965
- yield (style, " ", mouse_handler)
966
-
967
- if i == selected_item:
968
- yield ("[SetMenuPosition]", "")
969
- yield ("class:menu", Border.VERTICAL)
970
-
971
- yield ("", "\n")
972
-
973
- for i, item in enumerate(menu.children):
974
- result.extend(one_item(i, item))
975
-
976
- result.append(("class:menu", Border.BOTTOM_LEFT))
977
- result.append(("class:menu", Border.HORIZONTAL * (menu.width + 4)))
978
- result.append(("class:menu", Border.BOTTOM_RIGHT))
979
- return result
980
-
981
- return Window(FormattedTextControl(get_text_fragments), style="class:menu")
982
-
983
-
984
-
985
- class MenuItem:
986
- def __init__(
987
- self,
988
- text: str = "",
989
- handler = None,
990
- children = None,
991
- shortcut = None,
992
- disabled: bool = False,
993
- ) -> None:
994
- self.text = text
995
- self.handler = handler
996
- self.children = children or []
997
- self.shortcut = shortcut
998
- self.disabled = disabled
999
- self.selected_item = 0
1000
-
1001
- @property
1002
- def width(self) -> int:
1003
- if self.children:
1004
- return max(get_cwidth(c.text) for c in self.children)
1005
- else:
1006
- return 0
1007
-
1008
-
1009
- class DotDict(dict):
1010
- """
1011
- 可以通过点.访问内部key-value对的字典。此类型继承自dict。
1012
-
1013
- - 由于继承关系,此类型可以使用所有dict可以使用的方法
1014
- - 额外增加的点.访问方法使用示例如下
1015
-
1016
- 示例:
1017
- .. code:: Python
1018
-
1019
- mydict = DotDict()
1020
-
1021
- # 以下写内容访问等价
1022
- mydict["key1"] = "value1"
1023
- mydict.key1 = "value1"
1024
-
1025
- # 以下读访问等价
1026
- val = mydict["key1"]
1027
- val = mydict.key1
1028
- """
1029
-
1030
- def __getattr__(self, __key):
1031
- if (not __key in self.__dict__) and (not __key.startswith("__")):
1032
- return self.__getitem__(__key)
1033
-
1034
- def __setattr__(self, __name: str, __value):
1035
- if __name in self.__dict__:
1036
- object.__setattr__(self, __name, __value)
1037
- else:
1038
- self.__setitem__(__name, __value)
1039
-
1040
- def __getstate__(self):
1041
- return self
1042
-
1043
- def __setstate__(self, state):
1044
- self.update(state)
1045
-
1046
-
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
+ 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: 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
+ # def create_content(
301
+ # self, width: int, height: int, preview_search: bool = False
302
+ # ) -> UIContent:
303
+ # """
304
+ # Create a UIContent.
305
+ # """
306
+ # buffer = self.buffer
307
+
308
+ # # Trigger history loading of the buffer. We do this during the
309
+ # # rendering of the UI here, because it needs to happen when an
310
+ # # `Application` with its event loop is running. During the rendering of
311
+ # # the buffer control is the earliest place we can achieve this, where
312
+ # # we're sure the right event loop is active, and don't require user
313
+ # # interaction (like in a key binding).
314
+ # buffer.load_history_if_not_yet_loaded()
315
+
316
+ # # Get the document to be shown. If we are currently searching (the
317
+ # # search buffer has focus, and the preview_search filter is enabled),
318
+ # # then use the search document, which has possibly a different
319
+ # # text/cursor position.)
320
+ # search_control = self.search_buffer_control
321
+ # preview_now = preview_search or bool(
322
+ # # Only if this feature is enabled.
323
+ # self.preview_search()
324
+ # and
325
+ # # And something was typed in the associated search field.
326
+ # search_control
327
+ # and search_control.buffer.text
328
+ # and
329
+ # # And we are searching in this control. (Many controls can point to
330
+ # # the same search field, like in Pyvim.)
331
+ # get_app().layout.search_target_buffer_control == self
332
+ # )
333
+
334
+ # if preview_now and search_control is not None:
335
+ # ss = self.search_state
336
+
337
+ # document = buffer.document_for_search(
338
+ # SearchState(
339
+ # text=search_control.buffer.text,
340
+ # direction=ss.direction,
341
+ # ignore_case=ss.ignore_case,
342
+ # )
343
+ # )
344
+ # else:
345
+ # document = buffer.document
346
+
347
+ # get_processed_line = self._create_get_processed_line_func(
348
+ # document, width, height
349
+ # )
350
+ # self._last_get_processed_line = get_processed_line
351
+
352
+ # def translate_rowcol(row: int, col: int) -> Point:
353
+ # "Return the content column for this coordinate."
354
+ # return Point(x=get_processed_line(row).source_to_display(col), y=row)
355
+
356
+ # def get_line(i: int) -> StyleAndTextTuples:
357
+ # "Return the fragments for a given line number."
358
+ # fragments = get_processed_line(i).fragments
359
+
360
+ # # Add a space at the end, because that is a possible cursor
361
+ # # position. (When inserting after the input.) We should do this on
362
+ # # all the lines, not just the line containing the cursor. (Because
363
+ # # otherwise, line wrapping/scrolling could change when moving the
364
+ # # cursor around.)
365
+ # fragments = fragments + [("", " ")]
366
+ # return fragments
367
+
368
+ # content = UIContent(
369
+ # get_line=get_line,
370
+ # line_count=document.line_count,
371
+ # cursor_position=translate_rowcol(
372
+ # document.cursor_position_row, document.cursor_position_col
373
+ # ),
374
+ # )
375
+
376
+ # # If there is an auto completion going on, use that start point for a
377
+ # # pop-up menu position. (But only when this buffer has the focus --
378
+ # # there is only one place for a menu, determined by the focused buffer.)
379
+ # if get_app().layout.current_control == self:
380
+ # menu_position = self.menu_position() if self.menu_position else None
381
+ # if menu_position is not None:
382
+ # assert isinstance(menu_position, int)
383
+ # menu_row, menu_col = buffer.document.translate_index_to_position(
384
+ # menu_position
385
+ # )
386
+ # content.menu_position = translate_rowcol(menu_row, menu_col)
387
+ # elif buffer.complete_state:
388
+ # # Position for completion menu.
389
+ # # Note: We use 'min', because the original cursor position could be
390
+ # # behind the input string when the actual completion is for
391
+ # # some reason shorter than the text we had before. (A completion
392
+ # # can change and shorten the input.)
393
+ # menu_row, menu_col = buffer.document.translate_index_to_position(
394
+ # min(
395
+ # buffer.cursor_position,
396
+ # buffer.complete_state.original_document.cursor_position,
397
+ # )
398
+ # )
399
+ # content.menu_position = translate_rowcol(menu_row, menu_col)
400
+ # else:
401
+ # content.menu_position = None
402
+
403
+ # return content
404
+
405
+ def mouse_handler(self, mouse_event: MouseEvent):
406
+ """
407
+ 鼠标处理,修改内容包括:
408
+ 1. 在CommandLine获得焦点的时候,鼠标对本Control也可以操作
409
+ 2. 鼠标双击为选中行
410
+ """
411
+ buffer = self.buffer
412
+ position = mouse_event.position
413
+
414
+ # Focus buffer when clicked.
415
+ cur_control = get_app().layout.current_control
416
+ cur_buffer = get_app().layout.current_buffer
417
+ # 这里时修改的内容
418
+ if (cur_control == self) or (cur_buffer and cur_buffer.name == "input"):
419
+ if self._last_get_processed_line:
420
+ processed_line = self._last_get_processed_line(position.y)
421
+
422
+ # Translate coordinates back to the cursor position of the
423
+ # original input.
424
+ xpos = processed_line.display_to_source(position.x)
425
+ index = buffer.document.translate_row_col_to_index(position.y, xpos)
426
+
427
+ # Set the cursor position.
428
+ if mouse_event.event_type == MouseEventType.MOUSE_DOWN:
429
+ buffer.exit_selection()
430
+ buffer.cursor_position = index
431
+
432
+ elif (
433
+ mouse_event.event_type == MouseEventType.MOUSE_MOVE
434
+ and mouse_event.button != MouseButton.NONE
435
+ ):
436
+ # Click and drag to highlight a selection
437
+ if (
438
+ buffer.selection_state is None
439
+ and abs(buffer.cursor_position - index) > 0
440
+ ):
441
+ buffer.start_selection(selection_type=SelectionType.CHARACTERS)
442
+ buffer.cursor_position = index
443
+
444
+ elif mouse_event.event_type == MouseEventType.MOUSE_UP:
445
+ # When the cursor was moved to another place, select the text.
446
+ # (The >1 is actually a small but acceptable workaround for
447
+ # selecting text in Vi navigation mode. In navigation mode,
448
+ # the cursor can never be after the text, so the cursor
449
+ # will be repositioned automatically.)
450
+
451
+ if abs(buffer.cursor_position - index) > 1:
452
+ if buffer.selection_state is None:
453
+ buffer.start_selection(
454
+ selection_type=SelectionType.CHARACTERS
455
+ )
456
+ buffer.cursor_position = index
457
+
458
+ # Select word around cursor on double click.
459
+ # Two MOUSE_UP events in a short timespan are considered a double click.
460
+ double_click = (
461
+ self._last_click_timestamp
462
+ and time.time() - self._last_click_timestamp < 0.3
463
+ )
464
+ self._last_click_timestamp = time.time()
465
+
466
+ if double_click:
467
+ start = buffer.document.translate_row_col_to_index(position.y, 0)
468
+ end = buffer.document.translate_row_col_to_index(position.y + 1, 0) - 1
469
+ buffer.cursor_position = start
470
+ buffer.start_selection(selection_type=SelectionType.LINES)
471
+ buffer.cursor_position = end
472
+
473
+ else:
474
+ # Don't handle scroll events here.
475
+ return NotImplemented
476
+
477
+ # Not focused, but focusing on click events.
478
+ else:
479
+ if (
480
+ self.focus_on_click()
481
+ and mouse_event.event_type == MouseEventType.MOUSE_UP
482
+ ):
483
+ # Focus happens on mouseup. (If we did this on mousedown, the
484
+ # up event will be received at the point where this widget is
485
+ # focused and be handled anyway.)
486
+ get_app().layout.current_control = self
487
+ else:
488
+ return NotImplemented
489
+
490
+ return None
491
+
492
+ def move_cursor_down(self) -> None:
493
+ b = self.buffer
494
+ b.cursor_position += b.document.get_cursor_down_position()
495
+
496
+ def move_cursor_up(self) -> None:
497
+ b = self.buffer
498
+ b.cursor_position += b.document.get_cursor_up_position()
499
+
500
+ def move_cursor_right(self, count = 1) -> None:
501
+ b = self.buffer
502
+ b.cursor_position += count
503
+
504
+ def move_cursor_left(self, count = 1) -> None:
505
+ b = self.buffer
506
+ b.cursor_position -= count
507
+
508
+
509
+ class VSplitWindow(Window):
510
+ "修改的分块窗口,向上翻页时,下半部保持最后数据不变"
511
+
512
+ def _copy_body(
513
+ self,
514
+ ui_content: UIContent,
515
+ new_screen: Screen,
516
+ write_position: WritePosition,
517
+ move_x: int,
518
+ width: int,
519
+ vertical_scroll: int = 0,
520
+ horizontal_scroll: int = 0,
521
+ wrap_lines: bool = False,
522
+ highlight_lines: bool = False,
523
+ vertical_scroll_2: int = 0,
524
+ always_hide_cursor: bool = False,
525
+ has_focus: bool = False,
526
+ align: WindowAlign = WindowAlign.LEFT,
527
+ get_line_prefix = None,
528
+ isNotMargin = True,
529
+ ):
530
+ """
531
+ Copy the UIContent into the output screen.
532
+ Return (visible_line_to_row_col, rowcol_to_yx) tuple.
533
+
534
+ :param get_line_prefix: None or a callable that takes a line number
535
+ (int) and a wrap_count (int) and returns formatted text.
536
+ """
537
+ xpos = write_position.xpos + move_x
538
+ ypos = write_position.ypos
539
+ line_count = ui_content.line_count
540
+ new_buffer = new_screen.data_buffer
541
+ empty_char = _CHAR_CACHE["", ""]
542
+
543
+ # Map visible line number to (row, col) of input.
544
+ # 'col' will always be zero if line wrapping is off.
545
+ visible_line_to_row_col: dict[int, tuple[int, int]] = {}
546
+
547
+ # Maps (row, col) from the input to (y, x) screen coordinates.
548
+ rowcol_to_yx: dict[tuple[int, int], tuple[int, int]] = {}
549
+
550
+ def copy_line(
551
+ line: StyleAndTextTuples,
552
+ lineno: int,
553
+ x: int,
554
+ y: int,
555
+ is_input: bool = False,
556
+ ):
557
+ """
558
+ Copy over a single line to the output screen. This can wrap over
559
+ multiple lines in the output. It will call the prefix (prompt)
560
+ function before every line.
561
+ """
562
+ if is_input:
563
+ current_rowcol_to_yx = rowcol_to_yx
564
+ else:
565
+ current_rowcol_to_yx = {} # Throwaway dictionary.
566
+
567
+ # Draw line prefix.
568
+ if is_input and get_line_prefix:
569
+ prompt = to_formatted_text(get_line_prefix(lineno, 0))
570
+ x, y = copy_line(prompt, lineno, x, y, is_input=False)
571
+
572
+ # Scroll horizontally.
573
+ skipped = 0 # Characters skipped because of horizontal scrolling.
574
+ if horizontal_scroll and is_input:
575
+ h_scroll = horizontal_scroll
576
+ line = explode_text_fragments(line)
577
+ while h_scroll > 0 and line:
578
+ h_scroll -= get_cwidth(line[0][1])
579
+ skipped += 1
580
+ del line[:1] # Remove first character.
581
+
582
+ x -= h_scroll # When scrolling over double width character,
583
+ # this can end up being negative.
584
+
585
+ # Align this line. (Note that this doesn't work well when we use
586
+ # get_line_prefix and that function returns variable width prefixes.)
587
+ if align == WindowAlign.CENTER:
588
+ line_width = fragment_list_width(line)
589
+ if line_width < width:
590
+ x += (width - line_width) // 2
591
+ elif align == WindowAlign.RIGHT:
592
+ line_width = fragment_list_width(line)
593
+ if line_width < width:
594
+ x += width - line_width
595
+
596
+ col = 0
597
+ wrap_count = 0
598
+ for style, text, *_ in line:
599
+ new_buffer_row = new_buffer[y + ypos]
600
+
601
+ # Remember raw VT escape sequences. (E.g. FinalTerm's
602
+ # escape sequences.)
603
+ if "[ZeroWidthEscape]" in style:
604
+ new_screen.zero_width_escapes[y + ypos][x + xpos] += text
605
+ continue
606
+
607
+ for c in text:
608
+ char = _CHAR_CACHE[c, style]
609
+ char_width = char.width
610
+
611
+ # Wrap when the line width is exceeded.
612
+ if wrap_lines and x + char_width > width:
613
+ visible_line_to_row_col[y + 1] = (
614
+ lineno,
615
+ visible_line_to_row_col[y][1] + x,
616
+ )
617
+ y += 1
618
+ wrap_count += 1
619
+ x = 0
620
+
621
+ # Insert line prefix (continuation prompt).
622
+ if is_input and get_line_prefix:
623
+ prompt = to_formatted_text(
624
+ get_line_prefix(lineno, wrap_count)
625
+ )
626
+ x, y = copy_line(prompt, lineno, x, y, is_input=False)
627
+
628
+ new_buffer_row = new_buffer[y + ypos]
629
+
630
+ if y >= write_position.height:
631
+ return x, y # Break out of all for loops.
632
+
633
+ # Set character in screen and shift 'x'.
634
+ if x >= 0 and y >= 0 and x < width:
635
+ new_buffer_row[x + xpos] = char
636
+
637
+ # When we print a multi width character, make sure
638
+ # to erase the neighbours positions in the screen.
639
+ # (The empty string if different from everything,
640
+ # so next redraw this cell will repaint anyway.)
641
+ if char_width > 1:
642
+ for i in range(1, char_width):
643
+ new_buffer_row[x + xpos + i] = empty_char
644
+
645
+ # If this is a zero width characters, then it's
646
+ # probably part of a decomposed unicode character.
647
+ # See: https://en.wikipedia.org/wiki/Unicode_equivalence
648
+ # Merge it in the previous cell.
649
+ elif char_width == 0:
650
+ # Handle all character widths. If the previous
651
+ # character is a multiwidth character, then
652
+ # merge it two positions back.
653
+ for pw in [2, 1]: # Previous character width.
654
+ if (
655
+ x - pw >= 0
656
+ and new_buffer_row[x + xpos - pw].width == pw
657
+ ):
658
+ prev_char = new_buffer_row[x + xpos - pw]
659
+ char2 = _CHAR_CACHE[
660
+ prev_char.char + c, prev_char.style
661
+ ]
662
+ new_buffer_row[x + xpos - pw] = char2
663
+
664
+ # Keep track of write position for each character.
665
+ current_rowcol_to_yx[lineno, col + skipped] = (
666
+ y + ypos,
667
+ x + xpos,
668
+ )
669
+
670
+ col += 1
671
+ x += char_width
672
+ return x, y
673
+
674
+ # Copy content.
675
+ def copy() -> int:
676
+ y = -vertical_scroll_2
677
+ lineno = vertical_scroll
678
+
679
+ total = write_position.height
680
+ upper = (total - 1) // 2
681
+ below = total - upper - 1
682
+
683
+ if lineno + total < line_count:
684
+ if isinstance(self.content, SessionBufferControl):
685
+ b = self.content.buffer
686
+ b.split = True
687
+
688
+ while y < upper and lineno < line_count:
689
+ line = ui_content.get_line(lineno)
690
+ visible_line_to_row_col[y] = (lineno, horizontal_scroll)
691
+ x = 0
692
+ x, y = copy_line(line, lineno, x, y, is_input=True)
693
+ lineno += 1
694
+ y += 1
695
+
696
+ x = 0
697
+ x, y = copy_line([("","-"*width)], lineno, x, y, is_input=False)
698
+ y += 1
699
+
700
+ lineno = line_count - below
701
+ while y < total and lineno < line_count:
702
+ line = ui_content.get_line(lineno)
703
+ visible_line_to_row_col[y] = (lineno, horizontal_scroll)
704
+ x = 0
705
+ x, y = copy_line(line, lineno, x, y, is_input=True)
706
+ lineno += 1
707
+ y += 1
708
+
709
+ return y
710
+
711
+ else:
712
+ if isNotMargin and isinstance(self.content, SessionBufferControl):
713
+ b = self.content.buffer
714
+ b.split = False
715
+
716
+ while y < write_position.height and lineno < line_count:
717
+ # Take the next line and copy it in the real screen.
718
+ line = ui_content.get_line(lineno)
719
+
720
+ visible_line_to_row_col[y] = (lineno, horizontal_scroll)
721
+
722
+ # Copy margin and actual line.
723
+ x = 0
724
+ x, y = copy_line(line, lineno, x, y, is_input=True)
725
+
726
+ lineno += 1
727
+ y += 1
728
+ return y
729
+
730
+
731
+
732
+ copy()
733
+
734
+ def cursor_pos_to_screen_pos(row: int, col: int) -> Point:
735
+ "Translate row/col from UIContent to real Screen coordinates."
736
+ try:
737
+ y, x = rowcol_to_yx[row, col]
738
+ except KeyError:
739
+ # Normally this should never happen. (It is a bug, if it happens.)
740
+ # But to be sure, return (0, 0)
741
+ return Point(x=0, y=0)
742
+
743
+ # raise ValueError(
744
+ # 'Invalid position. row=%r col=%r, vertical_scroll=%r, '
745
+ # 'horizontal_scroll=%r, height=%r' %
746
+ # (row, col, vertical_scroll, horizontal_scroll, write_position.height))
747
+ else:
748
+ return Point(x=x, y=y)
749
+
750
+ # Set cursor and menu positions.
751
+ if ui_content.cursor_position:
752
+ screen_cursor_position = cursor_pos_to_screen_pos(
753
+ ui_content.cursor_position.y, ui_content.cursor_position.x
754
+ )
755
+
756
+ if has_focus:
757
+ new_screen.set_cursor_position(self, screen_cursor_position)
758
+
759
+ if always_hide_cursor:
760
+ new_screen.show_cursor = False
761
+ else:
762
+ new_screen.show_cursor = ui_content.show_cursor
763
+
764
+ self._highlight_digraph(new_screen)
765
+
766
+ if highlight_lines:
767
+ self._highlight_cursorlines(
768
+ new_screen,
769
+ screen_cursor_position,
770
+ xpos,
771
+ ypos,
772
+ width,
773
+ write_position.height,
774
+ )
775
+
776
+ # Draw input characters from the input processor queue.
777
+ if has_focus and ui_content.cursor_position:
778
+ self._show_key_processor_key_buffer(new_screen)
779
+
780
+ # Set menu position.
781
+ if ui_content.menu_position:
782
+ new_screen.set_menu_position(
783
+ self,
784
+ cursor_pos_to_screen_pos(
785
+ ui_content.menu_position.y, ui_content.menu_position.x
786
+ ),
787
+ )
788
+
789
+ # Update output screen height.
790
+ new_screen.height = max(new_screen.height, ypos + write_position.height)
791
+
792
+ return visible_line_to_row_col, rowcol_to_yx
793
+
794
+ def _copy_margin(
795
+ self,
796
+ margin_content: UIContent,
797
+ new_screen: Screen,
798
+ write_position: WritePosition,
799
+ move_x: int,
800
+ width: int,
801
+ ) -> None:
802
+ """
803
+ Copy characters from the margin screen to the real screen.
804
+ """
805
+ xpos = write_position.xpos + move_x
806
+ ypos = write_position.ypos
807
+
808
+ margin_write_position = WritePosition(xpos, ypos, width, write_position.height)
809
+ self._copy_body(margin_content, new_screen, margin_write_position, 0, width, isNotMargin=False)
810
+
811
+ def _scroll_down(self) -> None:
812
+ "向下滚屏,处理屏幕分隔"
813
+ info = self.render_info
814
+
815
+ if info is None:
816
+ return
817
+
818
+ if isinstance(self.content, SessionBufferControl):
819
+ b = self.content.buffer
820
+ d = b.document
821
+
822
+ b.exit_selection()
823
+ cur_line = d.cursor_position_row
824
+
825
+ # # 向下滚动时,如果存在自动折行情况,要判断本行被折成了几行,在行内时要逐行移动(此处未调试好)
826
+ # cur_col = d.cursor_position_col
827
+ # line = d.current_line
828
+ # line_width = len(line)
829
+ # line_start = d.translate_row_col_to_index(cur_line, 0)
830
+ # screen_width = info.window_width
831
+
832
+ # offset_y = cur_col // screen_width
833
+ # wraplines = math.ceil(1.0 * line_width / screen_width)
834
+
835
+ if cur_line < info.content_height:
836
+
837
+ # if offset_y < wraplines: # add
838
+ # self.content.move_cursor_right(screen_width) # add
839
+ # else: # add
840
+
841
+ self.content.move_cursor_down()
842
+ self.vertical_scroll = cur_line + 1
843
+
844
+ firstline = d.line_count - len(info.displayed_lines)
845
+ if cur_line >= firstline:
846
+ b.cursor_position = len(b.text)
847
+
848
+ def _scroll_up(self) -> None:
849
+ "向上滚屏,处理屏幕分隔"
850
+ info = self.render_info
851
+
852
+ if info is None:
853
+ return
854
+
855
+ #if info.cursor_position.y >= 1:
856
+ if isinstance(self.content, SessionBufferControl):
857
+ b = self.content.buffer
858
+ d = b.document
859
+
860
+ b.exit_selection()
861
+ cur_line = d.cursor_position_row
862
+ if cur_line > d.line_count - len(info.displayed_lines):
863
+ firstline = d.line_count - len(info.displayed_lines)
864
+ newpos = d.translate_row_col_to_index(firstline, 0)
865
+ b.cursor_position = newpos
866
+ cur_line = d.cursor_position_row
867
+ self.vertical_scroll = cur_line
868
+
869
+ elif cur_line > 0:
870
+ self.content.move_cursor_up()
871
+ self.vertical_scroll = cur_line - 1
872
+
873
+
874
+ class EasternButton(Button):
875
+ "解决增加中文等东亚全宽字符后不对齐问题"
876
+
877
+ def _get_text_fragments(self) -> StyleAndTextTuples:
878
+ # 主要改动在这里
879
+ width = self.width - (
880
+ get_cwidth(self.left_symbol) + get_cwidth(self.right_symbol)
881
+ ) - (get_cwidth(self.text) - len(self.text))
882
+
883
+
884
+ text = (f"{{:^{width}}}").format(self.text)
885
+
886
+ def handler(mouse_event: MouseEvent) -> None:
887
+ if (
888
+ self.handler is not None
889
+ and mouse_event.event_type == MouseEventType.MOUSE_UP
890
+ ):
891
+ self.handler()
892
+
893
+ return [
894
+ ("class:button.arrow", self.left_symbol, handler),
895
+ #("[SetCursorPosition]", ""),
896
+ ("class:button.text", text, handler),
897
+ ("class:button.arrow", self.right_symbol, handler),
898
+ ]
899
+
900
+ class EasternMenuContainer(MenuContainer):
901
+ "解决增加中文等东亚全宽字符后不对齐问题"
902
+
903
+ def _submenu(self, level: int = 0) -> Window:
904
+ def get_text_fragments() -> StyleAndTextTuples:
905
+ result: StyleAndTextTuples = []
906
+ if level < len(self.selected_menu):
907
+ menu = self._get_menu(level)
908
+ if menu.children:
909
+ result.append(("class:menu", Border.TOP_LEFT))
910
+ result.append(("class:menu", Border.HORIZONTAL * (menu.width + 4)))
911
+ result.append(("class:menu", Border.TOP_RIGHT))
912
+ result.append(("", "\n"))
913
+ try:
914
+ selected_item = self.selected_menu[level + 1]
915
+ except IndexError:
916
+ selected_item = -1
917
+
918
+ def one_item(
919
+ i: int, item: MenuItem
920
+ ) -> Iterable[OneStyleAndTextTuple]:
921
+ def mouse_handler(mouse_event: MouseEvent) -> None:
922
+ if item.disabled:
923
+ # The arrow keys can't interact with menu items that are disabled.
924
+ # The mouse shouldn't be able to either.
925
+ return
926
+ hover = mouse_event.event_type == MouseEventType.MOUSE_MOVE
927
+ if (
928
+ mouse_event.event_type == MouseEventType.MOUSE_UP
929
+ or hover
930
+ ):
931
+ app = get_app()
932
+ if not hover and item.handler:
933
+ app.layout.focus_last()
934
+ item.handler()
935
+ else:
936
+ self.selected_menu = self.selected_menu[
937
+ : level + 1
938
+ ] + [i]
939
+
940
+ if i == selected_item:
941
+ yield ("[SetCursorPosition]", "")
942
+ style = "class:menu-bar.selected-item"
943
+ else:
944
+ style = ""
945
+
946
+ yield ("class:menu", Border.VERTICAL)
947
+ if item.text == "-":
948
+ yield (
949
+ style + "class:menu-border",
950
+ f"{Border.HORIZONTAL * (menu.width + 3)}",
951
+ mouse_handler,
952
+ )
953
+ else:
954
+ # 主要改动在这里,其他地方都未更改.
955
+ adj_width = menu.width + 3 - (get_cwidth(item.text) - len(item.text))
956
+ yield (
957
+ style,
958
+ f" {item.text}".ljust(adj_width),
959
+ mouse_handler,
960
+ )
961
+
962
+ if item.children:
963
+ yield (style, ">", mouse_handler)
964
+ else:
965
+ yield (style, " ", mouse_handler)
966
+
967
+ if i == selected_item:
968
+ yield ("[SetMenuPosition]", "")
969
+ yield ("class:menu", Border.VERTICAL)
970
+
971
+ yield ("", "\n")
972
+
973
+ for i, item in enumerate(menu.children):
974
+ result.extend(one_item(i, item))
975
+
976
+ result.append(("class:menu", Border.BOTTOM_LEFT))
977
+ result.append(("class:menu", Border.HORIZONTAL * (menu.width + 4)))
978
+ result.append(("class:menu", Border.BOTTOM_RIGHT))
979
+ return result
980
+
981
+ return Window(FormattedTextControl(get_text_fragments), style="class:menu")
982
+
983
+
984
+
985
+ class MenuItem:
986
+ def __init__(
987
+ self,
988
+ text: str = "",
989
+ handler = None,
990
+ children = None,
991
+ shortcut = None,
992
+ disabled: bool = False,
993
+ ) -> None:
994
+ self.text = text
995
+ self.handler = handler
996
+ self.children = children or []
997
+ self.shortcut = shortcut
998
+ self.disabled = disabled
999
+ self.selected_item = 0
1000
+
1001
+ @property
1002
+ def width(self) -> int:
1003
+ if self.children:
1004
+ return max(get_cwidth(c.text) for c in self.children)
1005
+ else:
1006
+ return 0
1007
+
1008
+
1009
+ class DotDict(dict):
1010
+ """
1011
+ 可以通过点.访问内部key-value对的字典。此类型继承自dict。
1012
+
1013
+ - 由于继承关系,此类型可以使用所有dict可以使用的方法
1014
+ - 额外增加的点.访问方法使用示例如下
1015
+
1016
+ 示例:
1017
+ .. code:: Python
1018
+
1019
+ mydict = DotDict()
1020
+
1021
+ # 以下写内容访问等价
1022
+ mydict["key1"] = "value1"
1023
+ mydict.key1 = "value1"
1024
+
1025
+ # 以下读访问等价
1026
+ val = mydict["key1"]
1027
+ val = mydict.key1
1028
+ """
1029
+
1030
+ def __getattr__(self, __key):
1031
+ if (not __key in self.__dict__) and (not __key.startswith("__")):
1032
+ return self.__getitem__(__key)
1033
+
1034
+ def __setattr__(self, __name: str, __value):
1035
+ if __name in self.__dict__:
1036
+ object.__setattr__(self, __name, __value)
1037
+ else:
1038
+ self.__setitem__(__name, __value)
1039
+
1040
+ def __getstate__(self):
1041
+ return self
1042
+
1043
+ def __setstate__(self, state):
1044
+ self.update(state)
1045
+
1046
+
1047
1047