je-editor 0.0.104__py3-none-any.whl → 0.0.228__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- je_editor/__init__.py +26 -21
- je_editor/__main__.py +1 -1
- je_editor/code_scan/ruff_thread.py +58 -0
- je_editor/code_scan/watchdog_implement.py +56 -0
- je_editor/code_scan/watchdog_thread.py +78 -0
- je_editor/git_client/commit_graph.py +77 -0
- je_editor/git_client/git_action.py +175 -0
- je_editor/git_client/git_cli.py +66 -0
- je_editor/pyside_ui/browser/browser_download_window.py +75 -0
- je_editor/pyside_ui/browser/browser_serach_lineedit.py +51 -0
- je_editor/pyside_ui/browser/browser_view.py +87 -0
- je_editor/pyside_ui/browser/browser_widget.py +103 -0
- je_editor/pyside_ui/browser/main_browser_widget.py +85 -0
- je_editor/pyside_ui/code/auto_save/auto_save_manager.py +60 -0
- je_editor/pyside_ui/code/auto_save/auto_save_thread.py +59 -0
- je_editor/pyside_ui/code/code_format/pep8_format.py +130 -0
- je_editor/pyside_ui/code/code_process/code_exec.py +267 -0
- je_editor/pyside_ui/code/plaintext_code_edit/code_edit_plaintext.py +412 -0
- je_editor/pyside_ui/code/running_process_manager.py +48 -0
- je_editor/pyside_ui/code/shell_process/shell_exec.py +236 -0
- je_editor/pyside_ui/code/syntax/python_syntax.py +99 -0
- je_editor/pyside_ui/code/syntax/syntax_setting.py +95 -0
- je_editor/pyside_ui/code/textedit_code_result/code_record.py +75 -0
- je_editor/pyside_ui/code/variable_inspector/inspector_gui.py +172 -0
- je_editor/pyside_ui/dialog/ai_dialog/set_ai_dialog.py +71 -0
- je_editor/pyside_ui/dialog/file_dialog/create_file_dialog.py +68 -0
- je_editor/pyside_ui/dialog/file_dialog/open_file_dialog.py +111 -0
- je_editor/pyside_ui/dialog/file_dialog/save_file_dialog.py +67 -0
- je_editor/pyside_ui/dialog/search_ui/search_error_box.py +49 -0
- je_editor/pyside_ui/dialog/search_ui/search_text_box.py +49 -0
- je_editor/pyside_ui/git_ui/code_diff_compare/code_diff_viewer_widget.py +90 -0
- je_editor/pyside_ui/git_ui/code_diff_compare/line_number_code_viewer.py +141 -0
- je_editor/pyside_ui/git_ui/code_diff_compare/multi_file_diff_viewer.py +88 -0
- je_editor/pyside_ui/git_ui/code_diff_compare/side_by_side_diff_widget.py +284 -0
- je_editor/pyside_ui/git_ui/git_client/commit_table.py +65 -0
- je_editor/pyside_ui/git_ui/git_client/git_branch_tree_widget.py +156 -0
- je_editor/pyside_ui/git_ui/git_client/git_client_gui.py +799 -0
- je_editor/pyside_ui/git_ui/git_client/graph_view.py +218 -0
- je_editor/pyside_ui/main_ui/ai_widget/ai_config.py +34 -0
- je_editor/pyside_ui/main_ui/ai_widget/ask_thread.py +36 -0
- je_editor/pyside_ui/main_ui/ai_widget/chat_ui.py +147 -0
- je_editor/pyside_ui/main_ui/ai_widget/langchain_interface.py +84 -0
- je_editor/pyside_ui/main_ui/console_widget/console_gui.py +162 -0
- je_editor/pyside_ui/main_ui/console_widget/qprocess_adapter.py +84 -0
- je_editor/pyside_ui/main_ui/dock/__init__.py +0 -0
- je_editor/pyside_ui/main_ui/dock/destroy_dock.py +50 -0
- je_editor/pyside_ui/main_ui/editor/__init__.py +0 -0
- je_editor/pyside_ui/main_ui/editor/editor_widget.py +301 -0
- je_editor/pyside_ui/main_ui/editor/editor_widget_dock.py +70 -0
- je_editor/pyside_ui/main_ui/editor/process_input.py +101 -0
- je_editor/pyside_ui/main_ui/ipython_widget/__init__.py +0 -0
- je_editor/pyside_ui/main_ui/ipython_widget/rich_jupyter.py +78 -0
- je_editor/pyside_ui/main_ui/main_editor.py +369 -0
- je_editor/pyside_ui/main_ui/menu/__init__.py +0 -0
- je_editor/pyside_ui/main_ui/menu/check_style_menu/__init__.py +0 -0
- je_editor/pyside_ui/main_ui/menu/check_style_menu/build_check_style_menu.py +104 -0
- je_editor/pyside_ui/main_ui/menu/dock_menu/__init__.py +0 -0
- je_editor/pyside_ui/main_ui/menu/dock_menu/build_dock_menu.py +208 -0
- je_editor/pyside_ui/main_ui/menu/file_menu/__init__.py +0 -0
- je_editor/pyside_ui/main_ui/menu/file_menu/build_file_menu.py +186 -0
- je_editor/pyside_ui/main_ui/menu/help_menu/__init__.py +0 -0
- je_editor/pyside_ui/main_ui/menu/help_menu/build_help_menu.py +100 -0
- je_editor/pyside_ui/main_ui/menu/language_menu/__init__.py +0 -0
- je_editor/pyside_ui/main_ui/menu/language_menu/build_language_server.py +89 -0
- je_editor/pyside_ui/main_ui/menu/python_env_menu/__init__.py +0 -0
- je_editor/pyside_ui/main_ui/menu/python_env_menu/build_venv_menu.py +238 -0
- je_editor/pyside_ui/main_ui/menu/run_menu/__init__.py +0 -0
- je_editor/pyside_ui/main_ui/menu/run_menu/build_run_menu.py +160 -0
- je_editor/pyside_ui/main_ui/menu/run_menu/under_run_menu/__init__.py +0 -0
- je_editor/pyside_ui/main_ui/menu/run_menu/under_run_menu/build_debug_menu.py +109 -0
- je_editor/pyside_ui/main_ui/menu/run_menu/under_run_menu/build_program_menu.py +101 -0
- je_editor/pyside_ui/main_ui/menu/run_menu/under_run_menu/build_shell_menu.py +98 -0
- je_editor/pyside_ui/main_ui/menu/run_menu/under_run_menu/utils.py +41 -0
- je_editor/pyside_ui/main_ui/menu/set_menu_bar.py +63 -0
- je_editor/pyside_ui/main_ui/menu/style_menu/__init__.py +0 -0
- je_editor/pyside_ui/main_ui/menu/style_menu/build_style_menu.py +73 -0
- je_editor/pyside_ui/main_ui/menu/tab_menu/__init__.py +0 -0
- je_editor/pyside_ui/main_ui/menu/tab_menu/build_tab_menu.py +275 -0
- je_editor/pyside_ui/main_ui/menu/text_menu/__init__.py +0 -0
- je_editor/pyside_ui/main_ui/menu/text_menu/build_text_menu.py +135 -0
- je_editor/pyside_ui/main_ui/save_settings/__init__.py +0 -0
- je_editor/pyside_ui/main_ui/save_settings/setting_utils.py +33 -0
- je_editor/pyside_ui/main_ui/save_settings/user_color_setting_file.py +103 -0
- je_editor/pyside_ui/main_ui/save_settings/user_setting_file.py +58 -0
- je_editor/pyside_ui/main_ui/system_tray/__init__.py +0 -0
- je_editor/pyside_ui/main_ui/system_tray/extend_system_tray.py +90 -0
- je_editor/start_editor.py +32 -8
- je_editor/utils/encodings/python_encodings.py +100 -97
- je_editor/utils/exception/exception_tags.py +11 -11
- je_editor/utils/file/open/open_file.py +38 -22
- je_editor/utils/file/save/save_file.py +40 -16
- je_editor/utils/json/json_file.py +36 -15
- je_editor/utils/json_format/json_process.py +38 -2
- je_editor/utils/logging/__init__.py +0 -0
- je_editor/utils/logging/loggin_instance.py +57 -0
- je_editor/utils/multi_language/__init__.py +0 -0
- je_editor/utils/multi_language/english.py +221 -0
- je_editor/utils/multi_language/multi_language_wrapper.py +54 -0
- je_editor/utils/multi_language/traditional_chinese.py +214 -0
- je_editor/utils/redirect_manager/redirect_manager_class.py +67 -25
- je_editor/utils/venv_check/__init__.py +0 -0
- je_editor/utils/venv_check/check_venv.py +51 -0
- je_editor-0.0.228.dist-info/METADATA +99 -0
- je_editor-0.0.228.dist-info/RECORD +140 -0
- {je_editor-0.0.104.dist-info → je_editor-0.0.228.dist-info}/WHEEL +1 -1
- {je_editor-0.0.104.dist-info → je_editor-0.0.228.dist-info/licenses}/LICENSE +1 -1
- je_editor/pyside_ui/auto_save/auto_save_thread.py +0 -34
- je_editor/pyside_ui/code_editor/code_edit_plaintext.py +0 -143
- je_editor/pyside_ui/code_process/code_exec.py +0 -190
- je_editor/pyside_ui/code_result/code_record.py +0 -39
- je_editor/pyside_ui/colors/global_color.py +0 -4
- je_editor/pyside_ui/file_dialog/open_file_dialog.py +0 -27
- je_editor/pyside_ui/file_dialog/save_file_dialog.py +0 -24
- je_editor/pyside_ui/main_ui/editor_main_ui/main_editor.py +0 -183
- je_editor/pyside_ui/main_ui_setting/ui_setting.py +0 -36
- je_editor/pyside_ui/menu/menu_bar/check_style_menu/build_check_style_menu.py +0 -44
- je_editor/pyside_ui/menu/menu_bar/file_menu/build_file_menu.py +0 -30
- je_editor/pyside_ui/menu/menu_bar/help_menu/build_help_menu.py +0 -39
- je_editor/pyside_ui/menu/menu_bar/run_menu/build_run_menu.py +0 -102
- je_editor/pyside_ui/menu/menu_bar/set_menu_bar.py +0 -24
- je_editor/pyside_ui/menu/menu_bar/venv_menu/build_venv_menu.py +0 -74
- je_editor/pyside_ui/search_ui/search_error_box.py +0 -20
- je_editor/pyside_ui/search_ui/search_text_box.py +0 -20
- je_editor/pyside_ui/shell_process/shell_exec.py +0 -157
- je_editor/pyside_ui/syntax/python_syntax.py +0 -99
- je_editor/pyside_ui/treeview/project_treeview/set_project_treeview.py +0 -47
- je_editor/pyside_ui/user_setting/user_setting_file.py +0 -23
- je_editor-0.0.104.dist-info/METADATA +0 -84
- je_editor-0.0.104.dist-info/RECORD +0 -69
- /je_editor/{pyside_ui/auto_save → code_scan}/__init__.py +0 -0
- /je_editor/{pyside_ui/code_editor → git_client}/__init__.py +0 -0
- /je_editor/pyside_ui/{code_process → browser}/__init__.py +0 -0
- /je_editor/pyside_ui/{code_result → code}/__init__.py +0 -0
- /je_editor/pyside_ui/{colors → code/auto_save}/__init__.py +0 -0
- /je_editor/pyside_ui/{file_dialog → code/code_format}/__init__.py +0 -0
- /je_editor/pyside_ui/{main_ui/editor_main_ui → code/code_process}/__init__.py +0 -0
- /je_editor/pyside_ui/{main_ui_setting → code/plaintext_code_edit}/__init__.py +0 -0
- /je_editor/pyside_ui/{menu → code/shell_process}/__init__.py +0 -0
- /je_editor/pyside_ui/{menu/menu_bar → code/syntax}/__init__.py +0 -0
- /je_editor/pyside_ui/{menu/menu_bar/check_style_menu → code/textedit_code_result}/__init__.py +0 -0
- /je_editor/pyside_ui/{menu/menu_bar/file_menu → code/variable_inspector}/__init__.py +0 -0
- /je_editor/pyside_ui/{menu/menu_bar/help_menu → dialog}/__init__.py +0 -0
- /je_editor/pyside_ui/{menu/menu_bar/run_menu → dialog/ai_dialog}/__init__.py +0 -0
- /je_editor/pyside_ui/{menu/menu_bar/venv_menu → dialog/file_dialog}/__init__.py +0 -0
- /je_editor/pyside_ui/{search_ui → dialog/search_ui}/__init__.py +0 -0
- /je_editor/pyside_ui/{shell_process → git_ui}/__init__.py +0 -0
- /je_editor/pyside_ui/{syntax → git_ui/code_diff_compare}/__init__.py +0 -0
- /je_editor/pyside_ui/{treeview → git_ui/git_client}/__init__.py +0 -0
- /je_editor/pyside_ui/{treeview/project_treeview → main_ui/ai_widget}/__init__.py +0 -0
- /je_editor/pyside_ui/{user_setting → main_ui/console_widget}/__init__.py +0 -0
- {je_editor-0.0.104.dist-info → je_editor-0.0.228.dist-info}/top_level.txt +0 -0
je_editor/__init__.py
CHANGED
|
@@ -1,29 +1,34 @@
|
|
|
1
|
-
|
|
1
|
+
from je_editor.pyside_ui.browser.main_browser_widget import MainBrowserWidget
|
|
2
|
+
from je_editor.pyside_ui.code.code_process.code_exec import ExecManager
|
|
3
|
+
from je_editor.pyside_ui.code.shell_process.shell_exec import ShellManager
|
|
4
|
+
from je_editor.pyside_ui.code.syntax.python_syntax import PythonHighlighter
|
|
5
|
+
from je_editor.pyside_ui.code.syntax.syntax_setting import syntax_extend_setting_dict, syntax_rule_setting_dict
|
|
6
|
+
from je_editor.pyside_ui.main_ui.editor.editor_widget import EditorWidget
|
|
7
|
+
from je_editor.pyside_ui.main_ui.editor.editor_widget_dock import FullEditorWidget
|
|
8
|
+
from je_editor.pyside_ui.main_ui.main_editor import EDITOR_EXTEND_TAB
|
|
9
|
+
from je_editor.pyside_ui.main_ui.main_editor import EditorMain
|
|
10
|
+
from je_editor.pyside_ui.main_ui.save_settings.user_color_setting_file import user_setting_color_dict
|
|
11
|
+
from je_editor.pyside_ui.main_ui.save_settings.user_setting_file import user_setting_dict
|
|
2
12
|
from je_editor.start_editor import start_editor
|
|
3
|
-
|
|
4
|
-
from je_editor.
|
|
5
|
-
# Exceptions
|
|
13
|
+
from je_editor.utils.exception.exceptions import JEditorCantFindLanguageException
|
|
14
|
+
from je_editor.utils.exception.exceptions import JEditorContentFileException
|
|
6
15
|
from je_editor.utils.exception.exceptions import JEditorException
|
|
7
16
|
from je_editor.utils.exception.exceptions import JEditorExecException
|
|
17
|
+
from je_editor.utils.exception.exceptions import JEditorJsonException
|
|
18
|
+
from je_editor.utils.exception.exceptions import JEditorOpenFileException
|
|
8
19
|
from je_editor.utils.exception.exceptions import JEditorRunOnShellException
|
|
9
20
|
from je_editor.utils.exception.exceptions import JEditorSaveFileException
|
|
10
|
-
from je_editor.utils.
|
|
11
|
-
from je_editor.utils.
|
|
12
|
-
from je_editor.utils.
|
|
13
|
-
from je_editor.utils.exception.exceptions import JEditorJsonException
|
|
14
|
-
# Color
|
|
15
|
-
from je_editor.pyside_ui.colors.global_color import error_color
|
|
16
|
-
from je_editor.pyside_ui.colors.global_color import output_color
|
|
17
|
-
# Exec and shell
|
|
18
|
-
from je_editor.pyside_ui.code_process.code_exec import exec_manage
|
|
19
|
-
from je_editor.pyside_ui.shell_process.shell_exec import shell_manager
|
|
21
|
+
from je_editor.utils.multi_language.english import english_word_dict
|
|
22
|
+
from je_editor.utils.multi_language.multi_language_wrapper import language_wrapper
|
|
23
|
+
from je_editor.utils.multi_language.traditional_chinese import traditional_chinese_word_dict
|
|
20
24
|
|
|
21
25
|
__all__ = [
|
|
22
|
-
"start_editor", "EditorMain",
|
|
23
|
-
"JEditorException", "JEditorExecException",
|
|
24
|
-
"JEditorRunOnShellException", "JEditorSaveFileException",
|
|
25
|
-
"JEditorOpenFileException", "JEditorContentFileException",
|
|
26
|
-
"JEditorCantFindLanguageException", "JEditorJsonException",
|
|
27
|
-
"
|
|
28
|
-
"
|
|
26
|
+
"start_editor", "EditorMain", "EDITOR_EXTEND_TAB",
|
|
27
|
+
"JEditorException", "JEditorExecException", "FullEditorWidget",
|
|
28
|
+
"JEditorRunOnShellException", "JEditorSaveFileException", "syntax_rule_setting_dict",
|
|
29
|
+
"JEditorOpenFileException", "JEditorContentFileException", "syntax_extend_setting_dict",
|
|
30
|
+
"JEditorCantFindLanguageException", "JEditorJsonException", "PythonHighlighter",
|
|
31
|
+
"user_setting_dict", "user_setting_color_dict", "EditorWidget", "MainBrowserWidget",
|
|
32
|
+
"ExecManager", "ShellManager", "traditional_chinese_word_dict", "english_word_dict",
|
|
33
|
+
"language_wrapper"
|
|
29
34
|
]
|
je_editor/__main__.py
CHANGED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import threading
|
|
3
|
+
import time
|
|
4
|
+
from queue import Queue
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class RuffThread(threading.Thread):
|
|
8
|
+
"""
|
|
9
|
+
A thread class to run Ruff (a Python linter/formatter) as a subprocess.
|
|
10
|
+
使用子執行緒執行 Ruff (Python 程式碼檢查/格式化工具)。
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, ruff_commands: list, std_queue: Queue, stderr_queue: Queue):
|
|
14
|
+
"""
|
|
15
|
+
Initialize the RuffThread.
|
|
16
|
+
初始化 RuffThread。
|
|
17
|
+
|
|
18
|
+
:param ruff_commands: list of commands to run Ruff, e.g. ["ruff", "check"]
|
|
19
|
+
要執行的 Ruff 指令,例如 ["ruff", "check"]
|
|
20
|
+
:param std_queue: queue to store standard output
|
|
21
|
+
用來存放標準輸出的佇列
|
|
22
|
+
:param stderr_queue: queue to store error output
|
|
23
|
+
用來存放錯誤輸出的佇列
|
|
24
|
+
"""
|
|
25
|
+
super().__init__()
|
|
26
|
+
if ruff_commands is None:
|
|
27
|
+
self.ruff_commands = ["ruff", "check"]
|
|
28
|
+
else:
|
|
29
|
+
self.ruff_commands = ruff_commands
|
|
30
|
+
|
|
31
|
+
self.ruff_process = None
|
|
32
|
+
self.std_queue = std_queue
|
|
33
|
+
self.stderr_queue = stderr_queue
|
|
34
|
+
|
|
35
|
+
def run(self):
|
|
36
|
+
"""
|
|
37
|
+
Run the Ruff process in a separate thread.
|
|
38
|
+
在子執行緒中執行 Ruff 程式。
|
|
39
|
+
"""
|
|
40
|
+
# 啟動子程序,捕捉 stdout 與 stderr
|
|
41
|
+
self.ruff_process = subprocess.Popen(
|
|
42
|
+
self.ruff_commands,
|
|
43
|
+
stdout=subprocess.PIPE,
|
|
44
|
+
stderr=subprocess.PIPE,
|
|
45
|
+
text=True,
|
|
46
|
+
bufsize=1
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# 等待子程序結束
|
|
50
|
+
while self.ruff_process.poll() is None:
|
|
51
|
+
time.sleep(1)
|
|
52
|
+
else:
|
|
53
|
+
# 子程序結束後,讀取 stdout 與 stderr
|
|
54
|
+
for line in self.ruff_process.stdout:
|
|
55
|
+
self.std_queue.put(line.strip())
|
|
56
|
+
|
|
57
|
+
for line in self.ruff_process.stderr:
|
|
58
|
+
self.stderr_queue.put(line.strip())
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from queue import Queue
|
|
3
|
+
from typing import Dict
|
|
4
|
+
|
|
5
|
+
from watchdog.events import FileSystemEventHandler
|
|
6
|
+
|
|
7
|
+
from je_editor.code_scan.ruff_thread import RuffThread
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class RuffPythonFileChangeHandler(FileSystemEventHandler):
|
|
11
|
+
"""
|
|
12
|
+
File system event handler that runs Ruff when Python files are modified.
|
|
13
|
+
當 Python 檔案被修改時,自動觸發 Ruff 檢查。
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, ruff_commands: list = None, debounce_interval: float = 1.0):
|
|
17
|
+
"""
|
|
18
|
+
:param ruff_commands: Ruff command list, e.g. ["ruff", "check"]
|
|
19
|
+
:param debounce_interval: Minimum interval (seconds) between re-runs for the same file
|
|
20
|
+
同一檔案觸發 Ruff 的最小間隔秒數
|
|
21
|
+
"""
|
|
22
|
+
super().__init__()
|
|
23
|
+
self.ruff_commands = ruff_commands or ["ruff", "check"]
|
|
24
|
+
self.stdout_queue: Queue = Queue()
|
|
25
|
+
self.stderr_queue: Queue = Queue()
|
|
26
|
+
self.ruff_threads_dict: Dict[str, RuffThread] = {}
|
|
27
|
+
self.last_run_time: Dict[str, float] = {}
|
|
28
|
+
self.debounce_interval = debounce_interval
|
|
29
|
+
|
|
30
|
+
def _start_new_thread(self, file_path: str):
|
|
31
|
+
"""Helper to start a new Ruff thread for a given file."""
|
|
32
|
+
ruff_thread = RuffThread(self.ruff_commands, self.stdout_queue, self.stderr_queue)
|
|
33
|
+
self.ruff_threads_dict[file_path] = ruff_thread
|
|
34
|
+
self.last_run_time[file_path] = time.time()
|
|
35
|
+
ruff_thread.start()
|
|
36
|
+
|
|
37
|
+
def on_modified(self, event):
|
|
38
|
+
"""Triggered when a file is modified."""
|
|
39
|
+
if event.is_directory:
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
if not event.src_path.endswith(".py"):
|
|
43
|
+
return
|
|
44
|
+
|
|
45
|
+
now = time.time()
|
|
46
|
+
last_time = self.last_run_time.get(event.src_path, 0)
|
|
47
|
+
|
|
48
|
+
# Debounce: skip if last run was too recent
|
|
49
|
+
if now - last_time < self.debounce_interval:
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
ruff_thread = self.ruff_threads_dict.get(event.src_path)
|
|
53
|
+
|
|
54
|
+
if ruff_thread is None or not ruff_thread.is_alive():
|
|
55
|
+
self._start_new_thread(event.src_path)
|
|
56
|
+
# else: thread still running, skip
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import threading
|
|
3
|
+
import time
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from watchdog.observers import Observer
|
|
7
|
+
|
|
8
|
+
from je_editor.code_scan.watchdog_implement import RuffPythonFileChangeHandler
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class WatchdogThread(threading.Thread):
|
|
12
|
+
"""
|
|
13
|
+
A thread that runs a watchdog observer to monitor file changes.
|
|
14
|
+
使用 watchdog 監控檔案變化的執行緒。
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, check_path: str):
|
|
18
|
+
"""
|
|
19
|
+
:param check_path: Path to monitor (directory or file)
|
|
20
|
+
要監控的路徑(資料夾或檔案)
|
|
21
|
+
"""
|
|
22
|
+
super().__init__(daemon=True) # 設為 daemon,主程式結束時自動退出
|
|
23
|
+
self.check_path = Path(check_path).resolve()
|
|
24
|
+
self.ruff_handler = RuffPythonFileChangeHandler()
|
|
25
|
+
self.running = True
|
|
26
|
+
self.observer = Observer()
|
|
27
|
+
|
|
28
|
+
def run(self):
|
|
29
|
+
"""Start the watchdog observer loop."""
|
|
30
|
+
if not self.check_path.exists():
|
|
31
|
+
print(f"[Error] Path does not exist: {self.check_path}", file=sys.stderr)
|
|
32
|
+
return
|
|
33
|
+
|
|
34
|
+
# 設定監控
|
|
35
|
+
self.observer.schedule(self.ruff_handler, str(self.check_path), recursive=True)
|
|
36
|
+
self.observer.start()
|
|
37
|
+
print(f"[Watchdog] Monitoring started on {self.check_path}")
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
while self.running:
|
|
41
|
+
time.sleep(1)
|
|
42
|
+
# 這裡可以加上 queue 輸出處理
|
|
43
|
+
self._process_ruff_output()
|
|
44
|
+
except KeyboardInterrupt:
|
|
45
|
+
print("[Watchdog] Interrupted by user")
|
|
46
|
+
finally:
|
|
47
|
+
self.observer.stop()
|
|
48
|
+
self.observer.join()
|
|
49
|
+
print("[Watchdog] Monitoring stopped")
|
|
50
|
+
|
|
51
|
+
def stop(self):
|
|
52
|
+
"""Stop the watchdog thread safely."""
|
|
53
|
+
self.running = False
|
|
54
|
+
|
|
55
|
+
def _process_ruff_output(self):
|
|
56
|
+
"""Process stdout/stderr queues from Ruff threads."""
|
|
57
|
+
while not self.ruff_handler.stdout_queue.empty():
|
|
58
|
+
line = self.ruff_handler.stdout_queue.get()
|
|
59
|
+
print(f"[Ruff STDOUT] {line}")
|
|
60
|
+
|
|
61
|
+
while not self.ruff_handler.stderr_queue.empty():
|
|
62
|
+
line = self.ruff_handler.stderr_queue.get()
|
|
63
|
+
print(f"[Ruff STDERR] {line}", file=sys.stderr)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
if __name__ == '__main__':
|
|
67
|
+
# 預設監控當前目錄
|
|
68
|
+
path_to_watch = "."
|
|
69
|
+
watchdog_thread = WatchdogThread(path_to_watch)
|
|
70
|
+
watchdog_thread.start()
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
while True:
|
|
74
|
+
time.sleep(1)
|
|
75
|
+
except KeyboardInterrupt:
|
|
76
|
+
print("[Main] Stopping watchdog...")
|
|
77
|
+
watchdog_thread.stop()
|
|
78
|
+
watchdog_thread.join()
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from dataclasses import dataclass, field
|
|
3
|
+
from typing import List, Dict, Any
|
|
4
|
+
|
|
5
|
+
log = logging.getLogger(__name__)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class CommitNode:
|
|
10
|
+
commit_sha: str
|
|
11
|
+
author_name: str
|
|
12
|
+
commit_date: str
|
|
13
|
+
commit_message: str
|
|
14
|
+
parent_shas: List[str]
|
|
15
|
+
lane_index: int = -1 # assigned later
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class CommitGraph:
|
|
20
|
+
nodes: List[CommitNode] = field(default_factory=list)
|
|
21
|
+
index: Dict[str, int] = field(default_factory=dict) # sha -> row
|
|
22
|
+
|
|
23
|
+
def build(self, commits: List[Dict[str, Any]], refs: Dict[str, str] | None = None) -> None:
|
|
24
|
+
"""
|
|
25
|
+
Build commit graph from topo-ordered commits.
|
|
26
|
+
從 topo-order 的 commits 建立 commit graph。
|
|
27
|
+
"""
|
|
28
|
+
self.nodes = [
|
|
29
|
+
CommitNode(
|
|
30
|
+
commit_sha=commit["sha"],
|
|
31
|
+
author_name=commit["author"],
|
|
32
|
+
commit_date=commit["date"],
|
|
33
|
+
commit_message=commit["message"],
|
|
34
|
+
parent_shas=commit["parents"],
|
|
35
|
+
)
|
|
36
|
+
for commit in commits
|
|
37
|
+
]
|
|
38
|
+
self.index = {node.commit_sha: index for index, node in enumerate(self.nodes)}
|
|
39
|
+
self._assign_lanes()
|
|
40
|
+
|
|
41
|
+
def _assign_lanes(self) -> None:
|
|
42
|
+
"""
|
|
43
|
+
Assign lanes to commits, similar to `git log --graph`.
|
|
44
|
+
分配 lanes,模擬 `git log --graph` 的效果。
|
|
45
|
+
"""
|
|
46
|
+
active: Dict[int, str] = {} # lane -> sha
|
|
47
|
+
free_lanes: List[int] = []
|
|
48
|
+
|
|
49
|
+
for node in self.nodes:
|
|
50
|
+
# Step 1: 找到 lane
|
|
51
|
+
lane_found = next((lane for lane, sha in active.items() if sha == node.commit_sha), None)
|
|
52
|
+
|
|
53
|
+
if lane_found is not None:
|
|
54
|
+
node.lane_index = lane_found
|
|
55
|
+
elif free_lanes:
|
|
56
|
+
node.lane_index = free_lanes.pop(0)
|
|
57
|
+
else:
|
|
58
|
+
node.lane_index = 0 if not active else max(active.keys()) + 1
|
|
59
|
+
|
|
60
|
+
# Step 2: 更新 active
|
|
61
|
+
# 移除舊的 sha
|
|
62
|
+
active = {lane: sha for lane, sha in active.items() if sha != node.commit_sha}
|
|
63
|
+
|
|
64
|
+
# 父節點分配 lane
|
|
65
|
+
if node.parent_shas:
|
|
66
|
+
first_parent = node.parent_shas[0]
|
|
67
|
+
active[node.lane_index] = first_parent
|
|
68
|
+
for p in node.parent_shas[1:]:
|
|
69
|
+
pl = free_lanes.pop(0) if free_lanes else (max(active.keys()) + 1)
|
|
70
|
+
active[pl] = p
|
|
71
|
+
|
|
72
|
+
# Step 3: 更新 free_lanes
|
|
73
|
+
if active:
|
|
74
|
+
max_lane = max(active.keys())
|
|
75
|
+
used = set(active.keys())
|
|
76
|
+
all_lanes = set(range(max_lane + 1))
|
|
77
|
+
free_lanes = sorted(set(free_lanes).union(all_lanes - used))
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
|
|
4
|
+
from PySide6.QtCore import QThread, Signal
|
|
5
|
+
from git import Repo, GitCommandError, InvalidGitRepositoryError, NoSuchPathError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# Simple audit logger
|
|
9
|
+
def audit_log(repo_path: str, action: str, detail: str, ok: bool, err: str = ""):
|
|
10
|
+
"""
|
|
11
|
+
Append an audit log entry to 'audit.log' in the repo directory.
|
|
12
|
+
This is useful for compliance and traceability.
|
|
13
|
+
"""
|
|
14
|
+
try:
|
|
15
|
+
path = os.path.join(repo_path if repo_path else ".", "audit.log")
|
|
16
|
+
with open(path, "a", encoding="utf-8") as f:
|
|
17
|
+
ts = datetime.now().isoformat(timespec="seconds")
|
|
18
|
+
f.write(f"{ts}\taction={action}\tok={ok}\tdetail={detail}\terr={err}\n")
|
|
19
|
+
except Exception:
|
|
20
|
+
pass # Never let audit logging failure break the UI
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# Git service layer
|
|
24
|
+
class GitService:
|
|
25
|
+
"""
|
|
26
|
+
Encapsulates Git operations using GitPython.
|
|
27
|
+
Keeps UI logic separate from Git logic.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self):
|
|
31
|
+
self.repo: Repo | None = None
|
|
32
|
+
self.repo_path: str | None = None
|
|
33
|
+
|
|
34
|
+
def open_repo(self, path: str):
|
|
35
|
+
try:
|
|
36
|
+
self.repo = Repo(path)
|
|
37
|
+
self.repo_path = path
|
|
38
|
+
audit_log(path, "open_repo", path, True)
|
|
39
|
+
except (InvalidGitRepositoryError, NoSuchPathError) as e:
|
|
40
|
+
audit_log(path, "open_repo", path, False, str(e))
|
|
41
|
+
raise
|
|
42
|
+
|
|
43
|
+
def list_branches(self):
|
|
44
|
+
self._ensure_repo()
|
|
45
|
+
branches = [head.name for head in self.repo.heads]
|
|
46
|
+
audit_log(self.repo_path, "list_branches", ",".join(branches), True)
|
|
47
|
+
return branches
|
|
48
|
+
|
|
49
|
+
def current_branch(self):
|
|
50
|
+
self._ensure_repo()
|
|
51
|
+
try:
|
|
52
|
+
return self.repo.active_branch.name
|
|
53
|
+
except TypeError:
|
|
54
|
+
return "(detached HEAD)"
|
|
55
|
+
|
|
56
|
+
def checkout(self, branch: str):
|
|
57
|
+
self._ensure_repo()
|
|
58
|
+
try:
|
|
59
|
+
self.repo.git.checkout(branch)
|
|
60
|
+
audit_log(self.repo_path, "checkout", branch, True)
|
|
61
|
+
except GitCommandError as e:
|
|
62
|
+
audit_log(self.repo_path, "checkout", branch, False, str(e))
|
|
63
|
+
raise
|
|
64
|
+
|
|
65
|
+
def list_commits(self, branch: str, max_count: int = 100):
|
|
66
|
+
self._ensure_repo()
|
|
67
|
+
commits = list(self.repo.iter_commits(branch, max_count=max_count))
|
|
68
|
+
data = [
|
|
69
|
+
{
|
|
70
|
+
"hexsha": c.hexsha,
|
|
71
|
+
"summary": c.summary,
|
|
72
|
+
"author": c.author.name if c.author else "",
|
|
73
|
+
"date": datetime.fromtimestamp(c.committed_date).isoformat(sep=" ", timespec="seconds"),
|
|
74
|
+
}
|
|
75
|
+
for c in commits
|
|
76
|
+
]
|
|
77
|
+
audit_log(self.repo_path, "list_commits", f"{branch}:{len(data)}", True)
|
|
78
|
+
return data
|
|
79
|
+
|
|
80
|
+
def show_diff_of_commit(self, commit_sha: str) -> str:
|
|
81
|
+
self._ensure_repo()
|
|
82
|
+
commit = self.repo.commit(commit_sha)
|
|
83
|
+
parent = commit.parents[0] if commit.parents else None
|
|
84
|
+
if parent is None:
|
|
85
|
+
null_tree = self.repo.tree(NULL_TREE)
|
|
86
|
+
diffs = commit.diff(null_tree, create_patch=True)
|
|
87
|
+
else:
|
|
88
|
+
diffs = commit.diff(parent, create_patch=True)
|
|
89
|
+
text = []
|
|
90
|
+
for d in diffs:
|
|
91
|
+
try:
|
|
92
|
+
text.append(d.diff.decode("utf-8", errors="replace"))
|
|
93
|
+
except Exception:
|
|
94
|
+
pass
|
|
95
|
+
out = "".join(text) if text else "(No patch content)"
|
|
96
|
+
audit_log(self.repo_path, "show_diff", commit_sha, True)
|
|
97
|
+
return out
|
|
98
|
+
|
|
99
|
+
def stage_all(self):
|
|
100
|
+
self._ensure_repo()
|
|
101
|
+
try:
|
|
102
|
+
self.repo.git.add(all=True)
|
|
103
|
+
audit_log(self.repo_path, "stage_all", "git_client add -A", True)
|
|
104
|
+
except GitCommandError as e:
|
|
105
|
+
audit_log(self.repo_path, "stage_all", "git_client add -A", False, str(e))
|
|
106
|
+
raise
|
|
107
|
+
|
|
108
|
+
def commit(self, message: str):
|
|
109
|
+
self._ensure_repo()
|
|
110
|
+
if not message.strip():
|
|
111
|
+
raise ValueError("Commit message is empty.")
|
|
112
|
+
try:
|
|
113
|
+
self.repo.index.commit(message)
|
|
114
|
+
audit_log(self.repo_path, "commit", message, True)
|
|
115
|
+
except Exception as e:
|
|
116
|
+
audit_log(self.repo_path, "commit", message, False, str(e))
|
|
117
|
+
raise
|
|
118
|
+
|
|
119
|
+
def pull(self, remote: str = "origin", branch: str | None = None):
|
|
120
|
+
self._ensure_repo()
|
|
121
|
+
if branch is None:
|
|
122
|
+
branch = self.current_branch()
|
|
123
|
+
try:
|
|
124
|
+
res = self.repo.git.pull(remote, branch)
|
|
125
|
+
audit_log(self.repo_path, "pull", f"{remote}/{branch}", True)
|
|
126
|
+
return res
|
|
127
|
+
except GitCommandError as e:
|
|
128
|
+
audit_log(self.repo_path, "pull", f"{remote}/{branch}", False, str(e))
|
|
129
|
+
raise
|
|
130
|
+
|
|
131
|
+
def push(self, remote: str = "origin", branch: str | None = None):
|
|
132
|
+
self._ensure_repo()
|
|
133
|
+
if branch is None:
|
|
134
|
+
branch = self.current_branch()
|
|
135
|
+
try:
|
|
136
|
+
res = self.repo.git.push(remote, branch)
|
|
137
|
+
audit_log(self.repo_path, "push", f"{remote}/{branch}", True)
|
|
138
|
+
return res
|
|
139
|
+
except GitCommandError as e:
|
|
140
|
+
audit_log(self.repo_path, "push", f"{remote}/{branch}", False, str(e))
|
|
141
|
+
raise
|
|
142
|
+
|
|
143
|
+
def remotes(self):
|
|
144
|
+
self._ensure_repo()
|
|
145
|
+
return [r.name for r in self.repo.remotes]
|
|
146
|
+
|
|
147
|
+
def _ensure_repo(self):
|
|
148
|
+
if self.repo is None:
|
|
149
|
+
raise RuntimeError("Repository not opened.")
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
# Null tree constant for initial commit diff
|
|
153
|
+
NULL_TREE = "4b825dc642cb6eb9a060e54bf8d69288fbee4904"
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# Worker thread wrapper
|
|
157
|
+
class GitWorker(QThread):
|
|
158
|
+
"""
|
|
159
|
+
Runs a function in a separate thread to avoid blocking the UI.
|
|
160
|
+
Emits (result, error) when done.
|
|
161
|
+
"""
|
|
162
|
+
done = Signal(object, object)
|
|
163
|
+
|
|
164
|
+
def __init__(self, fn, *args, **kwargs):
|
|
165
|
+
super().__init__()
|
|
166
|
+
self.fn = fn
|
|
167
|
+
self.args = args
|
|
168
|
+
self.kwargs = kwargs
|
|
169
|
+
|
|
170
|
+
def run(self):
|
|
171
|
+
try:
|
|
172
|
+
res = self.fn(*self.args, **self.kwargs)
|
|
173
|
+
self.done.emit(res, None)
|
|
174
|
+
except Exception as e:
|
|
175
|
+
self.done.emit(None, e)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import subprocess
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import List, Dict
|
|
5
|
+
|
|
6
|
+
log = logging.getLogger(__name__)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class GitCLI:
|
|
10
|
+
def __init__(self, repo_path: Path):
|
|
11
|
+
self.repo_path = Path(repo_path)
|
|
12
|
+
|
|
13
|
+
def is_git_repo(self) -> bool:
|
|
14
|
+
return (self.repo_path / ".git").exists()
|
|
15
|
+
|
|
16
|
+
def _run(self, args: List[str]) -> str:
|
|
17
|
+
log.debug("git %s", " ".join(args))
|
|
18
|
+
res = subprocess.run(
|
|
19
|
+
["git"] + args,
|
|
20
|
+
cwd=self.repo_path,
|
|
21
|
+
stdout=subprocess.PIPE,
|
|
22
|
+
stderr=subprocess.PIPE,
|
|
23
|
+
text=True,
|
|
24
|
+
encoding="utf-8",
|
|
25
|
+
)
|
|
26
|
+
if res.returncode != 0:
|
|
27
|
+
log.error("Git failed: %s", res.stderr.strip())
|
|
28
|
+
raise RuntimeError(res.stderr.strip())
|
|
29
|
+
return res.stdout
|
|
30
|
+
|
|
31
|
+
def get_all_refs(self) -> Dict[str, str]:
|
|
32
|
+
# returns refname -> commit hash
|
|
33
|
+
out = self._run(["show-ref", "--heads", "--tags"])
|
|
34
|
+
refs = {}
|
|
35
|
+
for line in out.splitlines():
|
|
36
|
+
if not line.strip():
|
|
37
|
+
continue
|
|
38
|
+
sha, ref = line.split(" ", 1)
|
|
39
|
+
refs[ref.strip()] = sha.strip()
|
|
40
|
+
return refs
|
|
41
|
+
|
|
42
|
+
def get_commits(self, max_count: int = 500) -> List[Dict]:
|
|
43
|
+
"""
|
|
44
|
+
Return recent commits across all refs, with parents.
|
|
45
|
+
"""
|
|
46
|
+
fmt = "%H%x01%P%x01%an%x01%ad%x01%s"
|
|
47
|
+
out = self._run([
|
|
48
|
+
"log", "--date=short", f"--format={fmt}", "--all",
|
|
49
|
+
f"--max-count={max_count}", "--topo-order"
|
|
50
|
+
])
|
|
51
|
+
commits = []
|
|
52
|
+
for line in out.splitlines():
|
|
53
|
+
if not line.strip():
|
|
54
|
+
continue
|
|
55
|
+
parts = line.split("\x01")
|
|
56
|
+
if len(parts) != 5:
|
|
57
|
+
continue
|
|
58
|
+
sha, parents, author, date, msg = parts
|
|
59
|
+
commits.append({
|
|
60
|
+
"sha": sha,
|
|
61
|
+
"parents": [p for p in parents.strip().split() if p],
|
|
62
|
+
"author": author,
|
|
63
|
+
"date": date,
|
|
64
|
+
"message": msg,
|
|
65
|
+
})
|
|
66
|
+
return commits
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from PySide6.QtCore import Qt
|
|
2
|
+
from PySide6.QtWebEngineCore import QWebEngineDownloadRequest
|
|
3
|
+
from PySide6.QtWidgets import QWidget, QBoxLayout, QPlainTextEdit
|
|
4
|
+
|
|
5
|
+
from je_editor.utils.logging.loggin_instance import jeditor_logger
|
|
6
|
+
from je_editor.utils.multi_language.multi_language_wrapper import language_wrapper
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BrowserDownloadWindow(QWidget):
|
|
10
|
+
"""
|
|
11
|
+
A window widget to display details of a browser download.
|
|
12
|
+
瀏覽器下載視窗,用來顯示下載的詳細資訊。
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, download_instance: QWebEngineDownloadRequest):
|
|
16
|
+
"""
|
|
17
|
+
Initialize the download window with a given QWebEngineDownloadRequest.
|
|
18
|
+
使用指定的 QWebEngineDownloadRequest 初始化下載視窗。
|
|
19
|
+
"""
|
|
20
|
+
super().__init__()
|
|
21
|
+
# 記錄初始化訊息到 logger
|
|
22
|
+
jeditor_logger.info("Init BrowserDownloadWindow "
|
|
23
|
+
f"download_instance: {download_instance}")
|
|
24
|
+
|
|
25
|
+
# 設定視窗屬性:當視窗關閉時自動刪除
|
|
26
|
+
self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose)
|
|
27
|
+
|
|
28
|
+
# 建立垂直方向的 BoxLayout
|
|
29
|
+
self.box_layout = QBoxLayout(QBoxLayout.Direction.TopToBottom)
|
|
30
|
+
|
|
31
|
+
# 建立文字框來顯示下載細節,並設為唯讀
|
|
32
|
+
self.show_download_detail_plaintext = QPlainTextEdit()
|
|
33
|
+
self.show_download_detail_plaintext.setReadOnly(True)
|
|
34
|
+
|
|
35
|
+
# 設定視窗標題,支援多語言
|
|
36
|
+
self.setWindowTitle(language_wrapper.language_word_dict.get("browser_download_detail"))
|
|
37
|
+
|
|
38
|
+
# 儲存下載實例
|
|
39
|
+
self.download_instance = download_instance
|
|
40
|
+
|
|
41
|
+
# 綁定下載事件到對應的處理函式
|
|
42
|
+
self.download_instance.isFinishedChanged.connect(self.print_finish) # 當下載完成時
|
|
43
|
+
self.download_instance.interruptReasonChanged.connect(self.print_interrupt) # 當下載被中斷時
|
|
44
|
+
self.download_instance.stateChanged.connect(self.print_state) # 當下載狀態改變時
|
|
45
|
+
|
|
46
|
+
# 接受下載請求,開始下載
|
|
47
|
+
self.download_instance.accept()
|
|
48
|
+
|
|
49
|
+
# 將文字框加入版面配置
|
|
50
|
+
self.box_layout.addWidget(self.show_download_detail_plaintext)
|
|
51
|
+
self.setLayout(self.box_layout)
|
|
52
|
+
|
|
53
|
+
def print_finish(self):
|
|
54
|
+
"""
|
|
55
|
+
Slot function triggered when download finishes.
|
|
56
|
+
當下載完成時觸發,將完成狀態輸出到 logger 與文字框。
|
|
57
|
+
"""
|
|
58
|
+
jeditor_logger.info("BrowserDownloadWindow Print Download is Finished")
|
|
59
|
+
self.show_download_detail_plaintext.appendPlainText(str(self.download_instance.isFinished()))
|
|
60
|
+
|
|
61
|
+
def print_interrupt(self):
|
|
62
|
+
"""
|
|
63
|
+
Slot function triggered when download is interrupted.
|
|
64
|
+
當下載被中斷時觸發,將中斷原因輸出到 logger 與文字框。
|
|
65
|
+
"""
|
|
66
|
+
jeditor_logger.info("BrowserDownloadWindow Print interruptReason")
|
|
67
|
+
self.show_download_detail_plaintext.appendPlainText(str(self.download_instance.interruptReason()))
|
|
68
|
+
|
|
69
|
+
def print_state(self):
|
|
70
|
+
"""
|
|
71
|
+
Slot function triggered when download state changes.
|
|
72
|
+
當下載狀態改變時觸發,將狀態輸出到 logger 與文字框。
|
|
73
|
+
"""
|
|
74
|
+
jeditor_logger.info("BrowserDownloadWindow Print State")
|
|
75
|
+
self.show_download_detail_plaintext.appendPlainText(str(self.download_instance.state()))
|