je-editor 0.0.215__py3-none-any.whl → 0.0.216__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 (132) hide show
  1. git_client/commit_graph.py +88 -0
  2. git_client/git.py +181 -0
  3. git_client/git_cli.py +66 -0
  4. git_client/github.py +50 -0
  5. {je_editor-0.0.215.dist-info → je_editor-0.0.216.dist-info}/METADATA +2 -1
  6. je_editor-0.0.216.dist-info/RECORD +127 -0
  7. je_editor-0.0.216.dist-info/top_level.txt +4 -0
  8. pyside_ui/git_ui/commit_table.py +27 -0
  9. pyside_ui/git_ui/git_branch_tree_widget.py +119 -0
  10. pyside_ui/git_ui/git_client_gui.py +292 -0
  11. pyside_ui/git_ui/graph_view.py +174 -0
  12. pyside_ui/main_ui/ai_widget/ai_config.py +19 -0
  13. pyside_ui/main_ui/ai_widget/ask_thread.py +17 -0
  14. pyside_ui/main_ui/ai_widget/chat_ui.py +130 -0
  15. pyside_ui/main_ui/ai_widget/langchain_interface.py +45 -0
  16. pyside_ui/main_ui/menu/check_style_menu/build_check_style_menu.py +81 -0
  17. {je_editor/pyside_ui → pyside_ui}/main_ui/menu/dock_menu/build_dock_menu.py +2 -2
  18. {je_editor/pyside_ui → pyside_ui}/main_ui/menu/tab_menu/build_tab_menu.py +2 -2
  19. {je_editor/utils → utils}/multi_language/english.py +1 -1
  20. {je_editor/utils → utils}/multi_language/traditional_chinese.py +1 -1
  21. je_editor/__init__.py +0 -34
  22. je_editor/__main__.py +0 -18
  23. je_editor/start_editor.py +0 -17
  24. je_editor-0.0.215.dist-info/RECORD +0 -117
  25. je_editor-0.0.215.dist-info/top_level.txt +0 -1
  26. {je_editor/code_scan → code_scan}/__init__.py +0 -0
  27. {je_editor/code_scan → code_scan}/ruff_thread.py +0 -0
  28. {je_editor/code_scan → code_scan}/watchdog_implement.py +0 -0
  29. {je_editor/code_scan → code_scan}/watchdog_thread.py +0 -0
  30. {je_editor-0.0.215.dist-info → je_editor-0.0.216.dist-info}/WHEEL +0 -0
  31. {je_editor-0.0.215.dist-info → je_editor-0.0.216.dist-info}/licenses/LICENSE +0 -0
  32. {je_editor/pyside_ui → pyside_ui}/__init__.py +0 -0
  33. {je_editor/pyside_ui → pyside_ui}/browser/__init__.py +0 -0
  34. {je_editor/pyside_ui → pyside_ui}/browser/browser_download_window.py +0 -0
  35. {je_editor/pyside_ui → pyside_ui}/browser/browser_serach_lineedit.py +0 -0
  36. {je_editor/pyside_ui → pyside_ui}/browser/browser_view.py +0 -0
  37. {je_editor/pyside_ui → pyside_ui}/browser/browser_widget.py +0 -0
  38. {je_editor/pyside_ui → pyside_ui}/code/__init__.py +0 -0
  39. {je_editor/pyside_ui → pyside_ui}/code/auto_save/__init__.py +0 -0
  40. {je_editor/pyside_ui → pyside_ui}/code/auto_save/auto_save_manager.py +0 -0
  41. {je_editor/pyside_ui → pyside_ui}/code/auto_save/auto_save_thread.py +0 -0
  42. {je_editor/pyside_ui → pyside_ui}/code/code_format/__init__.py +0 -0
  43. {je_editor/pyside_ui → pyside_ui}/code/code_format/pep8_format.py +0 -0
  44. {je_editor/pyside_ui → pyside_ui}/code/code_process/__init__.py +0 -0
  45. {je_editor/pyside_ui → pyside_ui}/code/code_process/code_exec.py +0 -0
  46. {je_editor/pyside_ui → pyside_ui}/code/plaintext_code_edit/__init__.py +0 -0
  47. {je_editor/pyside_ui → pyside_ui}/code/plaintext_code_edit/code_edit_plaintext.py +0 -0
  48. {je_editor/pyside_ui → pyside_ui}/code/running_process_manager.py +0 -0
  49. {je_editor/pyside_ui → pyside_ui}/code/shell_process/__init__.py +0 -0
  50. {je_editor/pyside_ui → pyside_ui}/code/shell_process/shell_exec.py +0 -0
  51. {je_editor/pyside_ui → pyside_ui}/code/syntax/__init__.py +0 -0
  52. {je_editor/pyside_ui → pyside_ui}/code/syntax/python_syntax.py +0 -0
  53. {je_editor/pyside_ui → pyside_ui}/code/syntax/syntax_setting.py +0 -0
  54. {je_editor/pyside_ui → pyside_ui}/code/textedit_code_result/__init__.py +0 -0
  55. {je_editor/pyside_ui → pyside_ui}/code/textedit_code_result/code_record.py +0 -0
  56. {je_editor/pyside_ui → pyside_ui}/code/variable_inspector/__init__.py +0 -0
  57. {je_editor/pyside_ui → pyside_ui}/code/variable_inspector/inspector_gui.py +0 -0
  58. {je_editor/pyside_ui → pyside_ui}/dialog/__init__.py +0 -0
  59. {je_editor/pyside_ui → pyside_ui}/dialog/ai_dialog/__init__.py +0 -0
  60. {je_editor/pyside_ui → pyside_ui}/dialog/ai_dialog/set_ai_dialog.py +0 -0
  61. {je_editor/pyside_ui → pyside_ui}/dialog/file_dialog/__init__.py +0 -0
  62. {je_editor/pyside_ui → pyside_ui}/dialog/file_dialog/create_file_dialog.py +0 -0
  63. {je_editor/pyside_ui → pyside_ui}/dialog/file_dialog/open_file_dialog.py +0 -0
  64. {je_editor/pyside_ui → pyside_ui}/dialog/file_dialog/save_file_dialog.py +0 -0
  65. {je_editor/pyside_ui → pyside_ui}/dialog/search_ui/__init__.py +0 -0
  66. {je_editor/pyside_ui → pyside_ui}/dialog/search_ui/search_error_box.py +0 -0
  67. {je_editor/pyside_ui → pyside_ui}/dialog/search_ui/search_text_box.py +0 -0
  68. {je_editor/pyside_ui → pyside_ui}/main_ui/__init__.py +0 -0
  69. {je_editor/pyside_ui → pyside_ui}/main_ui/console_widget/__init__.py +0 -0
  70. {je_editor/pyside_ui → pyside_ui}/main_ui/console_widget/console_gui.py +0 -0
  71. {je_editor/pyside_ui → pyside_ui}/main_ui/console_widget/qprocess_adapter.py +0 -0
  72. {je_editor/pyside_ui → pyside_ui}/main_ui/dock/__init__.py +0 -0
  73. {je_editor/pyside_ui → pyside_ui}/main_ui/dock/destroy_dock.py +0 -0
  74. {je_editor/pyside_ui → pyside_ui}/main_ui/editor/__init__.py +0 -0
  75. {je_editor/pyside_ui → pyside_ui}/main_ui/editor/editor_widget.py +0 -0
  76. {je_editor/pyside_ui → pyside_ui}/main_ui/editor/editor_widget_dock.py +0 -0
  77. {je_editor/pyside_ui → pyside_ui}/main_ui/editor/process_input.py +0 -0
  78. {je_editor/pyside_ui → pyside_ui}/main_ui/ipython_widget/__init__.py +0 -0
  79. {je_editor/pyside_ui → pyside_ui}/main_ui/ipython_widget/rich_jupyter.py +0 -0
  80. {je_editor/pyside_ui → pyside_ui}/main_ui/main_editor.py +0 -0
  81. {je_editor/pyside_ui → pyside_ui}/main_ui/menu/__init__.py +0 -0
  82. {je_editor/pyside_ui → pyside_ui}/main_ui/menu/dock_menu/__init__.py +0 -0
  83. {je_editor/pyside_ui → pyside_ui}/main_ui/menu/file_menu/__init__.py +0 -0
  84. {je_editor/pyside_ui → pyside_ui}/main_ui/menu/file_menu/build_file_menu.py +0 -0
  85. {je_editor/pyside_ui → pyside_ui}/main_ui/menu/help_menu/__init__.py +0 -0
  86. {je_editor/pyside_ui → pyside_ui}/main_ui/menu/help_menu/build_help_menu.py +0 -0
  87. {je_editor/pyside_ui → pyside_ui}/main_ui/menu/language_menu/__init__.py +0 -0
  88. {je_editor/pyside_ui → pyside_ui}/main_ui/menu/language_menu/build_language_server.py +0 -0
  89. {je_editor/pyside_ui → pyside_ui}/main_ui/menu/python_env_menu/__init__.py +0 -0
  90. {je_editor/pyside_ui → pyside_ui}/main_ui/menu/python_env_menu/build_venv_menu.py +0 -0
  91. {je_editor/pyside_ui → pyside_ui}/main_ui/menu/run_menu/__init__.py +0 -0
  92. {je_editor/pyside_ui → pyside_ui}/main_ui/menu/run_menu/build_run_menu.py +0 -0
  93. {je_editor/pyside_ui → pyside_ui}/main_ui/menu/run_menu/under_run_menu/__init__.py +0 -0
  94. {je_editor/pyside_ui → pyside_ui}/main_ui/menu/run_menu/under_run_menu/build_debug_menu.py +0 -0
  95. {je_editor/pyside_ui → pyside_ui}/main_ui/menu/run_menu/under_run_menu/build_program_menu.py +0 -0
  96. {je_editor/pyside_ui → pyside_ui}/main_ui/menu/run_menu/under_run_menu/build_shell_menu.py +0 -0
  97. {je_editor/pyside_ui → pyside_ui}/main_ui/menu/run_menu/under_run_menu/utils.py +0 -0
  98. {je_editor/pyside_ui → pyside_ui}/main_ui/menu/set_menu_bar.py +0 -0
  99. {je_editor/pyside_ui → pyside_ui}/main_ui/menu/style_menu/__init__.py +0 -0
  100. {je_editor/pyside_ui → pyside_ui}/main_ui/menu/style_menu/build_style_menu.py +0 -0
  101. {je_editor/pyside_ui → pyside_ui}/main_ui/menu/tab_menu/__init__.py +0 -0
  102. {je_editor/pyside_ui → pyside_ui}/main_ui/menu/text_menu/__init__.py +0 -0
  103. {je_editor/pyside_ui → pyside_ui}/main_ui/menu/text_menu/build_text_menu.py +0 -0
  104. {je_editor/pyside_ui → pyside_ui}/main_ui/save_settings/__init__.py +0 -0
  105. {je_editor/pyside_ui → pyside_ui}/main_ui/save_settings/setting_utils.py +0 -0
  106. {je_editor/pyside_ui → pyside_ui}/main_ui/save_settings/user_color_setting_file.py +0 -0
  107. {je_editor/pyside_ui → pyside_ui}/main_ui/save_settings/user_setting_file.py +0 -0
  108. {je_editor/pyside_ui → pyside_ui}/main_ui/system_tray/__init__.py +0 -0
  109. {je_editor/pyside_ui → pyside_ui}/main_ui/system_tray/extend_system_tray.py +0 -0
  110. {je_editor/utils → utils}/__init__.py +0 -0
  111. {je_editor/utils → utils}/encodings/__init__.py +0 -0
  112. {je_editor/utils → utils}/encodings/python_encodings.py +0 -0
  113. {je_editor/utils → utils}/exception/__init__.py +0 -0
  114. {je_editor/utils → utils}/exception/exception_tags.py +0 -0
  115. {je_editor/utils → utils}/exception/exceptions.py +0 -0
  116. {je_editor/utils → utils}/file/__init__.py +0 -0
  117. {je_editor/utils → utils}/file/open/__init__.py +0 -0
  118. {je_editor/utils → utils}/file/open/open_file.py +0 -0
  119. {je_editor/utils → utils}/file/save/__init__.py +0 -0
  120. {je_editor/utils → utils}/file/save/save_file.py +0 -0
  121. {je_editor/utils → utils}/json/__init__.py +0 -0
  122. {je_editor/utils → utils}/json/json_file.py +0 -0
  123. {je_editor/utils → utils}/json_format/__init__.py +0 -0
  124. {je_editor/utils → utils}/json_format/json_process.py +0 -0
  125. {je_editor/utils → utils}/logging/__init__.py +0 -0
  126. {je_editor/utils → utils}/logging/loggin_instance.py +0 -0
  127. {je_editor/utils → utils}/multi_language/__init__.py +0 -0
  128. {je_editor/utils → utils}/multi_language/multi_language_wrapper.py +0 -0
  129. {je_editor/utils → utils}/redirect_manager/__init__.py +0 -0
  130. {je_editor/utils → utils}/redirect_manager/redirect_manager_class.py +0 -0
  131. {je_editor/utils → utils}/venv_check/__init__.py +0 -0
  132. {je_editor/utils → utils}/venv_check/check_venv.py +0 -0
@@ -0,0 +1,119 @@
1
+ import logging
2
+ from pathlib import Path
3
+
4
+ from PySide6.QtCore import QTimer, QFileSystemWatcher, Qt
5
+ from PySide6.QtGui import QAction
6
+ from PySide6.QtWidgets import (
7
+ QFileDialog, QToolBar, QMessageBox, QStatusBar,
8
+ QSplitter, QWidget, QVBoxLayout
9
+ )
10
+
11
+ from je_editor.git_client.commit_graph import CommitGraph
12
+ from je_editor.git_client.git_cli import GitCLI
13
+ from je_editor.pyside_ui.git_ui.commit_table import CommitTable
14
+ from je_editor.pyside_ui.git_ui.graph_view import CommitGraphView
15
+ from je_editor.utils.multi_language.multi_language_wrapper import language_wrapper
16
+
17
+ logging.basicConfig(level=logging.INFO)
18
+ log = logging.getLogger(__name__)
19
+
20
+
21
+ class GitTreeViewGUI(QWidget):
22
+ def __init__(self):
23
+ super().__init__()
24
+ self.language_wrapper_get = language_wrapper.language_word_dict.get
25
+ self.setWindowTitle(self.language_wrapper_get("git_graph_title"))
26
+
27
+ main_layout = QVBoxLayout(self)
28
+ main_layout.setContentsMargins(0, 0, 0, 0)
29
+ main_layout.setSpacing(0)
30
+
31
+ toolbar = QToolBar()
32
+ act_open = QAction(self.language_wrapper_get("git_graph_toolbar_open"), self)
33
+ act_open.triggered.connect(self.open_repo)
34
+ toolbar.addAction(act_open)
35
+
36
+ act_refresh = QAction(self.language_wrapper_get("git_graph_toolbar_refresh"), self)
37
+ act_refresh.triggered.connect(self.refresh_graph)
38
+ toolbar.addAction(act_refresh)
39
+
40
+ main_layout.addWidget(toolbar)
41
+
42
+ splitter = QSplitter(Qt.Orientation.Horizontal)
43
+ self.graph_view = CommitGraphView()
44
+ self.commit_table = CommitTable()
45
+ splitter.addWidget(self.graph_view)
46
+ splitter.addWidget(self.commit_table)
47
+ splitter.setSizes([600, 400])
48
+ main_layout.addWidget(splitter, 1) # 1 表示可伸縮
49
+
50
+ self.status = QStatusBar()
51
+ main_layout.addWidget(self.status)
52
+
53
+ self.repo_path = None
54
+ self.git = None
55
+
56
+ self.watcher = QFileSystemWatcher()
57
+ self.watcher.directoryChanged.connect(self._on_git_changed)
58
+ self.watcher.fileChanged.connect(self._on_git_changed)
59
+
60
+ self.refresh_timer = QTimer()
61
+ self.refresh_timer.setSingleShot(True)
62
+ self.refresh_timer.timeout.connect(self.refresh_graph)
63
+
64
+ self.commit_table.selectionModel().selectionChanged.connect(self._on_table_selection)
65
+
66
+ def _on_table_selection(self, selected, _):
67
+ if not selected.indexes():
68
+ return
69
+ row = selected.indexes()[0].row()
70
+ self.graph_view.focus_row(row)
71
+
72
+ def open_repo(self):
73
+ path = QFileDialog.getExistingDirectory(self, self.language_wrapper_get("git_graph_menu_open_repo"))
74
+ if not path:
75
+ return
76
+ repo_path = Path(path)
77
+ git = GitCLI(repo_path)
78
+ if not git.is_git_repo():
79
+ QMessageBox.warning(self, self.language_wrapper_get("git_graph_title"),
80
+ self.language_wrapper_get("git_graph_error_not_git"))
81
+ return
82
+ self.repo_path = repo_path
83
+ self.git = git
84
+ self._setup_watcher()
85
+ self.refresh_graph()
86
+
87
+ def _setup_watcher(self):
88
+ self.watcher.removePaths(self.watcher.files())
89
+ self.watcher.removePaths(self.watcher.directories())
90
+ if not self.repo_path:
91
+ return
92
+ git_dir = self.repo_path / ".git_client"
93
+ if git_dir.exists():
94
+ self.watcher.addPath(str(git_dir))
95
+ for f in ["HEAD", "packed-refs"]:
96
+ fp = git_dir / f
97
+ if fp.exists():
98
+ self.watcher.addPath(str(fp))
99
+ refs_dir = git_dir / "refs"
100
+ if refs_dir.exists():
101
+ self.watcher.addPath(str(refs_dir))
102
+
103
+ def _on_git_changed(self, path):
104
+ self.refresh_timer.start(500)
105
+
106
+ def refresh_graph(self):
107
+ if not self.git:
108
+ return
109
+ try:
110
+ refs = self.git.get_all_refs()
111
+ commits = self.git.get_commits(max_count=500)
112
+ graph = CommitGraph()
113
+ graph.build(commits, refs)
114
+ self.graph_view.set_graph(graph)
115
+ self.commit_table.set_commits(commits)
116
+ except Exception as e:
117
+ QMessageBox.critical(self, self.language_wrapper_get("git_graph_title"),
118
+ f"{self.language_wrapper_get('git_graph_error_exec_failed')}\n{e}")
119
+
@@ -0,0 +1,292 @@
1
+ import sys
2
+ import traceback
3
+
4
+ from PySide6.QtGui import QTextOption
5
+ from PySide6.QtWidgets import QWidget, QLabel, QPushButton, QComboBox, QHBoxLayout, QListWidget, QPlainTextEdit, \
6
+ QSizePolicy, QSplitter, QLineEdit, QVBoxLayout, QMessageBox, QFileDialog, QApplication, QInputDialog
7
+
8
+ from je_editor.utils.multi_language.multi_language_wrapper import language_wrapper
9
+ from je_editor.git_client.git import Worker, GitService
10
+ from je_editor.git_client.github import GitCloneHandler
11
+
12
+
13
+ class Gitgui(QWidget):
14
+
15
+ def __init__(self):
16
+ super().__init__()
17
+ self.git = GitService()
18
+ self.clone_handler = GitCloneHandler()
19
+ self.language_wrapper_get = language_wrapper.language_word_dict.get
20
+ self._init_ui()
21
+
22
+ def _init_ui(self):
23
+ # Top controls
24
+ self.repo_label = QLabel(self.language_wrapper_get("label_repo_initial"))
25
+ self.btn_open = QPushButton(self.language_wrapper_get("btn_open_repo"))
26
+ self.branch_combo = QComboBox()
27
+ self.btn_checkout = QPushButton(self.language_wrapper_get("btn_switch_branch"))
28
+ self.btn_pull = QPushButton(self.language_wrapper_get("btn_pull"))
29
+ self.btn_push = QPushButton(self.language_wrapper_get("btn_push"))
30
+ self.remote_combo = QComboBox()
31
+ self.clone_button = QPushButton(self.language_wrapper_get("btn_clone_remote"))
32
+ self.clone_button.clicked.connect(self._on_clone_remote_repo)
33
+
34
+ top = QHBoxLayout()
35
+ top.addWidget(self.repo_label, 2)
36
+ top.addWidget(self.btn_open)
37
+ top.addWidget(QLabel(self.language_wrapper_get("label_remote")))
38
+ top.addWidget(self.remote_combo)
39
+ top.addWidget(QLabel(self.language_wrapper_get("label_branch")))
40
+ top.addWidget(self.branch_combo, 1)
41
+ top.addWidget(self.btn_checkout)
42
+ top.addWidget(self.btn_pull)
43
+ top.addWidget(self.btn_push)
44
+ top.addWidget(self.clone_button)
45
+
46
+ # Left commits list
47
+ self.commit_list = QListWidget()
48
+ self.commit_list.setMinimumWidth(380)
49
+
50
+ # Right diff viewer
51
+ self.diff_view = QPlainTextEdit()
52
+ self.diff_view.setReadOnly(True)
53
+ self.diff_view.setWordWrapMode(QTextOption.WrapMode.NoWrap)
54
+ self.diff_view.setLineWrapMode(QPlainTextEdit.LineWrapMode.NoWrap)
55
+ self.diff_view.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
56
+ mono = self.font()
57
+ mono.setFamily("Consolas")
58
+ mono.setPointSize(10)
59
+ self.diff_view.setFont(mono)
60
+
61
+ splitter = QSplitter()
62
+ splitter.addWidget(self.commit_list)
63
+ splitter.addWidget(self.diff_view)
64
+ splitter.setStretchFactor(0, 0)
65
+ splitter.setStretchFactor(1, 1)
66
+
67
+ # Bottom commit box
68
+ self.msg_edit = QLineEdit()
69
+ self.msg_edit.setPlaceholderText(self.language_wrapper_get("placeholder_commit_message"))
70
+ self.btn_stage_all = QPushButton(self.language_wrapper_get("btn_stage_all"))
71
+ self.btn_commit = QPushButton(self.language_wrapper_get("btn_commit"))
72
+
73
+ bottom = QHBoxLayout()
74
+ bottom.addWidget(QLabel(self.language_wrapper_get("label_message")))
75
+ bottom.addWidget(self.msg_edit, 1)
76
+ bottom.addWidget(self.btn_stage_all)
77
+ bottom.addWidget(self.btn_commit)
78
+
79
+ # Layout
80
+ center_layout = QVBoxLayout()
81
+ center_layout.addLayout(top)
82
+ center_layout.addWidget(splitter, 1)
83
+ center_layout.addLayout(bottom)
84
+ self.setLayout(center_layout)
85
+
86
+ # Events
87
+ self.btn_open.clicked.connect(self.on_open_repo)
88
+ self.branch_combo.currentTextChanged.connect(self.on_branch_changed)
89
+ self.btn_checkout.clicked.connect(self.on_checkout)
90
+ self.commit_list.itemSelectionChanged.connect(self.on_commit_selected)
91
+ self.btn_stage_all.clicked.connect(self.on_stage_all)
92
+ self.btn_commit.clicked.connect(self.on_commit)
93
+ self.btn_pull.clicked.connect(self.on_pull)
94
+ self.btn_push.clicked.connect(self.on_push)
95
+
96
+ self._update_controls(enabled=False)
97
+
98
+ # ------------- UI helpers -------------
99
+ def _update_controls(self, enabled: bool):
100
+ for w in [
101
+ self.branch_combo, self.btn_checkout, self.commit_list,
102
+ self.btn_stage_all, self.btn_commit, self.btn_pull, self.btn_push, self.remote_combo
103
+ ]:
104
+ w.setEnabled(enabled)
105
+
106
+ def _error(self, title: str, err: Exception):
107
+ traceback.print_exc()
108
+ QMessageBox.critical(self, title, f"{title}\n\n{err}")
109
+
110
+ def _info(self, title: str, msg: str):
111
+ QMessageBox.information(self, title, msg)
112
+
113
+ # ------------- Event handlers -------------
114
+ def on_open_repo(self):
115
+ path = QFileDialog.getExistingDirectory(self, self.language_wrapper_get("dialog_choose_repo"))
116
+ if not path:
117
+ return
118
+ try:
119
+ self.git.open_repo(path)
120
+ self.repo_label.setText(f"Repo: {path}")
121
+ self._refresh_branches()
122
+ self._refresh_remotes()
123
+ self._update_controls(enabled=True)
124
+ self._load_commits_async()
125
+ except Exception as e:
126
+ self._error(self.language_wrapper_get("err_open_repo"), e)
127
+
128
+ def _refresh_remotes(self):
129
+ self.remote_combo.clear()
130
+ try:
131
+ remotes = self.git.remotes()
132
+ self.remote_combo.addItems(remotes if remotes else [self.language_wrapper_get("default_remote")])
133
+ except Exception:
134
+ self.remote_combo.addItem(self.language_wrapper_get("default_remote"))
135
+
136
+ def _refresh_branches(self):
137
+ self.branch_combo.blockSignals(True)
138
+ self.branch_combo.clear()
139
+ try:
140
+ branches = self.git.list_branches()
141
+ self.branch_combo.addItems(branches)
142
+ cur = self.git.current_branch()
143
+ idx = self.branch_combo.findText(cur)
144
+ if idx >= 0:
145
+ self.branch_combo.setCurrentIndex(idx)
146
+ except Exception as e:
147
+ self._error(self.language_wrapper_get("err_load_branches"), e)
148
+ finally:
149
+ self.branch_combo.blockSignals(False)
150
+
151
+ def on_branch_changed(self, branch: str):
152
+ if not branch:
153
+ return
154
+ self._load_commits_async(branch)
155
+
156
+ def _load_commits_async(self, branch: str | None = None):
157
+ if branch is None:
158
+ try:
159
+ branch = self.git.current_branch()
160
+ except Exception:
161
+ return
162
+ self.commit_list.clear()
163
+ self.diff_view.setPlainText("")
164
+ self.worker = Worker(self.git.list_commits, branch, 200)
165
+ self.worker.done.connect(self._on_commits_loaded)
166
+ self.worker.start()
167
+
168
+ def _on_commits_loaded(self, result, error):
169
+ if error:
170
+ self._error(self.language_wrapper_get("err_load_commits"), error)
171
+ return
172
+ for c in result:
173
+ self.commit_list.addItem(f"{c['hexsha'][:8]} {c['date']} {c['author']} {c['summary']}")
174
+ self.commit_list.setProperty("commit_data", result)
175
+
176
+ def on_checkout(self):
177
+ branch = self.branch_combo.currentText()
178
+ if not branch:
179
+ return
180
+
181
+ def after(res, err):
182
+ if err:
183
+ self._error(self.language_wrapper_get("err_checkout"), err)
184
+ else:
185
+ self._refresh_branches()
186
+ self._load_commits_async(branch)
187
+ self._info(
188
+ self.language_wrapper_get("info_checkout_title"),
189
+ f"{self.language_wrapper_get("info_checkout_msg")} {branch}"
190
+ )
191
+ self.worker = Worker(self.git.checkout, branch)
192
+ self.worker.done.connect(after)
193
+ self.worker.start()
194
+
195
+ def on_commit_selected(self):
196
+ items = self.commit_list.selectedIndexes()
197
+ if not items:
198
+ return
199
+ idx = items[0].row()
200
+ data = self.commit_list.property("commit_data") or []
201
+ if idx >= len(data):
202
+ return
203
+ sha = data[idx]["hexsha"]
204
+
205
+ def after(res, err):
206
+ if err:
207
+ self._error(self.language_wrapper_get("err_read_diff"), err)
208
+ else:
209
+ self.diff_view.setPlainText(res)
210
+
211
+ self.worker = Worker(self.git.show_diff_of_commit, sha)
212
+ self.worker.done.connect(after)
213
+ self.worker.start()
214
+
215
+ def on_stage_all(self):
216
+ def after(res, err):
217
+ if err:
218
+ self._error(self.language_wrapper_get("err_stage"), err)
219
+ else:
220
+ self._info(self.language_wrapper_get("info_stage_title"), self.language_wrapper_get("info_stage_msg"))
221
+
222
+ self.worker = Worker(self.git.stage_all)
223
+ self.worker.done.connect(after)
224
+ self.worker.start()
225
+
226
+ def on_commit(self):
227
+ msg = self.msg_edit.text()
228
+
229
+ def after(res, err):
230
+ if err:
231
+ self._error(self.language_wrapper_get("err_commit"), err)
232
+ else:
233
+ self.msg_edit.clear()
234
+ self._load_commits_async()
235
+ self._info(self.language_wrapper_get("info_commit_title"), self.language_wrapper_get("info_commit_msg"))
236
+
237
+ self.worker = Worker(self.git.commit, msg)
238
+ self.worker.done.connect(after)
239
+ self.worker.start()
240
+
241
+ def on_pull(self):
242
+ remote = self.remote_combo.currentText() or self.language_wrapper_get("default_remote")
243
+ branch = self.branch_combo.currentText()
244
+
245
+ def after(res, err):
246
+ if err:
247
+ self._error(self.language_wrapper_get("err_pull"), err)
248
+ else:
249
+ self._load_commits_async()
250
+ self._info(self.language_wrapper_get("info_pull_title"), str(res))
251
+
252
+ self.worker = Worker(self.git.pull, remote, branch)
253
+ self.worker.done.connect(after)
254
+ self.worker.start()
255
+
256
+ def on_push(self):
257
+ remote = self.remote_combo.currentText() or self.language_wrapper_get("default_remote")
258
+ branch = self.branch_combo.currentText()
259
+
260
+ def after(res, err):
261
+ if err:
262
+ self._error(self.language_wrapper_get("err_push"), err)
263
+ else:
264
+ self._info(self.language_wrapper_get("info_push_title"), str(res))
265
+
266
+ self.worker = Worker(self.git.push, remote, branch)
267
+ self.worker.done.connect(after)
268
+ self.worker.start()
269
+
270
+ def _on_clone_remote_repo(self):
271
+ """
272
+ UI handler for cloning a remote repository.
273
+ """
274
+ url, ok = QInputDialog.getText(self,
275
+ self.language_wrapper_get("dialog_clone_title"),
276
+ self.language_wrapper_get("dialog_clone_prompt"))
277
+ if not ok or not url.strip():
278
+ return
279
+
280
+ local_dir = QFileDialog.getExistingDirectory(self, self.language_wrapper_get("dialog_select_folder"))
281
+ if not local_dir:
282
+ return
283
+
284
+ try:
285
+ repo_path = self.clone_handler.clone_repo(url.strip(), local_dir)
286
+ self.git.open_repo(repo_path)
287
+ QMessageBox.information(self,
288
+ self.language_wrapper_get("info_clone_success_title"),
289
+ f"{self.language_wrapper_get("info_clone_success_msg")} {repo_path}")
290
+ except Exception as e:
291
+ QMessageBox.critical(self, self.language_wrapper_get("err_clone_failed_title"), str(e))
292
+
@@ -0,0 +1,174 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Optional
4
+
5
+ from PySide6.QtCore import Qt, QRectF, QPointF
6
+ from PySide6.QtGui import QPainter, QPainterPath, QPen, QColor, QBrush, QTransform
7
+ from PySide6.QtWidgets import (
8
+ QGraphicsView,
9
+ QGraphicsScene,
10
+ QGraphicsEllipseItem,
11
+ QGraphicsPathItem, QGraphicsSimpleTextItem,
12
+ )
13
+
14
+ from je_editor.git_client.commit_graph import CommitGraph
15
+ from je_editor.utils.multi_language.multi_language_wrapper import language_wrapper
16
+
17
+ # Layout constants
18
+ NODE_RADIUS = 6
19
+ ROW_HEIGHT = 28
20
+ LANE_WIDTH = 22
21
+ LEFT_MARGIN = 220 # space for SHA/message text area on the left
22
+
23
+ # Colors
24
+ EDGE_COLOR = QColor("#888")
25
+ TEXT_COLOR = QColor("#222")
26
+ BG_COLOR = QColor("#ffffff")
27
+
28
+ # A small, repeatable lane palette
29
+ LANE_COLORS = [
30
+ QColor("#4C78A8"),
31
+ QColor("#F58518"),
32
+ QColor("#E45756"),
33
+ QColor("#72B7B2"),
34
+ QColor("#54A24B"),
35
+ QColor("#EECA3B"),
36
+ QColor("#B279A2"),
37
+ QColor("#FF9DA6"),
38
+ QColor("#9D755D"),
39
+ QColor("#BAB0AC"),
40
+ ]
41
+
42
+
43
+ def lane_color(lane: int) -> QColor:
44
+ if lane < 0:
45
+ return QColor("#999")
46
+ return LANE_COLORS[lane % len(LANE_COLORS)]
47
+
48
+
49
+ class CommitGraphView(QGraphicsView):
50
+ def __init__(self, parent=None):
51
+ super().__init__(parent)
52
+ self.language_wrapper_get = language_wrapper.language_word_dict.get
53
+ self.setBackgroundBrush(QBrush(BG_COLOR))
54
+ self.setRenderHints(
55
+ self.renderHints()
56
+ | QPainter.RenderHint.Antialiasing
57
+ | QPainter.RenderHint.TextAntialiasing
58
+ )
59
+ self.setDragMode(QGraphicsView.DragMode.ScrollHandDrag)
60
+ self.setTransformationAnchor(QGraphicsView.ViewportAnchor.AnchorUnderMouse)
61
+
62
+ self._scene = QGraphicsScene(self)
63
+ self.setScene(self._scene)
64
+
65
+ self.graph: Optional[CommitGraph] = None
66
+ self._padding = 40
67
+ self._zoom = 1.0
68
+
69
+ def set_graph(self, graph: CommitGraph):
70
+ self.graph = graph
71
+ self._redraw()
72
+
73
+ def _lane_x(self, lane: int) -> float:
74
+ return lane * LANE_WIDTH + NODE_RADIUS * 2
75
+
76
+ def _row_y(self, row: int) -> float:
77
+ return row * ROW_HEIGHT + NODE_RADIUS * 2
78
+
79
+ def _redraw(self):
80
+ self._scene.clear()
81
+ if not self.graph or not self.graph.nodes:
82
+ self._scene.setSceneRect(QRectF(0, 0, 800, 400))
83
+ return
84
+
85
+ edge_pen = QPen(EDGE_COLOR, 2)
86
+ for row, node in enumerate(self.graph.nodes):
87
+ if not node.parent_shas:
88
+ continue
89
+ x0 = self._lane_x(node.lane_index)
90
+ y0 = self._row_y(row)
91
+ for p in node.parent_shas:
92
+ if p not in self.graph.index:
93
+ continue
94
+ prow = self.graph.index[p]
95
+ parent_node = self.graph.nodes[prow]
96
+ x1 = self._lane_x(parent_node.lane_index)
97
+ y1 = self._row_y(prow)
98
+ path = QPainterPath(QPointF(x0, y0))
99
+ ctrl_y = (y0 + y1) / 2.0
100
+ path.cubicTo(QPointF(x0, ctrl_y), QPointF(x1, ctrl_y), QPointF(x1, y1))
101
+ edge_item = QGraphicsPathItem(path)
102
+ edge_item.setPen(edge_pen)
103
+ self._scene.addItem(edge_item)
104
+
105
+ for row, node in enumerate(self.graph.nodes):
106
+ cx = self._lane_x(node.lane_index)
107
+ cy = self._row_y(row)
108
+
109
+ circle = QGraphicsEllipseItem(
110
+ QRectF(cx - NODE_RADIUS, cy - NODE_RADIUS, NODE_RADIUS * 2, NODE_RADIUS * 2)
111
+ )
112
+ circle.setBrush(QBrush(lane_color(node.lane_index)))
113
+ circle.setPen(QPen(Qt.PenStyle.NoPen))
114
+ circle.setToolTip(self.language_wrapper_get(
115
+ "git_graph_tooltip_commit"
116
+ ).format(short=node.commit_sha[:7],
117
+ author=node.author_name,
118
+ date=node.commit_date,
119
+ msg=node.commit_message))
120
+ self._scene.addItem(circle)
121
+
122
+ label_item = QGraphicsSimpleTextItem(str(row + 1))
123
+ label_item.setBrush(QBrush(TEXT_COLOR))
124
+ label_item.setPos(-30, cy - NODE_RADIUS * 2)
125
+ self._scene.addItem(label_item)
126
+
127
+ self._scene.setSceneRect(
128
+ QRectF(-40, 0,
129
+ self._lane_x(max(n.lane_index for n in self.graph.nodes) + 1) + self._padding,
130
+ self._row_y(len(self.graph.nodes)) + self._padding)
131
+ )
132
+
133
+ def _apply_initial_view(self):
134
+ # Start with a mild zoom to fit rows comfortably
135
+ self._zoom = 0.9
136
+ self._apply_zoom_transform()
137
+
138
+ # Zoom and interaction
139
+ def wheelEvent(self, event):
140
+ if not self._scene.items():
141
+ return
142
+
143
+ # Ctrl+Wheel to zoom, else scroll normally
144
+ if event.modifiers() & Qt.KeyboardModifier.ControlModifier:
145
+ angle = event.angleDelta().y()
146
+ factor = 1.0 + (0.1 if angle > 0 else -0.1)
147
+ self._zoom = max(0.1, min(3.0, self._zoom * factor))
148
+ self._apply_zoom_transform()
149
+ event.accept()
150
+ else:
151
+ super().wheelEvent(event)
152
+
153
+ def _apply_zoom_transform(self):
154
+ t = QTransform()
155
+ t.scale(self._zoom, self._zoom)
156
+ self.setTransform(t)
157
+
158
+ def resizeEvent(self, event):
159
+ super().resizeEvent(event)
160
+ # Keep scene rect; do not auto-fit. Users control zoom.
161
+ # Nothing else needed here.
162
+
163
+ # Optional helpers for external controllers
164
+ def focus_row(self, row: int):
165
+ """
166
+ Center the view around a specific row.
167
+ """
168
+ if not self.graph or row < 0 or row >= len(self.graph.nodes):
169
+ return
170
+ y = self._row_y(row)
171
+ rect = QRectF(0, y - ROW_HEIGHT * 2, self._scene.width(), ROW_HEIGHT * 4)
172
+ self.fitInView(rect, Qt.AspectRatioMode.KeepAspectRatio)
173
+ # Restore user zoom preference after focusing
174
+ self._apply_zoom_transform()
@@ -0,0 +1,19 @@
1
+ from queue import Queue
2
+
3
+
4
+ class AIConfig(object):
5
+
6
+ def __init__(self):
7
+ self.current_ai_model_system_prompt: str = ""
8
+ self.choosable_ai: dict[str, dict[str, str]] = {
9
+ "AI_model": {
10
+ "ai_base_url": "",
11
+ "ai_api_key": "",
12
+ "chat_model": "",
13
+ "prompt_template": "",
14
+ }
15
+ }
16
+ self.message_queue = Queue()
17
+
18
+
19
+ ai_config = AIConfig()
@@ -0,0 +1,17 @@
1
+ from threading import Thread
2
+
3
+ from je_editor.pyside_ui.main_ui.ai_widget.ai_config import ai_config
4
+ from je_editor.pyside_ui.main_ui.ai_widget.langchain_interface import LangChainInterface
5
+
6
+
7
+ class AskThread(Thread):
8
+
9
+ def __init__(self, lang_chain_interface: LangChainInterface, prompt):
10
+ super().__init__()
11
+ self.lang_chain_interface = lang_chain_interface
12
+ self.prompt = prompt
13
+
14
+
15
+ def run(self):
16
+ ai_response = self.lang_chain_interface.call_ai_model(prompt=self.prompt)
17
+ ai_config.message_queue.put(ai_response)