├── nwg_clipman ├── __init__.py ├── langs │ ├── zh_CN.json │ ├── ja_JP.json │ ├── en_US.json │ ├── pl_PL.json │ ├── cs_CZ.json │ ├── fr_FR.json │ ├── de_AT.json │ ├── de_DE.json │ ├── pt_BR.json │ ├── es_AR.json │ ├── es_ES.json │ ├── it_IT.json │ ├── pt_PT.json │ └── ru_RU.json ├── __about__.py ├── tools.py └── main.py ├── .github └── FUNDING.yml ├── .gitignore ├── nwg-clipman.desktop ├── setup.py ├── uninstall.sh ├── install.sh ├── LICENSE ├── README.md └── nwg-clipman.svg /nwg_clipman/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: nwg-piotr 2 | liberapay: nwg 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /venv 3 | /nwg_clipman.egg-info/ 4 | /build/ 5 | /dist/ 6 | /nwg_clipman/__pycache__ 7 | /result 8 | -------------------------------------------------------------------------------- /nwg_clipman/langs/zh_CN.json: -------------------------------------------------------------------------------- 1 | { 2 | "clear": "清除", 3 | "clear-clipboard-history": "清除剪贴板历史记录", 4 | "close": "关闭", 5 | "copy": "复制", 6 | "preview": "预览", 7 | "preview-unavailable": "无法预览" 8 | } 9 | -------------------------------------------------------------------------------- /nwg_clipman/langs/ja_JP.json: -------------------------------------------------------------------------------- 1 | { 2 | "clear": "クリア", 3 | "clear-clipboard-history": "履歴をクリアしますか", 4 | "close": "閉じる", 5 | "copy": "コピー", 6 | "preview": "プレビュー", 7 | "preview-unavailable": "プレビューを利用できません" 8 | } 9 | -------------------------------------------------------------------------------- /nwg_clipman/langs/en_US.json: -------------------------------------------------------------------------------- 1 | { 2 | "clear": "Clear", 3 | "clear-clipboard-history": "Clear clipboard history", 4 | "close": "Close", 5 | "copy": "Copy", 6 | "preview": "Preview", 7 | "preview-unavailable": "Preview unavailable" 8 | } -------------------------------------------------------------------------------- /nwg_clipman/langs/pl_PL.json: -------------------------------------------------------------------------------- 1 | { 2 | "clear": "Wyczyść", 3 | "clear-clipboard-history": "Wyczyścić historię schowka", 4 | "close": "Zamknij", 5 | "copy": "Kopiuj", 6 | "preview": "Podgląd", 7 | "preview-unavailable": "Podgląd niedostępny" 8 | } -------------------------------------------------------------------------------- /nwg_clipman/langs/cs_CZ.json: -------------------------------------------------------------------------------- 1 | { 2 | "clear": "Vyčistit", 3 | "clear-clipboard-history": "Vymazat historii schránky", 4 | "close": "Zavřít", 5 | "copy": "Kopírovat", 6 | "preview": "Náhled", 7 | "preview-unavailable": "Náhled není k dispozici" 8 | } 9 | -------------------------------------------------------------------------------- /nwg_clipman/langs/fr_FR.json: -------------------------------------------------------------------------------- 1 | { 2 | "clear": "Clair", 3 | "clear-clipboard-history": "Effacer l'historique du presse-papiers", 4 | "close": "Fermer", 5 | "copy": "Copie", 6 | "preview": "Aperçu", 7 | "preview-unavailable": "Aperçu indisponible" 8 | } -------------------------------------------------------------------------------- /nwg_clipman/langs/de_AT.json: -------------------------------------------------------------------------------- 1 | { 2 | "clear": "Klar", 3 | "clear-clipboard-history": "Verlauf der Zwischenablage löschen", 4 | "close": "Schließen", 5 | "copy": "Kopieren", 6 | "preview": "Vorschau", 7 | "preview-unavailable": "Vorschau nicht verfügbar" 8 | } -------------------------------------------------------------------------------- /nwg_clipman/langs/de_DE.json: -------------------------------------------------------------------------------- 1 | { 2 | "clear": "Klar", 3 | "clear-clipboard-history": "Verlauf der Zwischenablage löschen", 4 | "close": "Schließen", 5 | "copy": "Kopieren", 6 | "preview": "Vorschau", 7 | "preview-unavailable": "Vorschau nicht verfügbar" 8 | } -------------------------------------------------------------------------------- /nwg_clipman/langs/pt_BR.json: -------------------------------------------------------------------------------- 1 | { 2 | "clear": "Limpar", 3 | "clear-clipboard-history": "Limpar histórico da área de transferência", 4 | "close": "Fechar", 5 | "copy": "Copiar", 6 | "preview": "Prévia", 7 | "preview-unavailable": "Prévia indisponível" 8 | } 9 | -------------------------------------------------------------------------------- /nwg_clipman/__about__.py: -------------------------------------------------------------------------------- 1 | try: 2 | from importlib import metadata 3 | except ImportError: 4 | import importlib_metadata as metadata 5 | 6 | try: 7 | __version__ = metadata.version("nwg-clipman") 8 | except Exception: 9 | __version__ = "unknown" 10 | -------------------------------------------------------------------------------- /nwg_clipman/langs/es_AR.json: -------------------------------------------------------------------------------- 1 | { 2 | "clear": "Borrar", 3 | "clear-clipboard-history": "Borrar historial del portapapeles", 4 | "close": "Cerrar", 5 | "copy": "Copiar", 6 | "preview": "Vista previa", 7 | "preview-unavailable": "Vista previa no disponible" 8 | } 9 | -------------------------------------------------------------------------------- /nwg_clipman/langs/es_ES.json: -------------------------------------------------------------------------------- 1 | { 2 | "clear": "Borrar", 3 | "clear-clipboard-history": "Borrar historial del portapapeles", 4 | "close": "Cerrar", 5 | "copy": "Copiar", 6 | "preview": "Vista previa", 7 | "preview-unavailable": "Vista previa no disponible" 8 | } 9 | -------------------------------------------------------------------------------- /nwg_clipman/langs/it_IT.json: -------------------------------------------------------------------------------- 1 | { 2 | "clear": "Cancella", 3 | "clear-clipboard-history": "Cancella la cronologia degli appunti", 4 | "close": "Chiudi", 5 | "copy": "Copia", 6 | "preview": "Anteprima", 7 | "preview-unavailable": "Anteprima non disponibile" 8 | } 9 | -------------------------------------------------------------------------------- /nwg_clipman/langs/pt_PT.json: -------------------------------------------------------------------------------- 1 | { 2 | "clear": "Claro", 3 | "clear-clipboard-history": "Limpar histórico da área de transferência", 4 | "close": "Fechar", 5 | "copy": "Cópia", 6 | "preview": "Visualização", 7 | "preview-unavailable": "Visualização indisponível" 8 | } -------------------------------------------------------------------------------- /nwg_clipman/langs/ru_RU.json: -------------------------------------------------------------------------------- 1 | { 2 | "clear": "Очистить", 3 | "clear-clipboard-history": "Очистить историю буфера обмена", 4 | "close": "Закрыть", 5 | "copy": "Копировать", 6 | "preview": "Предварительный просмотр", 7 | "preview-unavailable": "Предварительный просмотр недоступен" 8 | } 9 | -------------------------------------------------------------------------------- /nwg-clipman.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Type=Application 3 | 4 | Name=Clipboard manager 5 | Name[pl]=Menedżer schowka 6 | 7 | GenericName=nwg-shell clipboard manager 8 | GenericName[pl]=Menedżer schowka nwg-shell 9 | 10 | Comment=Clipboard history search and management tool 11 | Comment[pl]=Narzędzie do wyszukiwania i zarządzania historią schowka 12 | 13 | Exec=nwg-clipman 14 | Icon=nwg-clipman 15 | Terminal=false 16 | Categories=Utility; -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from setuptools import setup, find_packages 4 | 5 | 6 | def read(f_name): 7 | return open(os.path.join(os.path.dirname(__file__), f_name)).read() 8 | 9 | 10 | setup( 11 | name='nwg-clipman', 12 | version='0.2.8', 13 | description='nwg-shell clipboard manager', 14 | packages=find_packages(), 15 | include_package_data=True, 16 | package_data={ 17 | "": ["langs/*"] 18 | }, 19 | url='https://github.com/nwg-piotr/nwg-clipman', 20 | license='MIT', 21 | author='Piotr Miller', 22 | author_email='nwg.piotr@gmail.com', 23 | python_requires='>=3.6.0', 24 | install_requires=[], 25 | entry_points={ 26 | 'gui_scripts': [ 27 | 'nwg-clipman = nwg_clipman.main:main' 28 | ] 29 | } 30 | ) 31 | -------------------------------------------------------------------------------- /uninstall.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | PROGRAM_NAME="nwg-clipman" 4 | MODULE_NAME="nwg_clipman" 5 | SITE_PACKAGES="$(python3 -c "import sysconfig; print(sysconfig.get_paths()['purelib'])")" 6 | PATTERN="$SITE_PACKAGES/$MODULE_NAME*" 7 | 8 | # Remove from site_packages 9 | for path in $PATTERN; do 10 | if [ -e "$path" ]; then 11 | echo "Removing $path" 12 | rm -r "$path" 13 | fi 14 | done 15 | 16 | [ -d "./dist" ] && rm -rf ./dist 17 | 18 | # Remove launcher script 19 | if [ -f "/usr/bin/$PROGRAM_NAME" ]; then 20 | echo "Removing /usr/bin/$PROGRAM_NAME" 21 | rm "/usr/bin/nwg-clipman" 22 | fi 23 | 24 | echo "Removing icon, .desktop file, license and readme" 25 | rm -f "/usr/share/pixmaps/$PROGRAM_NAME.svg" 26 | rm -f "/usr/share/applications/$PROGRAM_NAME.desktop" 27 | rm -f "/usr/share/licenses/$PROGRAM_NAME/LICENSE" 28 | rm -f "/usr/share/doc/$PROGRAM_NAME/README.md" 29 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Before running this script, make sure you have python-build, python-installer, 4 | # python-wheel and python-setuptools installed. 5 | 6 | PROGRAM_NAME="nwg-clipman" 7 | MODULE_NAME="nwg_clipman" 8 | SITE_PACKAGES="$(python3 -c "import sysconfig; print(sysconfig.get_paths()['purelib'])")" 9 | PATTERN="$SITE_PACKAGES/$MODULE_NAME*" 10 | 11 | # Remove from site_packages 12 | for path in $PATTERN; do 13 | if [ -e "$path" ]; then 14 | echo "Removing $path" 15 | rm -r "$path" 16 | fi 17 | done 18 | 19 | [ -d "./dist" ] && rm -rf ./dist 20 | 21 | # Remove launcher script 22 | if [ -f "/usr/bin/$PROGRAM_NAME" ]; then 23 | echo "Removing /usr/bin/$PROGRAM_NAME" 24 | rm "/usr/bin/nwg-clipman" 25 | fi 26 | 27 | python -m build --wheel --no-isolation 28 | python -m installer dist/*.whl 29 | 30 | install -Dm 644 -t "/usr/share/pixmaps" "$PROGRAM_NAME.svg" 31 | install -Dm 644 -t "/usr/share/applications" "$PROGRAM_NAME.desktop" 32 | install -Dm 644 -t "/usr/share/licenses/$PROGRAM_NAME" LICENSE 33 | install -Dm 644 -t "/usr/share/doc/$PROGRAM_NAME" README.md 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Piotr Miller 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. 22 | -------------------------------------------------------------------------------- /nwg_clipman/tools.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import subprocess 4 | import sys 5 | 6 | 7 | def eprint(*args, **kwargs): 8 | print(*args, file=sys.stderr, **kwargs) 9 | 10 | 11 | def is_command(cmd): 12 | cmd = cmd.split()[0] 13 | cmd = "command -v {}".format(cmd) 14 | try: 15 | is_cmd = subprocess.check_output( 16 | cmd, shell=True).decode("utf-8").strip() 17 | if is_cmd: 18 | return True 19 | 20 | except subprocess.CalledProcessError: 21 | return False 22 | 23 | 24 | def temp_dir(): 25 | if os.getenv("TMPDIR"): 26 | return os.getenv("TMPDIR") 27 | elif os.getenv("TEMP"): 28 | return os.getenv("TEMP") 29 | elif os.getenv("TMP"): 30 | return os.getenv("TMP") 31 | 32 | return "/tmp" 33 | 34 | 35 | def load_text_file(path): 36 | try: 37 | with open(path, 'r') as file: 38 | data = file.read() 39 | return data 40 | except Exception as e: 41 | return None 42 | 43 | 44 | def save_string(string, file): 45 | try: 46 | file = open(file, "wt") 47 | file.write(string) 48 | file.close() 49 | except Exception as e: 50 | print(f"Error writing '{file}': {e}") 51 | 52 | 53 | def load_json(path): 54 | try: 55 | with open(path, 'r') as f: 56 | return json.load(f) 57 | except Exception as e: 58 | print("Error loading json: {}".format(e)) 59 | return None 60 | 61 | 62 | def get_shell_data_dir(): 63 | data_dir = "" 64 | home = os.getenv("HOME") 65 | xdg_data_home = os.getenv("XDG_DATA_HOME") 66 | 67 | if xdg_data_home: 68 | data_dir = os.path.join(xdg_data_home, "nwg-shell/") 69 | else: 70 | if home: 71 | data_dir = os.path.join(home, ".local/share/nwg-shell/") 72 | 73 | return data_dir 74 | 75 | 76 | def load_shell_data(): 77 | shell_data_file = os.path.join(get_shell_data_dir(), "data") 78 | shell_data = load_json(shell_data_file) if os.path.isfile(shell_data_file) else {} 79 | 80 | defaults = { 81 | "interface-locale": "" 82 | } 83 | 84 | for key in defaults: 85 | if key not in shell_data: 86 | shell_data[key] = defaults[key] 87 | 88 | return shell_data 89 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | nwg-shell logo 2 |

nwg-clipman


3 | 4 | This program is a part of the [nwg-shell](https://nwg-piotr.github.io/nwg-shell) project. 5 | 6 | **Nwg-clipman** is a GTK3-based GUI for Senan Kelly's [cliphist](https://github.com/sentriz/cliphist). It provides access to previously copied items, as well 7 | as management of the clipboard history from a window opened on gtk-layer-shell. The program is intended for use with 8 | sway, Hyprland and other wlroots-based Wayland compositors. 9 | 10 | screenshot
11 | 12 | ## Features 13 | 14 | - image & text history item preview; 15 | - searching clipboard history by a textual phrase; 16 | - deleting selected history item; 17 | - clearing clipboard history. 18 | 19 | ## Dependencies 20 | 21 | - python >= 3.6; 22 | - python-gobject; 23 | - gtk3; 24 | - gtk-layer-shell; 25 | - wl-clipboard; 26 | - cliphist; 27 | - xdg-utils. 28 | 29 | ## Installation 30 | 31 | [![Packaging status](https://repology.org/badge/vertical-allrepos/nwg-clipman.svg)](https://repology.org/project/nwg-clipman/versions) 32 | 33 | The program may be installed by cloning this repository and executing the `install.sh` script (make sure you installed 34 | dependencies first). Then you need to edit your compositor config file, to enable storing clipboard history to cliphist. 35 | 36 | Example for sway: 37 | 38 | ```text 39 | exec wl-paste --type text --watch cliphist store 40 | exec wl-paste --type image --watch cliphist store 41 | ``` 42 | 43 | Example for Hyprland: 44 | 45 | ```text 46 | exec-once = wl-paste --type text --watch cliphist store 47 | exec-once = wl-paste --type image --watch cliphist store 48 | ``` 49 | 50 | You may omit the second line if you don't want images to be remembered. 51 | 52 | Then create a key binding to launch nwg-clipman: 53 | 54 | Example for sway: 55 | 56 | ```text 57 | bindsym Mod1+C exec nwg-clipman 58 | ``` 59 | 60 | Example for Hyprland: 61 | 62 | ```text 63 | bind = ALT, C, exec, nwg-clipman 64 | ``` 65 | 66 | ## Options 67 | 68 | ```text 69 | ❯ nwg-clipman -h 70 | usage: nwg-clipman [-h] [-v] [-n] [-w] 71 | 72 | options: 73 | -h, --help show this help message and exit 74 | -v, --version display Version information 75 | -n, --numbers show item Numbers in the list 76 | -w, --window run in regular Window, w/o layer shell 77 | ``` 78 | 79 | ## Hints 80 | 81 | - To see numbers in the cliboard history use the `nwg-clipman -n` command. 82 | - If you'd like the window to open normally, not on the layer shell, use the `nwg-clipman -w` command. 83 | - You may clear the search entry / close the program window with the `Esc` key. 84 | -------------------------------------------------------------------------------- /nwg-clipman.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | 32 | 34 | 39 | 40 | 44 | 49 | 50 | 54 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /nwg_clipman/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ 4 | nwg-shell clipboard manager (GUI for cliphist by Senan Kelly) 5 | Copyright (c) 2024-2025 Piotr Miller & Contributors 6 | e-mail: nwg.piotr@gmail.com 7 | Project: https://github.com/nwg-piotr/nwg-clipman 8 | License: MIT 9 | """ 10 | 11 | import argparse 12 | import os.path 13 | import signal 14 | import sys 15 | 16 | import gi 17 | 18 | gi.require_version('Gtk', '3.0') 19 | try: 20 | gi.require_version('GtkLayerShell', '0.1') 21 | except ValueError: 22 | raise RuntimeError('\n\n' + 23 | 'If you haven\'t installed GTK Layer Shell, you need to point Python to the\n' + 24 | 'library by setting GI_TYPELIB_PATH and LD_LIBRARY_PATH to /src/.\n' + 25 | 'For example you might need to run:\n\n' + 26 | 'GI_TYPELIB_PATH=build/src LD_LIBRARY_PATH=build/src python3 ' + ' '.join(sys.argv)) 27 | 28 | from nwg_clipman.tools import * 29 | from nwg_clipman.__about__ import __version__ 30 | from gi.repository import Gtk, Gdk, GtkLayerShell, GdkPixbuf, Pango 31 | 32 | dir_name = os.path.dirname(__file__) 33 | pid = os.getpid() 34 | args = None 35 | voc = {} 36 | tmp_file = os.path.join(temp_dir(), "clipman.dump") 37 | window = None 38 | search_entry = None 39 | flowbox_wrapper = None 40 | flowbox = None 41 | preview_frame = None 42 | btn_copy = None 43 | selected_item = None 44 | exit_code = None 45 | 46 | if not is_command("cliphist") or not is_command("wl-copy"): 47 | # die if dependencies check failed 48 | eprint("Dependencies (cliphist, wl-clipboard) check failed, terminating") 49 | sys.exit(1) 50 | 51 | 52 | def list_cliphist(): 53 | # query cliphist 54 | try: 55 | output = subprocess.check_output("cliphist list", shell=True).decode('utf-8', errors="ignore").splitlines() 56 | return output 57 | except subprocess.CalledProcessError: 58 | return [] 59 | 60 | 61 | def quit_with_exit_code(ec): 62 | global exit_code 63 | exit_code = ec 64 | Gtk.main_quit() 65 | 66 | 67 | def signal_handler(sig, frame): 68 | # gentle handle termination 69 | desc = {2: "SIGINT", 15: "SIGTERM"} 70 | if sig == 2 or sig == 15: 71 | eprint("Terminated with {}".format(desc[sig])) 72 | quit_with_exit_code(128 + sig) 73 | 74 | 75 | def terminate_old_instance(): 76 | pid_file = os.path.join(temp_dir(), "nwg-clipman-pid") 77 | if os.path.isfile(pid_file): 78 | try: 79 | old_pid = int(load_text_file(pid_file)) 80 | if old_pid != pid: 81 | print(f"Attempting to kill the old instance in case it's still running, pid: {old_pid}") 82 | os.kill(old_pid, signal.SIGINT) 83 | except: 84 | pass 85 | # save new pid 86 | save_string(str(pid), pid_file) 87 | 88 | 89 | def handle_keyboard(win, event): 90 | # on Esc key first search entry if not empty, else terminate 91 | global search_entry 92 | if event.type == Gdk.EventType.KEY_RELEASE and event.keyval == Gdk.KEY_Escape: 93 | if search_entry.get_text(): 94 | search_entry.set_text("") 95 | else: 96 | quit_with_exit_code(1) 97 | 98 | 99 | def load_vocabulary(): 100 | # translate UI 101 | global voc 102 | # basic vocabulary (en_US) 103 | voc = load_json(os.path.join(dir_name, "langs", "en_US.json")) 104 | if not voc: 105 | eprint("Failed loading vocabulary, terminating") 106 | sys.exit(1) 107 | 108 | # check "interface-locale" forced in nwg-shell data file - if forced, and the file exists 109 | shell_data = load_shell_data() 110 | 111 | lang = os.getenv("LANG") 112 | if lang is None: 113 | lang = "en_US" 114 | else: 115 | lang = lang.split(".")[0] if not shell_data["interface-locale"] else shell_data["interface-locale"] 116 | 117 | # translate if translation available 118 | if lang != "en_US": 119 | loc_file = os.path.join(dir_name, "langs", "{}.json".format(lang)) 120 | if os.path.isfile(loc_file): 121 | # localized vocabulary 122 | loc = load_json(loc_file) 123 | if not loc: 124 | eprint(f"Failed loading translation '{loc_file}'") 125 | else: 126 | print(f"Loaded translation file: '{loc_file}'") 127 | for key in loc: 128 | voc[key] = loc[key] 129 | else: 130 | eprint(f"Translation into {lang} not found") 131 | 132 | 133 | def on_enter_notify_event(widget, event): 134 | # highlight item 135 | widget.set_state_flags(Gtk.StateFlags.DROP_ACTIVE, clear=False) 136 | widget.set_state_flags(Gtk.StateFlags.SELECTED, clear=False) 137 | 138 | 139 | def on_leave_notify_event(widget, event): 140 | # clear highlight 141 | widget.unset_state_flags(Gtk.StateFlags.DROP_ACTIVE) 142 | widget.unset_state_flags(Gtk.StateFlags.SELECTED) 143 | 144 | 145 | def flowbox_filter(_search_entry): 146 | # filter flowbox visibility by search_entry content 147 | def filter_func(fb_child, _text): 148 | if _text in fb_child.get_name(): 149 | return True 150 | else: 151 | return False 152 | 153 | text = _search_entry.get_text() 154 | flowbox.set_filter_func(filter_func, text) 155 | 156 | 157 | def on_child_activated(fb, child): 158 | # on flowbox item clicked 159 | global selected_item 160 | selected_item = child.get_name() 161 | name = bytes(child.get_name(), 'utf-8') 162 | subprocess.run(f"cliphist decode > {tmp_file}", shell=True, input=name) 163 | preview() 164 | 165 | 166 | def preview(): 167 | # create preview frame content 168 | pixbuf = None 169 | try: 170 | pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(tmp_file, 256, 256) 171 | except Exception as e: 172 | pass 173 | 174 | for child in preview_frame.get_children(): 175 | child.destroy() 176 | preview_frame.set_label(voc["preview"]) 177 | 178 | if pixbuf: 179 | scrolled = Gtk.ScrolledWindow() 180 | scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) 181 | scrolled.set_min_content_height(256) 182 | preview_frame.add(scrolled) 183 | 184 | image = Gtk.Image.new_from_pixbuf(pixbuf) 185 | scrolled.add(image) 186 | else: 187 | scrolled = Gtk.ScrolledWindow() 188 | scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) 189 | scrolled.set_min_content_height(256) 190 | preview_frame.add(scrolled) 191 | 192 | text = load_text_file(tmp_file) 193 | if not text: 194 | text = voc["preview-unavailable"] 195 | label = Gtk.Label.new(text) 196 | label.set_max_width_chars(80) 197 | label.set_line_wrap(True) 198 | label.set_line_wrap_mode(Pango.WrapMode.CHAR) 199 | 200 | scrolled.add(label) 201 | scrolled.set_property("name", "preview-window") 202 | 203 | btn_copy.set_sensitive(True) 204 | preview_frame.show_all() 205 | 206 | 207 | def on_del_button(btn, name): 208 | # delete entry from cliphist 209 | eprint(f"Delete '{name}'") 210 | name = bytes(name, 'utf-8') 211 | subprocess.run("cliphist delete", shell=True, input=name) 212 | search_entry.set_text("") 213 | build_flowbox() 214 | 215 | 216 | def on_copy_button(btn): 217 | eprint(f"Copying: '{selected_item}'") 218 | name = bytes(selected_item, 'utf-8') 219 | subprocess.run("cliphist decode | wl-copy", shell=True, input=name) 220 | Gtk.main_quit() 221 | 222 | 223 | def on_wipe_button(btn): 224 | win = ConfirmationWindow() 225 | 226 | 227 | class ConfirmationWindow(Gtk.Window): 228 | def __init__(self): 229 | Gtk.Window.__init__(self, type=Gtk.WindowType.POPUP) 230 | self.set_modal(True) 231 | self.set_destroy_with_parent(True) 232 | 233 | self.connect("key-release-event", self.handle_keyboard) 234 | 235 | GtkLayerShell.init_for_window(self) 236 | GtkLayerShell.set_layer(self, GtkLayerShell.Layer.OVERLAY) 237 | 238 | vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0) 239 | vbox.set_property("name", "warning") 240 | self.add(vbox) 241 | lbl = Gtk.Label.new(f'{voc["clear-clipboard-history"]}?') 242 | vbox.pack_start(lbl, False, False, 6) 243 | hbox = Gtk.Box.new(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) 244 | 245 | vbox.pack_start(hbox, False, False, 6) 246 | btn = Gtk.Button.new_with_label(voc["clear"]) 247 | btn.connect("clicked", self.clear_history) 248 | hbox.pack_start(btn, False, False, 0) 249 | 250 | btn = Gtk.Button.new_with_label(voc["close"]) 251 | btn.connect("clicked", self.quit) 252 | hbox.pack_start(btn, False, False, 0) 253 | 254 | self.show_all() 255 | 256 | def quit(self, btn): 257 | self.destroy() 258 | 259 | def handle_keyboard(self, win, event): 260 | if event.type == Gdk.EventType.KEY_RELEASE and event.keyval == Gdk.KEY_Escape: 261 | self.destroy() 262 | 263 | def clear_history(self, btn): 264 | eprint("Wipe cliphist") 265 | subprocess.run("cliphist wipe", shell=True) 266 | 267 | quit_with_exit_code(2) 268 | 269 | 270 | class FlowboxItem(Gtk.Box): 271 | def __init__(self, parts): 272 | Gtk.EventBox.__init__(self, orientation=Gtk.Orientation.HORIZONTAL, spacing=6) 273 | 274 | if args.numbers: 275 | label = Gtk.Label.new(parts[0]) 276 | self.add(label) 277 | 278 | eb = Gtk.EventBox() 279 | self.pack_start(eb, True, True, 0) 280 | name = parts[1] 281 | label = Gtk.Label.new(name) 282 | label.set_max_width_chars(80) 283 | label.set_line_wrap(True) 284 | label.set_property("halign", Gtk.Align.START) 285 | eb.add(label) 286 | 287 | button = Gtk.Button.new_from_icon_name("edit-clear", Gtk.IconSize.MENU) 288 | button.set_property("name", "del-btn") 289 | button.set_property("margin-right", 9) 290 | button.connect("clicked", on_del_button, "\t".join(parts)) 291 | self.pack_end(button, False, False, 0) 292 | 293 | eb.connect('enter-notify-event', on_enter_notify_event) 294 | eb.connect('leave-notify-event', on_leave_notify_event) 295 | 296 | 297 | def build_flowbox(): 298 | global flowbox_wrapper 299 | global flowbox 300 | # destroy flowbox wrapper content, if any 301 | for item in flowbox_wrapper.get_children(): 302 | item.destroy() 303 | 304 | # build from scratch 305 | scrolled = Gtk.ScrolledWindow() 306 | scrolled.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) 307 | scrolled.set_min_content_height(300) 308 | scrolled.set_propagate_natural_height(True) 309 | flowbox_wrapper.add(scrolled) 310 | 311 | flowbox = Gtk.FlowBox() 312 | flowbox.set_selection_mode(Gtk.SelectionMode.NONE) 313 | flowbox.connect("child-activated", on_child_activated) 314 | flowbox.set_valign(Gtk.Align.START) 315 | flowbox.set_max_children_per_line(1) 316 | scrolled.add(flowbox) 317 | 318 | # query cliphist 319 | clip_hist = list_cliphist() 320 | 321 | for line in clip_hist: 322 | try: 323 | parts = line.split("\t") 324 | _name = parts[1] 325 | 326 | item = FlowboxItem(parts) 327 | child = Gtk.FlowBoxChild() 328 | # we will be filtering by _name 329 | child.set_name(line) 330 | child.add(item) 331 | flowbox.add(child) 332 | except IndexError: 333 | eprint(f"Error parsing line: {line}") 334 | 335 | flowbox_wrapper.show_all() 336 | 337 | 338 | def main(): 339 | # handle signals 340 | catchable_sigs = set(signal.Signals) - {signal.SIGKILL, signal.SIGSTOP} 341 | for sig in catchable_sigs: 342 | signal.signal(sig, signal_handler) 343 | 344 | # arguments 345 | parser = argparse.ArgumentParser() 346 | parser.add_argument("-v", "--version", action="version", 347 | version="%(prog)s version {}".format(__version__), 348 | help="display Version information") 349 | parser.add_argument("-n", "--numbers", action="store_true", help="show item Numbers in the list") 350 | parser.add_argument("-w", "--window", action="store_true", help="run in regular Window, w/o layer shell") 351 | 352 | global args 353 | args = parser.parse_args() 354 | 355 | # kill running instance, if any 356 | terminate_old_instance() 357 | 358 | global search_entry 359 | 360 | # UI strings localization 361 | load_vocabulary() 362 | 363 | global window 364 | window = Gtk.Window.new(Gtk.WindowType.TOPLEVEL) 365 | 366 | def exit_1(widget): 367 | quit_with_exit_code(1) 368 | 369 | if not args.window: 370 | # attach to gtk-layer-shell 371 | GtkLayerShell.init_for_window(window) 372 | GtkLayerShell.set_layer(window, GtkLayerShell.Layer.TOP) 373 | GtkLayerShell.set_exclusive_zone(window, 0) 374 | GtkLayerShell.set_keyboard_mode(window, GtkLayerShell.KeyboardMode.ON_DEMAND) 375 | 376 | window.connect('destroy', exit_1) 377 | window.connect("key-release-event", handle_keyboard) 378 | 379 | vbox = Gtk.Box.new(orientation=Gtk.Orientation.VERTICAL, spacing=6) 380 | vbox.set_property("name", "main-wrapper") 381 | window.add(vbox) 382 | 383 | hbox = Gtk.Box.new(Gtk.Orientation.HORIZONTAL, 0) 384 | vbox.pack_start(hbox, False, False, 0) 385 | 386 | # search entry 387 | search_entry = Gtk.SearchEntry() 388 | search_entry.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, "edit-clear-symbolic") 389 | search_entry.set_property("hexpand", True) 390 | search_entry.set_property("margin", 12) 391 | search_entry.set_size_request(700, 0) 392 | search_entry.connect('search_changed', flowbox_filter) 393 | hbox.pack_start(search_entry, False, True, 0) 394 | 395 | # "Clear" button next to search entry 396 | btn = Gtk.Button.new_with_label(voc["clear"]) 397 | btn.set_property("valign", Gtk.Align.CENTER) 398 | btn.connect("clicked", on_wipe_button) 399 | hbox.pack_start(btn, False, False, 6) 400 | 401 | # wrapper for the flowbox 402 | global flowbox_wrapper 403 | flowbox_wrapper = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=0) 404 | flowbox_wrapper.set_property("margin-left", 12) 405 | flowbox_wrapper.set_property("margin-right", 12) 406 | vbox.pack_start(flowbox_wrapper, False, False, 0) 407 | 408 | # clear flowbox wrapper, build content 409 | build_flowbox() 410 | 411 | hbox = Gtk.Box.new(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) 412 | hbox.set_property("margin", 12) 413 | vbox.pack_end(hbox, False, False, 0) 414 | 415 | global preview_frame 416 | preview_frame = Gtk.Frame.new(voc["preview"]) 417 | hbox.pack_start(preview_frame, True, True, 0) 418 | 419 | # temporary placeholder for future content preview 420 | placeholder_box = Gtk.Box.new(orientation=Gtk.Orientation.HORIZONTAL, spacing=0) 421 | preview_frame.add(placeholder_box) 422 | placeholder_box.set_size_request(0, 256) 423 | preview_frame.show_all() 424 | 425 | # "Clear" and "Close" buttons 426 | ibox = Gtk.Box.new(orientation=Gtk.Orientation.HORIZONTAL, spacing=6) 427 | ibox.set_homogeneous(True) 428 | hbox.pack_end(ibox, False, False, 0) 429 | 430 | # "Copy" button 431 | global btn_copy 432 | btn_copy = Gtk.Button.new_with_label(voc["copy"]) 433 | btn_copy.set_property("valign", Gtk.Align.END) 434 | btn_copy.connect("clicked", on_copy_button) 435 | btn_copy.set_sensitive(False) 436 | ibox.pack_start(btn_copy, True, True, 0) 437 | 438 | # "Close" button 439 | btn = Gtk.Button.new_with_label(voc["close"]) 440 | btn.set_property("valign", Gtk.Align.END) 441 | btn.connect("clicked", exit_1) 442 | ibox.pack_start(btn, True, True, 0) 443 | 444 | window.show_all() 445 | 446 | # customize styling 447 | screen = Gdk.Screen.get_default() 448 | provider = Gtk.CssProvider() 449 | style_context = Gtk.StyleContext() 450 | style_context.add_provider_for_screen(screen, provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) 451 | 452 | css = b""" 453 | #main-wrapper { background-color: rgba(0, 0, 0, 0.1) } 454 | #preview-window { padding: 0 6px 0 6px } 455 | #del-btn { background: none; border: none; margin: 0; padding: 0 } 456 | #del-btn:hover { background-color: rgba(255, 255, 255, 0.1) } 457 | #warning { border: solid 1px; padding: 24px; margin: 6px} 458 | """ 459 | provider.load_from_data(css) 460 | 461 | Gtk.main() 462 | 463 | 464 | if __name__ == "__main__": 465 | main() 466 | sys.exit(exit_code) 467 | --------------------------------------------------------------------------------