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