diff --git a/.gitignore b/.gitignore index 0a64079..b54ff75 100644 --- a/.gitignore +++ b/.gitignore @@ -150,4 +150,5 @@ dmypy.json **/.jeditor **/user_setting.* bing_cookies.* -**/output \ No newline at end of file +**/output +.claude/settings.local.json diff --git a/PLUGIN_GUIDE.md b/PLUGIN_GUIDE.md new file mode 100644 index 0000000..23ec075 --- /dev/null +++ b/PLUGIN_GUIDE.md @@ -0,0 +1,314 @@ +# PyBreeze Plugin Guide / 插件開發指南 + +PyBreeze (jeditor) supports external plugins for adding **syntax highlighting** and **UI translations**. + +PyBreeze (jeditor) 支援外部插件,可用於新增**語法高亮**和 **UI 翻譯**。 + +--- + +## Quick Start / 快速開始 + +1. Create a `.py` file in the `jeditor_plugins/` directory (under your working directory). +2. Define a `register()` function. +3. Optionally define `PLUGIN_NAME`, `PLUGIN_AUTHOR`, `PLUGIN_VERSION` for the Plugins menu. + + + +1. 在工作目錄下的 `jeditor_plugins/` 建立一個 `.py` 檔案。 +2. 定義一個 `register()` 函式。 +3. 可選:定義 `PLUGIN_NAME`、`PLUGIN_AUTHOR`、`PLUGIN_VERSION`,會顯示在插件選單中。 + +--- + +## Plugin Metadata / 插件元資料 + +```python +PLUGIN_NAME = "My Plugin" # Display name in the Plugins menu / 插件選單顯示名稱 +PLUGIN_AUTHOR = "Your Name" # Author / 作者 +PLUGIN_VERSION = "1.0.0" # Version / 版本號 +``` + +All three are optional. If omitted, the filename is used as the plugin name. + +三者皆為可選。若未定義,則以檔名作為插件名稱。 + +--- + +## Syntax Highlighting Plugin / 語法高亮插件 + +Use `register_programming_language()` to add syntax highlighting for file types. + +使用 `register_programming_language()` 為檔案類型新增語法高亮。 + +### API + +```python +from je_editor.plugins import register_programming_language + +register_programming_language( + suffix=".ext", # File extension / 副檔名 + syntax_words={...}, # Keyword groups / 關鍵字群組 + syntax_rules={...}, # Regex rules (optional) / 正則規則(可選) +) +``` + +### syntax_words format / syntax_words 格式 + +```python +from PySide6.QtGui import QColor + +syntax_words = { + "group_name": { + "words": ("keyword1", "keyword2", ...), # Tuple or set of keywords / 關鍵字元組或集合 + "color": QColor(r, g, b), # Highlight color / 高亮顏色 + }, + # More groups... +} +``` + +### syntax_rules format / syntax_rules 格式 + +```python +syntax_rules = { + "rule_name": { + "rules": (r"regex_pattern", ...), # Tuple of regex patterns / 正則表達式元組 + "color": QColor(r, g, b), # Highlight color / 高亮顏色 + }, +} +``` + +### Full Example / 完整範例 + +```python +"""Go syntax highlighting plugin.""" +from PySide6.QtGui import QColor +from je_editor.plugins import register_programming_language + +PLUGIN_NAME = "Go Syntax Highlighting" +PLUGIN_AUTHOR = "Your Name" +PLUGIN_VERSION = "1.0.0" + +go_syntax_words = { + "keywords": { + "words": ( + "break", "case", "chan", "const", "continue", + "default", "defer", "else", "fallthrough", "for", + "func", "go", "goto", "if", "import", + "interface", "map", "package", "range", "return", + "select", "struct", "switch", "type", "var", + ), + "color": QColor(86, 156, 214), + }, + "types": { + "words": ( + "bool", "byte", "complex64", "complex128", + "float32", "float64", "int", "int8", "int16", + "int32", "int64", "rune", "string", "uint", + "uint8", "uint16", "uint32", "uint64", "uintptr", + "error", "nil", "true", "false", "iota", + ), + "color": QColor(78, 201, 176), + }, +} + +go_syntax_rules = { + "single_line_comment": { + "rules": (r"//[^\n]*",), + "color": QColor(106, 153, 85), + }, +} + + +def register() -> None: + register_programming_language( + suffix=".go", + syntax_words=go_syntax_words, + syntax_rules=go_syntax_rules, + ) +``` + +### Multiple Suffixes / 多個副檔名 + +If a language uses multiple file extensions, register each suffix with the same `syntax_words`: + +若一個語言使用多個副檔名,用相同的 `syntax_words` 分別註冊每個副檔名: + +```python +def register() -> None: + for suffix in (".cpp", ".cxx", ".cc", ".h", ".hpp", ".hxx"): + register_programming_language( + suffix=suffix, + syntax_words=cpp_syntax_words, + syntax_rules=cpp_syntax_rules, + ) +``` + +They will be grouped under one submenu in the Plugins menu. + +它們會在插件選單中合併顯示在同一個子選單下。 + +--- + +## Translation Plugin / 翻譯插件 + +Use `register_natural_language()` to add a new UI language. + +使用 `register_natural_language()` 新增 UI 語言。 + +### API + +```python +from je_editor.plugins import register_natural_language + +register_natural_language( + language_key="French", # Internal key / 內部鍵值 + display_name="Francais", # Shown in Language menu / 語言選單顯示名稱 + word_dict={...}, # Translation dictionary / 翻譯字典 +) +``` + +### word_dict keys / word_dict 鍵值 + +The `word_dict` should contain the same keys as jeditor's built-in `english_word_dict`. +Common keys include: + +`word_dict` 應包含與 jeditor 內建 `english_word_dict` 相同的鍵值。 +常用鍵值包括: + +| Key | Description / 說明 | +|---|---| +| `application_name` | Window title / 視窗標題 | +| `file_menu_label` | File menu / 檔案選單 | +| `run_menu_label` | Run menu / 執行選單 | +| `tab_name_editor` | Editor tab / 編輯器分頁 | +| `language_menu_label` | Language menu / 語言選單 | +| `help_menu_label` | Help menu / 幫助選單 | + +For a complete list, refer to `je_editor.utils.multi_language.english.english_word_dict` +or see the example plugin `exe/jeditor_plugins/french.py`. + +完整鍵值列表請參考 `je_editor.utils.multi_language.english.english_word_dict`, +或參考範例插件 `exe/jeditor_plugins/french.py`。 + +### Full Example / 完整範例 + +```python +"""Japanese translation plugin.""" +from je_editor.plugins import register_natural_language + +PLUGIN_NAME = "Japanese Translation" +PLUGIN_AUTHOR = "Your Name" +PLUGIN_VERSION = "1.0.0" + +japanese_word_dict = { + "application_name": "JEditor", + "file_menu_label": "ファイル", + "run_menu_label": "実行", + "tab_name_editor": "エディタ", + "language_menu_label": "言語", + "language_menu_bar_english": "英語", + "language_menu_bar_traditional_chinese": "繁体字中国語", + "language_menu_bar_please_restart_messagebox": "アプリケーションを再起動してください", + # ... more keys +} + + +def register() -> None: + register_natural_language( + language_key="Japanese", + display_name="日本語", + word_dict=japanese_word_dict, + ) +``` + +--- + +## Run Config (Execute Files) / 執行設定 + +Plugins can register a `PLUGIN_RUN_CONFIG` to enable running files from the **Run with...** menu. + +插件可定義 `PLUGIN_RUN_CONFIG`,讓使用者可以從 **以...執行** 選單執行檔案。 + +### Interpreted Languages / 直譯式語言 + +For languages that run directly (Go, Java 11+, Python): + +直接執行的語言(Go、Java 11+、Python): + +```python +PLUGIN_RUN_CONFIG = { + "name": "Go", # Display name in menu / 選單顯示名稱 + "suffixes": (".go",), # Supported file types / 支援的副檔名 + "compiler": "go", # Executable / 執行檔 + "args": ("run",), # Args before file path / 檔案路徑前的參數 +} +# Runs: go run file.go +``` + +### Compiled Languages / 編譯式語言 + +For languages that need compile-then-run (C, C++, Rust): + +需要先編譯再執行的語言(C、C++、Rust): + +```python +PLUGIN_RUN_CONFIG = { + "name": "C (GCC)", + "suffixes": (".c",), + "compiler": "gcc", + "args": (), + "compile_then_run": True, # Compile first, then run output / 先編譯再執行 + "output_flag": "-o", # Flag for output binary / 輸出檔案的旗標 +} +# Compiles: gcc file.c -o file +# Then runs: ./file (Linux/Mac) or file.exe (Windows) +``` + +### Config Keys / 設定鍵值 + +| Key | Required | Description | +|---|---|---| +| `name` | Yes | Display name / 顯示名稱 | +| `suffixes` | Yes | Tuple of file extensions / 副檔名元組 | +| `compiler` | Yes | Compiler/interpreter executable / 編譯器或直譯器 | +| `args` | No | Extra args before file path / 檔案路徑前的額外參數 | +| `compile_then_run` | No | If `True`, compile first / 若為 `True` 則先編譯 | +| `output_flag` | No | Output file flag (default `"-o"`) / 輸出旗標 | + +--- + +## Directory Structure / 目錄結構 + +``` +working_directory/ + jeditor_plugins/ + my_syntax.py # Single-file plugin / 單檔插件 + my_language.py + my_package/ # Package plugin / 套件插件 + __init__.py +``` + +- Plugins are auto-discovered from `jeditor_plugins/` under the current working directory. +- Files starting with `_` or `.` are ignored. +- Each plugin must have a `register()` function. + + + +- 插件會從工作目錄下的 `jeditor_plugins/` 自動載入。 +- 以 `_` 或 `.` 開頭的檔案會被忽略。 +- 每個插件必須有 `register()` 函式。 + +--- + +## Existing Plugins / 現有插件 + +| Plugin | File | Type | Run Support | +|---|---|---|---| +| C Syntax Highlighting | `c_syntax.py` | Syntax (`.c`) | GCC compile & run | +| C++ Syntax Highlighting | `cpp_syntax.py` | Syntax (`.cpp`, `.cxx`, `.cc`, `.h`, `.hpp`, `.hxx`) | G++ compile & run | +| Go Syntax Highlighting | `go_syntax.py` | Syntax (`.go`) | `go run` | +| Java Syntax Highlighting | `java_syntax.py` | Syntax (`.java`) | `java` | +| Rust Syntax Highlighting | `rust_syntax.py` | Syntax (`.rs`) | rustc compile & run | +| French Translation | `french.py` | Language | - | + +All plugins are located in `exe/jeditor_plugins/`. diff --git a/dev.toml b/dev.toml index 4bb2008..dda7f27 100644 --- a/dev.toml +++ b/dev.toml @@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta" [project] name = "pybreeze_dev" -version = "1.0.10" +version = "1.0.12" authors = [ { name = "JE-Chen", email = "jechenmailman@gmail.com" }, ] diff --git a/pybreeze/__init__.py b/pybreeze/__init__.py index 80807f9..f1ca246 100644 --- a/pybreeze/__init__.py +++ b/pybreeze/__init__.py @@ -1,7 +1,19 @@ -from pybreeze.pybreeze_ui.editor_main.main_ui import PyBreezeMainWindow from pybreeze.pybreeze_ui.editor_main.main_ui import EDITOR_EXTEND_TAB +from pybreeze.pybreeze_ui.editor_main.main_ui import PyBreezeMainWindow from pybreeze.pybreeze_ui.editor_main.main_ui import start_editor +# Re-export jeditor plugin API for convenience +from je_editor import ( + load_external_plugins, + register_natural_language, + register_programming_language, +) + __all__ = [ - "start_editor", "PyBreezeMainWindow", "EDITOR_EXTEND_TAB" + "EDITOR_EXTEND_TAB", + "PyBreezeMainWindow", + "load_external_plugins", + "register_natural_language", + "register_programming_language", + "start_editor", ] diff --git a/pybreeze/__main__.py b/pybreeze/__main__.py index e69de29..a465590 100644 --- a/pybreeze/__main__.py +++ b/pybreeze/__main__.py @@ -0,0 +1,3 @@ +from pybreeze import start_editor + +start_editor() diff --git a/pybreeze/extend/mail_thunder_extend/mail_thunder_setting.py b/pybreeze/extend/mail_thunder_extend/mail_thunder_setting.py index f5f643a..a144208 100644 --- a/pybreeze/extend/mail_thunder_extend/mail_thunder_setting.py +++ b/pybreeze/extend/mail_thunder_extend/mail_thunder_setting.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import sys from email.mime.multipart import MIMEMultipart @@ -5,13 +7,13 @@ from pybreeze.utils.exception.exceptions import ITESendHtmlReportException -def send_after_test(html_report_path: str = None) -> None: +def send_after_test(html_report_path: str | None = None) -> None: try: from je_mail_thunder import SMTPWrapper mail_thunder_smtp: SMTPWrapper = SMTPWrapper() - if html_report_path is None and mail_thunder_smtp.login_state is True: + if html_report_path is None and mail_thunder_smtp.login_state: user: str = mail_thunder_smtp.user - with open("default_name.html", "r+") as file: + with open("default_name.html") as file: html_string: str = file.read() message = mail_thunder_smtp.create_message_with_attach( html_string, @@ -19,9 +21,9 @@ def send_after_test(html_report_path: str = None) -> None: "default_name.html", use_html=True) mail_thunder_smtp.send_message(message) mail_thunder_smtp.quit() - elif mail_thunder_smtp.login_state is True: + elif mail_thunder_smtp.login_state: user: str = mail_thunder_smtp.user - with open(html_report_path, "r+") as file: + with open(html_report_path) as file: html_string: str = file.read() message: MIMEMultipart = mail_thunder_smtp.create_message_with_attach( html_string, diff --git a/pybreeze/extend/process_executor/api_testka/api_testka_process.py b/pybreeze/extend/process_executor/api_testka/api_testka_process.py index 4e227b7..f4a8fac 100644 --- a/pybreeze/extend/process_executor/api_testka/api_testka_process.py +++ b/pybreeze/extend/process_executor/api_testka/api_testka_process.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Union +from typing import TYPE_CHECKING from pybreeze.extend.process_executor.process_executor_utils import build_process @@ -13,7 +13,7 @@ def call_api_testka( main_window: PyBreezeMainWindow, - exec_str: Union[str, None] = None, + exec_str: str | None = None, program_buffer: int = 1024000 ): build_process(main_window, "je_api_testka", exec_str, False, program_buffer) @@ -21,7 +21,7 @@ def call_api_testka( def call_api_testka_with_send( main_window: PyBreezeMainWindow, - exec_str: Union[str, None] = None, + exec_str: str | None = None, program_buffer: int = 1024000 ): build_process(main_window, "je_api_testka", exec_str, True, program_buffer) @@ -36,7 +36,7 @@ def call_api_testka_multi_file( if need_to_execute_list is not None \ and isinstance(need_to_execute_list, list) and len(need_to_execute_list) > 0: for execute_file in need_to_execute_list: - with open(execute_file, "r+") as test_script_json: + with open(execute_file) as test_script_json: call_api_testka( main_window, test_script_json.read(), @@ -56,7 +56,7 @@ def call_api_testka_multi_file_and_send( if need_to_execute_list is not None \ and isinstance(need_to_execute_list, list) and len(need_to_execute_list) > 0: for execute_file in need_to_execute_list: - with open(execute_file, "r+") as test_script_json: + with open(execute_file) as test_script_json: call_api_testka_with_send( main_window, test_script_json.read(), diff --git a/pybreeze/extend/process_executor/auto_control/auto_control_process.py b/pybreeze/extend/process_executor/auto_control/auto_control_process.py index ef6dcce..9683c59 100644 --- a/pybreeze/extend/process_executor/auto_control/auto_control_process.py +++ b/pybreeze/extend/process_executor/auto_control/auto_control_process.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Union +from typing import TYPE_CHECKING from pybreeze.extend.process_executor.process_executor_utils import build_process @@ -13,7 +13,7 @@ def call_auto_control( main_window: PyBreezeMainWindow, - exec_str: Union[str, None] = None, + exec_str: str | None = None, program_buffer: int = 1024000 ): build_process(main_window, "je_auto_control", exec_str, False, program_buffer) @@ -21,7 +21,7 @@ def call_auto_control( def call_auto_control_with_send( main_window: PyBreezeMainWindow, - exec_str: Union[str, None] = None, + exec_str: str | None = None, program_buffer: int = 1024000 ): build_process(main_window, "je_auto_control", exec_str, True, program_buffer) @@ -35,7 +35,7 @@ def call_auto_control_multi_file( if need_to_execute_list is not None \ and isinstance(need_to_execute_list, list) and len(need_to_execute_list) > 0: for execute_file in need_to_execute_list: - with open(execute_file, "r+") as test_script_json: + with open(execute_file) as test_script_json: call_auto_control( main_window, test_script_json.read(), @@ -52,7 +52,7 @@ def call_auto_control_multi_file_and_send( if need_to_execute_list is not None \ and isinstance(need_to_execute_list, list) and len(need_to_execute_list) > 0: for execute_file in need_to_execute_list: - with open(execute_file, "r+") as test_script_json: + with open(execute_file) as test_script_json: call_auto_control_with_send( main_window, test_script_json.read(), diff --git a/pybreeze/extend/process_executor/file_automation/file_automation_process.py b/pybreeze/extend/process_executor/file_automation/file_automation_process.py index 02ca8bf..3635b5b 100644 --- a/pybreeze/extend/process_executor/file_automation/file_automation_process.py +++ b/pybreeze/extend/process_executor/file_automation/file_automation_process.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Union +from typing import TYPE_CHECKING from pybreeze.extend.process_executor.process_executor_utils import build_process @@ -13,7 +13,7 @@ def call_file_automation_test( main_window: PyBreezeMainWindow, - exec_str: Union[str, None] = None, + exec_str: str | None = None, program_buffer: int = 1024000 ): build_process(main_window, "automation_file", exec_str, False, program_buffer) @@ -21,7 +21,7 @@ def call_file_automation_test( def call_file_automation_test_with_send( main_window: PyBreezeMainWindow, - exec_str: Union[str, None] = None, + exec_str: str | None = None, program_buffer: int = 1024000 ): build_process(main_window, "automation_file", exec_str, True, program_buffer) @@ -36,7 +36,7 @@ def call_file_automation_test_multi_file( if need_to_execute_list is not None \ and isinstance(need_to_execute_list, list) and len(need_to_execute_list) > 0: for execute_file in need_to_execute_list: - with open(execute_file, "r+") as test_script_json: + with open(execute_file) as test_script_json: call_file_automation_test( main_window, test_script_json.read(), @@ -56,7 +56,7 @@ def call_file_automation_test_multi_file_and_send( if need_to_execute_list is not None \ and isinstance(need_to_execute_list, list) and len(need_to_execute_list) > 0: for execute_file in need_to_execute_list: - with open(execute_file, "r+") as test_script_json: + with open(execute_file) as test_script_json: call_file_automation_test_with_send( main_window, test_script_json.read(), diff --git a/pybreeze/extend/process_executor/file_runner_process.py b/pybreeze/extend/process_executor/file_runner_process.py new file mode 100644 index 0000000..6d913fb --- /dev/null +++ b/pybreeze/extend/process_executor/file_runner_process.py @@ -0,0 +1,240 @@ +""" +File Runner Process - runs arbitrary language files via plugin run configs. + +Supports two modes: +- Direct run: compiler [args...] file (e.g. go run main.go, java Main.java) +- Compile then run: compiler file -o output && ./output (e.g. gcc main.c -o main) +""" +from __future__ import annotations + +import os +import queue +import subprocess +import sys +from pathlib import Path +from queue import Queue +from threading import Thread + +from PySide6.QtCore import QTimer +from PySide6.QtGui import QTextCharFormat + +from je_editor.pyside_ui.main_ui.save_settings.user_color_setting_file import actually_color_dict + +from pybreeze.pybreeze_ui.show_code_window.code_window import CodeWindow + + +class FileRunnerProcess: + """Manages subprocess execution for any language file.""" + + def __init__( + self, + main_window: CodeWindow, + program_encoding: str = "utf-8", + program_buffer_size: int = 1024, + ): + self.main_window = main_window + self.program_encoding = program_encoding + self.program_buffer_size = program_buffer_size + self.still_running: bool = False + self.process: subprocess.Popen | None = None + self.output_queue: Queue = Queue() + self.error_queue: Queue = Queue() + self.timer: QTimer | None = None + self._stdout_thread: Thread | None = None + self._stderr_thread: Thread | None = None + + def run_file(self, run_config: dict, file_path: str) -> None: + """ + Run a file using the given plugin run config. + + run_config keys: + name: str - display name + compiler: str - executable name (e.g. "go", "gcc", "java") + args: tuple[str] - args between compiler and file (e.g. ("run",)) + compile_then_run: bool (optional) - if True, compile first then run output + output_flag: str (optional) - flag for output file (e.g. "-o") + """ + compile_then_run = run_config.get("compile_then_run", False) + compiler = run_config["compiler"] + args = list(run_config.get("args", ())) + + if compile_then_run: + self._compile_and_run(compiler, args, run_config.get("output_flag", "-o"), file_path) + else: + command = [compiler] + args + [file_path] + self._start_process(command) + + def _compile_and_run(self, compiler: str, args: list, output_flag: str, file_path: str) -> None: + """Compile, then run the output binary.""" + path = Path(file_path) + output_name = str(path.with_suffix("")) + if sys.platform in ("win32", "cygwin", "msys"): + output_name += ".exe" + + compile_cmd = [compiler] + args + [file_path, output_flag, output_name] + self._append_text(f"[Compile] {' '.join(compile_cmd)}\n", is_error=False) + + try: + result = subprocess.run( + compile_cmd, + capture_output=True, + timeout=60, + ) + except FileNotFoundError: + self._append_text(f"[Error] Compiler not found: {compiler}\n", is_error=True) + return + except subprocess.TimeoutExpired: + self._append_text("[Error] Compilation timed out (60s)\n", is_error=True) + return + + if result.stdout: + self._append_text(result.stdout.decode(self.program_encoding, "replace"), is_error=False) + if result.stderr: + self._append_text(result.stderr.decode(self.program_encoding, "replace"), is_error=True) + + if result.returncode != 0: + self._append_text(f"[Compile failed] exit code {result.returncode}\n", is_error=True) + return + + self._append_text(f"[Run] {output_name}\n", is_error=False) + self._start_process([output_name], cleanup_binary=output_name) + + def _start_process(self, command: list[str], cleanup_binary: str | None = None) -> None: + """Launch subprocess and start output reading.""" + self._cleanup_binary = cleanup_binary + + cmd_display = " ".join(command) + self._append_text(f"> {cmd_display}\n", is_error=False) + + try: + self.process = subprocess.Popen( + command, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + stdin=subprocess.PIPE, + shell=False, + ) + except FileNotFoundError: + self._append_text(f"[Error] Command not found: {command[0]}\n", is_error=True) + return + + self.still_running = True + + self._stdout_thread = Thread(target=self._read_stdout, daemon=True) + self._stdout_thread.start() + + self._stderr_thread = Thread(target=self._read_stderr, daemon=True) + self._stderr_thread.start() + + self.main_window.show() + self.timer = QTimer() + self.timer.setInterval(50) + self.timer.timeout.connect(self._pull_text) + self.timer.start() + + def _pull_text(self) -> None: + """Timer callback: pump queues to UI.""" + try: + while not self.output_queue.empty(): + msg = self.output_queue.get_nowait() + msg = str(msg).strip() + if msg: + self._append_text(msg + "\n", is_error=False) + except queue.Empty: + pass + + try: + while not self.error_queue.empty(): + msg = self.error_queue.get_nowait() + msg = str(msg).strip() + if msg: + self._append_text(msg + "\n", is_error=True) + except queue.Empty: + pass + + if self.process is not None: + self.process.poll() + if self.process.returncode is not None: + self._finish() + + def _finish(self) -> None: + """Clean up after process exits.""" + self.still_running = False + if self.timer and self.timer.isActive(): + self.timer.stop() + + # Drain remaining output directly (not via _pull_text to avoid recursion) + self._drain_queues() + + if self.process is not None: + self._append_text( + f"\n[Process exited with code {self.process.returncode}]\n", + is_error=self.process.returncode != 0, + ) + self.process = None + + # Clean up compiled binary + if self._cleanup_binary: + try: + os.remove(self._cleanup_binary) + except OSError: + pass + + def _drain_queues(self) -> None: + """Drain all remaining messages from output/error queues to UI.""" + while not self.output_queue.empty(): + try: + msg = self.output_queue.get_nowait() + msg = str(msg).strip() + if msg: + self._append_text(msg + "\n", is_error=False) + except queue.Empty: + break + while not self.error_queue.empty(): + try: + msg = self.error_queue.get_nowait() + msg = str(msg).strip() + if msg: + self._append_text(msg + "\n", is_error=True) + except queue.Empty: + break + + def _read_stdout(self) -> None: + try: + while self.still_running: + proc = self.process + if proc is None: + break + data = proc.stdout.readline(self.program_buffer_size) + if not data and proc.poll() is not None: + break + if isinstance(data, bytes): + data = data.decode(self.program_encoding, "replace") + if data: + self.output_queue.put(data) + except (OSError, ValueError): + pass + + def _read_stderr(self) -> None: + try: + while self.still_running: + proc = self.process + if proc is None: + break + data = proc.stderr.readline(self.program_buffer_size) + if not data and proc.poll() is not None: + break + if isinstance(data, bytes): + data = data.decode(self.program_encoding, "replace") + if data: + self.error_queue.put(data) + except (OSError, ValueError): + pass + + def _append_text(self, text: str, is_error: bool) -> None: + """Append text to the code result widget.""" + text_cursor = self.main_window.code_result.textCursor() + text_format = QTextCharFormat() + color_key = "error_output_color" if is_error else "normal_output_color" + text_format.setForeground(actually_color_dict.get(color_key)) + text_cursor.insertText(text, text_format) diff --git a/pybreeze/extend/process_executor/load_density/load_density_process.py b/pybreeze/extend/process_executor/load_density/load_density_process.py index 41b6c30..e172f4a 100644 --- a/pybreeze/extend/process_executor/load_density/load_density_process.py +++ b/pybreeze/extend/process_executor/load_density/load_density_process.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Union +from typing import TYPE_CHECKING from pybreeze.extend.process_executor.process_executor_utils import build_process @@ -13,7 +13,7 @@ def call_load_density( main_window: PyBreezeMainWindow, - exec_str: Union[str, None] = None, + exec_str: str | None = None, program_buffer: int = 1024000 ): build_process(main_window, "je_load_density", exec_str, False, program_buffer) @@ -21,7 +21,7 @@ def call_load_density( def call_load_density_with_send( main_window: PyBreezeMainWindow, - exec_str: Union[str, None] = None, + exec_str: str | None = None, program_buffer: int = 1024000 ): build_process(main_window, "je_load_density", exec_str, True, program_buffer) @@ -36,7 +36,7 @@ def call_load_density_multi_file( if need_to_execute_list is not None and isinstance(need_to_execute_list, list) and len( need_to_execute_list) > 0: for execute_file in need_to_execute_list: - with open(execute_file, "r+") as test_script_json: + with open(execute_file) as test_script_json: call_load_density( main_window, test_script_json.read(), @@ -56,7 +56,7 @@ def call_load_density_multi_file_and_send( if need_to_execute_list is not None and isinstance(need_to_execute_list, list) and len( need_to_execute_list) > 0: for execute_file in need_to_execute_list: - with open(execute_file, "r+") as test_script_json: + with open(execute_file) as test_script_json: call_load_density_with_send( main_window, test_script_json.read(), diff --git a/pybreeze/extend/process_executor/mail_thunder/mail_thunder_process.py b/pybreeze/extend/process_executor/mail_thunder/mail_thunder_process.py index 0e5f24a..efc76fa 100644 --- a/pybreeze/extend/process_executor/mail_thunder/mail_thunder_process.py +++ b/pybreeze/extend/process_executor/mail_thunder/mail_thunder_process.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Union +from typing import TYPE_CHECKING from pybreeze.extend.process_executor.process_executor_utils import build_process @@ -10,7 +10,7 @@ def call_mail_thunder( main_window: PyBreezeMainWindow, - exec_str: Union[str, None] = None, + exec_str: str | None = None, program_buffer: int = 1024000 ): build_process(main_window, "je_mail_thunder", exec_str, False, program_buffer) diff --git a/pybreeze/extend/process_executor/process_executor_utils.py b/pybreeze/extend/process_executor/process_executor_utils.py index ea7093a..b512b31 100644 --- a/pybreeze/extend/process_executor/process_executor_utils.py +++ b/pybreeze/extend/process_executor/process_executor_utils.py @@ -2,7 +2,7 @@ import json import sys -from typing import TYPE_CHECKING, Union +from typing import TYPE_CHECKING from je_editor import EditorWidget @@ -19,7 +19,7 @@ def build_process( main_window: PyBreezeMainWindow, package: str, - exec_str: Union[str, None] = None, + exec_str: str | None = None, send_mail: bool = False, program_buffer: int = 1024000, ): @@ -56,13 +56,13 @@ def start_process( if send_mail: process = TaskProcessManager( main_window=code_window, + task_done_trigger_function=send_after_test, program_buffer_size=program_buffer, program_encoding=main_window.encoding ) else: process = TaskProcessManager( code_window, - task_done_trigger_function=send_after_test, program_buffer_size=program_buffer, program_encoding=main_window.encoding ) diff --git a/pybreeze/extend/process_executor/python_task_process_manager.py b/pybreeze/extend/process_executor/python_task_process_manager.py index 7450a1d..80ca3c2 100644 --- a/pybreeze/extend/process_executor/python_task_process_manager.py +++ b/pybreeze/extend/process_executor/python_task_process_manager.py @@ -1,4 +1,5 @@ -import json +from __future__ import annotations + import queue import subprocess import sys @@ -7,7 +8,7 @@ from pathlib import Path from queue import Queue from threading import Thread -from typing import Union + from PySide6.QtCore import QTimer from PySide6.QtGui import QTextCharFormat @@ -17,27 +18,27 @@ from pybreeze.pybreeze_ui.show_code_window.code_window import CodeWindow -class TaskProcessManager(object): +class TaskProcessManager: def __init__( self, main_window: CodeWindow, - task_done_trigger_function: typing.Callable = None, - error_trigger_function: typing.Callable = None, + task_done_trigger_function: typing.Callable | None = None, + error_trigger_function: typing.Callable | None = None, program_buffer_size: int = 1024, program_encoding: str = "utf-8" ): super().__init__() self.compiler_path = None # ite_instance param - self.read_program_error_output_from_thread: Union[threading.Thread, None] = None - self.read_program_output_from_thread: Union[threading.Thread, None] = None + self.read_program_error_output_from_thread: threading.Thread | None = None + self.read_program_output_from_thread: threading.Thread | None = None self.main_window: CodeWindow = main_window self.timer: QTimer = QTimer(self.main_window) self.still_run_program: bool = True self.program_encoding: str = program_encoding self.run_output_queue: Queue = Queue() self.run_error_queue: Queue = Queue() - self.process: Union[subprocess.Popen, None] = None + self.process: subprocess.Popen | None = None self.task_done_trigger_function: typing.Callable = task_done_trigger_function self.error_trigger_function: typing.Callable = error_trigger_function @@ -57,23 +58,19 @@ def renew_path(self) -> None: def start_test_process(self, package: str, exec_str: str): self.renew_path() if sys.platform in ["win32", "cygwin", "msys"]: - exec_str = json.dumps(exec_str) - args = [ - self.compiler_path, - "-m", - package, - "--execute_str", - exec_str - ] - else: - args = " ".join([f"{self.compiler_path}", f"-m {package}", "--execute_str", f"{exec_str}"]) - self.process: subprocess.Popen = subprocess.Popen( + exec_str = __import__("json").dumps(exec_str) + args = [ + str(self.compiler_path), + "-m", + package, + "--execute_str", + exec_str + ] + self.process = subprocess.Popen( args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, - shell=True, - encoding=self.program_encoding ) self.still_run_program = True # program output message queue thread @@ -143,7 +140,7 @@ def exit_program(self): self.read_program_output_from_thread = None if self.read_program_error_output_from_thread is not None: self.read_program_error_output_from_thread = None - self.print_and_clear_queue() + self.drain_and_display_queue() if self.process is not None: self.process.terminate() text_cursor = self.main_window.code_result.textCursor() @@ -152,26 +149,56 @@ def exit_program(self): text_cursor.insertText(f"Task exit with code {self.process.returncode}", text_format) text_cursor.insertBlock() self.process = None + if self.task_done_trigger_function is not None: + try: + self.task_done_trigger_function() + except Exception as e: + print(repr(e), file=sys.stderr) - def print_and_clear_queue(self): - self.run_output_queue = queue.Queue() - self.run_error_queue = queue.Queue() + def drain_and_display_queue(self): + while not self.run_output_queue.empty(): + try: + output_message = self.run_output_queue.get_nowait() + output_message = str(output_message).strip() + if output_message: + text_cursor = self.main_window.code_result.textCursor() + text_format = QTextCharFormat() + text_format.setForeground(actually_color_dict.get("normal_output_color")) + text_cursor.insertText(output_message, text_format) + text_cursor.insertBlock() + except queue.Empty: + break + while not self.run_error_queue.empty(): + try: + error_message = self.run_error_queue.get_nowait() + error_message = str(error_message).strip() + if error_message: + text_cursor = self.main_window.code_result.textCursor() + text_format = QTextCharFormat() + text_format.setForeground(actually_color_dict.get("error_output_color")) + text_cursor.insertText(error_message, text_format) + text_cursor.insertBlock() + except queue.Empty: + break def read_program_output_from_process(self): while self.still_run_program: - self.process: subprocess.Popen - program_output_data = self.process.stdout.readline(self.program_buffer_size) \ - .decode("utf-8", "replace") - if self.process: - self.process.stdout.flush() - if program_output_data.strip() != "": + proc = self.process + if proc is None: + break + program_output_data = proc.stdout.readline(self.program_buffer_size) + if isinstance(program_output_data, bytes): + program_output_data = program_output_data.decode(self.program_encoding, "replace") + if program_output_data.strip(): self.run_output_queue.put(program_output_data) def read_program_error_output_from_process(self): while self.still_run_program: - program_error_output_data = self.process.stderr.readline(self.program_buffer_size) \ - .decode("utf-8", "replace") - if self.process: - self.process.stderr.flush() - if program_error_output_data.strip() != "": + proc = self.process + if proc is None: + break + program_error_output_data = proc.stderr.readline(self.program_buffer_size) + if isinstance(program_error_output_data, bytes): + program_error_output_data = program_error_output_data.decode(self.program_encoding, "replace") + if program_error_output_data.strip(): self.run_error_queue.put(program_error_output_data) diff --git a/pybreeze/extend/process_executor/test_pioneer/test_pioneer_process_manager.py b/pybreeze/extend/process_executor/test_pioneer/test_pioneer_process_manager.py index 3f79f85..68f88eb 100644 --- a/pybreeze/extend/process_executor/test_pioneer/test_pioneer_process_manager.py +++ b/pybreeze/extend/process_executor/test_pioneer/test_pioneer_process_manager.py @@ -6,7 +6,7 @@ import threading from pathlib import Path from queue import Queue -from typing import TYPE_CHECKING, Union +from typing import TYPE_CHECKING from PySide6.QtCore import QTimer from PySide6.QtGui import QTextCharFormat @@ -20,7 +20,7 @@ from pybreeze.pybreeze_ui.editor_main.main_ui import PyBreezeMainWindow -class TestPioneerProcess(object): +class TestPioneerProcess: def __init__( self, @@ -37,10 +37,11 @@ def __init__( self._main_window.clear_code_result() self._still_run_program: bool = False self._program_buffer_size = program_buffer + self._program_encoding = encoding self._run_output_queue: Queue = Queue() self._run_error_queue: Queue = Queue() - self._read_program_error_output_from_thread: Union[threading.Thread, None] = None - self._read_program_output_from_thread: Union[threading.Thread, None] = None + self._read_program_error_output_from_thread: threading.Thread | None = None + self._read_program_output_from_thread: threading.Thread | None = None self._timer: QTimer = QTimer(self._code_window) if self._main_window.python_compiler is None: # Renew compiler path @@ -51,25 +52,18 @@ def __init__( self._compiler_path = check_and_choose_venv(venv_path) else: self._compiler_path = main_window.python_compiler - if sys.platform in ["win32", "cygwin", "msys"]: - args = [ - self._compiler_path, - "-m", - "test_pioneer", - "-e", - executable_path - ] - else: - args = " ".join([ - f"{self._compiler_path}", "-m test_pioneer", "-e", f"{executable_path}" - ]) - self._process: subprocess.Popen = subprocess.Popen( + args = [ + str(self._compiler_path), + "-m", + "test_pioneer", + "-e", + executable_path + ] + self._process: subprocess.Popen | None = subprocess.Popen( args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, - shell=True, - encoding=encoding ) # Pyside UI update method @@ -118,7 +112,7 @@ def exit_program(self): self._read_program_output_from_thread = None if self._read_program_error_output_from_thread is not None: self._read_program_error_output_from_thread = None - self.print_and_clear_queue() + self.drain_and_clear_queue() if self._process is not None: self._process.terminate() text_cursor = self._code_window.code_result.textCursor() @@ -128,27 +122,52 @@ def exit_program(self): text_cursor.insertBlock() self._process = None - def print_and_clear_queue(self): - self._run_output_queue = queue.Queue() - self._run_error_queue = queue.Queue() + def drain_and_clear_queue(self): + while not self._run_output_queue.empty(): + try: + output_message = self._run_output_queue.get_nowait() + output_message = str(output_message).strip() + if output_message: + text_cursor = self._code_window.code_result.textCursor() + text_format = QTextCharFormat() + text_format.setForeground(actually_color_dict.get("normal_output_color")) + text_cursor.insertText(output_message, text_format) + text_cursor.insertBlock() + except queue.Empty: + break + while not self._run_error_queue.empty(): + try: + error_message = self._run_error_queue.get_nowait() + error_message = str(error_message).strip() + if error_message: + text_cursor = self._code_window.code_result.textCursor() + text_format = QTextCharFormat() + text_format.setForeground(actually_color_dict.get("error_output_color")) + text_cursor.insertText(error_message, text_format) + text_cursor.insertBlock() + except queue.Empty: + break def read_program_output_from_process(self): while self._still_run_program: - self.process: subprocess.Popen - program_output_data = self._process.stdout.readline(self._program_buffer_size) \ - .decode("utf-8", "replace") - if self._process: - self._process.stdout.flush() - if program_output_data.strip() != "": + proc = self._process + if proc is None: + break + program_output_data = proc.stdout.readline(self._program_buffer_size) + if isinstance(program_output_data, bytes): + program_output_data = program_output_data.decode(self._program_encoding, "replace") + if program_output_data.strip(): self._run_output_queue.put(program_output_data) def read_program_error_output_from_process(self): while self._still_run_program: - program_error_output_data = self._process.stderr.readline(self._program_buffer_size) \ - .decode("utf-8", "replace") - if self._process: - self._process.stderr.flush() - if program_error_output_data.strip() != "": + proc = self._process + if proc is None: + break + program_error_output_data = proc.stderr.readline(self._program_buffer_size) + if isinstance(program_error_output_data, bytes): + program_error_output_data = program_error_output_data.decode(self._program_encoding, "replace") + if program_error_output_data.strip(): self._run_error_queue.put(program_error_output_data) def start_test_pioneer_process(self): diff --git a/pybreeze/extend/process_executor/web_runner/web_runner_process.py b/pybreeze/extend/process_executor/web_runner/web_runner_process.py index 7d468d6..69a27e2 100644 --- a/pybreeze/extend/process_executor/web_runner/web_runner_process.py +++ b/pybreeze/extend/process_executor/web_runner/web_runner_process.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Union +from typing import TYPE_CHECKING from pybreeze.extend.process_executor.process_executor_utils import build_process @@ -13,7 +13,7 @@ def call_web_runner_test( main_window: PyBreezeMainWindow, - exec_str: Union[str, None] = None, + exec_str: str | None = None, program_buffer: int = 1024000 ): build_process(main_window, "je_web_runner", exec_str, False, program_buffer) @@ -21,7 +21,7 @@ def call_web_runner_test( def call_web_runner_test_with_send( main_window: PyBreezeMainWindow, - exec_str: Union[str, None] = None, + exec_str: str | None = None, program_buffer: int = 1024000 ): build_process(main_window, "je_web_runner", exec_str, True, program_buffer) @@ -36,7 +36,7 @@ def call_web_runner_test_multi_file( if need_to_execute_list is not None and isinstance(need_to_execute_list, list) and len( need_to_execute_list) > 0: for execute_file in need_to_execute_list: - with open(execute_file, "r+") as test_script_json: + with open(execute_file) as test_script_json: call_web_runner_test( main_window, test_script_json.read(), @@ -56,7 +56,7 @@ def call_web_runner_test_multi_file_and_send( if need_to_execute_list is not None and isinstance(need_to_execute_list, list) and len( need_to_execute_list) > 0: for execute_file in need_to_execute_list: - with open(execute_file, "r+") as test_script_json: + with open(execute_file) as test_script_json: call_web_runner_test_with_send( main_window, test_script_json.read(), diff --git a/pybreeze/extend_multi_language/extend_english.py b/pybreeze/extend_multi_language/extend_english.py index 8a3146f..b3dbb03 100644 --- a/pybreeze/extend_multi_language/extend_english.py +++ b/pybreeze/extend_multi_language/extend_english.py @@ -1,280 +1,311 @@ from je_editor import english_word_dict +# PyBreeze-specific English translations +pybreeze_english_word_dict = { + # application name + "application_name": "PyBreeze", + # Menubar + "automation_menu_label": "Automation", + "install_menu_label": "Install", + # Normal label + "run_label": "Run", + "help_label": "HELP", + "project_label": "Project", + # Tab tools menu + "tab_menu_jupyterlab_tab_name": "JupyterLab", + # APITestka Menu + "apitestka_menu_label": "APITestka", + "apitestka_run_script_label": "Run APITestka Script", + "apitestka_run_script_with_send_label": "Run APITestka With Send", + "apitestka_run_multi_script_label": "Run Multi APITestka Script", + "apitestka_run_multi_script_with_send_label": "Run Multi APITestka Script With Send", + "apitestka_doc_label": "Open APITestka Doc", + "apitestka_doc_tab_label": "APITestka Doc", + "apitestka_github_label": "Open APITestka GitHub", + "apitestka_github_tab_label": "APITestka GitHub", + "apitestka_create_project_label": "Create APITestka Project", + # Autocontrol Menu + "autocontrol_menu_label": "Autocontrol", + "autocontrol_run_script_label": "Run Autocontrol Script", + "autocontrol_run_script_with_send_label": "Run Autocontrol With Send", + "autocontrol_run_multi_script_label": "Run Multi Autocontrol Script", + "autocontrol_run_multi_script_with_send_label": "Run Multi Autocontrol Script With Send", + "autocontrol_doc_label": "Open Autocontrol Doc", + "autocontrol_doc_tab_label": "Autocontrol Doc", + "autocontrol_github_label": "Open Autocontrol GitHub", + "autocontrol_github_tab_label": "Autocontrol GitHub", + "autocontrol_create_project_label": "Create Autocontrol Project", + "autocontrol_record_menu_label": "Record", + "autocontrol_record_start_label": "Record Start", + "autocontrol_record_stop_label": "Record Stop", + # File Automation Menu + "file_automation_menu_label": "FileAutomation", + "file_automation_run_script_label": "Run FileAutomation Script", + "file_automation_run_script_with_send_label": "Run FileAutomation With Send", + "file_automation_run_multi_script_label": "Run Multi FileAutomation Script", + "file_automation_run_multi_script_with_send_label": "Run Multi FileAutomation Script With Send", + "file_automation_doc_label": "Open FileAutomation Doc", + "file_automation_doc_tab_label": "FileAutomation Doc", + "file_automation_github_label": "Open FileAutomation GitHub", + "file_automation_github_tab_label": "FileAutomation GitHub", + "file_automation_create_project_label": "Create FileAutomation Project", + # Load Density Menu + "load_density_menu_label": "LoadDensity", + "load_density_run_script_label": "Run LoadDensity Script", + "load_density_run_script_with_send_label": "Run LoadDensity With Send", + "load_density_run_multi_script_label": "Run Multi LoadDensity Script", + "load_density_run_multi_script_with_send_label": "Run Multi LoadDensity Script With Send", + "load_density_doc_label": "Open LoadDensity Doc", + "load_density_doc_tab_label": "LoadDensity Doc", + "load_density_github_label": "Open LoadDensity GitHub", + "load_density_github_tab_label": "LoadDensity GitHub", + "load_density_create_project_label": "Create LoadDensity Project", + # Mail Thunder Menu + "mail_thunder_menu_label": "MailThunder", + "mail_thunder_run_script_label": "Run MailThunder Script", + "mail_thunder_doc_label": "Open MailThunder Doc", + "mail_thunder_doc_tab_label": "MailThunder Doc", + "mail_thunder_github_label": "Open MailThunder GitHub", + "mail_thunder_github_tab_label": "MailThunder GitHub", + "mail_thunder_create_project_label": "Create MailThunder Project", + # Webrunner Menu + "web_runner_menu_label": "WebRunner", + "web_runner_run_script_label": "Run WebRunner Script", + "web_runner_run_script_with_send_label": "Run WebRunner With Send", + "web_runner_run_multi_script_label": "Run Multi WebRunner Script", + "web_runner_run_multi_script_with_send_label": "Run Multi WebRunner Script With Send", + "web_runner_doc_label": "Open WebRunner Doc", + "web_runner_doc_tab_label": "WebRunner Doc", + "web_runner_github_label": "Open WebRunner GitHub", + "web_runner_github_tab_label": "WebRunner GitHub", + "web_runner_create_project_label": "Create WebRunner Project", + # Install Menu + "install_menu_autocontrol": "Install AutoControl", + "install_menu_apitestka": "Install APITestka", + "install_menu_loaddensity": "Install LoadDensity", + "install_menu_webrunner": "Install WebRunner", + "install_menu_automation_file": "Install Automation File", + "install_menu_mail_thunder": "Install MailThunder", + "install_menu_tools_install_menu_label": "Tools", + "install_menu_tools_install_build_tools": "Install Build Tools", + # Tools Menu + "tools_menu_re_edge_gpt_label": "ReEdgeGPT", + "tools_menu_re_edge_gpt_doc_label": "Open ReEdgeGPT Doc", + "tools_menu_re_edge_gpt_doc_tab_label": "ReEdgeGPT Doc", + "tools_menu_re_edge_gpt_github_label": "Open ReEdgeGPT GitHub", + "tools_menu_re_edge_gpt_github_tab_label": "ReEdgeGPT GitHub", + # Test Pioneer Menu + "test_pioneer_label": "TestPioneer", + "test_pioneer_create_template_label": "Create TestPioneer Yaml template", + "test_pioneer_run_yaml": "Execute Test Pioneer Yaml", + "test_pioneer_not_choose_yaml": "Please choose a Yaml file", + # SSH command widget + "ssh_command_widget_window_title_ssh_command_widget": "SSH Command Widget", + "ssh_command_widget_button_label_send_command": "Send", + "ssh_command_widget_input_placeholder_command_line": "Type command then Enter...", + "ssh_command_widget_dialog_title_input_error": "Input error", + "ssh_command_widget_dialog_message_input_error_host_user_required": "Host and username are required.", + "ssh_command_widget_dialog_title_key_error": "Key error", + "ssh_command_widget_dialog_message_key_file_not_exist": "Key file does not exist.", + "ssh_command_widget_error_message_unsupported_private_key": "Unsupported or invalid private key.", + "ssh_command_widget_error_message_key_auth_failed": "Key auth failed", + "ssh_command_widget_status_label_connected": "Connected", + "ssh_command_widget_status_label_disconnected": "Disconnected", + "ssh_command_widget_log_message_connected": "Connected to", + "ssh_command_widget_log_message_error": "[Error] ", + "ssh_command_widget_error_message_decode_failed": " ", + "ssh_command_widget_log_message_channel_closed": "[Channel closed]", + "ssh_command_widget_error_message_reader_failed": "Reader error", + "ssh_command_widget_log_message_reader_closed": "Reader closed", + "ssh_command_widget_error_message_send_failed": "[Send error]", + "ssh_command_widget_dialog_title_not_connected": "Not connected", + "ssh_command_widget_dialog_message_not_connected_shell": "SSH shell is not connected.", + "ssh_command_widget_log_message_disconnect_in_progress": "[Disconnecting...]", + # SSH File Viewer GUI + "ssh_file_viewer_dialog_title_list_error": "List error", + "ssh_file_viewer_dialog_message_list_failed": "Failed to list", + "ssh_file_viewer_dialog_title_operation_failed": "Operation failed", + "ssh_file_viewer_dialog_message_operation_failed": "Error", + "ssh_file_viewer_context_menu_action_refresh": "Refresh", + "ssh_file_viewer_context_menu_action_create_folder": "Create folder", + "ssh_file_viewer_context_menu_action_rename": "Rename", + "ssh_file_viewer_context_menu_action_delete": "Delete", + "ssh_file_viewer_context_menu_action_download": "Download", + "ssh_file_viewer_context_menu_action_upload": "Upload to this folder", + "ssh_file_viewer_log_message_refreshing": "Refreshing current item children (or root)", + "ssh_file_viewer_dialog_title_no_selection": "No selection", + "ssh_file_viewer_dialog_message_select_folder_to_create": "Select a folder to create inside.", + "ssh_file_viewer_dialog_title_create_folder": "Create folder", + "ssh_file_viewer_dialog_label_folder_name": "Folder name:", + "ssh_file_viewer_dialog_title_rename": "Rename", + "ssh_file_viewer_dialog_label_new_name_for_item": "New name for", + "ssh_file_viewer_dialog_title_confirm_delete": "Confirm delete", + "ssh_file_viewer_dialog_message_confirm_delete": "Delete", + "ssh_file_viewer_dialog_title_invalid_selection": "Invalid selection", + "ssh_file_viewer_dialog_message_select_file_to_download": "Select a file to download.", + "ssh_file_viewer_dialog_title_save_as": "Save as", + "ssh_file_viewer_dialog_title_downloaded": "Downloaded", + "ssh_file_viewer_dialog_message_saved_to": "Saved to", + "ssh_file_viewer_dialog_title_select_local_file": "Select local file to upload", + "ssh_file_viewer_dialog_title_uploaded": "Uploaded", + "ssh_file_viewer_dialog_message_uploaded_to": "Uploaded to", + "ssh_file_viewer_dialog_label_input_text": "Input text", + "ssh_file_viewer_dialog_button_ok": "OK", + "ssh_file_viewer_window_title_file_tree_manager": "SSH File TreeView Manager", + "ssh_file_viewer_tree_header_name": "Name", + "ssh_file_viewer_tree_header_type": "Type", + "ssh_file_viewer_tree_header_size": "Size", + "ssh_file_viewer_tree_header_path": "Path", + "ssh_file_viewer_dialog_title_missing_input": "Missing input", + "ssh_file_viewer_dialog_message_missing_input": "Host, user, and password are required.", + "ssh_file_viewer_dialog_title_connection_failed": "Connection failed", + "ssh_file_viewer_dialog_message_connection_failed": "Failed to connect", + # SSH Login Widget + "ssh_login_widget_label_host": "Host", + "ssh_login_widget_label_port": "Port", + "ssh_login_widget_label_user": "User", + "ssh_login_widget_label_key": "Key", + "ssh_login_widget_label_password": "Password", + "ssh_login_widget_placeholder_host": "Host (e.g., 192.168.0.10)", + "ssh_login_widget_placeholder_username": "Username", + "ssh_login_widget_placeholder_password": "Password", + "ssh_login_widget_placeholder_private_key": "Private key path (.pem/.ppk)", + "ssh_login_widget_button_use_key_auth": "Use key auth", + "ssh_login_widget_button_connect": "Connect", + "ssh_login_widget_button_disconnect": "Disconnect", + "ssh_login_widget_status_disconnected": "Disconnected", + # AI Code Review GUI + "ai_code_review_gui_window_title": "AI Code-Review Client", + "ai_code_review_gui_label_url": "URL:", + "ai_code_review_gui_label_method": "Method:", + "ai_code_review_gui_label_code_to_send": "Code to Send:", + "ai_code_review_gui_label_response": "Response:", + "ai_code_review_gui_button_send_request": "Send Request", + "ai_code_review_gui_button_accept_response": "Accept Response", + "ai_code_review_gui_button_reject_response": "Reject Response", + "ai_code_review_gui_message_enter_valid_url": "Please enter a valid URL", + "ai_code_review_gui_message_url_already_recorded": "This URL is already recorded, still sending request...", + "ai_code_review_gui_message_new_url_recorded": "New URL recorded, sending request...", + "ai_code_review_gui_message_unsupported_http_method": "Unsupported HTTP method", + "ai_code_review_gui_message_error": "Error", + "ai_code_review_gui_status_accepted": "[Accepted]", + "ai_code_review_gui_status_rejected": "[Rejected]", + "ai_code_review_gui_status_save_failed": "Save failed", + # CoT Prompt Editor + "cot_prompt_editor_window_title": "CoT Prompt Editor", + "cot_prompt_editor_groupbox_edit_file_content": "Edit File Content", + "cot_prompt_editor_button_create_file": "Create File", + "cot_prompt_editor_button_save_file": "Save", + "cot_prompt_editor_button_reload_file": "Reload", + "cot_prompt_editor_msgbox_info_title": "Info", + "cot_prompt_editor_msgbox_success_title": "Success", + "cot_prompt_editor_msgbox_error_title": "Error", + "cot_prompt_editor_msgbox_file_exists": "File {filename} already exists, no need to create", + "cot_prompt_editor_msgbox_file_created": "File {filename} has been created", + "cot_prompt_editor_msgbox_file_saved": "File {filename} saved", + "cot_prompt_editor_msgbox_no_file_selected": "No file selected", + "cot_prompt_editor_file_not_exist": "(File {filename} does not exist)", + # Skill Prompt Editor + "skill_prompt_editor_window_title": "Skill Prompt Editor", + "skill_prompt_editor_groupbox_edit_file_content": "Edit File Content", + "skill_prompt_editor_button_create_file": "Create File", + "skill_prompt_editor_button_save_file": "Save", + "skill_prompt_editor_button_reload_file": "Reload", + "skill_prompt_editor_msgbox_info_title": "Info", + "skill_prompt_editor_msgbox_success_title": "Success", + "skill_prompt_editor_msgbox_error_title": "Error", + "skill_prompt_editor_msgbox_file_exists": "File {filename} already exists, no need to create", + "skill_prompt_editor_msgbox_file_created": "File {filename} has been created", + "skill_prompt_editor_msgbox_file_saved": "File {filename} saved", + "skill_prompt_editor_msgbox_no_file_selected": "No file selected", + "skill_prompt_editor_file_not_exist": "(File {filename} does not exist)", + # Extend Menu + "extend_tools_menu_tools_menu": "Tools", + "extend_tools_menu_tools_ssh_menu": "SSH", + "extend_tools_menu_tools_ai_menu": "AI", + "extend_tools_menu_ssh_client_tab_action": "SSH Client Tab", + "extend_tools_menu_ssh_client_tab_label": "SSH Client", + "extend_tools_menu_ai_code_review_tab_action": "AI Code-Review Tab", + "extend_tools_menu_ai_code_review_tab_label": "AI Code-Review", + "extend_tools_menu_cot_prompt_editor_tab_action": "CoT Prompt Editor", + "extend_tools_menu_cot_prompt_editor_tab_label": "CoT Prompt Editor", + "extend_tools_menu_skill_prompt_editor_tab_action": "Skill Prompt Editor", + "extend_tools_menu_skill_prompt_editor_tab_label": "Skill Prompt Editor", + "extend_tools_menu_skill_prompt_send_tab_label": "Skill Send GUI", + "extend_tools_menu_dock_ssh_menu": "SSH", + "extend_tools_menu_dock_ai_menu": "AI", + "extend_tools_menu_ssh_client_dock_action": "SSH Client Dock", + "extend_tools_menu_ai_code_review_dock_action": "AI Code-Review Dock", + "extend_tools_menu_cot_prompt_editor_dock_action": "CoT Prompt Editor Dock", + "extend_tools_menu_skill_prompt_editor_dock_action": "Skill Prompt Editor Dock", + "extend_tools_menu_ssh_client_dock_title": "SSH Client", + "extend_tools_menu_ai_code_review_dock_title": "AI Code-Review", + "extend_tools_menu_cot_prompt_editor_dock_title": "CoT PromptEditor", + "extend_tools_menu_skill_prompt_editor_dock_title": "Skill PromptEditor", + "extend_tools_menu_skill_prompt_send_dock_action": "Skill Prompt Dock", + "extend_tools_menu_skill_prompt_send_dock_title": "Skill Send GUI", + # CoT code-review GUI + "cot_gui_window_title": "Prompt Sender UI", + "cot_gui_label_api_url": "API URL:", + "cot_gui_placeholder_api_url": "Please enter the API URL to send, e.g. http://127.0.0.1:5000/api", + "cot_gui_placeholder_code_paste_area": "You can put the code to be sent", + "cot_gui_label_prompt_area": "Prompt Area", + "cot_gui_label_response_area": "Response Area", + "cot_gui_button_send": "Start Sending", + "cot_gui_error_read_file": "Unable to read file:", + "cot_gui_error_no_url": "Please enter the API URL first!", + "cot_gui_error_sending": "Error sending:", + # Skills GUI + "skills_finished_signal": "Success or error message", + "skills_error_signal": "Exception occurred", + "skills_error_status": "Error: {status_code}\n{text}", + "skills_exception": "Exception occurred: {error}", + "skills_api_url_label": "LLM API URL:", + "skills_api_url_placeholder": "Enter the API URL to send, e.g. http://127.0.0.1:5000/api", + "skills_prompt_select_label": "Select Prompt Template:", + "skills_prompt_label": "Prompt:", + "skills_send_button": "Send", + "skills_response_label": "Response:", + "skills_missing_input": "Please enter API URL and Prompt", + "skills_generating": "Generating...", + # JupyterLab GUI + "jupyterlab_init": "Initializing...", + "jupyterlab_downloading": "Downloading...", + "jupyterlab_loading": "Loading...", + "jupyterlab_timeout": "JupyterLab Timeout", + "jupyterlab_init_failed": "JupyterLab init failed", + # Plugin Menu + "plugin_menu_label": "Plugins", + "plugin_menu_about": "About", + "plugin_menu_run_with": "Run with {name}", + # Run with Menu + "run_with_menu_label": "Run with...", + "run_with_suffix_mismatch": "Current file ({suffix}) does not match expected suffixes: {expected}", + # Plugin Browser + "plugin_browser_tab_name": "Plugin Browser", + "plugin_browser_repo_label": "Repository URL:", + "plugin_browser_fetch_btn": "Fetch Plugins", + "plugin_browser_col_name": "Plugin", + "plugin_browser_col_path": "Path", + "plugin_browser_col_size": "Size", + "plugin_browser_select_hint": "Select a plugin to view details", + "plugin_browser_download_btn": "Download && Install", + "plugin_browser_loading_source": "Loading source code...", + "plugin_browser_overwrite_title": "Plugin exists", + "plugin_browser_overwrite_msg": "{name} already exists. Overwrite?", + "plugin_browser_invalid_url": "Invalid repository URL", + "plugin_browser_status_ready": "Ready", + "plugin_browser_status_fetching": "Fetching plugin list...", + "plugin_browser_status_loaded": "Loaded {count} plugins", + "plugin_browser_status_downloading": "Downloading {name}...", + "plugin_browser_status_installed": "Installed: {path}", + "plugin_browser_restart_hint": "Plugin downloaded to:\n{path}\n\nPlease restart the editor to activate.", +} + def update_english_word_dict(): - english_word_dict.update( - { - # application name - "application_name": "PyBreeze", - # Menubar - "automation_menu_label": "Automation", - "install_menu_label": "Install", - # Normal label - "run_label": "Run", - "help_label": "HELP", - "project_label": "Project", - # Tab tools menu - "tab_menu_jupyterlab_tab_name": "JupyterLab", - # APITestka Menu - "apitestka_menu_label": "APITestka", - "apitestka_run_script_label": "Run APITestka Script", - "apitestka_run_script_with_send_label": "Run APITestka With Send", - "apitestka_run_multi_script_label": "Run Multi APITestka Script", - "apitestka_run_multi_script_with_send_label": "Run Multi APITestka Script With Send", - "apitestka_doc_label": "Open APITestka Doc", - "apitestka_doc_tab_label": "APITestka Doc", - "apitestka_github_label": "Open APITestka GitHub", - "apitestka_github_tab_label": "APITestka GitHub", - "apitestka_create_project_label": "Create APITestka Project", - # Autocontrol Menu - "autocontrol_menu_label": "Autocontrol", - "autocontrol_run_script_label": "Run Autocontrol Script", - "autocontrol_run_script_with_send_label": "Run Autocontrol With Send", - "autocontrol_run_multi_script_label": "Run Multi Autocontrol Script", - "autocontrol_run_multi_script_with_send_label": "Run Multi Autocontrol Script With Send", - "autocontrol_doc_label": "Open Autocontrol Doc", - "autocontrol_doc_tab_label": "Autocontrol Doc", - "autocontrol_github_label": "Open Autocontrol GitHub", - "autocontrol_github_tab_label": "Autocontrol GitHub", - "autocontrol_create_project_label": "Create Autocontrol Project", - "autocontrol_record_menu_label": "Record", - "autocontrol_record_start_label": "Record Start", - "autocontrol_record_stop_label": "Record Stop", - # File Automation Menu - "file_automation_menu_label": "FileAutomation", - "file_automation_run_script_label": "Run FileAutomation Script", - "file_automation_run_script_with_send_label": "Run FileAutomation With Send", - "file_automation_run_multi_script_label": "Run Multi FileAutomation Script", - "file_automation_run_multi_script_with_send_label": "Run Multi FileAutomation Script With Send", - "file_automation_doc_label": "Open FileAutomation Doc", - "file_automation_doc_tab_label": "FileAutomation Doc", - "file_automation_github_label": "Open FileAutomation GitHub", - "file_automation_github_tab_label": "FileAutomation GitHub", - "file_automation_create_project_label": "Create FileAutomation Project", - # Load Density Menu - "load_density_menu_label": "LoadDensity", - "load_density_run_script_label": "Run LoadDensity Script", - "load_density_run_script_with_send_label": "Run LoadDensity With Send", - "load_density_run_multi_script_label": "Run Multi LoadDensity Script", - "load_density_run_multi_script_with_send_label": "Run Multi LoadDensity Script With Send", - "load_density_doc_label": "Open LoadDensity Doc", - "load_density_doc_tab_label": "LoadDensity Doc", - "load_density_github_label": "Open LoadDensity GitHub", - "load_density_github_tab_label": "LoadDensity GitHub", - "load_density_create_project_label": "Create LoadDensity Project", - # Mail Thunder Menu - "mail_thunder_menu_label": "MailThunder", - "mail_thunder_run_script_label": "Run MailThunder Script", - "mail_thunder_doc_label": "Open MailThunder Doc", - "mail_thunder_doc_tab_label": "MailThunder Doc", - "mail_thunder_github_label": "Open MailThunder GitHub", - "mail_thunder_github_tab_label": "MailThunder GitHub", - "mail_thunder_create_project_label": "Create MailThunder Project", - # Webrunner Menu - "web_runner_menu_label": "WebRunner", - "web_runner_run_script_label": "Run WebRunner Script", - "web_runner_run_script_with_send_label": "Run WebRunner With Send", - "web_runner_run_multi_script_label": "Run Multi WebRunner Script", - "web_runner_run_multi_script_with_send_label": "Run Multi WebRunner Script With Send", - "web_runner_doc_label": "Open WebRunner Doc", - "web_runner_doc_tab_label": "WebRunner Doc", - "web_runner_github_label": "Open WebRunner GitHub", - "web_runner_github_tab_label": "WebRunner GitHub", - "web_runner_create_project_label": "Create WebRunner Project", - # Install Menu - "install_menu_autocontrol": "Install AutoControl", - "install_menu_apitestka": "Install APITestka", - "install_menu_loaddensity": "Install LoadDensity", - "install_menu_webrunner": "Install WebRunner", - "install_menu_automation_file": "Install Automation File", - "install_menu_mail_thunder": "Install MailThunder", - "install_menu_tools_install_menu_label": "Tools", - "install_menu_tools_install_build_tools": "Install Build Tools", - # Tools Menu - "tools_menu_re_edge_gpt_label": "ReEdgeGPT", - "tools_menu_re_edge_gpt_doc_label": "Open ReEdgeGPT Doc", - "tools_menu_re_edge_gpt_doc_tab_label": "ReEdgeGPT Doc", - "tools_menu_re_edge_gpt_github_label": "Open ReEdgeGPT GitHub", - "tools_menu_re_edge_gpt_github_tab_label": "ReEdgeGPT GitHub", - # Test Pioneer Menu - "test_pioneer_label": "TestPioneer", - "test_pioneer_create_template_label": "Create TestPioneer Yaml template", - "test_pioneer_run_yaml": "Execute Test Pioneer Yaml", - "test_pioneer_not_choose_yaml": "Please choose a Yaml file", - # SSH command widget - "ssh_command_widget_window_title_ssh_command_widget": "SSH Command Widget", - "ssh_command_widget_button_label_send_command": "Send", - "ssh_command_widget_input_placeholder_command_line": "Type command then Enter...", - "ssh_command_widget_dialog_title_input_error": "Input error", - "ssh_command_widget_dialog_message_input_error_host_user_required": "Host and username are required.", - "ssh_command_widget_dialog_title_key_error": "Key error", - "ssh_command_widget_dialog_message_key_file_not_exist": "Key file does not exist.", - "ssh_command_widget_error_message_unsupported_private_key": "Unsupported or invalid private key.", - "ssh_command_widget_error_message_key_auth_failed": "Key auth failed", - "ssh_command_widget_status_label_connected": "Connected", - "ssh_command_widget_status_label_disconnected": "Disconnected", - "ssh_command_widget_log_message_connected": "Connected to", - "ssh_command_widget_log_message_error": "[Error] ", - "ssh_command_widget_error_message_decode_failed": " ", - "ssh_command_widget_log_message_channel_closed": "[Channel closed]", - "ssh_command_widget_error_message_reader_failed": "Reader error", - "ssh_command_widget_log_message_reader_closed": "Reader closed", - "ssh_command_widget_error_message_send_failed": "[Send error]", - "ssh_command_widget_dialog_title_not_connected": "Not connected", - "ssh_command_widget_dialog_message_not_connected_shell": "SSH shell is not connected.", - "ssh_command_widget_log_message_disconnect_in_progress": "[Disconnecting...]", - # SSH File Viewer GUI - "ssh_file_viewer_dialog_title_list_error": "List error", - "ssh_file_viewer_dialog_message_list_failed": "Failed to list", - "ssh_file_viewer_dialog_title_operation_failed": "Operation failed", - "ssh_file_viewer_dialog_message_operation_failed": "Error", - "ssh_file_viewer_context_menu_action_refresh": "Refresh", - "ssh_file_viewer_context_menu_action_create_folder": "Create folder", - "ssh_file_viewer_context_menu_action_rename": "Rename", - "ssh_file_viewer_context_menu_action_delete": "Delete", - "ssh_file_viewer_context_menu_action_download": "Download", - "ssh_file_viewer_context_menu_action_upload": "Upload to this folder", - "ssh_file_viewer_log_message_refreshing": "Refreshing current item children (or root)", - "ssh_file_viewer_dialog_title_no_selection": "No selection", - "ssh_file_viewer_dialog_message_select_folder_to_create": "Select a folder to create inside.", - "ssh_file_viewer_dialog_title_create_folder": "Create folder", - "ssh_file_viewer_dialog_label_folder_name": "Folder name:", - "ssh_file_viewer_dialog_title_rename": "Rename", - "ssh_file_viewer_dialog_label_new_name_for_item": "New name for", - "ssh_file_viewer_dialog_title_confirm_delete": "Confirm delete", - "ssh_file_viewer_dialog_message_confirm_delete": "Delete", - "ssh_file_viewer_dialog_title_invalid_selection": "Invalid selection", - "ssh_file_viewer_dialog_message_select_file_to_download": "Select a file to download.", - "ssh_file_viewer_dialog_title_save_as": "Save as", - "ssh_file_viewer_dialog_title_downloaded": "Downloaded", - "ssh_file_viewer_dialog_message_saved_to": "Saved to", - "ssh_file_viewer_dialog_title_select_local_file": "Select local file to upload", - "ssh_file_viewer_dialog_title_uploaded": "Uploaded", - "ssh_file_viewer_dialog_message_uploaded_to": "Uploaded to", - "ssh_file_viewer_dialog_label_input_text": "Input text", - "ssh_file_viewer_dialog_button_ok": "OK", - "ssh_file_viewer_window_title_file_tree_manager": "SSH File TreeView Manager", - "ssh_file_viewer_tree_header_name": "Name", - "ssh_file_viewer_tree_header_type": "Type", - "ssh_file_viewer_tree_header_size": "Size", - "ssh_file_viewer_tree_header_path": "Path", - "ssh_file_viewer_dialog_title_missing_input": "Missing input", - "ssh_file_viewer_dialog_message_missing_input": "Host, user, and password are required.", - "ssh_file_viewer_dialog_title_connection_failed": "Connection failed", - "ssh_file_viewer_dialog_message_connection_failed": "Failed to connect", - # SSH Login Widget - "ssh_login_widget_label_host": "Host", - "ssh_login_widget_label_port": "Port", - "ssh_login_widget_label_user": "User", - "ssh_login_widget_label_key": "Key", - "ssh_login_widget_label_password": "Password", - "ssh_login_widget_placeholder_host": "Host (e.g., 192.168.0.10)", - "ssh_login_widget_placeholder_username": "Username", - "ssh_login_widget_placeholder_password": "Password", - "ssh_login_widget_placeholder_private_key": "Private key path (.pem/.ppk)", - "ssh_login_widget_button_use_key_auth": "Use key auth", - "ssh_login_widget_button_connect": "Connect", - "ssh_login_widget_button_disconnect": "Disconnect", - "ssh_login_widget_status_disconnected": "Disconnected", - # AI Code Review GUI - "ai_code_review_gui_window_title": "AI Code-Review Client", - "ai_code_review_gui_label_url": "URL:", - "ai_code_review_gui_label_method": "Method:", - "ai_code_review_gui_label_code_to_send": "Code to Send:", - "ai_code_review_gui_label_response": "Response:", - "ai_code_review_gui_button_send_request": "Send Request", - "ai_code_review_gui_button_accept_response": "Accept Response", - "ai_code_review_gui_button_reject_response": "Reject Response", - "ai_code_review_gui_message_enter_valid_url": "Please enter a valid URL", - "ai_code_review_gui_message_url_already_recorded": "This URL is already recorded, still sending request...", - "ai_code_review_gui_message_new_url_recorded": "New URL recorded, sending request...", - "ai_code_review_gui_message_unsupported_http_method": "Unsupported HTTP method", - "ai_code_review_gui_message_error": "Error", - "ai_code_review_gui_status_accepted": "[Accepted]", - "ai_code_review_gui_status_rejected": "[Rejected]", - "ai_code_review_gui_status_save_failed": "Save failed", - # CoT Prompt Editor - "cot_prompt_editor_window_title": "CoT Prompt Editor", - "cot_prompt_editor_groupbox_edit_file_content": "Edit File Content", - "cot_prompt_editor_button_create_file": "Create File", - "cot_prompt_editor_button_save_file": "Save", - "cot_prompt_editor_button_reload_file": "Reload", - "cot_prompt_editor_msgbox_info_title": "Info", - "cot_prompt_editor_msgbox_success_title": "Success", - "cot_prompt_editor_msgbox_error_title": "Error", - "cot_prompt_editor_msgbox_file_exists": "File {filename} already exists, no need to create", - "cot_prompt_editor_msgbox_file_created": "File {filename} has been created", - "cot_prompt_editor_msgbox_file_saved": "File {filename} saved", - "cot_prompt_editor_msgbox_no_file_selected": "No file selected", - "cot_prompt_editor_file_not_exist": "(File {filename} does not exist)", - # Skill Prompt Editor - "skill_prompt_editor_window_title": "Skill Prompt Editor", - "skill_prompt_editor_groupbox_edit_file_content": "Edit File Content", - "skill_prompt_editor_button_create_file": "Create File", - "skill_prompt_editor_button_save_file": "Save", - "skill_prompt_editor_button_reload_file": "Reload", - "skill_prompt_editor_msgbox_info_title": "Info", - "skill_prompt_editor_msgbox_success_title": "Success", - "skill_prompt_editor_msgbox_error_title": "Error", - "skill_prompt_editor_msgbox_file_exists": "File {filename} already exists, no need to create", - "skill_prompt_editor_msgbox_file_created": "File {filename} has been created", - "skill_prompt_editor_msgbox_file_saved": "File {filename} saved", - "skill_prompt_editor_msgbox_no_file_selected": "No file selected", - "skill_prompt_editor_file_not_exist": "(File {filename} does not exist)", - # Extend Menu - "extend_tools_menu_tools_menu": "Tools", - "extend_tools_menu_tools_ssh_menu": "SSH", - "extend_tools_menu_tools_ai_menu": "AI", - "extend_tools_menu_ssh_client_tab_action": "SSH Client Tab", - "extend_tools_menu_ssh_client_tab_label": "SSH Client", - "extend_tools_menu_ai_code_review_tab_action": "AI Code-Review Tab", - "extend_tools_menu_ai_code_review_tab_label": "AI Code-Review", - "extend_tools_menu_cot_prompt_editor_tab_action": "CoT Prompt Editor", - "extend_tools_menu_cot_prompt_editor_tab_label": "CoT Prompt Editor", - "extend_tools_menu_skill_prompt_editor_tab_action": "Skill Prompt Editor", - "extend_tools_menu_skill_prompt_editor_tab_label": "Skill Prompt Editor", - "extend_tools_menu_skill_prompt_send_tab_label": "Skill Send GUI", - "extend_tools_menu_dock_ssh_menu": "SSH", - "extend_tools_menu_dock_ai_menu": "AI", - "extend_tools_menu_ssh_client_dock_action": "SSH Client Dock", - "extend_tools_menu_ai_code_review_dock_action": "AI Code-Review Dock", - "extend_tools_menu_cot_prompt_editor_dock_action": "CoT Prompt Editor Dock", - "extend_tools_menu_skill_prompt_editor_dock_action": "Skill Prompt Editor Dock", - "extend_tools_menu_ssh_client_dock_title": "SSH Client", - "extend_tools_menu_ai_code_review_dock_title": "AI Code-Review", - "extend_tools_menu_cot_prompt_editor_dock_title": "CoT PromptEditor", - "extend_tools_menu_skill_prompt_editor_dock_title": "Skill PromptEditor", - "extend_tools_menu_skill_prompt_send_dock_action": "Skill Prompt Dock", - "extend_tools_menu_skill_prompt_send_dock_title": "Skill Send GUI", - # CoT code-review GUI - "cot_gui_window_title": "Prompt Sender UI", - "cot_gui_label_api_url": "API URL:", - "cot_gui_placeholder_api_url": "Please enter the API URL to send, e.g. http://127.0.0.1:5000/api", - "cot_gui_placeholder_code_paste_area": "You can put the code to be sent", - "cot_gui_label_prompt_area": "Prompt Area", - "cot_gui_label_response_area": "Response Area", - "cot_gui_button_send": "Start Sending", - "cot_gui_error_read_file": "Unable to read file:", - "cot_gui_error_no_url": "Please enter the API URL first!", - "cot_gui_error_sending": "Error sending:", - # Skills GUI - "skills_finished_signal": "Success or error message", - "skills_error_signal": "Exception occurred", - "skills_error_status": "Error: {status_code}\n{text}", - "skills_exception": "Exception occurred: {error}", - "skills_api_url_label": "LLM API URL:", - "skills_api_url_placeholder": "Enter the API URL to send, e.g. http://127.0.0.1:5000/api", - "skills_prompt_select_label": "Select Prompt Template:", - "skills_prompt_label": "Prompt:", - "skills_send_button": "Send", - "skills_response_label": "Response:", - "skills_missing_input": "Please enter API URL and Prompt", - "skills_generating": "Generating...", - # JupyterLab GUI - "jupyterlab_init": "Initializing...", - "jupyterlab_downloading": "Downloading...", - "jupyterlab_loading": "Loading...", - "jupyterlab_timeout": "JupyterLab Timeout", - "jupyterlab_init_failed": "JupyterLab init failed", - } - ) + # Mutate jeditor's built-in English dict in-place. + # This works because language_wrapper.language_word_dict is a direct reference + # to english_word_dict — do NOT use register_natural_language() for built-in + # languages, as that would replace the reference and break the link. + english_word_dict.update(pybreeze_english_word_dict) diff --git a/pybreeze/extend_multi_language/extend_traditional_chinese.py b/pybreeze/extend_multi_language/extend_traditional_chinese.py index c0f9084..4a19d0a 100644 --- a/pybreeze/extend_multi_language/extend_traditional_chinese.py +++ b/pybreeze/extend_multi_language/extend_traditional_chinese.py @@ -1,280 +1,312 @@ from je_editor import traditional_chinese_word_dict +# PyBreeze-specific Traditional Chinese translations +pybreeze_traditional_chinese_word_dict = { + # application name + "application_name": "PyBreeze", + # Menubar + "automation_menu_label": "自動化", + "install_menu_label": "安裝", + # Normal label + "run_label": "運行", + "help_label": "幫助", + "project_label": "專案", + # Tab tools menu + "tab_menu_jupyterlab_tab_name": "JupyterLab", + # APITestka Menu + "apitestka_menu_label": "APITestka", + "apitestka_run_script_label": "運行 APITestka 腳本", + "apitestka_run_script_with_send_label": "運行 APITestka 腳本並寄信", + "apitestka_run_multi_script_label": "運行多個 APITestka 腳本", + "apitestka_run_multi_script_with_send_label": "運行多個 APITestka 腳本並寄信", + "apitestka_doc_label": "開啟 APITestka 文件", + "apitestka_doc_tab_label": "APITestka 文件", + "apitestka_github_label": "開啟 APITestka GitHub", + "apitestka_github_tab_label": "APITestka GitHub", + "apitestka_create_project_label": "建立 APITestka 專案", + # Autocontrol Menu + "autocontrol_menu_label": "Autocontrol", + "autocontrol_run_script_label": "運行 Autocontrol 腳本", + "autocontrol_run_script_with_send_label": "運行 Autocontrol 腳本並寄信", + "autocontrol_run_multi_script_label": "運行 Multi Autocontrol 腳本", + "autocontrol_run_multi_script_with_send_label": "運行 Multi Autocontrol 腳本 腳本並寄信", + "autocontrol_doc_label": "開啟 Autocontrol 文件", + "autocontrol_doc_tab_label": "Autocontrol 文件", + "autocontrol_github_label": "開啟 Autocontrol GitHub", + "autocontrol_github_tab_label": "Autocontrol GitHub", + "autocontrol_create_project_label": "建立 Autocontrol 專案", + "autocontrol_record_menu_label": "紀錄", + "autocontrol_record_start_label": "紀錄 Start", + "autocontrol_record_stop_label": "紀錄 Stop", + # File Automation Menu + "file_automation_menu_label": "FileAutomation", + "file_automation_run_script_label": "運行 FileAutomation 腳本", + "file_automation_run_script_with_send_label": "運行 FileAutomation 腳本並寄信", + "file_automation_run_multi_script_label": "運行 Multi FileAutomation 腳本", + "file_automation_run_multi_script_with_send_label": "運行 Multi FileAutomation 腳本 腳本並寄信", + "file_automation_doc_label": "開啟 FileAutomation 文件", + "file_automation_doc_tab_label": "FileAutomation 文件", + "file_automation_github_label": "開啟 FileAutomation GitHub", + "file_automation_github_tab_label": "FileAutomation GitHub", + "file_automation_create_project_label": "建立 FileAutomation 專案", + # Load Density Menu + "load_density_menu_label": "LoadDensity", + "load_density_run_script_label": "運行 LoadDensity 腳本", + "load_density_run_script_with_send_label": "運行 LoadDensity 腳本並寄信", + "load_density_run_multi_script_label": "運行 Multi LoadDensity 腳本", + "load_density_run_multi_script_with_send_label": "運行 Multi LoadDensity 腳本 腳本並寄信", + "load_density_doc_label": "開啟 LoadDensity 文件", + "load_density_doc_tab_label": "LoadDensity 文件", + "load_density_github_label": "開啟 LoadDensity GitHub", + "load_density_github_tab_label": "LoadDensity GitHub", + "load_density_create_project_label": "建立 LoadDensity 專案", + # Mail Thunder Menu + "mail_thunder_menu_label": "MailThunder", + "mail_thunder_run_script_label": "運行 MailThunder 腳本", + "mail_thunder_doc_label": "開啟 MailThunder 文件", + "mail_thunder_doc_tab_label": "MailThunder 文件", + "mail_thunder_github_label": "開啟 MailThunder GitHub", + "mail_thunder_github_tab_label": "MailThunder GitHub", + "mail_thunder_create_project_label": "建立 MailThunder 專案", + # Webrunner Menu + "web_runner_menu_label": "WebRunner", + "web_runner_run_script_label": "運行 WebRunner 腳本", + "web_runner_run_script_with_send_label": "運行 WebRunner 腳本並寄信", + "web_runner_run_multi_script_label": "運行 Multi WebRunner 腳本", + "web_runner_run_multi_script_with_send_label": "運行 Multi WebRunner 腳本 腳本並寄信", + "web_runner_doc_label": "開啟 WebRunner 文件", + "web_runner_doc_tab_label": "WebRunner 文件", + "web_runner_github_label": "開啟 WebRunner GitHub", + "web_runner_github_tab_label": "WebRunner GitHub", + "web_runner_create_project_label": "建立 WebRunner 專案", + # Install Menu + "install_menu_autocontrol": "安裝 AutoControl", + "install_menu_apitestka": "安裝 APITestka", + "install_menu_loaddensity": "安裝 LoadDensity", + "install_menu_webrunner": "安裝 WebRunner", + "install_menu_automation_file": "安裝 Automation File", + "install_menu_mail_thunder": "安裝 MailThunder", + "install_menu_tools_install_menu_label": "工具", + "install_menu_tools_install_build_tools": "安裝 Build Tools", + # Tools Menu + "tools_menu_re_edge_gpt_label": "ReEdgeGPT", + "tools_menu_re_edge_gpt_doc_label": "開啟 ReEdgeGPT 文件", + "tools_menu_re_edge_gpt_doc_tab_label": "ReEdgeGPT 文件", + "tools_menu_re_edge_gpt_github_label": "開啟 ReEdgeGPT GitHub", + "tools_menu_re_edge_gpt_github_tab_label": "ReEdgeGPT GitHub", + # Test Pioneer Menu + "test_pioneer_label": "TestPioneer", + "test_pioneer_create_template_label": "建立 TestPioneer Yaml 模板", + "test_pioneer_run_yaml": "執行 Test Pioneer Yaml", + "test_pioneer_not_choose_yaml": "請選擇 Yaml 檔案", + # SSH command widget + "ssh_command_widget_window_title_ssh_command_widget": "SSH 指令介面", + "ssh_command_widget_button_label_send_command": "送出", + "ssh_command_widget_input_placeholder_command_line": "輸入指令後按 Enter...", + "ssh_command_widget_dialog_title_input_error": "輸入錯誤", + "ssh_command_widget_dialog_message_input_error_host_user_required": "必須輸入主機與使用者名稱。", + "ssh_command_widget_dialog_title_key_error": "金鑰錯誤", + "ssh_command_widget_dialog_message_key_file_not_exist": "金鑰檔案不存在。", + "ssh_command_widget_error_message_unsupported_private_key": "不支援或無效的私鑰。", + "ssh_command_widget_error_message_key_auth_failed": "金鑰驗證失敗", + "ssh_command_widget_status_label_connected": "已連線", + "ssh_command_widget_status_label_disconnected": "已斷線", + "ssh_command_widget_log_message_connected": "已連線至", + "ssh_command_widget_log_message_error": "[錯誤]", + "ssh_command_widget_error_message_decode_failed": "<解碼錯誤> {error}", + "ssh_command_widget_log_message_channel_closed": "[通道已關閉]", + "ssh_command_widget_error_message_reader_failed": "讀取錯誤", + "ssh_command_widget_log_message_reader_closed": "讀取已關閉", + "ssh_command_widget_error_message_send_failed": "[傳送錯誤]", + "ssh_command_widget_dialog_title_not_connected": "尚未連線", + "ssh_command_widget_dialog_message_not_connected_shell": "SSH shell 尚未連線。", + "ssh_command_widget_log_message_disconnect_in_progress": "[正在斷線...]", + # SSH File Viewer GUI + "ssh_file_viewer_dialog_title_list_error": "列出錯誤", + "ssh_file_viewer_dialog_message_list_failed": "無法列出", + "ssh_file_viewer_dialog_title_operation_failed": "操作失敗", + "ssh_file_viewer_dialog_message_operation_failed": "錯誤", + "ssh_file_viewer_context_menu_action_refresh": "重新整理", + "ssh_file_viewer_context_menu_action_create_folder": "建立資料夾", + "ssh_file_viewer_context_menu_action_rename": "重新命名", + "ssh_file_viewer_context_menu_action_delete": "刪除", + "ssh_file_viewer_context_menu_action_download": "下載", + "ssh_file_viewer_context_menu_action_upload": "上傳至此資料夾", + "ssh_file_viewer_log_message_refreshing": "正在重新整理目前項目的子項(或根)", + "ssh_file_viewer_dialog_title_no_selection": "未選取", + "ssh_file_viewer_dialog_message_select_folder_to_create": "請選擇一個資料夾以建立子資料夾。", + "ssh_file_viewer_dialog_title_create_folder": "建立資料夾", + "ssh_file_viewer_dialog_label_folder_name": "資料夾名稱:", + "ssh_file_viewer_dialog_title_rename": "重新命名", + "ssh_file_viewer_dialog_label_new_name_for_item": "新名稱", + "ssh_file_viewer_dialog_title_confirm_delete": "確認刪除", + "ssh_file_viewer_dialog_message_confirm_delete": "是否刪除", + "ssh_file_viewer_dialog_title_invalid_selection": "選取無效", + "ssh_file_viewer_dialog_message_select_file_to_download": "請選擇要下載的檔案。", + "ssh_file_viewer_dialog_title_save_as": "另存新檔", + "ssh_file_viewer_dialog_title_downloaded": "已下載", + "ssh_file_viewer_dialog_message_saved_to": "已儲存至", + "ssh_file_viewer_dialog_title_select_local_file": "選擇要上傳的本地檔案", + "ssh_file_viewer_dialog_title_uploaded": "已上傳", + "ssh_file_viewer_dialog_message_uploaded_to": "已上傳至", + "ssh_file_viewer_dialog_label_input_text": "輸入文字", + "ssh_file_viewer_dialog_button_ok": "確定", + "ssh_file_viewer_window_title_file_tree_manager": "SSH 檔案樹狀管理器", + "ssh_file_viewer_tree_header_name": "名稱", + "ssh_file_viewer_tree_header_type": "類型", + "ssh_file_viewer_tree_header_size": "大小", + "ssh_file_viewer_tree_header_path": "路徑", + "ssh_file_viewer_dialog_title_missing_input": "缺少輸入", + "ssh_file_viewer_dialog_message_missing_input": "必須輸入主機、使用者與密碼。", + "ssh_file_viewer_dialog_title_connection_failed": "連線失敗", + "ssh_file_viewer_dialog_message_connection_failed": "連線失敗", + # SSH Login Widget + "ssh_login_widget_label_host": "主機", + "ssh_login_widget_label_port": "連接埠", + "ssh_login_widget_label_user": "使用者", + "ssh_login_widget_label_key": "金鑰", + "ssh_login_widget_label_password": "密碼", + "ssh_login_widget_placeholder_host": "主機 (例如: 192.168.0.10)", + "ssh_login_widget_placeholder_username": "使用者名稱", + "ssh_login_widget_placeholder_password": "密碼", + "ssh_login_widget_placeholder_private_key": "私鑰路徑 (.pem/.ppk)", + "ssh_login_widget_button_use_key_auth": "使用金鑰驗證", + "ssh_login_widget_button_connect": "連線", + "ssh_login_widget_button_disconnect": "斷線", + "ssh_login_widget_status_disconnected": "已斷線", + # AI Code Review GUI + "ai_code_review_gui_window_title": "AI 程式碼審查客戶端", + "ai_code_review_gui_label_url": "網址:", + "ai_code_review_gui_label_method": "方法:", + "ai_code_review_gui_label_code_to_send": "要送出的程式碼:", + "ai_code_review_gui_label_response": "回應:", + "ai_code_review_gui_button_send_request": "送出請求", + "ai_code_review_gui_button_accept_response": "接受回應", + "ai_code_review_gui_button_reject_response": "拒絕回應", + "ai_code_review_gui_message_enter_valid_url": "請輸入有效的網址", + "ai_code_review_gui_message_url_already_recorded": "此網址已被紀錄,仍然送出請求...", + "ai_code_review_gui_message_new_url_recorded": "新網址已紀錄,正在送出請求...", + "ai_code_review_gui_message_unsupported_http_method": "不支援的 HTTP 方法", + "ai_code_review_gui_message_error": "錯誤", + "ai_code_review_gui_status_accepted": "[已接受]", + "ai_code_review_gui_status_rejected": "[已拒絕]", + "ai_code_review_gui_status_save_failed": "儲存失敗", + # Cot Prompt Editor + "cot_prompt_editor_window_title": "CoT 提示編輯器", + "cot_prompt_editor_groupbox_edit_file_content": "編輯檔案內容", + "cot_prompt_editor_button_create_file": "建立檔案", + "cot_prompt_editor_button_save_file": "儲存", + "cot_prompt_editor_button_reload_file": "重新載入", + "cot_prompt_editor_msgbox_info_title": "資訊", + "cot_prompt_editor_msgbox_success_title": "成功", + "cot_prompt_editor_msgbox_error_title": "錯誤", + "cot_prompt_editor_msgbox_file_exists": "檔案 {filename} 已存在,無需建立", + "cot_prompt_editor_msgbox_file_created": "檔案 {filename} 已建立", + "cot_prompt_editor_msgbox_file_saved": "檔案 {filename} 已儲存", + "cot_prompt_editor_msgbox_no_file_selected": "尚未選擇檔案", + "cot_prompt_editor_file_not_exist": "(檔案 {filename} 不存在)", + # Skill Prompt Editor + "skill_prompt_editor_window_title": "技能提示編輯器", + "skill_prompt_editor_groupbox_edit_file_content": "編輯檔案內容", + "skill_prompt_editor_button_create_file": "建立檔案", + "skill_prompt_editor_button_save_file": "儲存", + "skill_prompt_editor_button_reload_file": "重新載入", + "skill_prompt_editor_msgbox_info_title": "資訊", + "skill_prompt_editor_msgbox_success_title": "成功", + "skill_prompt_editor_msgbox_error_title": "錯誤", + "skill_prompt_editor_msgbox_file_exists": "檔案 {filename} 已存在,無需建立", + "skill_prompt_editor_msgbox_file_created": "檔案 {filename} 已建立", + "skill_prompt_editor_msgbox_file_saved": "檔案 {filename} 已儲存", + "skill_prompt_editor_msgbox_no_file_selected": "未選擇檔案", + "skill_prompt_editor_file_not_exist": "(檔案 {filename} 不存在)", + # Extend Menu + "extend_tools_menu_tools_menu": "工具", + "extend_tools_menu_tools_ssh_menu": "SSH", + "extend_tools_menu_tools_ai_menu": "AI", + "extend_tools_menu_ssh_client_tab_action": "SSH 用戶端分頁", + "extend_tools_menu_ssh_client_tab_label": "SSH 用戶端", + "extend_tools_menu_ai_code_review_tab_action": "AI 程式碼審查分頁", + "extend_tools_menu_ai_code_review_tab_label": "AI 程式碼審查", + "extend_tools_menu_cot_prompt_editor_tab_action": "CoT 提示詞編輯器", + "extend_tools_menu_cot_prompt_editor_tab_label": "CoT 提示詞編輯器", + "extend_tools_menu_skill_prompt_editor_tab_action": "Skill 提示詞編輯器", + "extend_tools_menu_skill_prompt_editor_tab_label": "Skill 提示詞編輯器", + "extend_tools_menu_skill_prompt_send_tab_label": "Skill 提示詞傳送 GUI", + "extend_tools_menu_dock_ssh_menu": "SSH", + "extend_tools_menu_dock_ai_menu": "AI", + "extend_tools_menu_ssh_client_dock_action": "SSH 用戶端停駐窗格", + "extend_tools_menu_ai_code_review_dock_action": "AI 程式碼審查停駐窗格", + "extend_tools_menu_cot_prompt_editor_dock_action": "CoT 提示詞編輯器停駐窗格", + "extend_tools_menu_skill_prompt_editor_dock_action": "Skill 提示詞編輯器停駐窗格", + "extend_tools_menu_ssh_client_dock_title": "SSH 用戶端", + "extend_tools_menu_ai_code_review_dock_title": "AI 程式碼審查", + "extend_tools_menu_cot_prompt_editor_dock_title": "CoT 提示詞編輯器", + "extend_tools_menu_skill_prompt_editor_dock_title": "Skill 提示詞編輯器", + "extend_tools_menu_skill_prompt_send_dock_action": "Skill Prompt 傳送停駐窗格", + "extend_tools_menu_skill_prompt_send_dock_title": "Skill 提示詞傳送 GUI", + # CoT code-review GUI + "cot_gui_window_title": "Prompt Sender UI", + "cot_gui_label_api_url": "API URL:", + "cot_gui_placeholder_api_url": "請輸入要傳送的 API URL,例如 http://127.0.0.1:5000/api", + "cot_gui_placeholder_code_paste_area": "這裡會顯示要傳送的 Prompt 內容", + "cot_gui_label_prompt_area": "傳送資料區域", + "cot_gui_label_response_area": "回傳區域", + "cot_gui_button_send": "開始傳送", + "cot_gui_error_read_file": "無法讀取檔案:", + "cot_gui_error_no_url": "請先輸入 API URL!", + "cot_gui_error_sending": "Error sending:", + # Skills GUI + "skills_finished_signal": "成功或錯誤訊息", + "skills_error_signal": "發生例外", + "skills_error_status": "錯誤: {status_code}\n{text}", + "skills_exception": "發生例外: {error}", + "skills_api_url_label": "LLM API URL:", + "skills_api_url_placeholder": "請輸入要傳送的 API URL,例如 http://127.0.0.1:5000/api", + "skills_prompt_select_label": "選擇 Prompt 範本:", + "skills_prompt_label": "Prompt:", + "skills_send_button": "傳送", + "skills_response_label": "回傳結果:", + "skills_missing_input": "請輸入 API URL 和 Prompt", + "skills_generating": "產生中...", + # JupyterLab GUI + "jupyterlab_init": "初始化中...", + "jupyterlab_downloading": "下載中...", + "jupyterlab_loading": "載入中...", + "jupyterlab_timeout": "JupyterLab 啟動超時", + "jupyterlab_init_failed": "JupyterLab 啟動失敗", + # Plugin Menu + "plugin_menu_label": "插件", + "plugin_menu_about": "關於", + "plugin_menu_run_with": "以 {name} 執行", + # Run with Menu + "run_with_menu_label": "以...執行", + "run_with_suffix_mismatch": "目前的檔案 ({suffix}) 與預期的副檔名不符: {expected}", + # Plugin Browser + "plugin_browser_tab_name": "插件瀏覽器", + "plugin_browser_repo_label": "儲存庫 URL:", + "plugin_browser_fetch_btn": "取得插件", + "plugin_browser_col_name": "插件", + "plugin_browser_col_path": "路徑", + "plugin_browser_col_size": "大小", + "plugin_browser_select_hint": "選擇插件以檢視詳細資訊", + "plugin_browser_download_btn": "下載並安裝", + "plugin_browser_loading_source": "正在載入原始碼...", + "plugin_browser_overwrite_title": "插件已存在", + "plugin_browser_overwrite_msg": "{name} 已存在,是否覆蓋?", + "plugin_browser_invalid_url": "無效的儲存庫 URL", + "plugin_browser_status_ready": "就緒", + "plugin_browser_status_fetching": "正在取得插件列表...", + "plugin_browser_status_loaded": "已載入 {count} 個插件", + "plugin_browser_status_downloading": "正在下載 {name}...", + "plugin_browser_status_installed": "已安裝:{path}", + "plugin_browser_restart_hint": "插件已下載至:\n{path}\n\n請重新啟動編輯器以啟用。", +} + def update_traditional_chinese_word_dict(): - traditional_chinese_word_dict.update( - { - # application name - "application_name": "PyBreeze", - # Menubar - "automation_menu_label": "自動化", - "install_menu_label": "安裝", - # Normal label - "run_label": "運行", - "help_label": "幫助", - "project_label": "專案", - # Tab tools menu - "tab_menu_jupyterlab_tab_name": "JupyterLab", - # APITestka Menu - "apitestka_menu_label": "APITestka", - "apitestka_run_script_label": "運行 APITestka 腳本", - "apitestka_run_script_with_send_label": "運行 APITestka 腳本並寄信", - "apitestka_run_multi_script_label": "運行多個 APITestka 腳本", - "apitestka_run_multi_script_with_send_label": "運行多個 APITestka 腳本並寄信", - "apitestka_doc_label": "開啟 APITestka 文件", - "apitestka_doc_tab_label": "APITestka 文件", - "apitestka_github_label": "開啟 APITestka GitHub", - "apitestka_github_tab_label": "APITestka GitHub", - "apitestka_create_project_label": "建立 APITestka 專案", - # Autocontrol Menu - "autocontrol_menu_label": "Autocontrol", - "autocontrol_run_script_label": "運行 Autocontrol 腳本", - "autocontrol_run_script_with_send_label": "運行 Autocontrol 腳本並寄信", - "autocontrol_run_multi_script_label": "運行 Multi Autocontrol 腳本", - "autocontrol_run_multi_script_with_send_label": "運行 Multi Autocontrol 腳本 腳本並寄信", - "autocontrol_doc_label": "開啟 Autocontrol 文件", - "autocontrol_doc_tab_label": "Autocontrol 文件", - "autocontrol_github_label": "開啟 Autocontrol GitHub", - "autocontrol_github_tab_label": "Autocontrol GitHub", - "autocontrol_create_project_label": "建立 Autocontrol 專案", - "autocontrol_record_menu_label": "紀錄", - "autocontrol_record_start_label": "紀錄 Start", - "autocontrol_record_stop_label": "紀錄 Stop", - # File Automation Menu - "file_automation_menu_label": "FileAutomation", - "file_automation_run_script_label": "運行 FileAutomation 腳本", - "file_automation_run_script_with_send_label": "運行 FileAutomation 腳本並寄信", - "file_automation_run_multi_script_label": "運行 Multi FileAutomation 腳本", - "file_automation_run_multi_script_with_send_label": "運行 Multi FileAutomation 腳本 腳本並寄信", - "file_automation_doc_label": "開啟 FileAutomation 文件", - "file_automation_doc_tab_label": "FileAutomation 文件", - "file_automation_github_label": "開啟 FileAutomation GitHub", - "file_automation_github_tab_label": "FileAutomation GitHub", - "file_automation_create_project_label": "建立 FileAutomation 專案", - # Load Density Menu - "load_density_menu_label": "LoadDensity", - "load_density_run_script_label": "運行 LoadDensity 腳本", - "load_density_run_script_with_send_label": "運行 LoadDensity 腳本並寄信", - "load_density_run_multi_script_label": "運行 Multi LoadDensity 腳本", - "load_density_run_multi_script_with_send_label": "運行 Multi LoadDensity 腳本 腳本並寄信", - "load_density_doc_label": "開啟 LoadDensity 文件", - "load_density_doc_tab_label": "LoadDensity 文件", - "load_density_github_label": "開啟 LoadDensity GitHub", - "load_density_github_tab_label": "LoadDensity GitHub", - "load_density_create_project_label": "建立 LoadDensity 專案", - # Mail Thunder Menu - "mail_thunder_menu_label": "MailThunder", - "mail_thunder_run_script_label": "運行 MailThunder 腳本", - "mail_thunder_doc_label": "開啟 MailThunder 文件", - "mail_thunder_doc_tab_label": "MailThunder 文件", - "mail_thunder_github_label": "開啟 MailThunder GitHub", - "mail_thunder_github_tab_label": "MailThunder GitHub", - "mail_thunder_create_project_label": "建立 MailThunder 專案", - # Webrunner Menu - "web_runner_menu_label": "WebRunner", - "web_runner_run_script_label": "運行 WebRunner 腳本", - "web_runner_run_script_with_send_label": "運行 WebRunner 腳本並寄信", - "web_runner_run_multi_script_label": "運行 Multi WebRunner 腳本", - "web_runner_run_multi_script_with_send_label": "運行 Multi WebRunner 腳本 腳本並寄信", - "web_runner_doc_label": "開啟 WebRunner 文件", - "web_runner_doc_tab_label": "WebRunner 文件", - "web_runner_github_label": "開啟 WebRunner GitHub", - "web_runner_github_tab_label": "WebRunner GitHub", - "web_runner_create_project_label": "建立 WebRunner 專案", - # Install Menu - "install_menu_autocontrol": "安裝 AutoControl", - "install_menu_apitestka": "安裝 APITestka", - "install_menu_loaddensity": "安裝 LoadDensity", - "install_menu_webrunner": "安裝 WebRunner", - "install_menu_automation_file": "安裝 Automation File", - "install_menu_mail_thunder": "安裝 MailThunder", - "install_menu_tools_install_menu_label": "工具", - "install_menu_tools_install_build_tools": "安裝 Build Tools", - # Tools Menu - "tools_menu_re_edge_gpt_label": "ReEdgeGPT", - "tools_menu_re_edge_gpt_doc_label": "開啟 ReEdgeGPT 文件", - "tools_menu_re_edge_gpt_doc_tab_label": "ReEdgeGPT 文件", - "tools_menu_re_edge_gpt_github_label": "開啟 ReEdgeGPT GitHub", - "tools_menu_re_edge_gpt_github_tab_label": "ReEdgeGPT GitHub", - # Test Pioneer Menu - "test_pioneer_label": "TestPioneer", - "test_pioneer_create_template_label": "建立 TestPioneer Yaml 模板", - "test_pioneer_run_yaml": "執行 Test Pioneer Yaml", - "test_pioneer_not_choose_yaml": "請選擇 Yaml 檔案", - # SSH command widget - "ssh_command_widget_window_title_ssh_command_widget": "SSH 指令介面", - "ssh_command_widget_button_label_send_command": "送出", - "ssh_command_widget_input_placeholder_command_line": "輸入指令後按 Enter...", - "ssh_command_widget_dialog_title_input_error": "輸入錯誤", - "ssh_command_widget_dialog_message_input_error_host_user_required": "必須輸入主機與使用者名稱。", - "ssh_command_widget_dialog_title_key_error": "金鑰錯誤", - "ssh_command_widget_dialog_message_key_file_not_exist": "金鑰檔案不存在。", - "ssh_command_widget_error_message_unsupported_private_key": "不支援或無效的私鑰。", - "ssh_command_widget_error_message_key_auth_failed": "金鑰驗證失敗", - "ssh_command_widget_status_label_connected": "已連線", - "ssh_command_widget_status_label_disconnected": "已斷線", - "ssh_command_widget_log_message_connected": "已連線至", - "ssh_command_widget_log_message_error": "[錯誤]", - "ssh_command_widget_error_message_decode_failed": "<解碼錯誤> {error}", - "ssh_command_widget_log_message_channel_closed": "[通道已關閉]", - "ssh_command_widget_error_message_reader_failed": "讀取錯誤", - "ssh_command_widget_log_message_reader_closed": "讀取已關閉", - "ssh_command_widget_error_message_send_failed": "[傳送錯誤]", - "ssh_command_widget_dialog_title_not_connected": "尚未連線", - "ssh_command_widget_dialog_message_not_connected_shell": "SSH shell 尚未連線。", - "ssh_command_widget_log_message_disconnect_in_progress": "[正在斷線...]", - # SSH File Viewer GUI - "ssh_file_viewer_dialog_title_list_error": "列出錯誤", - "ssh_file_viewer_dialog_message_list_failed": "無法列出", - "ssh_file_viewer_dialog_title_operation_failed": "操作失敗", - "ssh_file_viewer_dialog_message_operation_failed": "錯誤", - "ssh_file_viewer_context_menu_action_refresh": "重新整理", - "ssh_file_viewer_context_menu_action_create_folder": "建立資料夾", - "ssh_file_viewer_context_menu_action_rename": "重新命名", - "ssh_file_viewer_context_menu_action_delete": "刪除", - "ssh_file_viewer_context_menu_action_download": "下載", - "ssh_file_viewer_context_menu_action_upload": "上傳至此資料夾", - "ssh_file_viewer_log_message_refreshing": "正在重新整理目前項目的子項(或根)", - "ssh_file_viewer_dialog_title_no_selection": "未選取", - "ssh_file_viewer_dialog_message_select_folder_to_create": "請選擇一個資料夾以建立子資料夾。", - "ssh_file_viewer_dialog_title_create_folder": "建立資料夾", - "ssh_file_viewer_dialog_label_folder_name": "資料夾名稱:", - "ssh_file_viewer_dialog_title_rename": "重新命名", - "ssh_file_viewer_dialog_label_new_name_for_item": "新名稱", - "ssh_file_viewer_dialog_title_confirm_delete": "確認刪除", - "ssh_file_viewer_dialog_message_confirm_delete": "是否刪除", - "ssh_file_viewer_dialog_title_invalid_selection": "選取無效", - "ssh_file_viewer_dialog_message_select_file_to_download": "請選擇要下載的檔案。", - "ssh_file_viewer_dialog_title_save_as": "另存新檔", - "ssh_file_viewer_dialog_title_downloaded": "已下載", - "ssh_file_viewer_dialog_message_saved_to": "已儲存至", - "ssh_file_viewer_dialog_title_select_local_file": "選擇要上傳的本地檔案", - "ssh_file_viewer_dialog_title_uploaded": "已上傳", - "ssh_file_viewer_dialog_message_uploaded_to": "已上傳至", - "ssh_file_viewer_dialog_label_input_text": "輸入文字", - "ssh_file_viewer_dialog_button_ok": "確定", - "ssh_file_viewer_window_title_file_tree_manager": "SSH 檔案樹狀管理器", - "ssh_file_viewer_tree_header_name": "名稱", - "ssh_file_viewer_tree_header_type": "類型", - "ssh_file_viewer_tree_header_size": "大小", - "ssh_file_viewer_tree_header_path": "路徑", - "ssh_file_viewer_dialog_title_missing_input": "缺少輸入", - "ssh_file_viewer_dialog_message_missing_input": "必須輸入主機、使用者與密碼。", - "ssh_file_viewer_dialog_title_connection_failed": "連線失敗", - "ssh_file_viewer_dialog_message_connection_failed": "連線失敗", - # SSH Login Widget - "ssh_login_widget_label_host": "主機", - "ssh_login_widget_label_port": "連接埠", - "ssh_login_widget_label_user": "使用者", - "ssh_login_widget_label_key": "金鑰", - "ssh_login_widget_label_password": "密碼", - "ssh_login_widget_placeholder_host": "主機 (例如: 192.168.0.10)", - "ssh_login_widget_placeholder_username": "使用者名稱", - "ssh_login_widget_placeholder_password": "密碼", - "ssh_login_widget_placeholder_private_key": "私鑰路徑 (.pem/.ppk)", - "ssh_login_widget_button_use_key_auth": "使用金鑰驗證", - "ssh_login_widget_button_connect": "連線", - "ssh_login_widget_button_disconnect": "斷線", - "ssh_login_widget_status_disconnected": "已斷線", - # AI Code Review GUI - "ai_code_review_gui_window_title": "AI 程式碼審查客戶端", - "ai_code_review_gui_label_url": "網址:", - "ai_code_review_gui_label_method": "方法:", - "ai_code_review_gui_label_code_to_send": "要送出的程式碼:", - "ai_code_review_gui_label_response": "回應:", - "ai_code_review_gui_button_send_request": "送出請求", - "ai_code_review_gui_button_accept_response": "接受回應", - "ai_code_review_gui_button_reject_response": "拒絕回應", - "ai_code_review_gui_message_enter_valid_url": "請輸入有效的網址", - "ai_code_review_gui_message_url_already_recorded": "此網址已被紀錄,仍然送出請求...", - "ai_code_review_gui_message_new_url_recorded": "新網址已紀錄,正在送出請求...", - "ai_code_review_gui_message_unsupported_http_method": "不支援的 HTTP 方法", - "ai_code_review_gui_message_error": "錯誤", - "ai_code_review_gui_status_accepted": "[已接受]", - "ai_code_review_gui_status_rejected": "[已拒絕]", - "ai_code_review_gui_status_save_failed": "儲存失敗", - # Cot Prompt Editor - "cot_prompt_editor_window_title": "CoT 提示編輯器", - "cot_prompt_editor_groupbox_edit_file_content": "編輯檔案內容", - "cot_prompt_editor_button_create_file": "建立檔案", - "cot_prompt_editor_button_save_file": "儲存", - "cot_prompt_editor_button_reload_file": "重新載入", - "cot_prompt_editor_msgbox_info_title": "資訊", - "cot_prompt_editor_msgbox_success_title": "成功", - "cot_prompt_editor_msgbox_error_title": "錯誤", - "cot_prompt_editor_msgbox_file_exists": "檔案 {filename} 已存在,無需建立", - "cot_prompt_editor_msgbox_file_created": "檔案 {filename} 已建立", - "cot_prompt_editor_msgbox_file_saved": "檔案 {filename} 已儲存", - "cot_prompt_editor_msgbox_no_file_selected": "尚未選擇檔案", - "cot_prompt_editor_file_not_exist": "(檔案 {filename} 不存在)", - # Skill Prompt Editor - "skill_prompt_editor_window_title": "技能提示編輯器", - "skill_prompt_editor_groupbox_edit_file_content": "編輯檔案內容", - "skill_prompt_editor_button_create_file": "建立檔案", - "skill_prompt_editor_button_save_file": "儲存", - "skill_prompt_editor_button_reload_file": "重新載入", - "skill_prompt_editor_msgbox_info_title": "資訊", - "skill_prompt_editor_msgbox_success_title": "成功", - "skill_prompt_editor_msgbox_error_title": "錯誤", - "skill_prompt_editor_msgbox_file_exists": "檔案 {filename} 已存在,無需建立", - "skill_prompt_editor_msgbox_file_created": "檔案 {filename} 已建立", - "skill_prompt_editor_msgbox_file_saved": "檔案 {filename} 已儲存", - "skill_prompt_editor_msgbox_no_file_selected": "未選擇檔案", - "skill_prompt_editor_file_not_exist": "(檔案 {filename} 不存在)", - # Extend Menu - "extend_tools_menu_tools_menu": "工具", - "extend_tools_menu_tools_ssh_menu": "SSH", - "extend_tools_menu_tools_ai_menu": "AI", - "extend_tools_menu_ssh_client_tab_action": "SSH 用戶端分頁", - "extend_tools_menu_ssh_client_tab_label": "SSH 用戶端", - "extend_tools_menu_ai_code_review_tab_action": "AI 程式碼審查分頁", - "extend_tools_menu_ai_code_review_tab_label": "AI 程式碼審查", - "extend_tools_menu_cot_prompt_editor_tab_action": "CoT 提示詞編輯器", - "extend_tools_menu_cot_prompt_editor_tab_label": "CoT 提示詞編輯器", - "extend_tools_menu_skill_prompt_editor_tab_action": "Skill 提示詞編輯器", - "extend_tools_menu_skill_prompt_editor_tab_label": "Skill 提示詞編輯器", - "extend_tools_menu_skill_prompt_send_tab_label": "Skill 提示詞傳送 GUI", - "extend_tools_menu_dock_ssh_menu": "SSH", - "extend_tools_menu_dock_ai_menu": "AI", - "extend_tools_menu_ssh_client_dock_action": "SSH 用戶端停駐窗格", - "extend_tools_menu_ai_code_review_dock_action": "AI 程式碼審查停駐窗格", - "extend_tools_menu_cot_prompt_editor_dock_action": "CoT 提示詞編輯器停駐窗格", - "extend_tools_menu_skill_prompt_editor_dock_action": "Skill 提示詞編輯器停駐窗格", - "extend_tools_menu_ssh_client_dock_title": "SSH 用戶端", - "extend_tools_menu_ai_code_review_dock_title": "AI 程式碼審查", - "extend_tools_menu_cot_prompt_editor_dock_title": "CoT 提示詞編輯器", - "extend_tools_menu_skill_prompt_editor_dock_title": "Skill 提示詞編輯器", - "extend_tools_menu_skill_prompt_send_dock_action": "Skill Prompt 傳送停駐窗格", - "extend_tools_menu_skill_prompt_send_dock_title": "Skill 提示詞傳送 GUI", - # CoT code-review GUI - "cot_gui_window_title": "Prompt Sender UI", - "cot_gui_label_api_url": "API URL:", - "cot_gui_placeholder_api_url": "請輸入要傳送的 API URL,例如 http://127.0.0.1:5000/api", - "cot_gui_placeholder_code_paste_area": "這裡會顯示要傳送的 Prompt 內容", - "cot_gui_label_prompt_area": "傳送資料區域", - "cot_gui_label_response_area": "回傳區域", - "cot_gui_button_send": "開始傳送", - "cot_gui_error_read_file": "無法讀取檔案:", - "cot_gui_error_no_url": "請先輸入 API URL!", - "cot_gui_error_sending": "Error sending:", - # Skills GUI - "skills_finished_signal": "成功或錯誤訊息", - "skills_error_signal": "發生例外", - "skills_error_status": "錯誤: {status_code}\n{text}", - "skills_exception": "發生例外: {error}", - "skills_api_url_label": "LLM API URL:", - "skills_api_url_placeholder": "請輸入要傳送的 API URL,例如 http://127.0.0.1:5000/api", - "skills_prompt_select_label": "選擇 Prompt 範本:", - "skills_prompt_label": "Prompt:", - "skills_send_button": "傳送", - "skills_response_label": "回傳結果:", - "skills_missing_input": "請輸入 API URL 和 Prompt", - "skills_generating": "產生中...", - # JupyterLab GUI - "jupyterlab_init": "初始化中...", - "jupyterlab_downloading": "下載中...", - "jupyterlab_loading": "載入中...", - "jupyterlab_timeout": "JupyterLab 啟動超時", - "jupyterlab_init_failed": "JupyterLab 啟動失敗", - } - ) + # Mutate jeditor's built-in Traditional Chinese dict in-place. + # This works because language_wrapper.choose_language_dict["Traditional_Chinese"] + # is a direct reference to traditional_chinese_word_dict — do NOT use + # register_natural_language() for built-in languages, as that would replace + # the reference and break the link. + traditional_chinese_word_dict.update(pybreeze_traditional_chinese_word_dict) diff --git a/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_command_widget.py b/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_command_widget.py index 1db7639..deae551 100644 --- a/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_command_widget.py +++ b/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_command_widget.py @@ -162,7 +162,7 @@ def connect_ssh(self): self.ssh_client.connect(hostname=host, port=port, username=user, pkey=pkey, timeout=10) except Exception as e: raise RuntimeError( - f"{self.word_dict.get('ssh_command_widget_error_message_key_auth_failed')} {e}") + f"{self.word_dict.get('ssh_command_widget_error_message_key_auth_failed')} {e}") from e else: self.ssh_client.connect( hostname=host, port=port, username=user, password=password, timeout=10 @@ -175,7 +175,7 @@ def connect_ssh(self): self.reader_thread.closed.connect(self._on_closed) self.reader_thread.start() self.login_widget.status_label.setText( - self.word_dict.get("ssh_command_widget_dialog_title_not_connected")) + self.word_dict.get("ssh_command_widget_log_message_connected")) self.append_text(f"{self.word_dict.get('ssh_command_widget_log_message_connected')}" f" {host}:{port} as {user}\n") except Exception as e: diff --git a/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_file_viewer_widget.py b/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_file_viewer_widget.py index 33d2139..c79727b 100644 --- a/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_file_viewer_widget.py +++ b/pybreeze/pybreeze_ui/connect_gui/ssh/ssh_file_viewer_widget.py @@ -1,6 +1,5 @@ import os from pathlib import Path -from typing import Optional import paramiko from PySide6.QtCore import Qt, QEvent @@ -21,11 +20,12 @@ class SFTPClientWrapper: def __init__(self): self.word_dict = language_wrapper.language_word_dict - self._ssh: Optional[paramiko.SSHClient] = None - self._sftp: Optional[paramiko.SFTPClient] = None + self._ssh: paramiko.SSHClient | None = None + self._sftp: paramiko.SFTPClient | None = None self.root_path: str = "/" - def connect(self, host: str, port: int, username: str, password: str): + def connect(self, host: str, port: int, username: str, password: str, + use_key: bool = False, key_path: str = ""): """ Establish SSH + SFTP connection. 建立 SSH + SFTP 連線。 @@ -33,7 +33,21 @@ def connect(self, host: str, port: int, username: str, password: str): self.close() self._ssh = paramiko.SSHClient() self._ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - self._ssh.connect(hostname=host, port=port, username=username, password=password) + if use_key and key_path: + pkey = None + for KeyType in (paramiko.RSAKey, paramiko.Ed25519Key, paramiko.ECDSAKey): + try: + pkey = KeyType.from_private_key_file(key_path, password if password else None) + break + except Exception: + continue + if pkey is None: + raise ValueError( + self.word_dict.get("ssh_command_widget_error_message_unsupported_private_key") + ) + self._ssh.connect(hostname=host, port=port, username=username, pkey=pkey, timeout=10) + else: + self._ssh.connect(hostname=host, port=port, username=username, password=password, timeout=10) self._sftp = self._ssh.open_sftp() def close(self): @@ -85,7 +99,7 @@ def is_dir(self, path: str) -> bool: # S_ISDIR check via stat.S_ISDIR import stat return stat.S_ISDIR(st.st_mode) - except IOError: + except OSError: return False def mkdir(self, path: str): @@ -183,18 +197,20 @@ def _connect(self): 連線 SSH 並載入根目錄。 """ host = self.login_widget.host_edit.text().strip() - port = int(self.login_widget.port_spin.text().strip() or "22") + port = self.login_widget.port_spin.value() user = self.login_widget.user_edit.text().strip() pwd = self.login_widget.pass_edit.text() + use_key = self.login_widget.use_key_check.isChecked() + key_path = self.login_widget.key_edit.text().strip() - if not host or not user or not pwd: + if not host or not user: QMessageBox.warning( self, self.word_dict.get("ssh_file_viewer_dialog_title_missing_input"), self.word_dict.get("ssh_file_viewer_dialog_message_missing_input")) return try: - self.client.connect(host, port, user, pwd) + self.client.connect(host, port, user, pwd, use_key, key_path) self.load_root("/") except Exception as e: QMessageBox.critical( @@ -344,7 +360,7 @@ def on_context_menu(self, pos): self.word_dict.get("ssh_file_viewer_dialog_title_operation_failed"), f"{self.word_dict.get('ssh_file_viewer_dialog_message_operation_failed')}: {e}") - def action_refresh(self, item: Optional[QTreeWidgetItem]): + def action_refresh(self, item: QTreeWidgetItem | None): """ Refresh current item children (or root). 重新整理目前項目的子項(或根)。 @@ -358,7 +374,7 @@ def action_refresh(self, item: Optional[QTreeWidgetItem]): self.add_placeholder(target) self.on_item_expanded(target) - def action_create_folder(self, item: Optional[QTreeWidgetItem]): + def action_create_folder(self, item: QTreeWidgetItem | None): """ Create a subfolder under the selected directory. 在選定目錄下建立子資料夾。 @@ -382,7 +398,7 @@ def action_create_folder(self, item: Optional[QTreeWidgetItem]): self.client.mkdir(new_path) self.action_refresh(item) - def action_rename(self, item: Optional[QTreeWidgetItem]): + def action_rename(self, item: QTreeWidgetItem | None): """ Rename selected item. 重新命名選定項目。 @@ -403,7 +419,7 @@ def action_rename(self, item: Optional[QTreeWidgetItem]): item.setText(0, new_name.strip()) item.setText(3, new_path) - def action_delete(self, item: Optional[QTreeWidgetItem]): + def action_delete(self, item: QTreeWidgetItem | None): """ Delete selected file/folder (folder must be empty). 刪除選定檔案/資料夾(資料夾需為空)。 @@ -430,7 +446,7 @@ def action_delete(self, item: Optional[QTreeWidgetItem]): else: self.tree.takeTopLevelItem(self.tree.indexOfTopLevelItem(item)) - def action_download(self, item: Optional[QTreeWidgetItem]): + def action_download(self, item: QTreeWidgetItem | None): """ Download selected file to local. 將選定檔案下載至本地。 @@ -455,7 +471,7 @@ def action_download(self, item: Optional[QTreeWidgetItem]): self.word_dict.get("ssh_file_viewer_dialog_title_downloaded"), f"{self.word_dict.get('ssh_file_viewer_dialog_message_saved_to')}: {local_path}") - def action_upload(self, item: Optional[QTreeWidgetItem]): + def action_upload(self, item: QTreeWidgetItem | None): """ Upload a local file into the selected folder. 將本地檔案上傳至所選資料夾。 diff --git a/pybreeze/pybreeze_ui/connect_gui/url/ai_code_review_gui.py b/pybreeze/pybreeze_ui/connect_gui/url/ai_code_review_gui.py index 19aa622..1e4804b 100644 --- a/pybreeze/pybreeze_ui/connect_gui/url/ai_code_review_gui.py +++ b/pybreeze/pybreeze_ui/connect_gui/url/ai_code_review_gui.py @@ -115,7 +115,7 @@ def send_request(self): # 檢查 URL 是否已紀錄 if os.path.exists(self.url_file): - with open(self.url_file, "r", encoding="utf-8") as f: + with open(self.url_file, encoding="utf-8") as f: urls = [line.strip() for line in f.readlines()] else: urls = [] @@ -174,4 +174,4 @@ def save_stats(self): app = QApplication(sys.argv) window = AICodeReviewClient() window.showMaximized() - sys.exit(app.exec()) \ No newline at end of file + sys.exit(app.exec()) diff --git a/pybreeze/pybreeze_ui/editor_main/main_ui.py b/pybreeze/pybreeze_ui/editor_main/main_ui.py index 9919930..e6d7479 100644 --- a/pybreeze/pybreeze_ui/editor_main/main_ui.py +++ b/pybreeze/pybreeze_ui/editor_main/main_ui.py @@ -2,7 +2,6 @@ import sys from os import environ from pathlib import Path -from typing import List, Dict, Type environ["LOCUST_SKIP_MONKEY_PATCH"] = "1" @@ -18,7 +17,7 @@ syntax_extend_package -EDITOR_EXTEND_TAB: Dict[str, Type[QWidget]] = { +EDITOR_EXTEND_TAB: dict[str, type[QWidget]] = { } @@ -26,8 +25,11 @@ class PyBreezeMainWindow(EditorMain): def __init__(self, debug_mode: bool = False, show_system_tray_ray: bool = False, extend: bool = False) -> None: super().__init__(debug_mode, show_system_tray_ray, extend=True) + # Note: EditorMain.__init__ already calls load_external_plugins() + # which auto-discovers jeditor_plugins/ in the current working directory. + # Third-party plugins placed there will be loaded automatically. - self.current_run_code_window: List[QWidget] = list() + self.current_run_code_window: list[QWidget] = list() # Project compiler if user not choose this will use which to find self.python_compiler = None # Delete JEditor help diff --git a/pybreeze/pybreeze_ui/extend_ai_gui/code_review/code_review_thread.py b/pybreeze/pybreeze_ui/extend_ai_gui/code_review/code_review_thread.py index 5008399..43f3440 100644 --- a/pybreeze/pybreeze_ui/extend_ai_gui/code_review/code_review_thread.py +++ b/pybreeze/pybreeze_ui/extend_ai_gui/code_review/code_review_thread.py @@ -75,7 +75,7 @@ def run(self): case _: continue except Exception as e: - reply_text = f"{language_wrapper.language_word_dict.get("cot_gui_error_sending")} {file} {e}" + reply_text = f"{language_wrapper.language_word_dict.get('cot_gui_error_sending')} {file} {e}" # 發送訊號更新 UI - self.update_response.emit(file, reply_text) \ No newline at end of file + self.update_response.emit(file, reply_text) diff --git a/pybreeze/pybreeze_ui/extend_ai_gui/code_review/cot_code_review_gui.py b/pybreeze/pybreeze_ui/extend_ai_gui/code_review/cot_code_review_gui.py index 70e4659..49b0aca 100644 --- a/pybreeze/pybreeze_ui/extend_ai_gui/code_review/cot_code_review_gui.py +++ b/pybreeze/pybreeze_ui/extend_ai_gui/code_review/cot_code_review_gui.py @@ -77,6 +77,8 @@ def start_sending(self): def handle_response(self, filename, response): self.responses[filename] = response - self.response_selector.addItem(filename) # 加入 ComboBox + if self.response_selector.findText(filename) == -1: + self.response_selector.addItem(filename) # 自動顯示最新回覆 self.response_selector.setCurrentText(filename) + self.show_response(filename) diff --git a/pybreeze/pybreeze_ui/extend_ai_gui/prompt_edit_gui/cot_code_review_prompt_templates/code_smell_detector.py b/pybreeze/pybreeze_ui/extend_ai_gui/prompt_edit_gui/cot_code_review_prompt_templates/code_smell_detector.py index ca8b14e..41b6274 100644 --- a/pybreeze/pybreeze_ui/extend_ai_gui/prompt_edit_gui/cot_code_review_prompt_templates/code_smell_detector.py +++ b/pybreeze/pybreeze_ui/extend_ai_gui/prompt_edit_gui/cot_code_review_prompt_templates/code_smell_detector.py @@ -19,4 +19,4 @@ ### Code: {code_diff} -""" \ No newline at end of file +""" diff --git a/pybreeze/pybreeze_ui/extend_ai_gui/prompt_edit_gui/cot_code_review_prompt_templates/judge.py b/pybreeze/pybreeze_ui/extend_ai_gui/prompt_edit_gui/cot_code_review_prompt_templates/judge.py index a536c06..51c8b18 100644 --- a/pybreeze/pybreeze_ui/extend_ai_gui/prompt_edit_gui/cot_code_review_prompt_templates/judge.py +++ b/pybreeze/pybreeze_ui/extend_ai_gui/prompt_edit_gui/cot_code_review_prompt_templates/judge.py @@ -88,4 +88,4 @@ ## Origin code {code_diff} -""" \ No newline at end of file +""" diff --git a/pybreeze/pybreeze_ui/extend_ai_gui/prompt_edit_gui/cot_code_review_prompt_templates/judge_single_review.py b/pybreeze/pybreeze_ui/extend_ai_gui/prompt_edit_gui/cot_code_review_prompt_templates/judge_single_review.py index 4e8ed8f..90a888a 100644 --- a/pybreeze/pybreeze_ui/extend_ai_gui/prompt_edit_gui/cot_code_review_prompt_templates/judge_single_review.py +++ b/pybreeze/pybreeze_ui/extend_ai_gui/prompt_edit_gui/cot_code_review_prompt_templates/judge_single_review.py @@ -82,4 +82,4 @@ ## Origin code {code_diff} -""" \ No newline at end of file +""" diff --git a/pybreeze/pybreeze_ui/extend_ai_gui/prompt_edit_gui/cot_code_review_prompt_templates/linter.py b/pybreeze/pybreeze_ui/extend_ai_gui/prompt_edit_gui/cot_code_review_prompt_templates/linter.py index 863794f..f02c698 100644 --- a/pybreeze/pybreeze_ui/extend_ai_gui/prompt_edit_gui/cot_code_review_prompt_templates/linter.py +++ b/pybreeze/pybreeze_ui/extend_ai_gui/prompt_edit_gui/cot_code_review_prompt_templates/linter.py @@ -14,4 +14,4 @@ Now analyze the following code: {code_diff} -""" \ No newline at end of file +""" diff --git a/pybreeze/pybreeze_ui/extend_ai_gui/prompt_edit_gui/cot_prompt_editor_widget.py b/pybreeze/pybreeze_ui/extend_ai_gui/prompt_edit_gui/cot_prompt_editor_widget.py index 00f0c6c..60b4d75 100644 --- a/pybreeze/pybreeze_ui/extend_ai_gui/prompt_edit_gui/cot_prompt_editor_widget.py +++ b/pybreeze/pybreeze_ui/extend_ai_gui/prompt_edit_gui/cot_prompt_editor_widget.py @@ -84,7 +84,7 @@ def load_file_content(self, index): filename = self.prompt_files[index] self.current_file = filename if os.path.exists(filename): - with open(filename, "r", encoding="utf-8") as f: + with open(filename, encoding="utf-8") as f: content = f.read() self.middle_editor.setPlainText(content) else: diff --git a/pybreeze/pybreeze_ui/extend_ai_gui/prompt_edit_gui/skills_prompt_editor_widget.py b/pybreeze/pybreeze_ui/extend_ai_gui/prompt_edit_gui/skills_prompt_editor_widget.py index eaa0eb4..c124311 100644 --- a/pybreeze/pybreeze_ui/extend_ai_gui/prompt_edit_gui/skills_prompt_editor_widget.py +++ b/pybreeze/pybreeze_ui/extend_ai_gui/prompt_edit_gui/skills_prompt_editor_widget.py @@ -82,7 +82,7 @@ def load_file_content(self, index): filename = self.skill_files[index] self.current_file = filename if os.path.exists(filename): - with open(filename, "r", encoding="utf-8") as f: + with open(filename, encoding="utf-8") as f: content = f.read() self.middle_editor.setPlainText(content) else: diff --git a/pybreeze/pybreeze_ui/extend_ai_gui/prompt_edit_gui/skills_prompt_templates/code_explainer.py b/pybreeze/pybreeze_ui/extend_ai_gui/prompt_edit_gui/skills_prompt_templates/code_explainer.py index 20095d1..33487fb 100644 --- a/pybreeze/pybreeze_ui/extend_ai_gui/prompt_edit_gui/skills_prompt_templates/code_explainer.py +++ b/pybreeze/pybreeze_ui/extend_ai_gui/prompt_edit_gui/skills_prompt_templates/code_explainer.py @@ -18,4 +18,4 @@ Code: {code_diff} -""" \ No newline at end of file +""" diff --git a/pybreeze/pybreeze_ui/extend_ai_gui/prompt_edit_gui/skills_prompt_templates/code_review.py b/pybreeze/pybreeze_ui/extend_ai_gui/prompt_edit_gui/skills_prompt_templates/code_review.py index b1b8b4b..889be30 100644 --- a/pybreeze/pybreeze_ui/extend_ai_gui/prompt_edit_gui/skills_prompt_templates/code_review.py +++ b/pybreeze/pybreeze_ui/extend_ai_gui/prompt_edit_gui/skills_prompt_templates/code_review.py @@ -34,4 +34,4 @@ - Group results by diff: e.g., **Diff #1**, **Diff #2**, etc. - Within each diff, strictly divide output into the three sections: **Summary**, **Linting Issues**, **Code Smells**. - Present findings in bullet points or tables for readability. -""" \ No newline at end of file +""" diff --git a/pybreeze/pybreeze_ui/extend_ai_gui/skills/skills_send_gui.py b/pybreeze/pybreeze_ui/extend_ai_gui/skills/skills_send_gui.py index 047f503..357e032 100644 --- a/pybreeze/pybreeze_ui/extend_ai_gui/skills/skills_send_gui.py +++ b/pybreeze/pybreeze_ui/extend_ai_gui/skills/skills_send_gui.py @@ -1,7 +1,6 @@ -import sys import requests from PySide6.QtWidgets import ( - QApplication, QWidget, QVBoxLayout, QLineEdit, + QWidget, QVBoxLayout, QLineEdit, QTextEdit, QPushButton, QLabel, QComboBox ) from PySide6.QtCore import QThread, Signal diff --git a/pybreeze/pybreeze_ui/jupyter_lab_gui/jupyer_lab_thread.py b/pybreeze/pybreeze_ui/jupyter_lab_gui/jupyter_lab_thread.py similarity index 79% rename from pybreeze/pybreeze_ui/jupyter_lab_gui/jupyer_lab_thread.py rename to pybreeze/pybreeze_ui/jupyter_lab_gui/jupyter_lab_thread.py index 6a43c36..ef21d06 100644 --- a/pybreeze/pybreeze_ui/jupyter_lab_gui/jupyer_lab_thread.py +++ b/pybreeze/pybreeze_ui/jupyter_lab_gui/jupyter_lab_thread.py @@ -25,23 +25,28 @@ def get_venv_python(): return sys.executable # 嘗試從常見位置找 venv - possible_paths = [ - os.path.join(os.getcwd(), "venv", "Scripts", "python.exe"), - os.path.join(os.getcwd(), ".venv", "Scripts", "python.exe"), - ] + if sys.platform in ["win32", "cygwin", "msys"]: + possible_paths = [ + os.path.join(os.getcwd(), "venv", "Scripts", "python.exe"), + os.path.join(os.getcwd(), ".venv", "Scripts", "python.exe"), + ] + else: + possible_paths = [ + os.path.join(os.getcwd(), "venv", "bin", "python"), + os.path.join(os.getcwd(), ".venv", "bin", "python"), + ] for path in possible_paths: if os.path.exists(path): return path - raise RuntimeError("找不到 venv 的 python.exe") + raise RuntimeError("Cannot find venv python executable") def is_jupyter_installed(python_exe): result = subprocess.run( [python_exe, "-m", "pip", "show", "jupyterlab"], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE + capture_output=True, ) return result.returncode == 0 @@ -51,6 +56,10 @@ class JupyterLauncherThread(QThread): status_update = Signal(str) error_occurred = Signal(str) + def __init__(self, parent=None): + super().__init__(parent) + self.process = None + def run(self): try: python_exe = get_venv_python() @@ -108,5 +117,8 @@ def run(self): pybreeze_logger.info(err) def stop(self): - if self.process: - self.process.terminate() \ No newline at end of file + if self.process is not None: + try: + self.process.terminate() + except OSError: + pass diff --git a/pybreeze/pybreeze_ui/jupyter_lab_gui/jupyter_lab_widget.py b/pybreeze/pybreeze_ui/jupyter_lab_gui/jupyter_lab_widget.py index 4324057..4babc75 100644 --- a/pybreeze/pybreeze_ui/jupyter_lab_gui/jupyter_lab_widget.py +++ b/pybreeze/pybreeze_ui/jupyter_lab_gui/jupyter_lab_widget.py @@ -5,7 +5,7 @@ from PySide6.QtWidgets import QApplication, QVBoxLayout, QWidget, QLabel from je_editor import language_wrapper -from pybreeze.pybreeze_ui.jupyter_lab_gui.jupyer_lab_thread import JupyterLauncherThread +from pybreeze.pybreeze_ui.jupyter_lab_gui.jupyter_lab_thread import JupyterLauncherThread from pybreeze.utils.logging.logger import pybreeze_logger diff --git a/pybreeze/pybreeze_ui/menu/build_menubar.py b/pybreeze/pybreeze_ui/menu/build_menubar.py index b5de5cd..2f80918 100644 --- a/pybreeze/pybreeze_ui/menu/build_menubar.py +++ b/pybreeze/pybreeze_ui/menu/build_menubar.py @@ -3,6 +3,8 @@ from typing import TYPE_CHECKING from pybreeze.pybreeze_ui.menu.extend_jeditor_tab_menu.jupyter_lab_tab import extend_tab_tools_menu +from pybreeze.pybreeze_ui.menu.plugin_menu.build_plugin_menu import set_plugin_menu +from pybreeze.pybreeze_ui.menu.plugin_menu.build_run_with_menu import set_run_with_menu from pybreeze.pybreeze_ui.menu.tools.tools_menu import build_tools_menu, extend_dock_menu if TYPE_CHECKING: @@ -47,3 +49,5 @@ def add_menu_to_menubar(ui_we_want_to_set: PyBreezeMainWindow): build_tools_menu(ui_we_want_to_set=ui_we_want_to_set) extend_dock_menu(ui_we_want_to_set=ui_we_want_to_set) extend_tab_tools_menu(ui_we_want_to_set=ui_we_want_to_set) + set_run_with_menu(ui_we_want_to_set=ui_we_want_to_set) + set_plugin_menu(ui_we_want_to_set=ui_we_want_to_set) diff --git a/pybreeze/pybreeze_ui/menu/extend_jeditor_tab_menu/jupyter_lab_tab.py b/pybreeze/pybreeze_ui/menu/extend_jeditor_tab_menu/jupyter_lab_tab.py index f262101..0430648 100644 --- a/pybreeze/pybreeze_ui/menu/extend_jeditor_tab_menu/jupyter_lab_tab.py +++ b/pybreeze/pybreeze_ui/menu/extend_jeditor_tab_menu/jupyter_lab_tab.py @@ -2,7 +2,6 @@ from typing import TYPE_CHECKING -import je_editor from PySide6.QtGui import QAction from PySide6.QtWidgets import QMenu from je_editor import language_wrapper @@ -28,4 +27,4 @@ def add_jupyterlab_tab(ui_we_want_to_set: PyBreezeMainWindow): ui_we_want_to_set.tab_widget.addTab( JupyterLabWidget(), f"{language_wrapper.language_word_dict.get('tab_menu_jupyterlab_tab_name')} " - f"{ui_we_want_to_set.tab_widget.count()}") \ No newline at end of file + f"{ui_we_want_to_set.tab_widget.count()}") diff --git a/pybreeze/pybreeze_ui/menu/plugin_menu/__init__.py b/pybreeze/pybreeze_ui/menu/plugin_menu/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pybreeze/pybreeze_ui/menu/plugin_menu/build_plugin_menu.py b/pybreeze/pybreeze_ui/menu/plugin_menu/build_plugin_menu.py new file mode 100644 index 0000000..83bd20a --- /dev/null +++ b/pybreeze/pybreeze_ui/menu/plugin_menu/build_plugin_menu.py @@ -0,0 +1,203 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING + +from PySide6.QtGui import QAction +from PySide6.QtWidgets import QMessageBox + +from je_editor import language_wrapper +from je_editor.plugins import get_all_plugin_metadata + +if TYPE_CHECKING: + from pybreeze.pybreeze_ui.editor_main.main_ui import PyBreezeMainWindow + + +def set_plugin_menu(ui_we_want_to_set: PyBreezeMainWindow) -> None: + """ + 建立插件選單,顯示所有已載入插件的名稱、版本、作者。 + Build Plugin menu showing all loaded plugins with name, version, author. + 同一語言若支援多種副檔名,以子選單呈現。 + If a language plugin supports multiple suffixes, show them in a submenu. + """ + metadata_list = get_all_plugin_metadata() + if not metadata_list: + return + + ui_we_want_to_set.plugin_menu = ui_we_want_to_set.menu.addMenu( + language_wrapper.language_word_dict.get("plugin_menu_label", "Plugins") + ) + + # 插件瀏覽器入口 / Plugin browser entry + browse_action = QAction( + language_wrapper.language_word_dict.get("plugin_browser_tab_name", "Plugin Browser"), + ui_we_want_to_set.plugin_menu, + ) + browse_action.triggered.connect(lambda: _open_plugin_browser(ui_we_want_to_set)) + ui_we_want_to_set.plugin_menu.addAction(browse_action) + ui_we_want_to_set.plugin_menu.addSeparator() + + for meta in metadata_list: + plugin_name = meta.get("name", "Unknown") + plugin_author = meta.get("author", "") + plugin_version = meta.get("version", "") + run_config = meta.get("run_config") + + if run_config is not None: + suffixes = run_config.get("suffixes", ()) + config_name = run_config.get("name", plugin_name) + + if len(suffixes) > 1: + # 多種副檔名:建立子選單 + # Multiple suffixes: create a submenu + sub_menu = ui_we_want_to_set.plugin_menu.addMenu(config_name) + + # 「關於」動作 + # "About" action + about_action = QAction( + language_wrapper.language_word_dict.get("plugin_menu_about", "About"), + sub_menu, + ) + about_action.triggered.connect( + _make_about_callback(plugin_name, plugin_version, plugin_author) + ) + sub_menu.addAction(about_action) + sub_menu.addSeparator() + + # 每個副檔名一個執行動作 + # One run action per suffix + for suffix in suffixes: + run_action = QAction( + language_wrapper.language_word_dict.get( + "plugin_menu_run_with", "Run with {name}" + ).format(name=f"{config_name} ({suffix})"), + sub_menu, + ) + run_action.triggered.connect( + _make_run_callback(ui_we_want_to_set, run_config, suffix) + ) + sub_menu.addAction(run_action) + else: + # 單一副檔名:建立子選單含關於與執行 + # Single suffix: submenu with about and run + sub_menu = ui_we_want_to_set.plugin_menu.addMenu(config_name) + + about_action = QAction( + language_wrapper.language_word_dict.get("plugin_menu_about", "About"), + sub_menu, + ) + about_action.triggered.connect( + _make_about_callback(plugin_name, plugin_version, plugin_author) + ) + sub_menu.addAction(about_action) + sub_menu.addSeparator() + + suffix = suffixes[0] if suffixes else "" + run_action = QAction( + language_wrapper.language_word_dict.get( + "plugin_menu_run_with", "Run with {name}" + ).format(name=config_name), + sub_menu, + ) + run_action.triggered.connect( + _make_run_callback(ui_we_want_to_set, run_config, suffix) + ) + sub_menu.addAction(run_action) + else: + # 沒有執行設定的插件(如翻譯插件),只顯示關於 + # Plugins without run config (e.g. translation), show about only + about_action = QAction(plugin_name, ui_we_want_to_set.plugin_menu) + about_action.triggered.connect( + _make_about_callback(plugin_name, plugin_version, plugin_author) + ) + ui_we_want_to_set.plugin_menu.addAction(about_action) + + +def _open_plugin_browser(ui_we_want_to_set: PyBreezeMainWindow) -> None: + """ + 開啟插件瀏覽器分頁。 + Open plugin browser tab. + """ + from je_editor.pyside_ui.main_ui.plugin_browser.plugin_browser_widget import PluginBrowserWidget + + tab_name = language_wrapper.language_word_dict.get("plugin_browser_tab_name", "Plugin Browser") + ui_we_want_to_set.tab_widget.addTab( + PluginBrowserWidget(), + f"{tab_name} {ui_we_want_to_set.tab_widget.count()}" + ) + + +def _make_about_callback(name: str, version: str, author: str): + """ + 建立顯示插件資訊的回呼函式。 + Create a callback to show plugin info dialog. + """ + def callback(): + message_box = QMessageBox() + message_box.setWindowTitle(name) + message_box.setText( + f"{name}\n" + f"Version: {version}\n" + f"Author: {author}" + ) + message_box.exec() + return callback + + +def _make_run_callback(ui_we_want_to_set: PyBreezeMainWindow, run_config: dict, suffix: str): + """ + 建立使用插件執行設定來執行程式的回呼函式。 + Create a callback to run a program using plugin run config. + 使用 PyBreeze 的 FileRunnerProcess 與 CodeWindow。 + Uses PyBreeze's FileRunnerProcess and CodeWindow. + """ + def callback(): + from pathlib import Path + from PySide6.QtWidgets import QMessageBox + + from je_editor.pyside_ui.main_ui.editor.editor_widget import EditorWidget + from je_editor.utils.file.save.save_file import write_file + + from pybreeze.extend.process_executor.file_runner_process import FileRunnerProcess + from pybreeze.pybreeze_ui.show_code_window.code_window import CodeWindow + + widget = ui_we_want_to_set.tab_widget.currentWidget() + if not isinstance(widget, EditorWidget): + return + + # 取得並儲存檔案 / Get and save file + if widget.current_file: + write_file(widget.current_file, widget.code_edit.toPlainText()) + file_path = widget.current_file + else: + from je_editor.pyside_ui.dialog.file_dialog.save_file_dialog import choose_file_get_save_file_path + if not choose_file_get_save_file_path(ui_we_want_to_set): + return + file_path = widget.current_file + + # 檢查副檔名是否匹配 / Check suffix match + file_suffix = Path(file_path).suffix.lower() + supported = run_config.get("suffixes", ()) + if supported and file_suffix not in supported: + msg = QMessageBox(ui_we_want_to_set) + msg.setWindowTitle(language_wrapper.language_word_dict.get("run_with_menu_label", "Run with...")) + msg.setText( + language_wrapper.language_word_dict.get( + "run_with_suffix_mismatch", + "Current file ({suffix}) does not match expected suffixes: {expected}", + ).format(suffix=file_suffix, expected=", ".join(supported)) + ) + msg.exec() + return + + # 建立 CodeWindow 並執行 / Create CodeWindow and run + code_window = CodeWindow() + code_window.setWindowTitle(f"{run_config['name']} - {Path(file_path).name}") + ui_we_want_to_set.current_run_code_window.append(code_window) + + runner = FileRunnerProcess( + main_window=code_window, + program_encoding=ui_we_want_to_set.encoding, + ) + runner.run_file(run_config, file_path) + + return callback diff --git a/pybreeze/pybreeze_ui/menu/plugin_menu/build_run_with_menu.py b/pybreeze/pybreeze_ui/menu/plugin_menu/build_run_with_menu.py new file mode 100644 index 0000000..13c28b9 --- /dev/null +++ b/pybreeze/pybreeze_ui/menu/plugin_menu/build_run_with_menu.py @@ -0,0 +1,98 @@ +""" +"Run with..." menu — lets users run the current file with a plugin-registered runner. + +Uses je_editor's plugin run config registry instead of scanning sys.modules. +""" +from __future__ import annotations + +from pathlib import Path +from typing import TYPE_CHECKING + +from PySide6.QtGui import QAction +from PySide6.QtWidgets import QMessageBox + +from je_editor import language_wrapper +from je_editor.plugins import get_all_plugin_run_configs +from je_editor.pyside_ui.main_ui.editor.editor_widget import EditorWidget +from je_editor.utils.file.save.save_file import write_file + +from pybreeze.extend.process_executor.file_runner_process import FileRunnerProcess +from pybreeze.pybreeze_ui.show_code_window.code_window import CodeWindow + +if TYPE_CHECKING: + from pybreeze.pybreeze_ui.editor_main.main_ui import PyBreezeMainWindow + + +def _get_current_file(main_window: PyBreezeMainWindow) -> str | None: + """Get and save the current editor file, return its path or None.""" + widget = main_window.tab_widget.currentWidget() + if not isinstance(widget, EditorWidget): + return None + + # If file already saved, just write current content + if widget.current_file: + write_file(widget.current_file, widget.code_edit.toPlainText()) + return widget.current_file + + # No file yet — ask user to save first + from je_editor.pyside_ui.dialog.file_dialog.save_file_dialog import choose_file_get_save_file_path + if choose_file_get_save_file_path(main_window): + return widget.current_file + return None + + +def _run_with(main_window: PyBreezeMainWindow, run_config: dict) -> None: + """Execute the current file using the given run config.""" + file_path = _get_current_file(main_window) + if not file_path: + return + + # Check suffix match + suffix = Path(file_path).suffix.lower() + supported = run_config.get("suffixes", ()) + if supported and suffix not in supported: + msg = QMessageBox(main_window) + msg.setWindowTitle(language_wrapper.language_word_dict.get("run_with_menu_label")) + msg.setText( + language_wrapper.language_word_dict.get("run_with_suffix_mismatch").format( + suffix=suffix, + expected=", ".join(supported), + ) + ) + msg.exec() + return + + code_window = CodeWindow() + code_window.setWindowTitle(f"{run_config['name']} - {Path(file_path).name}") + main_window.current_run_code_window.append(code_window) + + runner = FileRunnerProcess( + main_window=code_window, + program_encoding=main_window.encoding, + ) + runner.run_file(run_config, file_path) + + +def set_run_with_menu(ui_we_want_to_set: PyBreezeMainWindow) -> None: + """Build the 'Run with...' submenu. Only creates if configs exist.""" + configs = get_all_plugin_run_configs() + if not configs: + return + + # 依名稱排序 / Sort by name + configs = sorted(configs, key=lambda c: c.get("name", "")) + + ui_we_want_to_set.run_with_menu = ui_we_want_to_set.run_menu.addMenu( + language_wrapper.language_word_dict.get("run_with_menu_label") + ) + + for config in configs: + name = config.get("name", "Unknown") + suffixes = ", ".join(config.get("suffixes", ())) + label = f"{name} ({suffixes})" if suffixes else name + + action = QAction(label, ui_we_want_to_set.run_with_menu) + action.triggered.connect( + lambda checked=False, cfg=config: _run_with(ui_we_want_to_set, cfg) + ) + ui_we_want_to_set.run_with_menu.addAction(action) diff --git a/pybreeze/pybreeze_ui/menu/tools/tools_menu.py b/pybreeze/pybreeze_ui/menu/tools/tools_menu.py index 36c0bad..2e0f29a 100644 --- a/pybreeze/pybreeze_ui/menu/tools/tools_menu.py +++ b/pybreeze/pybreeze_ui/menu/tools/tools_menu.py @@ -121,7 +121,7 @@ def extend_dock_menu(ui_we_want_to_set: PyBreezeMainWindow): lambda: add_dock(ui_we_want_to_set, "SkillSendGUI")) ui_we_want_to_set.dock_ai_menu.addAction(ui_we_want_to_set.tools_skill_send_dock_action) -def add_dock(ui_we_want_to_set: PyBreezeMainWindow, widget_type: str = None): +def add_dock(ui_we_want_to_set: PyBreezeMainWindow, widget_type: str | None = None): jeditor_logger.info("build_dock_menu.py add_dock_widget " f"ui_we_want_to_set: {ui_we_want_to_set} " f"widget_type: {widget_type}") diff --git a/pybreeze/pybreeze_ui/syntax/syntax_extend.py b/pybreeze/pybreeze_ui/syntax/syntax_extend.py index 712feae..e96eba4 100644 --- a/pybreeze/pybreeze_ui/syntax/syntax_extend.py +++ b/pybreeze/pybreeze_ui/syntax/syntax_extend.py @@ -2,7 +2,7 @@ from typing import TYPE_CHECKING -from je_editor import EditorWidget +from je_editor import EditorWidget, register_programming_language if TYPE_CHECKING: from pybreeze.pybreeze_ui.editor_main.main_ui import PyBreezeMainWindow @@ -12,29 +12,26 @@ package_keyword_list from pybreeze.utils.manager.package_manager.package_manager_class import package_manager -from je_editor import syntax_extend_setting_dict - def syntax_extend_package(main_window: PyBreezeMainWindow) -> None: - syntax_extend_setting_dict.update({".json": {}}) + # Register JSON syntax keywords for each automation package + json_syntax_words = {} for package in package_manager.syntax_check_list: - syntax_extend_setting_dict.get(".json").update( - { - package: { - "words": set(package_keyword_list.get(package)), - "color": QColor(255, 255, 0) - } - } - ) - syntax_extend_setting_dict.update({".yml": {}}) - syntax_extend_setting_dict.get(".yml").update( - { - "test_pioneer": { - "words": set(package_keyword_list.get("test_pioneer")), - "color": QColor(255, 153, 0) - } + json_syntax_words[package] = { + "words": set(package_keyword_list.get(package)), + "color": QColor(255, 255, 0), + } + register_programming_language(".json", json_syntax_words) + + # Register YAML syntax keywords for test_pioneer + yml_syntax_words = { + "test_pioneer": { + "words": set(package_keyword_list.get("test_pioneer")), + "color": QColor(255, 153, 0), } - ) + } + register_programming_language(".yml", yml_syntax_words) + widget = main_window.tab_widget.currentWidget() if isinstance(widget, EditorWidget): widget.code_edit.reset_highlighter() diff --git a/pybreeze/utils/file_process/get_dir_file_list.py b/pybreeze/utils/file_process/get_dir_file_list.py index ad2bfff..deb2821 100644 --- a/pybreeze/utils/file_process/get_dir_file_list.py +++ b/pybreeze/utils/file_process/get_dir_file_list.py @@ -15,7 +15,7 @@ def get_dir_files_as_list(dir_path: str = getcwd(), default_search_file_extensio :return: [] if nothing searched or [file1, file2.... files] file was searched """ return [ - abspath(join(dir_path, file)) for root, dirs, files in walk(dir_path) + abspath(join(root, file)) for root, dirs, files in walk(dir_path) for file in files if file.endswith(default_search_file_extension.lower()) ] diff --git a/pybreeze/utils/json_format/json_process.py b/pybreeze/utils/json_format/json_process.py index e4eba3d..1d944c0 100644 --- a/pybreeze/utils/json_format/json_process.py +++ b/pybreeze/utils/json_format/json_process.py @@ -17,12 +17,12 @@ def __process_json(json_string: str, **kwargs) -> str: except TypeError: try: return dumps(json_string, indent=4, sort_keys=True, **kwargs) - except TypeError: - raise ITEJsonException(wrong_json_data_error) + except TypeError as err: + raise ITEJsonException(wrong_json_data_error) from err def reformat_json(json_string: str, **kwargs) -> str: try: return __process_json(json_string, **kwargs) - except ITEJsonException: - raise ITEJsonException(cant_reformat_json_error) + except ITEJsonException as err: + raise ITEJsonException(cant_reformat_json_error) from err diff --git a/pybreeze/utils/logging/logger.py b/pybreeze/utils/logging/logger.py index 7a3f905..4529de5 100644 --- a/pybreeze/utils/logging/logger.py +++ b/pybreeze/utils/logging/logger.py @@ -1,8 +1,6 @@ import logging from logging.handlers import RotatingFileHandler -import pybreeze - # 設定 root logger 等級 Set root logger level logging.root.setLevel(logging.DEBUG) diff --git a/pybreeze/utils/manager/package_manager/package_manager_class.py b/pybreeze/utils/manager/package_manager/package_manager_class.py index 6bfec98..61c31b6 100644 --- a/pybreeze/utils/manager/package_manager/package_manager_class.py +++ b/pybreeze/utils/manager/package_manager/package_manager_class.py @@ -1,7 +1,7 @@ import os -class PackageManager(object): +class PackageManager: def __init__(self): os.environ["WDM_LOG"] = "0" diff --git a/pyproject.toml b/stable.toml similarity index 98% rename from pyproject.toml rename to stable.toml index cf21500..b2a80b2 100644 --- a/pyproject.toml +++ b/stable.toml @@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta" [project] name = "pybreeze" -version = "1.0.12" +version = "1.0.14" authors = [ { name = "JE-Chen", email = "jechenmailman@gmail.com" }, ]