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