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 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
+ [![PyPI](https://img.shields.io/pypi/v/clipsy)](https://pypi.org/project/clipsy/)
35
+ [![CI](https://github.com/brencon/clipsy/actions/workflows/ci.yml/badge.svg)](https://github.com/brencon/clipsy/actions/workflows/ci.yml)
36
+ [![codecov](https://codecov.io/gh/brencon/clipsy/branch/main/graph/badge.svg)](https://codecov.io/gh/brencon/clipsy)
37
+ ![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue.svg)
38
+ ![Platform: macOS](https://img.shields.io/badge/platform-macOS-lightgrey.svg)
39
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](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
+ [![PyPI](https://img.shields.io/pypi/v/clipsy)](https://pypi.org/project/clipsy/)
4
+ [![CI](https://github.com/brencon/clipsy/actions/workflows/ci.yml/badge.svg)](https://github.com/brencon/clipsy/actions/workflows/ci.yml)
5
+ [![codecov](https://codecov.io/gh/brencon/clipsy/branch/main/graph/badge.svg)](https://codecov.io/gh/brencon/clipsy)
6
+ ![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue.svg)
7
+ ![Platform: macOS](https://img.shields.io/badge/platform-macOS-lightgrey.svg)
8
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,2 @@
1
+ __version__ = "1.5.0"
2
+ __app_name__ = "Clipsy"
@@ -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