main_gui.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784
  1. #!/usr/bin/env python
  2. # -*- encoding: utf-8 -*-
  3. '''
  4. @Contact : liuyuqi.gov@msn.cn
  5. @Time : 2023/04/27 02:55:59
  6. @License : Copyright © 2017-2022 liuyuqi. All Rights Reserved.
  7. @Desc : repo_sync GUI入口
  8. 打包说明:
  9. 使用PyInstaller可以将此应用打包为单个可执行文件:
  10. 1. 安装PyInstaller: pip install pyinstaller
  11. 2. 打包命令: pyinstaller --onefile --windowed --icon=icon.ico main_gui.py
  12. (如果有图标文件,可以通过--icon参数指定)
  13. 3. 打包后的可执行文件将位于dist目录中
  14. '''
  15. import sys
  16. import os
  17. import threading
  18. import subprocess
  19. import yaml
  20. try:
  21. from PyQt5.QtWidgets import (
  22. QApplication, QTabWidget, QWidget, QVBoxLayout, QHBoxLayout, QLabel,
  23. QRadioButton, QPushButton, QButtonGroup, QGroupBox, QMessageBox,
  24. QLineEdit, QScrollArea, QFileDialog, QFormLayout, QCheckBox, QTextEdit,
  25. QComboBox, QDialog, QDialogButtonBox, QGridLayout, QToolButton, QListWidget, QListWidgetItem
  26. )
  27. from PyQt5.QtCore import Qt, pyqtSignal, QObject
  28. HAS_QT = True
  29. except ImportError:
  30. HAS_QT = False
  31. print("PyQt5 not installed, GUI mode not available")
  32. # 直接导入repo_sync模块
  33. sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
  34. try:
  35. from repo_sync.repo_sync import RepoSync
  36. from repo_sync.version import __version__
  37. from repo_sync.utils.config_reader import ConfigReader
  38. except ImportError:
  39. print("无法导入repo_sync模块,尝试直接导入...")
  40. try:
  41. from repo_sync import RepoSync
  42. from repo_sync.version import __version__
  43. from repo_sync.utils.config_reader import ConfigReader
  44. except ImportError:
  45. print("导入repo_sync模块失败,请确保repo_sync已正确安装")
  46. __version__ = "未知"
  47. # 创建一个空的RepoSync类作为替代
  48. class RepoSync:
  49. def __init__(self, params):
  50. self.params = params
  51. def run(self):
  52. print("RepoSync模块未找到,无法执行操作")
  53. # 确保config.yml文件存在
  54. def ensure_config_file():
  55. config_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'config.yml')
  56. if not os.path.exists(config_file):
  57. # 创建空的config.yml文件
  58. default_config = {
  59. 'accounts': {
  60. 'github': {
  61. 'enable': 1,
  62. '1': {
  63. 'username': '',
  64. 'token': '',
  65. 'private': True
  66. }
  67. }
  68. },
  69. 'log': {
  70. 'level': 'debug',
  71. 'file': '/tmp/repo_sync.log',
  72. 'max_size': '100MB',
  73. 'max_backups': 3,
  74. 'max_age': 7,
  75. 'console_formatter': {
  76. 'level': 'debug',
  77. 'format': '%(asctime)s - %(levelname)s - %(message)s'
  78. },
  79. 'file_formatter': {
  80. 'level': 'debug',
  81. 'format': '%(asctime)s - %(levelname)s - %(message)s'
  82. }
  83. }
  84. }
  85. with open(config_file, 'w', encoding='utf-8') as f:
  86. yaml.dump(default_config, f, default_flow_style=False)
  87. return config_file
  88. class SettingsTab(QWidget):
  89. def __init__(self, parent=None):
  90. super().__init__(parent)
  91. # 确保config.yml文件存在
  92. ensure_config_file()
  93. # 初始化account_lists字典
  94. self.account_lists = {}
  95. self.config_reader = ConfigReader()
  96. self.init_ui()
  97. def init_ui(self):
  98. layout = QVBoxLayout()
  99. # 创建平台选择标签页
  100. self.platform_tabs = QTabWidget()
  101. # 平台配置
  102. self.platform_configs = {
  103. "github": ["username", "token", "private"],
  104. "gitlab": ["host", "username", "token", "private"],
  105. "gitee": ["username", "token", "private"],
  106. "gitcode": ["username", "token", "private"],
  107. "git.yoq.me": ["username", "token", "private"],
  108. "coding": ["username", "token", "project", "private"],
  109. "aliyun": ["compoanyid", "group_id", "username", "token", "private"],
  110. "cnb": ["username", "token", "private"]
  111. }
  112. # 为每个平台创建标签页
  113. self.platform_pages = {}
  114. for platform in self.platform_configs.keys():
  115. page = QWidget()
  116. page_layout = QVBoxLayout()
  117. # 账户列表
  118. account_group = QGroupBox("Accounts")
  119. account_layout = QVBoxLayout()
  120. account_list = QListWidget()
  121. self.account_lists[platform] = account_list
  122. account_list.currentItemChanged.connect(lambda current, previous, p=platform: self.select_account(p, current))
  123. account_buttons = QHBoxLayout()
  124. add_btn = QPushButton("Add Account")
  125. add_btn.clicked.connect(lambda checked, p=platform: self.add_account(p))
  126. delete_btn = QPushButton("Delete Account")
  127. delete_btn.clicked.connect(lambda checked, p=platform: self.delete_account(p))
  128. enable_btn = QPushButton("Enable Account")
  129. enable_btn.clicked.connect(lambda checked, p=platform: self.enable_account(p))
  130. account_buttons.addWidget(add_btn)
  131. account_buttons.addWidget(delete_btn)
  132. account_buttons.addWidget(enable_btn)
  133. account_layout.addWidget(account_list)
  134. account_layout.addLayout(account_buttons)
  135. account_group.setLayout(account_layout)
  136. # 账户详情表单
  137. form_group = QGroupBox("Account Details")
  138. form_layout = QFormLayout()
  139. form_group.setLayout(form_layout)
  140. page_layout.addWidget(account_group)
  141. page_layout.addWidget(form_group)
  142. # 保存按钮
  143. save_btn = QPushButton("Save Settings")
  144. save_btn.clicked.connect(self.save_settings)
  145. page_layout.addWidget(save_btn)
  146. page.setLayout(page_layout)
  147. self.platform_pages[platform] = {
  148. "form": form_layout
  149. }
  150. self.platform_tabs.addTab(page, platform)
  151. layout.addWidget(self.platform_tabs)
  152. self.setLayout(layout)
  153. # 加载设置
  154. self.load_settings()
  155. def load_settings(self):
  156. # 为每个平台加载账户
  157. for platform in self.platform_configs.keys():
  158. account_list = self.account_lists[platform]
  159. account_list.clear()
  160. # 获取该平台的所有账户
  161. accounts = self.config_reader.get_platform_accounts(platform)
  162. # 添加到列表
  163. for account in accounts:
  164. item = QListWidgetItem(account)
  165. account_list.addItem(item)
  166. # 选择第一个账户
  167. if account_list.count() > 0:
  168. account_list.setCurrentRow(0)
  169. self.select_account(platform, account_list.item(0))
  170. def select_account(self, platform, item):
  171. if not item:
  172. return
  173. account = item.text()
  174. form = self.platform_pages[platform]["form"]
  175. # 清空表单
  176. while form.rowCount() > 0:
  177. form.removeRow(0)
  178. # 获取账户配置
  179. account_config = self.config_reader.get_account_config(platform, account)
  180. # 创建表单项
  181. self.field_widgets = {}
  182. for field in self.platform_configs[platform]:
  183. value = account_config.get(field, "")
  184. if field == "private":
  185. widget = QCheckBox()
  186. widget.setChecked(value if isinstance(value, bool) else value.lower() != "false")
  187. else:
  188. widget = QLineEdit()
  189. if field in ["token", "password"]:
  190. widget.setEchoMode(QLineEdit.Password)
  191. widget.setText(str(value))
  192. self.field_widgets[field] = widget
  193. form.addRow(f"{field.capitalize()}:", widget)
  194. def add_account(self, platform):
  195. dialog = AddAccountDialog(platform, self)
  196. if dialog.exec_() == QDialog.Accepted:
  197. account_data = dialog.get_account_data()
  198. account_name = account_data["name"]
  199. if not account_name:
  200. QMessageBox.warning(self, "Warning", "Account name cannot be empty.")
  201. return
  202. # 更新config.yml文件
  203. config = self.config_reader.config
  204. if platform not in config['accounts']:
  205. config['accounts'][platform] = {'enable': 1}
  206. # 添加新账户
  207. config['accounts'][platform][account_name] = {
  208. field: value for field, value in account_data.items() if field != 'name'
  209. }
  210. # 保存配置
  211. with open(self.config_reader.config_path, 'w', encoding='utf-8') as f:
  212. yaml.dump(config, f, default_flow_style=False)
  213. # 重新加载设置
  214. self.config_reader = ConfigReader()
  215. self.load_settings()
  216. # 选择新账户
  217. account_list = self.account_lists[platform]
  218. for i in range(account_list.count()):
  219. if account_list.item(i).text() == account_name:
  220. account_list.setCurrentRow(i)
  221. self.select_account(platform, account_list.item(i))
  222. break
  223. def delete_account(self, platform):
  224. account_list = self.account_lists[platform]
  225. current_item = account_list.currentItem()
  226. if not current_item:
  227. return
  228. account = current_item.text()
  229. reply = QMessageBox.question(
  230. self,
  231. "Confirm Deletion",
  232. f"Are you sure you want to delete the account '{account}' for {platform}?",
  233. QMessageBox.Yes | QMessageBox.No,
  234. QMessageBox.No
  235. )
  236. if reply == QMessageBox.Yes:
  237. # 更新config.yml文件
  238. config = self.config_reader.config
  239. if platform in config['accounts'] and account in config['accounts'][platform]:
  240. del config['accounts'][platform][account]
  241. # 保存配置
  242. with open(self.config_reader.config_path, 'w', encoding='utf-8') as f:
  243. yaml.dump(config, f, default_flow_style=False)
  244. # 重新加载设置
  245. self.config_reader = ConfigReader()
  246. self.load_settings()
  247. def enable_account(self, platform):
  248. account_list = self.account_lists[platform]
  249. current_item = account_list.currentItem()
  250. if not current_item:
  251. return
  252. account = current_item.text()
  253. # 更新config.yml文件
  254. config = self.config_reader.config
  255. if platform in config['accounts']:
  256. # 将当前账户的配置复制到第一个账户位置
  257. account_config = config['accounts'][platform][account]
  258. config['accounts'][platform]['1'] = account_config
  259. # 保存配置
  260. with open(self.config_reader.config_path, 'w', encoding='utf-8') as f:
  261. yaml.dump(config, f, default_flow_style=False)
  262. QMessageBox.information(
  263. self,
  264. "Success",
  265. f"Account '{account}' has been enabled for {platform}."
  266. )
  267. # 重新加载设置
  268. self.config_reader = ConfigReader()
  269. self.load_settings()
  270. def save_settings(self):
  271. # 保存当前显示的账户配置
  272. platform = list(self.platform_configs.keys())[self.platform_tabs.currentIndex()]
  273. account_list = self.account_lists[platform]
  274. current_item = account_list.currentItem()
  275. if current_item and hasattr(self, 'field_widgets'):
  276. account = current_item.text()
  277. # 更新config.yml文件
  278. config = self.config_reader.config
  279. if platform not in config['accounts']:
  280. config['accounts'][platform] = {'enable': 1}
  281. # 更新账户配置
  282. account_config = {}
  283. for field, widget in self.field_widgets.items():
  284. if isinstance(widget, QCheckBox):
  285. value = widget.isChecked()
  286. else:
  287. value = widget.text().strip()
  288. if value:
  289. account_config[field] = value
  290. config['accounts'][platform][account] = account_config
  291. # 保存配置
  292. with open(self.config_reader.config_path, 'w', encoding='utf-8') as f:
  293. yaml.dump(config, f, default_flow_style=False)
  294. QMessageBox.information(self, "Success", "Settings saved successfully!")
  295. class AddAccountDialog(QDialog):
  296. def __init__(self, platform, parent=None):
  297. super().__init__(parent)
  298. self.platform = platform
  299. self.init_ui()
  300. def init_ui(self):
  301. self.setWindowTitle(f"Add {self.platform} Account")
  302. layout = QFormLayout()
  303. # 账户名称
  304. self.name_edit = QLineEdit()
  305. layout.addRow("Account Name:", self.name_edit)
  306. # 其他字段
  307. self.field_widgets = {}
  308. platform_configs = {
  309. "github": ["username", "token", "private"],
  310. "gitlab": ["host", "username", "token", "private"],
  311. "gitee": ["username", "token", "private"],
  312. "gitcode": ["username", "token", "private"],
  313. "git.yoq.me": ["username", "token", "private"],
  314. "coding": ["username", "token", "project", "private"],
  315. "aliyun": ["compoanyid", "group_id", "username", "token", "private"],
  316. "cnb": ["username", "token", "private"]
  317. }
  318. for field in platform_configs[self.platform]:
  319. if field == "private":
  320. widget = QCheckBox()
  321. widget.setChecked(True)
  322. else:
  323. widget = QLineEdit()
  324. if field in ["token", "password"]:
  325. widget.setEchoMode(QLineEdit.Password)
  326. self.field_widgets[field] = widget
  327. layout.addRow(f"{field.capitalize()}:", widget)
  328. # 按钮
  329. buttons = QDialogButtonBox(
  330. QDialogButtonBox.Ok | QDialogButtonBox.Cancel,
  331. Qt.Horizontal, self)
  332. buttons.accepted.connect(self.accept)
  333. buttons.rejected.connect(self.reject)
  334. layout.addRow(buttons)
  335. self.setLayout(layout)
  336. def get_account_data(self):
  337. data = {"name": self.name_edit.text().strip()}
  338. for field, widget in self.field_widgets.items():
  339. if isinstance(widget, QCheckBox):
  340. data[field] = widget.isChecked()
  341. else:
  342. data[field] = widget.text().strip()
  343. return data
  344. class MainTab(QWidget):
  345. def __init__(self, parent=None):
  346. super().__init__(parent)
  347. layout = QVBoxLayout()
  348. # 路径选择
  349. path_layout = QHBoxLayout()
  350. self.path_label = QLabel("Local Path: ")
  351. self.path_edit = QLineEdit()
  352. self.path_edit.setText(get_active_explorer_path())
  353. self.path_btn = QPushButton("Browse...")
  354. self.path_btn.clicked.connect(self.choose_path)
  355. path_layout.addWidget(self.path_label)
  356. path_layout.addWidget(self.path_edit)
  357. path_layout.addWidget(self.path_btn)
  358. layout.addLayout(path_layout)
  359. # 操作
  360. op_group = QGroupBox("Operation:")
  361. op_layout = QHBoxLayout()
  362. self.op_buttons = QButtonGroup(self)
  363. for i, op in enumerate(["create", "push", "pull", "clone", "delete"]):
  364. btn = QRadioButton(op.capitalize())
  365. if i == 1:
  366. btn.setChecked(True)
  367. self.op_buttons.addButton(btn, i)
  368. op_layout.addWidget(btn)
  369. op_group.setLayout(op_layout)
  370. layout.addWidget(op_group)
  371. # 平台
  372. pf_group = QGroupBox("Platform:")
  373. pf_layout = QHBoxLayout()
  374. self.pf_buttons = QButtonGroup(self)
  375. self.platforms = ["github", "gitlab", "gitee", "gitcode", "git.yoq.me", "coding", "aliyun", "cnb"]
  376. for i, pf in enumerate(self.platforms):
  377. btn = QRadioButton(pf.capitalize())
  378. if i == 0:
  379. btn.setChecked(True)
  380. self.pf_buttons.addButton(btn, i)
  381. pf_layout.addWidget(btn)
  382. pf_group.setLayout(pf_layout)
  383. layout.addWidget(pf_group)
  384. # 账户选择
  385. account_layout = QHBoxLayout()
  386. account_layout.addWidget(QLabel("Account:"))
  387. self.account_combo = QComboBox()
  388. self.account_combo.setMinimumWidth(200)
  389. account_layout.addWidget(self.account_combo)
  390. account_layout.addStretch()
  391. layout.addLayout(account_layout)
  392. # 按钮区域 (run和cancel)
  393. btn_layout = QHBoxLayout()
  394. self.run_btn = QPushButton("run it")
  395. self.run_btn.clicked.connect(self.run_repo_sync)
  396. self.cancel_btn = QPushButton("Cancel")
  397. self.cancel_btn.clicked.connect(self.cancel_operation)
  398. self.cancel_btn.setEnabled(False)
  399. btn_layout.addWidget(self.run_btn)
  400. btn_layout.addWidget(self.cancel_btn)
  401. layout.addLayout(btn_layout)
  402. # 命令执行结果
  403. result_group = QGroupBox("Execution Result:")
  404. result_layout = QVBoxLayout()
  405. self.result_text = QTextEdit()
  406. self.result_text.setReadOnly(True)
  407. result_layout.addWidget(self.result_text)
  408. result_group.setLayout(result_layout)
  409. layout.addWidget(result_group)
  410. self.setLayout(layout)
  411. # 命令执行相关
  412. self.process = None
  413. self.command_signals = CommandSignals()
  414. self.command_signals.output.connect(self.update_output)
  415. self.command_signals.finished.connect(self.process_finished)
  416. # 初始化配置读取器
  417. self.config_reader = ConfigReader()
  418. # 平台变更时更新账户列表
  419. self.pf_buttons.buttonClicked.connect(self.update_account_list)
  420. self.update_account_list()
  421. def choose_path(self):
  422. path = QFileDialog.getExistingDirectory(self, "Select Directory")
  423. if path:
  424. self.path_edit.setText(path)
  425. def update_account_list(self):
  426. self.account_combo.clear()
  427. pf_id = self.pf_buttons.checkedId()
  428. platform = self.platforms[pf_id]
  429. # 读取所有账户
  430. accounts = self.get_platform_accounts(platform)
  431. # 找出启用的账户
  432. enabled_account = "1"
  433. # 添加账户到下拉框,启用的账户放在最前面
  434. if enabled_account in accounts:
  435. accounts.remove(enabled_account)
  436. self.account_combo.addItem(f"{enabled_account} (启用中)")
  437. for account in accounts:
  438. self.account_combo.addItem(account)
  439. # 默认选择启用的账户
  440. self.account_combo.setCurrentIndex(0)
  441. def get_platform_accounts(self, platform):
  442. # 读取config.yml文件中的所有配置
  443. accounts = set()
  444. # 获取该平台的所有账户
  445. try:
  446. platform_accounts = self.config_reader.get_platform_accounts(platform)
  447. for account in platform_accounts:
  448. accounts.add(account)
  449. except Exception as e:
  450. print(f"Error getting platform accounts: {str(e)}")
  451. # 确保至少有一个默认账户
  452. if not accounts:
  453. accounts.add("1")
  454. return sorted(list(accounts))
  455. def run_repo_sync(self):
  456. repo_path = self.path_edit.text().strip()
  457. if not repo_path:
  458. QMessageBox.warning(self, "Warning", "Please select a local path.")
  459. return
  460. op_id = self.op_buttons.checkedId()
  461. pf_id = self.pf_buttons.checkedId()
  462. op = ["create", "push", "pull", "clone", "delete"][op_id]
  463. pf = self.platforms[pf_id]
  464. # 获取选择的账户名(去掉可能的"(启用中)"后缀)
  465. account_text = self.account_combo.currentText()
  466. account = account_text.split(" (")[0] if " (" in account_text else account_text
  467. # 清空结果区域
  468. self.result_text.clear()
  469. # 检查平台配置
  470. account_config = self.config_reader.get_account_config(pf, account)
  471. if not account_config.get('token'):
  472. QMessageBox.warning(self, "Warning", f"Please configure {pf} token for account '{account}' in Settings tab first.")
  473. return
  474. # 构建参数
  475. args = {
  476. 'command': op,
  477. 'platform': pf,
  478. 'repo_path': repo_path
  479. }
  480. # 如果不是默认账户,需要设置环境变量
  481. if account != "1":
  482. # 保存原始环境变量
  483. self.original_env = {}
  484. for field, value in account_config.items():
  485. env_var = f"{pf}_{field}"
  486. if env_var in os.environ:
  487. self.original_env[env_var] = os.environ[env_var]
  488. os.environ[env_var] = str(value)
  489. # 显示执行命令
  490. cmd_str = f"repo_sync {op} -p {pf} -repo_path {repo_path}"
  491. self.result_text.append(f"Running: {cmd_str}\n")
  492. # 执行命令
  493. self.run_btn.setEnabled(False)
  494. self.cancel_btn.setEnabled(True)
  495. # 重定向标准输出和标准错误
  496. self.original_stdout = sys.stdout
  497. self.original_stderr = sys.stderr
  498. sys.stdout = self.OutputRedirector(self.command_signals)
  499. sys.stderr = self.OutputRedirector(self.command_signals)
  500. # 在新线程中执行命令
  501. self.process_thread = threading.Thread(
  502. target=self.run_module,
  503. args=(args,)
  504. )
  505. self.process_thread.daemon = True
  506. self.process_thread.start()
  507. def run_module(self, args):
  508. try:
  509. from repo_sync import main
  510. self.running = True
  511. return_code = 0
  512. try:
  513. # 调用 repo_sync 的 main 函数
  514. main(args)
  515. except SystemExit as e:
  516. return_code = e.code if isinstance(e.code, int) else 1
  517. except Exception as e:
  518. self.command_signals.output.emit(f"Error: {str(e)}")
  519. return_code = 1
  520. finally:
  521. self.command_signals.finished.emit(return_code)
  522. # 恢复标准输出和标准错误
  523. sys.stdout = self.original_stdout
  524. sys.stderr = self.original_stderr
  525. # 恢复环境变量
  526. if hasattr(self, 'original_env'):
  527. for var, value in self.original_env.items():
  528. os.environ[var] = value
  529. delattr(self, 'original_env')
  530. except Exception as e:
  531. self.command_signals.output.emit(f"Error: {str(e)}")
  532. self.command_signals.finished.emit(1)
  533. # 恢复标准输出和标准错误
  534. sys.stdout = self.original_stdout
  535. sys.stderr = self.original_stderr
  536. # 输出重定向类
  537. class OutputRedirector:
  538. def __init__(self, signals):
  539. self.signals = signals
  540. self.buffer = ""
  541. def write(self, text):
  542. self.buffer += text
  543. if '\n' in self.buffer:
  544. lines = self.buffer.split('\n')
  545. for line in lines[:-1]:
  546. self.signals.output.emit(line)
  547. self.buffer = lines[-1]
  548. def flush(self):
  549. if self.buffer:
  550. self.signals.output.emit(self.buffer)
  551. self.buffer = ""
  552. def run_process(self, cmd, env=None):
  553. try:
  554. self.process = subprocess.Popen(
  555. cmd,
  556. stdout=subprocess.PIPE,
  557. stderr=subprocess.STDOUT,
  558. text=True,
  559. bufsize=1,
  560. universal_newlines=True,
  561. env=env
  562. )
  563. # 读取输出
  564. for line in self.process.stdout:
  565. self.command_signals.output.emit(line)
  566. self.process.wait()
  567. self.command_signals.finished.emit(self.process.returncode)
  568. except Exception as e:
  569. self.command_signals.output.emit(f"Error: {str(e)}")
  570. self.command_signals.finished.emit(1)
  571. def update_output(self, text):
  572. self.result_text.append(text)
  573. # 自动滚动到底部
  574. self.result_text.verticalScrollBar().setValue(
  575. self.result_text.verticalScrollBar().maximum()
  576. )
  577. def process_finished(self, return_code):
  578. self.process = None
  579. self.run_btn.setEnabled(True)
  580. self.cancel_btn.setEnabled(False)
  581. if return_code == 0:
  582. self.result_text.append("\nOperation completed successfully.")
  583. else:
  584. self.result_text.append(f"\nOperation failed with return code {return_code}.")
  585. def cancel_operation(self):
  586. if self.process:
  587. self.process.terminate()
  588. self.result_text.append("\nOperation cancelled by user.")
  589. else:
  590. # 直接调用模式下,通过退出标志通知线程结束
  591. self.running = False
  592. self.result_text.append("\nOperation cancelled by user.")
  593. # 恢复标准输出和标准错误
  594. if hasattr(self, 'original_stdout') and hasattr(self, 'original_stderr'):
  595. sys.stdout = self.original_stdout
  596. sys.stderr = self.original_stderr
  597. # 恢复环境变量
  598. if hasattr(self, 'original_env'):
  599. for var, value in self.original_env.items():
  600. os.environ[var] = value
  601. delattr(self, 'original_env')
  602. # Explorer路径获取
  603. try:
  604. import win32com.client
  605. def get_active_explorer_path():
  606. shell = win32com.client.Dispatch("Shell.Application")
  607. for window in shell.Windows():
  608. if window.Name in ["文件资源管理器", "Windows Explorer"]:
  609. return window.Document.Folder.Self.Path
  610. return os.getcwd()
  611. except ImportError:
  612. def get_active_explorer_path():
  613. return os.getcwd()
  614. # 命令执行信号类
  615. class CommandSignals(QObject):
  616. output = pyqtSignal(str)
  617. finished = pyqtSignal(int)
  618. class AboutTab(QWidget):
  619. def __init__(self, parent=None):
  620. super().__init__(parent)
  621. layout = QVBoxLayout()
  622. layout.addWidget(QLabel(f"repo_sync tools v{__version__}"))
  623. layout.addWidget(QLabel("作者: liuyuqi.gov@msn.cn"))
  624. layout.addWidget(QLabel("GitHub: https://github.com/jianboy/repo_sync"))
  625. layout.addWidget(QLabel("\n功能说明:"))
  626. layout.addWidget(QLabel("- 支持多个代码托管平台"))
  627. layout.addWidget(QLabel("- 支持创建/推送/拉取/克隆/删除操作"))
  628. layout.addWidget(QLabel("- 自动获取资源管理器当前路径"))
  629. layout.addWidget(QLabel("- 配置信息保存在config.yml文件中"))
  630. layout.addWidget(QLabel("- 支持每个平台配置多个账户"))
  631. layout.addWidget(QLabel("- 命令行执行结果实时显示"))
  632. self.setLayout(layout)
  633. class RepoSyncMainWindow(QTabWidget):
  634. def __init__(self):
  635. super().__init__()
  636. self.setWindowTitle('repo_sync tools v1.12')
  637. self.resize(800, 700)
  638. self.main_tab = MainTab()
  639. self.addTab(self.main_tab, '主界面')
  640. self.settings_tab = SettingsTab()
  641. self.addTab(self.settings_tab, '设置')
  642. self.about_tab = AboutTab()
  643. self.addTab(self.about_tab, '关于')
  644. def main():
  645. """GUI主入口函数"""
  646. if not HAS_QT:
  647. print("PyQt5 not installed. Please install it with: pip install PyQt5")
  648. print("Running in fallback mode...")
  649. # 这里可以添加一个简单的命令行界面作为后备
  650. return
  651. try:
  652. # 确保config.yml文件存在
  653. ensure_config_file()
  654. app = QApplication(sys.argv)
  655. window = RepoSyncMainWindow()
  656. window.show()
  657. sys.exit(app.exec_())
  658. except Exception as e:
  659. print(f"Error starting GUI: {str(e)}")
  660. import traceback
  661. traceback.print_exc()
  662. if __name__ == '__main__':
  663. main()