├── 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 |
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 | 
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 | [](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 |
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 |
--------------------------------------------------------------------------------