feat: add modern GUI interface and cross-platform support

- Add PyQt6-based modern GUI
- Add system tray support
- Add dark/light theme support
- Refactor app into service architecture
- Update documentation with GUI features
- Add cross-platform compatibility
This commit is contained in:
zhukang 2025-01-15 21:10:59 +08:00
parent c0b593958d
commit f5849d416d
5 changed files with 313 additions and 116 deletions

View File

@ -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
# 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。

View File

@ -1,3 +1,4 @@
[Settings]
double_click_threshold = 0.3
save_location = F:\BaiduSyncdisk\note\NoteForZhukang\000 inbox
save_location = F:/BaiduSyncdisk/note/NoteForZhukang/000 inbox

View File

@ -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()

View File

@ -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
)

View File

@ -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]