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/pymud.py CHANGED
@@ -1,1242 +1,1281 @@
1
- import asyncio, functools, re, os, webbrowser, threading
2
- from datetime import datetime
3
- from pathlib import Path
4
- from prompt_toolkit.shortcuts import set_title, radiolist_dialog
5
- from prompt_toolkit.output import ColorDepth
6
- from prompt_toolkit.clipboard.pyperclip import PyperclipClipboard
7
- from prompt_toolkit import HTML
8
- from prompt_toolkit.buffer import Buffer
9
- from prompt_toolkit.application import Application
10
- from prompt_toolkit.filters import Condition
11
- from prompt_toolkit.key_binding import KeyBindings
12
- from prompt_toolkit.layout import ConditionalContainer, Float, VSplit, HSplit, Window, WindowAlign, ScrollbarMargin, NumberedMargin
13
- from prompt_toolkit.layout.layout import Layout
14
- from prompt_toolkit.layout.controls import FormattedTextControl
15
- from prompt_toolkit.layout.dimension import D
16
- from prompt_toolkit.layout.menus import CompletionsMenu
17
- from prompt_toolkit.styles import Style
18
- from prompt_toolkit.widgets import Label, MenuItem, TextArea
19
- from prompt_toolkit.mouse_events import MouseEvent, MouseEventType
20
- from prompt_toolkit.cursor_shapes import CursorShape
21
- from prompt_toolkit.key_binding import KeyPress, KeyPressEvent
22
- from prompt_toolkit.keys import Keys
23
- from prompt_toolkit.filters import (
24
- Condition,
25
- is_true,
26
- to_filter,
27
- )
28
- from prompt_toolkit.formatted_text import (
29
- Template,
30
- )
31
- from prompt_toolkit.layout.processors import (
32
- DisplayMultipleCursors,
33
- HighlightSearchProcessor,
34
- HighlightSelectionProcessor,
35
- )
36
- from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
37
-
38
- from .objects import CodeBlock
39
- from .extras import MudFormatProcessor, SessionBuffer, EasternMenuContainer, VSplitWindow, SessionBufferControl, DotDict
40
- from .modules import Plugin
41
- from .session import Session
42
- from .settings import Settings
43
- from .dialogs import MessageDialog, WelcomeDialog, QueryDialog, NewSessionDialog, LogSelectionDialog
44
-
45
- from enum import Enum
46
-
47
- class STATUS_DISPLAY(Enum):
48
- NONE = 0
49
- HORIZON = 1
50
- VERTICAL = 2
51
- FLOAT = 3
52
-
53
- class PyMudApp:
54
- """
55
- PYMUD程序管理主对象,对窗体、操作及所有会话进行管理。
56
-
57
- PyMudApp对象不需要手动创建,在命令行中执行 ``python -m pymud`` 时会自动创建对象实例。
58
-
59
- 参数:
60
- - ``cfg_data``: 替代配置数据,由本地pymud.cfg文件读取,用于覆盖settings.py中的默认Settings数据
61
-
62
- 可替代字典: 含义请查阅 `应用配置及本地化 <settings.html>`_
63
- - sessions: 用于创建菜单栏会话的字典
64
- - client: 用于配置客户端属性的字典
65
- - text: 用于各默认显示文字内容的字典
66
- - server: 用于服务器选项的配置字典
67
- - styles: 用于显示样式的定义字典
68
- - keys: 用于快捷键定义的字典
69
-
70
- *替代配置按不同的dict使用dict.update进行更新覆盖,因此可以仅指定需替代的部分。*
71
- """
72
-
73
- def __init__(self, cfg_data = None) -> None:
74
- """
75
- 构造PyMudApp对象实例,并加载替代配置。
76
- """
77
-
78
- if cfg_data and isinstance(cfg_data, dict):
79
- for key in cfg_data.keys():
80
- if key == "sessions":
81
- Settings.sessions = cfg_data[key]
82
- elif key == "client":
83
- Settings.client.update(cfg_data[key])
84
- elif key == "text":
85
- Settings.text.update(cfg_data[key])
86
- elif key == "server":
87
- Settings.server.update(cfg_data[key])
88
- elif key == "styles":
89
- Settings.styles.update(cfg_data[key])
90
- elif key == "keys":
91
- Settings.keys.update(cfg_data[key])
92
-
93
- self._mouse_support = True
94
- self._plugins = DotDict() # 增加 插件 字典
95
- self._globals = DotDict() # 增加所有session使用的全局变量
96
- self._onTimerCallbacks = dict()
97
- self.sessions = {}
98
- self.current_session = None
99
- self.status_display = STATUS_DISPLAY(Settings.client["status_display"])
100
-
101
- self.keybindings = KeyBindings()
102
- self.keybindings.add(Keys.PageUp, is_global = True)(self.page_up)
103
- self.keybindings.add(Keys.PageDown, is_global = True)(self.page_down)
104
- self.keybindings.add(Keys.ControlZ, is_global = True)(self.hide_history)
105
- self.keybindings.add(Keys.ControlC, is_global = True)(self.copy_selection) # Control-C 复制文本
106
- self.keybindings.add(Keys.ControlR, is_global = True)(self.copy_selection) # Control-R 复制带有ANSI标记的文本(适用于整行复制)
107
- self.keybindings.add(Keys.Right, is_global = True)(self.complete_autosuggest) # 右箭头补完建议
108
- self.keybindings.add(Keys.Backspace)(self.delete_selection)
109
- self.keybindings.add(Keys.ControlLeft, is_global = True)(self.change_session) # Control-左右箭头切换当前会话
110
- self.keybindings.add(Keys.ControlRight, is_global = True)(self.change_session)
111
- self.keybindings.add(Keys.F1, is_global=True)(lambda event: webbrowser.open(Settings.__website__))
112
- self.keybindings.add(Keys.F2, is_global=True)(self.toggle_mousesupport)
113
-
114
- used_keys = [Keys.PageUp, Keys.PageDown, Keys.ControlZ, Keys.ControlC, Keys.ControlR, Keys.Up, Keys.Down, Keys.Left, Keys.Right, Keys.ControlLeft, Keys.ControlRight, Keys.Backspace, Keys.Delete, Keys.F1, Keys.F2]
115
-
116
- for key, binding in Settings.keys.items():
117
- if (key not in used_keys) and binding and isinstance(binding, str):
118
- self.keybindings.add(key, is_global = True)(self.custom_key_press)
119
-
120
- self.initUI()
121
-
122
- # 对剪贴板进行处理,经测试,android下的termux中,pyperclip无法使用,因此要使用默认的InMemoryClipboard
123
- clipboard = None
124
- try:
125
- clipboard = PyperclipClipboard()
126
- clipboard.set_text("test pyperclip")
127
- clipboard.set_text("")
128
- except:
129
- clipboard = None
130
-
131
- self.app = Application(
132
- layout = Layout(self.root_container, focused_element=self.commandLine),
133
- enable_page_navigation_bindings=True,
134
- style=self.style,
135
- mouse_support=to_filter(self._mouse_support),
136
- full_screen=True,
137
- color_depth=ColorDepth.TRUE_COLOR,
138
- clipboard=clipboard,
139
- key_bindings=self.keybindings,
140
- cursor=CursorShape.BLINKING_UNDERLINE
141
- )
142
-
143
- set_title("{} {}".format(Settings.__appname__, Settings.__version__))
144
- self.set_status(Settings.text["welcome"])
145
-
146
- self.loggers = dict() # 所有记录字典
147
- self.showLog = False # 是否显示记录页
148
- self.logFileShown = '' # 记录页显示的记录文件名
149
- self.logSessionBuffer = SessionBuffer()
150
- self.logSessionBuffer.name = "LOGBUFFER"
151
-
152
- self.load_plugins()
153
-
154
- async def onSystemTimerTick(self):
155
- while True:
156
- await asyncio.sleep(1)
157
- self.app.invalidate()
158
- for callback in self._onTimerCallbacks.values():
159
- if callable(callback):
160
- callback()
161
-
162
- def addTimerTickCallback(self, name, func):
163
- '注册一个系统定时器回调,每1s触发一次。指定name为回调函数关键字,func为回调函数。'
164
- if callable(func) and (not name in self._onTimerCallbacks.keys()):
165
- self._onTimerCallbacks[name] = func
166
-
167
- def removeTimerTickCallback(self, name):
168
- '从系统定时器回调中移除一个回调函数。指定name为回调函数关键字。'
169
- if name in self._onTimerCallbacks.keys():
170
- self._onTimerCallbacks.pop(name)
171
-
172
- def initUI(self):
173
- """初始化UI界面"""
174
- self.style = Style.from_dict(Settings.styles)
175
- self.status_message = ""
176
- self.showHistory = False
177
- self.wrap_lines = True
178
-
179
- self.commandLine = TextArea(
180
- prompt=self.get_input_prompt,
181
- multiline = False,
182
- accept_handler = self.enter_pressed,
183
- height=D(min=1),
184
- auto_suggest = AutoSuggestFromHistory(),
185
- focus_on_click=True,
186
- name = "input",
187
- )
188
-
189
- self.status_bar = VSplit(
190
- [
191
- Window(FormattedTextControl(self.get_statusbar_text), style="class:status", align = WindowAlign.LEFT),
192
- Window(FormattedTextControl(self.get_statusbar_right_text), style="class:status.right", width = D(preferred=40), align = WindowAlign.RIGHT),
193
- ],
194
- height = 1,
195
- style ="class:status"
196
- )
197
-
198
- # 增加状态窗口显示
199
- self.statusView = FormattedTextControl(
200
- text = self.get_statuswindow_text,
201
- show_cursor=False
202
- )
203
-
204
- self.mudFormatProc = MudFormatProcessor()
205
-
206
- self.consoleView = SessionBufferControl(
207
- buffer = None,
208
- input_processors=[
209
- self.mudFormatProc,
210
- HighlightSearchProcessor(),
211
- HighlightSelectionProcessor(),
212
- DisplayMultipleCursors(),
213
- ],
214
- focus_on_click = False,
215
- )
216
-
217
-
218
- self.console = VSplitWindow(
219
- content = self.consoleView,
220
- width = D(preferred = Settings.client["naws_width"]),
221
- height = D(preferred = Settings.client["naws_height"]),
222
- wrap_lines=Condition(lambda: is_true(self.wrap_lines)),
223
- #left_margins=[NumberedMargin()],
224
- #right_margins=[ScrollbarMargin(True)],
225
- style="class:text-area"
226
- )
227
-
228
- console_with_bottom_status = ConditionalContainer(
229
- content = HSplit(
230
- [
231
- self.console,
232
- Window(char = "—", height = 1),
233
- Window(content = self.statusView, height = Settings.client["status_height"]),
234
- ]
235
- ),
236
- filter = to_filter(self.status_display == STATUS_DISPLAY.HORIZON)
237
- )
238
-
239
-
240
- console_with_right_status = ConditionalContainer(
241
- content = VSplit(
242
- [
243
- self.console,
244
- Window(char = "|", width = 1),
245
- Window(content = self.statusView, width = Settings.client["status_width"]),
246
- ]
247
- ),
248
- filter = to_filter(self.status_display == STATUS_DISPLAY.VERTICAL)
249
- )
250
-
251
- console_without_status = ConditionalContainer(
252
- content = self.console,
253
- filter = to_filter(self.status_display == STATUS_DISPLAY.NONE)
254
- )
255
-
256
- body = HSplit(
257
- [
258
- console_without_status,
259
- console_with_right_status,
260
- console_with_bottom_status
261
- ]
262
- )
263
-
264
- fill = functools.partial(Window, style="class:frame.border")
265
- top_row_with_title = VSplit(
266
- [
267
- #fill(width=1, height=1, char=Border.TOP_LEFT),
268
- fill(char = "\u2500"),
269
- fill(width=1, height=1, char="|"),
270
- # Notice: we use `Template` here, because `self.title` can be an
271
- # `HTML` object for instance.
272
- Label(
273
- lambda: Template(" {} ").format(self.get_frame_title),
274
- style="class:frame.label",
275
- dont_extend_width=True,
276
- ),
277
- fill(width=1, height=1, char="|"),
278
- fill(char = "\u2500"),
279
- #fill(width=1, height=1, char=Border.TOP_RIGHT),
280
- ],
281
- height=1,
282
- )
283
-
284
- new_body = HSplit([
285
- top_row_with_title,
286
- body,
287
- fill(height = 1, char = "\u2500"),
288
- ])
289
-
290
- #self.console_frame = Frame(body = body, title = self.get_frame_title)
291
-
292
- self.body = HSplit([
293
- new_body,
294
- #self.console_frame,
295
- self.commandLine,
296
- self.status_bar
297
- ])
298
-
299
- self.root_container = EasternMenuContainer(
300
- body = self.body,
301
- menu_items=[
302
- MenuItem(
303
- Settings.text["world"],
304
- children=self.create_world_menus(),
305
- ),
306
- MenuItem(
307
- Settings.text["session"],
308
- children=[
309
- MenuItem(Settings.text["connect"], handler = self.act_connect),
310
- MenuItem(Settings.text["disconnect"], handler = self.act_discon),
311
- MenuItem(Settings.text["closesession"], handler = self.act_close_session),
312
- MenuItem(Settings.text["autoreconnect"], handler = self.act_autoreconnect),
313
- MenuItem("-", disabled=True),
314
- MenuItem(Settings.text["echoinput"], handler = self.act_echoinput),
315
- MenuItem(Settings.text["nosplit"], handler = self.act_nosplit),
316
- MenuItem(Settings.text["copy"], handler = self.act_copy),
317
- MenuItem(Settings.text["copyraw"], handler = self.act_copyraw),
318
- MenuItem(Settings.text["clearsession"], handler = self.act_clearsession),
319
- MenuItem("-", disabled=True),
320
- MenuItem(Settings.text["reloadconfig"], handler = self.act_reload),
321
- ]
322
- ),
323
-
324
- # MenuItem(
325
- # Settings.text["layout"],
326
- # children = [
327
- # MenuItem(Settings.text["hide"], handler = functools.partial(self.act_change_layout, False)),
328
- # MenuItem(Settings.text["horizon"], handler = functools.partial(self.act_change_layout, True)),
329
- # MenuItem(Settings.text["vertical"], handler = functools.partial(self.act_change_layout, True)),
330
- # ]
331
- # ),
332
-
333
- MenuItem(
334
- Settings.text["help"],
335
- children=[
336
- MenuItem(Settings.text["about"], handler = self.act_about)
337
- ]
338
- ),
339
-
340
- MenuItem(
341
- "", # 增加一个空名称MenuItem,单机后焦点移动至命令行输入处,阻止右侧空白栏点击响应
342
- handler = lambda : self.app.layout.focus(self.commandLine)
343
- )
344
- ],
345
- floats=[
346
- Float(
347
- xcursor=True,
348
- ycursor=True,
349
- content=CompletionsMenu(max_height=16, scroll_offset=1)
350
- )
351
- ],
352
- )
353
-
354
- def create_world_menus(self):
355
- "创建世界子菜单,其中根据本地pymud.cfg中的有关配置创建会话有关子菜单"
356
- menus = []
357
- menus.append(MenuItem(Settings.text["new_session"], handler = self.act_new))
358
- menus.append(MenuItem("-", disabled=True))
359
-
360
- ss = Settings.sessions
361
-
362
- for key, site in ss.items():
363
- menu = MenuItem(key)
364
- for name in site["chars"].keys():
365
- sub = MenuItem(name, handler = functools.partial(self._quickHandleSession, key, name))
366
- menu.children.append(sub)
367
- menus.append(menu)
368
-
369
- menus.append(MenuItem("-", disabled=True))
370
- menus.append(MenuItem(Settings.text["show_log"], handler = self.show_logSelectDialog))
371
- menus.append(MenuItem("-", disabled=True))
372
- menus.append(MenuItem(Settings.text["exit"], handler=self.act_exit))
373
-
374
- return menus
375
-
376
- def invalidate(self):
377
- "刷新显示界面"
378
- self.app.invalidate()
379
-
380
- def scroll(self, lines = 1):
381
- "内容滚动指定行数,小于0为向上滚动,大于0为向下滚动"
382
- if self.current_session:
383
- s = self.current_session
384
- b = s.buffer
385
- elif self.showLog:
386
- b = self.logSessionBuffer
387
-
388
- if isinstance(b, Buffer):
389
- if lines < 0:
390
- b.cursor_up(-1 * lines)
391
- elif lines > 0:
392
- b.cursor_down(lines)
393
-
394
- def page_up(self, event: KeyPressEvent) -> None:
395
- "快捷键PageUp: 用于向上翻页。翻页页数为显示窗口行数的一半减去一行。"
396
- #lines = (self.app.output.get_size().rows - 5) // 2 - 1
397
- lines = self.get_height() // 2 - 1
398
- self.scroll(-1 * lines)
399
-
400
- def page_down(self, event: KeyPressEvent) -> None:
401
- "快捷键PageDown: 用于向下翻页。翻页页数为显示窗口行数的一半减去一行。"
402
- #lines = (self.app.output.get_size().rows - 5) // 2 - 1
403
- lines = self.get_height() // 2 - 1
404
- self.scroll(lines)
405
-
406
- def custom_key_press(self, event: KeyPressEvent):
407
- "自定义快捷键功能实现,根据keys字典配置在当前会话执行指定指令"
408
- if (len(event.key_sequence) == 1) and (event.key_sequence[-1].key in Settings.keys.keys()):
409
- cmd = Settings.keys[event.key_sequence[-1].key]
410
- if self.current_session:
411
- self.current_session.exec_command(cmd)
412
-
413
- def hide_history(self, event: KeyPressEvent) -> None:
414
- """快捷键Ctrl+Z: 关闭历史行显示"""
415
- self.act_nosplit()
416
-
417
- def copy_selection(self, event: KeyPressEvent)-> None:
418
- """快捷键Ctrl+C/Ctrl+R: 复制选择内容。根据按键不同选择文本复制方式和RAW复制方式"""
419
- if event.key_sequence[-1].key == Keys.ControlC:
420
- self.copy()
421
- elif event.key_sequence[-1].key == Keys.ControlR:
422
- self.copy(raw = True)
423
-
424
- def delete_selection(self, event: KeyPressEvent):
425
- event.key_sequence
426
- b = event.current_buffer
427
- if b.selection_state:
428
- event.key_processor.feed(KeyPress(Keys.Delete), first=True)
429
- else:
430
- b.delete_before_cursor(1)
431
-
432
- def complete_autosuggest(self, event: KeyPressEvent):
433
- """快捷键右箭头→: 自动完成建议"""
434
- b = event.current_buffer
435
- if b.cursor_position == len(b.text):
436
- s = b.auto_suggest.get_suggestion(b, b.document)
437
- if s:
438
- b.insert_text(s.text, fire_event=False)
439
- else:
440
- b.cursor_right()
441
-
442
- def change_session(self, event: KeyPressEvent):
443
- """快捷键Ctrl+左右箭头: 切换会话"""
444
- if self.current_session:
445
- current = self.current_session.name
446
- keys = list(self.sessions.keys())
447
- idx = keys.index(current)
448
- count = len(keys)
449
-
450
- if event.key_sequence[-1].key == Keys.ControlRight:
451
- if idx < count - 1:
452
- new_key = keys[idx+1]
453
- self.activate_session(new_key)
454
-
455
- elif (idx == count -1) and self.showLog:
456
- self.showLogInTab()
457
-
458
- elif event.key_sequence[-1].key == Keys.ControlLeft:
459
- if idx > 0:
460
- new_key = keys[idx-1]
461
- self.activate_session(new_key)
462
-
463
- else:
464
- if self.showLog:
465
- if event.key_sequence[-1].key == Keys.ControlLeft:
466
- keys = list(self.sessions.keys())
467
- if len(keys) > 0:
468
- new_key = keys[-1]
469
- self.activate_session(new_key)
470
-
471
- def toggle_mousesupport(self, event: KeyPressEvent):
472
- """快捷键F2: 切换鼠标支持状态。用于远程连接时本地复制命令执行操作"""
473
- self._mouse_support = not self._mouse_support
474
- if self._mouse_support:
475
- self.app.renderer.output.enable_mouse_support()
476
- else:
477
- self.app.renderer.output.disable_mouse_support()
478
-
479
- def copy(self, raw = False):
480
- """
481
- 复制会话中的选中内容
482
-
483
- :param raw: 指定采取文本模式还是ANSI格式模式
484
-
485
- ``注意: 复制的内容仅存在于运行环境的剪贴板中。若使用ssh远程,该复制命令不能访问本地剪贴板。``
486
- """
487
-
488
- b = self.consoleView.buffer
489
- if b.selection_state:
490
- cur1, cur2 = b.selection_state.original_cursor_position, b.document.cursor_position
491
- start, end = min(cur1, cur2), max(cur1, cur2)
492
- srow, scol = b.document.translate_index_to_position(start)
493
- erow, ecol = b.document.translate_index_to_position(end)
494
- # srow, scol = b.document.translate_index_to_position(b.selection_state.original_cursor_position)
495
- # erow, ecol = b.document.translate_index_to_position(b.document.cursor_position)
496
-
497
- if not raw:
498
- # Control-C 复制纯文本
499
- if srow == erow:
500
- # 单行情况
501
- #line = b.document.current_line
502
- line = self.mudFormatProc.line_correction(b.document.current_line)
503
- start = max(0, scol)
504
- end = min(ecol, len(line))
505
- #line_plain = re.sub(r"\x1b\[[0-9;]*[a-zA-Z]", "", line, flags = re.IGNORECASE).replace("\r", "").replace("\x00", "")
506
- line_plain = Session.PLAIN_TEXT_REGX.sub("", line).replace("\r", "").replace("\x00", "")
507
- #line_plain = re.sub("\x1b\\[[^mz]+[mz]", "", line).replace("\r", "").replace("\x00", "")
508
- selection = line_plain[start:end]
509
- self.app.clipboard.set_text(selection)
510
- self.set_status("已复制:{}".format(selection))
511
- if self.current_session:
512
- self.current_session.setVariable("%copy", selection)
513
- else:
514
- # 多行只认行
515
- lines = []
516
- for row in range(srow, erow + 1):
517
- line = b.document.lines[row]
518
- #line_plain = re.sub(r"\x1b\[[0-9;]*[a-zA-Z]", "", line, flags = re.IGNORECASE).replace("\r", "").replace("\x00", "")
519
- line_plain = Session.PLAIN_TEXT_REGX.sub("", line).replace("\r", "").replace("\x00", "")
520
- lines.append(line_plain)
521
-
522
- self.app.clipboard.set_text("\n".join(lines))
523
- self.set_status("已复制:行数{}".format(1 + erow - srow))
524
-
525
- if self.current_session:
526
- self.current_session.setVariable("%copy", "\n".join(lines))
527
-
528
- else:
529
- # Control-R 复制带有ANSI标记的原始内容(对应字符关系会不正确,因此RAW复制时自动整行复制)
530
- if srow == erow:
531
- line = b.document.current_line
532
- self.app.clipboard.set_text(line)
533
- self.set_status("已复制:{}".format(line))
534
-
535
- if self.current_session:
536
- self.current_session.setVariable("%copy", line)
537
-
538
- else:
539
- lines = b.document.lines[srow:erow+1]
540
- copy_raw_text = "".join(lines)
541
- self.app.clipboard.set_text(copy_raw_text)
542
- self.set_status("已复制:行数{}".format(1 + erow - srow))
543
-
544
- if self.current_session:
545
- self.current_session.setVariable("%copy", copy_raw_text)
546
-
547
- # data = self.consoleView.buffer.copy_selection()
548
- # self.app.clipboard.set_data(data)
549
- # self.set_status("已复制:{}".format(data.text))
550
-
551
- # self.current_session.setVariable("%copy", data.text)
552
- else:
553
- self.set_status("未选中任何内容...")
554
-
555
- def create_session(self, name, host, port, encoding = None, after_connect = None, scripts = None, userid = None):
556
- """
557
- 创建一个会话。菜单或者#session命令均调用本函数执行创建会话。
558
-
559
- :param name: 会话名称
560
- :param host: 服务器域名或IP地址
561
- :param port: 端口号
562
- :param encoding: 服务器编码
563
- :param after_connect: 连接后要向服务器发送的内容,用来实现自动登录功能
564
- :param scripts: 要加载的脚本清单
565
- :param userid: 自动登录的ID(获取自cfg文件中的定义,绑定到菜单),将以该值在该会话中创建一个名为id的变量
566
- """
567
- result = False
568
- encoding = encoding or Settings.server["default_encoding"]
569
-
570
- if name not in self.sessions.keys():
571
- session = Session(self, name, host, port, encoding, after_connect, scripts = scripts)
572
- session.setVariable("id", userid)
573
- self.sessions[name] = session
574
- self.activate_session(name)
575
-
576
- for plugin in self._plugins.values():
577
- if isinstance(plugin, Plugin):
578
- plugin.onSessionCreate(session)
579
-
580
- result = True
581
- else:
582
- self.set_status(f"错误!已存在一个名为{name}的会话,请更换名称再试.")
583
-
584
- return result
585
-
586
- def show_logSelectDialog(self):
587
- async def coroutine():
588
- head_line = " {}{}{}".format('记录文件名'.ljust(15), '文件大小'.rjust(16), '最后修改时间'.center(17))
589
-
590
- log_list = list()
591
- files = [f for f in os.listdir('.') if os.path.isfile(f) and f.endswith('.log')]
592
- for file in files:
593
- file = os.path.abspath(file)
594
- filename = os.path.basename(file).ljust(20)
595
- filesize = f"{os.path.getsize(file):,} Bytes".rjust(20)
596
- # ctime = datetime.fromtimestamp(os.path.getctime(file)).strftime('%Y-%m-%d %H:%M:%S').rjust(23)
597
- mtime = datetime.fromtimestamp(os.path.getmtime(file)).strftime('%Y-%m-%d %H:%M:%S').rjust(23)
598
-
599
- file_display_line = "{}{}{}".format(filename, filesize, mtime)
600
- log_list.append((file, file_display_line))
601
-
602
- logDir = os.path.abspath(os.path.join(os.curdir, 'log'))
603
- if os.path.exists(logDir):
604
- files = [f for f in os.listdir(logDir) if f.endswith('.log')]
605
- for file in files:
606
- file = os.path.join(logDir, file)
607
- filename = ('log/' + os.path.basename(file)).ljust(20)
608
- filesize = f"{os.path.getsize(file):,} Bytes".rjust(20)
609
- # ctime = datetime.fromtimestamp(os.path.getctime(file)).strftime('%Y-%m-%d %H:%M:%S').rjust(23)
610
- mtime = datetime.fromtimestamp(os.path.getmtime(file)).strftime('%Y-%m-%d %H:%M:%S').rjust(23)
611
-
612
- file_display_line = "{}{}{}".format(filename, filesize, mtime)
613
- log_list.append((file, file_display_line))
614
-
615
- dialog = LogSelectionDialog(
616
- text = head_line,
617
- values = log_list
618
- )
619
-
620
- result = await self.show_dialog_as_float(dialog)
621
-
622
- if result:
623
- self.logFileShown = result
624
- self.showLogInTab()
625
-
626
- asyncio.ensure_future(coroutine())
627
-
628
- def showLogInTab(self):
629
- "在记录也显示LOG记录"
630
- self.current_session = None
631
- self.showLog = True
632
-
633
- if self.logFileShown:
634
- filename = os.path.abspath(self.logFileShown)
635
- if os.path.exists(filename):
636
- lock = threading.RLock()
637
- lock.acquire()
638
- with open(filename, 'r', encoding = 'utf-8', errors = 'ignore') as file:
639
- self.logSessionBuffer._set_text(file.read())
640
- lock.release()
641
-
642
- self.logSessionBuffer.cursor_position = len(self.logSessionBuffer.text)
643
- self.consoleView.buffer = self.logSessionBuffer
644
- self.app.invalidate()
645
-
646
- def activate_session(self, key):
647
- "激活指定名称的session,并将该session设置为当前session"
648
- session = self.sessions.get(key, None)
649
-
650
- if isinstance(session, Session):
651
- self.current_session = session
652
- self.consoleView.buffer = session.buffer
653
- #self.set_status(Settings.text["session_changed"].format(session.name))
654
- self.app.invalidate()
655
-
656
- def close_session(self):
657
- "关闭当前会话。若当前会话处于连接状态,将弹出对话框以确认。"
658
- async def coroutine():
659
- if self.current_session:
660
- if self.current_session.connected:
661
- dlgQuery = QueryDialog(HTML('<b fg="red">警告</b>'), HTML('<style fg="red">当前会话 {0} 还处于连接状态,确认要关闭?</style>'.format(self.current_session.name)))
662
- result = await self.show_dialog_as_float(dlgQuery)
663
- if result:
664
- self.current_session.disconnect()
665
-
666
- # 增加延时等待确保会话关闭
667
- while self.current_session.connected:
668
- await asyncio.sleep(0.1)
669
-
670
- else:
671
- return
672
-
673
- for plugin in self._plugins.values():
674
- if isinstance(plugin, Plugin):
675
- plugin.onSessionDestroy(self.current_session)
676
-
677
- name = self.current_session.name
678
- self.current_session.closeLoggers()
679
- self.current_session.clean()
680
- self.current_session = None
681
- self.consoleView.buffer = SessionBuffer()
682
- self.sessions.pop(name)
683
- #self.set_status(f"会话 {name} 已关闭")
684
- if len(self.sessions.keys()) > 0:
685
- new_sess = list(self.sessions.keys())[0]
686
- self.activate_session(new_sess)
687
- #self.set_status(f"当前会话已切换为 {self.current_session.name}")
688
-
689
- asyncio.ensure_future(coroutine())
690
-
691
- # 菜单选项操作 - 开始
692
-
693
- def act_new(self):
694
- "菜单: 创建新会话"
695
- async def coroutine():
696
- dlgNew = NewSessionDialog()
697
- result = await self.show_dialog_as_float(dlgNew)
698
- if result:
699
- self.create_session(*result)
700
- return result
701
-
702
- asyncio.ensure_future(coroutine())
703
-
704
- def act_connect(self):
705
- "菜单: 连接/重新连接"
706
- if self.current_session:
707
- self.current_session.handle_connect()
708
-
709
- def act_discon(self):
710
- "菜单: 断开连接"
711
- if self.current_session:
712
- self.current_session.disconnect()
713
-
714
- def act_nosplit(self):
715
- "菜单: 取消分屏"
716
- if self.current_session:
717
- s = self.current_session
718
- b = s.buffer
719
- b.exit_selection()
720
- b.cursor_position = len(b.text)
721
-
722
- elif self.showLog:
723
- b = self.logSessionBuffer
724
- b.exit_selection()
725
- b.cursor_position = len(b.text)
726
-
727
- def act_close_session(self):
728
- "菜单: 关闭当前会话"
729
- if self.current_session:
730
- self.close_session()
731
-
732
- elif self.showLog:
733
- self.showLog = False
734
- self.logSessionBuffer.text = ""
735
- if len(self.sessions.keys()) > 0:
736
- new_sess = list(self.sessions.keys())[0]
737
- self.activate_session(new_sess)
738
-
739
- def act_echoinput(self):
740
- "菜单: 显示/隐藏输入指令"
741
- val = not Settings.client["echo_input"]
742
- Settings.client["echo_input"] = val
743
- if self.current_session:
744
- self.current_session.info(f"回显输入命令被设置为:{'打开' if val else '关闭'}")
745
-
746
- def act_autoreconnect(self):
747
- "菜单: 打开/关闭自动重连"
748
- val = not Settings.client["auto_reconnect"]
749
- Settings.client["auto_reconnect"] = val
750
- if self.current_session:
751
- self.current_session.info(f"自动重连被设置为:{'打开' if val else '关闭'}")
752
-
753
- def act_copy(self):
754
- "菜单: 复制纯文本"
755
- self.copy()
756
-
757
- def act_copyraw(self):
758
- "菜单: 复制(ANSI)"
759
- self.copy(raw = True)
760
-
761
- def act_clearsession(self):
762
- "菜单: 清空会话内容"
763
- self.consoleView.buffer.text = ""
764
-
765
- def act_reload(self):
766
- "菜单: 重新加载脚本配置"
767
- if self.current_session:
768
- self.current_session.handle_reload()
769
-
770
- # 暂未实现该功能
771
- def act_change_layout(self, layout):
772
- #if isinstance(layout, STATUS_DISPLAY):
773
- self.status_display = layout
774
- #self.console_frame.body.reset()
775
- # if layout == STATUS_DISPLAY.HORIZON:
776
- # self.console_frame.body = self.console_with_horizon_status
777
- # elif layout == STATUS_DISPLAY.VERTICAL:
778
- # self.console_frame.body = self.console_with_vertical_status
779
- # elif layout == STATUS_DISPLAY.NONE:
780
- # self.console_frame.body = self.console_without_status
781
-
782
- #self.show_message("布局调整", f"已将布局设置为{layout}")
783
- self.app.invalidate()
784
-
785
- def act_exit(self):
786
- """菜单: 退出"""
787
- async def coroutine():
788
- con_sessions = list()
789
- for session in self.sessions.values():
790
- if session.connected:
791
- con_sessions.append(session.name)
792
-
793
- if len(con_sessions) > 0:
794
- dlgQuery = QueryDialog(HTML('<b fg="red">程序退出警告</b>'), HTML('<style fg="red">尚有 {0} 个会话 {1} 还处于连接状态,确认要关闭?</style>'.format(len(con_sessions), ", ".join(con_sessions))))
795
- result = await self.show_dialog_as_float(dlgQuery)
796
- if result:
797
- for ss_name in con_sessions:
798
- ss = self.sessions[ss_name]
799
- ss.disconnect()
800
-
801
- # 增加延时等待确保会话关闭
802
- while ss.connected:
803
- await asyncio.sleep(0.1)
804
-
805
- for plugin in self._plugins.values():
806
- if isinstance(plugin, Plugin):
807
- plugin.onSessionDestroy(ss)
808
-
809
- else:
810
- return
811
-
812
- self.app.exit()
813
-
814
- asyncio.ensure_future(coroutine())
815
-
816
- def act_about(self):
817
- "菜单: 关于"
818
- dialog_about = WelcomeDialog(True)
819
- self.show_dialog(dialog_about)
820
-
821
- # 菜单选项操作 - 完成
822
-
823
- def get_input_prompt(self):
824
- "命令输入行提示符"
825
- return HTML(Settings.text["input_prompt"])
826
-
827
- def btn_title_clicked(self, name, mouse_event: MouseEvent):
828
- "顶部会话标签点击切换鼠标事件"
829
- if mouse_event.event_type == MouseEventType.MOUSE_UP:
830
- if name == '[LOG]':
831
- self.showLogInTab()
832
- else:
833
- self.activate_session(name)
834
-
835
- def get_frame_title(self):
836
- "顶部会话标题选项卡"
837
- if len(self.sessions.keys()) == 0:
838
- if not self.showLog:
839
- return Settings.__appname__ + " " + Settings.__version__
840
- else:
841
- if self.logFileShown:
842
- return f'[LOG] {self.logFileShown}'
843
- else:
844
- return f'[LOG]'
845
-
846
- title_formatted_list = []
847
- for key, session in self.sessions.items():
848
- if session == self.current_session:
849
- if session.connected:
850
- style = Settings.styles["selected.connected"]
851
- else:
852
- style = Settings.styles["selected"]
853
-
854
- else:
855
- if session.connected:
856
- style = Settings.styles["normal.connected"]
857
- else:
858
- style = Settings.styles["normal"]
859
-
860
- title_formatted_list.append((style, key, functools.partial(self.btn_title_clicked, key)))
861
- title_formatted_list.append(("", " | "))
862
-
863
- if self.showLog:
864
- if self.current_session is None:
865
- style = style = Settings.styles["selected"]
866
- else:
867
- style = Settings.styles["normal"]
868
-
869
- title = f'[LOG] {self.logFileShown}' if self.logFileShown else f'[LOG]'
870
-
871
- title_formatted_list.append((style, title, functools.partial(self.btn_title_clicked, '[LOG]')))
872
- title_formatted_list.append(("", " | "))
873
-
874
- return title_formatted_list[:-1]
875
-
876
- def get_statusbar_text(self):
877
- "状态栏内容"
878
- return [
879
- ("class:status", " "),
880
- ("class:status", self.status_message),
881
- ]
882
-
883
- def get_statusbar_right_text(self):
884
- "状态栏右侧内容"
885
- con_str, mouse_support, tri_status = "", "", ""
886
- if not self._mouse_support:
887
- mouse_support = "鼠标已禁用 "
888
-
889
- if self.current_session:
890
- if self.current_session._ignore:
891
- tri_status = "全局禁用 "
892
-
893
- if not self.current_session.connected:
894
- con_str = "未连接"
895
- else:
896
- dura = self.current_session.duration
897
- DAY, HOUR, MINUTE = 86400, 3600, 60
898
- days, hours, mins, secs = 0,0,0,0
899
- days = dura // DAY
900
- dura = dura - days * DAY
901
- hours = dura // HOUR
902
- dura = dura - hours * HOUR
903
- mins = dura // MINUTE
904
- sec = dura - mins * MINUTE
905
-
906
- if days > 0:
907
- con_str = "已连接:{:.0f}天{:.0f}小时{:.0f}分{:.0f}秒".format(days, hours, mins, sec)
908
- elif hours > 0:
909
- con_str = "已连接:{:.0f}小时{:.0f}分{:.0f}秒".format(hours, mins, sec)
910
- elif mins > 0:
911
- con_str = "已连接:{:.0f}分{:.0f}秒".format(mins, sec)
912
- else:
913
- con_str = "已连接:{:.0f}秒".format(sec)
914
-
915
- return "{}{}{} {} {} ".format(mouse_support, tri_status, con_str, Settings.__appname__, Settings.__version__)
916
-
917
- def get_statuswindow_text(self):
918
- "状态窗口: status_maker 的内容"
919
- text = ""
920
- if self.current_session:
921
- text = self.current_session.get_status()
922
-
923
- return text
924
-
925
- def set_status(self, msg):
926
- """
927
- 在状态栏中上显示消息。可在代码中调用
928
-
929
- :param msg: 要显示的消息
930
- """
931
- self.status_message = msg
932
- self.app.invalidate()
933
-
934
- def _quickHandleSession(self, group, name):
935
- '''
936
- 根据指定的组名和会话角色名,从Settings内容,创建一个会话
937
- '''
938
- handled = False
939
- if name in self.sessions.keys():
940
- self.activate_session(name)
941
- handled = True
942
-
943
- else:
944
- site = Settings.sessions[group]
945
- if name in site["chars"].keys():
946
- host = site["host"]
947
- port = site["port"]
948
- encoding = site["encoding"]
949
- autologin = site["autologin"]
950
- default_script = site["default_script"]
951
-
952
- def_scripts = list()
953
- if isinstance(default_script, str):
954
- def_scripts.extend(default_script.split(","))
955
- elif isinstance(default_script, (list, tuple)):
956
- def_scripts.extend(default_script)
957
-
958
- charinfo = site["chars"][name]
959
-
960
- after_connect = autologin.format(charinfo[0], charinfo[1])
961
- sess_scripts = list()
962
- sess_scripts.extend(def_scripts)
963
-
964
- if len(charinfo) == 3:
965
- session_script = charinfo[2]
966
- if session_script:
967
- if isinstance(session_script, str):
968
- sess_scripts.extend(session_script.split(","))
969
- elif isinstance(session_script, (list, tuple)):
970
- sess_scripts.extend(session_script)
971
-
972
- self.create_session(name, host, port, encoding, after_connect, sess_scripts, charinfo[0])
973
- handled = True
974
-
975
- return handled
976
-
977
-
978
- def handle_session(self, *args):
979
- '''
980
- 嵌入命令 #session 的执行函数,创建一个远程连接会话。
981
- 该函数不应该在代码中直接调用。
982
-
983
- 使用:
984
- - #session {name} {host} {port} {encoding}
985
- - 当不指定 Encoding: 时, 默认使用utf-8编码
986
- - 可以直接使用 #{名称} 切换会话和操作会话命令
987
-
988
- - #session {group}.{name}
989
- - 相当于直接点击菜单{group}下的{name}菜单来创建会话. 当该会话已存在时,切换到该会话
990
-
991
- 参数:
992
- :name: 会话名称
993
- :host: 服务器域名或IP地址
994
- :port: 端口号
995
- :encoding: 编码格式,不指定时默认为 utf8
996
-
997
- :group: 组名, 即配置文件中, sessions 字段下的某个关键字
998
- :name: 会话快捷名称, 上述 group 关键字下的 chars 字段中的某个关键字
999
-
1000
- 示例:
1001
- ``#session {名称} {宿主机} {端口} {编码}``
1002
- 创建一个远程连接会话,使用指定编码格式连接到远程宿主机的指定端口并保存为 {名称} 。其中,编码可以省略,此时使用Settings.server["default_encoding"]的值,默认为utf8
1003
- ``#session newstart mud.pkuxkx.net 8080 GBK``
1004
- 使用GBK编码连接到mud.pkuxkx.net的8080端口,并将该会话命名为newstart
1005
- ``#session newstart mud.pkuxkx.net 8081``
1006
- 使用UTF8编码连接到mud.pkuxkx.net的8081端口,并将该会话命名为newstart
1007
- ``#newstart``
1008
- 将名称为newstart的会话切换为当前会话
1009
- ``#newstart give miui gold``
1010
- 使名称为newstart的会话执行give miui gold指令,但不切换到该会话
1011
-
1012
- ``#session pkuxkx.newstart``
1013
- 通过指定快捷配置创建会话,相当于点击 世界->pkuxkx->newstart 菜单创建会话。若该会话存在,则切换到该会话
1014
-
1015
- 相关命令:
1016
- - #close
1017
- - #exit
1018
-
1019
- '''
1020
-
1021
- nothandle = True
1022
- errmsg = "错误的#session命令"
1023
- if len(args) == 1:
1024
- host_session = args[0]
1025
- if '.' in host_session:
1026
- group, name = host_session.split('.')
1027
- nothandle = not self._quickHandleSession(group, name)
1028
-
1029
- else:
1030
- errmsg = f'通过单一参数快速创建会话时,要使用 group.name 形式,如 #session pkuxkx.newstart'
1031
-
1032
- elif len(args) >= 3:
1033
- session_name = args[0]
1034
- session_host = args[1]
1035
- session_port = int(args[2])
1036
- if len(args) == 4:
1037
- session_encoding = args[3]
1038
- else:
1039
- session_encoding = Settings.server["default_encoding"]
1040
-
1041
- self.create_session(session_name, session_host, session_port, session_encoding)
1042
- nothandle = False
1043
-
1044
- if nothandle:
1045
- self.set_status(errmsg)
1046
-
1047
- def enter_pressed(self, buffer: Buffer):
1048
- "命令行回车按键处理"
1049
- cmd_line = buffer.text
1050
- space_index = cmd_line.find(" ")
1051
-
1052
- if len(cmd_line) == 0:
1053
- if self.current_session:
1054
- self.current_session.writeline("")
1055
-
1056
- elif cmd_line[0] != Settings.client["appcmdflag"]:
1057
- if self.current_session:
1058
- self.current_session.last_command = cmd_line
1059
-
1060
- if cmd_line.startswith("#session"):
1061
- cmd_tuple = cmd_line[1:].split()
1062
- self.handle_session(*cmd_tuple[1:])
1063
-
1064
- else:
1065
- if self.current_session:
1066
- if len(cmd_line) == 0:
1067
- self.current_session.writeline("")
1068
- else:
1069
- try:
1070
- self.current_session.log.log(f"命令行键入: {cmd_line}\n")
1071
-
1072
- cb = CodeBlock(cmd_line)
1073
- cb.execute(self.current_session)
1074
- except Exception as e:
1075
- self.current_session.warning(e)
1076
- self.current_session.exec_command(cmd_line)
1077
- else:
1078
- if cmd_line == "#exit":
1079
- self.act_exit()
1080
- elif (cmd_line == "#close") and self.showLog:
1081
- self.act_close_session()
1082
- else:
1083
- self.set_status("当前没有正在运行的session.")
1084
-
1085
- # 配置:命令行内容保留
1086
- if Settings.client["remain_last_input"]:
1087
- buffer.cursor_position = 0
1088
- buffer.start_selection()
1089
- buffer.cursor_right(len(cmd_line))
1090
- return True
1091
-
1092
- else:
1093
- return False
1094
-
1095
- @property
1096
- def globals(self):
1097
- """
1098
- 全局变量,快捷点访问器
1099
- 用于替代get_globals与set_globals函数的调用
1100
- """
1101
- return self._globals
1102
-
1103
- def get_globals(self, name, default = None):
1104
- """
1105
- 获取PYMUD全局变量
1106
-
1107
- :param name: 全局变量名称
1108
- :param default: 当全局变量不存在时的返回值
1109
- """
1110
- if name in self._globals.keys():
1111
- return self._globals[name]
1112
- else:
1113
- return default
1114
-
1115
- def set_globals(self, name, value):
1116
- """
1117
- 设置PYMUD全局变量
1118
-
1119
- :param name: 全局变量名称
1120
- :param value: 全局变量值。值可以为任何类型。
1121
- """
1122
- self._globals[name] = value
1123
-
1124
- def del_globals(self, name):
1125
- """
1126
- 移除一个PYMUD全局变量
1127
- 移除全局变量是从字典中删除该变量,而不是将其设置为None
1128
-
1129
- :param name: 全局变量名称
1130
- """
1131
- if name in self._globals.keys():
1132
- self._globals.pop(name)
1133
-
1134
- @property
1135
- def plugins(self):
1136
- "所有已加载的插件列表,快捷点访问器"
1137
- return self._plugins
1138
-
1139
- def show_message(self, title, text, modal = True):
1140
- "显示一个消息对话框"
1141
- async def coroutine():
1142
- dialog = MessageDialog(title, text, modal)
1143
- await self.show_dialog_as_float(dialog)
1144
-
1145
- asyncio.ensure_future(coroutine())
1146
-
1147
- def show_dialog(self, dialog):
1148
- "显示一个给定的对话框"
1149
- async def coroutine():
1150
- await self.show_dialog_as_float(dialog)
1151
-
1152
- asyncio.ensure_future(coroutine())
1153
-
1154
- async def show_dialog_as_float(self, dialog):
1155
- "显示弹出式窗口."
1156
- float_ = Float(content=dialog)
1157
- self.root_container.floats.insert(0, float_)
1158
-
1159
- self.app.layout.focus(dialog)
1160
- result = await dialog.future
1161
- self.app.layout.focus(self.commandLine)
1162
-
1163
- if float_ in self.root_container.floats:
1164
- self.root_container.floats.remove(float_)
1165
-
1166
- return result
1167
-
1168
- async def run_async(self):
1169
- "以异步方式运行本程序"
1170
- asyncio.create_task(self.onSystemTimerTick())
1171
- await self.app.run_async(set_exception_handler = False)
1172
-
1173
- def run(self):
1174
- "运行本程序"
1175
- #self.app.run(set_exception_handler = False)
1176
- asyncio.run(self.run_async())
1177
-
1178
- def get_width(self):
1179
- "获取ConsoleView的实际宽度,等于输出宽度,(已经没有左右线条和滚动条了)"
1180
- size = self.app.output.get_size().columns
1181
- if Settings.client["status_display"] == 2:
1182
- size = size - Settings.client["status_width"] - 1
1183
- return size
1184
-
1185
- def get_height(self):
1186
- "获取ConsoleView的实际高度,等于输出高度-5,(上下线条,菜单,命令栏,状态栏)"
1187
- size = self.app.output.get_size().rows - 5
1188
-
1189
- if Settings.client["status_display"] == 1:
1190
- size = size - Settings.client["status_height"] - 1
1191
- return size
1192
-
1193
- #####################################
1194
- # plugins 处理
1195
- #####################################
1196
- def load_plugins(self):
1197
- "加载插件。将加载pymud包的plugins目录下插件,以及当前目录的plugins目录下插件"
1198
- # 首先加载系统目录下的插件
1199
- current_dir = os.path.dirname(__file__)
1200
- plugins_dir = os.path.join(current_dir, "plugins")
1201
- if os.path.exists(plugins_dir):
1202
- for file in os.listdir(plugins_dir):
1203
- if file.endswith(".py"):
1204
- try:
1205
- file_path = os.path.join(plugins_dir, file)
1206
- file_name = file[:-3]
1207
- plugin = Plugin(file_name, file_path)
1208
- self._plugins[plugin.name] = plugin
1209
- plugin.onAppInit(self)
1210
- except Exception as e:
1211
- self.set_status(f"文件: {plugins_dir}\\{file} 不是一个合法的插件文件,加载错误,信息为: {e}")
1212
-
1213
- # 然后加载当前目录下的插件
1214
- current_dir = os.path.abspath(".")
1215
- plugins_dir = os.path.join(current_dir, "plugins")
1216
- if os.path.exists(plugins_dir):
1217
- for file in os.listdir(plugins_dir):
1218
- if file.endswith(".py"):
1219
- try:
1220
- file_path = os.path.join(plugins_dir, file)
1221
- file_name = file[:-3]
1222
- plugin = Plugin(file_name, file_path)
1223
- self._plugins[plugin.name] = plugin
1224
- plugin.onAppInit(self)
1225
- except Exception as e:
1226
- self.set_status(f"文件: {plugins_dir}\\{file} 不是一个合法的插件文件. 加载错误,信息为: {e}")
1227
-
1228
- def reload_plugin(self, plugin: Plugin):
1229
- "重新加载指定插件"
1230
- for session in self.sessions.values():
1231
- plugin.onSessionDestroy(session)
1232
-
1233
- plugin.reload()
1234
- plugin.onAppInit(self)
1235
-
1236
- for session in self.sessions.values():
1237
- plugin.onSessionCreate(session)
1238
-
1239
-
1240
- def startApp(cfg_data = None):
1241
- app = PyMudApp(cfg_data)
1242
- app.run()
1
+ import asyncio, functools, re, os, webbrowser, threading
2
+ from datetime import datetime
3
+ from pathlib import Path
4
+ from prompt_toolkit.shortcuts import set_title, radiolist_dialog
5
+ from prompt_toolkit.output import ColorDepth
6
+ from prompt_toolkit.clipboard.pyperclip import PyperclipClipboard
7
+ from prompt_toolkit import HTML
8
+ from prompt_toolkit.buffer import Buffer
9
+ from prompt_toolkit.application import Application
10
+ from prompt_toolkit.filters import Condition
11
+ from prompt_toolkit.key_binding import KeyBindings
12
+ from prompt_toolkit.layout import ConditionalContainer, Float, VSplit, HSplit, Window, WindowAlign, ScrollbarMargin, NumberedMargin
13
+ from prompt_toolkit.layout.layout import Layout
14
+ from prompt_toolkit.layout.controls import FormattedTextControl
15
+ from prompt_toolkit.layout.dimension import D
16
+ from prompt_toolkit.layout.menus import CompletionsMenu
17
+ from prompt_toolkit.styles import Style
18
+ from prompt_toolkit.widgets import Label, MenuItem, TextArea
19
+ from prompt_toolkit.mouse_events import MouseEvent, MouseEventType
20
+ from prompt_toolkit.cursor_shapes import CursorShape
21
+ from prompt_toolkit.key_binding import KeyPress, KeyPressEvent
22
+ from prompt_toolkit.keys import Keys
23
+ from prompt_toolkit.filters import (
24
+ Condition,
25
+ is_true,
26
+ to_filter,
27
+ )
28
+ from prompt_toolkit.formatted_text import (
29
+ Template,
30
+ )
31
+ from prompt_toolkit.layout.processors import (
32
+ DisplayMultipleCursors,
33
+ HighlightSearchProcessor,
34
+ HighlightSelectionProcessor,
35
+ )
36
+ from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
37
+
38
+ from .objects import CodeBlock
39
+ from .extras import MudFormatProcessor, SessionBuffer, EasternMenuContainer, VSplitWindow, SessionBufferControl, DotDict
40
+ from .modules import Plugin
41
+ from .session import Session
42
+ from .settings import Settings
43
+ from .dialogs import MessageDialog, WelcomeDialog, QueryDialog, NewSessionDialog, LogSelectionDialog
44
+
45
+ from enum import Enum
46
+
47
+ class STATUS_DISPLAY(Enum):
48
+ NONE = 0
49
+ HORIZON = 1
50
+ VERTICAL = 2
51
+ FLOAT = 3
52
+
53
+ class PyMudApp:
54
+ """
55
+ PYMUD程序管理主对象,对窗体、操作及所有会话进行管理。
56
+
57
+ PyMudApp对象不需要手动创建,在命令行中执行 ``python -m pymud`` 时会自动创建对象实例。
58
+
59
+ 参数:
60
+ - ``cfg_data``: 替代配置数据,由本地pymud.cfg文件读取,用于覆盖settings.py中的默认Settings数据
61
+
62
+ 可替代字典: 含义请查阅 `应用配置及本地化 <settings.html>`_
63
+ - sessions: 用于创建菜单栏会话的字典
64
+ - client: 用于配置客户端属性的字典
65
+ - text: 用于各默认显示文字内容的字典
66
+ - server: 用于服务器选项的配置字典
67
+ - styles: 用于显示样式的定义字典
68
+ - keys: 用于快捷键定义的字典
69
+
70
+ *替代配置按不同的dict使用dict.update进行更新覆盖,因此可以仅指定需替代的部分。*
71
+ """
72
+
73
+ def __init__(self, cfg_data = None) -> None:
74
+ """
75
+ 构造PyMudApp对象实例,并加载替代配置。
76
+ """
77
+
78
+ if cfg_data and isinstance(cfg_data, dict):
79
+ for key in cfg_data.keys():
80
+ if key == "sessions":
81
+ Settings.sessions = cfg_data[key]
82
+ elif key == "client":
83
+ Settings.client.update(cfg_data[key])
84
+ elif key == "text":
85
+ Settings.text.update(cfg_data[key])
86
+ elif key == "server":
87
+ Settings.server.update(cfg_data[key])
88
+ elif key == "styles":
89
+ Settings.styles.update(cfg_data[key])
90
+ elif key == "keys":
91
+ Settings.keys.update(cfg_data[key])
92
+
93
+ self._mouse_support = True
94
+ self._plugins = DotDict() # 增加 插件 字典
95
+ self._globals = DotDict() # 增加所有session使用的全局变量
96
+ self._onTimerCallbacks = dict()
97
+ self.sessions = {}
98
+ self.current_session = None
99
+ self.status_display = STATUS_DISPLAY(Settings.client["status_display"])
100
+
101
+ self.keybindings = KeyBindings()
102
+ self.keybindings.add(Keys.PageUp, is_global = True)(self.page_up)
103
+ self.keybindings.add(Keys.PageDown, is_global = True)(self.page_down)
104
+ self.keybindings.add(Keys.ControlZ, is_global = True)(self.hide_history)
105
+ self.keybindings.add(Keys.ControlC, is_global = True)(self.copy_selection) # Control-C 复制文本
106
+ self.keybindings.add(Keys.ControlR, is_global = True)(self.copy_selection) # Control-R 复制带有ANSI标记的文本(适用于整行复制)
107
+ self.keybindings.add(Keys.Right, is_global = True)(self.complete_autosuggest) # 右箭头补完建议
108
+ self.keybindings.add(Keys.Backspace)(self.delete_selection)
109
+ self.keybindings.add(Keys.ControlLeft, is_global = True)(self.change_session) # Control-左右箭头切换当前会话
110
+ self.keybindings.add(Keys.ControlRight, is_global = True)(self.change_session)
111
+ self.keybindings.add(Keys.ShiftLeft, is_global = True)(self.change_session) # Shift-左右箭头切换当前会话
112
+ self.keybindings.add(Keys.ShiftRight, is_global = True)(self.change_session) # 适配 MacOS系统
113
+ self.keybindings.add(Keys.F1, is_global=True)(lambda event: webbrowser.open(Settings.__website__))
114
+ self.keybindings.add(Keys.F2, is_global=True)(self.toggle_mousesupport)
115
+
116
+ used_keys = [Keys.PageUp, Keys.PageDown, Keys.ControlZ, Keys.ControlC, Keys.ControlR, Keys.Up, Keys.Down, Keys.Left, Keys.Right, Keys.ControlLeft, Keys.ControlRight, Keys.Backspace, Keys.Delete, Keys.F1, Keys.F2]
117
+
118
+ for key, binding in Settings.keys.items():
119
+ if (key not in used_keys) and binding and isinstance(binding, str):
120
+ self.keybindings.add(key, is_global = True)(self.custom_key_press)
121
+
122
+ self.initUI()
123
+
124
+ # 对剪贴板进行处理,经测试,android下的termux中,pyperclip无法使用,因此要使用默认的InMemoryClipboard
125
+ clipboard = None
126
+ try:
127
+ clipboard = PyperclipClipboard()
128
+ clipboard.set_text("test pyperclip")
129
+ clipboard.set_text("")
130
+ except:
131
+ clipboard = None
132
+
133
+ self.app = Application(
134
+ layout = Layout(self.root_container, focused_element=self.commandLine),
135
+ enable_page_navigation_bindings=True,
136
+ style=self.style,
137
+ mouse_support=to_filter(self._mouse_support),
138
+ full_screen=True,
139
+ color_depth=ColorDepth.TRUE_COLOR,
140
+ clipboard=clipboard,
141
+ key_bindings=self.keybindings,
142
+ cursor=CursorShape.BLINKING_UNDERLINE
143
+ )
144
+
145
+ set_title("{} {}".format(Settings.__appname__, Settings.__version__))
146
+ self.set_status(Settings.text["welcome"])
147
+
148
+ self.loggers = dict() # 所有记录字典
149
+ self.showLog = False # 是否显示记录页
150
+ self.logFileShown = '' # 记录页显示的记录文件名
151
+ self.logSessionBuffer = SessionBuffer()
152
+ self.logSessionBuffer.name = "LOGBUFFER"
153
+
154
+ self.load_plugins()
155
+
156
+ async def onSystemTimerTick(self):
157
+ while True:
158
+ await asyncio.sleep(1)
159
+ self.app.invalidate()
160
+ for callback in self._onTimerCallbacks.values():
161
+ if callable(callback):
162
+ callback()
163
+
164
+ def addTimerTickCallback(self, name, func):
165
+ '注册一个系统定时器回调,每1s触发一次。指定name为回调函数关键字,func为回调函数。'
166
+ if callable(func) and (not name in self._onTimerCallbacks.keys()):
167
+ self._onTimerCallbacks[name] = func
168
+
169
+ def removeTimerTickCallback(self, name):
170
+ '从系统定时器回调中移除一个回调函数。指定name为回调函数关键字。'
171
+ if name in self._onTimerCallbacks.keys():
172
+ self._onTimerCallbacks.pop(name)
173
+
174
+ def initUI(self):
175
+ """初始化UI界面"""
176
+ self.style = Style.from_dict(Settings.styles)
177
+ self.status_message = ""
178
+ self.showHistory = False
179
+ self.wrap_lines = True
180
+
181
+ self.commandLine = TextArea(
182
+ prompt=self.get_input_prompt,
183
+ multiline = False,
184
+ accept_handler = self.enter_pressed,
185
+ height=D(min=1),
186
+ auto_suggest = AutoSuggestFromHistory(),
187
+ focus_on_click=True,
188
+ name = "input",
189
+ )
190
+
191
+ self.status_bar = VSplit(
192
+ [
193
+ Window(FormattedTextControl(self.get_statusbar_text), style="class:status", align = WindowAlign.LEFT),
194
+ Window(FormattedTextControl(self.get_statusbar_right_text), style="class:status.right", width = D(preferred=40), align = WindowAlign.RIGHT),
195
+ ],
196
+ height = 1,
197
+ style ="class:status"
198
+ )
199
+
200
+ # 增加状态窗口显示
201
+ self.statusView = FormattedTextControl(
202
+ text = self.get_statuswindow_text,
203
+ show_cursor=False
204
+ )
205
+
206
+ self.mudFormatProc = MudFormatProcessor()
207
+
208
+ self.consoleView = SessionBufferControl(
209
+ buffer = None,
210
+ input_processors=[
211
+ self.mudFormatProc,
212
+ HighlightSearchProcessor(),
213
+ HighlightSelectionProcessor(),
214
+ DisplayMultipleCursors(),
215
+ ],
216
+ focus_on_click = False,
217
+ )
218
+
219
+
220
+ self.console = VSplitWindow(
221
+ content = self.consoleView,
222
+ width = D(preferred = Settings.client["naws_width"]),
223
+ height = D(preferred = Settings.client["naws_height"]),
224
+ wrap_lines=Condition(lambda: is_true(self.wrap_lines)),
225
+ #left_margins=[NumberedMargin()],
226
+ #right_margins=[ScrollbarMargin(True)],
227
+ style="class:text-area"
228
+ )
229
+
230
+ console_with_bottom_status = ConditionalContainer(
231
+ content = HSplit(
232
+ [
233
+ self.console,
234
+ ConditionalContainer(content = Window(char = "—", height = 1), filter = Settings.client["status_divider"]),
235
+ #Window(char = "—", height = 1),
236
+ Window(content = self.statusView, height = Settings.client["status_height"]),
237
+ ]
238
+ ),
239
+ filter = to_filter(self.status_display == STATUS_DISPLAY.HORIZON)
240
+ )
241
+
242
+
243
+ console_with_right_status = ConditionalContainer(
244
+ content = VSplit(
245
+ [
246
+ self.console,
247
+ ConditionalContainer(content = Window(char = "|", width = 1), filter = Settings.client["status_divider"]),
248
+ Window(content = self.statusView, width = Settings.client["status_width"]),
249
+ ]
250
+ ),
251
+ filter = to_filter(self.status_display == STATUS_DISPLAY.VERTICAL)
252
+ )
253
+
254
+ console_without_status = ConditionalContainer(
255
+ content = self.console,
256
+ filter = to_filter(self.status_display == STATUS_DISPLAY.NONE)
257
+ )
258
+
259
+ body = HSplit(
260
+ [
261
+ console_without_status,
262
+ console_with_right_status,
263
+ console_with_bottom_status
264
+ ]
265
+ )
266
+
267
+ fill = functools.partial(Window, style="class:frame.border")
268
+ top_row_with_title = VSplit(
269
+ [
270
+ #fill(width=1, height=1, char=Border.TOP_LEFT),
271
+ fill(char = "\u2500"),
272
+ fill(width=1, height=1, char="|"),
273
+ # Notice: we use `Template` here, because `self.title` can be an
274
+ # `HTML` object for instance.
275
+ Label(
276
+ lambda: Template(" {} ").format(self.get_frame_title),
277
+ style="class:frame.label",
278
+ dont_extend_width=True,
279
+ ),
280
+ fill(width=1, height=1, char="|"),
281
+ fill(char = "\u2500"),
282
+ #fill(width=1, height=1, char=Border.TOP_RIGHT),
283
+ ],
284
+ height=1,
285
+ )
286
+
287
+ new_body = HSplit([
288
+ top_row_with_title,
289
+ body,
290
+ fill(height = 1, char = "\u2500"),
291
+ ])
292
+
293
+ #self.console_frame = Frame(body = body, title = self.get_frame_title)
294
+
295
+ self.body = HSplit([
296
+ new_body,
297
+ #self.console_frame,
298
+ self.commandLine,
299
+ self.status_bar
300
+ ])
301
+
302
+ self.root_container = EasternMenuContainer(
303
+ body = self.body,
304
+ menu_items=[
305
+ MenuItem(
306
+ Settings.text["world"],
307
+ children=self.create_world_menus(),
308
+ ),
309
+ MenuItem(
310
+ Settings.text["session"],
311
+ children=[
312
+ MenuItem(Settings.text["connect"], handler = self.act_connect),
313
+ MenuItem(Settings.text["disconnect"], handler = self.act_discon),
314
+ MenuItem(Settings.text["closesession"], handler = self.act_close_session),
315
+ MenuItem(Settings.text["autoreconnect"], handler = self.act_autoreconnect),
316
+ MenuItem("-", disabled=True),
317
+ MenuItem(Settings.text["nosplit"], handler = self.act_nosplit),
318
+ MenuItem(Settings.text["echoinput"], handler = self.act_echoinput),
319
+ MenuItem(Settings.text["beautify"], handler = self.act_beautify),
320
+ MenuItem(Settings.text["copy"], handler = self.act_copy),
321
+ MenuItem(Settings.text["copyraw"], handler = self.act_copyraw),
322
+ MenuItem(Settings.text["clearsession"], handler = self.act_clearsession),
323
+ MenuItem("-", disabled=True),
324
+
325
+ MenuItem(Settings.text["reloadconfig"], handler = self.act_reload),
326
+ ]
327
+ ),
328
+
329
+ # MenuItem(
330
+ # Settings.text["layout"],
331
+ # children = [
332
+ # MenuItem(Settings.text["hide"], handler = functools.partial(self.act_change_layout, False)),
333
+ # MenuItem(Settings.text["horizon"], handler = functools.partial(self.act_change_layout, True)),
334
+ # MenuItem(Settings.text["vertical"], handler = functools.partial(self.act_change_layout, True)),
335
+ # ]
336
+ # ),
337
+
338
+ MenuItem(
339
+ Settings.text["help"],
340
+ children=[
341
+ MenuItem(Settings.text["about"], handler = self.act_about)
342
+ ]
343
+ ),
344
+
345
+ MenuItem(
346
+ "", # 增加一个空名称MenuItem,单机后焦点移动至命令行输入处,阻止右侧空白栏点击响应
347
+ handler = lambda : self.app.layout.focus(self.commandLine)
348
+ )
349
+ ],
350
+ floats=[
351
+ Float(
352
+ xcursor=True,
353
+ ycursor=True,
354
+ content=CompletionsMenu(max_height=16, scroll_offset=1)
355
+ )
356
+ ],
357
+ )
358
+
359
+ def create_world_menus(self):
360
+ "创建世界子菜单,其中根据本地pymud.cfg中的有关配置创建会话有关子菜单"
361
+ menus = []
362
+ menus.append(MenuItem(Settings.text["new_session"], handler = self.act_new))
363
+ menus.append(MenuItem("-", disabled=True))
364
+
365
+ ss = Settings.sessions
366
+
367
+ for key, site in ss.items():
368
+ menu = MenuItem(key)
369
+ for name in site["chars"].keys():
370
+ sub = MenuItem(name, handler = functools.partial(self._quickHandleSession, key, name))
371
+ menu.children.append(sub)
372
+ menus.append(menu)
373
+
374
+ menus.append(MenuItem("-", disabled=True))
375
+ menus.append(MenuItem(Settings.text["show_log"], handler = self.show_logSelectDialog))
376
+ menus.append(MenuItem("-", disabled=True))
377
+ menus.append(MenuItem(Settings.text["exit"], handler=self.act_exit))
378
+
379
+ return menus
380
+
381
+ def invalidate(self):
382
+ "刷新显示界面"
383
+ self.app.invalidate()
384
+
385
+ def scroll(self, lines = 1):
386
+ "内容滚动指定行数,小于0为向上滚动,大于0为向下滚动"
387
+ if self.current_session:
388
+ s = self.current_session
389
+ b = s.buffer
390
+ elif self.showLog:
391
+ b = self.logSessionBuffer
392
+
393
+ if isinstance(b, Buffer):
394
+ if lines < 0:
395
+ b.cursor_up(-1 * lines)
396
+ elif lines > 0:
397
+ b.cursor_down(lines)
398
+
399
+ def page_up(self, event: KeyPressEvent) -> None:
400
+ "快捷键PageUp: 用于向上翻页。翻页页数为显示窗口行数的一半减去一行。"
401
+ #lines = (self.app.output.get_size().rows - 5) // 2 - 1
402
+ lines = self.get_height() // 2 - 1
403
+ self.scroll(-1 * lines)
404
+
405
+ def page_down(self, event: KeyPressEvent) -> None:
406
+ "快捷键PageDown: 用于向下翻页。翻页页数为显示窗口行数的一半减去一行。"
407
+ #lines = (self.app.output.get_size().rows - 5) // 2 - 1
408
+ lines = self.get_height() // 2 - 1
409
+ self.scroll(lines)
410
+
411
+ def custom_key_press(self, event: KeyPressEvent):
412
+ "自定义快捷键功能实现,根据keys字典配置在当前会话执行指定指令"
413
+ if (len(event.key_sequence) == 1) and (event.key_sequence[-1].key in Settings.keys.keys()):
414
+ cmd = Settings.keys[event.key_sequence[-1].key]
415
+ if self.current_session:
416
+ self.current_session.exec_command(cmd)
417
+
418
+ def hide_history(self, event: KeyPressEvent) -> None:
419
+ """快捷键Ctrl+Z: 关闭历史行显示"""
420
+ self.act_nosplit()
421
+
422
+ def copy_selection(self, event: KeyPressEvent)-> None:
423
+ """快捷键Ctrl+C/Ctrl+R: 复制选择内容。根据按键不同选择文本复制方式和RAW复制方式"""
424
+ if event.key_sequence[-1].key == Keys.ControlC:
425
+ self.copy()
426
+ elif event.key_sequence[-1].key == Keys.ControlR:
427
+ self.copy(raw = True)
428
+
429
+ def delete_selection(self, event: KeyPressEvent):
430
+ event.key_sequence
431
+ b = event.current_buffer
432
+ if b.selection_state:
433
+ event.key_processor.feed(KeyPress(Keys.Delete), first=True)
434
+ else:
435
+ b.delete_before_cursor(1)
436
+
437
+ def complete_autosuggest(self, event: KeyPressEvent):
438
+ """快捷键右箭头→: 自动完成建议"""
439
+ b = event.current_buffer
440
+ if b.cursor_position == len(b.text):
441
+ s = b.auto_suggest.get_suggestion(b, b.document)
442
+ if s:
443
+ b.insert_text(s.text, fire_event=False)
444
+ else:
445
+ b.cursor_right()
446
+
447
+ def change_session(self, event: KeyPressEvent):
448
+ """快捷键Ctrl/Shift+左右箭头: 切换会话"""
449
+ if self.current_session:
450
+ current = self.current_session.name
451
+ keys = list(self.sessions.keys())
452
+ idx = keys.index(current)
453
+ count = len(keys)
454
+
455
+ if (event.key_sequence[-1].key == Keys.ControlRight) or (event.key_sequence[-1].key == Keys.ShiftRight):
456
+ if idx < count - 1:
457
+ new_key = keys[idx+1]
458
+ self.activate_session(new_key)
459
+
460
+ elif (idx == count -1) and self.showLog:
461
+ self.showLogInTab()
462
+
463
+ elif (event.key_sequence[-1].key == Keys.ControlLeft) or (event.key_sequence[-1].key == Keys.ShiftLeft):
464
+ if idx > 0:
465
+ new_key = keys[idx-1]
466
+ self.activate_session(new_key)
467
+
468
+ else:
469
+ if self.showLog:
470
+ if (event.key_sequence[-1].key == Keys.ControlRight) or (event.key_sequence[-1].key == Keys.ShiftRight):
471
+ keys = list(self.sessions.keys())
472
+ if len(keys) > 0:
473
+ new_key = keys[-1]
474
+ self.activate_session(new_key)
475
+
476
+ def toggle_mousesupport(self, event: KeyPressEvent):
477
+ """快捷键F2: 切换鼠标支持状态。用于远程连接时本地复制命令执行操作"""
478
+ self._mouse_support = not self._mouse_support
479
+ if self._mouse_support:
480
+ self.app.renderer.output.enable_mouse_support()
481
+ else:
482
+ self.app.renderer.output.disable_mouse_support()
483
+
484
+ def copy(self, raw = False):
485
+ """
486
+ 复制会话中的选中内容
487
+
488
+ :param raw: 指定采取文本模式还是ANSI格式模式
489
+
490
+ ``注意: 复制的内容仅存在于运行环境的剪贴板中。若使用ssh远程,该复制命令不能访问本地剪贴板。``
491
+ """
492
+
493
+ b = self.consoleView.buffer
494
+ if b.selection_state:
495
+ cur1, cur2 = b.selection_state.original_cursor_position, b.document.cursor_position
496
+ start, end = min(cur1, cur2), max(cur1, cur2)
497
+ srow, scol = b.document.translate_index_to_position(start)
498
+ erow, ecol = b.document.translate_index_to_position(end)
499
+ # srow, scol = b.document.translate_index_to_position(b.selection_state.original_cursor_position)
500
+ # erow, ecol = b.document.translate_index_to_position(b.document.cursor_position)
501
+
502
+ if not raw:
503
+ # Control-C 复制纯文本
504
+ if srow == erow:
505
+ # 单行情况
506
+ #line = b.document.current_line
507
+ line = self.mudFormatProc.line_correction(b.document.current_line)
508
+ start = max(0, scol)
509
+ end = min(ecol, len(line))
510
+ #line_plain = re.sub(r"\x1b\[[0-9;]*[a-zA-Z]", "", line, flags = re.IGNORECASE).replace("\r", "").replace("\x00", "")
511
+ line_plain = Session.PLAIN_TEXT_REGX.sub("", line).replace("\r", "").replace("\x00", "")
512
+ #line_plain = re.sub("\x1b\\[[^mz]+[mz]", "", line).replace("\r", "").replace("\x00", "")
513
+ selection = line_plain[start:end]
514
+ self.app.clipboard.set_text(selection)
515
+ self.set_status("已复制:{}".format(selection))
516
+ if self.current_session:
517
+ self.current_session.setVariable("%copy", selection)
518
+ else:
519
+ # 多行只认行
520
+ lines = []
521
+ for row in range(srow, erow + 1):
522
+ line = b.document.lines[row]
523
+ #line_plain = re.sub(r"\x1b\[[0-9;]*[a-zA-Z]", "", line, flags = re.IGNORECASE).replace("\r", "").replace("\x00", "")
524
+ line_plain = Session.PLAIN_TEXT_REGX.sub("", line).replace("\r", "").replace("\x00", "")
525
+ lines.append(line_plain)
526
+
527
+ self.app.clipboard.set_text("\n".join(lines))
528
+ self.set_status("已复制:行数{}".format(1 + erow - srow))
529
+
530
+ if self.current_session:
531
+ self.current_session.setVariable("%copy", "\n".join(lines))
532
+
533
+ else:
534
+ # Control-R 复制带有ANSI标记的原始内容(对应字符关系会不正确,因此RAW复制时自动整行复制)
535
+ if srow == erow:
536
+ line = b.document.current_line
537
+ self.app.clipboard.set_text(line)
538
+ self.set_status("已复制:{}".format(line))
539
+
540
+ if self.current_session:
541
+ self.current_session.setVariable("%copy", line)
542
+
543
+ else:
544
+ lines = b.document.lines[srow:erow+1]
545
+ copy_raw_text = "".join(lines)
546
+ self.app.clipboard.set_text(copy_raw_text)
547
+ self.set_status("已复制:行数{}".format(1 + erow - srow))
548
+
549
+ if self.current_session:
550
+ self.current_session.setVariable("%copy", copy_raw_text)
551
+
552
+ # data = self.consoleView.buffer.copy_selection()
553
+ # self.app.clipboard.set_data(data)
554
+ # self.set_status("已复制:{}".format(data.text))
555
+
556
+ # self.current_session.setVariable("%copy", data.text)
557
+ else:
558
+ self.set_status("未选中任何内容...")
559
+
560
+ def create_session(self, name, host, port, encoding = None, after_connect = None, scripts = None, userid = None):
561
+ """
562
+ 创建一个会话。菜单或者#session命令均调用本函数执行创建会话。
563
+
564
+ :param name: 会话名称
565
+ :param host: 服务器域名或IP地址
566
+ :param port: 端口号
567
+ :param encoding: 服务器编码
568
+ :param after_connect: 连接后要向服务器发送的内容,用来实现自动登录功能
569
+ :param scripts: 要加载的脚本清单
570
+ :param userid: 自动登录的ID(获取自cfg文件中的定义,绑定到菜单),将以该值在该会话中创建一个名为id的变量
571
+ """
572
+ result = False
573
+ encoding = encoding or Settings.server["default_encoding"]
574
+
575
+ if name not in self.sessions.keys():
576
+ session = Session(self, name, host, port, encoding, after_connect, scripts = scripts)
577
+ session.setVariable("id", userid)
578
+ self.sessions[name] = session
579
+ self.activate_session(name)
580
+
581
+ for plugin in self._plugins.values():
582
+ if isinstance(plugin, Plugin):
583
+ plugin.onSessionCreate(session)
584
+
585
+ result = True
586
+ else:
587
+ self.set_status(f"错误!已存在一个名为{name}的会话,请更换名称再试.")
588
+
589
+ return result
590
+
591
+ def show_logSelectDialog(self):
592
+ async def coroutine():
593
+ head_line = " {}{}{}".format('记录文件名'.ljust(15), '文件大小'.rjust(16), '最后修改时间'.center(17))
594
+
595
+ log_list = list()
596
+ files = [f for f in os.listdir('.') if os.path.isfile(f) and f.endswith('.log')]
597
+ for file in files:
598
+ file = os.path.abspath(file)
599
+ filename = os.path.basename(file).ljust(20)
600
+ filesize = f"{os.path.getsize(file):,} Bytes".rjust(20)
601
+ # ctime = datetime.fromtimestamp(os.path.getctime(file)).strftime('%Y-%m-%d %H:%M:%S').rjust(23)
602
+ mtime = datetime.fromtimestamp(os.path.getmtime(file)).strftime('%Y-%m-%d %H:%M:%S').rjust(23)
603
+
604
+ file_display_line = "{}{}{}".format(filename, filesize, mtime)
605
+ log_list.append((file, file_display_line))
606
+
607
+ logDir = os.path.abspath(os.path.join(os.curdir, 'log'))
608
+ if os.path.exists(logDir):
609
+ files = [f for f in os.listdir(logDir) if f.endswith('.log')]
610
+ for file in files:
611
+ file = os.path.join(logDir, file)
612
+ filename = ('log/' + os.path.basename(file)).ljust(20)
613
+ filesize = f"{os.path.getsize(file):,} Bytes".rjust(20)
614
+ # ctime = datetime.fromtimestamp(os.path.getctime(file)).strftime('%Y-%m-%d %H:%M:%S').rjust(23)
615
+ mtime = datetime.fromtimestamp(os.path.getmtime(file)).strftime('%Y-%m-%d %H:%M:%S').rjust(23)
616
+
617
+ file_display_line = "{}{}{}".format(filename, filesize, mtime)
618
+ log_list.append((file, file_display_line))
619
+
620
+ dialog = LogSelectionDialog(
621
+ text = head_line,
622
+ values = log_list
623
+ )
624
+
625
+ result = await self.show_dialog_as_float(dialog)
626
+
627
+ if result:
628
+ self.logFileShown = result
629
+ self.showLogInTab()
630
+
631
+ asyncio.ensure_future(coroutine())
632
+
633
+ def showLogInTab(self):
634
+ "在记录也显示LOG记录"
635
+ self.current_session = None
636
+ self.showLog = True
637
+
638
+ if self.logFileShown:
639
+ filename = os.path.abspath(self.logFileShown)
640
+ if os.path.exists(filename):
641
+ lock = threading.RLock()
642
+ lock.acquire()
643
+ with open(filename, 'r', encoding = 'utf-8', errors = 'ignore') as file:
644
+ self.logSessionBuffer._set_text(file.read())
645
+ lock.release()
646
+
647
+ self.logSessionBuffer.cursor_position = len(self.logSessionBuffer.text)
648
+ self.consoleView.buffer = self.logSessionBuffer
649
+ self.app.invalidate()
650
+
651
+ def activate_session(self, key):
652
+ "激活指定名称的session,并将该session设置为当前session"
653
+ session = self.sessions.get(key, None)
654
+
655
+ if isinstance(session, Session):
656
+ self.current_session = session
657
+ self.consoleView.buffer = session.buffer
658
+ #self.set_status(Settings.text["session_changed"].format(session.name))
659
+ self.app.invalidate()
660
+
661
+ def close_session(self):
662
+ "关闭当前会话。若当前会话处于连接状态,将弹出对话框以确认。"
663
+ async def coroutine():
664
+ if self.current_session:
665
+ if self.current_session.connected:
666
+ dlgQuery = QueryDialog(HTML('<b fg="red">警告</b>'), HTML('<style fg="red">当前会话 {0} 还处于连接状态,确认要关闭?</style>'.format(self.current_session.name)))
667
+ result = await self.show_dialog_as_float(dlgQuery)
668
+ if result:
669
+ self.current_session.disconnect()
670
+
671
+ # 增加延时等待确保会话关闭
672
+ wait_time = 0
673
+ while self.current_session.connected:
674
+ await asyncio.sleep(0.1)
675
+ wait_time += 1
676
+ if wait_time > 100:
677
+ self.current_session.onDisconnected(None)
678
+ break
679
+
680
+ else:
681
+ return
682
+
683
+ for plugin in self._plugins.values():
684
+ if isinstance(plugin, Plugin):
685
+ plugin.onSessionDestroy(self.current_session)
686
+
687
+ name = self.current_session.name
688
+ self.current_session.closeLoggers()
689
+ self.current_session.clean()
690
+ self.current_session = None
691
+ self.consoleView.buffer = SessionBuffer()
692
+ self.sessions.pop(name)
693
+ #self.set_status(f"会话 {name} 已关闭")
694
+ if len(self.sessions.keys()) > 0:
695
+ new_sess = list(self.sessions.keys())[0]
696
+ self.activate_session(new_sess)
697
+ #self.set_status(f"当前会话已切换为 {self.current_session.name}")
698
+
699
+ asyncio.ensure_future(coroutine())
700
+
701
+ # 菜单选项操作 - 开始
702
+
703
+ def act_new(self):
704
+ "菜单: 创建新会话"
705
+ async def coroutine():
706
+ dlgNew = NewSessionDialog()
707
+ result = await self.show_dialog_as_float(dlgNew)
708
+ if result:
709
+ self.create_session(*result)
710
+ return result
711
+
712
+ asyncio.ensure_future(coroutine())
713
+
714
+ def act_connect(self):
715
+ "菜单: 连接/重新连接"
716
+ if self.current_session:
717
+ self.current_session.handle_connect()
718
+
719
+ def act_discon(self):
720
+ "菜单: 断开连接"
721
+ if self.current_session:
722
+ self.current_session.disconnect()
723
+
724
+ def act_nosplit(self):
725
+ "菜单: 取消分屏"
726
+ if self.current_session:
727
+ s = self.current_session
728
+ b = s.buffer
729
+ b.exit_selection()
730
+ b.cursor_position = len(b.text)
731
+
732
+ elif self.showLog:
733
+ b = self.logSessionBuffer
734
+ b.exit_selection()
735
+ b.cursor_position = len(b.text)
736
+
737
+ def act_close_session(self):
738
+ "菜单: 关闭当前会话"
739
+ if self.current_session:
740
+ self.close_session()
741
+
742
+ elif self.showLog:
743
+ self.showLog = False
744
+ self.logSessionBuffer.text = ""
745
+ if len(self.sessions.keys()) > 0:
746
+ new_sess = list(self.sessions.keys())[0]
747
+ self.activate_session(new_sess)
748
+
749
+ def act_beautify(self):
750
+ "菜单: 打开/关闭美化显示"
751
+ val = not Settings.client["beautify"]
752
+ Settings.client["beautify"] = val
753
+ if self.current_session:
754
+ self.current_session.info(f"显示美化已{'打开' if val else '关闭'}!")
755
+
756
+ def act_echoinput(self):
757
+ "菜单: 显示/隐藏输入指令"
758
+ val = not Settings.client["echo_input"]
759
+ Settings.client["echo_input"] = val
760
+ if self.current_session:
761
+ self.current_session.info(f"回显输入命令被设置为:{'打开' if val else '关闭'}")
762
+
763
+ def act_autoreconnect(self):
764
+ "菜单: 打开/关闭自动重连"
765
+ val = not Settings.client["auto_reconnect"]
766
+ Settings.client["auto_reconnect"] = val
767
+ if self.current_session:
768
+ self.current_session.info(f"自动重连被设置为:{'打开' if val else '关闭'}")
769
+
770
+ def act_copy(self):
771
+ "菜单: 复制纯文本"
772
+ self.copy()
773
+
774
+ def act_copyraw(self):
775
+ "菜单: 复制(ANSI)"
776
+ self.copy(raw = True)
777
+
778
+ def act_clearsession(self):
779
+ "菜单: 清空会话内容"
780
+ self.consoleView.buffer.text = ""
781
+
782
+ def act_reload(self):
783
+ "菜单: 重新加载脚本配置"
784
+ if self.current_session:
785
+ self.current_session.handle_reload()
786
+
787
+ # 暂未实现该功能
788
+ def act_change_layout(self, layout):
789
+ #if isinstance(layout, STATUS_DISPLAY):
790
+ self.status_display = layout
791
+ #self.console_frame.body.reset()
792
+ # if layout == STATUS_DISPLAY.HORIZON:
793
+ # self.console_frame.body = self.console_with_horizon_status
794
+ # elif layout == STATUS_DISPLAY.VERTICAL:
795
+ # self.console_frame.body = self.console_with_vertical_status
796
+ # elif layout == STATUS_DISPLAY.NONE:
797
+ # self.console_frame.body = self.console_without_status
798
+
799
+ #self.show_message("布局调整", f"已将布局设置为{layout}")
800
+ self.app.invalidate()
801
+
802
+ def act_exit(self):
803
+ """菜单: 退出"""
804
+ async def coroutine():
805
+ con_sessions = list()
806
+ for session in self.sessions.values():
807
+ if session.connected:
808
+ con_sessions.append(session.name)
809
+
810
+ if len(con_sessions) > 0:
811
+ dlgQuery = QueryDialog(HTML('<b fg="red">程序退出警告</b>'), HTML('<style fg="red">尚有 {0} 个会话 {1} 还处于连接状态,确认要关闭?</style>'.format(len(con_sessions), ", ".join(con_sessions))))
812
+ result = await self.show_dialog_as_float(dlgQuery)
813
+ if result:
814
+ for ss_name in con_sessions:
815
+ ss = self.sessions[ss_name]
816
+ ss.disconnect()
817
+
818
+ # 增加延时等待确保会话关闭
819
+ wait_time = 0
820
+ while ss.connected:
821
+ await asyncio.sleep(0.1)
822
+ wait_time += 1
823
+ if wait_time > 100:
824
+ ss.onDisconnected(None)
825
+ break
826
+
827
+ for plugin in self._plugins.values():
828
+ if isinstance(plugin, Plugin):
829
+ plugin.onSessionDestroy(ss)
830
+
831
+ else:
832
+ return
833
+
834
+ self.app.exit()
835
+
836
+ asyncio.ensure_future(coroutine())
837
+
838
+ def act_about(self):
839
+ "菜单: 关于"
840
+ dialog_about = WelcomeDialog(True)
841
+ self.show_dialog(dialog_about)
842
+
843
+ # 菜单选项操作 - 完成
844
+
845
+ def get_input_prompt(self):
846
+ "命令输入行提示符"
847
+ return HTML(Settings.text["input_prompt"])
848
+
849
+ def btn_title_clicked(self, name, mouse_event: MouseEvent):
850
+ "顶部会话标签点击切换鼠标事件"
851
+ if mouse_event.event_type == MouseEventType.MOUSE_UP:
852
+ if name == '[LOG]':
853
+ self.showLogInTab()
854
+ else:
855
+ self.activate_session(name)
856
+
857
+ def get_frame_title(self):
858
+ "顶部会话标题选项卡"
859
+ if len(self.sessions.keys()) == 0:
860
+ if not self.showLog:
861
+ return Settings.__appname__ + " " + Settings.__version__
862
+ else:
863
+ if self.logFileShown:
864
+ return f'[LOG] {self.logFileShown}'
865
+ else:
866
+ return f'[LOG]'
867
+
868
+ title_formatted_list = []
869
+ for key, session in self.sessions.items():
870
+ if session == self.current_session:
871
+ if session.connected:
872
+ style = Settings.styles["selected.connected"]
873
+ else:
874
+ style = Settings.styles["selected"]
875
+
876
+ else:
877
+ if session.connected:
878
+ style = Settings.styles["normal.connected"]
879
+ else:
880
+ style = Settings.styles["normal"]
881
+
882
+ title_formatted_list.append((style, key, functools.partial(self.btn_title_clicked, key)))
883
+ title_formatted_list.append(("", " | "))
884
+
885
+ if self.showLog:
886
+ if self.current_session is None:
887
+ style = style = Settings.styles["selected"]
888
+ else:
889
+ style = Settings.styles["normal"]
890
+
891
+ title = f'[LOG] {self.logFileShown}' if self.logFileShown else f'[LOG]'
892
+
893
+ title_formatted_list.append((style, title, functools.partial(self.btn_title_clicked, '[LOG]')))
894
+ title_formatted_list.append(("", " | "))
895
+
896
+ return title_formatted_list[:-1]
897
+
898
+ def get_statusbar_text(self):
899
+ "状态栏内容"
900
+ return [
901
+ ("class:status", " "),
902
+ ("class:status", self.status_message),
903
+ ]
904
+
905
+ def get_statusbar_right_text(self):
906
+ "状态栏右侧内容"
907
+ con_str, mouse_support, tri_status, beautify = "", "", "", ""
908
+ if not Settings.client["beautify"]:
909
+ beautify = "美化已关闭 "
910
+
911
+ if not self._mouse_support:
912
+ mouse_support = "鼠标已禁用 "
913
+
914
+ if self.current_session:
915
+ if self.current_session._ignore:
916
+ tri_status = "全局禁用 "
917
+
918
+ if not self.current_session.connected:
919
+ con_str = "未连接"
920
+ else:
921
+ dura = self.current_session.duration
922
+ DAY, HOUR, MINUTE = 86400, 3600, 60
923
+ days, hours, mins, secs = 0,0,0,0
924
+ days = dura // DAY
925
+ dura = dura - days * DAY
926
+ hours = dura // HOUR
927
+ dura = dura - hours * HOUR
928
+ mins = dura // MINUTE
929
+ sec = dura - mins * MINUTE
930
+
931
+ if days > 0:
932
+ con_str = "已连接:{:.0f}天{:.0f}小时{:.0f}分{:.0f}秒".format(days, hours, mins, sec)
933
+ elif hours > 0:
934
+ con_str = "已连接:{:.0f}小时{:.0f}分{:.0f}秒".format(hours, mins, sec)
935
+ elif mins > 0:
936
+ con_str = "已连接:{:.0f}分{:.0f}秒".format(mins, sec)
937
+ else:
938
+ con_str = "已连接:{:.0f}秒".format(sec)
939
+
940
+ return "{}{}{}{} {} {} ".format(beautify, mouse_support, tri_status, con_str, Settings.__appname__, Settings.__version__)
941
+
942
+ def get_statuswindow_text(self):
943
+ "状态窗口: status_maker 的内容"
944
+ text = ""
945
+
946
+ try:
947
+ if self.current_session:
948
+ text = self.current_session.get_status()
949
+ except Exception as e:
950
+ text = f"{e}"
951
+
952
+ return text
953
+
954
+ def set_status(self, msg):
955
+ """
956
+ 在状态栏中上显示消息。可在代码中调用
957
+
958
+ :param msg: 要显示的消息
959
+ """
960
+ self.status_message = msg
961
+ self.app.invalidate()
962
+
963
+ def _quickHandleSession(self, group, name):
964
+ '''
965
+ 根据指定的组名和会话角色名,从Settings内容,创建一个会话
966
+ '''
967
+ handled = False
968
+ if name in self.sessions.keys():
969
+ self.activate_session(name)
970
+ handled = True
971
+
972
+ else:
973
+ site = Settings.sessions[group]
974
+ if name in site["chars"].keys():
975
+ host = site["host"]
976
+ port = site["port"]
977
+ encoding = site["encoding"]
978
+ autologin = site["autologin"]
979
+ default_script = site["default_script"]
980
+
981
+ def_scripts = list()
982
+ if isinstance(default_script, str):
983
+ def_scripts.extend(default_script.split(","))
984
+ elif isinstance(default_script, (list, tuple)):
985
+ def_scripts.extend(default_script)
986
+
987
+ charinfo = site["chars"][name]
988
+
989
+ after_connect = autologin.format(charinfo[0], charinfo[1])
990
+ sess_scripts = list()
991
+ sess_scripts.extend(def_scripts)
992
+
993
+ if len(charinfo) == 3:
994
+ session_script = charinfo[2]
995
+ if session_script:
996
+ if isinstance(session_script, str):
997
+ sess_scripts.extend(session_script.split(","))
998
+ elif isinstance(session_script, (list, tuple)):
999
+ sess_scripts.extend(session_script)
1000
+
1001
+ self.create_session(name, host, port, encoding, after_connect, sess_scripts, charinfo[0])
1002
+ handled = True
1003
+
1004
+ return handled
1005
+
1006
+
1007
+ def handle_session(self, *args):
1008
+ '''
1009
+ 嵌入命令 #session 的执行函数,创建一个远程连接会话。
1010
+ 该函数不应该在代码中直接调用。
1011
+
1012
+ 使用:
1013
+ - #session {name} {host} {port} {encoding}
1014
+ - 当不指定 Encoding: 时, 默认使用utf-8编码
1015
+ - 可以直接使用 #{名称} 切换会话和操作会话命令
1016
+
1017
+ - #session {group}.{name}
1018
+ - 相当于直接点击菜单{group}下的{name}菜单来创建会话. 当该会话已存在时,切换到该会话
1019
+
1020
+ 参数:
1021
+ :name: 会话名称
1022
+ :host: 服务器域名或IP地址
1023
+ :port: 端口号
1024
+ :encoding: 编码格式,不指定时默认为 utf8
1025
+
1026
+ :group: 组名, 即配置文件中, sessions 字段下的某个关键字
1027
+ :name: 会话快捷名称, 上述 group 关键字下的 chars 字段中的某个关键字
1028
+
1029
+ 示例:
1030
+ ``#session {名称} {宿主机} {端口} {编码}``
1031
+ 创建一个远程连接会话,使用指定编码格式连接到远程宿主机的指定端口并保存为 {名称} 。其中,编码可以省略,此时使用Settings.server["default_encoding"]的值,默认为utf8
1032
+ ``#session newstart mud.pkuxkx.net 8080 GBK``
1033
+ 使用GBK编码连接到mud.pkuxkx.net的8080端口,并将该会话命名为newstart
1034
+ ``#session newstart mud.pkuxkx.net 8081``
1035
+ 使用UTF8编码连接到mud.pkuxkx.net的8081端口,并将该会话命名为newstart
1036
+ ``#newstart``
1037
+ 将名称为newstart的会话切换为当前会话
1038
+ ``#newstart give miui gold``
1039
+ 使名称为newstart的会话执行give miui gold指令,但不切换到该会话
1040
+
1041
+ ``#session pkuxkx.newstart``
1042
+ 通过指定快捷配置创建会话,相当于点击 世界->pkuxkx->newstart 菜单创建会话。若该会话存在,则切换到该会话
1043
+
1044
+ 相关命令:
1045
+ - #close
1046
+ - #exit
1047
+
1048
+ '''
1049
+
1050
+ nothandle = True
1051
+ errmsg = "错误的#session命令"
1052
+ if len(args) == 1:
1053
+ host_session = args[0]
1054
+ if '.' in host_session:
1055
+ group, name = host_session.split('.')
1056
+ nothandle = not self._quickHandleSession(group, name)
1057
+
1058
+ else:
1059
+ errmsg = f'通过单一参数快速创建会话时,要使用 group.name 形式,如 #session pkuxkx.newstart'
1060
+
1061
+ elif len(args) >= 3:
1062
+ session_name = args[0]
1063
+ session_host = args[1]
1064
+ session_port = int(args[2])
1065
+ if len(args) == 4:
1066
+ session_encoding = args[3]
1067
+ else:
1068
+ session_encoding = Settings.server["default_encoding"]
1069
+
1070
+ self.create_session(session_name, session_host, session_port, session_encoding)
1071
+ nothandle = False
1072
+
1073
+ if nothandle:
1074
+ self.set_status(errmsg)
1075
+
1076
+ def enter_pressed(self, buffer: Buffer):
1077
+ "命令行回车按键处理"
1078
+ cmd_line = buffer.text
1079
+ space_index = cmd_line.find(" ")
1080
+
1081
+ if len(cmd_line) == 0:
1082
+ if self.current_session:
1083
+ self.current_session.writeline("")
1084
+
1085
+ elif cmd_line[0] != Settings.client["appcmdflag"]:
1086
+ if self.current_session:
1087
+ self.current_session.last_command = cmd_line
1088
+
1089
+ if cmd_line.startswith("#session"):
1090
+ cmd_tuple = cmd_line[1:].split()
1091
+ self.handle_session(*cmd_tuple[1:])
1092
+
1093
+ else:
1094
+ if self.current_session:
1095
+ if len(cmd_line) == 0:
1096
+ self.current_session.writeline("")
1097
+ else:
1098
+ try:
1099
+ self.current_session.log.log(f"命令行键入: {cmd_line}\n")
1100
+
1101
+ cb = CodeBlock(cmd_line)
1102
+ cb.execute(self.current_session)
1103
+ except Exception as e:
1104
+ self.current_session.warning(e)
1105
+ self.current_session.exec_command(cmd_line)
1106
+ else:
1107
+ if cmd_line == "#exit":
1108
+ self.act_exit()
1109
+ elif (cmd_line == "#close") and self.showLog:
1110
+ self.act_close_session()
1111
+ else:
1112
+ self.set_status("当前没有正在运行的session.")
1113
+
1114
+ # 配置:命令行内容保留
1115
+ if Settings.client["remain_last_input"]:
1116
+ buffer.cursor_position = 0
1117
+ buffer.start_selection()
1118
+ buffer.cursor_right(len(cmd_line))
1119
+ return True
1120
+
1121
+ else:
1122
+ return False
1123
+
1124
+ @property
1125
+ def globals(self):
1126
+ """
1127
+ 全局变量,快捷点访问器
1128
+ 用于替代get_globals与set_globals函数的调用
1129
+ """
1130
+ return self._globals
1131
+
1132
+ def get_globals(self, name, default = None):
1133
+ """
1134
+ 获取PYMUD全局变量
1135
+
1136
+ :param name: 全局变量名称
1137
+ :param default: 当全局变量不存在时的返回值
1138
+ """
1139
+ if name in self._globals.keys():
1140
+ return self._globals[name]
1141
+ else:
1142
+ return default
1143
+
1144
+ def set_globals(self, name, value):
1145
+ """
1146
+ 设置PYMUD全局变量
1147
+
1148
+ :param name: 全局变量名称
1149
+ :param value: 全局变量值。值可以为任何类型。
1150
+ """
1151
+ self._globals[name] = value
1152
+
1153
+ def del_globals(self, name):
1154
+ """
1155
+ 移除一个PYMUD全局变量
1156
+ 移除全局变量是从字典中删除该变量,而不是将其设置为None
1157
+
1158
+ :param name: 全局变量名称
1159
+ """
1160
+ if name in self._globals.keys():
1161
+ self._globals.pop(name)
1162
+
1163
+ @property
1164
+ def plugins(self):
1165
+ "所有已加载的插件列表,快捷点访问器"
1166
+ return self._plugins
1167
+
1168
+ def show_message(self, title, text, modal = True):
1169
+ "显示一个消息对话框"
1170
+ async def coroutine():
1171
+ dialog = MessageDialog(title, text, modal)
1172
+ await self.show_dialog_as_float(dialog)
1173
+
1174
+ asyncio.ensure_future(coroutine())
1175
+
1176
+ def show_dialog(self, dialog):
1177
+ "显示一个给定的对话框"
1178
+ async def coroutine():
1179
+ await self.show_dialog_as_float(dialog)
1180
+
1181
+ asyncio.ensure_future(coroutine())
1182
+
1183
+ async def show_dialog_as_float(self, dialog):
1184
+ "显示弹出式窗口."
1185
+ float_ = Float(content=dialog)
1186
+ self.root_container.floats.insert(0, float_)
1187
+
1188
+ self.app.layout.focus(dialog)
1189
+ result = await dialog.future
1190
+ self.app.layout.focus(self.commandLine)
1191
+
1192
+ if float_ in self.root_container.floats:
1193
+ self.root_container.floats.remove(float_)
1194
+
1195
+ return result
1196
+
1197
+ async def run_async(self):
1198
+ "以异步方式运行本程序"
1199
+ # 运行插件启动应用,放在此处,确保插件初始化在event_loop创建完成之后运行
1200
+ for plugin in self._plugins.values():
1201
+ if isinstance(plugin, Plugin):
1202
+ plugin.onAppInit(self)
1203
+
1204
+ asyncio.create_task(self.onSystemTimerTick())
1205
+ await self.app.run_async(set_exception_handler = False)
1206
+
1207
+ # 当应用退出时,运行插件销毁应用
1208
+ for plugin in self._plugins.values():
1209
+ if isinstance(plugin, Plugin):
1210
+ plugin.onAppDestroy(self)
1211
+
1212
+ def run(self):
1213
+ "运行本程序"
1214
+ #self.app.run(set_exception_handler = False)
1215
+ asyncio.run(self.run_async())
1216
+
1217
+ def get_width(self):
1218
+ "获取ConsoleView的实际宽度,等于输出宽度,(已经没有左右线条和滚动条了)"
1219
+ size = self.app.output.get_size().columns
1220
+ if Settings.client["status_display"] == 2:
1221
+ size = size - Settings.client["status_width"] - 1
1222
+ return size
1223
+
1224
+ def get_height(self):
1225
+ "获取ConsoleView的实际高度,等于输出高度-5,(上下线条,菜单,命令栏,状态栏)"
1226
+ size = self.app.output.get_size().rows - 5
1227
+
1228
+ if Settings.client["status_display"] == 1:
1229
+ size = size - Settings.client["status_height"] - 1
1230
+ return size
1231
+
1232
+ #####################################
1233
+ # plugins 处理
1234
+ #####################################
1235
+ def load_plugins(self):
1236
+ "加载插件。将加载pymud包的plugins目录下插件,以及当前目录的plugins目录下插件"
1237
+ # 首先加载系统目录下的插件
1238
+ current_dir = os.path.dirname(__file__)
1239
+ plugins_dir = os.path.join(current_dir, "plugins")
1240
+ if os.path.exists(plugins_dir):
1241
+ for file in os.listdir(plugins_dir):
1242
+ if file.endswith(".py"):
1243
+ try:
1244
+ file_path = os.path.join(plugins_dir, file)
1245
+ file_name = file[:-3]
1246
+ plugin = Plugin(file_name, file_path)
1247
+ self._plugins[plugin.name] = plugin
1248
+ # plugin.onAppInit(self)
1249
+ except Exception as e:
1250
+ self.set_status(f"文件: {plugins_dir}\\{file} 不是一个合法的插件文件,加载错误,信息为: {e}")
1251
+
1252
+ # 然后加载当前目录下的插件
1253
+ current_dir = os.path.abspath(".")
1254
+ plugins_dir = os.path.join(current_dir, "plugins")
1255
+ if os.path.exists(plugins_dir):
1256
+ for file in os.listdir(plugins_dir):
1257
+ if file.endswith(".py"):
1258
+ try:
1259
+ file_path = os.path.join(plugins_dir, file)
1260
+ file_name = file[:-3]
1261
+ plugin = Plugin(file_name, file_path)
1262
+ self._plugins[plugin.name] = plugin
1263
+ plugin.onAppInit(self)
1264
+ except Exception as e:
1265
+ self.set_status(f"文件: {plugins_dir}\\{file} 不是一个合法的插件文件. 加载错误,信息为: {e}")
1266
+
1267
+ def reload_plugin(self, plugin: Plugin):
1268
+ "重新加载指定插件"
1269
+ for session in self.sessions.values():
1270
+ plugin.onSessionDestroy(session)
1271
+
1272
+ plugin.reload()
1273
+ plugin.onAppInit(self)
1274
+
1275
+ for session in self.sessions.values():
1276
+ plugin.onSessionCreate(session)
1277
+
1278
+
1279
+ def startApp(cfg_data = None):
1280
+ app = PyMudApp(cfg_data)
1281
+ app.run()