feat: add clip-vault clipboard history daemon with webhook fanout

This commit is contained in:
dualfroz committed 2026-07-29 15:26:23 +02:00
commit 013c550047
8 files changed
+549

No files matched your search

+130
View File
@@ -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()