init
This commit is contained in:
Executable
+307
@@ -0,0 +1,307 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Rofi clipboard manager
|
||||
Usage:
|
||||
roficlip.py --daemon [-q | --quiet]
|
||||
roficlip.py --show [--persistent | --actions] [-q | --quiet] [<item>]
|
||||
roficlip.py --add [-q | --quiet ]
|
||||
roficlip.py --remove [-q | --quiet]
|
||||
roficlip.py --edit
|
||||
roficlip.py (-h | --help)
|
||||
roficlip.py (-v | --version)
|
||||
|
||||
Arguments:
|
||||
<item> Selected item to be used with rofi
|
||||
|
||||
Commands:
|
||||
--daemon Run clipboard manager daemon.
|
||||
--show Show clipboard history.
|
||||
--persistent Select to show persistent history.
|
||||
--actions Select to show actions defined in config.
|
||||
--add Add current clipboard to persistent storage.
|
||||
--remove Remove current clipboard from persistent storage.
|
||||
--edit Edit persistent storage with text editor.
|
||||
-q, --quiet Do not notify, even if notification enabled in config.
|
||||
-h, --help Show this screen.
|
||||
-v, --version Show version.
|
||||
|
||||
"""
|
||||
import errno
|
||||
import os
|
||||
import sys
|
||||
import stat
|
||||
import struct
|
||||
from subprocess import Popen, DEVNULL
|
||||
from tempfile import NamedTemporaryFile
|
||||
|
||||
try:
|
||||
import gi
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk, Gdk, GLib
|
||||
except ImportError:
|
||||
raise
|
||||
|
||||
import yaml
|
||||
from docopt import docopt
|
||||
from xdg import BaseDirectory
|
||||
|
||||
try:
|
||||
import notify2
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
class ClipboardManager():
|
||||
def __init__(self):
|
||||
# Init databases and fifo
|
||||
name = 'roficlip'
|
||||
self.ring_db = '{0}/{1}'.format(BaseDirectory.save_data_path(name), 'ring.db')
|
||||
self.persist_db = '{0}/{1}'.format(BaseDirectory.save_data_path(name), 'persistent.db')
|
||||
self.fifo_path = '{0}/{1}.fifo'.format(BaseDirectory.get_runtime_dir(strict=False), name)
|
||||
self.config_path = '{0}/settings'.format(BaseDirectory.save_config_path(name))
|
||||
if not os.path.isfile(self.ring_db):
|
||||
open(self.ring_db, "a+").close()
|
||||
if not os.path.isfile(self.persist_db):
|
||||
open(self.persist_db, "a+").close()
|
||||
if (
|
||||
not os.path.exists(self.fifo_path) or
|
||||
not stat.S_ISFIFO(os.stat(self.fifo_path).st_mode)
|
||||
):
|
||||
os.mkfifo(self.fifo_path)
|
||||
self.fifo = os.open(self.fifo_path, os.O_RDONLY | os.O_NONBLOCK)
|
||||
|
||||
# Init clipboard and read databases
|
||||
self.cb = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
|
||||
self.ring = self.read(self.ring_db)
|
||||
self.persist = self.read(self.persist_db)
|
||||
|
||||
# Load settings
|
||||
self.load_config()
|
||||
|
||||
# Init notifications
|
||||
if self.cfg['notify'] and 'notify2' in sys.modules:
|
||||
self.notify = notify2
|
||||
self.notify.init(name)
|
||||
else:
|
||||
self.cfg['notify'] = False
|
||||
|
||||
def daemon(self):
|
||||
"""
|
||||
Clipboard Manager daemon.
|
||||
"""
|
||||
GLib.timeout_add(300, self.cb_watcher)
|
||||
GLib.timeout_add(300, self.fifo_watcher)
|
||||
Gtk.main()
|
||||
|
||||
def cb_watcher(self):
|
||||
"""
|
||||
Callback function.
|
||||
Watch clipboard and write changes to ring database.
|
||||
Must return "True" for continuous operation.
|
||||
"""
|
||||
clip = self.cb.wait_for_text()
|
||||
if self.sync_items(clip, self.ring):
|
||||
self.ring = self.ring[0:self.cfg['ring_size']]
|
||||
self.write(self.ring_db, self.ring)
|
||||
return True
|
||||
|
||||
def fifo_watcher(self):
|
||||
"""
|
||||
Callback function.
|
||||
Copy contents from fifo to clipboard.
|
||||
Must return "True" for continuous operation.
|
||||
"""
|
||||
try:
|
||||
fifo_in = os.read(self.fifo, 65536)
|
||||
except OSError as err:
|
||||
if err.errno == errno.EAGAIN or err.errno == errno.EWOULDBLOCK:
|
||||
fifo_in = None
|
||||
else:
|
||||
raise
|
||||
if fifo_in:
|
||||
self.cb.set_text(fifo_in.decode('utf-8'), -1)
|
||||
self.notify_send('Copied to the clipboard.')
|
||||
return True
|
||||
|
||||
def sync_items(self, clip, items):
|
||||
"""
|
||||
Sync clipboard contents with specified items dict when needed.
|
||||
Return "True" if dict modified, otherwise "False".
|
||||
"""
|
||||
if clip and (not items or clip != items[0]):
|
||||
if clip in items:
|
||||
items.remove(clip)
|
||||
items.insert(0, clip)
|
||||
return True
|
||||
return False
|
||||
|
||||
def copy_item(self, clip):
|
||||
"""
|
||||
Writes to fifo item that should be copied to clipboard.
|
||||
"""
|
||||
if clip:
|
||||
with open(self.fifo_path, "w") as file:
|
||||
file.write(clip)
|
||||
file.close()
|
||||
|
||||
def show_items(self, items):
|
||||
"""
|
||||
Format and show contents of specified items dict (for rofi).
|
||||
"""
|
||||
for clip in items:
|
||||
clip = clip.replace('\n', self.cfg['newline_char'])
|
||||
|
||||
# Move text after last \# to beginning of string
|
||||
if args['--persistent'] and self.cfg['show_comments_first'] and '#' in clip:
|
||||
# Save index of last \#
|
||||
idx = clip.rfind('#')
|
||||
# Format string
|
||||
clip = '{} ➜ {}'.format(clip[idx+1:], clip[:idx])
|
||||
preview = clip[0:self.cfg['preview_width']]
|
||||
print(preview)
|
||||
|
||||
def persistent_add(self):
|
||||
"""
|
||||
Add current clipboard to persistent storage.
|
||||
"""
|
||||
clip = self.cb.wait_for_text()
|
||||
if self.sync_items(clip, self.persist):
|
||||
self.write(self.persist_db, self.persist)
|
||||
self.notify_send('Added to persistent.')
|
||||
|
||||
def persistent_remove(self):
|
||||
"""
|
||||
Remove current clipboard from persistent storage.
|
||||
"""
|
||||
clip = self.cb.wait_for_text()
|
||||
if clip and clip in self.persist:
|
||||
self.persist.remove(clip)
|
||||
self.write(self.persist_db, self.persist)
|
||||
self.notify_send('Removed from persistent.')
|
||||
|
||||
def persistent_edit(self):
|
||||
"""
|
||||
Edit persistent storage with text editor.
|
||||
New line char will be used as separator.
|
||||
"""
|
||||
editor = os.getenv('EDITOR')
|
||||
if self.persist and editor:
|
||||
try:
|
||||
tmp = NamedTemporaryFile(mode='w+')
|
||||
for clip in self.persist:
|
||||
clip = '{}\n'.format(clip.replace('\n', self.cfg['newline_char']))
|
||||
tmp.write(clip)
|
||||
tmp.flush()
|
||||
except IOError as e:
|
||||
print("I/O error({0}): {1}".format(e.errno, e.strerror))
|
||||
else:
|
||||
proc = Popen([editor, tmp.name], stdout=DEVNULL, stderr=DEVNULL)
|
||||
ret = proc.wait()
|
||||
if ret == 0:
|
||||
tmp.seek(0, 0)
|
||||
clips = tmp.read().splitlines()
|
||||
if clips:
|
||||
self.persist = []
|
||||
for clip in clips:
|
||||
clip = clip.replace('\n', '')
|
||||
clip = clip.replace(self.cfg['newline_char'], '\n')
|
||||
self.persist.append(clip)
|
||||
self.write(self.persist_db, self.persist)
|
||||
finally:
|
||||
tmp.close()
|
||||
|
||||
def do_action(self, action):
|
||||
"""
|
||||
Run selected action on clipboard contents.
|
||||
"""
|
||||
if action:
|
||||
clip = self.cb.wait_for_text()
|
||||
params = self.actions[action].split(' ')
|
||||
while '%s' in params:
|
||||
params[params.index('%s')] = clip
|
||||
Popen(params, stdout=DEVNULL, stderr=DEVNULL)
|
||||
self.notify_send("Action: {}".format(action))
|
||||
|
||||
def notify_send(self, text):
|
||||
"""
|
||||
Show desktop notification.
|
||||
"""
|
||||
if self.cfg['notify']:
|
||||
n = self.notify.Notification("Roficlip", text)
|
||||
n.timeout = self.cfg['notify_timeout'] * 1000
|
||||
n.show()
|
||||
|
||||
def read(self, fd):
|
||||
"""
|
||||
Helper function. Binary reader.
|
||||
"""
|
||||
result = []
|
||||
with open(fd, "rb") as file:
|
||||
bytes_read = file.read(4)
|
||||
while bytes_read:
|
||||
chunksize = struct.unpack('>i', bytes_read)[0]
|
||||
bytes_read = file.read(chunksize)
|
||||
result.append(bytes_read.decode('utf-8'))
|
||||
bytes_read = file.read(4)
|
||||
return result
|
||||
|
||||
def write(self, fd, items):
|
||||
"""
|
||||
Helper function. Binary writer.
|
||||
"""
|
||||
with open(fd, 'wb') as file:
|
||||
for item in items:
|
||||
item = item.encode('utf-8')
|
||||
file.write(struct.pack('>i', len(item)))
|
||||
file.write(item)
|
||||
|
||||
def load_config(self):
|
||||
"""
|
||||
Read config if exists, and/or provide defaults.
|
||||
"""
|
||||
# default settings
|
||||
settings = {
|
||||
'settings': {
|
||||
'ring_size': 20,
|
||||
'preview_width': 100,
|
||||
'newline_char': '¬',
|
||||
'notify': True,
|
||||
'notify_timeout': 1,
|
||||
'show_comments_first': False,
|
||||
},
|
||||
'actions': {}
|
||||
}
|
||||
if os.path.isfile(self.config_path):
|
||||
with open(self.config_path, "r") as file:
|
||||
config = yaml.safe_load(file)
|
||||
for key in {'settings', 'actions'}:
|
||||
if key in config:
|
||||
settings[key].update(config[key])
|
||||
self.cfg = settings['settings']
|
||||
self.actions = settings['actions']
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
cm = ClipboardManager()
|
||||
args = docopt(__doc__, version='0.4')
|
||||
if args['--quiet']:
|
||||
cm.cfg['notify'] = False
|
||||
if args['--daemon']:
|
||||
cm.daemon()
|
||||
elif (args['--show'] and not args['--actions']):
|
||||
if args['<item>']:
|
||||
cm.copy_item(args['<item>'])
|
||||
else:
|
||||
cm.show_items(cm.persist if args['--persistent'] else cm.ring)
|
||||
elif (args['--show'] and args['--actions']):
|
||||
if args['<item>']:
|
||||
cm.do_action(args['<item>'])
|
||||
else:
|
||||
cm.show_items(cm.actions)
|
||||
elif args['--add']:
|
||||
cm.persistent_add()
|
||||
elif args['--remove']:
|
||||
cm.persistent_remove()
|
||||
elif args['--edit']:
|
||||
cm.persistent_edit()
|
||||
exit(0)
|
||||
Reference in New Issue
Block a user