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.
Files changed (151) hide show
  1. je_editor/__init__.py +26 -21
  2. je_editor/__main__.py +1 -1
  3. je_editor/code_scan/ruff_thread.py +58 -0
  4. je_editor/code_scan/watchdog_implement.py +56 -0
  5. je_editor/code_scan/watchdog_thread.py +78 -0
  6. je_editor/git_client/commit_graph.py +77 -0
  7. je_editor/git_client/git_action.py +175 -0
  8. je_editor/git_client/git_cli.py +66 -0
  9. je_editor/pyside_ui/browser/browser_download_window.py +75 -0
  10. je_editor/pyside_ui/browser/browser_serach_lineedit.py +51 -0
  11. je_editor/pyside_ui/browser/browser_view.py +87 -0
  12. je_editor/pyside_ui/browser/browser_widget.py +103 -0
  13. je_editor/pyside_ui/browser/main_browser_widget.py +85 -0
  14. je_editor/pyside_ui/code/auto_save/auto_save_manager.py +60 -0
  15. je_editor/pyside_ui/code/auto_save/auto_save_thread.py +59 -0
  16. je_editor/pyside_ui/code/code_format/pep8_format.py +130 -0
  17. je_editor/pyside_ui/code/code_process/code_exec.py +267 -0
  18. je_editor/pyside_ui/code/plaintext_code_edit/code_edit_plaintext.py +412 -0
  19. je_editor/pyside_ui/code/running_process_manager.py +48 -0
  20. je_editor/pyside_ui/code/shell_process/shell_exec.py +236 -0
  21. je_editor/pyside_ui/code/syntax/python_syntax.py +99 -0
  22. je_editor/pyside_ui/code/syntax/syntax_setting.py +95 -0
  23. je_editor/pyside_ui/code/textedit_code_result/code_record.py +75 -0
  24. je_editor/pyside_ui/code/variable_inspector/inspector_gui.py +172 -0
  25. je_editor/pyside_ui/dialog/ai_dialog/set_ai_dialog.py +71 -0
  26. je_editor/pyside_ui/dialog/file_dialog/create_file_dialog.py +68 -0
  27. je_editor/pyside_ui/dialog/file_dialog/open_file_dialog.py +111 -0
  28. je_editor/pyside_ui/dialog/file_dialog/save_file_dialog.py +67 -0
  29. je_editor/pyside_ui/dialog/search_ui/search_error_box.py +49 -0
  30. je_editor/pyside_ui/dialog/search_ui/search_text_box.py +49 -0
  31. je_editor/pyside_ui/git_ui/code_diff_compare/code_diff_viewer_widget.py +90 -0
  32. je_editor/pyside_ui/git_ui/code_diff_compare/line_number_code_viewer.py +141 -0
  33. je_editor/pyside_ui/git_ui/code_diff_compare/multi_file_diff_viewer.py +88 -0
  34. je_editor/pyside_ui/git_ui/code_diff_compare/side_by_side_diff_widget.py +284 -0
  35. je_editor/pyside_ui/git_ui/git_client/commit_table.py +65 -0
  36. je_editor/pyside_ui/git_ui/git_client/git_branch_tree_widget.py +156 -0
  37. je_editor/pyside_ui/git_ui/git_client/git_client_gui.py +799 -0
  38. je_editor/pyside_ui/git_ui/git_client/graph_view.py +218 -0
  39. je_editor/pyside_ui/main_ui/ai_widget/ai_config.py +34 -0
  40. je_editor/pyside_ui/main_ui/ai_widget/ask_thread.py +36 -0
  41. je_editor/pyside_ui/main_ui/ai_widget/chat_ui.py +147 -0
  42. je_editor/pyside_ui/main_ui/ai_widget/langchain_interface.py +84 -0
  43. je_editor/pyside_ui/main_ui/console_widget/console_gui.py +162 -0
  44. je_editor/pyside_ui/main_ui/console_widget/qprocess_adapter.py +84 -0
  45. je_editor/pyside_ui/main_ui/dock/__init__.py +0 -0
  46. je_editor/pyside_ui/main_ui/dock/destroy_dock.py +50 -0
  47. je_editor/pyside_ui/main_ui/editor/__init__.py +0 -0
  48. je_editor/pyside_ui/main_ui/editor/editor_widget.py +301 -0
  49. je_editor/pyside_ui/main_ui/editor/editor_widget_dock.py +70 -0
  50. je_editor/pyside_ui/main_ui/editor/process_input.py +101 -0
  51. je_editor/pyside_ui/main_ui/ipython_widget/__init__.py +0 -0
  52. je_editor/pyside_ui/main_ui/ipython_widget/rich_jupyter.py +78 -0
  53. je_editor/pyside_ui/main_ui/main_editor.py +369 -0
  54. je_editor/pyside_ui/main_ui/menu/__init__.py +0 -0
  55. je_editor/pyside_ui/main_ui/menu/check_style_menu/__init__.py +0 -0
  56. je_editor/pyside_ui/main_ui/menu/check_style_menu/build_check_style_menu.py +104 -0
  57. je_editor/pyside_ui/main_ui/menu/dock_menu/__init__.py +0 -0
  58. je_editor/pyside_ui/main_ui/menu/dock_menu/build_dock_menu.py +208 -0
  59. je_editor/pyside_ui/main_ui/menu/file_menu/__init__.py +0 -0
  60. je_editor/pyside_ui/main_ui/menu/file_menu/build_file_menu.py +186 -0
  61. je_editor/pyside_ui/main_ui/menu/help_menu/__init__.py +0 -0
  62. je_editor/pyside_ui/main_ui/menu/help_menu/build_help_menu.py +100 -0
  63. je_editor/pyside_ui/main_ui/menu/language_menu/__init__.py +0 -0
  64. je_editor/pyside_ui/main_ui/menu/language_menu/build_language_server.py +89 -0
  65. je_editor/pyside_ui/main_ui/menu/python_env_menu/__init__.py +0 -0
  66. je_editor/pyside_ui/main_ui/menu/python_env_menu/build_venv_menu.py +238 -0
  67. je_editor/pyside_ui/main_ui/menu/run_menu/__init__.py +0 -0
  68. je_editor/pyside_ui/main_ui/menu/run_menu/build_run_menu.py +160 -0
  69. je_editor/pyside_ui/main_ui/menu/run_menu/under_run_menu/__init__.py +0 -0
  70. je_editor/pyside_ui/main_ui/menu/run_menu/under_run_menu/build_debug_menu.py +109 -0
  71. je_editor/pyside_ui/main_ui/menu/run_menu/under_run_menu/build_program_menu.py +101 -0
  72. je_editor/pyside_ui/main_ui/menu/run_menu/under_run_menu/build_shell_menu.py +98 -0
  73. je_editor/pyside_ui/main_ui/menu/run_menu/under_run_menu/utils.py +41 -0
  74. je_editor/pyside_ui/main_ui/menu/set_menu_bar.py +63 -0
  75. je_editor/pyside_ui/main_ui/menu/style_menu/__init__.py +0 -0
  76. je_editor/pyside_ui/main_ui/menu/style_menu/build_style_menu.py +73 -0
  77. je_editor/pyside_ui/main_ui/menu/tab_menu/__init__.py +0 -0
  78. je_editor/pyside_ui/main_ui/menu/tab_menu/build_tab_menu.py +275 -0
  79. je_editor/pyside_ui/main_ui/menu/text_menu/__init__.py +0 -0
  80. je_editor/pyside_ui/main_ui/menu/text_menu/build_text_menu.py +135 -0
  81. je_editor/pyside_ui/main_ui/save_settings/__init__.py +0 -0
  82. je_editor/pyside_ui/main_ui/save_settings/setting_utils.py +33 -0
  83. je_editor/pyside_ui/main_ui/save_settings/user_color_setting_file.py +103 -0
  84. je_editor/pyside_ui/main_ui/save_settings/user_setting_file.py +58 -0
  85. je_editor/pyside_ui/main_ui/system_tray/__init__.py +0 -0
  86. je_editor/pyside_ui/main_ui/system_tray/extend_system_tray.py +90 -0
  87. je_editor/start_editor.py +32 -8
  88. je_editor/utils/encodings/python_encodings.py +100 -97
  89. je_editor/utils/exception/exception_tags.py +11 -11
  90. je_editor/utils/file/open/open_file.py +38 -22
  91. je_editor/utils/file/save/save_file.py +40 -16
  92. je_editor/utils/json/json_file.py +36 -15
  93. je_editor/utils/json_format/json_process.py +38 -2
  94. je_editor/utils/logging/__init__.py +0 -0
  95. je_editor/utils/logging/loggin_instance.py +57 -0
  96. je_editor/utils/multi_language/__init__.py +0 -0
  97. je_editor/utils/multi_language/english.py +221 -0
  98. je_editor/utils/multi_language/multi_language_wrapper.py +54 -0
  99. je_editor/utils/multi_language/traditional_chinese.py +214 -0
  100. je_editor/utils/redirect_manager/redirect_manager_class.py +67 -25
  101. je_editor/utils/venv_check/__init__.py +0 -0
  102. je_editor/utils/venv_check/check_venv.py +51 -0
  103. je_editor-0.0.228.dist-info/METADATA +99 -0
  104. je_editor-0.0.228.dist-info/RECORD +140 -0
  105. {je_editor-0.0.104.dist-info → je_editor-0.0.228.dist-info}/WHEEL +1 -1
  106. {je_editor-0.0.104.dist-info → je_editor-0.0.228.dist-info/licenses}/LICENSE +1 -1
  107. je_editor/pyside_ui/auto_save/auto_save_thread.py +0 -34
  108. je_editor/pyside_ui/code_editor/code_edit_plaintext.py +0 -143
  109. je_editor/pyside_ui/code_process/code_exec.py +0 -190
  110. je_editor/pyside_ui/code_result/code_record.py +0 -39
  111. je_editor/pyside_ui/colors/global_color.py +0 -4
  112. je_editor/pyside_ui/file_dialog/open_file_dialog.py +0 -27
  113. je_editor/pyside_ui/file_dialog/save_file_dialog.py +0 -24
  114. je_editor/pyside_ui/main_ui/editor_main_ui/main_editor.py +0 -183
  115. je_editor/pyside_ui/main_ui_setting/ui_setting.py +0 -36
  116. je_editor/pyside_ui/menu/menu_bar/check_style_menu/build_check_style_menu.py +0 -44
  117. je_editor/pyside_ui/menu/menu_bar/file_menu/build_file_menu.py +0 -30
  118. je_editor/pyside_ui/menu/menu_bar/help_menu/build_help_menu.py +0 -39
  119. je_editor/pyside_ui/menu/menu_bar/run_menu/build_run_menu.py +0 -102
  120. je_editor/pyside_ui/menu/menu_bar/set_menu_bar.py +0 -24
  121. je_editor/pyside_ui/menu/menu_bar/venv_menu/build_venv_menu.py +0 -74
  122. je_editor/pyside_ui/search_ui/search_error_box.py +0 -20
  123. je_editor/pyside_ui/search_ui/search_text_box.py +0 -20
  124. je_editor/pyside_ui/shell_process/shell_exec.py +0 -157
  125. je_editor/pyside_ui/syntax/python_syntax.py +0 -99
  126. je_editor/pyside_ui/treeview/project_treeview/set_project_treeview.py +0 -47
  127. je_editor/pyside_ui/user_setting/user_setting_file.py +0 -23
  128. je_editor-0.0.104.dist-info/METADATA +0 -84
  129. je_editor-0.0.104.dist-info/RECORD +0 -69
  130. /je_editor/{pyside_ui/auto_save → code_scan}/__init__.py +0 -0
  131. /je_editor/{pyside_ui/code_editor → git_client}/__init__.py +0 -0
  132. /je_editor/pyside_ui/{code_process → browser}/__init__.py +0 -0
  133. /je_editor/pyside_ui/{code_result → code}/__init__.py +0 -0
  134. /je_editor/pyside_ui/{colors → code/auto_save}/__init__.py +0 -0
  135. /je_editor/pyside_ui/{file_dialog → code/code_format}/__init__.py +0 -0
  136. /je_editor/pyside_ui/{main_ui/editor_main_ui → code/code_process}/__init__.py +0 -0
  137. /je_editor/pyside_ui/{main_ui_setting → code/plaintext_code_edit}/__init__.py +0 -0
  138. /je_editor/pyside_ui/{menu → code/shell_process}/__init__.py +0 -0
  139. /je_editor/pyside_ui/{menu/menu_bar → code/syntax}/__init__.py +0 -0
  140. /je_editor/pyside_ui/{menu/menu_bar/check_style_menu → code/textedit_code_result}/__init__.py +0 -0
  141. /je_editor/pyside_ui/{menu/menu_bar/file_menu → code/variable_inspector}/__init__.py +0 -0
  142. /je_editor/pyside_ui/{menu/menu_bar/help_menu → dialog}/__init__.py +0 -0
  143. /je_editor/pyside_ui/{menu/menu_bar/run_menu → dialog/ai_dialog}/__init__.py +0 -0
  144. /je_editor/pyside_ui/{menu/menu_bar/venv_menu → dialog/file_dialog}/__init__.py +0 -0
  145. /je_editor/pyside_ui/{search_ui → dialog/search_ui}/__init__.py +0 -0
  146. /je_editor/pyside_ui/{shell_process → git_ui}/__init__.py +0 -0
  147. /je_editor/pyside_ui/{syntax → git_ui/code_diff_compare}/__init__.py +0 -0
  148. /je_editor/pyside_ui/{treeview → git_ui/git_client}/__init__.py +0 -0
  149. /je_editor/pyside_ui/{treeview/project_treeview → main_ui/ai_widget}/__init__.py +0 -0
  150. /je_editor/pyside_ui/{user_setting → main_ui/console_widget}/__init__.py +0 -0
  151. {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
- # start editor
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
- # Editor
4
- from je_editor.pyside_ui.main_ui.editor_main_ui.main_editor import EditorMain
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.exception.exceptions import JEditorOpenFileException
11
- from je_editor.utils.exception.exceptions import JEditorContentFileException
12
- from je_editor.utils.exception.exceptions import JEditorCantFindLanguageException
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
- "error_color", "output_color",
28
- "exec_manage", "shell_manager"
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
@@ -2,7 +2,7 @@ if __name__ == "__main__":
2
2
  # argparse
3
3
  import argparse
4
4
 
5
- from je_editor.tkinter_ui.editor_main_ui.tkinter_editor import start_editor
5
+ from je_editor.start_editor import start_editor
6
6
 
7
7
  argparse_event_dict = {
8
8
  "start": start_editor,
@@ -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()))