PyBreeze 1.0.4__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.
- automation_ide/__init__.py +7 -0
- automation_ide/__main__.py +0 -0
- automation_ide/automation_editor_ui/__init__.py +0 -0
- automation_ide/automation_editor_ui/connect_gui/__init__.py +0 -0
- automation_ide/automation_editor_ui/connect_gui/ssh/__init__.py +0 -0
- automation_ide/automation_editor_ui/connect_gui/ssh/ssh_command_widget.py +246 -0
- automation_ide/automation_editor_ui/connect_gui/ssh/ssh_file_viewer_widget.py +501 -0
- automation_ide/automation_editor_ui/connect_gui/ssh/ssh_login_widget.py +67 -0
- automation_ide/automation_editor_ui/connect_gui/ssh/ssh_main_widget.py +49 -0
- automation_ide/automation_editor_ui/connect_gui/url/__init__.py +0 -0
- automation_ide/automation_editor_ui/connect_gui/url/ai_code_review_gui.py +177 -0
- automation_ide/automation_editor_ui/editor_main/__init__.py +0 -0
- automation_ide/automation_editor_ui/editor_main/main_ui.py +104 -0
- automation_ide/automation_editor_ui/extend_ai_gui/__init__.py +0 -0
- automation_ide/automation_editor_ui/extend_ai_gui/ai_gui_global_variable.py +40 -0
- automation_ide/automation_editor_ui/extend_ai_gui/code_review/__init__.py +0 -0
- automation_ide/automation_editor_ui/extend_ai_gui/code_review/code_review_thread.py +81 -0
- automation_ide/automation_editor_ui/extend_ai_gui/code_review/cot_code_review_gui.py +82 -0
- automation_ide/automation_editor_ui/extend_ai_gui/prompt_edit_gui/__init__.py +0 -0
- automation_ide/automation_editor_ui/extend_ai_gui/prompt_edit_gui/cot_code_review_prompt_templates/__init__.py +0 -0
- automation_ide/automation_editor_ui/extend_ai_gui/prompt_edit_gui/cot_code_review_prompt_templates/code_smell_detector.py +22 -0
- automation_ide/automation_editor_ui/extend_ai_gui/prompt_edit_gui/cot_code_review_prompt_templates/first_code_review.py +17 -0
- automation_ide/automation_editor_ui/extend_ai_gui/prompt_edit_gui/cot_code_review_prompt_templates/first_summary_prompt.py +17 -0
- automation_ide/automation_editor_ui/extend_ai_gui/prompt_edit_gui/cot_code_review_prompt_templates/global_rule.py +59 -0
- automation_ide/automation_editor_ui/extend_ai_gui/prompt_edit_gui/cot_code_review_prompt_templates/judge.py +91 -0
- automation_ide/automation_editor_ui/extend_ai_gui/prompt_edit_gui/cot_code_review_prompt_templates/judge_single_review.py +85 -0
- automation_ide/automation_editor_ui/extend_ai_gui/prompt_edit_gui/cot_code_review_prompt_templates/linter.py +17 -0
- automation_ide/automation_editor_ui/extend_ai_gui/prompt_edit_gui/cot_code_review_prompt_templates/step_by_step_analysis.py +35 -0
- automation_ide/automation_editor_ui/extend_ai_gui/prompt_edit_gui/cot_code_review_prompt_templates/total_summary.py +66 -0
- automation_ide/automation_editor_ui/extend_ai_gui/prompt_edit_gui/cot_prompt_editor_widget.py +136 -0
- automation_ide/automation_editor_ui/extend_ai_gui/prompt_edit_gui/skills_prompt_editor_widget.py +136 -0
- automation_ide/automation_editor_ui/extend_ai_gui/prompt_edit_gui/skills_prompt_templates/__init__.py +0 -0
- automation_ide/automation_editor_ui/extend_ai_gui/prompt_edit_gui/skills_prompt_templates/code_explainer.py +21 -0
- automation_ide/automation_editor_ui/extend_ai_gui/prompt_edit_gui/skills_prompt_templates/code_review.py +37 -0
- automation_ide/automation_editor_ui/extend_ai_gui/skills/__init__.py +0 -0
- automation_ide/automation_editor_ui/extend_ai_gui/skills/skills_send_gui.py +98 -0
- automation_ide/automation_editor_ui/extend_multi_language/__init__.py +0 -0
- automation_ide/automation_editor_ui/extend_multi_language/extend_english.py +272 -0
- automation_ide/automation_editor_ui/extend_multi_language/extend_traditional_chinese.py +272 -0
- automation_ide/automation_editor_ui/extend_multi_language/update_language_dict.py +8 -0
- automation_ide/automation_editor_ui/menu/__init__.py +0 -0
- automation_ide/automation_editor_ui/menu/automation_menu/__init__.py +0 -0
- automation_ide/automation_editor_ui/menu/automation_menu/api_testka_menu/__init__.py +0 -0
- automation_ide/automation_editor_ui/menu/automation_menu/api_testka_menu/build_api_testka_menu.py +137 -0
- automation_ide/automation_editor_ui/menu/automation_menu/auto_control_menu/__init__.py +0 -0
- automation_ide/automation_editor_ui/menu/automation_menu/auto_control_menu/build_autocontrol_menu.py +169 -0
- automation_ide/automation_editor_ui/menu/automation_menu/automation_file_menu/__init__.py +0 -0
- automation_ide/automation_editor_ui/menu/automation_menu/automation_file_menu/build_automation_file_menu.py +122 -0
- automation_ide/automation_editor_ui/menu/automation_menu/load_density_menu/__init__.py +0 -0
- automation_ide/automation_editor_ui/menu/automation_menu/load_density_menu/build_load_density_menu.py +137 -0
- automation_ide/automation_editor_ui/menu/automation_menu/mail_thunder_menu/__init__.py +0 -0
- automation_ide/automation_editor_ui/menu/automation_menu/mail_thunder_menu/build_mail_thunder_menu.py +88 -0
- automation_ide/automation_editor_ui/menu/automation_menu/test_pioneer_menu/__init__.py +0 -0
- automation_ide/automation_editor_ui/menu/automation_menu/test_pioneer_menu/build_test_pioneer_menu.py +62 -0
- automation_ide/automation_editor_ui/menu/automation_menu/web_runner_menu/__init__.py +0 -0
- automation_ide/automation_editor_ui/menu/automation_menu/web_runner_menu/build_webrunner_menu.py +119 -0
- automation_ide/automation_editor_ui/menu/build_menubar.py +47 -0
- automation_ide/automation_editor_ui/menu/install_menu/__init__.py +0 -0
- automation_ide/automation_editor_ui/menu/install_menu/automation_menu/__init__.py +0 -0
- automation_ide/automation_editor_ui/menu/install_menu/automation_menu/build_automation_install_menu.py +82 -0
- automation_ide/automation_editor_ui/menu/install_menu/install_utils.py +21 -0
- automation_ide/automation_editor_ui/menu/install_menu/tools_menu/__init__.py +0 -0
- automation_ide/automation_editor_ui/menu/install_menu/tools_menu/build_tool_install_menu.py +29 -0
- automation_ide/automation_editor_ui/menu/menu_utils.py +16 -0
- automation_ide/automation_editor_ui/menu/tools/__init__.py +0 -0
- automation_ide/automation_editor_ui/menu/tools/tools_menu.py +162 -0
- automation_ide/automation_editor_ui/show_code_window/__init__.py +0 -0
- automation_ide/automation_editor_ui/show_code_window/code_window.py +21 -0
- automation_ide/automation_editor_ui/syntax/__init__.py +0 -0
- automation_ide/automation_editor_ui/syntax/syntax_extend.py +40 -0
- automation_ide/automation_editor_ui/syntax/syntax_keyword.py +163 -0
- automation_ide/extend/__init__.py +0 -0
- automation_ide/extend/mail_thunder_extend/__init__.py +0 -0
- automation_ide/extend/mail_thunder_extend/mail_thunder_setting.py +36 -0
- automation_ide/extend/process_executor/__init__.py +0 -0
- automation_ide/extend/process_executor/api_testka/__init__.py +0 -0
- automation_ide/extend/process_executor/api_testka/api_testka_process.py +66 -0
- automation_ide/extend/process_executor/auto_control/__init__.py +0 -0
- automation_ide/extend/process_executor/auto_control/auto_control_process.py +62 -0
- automation_ide/extend/process_executor/file_automation/__init__.py +0 -0
- automation_ide/extend/process_executor/file_automation/file_automation_process.py +66 -0
- automation_ide/extend/process_executor/load_density/__init__.py +0 -0
- automation_ide/extend/process_executor/load_density/load_density_process.py +66 -0
- automation_ide/extend/process_executor/mail_thunder/__init__.py +0 -0
- automation_ide/extend/process_executor/mail_thunder/mail_thunder_process.py +16 -0
- automation_ide/extend/process_executor/process_executor_utils.py +72 -0
- automation_ide/extend/process_executor/python_task_process_manager.py +177 -0
- automation_ide/extend/process_executor/test_pioneer/__init__.py +0 -0
- automation_ide/extend/process_executor/test_pioneer/test_pioneer_process_manager.py +181 -0
- automation_ide/extend/process_executor/web_runner/__init__.py +0 -0
- automation_ide/extend/process_executor/web_runner/web_runner_process.py +66 -0
- automation_ide/utils/__init__.py +0 -0
- automation_ide/utils/exception/__init__.py +0 -0
- automation_ide/utils/exception/exception_tags.py +37 -0
- automation_ide/utils/exception/exceptions.py +50 -0
- automation_ide/utils/file_process/__init__.py +0 -0
- automation_ide/utils/file_process/get_dir_file_list.py +29 -0
- automation_ide/utils/json_format/__init__.py +1 -0
- automation_ide/utils/json_format/json_process.py +28 -0
- automation_ide/utils/logging/__init__.py +0 -0
- automation_ide/utils/logging/logger.py +50 -0
- automation_ide/utils/manager/__init__.py +0 -0
- automation_ide/utils/manager/package_manager/__init__.py +0 -0
- automation_ide/utils/manager/package_manager/package_manager_class.py +18 -0
- pybreeze-1.0.4.dist-info/METADATA +69 -0
- pybreeze-1.0.4.dist-info/RECORD +109 -0
- pybreeze-1.0.4.dist-info/WHEEL +5 -0
- pybreeze-1.0.4.dist-info/licenses/LICENSE +21 -0
- pybreeze-1.0.4.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import paramiko
|
|
6
|
+
from PySide6.QtCore import Qt, QEvent
|
|
7
|
+
from PySide6.QtWidgets import (
|
|
8
|
+
QWidget, QVBoxLayout, QLineEdit, QPushButton, QTreeWidget, QTreeWidgetItem,
|
|
9
|
+
QMenu, QFileDialog, QMessageBox, QSplitter, QInputDialog, QStyle
|
|
10
|
+
)
|
|
11
|
+
from je_editor import language_wrapper
|
|
12
|
+
|
|
13
|
+
from automation_ide.automation_editor_ui.connect_gui.ssh.ssh_login_widget import LoginWidget
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class SFTPClientWrapper:
|
|
17
|
+
"""
|
|
18
|
+
Lightweight wrapper around Paramiko SFTP client.
|
|
19
|
+
輕量級封裝 Paramiko SFTP 客戶端,提供基本操作。
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self):
|
|
23
|
+
self.word_dict = language_wrapper.language_word_dict
|
|
24
|
+
self._ssh: Optional[paramiko.SSHClient] = None
|
|
25
|
+
self._sftp: Optional[paramiko.SFTPClient] = None
|
|
26
|
+
self.root_path: str = "/"
|
|
27
|
+
|
|
28
|
+
def connect(self, host: str, port: int, username: str, password: str):
|
|
29
|
+
"""
|
|
30
|
+
Establish SSH + SFTP connection.
|
|
31
|
+
建立 SSH + SFTP 連線。
|
|
32
|
+
"""
|
|
33
|
+
self.close()
|
|
34
|
+
self._ssh = paramiko.SSHClient()
|
|
35
|
+
self._ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
|
36
|
+
self._ssh.connect(hostname=host, port=port, username=username, password=password)
|
|
37
|
+
self._sftp = self._ssh.open_sftp()
|
|
38
|
+
|
|
39
|
+
def close(self):
|
|
40
|
+
"""
|
|
41
|
+
Close SFTP and SSH safely.
|
|
42
|
+
安全關閉 SFTP 與 SSH。
|
|
43
|
+
"""
|
|
44
|
+
try:
|
|
45
|
+
if self._sftp:
|
|
46
|
+
self._sftp.close()
|
|
47
|
+
finally:
|
|
48
|
+
self._sftp = None
|
|
49
|
+
try:
|
|
50
|
+
if self._ssh:
|
|
51
|
+
self._ssh.close()
|
|
52
|
+
finally:
|
|
53
|
+
self._ssh = None
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def connected(self) -> bool:
|
|
57
|
+
"""
|
|
58
|
+
Check connection state.
|
|
59
|
+
檢查連線狀態。
|
|
60
|
+
"""
|
|
61
|
+
return self._ssh is not None and self._sftp is not None
|
|
62
|
+
|
|
63
|
+
def list_dir(self, path: str):
|
|
64
|
+
"""
|
|
65
|
+
List directory entries with stat attributes.
|
|
66
|
+
列出目錄項目(含屬性)。
|
|
67
|
+
"""
|
|
68
|
+
if not self.connected:
|
|
69
|
+
raise RuntimeError(
|
|
70
|
+
self.word_dict.get("ssh_command_widget_dialog_title_not_connected")
|
|
71
|
+
)
|
|
72
|
+
return self._sftp.listdir_attr(path)
|
|
73
|
+
|
|
74
|
+
def is_dir(self, path: str) -> bool:
|
|
75
|
+
"""
|
|
76
|
+
Determine if target path is a directory.
|
|
77
|
+
判斷路徑是否為目錄。
|
|
78
|
+
"""
|
|
79
|
+
if not self.connected:
|
|
80
|
+
raise RuntimeError(self.word_dict.get(
|
|
81
|
+
"ssh_command_widget_dialog_title_not_connected"
|
|
82
|
+
))
|
|
83
|
+
try:
|
|
84
|
+
st = self._sftp.stat(path)
|
|
85
|
+
# S_ISDIR check via stat.S_ISDIR
|
|
86
|
+
import stat
|
|
87
|
+
return stat.S_ISDIR(st.st_mode)
|
|
88
|
+
except IOError:
|
|
89
|
+
return False
|
|
90
|
+
|
|
91
|
+
def mkdir(self, path: str):
|
|
92
|
+
"""
|
|
93
|
+
Create directory.
|
|
94
|
+
建立目錄。
|
|
95
|
+
"""
|
|
96
|
+
self._sftp.mkdir(path)
|
|
97
|
+
|
|
98
|
+
def remove_file(self, path: str):
|
|
99
|
+
"""
|
|
100
|
+
Remove file.
|
|
101
|
+
刪除檔案。
|
|
102
|
+
"""
|
|
103
|
+
self._sftp.remove(path)
|
|
104
|
+
|
|
105
|
+
def remove_dir(self, path: str):
|
|
106
|
+
"""
|
|
107
|
+
Remove empty directory.
|
|
108
|
+
刪除空目錄。
|
|
109
|
+
"""
|
|
110
|
+
self._sftp.rmdir(path)
|
|
111
|
+
|
|
112
|
+
def rename(self, old_path: str, new_path: str):
|
|
113
|
+
"""
|
|
114
|
+
Rename file/folder.
|
|
115
|
+
重新命名檔案/資料夾。
|
|
116
|
+
"""
|
|
117
|
+
self._sftp.rename(old_path, new_path)
|
|
118
|
+
|
|
119
|
+
def download(self, remote_path: str, local_path: str):
|
|
120
|
+
"""
|
|
121
|
+
Download remote to local.
|
|
122
|
+
下載遠端檔案至本地。
|
|
123
|
+
"""
|
|
124
|
+
self._sftp.get(remote_path, local_path)
|
|
125
|
+
|
|
126
|
+
def upload(self, local_path: str, remote_path: str):
|
|
127
|
+
"""
|
|
128
|
+
Upload local to remote.
|
|
129
|
+
上傳本地檔案至遠端。
|
|
130
|
+
"""
|
|
131
|
+
self._sftp.put(local_path, remote_path)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class SSHFileTreeManager(QWidget):
|
|
135
|
+
"""
|
|
136
|
+
QWidget: connection form + tree + context menu.
|
|
137
|
+
QWidget:連線表單 + 樹狀檔案管理 + 右鍵選單。
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
def __init__(self, external_login_widget: LoginWidget = None, add_login_widget: bool = True):
|
|
141
|
+
super().__init__()
|
|
142
|
+
self.word_dict = language_wrapper.language_word_dict
|
|
143
|
+
self.setWindowTitle(
|
|
144
|
+
self.word_dict.get("ssh_file_viewer_window_title_file_tree_manager")
|
|
145
|
+
)
|
|
146
|
+
self.add_login_widget = add_login_widget
|
|
147
|
+
|
|
148
|
+
self.client = SFTPClientWrapper()
|
|
149
|
+
|
|
150
|
+
if self.add_login_widget:
|
|
151
|
+
# 使用獨立的登入介面
|
|
152
|
+
self.login_widget = LoginWidget()
|
|
153
|
+
else:
|
|
154
|
+
if external_login_widget is None:
|
|
155
|
+
external_login_widget = LoginWidget()
|
|
156
|
+
self.login_widget = external_login_widget
|
|
157
|
+
|
|
158
|
+
# UI: Tree
|
|
159
|
+
self.tree = QTreeWidget()
|
|
160
|
+
self.tree.setHeaderLabels(["Name", "Type", "Size", "Path"])
|
|
161
|
+
self.tree.itemExpanded.connect(self.on_item_expanded)
|
|
162
|
+
self.tree.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
|
163
|
+
self.tree.customContextMenuRequested.connect(self.on_context_menu)
|
|
164
|
+
|
|
165
|
+
# Layouts
|
|
166
|
+
splitter = QSplitter(Qt.Orientation.Vertical)
|
|
167
|
+
splitter.addWidget(self.login_widget) # 插入登入介面
|
|
168
|
+
splitter.addWidget(self.tree)
|
|
169
|
+
splitter.setStretchFactor(1, 1)
|
|
170
|
+
|
|
171
|
+
container = QWidget()
|
|
172
|
+
root_layout = QVBoxLayout(container)
|
|
173
|
+
root_layout.addWidget(splitter)
|
|
174
|
+
self.setLayout(root_layout)
|
|
175
|
+
|
|
176
|
+
# Signals
|
|
177
|
+
self.login_widget.connect_btn.clicked.connect(self._connect)
|
|
178
|
+
self.login_widget.disconnect_btn.clicked.connect(self._disconnect)
|
|
179
|
+
|
|
180
|
+
def _connect(self):
|
|
181
|
+
"""
|
|
182
|
+
Connect to SSH and load root.
|
|
183
|
+
連線 SSH 並載入根目錄。
|
|
184
|
+
"""
|
|
185
|
+
host = self.login_widget.host_edit.text().strip()
|
|
186
|
+
port = int(self.login_widget.port_spin.text().strip() or "22")
|
|
187
|
+
user = self.login_widget.user_edit.text().strip()
|
|
188
|
+
pwd = self.login_widget.pass_edit.text()
|
|
189
|
+
|
|
190
|
+
if not host or not user or not pwd:
|
|
191
|
+
QMessageBox.warning(
|
|
192
|
+
self,
|
|
193
|
+
self.word_dict.get("ssh_file_viewer_dialog_title_missing_input"),
|
|
194
|
+
self.word_dict.get("ssh_file_viewer_dialog_message_missing_input"))
|
|
195
|
+
return
|
|
196
|
+
try:
|
|
197
|
+
self.client.connect(host, port, user, pwd)
|
|
198
|
+
self.load_root("/")
|
|
199
|
+
except Exception as e:
|
|
200
|
+
QMessageBox.critical(
|
|
201
|
+
self,
|
|
202
|
+
self.word_dict.get("ssh_file_viewer_dialog_title_connection_failed"),
|
|
203
|
+
f"{self.word_dict.get('ssh_file_viewer_dialog_message_connection_failed')}: {e}")
|
|
204
|
+
|
|
205
|
+
def _disconnect(self):
|
|
206
|
+
"""
|
|
207
|
+
Disconnect SSH.
|
|
208
|
+
斷線 SSH。
|
|
209
|
+
"""
|
|
210
|
+
self.client.close()
|
|
211
|
+
self.tree.clear()
|
|
212
|
+
|
|
213
|
+
def load_root(self, path: str = "/"):
|
|
214
|
+
"""
|
|
215
|
+
Clear and load root items.
|
|
216
|
+
清空並載入根項目。
|
|
217
|
+
"""
|
|
218
|
+
self.tree.clear()
|
|
219
|
+
root_item = self.make_item("/", "dir", 0, "/")
|
|
220
|
+
# Add a placeholder child to show expandable icon
|
|
221
|
+
self.add_placeholder(root_item)
|
|
222
|
+
self.tree.addTopLevelItem(root_item)
|
|
223
|
+
root_item.setExpanded(True)
|
|
224
|
+
self.populate_children(root_item)
|
|
225
|
+
|
|
226
|
+
def make_item(self, name: str, typ: str, size: int, full_path: str) -> QTreeWidgetItem:
|
|
227
|
+
"""
|
|
228
|
+
Create a tree item with metadata.
|
|
229
|
+
建立帶有中繼資料的樹狀項目。
|
|
230
|
+
"""
|
|
231
|
+
item = QTreeWidgetItem([name, typ, str(size), full_path])
|
|
232
|
+
if typ == "dir":
|
|
233
|
+
# Use QStyle enum for standard icons
|
|
234
|
+
# 使用 QStyle 列舉取得標準資料夾圖示
|
|
235
|
+
item.setIcon(0, self.style().standardIcon(QStyle.StandardPixmap.SP_DirIcon))
|
|
236
|
+
else:
|
|
237
|
+
# Use QStyle enum for standard file icon
|
|
238
|
+
# 使用 QStyle 列舉取得標準檔案圖示
|
|
239
|
+
item.setIcon(0, self.style().standardIcon(QStyle.StandardPixmap.SP_FileIcon))
|
|
240
|
+
return item
|
|
241
|
+
|
|
242
|
+
def add_placeholder(self, item: QTreeWidgetItem):
|
|
243
|
+
"""
|
|
244
|
+
Add a dummy child to indicate lazy-load.
|
|
245
|
+
加入占位子項以表示可延遲載入。
|
|
246
|
+
"""
|
|
247
|
+
placeholder = QTreeWidgetItem(["...", "", "", ""])
|
|
248
|
+
item.addChild(placeholder)
|
|
249
|
+
|
|
250
|
+
def is_placeholder_present(self, item: QTreeWidgetItem) -> bool:
|
|
251
|
+
"""
|
|
252
|
+
Check if the first child is a placeholder.
|
|
253
|
+
檢查第一個子項是否為占位項。
|
|
254
|
+
"""
|
|
255
|
+
if item.childCount() == 0:
|
|
256
|
+
return False
|
|
257
|
+
return item.child(0).text(0) == "..."
|
|
258
|
+
|
|
259
|
+
def on_item_expanded(self, item: QTreeWidgetItem):
|
|
260
|
+
"""
|
|
261
|
+
When a directory is expanded, populate its children.
|
|
262
|
+
展開目錄時載入子項目。
|
|
263
|
+
"""
|
|
264
|
+
# Only load once
|
|
265
|
+
if self.is_placeholder_present(item):
|
|
266
|
+
# Remove placeholder then populate
|
|
267
|
+
item.takeChild(0)
|
|
268
|
+
self.populate_children(item)
|
|
269
|
+
|
|
270
|
+
def populate_children(self, parent_item: QTreeWidgetItem):
|
|
271
|
+
"""
|
|
272
|
+
List items under parent path and add to tree.
|
|
273
|
+
列出父路徑下的項目並加入樹。
|
|
274
|
+
"""
|
|
275
|
+
if not self.client.connected:
|
|
276
|
+
return
|
|
277
|
+
path = parent_item.text(3)
|
|
278
|
+
try:
|
|
279
|
+
entries = self.client.list_dir(path)
|
|
280
|
+
# Sort: dirs first, then files
|
|
281
|
+
import stat
|
|
282
|
+
dirs = []
|
|
283
|
+
files = []
|
|
284
|
+
for e in entries:
|
|
285
|
+
name = e.filename
|
|
286
|
+
full_path = os.path.join(path if path != "/" else "", name)
|
|
287
|
+
full_path = full_path if full_path.startswith("/") else f"/{full_path}"
|
|
288
|
+
is_dir = stat.S_ISDIR(e.st_mode)
|
|
289
|
+
if is_dir:
|
|
290
|
+
dirs.append((name, e))
|
|
291
|
+
else:
|
|
292
|
+
files.append((name, e))
|
|
293
|
+
for name, e in dirs + files:
|
|
294
|
+
import stat as _stat
|
|
295
|
+
full_path = os.path.join(path if path != "/" else "", name)
|
|
296
|
+
full_path = full_path if full_path.startswith("/") else f"/{full_path}"
|
|
297
|
+
typ = "dir" if _stat.S_ISDIR(e.st_mode) else "file"
|
|
298
|
+
size = e.st_size if typ == "file" else 0
|
|
299
|
+
child = self.make_item(name, typ, size, full_path)
|
|
300
|
+
parent_item.addChild(child)
|
|
301
|
+
if typ == "dir":
|
|
302
|
+
self.add_placeholder(child)
|
|
303
|
+
except Exception as ex:
|
|
304
|
+
QMessageBox.critical(
|
|
305
|
+
self,
|
|
306
|
+
self.word_dict.get("ssh_file_viewer_dialog_title_list_error"),
|
|
307
|
+
f"{self.word_dict.get('ssh_file_viewer_dialog_message_list_failed')} '{path}': {ex}")
|
|
308
|
+
|
|
309
|
+
def on_context_menu(self, pos):
|
|
310
|
+
"""
|
|
311
|
+
Show context menu for file operations.
|
|
312
|
+
顯示右鍵選單以進行檔案操作。
|
|
313
|
+
"""
|
|
314
|
+
item = self.tree.itemAt(pos)
|
|
315
|
+
menu = QMenu(self)
|
|
316
|
+
|
|
317
|
+
refresh_act = menu.addAction("Refresh")
|
|
318
|
+
create_act = menu.addAction("Create folder")
|
|
319
|
+
rename_act = menu.addAction("Rename")
|
|
320
|
+
delete_act = menu.addAction("Delete")
|
|
321
|
+
download_act = menu.addAction("Download")
|
|
322
|
+
upload_act = menu.addAction("Upload to this folder")
|
|
323
|
+
|
|
324
|
+
action = menu.exec_(self.tree.viewport().mapToGlobal(pos))
|
|
325
|
+
if action is None:
|
|
326
|
+
return
|
|
327
|
+
|
|
328
|
+
try:
|
|
329
|
+
if action == refresh_act:
|
|
330
|
+
self.action_refresh(item)
|
|
331
|
+
elif action == create_act:
|
|
332
|
+
self.action_create_folder(item)
|
|
333
|
+
elif action == rename_act:
|
|
334
|
+
self.action_rename(item)
|
|
335
|
+
elif action == delete_act:
|
|
336
|
+
self.action_delete(item)
|
|
337
|
+
elif action == download_act:
|
|
338
|
+
self.action_download(item)
|
|
339
|
+
elif action == upload_act:
|
|
340
|
+
self.action_upload(item)
|
|
341
|
+
except Exception as e:
|
|
342
|
+
QMessageBox.critical(
|
|
343
|
+
self,
|
|
344
|
+
self.word_dict.get("ssh_file_viewer_dialog_title_operation_failed"),
|
|
345
|
+
f"{self.word_dict.get('ssh_file_viewer_dialog_message_operation_failed')}: {e}")
|
|
346
|
+
|
|
347
|
+
def action_refresh(self, item: Optional[QTreeWidgetItem]):
|
|
348
|
+
"""
|
|
349
|
+
Refresh current item children (or root).
|
|
350
|
+
重新整理目前項目的子項(或根)。
|
|
351
|
+
"""
|
|
352
|
+
target = item or (self.tree.topLevelItem(0) if self.tree.topLevelItemCount() else None)
|
|
353
|
+
if target is None:
|
|
354
|
+
return
|
|
355
|
+
# Clear and reload
|
|
356
|
+
target.takeChildren()
|
|
357
|
+
if target.text(1) == "dir":
|
|
358
|
+
self.add_placeholder(target)
|
|
359
|
+
self.on_item_expanded(target)
|
|
360
|
+
|
|
361
|
+
def action_create_folder(self, item: Optional[QTreeWidgetItem]):
|
|
362
|
+
"""
|
|
363
|
+
Create a subfolder under the selected directory.
|
|
364
|
+
在選定目錄下建立子資料夾。
|
|
365
|
+
"""
|
|
366
|
+
if item is None:
|
|
367
|
+
QMessageBox.information(
|
|
368
|
+
self,
|
|
369
|
+
self.word_dict.get("ssh_file_viewer_dialog_title_no_selection"),
|
|
370
|
+
self.word_dict.get("ssh_file_viewer_dialog_message_select_folder_to_create"))
|
|
371
|
+
return
|
|
372
|
+
base_path = item.text(3)
|
|
373
|
+
if item.text(1) != "dir":
|
|
374
|
+
base_path = os.path.dirname(base_path)
|
|
375
|
+
name, ok = self.get_text(
|
|
376
|
+
self.word_dict.get("ssh_file_viewer_dialog_title_create_folder"),
|
|
377
|
+
self.word_dict.get("ssh_file_viewer_dialog_label_folder_name"))
|
|
378
|
+
if not ok or not name.strip():
|
|
379
|
+
return
|
|
380
|
+
new_path = os.path.join(base_path if base_path != "/" else "", name.strip())
|
|
381
|
+
new_path = new_path if new_path.startswith("/") else f"/{new_path}"
|
|
382
|
+
self.client.mkdir(new_path)
|
|
383
|
+
self.action_refresh(item)
|
|
384
|
+
|
|
385
|
+
def action_rename(self, item: Optional[QTreeWidgetItem]):
|
|
386
|
+
"""
|
|
387
|
+
Rename selected item.
|
|
388
|
+
重新命名選定項目。
|
|
389
|
+
"""
|
|
390
|
+
if item is None:
|
|
391
|
+
return
|
|
392
|
+
old_path = item.text(3)
|
|
393
|
+
new_name, ok = self.get_text(
|
|
394
|
+
self.word_dict.get("ssh_file_viewer_dialog_title_rename"),
|
|
395
|
+
f"{self.word_dict.get('ssh_file_viewer_dialog_label_new_name_for_item')}: {item.text(0)}")
|
|
396
|
+
if not ok or not new_name.strip():
|
|
397
|
+
return
|
|
398
|
+
base = os.path.dirname(old_path) or "/"
|
|
399
|
+
new_path = os.path.join(base if base != "/" else "", new_name.strip())
|
|
400
|
+
new_path = new_path if new_path.startswith("/") else f"/{new_path}"
|
|
401
|
+
self.client.rename(old_path, new_path)
|
|
402
|
+
# Update item display
|
|
403
|
+
item.setText(0, new_name.strip())
|
|
404
|
+
item.setText(3, new_path)
|
|
405
|
+
|
|
406
|
+
def action_delete(self, item: Optional[QTreeWidgetItem]):
|
|
407
|
+
"""
|
|
408
|
+
Delete selected file/folder (folder must be empty).
|
|
409
|
+
刪除選定檔案/資料夾(資料夾需為空)。
|
|
410
|
+
"""
|
|
411
|
+
if item is None:
|
|
412
|
+
return
|
|
413
|
+
path = item.text(3)
|
|
414
|
+
reply = QMessageBox.question(
|
|
415
|
+
self,
|
|
416
|
+
self.word_dict.get("ssh_file_viewer_dialog_title_confirm_delete"),
|
|
417
|
+
f"{self.word_dict.get('ssh_file_viewer_dialog_message_confirm_delete')} '{path}'?",
|
|
418
|
+
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
|
|
419
|
+
)
|
|
420
|
+
if reply != QMessageBox.StandardButton.Yes:
|
|
421
|
+
return
|
|
422
|
+
# Try dir first; if fails, try file
|
|
423
|
+
if item.text(1) == "dir":
|
|
424
|
+
self.client.remove_dir(path)
|
|
425
|
+
else:
|
|
426
|
+
self.client.remove_file(path)
|
|
427
|
+
parent = item.parent()
|
|
428
|
+
if parent:
|
|
429
|
+
parent.removeChild(item)
|
|
430
|
+
else:
|
|
431
|
+
self.tree.takeTopLevelItem(self.tree.indexOfTopLevelItem(item))
|
|
432
|
+
|
|
433
|
+
def action_download(self, item: Optional[QTreeWidgetItem]):
|
|
434
|
+
"""
|
|
435
|
+
Download selected file to local.
|
|
436
|
+
將選定檔案下載至本地。
|
|
437
|
+
"""
|
|
438
|
+
if item is None or item.text(1) != "file":
|
|
439
|
+
QMessageBox.information(
|
|
440
|
+
self,
|
|
441
|
+
self.word_dict.get("ssh_file_viewer_dialog_title_invalid_selection"),
|
|
442
|
+
self.word_dict.get("ssh_file_viewer_dialog_message_select_file_to_download"))
|
|
443
|
+
return
|
|
444
|
+
remote_path = item.text(3)
|
|
445
|
+
suggested = Path(remote_path).name
|
|
446
|
+
local_path, _ = QFileDialog.getSaveFileName(
|
|
447
|
+
self,
|
|
448
|
+
self.word_dict.get("ssh_file_viewer_dialog_title_save_as"),
|
|
449
|
+
suggested)
|
|
450
|
+
if not local_path:
|
|
451
|
+
return
|
|
452
|
+
self.client.download(remote_path, local_path)
|
|
453
|
+
QMessageBox.information(
|
|
454
|
+
self,
|
|
455
|
+
self.word_dict.get("ssh_file_viewer_dialog_title_downloaded"),
|
|
456
|
+
f"{self.word_dict.get('ssh_file_viewer_dialog_message_saved_to')}: {local_path}")
|
|
457
|
+
|
|
458
|
+
def action_upload(self, item: Optional[QTreeWidgetItem]):
|
|
459
|
+
"""
|
|
460
|
+
Upload a local file into the selected folder.
|
|
461
|
+
將本地檔案上傳至所選資料夾。
|
|
462
|
+
"""
|
|
463
|
+
if item is None:
|
|
464
|
+
return
|
|
465
|
+
target_dir = item.text(3) if item.text(1) == "dir" else os.path.dirname(item.text(3))
|
|
466
|
+
local_path, _ = QFileDialog.getOpenFileName(
|
|
467
|
+
self, self.word_dict.get("ssh_file_viewer_dialog_title_select_local_file"), "")
|
|
468
|
+
if not local_path:
|
|
469
|
+
return
|
|
470
|
+
filename = os.path.basename(local_path)
|
|
471
|
+
remote_path = os.path.join(target_dir if target_dir != "/" else "", filename)
|
|
472
|
+
remote_path = remote_path if remote_path.startswith("/") else f"/{remote_path}"
|
|
473
|
+
self.client.upload(local_path, remote_path)
|
|
474
|
+
QMessageBox.information(
|
|
475
|
+
self,
|
|
476
|
+
self.word_dict.get("ssh_file_viewer_dialog_title_uploaded"),
|
|
477
|
+
f"{self.word_dict.get('ssh_file_viewer_dialog_message_uploaded_to')}: {remote_path}")
|
|
478
|
+
# Refresh folder contents
|
|
479
|
+
self.action_refresh(item)
|
|
480
|
+
|
|
481
|
+
def get_text(self, title: str, label: str):
|
|
482
|
+
"""
|
|
483
|
+
Simple input dialog using QMessageBox alternative.
|
|
484
|
+
簡易文字輸入對話框(基於 QLineEdit)。
|
|
485
|
+
"""
|
|
486
|
+
text, ok = QInputDialog.getText(self, title, label)
|
|
487
|
+
return text, ok
|
|
488
|
+
|
|
489
|
+
def eventFilter(self, obj, event):
|
|
490
|
+
"""
|
|
491
|
+
Allow Enter to trigger OK in our improvised input dialog.
|
|
492
|
+
允許在自製輸入框中使用 Enter 觸發確定。
|
|
493
|
+
"""
|
|
494
|
+
if isinstance(obj, QLineEdit) and event.type() == QEvent.Type.KeyPress:
|
|
495
|
+
if event.key() in (Qt.Key.Key_Return, Qt.Key.Key_Enter):
|
|
496
|
+
w = obj.window()
|
|
497
|
+
for b in w.findChildren(QPushButton):
|
|
498
|
+
if b.text().lower() in (self.word_dict.get("ssh_file_viewer_dialog_button_ok"),):
|
|
499
|
+
b.click()
|
|
500
|
+
return True
|
|
501
|
+
return super().eventFilter(obj, event)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from PySide6.QtWidgets import (
|
|
2
|
+
QWidget, QHBoxLayout, QVBoxLayout, QLabel,
|
|
3
|
+
QLineEdit, QSpinBox, QCheckBox, QPushButton
|
|
4
|
+
)
|
|
5
|
+
from je_editor import language_wrapper
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class LoginWidget(QWidget):
|
|
9
|
+
"""登入介面獨立 QWidget"""
|
|
10
|
+
|
|
11
|
+
def __init__(self, parent=None):
|
|
12
|
+
super().__init__(parent)
|
|
13
|
+
|
|
14
|
+
# UI 控制元件
|
|
15
|
+
self.host_edit = QLineEdit()
|
|
16
|
+
self.port_spin = QSpinBox()
|
|
17
|
+
self.user_edit = QLineEdit()
|
|
18
|
+
self.pass_edit = QLineEdit()
|
|
19
|
+
self.key_edit = QLineEdit()
|
|
20
|
+
self.use_key_check = QCheckBox(language_wrapper.language_word_dict.get("ssh_login_widget_button_use_key_auth"))
|
|
21
|
+
self.connect_btn = QPushButton(language_wrapper.language_word_dict.get("ssh_login_widget_button_connect"))
|
|
22
|
+
self.disconnect_btn = QPushButton(language_wrapper.language_word_dict.get("ssh_login_widget_button_disconnect"))
|
|
23
|
+
self.status_label = QLabel(language_wrapper.language_word_dict.get("ssh_login_widget_status_disconnected"))
|
|
24
|
+
|
|
25
|
+
# 初始化 UI
|
|
26
|
+
self._setup_ui()
|
|
27
|
+
|
|
28
|
+
def _setup_ui(self):
|
|
29
|
+
"""建立登入介面 UI"""
|
|
30
|
+
self.host_edit.setPlaceholderText(language_wrapper.language_word_dict.get("ssh_login_widget_placeholder_host"))
|
|
31
|
+
self.port_spin.setRange(1, 65535)
|
|
32
|
+
self.port_spin.setValue(22)
|
|
33
|
+
self.user_edit.setPlaceholderText(
|
|
34
|
+
language_wrapper.language_word_dict.get("ssh_login_widget_placeholder_username"))
|
|
35
|
+
self.pass_edit.setPlaceholderText(
|
|
36
|
+
language_wrapper.language_word_dict.get("ssh_login_widget_placeholder_password"))
|
|
37
|
+
self.pass_edit.setEchoMode(QLineEdit.EchoMode.Password)
|
|
38
|
+
self.key_edit.setPlaceholderText(
|
|
39
|
+
language_wrapper.language_word_dict.get("ssh_login_widget_placeholder_private_key"))
|
|
40
|
+
|
|
41
|
+
# 佈局設計
|
|
42
|
+
top = QHBoxLayout()
|
|
43
|
+
top.addWidget(QLabel(language_wrapper.language_word_dict.get("ssh_login_widget_label_host")))
|
|
44
|
+
top.addWidget(self.host_edit)
|
|
45
|
+
top.addWidget(QLabel(language_wrapper.language_word_dict.get("ssh_login_widget_label_port")))
|
|
46
|
+
top.addWidget(self.port_spin)
|
|
47
|
+
top.addWidget(QLabel(language_wrapper.language_word_dict.get("ssh_login_widget_label_user")))
|
|
48
|
+
top.addWidget(self.user_edit)
|
|
49
|
+
|
|
50
|
+
auth = QHBoxLayout()
|
|
51
|
+
auth.addWidget(self.use_key_check)
|
|
52
|
+
auth.addWidget(QLabel(language_wrapper.language_word_dict.get("ssh_login_widget_label_key")))
|
|
53
|
+
auth.addWidget(self.key_edit)
|
|
54
|
+
auth.addWidget(QLabel(language_wrapper.language_word_dict.get("ssh_login_widget_label_password")))
|
|
55
|
+
auth.addWidget(self.pass_edit)
|
|
56
|
+
|
|
57
|
+
conn = QHBoxLayout()
|
|
58
|
+
conn.addWidget(self.connect_btn)
|
|
59
|
+
conn.addWidget(self.disconnect_btn)
|
|
60
|
+
conn.addWidget(self.status_label)
|
|
61
|
+
|
|
62
|
+
root = QVBoxLayout()
|
|
63
|
+
root.addLayout(top)
|
|
64
|
+
root.addLayout(auth)
|
|
65
|
+
root.addLayout(conn)
|
|
66
|
+
|
|
67
|
+
self.setLayout(root)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
from PySide6.QtCore import Qt
|
|
4
|
+
from PySide6.QtWidgets import QWidget, QVBoxLayout, QSplitter, QApplication, QSizePolicy
|
|
5
|
+
|
|
6
|
+
from automation_ide.automation_editor_ui.connect_gui.ssh.ssh_command_widget import SSHCommandWidget
|
|
7
|
+
from automation_ide.automation_editor_ui.connect_gui.ssh.ssh_file_viewer_widget import SSHFileTreeManager
|
|
8
|
+
from automation_ide.automation_editor_ui.connect_gui.ssh.ssh_login_widget import LoginWidget
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SSHMainWidget(QWidget):
|
|
12
|
+
def __init__(self, parent=None):
|
|
13
|
+
super().__init__(parent)
|
|
14
|
+
|
|
15
|
+
# 已經實作好的元件
|
|
16
|
+
self.login_widget = LoginWidget()
|
|
17
|
+
self.login_widget.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
|
|
18
|
+
self.file_tree = SSHFileTreeManager(external_login_widget=self.login_widget, add_login_widget=False)
|
|
19
|
+
self.command_widget = SSHCommandWidget(external_login_widget=self.login_widget, add_login_widget=False)
|
|
20
|
+
|
|
21
|
+
# 整體垂直布局
|
|
22
|
+
main_layout = QVBoxLayout(self)
|
|
23
|
+
|
|
24
|
+
# 上方放登入區
|
|
25
|
+
main_layout.addWidget(self.login_widget)
|
|
26
|
+
|
|
27
|
+
# Splitter 左右分割
|
|
28
|
+
splitter = QSplitter(Qt.Orientation.Horizontal)
|
|
29
|
+
splitter.addWidget(self.file_tree) # 左邊檔案瀏覽器
|
|
30
|
+
splitter.addWidget(self.command_widget) # 右邊命令區
|
|
31
|
+
# 設定初始比例:左邊 30%,右邊 70%
|
|
32
|
+
splitter.setSizes([300, 700]) # 數值會依視窗大小縮放
|
|
33
|
+
splitter.setStretchFactor(0, 1)
|
|
34
|
+
splitter.setStretchFactor(1, 2)
|
|
35
|
+
|
|
36
|
+
# 讓拖曳更平滑:加寬 handle 並避免完全收合
|
|
37
|
+
splitter.setHandleWidth(8)
|
|
38
|
+
splitter.setCollapsible(0, False)
|
|
39
|
+
splitter.setCollapsible(1, False)
|
|
40
|
+
|
|
41
|
+
main_layout.addWidget(splitter)
|
|
42
|
+
self.setLayout(main_layout)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
if __name__ == "__main__":
|
|
46
|
+
app = QApplication(sys.argv)
|
|
47
|
+
win = SSHMainWidget()
|
|
48
|
+
win.showMaximized()
|
|
49
|
+
sys.exit(app.exec())
|
|
File without changes
|