clipsy 1.8.0__py3-none-any.whl → 1.9.1__py3-none-any.whl
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/__init__.py +1 -1
- clipsy/app.py +54 -3
- clipsy/config.py +1 -0
- clipsy/storage.py +15 -0
- {clipsy-1.8.0.dist-info → clipsy-1.9.1.dist-info}/METADATA +23 -2
- clipsy-1.9.1.dist-info/RECORD +15 -0
- clipsy-1.8.0.dist-info/RECORD +0 -15
- {clipsy-1.8.0.dist-info → clipsy-1.9.1.dist-info}/WHEEL +0 -0
- {clipsy-1.8.0.dist-info → clipsy-1.9.1.dist-info}/entry_points.txt +0 -0
- {clipsy-1.8.0.dist-info → clipsy-1.9.1.dist-info}/licenses/LICENSE +0 -0
- {clipsy-1.8.0.dist-info → clipsy-1.9.1.dist-info}/top_level.txt +0 -0
clipsy/__init__.py
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
__version__ = "1.
|
|
1
|
+
__version__ = "1.9.1"
|
|
2
2
|
__app_name__ = "Clipsy"
|
clipsy/app.py
CHANGED
|
@@ -5,7 +5,7 @@ from pathlib import Path
|
|
|
5
5
|
import rumps
|
|
6
6
|
|
|
7
7
|
from clipsy import __version__
|
|
8
|
-
from clipsy.config import DB_PATH, IMAGE_DIR, MENU_DISPLAY_COUNT, POLL_INTERVAL, REDACT_SENSITIVE, THUMBNAIL_SIZE
|
|
8
|
+
from clipsy.config import DB_PATH, IMAGE_DIR, MAX_PINNED_ENTRIES, MENU_DISPLAY_COUNT, POLL_INTERVAL, REDACT_SENSITIVE, THUMBNAIL_SIZE
|
|
9
9
|
from clipsy.models import ClipboardEntry, ContentType
|
|
10
10
|
from clipsy.monitor import ClipboardMonitor
|
|
11
11
|
from clipsy.storage import StorageManager
|
|
@@ -34,10 +34,24 @@ class ClipsyApp(rumps.App):
|
|
|
34
34
|
None, # separator
|
|
35
35
|
]
|
|
36
36
|
|
|
37
|
-
entries = self._storage.get_recent(limit=MENU_DISPLAY_COUNT)
|
|
38
37
|
self._entry_ids.clear()
|
|
39
38
|
|
|
40
|
-
if
|
|
39
|
+
# Add pinned submenu if there are pinned entries
|
|
40
|
+
pinned_entries = self._storage.get_pinned()
|
|
41
|
+
if pinned_entries:
|
|
42
|
+
pinned_menu = rumps.MenuItem("📌 Pinned")
|
|
43
|
+
for entry in pinned_entries:
|
|
44
|
+
pinned_menu.add(self._create_entry_menu_item(entry))
|
|
45
|
+
pinned_menu.add(None) # separator
|
|
46
|
+
pinned_menu.add(rumps.MenuItem("Clear Pinned", callback=self._on_clear_pinned))
|
|
47
|
+
self.menu.add(pinned_menu)
|
|
48
|
+
self.menu.add(None) # separator
|
|
49
|
+
|
|
50
|
+
entries = self._storage.get_recent(limit=MENU_DISPLAY_COUNT)
|
|
51
|
+
# Filter out pinned entries from recent list
|
|
52
|
+
entries = [e for e in entries if not e.pinned]
|
|
53
|
+
|
|
54
|
+
if not entries and not pinned_entries:
|
|
41
55
|
self.menu.add(rumps.MenuItem("(No clipboard history)", callback=None))
|
|
42
56
|
else:
|
|
43
57
|
for entry in entries:
|
|
@@ -118,6 +132,17 @@ class ClipsyApp(rumps.App):
|
|
|
118
132
|
if entry is None:
|
|
119
133
|
return
|
|
120
134
|
|
|
135
|
+
# Check if Option key is held (for pin toggle)
|
|
136
|
+
try:
|
|
137
|
+
from AppKit import NSAlternateKeyMask, NSEvent
|
|
138
|
+
|
|
139
|
+
modifier_flags = NSEvent.modifierFlags()
|
|
140
|
+
if modifier_flags & NSAlternateKeyMask:
|
|
141
|
+
self._on_pin_toggle(entry)
|
|
142
|
+
return
|
|
143
|
+
except Exception:
|
|
144
|
+
pass # If we can't check modifiers, proceed with normal copy
|
|
145
|
+
|
|
121
146
|
try:
|
|
122
147
|
from AppKit import NSPasteboard, NSPasteboardTypePNG, NSPasteboardTypeString
|
|
123
148
|
from Foundation import NSData
|
|
@@ -161,6 +186,32 @@ class ClipsyApp(rumps.App):
|
|
|
161
186
|
except Exception:
|
|
162
187
|
logger.exception("Error copying entry to clipboard")
|
|
163
188
|
|
|
189
|
+
def _on_pin_toggle(self, entry: ClipboardEntry) -> None:
|
|
190
|
+
"""Toggle pin status for an entry."""
|
|
191
|
+
if entry.pinned:
|
|
192
|
+
# Unpin
|
|
193
|
+
self._storage.toggle_pin(entry.id)
|
|
194
|
+
rumps.notification("Clipsy", "", "Unpinned", sound=False)
|
|
195
|
+
else:
|
|
196
|
+
# Check if we can pin (not sensitive, under limit)
|
|
197
|
+
if entry.is_sensitive:
|
|
198
|
+
rumps.notification("Clipsy", "", "Cannot pin sensitive data", sound=False)
|
|
199
|
+
return
|
|
200
|
+
|
|
201
|
+
if self._storage.count_pinned() >= MAX_PINNED_ENTRIES:
|
|
202
|
+
rumps.notification("Clipsy", "", f"Maximum {MAX_PINNED_ENTRIES} pinned items", sound=False)
|
|
203
|
+
return
|
|
204
|
+
|
|
205
|
+
self._storage.toggle_pin(entry.id)
|
|
206
|
+
rumps.notification("Clipsy", "", "Pinned", sound=False)
|
|
207
|
+
|
|
208
|
+
self._refresh_menu()
|
|
209
|
+
|
|
210
|
+
def _on_clear_pinned(self, _sender) -> None:
|
|
211
|
+
"""Clear all pinned entries."""
|
|
212
|
+
self._storage.clear_pinned()
|
|
213
|
+
self._refresh_menu()
|
|
214
|
+
|
|
164
215
|
def _on_search(self, _sender) -> None:
|
|
165
216
|
response = rumps.Window(
|
|
166
217
|
message="Search clipboard history:",
|
clipsy/config.py
CHANGED
|
@@ -25,3 +25,4 @@ def _parse_menu_display_count() -> int:
|
|
|
25
25
|
MENU_DISPLAY_COUNT = _parse_menu_display_count()
|
|
26
26
|
THUMBNAIL_SIZE = (32, 32) # pixels, for menu icon display
|
|
27
27
|
REDACT_SENSITIVE = True # mask sensitive data in preview (API keys, passwords, etc.)
|
|
28
|
+
MAX_PINNED_ENTRIES = 5 # maximum number of pinned entries allowed
|
clipsy/storage.py
CHANGED
|
@@ -211,6 +211,21 @@ class StorageManager:
|
|
|
211
211
|
row = self._conn.execute("SELECT COUNT(*) as cnt FROM clipboard_entries").fetchone()
|
|
212
212
|
return row["cnt"]
|
|
213
213
|
|
|
214
|
+
def get_pinned(self) -> list[ClipboardEntry]:
|
|
215
|
+
rows = self._conn.execute(
|
|
216
|
+
"SELECT * FROM clipboard_entries WHERE pinned = 1 ORDER BY created_at DESC"
|
|
217
|
+
).fetchall()
|
|
218
|
+
return [self._row_to_entry(r) for r in rows]
|
|
219
|
+
|
|
220
|
+
def count_pinned(self) -> int:
|
|
221
|
+
row = self._conn.execute("SELECT COUNT(*) as cnt FROM clipboard_entries WHERE pinned = 1").fetchone()
|
|
222
|
+
return row["cnt"]
|
|
223
|
+
|
|
224
|
+
def clear_pinned(self) -> None:
|
|
225
|
+
"""Unpin all pinned entries."""
|
|
226
|
+
self._conn.execute("UPDATE clipboard_entries SET pinned = 0 WHERE pinned = 1")
|
|
227
|
+
self._conn.commit()
|
|
228
|
+
|
|
214
229
|
def close(self) -> None:
|
|
215
230
|
self._conn.close()
|
|
216
231
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: clipsy
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.9.1
|
|
4
4
|
Summary: Lightweight clipboard history manager for macOS
|
|
5
5
|
Author-email: Brendan Conrad <brendan.conrad@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -46,6 +46,8 @@ A lightweight clipboard history manager for macOS. Runs as a menu bar icon — n
|
|
|
46
46
|
- **Image thumbnails** — Visual previews for copied images in the menu
|
|
47
47
|
- **Sensitive data masking** — Auto-detects API keys, passwords, SSNs, credit cards, private keys, and tokens; displays masked previews with 🔒 icon
|
|
48
48
|
- **Search** — Full-text search across all clipboard entries (SQLite FTS5)
|
|
49
|
+
- **Rich text preservation** — Preserves RTF and HTML formatting when re-copying from history (e.g., bold, italic, links from web pages)
|
|
50
|
+
- **Pin favorites** — Option-click to pin up to 5 frequently-used snippets (sensitive data cannot be pinned)
|
|
49
51
|
- **Click to re-copy** — Click any entry in the menu to put it back on your clipboard
|
|
50
52
|
- **Deduplication** — Copying the same content twice bumps it to the top instead of creating a duplicate
|
|
51
53
|
- **Auto-purge** — Keeps the most recent 500 entries, automatically cleans up old ones
|
|
@@ -103,11 +105,17 @@ Then just use your Mac normally. Every time you copy something, it shows up in t
|
|
|
103
105
|
├── ──────────────────
|
|
104
106
|
├── Search...
|
|
105
107
|
├── ──────────────────
|
|
108
|
+
├── 📌 Pinned ►
|
|
109
|
+
│ ├── "my-api-endpoint.com/v1..."
|
|
110
|
+
│ ├── "SELECT * FROM users..."
|
|
111
|
+
│ ├── ──────────────────
|
|
112
|
+
│ └── Clear Pinned
|
|
113
|
+
├── ──────────────────
|
|
106
114
|
├── "Meeting notes for Q3 plan..."
|
|
107
115
|
├── "https://github.com/example..."
|
|
108
116
|
├── 🔒 "password=••••••••"
|
|
109
117
|
├── [thumbnail] "[Image: 1920x1080]"
|
|
110
|
-
├── ... (up to 10 items)
|
|
118
|
+
├── ... (up to 10 items, configurable)
|
|
111
119
|
├── ──────────────────
|
|
112
120
|
├── Clear History
|
|
113
121
|
├── ──────────────────
|
|
@@ -116,6 +124,8 @@ Then just use your Mac normally. Every time you copy something, it shows up in t
|
|
|
116
124
|
└── Quit Clipsy
|
|
117
125
|
```
|
|
118
126
|
|
|
127
|
+
**Tip:** Hold **Option (⌥)** while clicking an entry to pin/unpin it.
|
|
128
|
+
|
|
119
129
|
## Commands
|
|
120
130
|
|
|
121
131
|
```bash
|
|
@@ -125,6 +135,17 @@ clipsy uninstall # Remove from login items
|
|
|
125
135
|
clipsy run # Run in foreground (for debugging)
|
|
126
136
|
```
|
|
127
137
|
|
|
138
|
+
## Configuration
|
|
139
|
+
|
|
140
|
+
| Variable | Default | Range | Description |
|
|
141
|
+
|----------|---------|-------|-------------|
|
|
142
|
+
| `CLIPSY_MENU_DISPLAY_COUNT` | `10` | 5–50 | Number of entries shown in the menu |
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
# Example: show 20 entries in the menu
|
|
146
|
+
export CLIPSY_MENU_DISPLAY_COUNT=20
|
|
147
|
+
```
|
|
148
|
+
|
|
128
149
|
## Data Storage
|
|
129
150
|
|
|
130
151
|
All data is stored in `~/.local/share/clipsy/`:
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
clipsy/__init__.py,sha256=2Kv8tRNneXIx_aRNo4vtSs8XH-R5BqOXMMDbWDoXWCw,46
|
|
2
|
+
clipsy/__main__.py,sha256=AzUjrEhnuVX5Mkk3QhfmAO17cyvIYOABsz7QWYmw1r8,4991
|
|
3
|
+
clipsy/app.py,sha256=scM-po0Kw1SOg_pDlMxsgbzhDQWS4BAvv5Iq64GLJEc,9861
|
|
4
|
+
clipsy/config.py,sha256=NOElM7jJ73AWQ1sf09dxg82-1nGErXd1zT63v2MPGpA,980
|
|
5
|
+
clipsy/models.py,sha256=9wuaq1t9HhaH8QYuieN7fEVM_RTYEvv6ZUCM_InOMKk,632
|
|
6
|
+
clipsy/monitor.py,sha256=PBTxK53arFm5VVCHIYlxz8eoYQrgTwWsIWNE49IE7oU,6904
|
|
7
|
+
clipsy/redact.py,sha256=EU2nq8oEDYLZ_I_9tX2bXACIF971a0V_ApKdvpN4VGY,7284
|
|
8
|
+
clipsy/storage.py,sha256=WE3EeOLcSgw-0it1qPk5rS-vDRC3-XDILaMZquBFR5Y,10659
|
|
9
|
+
clipsy/utils.py,sha256=rztT20cXYJ7633q3oLwt6Fv0Ubq9hm5bPPKDkWyvxdI,2208
|
|
10
|
+
clipsy-1.9.1.dist-info/licenses/LICENSE,sha256=bZ7uVihgPnLGryi5ugUgkVrEoho0_hFAd3bVdhYXGqs,1071
|
|
11
|
+
clipsy-1.9.1.dist-info/METADATA,sha256=bGB2xzIee5yjGFGZIuEbfsMkfr7vX0vD6LEx6b0HJrM,6780
|
|
12
|
+
clipsy-1.9.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
13
|
+
clipsy-1.9.1.dist-info/entry_points.txt,sha256=ISIoWU3Zj-4NEuj2pJW96LxpsvjRJJH73otv9gA9YSs,48
|
|
14
|
+
clipsy-1.9.1.dist-info/top_level.txt,sha256=trxprVJk4ZMudCshc7PD0N9iFgQO4Tq4sW5L5wLduns,7
|
|
15
|
+
clipsy-1.9.1.dist-info/RECORD,,
|
clipsy-1.8.0.dist-info/RECORD
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
clipsy/__init__.py,sha256=2M96CO-qBjhCYnYgi1FZuen5WyQOvlbtLUIIPCDsES0,46
|
|
2
|
-
clipsy/__main__.py,sha256=AzUjrEhnuVX5Mkk3QhfmAO17cyvIYOABsz7QWYmw1r8,4991
|
|
3
|
-
clipsy/app.py,sha256=e987geHCfDs3_9GulTZaS2iJT-BUL_iwpNtAmt6kPW8,7786
|
|
4
|
-
clipsy/config.py,sha256=YetSNZkSkSOHYnCZpty6sGeG99MajbN4goL8UkzJ62A,913
|
|
5
|
-
clipsy/models.py,sha256=9wuaq1t9HhaH8QYuieN7fEVM_RTYEvv6ZUCM_InOMKk,632
|
|
6
|
-
clipsy/monitor.py,sha256=PBTxK53arFm5VVCHIYlxz8eoYQrgTwWsIWNE49IE7oU,6904
|
|
7
|
-
clipsy/redact.py,sha256=EU2nq8oEDYLZ_I_9tX2bXACIF971a0V_ApKdvpN4VGY,7284
|
|
8
|
-
clipsy/storage.py,sha256=ZcuW2uFvJM0Cd3UQEWIMSZYH2DYWM6lD6yol6VpM5io,10047
|
|
9
|
-
clipsy/utils.py,sha256=rztT20cXYJ7633q3oLwt6Fv0Ubq9hm5bPPKDkWyvxdI,2208
|
|
10
|
-
clipsy-1.8.0.dist-info/licenses/LICENSE,sha256=bZ7uVihgPnLGryi5ugUgkVrEoho0_hFAd3bVdhYXGqs,1071
|
|
11
|
-
clipsy-1.8.0.dist-info/METADATA,sha256=UMKIznK4wUar5eV0ic7QVLuoRdZN4fMT6AL34BbuHdE,5884
|
|
12
|
-
clipsy-1.8.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
13
|
-
clipsy-1.8.0.dist-info/entry_points.txt,sha256=ISIoWU3Zj-4NEuj2pJW96LxpsvjRJJH73otv9gA9YSs,48
|
|
14
|
-
clipsy-1.8.0.dist-info/top_level.txt,sha256=trxprVJk4ZMudCshc7PD0N9iFgQO4Tq4sW5L5wLduns,7
|
|
15
|
-
clipsy-1.8.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|