Browse Source

Merge branch 'release/1.11.0'

liuyuqi-dellpc 2 months ago
parent
commit
b37aed8bc7

+ 821 - 0
gui_main.py

@@ -0,0 +1,821 @@
+#!/usr/bin/env python
+# -*- encoding: utf-8 -*-
+'''
+@Contact :   liuyuqi.gov@msn.cn
+@Time    :   2023/04/27 02:55:59
+@License :   Copyright © 2017-2022 liuyuqi. All Rights Reserved.
+@Desc    :   repo_sync GUI入口
+'''
+import sys
+import os
+import threading
+import subprocess
+try:
+    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, QListWidget, QListWidgetItem
+    )
+    from PyQt5.QtCore import Qt, pyqtSignal, QObject
+    HAS_QT = True
+except ImportError:
+    HAS_QT = False
+    print("PyQt5 not installed, GUI mode not available")
+
+# 直接导入repo_sync模块
+sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
+try:
+    from repo_sync.repo_sync import RepoSync
+    from repo_sync.version import __version__
+except ImportError:
+    print("无法导入repo_sync模块,尝试直接导入...")
+    try:
+        from repo_sync import RepoSync
+        from repo_sync.version import __version__
+    except ImportError:
+        print("导入repo_sync模块失败,请确保repo_sync已正确安装")
+        __version__ = "未知"
+        
+        # 创建一个空的RepoSync类作为替代
+        class RepoSync:
+            def __init__(self, params):
+                self.params = params
+            def run(self):
+                print("RepoSync模块未找到,无法执行操作")
+
+try:
+    from dotenv import load_dotenv, set_key, find_dotenv, dotenv_values
+except ImportError:
+    print("python-dotenv未安装,请使用pip install python-dotenv安装")
+    sys.exit(1)
+    
+import json
+import uuid
+
+# 确保.env文件存在
+def ensure_env_file():
+    env_file = find_dotenv()
+    if not env_file:
+        # 创建空的.env文件
+        env_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), '.env')
+        with open(env_file, 'w') as f:
+            f.write("# repo_sync配置文件\n")
+    return env_file
+
+# 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)
+        
+        # 找出启用的账户
+        enabled_account = "default"
+        env_values = dotenv_values(find_dotenv())
+        for account in accounts:
+            if account != "default" and env_values.get(f"{platform}_{account}_enabled", "").lower() == "true":
+                enabled_account = account
+                break
+        
+        # 添加账户到下拉框,启用的账户放在最前面
+        if enabled_account in accounts:
+            accounts.remove(enabled_account)
+            self.account_combo.addItem(f"{enabled_account} (启用中)")
+            
+        for account in accounts:
+            self.account_combo.addItem(account)
+        
+        # 默认选择启用的账户
+        self.account_combo.setCurrentIndex(0)
+
+    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" and account_name != "":
+                    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_text = self.account_combo.currentText()
+        account = account_text.split(" (")[0] if " (" in account_text else account_text
+        
+        # 清空结果区域
+        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])
+        
+        # 如果不是默认账户,需要设置环境变量
+        env = os.environ.copy()
+        if account != "default":
+            # 读取账户配置
+            env_values = dotenv_values(find_dotenv())
+            for key, value in env_values.items():
+                if key.startswith(f"{pf}_{account}_"):
+                    field = key[len(f"{pf}_{account}_"):]
+                    env[f"{pf}_{field}"] = value
+        
+        # 执行命令
+        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, env)
+        )
+        self.process_thread.daemon = True
+        self.process_thread.start()
+
+    def run_process(self, cmd, env=None):
+        try:
+            self.process = subprocess.Popen(
+                cmd,
+                stdout=subprocess.PIPE,
+                stderr=subprocess.STDOUT,
+                text=True,
+                bufsize=1,
+                universal_newlines=True,
+                env=env
+            )
+            
+            # 读取输出
+            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)
+        # 确保.env文件存在
+        ensure_env_file()
+        # 初始化account_lists字典
+        self.account_lists = {}
+        self.init_ui()
+
+    def init_ui(self):
+        layout = QVBoxLayout()
+        
+        # 创建平台选择标签页
+        self.platform_tabs = QTabWidget()
+        
+        # 平台配置
+        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_pages = {}
+        for platform in self.platform_configs.keys():
+            page = QWidget()
+            page_layout = QVBoxLayout()
+            
+            # 账户管理区域
+            account_group = QGroupBox("账户管理")
+            account_layout = QVBoxLayout()
+            
+            # 账户列表
+            accounts_list_layout = QHBoxLayout()
+            
+            # 左侧:账户列表
+            account_list = QListWidget()
+            account_list.setMinimumWidth(200)
+            account_list.itemClicked.connect(lambda item, p=platform: self.select_account(p, item))
+            account_list.setStyleSheet("""
+                QListWidget::item:selected { background-color: #a6d8ff; }
+                QListWidget::item[enabled="true"] { font-weight: bold; color: #0066cc; }
+            """)
+            accounts_list_layout.addWidget(account_list)
+            self.account_lists[platform] = account_list
+            
+            # 右侧:账户详情
+            account_details = QWidget()
+            account_form = QFormLayout()
+            account_details.setLayout(account_form)
+            accounts_list_layout.addWidget(account_details, 1)
+            
+            account_layout.addLayout(accounts_list_layout)
+            
+            # 账户操作按钮
+            buttons_layout = QHBoxLayout()
+            add_btn = QPushButton("添加账户")
+            add_btn.clicked.connect(lambda checked=False, p=platform: self.add_account(p))
+            delete_btn = QPushButton("删除账户")
+            delete_btn.clicked.connect(lambda checked=False, p=platform: self.delete_account(p))
+            enable_btn = QPushButton("设为启用")
+            enable_btn.clicked.connect(lambda checked=False, p=platform: self.enable_account(p))
+            
+            buttons_layout.addWidget(add_btn)
+            buttons_layout.addWidget(delete_btn)
+            buttons_layout.addWidget(enable_btn)
+            buttons_layout.addStretch()
+            
+            account_layout.addLayout(buttons_layout)
+            account_group.setLayout(account_layout)
+            page_layout.addWidget(account_group)
+            
+            # 保存页面引用
+            self.platform_pages[platform] = {
+                "page": page,
+                "form": account_form,
+                "details": account_details
+            }
+            
+            page.setLayout(page_layout)
+            self.platform_tabs.addTab(page, platform.capitalize())
+        
+        layout.addWidget(self.platform_tabs)
+        
+        # 保存按钮
+        self.save_btn = QPushButton("保存设置")
+        self.save_btn.clicked.connect(self.save_settings)
+        layout.addWidget(self.save_btn, alignment=Qt.AlignCenter)
+        
+        self.setLayout(layout)
+        
+        # 加载设置
+        try:
+            self.load_settings()
+        except Exception as e:
+            print(f"加载设置时出错: {e}")
+            import traceback
+            traceback.print_exc()
+        
+        # 连接标签页变更事件
+        self.platform_tabs.currentChanged.connect(self.tab_changed)
+
+    def tab_changed(self, index):
+        try:
+            # 使用有序字典确保顺序一致性
+            platforms = list(self.platform_configs.keys())
+            if index < 0 or index >= len(platforms):
+                return
+            platform = platforms[index]
+            self.update_account_details(platform)
+        except Exception as e:
+            print(f"Error in tab_changed: {e}")
+
+    def load_settings(self):
+        # 读取.env文件
+        try:
+            env_file = find_dotenv()
+            if not env_file:
+                print("找不到.env文件,创建一个空的")
+                env_file = ensure_env_file()
+                
+            env_values = dotenv_values(env_file)
+            print(f"成功读取.env文件: {env_file}")
+            
+            # 为每个平台加载账户
+            for platform in self.platform_configs.keys():
+                if platform not in self.account_lists:
+                    print(f"警告: 找不到平台 {platform} 的账户列表")
+                    continue
+                    
+                account_list = self.account_lists[platform]
+                account_list.clear()
+                
+                # 查找该平台的所有账户
+                accounts = self.get_platform_accounts(platform, env_values)
+                
+                # 添加到列表
+                for account in accounts:
+                    item = QListWidgetItem(account)
+                    # 标记默认账户
+                    if account == "default":
+                        item.setData(Qt.UserRole, "default")
+                        # 检查是否有其他账户被设为启用
+                        is_enabled = True
+                        for other_account in accounts:
+                            if other_account != "default" and self.is_account_enabled(platform, other_account, env_values):
+                                is_enabled = False
+                                break
+                        item.setData(Qt.UserRole + 1, is_enabled)
+                    else:
+                        item.setData(Qt.UserRole, account)
+                        item.setData(Qt.UserRole + 1, self.is_account_enabled(platform, account, env_values))
+                    
+                    # 设置启用状态的显示
+                    if item.data(Qt.UserRole + 1):
+                        item.setData(Qt.UserRole + 2, "true")
+                        item.setText(f"{account} (启用中)")
+                    
+                    account_list.addItem(item)
+                
+                # 选择第一个账户
+                if account_list.count() > 0:
+                    account_list.setCurrentRow(0)
+                    self.select_account(platform, account_list.item(0))
+        except Exception as e:
+            print(f"加载设置时出错: {e}")
+            import traceback
+            traceback.print_exc()
+
+    def is_account_enabled(self, platform, account, env_values=None):
+        """检查账户是否被设为启用"""
+        if env_values is None:
+            env_values = dotenv_values(find_dotenv())
+        
+        if account == "default":
+            # 默认账户,检查是否有其他账户被设为启用
+            for key in env_values.keys():
+                if key.startswith(f"{platform}_") and "_enabled" in key and env_values[key].lower() == "true":
+                    return False
+            return True
+        else:
+            # 其他账户,检查是否有enabled标记
+            return env_values.get(f"{platform}_{account}_enabled", "").lower() == "true"
+
+    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" and account_name != "":
+                    accounts.add(account_name)
+        
+        return sorted(list(accounts))
+
+    def select_account(self, platform, item):
+        if not item:
+            return
+        
+        account = item.data(Qt.UserRole)
+        form = self.platform_pages[platform]["form"]
+        
+        # 清空表单
+        while form.rowCount() > 0:
+            form.removeRow(0)
+        
+        # 读取账户配置
+        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()
+            
+            # 选择新账户
+            account_list = self.account_lists[platform]
+            for i in range(account_list.count()):
+                if account_list.item(i).data(Qt.UserRole) == account_name:
+                    account_list.setCurrentRow(i)
+                    self.select_account(platform, account_list.item(i))
+                    break
+
+    def delete_account(self, platform):
+        account_list = self.account_lists[platform]
+        current_item = account_list.currentItem()
+        
+        if not current_item:
+            return
+            
+        account = current_item.data(Qt.UserRole)
+        
+        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):
+        account_list = self.account_lists[platform]
+        current_item = account_list.currentItem()
+        
+        if not current_item:
+            return
+            
+        account = current_item.data(Qt.UserRole)
+        
+        # 读取账户配置
+        env_file = find_dotenv()
+        env_values = dotenv_values(env_file)
+        
+        # 先清除所有账户的启用状态
+        for key in list(env_values.keys()):
+            if key.startswith(f"{platform}_") and key.endswith("_enabled"):
+                del env_values[key]
+        
+        # 设置当前账户为启用
+        if account != "default":
+            set_key(env_file, f"{platform}_{account}_enabled", "true")
+            
+            # 将账户配置复制到默认配置
+            account_config = {}
+            prefix = f"{platform}_{account}_"
+            for key, value in env_values.items():
+                if key.startswith(prefix):
+                    field = key[len(prefix):]
+                    if field != "enabled":  # 不复制enabled标记
+                        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 for {platform}."
+        )
+        
+        # 重新加载设置
+        self.load_settings()
+
+    def update_account_details(self, platform):
+        account_list = self.account_lists[platform]
+        current_item = account_list.currentItem()
+        
+        if current_item:
+            self.select_account(platform, current_item)
+
+    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')
+        
+        # 保存当前显示的账户配置
+        platform = list(self.platform_configs.keys())[self.platform_tabs.currentIndex()]
+        account_list = self.account_lists[platform]
+        current_item = account_list.currentItem()
+        
+        if current_item and hasattr(self, 'field_widgets'):
+            account = current_item.data(Qt.UserRole)
+            
+            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!")
+        
+        # 如果修改的是启用的账户,更新默认配置
+        if current_item and current_item.data(Qt.UserRole + 1):
+            self.enable_account(platform)
+
+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():
+    """GUI主入口函数"""
+    if not HAS_QT:
+        print("PyQt5 not installed. Please install it with: pip install PyQt5")
+        print("Running in fallback mode...")
+        # 这里可以添加一个简单的命令行界面作为后备
+        return
+        
+    try:
+        # 确保.env文件存在
+        ensure_env_file()
+        
+        app = QApplication(sys.argv)
+        window = RepoSyncMainWindow()
+        window.show()
+        sys.exit(app.exec_())
+    except Exception as e:
+        print(f"Error starting GUI: {str(e)}")
+        import traceback
+        traceback.print_exc()
+
+if __name__ == '__main__':
+    main() 

+ 5 - 2
repo_sync/__init__.py

@@ -1,6 +1,7 @@
 from .repo_sync import RepoSync
 from .version import __version__
 from .options import parser_args
+from .utils.logger import logger
 import sys
 
 def main(argv=None):
@@ -8,7 +9,7 @@ def main(argv=None):
     try:
         args = parser_args()
         if args.get('version'):
-            print(__version__)
+            logger.info(__version__)
             sys.exit(0)
         if args.get('command', '') == '':
             # logging.error("command is empty")
@@ -17,4 +18,6 @@ def main(argv=None):
         rs = RepoSync(args)
         rs.run()
     except KeyboardInterrupt:
-        sys.exit('\nERROR: Interrupted by user')
+        logger.error('ERROR: Interrupted by user')
+        sys.exit(1)
+

+ 752 - 0
repo_sync/gui_main.py

@@ -0,0 +1,752 @@
+import sys
+import os
+import threading
+import subprocess
+try:
+    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, QListWidget, QListWidgetItem
+    )
+    from PyQt5.QtCore import Qt, pyqtSignal, QObject
+    HAS_QT = True
+except ImportError:
+    HAS_QT = False
+    print("PyQt5 not installed, GUI mode not available")
+
+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)
+        
+        # 找出启用的账户
+        enabled_account = "default"
+        env_values = dotenv_values(find_dotenv())
+        for account in accounts:
+            if account != "default" and env_values.get(f"{platform}_{account}_enabled", "").lower() == "true":
+                enabled_account = account
+                break
+        
+        # 添加账户到下拉框,启用的账户放在最前面
+        if enabled_account in accounts:
+            accounts.remove(enabled_account)
+            self.account_combo.addItem(f"{enabled_account} (启用中)")
+            
+        for account in accounts:
+            self.account_combo.addItem(account)
+        
+        # 默认选择启用的账户
+        self.account_combo.setCurrentIndex(0)
+
+    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" and account_name != "":
+                    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_text = self.account_combo.currentText()
+        account = account_text.split(" (")[0] if " (" in account_text else account_text
+        
+        # 清空结果区域
+        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])
+        
+        # 如果不是默认账户,需要设置环境变量
+        env = os.environ.copy()
+        if account != "default":
+            # 读取账户配置
+            env_values = dotenv_values(find_dotenv())
+            for key, value in env_values.items():
+                if key.startswith(f"{pf}_{account}_"):
+                    field = key[len(f"{pf}_{account}_"):]
+                    env[f"{pf}_{field}"] = value
+        
+        # 执行命令
+        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, env)
+        )
+        self.process_thread.daemon = True
+        self.process_thread.start()
+
+    def run_process(self, cmd, env=None):
+        try:
+            self.process = subprocess.Popen(
+                cmd,
+                stdout=subprocess.PIPE,
+                stderr=subprocess.STDOUT,
+                text=True,
+                bufsize=1,
+                universal_newlines=True,
+                env=env
+            )
+            
+            # 读取输出
+            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()
+
+    def init_ui(self):
+        layout = QVBoxLayout()
+        
+        # 创建平台选择标签页
+        self.platform_tabs = QTabWidget()
+        
+        # 平台配置
+        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_pages = {}
+        for platform in self.platform_configs.keys():
+            page = QWidget()
+            page_layout = QVBoxLayout()
+            
+            # 账户管理区域
+            account_group = QGroupBox("账户管理")
+            account_layout = QVBoxLayout()
+            
+            # 账户列表
+            accounts_list_layout = QHBoxLayout()
+            
+            # 左侧:账户列表
+            self.account_lists = {}
+            account_list = QListWidget()
+            account_list.setMinimumWidth(200)
+            account_list.itemClicked.connect(lambda item, p=platform: self.select_account(p, item))
+            account_list.setStyleSheet("""
+                QListWidget::item:selected { background-color: #a6d8ff; }
+                QListWidget::item[enabled="true"] { font-weight: bold; color: #0066cc; }
+            """)
+            accounts_list_layout.addWidget(account_list)
+            self.account_lists[platform] = account_list
+            
+            # 右侧:账户详情
+            account_details = QWidget()
+            account_form = QFormLayout()
+            account_details.setLayout(account_form)
+            accounts_list_layout.addWidget(account_details, 1)
+            
+            account_layout.addLayout(accounts_list_layout)
+            
+            # 账户操作按钮
+            buttons_layout = QHBoxLayout()
+            add_btn = QPushButton("添加账户")
+            add_btn.clicked.connect(lambda checked=False, p=platform: self.add_account(p))
+            delete_btn = QPushButton("删除账户")
+            delete_btn.clicked.connect(lambda checked=False, p=platform: self.delete_account(p))
+            enable_btn = QPushButton("设为启用")
+            enable_btn.clicked.connect(lambda checked=False, p=platform: self.enable_account(p))
+            
+            buttons_layout.addWidget(add_btn)
+            buttons_layout.addWidget(delete_btn)
+            buttons_layout.addWidget(enable_btn)
+            buttons_layout.addStretch()
+            
+            account_layout.addLayout(buttons_layout)
+            account_group.setLayout(account_layout)
+            page_layout.addWidget(account_group)
+            
+            # 保存页面引用
+            self.platform_pages[platform] = {
+                "page": page,
+                "form": account_form,
+                "details": account_details
+            }
+            
+            page.setLayout(page_layout)
+            self.platform_tabs.addTab(page, platform.capitalize())
+        
+        layout.addWidget(self.platform_tabs)
+        
+        # 保存按钮
+        self.save_btn = QPushButton("保存设置")
+        self.save_btn.clicked.connect(self.save_settings)
+        layout.addWidget(self.save_btn, alignment=Qt.AlignCenter)
+        
+        self.setLayout(layout)
+        
+        # 加载设置
+        self.load_settings()
+        
+        # 连接标签页变更事件
+        self.platform_tabs.currentChanged.connect(self.tab_changed)
+
+    def tab_changed(self, index):
+        try:
+            # 使用有序字典确保顺序一致性
+            platforms = list(self.platform_configs.keys())
+            if index < 0 or index >= len(platforms):
+                return
+            platform = platforms[index]
+            self.update_account_details(platform)
+        except Exception as e:
+            print(f"Error in tab_changed: {e}")
+
+    def load_settings(self):
+        # 读取.env文件
+        env_values = dotenv_values(find_dotenv())
+        
+        # 为每个平台加载账户
+        for platform in self.platform_configs.keys():
+            account_list = self.account_lists[platform]
+            account_list.clear()
+            
+            # 查找该平台的所有账户
+            accounts = self.get_platform_accounts(platform, env_values)
+            
+            # 添加到列表
+            for account in accounts:
+                item = QListWidgetItem(account)
+                # 标记默认账户
+                if account == "default":
+                    item.setData(Qt.UserRole, "default")
+                    # 检查是否有其他账户被设为启用
+                    is_enabled = True
+                    for other_account in accounts:
+                        if other_account != "default" and self.is_account_enabled(platform, other_account, env_values):
+                            is_enabled = False
+                            break
+                    item.setData(Qt.UserRole + 1, is_enabled)
+                else:
+                    item.setData(Qt.UserRole, account)
+                    item.setData(Qt.UserRole + 1, self.is_account_enabled(platform, account, env_values))
+                
+                # 设置启用状态的显示
+                if item.data(Qt.UserRole + 1):
+                    item.setData(Qt.UserRole + 2, "true")
+                    item.setText(f"{account} (启用中)")
+                
+                account_list.addItem(item)
+            
+            # 选择第一个账户
+            if account_list.count() > 0:
+                account_list.setCurrentRow(0)
+                self.select_account(platform, account_list.item(0))
+
+    def is_account_enabled(self, platform, account, env_values=None):
+        """检查账户是否被设为启用"""
+        if env_values is None:
+            env_values = dotenv_values(find_dotenv())
+        
+        if account == "default":
+            # 默认账户,检查是否有其他账户被设为启用
+            for key in env_values.keys():
+                if key.startswith(f"{platform}_") and "_enabled" in key and env_values[key].lower() == "true":
+                    return False
+            return True
+        else:
+            # 其他账户,检查是否有enabled标记
+            return env_values.get(f"{platform}_{account}_enabled", "").lower() == "true"
+
+    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" and account_name != "":
+                    accounts.add(account_name)
+        
+        return sorted(list(accounts))
+
+    def select_account(self, platform, item):
+        if not item:
+            return
+        
+        account = item.data(Qt.UserRole)
+        form = self.platform_pages[platform]["form"]
+        
+        # 清空表单
+        while form.rowCount() > 0:
+            form.removeRow(0)
+        
+        # 读取账户配置
+        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()
+            
+            # 选择新账户
+            account_list = self.account_lists[platform]
+            for i in range(account_list.count()):
+                if account_list.item(i).data(Qt.UserRole) == account_name:
+                    account_list.setCurrentRow(i)
+                    self.select_account(platform, account_list.item(i))
+                    break
+
+    def delete_account(self, platform):
+        account_list = self.account_lists[platform]
+        current_item = account_list.currentItem()
+        
+        if not current_item:
+            return
+            
+        account = current_item.data(Qt.UserRole)
+        
+        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):
+        account_list = self.account_lists[platform]
+        current_item = account_list.currentItem()
+        
+        if not current_item:
+            return
+            
+        account = current_item.data(Qt.UserRole)
+        
+        # 读取账户配置
+        env_file = find_dotenv()
+        env_values = dotenv_values(env_file)
+        
+        # 先清除所有账户的启用状态
+        for key in list(env_values.keys()):
+            if key.startswith(f"{platform}_") and key.endswith("_enabled"):
+                del env_values[key]
+        
+        # 设置当前账户为启用
+        if account != "default":
+            set_key(env_file, f"{platform}_{account}_enabled", "true")
+            
+            # 将账户配置复制到默认配置
+            account_config = {}
+            prefix = f"{platform}_{account}_"
+            for key, value in env_values.items():
+                if key.startswith(prefix):
+                    field = key[len(prefix):]
+                    if field != "enabled":  # 不复制enabled标记
+                        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 for {platform}."
+        )
+        
+        # 重新加载设置
+        self.load_settings()
+
+    def update_account_details(self, platform):
+        account_list = self.account_lists[platform]
+        current_item = account_list.currentItem()
+        
+        if current_item:
+            self.select_account(platform, current_item)
+
+    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')
+        
+        # 保存当前显示的账户配置
+        platform = list(self.platform_configs.keys())[self.platform_tabs.currentIndex()]
+        account_list = self.account_lists[platform]
+        current_item = account_list.currentItem()
+        
+        if current_item and hasattr(self, 'field_widgets'):
+            account = current_item.data(Qt.UserRole)
+            
+            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!")
+        
+        # 如果修改的是启用的账户,更新默认配置
+        if current_item and current_item.data(Qt.UserRole + 1):
+            self.enable_account(platform)
+
+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():
+    """GUI主入口函数"""
+    if not HAS_QT:
+        print("PyQt5 not installed. Please install it with: pip install PyQt5")
+        print("Running in fallback mode...")
+        # 这里可以添加一个简单的命令行界面作为后备
+        return
+        
+    try:
+        app = QApplication(sys.argv)
+        window = RepoSyncMainWindow()
+        window.show()
+        sys.exit(app.exec_())
+    except Exception as e:
+        print(f"Error starting GUI: {str(e)}")
+        import traceback
+        traceback.print_exc()
+
+if __name__ == '__main__':
+    main() 

+ 2 - 0
repo_sync/options.py

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

+ 14 - 15
repo_sync/platform/aliyun.py

@@ -9,14 +9,13 @@ read docs:
 https://help.aliyun.com/document_detail/460450.html?spm=a2c4g.460449.0.0.4cc62367VCclNI
 '''
 
-from traceback import print_tb
 from repo_sync.platform.base_platform import BasePlatform
 import csv,subprocess
 import os
 from repo_sync.models import Repo
 from repo_sync.utils.colors import bcolors
 import json
-
+from repo_sync.utils.logger import logger
 class AliyunDevOpsIE(BasePlatform):
     """aliyun devops"""
 
@@ -37,7 +36,7 @@ class AliyunDevOpsIE(BasePlatform):
         url = f'{self._api}/codeup/organizations/{self.companyId}/repositories/{repo_name}'
         r = self.sess.get(url)
         if r.status_code != 200:
-            print(f"{bcolors.FAIL}get repo info failed, status code {r.status_code}{bcolors.ENDC}")
+            logger.error(f'get repo info failed, status code {r.status_code}')
             return False
         return True
 
@@ -53,15 +52,15 @@ class AliyunDevOpsIE(BasePlatform):
             r = self.sess.post(url, data=json.dumps(form_data))
             # r = self.sess.post(url, json=json.dumps(form_data))
             if r.status_code != 200:
-                print(f"{bcolors.FAIL}create repo {repo_name} failed, status code {r.status_code}{bcolors.ENDC}")
-                print(f"{bcolors.FAIL}response: {r.text}{bcolors.ENDC}")
+                logger.error(f'create repo {repo_name} failed, status code {r.status_code}')
+                logger.error(f'response: {r.text}')
                 return False
-            print(f"{bcolors.OKGREEN}create repo {repo_name} success{bcolors.ENDC}")
-            print(f"{bcolors.OKGREEN}https://codeup.aliyun.com/{self.companyId}/{repo_name}{bcolors.ENDC}")
+            logger.info(f'create repo {repo_name} success')
+            logger.info(f'https://codeup.aliyun.com/{self.companyId}/{repo_name}')
             return True
         else:
-            print(f"{bcolors.OKGREEN}repo {repo_name} already exists{bcolors.ENDC}")
-            print(f"{bcolors.OKGREEN}https://codeup.aliyun.com/{self.companyId}/{repo_name}{bcolors.ENDC}")
+            logger.info(f'repo {repo_name} already exists')
+            logger.info(f'https://codeup.aliyun.com/{self.companyId}/{repo_name}')
             return True 
 
     def delete(self, repo_name: str):
@@ -71,15 +70,15 @@ class AliyunDevOpsIE(BasePlatform):
 
             response = self.sess.delete(url)
             if response.status_code == 200:
-                print(f"{bcolors.OKGREEN}Project: {repo_name} deleted from aliyun successfully!{bcolors.ENDC}")
+                logger.info(f'Project: {repo_name} deleted from aliyun successfully!')
             else:
-                print(f'{bcolors.FAIL}Failed to delete project: {repo_name} from aliyun. Error {response.status_code}: {response.text}{bcolors.ENDC}')
+                logger.error(f'Failed to delete project: {repo_name} from aliyun. Error {response.status_code}: {response.text}')
     
     def pull(self, local_repo_path: str):
         if local_repo_path[-1] == os.path.sep:
             local_repo_path = local_repo_path[:-1]
         repo_name = local_repo_path.split(os.path.sep)[-1]
-        print(f"{bcolors.OKGREEN}pull repo:{self.username}/{repo_name} from aliyun{bcolors.ENDC}")
+        logger.info(f'pull repo:{self.username}/{repo_name} from aliyun')
         os.chdir(local_repo_path)
 
         os.system('git remote remove origin_aliyun')
@@ -90,14 +89,14 @@ class AliyunDevOpsIE(BasePlatform):
         os.system('git remote remove origin_aliyun')
         os.chdir('..')
         
-        print(bcolors.OKGREEN + 'pull from aliyun success' + bcolors.ENDC)
+        logger.info(f'pull from aliyun success')
 
     def push(self, local_repo_path: str):
         """ push local repo to aliyun"""
         if local_repo_path[-1] == os.path.sep:
             local_repo_path = local_repo_path[:-1]
         repo_name = local_repo_path.split(os.path.sep)[-1]
-        print(bcolors.WARNING + f'push repo:{self.username}/{repo_name} to aliyun' + bcolors.ENDC)
+        logger.info(f'push repo:{self.username}/{repo_name} to aliyun')
         self.create_repo(repo_name)
         os.chdir(local_repo_path)
         os.system('git remote remove origin_aliyun')
@@ -111,7 +110,7 @@ class AliyunDevOpsIE(BasePlatform):
         os.system('git remote remove origin_aliyun')
         os.chdir('..')
         
-        print(bcolors.OKGREEN + 'push to aliyun success' + bcolors.ENDC)
+        logger.info(f'push to aliyun success')
 
 
     def clone(self):

+ 2 - 2
repo_sync/platform/base_platform.py

@@ -1,6 +1,6 @@
 import requests,csv,os
 from repo_sync.models import Repo
-from repo_sync.utils.colors import bcolors
+from repo_sync.utils.logger import logger
 
 class BasePlatform(object):
     """base platform"""
@@ -57,7 +57,7 @@ class BasePlatform(object):
     def save_csv(self):
         with open(self.repo_list_path, 'w', newline='') as f:
             if len(self.repos) == 0:
-                print(f"{bcolors.WARNING}repo list is empty, please delete repo_list.csv and try again{bcolors.ENDC}")
+                logger.warning("repo list is empty, please delete repo_list.csv and try again")
                 return
             writer = csv.DictWriter(f, fieldnames=self.repos[0].__dict__.keys(), lineterminator='\n')
             writer.writeheader()

+ 12 - 13
repo_sync/platform/cnb.py

@@ -9,7 +9,7 @@
 from .base_platform import BasePlatform
 from repo_sync.utils.colors import bcolors
 import os, subprocess,json
-
+from repo_sync.utils.logger import logger
 
 class CnbIE(BasePlatform):
     """ cnb platform """
@@ -33,29 +33,28 @@ class CnbIE(BasePlatform):
              "visibility": "private" if self.repo_private else "public"
              })
             if response.status_code == 201:
-                print(bcolors.OKGREEN + f'create repo {self.group}/{repo_name} success' + bcolors.ENDC)
+                logger.info(f'create repo {self.group}/{repo_name} success')
             else:
-                print(bcolors.FAIL + f'create repo {self.group}/{repo_name} failed. {response.text}' + bcolors.ENDC)
+                logger.error(f'create repo {self.group}/{repo_name} failed. {response.text}')
         else:
-            print(bcolors.WARNING + f'repo {self.group}/{repo_name} already exists' + bcolors.ENDC)
+            logger.warning(f'repo {self.group}/{repo_name} already exists')
 
     def delete(self, repo_name: str):
         """ delete a repo """
         url = f'{self._api}/{self.group}/{repo_name}'
         response = self.sess.delete(url)
         if response.status_code == 200:
-            print(bcolors.OKGREEN + f'delete repo {self.group}/{repo_name} success' + bcolors.ENDC)
+            logger.info(f'delete repo {self.group}/{repo_name} success')
         else:
-            print(bcolors.FAIL + f'delete repo {self.group}/{repo_name} failed. {response.text}' + bcolors.ENDC)
+            logger.error(f'delete repo {self.group}/{repo_name} failed. {response.text}')
             return
-        print(bcolors.OKGREEN + f'create repo {self.group}/{repo_name} success' + bcolors.ENDC)
-        print(f'{bcolors.OKGREEN}{self._host}/{self.group}/{repo_name}{bcolors.ENDC}')
+        logger.info(f'{self._host}/{self.group}/{repo_name}')
             
     def push(self, local_repo_path: str):
         if local_repo_path[-1] == os.path.sep:
             local_repo_path = local_repo_path[:-1]
         repo_name = local_repo_path.split(os.path.sep)[-1]
-        print(bcolors.WARNING + f'push repo:{self.group}/{repo_name} to cnb' + bcolors.ENDC)
+        logger.info(f'push repo:{self.group}/{repo_name} to cnb')
         self.create_repo(repo_name)
         os.chdir(local_repo_path)
         os.system('git remote remove origin_cnb')
@@ -69,13 +68,13 @@ class CnbIE(BasePlatform):
         os.system('git remote remove origin_cnb')
         os.chdir('..')
         
-        print(bcolors.OKGREEN + 'push to cnb success' + bcolors.ENDC)
+        logger.info(f'push to cnb success')
 
     def pull(self, local_repo_path: str):
         if local_repo_path[-1] == os.path.sep:
             local_repo_path = local_repo_path[:-1]
         repo_name = local_repo_path.split(os.path.sep)[-1]
-        print(bcolors.WARNING + f'pull repo:{self.group}/{repo_name} from cnb' + bcolors.ENDC)
+        logger.info(f'pull repo:{self.group}/{repo_name} from cnb')
         
         os.chdir(local_repo_path)
         os.system('git remote remove origin_cnb')
@@ -87,7 +86,7 @@ class CnbIE(BasePlatform):
         os.system('git remote remove origin_cnb')
         os.chdir('..')
         
-        print(bcolors.OKGREEN + 'pull from cnb success' + bcolors.ENDC)
+        logger.info(f'pull from cnb success')
     
 
     def _repo_exists(self, repo_name: str) -> bool:
@@ -96,7 +95,7 @@ class CnbIE(BasePlatform):
         try:
             response = self.sess.get(url)
             if response.status_code == 200:
-                print(f'{bcolors.OKGREEN}repo: {repo_name} is existed. {bcolors.ENDC}')
+                logger.info(f'repo: {repo_name} is existed.')
                 return True
         except Exception as e:
             return False

+ 23 - 23
repo_sync/platform/coding/coding.py

@@ -14,6 +14,7 @@ from repo_sync.platform.base_platform import BasePlatform
 from .project import Project
 from .repo import Repo
 from repo_sync.utils.colors import bcolors
+from repo_sync.utils.logger import logger
 
 class CodingIE(BasePlatform):
     """coding util"""
@@ -44,10 +45,10 @@ class CodingIE(BasePlatform):
         r = self.sess.post(self.url, json=data)
         if r.status_code == 200:
             res_data = r.json()
-            print(bcolors.OKGREEN + 'Create project success' + bcolors.ENDC)
+            logger.info(f'Create project success: {res_data}')
             return True
         else:
-            print(bcolors.FAIL + 'Failed to create project' + bcolors.ENDC)
+            logger.error(f'Failed to create project: {r.text}')
             return False
     
     def delete_project(self):
@@ -58,10 +59,10 @@ class CodingIE(BasePlatform):
         r = self.sess.post(self.url, json=data)
         if r.status_code == 200:
             res_data = r.json()
-            print(bcolors.OKGREEN + 'Delete project success' + bcolors.ENDC)
+            logger.info(f'Delete project success: {res_data}')
             return True
         else:
-            print(bcolors.FAIL + 'Failed to delete project' + bcolors.ENDC)
+            logger.error(f'Failed to delete project: {r.text}')
             return False
     
     def get_project_list(self):
@@ -134,9 +135,9 @@ class CodingIE(BasePlatform):
                         currentPage += 1
                     return DepotList
                 else:
-                    print(bcolors.WARNING + f'Cannot find repo in project {self.project_name}' + bcolors.ENDC)
+                    logger.warning(f'Cannot find repo in project {self.project_name}')
             except Exception as e:
-                print(bcolors.FAIL + str(e) + bcolors.ENDC)
+                logger.error(f'Error retrieving repo list: {str(e)}')
         
     def _repo_exists(self, repo_name: str):
         """get repo list"""
@@ -170,14 +171,14 @@ class CodingIE(BasePlatform):
                             )
                         break
                     if depot is None:
-                        print(bcolors.WARNING + f'Cannot find repo {repo_name} in project {self.project_name}' + bcolors.ENDC)
+                        logger.warning(f'Cannot find repo {repo_name} in project {self.project_name}')
                     else:
-                        print(bcolors.OKGREEN + f'Find repo {repo_name} in project {self.project_name}' + bcolors.ENDC)
+                        logger.info(f'Find repo {repo_name} in project {self.project_name}')
                     return depot
                 else:
-                    print(bcolors.WARNING + f'Cannot find repo {repo_name} in project {self.project_name}' + bcolors.ENDC)
+                    logger.warning(f'Cannot find repo {repo_name} in project {self.project_name}')
             except Exception as e:
-                print(bcolors.FAIL + f'Cannot find repo {repo_name} in project {self.project_name}: {str(e)}' + bcolors.ENDC)
+                logger.error(f'Cannot find repo {repo_name} in project {self.project_name}: {str(e)}')
     
     def get_project_info(self) -> Project:
         data = {
@@ -203,8 +204,8 @@ class CodingIE(BasePlatform):
                     )
                     return project
             except Exception as e:
-                print(bcolors.FAIL + 'Error retrieving project info: ' + str(e) + bcolors.ENDC)
-                print(bcolors.FAIL + str(res_data) + bcolors.ENDC)
+                logger.error(f'Error retrieving project info: {str(e)}')
+                logger.error(f'{res_data}')
     
     def create_repo(self, repo_name: str):
         """create a repo"""
@@ -222,14 +223,14 @@ class CodingIE(BasePlatform):
                 }
                 r = self.sess.post(self.url, json=data)
                 if r.status_code == 200:
-                    print(bcolors.OKGREEN + f'Create repo {repo_name} success' + bcolors.ENDC)
-                    print(bcolors.OKGREEN + f'https://e.coding.net/{self.username}/{self.project_name}/{repo_name}' + bcolors.ENDC)
+                    logger.info(f'Create repo {repo_name} success')
+                    logger.info(f'https://e.coding.net/{self.username}/{self.project_name}/{repo_name}')
                     return True
                 else:
-                    print(bcolors.FAIL + 'Failed to create repo' + bcolors.ENDC)
+                    logger.error(f'Failed to create repo')
                     return False
         else:
-            print(bcolors.FAIL + f"Project: {self.project_name} does not exist, cannot create repo in it." + bcolors.ENDC)
+            logger.error(f"Project: {self.project_name} does not exist, cannot create repo in it.")
     
     def delete(self, repo_name: str):
             """delete a repo"""
@@ -241,7 +242,7 @@ class CodingIE(BasePlatform):
                     }
                 r = self.sess.post(self.url, json=data)
                 if r.status_code == 200:
-                    print(f'{bcolors.OKGREEN}delete repo {repo_name} success{bcolors.ENDC}', data, r.json())
+                    logger.info(f'delete repo {repo_name} success')
                     return True
                 else:
                     return False
@@ -253,7 +254,7 @@ class CodingIE(BasePlatform):
         if local_repo_path[-1] == os.path.sep:
             local_repo_path = local_repo_path[:-1]
         repo_name = local_repo_path.split(os.path.sep)[-1]
-        print(f'{bcolors.OKGREEN} pull repo:{self.username}/{repo_name} from coding{bcolors.ENDC}')
+        logger.info(f'pull repo:{self.username}/{repo_name} from coding')
         os.chdir(local_repo_path)
         try:
             os.system('git remote remove origin_coding')
@@ -268,7 +269,7 @@ class CodingIE(BasePlatform):
         os.system(f'git pull origin_coding {current_branch}')
         os.system('git remote remove origin_coding')
         os.chdir('..')
-        print(f'{bcolors.OKGREEN}pull from coding success{bcolors.ENDC}')
+        logger.info(f'pull from coding success')
 
     def push(self, local_repo_path: str):
         ''' push a local repo to remote
@@ -279,7 +280,7 @@ class CodingIE(BasePlatform):
         if local_repo_path[-1] == os.path.sep:
             local_repo_path = local_repo_path[:-1]
         repo_name = local_repo_path.split(os.path.sep)[-1]
-        print(f'{bcolors.OKGREEN}push repo:{self.username}/{repo_name} to coding{bcolors.ENDC}')
+        logger.info(f'push repo:{self.username}/{repo_name} to coding')
         self.create_repo(repo_name=repo_name)
         os.chdir(local_repo_path)
 
@@ -295,7 +296,7 @@ class CodingIE(BasePlatform):
         os.system(f'git push -u origin_coding {current_branch}')
         os.system('git remote remove origin_coding')
         os.chdir('..')
-        print(f'{bcolors.OKGREEN}push to coding success{bcolors.ENDC}')
+        logger.info(f'push to coding success')
 
     def clone(self, repo_path: str):
         ''' clone all repo from remote
@@ -305,9 +306,8 @@ class CodingIE(BasePlatform):
         for repo in repos:
             try:
                 cmd = f'git clone https://{self.username}:{self.token}@e.coding.net/{self.username}/{self.project_name}/{repo["Name"]}.git {repo_path}/{repo["Name"]}'
-                # print(cmd)
                 os.system(cmd)
-                print(f'{bcolors.OKGREEN}clone success{bcolors.ENDC}')
+                logger.info(f'clone success')
             except Exception as e:
                 pass
 

+ 11 - 11
repo_sync/platform/gitcode.py

@@ -14,7 +14,7 @@ api 和 gitcode 类似
 from .base_platform import BasePlatform
 from repo_sync.utils.colors import bcolors
 import os, subprocess,json
-
+from repo_sync.utils.logger import logger
 class GitcodeIE(BasePlatform):
     """ gitcode platform """
     
@@ -45,19 +45,19 @@ class GitcodeIE(BasePlatform):
             # r = self.sess.post(url, params=json.dumps(form_data))
             r = self.sess.post(url, data=json.dumps(form_data))
             if r.status_code != 200:
-                print(bcolors.FAIL + f'create repo {repo_name} failed, status code {r.status_code}' + bcolors.ENDC)
+                logger.error(f'create repo {repo_name} failed, status code {r.status_code}')
                 return
-            print(bcolors.OKGREEN + f'create repo {repo_name} success' + bcolors.ENDC)
-            print(f'{bcolors.OKGREEN}{self._host}/{self.username}/{repo_name}{bcolors.ENDC}')
+            logger.info(f'create repo {repo_name} success')
+            logger.info(f'{self._host}/{self.username}/{repo_name}')
             
     def delete(self, repo_name: str):
         """ delete a repo """
         url = f'{self._api}/repos/{self.username}/{repo_name}'
         response = self.sess.delete(url)
         if response.status_code == 204:
-            print(bcolors.OKBLUE + f'Repository: {repo_name} deleted from gitcode successfully!' + bcolors.ENDC)
+            logger.info(f'Repository: {repo_name} deleted from gitcode successfully!')
         else:
-            print(bcolors.FAIL + f'Failed to delete repository: {repo_name} from gitcode. Error {response.status_code}: {response.text}' + bcolors.ENDC)
+            logger.error(f'Failed to delete repository: {repo_name} from gitcode. Error {response.status_code}: {response.text}')
     
 
     def get_repo_list(self) -> list:
@@ -71,7 +71,7 @@ class GitcodeIE(BasePlatform):
         if local_repo_path[-1] == os.path.sep:
             local_repo_path = local_repo_path[:-1]
         repo_name = local_repo_path.split(os.path.sep)[-1]
-        print(bcolors.WARNING + f'push repo:{self.username}/{repo_name} to gitcode' + bcolors.ENDC)
+        logger.info(f'push repo:{self.username}/{repo_name} to gitcode')
         self.create_repo(repo_name)
         os.chdir(local_repo_path)
         os.system('git remote remove origin_gitcode')
@@ -85,13 +85,13 @@ class GitcodeIE(BasePlatform):
         os.system('git remote remove origin_gitcode')
         os.chdir('..')
         
-        print(bcolors.OKGREEN + 'push to gitcode success' + bcolors.ENDC)
+        logger.info(f'push to gitcode success')
 
     def pull(self, local_repo_path: str):
         if local_repo_path[-1] == os.path.sep:
             local_repo_path = local_repo_path[:-1]
         repo_name = local_repo_path.split(os.path.sep)[-1]
-        print(bcolors.WARNING + f'pull repo:{self.username}/{repo_name} from gitcode' + bcolors.ENDC)
+        logger.info(f'pull repo:{self.username}/{repo_name} from gitcode')
         
         os.chdir(local_repo_path)
         os.system('git remote remove origin_gitcode')
@@ -103,7 +103,7 @@ class GitcodeIE(BasePlatform):
         os.system('git remote remove origin_gitcode')
         os.chdir('..')
         
-        print(bcolors.OKGREEN + 'pull from gitcode success' + bcolors.ENDC)
+        logger.info(f'pull from gitcode success')
     
     def _repo_exists(self, repo_name: str) -> bool:
         """ check if repo exists """
@@ -111,7 +111,7 @@ class GitcodeIE(BasePlatform):
         try:
             response = self.sess.get(url)
             if response.status_code == 200:
-                print(f'{bcolors.OKGREEN}repo: {repo_name} is existed. {bcolors.ENDC}')
+                logger.info(f'repo: {repo_name} is existed.')
                 return True
         except Exception as e:
             return False

+ 12 - 12
repo_sync/platform/gitee.py

@@ -12,7 +12,7 @@ import csv, subprocess
 import os
 from repo_sync.models import Repo
 from repo_sync.utils.colors import bcolors
-
+from repo_sync.utils.logger import logger
 class GiteeIE(BasePlatform):
     """gitee async"""
     _host = 'https://gitee.com'
@@ -33,19 +33,19 @@ class GiteeIE(BasePlatform):
             }
             r = self.sess.post(url, params=form_data)
             if r.status_code != 201:
-                print(bcolors.FAIL + f'create repo {repo_name} failed, status code {r.status_code}' + bcolors.ENDC)
+                logger.error(f'create repo {repo_name} failed, status code {r.status_code}')
                 return
-            print(bcolors.OKGREEN + f'create repo {repo_name} success' + bcolors.ENDC)
-            print(bcolors.OKGREEN + f'{self._host}/{self.username}/{repo_name}' + bcolors.ENDC)
+            logger.info(f'create repo {repo_name} success')
+            logger.info(f'{self._host}/{self.username}/{repo_name}')
 
     def delete(self, repo_name: str):
         """delete a repo"""
         url = f'{self._api}/repos/{self.username}/{repo_name}'
         response = self.sess.delete(url)
         if response.status_code == 204:
-            print(bcolors.OKBLUE + f'Repository: {repo_name} deleted from gitee successfully!' + bcolors.ENDC)
+            logger.info(f'Repository: {repo_name} deleted from gitee successfully!')
         else:
-            print(bcolors.FAIL + f'Failed to delete repository: {repo_name} from gitee. Error {response.status_code}: {response.text}' + bcolors.ENDC)
+            logger.error(f'Failed to delete repository: {repo_name} from gitee. Error {response.status_code}: {response.text}')
     
     def get_repo_list(self) -> list:
         """get repo list"""
@@ -61,7 +61,7 @@ class GiteeIE(BasePlatform):
         url = f'{self._api}/user/repos'
         r = self.sess.get(url)
         if r.status_code != 200:
-            print(bcolors.FAIL + f'get repo list failed, status code {r.status_code}' + bcolors.ENDC)
+            logger.error(f'get repo list failed, status code {r.status_code}')
             return
         
         repo_list = r.json()
@@ -75,7 +75,7 @@ class GiteeIE(BasePlatform):
         if local_repo_path[-1] == os.path.sep:
             local_repo_path = local_repo_path[:-1]
         repo_name = local_repo_path.split(os.path.sep)[-1]
-        print(bcolors.WARNING + f'pull repo:{self.username}/{repo_name} from gitee' + bcolors.ENDC)
+        logger.info(f'pull repo:{self.username}/{repo_name} from gitee')
         
         os.chdir(local_repo_path)
         os.system('git remote remove origin_gitee')
@@ -87,13 +87,13 @@ class GiteeIE(BasePlatform):
         os.system('git remote remove origin_gitee')
         os.chdir('..')
         
-        print(bcolors.OKGREEN + 'pull from gitee success' + bcolors.ENDC)
+        logger.info(f'pull from gitee success')
     
     def push(self, local_repo_path: str):
         if local_repo_path[-1] == os.path.sep:
             local_repo_path = local_repo_path[:-1]
         repo_name = local_repo_path.split(os.path.sep)[-1]
-        print(bcolors.WARNING + f'push repo:{self.username}/{repo_name} to gitee' + bcolors.ENDC)
+        logger.info(f'push repo:{self.username}/{repo_name} to gitee')
         self.create_repo(repo_name)
         os.chdir(local_repo_path)
         os.system('git remote remove origin_gitee')
@@ -107,7 +107,7 @@ class GiteeIE(BasePlatform):
         os.system('git remote remove origin_gitee')
         os.chdir('..')
         
-        print(bcolors.OKGREEN + 'push to gitee success' + bcolors.ENDC)
+        logger.info(f'push to gitee success')
     
     def _repo_exists(self, repo_name: str):
         """check if a repo exists
@@ -120,7 +120,7 @@ class GiteeIE(BasePlatform):
         try:
             response = self.sess.get(url)
             if response.status_code == 200 and response.json()['message'] != 'Not Found Project':
-                print(f'{bcolors.OKGREEN}repo: {repo_name} is existed. {bcolors.ENDC}')
+                logger.info(f'repo: {repo_name} is existed.')
                 return True
         except Exception as e:
             return False

+ 14 - 14
repo_sync/platform/github.py

@@ -11,7 +11,7 @@ import json
 import csv, subprocess
 from repo_sync.models import Repo
 from .base_platform import BasePlatform
-from repo_sync.utils.colors import bcolors
+from repo_sync.utils.logger import logger
 
 class GithubIE(BasePlatform):
     """github util"""
@@ -37,20 +37,20 @@ class GithubIE(BasePlatform):
             }
             r = self.sess.post(url, data=json.dumps(payload))
             if r.status_code != 201:
-                print(f'{bcolors.FAIL}create repo {repo_name} failed, status code {r.status_code}{bcolors.ENDC}')
+                logger.error(f'create repo {repo_name} failed, status code {r.status_code}')
                 return
-            print(f'{bcolors.OKGREEN}create repo {repo_name} success{bcolors.ENDC}')
-            print(f'{bcolors.OKGREEN}{self._host}/{self.username}/{repo_name}{bcolors.ENDC}')
+            logger.info(f'create repo {repo_name} success')
+            logger.info(f'{self._host}/{self.username}/{repo_name}')
             
     def delete(self, repo_name: str):
         """delete a repo, maybe request a confirm by input"""
         url = f'{self._host}/repos/{self.username}/{repo_name}'
         response = self.sess.delete(url)
         if response.status_code == 204:
-            print(f'{bcolors.OKGREEN}Repository: {repo_name} deleted from github successfully!{bcolors.ENDC}')
+            logger.info(f'Repository: {repo_name} deleted from github successfully!')
         else:
-            print(f'{bcolors.FAIL}Failed to delete repository: {repo_name} from github. Error {response.status_code}: {response.text}{bcolors.ENDC}')
-        print(f'{bcolors.WARNING}delete repo: {repo_name} from github success{bcolors.ENDC}')
+            logger.error(f'Failed to delete repository: {repo_name} from github. Error {response.status_code}: {response.text}')
+        logger.warning(f'delete repo: {repo_name} from github success')
 
     def _repo_exists(self, repo_name: str):
         """check if a repo exists"""
@@ -58,7 +58,7 @@ class GithubIE(BasePlatform):
         try:
             response = self.sess.get(url)
             if response.status_code == 200:
-                print(f'{bcolors.OKGREEN}repo: {repo_name} is existed. {bcolors.ENDC}')
+                logger.info(f'repo: {repo_name} is existed.')
                 return True
         except Exception as e:
             return False
@@ -68,7 +68,7 @@ class GithubIE(BasePlatform):
         if local_repo_path[-1] == os.path.sep:
             local_repo_path = local_repo_path[:-1]
         repo_name = local_repo_path.split(os.path.sep)[-1]
-        print(f'{bcolors.OKGREEN}pull repo: {self.username}/{repo_name} from github{bcolors.ENDC}')
+        logger.info(f'pull repo: {self.username}/{repo_name} from github')
         
         os.chdir(local_repo_path)
         os.system('git remote remove origin_github')
@@ -82,14 +82,14 @@ class GithubIE(BasePlatform):
         os.system(f'git pull origin_github {current_branch}')
         os.system('git remote remove origin_github')
         os.chdir('..')
-        print(f'{bcolors.OKGREEN}pull from github success{bcolors.ENDC}')
+        logger.info(f'pull from github success')
 
     def push(self, local_repo_path: str):
         """push a local repo to remote"""
         if local_repo_path[-1] == os.path.sep:
             local_repo_path = local_repo_path[:-1]
         repo_name = local_repo_path.split(os.path.sep)[-1]
-        print(f'{bcolors.OKGREEN}push repo: {self.username}/{repo_name} to github{bcolors.ENDC}')
+        logger.info(f'push repo: {self.username}/{repo_name} to github')
         self.create_repo(repo_name)
         os.chdir(local_repo_path)
         os.system('git remote remove origin_github')
@@ -105,12 +105,12 @@ class GithubIE(BasePlatform):
         os.system(f'git push -u origin_github {current_branch}')
         os.system('git remote remove origin_github')
         os.chdir('..')
-        print(f'{bcolors.OKGREEN}push to github success{bcolors.ENDC}')
+        logger.info(f'push to github success')
 
     def get_repo_list(self) -> list:
         """get all repo list of a user"""
         if os.path.exists(self.repo_list_path):
-            print(f'{bcolors.OKGREEN}repo is exist, please read from {self.repo_list_path} file{bcolors.ENDC}')
+            logger.info(f'repo is exist, please read from {self.repo_list_path} file')
             with open(self.repo_list_path, 'r', newline='') as csvfile:
                 reader = csv.reader(csvfile)
                 for row in reader:
@@ -123,7 +123,7 @@ class GithubIE(BasePlatform):
             while True:
                 r = self.sess.get(url, params={'type': 'all', 'page': page_num})
                 if r.status_code != 200:
-                    print(f'{bcolors.FAIL}request url {url} failed, status code {r.status_code}{bcolors.ENDC}')
+                    logger.error(f'request url {url} failed, status code {r.status_code}')
                     return
                 repo_list = r.json()
                 for repo in repo_list:

+ 15 - 15
repo_sync/platform/gitlab.py

@@ -13,7 +13,7 @@ import csv
 import subprocess
 from .base_platform import BasePlatform
 from repo_sync.models import Repo
-from repo_sync.utils.colors import bcolors
+from repo_sync.utils.logger import logger
 
 class GitlabIE(BasePlatform):
     """gitlab async"""
@@ -35,10 +35,10 @@ class GitlabIE(BasePlatform):
             }
             r = self.sess.post(url, data=json.dumps(payload))
             if r.status_code != 201:
-                print(f"{bcolors.FAIL}create repo {repo_name} failed, status code {r.status_code}{bcolors.ENDC}")
+                logger.error(f"create repo {repo_name} failed, status code {r.status_code}")
                 return
-            print(f"{bcolors.OKGREEN}create repo {repo_name} success{bcolors.ENDC}")
-            print(f'{bcolors.OKGREEN}{self.host}/{self.username}/{repo_name}{bcolors.ENDC}')
+            logger.info(f"create repo {repo_name} success")
+            logger.info(f'{self.host}/{self.username}/{repo_name}')
             # for repo in self.repos:
             #     if repo.name == repo_name:
             #         repo.url = r.json()["web_url"]
@@ -58,22 +58,22 @@ class GitlabIE(BasePlatform):
                 url = f"{self.host}/api/v4/projects/{project_id}"
                 response = self.sess.delete(url)
                 if response.status_code == 202:
-                    print(f"{bcolors.OKGREEN}Repository: {repo_name} deleted from gitlab successfully!{bcolors.ENDC}")
+                    logger.info(f"Repository: {repo_name} deleted from gitlab successfully!")
                 else:
-                    print(
-                        f"{bcolors.FAIL}Failed to delete repository: {repo_name} from gitlab. Error {response.status_code}: {response.text}{bcolors.ENDC}"
+                    logger.error(
+                        f"Failed to delete repository: {repo_name} from gitlab. Error {response.status_code}: {response.text}"
                     )
             except Exception as e:
-                print(f"{bcolors.FAIL}Failed to delete repository: {repo_name} from gitlab. Error {e}, check repo is exist first.{bcolors.ENDC}")
+                logger.error(f"Failed to delete repository: {repo_name} from gitlab. Error {e}, check repo is exist first.")
         else:
-            print(f"{bcolors.FAIL}Failed to delete repository: {repo_name} from gitlab. Error {r.status_code}: {r.text}{bcolors.ENDC}")
+            logger.error(f"Failed to delete repository: {repo_name} from gitlab. Error {r.status_code}: {r.text}")
     
     def get_repo_list(self, username: str) -> list:
         """get repo list"""
         url = f"{self.host}/api/v4/users/{username}/projects"
         r = self.sess.get(url)
         if r.status_code != 200:
-            print(f"{bcolors.FAIL}get repo list failed, status code {r.status_code}{bcolors.ENDC}")
+            logger.error(f"get repo list failed, status code {r.status_code}")
             return []
         repo_list = []
         for res in r.json():
@@ -94,7 +94,7 @@ class GitlabIE(BasePlatform):
         if local_repo_path[-1] == os.path.sep:
             local_repo_path = local_repo_path[:-1]
         repo_name = local_repo_path.split(os.path.sep)[-1]
-        print(f"{bcolors.OKGREEN}push repo:{self.username}/{repo_name} to gitlab{bcolors.ENDC}")
+        logger.info(f"pull repo:{self.username}/{repo_name} from gitlab")
         self.create_repo(repo_name)
         pur_host = re.search(r'(?<=//)[^/]+', self.host).group()
         os.chdir(local_repo_path)
@@ -108,7 +108,7 @@ class GitlabIE(BasePlatform):
         os.system(f'git pull origin_gitlab {current_branch}')
         os.system("git remote remove origin_gitlab")
         os.chdir("..")
-        print(f"{bcolors.OKGREEN}pull repo:{self.username}/{repo_name} from gitlab success{bcolors.ENDC}")
+        logger.info(f"pull repo:{self.username}/{repo_name} from gitlab success")
     
     def push(self, local_repo_path: str):
         """push a local repo to remote
@@ -118,7 +118,7 @@ class GitlabIE(BasePlatform):
         if local_repo_path[-1] == os.path.sep:
             local_repo_path = local_repo_path[:-1]
         repo_name = local_repo_path.split(os.path.sep)[-1]
-        print(f"{bcolors.OKGREEN}push repo:{self.username}/{repo_name} to gitlab{bcolors.ENDC}")
+        logger.info(f"push repo:{self.username}/{repo_name} to gitlab")
         self.create_repo(repo_name)
         pur_host = re.search(r'(?<=//)[^/]+', self.host).group()
         os.chdir(local_repo_path)
@@ -133,7 +133,7 @@ class GitlabIE(BasePlatform):
         os.system(f"git push -u origin_gitlab {current_branch}")
         os.system("git remote remove origin_gitlab")
         os.chdir("..")
-        print(f"{bcolors.OKGREEN}push repo:{self.username}/{repo_name} to gitlab success{bcolors.ENDC}")
+        logger.info(f"push repo:{self.username}/{repo_name} to gitlab success")
     
     def clone(self):
         pass
@@ -147,7 +147,7 @@ class GitlabIE(BasePlatform):
         if r.status_code == 200:
             try:
                 project_id = r.json()[0]["id"]
-                print(f'{bcolors.OKGREEN}repo: {repo_name} is existed. {bcolors.ENDC}')
+                logger.info(f'repo: {repo_name} is existed.')
                 return True
             except Exception as e:
                 return False

+ 12 - 12
repo_sync/platform/gogs.py

@@ -10,7 +10,7 @@ from .base_platform import BasePlatform
 import csv, re, subprocess
 import json, os
 from repo_sync.utils.colors import bcolors
-
+from repo_sync.utils.logger import logger
 class GogsIE(BasePlatform):
     """ gogs plotform class """
     gityoqi_repo_list = 'gityoqi_repo_list.csv'
@@ -33,10 +33,10 @@ class GogsIE(BasePlatform):
         }
         r = self.sess.post(url, data=json.dumps(payload))
         if r.status_code != 201:
-            print(bcolors.FAIL + f'create org repo {repo_name} failed, status code {r.status_code}' + bcolors.ENDC)
+            logger.error(f'create org repo {repo_name} failed, status code {r.status_code}')
             return
-        print(bcolors.OKGREEN + f'create org repo {repo_name} success' + bcolors.ENDC)
-        print(f'{bcolors.OKGREEN}{self._host}/{org_name}/{repo_name}{bcolors.ENDC}')
+        logger.info(f'create org repo {repo_name} success')
+        logger.info(f'{self._host}/{org_name}/{repo_name}')
         
     def create_repo(self, repo_name: str):
         """create a repo"""
@@ -52,28 +52,28 @@ class GogsIE(BasePlatform):
         }
         r = self.sess.post(url, data=json.dumps(payload))
         if r.status_code != 201:
-            print(bcolors.FAIL + f'create repo {repo_name} failed, status code {r.status_code}' + bcolors.ENDC)
+            logger.error(f'create repo {repo_name} failed, status code {r.status_code}')
             return
-        print(bcolors.OKGREEN + f'create repo {repo_name} success' + bcolors.ENDC)
+        logger.info(f'create repo {repo_name} success')
 
     def delete(self, repo_name: str):
         """delete a repo, maybe request a confirm by input"""
         url = f'{self._host}/api/v1/repos/{self.username}/{repo_name}'
         r = self.sess.delete(url)
         if r.status_code != 204:
-            print(bcolors.FAIL + f'delete repo {repo_name} failed, status code {r.status_code}' + bcolors.ENDC)
+            logger.error(f'delete repo {repo_name} failed, status code {r.status_code}')
             return
-        print(bcolors.OKGREEN + f'delete repo {repo_name} success' + bcolors.ENDC)
+        logger.info(f'delete repo {repo_name} success')
 
     def get_repo_list(self, repo_name: str):
         """get repo list"""
         url = f'{self._host}/api/v1/users/{self.username}/repos'
         r = self.sess.get(url)
         if r.status_code != 200:
-            print(bcolors.FAIL + f'get repo list failed, status code {r.status_code}' + bcolors.ENDC)
+            logger.error(f'get repo list failed, status code {r.status_code}')
             return
         self.repos = r.json()
-        print(bcolors.OKGREEN + 'get repo list success' + bcolors.ENDC)
+        logger.info(f'get repo list success')
 
     def clone(self):
         pass
@@ -82,7 +82,7 @@ class GogsIE(BasePlatform):
         if local_repo_path[-1] == os.path.sep:
             local_repo_path = local_repo_path[:-1]
         repo_name = local_repo_path.split(os.path.sep)[-1]
-        print(f'{bcolors.WARNING}pull repo:{self.username}/{repo_name} from {self._host}{bcolors.ENDC}')
+        logger.info(f'pull repo:{self.username}/{repo_name} from {self._host}')
         pur_host = re.search(r'(?<=//)[^/]+', self._host).group()
         os.chdir(local_repo_path)
         os.system('git remote remove origin_gogs')
@@ -100,7 +100,7 @@ class GogsIE(BasePlatform):
         if local_repo_path[-1] == os.path.sep:
             local_repo_path = local_repo_path[:-1]
         repo_name = local_repo_path.split(os.path.sep)[-1]
-        print(f'{bcolors.WARNING}push repo:{self.username}/{repo_name} to {self._host}{bcolors.ENDC}')
+        logger.info(f'push repo:{self.username}/{repo_name} to {self._host}')
         pur_host = re.search(r'(?<=//)[^/]+', self._host).group()
         os.chdir(local_repo_path)
         os.system('git remote remove origin_gogs')

+ 8 - 24
repo_sync/repo_sync.py

@@ -8,9 +8,9 @@
 '''
 import os,csv,re
 import logging
+from .utils.logger import logger
 from .platform import gen_extractor_classes
 from .models import Repo
-from repo_sync.utils.colors import bcolors
 
 class RepoSync(object):
     '''
@@ -20,11 +20,12 @@ class RepoSync(object):
 
     def __init__(self, params: dict, debug=False):
         self.args = None
-        self.logger = None
-        self.init_logger(debug)
+        
+        if debug:
+            logger.setLevel(logging.DEBUG)
 
         self.params = params
-        self.params['logger'] = self.logger
+        self.params['logger'] = logger
         self.platforms = []
         self.repos = []
         for p in gen_extractor_classes():
@@ -45,7 +46,7 @@ class RepoSync(object):
                     self._find_git_repo(path=current_path, repo_name=dir)
         with open(self.repo_list_path, 'w') as f:
             if len(self.repos) == 0:
-                print(f"{bcolors.WARNING}repo list is empty, please delete repo_list.csv and try again{bcolors.ENDC}")
+                logger.warning("repo list is empty, please delete repo_list.csv and try again")
                 return
             writer = csv.DictWriter(
                 f, fieldnames=self.repos[0].__dict__.keys(), lineterminator='\n'
@@ -68,7 +69,7 @@ class RepoSync(object):
                 repo.local_path = path
                 self.repos.append(repo)
         except Exception as e:
-            print(f"{bcolors.OKGREEN}skip {path} because of {e}{bcolors.ENDC}")
+            logger.info(f"skip {path} because of {e}")
 
     def run(self):
         '''
@@ -103,24 +104,7 @@ class RepoSync(object):
                         elif command == 'pull':
                             current_platform(username, token, host, self.params).pull(repo.local_path)
             else:
-                logging.info(
-                    'repo list is not exist, please run list_local command first'
-                )
-
-    def init_logger(self, debug:bool):
-        '''
-        init logger
-        '''
-        self.logger = logging.getLogger(__name__)
-        if debug:
-            self.logger.setLevel(logging.DEBUG)
-        else:
-            self.logger.setLevel(logging.INFO)
-        formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
-        console_handler = logging.StreamHandler()
-        console_handler.setLevel(logging.DEBUG)
-        console_handler.setFormatter(formatter)
-        self.logger.addHandler(console_handler)
+                logger.info('repo list is not exist, please run list_local command first')
 
     def update(self):
         '''

+ 51 - 0
repo_sync/utils/logger.py

@@ -0,0 +1,51 @@
+import logging
+from termcolor import colored
+
+class ColoredFormatter(logging.Formatter):
+    def format(self, record):
+        # 保留原始消息不变
+        original_msg = record.msg
+        if record.levelno == logging.DEBUG:
+            record.msg = colored(record.msg, 'blue')
+        elif record.levelno == logging.INFO:
+            record.msg = colored(record.msg, 'green')
+        elif record.levelno == logging.WARNING:
+            record.msg = colored(record.msg, 'yellow')
+        elif record.levelno == logging.ERROR:
+            record.msg = colored(record.msg, 'red')
+        elif record.levelno == logging.CRITICAL:
+            record.msg = colored(record.msg, 'magenta')
+        
+        # 调用父类格式化方法
+        formatted_message = super().format(record)
+        
+        # 恢复原始消息
+        record.msg = original_msg
+        
+        return formatted_message
+
+# Configure logger
+logger = logging.getLogger('repo_sync')
+logger.setLevel(logging.INFO)
+
+# Create console handler
+console_handler = logging.StreamHandler()
+console_handler.setLevel(logging.INFO)
+
+# Create file handler
+file_handler = logging.FileHandler('repo_sync.log')
+file_handler.setLevel(logging.INFO)
+
+# Use the custom colored formatter
+console_formatter = ColoredFormatter('%(asctime)s - %(levelname)s - %(message)s')
+console_handler.setFormatter(console_formatter)
+
+file_formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
+file_handler.setFormatter(file_formatter)
+
+# Add handler to logger
+logger.addHandler(console_handler)
+logger.addHandler(file_handler)
+
+# Export logger
+__all__ = ['logger'] 

+ 5 - 0
requirements-win.txt

@@ -0,0 +1,5 @@
+requests==2.27.1
+python-dotenv==1.0.0
+PyQt5
+pywin32
+termcolor==2.3.0

+ 2 - 1
requirements.txt

@@ -1,2 +1,3 @@
 requests==2.27.1
-python-dotenv==1.0.0
+python-dotenv==1.0.0
+termcolor==2.3.0