├── .gitmodules
├── QlipMon.pro
├── README.md
├── common.pri
├── qlipmon.xml
├── rofi
├── config.cpp
├── config.h
├── qlipdata.cpp
├── qlipdata.h
├── qlipmon_interface.cpp
├── qlipmon_interface.h
├── rofi-qlipmon_global.h
├── rofi.pro
└── rofiqlipmon.cpp
├── server
├── config.cpp
├── config.h
├── database.cpp
├── database.h
├── database_entry.cpp
├── database_entry.h
├── dbus.cpp
├── dbus.h
├── main.cpp
├── qlipmon-server.service
├── qlipmon.cpp
├── qlipmon.h
├── qlipmon_adaptor.cpp
├── qlipmon_adaptor.h
└── server.pro
└── xml2cpp.sh
/.gitmodules:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vcalv/qlipmon/505c54f59b3d84344e680370ae3d2826ead4592a/.gitmodules
--------------------------------------------------------------------------------
/QlipMon.pro:
--------------------------------------------------------------------------------
1 | include(./common.pri)
2 |
3 | TEMPLATE = subdirs
4 | SUBDIRS = \
5 | server \
6 | rofi
7 |
8 | OTHER_FILES += qlipmon.xml arch/PKGBUILD
9 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Qlipmon
2 |
3 | ## Description
4 |
5 | Clipboard history saver with native rofi plugin and dbus interface.
6 |
7 | There's two components:
8 |
9 | * A server that runs in the background (run from startx or via `systemctl --user (start/enable/disable) qlipmon-server.service`) and saves every clipboard selection in memory
10 | * A rofi plugin that allows you to select previous clipboard selections
11 |
12 | Configuration for the server can be done either via the command line (`qlipmon --help`) or via ini file stored in `$HOME/.config/qlipmon/server.ini`
13 |
14 | The rofi plugin can only be configured via ini file stored in `$HOME/.config/qlipmon/rofi.ini`
15 |
16 | The rofi plugin can be selected by running `rofi -modi qlipmon -show qlipmon`.
17 |
18 | After running each of the components the ini files should be populated with sensible default values.
19 |
20 | There is also a dbus interface that allows you get get previous selections or even change the current one.
21 |
22 | Every time a new selection is made, a dbus broadcast is also emitted allowing you to listen for these event and take any action you want.
23 | Selection broadcast is disabled by default and can be enabled via command line, ini configuration or via dbus interface.
24 |
25 | Selections are saved in memory only and for now no persistence is supported,
26 |
27 |
28 | ## Rationale
29 |
30 | After searching for a rofi plugin for clipboard management I didn't find any that worked properly.
31 |
32 | * In some, the selections were truncated and distinct selections with equal truncations were considered to be identical.
33 | * In others there was no real integration with rofi, meaning you couldn't run it as part of combi for example.
34 | * Others were constantly polling the selection buffer and did not employ an asynchronous approach
35 | * I also wanted the ability to get notified of new selections and take any actions I deemed appropriate. A dbus interface seemed perfect for this.
36 |
37 |
38 | Built using [QT](https://qt.io)
39 |
--------------------------------------------------------------------------------
/common.pri:
--------------------------------------------------------------------------------
1 | isEmpty(QLIPMON_DBUS_FQDN){
2 | QLIPMON_DBUS_FQDN = org.qlipmon
3 | }
4 |
5 | isEmpty(QLIPMON_DBUS_PATH){
6 | QLIPMON_DBUS_PATH = /Qlipmon
7 | }
8 |
9 | isEmpty(PREFIX){
10 | PREFIX = /usr
11 | }
12 |
13 |
14 |
15 | DEFINES += QLIPMON_DBUS_FQDN='\'"$$QLIPMON_DBUS_FQDN"\''
16 | DEFINES += QLIPMON_DBUS_PATH='\'"$$QLIPMON_DBUS_PATH"\''
17 |
18 | #disable qDebug() in release builds
19 | CONFIG(release, debug|release):DEFINES += QT_NO_DEBUG_OUTPUT
20 |
--------------------------------------------------------------------------------
/qlipmon.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/rofi/config.cpp:
--------------------------------------------------------------------------------
1 | #include "config.h"
2 |
3 | #include
4 | #include
5 |
6 | static QSettings getSettings(){
7 | //QSettings settings(path, QSettings::Format::NativeFormat);
8 | return QSettings("qlipmon", "rofi");
9 | }
10 |
11 | void Config::load(){
12 | QSettings settings = getSettings();
13 | qDebug()<<"loading settings from file "<
5 |
6 | class Config{
7 | public:
8 | bool duplicates = false;
9 | int kind = -1; // -1 => all;
10 | int numberEntries = 0; // infinite
11 |
12 | //void load(const QString& path);
13 | void load();
14 | //void save(const QString& path);
15 | void save();
16 |
17 | ~Config();
18 | };
19 |
20 | QDebug &operator<<(QDebug &out, const Config &c);
21 |
22 | #endif // CONFIG_H
23 |
--------------------------------------------------------------------------------
/rofi/qlipdata.cpp:
--------------------------------------------------------------------------------
1 | #include "qlipdata.h"
2 |
3 | #include "config.h"
4 |
5 |
6 | RofiData* QlipData::getEntries(){
7 | Config config;
8 | RofiData *ret = new RofiData;
9 |
10 | config.load();
11 | qDebug()<<"loaded config "<error = false;
18 | ret->errorString = "";
19 | ret->entries = reply.value();
20 | auto &entries = ret->entries;
21 |
22 | qDebug()<<"Got a list with "< 0 && entries.size() > config.numberEntries){
24 | entries.erase(entries.begin() + config.numberEntries, entries.end());
25 | }
26 | }else{
27 | qWarning()<<"Error getting clipboard data: "<error = true;
29 | ret->errorString = reply.error().message();
30 | }
31 |
32 | return ret;
33 | }
34 |
35 | void QlipData::setText(const QString &txt){
36 | QlipMonInterface _interface(QLIPMON_DBUS_FQDN, QLIPMON_DBUS_PATH, QDBusConnection::sessionBus(), 0);
37 | Config config;
38 | config.load();
39 |
40 | _interface.setText(txt, config.kind);
41 | }
42 |
--------------------------------------------------------------------------------
/rofi/qlipdata.h:
--------------------------------------------------------------------------------
1 | #ifndef QLIPDATA_H
2 | #define QLIPDATA_H
3 |
4 | #include
5 |
6 | extern "C"{
7 | #include
8 | }
9 |
10 | #include "qlipmon_interface.h"
11 |
12 | class RofiData{
13 | public:
14 | QStringList entries;
15 | bool error;
16 | QString errorString;
17 | };
18 |
19 | class QlipData
20 | {
21 | public:
22 | static RofiData* getEntries();
23 | static void setText(const QString &txt);
24 | };
25 |
26 | #endif // QLIPDATA_H
27 |
--------------------------------------------------------------------------------
/rofi/qlipmon_interface.cpp:
--------------------------------------------------------------------------------
1 | /*
2 | * This file was generated by qdbusxml2cpp version 0.8
3 | * Command line was: qdbusxml2cpp -V -i qlipmon_interface.h --classname QlipMonInterface -p :qlipmon_interface.cpp ../qlipmon.xml
4 | *
5 | * qdbusxml2cpp is Copyright (C) 2020 The Qt Company Ltd.
6 | *
7 | * This is an auto-generated file.
8 | * This file may have been hand-edited. Look for HAND-EDIT comments
9 | * before re-generating it.
10 | */
11 |
12 | #include "qlipmon_interface.h"
13 | /*
14 | * Implementation of interface class QlipMonInterface
15 | */
16 |
17 | QlipMonInterface::QlipMonInterface(const QString &service, const QString &path, const QDBusConnection &connection, QObject *parent)
18 | : QDBusAbstractInterface(service, path, staticInterfaceName(), connection, parent)
19 | {
20 | }
21 |
22 | QlipMonInterface::~QlipMonInterface()
23 | {
24 | }
25 |
26 |
--------------------------------------------------------------------------------
/rofi/qlipmon_interface.h:
--------------------------------------------------------------------------------
1 | /*
2 | * This file was generated by qdbusxml2cpp version 0.8
3 | * Command line was: qdbusxml2cpp -V -i ../server/database_entry.h --classname QlipMonInterface -p qlipmon_interface.h: ../qlipmon.xml
4 | *
5 | * qdbusxml2cpp is Copyright (C) 2020 The Qt Company Ltd.
6 | *
7 | * This is an auto-generated file.
8 | * Do not edit! All changes made to it will be lost.
9 | */
10 |
11 | #ifndef QLIPMON_INTERFACE_H
12 | #define QLIPMON_INTERFACE_H
13 |
14 | #include
15 | #include
16 | #include
17 | #include
18 | #include
19 | #include
20 | #include
21 | #include
22 | #include "../server/database_entry.h"
23 |
24 | /*
25 | * Proxy class for interface QLIPMON_DBUS_FQDN
26 | */
27 | class QlipMonInterface: public QDBusAbstractInterface
28 | {
29 | Q_OBJECT
30 | public:
31 | static inline const char *staticInterfaceName()
32 | { return QLIPMON_DBUS_FQDN; }
33 |
34 | public:
35 | QlipMonInterface(const QString &service, const QString &path, const QDBusConnection &connection, QObject *parent = nullptr);
36 |
37 | ~QlipMonInterface();
38 |
39 | Q_PROPERTY(bool broadcast READ broadcast WRITE setBroadcast)
40 | inline bool broadcast() const
41 | { return qvariant_cast< bool >(property("broadcast")); }
42 | inline void setBroadcast(bool value)
43 | { setProperty("broadcast", QVariant::fromValue(value)); }
44 |
45 | public Q_SLOTS: // METHODS
46 | inline QDBusPendingReply > getHistory()
47 | {
48 | QList argumentList;
49 | return asyncCallWithArgumentList(QStringLiteral("getHistory"), argumentList);
50 | }
51 |
52 | inline QDBusPendingReply getLastText()
53 | {
54 | QList argumentList;
55 | return asyncCallWithArgumentList(QStringLiteral("getLastText"), argumentList);
56 | }
57 |
58 | inline QDBusPendingReply getLastText(int mode)
59 | {
60 | QList argumentList;
61 | argumentList << QVariant::fromValue(mode);
62 | return asyncCallWithArgumentList(QStringLiteral("getLastText"), argumentList);
63 | }
64 |
65 | inline QDBusPendingReply getTextHistory()
66 | {
67 | QList argumentList;
68 | return asyncCallWithArgumentList(QStringLiteral("getTextHistory"), argumentList);
69 | }
70 |
71 | inline QDBusPendingReply getTextHistory(int mode)
72 | {
73 | QList argumentList;
74 | argumentList << QVariant::fromValue(mode);
75 | return asyncCallWithArgumentList(QStringLiteral("getTextHistory"), argumentList);
76 | }
77 |
78 | inline QDBusPendingReply getTextHistory(int mode, bool duplicates)
79 | {
80 | QList argumentList;
81 | argumentList << QVariant::fromValue(mode) << QVariant::fromValue(duplicates);
82 | return asyncCallWithArgumentList(QStringLiteral("getTextHistory"), argumentList);
83 | }
84 |
85 | inline QDBusPendingReply<> setText(const QString &text)
86 | {
87 | QList argumentList;
88 | argumentList << QVariant::fromValue(text);
89 | return asyncCallWithArgumentList(QStringLiteral("setText"), argumentList);
90 | }
91 |
92 | inline QDBusPendingReply<> setText(const QString &text, int mode)
93 | {
94 | QList argumentList;
95 | argumentList << QVariant::fromValue(text) << QVariant::fromValue(mode);
96 | return asyncCallWithArgumentList(QStringLiteral("setText"), argumentList);
97 | }
98 |
99 | Q_SIGNALS: // SIGNALS
100 | void updated(const QString &text, int mode);
101 | };
102 |
103 | namespace DOMAIN {
104 | typedef ::QlipMonInterface DOMAIN;
105 | }
106 | #endif
107 |
--------------------------------------------------------------------------------
/rofi/rofi-qlipmon_global.h:
--------------------------------------------------------------------------------
1 | #ifndef ROFIQLIPMON_GLOBAL_H
2 | #define ROFIQLIPMON_GLOBAL_H
3 |
4 | #include
5 |
6 | #if defined(ROFIQLIPMON_LIBRARY)
7 | # define ROFIQLIPMON_EXPORT Q_DECL_EXPORT
8 | #else
9 | # define ROFIQLIPMON_EXPORT Q_DECL_IMPORT
10 | #endif
11 |
12 | #endif // ROFIQLIPMON_GLOBAL_H
13 |
--------------------------------------------------------------------------------
/rofi/rofi.pro:
--------------------------------------------------------------------------------
1 | include(../common.pri)
2 |
3 | TARGET = qlipmon
4 |
5 | QT = dbus
6 |
7 | TEMPLATE = lib
8 | DEFINES += ROFIQLIPMON_LIBRARY
9 |
10 | CONFIG += plugin
11 | CONFIG += no_plugin_name_prefix
12 | CONFIG += skip_target_version_ext
13 |
14 | #CONFIG += c++17 link_pkgconfig
15 | #PKGCONFIG += rofi
16 |
17 | CONFIG += c++17
18 |
19 | #ugly hack because qmake replaces -I/usb/include by -isystem /usr/include and breaks g++
20 | QMAKE_CXXFLAGS += $$system("pkgconf --cflags-only-I rofi")
21 |
22 | #I just need the headers, no need to link agains QtGUI
23 | QMAKE_CXXFLAGS += $$system("pkgconf --cflags-only-I Qt5Gui")
24 |
25 | # The following define makes your compiler emit warnings if you use
26 | # any Qt feature that has been marked deprecated (the exact warnings
27 | # depend on your compiler). Please consult the documentation of the
28 | # deprecated API in order to know how to port your code away from it.
29 | DEFINES += QT_DEPRECATED_WARNINGS
30 |
31 | # You can also make your code fail to compile if it uses deprecated APIs.
32 | # In order to do so, uncomment the following line.
33 | # You can also select to disable deprecated APIs only up to a certain version of Qt.
34 | #DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
35 |
36 | SOURCES += \
37 | qlipmon_interface.cpp \
38 | config.cpp \
39 | qlipdata.cpp \
40 | rofiqlipmon.cpp \
41 | ../server/database_entry.cpp \
42 |
43 | HEADERS += \
44 | qlipmon_interface.h \
45 | config.h \
46 | qlipdata.h \
47 | rofi-qlipmon_global.h \
48 |
49 | # Default rules for deployment.
50 | unix {
51 | target.path = $${PREFIX}/lib/rofi/
52 | }
53 | !isEmpty(target.path): INSTALLS += target
54 |
--------------------------------------------------------------------------------
/rofi/rofiqlipmon.cpp:
--------------------------------------------------------------------------------
1 | #include
2 |
3 | extern "C" {
4 | #include
5 | #include
6 | #include
7 | }
8 |
9 | #include "qlipdata.h"
10 |
11 | RofiData* RofiDataFromMode(const Mode *mode){
12 | return reinterpret_cast (mode_get_private_data(mode));
13 | }
14 |
15 | static char* QStringDupa(const QString& line){
16 | QByteArray ba = line.toLocal8Bit();
17 | return g_strdup(ba.data());
18 | }
19 |
20 |
21 | /**
22 | * @param mode The mode to initialize
23 | *
24 | * Initialize mode
25 | *
26 | * @returns FALSE if there was a failure, TRUE if successful
27 | */
28 | static int qlipmon_mode_init(Mode *sw) {
29 | if (mode_get_private_data(sw) == nullptr) {
30 | RofiData *data = QlipData::getEntries();
31 | mode_set_private_data(sw, reinterpret_cast(data));
32 | }
33 | return TRUE;
34 | }
35 |
36 | /**
37 | * @param mode The mode to query
38 | *
39 | * Get the number of entries in the mode.
40 | *
41 | * @returns an unsigned in with the number of entries.
42 | */
43 | static unsigned int qlipmon_mode_get_num_entries(const Mode *sw) {
44 | RofiData* data = RofiDataFromMode(sw);
45 | if (data->error){
46 | return 0;
47 | }else{
48 | return data->entries.size();
49 | }
50 | }
51 |
52 | /**
53 | * @param mode The mode to query
54 | * @param menu_retv The menu return value.
55 | * @param input Pointer to the user input string. [in][out]
56 | * @param selected_line the line selected by the user.
57 | *
58 | * Acts on the user interaction.
59 | *
60 | * @returns the next #ModeMode.
61 | */
62 | static ModeMode qlipmon_mode_result(
63 | Mode *sw,
64 | int mretv,
65 | char **input,
66 | unsigned int selected_line
67 | ) {
68 | ModeMode retv = MODE_EXIT;
69 | Q_UNUSED( sw )
70 | Q_UNUSED( input )
71 |
72 | RofiData* data = RofiDataFromMode(sw);
73 |
74 |
75 | if (mretv & MENU_NEXT) {
76 | retv = NEXT_DIALOG;
77 | } else if (mretv & MENU_PREVIOUS) {
78 | retv = PREVIOUS_DIALOG;
79 | } else if (mretv & MENU_QUICK_SWITCH) {
80 | retv = (ModeMode) (mretv & MENU_LOWER_MASK);
81 | } else if ((mretv & MENU_OK) ) {
82 | if(!data->error){
83 | const QString selected = data->entries.value(selected_line);
84 | qDebug()<<"Selected = " << selected;
85 | QlipData::setText(selected);
86 | }
87 | retv = MODE_EXIT;
88 | } else if ((mretv & MENU_ENTRY_DELETE) == MENU_ENTRY_DELETE) {
89 | retv = RELOAD_DIALOG;
90 | }
91 | return retv;
92 | }
93 |
94 | /**
95 | * @param mode The mode to destroy
96 | *
97 | * Destroy the mode
98 | */
99 | static void qlipmon_mode_destroy(Mode *sw) {
100 | RofiData* data = RofiDataFromMode(sw);
101 | if (data != nullptr) {
102 | delete data;
103 | }
104 | }
105 |
106 | /**
107 | * @param mode The mode to query
108 | *
109 | * Query the mode for a user display.
110 | *
111 | * @return a new allocated (valid pango markup) message to display (user should
112 | * free).
113 | */
114 | static char *qlipmon_get_message(const Mode *sw) {
115 | RofiData* data = RofiDataFromMode(sw);
116 | if(data->error){
117 | QString line = QString("QlipMon Error: ");
118 | line += QString("") + data->errorString + QString("");
119 | return QStringDupa(line);
120 | }else if( 0 == data->entries.size() ){
121 | QString line = QString("QlipMon: No clipboard history!");
122 | return QStringDupa(line);
123 | }else{
124 | return NULL;
125 | }
126 | }
127 |
128 | /**
129 | * @param mode The mode to query
130 | * @param selected_line The entry to query
131 | * @param state The state of the entry [out]
132 | * @param attribute_list List of extra (pango) attribute to apply when displaying. [out][null]
133 | * @param get_entry If the should be returned.
134 | *
135 | * Returns the string as it should be displayed for the entry and the state of how it should be displayed.
136 | *
137 | * @returns allocated new string and state when get_entry is TRUE otherwise just the state.
138 | */
139 | static char *get_display_value(
140 | const Mode *sw,
141 | unsigned int selected_line,
142 | int *state,
143 | GList **attr_list,
144 | int get_entry
145 | ) {
146 | Q_UNUSED( get_entry )
147 | Q_UNUSED( state )
148 | Q_UNUSED( attr_list )
149 | RofiData* data = RofiDataFromMode(sw);
150 |
151 | // Rofi is not yet exporting these constants in their headers
152 | // *state |= MARKUP;
153 | // https://github.com/DaveDavenport/rofi/blob/79adae77d72be3de96d1c4e6d53b6bae4cb7e00e/include/widgets/textbox.h#L104
154 | //*state |= 8;
155 |
156 | if(data->error){
157 | return NULL;
158 | }else{
159 | QString line = data->entries.value(selected_line);
160 | line.replace("\n", "⏎");
161 | line.replace("\t", "⭾");
162 | return QStringDupa(line);
163 | }
164 |
165 | }
166 |
167 | /**
168 | * @param sw The mode object.
169 | * @param tokens The tokens to match against.
170 | * @param index The index in this plugin to match against.
171 | *
172 | * Match the entry.
173 | *
174 | * @param returns try when a match.
175 | */
176 | static int qlipmon_token_match(const Mode *sw, rofi_int_matcher **tokens, unsigned int index) {
177 | RofiData* data = RofiDataFromMode(sw);
178 |
179 | if(data->error){
180 | // always display error message.
181 | // Mute since now no message is displayed in the ellement list
182 | // error message is displayed in the status bar
183 | return TRUE;
184 | }else{
185 | QByteArray ba = data->entries.value(index).toLocal8Bit();
186 | return helper_token_match(tokens, ba.data());
187 | }
188 | }
189 |
190 | cairo_surface_t * qlipmon_get_icon ( const Mode *mode, unsigned int selected_line, int height ){
191 | Q_UNUSED( mode )
192 | Q_UNUSED( selected_line )
193 | Q_UNUSED( height )
194 | return nullptr;
195 | }
196 |
197 | static char _name[] = "qlipmon";
198 |
199 | G_MODULE_EXPORT Mode mode = {
200 | .abi_version = ABI_VERSION,
201 | .name = _name,
202 | .cfg_name_key = {},
203 | //uncommenting this results in segfault
204 | .display_name = {},
205 | ._init = qlipmon_mode_init,
206 | ._destroy = qlipmon_mode_destroy,
207 | ._get_num_entries = qlipmon_mode_get_num_entries,
208 | ._result = qlipmon_mode_result,
209 | ._token_match = qlipmon_token_match,
210 | ._get_display_value = get_display_value,
211 | ._get_icon = nullptr,
212 | ._get_completion = nullptr,
213 | ._preprocess_input = nullptr,
214 | ._get_message = qlipmon_get_message,
215 | .private_data = nullptr,
216 | .free = nullptr,
217 | .ed = {},
218 | .module = {},
219 | };
220 |
--------------------------------------------------------------------------------
/server/config.cpp:
--------------------------------------------------------------------------------
1 | #include "config.h"
2 | #include
3 | #include
4 |
5 | static QSettings getSettings(){
6 | //QSettings settings(path, QSettings::Format::NativeFormat);
7 | return QSettings("qlipmon", "server");
8 | }
9 |
10 | void Config::loadArgs(const QStringList &args){
11 | load(); // load values from config
12 |
13 | QCommandLineParser parser;
14 | parser.setApplicationDescription("Monitors clipboard history");
15 | parser.addHelpOption();
16 | parser.addVersionOption();
17 |
18 | QCommandLineOption historyNumberOption(QStringList() << "n" << "limit-history");
19 | historyNumberOption.setDescription(QString("maximum number of elements saved (0 is no history, negative is infinite, default is ")+QString::number(numberEntries)+").");
20 | historyNumberOption.setValueName("number of elements");
21 | historyNumberOption.setDefaultValue(QString::number(numberEntries));
22 | parser.addOption(historyNumberOption);
23 |
24 | QCommandLineOption dbusOption(QStringList() << "d" << "dbus");
25 | dbusOption.setDescription("enable dbus");
26 | dbusOption.setValueName("true/false");
27 | dbusOption.setDefaultValue("true");
28 | parser.addOption(dbusOption);
29 |
30 | QCommandLineOption broadcastOption(QStringList() << "b" << "broadcast");
31 | broadcastOption.setDescription("broadcast new selection via dbus even");
32 | broadcastOption.setValueName("true/false");
33 | broadcastOption.setDefaultValue("false");
34 | parser.addOption(broadcastOption);
35 |
36 | QCommandLineOption saveOption(QStringList() << "s" << "save config");
37 | saveOption.setDescription("save command line values in config file");
38 | parser.addOption(saveOption);
39 |
40 | parser.process(args);
41 |
42 | const long _history_number = parser.value(historyNumberOption).toLong();
43 |
44 | if (parser.isSet(broadcastOption))
45 | broadcast = parser.value(broadcastOption).toLower() == "true";
46 |
47 | if (parser.isSet(dbusOption))
48 | dbus = parser.value(dbusOption).toLower() == "true";
49 |
50 | numberEntries = _history_number;
51 | qDebug()<<"Config after command line parsing = "<<*this;
52 |
53 | if (parser.isSet(saveOption))
54 | save();
55 | }
56 |
57 | void Config::loadArgs(int argc, char* argv[]){
58 | const QStringList args(argv, argv + argc);
59 | loadArgs(args);
60 | }
61 |
62 | void Config::load(){
63 | QSettings settings = getSettings();
64 | qDebug()<<"loading settings from file "<
5 |
6 | class Config{
7 | public:
8 | int numberEntries = 500;
9 | bool broadcast = true;
10 | bool dbus = true;
11 |
12 | //void load(const QString& path);
13 | void load();
14 | //void save(const QString& path);
15 | void save();
16 |
17 | void loadArgs(const QStringList &args);
18 | void loadArgs(int argc, char* argv[]);
19 | };
20 |
21 | QDebug &operator<<(QDebug &out, const Config &c);
22 |
23 |
24 | #endif // CONFIG_H
25 |
--------------------------------------------------------------------------------
/server/database.cpp:
--------------------------------------------------------------------------------
1 | #include "database.h"
2 | #include
3 | #include
4 | #include
5 | #include
6 | #include
7 | #include
8 | #include
9 | #include
10 |
11 | #include
12 |
13 | static QMutex __mtx;
14 | static QAtomicInt __count(0);
15 |
16 | /*
17 | TODO
18 | limit number of entries or space
19 | deal with duplicate entries better
20 | TTS ?
21 | */
22 |
23 |
24 | static QSqlDatabase __database(){
25 | return QSqlDatabase::database();
26 | }
27 |
28 | static bool __transaction(){
29 | return __database().transaction();
30 | }
31 |
32 | static bool __commit(){
33 | return __database().commit();
34 | }
35 |
36 | static bool __rollback(){
37 | return __database().rollback();
38 | }
39 |
40 | static database_entry entryFromQuery(QSqlQuery& query){
41 | database_entry ret;
42 | ret.id = query.value(0).toULongLong();
43 | ret.text = query.value(1).toString();
44 | ret.mode = QClipboard::Mode(query.value(2).toInt());
45 | ret.when = QDateTime::fromMSecsSinceEpoch(query.value(3).toLongLong());
46 |
47 | return ret;
48 | }
49 |
50 | static QList entriesFromSQL(const QString &sql){
51 | QSqlQuery query(sql);
52 | query.exec();
53 |
54 | if(!query.isActive())
55 | qWarning() << "SQL SELECT ERROR: " << query.lastError().text();
56 |
57 | QList ret;
58 |
59 | while(query.next()){
60 | ret.append(entryFromQuery(query));
61 | }
62 |
63 | return ret;
64 |
65 | }
66 |
67 | database::~database(){
68 | qDebug()<<"~database";
69 | QMutexLocker locker(&__mtx);
70 |
71 | if(0==--__count){
72 | qDebug()<<"Closing database";
73 | QSqlDatabase db = __database();
74 | db.close();
75 |
76 | if(db.open()){
77 | qCritical() << db.lastError();
78 | return;
79 | }
80 | }
81 |
82 | }
83 |
84 | database::database(const int _numberEntries){
85 | numberEntries = _numberEntries;
86 | QMutexLocker locker(&__mtx);
87 |
88 | if (__count){
89 | ++__count;
90 | qDebug()<<"Database already initalized [count = "<<__count<<"]";;
91 | return;
92 | }
93 |
94 | qDebug() << "Available QtSQL drivers:" << QSqlDatabase::drivers();
95 | const QString DRIVER("QSQLITE");
96 | QSqlDatabase db = QSqlDatabase::addDatabase(DRIVER);
97 | db.setDatabaseName(":memory:");
98 |
99 | if(!db.open()){
100 | qCritical() << db.lastError();
101 | return;
102 | }
103 |
104 | const QStringList DDLs ={
105 | "CREATE TABLE texts ("
106 | "id INTEGER PRIMARY KEY AUTOINCREMENT,"
107 | "text TEXT NOT NULL"
108 | ");",
109 | "CREATE TABLE pastes ("
110 | "id INTEGER PRIMARY KEY AUTOINCREMENT,"
111 | "text_id INTEGER REFERENCES texts(id) ON DELETE CASCADE,"
112 | "mode INTEGER NOT NULL,"
113 | "ts INTEGER DEFAULT NULL"
114 | ");",
115 | "CREATE UNIQUE INDEX idx_texts_text ON texts(text);"
116 | };
117 |
118 | for (const QString& DDL: DDLs){
119 | QSqlQuery query(DDL);
120 | if(!query.isActive()){
121 | qWarning() << "SQL CREATE ERROR: " << query.lastError().text();
122 | return;
123 | }
124 | }
125 |
126 | ++__count;;
127 |
128 | }
129 |
130 | void database::_save(QString text, QClipboard::Mode mode){
131 | qDebug()<<"save("< database::getDuplicateEntries(){
241 | return entriesFromSQL("SELECT pastes.id as id, text, mode, ts FROM pastes JOIN texts ON (pastes.text_id=texts.id) ORDER BY id DESC");
242 | }
243 |
244 | QList database::getUniqueEntries(){
245 | return entriesFromSQL(
246 | "SELECT pastes.id as id, text, mode, ts FROM pastes "
247 | "JOIN texts ON (pastes.text_id=texts.id) "
248 | "WHERE pastes.id IN ("
249 | "SELECT MAX(id) FROM pastes GROUP BY (text_id)"
250 | ")"
251 | "ORDER BY id DESC;"
252 | );
253 | }
254 |
255 |
256 | void database::__cleanup(){
257 | qDebug()<<"Cleaning up for "<
5 | #include
6 | #include
7 | #include
8 |
9 | #include "database_entry.h"
10 |
11 |
12 | class database
13 | {
14 | public:
15 |
16 | database(const int numberEntries=500);
17 | ~database();
18 |
19 | database_entry getLast();
20 | QList getUniqueEntries();
21 | QList getDuplicateEntries();
22 | QString getLast(QClipboard::Mode mode);
23 |
24 |
25 | protected:
26 | void _save(QString text, QClipboard::Mode mode);
27 |
28 | private:
29 | int numberEntries;
30 | void __cleanup();
31 |
32 | };
33 |
34 | #endif // DATABASE_H
35 |
--------------------------------------------------------------------------------
/server/database_entry.cpp:
--------------------------------------------------------------------------------
1 | #include "database_entry.h"
2 | #include
3 | #include
4 |
5 |
6 | /// Initializer
7 | class Init {
8 | public:
9 | Init() {
10 | qDebug()<<"Registering types";
11 | qRegisterMetaType("database_entry");
12 | qDBusRegisterMetaType();
13 |
14 | qRegisterMetaType>("list_database_entry");
15 | qDBusRegisterMetaType>();
16 | qDebug()<<"Types registered";
17 | }
18 | };
19 | static Init _initTypes;
20 |
21 | QDBusArgument &operator<<(QDBusArgument &argument, const database_entry &e ){
22 | argument.beginStructure();
23 | argument << e.id;
24 | argument << e.text;
25 | argument << e.mode;
26 | argument << e.when.toMSecsSinceEpoch();
27 | argument.endStructure();
28 | return argument;
29 | }
30 |
31 | const QDBusArgument &operator>>(const QDBusArgument &argument, database_entry &e )
32 | {
33 | qint64 when;
34 | int mode;
35 |
36 | argument.beginStructure();
37 | argument >> e.id;
38 | argument >> e.text;
39 | argument >> mode;
40 | argument >> when;
41 | argument.endStructure();
42 |
43 | e.mode = QClipboard::Mode(mode);
44 | e.when = QDateTime::fromMSecsSinceEpoch(when);
45 | return argument;
46 | }
47 |
48 | QDebug &operator<<(QDebug &out, const database_entry &e){
49 | out<<"database::entry{ id:"<
6 | #include
7 | #include
8 | #include
9 | #include
10 | #include
11 |
12 | #include
13 |
14 |
15 | struct database_entry{
16 | quint64 id;
17 | QString text;
18 | QClipboard::Mode mode;
19 | QDateTime when;
20 | };
21 |
22 | QDebug &operator<<(QDebug &out, const database_entry &entry);
23 | const QDBusArgument &operator>>(const QDBusArgument &argument, database_entry &e );
24 |
25 | Q_DECLARE_METATYPE( database_entry );
26 | Q_DECLARE_METATYPE( QList );
27 |
28 | #endif // DATABASE_ENTRY_H
29 |
--------------------------------------------------------------------------------
/server/dbus.cpp:
--------------------------------------------------------------------------------
1 | #include "dbus.h"
2 |
3 | Dbus::Dbus()
4 | {
5 |
6 | }
7 |
--------------------------------------------------------------------------------
/server/dbus.h:
--------------------------------------------------------------------------------
1 | #ifndef DBUS_H
2 | #define DBUS_H
3 |
4 | #include
5 |
6 | class Dbus
7 | {
8 | public:
9 | Dbus();
10 | };
11 |
12 | #endif // DBUS_H
13 |
--------------------------------------------------------------------------------
/server/main.cpp:
--------------------------------------------------------------------------------
1 | #include "qlipmon.h"
2 |
3 | #include
4 | #include
5 | #include
6 |
7 | class Application: public QApplication {
8 | private:
9 | QlipMon *qlipmon;
10 |
11 | public:
12 | Application(int argc, char* argv[]):
13 | QApplication(argc, argv),
14 | qlipmon(nullptr)
15 | {
16 |
17 | setApplicationName("QlipMon");
18 | setApplicationVersion("1.0");
19 |
20 | Config config;
21 | config.loadArgs(argc, argv);
22 | qlipmon = new QlipMon(config);
23 |
24 |
25 | }
26 |
27 | ~Application(){
28 | if (qlipmon != nullptr)
29 | delete qlipmon;
30 | }
31 | };
32 |
33 | int main(int argc, char *argv[])
34 | {
35 | Application app(argc, argv);
36 | return app.exec();
37 | }
38 |
--------------------------------------------------------------------------------
/server/qlipmon-server.service:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=Qlipmon clipboard manager for user %u
3 | After=graphical-session.target
4 |
5 | [Service]
6 | ExecStart=qlipmon-server
7 | Type=simple
8 | After=graphical-session.target
9 | Restart=on-failure
10 | RestartSec=5
11 | StartLimitInterval=60s
12 | StartLimitBurst=3
13 |
14 | [Install]
15 | WantedBy=default.target
16 |
--------------------------------------------------------------------------------
/server/qlipmon.cpp:
--------------------------------------------------------------------------------
1 | #include "qlipmon.h"
2 | #include "config.h"
3 | #include "qlipmon_adaptor.h"
4 |
5 | #include
6 | #include
7 |
8 | #include
9 | #include
10 | #include
11 |
12 |
13 | QlipMon::QlipMon(const Config& config, QObject *parent): QObject(parent), database(config.numberEntries){
14 | QClipboard* clip = QGuiApplication::clipboard();
15 | QObject::connect(clip, &QClipboard::changed, this, &QlipMon::changed);
16 |
17 | if(config.dbus){
18 | new QlipmonAdaptor(this);
19 | QDBusConnection connection = QDBusConnection::sessionBus();
20 | connection.registerObject(QLIPMON_DBUS_PATH, this);
21 | connection.registerService(QLIPMON_DBUS_FQDN);
22 | }else{
23 | qWarning()<<"No DBUS interface. Why even run this?";
24 | }
25 |
26 | setProperty("broadcast", config.broadcast);
27 | }
28 |
29 | void QlipMon::changed(QClipboard::Mode mode){
30 | QString text = clip->text(mode);
31 |
32 | if(_broadcast){
33 | qDebug()<<"broadcasting";
34 | emit updated(text, (int)(mode));
35 | }else{
36 | qDebug()<<"NOT broadcasting";
37 | }
38 |
39 | _save(text, mode);
40 | }
41 |
42 | QString QlipMon::getLastText(int mode){
43 | return database::getLast(QClipboard::Mode(mode));
44 | }
45 |
46 | QStringList QlipMon::getTextHistory(int _mode, bool duplicates){
47 | const auto mode = QClipboard::Mode(_mode);
48 | QStringList ret;
49 |
50 | auto entries = duplicates ? database::getDuplicateEntries(): database::getUniqueEntries();
51 |
52 | for (const database_entry &entry : entries){
53 | if(_mode < 0 || entry.mode == mode){
54 | ret.append(entry.text);
55 | }
56 | }
57 | return ret;
58 | }
59 |
60 | QList QlipMon::getHistory(){
61 | return database::getUniqueEntries();
62 | }
63 |
64 | void QlipMon::setText(const QString &text, int mode){
65 | qDebug()<<"setText("<setText(text, QClipboard::Mode::Clipboard);
70 | clip->setText(text, QClipboard::Mode::Selection);
71 | }else{
72 | clip->setText(text, QClipboard::Mode(mode));
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/server/qlipmon.h:
--------------------------------------------------------------------------------
1 | #ifndef QLIPMON_H
2 | #define QLIPMON_H
3 |
4 | #include "database.h"
5 | #include "database_entry.h"
6 | #include "config.h"
7 |
8 | #include
9 | #include
10 | #include
11 | #include
12 | #include
13 |
14 | class QlipMon : public QObject, public database
15 | {
16 | Q_OBJECT
17 | //Q_CLASSINFO("D-Bus Interface", QLIPMON_DBUS_FQDN)
18 | Q_PROPERTY(bool broadcast MEMBER _broadcast)
19 |
20 | public:
21 | //explicit QlipMon(QObject *parent = nullptr);
22 | explicit QlipMon(const Config& config, QObject *parent = nullptr);
23 |
24 | public slots:
25 | void setText(const QString &text, int mode=0);
26 | QString getLastText(int mode=0);
27 | QStringList getTextHistory(int mode=-1, bool duplicates=false);
28 | QList getHistory();
29 |
30 |
31 | signals:
32 | void updated(QString text, int mode);
33 |
34 | private:
35 | QClipboard* clip;
36 | bool _broadcast;
37 |
38 | private slots:
39 | void changed(QClipboard::Mode mode);
40 |
41 | };
42 |
43 |
44 | #endif // QLIPMON_H
45 |
--------------------------------------------------------------------------------
/server/qlipmon_adaptor.cpp:
--------------------------------------------------------------------------------
1 | /*
2 | * This file was generated by qdbusxml2cpp version 0.8
3 | * Command line was: qdbusxml2cpp -V -c QlipmonAdaptor -i qlipmon_adaptor.h -a :qlipmon_adaptor.cpp ../qlipmon.xml
4 | *
5 | * qdbusxml2cpp is Copyright (C) 2020 The Qt Company Ltd.
6 | *
7 | * This is an auto-generated file.
8 | * Do not edit! All changes made to it will be lost.
9 | */
10 |
11 | #include "qlipmon_adaptor.h"
12 | #include
13 | #include
14 | #include
15 | #include
16 | #include
17 | #include
18 | #include
19 |
20 | /*
21 | * Implementation of adaptor class QlipmonAdaptor
22 | */
23 |
24 | QlipmonAdaptor::QlipmonAdaptor(QObject *parent)
25 | : QDBusAbstractAdaptor(parent)
26 | {
27 | // constructor
28 | setAutoRelaySignals(true);
29 | }
30 |
31 | QlipmonAdaptor::~QlipmonAdaptor()
32 | {
33 | // destructor
34 | }
35 |
36 | bool QlipmonAdaptor::broadcast() const
37 | {
38 | // get the value of property broadcast
39 | return qvariant_cast< bool >(parent()->property("broadcast"));
40 | }
41 |
42 | void QlipmonAdaptor::setBroadcast(bool value)
43 | {
44 | // set the value of property broadcast
45 | parent()->setProperty("broadcast", QVariant::fromValue(value));
46 | }
47 |
48 | QList QlipmonAdaptor::getHistory()
49 | {
50 | // handle method call DOMAIN.DOMAIN.getHistory
51 | QList out0;
52 | QMetaObject::invokeMethod(parent(), "getHistory", Q_RETURN_ARG(QList, out0));
53 | return out0;
54 | }
55 |
56 | QString QlipmonAdaptor::getLastText()
57 | {
58 | // handle method call DOMAIN.DOMAIN.getLastText
59 | QString out0;
60 | QMetaObject::invokeMethod(parent(), "getLastText", Q_RETURN_ARG(QString, out0));
61 | return out0;
62 | }
63 |
64 | QString QlipmonAdaptor::getLastText(int mode)
65 | {
66 | // handle method call DOMAIN.DOMAIN.getLastText
67 | QString out0;
68 | QMetaObject::invokeMethod(parent(), "getLastText", Q_RETURN_ARG(QString, out0), Q_ARG(int, mode));
69 | return out0;
70 | }
71 |
72 | QStringList QlipmonAdaptor::getTextHistory()
73 | {
74 | // handle method call DOMAIN.DOMAIN.getTextHistory
75 | QStringList out0;
76 | QMetaObject::invokeMethod(parent(), "getTextHistory", Q_RETURN_ARG(QStringList, out0));
77 | return out0;
78 | }
79 |
80 | QStringList QlipmonAdaptor::getTextHistory(int mode)
81 | {
82 | // handle method call DOMAIN.DOMAIN.getTextHistory
83 | QStringList out0;
84 | QMetaObject::invokeMethod(parent(), "getTextHistory", Q_RETURN_ARG(QStringList, out0), Q_ARG(int, mode));
85 | return out0;
86 | }
87 |
88 | QStringList QlipmonAdaptor::getTextHistory(int mode, bool duplicates)
89 | {
90 | // handle method call DOMAIN.DOMAIN.getTextHistory
91 | QStringList out0;
92 | QMetaObject::invokeMethod(parent(), "getTextHistory", Q_RETURN_ARG(QStringList, out0), Q_ARG(int, mode), Q_ARG(bool, duplicates));
93 | return out0;
94 | }
95 |
96 | void QlipmonAdaptor::setText(const QString &text)
97 | {
98 | // handle method call DOMAIN.DOMAIN.setText
99 | QMetaObject::invokeMethod(parent(), "setText", Q_ARG(QString, text));
100 | }
101 |
102 | void QlipmonAdaptor::setText(const QString &text, int mode)
103 | {
104 | // handle method call DOMAIN.DOMAIN.setText
105 | QMetaObject::invokeMethod(parent(), "setText", Q_ARG(QString, text), Q_ARG(int, mode));
106 | }
107 |
108 |
--------------------------------------------------------------------------------
/server/qlipmon_adaptor.h:
--------------------------------------------------------------------------------
1 | /*
2 | * This file was generated by qdbusxml2cpp version 0.8
3 | * Command line was: qdbusxml2cpp -V -c QlipmonAdaptor -i database_entry.h -a qlipmon_adaptor.h: ../qlipmon.xml
4 | *
5 | * qdbusxml2cpp is Copyright (C) 2020 The Qt Company Ltd.
6 | *
7 | * This is an auto-generated file.
8 | * This file may have been hand-edited. Look for HAND-EDIT comments
9 | * before re-generating it.
10 | */
11 |
12 | #ifndef QLIPMON_ADAPTOR_H
13 | #define QLIPMON_ADAPTOR_H
14 |
15 | #include
16 | #include
17 | #include "database_entry.h"
18 | QT_BEGIN_NAMESPACE
19 | class QByteArray;
20 | template class QList;
21 | template class QMap;
22 | class QString;
23 | class QStringList;
24 | class QVariant;
25 | QT_END_NAMESPACE
26 |
27 | /*
28 | * Adaptor class for interface QLIPMON_DBUS_FQDN
29 | */
30 | class QlipmonAdaptor: public QDBusAbstractAdaptor
31 | {
32 | Q_OBJECT
33 | Q_CLASSINFO("D-Bus Interface", QLIPMON_DBUS_FQDN)
34 | Q_CLASSINFO("D-Bus Introspection", ""
35 | " \n"
36 | " \n"
37 | " \n"
38 | " \n"
39 | " \n"
40 | " \n"
41 | " \n"
42 | " \n"
43 | " \n"
44 | " \n"
45 | " \n"
46 | " \n"
47 | " \n"
48 | " \n"
49 | " \n"
50 | " \n"
51 | " \n"
52 | " \n"
53 | " \n"
54 | " \n"
55 | " \n"
56 | " \n"
57 | " \n"
58 | " \n"
59 | " \n"
60 | " \n"
61 | " \n"
62 | " \n"
63 | " \n"
64 | " \n"
65 | " \n"
66 | " \n"
67 | " \n"
68 | " \n"
69 | " \n"
70 | " \n"
71 | " \n"
72 | "")
73 | public:
74 | QlipmonAdaptor(QObject *parent);
75 | virtual ~QlipmonAdaptor();
76 |
77 | public: // PROPERTIES
78 | Q_PROPERTY(bool broadcast READ broadcast WRITE setBroadcast)
79 | bool broadcast() const;
80 | void setBroadcast(bool value);
81 |
82 | public Q_SLOTS: // METHODS
83 | QList getHistory();
84 | QString getLastText();
85 | QString getLastText(int mode);
86 | QStringList getTextHistory();
87 | QStringList getTextHistory(int mode);
88 | QStringList getTextHistory(int mode, bool duplicates);
89 | void setText(const QString &text);
90 | void setText(const QString &text, int mode);
91 | Q_SIGNALS: // SIGNALS
92 | void updated(const QString &text, int mode);
93 | };
94 |
95 | #endif
96 |
--------------------------------------------------------------------------------
/server/server.pro:
--------------------------------------------------------------------------------
1 | include(../common.pri)
2 |
3 | TARGET=qlipmon-server
4 |
5 | greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
6 | QT += core gui sql dbus
7 |
8 | CONFIG += c++17
9 |
10 | # The following define makes your compiler emit warnings if you use
11 | # any Qt feature that has been marked deprecated (the exact warnings
12 | # depend on your compiler). Please consult the documentation of the
13 | # deprecated API in order to know how to port your code away from it.
14 | DEFINES += QT_DEPRECATED_WARNINGS
15 |
16 | # You can also make your code fail to compile if it uses deprecated APIs.
17 | # In order to do so, uncomment the following line.
18 | # You can also select to disable deprecated APIs only up to a certain version of Qt.
19 | #DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
20 |
21 | SOURCES += \
22 | qlipmon_adaptor.cpp \
23 | config.cpp \
24 | main.cpp \
25 | qlipmon.cpp \
26 | database.cpp \
27 | database_entry.cpp \
28 |
29 | HEADERS += \
30 | qlipmon_adaptor.h \
31 | config.h \
32 | qlipmon.h \
33 | database.h \
34 | database_entry.h \
35 |
36 | target.path = $${PREFIX}/bin
37 | INSTALLS += target
38 |
39 | systemd.path = $${PREFIX}/lib/systemd/user/
40 | systemd.files += qlipmon-server.service
41 |
42 | INSTALLS += systemd
43 |
--------------------------------------------------------------------------------
/xml2cpp.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh -ex
2 |
3 | cd `dirname "$0"`;
4 |
5 | #PROCESSOR="qdbusxml2cpp -V -N"
6 | PROCESSOR="qdbusxml2cpp -V"
7 |
8 | function process_file(){
9 | sed 's/\\"DOMAIN.DOMAIN\\"/\\"" QLIPMON_DBUS_FQDN "\\"/g' -i $1
10 | sed 's/\"DOMAIN.DOMAIN\"/QLIPMON_DBUS_FQDN/g' -i $1
11 | sed 's/DOMAIN.DOMAIN/QLIPMON_DBUS_FQDN/g' -i $1
12 | }
13 |
14 | cd server
15 |
16 | $PROCESSOR -c QlipmonAdaptor -i database_entry.h -a qlipmon_adaptor.h: ../qlipmon.xml
17 | $PROCESSOR -c QlipmonAdaptor -i qlipmon_adaptor.h -a :qlipmon_adaptor.cpp ../qlipmon.xml
18 |
19 | process_file qlipmon_adaptor.h;
20 |
21 | cd ../rofi
22 | $PROCESSOR -i ../server/database_entry.h --classname QlipMonInterface -p qlipmon_interface.h: ../qlipmon.xml
23 | $PROCESSOR -i qlipmon_interface.h --classname QlipMonInterface -p :qlipmon_interface.cpp ../qlipmon.xml
24 |
25 | process_file qlipmon_interface.h
26 |
--------------------------------------------------------------------------------