liuyuqi-cnb 3 months ago
parent
commit
b3e1499ef1
5 changed files with 1075 additions and 61 deletions
  1. 439 57
      gui_main.py
  2. 6 2
      main.py
  3. 13 1
      repo_sync/__init__.py
  4. 614 0
      repo_sync/gui_main.py
  5. 3 1
      repo_sync/options.py

+ 439 - 57
gui_main.py

@@ -1,14 +1,18 @@
 import sys
 import os
+import threading
+import subprocess
 from PyQt5.QtWidgets import (
     QApplication, QTabWidget, QWidget, QVBoxLayout, QHBoxLayout, QLabel, 
     QRadioButton, QPushButton, QButtonGroup, QGroupBox, QMessageBox, 
-    QLineEdit, QScrollArea, QFileDialog, QFormLayout, QCheckBox
+    QLineEdit, QScrollArea, QFileDialog, QFormLayout, QCheckBox, QTextEdit,
+    QComboBox, QDialog, QDialogButtonBox, QGridLayout, QToolButton
 )
-from PyQt5.QtCore import Qt
+from PyQt5.QtCore import Qt, pyqtSignal, QObject
 from repo_sync import RepoSync, __version__
-from dotenv import load_dotenv, set_key, find_dotenv
+from dotenv import load_dotenv, set_key, find_dotenv, dotenv_values
 import json
+import uuid
 
 # Explorer路径获取
 try:
@@ -23,6 +27,11 @@ except ImportError:
     def get_active_explorer_path():
         return os.getcwd()
 
+# 命令执行信号类
+class CommandSignals(QObject):
+    output = pyqtSignal(str)
+    finished = pyqtSignal(int)
+
 class MainTab(QWidget):
     def __init__(self, parent=None):
         super().__init__(parent)
@@ -67,17 +76,80 @@ class MainTab(QWidget):
         pf_group.setLayout(pf_layout)
         layout.addWidget(pf_group)
         
-        # run按钮
+        # 账户选择
+        account_layout = QHBoxLayout()
+        account_layout.addWidget(QLabel("Account:"))
+        self.account_combo = QComboBox()
+        self.account_combo.setMinimumWidth(200)
+        account_layout.addWidget(self.account_combo)
+        account_layout.addStretch()
+        layout.addLayout(account_layout)
+        
+        # 按钮区域 (run和cancel)
+        btn_layout = QHBoxLayout()
         self.run_btn = QPushButton("run it")
         self.run_btn.clicked.connect(self.run_repo_sync)
-        layout.addWidget(self.run_btn, alignment=Qt.AlignCenter)
+        self.cancel_btn = QPushButton("Cancel")
+        self.cancel_btn.clicked.connect(self.cancel_operation)
+        self.cancel_btn.setEnabled(False)
+        btn_layout.addWidget(self.run_btn)
+        btn_layout.addWidget(self.cancel_btn)
+        layout.addLayout(btn_layout)
+        
+        # 命令执行结果
+        result_group = QGroupBox("Execution Result:")
+        result_layout = QVBoxLayout()
+        self.result_text = QTextEdit()
+        self.result_text.setReadOnly(True)
+        result_layout.addWidget(self.result_text)
+        result_group.setLayout(result_layout)
+        layout.addWidget(result_group)
+        
         self.setLayout(layout)
+        
+        # 命令执行相关
+        self.process = None
+        self.command_signals = CommandSignals()
+        self.command_signals.output.connect(self.update_output)
+        self.command_signals.finished.connect(self.process_finished)
+        
+        # 平台变更时更新账户列表
+        self.pf_buttons.buttonClicked.connect(self.update_account_list)
+        self.update_account_list()
 
     def choose_path(self):
         path = QFileDialog.getExistingDirectory(self, "Select Directory")
         if path:
             self.path_edit.setText(path)
 
+    def update_account_list(self):
+        self.account_combo.clear()
+        pf_id = self.pf_buttons.checkedId()
+        platform = self.platforms[pf_id]
+        
+        # 读取所有账户
+        accounts = self.get_platform_accounts(platform)
+        for account_name in accounts:
+            self.account_combo.addItem(account_name)
+
+    def get_platform_accounts(self, platform):
+        # 读取.env文件中的所有配置
+        env_values = dotenv_values(find_dotenv())
+        accounts = set()
+        
+        # 默认账户
+        accounts.add("default")
+        
+        # 查找带有账户名的配置
+        prefix = f"{platform}_"
+        for key in env_values.keys():
+            if key.startswith(prefix) and "_" in key[len(prefix):]:
+                account_name = key[len(prefix):].split("_")[0]
+                if account_name != "default":
+                    accounts.add(account_name)
+        
+        return sorted(list(accounts))
+
     def run_repo_sync(self):
         repo_path = self.path_edit.text().strip()
         if not repo_path:
@@ -88,28 +160,139 @@ class MainTab(QWidget):
         pf_id = self.pf_buttons.checkedId()
         op = ["create", "push", "pull", "clone", "delete"][op_id]
         pf = self.platforms[pf_id]
+        account = self.account_combo.currentText()
+        
+        # 清空结果区域
+        self.result_text.clear()
         
         # 检查平台配置
         load_dotenv()
-        if not os.getenv(f"{pf}_token"):
-            QMessageBox.warning(self, "Warning", f"Please configure {pf} token in Settings tab first.")
+        token_key = f"{pf}_{account}_token" if account != "default" else f"{pf}_token"
+        if not os.getenv(token_key):
+            QMessageBox.warning(self, "Warning", f"Please configure {pf} token for account '{account}' in Settings tab first.")
             return
             
-        params = {
-            "command": op,
-            "platform": pf,
-            "repo_path": repo_path,
-            f"{pf}_token": os.getenv(f"{pf}_token"),
-            f"{pf}_username": os.getenv(f"{pf}_username"),
-            f"{pf}_private": os.getenv(f"{pf}_private", "true")
-        }
+        # 构建命令
+        cmd = [sys.executable, "-m", "repo_sync"]
+        cmd.append(op)
+        cmd.extend(["-p", pf])
+        cmd.extend(["-repo_path", repo_path])
+        
+        # 执行命令
+        self.run_btn.setEnabled(False)
+        self.cancel_btn.setEnabled(True)
+        self.result_text.append(f"Running: {' '.join(cmd)}\n")
         
+        # 在新线程中执行命令
+        self.process_thread = threading.Thread(
+            target=self.run_process,
+            args=(cmd,)
+        )
+        self.process_thread.daemon = True
+        self.process_thread.start()
+
+    def run_process(self, cmd):
         try:
-            rs = RepoSync(params)
-            rs.run()
-            QMessageBox.information(self, "Success", f"Operation '{op}' on '{pf}' finished.")
+            self.process = subprocess.Popen(
+                cmd,
+                stdout=subprocess.PIPE,
+                stderr=subprocess.STDOUT,
+                text=True,
+                bufsize=1,
+                universal_newlines=True
+            )
+            
+            # 读取输出
+            for line in self.process.stdout:
+                self.command_signals.output.emit(line)
+            
+            self.process.wait()
+            self.command_signals.finished.emit(self.process.returncode)
         except Exception as e:
-            QMessageBox.critical(self, "Error", str(e))
+            self.command_signals.output.emit(f"Error: {str(e)}")
+            self.command_signals.finished.emit(1)
+
+    def update_output(self, text):
+        self.result_text.append(text)
+        # 自动滚动到底部
+        self.result_text.verticalScrollBar().setValue(
+            self.result_text.verticalScrollBar().maximum()
+        )
+
+    def process_finished(self, return_code):
+        self.process = None
+        self.run_btn.setEnabled(True)
+        self.cancel_btn.setEnabled(False)
+        
+        if return_code == 0:
+            self.result_text.append("\nOperation completed successfully.")
+        else:
+            self.result_text.append(f"\nOperation failed with return code {return_code}.")
+
+    def cancel_operation(self):
+        if self.process:
+            self.process.terminate()
+            self.result_text.append("\nOperation cancelled by user.")
+
+# 添加账户对话框
+class AddAccountDialog(QDialog):
+    def __init__(self, platform, parent=None):
+        super().__init__(parent)
+        self.platform = platform
+        self.setWindowTitle(f"Add {platform.capitalize()} Account")
+        self.resize(400, 200)
+        
+        layout = QVBoxLayout()
+        
+        form_layout = QFormLayout()
+        self.account_name = QLineEdit()
+        form_layout.addRow("Account Name:", self.account_name)
+        
+        # 根据平台添加相应字段
+        self.fields = {}
+        platform_fields = {
+            "github": ["username", "token", "private"],
+            "gitlab": ["host", "username", "token", "private"],
+            "gitee": ["username", "token", "private"],
+            "gitcode": ["username", "token", "private"],
+            "git.yoq.me": ["username", "token", "private"],
+            "coding": ["username", "token", "project", "private"],
+            "aliyun": ["compoanyid", "group_id", "username", "token", "private"],
+            "cnb": ["username", "token", "private"]
+        }
+        
+        for field in platform_fields.get(platform, ["username", "token"]):
+            if field == "private":
+                widget = QCheckBox()
+                widget.setChecked(True)
+            else:
+                widget = QLineEdit()
+                if field in ["token", "password"]:
+                    widget.setEchoMode(QLineEdit.Password)
+            
+            self.fields[field] = widget
+            form_layout.addRow(f"{field.capitalize()}:", widget)
+        
+        layout.addLayout(form_layout)
+        
+        # 按钮
+        buttons = QDialogButtonBox(
+            QDialogButtonBox.Ok | QDialogButtonBox.Cancel
+        )
+        buttons.accepted.connect(self.accept)
+        buttons.rejected.connect(self.reject)
+        layout.addWidget(buttons)
+        
+        self.setLayout(layout)
+    
+    def get_account_data(self):
+        data = {"name": self.account_name.text()}
+        for field, widget in self.fields.items():
+            if isinstance(widget, QCheckBox):
+                data[field] = widget.isChecked()
+            else:
+                data[field] = widget.text()
+        return data
 
 class SettingsTab(QWidget):
     def __init__(self, parent=None):
@@ -124,7 +307,7 @@ class SettingsTab(QWidget):
         scroll = QScrollArea()
         scroll.setWidgetResizable(True)
         content = QWidget()
-        self.form_layout = QFormLayout()
+        content_layout = QVBoxLayout()
         
         # 平台配置
         self.platform_configs = {
@@ -138,61 +321,258 @@ class SettingsTab(QWidget):
             "cnb": ["username", "token", "private"]
         }
         
-        self.config_widgets = {}
-        for platform, fields in self.platform_configs.items():
+        # 为每个平台创建分组
+        self.platform_groups = {}
+        for platform in self.platform_configs.keys():
             group = QGroupBox(platform.capitalize())
-            group_layout = QFormLayout()
-            
-            platform_widgets = {}
-            for field in fields:
-                if field == "private":
-                    widget = QCheckBox()
-                    widget.setChecked(True)
-                else:
-                    widget = QLineEdit()
-                    if field in ["token", "password"]:
-                        widget.setEchoMode(QLineEdit.Password)
-                
-                field_name = f"{platform}_{field}"
-                platform_widgets[field_name] = widget
-                group_layout.addRow(f"{field.capitalize()}:", widget)
+            group_layout = QVBoxLayout()
+            
+            # 账户选择和管理
+            account_header = QHBoxLayout()
+            account_header.addWidget(QLabel("Accounts:"))
+            account_combo = QComboBox()
+            account_combo.setMinimumWidth(200)
+            account_header.addWidget(account_combo)
+            
+            # 添加账户按钮
+            add_btn = QToolButton()
+            add_btn.setText("+")
+            add_btn.clicked.connect(lambda checked, p=platform: self.add_account(p))
+            account_header.addWidget(add_btn)
+            
+            # 删除账户按钮
+            del_btn = QToolButton()
+            del_btn.setText("-")
+            del_btn.clicked.connect(lambda checked, p=platform, c=account_combo: self.delete_account(p, c))
+            account_header.addWidget(del_btn)
+            
+            # 启用账户按钮
+            enable_btn = QPushButton("Enable")
+            enable_btn.clicked.connect(lambda checked, p=platform, c=account_combo: self.enable_account(p, c))
+            account_header.addWidget(enable_btn)
+            
+            group_layout.addLayout(account_header)
+            
+            # 账户详情区域
+            account_details = QWidget()
+            account_form = QFormLayout()
+            account_details.setLayout(account_form)
+            group_layout.addWidget(account_details)
             
-            self.config_widgets.update(platform_widgets)
             group.setLayout(group_layout)
-            self.form_layout.addRow(group)
+            content_layout.addWidget(group)
+            
+            # 保存引用
+            self.platform_groups[platform] = {
+                "group": group,
+                "combo": account_combo,
+                "details": account_details,
+                "form": account_form
+            }
+        
+        content.setLayout(content_layout)
+        scroll.setWidget(content)
+        layout.addWidget(scroll)
         
         # 保存按钮
         self.save_btn = QPushButton("Save Settings")
         self.save_btn.clicked.connect(self.save_settings)
-        
-        content.setLayout(self.form_layout)
-        scroll.setWidget(content)
-        layout.addWidget(scroll)
         layout.addWidget(self.save_btn, alignment=Qt.AlignCenter)
+        
         self.setLayout(layout)
 
     def load_settings(self):
-        load_dotenv()
-        for field_name, widget in self.config_widgets.items():
-            value = os.getenv(field_name)
-            if isinstance(widget, QCheckBox):
-                widget.setChecked(value != "false")
-            elif value:
+        # 读取.env文件
+        env_values = dotenv_values(find_dotenv())
+        
+        # 为每个平台加载账户
+        for platform, group_data in self.platform_groups.items():
+            combo = group_data["combo"]
+            combo.clear()
+            
+            # 查找该平台的所有账户
+            accounts = self.get_platform_accounts(platform, env_values)
+            
+            # 添加到下拉框
+            for account in accounts:
+                combo.addItem(account)
+            
+            # 连接选择变更事件
+            combo.currentIndexChanged.connect(
+                lambda idx, p=platform: self.update_account_details(p)
+            )
+            
+            # 更新当前选择的账户详情
+            if combo.count() > 0:
+                self.update_account_details(platform)
+
+    def get_platform_accounts(self, platform, env_values=None):
+        if env_values is None:
+            env_values = dotenv_values(find_dotenv())
+        
+        accounts = set()
+        # 默认账户
+        accounts.add("default")
+        
+        # 查找带有账户名的配置
+        prefix = f"{platform}_"
+        for key in env_values.keys():
+            if key.startswith(prefix) and "_" in key[len(prefix):]:
+                account_name = key[len(prefix):].split("_")[0]
+                if account_name != "default":
+                    accounts.add(account_name)
+        
+        return sorted(list(accounts))
+
+    def update_account_details(self, platform):
+        group_data = self.platform_groups[platform]
+        combo = group_data["combo"]
+        form = group_data["form"]
+        
+        # 清空表单
+        while form.rowCount() > 0:
+            form.removeRow(0)
+        
+        # 获取当前选择的账户
+        account = combo.currentText()
+        if not account:
+            return
+        
+        # 读取账户配置
+        env_values = dotenv_values(find_dotenv())
+        
+        # 创建表单项
+        self.field_widgets = {}
+        for field in self.platform_configs[platform]:
+            key = f"{platform}_{account}_{field}" if account != "default" else f"{platform}_{field}"
+            value = env_values.get(key, "")
+            
+            if field == "private":
+                widget = QCheckBox()
+                widget.setChecked(value.lower() != "false")
+            else:
+                widget = QLineEdit()
+                if field in ["token", "password"]:
+                    widget.setEchoMode(QLineEdit.Password)
                 widget.setText(value)
+            
+            self.field_widgets[key] = widget
+            form.addRow(f"{field.capitalize()}:", widget)
+
+    def add_account(self, platform):
+        dialog = AddAccountDialog(platform, self)
+        if dialog.exec_() == QDialog.Accepted:
+            account_data = dialog.get_account_data()
+            account_name = account_data["name"]
+            
+            if not account_name:
+                QMessageBox.warning(self, "Warning", "Account name cannot be empty.")
+                return
+            
+            # 更新.env文件
+            env_file = find_dotenv()
+            if not env_file:
+                env_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), '.env')
+            
+            for field, value in account_data.items():
+                if field == "name":
+                    continue
+                
+                key = f"{platform}_{account_name}_{field}"
+                if isinstance(value, bool):
+                    value = str(value).lower()
+                
+                set_key(env_file, key, value)
+            
+            # 重新加载设置
+            self.load_settings()
+            
+            # 选择新账户
+            combo = self.platform_groups[platform]["combo"]
+            idx = combo.findText(account_name)
+            if idx >= 0:
+                combo.setCurrentIndex(idx)
+
+    def delete_account(self, platform, combo):
+        account = combo.currentText()
+        if account == "default":
+            QMessageBox.warning(self, "Warning", "Cannot delete the default account.")
+            return
+        
+        reply = QMessageBox.question(
+            self, 
+            "Confirm Deletion",
+            f"Are you sure you want to delete the account '{account}' for {platform}?",
+            QMessageBox.Yes | QMessageBox.No,
+            QMessageBox.No
+        )
+        
+        if reply == QMessageBox.Yes:
+            # 删除.env中的相关配置
+            env_file = find_dotenv()
+            env_values = dotenv_values(env_file)
+            
+            prefix = f"{platform}_{account}_"
+            keys_to_remove = [k for k in env_values.keys() if k.startswith(prefix)]
+            
+            # 重写.env文件,排除要删除的键
+            with open(env_file, 'w') as f:
+                for k, v in env_values.items():
+                    if k not in keys_to_remove:
+                        f.write(f"{k}={v}\n")
+            
+            # 重新加载设置
+            self.load_settings()
+
+    def enable_account(self, platform, combo):
+        account = combo.currentText()
+        if account == "default":
+            QMessageBox.information(self, "Information", "Default account is already enabled.")
+            return
+        
+        # 读取账户配置
+        env_file = find_dotenv()
+        env_values = dotenv_values(env_file)
+        
+        # 获取账户配置
+        account_config = {}
+        prefix = f"{platform}_{account}_"
+        for key, value in env_values.items():
+            if key.startswith(prefix):
+                field = key[len(prefix):]
+                account_config[field] = value
+        
+        # 更新默认配置
+        for field, value in account_config.items():
+            default_key = f"{platform}_{field}"
+            set_key(env_file, default_key, value)
+        
+        QMessageBox.information(
+            self, 
+            "Success", 
+            f"Account '{account}' has been enabled as the default for {platform}."
+        )
 
     def save_settings(self):
         env_file = find_dotenv()
         if not env_file:
             env_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), '.env')
         
-        for field_name, widget in self.config_widgets.items():
-            if isinstance(widget, QCheckBox):
-                value = str(widget.isChecked()).lower()
-            else:
-                value = widget.text().strip()
+        # 保存当前显示的账户配置
+        for platform, group_data in self.platform_groups.items():
+            combo = group_data["combo"]
+            account = combo.currentText()
             
-            if value:
-                set_key(env_file, field_name, value)
+            if account and hasattr(self, 'field_widgets'):
+                for key, widget in self.field_widgets.items():
+                    if key.startswith(f"{platform}_{account}"):
+                        if isinstance(widget, QCheckBox):
+                            value = str(widget.isChecked()).lower()
+                        else:
+                            value = widget.text().strip()
+                        
+                        if value:
+                            set_key(env_file, key, value)
         
         QMessageBox.information(self, "Success", "Settings saved successfully!")
 
@@ -208,13 +588,15 @@ class AboutTab(QWidget):
         layout.addWidget(QLabel("- 支持创建/推送/拉取/克隆/删除操作"))
         layout.addWidget(QLabel("- 自动获取资源管理器当前路径"))
         layout.addWidget(QLabel("- 配置信息保存在.env文件中"))
+        layout.addWidget(QLabel("- 支持每个平台配置多个账户"))
+        layout.addWidget(QLabel("- 命令行执行结果实时显示"))
         self.setLayout(layout)
 
 class RepoSyncMainWindow(QTabWidget):
     def __init__(self):
         super().__init__()
         self.setWindowTitle('repo_sync tools v1.12')
-        self.resize(800, 600)
+        self.resize(800, 700)
         self.main_tab = MainTab()
         self.addTab(self.main_tab, '主界面')
         self.settings_tab = SettingsTab()

+ 6 - 2
main.py

@@ -6,7 +6,11 @@
 @License :   Copyright © 2017-2022 liuyuqi. All Rights Reserved.
 @Desc    :   
 '''
-from repo_sync import main
+from repo_sync import main, gui_main
+import sys
 
 if __name__=='__main__':
-    main()
+    if len(sys.argv) > 1 and sys.argv[1] == 'gui':
+        gui_main()
+    else:
+        main()

+ 13 - 1
repo_sync/__init__.py

@@ -21,4 +21,16 @@ def main(argv=None):
         logger.error('ERROR: Interrupted by user')
         sys.exit(1)
 
-# GUI入口预留
+# GUI入口
+def gui_main():
+    """GUI entry point of the program"""
+    try:
+        from .gui_main import main as gui_main_func
+        gui_main_func()
+    except ImportError as e:
+        print(f"GUI dependencies not installed: {e}")
+        print("Please install PyQt5 and pywin32 with: pip install PyQt5 pywin32")
+        sys.exit(1)
+    except Exception as e:
+        print(f"Error starting GUI: {e}")
+        sys.exit(1)

+ 614 - 0
repo_sync/gui_main.py

@@ -0,0 +1,614 @@
+import sys
+import os
+import threading
+import subprocess
+from PyQt5.QtWidgets import (
+    QApplication, QTabWidget, QWidget, QVBoxLayout, QHBoxLayout, QLabel, 
+    QRadioButton, QPushButton, QButtonGroup, QGroupBox, QMessageBox, 
+    QLineEdit, QScrollArea, QFileDialog, QFormLayout, QCheckBox, QTextEdit,
+    QComboBox, QDialog, QDialogButtonBox, QGridLayout, QToolButton
+)
+from PyQt5.QtCore import Qt, pyqtSignal, QObject
+from repo_sync import RepoSync, __version__
+from dotenv import load_dotenv, set_key, find_dotenv, dotenv_values
+import json
+import uuid
+
+# Explorer路径获取
+try:
+    import win32com.client
+    def get_active_explorer_path():
+        shell = win32com.client.Dispatch("Shell.Application")
+        for window in shell.Windows():
+            if window.Name in ["文件资源管理器", "Windows Explorer"]:
+                return window.Document.Folder.Self.Path
+        return os.getcwd()
+except ImportError:
+    def get_active_explorer_path():
+        return os.getcwd()
+
+# 命令执行信号类
+class CommandSignals(QObject):
+    output = pyqtSignal(str)
+    finished = pyqtSignal(int)
+
+class MainTab(QWidget):
+    def __init__(self, parent=None):
+        super().__init__(parent)
+        layout = QVBoxLayout()
+        
+        # 路径选择
+        path_layout = QHBoxLayout()
+        self.path_label = QLabel("Local Path: ")
+        self.path_edit = QLineEdit()
+        self.path_edit.setText(get_active_explorer_path())
+        self.path_btn = QPushButton("Browse...")
+        self.path_btn.clicked.connect(self.choose_path)
+        path_layout.addWidget(self.path_label)
+        path_layout.addWidget(self.path_edit)
+        path_layout.addWidget(self.path_btn)
+        layout.addLayout(path_layout)
+        
+        # 操作
+        op_group = QGroupBox("Operation:")
+        op_layout = QHBoxLayout()
+        self.op_buttons = QButtonGroup(self)
+        for i, op in enumerate(["create", "push", "pull", "clone", "delete"]):
+            btn = QRadioButton(op.capitalize())
+            if i == 0:
+                btn.setChecked(True)
+            self.op_buttons.addButton(btn, i)
+            op_layout.addWidget(btn)
+        op_group.setLayout(op_layout)
+        layout.addWidget(op_group)
+        
+        # 平台
+        pf_group = QGroupBox("Platform:")
+        pf_layout = QHBoxLayout()
+        self.pf_buttons = QButtonGroup(self)
+        self.platforms = ["github", "gitlab", "gitee", "gitcode", "git.yoq.me", "coding", "aliyun", "cnb"]
+        for i, pf in enumerate(self.platforms):
+            btn = QRadioButton(pf.capitalize())
+            if i == 0:
+                btn.setChecked(True)
+            self.pf_buttons.addButton(btn, i)
+            pf_layout.addWidget(btn)
+        pf_group.setLayout(pf_layout)
+        layout.addWidget(pf_group)
+        
+        # 账户选择
+        account_layout = QHBoxLayout()
+        account_layout.addWidget(QLabel("Account:"))
+        self.account_combo = QComboBox()
+        self.account_combo.setMinimumWidth(200)
+        account_layout.addWidget(self.account_combo)
+        account_layout.addStretch()
+        layout.addLayout(account_layout)
+        
+        # 按钮区域 (run和cancel)
+        btn_layout = QHBoxLayout()
+        self.run_btn = QPushButton("run it")
+        self.run_btn.clicked.connect(self.run_repo_sync)
+        self.cancel_btn = QPushButton("Cancel")
+        self.cancel_btn.clicked.connect(self.cancel_operation)
+        self.cancel_btn.setEnabled(False)
+        btn_layout.addWidget(self.run_btn)
+        btn_layout.addWidget(self.cancel_btn)
+        layout.addLayout(btn_layout)
+        
+        # 命令执行结果
+        result_group = QGroupBox("Execution Result:")
+        result_layout = QVBoxLayout()
+        self.result_text = QTextEdit()
+        self.result_text.setReadOnly(True)
+        result_layout.addWidget(self.result_text)
+        result_group.setLayout(result_layout)
+        layout.addWidget(result_group)
+        
+        self.setLayout(layout)
+        
+        # 命令执行相关
+        self.process = None
+        self.command_signals = CommandSignals()
+        self.command_signals.output.connect(self.update_output)
+        self.command_signals.finished.connect(self.process_finished)
+        
+        # 平台变更时更新账户列表
+        self.pf_buttons.buttonClicked.connect(self.update_account_list)
+        self.update_account_list()
+
+    def choose_path(self):
+        path = QFileDialog.getExistingDirectory(self, "Select Directory")
+        if path:
+            self.path_edit.setText(path)
+
+    def update_account_list(self):
+        self.account_combo.clear()
+        pf_id = self.pf_buttons.checkedId()
+        platform = self.platforms[pf_id]
+        
+        # 读取所有账户
+        accounts = self.get_platform_accounts(platform)
+        for account_name in accounts:
+            self.account_combo.addItem(account_name)
+
+    def get_platform_accounts(self, platform):
+        # 读取.env文件中的所有配置
+        env_values = dotenv_values(find_dotenv())
+        accounts = set()
+        
+        # 默认账户
+        accounts.add("default")
+        
+        # 查找带有账户名的配置
+        prefix = f"{platform}_"
+        for key in env_values.keys():
+            if key.startswith(prefix) and "_" in key[len(prefix):]:
+                account_name = key[len(prefix):].split("_")[0]
+                if account_name != "default":
+                    accounts.add(account_name)
+        
+        return sorted(list(accounts))
+
+    def run_repo_sync(self):
+        repo_path = self.path_edit.text().strip()
+        if not repo_path:
+            QMessageBox.warning(self, "Warning", "Please select a local path.")
+            return
+            
+        op_id = self.op_buttons.checkedId()
+        pf_id = self.pf_buttons.checkedId()
+        op = ["create", "push", "pull", "clone", "delete"][op_id]
+        pf = self.platforms[pf_id]
+        account = self.account_combo.currentText()
+        
+        # 清空结果区域
+        self.result_text.clear()
+        
+        # 检查平台配置
+        load_dotenv()
+        token_key = f"{pf}_{account}_token" if account != "default" else f"{pf}_token"
+        if not os.getenv(token_key):
+            QMessageBox.warning(self, "Warning", f"Please configure {pf} token for account '{account}' in Settings tab first.")
+            return
+            
+        # 构建命令
+        cmd = [sys.executable, "-m", "repo_sync"]
+        cmd.append(op)
+        cmd.extend(["-p", pf])
+        cmd.extend(["-repo_path", repo_path])
+        
+        # 执行命令
+        self.run_btn.setEnabled(False)
+        self.cancel_btn.setEnabled(True)
+        self.result_text.append(f"Running: {' '.join(cmd)}\n")
+        
+        # 在新线程中执行命令
+        self.process_thread = threading.Thread(
+            target=self.run_process,
+            args=(cmd,)
+        )
+        self.process_thread.daemon = True
+        self.process_thread.start()
+
+    def run_process(self, cmd):
+        try:
+            self.process = subprocess.Popen(
+                cmd,
+                stdout=subprocess.PIPE,
+                stderr=subprocess.STDOUT,
+                text=True,
+                bufsize=1,
+                universal_newlines=True
+            )
+            
+            # 读取输出
+            for line in self.process.stdout:
+                self.command_signals.output.emit(line)
+            
+            self.process.wait()
+            self.command_signals.finished.emit(self.process.returncode)
+        except Exception as e:
+            self.command_signals.output.emit(f"Error: {str(e)}")
+            self.command_signals.finished.emit(1)
+
+    def update_output(self, text):
+        self.result_text.append(text)
+        # 自动滚动到底部
+        self.result_text.verticalScrollBar().setValue(
+            self.result_text.verticalScrollBar().maximum()
+        )
+
+    def process_finished(self, return_code):
+        self.process = None
+        self.run_btn.setEnabled(True)
+        self.cancel_btn.setEnabled(False)
+        
+        if return_code == 0:
+            self.result_text.append("\nOperation completed successfully.")
+        else:
+            self.result_text.append(f"\nOperation failed with return code {return_code}.")
+
+    def cancel_operation(self):
+        if self.process:
+            self.process.terminate()
+            self.result_text.append("\nOperation cancelled by user.")
+
+# 添加账户对话框
+class AddAccountDialog(QDialog):
+    def __init__(self, platform, parent=None):
+        super().__init__(parent)
+        self.platform = platform
+        self.setWindowTitle(f"Add {platform.capitalize()} Account")
+        self.resize(400, 200)
+        
+        layout = QVBoxLayout()
+        
+        form_layout = QFormLayout()
+        self.account_name = QLineEdit()
+        form_layout.addRow("Account Name:", self.account_name)
+        
+        # 根据平台添加相应字段
+        self.fields = {}
+        platform_fields = {
+            "github": ["username", "token", "private"],
+            "gitlab": ["host", "username", "token", "private"],
+            "gitee": ["username", "token", "private"],
+            "gitcode": ["username", "token", "private"],
+            "git.yoq.me": ["username", "token", "private"],
+            "coding": ["username", "token", "project", "private"],
+            "aliyun": ["compoanyid", "group_id", "username", "token", "private"],
+            "cnb": ["username", "token", "private"]
+        }
+        
+        for field in platform_fields.get(platform, ["username", "token"]):
+            if field == "private":
+                widget = QCheckBox()
+                widget.setChecked(True)
+            else:
+                widget = QLineEdit()
+                if field in ["token", "password"]:
+                    widget.setEchoMode(QLineEdit.Password)
+            
+            self.fields[field] = widget
+            form_layout.addRow(f"{field.capitalize()}:", widget)
+        
+        layout.addLayout(form_layout)
+        
+        # 按钮
+        buttons = QDialogButtonBox(
+            QDialogButtonBox.Ok | QDialogButtonBox.Cancel
+        )
+        buttons.accepted.connect(self.accept)
+        buttons.rejected.connect(self.reject)
+        layout.addWidget(buttons)
+        
+        self.setLayout(layout)
+    
+    def get_account_data(self):
+        data = {"name": self.account_name.text()}
+        for field, widget in self.fields.items():
+            if isinstance(widget, QCheckBox):
+                data[field] = widget.isChecked()
+            else:
+                data[field] = widget.text()
+        return data
+
+class SettingsTab(QWidget):
+    def __init__(self, parent=None):
+        super().__init__(parent)
+        self.init_ui()
+        self.load_settings()
+
+    def init_ui(self):
+        layout = QVBoxLayout()
+        
+        # 创建滚动区域
+        scroll = QScrollArea()
+        scroll.setWidgetResizable(True)
+        content = QWidget()
+        content_layout = QVBoxLayout()
+        
+        # 平台配置
+        self.platform_configs = {
+            "github": ["username", "token", "private"],
+            "gitlab": ["host", "username", "token", "private"],
+            "gitee": ["username", "token", "private"],
+            "gitcode": ["username", "token", "private"],
+            "git.yoq.me": ["username", "token", "private"],
+            "coding": ["username", "token", "project", "private"],
+            "aliyun": ["compoanyid", "group_id", "username", "token", "private"],
+            "cnb": ["username", "token", "private"]
+        }
+        
+        # 为每个平台创建分组
+        self.platform_groups = {}
+        for platform in self.platform_configs.keys():
+            group = QGroupBox(platform.capitalize())
+            group_layout = QVBoxLayout()
+            
+            # 账户选择和管理
+            account_header = QHBoxLayout()
+            account_header.addWidget(QLabel("Accounts:"))
+            account_combo = QComboBox()
+            account_combo.setMinimumWidth(200)
+            account_header.addWidget(account_combo)
+            
+            # 添加账户按钮
+            add_btn = QToolButton()
+            add_btn.setText("+")
+            add_btn.clicked.connect(lambda checked, p=platform: self.add_account(p))
+            account_header.addWidget(add_btn)
+            
+            # 删除账户按钮
+            del_btn = QToolButton()
+            del_btn.setText("-")
+            del_btn.clicked.connect(lambda checked, p=platform, c=account_combo: self.delete_account(p, c))
+            account_header.addWidget(del_btn)
+            
+            # 启用账户按钮
+            enable_btn = QPushButton("Enable")
+            enable_btn.clicked.connect(lambda checked, p=platform, c=account_combo: self.enable_account(p, c))
+            account_header.addWidget(enable_btn)
+            
+            group_layout.addLayout(account_header)
+            
+            # 账户详情区域
+            account_details = QWidget()
+            account_form = QFormLayout()
+            account_details.setLayout(account_form)
+            group_layout.addWidget(account_details)
+            
+            group.setLayout(group_layout)
+            content_layout.addWidget(group)
+            
+            # 保存引用
+            self.platform_groups[platform] = {
+                "group": group,
+                "combo": account_combo,
+                "details": account_details,
+                "form": account_form
+            }
+        
+        content.setLayout(content_layout)
+        scroll.setWidget(content)
+        layout.addWidget(scroll)
+        
+        # 保存按钮
+        self.save_btn = QPushButton("Save Settings")
+        self.save_btn.clicked.connect(self.save_settings)
+        layout.addWidget(self.save_btn, alignment=Qt.AlignCenter)
+        
+        self.setLayout(layout)
+
+    def load_settings(self):
+        # 读取.env文件
+        env_values = dotenv_values(find_dotenv())
+        
+        # 为每个平台加载账户
+        for platform, group_data in self.platform_groups.items():
+            combo = group_data["combo"]
+            combo.clear()
+            
+            # 查找该平台的所有账户
+            accounts = self.get_platform_accounts(platform, env_values)
+            
+            # 添加到下拉框
+            for account in accounts:
+                combo.addItem(account)
+            
+            # 连接选择变更事件
+            combo.currentIndexChanged.connect(
+                lambda idx, p=platform: self.update_account_details(p)
+            )
+            
+            # 更新当前选择的账户详情
+            if combo.count() > 0:
+                self.update_account_details(platform)
+
+    def get_platform_accounts(self, platform, env_values=None):
+        if env_values is None:
+            env_values = dotenv_values(find_dotenv())
+        
+        accounts = set()
+        # 默认账户
+        accounts.add("default")
+        
+        # 查找带有账户名的配置
+        prefix = f"{platform}_"
+        for key in env_values.keys():
+            if key.startswith(prefix) and "_" in key[len(prefix):]:
+                account_name = key[len(prefix):].split("_")[0]
+                if account_name != "default":
+                    accounts.add(account_name)
+        
+        return sorted(list(accounts))
+
+    def update_account_details(self, platform):
+        group_data = self.platform_groups[platform]
+        combo = group_data["combo"]
+        form = group_data["form"]
+        
+        # 清空表单
+        while form.rowCount() > 0:
+            form.removeRow(0)
+        
+        # 获取当前选择的账户
+        account = combo.currentText()
+        if not account:
+            return
+        
+        # 读取账户配置
+        env_values = dotenv_values(find_dotenv())
+        
+        # 创建表单项
+        self.field_widgets = {}
+        for field in self.platform_configs[platform]:
+            key = f"{platform}_{account}_{field}" if account != "default" else f"{platform}_{field}"
+            value = env_values.get(key, "")
+            
+            if field == "private":
+                widget = QCheckBox()
+                widget.setChecked(value.lower() != "false")
+            else:
+                widget = QLineEdit()
+                if field in ["token", "password"]:
+                    widget.setEchoMode(QLineEdit.Password)
+                widget.setText(value)
+            
+            self.field_widgets[key] = widget
+            form.addRow(f"{field.capitalize()}:", widget)
+
+    def add_account(self, platform):
+        dialog = AddAccountDialog(platform, self)
+        if dialog.exec_() == QDialog.Accepted:
+            account_data = dialog.get_account_data()
+            account_name = account_data["name"]
+            
+            if not account_name:
+                QMessageBox.warning(self, "Warning", "Account name cannot be empty.")
+                return
+            
+            # 更新.env文件
+            env_file = find_dotenv()
+            if not env_file:
+                env_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), '.env')
+            
+            for field, value in account_data.items():
+                if field == "name":
+                    continue
+                
+                key = f"{platform}_{account_name}_{field}"
+                if isinstance(value, bool):
+                    value = str(value).lower()
+                
+                set_key(env_file, key, value)
+            
+            # 重新加载设置
+            self.load_settings()
+            
+            # 选择新账户
+            combo = self.platform_groups[platform]["combo"]
+            idx = combo.findText(account_name)
+            if idx >= 0:
+                combo.setCurrentIndex(idx)
+
+    def delete_account(self, platform, combo):
+        account = combo.currentText()
+        if account == "default":
+            QMessageBox.warning(self, "Warning", "Cannot delete the default account.")
+            return
+        
+        reply = QMessageBox.question(
+            self, 
+            "Confirm Deletion",
+            f"Are you sure you want to delete the account '{account}' for {platform}?",
+            QMessageBox.Yes | QMessageBox.No,
+            QMessageBox.No
+        )
+        
+        if reply == QMessageBox.Yes:
+            # 删除.env中的相关配置
+            env_file = find_dotenv()
+            env_values = dotenv_values(env_file)
+            
+            prefix = f"{platform}_{account}_"
+            keys_to_remove = [k for k in env_values.keys() if k.startswith(prefix)]
+            
+            # 重写.env文件,排除要删除的键
+            with open(env_file, 'w') as f:
+                for k, v in env_values.items():
+                    if k not in keys_to_remove:
+                        f.write(f"{k}={v}\n")
+            
+            # 重新加载设置
+            self.load_settings()
+
+    def enable_account(self, platform, combo):
+        account = combo.currentText()
+        if account == "default":
+            QMessageBox.information(self, "Information", "Default account is already enabled.")
+            return
+        
+        # 读取账户配置
+        env_file = find_dotenv()
+        env_values = dotenv_values(env_file)
+        
+        # 获取账户配置
+        account_config = {}
+        prefix = f"{platform}_{account}_"
+        for key, value in env_values.items():
+            if key.startswith(prefix):
+                field = key[len(prefix):]
+                account_config[field] = value
+        
+        # 更新默认配置
+        for field, value in account_config.items():
+            default_key = f"{platform}_{field}"
+            set_key(env_file, default_key, value)
+        
+        QMessageBox.information(
+            self, 
+            "Success", 
+            f"Account '{account}' has been enabled as the default for {platform}."
+        )
+
+    def save_settings(self):
+        env_file = find_dotenv()
+        if not env_file:
+            env_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), '.env')
+        
+        # 保存当前显示的账户配置
+        for platform, group_data in self.platform_groups.items():
+            combo = group_data["combo"]
+            account = combo.currentText()
+            
+            if account and hasattr(self, 'field_widgets'):
+                for key, widget in self.field_widgets.items():
+                    if key.startswith(f"{platform}_{account}"):
+                        if isinstance(widget, QCheckBox):
+                            value = str(widget.isChecked()).lower()
+                        else:
+                            value = widget.text().strip()
+                        
+                        if value:
+                            set_key(env_file, key, value)
+        
+        QMessageBox.information(self, "Success", "Settings saved successfully!")
+
+class AboutTab(QWidget):
+    def __init__(self, parent=None):
+        super().__init__(parent)
+        layout = QVBoxLayout()
+        layout.addWidget(QLabel(f"repo_sync tools v{__version__}"))
+        layout.addWidget(QLabel("作者: liuyuqi.gov@msn.cn"))
+        layout.addWidget(QLabel("GitHub: https://github.com/jianboy/repo_sync"))
+        layout.addWidget(QLabel("\n功能说明:"))
+        layout.addWidget(QLabel("- 支持多个代码托管平台"))
+        layout.addWidget(QLabel("- 支持创建/推送/拉取/克隆/删除操作"))
+        layout.addWidget(QLabel("- 自动获取资源管理器当前路径"))
+        layout.addWidget(QLabel("- 配置信息保存在.env文件中"))
+        layout.addWidget(QLabel("- 支持每个平台配置多个账户"))
+        layout.addWidget(QLabel("- 命令行执行结果实时显示"))
+        self.setLayout(layout)
+
+class RepoSyncMainWindow(QTabWidget):
+    def __init__(self):
+        super().__init__()
+        self.setWindowTitle('repo_sync tools v1.12')
+        self.resize(800, 700)
+        self.main_tab = MainTab()
+        self.addTab(self.main_tab, '主界面')
+        self.settings_tab = SettingsTab()
+        self.addTab(self.settings_tab, '设置')
+        self.about_tab = AboutTab()
+        self.addTab(self.about_tab, '关于')
+
+def main():
+    app = QApplication(sys.argv)
+    window = RepoSyncMainWindow()
+    window.show()
+    sys.exit(app.exec_())
+
+if __name__ == '__main__':
+    main() 

+ 3 - 1
repo_sync/options.py

@@ -23,7 +23,9 @@ def parser_args(overrideArguments=None):
     argparser.add_argument(
         'command',
         help='command: ',
-        choices=['create', 'clone', 'push', 'delete', 'pull'],
+        choices=['create', 'clone', 'push', 'delete', 'pull', 'gui'],
+        nargs='?',
+        default=''
     )
     argparser.add_argument('-d', '--debug', help='debug mode', action='store_true')
     argparser.add_argument(