clipsy 1.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- clipsy-1.5.0/LICENSE +21 -0
- clipsy-1.5.0/PKG-INFO +166 -0
- clipsy-1.5.0/README.md +135 -0
- clipsy-1.5.0/pyproject.toml +69 -0
- clipsy-1.5.0/setup.cfg +4 -0
- clipsy-1.5.0/src/clipsy/__init__.py +2 -0
- clipsy-1.5.0/src/clipsy/__main__.py +26 -0
- clipsy-1.5.0/src/clipsy/app.py +199 -0
- clipsy-1.5.0/src/clipsy/config.py +16 -0
- clipsy-1.5.0/src/clipsy/models.py +26 -0
- clipsy-1.5.0/src/clipsy/monitor.py +153 -0
- clipsy-1.5.0/src/clipsy/redact.py +211 -0
- clipsy-1.5.0/src/clipsy/storage.py +258 -0
- clipsy-1.5.0/src/clipsy/utils.py +72 -0
- clipsy-1.5.0/src/clipsy.egg-info/PKG-INFO +166 -0
- clipsy-1.5.0/src/clipsy.egg-info/SOURCES.txt +23 -0
- clipsy-1.5.0/src/clipsy.egg-info/dependency_links.txt +1 -0
- clipsy-1.5.0/src/clipsy.egg-info/entry_points.txt +2 -0
- clipsy-1.5.0/src/clipsy.egg-info/requires.txt +5 -0
- clipsy-1.5.0/src/clipsy.egg-info/top_level.txt +1 -0
- clipsy-1.5.0/tests/test_app.py +204 -0
- clipsy-1.5.0/tests/test_monitor.py +371 -0
- clipsy-1.5.0/tests/test_redact.py +283 -0
- clipsy-1.5.0/tests/test_storage.py +348 -0
- clipsy-1.5.0/tests/test_utils.py +150 -0
clipsy-1.5.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Brendan Conrad
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
clipsy-1.5.0/PKG-INFO
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: clipsy
|
|
3
|
+
Version: 1.5.0
|
|
4
|
+
Summary: Lightweight clipboard history manager for macOS
|
|
5
|
+
Author-email: Brendan Conrad <brendan.conrad@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/brencon/clipsy
|
|
8
|
+
Project-URL: Repository, https://github.com/brencon/clipsy
|
|
9
|
+
Project-URL: Issues, https://github.com/brencon/clipsy/issues
|
|
10
|
+
Keywords: clipboard,macos,menu-bar,history,productivity
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: MacOS X
|
|
13
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: MacOS
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Desktop Environment
|
|
22
|
+
Classifier: Topic :: Utilities
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Requires-Dist: rumps>=0.4.0
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# Clipsy
|
|
33
|
+
|
|
34
|
+
[](https://pypi.org/project/clipsy/)
|
|
35
|
+
[](https://github.com/brencon/clipsy/actions/workflows/ci.yml)
|
|
36
|
+
[](https://codecov.io/gh/brencon/clipsy)
|
|
37
|
+

|
|
38
|
+

|
|
39
|
+
[](LICENSE)
|
|
40
|
+
|
|
41
|
+
A lightweight clipboard history manager for macOS. Runs as a menu bar icon — no admin privileges, no code signing, no App Store required.
|
|
42
|
+
|
|
43
|
+
## Features
|
|
44
|
+
|
|
45
|
+
- **Clipboard history** — Automatically captures text, images, and file copies
|
|
46
|
+
- **Image thumbnails** — Visual previews for copied images in the menu
|
|
47
|
+
- **Sensitive data masking** — Auto-detects API keys, passwords, SSNs, credit cards, private keys, and tokens; displays masked previews with 🔒 icon
|
|
48
|
+
- **Search** — Full-text search across all clipboard entries (SQLite FTS5)
|
|
49
|
+
- **Click to re-copy** — Click any entry in the menu to put it back on your clipboard
|
|
50
|
+
- **Deduplication** — Copying the same content twice bumps it to the top instead of creating a duplicate
|
|
51
|
+
- **Auto-purge** — Keeps the most recent 500 entries, automatically cleans up old ones
|
|
52
|
+
- **Persistent storage** — History survives app restarts (SQLite database)
|
|
53
|
+
- **Corporate IT friendly** — Runs as a plain Python process, no `.app` bundle or Gatekeeper issues
|
|
54
|
+
|
|
55
|
+
## Requirements
|
|
56
|
+
|
|
57
|
+
- macOS
|
|
58
|
+
- Python 3.10+ (Homebrew recommended: `brew install python3`)
|
|
59
|
+
|
|
60
|
+
## Installation
|
|
61
|
+
|
|
62
|
+
### Via pip (recommended)
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
pip install clipsy
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### From source
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
git clone https://github.com/brencon/clipsy.git
|
|
72
|
+
cd clipsy
|
|
73
|
+
python3 -m venv .venv
|
|
74
|
+
.venv/bin/pip install -e .
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Usage
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
# Run clipsy (a scissors icon appears in your menu bar)
|
|
81
|
+
clipsy
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Then just use your Mac normally. Every time you copy something, it shows up in the Clipsy menu:
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
[✂️ Icon]
|
|
88
|
+
├── Clipsy - Clipboard History
|
|
89
|
+
├── ──────────────────
|
|
90
|
+
├── Search...
|
|
91
|
+
├── ──────────────────
|
|
92
|
+
├── "Meeting notes for Q3 plan..."
|
|
93
|
+
├── "https://github.com/example..."
|
|
94
|
+
├── 🔒 "password=••••••••"
|
|
95
|
+
├── [thumbnail] "[Image: 1920x1080]"
|
|
96
|
+
├── ... (up to 10 items)
|
|
97
|
+
├── ──────────────────
|
|
98
|
+
├── Clear History
|
|
99
|
+
├── ──────────────────
|
|
100
|
+
├── Support Clipsy
|
|
101
|
+
├── ──────────────────
|
|
102
|
+
└── Quit Clipsy
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Auto-Start on Login
|
|
106
|
+
|
|
107
|
+
Run clipsy automatically when you log in — no terminal needed:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
# Install as a LaunchAgent
|
|
111
|
+
scripts/install_launchagent.sh install
|
|
112
|
+
|
|
113
|
+
# Check status
|
|
114
|
+
scripts/install_launchagent.sh status
|
|
115
|
+
|
|
116
|
+
# Remove auto-start
|
|
117
|
+
scripts/install_launchagent.sh uninstall
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Data Storage
|
|
121
|
+
|
|
122
|
+
All data is stored in `~/.local/share/clipsy/`:
|
|
123
|
+
|
|
124
|
+
| File | Purpose |
|
|
125
|
+
|------|---------|
|
|
126
|
+
| `clipsy.db` | SQLite database with clipboard entries |
|
|
127
|
+
| `images/` | Saved clipboard images (PNG files) |
|
|
128
|
+
| `clipsy.log` | Application log |
|
|
129
|
+
|
|
130
|
+
## Development
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
# Install with dev dependencies
|
|
134
|
+
.venv/bin/pip install -e ".[dev]"
|
|
135
|
+
|
|
136
|
+
# Run tests
|
|
137
|
+
.venv/bin/python -m pytest tests/ -v
|
|
138
|
+
|
|
139
|
+
# Run with coverage
|
|
140
|
+
.venv/bin/python -m pytest tests/ --cov=clipsy --cov-report=term-missing
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Architecture
|
|
144
|
+
|
|
145
|
+
```
|
|
146
|
+
NSPasteboard → monitor.py → redact.py → storage.py (SQLite) → app.py (menu bar UI)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
- **`app.py`** — `rumps.App` subclass; renders the menu bar dropdown, handles clicks and search
|
|
150
|
+
- **`monitor.py`** — Polls `NSPasteboard.changeCount()` every 0.5s; detects text, images, and file copies
|
|
151
|
+
- **`storage.py`** — SQLite with FTS5 full-text search, SHA-256 deduplication, auto-purge
|
|
152
|
+
- **`redact.py`** — Sensitive data detection and masking (API keys, passwords, SSN, credit cards, tokens)
|
|
153
|
+
- **`config.py`** — Constants, paths, limits
|
|
154
|
+
- **`models.py`** — `ClipboardEntry` dataclass, `ContentType` enum
|
|
155
|
+
- **`utils.py`** — Hashing, text truncation, PNG dimension parsing, thumbnail generation
|
|
156
|
+
|
|
157
|
+
### Dependencies
|
|
158
|
+
|
|
159
|
+
Only one external dependency:
|
|
160
|
+
|
|
161
|
+
- **`rumps`** — macOS menu bar app framework (brings `pyobjc-framework-Cocoa` transitively)
|
|
162
|
+
- **`sqlite3`** — Built into Python
|
|
163
|
+
|
|
164
|
+
## License
|
|
165
|
+
|
|
166
|
+
MIT License — see [LICENSE](LICENSE) for details.
|
clipsy-1.5.0/README.md
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# Clipsy
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/clipsy/)
|
|
4
|
+
[](https://github.com/brencon/clipsy/actions/workflows/ci.yml)
|
|
5
|
+
[](https://codecov.io/gh/brencon/clipsy)
|
|
6
|
+

|
|
7
|
+

|
|
8
|
+
[](LICENSE)
|
|
9
|
+
|
|
10
|
+
A lightweight clipboard history manager for macOS. Runs as a menu bar icon — no admin privileges, no code signing, no App Store required.
|
|
11
|
+
|
|
12
|
+
## Features
|
|
13
|
+
|
|
14
|
+
- **Clipboard history** — Automatically captures text, images, and file copies
|
|
15
|
+
- **Image thumbnails** — Visual previews for copied images in the menu
|
|
16
|
+
- **Sensitive data masking** — Auto-detects API keys, passwords, SSNs, credit cards, private keys, and tokens; displays masked previews with 🔒 icon
|
|
17
|
+
- **Search** — Full-text search across all clipboard entries (SQLite FTS5)
|
|
18
|
+
- **Click to re-copy** — Click any entry in the menu to put it back on your clipboard
|
|
19
|
+
- **Deduplication** — Copying the same content twice bumps it to the top instead of creating a duplicate
|
|
20
|
+
- **Auto-purge** — Keeps the most recent 500 entries, automatically cleans up old ones
|
|
21
|
+
- **Persistent storage** — History survives app restarts (SQLite database)
|
|
22
|
+
- **Corporate IT friendly** — Runs as a plain Python process, no `.app` bundle or Gatekeeper issues
|
|
23
|
+
|
|
24
|
+
## Requirements
|
|
25
|
+
|
|
26
|
+
- macOS
|
|
27
|
+
- Python 3.10+ (Homebrew recommended: `brew install python3`)
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
### Via pip (recommended)
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install clipsy
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### From source
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
git clone https://github.com/brencon/clipsy.git
|
|
41
|
+
cd clipsy
|
|
42
|
+
python3 -m venv .venv
|
|
43
|
+
.venv/bin/pip install -e .
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Usage
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
# Run clipsy (a scissors icon appears in your menu bar)
|
|
50
|
+
clipsy
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Then just use your Mac normally. Every time you copy something, it shows up in the Clipsy menu:
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
[✂️ Icon]
|
|
57
|
+
├── Clipsy - Clipboard History
|
|
58
|
+
├── ──────────────────
|
|
59
|
+
├── Search...
|
|
60
|
+
├── ──────────────────
|
|
61
|
+
├── "Meeting notes for Q3 plan..."
|
|
62
|
+
├── "https://github.com/example..."
|
|
63
|
+
├── 🔒 "password=••••••••"
|
|
64
|
+
├── [thumbnail] "[Image: 1920x1080]"
|
|
65
|
+
├── ... (up to 10 items)
|
|
66
|
+
├── ──────────────────
|
|
67
|
+
├── Clear History
|
|
68
|
+
├── ──────────────────
|
|
69
|
+
├── Support Clipsy
|
|
70
|
+
├── ──────────────────
|
|
71
|
+
└── Quit Clipsy
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Auto-Start on Login
|
|
75
|
+
|
|
76
|
+
Run clipsy automatically when you log in — no terminal needed:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
# Install as a LaunchAgent
|
|
80
|
+
scripts/install_launchagent.sh install
|
|
81
|
+
|
|
82
|
+
# Check status
|
|
83
|
+
scripts/install_launchagent.sh status
|
|
84
|
+
|
|
85
|
+
# Remove auto-start
|
|
86
|
+
scripts/install_launchagent.sh uninstall
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Data Storage
|
|
90
|
+
|
|
91
|
+
All data is stored in `~/.local/share/clipsy/`:
|
|
92
|
+
|
|
93
|
+
| File | Purpose |
|
|
94
|
+
|------|---------|
|
|
95
|
+
| `clipsy.db` | SQLite database with clipboard entries |
|
|
96
|
+
| `images/` | Saved clipboard images (PNG files) |
|
|
97
|
+
| `clipsy.log` | Application log |
|
|
98
|
+
|
|
99
|
+
## Development
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
# Install with dev dependencies
|
|
103
|
+
.venv/bin/pip install -e ".[dev]"
|
|
104
|
+
|
|
105
|
+
# Run tests
|
|
106
|
+
.venv/bin/python -m pytest tests/ -v
|
|
107
|
+
|
|
108
|
+
# Run with coverage
|
|
109
|
+
.venv/bin/python -m pytest tests/ --cov=clipsy --cov-report=term-missing
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Architecture
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
NSPasteboard → monitor.py → redact.py → storage.py (SQLite) → app.py (menu bar UI)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
- **`app.py`** — `rumps.App` subclass; renders the menu bar dropdown, handles clicks and search
|
|
119
|
+
- **`monitor.py`** — Polls `NSPasteboard.changeCount()` every 0.5s; detects text, images, and file copies
|
|
120
|
+
- **`storage.py`** — SQLite with FTS5 full-text search, SHA-256 deduplication, auto-purge
|
|
121
|
+
- **`redact.py`** — Sensitive data detection and masking (API keys, passwords, SSN, credit cards, tokens)
|
|
122
|
+
- **`config.py`** — Constants, paths, limits
|
|
123
|
+
- **`models.py`** — `ClipboardEntry` dataclass, `ContentType` enum
|
|
124
|
+
- **`utils.py`** — Hashing, text truncation, PNG dimension parsing, thumbnail generation
|
|
125
|
+
|
|
126
|
+
### Dependencies
|
|
127
|
+
|
|
128
|
+
Only one external dependency:
|
|
129
|
+
|
|
130
|
+
- **`rumps`** — macOS menu bar app framework (brings `pyobjc-framework-Cocoa` transitively)
|
|
131
|
+
- **`sqlite3`** — Built into Python
|
|
132
|
+
|
|
133
|
+
## License
|
|
134
|
+
|
|
135
|
+
MIT License — see [LICENSE](LICENSE) for details.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "clipsy"
|
|
7
|
+
version = "1.5.0"
|
|
8
|
+
description = "Lightweight clipboard history manager for macOS"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "Brendan Conrad", email = "brendan.conrad@gmail.com"},
|
|
14
|
+
]
|
|
15
|
+
keywords = ["clipboard", "macos", "menu-bar", "history", "productivity"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Environment :: MacOS X",
|
|
19
|
+
"Intended Audience :: End Users/Desktop",
|
|
20
|
+
"License :: OSI Approved :: MIT License",
|
|
21
|
+
"Operating System :: MacOS",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Programming Language :: Python :: 3.13",
|
|
27
|
+
"Topic :: Desktop Environment",
|
|
28
|
+
"Topic :: Utilities",
|
|
29
|
+
]
|
|
30
|
+
dependencies = [
|
|
31
|
+
"rumps>=0.4.0",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.urls]
|
|
35
|
+
Homepage = "https://github.com/brencon/clipsy"
|
|
36
|
+
Repository = "https://github.com/brencon/clipsy"
|
|
37
|
+
Issues = "https://github.com/brencon/clipsy/issues"
|
|
38
|
+
|
|
39
|
+
[project.optional-dependencies]
|
|
40
|
+
dev = [
|
|
41
|
+
"pytest>=7.0",
|
|
42
|
+
"pytest-cov>=4.0",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
[project.scripts]
|
|
46
|
+
clipsy = "clipsy.__main__:main"
|
|
47
|
+
|
|
48
|
+
[tool.setuptools.packages.find]
|
|
49
|
+
where = ["src"]
|
|
50
|
+
|
|
51
|
+
[tool.pytest.ini_options]
|
|
52
|
+
testpaths = ["tests"]
|
|
53
|
+
pythonpath = ["src"]
|
|
54
|
+
|
|
55
|
+
[tool.semantic_release]
|
|
56
|
+
version_toml = ["pyproject.toml:project.version"]
|
|
57
|
+
version_variables = ["src/clipsy/__init__.py:__version__"]
|
|
58
|
+
branch = "main"
|
|
59
|
+
build_command = ""
|
|
60
|
+
commit_message = "chore(release): v{version}"
|
|
61
|
+
tag_format = "v{version}"
|
|
62
|
+
|
|
63
|
+
[tool.semantic_release.changelog]
|
|
64
|
+
changelog_file = "CHANGELOG.md"
|
|
65
|
+
|
|
66
|
+
[tool.semantic_release.commit_parser_options]
|
|
67
|
+
allowed_tags = ["feat", "fix", "docs", "style", "refactor", "perf", "test", "build", "ci", "chore"]
|
|
68
|
+
minor_tags = ["feat"]
|
|
69
|
+
patch_tags = ["fix", "perf"]
|
clipsy-1.5.0/setup.cfg
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
from clipsy.config import LOG_PATH
|
|
5
|
+
from clipsy.utils import ensure_dirs
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def main():
|
|
9
|
+
ensure_dirs()
|
|
10
|
+
|
|
11
|
+
logging.basicConfig(
|
|
12
|
+
level=logging.INFO,
|
|
13
|
+
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
14
|
+
handlers=[
|
|
15
|
+
logging.FileHandler(LOG_PATH),
|
|
16
|
+
logging.StreamHandler(sys.stderr),
|
|
17
|
+
],
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
from clipsy.app import ClipsyApp
|
|
21
|
+
app = ClipsyApp()
|
|
22
|
+
app.run()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
if __name__ == "__main__":
|
|
26
|
+
main()
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import webbrowser
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import rumps
|
|
6
|
+
|
|
7
|
+
from clipsy import __version__
|
|
8
|
+
from clipsy.config import DB_PATH, IMAGE_DIR, MENU_DISPLAY_COUNT, POLL_INTERVAL, REDACT_SENSITIVE, THUMBNAIL_SIZE
|
|
9
|
+
from clipsy.models import ClipboardEntry, ContentType
|
|
10
|
+
from clipsy.monitor import ClipboardMonitor
|
|
11
|
+
from clipsy.storage import StorageManager
|
|
12
|
+
from clipsy.utils import create_thumbnail, ensure_dirs
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
ENTRY_KEY_PREFIX = "clipsy_entry_"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ClipsyApp(rumps.App):
|
|
20
|
+
def __init__(self):
|
|
21
|
+
super().__init__("Clipsy", title="✂️", quit_button=None)
|
|
22
|
+
ensure_dirs()
|
|
23
|
+
self._storage = StorageManager(DB_PATH)
|
|
24
|
+
self._monitor = ClipboardMonitor(self._storage, on_change=self._refresh_menu)
|
|
25
|
+
self._entry_ids: dict[str, int] = {}
|
|
26
|
+
self._build_menu()
|
|
27
|
+
|
|
28
|
+
def _build_menu(self) -> None:
|
|
29
|
+
self.menu.clear()
|
|
30
|
+
self.menu = [
|
|
31
|
+
rumps.MenuItem(f"Clipsy v{__version__} - Clipboard History", callback=None),
|
|
32
|
+
None, # separator
|
|
33
|
+
rumps.MenuItem("Search...", callback=self._on_search),
|
|
34
|
+
None, # separator
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
entries = self._storage.get_recent(limit=MENU_DISPLAY_COUNT)
|
|
38
|
+
self._entry_ids.clear()
|
|
39
|
+
|
|
40
|
+
if not entries:
|
|
41
|
+
self.menu.add(rumps.MenuItem("(No clipboard history)", callback=None))
|
|
42
|
+
else:
|
|
43
|
+
for entry in entries:
|
|
44
|
+
self.menu.add(self._create_entry_menu_item(entry))
|
|
45
|
+
|
|
46
|
+
self.menu.add(None) # separator
|
|
47
|
+
self.menu.add(rumps.MenuItem("Clear History", callback=self._on_clear))
|
|
48
|
+
self.menu.add(None) # separator
|
|
49
|
+
self.menu.add(rumps.MenuItem("Support Clipsy", callback=self._on_support))
|
|
50
|
+
self.menu.add(None) # separator
|
|
51
|
+
self.menu.add(rumps.MenuItem("Quit Clipsy", callback=self._on_quit))
|
|
52
|
+
|
|
53
|
+
def _create_entry_menu_item(self, entry: ClipboardEntry) -> rumps.MenuItem:
|
|
54
|
+
"""Create a menu item for a clipboard entry."""
|
|
55
|
+
key = f"{ENTRY_KEY_PREFIX}{entry.id}"
|
|
56
|
+
self._entry_ids[key] = entry.id
|
|
57
|
+
display_text = self._get_display_preview(entry)
|
|
58
|
+
|
|
59
|
+
if entry.content_type == ContentType.IMAGE:
|
|
60
|
+
thumb_path = self._ensure_thumbnail(entry)
|
|
61
|
+
if thumb_path:
|
|
62
|
+
item = rumps.MenuItem(
|
|
63
|
+
display_text,
|
|
64
|
+
callback=self._on_entry_click,
|
|
65
|
+
icon=thumb_path,
|
|
66
|
+
dimensions=(32, 32),
|
|
67
|
+
template=False,
|
|
68
|
+
)
|
|
69
|
+
else:
|
|
70
|
+
item = rumps.MenuItem(display_text, callback=self._on_entry_click)
|
|
71
|
+
else:
|
|
72
|
+
item = rumps.MenuItem(display_text, callback=self._on_entry_click)
|
|
73
|
+
|
|
74
|
+
item._id = key
|
|
75
|
+
return item
|
|
76
|
+
|
|
77
|
+
def _get_display_preview(self, entry: ClipboardEntry) -> str:
|
|
78
|
+
"""Get the display preview for an entry, masking sensitive data if enabled."""
|
|
79
|
+
if REDACT_SENSITIVE and entry.is_sensitive and entry.masked_preview:
|
|
80
|
+
return f"🔒 {entry.masked_preview}"
|
|
81
|
+
return entry.preview
|
|
82
|
+
|
|
83
|
+
def _ensure_thumbnail(self, entry: ClipboardEntry) -> str | None:
|
|
84
|
+
"""Ensure a thumbnail exists for an image entry, generating if needed."""
|
|
85
|
+
if entry.thumbnail_path:
|
|
86
|
+
return entry.thumbnail_path
|
|
87
|
+
|
|
88
|
+
if not entry.image_path:
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
# Generate thumbnail for legacy entries
|
|
92
|
+
image_path = Path(entry.image_path)
|
|
93
|
+
if not image_path.exists():
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
thumb_filename = image_path.stem + "_thumb.png"
|
|
97
|
+
thumb_path = IMAGE_DIR / thumb_filename
|
|
98
|
+
|
|
99
|
+
if thumb_path.exists() or create_thumbnail(str(image_path), str(thumb_path), THUMBNAIL_SIZE):
|
|
100
|
+
self._storage.update_thumbnail_path(entry.id, str(thumb_path))
|
|
101
|
+
return str(thumb_path)
|
|
102
|
+
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
def _refresh_menu(self) -> None:
|
|
106
|
+
self._build_menu()
|
|
107
|
+
|
|
108
|
+
@rumps.timer(POLL_INTERVAL)
|
|
109
|
+
def _poll_clipboard(self, _sender) -> None:
|
|
110
|
+
self._monitor.check_clipboard()
|
|
111
|
+
|
|
112
|
+
def _on_entry_click(self, sender) -> None:
|
|
113
|
+
entry_id = self._entry_ids.get(getattr(sender, "_id", ""))
|
|
114
|
+
if entry_id is None:
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
entry = self._storage.get_entry(entry_id)
|
|
118
|
+
if entry is None:
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
from AppKit import NSPasteboard, NSPasteboardTypePNG, NSPasteboardTypeString
|
|
123
|
+
|
|
124
|
+
pb = NSPasteboard.generalPasteboard()
|
|
125
|
+
|
|
126
|
+
copied = False
|
|
127
|
+
|
|
128
|
+
if entry.content_type == ContentType.TEXT and entry.text_content:
|
|
129
|
+
pb.clearContents()
|
|
130
|
+
pb.setString_forType_(entry.text_content, NSPasteboardTypeString)
|
|
131
|
+
self._monitor.sync_change_count()
|
|
132
|
+
copied = True
|
|
133
|
+
|
|
134
|
+
elif entry.content_type == ContentType.IMAGE and entry.image_path:
|
|
135
|
+
from Foundation import NSData
|
|
136
|
+
img_data = NSData.dataWithContentsOfFile_(entry.image_path)
|
|
137
|
+
if img_data:
|
|
138
|
+
pb.clearContents()
|
|
139
|
+
pb.setData_forType_(img_data, NSPasteboardTypePNG)
|
|
140
|
+
self._monitor.sync_change_count()
|
|
141
|
+
copied = True
|
|
142
|
+
|
|
143
|
+
elif entry.content_type == ContentType.FILE and entry.text_content:
|
|
144
|
+
pb.clearContents()
|
|
145
|
+
pb.setString_forType_(entry.text_content, NSPasteboardTypeString)
|
|
146
|
+
self._monitor.sync_change_count()
|
|
147
|
+
copied = True
|
|
148
|
+
|
|
149
|
+
if copied:
|
|
150
|
+
self._storage.update_timestamp(entry_id)
|
|
151
|
+
self._refresh_menu()
|
|
152
|
+
rumps.notification("Clipsy", "", "Copied to clipboard", sound=False)
|
|
153
|
+
except Exception:
|
|
154
|
+
logger.exception("Error copying entry to clipboard")
|
|
155
|
+
|
|
156
|
+
def _on_search(self, _sender) -> None:
|
|
157
|
+
response = rumps.Window(
|
|
158
|
+
message="Search clipboard history:",
|
|
159
|
+
title="Clipsy Search",
|
|
160
|
+
default_text="",
|
|
161
|
+
ok="Search",
|
|
162
|
+
cancel="Cancel",
|
|
163
|
+
dimensions=(300, 24),
|
|
164
|
+
).run()
|
|
165
|
+
|
|
166
|
+
if response.clicked and response.text.strip():
|
|
167
|
+
query = response.text.strip()
|
|
168
|
+
results = self._storage.search(query, limit=MENU_DISPLAY_COUNT)
|
|
169
|
+
|
|
170
|
+
if not results:
|
|
171
|
+
rumps.alert("Clipsy Search", f'No results for "{query}"')
|
|
172
|
+
return
|
|
173
|
+
|
|
174
|
+
self.menu.clear()
|
|
175
|
+
self._entry_ids.clear()
|
|
176
|
+
self.menu = [
|
|
177
|
+
rumps.MenuItem(f'Search: "{query}" ({len(results)} results)', callback=None),
|
|
178
|
+
None,
|
|
179
|
+
rumps.MenuItem("Show All", callback=lambda _: self._refresh_menu()),
|
|
180
|
+
None,
|
|
181
|
+
]
|
|
182
|
+
|
|
183
|
+
for entry in results:
|
|
184
|
+
self.menu.add(self._create_entry_menu_item(entry))
|
|
185
|
+
|
|
186
|
+
self.menu.add(None)
|
|
187
|
+
self.menu.add(rumps.MenuItem("Quit Clipsy", callback=self._on_quit))
|
|
188
|
+
|
|
189
|
+
def _on_clear(self, _sender) -> None:
|
|
190
|
+
if rumps.alert("Clipsy", "Clear all clipboard history?", ok="Clear", cancel="Cancel"):
|
|
191
|
+
self._storage.clear_all()
|
|
192
|
+
self._refresh_menu()
|
|
193
|
+
|
|
194
|
+
def _on_support(self, _sender) -> None:
|
|
195
|
+
webbrowser.open("https://github.com/sponsors/brencon")
|
|
196
|
+
|
|
197
|
+
def _on_quit(self, _sender) -> None:
|
|
198
|
+
self._storage.close()
|
|
199
|
+
rumps.quit_application()
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
DATA_DIR = Path(os.environ.get("CLIPSY_DATA_DIR", Path.home() / ".local" / "share" / "clipsy"))
|
|
5
|
+
DB_PATH = DATA_DIR / "clipsy.db"
|
|
6
|
+
IMAGE_DIR = DATA_DIR / "images"
|
|
7
|
+
LOG_PATH = DATA_DIR / "clipsy.log"
|
|
8
|
+
|
|
9
|
+
POLL_INTERVAL = 0.5 # seconds between clipboard checks
|
|
10
|
+
MAX_ENTRIES = 500 # auto-purge threshold
|
|
11
|
+
MAX_TEXT_SIZE = 1_000_000 # 1MB text limit
|
|
12
|
+
MAX_IMAGE_SIZE = 10_000_000 # 10MB image limit
|
|
13
|
+
PREVIEW_LENGTH = 60 # characters shown in menu item
|
|
14
|
+
MENU_DISPLAY_COUNT = 10 # items shown in dropdown
|
|
15
|
+
THUMBNAIL_SIZE = (32, 32) # pixels, for menu icon display
|
|
16
|
+
REDACT_SENSITIVE = True # mask sensitive data in preview (API keys, passwords, etc.)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ContentType(str, Enum):
|
|
7
|
+
TEXT = "text"
|
|
8
|
+
IMAGE = "image"
|
|
9
|
+
FILE = "file"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class ClipboardEntry:
|
|
14
|
+
id: int | None
|
|
15
|
+
content_type: ContentType
|
|
16
|
+
text_content: str | None
|
|
17
|
+
image_path: str | None
|
|
18
|
+
preview: str
|
|
19
|
+
content_hash: str
|
|
20
|
+
byte_size: int
|
|
21
|
+
created_at: datetime
|
|
22
|
+
pinned: bool = False
|
|
23
|
+
source_app: str | None = None
|
|
24
|
+
thumbnail_path: str | None = None
|
|
25
|
+
is_sensitive: bool = False
|
|
26
|
+
masked_preview: str | None = None
|