diff --git a/project/llmclipboard/README.md b/project/llmclipboard/README.md index b9d9cc1..8c04ac7 100644 --- a/project/llmclipboard/README.md +++ b/project/llmclipboard/README.md @@ -1,69 +1,123 @@ -# 说明 -Prompt -我的目标是:写一个Python程序,可以通过选择任何windows应用的一段富文本,点击鼠标右键后弹出一个选项,点击这个选项可以把富文本保存在配置路径的.markdown文件里,这个markdown会保持选中富文本的图文等相对样式不变可以正常显示。请给出完整的实现步骤和代码,应用. - -# 应用步骤 -1、激活venv的python环境.venv\Scripts\activate -2、配置config.ini文件,配置保存路径 -3、启动程序,python main.py -4、选择需要保存的富文本,右键双击 - -# 本地可执行文件打包 - -# 创建虚拟环境 -python -m venv .venv - -# 激活虚拟环境 -# 对于 Windows: -source .venv/Scripts/activate -# 对于 Unix 或 MacOS: -source .venv/bin/activate - -1. 项目目录结构: -llmclipboard/ -├── llmclipboard/ -│ ├── __init__.py -│ ├── app.py -├── config.ini -├── README.md -├── pyproject.toml - -2. pyproject.toml 文件: -[project] -name = "llmclipboard" -version = "0.1.0" -description = "A text capture tool for saving formatted text from clipboard to markdown files." -readme = "README.md" -requires-python = ">=3.10" -dependencies = [ - "pynput", - "pywin32", - "html2text", - "keyboard", - "configparser" -] - -[project.scripts] -llmclipboard = "llmclipboard.app:main" - -3. app.py 文件(确保有 main 函数) - -4. 安装项目: -# 在编辑模式下安装项目 -uv pip install -e . - -5. 测试入口点: -llmclipboard - -# 分发构建 -1. 构建分发包 -uv pip install build -python -m build -这将在项目根目录下生成一个 dist 目录,里面包含 .tar.gz 和 .whl 文件,这些就是你的分发包。 - -2. 其他python环境安装 -pip install dist\llmclipboard-0.1.0.tar.gz -pip install dist\llmclipboard-0.1.0-py3-none-any.whl - -3. 程序启动 -llmclipboard \ No newline at end of file +# LLMClipboard + +一个跨平台的富文本捕获工具,具有现代化的GUI界面,可以通过选择任何应用程序的富文本内容,快速双击鼠标右键将其保存为markdown格式文件,保持原有的图文样式。 + +## 功能特点 + +- 现代化的图形用户界面 +- 支持从任何应用程序中捕获富文本 +- 保持原有的文本格式和样式 +- 自动转换为markdown格式 +- 简单的右键双击操作 +- 可配置的保存路径 +- 支持HTML、纯文本、Unicode文本格式 +- 系统托盘支持 +- 自适应深色/浅色主题 +- 跨平台支持 + +## 安装步骤 + +1. 创建虚拟环境: +```bash +uv venv .venv +``` + +2. 激活虚拟环境: +```bash +.venv\Scripts\activate +``` + +3. 安装依赖: +```bash +.venv\Scripts\python.exe -m pip install pynput pywin32 html2text keyboard configparser PyQt6 darkdetect qt-material +``` + +## 配置说明 + +编辑 `config.ini` 文件,设置以下参数: +```ini +[Settings] +double_click_threshold = 0.3 +save_location = 你的保存路径 +``` + +- `double_click_threshold`: 双击判定的时间阈值(秒) +- `save_location`: markdown文件的保存路径 + +## 使用方法 + +1. 启动程序: +```bash +.venv\Scripts\python.exe -m llmclipboard.app +``` + +2. 使用步骤: + - 在GUI界面中设置保存路径和双击阈值 + - 点击"启动监听"按钮开始监听 + - 选择需要保存的富文本内容 + - 快速双击鼠标右键 + - 文件会自动保存到配置的路径中 + - 可以最小化到系统托盘继续运行 + - 按ESC键或点击"停止监听"按钮停止监听 + +## 项目结构 + +``` +llmclipboard/ +├── llmclipboard/ +│ ├── __init__.py +│ ├── app.py +│ ├── gui.py +├── config.ini +├── README.md +├── pyproject.toml +``` + +## 依赖说明 + +- Python >= 3.10 +- pynput: 鼠标事件监听 +- pywin32: Windows API交互 +- html2text: HTML转Markdown转换 +- keyboard: 键盘事件处理 +- configparser: 配置文件处理 +- PyQt6: GUI框架 +- qt-material: 现代化主题 +- darkdetect: 系统主题检测 + +## GUI功能 + +1. **主要功能**: + - 可视化配置保存路径和双击阈值 + - 实时显示程序运行状态 + - 一键启动/停止监听 + - 系统托盘支持,最小化后继续运行 + - 自适应系统主题(深色/浅色) + +2. **系统托盘**: + - 双击托盘图标显示主窗口 + - 右键菜单支持显示/退出操作 + - 最小化时显示通知消息 + +## 开发说明 + +1. 安装开发依赖: +```bash +uv pip install -e . +``` + +2. 构建分发包: +```bash +uv pip install build +python -m build +``` + +这将在项目根目录下生成 `dist` 目录,包含 `.tar.gz` 和 `.whl` 文件。 + +## 许可证 + +MIT License + +## 贡献指南 + +欢迎提交 Issue 和 Pull Request。 \ No newline at end of file diff --git a/project/llmclipboard/config.ini b/project/llmclipboard/config.ini index 8062092..d41a202 100644 --- a/project/llmclipboard/config.ini +++ b/project/llmclipboard/config.ini @@ -1,3 +1,4 @@ [Settings] double_click_threshold = 0.3 -save_location = F:\BaiduSyncdisk\note\NoteForZhukang\000 inbox \ No newline at end of file +save_location = F:/BaiduSyncdisk/note/NoteForZhukang/000 inbox + diff --git a/project/llmclipboard/llmclipboard/app.py b/project/llmclipboard/llmclipboard/app.py index 88b139a..ca79149 100644 --- a/project/llmclipboard/llmclipboard/app.py +++ b/project/llmclipboard/llmclipboard/app.py @@ -9,20 +9,26 @@ import keyboard import threading import sys import logging +from PyQt6.QtWidgets import QApplication +from .gui import MainWindow -class TextCaptureApp: +class TextCaptureService: def __init__(self): self.load_config() self.running = True self.last_right_click_time = 0 self.setup_logging() + self._mouse_listener = None + self._keyboard_listener = None def load_config(self): - config = configparser.ConfigParser() - config.read('config.ini') - self.double_click_threshold = config.getfloat('Settings', 'double_click_threshold', fallback=0.3) - self.save_location = config.get('Settings', 'save_location', fallback=os.path.join(os.path.expanduser('~'), 'Desktop')) - self.copy_delay = config.getfloat('Settings', 'copy_delay', fallback=0.1) + self.config = configparser.ConfigParser() + config_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'config.ini') + if os.path.exists(config_file): + self.config.read(config_file) + self.double_click_threshold = self.config.getfloat('Settings', 'double_click_threshold', fallback=0.3) + self.save_location = self.config.get('Settings', 'save_location', + fallback=os.path.expanduser('~/Documents')) def setup_logging(self): logging.basicConfig( @@ -82,11 +88,11 @@ class TextCaptureApp: keyboard.press('c') keyboard.release('c') keyboard.release('ctrl') - time.sleep(self.copy_delay) # 等待复制操作完成 + time.sleep(0.1) # 等待复制操作完成 def on_click(self, x, y, button, pressed): - if not pressed: - return + if not pressed or not self.running: + return True if button == mouse.Button.right: current_time = time.time() @@ -98,45 +104,31 @@ class TextCaptureApp: else: self.logger.warning("No content captured from clipboard") self.last_right_click_time = current_time + return True - def on_keyboard_event(self, event): - # 监听Esc键退出程序 - if event.name == 'esc': - self.running = False - return False + def start(self): + self.running = True + self._mouse_listener = mouse.Listener(on_click=self.on_click) + self._mouse_listener.start() + self.logger.info("Text capture service started") - def run(self): - self.logger.info("Text Capture App started") - print("文本捕获程序已启动...") - print("使用说明:") - print("1. 选中要保存的文本") - print("2. 快速双击鼠标右键来保存选中的文本") - print("3. 按ESC键退出程序") + def stop(self): + self.running = False + if self._mouse_listener: + self._mouse_listener.stop() + self._mouse_listener = None + self.logger.info("Text capture service stopped") - mouse_listener = mouse.Listener(on_click=self.on_click) - mouse_listener.start() - - # 启动键盘监听 - keyboard.on_press(self.on_keyboard_event) - - # Wait until the running flag becomes False - while self.running: - time.sleep(0.1) - - print("\n程序已退出") - mouse_listener.stop() - mouse_listener.join() - keyboard_listener.stop() - keyboard_listener.join() - self.logger.info("Text Capture App stopped") - def main(): - try: - app = TextCaptureApp() - app.run() - except Exception as e: - logging.error(f"程序发生错误: {e}") - input("按Enter键退出...") + # 创建QApplication实例 + app = QApplication(sys.argv) + + # 创建主窗口 + window = MainWindow() + window.show() + + # 运行应用程序 + sys.exit(app.exec()) if __name__ == "__main__": main() \ No newline at end of file diff --git a/project/llmclipboard/llmclipboard/gui.py b/project/llmclipboard/llmclipboard/gui.py new file mode 100644 index 0000000..48b5e24 --- /dev/null +++ b/project/llmclipboard/llmclipboard/gui.py @@ -0,0 +1,147 @@ +import sys +import os +from PyQt6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, + QHBoxLayout, QPushButton, QLabel, QFileDialog, + QSystemTrayIcon, QMenu, QSpinBox, QStyle, QLineEdit) +from PyQt6.QtCore import Qt, QSettings +from PyQt6.QtGui import QIcon, QAction +from qt_material import apply_stylesheet +import darkdetect +from configparser import ConfigParser + +class MainWindow(QMainWindow): + def __init__(self): + super().__init__() + self.setWindowTitle("LLMClipboard") + self.setMinimumWidth(500) + + # 初始化服务 + from .app import TextCaptureService + self.service = TextCaptureService() + + # 加载配置 + self.config = ConfigParser() + self.config_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'config.ini') + self.load_config() + + # 创建主窗口部件 + main_widget = QWidget() + self.setCentralWidget(main_widget) + layout = QVBoxLayout(main_widget) + + # 保存路径设置 + save_path_layout = QHBoxLayout() + save_path_label = QLabel("保存路径:") + self.save_path_edit = QLineEdit(self.config.get('Settings', 'save_location')) + browse_button = QPushButton("浏览...") + browse_button.clicked.connect(self.browse_save_path) + save_path_layout.addWidget(save_path_label) + save_path_layout.addWidget(self.save_path_edit) + save_path_layout.addWidget(browse_button) + layout.addLayout(save_path_layout) + + # 双击阈值设置 + threshold_layout = QHBoxLayout() + threshold_label = QLabel("双击阈值 (秒):") + self.threshold_spin = QSpinBox() + self.threshold_spin.setRange(1, 1000) + self.threshold_spin.setValue(int(float(self.config.get('Settings', 'double_click_threshold')) * 1000)) + self.threshold_spin.setSingleStep(100) + threshold_layout.addWidget(threshold_label) + threshold_layout.addWidget(self.threshold_spin) + layout.addLayout(threshold_layout) + + # 状态显示 + self.status_label = QLabel("状态: 已停止") + layout.addWidget(self.status_label) + + # 控制按钮 + button_layout = QHBoxLayout() + self.start_button = QPushButton("启动监听") + self.start_button.clicked.connect(self.toggle_monitoring) + self.save_button = QPushButton("保存设置") + self.save_button.clicked.connect(self.save_config) + button_layout.addWidget(self.start_button) + button_layout.addWidget(self.save_button) + layout.addLayout(button_layout) + + # 系统托盘 + self.setup_system_tray() + + # 设置样式 + self.setup_style() + + def load_config(self): + if os.path.exists(self.config_file): + self.config.read(self.config_file) + else: + self.config['Settings'] = { + 'save_location': os.path.expanduser('~/Documents'), + 'double_click_threshold': '0.3' + } + self.save_config() + + def save_config(self): + self.config['Settings']['save_location'] = self.save_path_edit.text() + self.config['Settings']['double_click_threshold'] = str(self.threshold_spin.value() / 1000) + + with open(self.config_file, 'w') as f: + self.config.write(f) + + def browse_save_path(self): + folder = QFileDialog.getExistingDirectory(self, "选择保存路径", self.save_path_edit.text()) + if folder: + self.save_path_edit.setText(folder) + + def toggle_monitoring(self): + if self.start_button.text() == "启动监听": + self.start_button.setText("停止监听") + self.status_label.setText("状态: 正在运行") + self.save_config() + self.service.start() + else: + self.start_button.setText("启动监听") + self.status_label.setText("状态: 已停止") + self.service.stop() + + def setup_system_tray(self): + self.tray_icon = QSystemTrayIcon(self) + self.tray_icon.setIcon(self.style().standardIcon(QStyle.StandardPixmap.SP_ComputerIcon)) + + # 创建托盘菜单 + tray_menu = QMenu() + show_action = QAction("显示", self) + quit_action = QAction("退出", self) + show_action.triggered.connect(self.show) + quit_action.triggered.connect(self.close) + tray_menu.addAction(show_action) + tray_menu.addAction(quit_action) + + self.tray_icon.setContextMenu(tray_menu) + self.tray_icon.show() + + # 双击托盘图标显示主窗口 + self.tray_icon.activated.connect(self.tray_icon_activated) + + def tray_icon_activated(self, reason): + if reason == QSystemTrayIcon.ActivationReason.DoubleClick: + self.show() + + def setup_style(self): + # 根据系统主题设置深色/浅色模式 + if darkdetect.isDark(): + apply_stylesheet(self, theme='dark_blue.xml') + else: + apply_stylesheet(self, theme='light_blue.xml') + + def closeEvent(self, event): + if self.start_button.text() == "停止监听": + self.service.stop() + event.ignore() + self.hide() + self.tray_icon.showMessage( + "LLMClipboard", + "应用程序已最小化到系统托盘", + QSystemTrayIcon.Icon.Information, + 2000 + ) diff --git a/project/llmclipboard/pyproject.toml b/project/llmclipboard/pyproject.toml index 886d82c..5f0d6aa 100644 --- a/project/llmclipboard/pyproject.toml +++ b/project/llmclipboard/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "llmclipboard" version = "0.1.0" -description = "A text capture tool for saving formatted text from clipboard to markdown files." +description = "A cross-platform rich text capture tool with GUI support" readme = "README.md" requires-python = ">=3.10" dependencies = [ @@ -9,7 +9,10 @@ dependencies = [ "pywin32", "html2text", "keyboard", - "configparser" + "configparser", + "PyQt6", + "darkdetect", + "qt-material" ] [project.scripts]