merge: import clip-vault sources with history
git-subtree-dir: clip-vault git-subtree-mainline:c263a13641git-subtree-split:013c550047
This commit is contained in:
commit
e3622e32d6
8 files changed
+549
No files matched your search
Whitespace-only changes.
@@ -0,0 +1,130 @@
|
|||||||
|
import argparse
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
from clip_vault import storage, webhook
|
||||||
|
from clip_vault.config import loadConfig, configPath, logPath
|
||||||
|
|
||||||
|
|
||||||
|
def formatRow(row):
|
||||||
|
clipId, content, sourceWindow, createdAt, pinned = row
|
||||||
|
preview = content.replace("\n", " ").strip()
|
||||||
|
if len(preview) > 80:
|
||||||
|
preview = preview[:77] + "..."
|
||||||
|
when = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(createdAt))
|
||||||
|
pinMark = "*" if pinned else " "
|
||||||
|
source = f" [{sourceWindow}]" if sourceWindow else ""
|
||||||
|
return f"{clipId:>5} {pinMark} {when}{source} {preview}"
|
||||||
|
|
||||||
|
|
||||||
|
def commandList(args):
|
||||||
|
rows = storage.listClips(limit=args.limit, search=args.search)
|
||||||
|
for row in rows:
|
||||||
|
print(formatRow(row))
|
||||||
|
|
||||||
|
|
||||||
|
def commandShow(args):
|
||||||
|
row = storage.getClip(args.id)
|
||||||
|
if not row:
|
||||||
|
print(f"no clip with id {args.id}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
print(row[1])
|
||||||
|
|
||||||
|
|
||||||
|
def commandCopy(args):
|
||||||
|
row = storage.getClip(args.id)
|
||||||
|
if not row:
|
||||||
|
print(f"no clip with id {args.id}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
subprocess.run(["xclip", "-selection", "clipboard"], input=row[1].encode("utf-8"))
|
||||||
|
print(f"copied clip {args.id} to clipboard")
|
||||||
|
|
||||||
|
|
||||||
|
def commandPin(args):
|
||||||
|
storage.setPinned(args.id, True)
|
||||||
|
print(f"pinned {args.id}")
|
||||||
|
|
||||||
|
|
||||||
|
def commandUnpin(args):
|
||||||
|
storage.setPinned(args.id, False)
|
||||||
|
print(f"unpinned {args.id}")
|
||||||
|
|
||||||
|
|
||||||
|
def commandDelete(args):
|
||||||
|
storage.deleteClip(args.id)
|
||||||
|
print(f"deleted {args.id}")
|
||||||
|
|
||||||
|
|
||||||
|
def commandClear(args):
|
||||||
|
storage.clearHistory(keepPinned=not args.all)
|
||||||
|
print("history cleared" + ("" if args.all else " (pinned items kept)"))
|
||||||
|
|
||||||
|
|
||||||
|
def commandConfig(args):
|
||||||
|
print(f"config: {configPath}")
|
||||||
|
print(f"log: {logPath}")
|
||||||
|
|
||||||
|
|
||||||
|
def commandTestWebhook(args):
|
||||||
|
config = loadConfig()
|
||||||
|
target = next((w for w in config.get("webhooks", []) if w.get("name") == args.name), None)
|
||||||
|
if not target:
|
||||||
|
print(f"no webhook named '{args.name}' in config", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
ok = webhook.sendWebhook(target, args.content or "clip-vault test payload", "clip-vault-cli")
|
||||||
|
print("sent ok" if ok else "send failed, check " + logPath)
|
||||||
|
|
||||||
|
|
||||||
|
def buildParser():
|
||||||
|
parser = argparse.ArgumentParser(prog="clip-vault", description="Clipboard history manager with configurable webhook forwarding")
|
||||||
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||||
|
|
||||||
|
listParser = subparsers.add_parser("list", help="show recent clipboard history")
|
||||||
|
listParser.add_argument("--limit", type=int, default=30)
|
||||||
|
listParser.add_argument("--search", type=str, default=None)
|
||||||
|
listParser.set_defaults(func=commandList)
|
||||||
|
|
||||||
|
showParser = subparsers.add_parser("show", help="print full content of a clip")
|
||||||
|
showParser.add_argument("id", type=int)
|
||||||
|
showParser.set_defaults(func=commandShow)
|
||||||
|
|
||||||
|
copyParser = subparsers.add_parser("copy", help="copy a stored clip back to the clipboard")
|
||||||
|
copyParser.add_argument("id", type=int)
|
||||||
|
copyParser.set_defaults(func=commandCopy)
|
||||||
|
|
||||||
|
pinParser = subparsers.add_parser("pin", help="pin a clip so it survives trimming/clear")
|
||||||
|
pinParser.add_argument("id", type=int)
|
||||||
|
pinParser.set_defaults(func=commandPin)
|
||||||
|
|
||||||
|
unpinParser = subparsers.add_parser("unpin", help="unpin a clip")
|
||||||
|
unpinParser.add_argument("id", type=int)
|
||||||
|
unpinParser.set_defaults(func=commandUnpin)
|
||||||
|
|
||||||
|
deleteParser = subparsers.add_parser("delete", help="delete a clip")
|
||||||
|
deleteParser.add_argument("id", type=int)
|
||||||
|
deleteParser.set_defaults(func=commandDelete)
|
||||||
|
|
||||||
|
clearParser = subparsers.add_parser("clear", help="clear history")
|
||||||
|
clearParser.add_argument("--all", action="store_true", help="also delete pinned clips")
|
||||||
|
clearParser.set_defaults(func=commandClear)
|
||||||
|
|
||||||
|
configParser = subparsers.add_parser("config", help="show config and log file paths")
|
||||||
|
configParser.set_defaults(func=commandConfig)
|
||||||
|
|
||||||
|
testWebhookParser = subparsers.add_parser("test-webhook", help="send a test payload through a configured webhook")
|
||||||
|
testWebhookParser.add_argument("name", type=str)
|
||||||
|
testWebhookParser.add_argument("--content", type=str, default=None)
|
||||||
|
testWebhookParser.set_defaults(func=commandTestWebhook)
|
||||||
|
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = buildParser()
|
||||||
|
args = parser.parse_args()
|
||||||
|
args.func(args)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
configDir = os.path.expanduser("~/.config/clip-vault")
|
||||||
|
configPath = os.path.join(configDir, "config.json")
|
||||||
|
defaultConfigPath = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "config.default.json")
|
||||||
|
dbPath = os.path.join(configDir, "history.sqlite3")
|
||||||
|
logPath = os.path.join(configDir, "webhook.log")
|
||||||
|
|
||||||
|
|
||||||
|
def loadConfig():
|
||||||
|
if not os.path.exists(configPath):
|
||||||
|
installDefaultConfig()
|
||||||
|
with open(configPath) as configFile:
|
||||||
|
return json.load(configFile)
|
||||||
|
|
||||||
|
|
||||||
|
def installDefaultConfig():
|
||||||
|
os.makedirs(configDir, exist_ok=True)
|
||||||
|
with open(defaultConfigPath) as defaultFile:
|
||||||
|
defaultContent = defaultFile.read()
|
||||||
|
with open(configPath, "w") as configFile:
|
||||||
|
configFile.write(defaultContent)
|
||||||
|
|
||||||
|
|
||||||
|
def saveConfig(config):
|
||||||
|
os.makedirs(configDir, exist_ok=True)
|
||||||
|
with open(configPath, "w") as configFile:
|
||||||
|
json.dump(config, configFile, indent=2)
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
import hashlib
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
import gi
|
||||||
|
|
||||||
|
gi.require_version("Gtk", "3.0")
|
||||||
|
try:
|
||||||
|
gi.require_version("AyatanaAppIndicator3", "0.1")
|
||||||
|
from gi.repository import AyatanaAppIndicator3 as AppIndicator3
|
||||||
|
except ValueError:
|
||||||
|
gi.require_version("AppIndicator3", "0.1")
|
||||||
|
from gi.repository import AppIndicator3
|
||||||
|
|
||||||
|
from gi.repository import GLib, Gtk
|
||||||
|
|
||||||
|
from clip_vault import storage, webhook
|
||||||
|
from clip_vault.config import loadConfig
|
||||||
|
|
||||||
|
|
||||||
|
def activeWindowTitle():
|
||||||
|
try:
|
||||||
|
output = subprocess.run(
|
||||||
|
["xdotool", "getactivewindow", "getwindowname"], capture_output=True, text=True, timeout=1
|
||||||
|
)
|
||||||
|
return output.stdout.strip() if output.returncode == 0 else None
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class ClipVaultDaemon:
|
||||||
|
def __init__(self):
|
||||||
|
self.config = loadConfig()
|
||||||
|
self.paused = False
|
||||||
|
self.lastHash = None
|
||||||
|
from gi.repository import Gdk
|
||||||
|
self.clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
|
||||||
|
self.clipboard.connect("owner-change", self.onOwnerChange)
|
||||||
|
self.indicator = self.buildIndicator()
|
||||||
|
|
||||||
|
def buildIndicator(self):
|
||||||
|
indicator = AppIndicator3.Indicator.new(
|
||||||
|
"clip-vault", "edit-copy-symbolic", AppIndicator3.IndicatorCategory.APPLICATION_STATUS
|
||||||
|
)
|
||||||
|
indicator.set_status(AppIndicator3.IndicatorStatus.ACTIVE)
|
||||||
|
indicator.set_menu(self.buildMenu())
|
||||||
|
return indicator
|
||||||
|
|
||||||
|
def buildMenu(self):
|
||||||
|
menu = Gtk.Menu()
|
||||||
|
|
||||||
|
pauseItem = Gtk.CheckMenuItem(label="Pause capture")
|
||||||
|
pauseItem.set_active(self.paused)
|
||||||
|
pauseItem.connect("toggled", self.onTogglePause)
|
||||||
|
menu.append(pauseItem)
|
||||||
|
|
||||||
|
menu.append(Gtk.SeparatorMenuItem())
|
||||||
|
|
||||||
|
recent = storage.listClips(limit=10)
|
||||||
|
if recent:
|
||||||
|
for clipId, content, sourceWindow, createdAt, pinned in recent:
|
||||||
|
preview = content.replace("\n", " ").strip()
|
||||||
|
if len(preview) > 60:
|
||||||
|
preview = preview[:57] + "..."
|
||||||
|
label = ("* " if pinned else "") + preview
|
||||||
|
item = Gtk.MenuItem(label=label or "(empty)")
|
||||||
|
item.connect("activate", self.onRecopy, content)
|
||||||
|
menu.append(item)
|
||||||
|
else:
|
||||||
|
emptyItem = Gtk.MenuItem(label="(no history yet)")
|
||||||
|
emptyItem.set_sensitive(False)
|
||||||
|
menu.append(emptyItem)
|
||||||
|
|
||||||
|
menu.append(Gtk.SeparatorMenuItem())
|
||||||
|
|
||||||
|
reloadItem = Gtk.MenuItem(label="Reload config")
|
||||||
|
reloadItem.connect("activate", self.onReloadConfig)
|
||||||
|
menu.append(reloadItem)
|
||||||
|
|
||||||
|
quitItem = Gtk.MenuItem(label="Quit")
|
||||||
|
quitItem.connect("activate", self.onQuit)
|
||||||
|
menu.append(quitItem)
|
||||||
|
|
||||||
|
menu.show_all()
|
||||||
|
return menu
|
||||||
|
|
||||||
|
def refreshMenu(self):
|
||||||
|
self.indicator.set_menu(self.buildMenu())
|
||||||
|
|
||||||
|
def onTogglePause(self, checkItem):
|
||||||
|
self.paused = checkItem.get_active()
|
||||||
|
|
||||||
|
def onRecopy(self, _menuItem, content):
|
||||||
|
self.clipboard.set_text(content, -1)
|
||||||
|
self.clipboard.store()
|
||||||
|
|
||||||
|
def onReloadConfig(self, _menuItem):
|
||||||
|
self.config = loadConfig()
|
||||||
|
|
||||||
|
def onQuit(self, _menuItem):
|
||||||
|
Gtk.main_quit()
|
||||||
|
|
||||||
|
def isIgnored(self, content, sourceWindow):
|
||||||
|
if sourceWindow:
|
||||||
|
for pattern in self.config.get("ignoreWindowPatterns", []):
|
||||||
|
if re.search(pattern, sourceWindow, re.IGNORECASE):
|
||||||
|
return True
|
||||||
|
for pattern in self.config.get("ignoreContentPatterns", []):
|
||||||
|
if re.search(pattern, content):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def onOwnerChange(self, clipboard, _event):
|
||||||
|
if self.paused:
|
||||||
|
return
|
||||||
|
GLib.idle_add(self.captureCurrentClipboard)
|
||||||
|
|
||||||
|
def captureCurrentClipboard(self):
|
||||||
|
text = self.clipboard.wait_for_text()
|
||||||
|
if not text:
|
||||||
|
return False
|
||||||
|
content = text
|
||||||
|
minLength = self.config.get("minContentLength", 2)
|
||||||
|
maxLength = self.config.get("maxContentLength", 200000)
|
||||||
|
if len(content) < minLength or len(content) > maxLength:
|
||||||
|
return False
|
||||||
|
|
||||||
|
contentHash = hashlib.sha256(content.encode("utf-8")).hexdigest()
|
||||||
|
if contentHash == self.lastHash:
|
||||||
|
return False
|
||||||
|
self.lastHash = contentHash
|
||||||
|
|
||||||
|
sourceWindow = activeWindowTitle()
|
||||||
|
if self.isIgnored(content, sourceWindow):
|
||||||
|
return False
|
||||||
|
|
||||||
|
storage.addClip(content, contentHash, sourceWindow)
|
||||||
|
storage.trimHistory(self.config.get("maxHistory", 500))
|
||||||
|
|
||||||
|
def onWebhookDone(sentNames):
|
||||||
|
if sentNames:
|
||||||
|
GLib.idle_add(self.markWebhookSent, contentHash, sentNames)
|
||||||
|
|
||||||
|
webhook.dispatchAsync(content, sourceWindow, self.config, onDone=onWebhookDone)
|
||||||
|
|
||||||
|
GLib.idle_add(self.refreshMenu)
|
||||||
|
return False
|
||||||
|
|
||||||
|
def markWebhookSent(self, contentHash, sentNames):
|
||||||
|
connection = storage.connect()
|
||||||
|
connection.execute(
|
||||||
|
"UPDATE clips SET webhookSentTo = ? WHERE id = (SELECT id FROM clips WHERE contentHash = ? ORDER BY id DESC LIMIT 1)",
|
||||||
|
(",".join(sentNames), contentHash),
|
||||||
|
)
|
||||||
|
connection.commit()
|
||||||
|
connection.close()
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
daemon = ClipVaultDaemon()
|
||||||
|
Gtk.main()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import os
|
||||||
|
import sqlite3
|
||||||
|
import time
|
||||||
|
|
||||||
|
from clip_vault.config import configDir, dbPath
|
||||||
|
|
||||||
|
|
||||||
|
def connect():
|
||||||
|
os.makedirs(configDir, exist_ok=True)
|
||||||
|
connection = sqlite3.connect(dbPath)
|
||||||
|
connection.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS clips (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
contentHash TEXT NOT NULL,
|
||||||
|
sourceWindow TEXT,
|
||||||
|
createdAt REAL NOT NULL,
|
||||||
|
pinned INTEGER NOT NULL DEFAULT 0,
|
||||||
|
webhookSentTo TEXT
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
connection.execute("CREATE INDEX IF NOT EXISTS idxContentHash ON clips(contentHash)")
|
||||||
|
return connection
|
||||||
|
|
||||||
|
|
||||||
|
def addClip(content, contentHash, sourceWindow, webhookSentTo=None):
|
||||||
|
connection = connect()
|
||||||
|
existing = connection.execute(
|
||||||
|
"SELECT id FROM clips WHERE contentHash = ? ORDER BY id DESC LIMIT 1", (contentHash,)
|
||||||
|
).fetchone()
|
||||||
|
if existing:
|
||||||
|
connection.execute("DELETE FROM clips WHERE id = ?", (existing[0],))
|
||||||
|
connection.execute(
|
||||||
|
"INSERT INTO clips (content, contentHash, sourceWindow, createdAt, pinned, webhookSentTo) VALUES (?, ?, ?, ?, 0, ?)",
|
||||||
|
(content, contentHash, sourceWindow, time.time(), webhookSentTo),
|
||||||
|
)
|
||||||
|
connection.commit()
|
||||||
|
connection.close()
|
||||||
|
|
||||||
|
|
||||||
|
def trimHistory(maxHistory):
|
||||||
|
connection = connect()
|
||||||
|
connection.execute(
|
||||||
|
"""
|
||||||
|
DELETE FROM clips WHERE id IN (
|
||||||
|
SELECT id FROM clips WHERE pinned = 0
|
||||||
|
ORDER BY createdAt DESC LIMIT -1 OFFSET ?
|
||||||
|
)
|
||||||
|
""",
|
||||||
|
(maxHistory,),
|
||||||
|
)
|
||||||
|
connection.commit()
|
||||||
|
connection.close()
|
||||||
|
|
||||||
|
|
||||||
|
def listClips(limit=50, search=None):
|
||||||
|
connection = connect()
|
||||||
|
if search:
|
||||||
|
rows = connection.execute(
|
||||||
|
"SELECT id, content, sourceWindow, createdAt, pinned FROM clips WHERE content LIKE ? ORDER BY createdAt DESC LIMIT ?",
|
||||||
|
(f"%{search}%", limit),
|
||||||
|
).fetchall()
|
||||||
|
else:
|
||||||
|
rows = connection.execute(
|
||||||
|
"SELECT id, content, sourceWindow, createdAt, pinned FROM clips ORDER BY createdAt DESC LIMIT ?",
|
||||||
|
(limit,),
|
||||||
|
).fetchall()
|
||||||
|
connection.close()
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def getClip(clipId):
|
||||||
|
connection = connect()
|
||||||
|
row = connection.execute("SELECT id, content, sourceWindow, createdAt, pinned FROM clips WHERE id = ?", (clipId,)).fetchone()
|
||||||
|
connection.close()
|
||||||
|
return row
|
||||||
|
|
||||||
|
|
||||||
|
def setPinned(clipId, pinned):
|
||||||
|
connection = connect()
|
||||||
|
connection.execute("UPDATE clips SET pinned = ? WHERE id = ?", (1 if pinned else 0, clipId))
|
||||||
|
connection.commit()
|
||||||
|
connection.close()
|
||||||
|
|
||||||
|
|
||||||
|
def deleteClip(clipId):
|
||||||
|
connection = connect()
|
||||||
|
connection.execute("DELETE FROM clips WHERE id = ?", (clipId,))
|
||||||
|
connection.commit()
|
||||||
|
connection.close()
|
||||||
|
|
||||||
|
|
||||||
|
def clearHistory(keepPinned=True):
|
||||||
|
connection = connect()
|
||||||
|
if keepPinned:
|
||||||
|
connection.execute("DELETE FROM clips WHERE pinned = 0")
|
||||||
|
else:
|
||||||
|
connection.execute("DELETE FROM clips")
|
||||||
|
connection.commit()
|
||||||
|
connection.close()
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import json
|
||||||
|
import re
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
from clip_vault.config import logPath
|
||||||
|
|
||||||
|
|
||||||
|
def logLine(line):
|
||||||
|
with open(logPath, "a") as logFile:
|
||||||
|
logFile.write(f"{time.strftime('%Y-%m-%d %H:%M:%S')} {line}\n")
|
||||||
|
|
||||||
|
|
||||||
|
def matchingWebhooks(content, config):
|
||||||
|
matches = []
|
||||||
|
for webhook in config.get("webhooks", []):
|
||||||
|
if not webhook.get("enabled"):
|
||||||
|
continue
|
||||||
|
pattern = webhook.get("matchPattern", ".*")
|
||||||
|
try:
|
||||||
|
if re.search(pattern, content):
|
||||||
|
matches.append(webhook)
|
||||||
|
except re.error as regexError:
|
||||||
|
logLine(f"invalid matchPattern in webhook '{webhook.get('name')}': {regexError}")
|
||||||
|
return matches
|
||||||
|
|
||||||
|
|
||||||
|
def sendWebhook(webhook, content, sourceWindow):
|
||||||
|
body = {"content": content, "timestamp": time.time()}
|
||||||
|
if webhook.get("includeSourceWindow", True):
|
||||||
|
body["sourceWindow"] = sourceWindow
|
||||||
|
data = json.dumps(body).encode("utf-8")
|
||||||
|
headers = {"Content-Type": "application/json"}
|
||||||
|
headers.update(webhook.get("headers", {}))
|
||||||
|
request = urllib.request.Request(webhook["url"], data=data, headers=headers, method=webhook.get("method", "POST"))
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(request, timeout=webhook.get("timeoutSeconds", 4)) as response:
|
||||||
|
logLine(f"webhook '{webhook.get('name')}' -> {response.status}")
|
||||||
|
return True
|
||||||
|
except Exception as sendError:
|
||||||
|
logLine(f"webhook '{webhook.get('name')}' failed: {sendError}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def dispatchAsync(content, sourceWindow, config, onDone=None):
|
||||||
|
matches = matchingWebhooks(content, config)
|
||||||
|
if not matches:
|
||||||
|
if onDone:
|
||||||
|
onDone([])
|
||||||
|
return
|
||||||
|
|
||||||
|
def worker():
|
||||||
|
sentNames = []
|
||||||
|
for webhook in matches:
|
||||||
|
if sendWebhook(webhook, content, sourceWindow):
|
||||||
|
sentNames.append(webhook.get("name", webhook["url"]))
|
||||||
|
if onDone:
|
||||||
|
onDone(sentNames)
|
||||||
|
|
||||||
|
threading.Thread(target=worker, daemon=True).start()
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"maxHistory": 500,
|
||||||
|
"minContentLength": 2,
|
||||||
|
"maxContentLength": 200000,
|
||||||
|
"pollFallbackSeconds": 2,
|
||||||
|
"ignoreWindowPatterns": [
|
||||||
|
"KeePassXC",
|
||||||
|
"Bitwarden",
|
||||||
|
"1Password"
|
||||||
|
],
|
||||||
|
"ignoreContentPatterns": [
|
||||||
|
"-----BEGIN (RSA |OPENSSH |EC |DSA )?PRIVATE KEY-----"
|
||||||
|
],
|
||||||
|
"webhooks": [
|
||||||
|
{
|
||||||
|
"name": "example-disabled",
|
||||||
|
"enabled": false,
|
||||||
|
"url": "https://example.com/webhook",
|
||||||
|
"method": "POST",
|
||||||
|
"matchPattern": ".*",
|
||||||
|
"headers": {},
|
||||||
|
"timeoutSeconds": 4,
|
||||||
|
"includeSourceWindow": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Executable
+37
@@ -0,0 +1,37 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
sourceDir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
installDir="$HOME/.local/share/clip-vault"
|
||||||
|
binDir="$HOME/.local/bin"
|
||||||
|
autostartDir="$HOME/.config/autostart"
|
||||||
|
|
||||||
|
mkdir -p "$installDir" "$binDir" "$autostartDir"
|
||||||
|
rsync -a --delete "$sourceDir/clip_vault" "$installDir/"
|
||||||
|
cp "$sourceDir/config.default.json" "$installDir/config.default.json"
|
||||||
|
|
||||||
|
cat > "$binDir/clip-vault" <<EOF
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
exec python3 -m clip_vault.cli "\$@"
|
||||||
|
EOF
|
||||||
|
chmod +x "$binDir/clip-vault"
|
||||||
|
|
||||||
|
cat > "$binDir/clip-vault-daemon" <<EOF
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
exec python3 -m clip_vault.daemon
|
||||||
|
EOF
|
||||||
|
chmod +x "$binDir/clip-vault-daemon"
|
||||||
|
|
||||||
|
cat > "$autostartDir/clip-vault.desktop" <<EOF
|
||||||
|
[Desktop Entry]
|
||||||
|
Type=Application
|
||||||
|
Name=Clip Vault
|
||||||
|
Exec=env PYTHONPATH=$installDir $binDir/clip-vault-daemon
|
||||||
|
X-GNOME-Autostart-enabled=true
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "installed. PYTHONPATH must include $installDir when running clip-vault/clip-vault-daemon by hand:"
|
||||||
|
echo " export PYTHONPATH=$installDir:\$PYTHONPATH"
|
||||||
|
echo " clip-vault-daemon & # starts tray + clipboard watcher"
|
||||||
|
echo " clip-vault list # view history from the CLI"
|
||||||
|
echo "config file: ~/.config/clip-vault/config.json (edit webhooks there)"
|
||||||
Reference in new issue
Block a user