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:
parent
c0b593958d
commit
f5849d416d
@ -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。
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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()
|
||||
147
project/llmclipboard/llmclipboard/gui.py
Normal file
147
project/llmclipboard/llmclipboard/gui.py
Normal 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
|
||||
)
|
||||
@ -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]
|
||||
|
||||
Loading…
Reference in New Issue
Block a user