1 /*
2 Copyright (c) 2020, Lukas Holecek <hluk@email.cz>
3
4 This file is part of CopyQ.
5
6 CopyQ is free software: you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation, either version 3 of the License, or
9 (at your option) any later version.
10
11 CopyQ is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 GNU General Public License for more details.
15
16 You should have received a copy of the GNU General Public License
17 along with CopyQ. If not, see <http://www.gnu.org/licenses/>.
18 */
19
20 #include "commandstore.h"
21
22 #include "common/command.h"
23 #include "common/common.h"
24 #include "common/config.h"
25 #include "common/mimetypes.h"
26 #include "common/settings.h"
27 #include "common/temporarysettings.h"
28 #include "common/textdata.h"
29
30 #include <QSettings>
31 #include <QString>
32
33 namespace {
34
normalizeLineBreaks(QString & cmd)35 void normalizeLineBreaks(QString &cmd)
36 {
37 if (cmd.startsWith("\n ")) {
38 cmd.remove(0, 5);
39 cmd.replace("\n ", "\n");
40 } else if (cmd.startsWith("\r\n ")) {
41 cmd.remove(0, 6);
42 cmd.replace("\r\n ", "\n");
43 }
44 }
45
loadCommand(const QSettings & settings,Commands * commands)46 void loadCommand(const QSettings &settings, Commands *commands)
47 {
48 Command c;
49 c.enable = settings.value("Enable", true).toBool();
50
51 c.name = settings.value("Name").toString();
52 c.re = QRegularExpression( settings.value("Match").toString() );
53 c.wndre = QRegularExpression( settings.value("Window").toString() );
54 c.matchCmd = settings.value("MatchCommand").toString();
55 c.cmd = settings.value("Command").toString();
56 c.sep = settings.value("Separator").toString();
57
58 c.input = settings.value("Input").toString();
59 if ( c.input == "false" || c.input == "true" )
60 c.input = c.input == "true" ? QString(mimeText) : QString();
61
62 c.output = settings.value("Output").toString();
63 if ( c.output == "false" || c.output == "true" )
64 c.output = c.output == "true" ? QString(mimeText) : QString();
65
66 c.wait = settings.value("Wait").toBool();
67 c.automatic = settings.value("Automatic").toBool();
68 c.display = settings.value("Display").toBool();
69 c.transform = settings.value("Transform").toBool();
70 c.hideWindow = settings.value("HideWindow").toBool();
71 c.icon = settings.value("Icon").toString();
72 c.shortcuts = settings.value("Shortcut").toStringList();
73 c.globalShortcuts = settings.value("GlobalShortcut").toStringList();
74 c.tab = settings.value("Tab").toString();
75 c.outputTab = settings.value("OutputTab").toString();
76 c.inMenu = settings.value("InMenu").toBool();
77 c.isScript = settings.value("IsScript").toBool();
78
79 const auto globalShortcutsOption = settings.value("IsGlobalShortcut");
80 if ( globalShortcutsOption.isValid() ) {
81 c.isGlobalShortcut = settings.value("IsGlobalShortcut").toBool();
82 } else {
83 // Backwards compatibility with v3.1.2 and below.
84 if ( c.globalShortcuts.contains("DISABLED") )
85 c.globalShortcuts.clear();
86 c.isGlobalShortcut = !c.globalShortcuts.isEmpty();
87 }
88
89 if (settings.value("Ignore").toBool())
90 c.remove = c.automatic = true;
91 else
92 c.remove = settings.value("Remove").toBool();
93
94 commands->append(c);
95 }
96
saveValue(const char * key,const QRegularExpression & re,QSettings * settings)97 void saveValue(const char *key, const QRegularExpression &re, QSettings *settings)
98 {
99 settings->setValue(key, re.pattern());
100 }
101
saveValue(const char * key,const QVariant & value,QSettings * settings)102 void saveValue(const char *key, const QVariant &value, QSettings *settings)
103 {
104 settings->setValue(key, value);
105 }
106
107 /// Save only modified command properties.
108 template <typename Member>
saveNewValue(const char * key,const Command & command,const Member & member,QSettings * settings)109 void saveNewValue(const char *key, const Command &command, const Member &member, QSettings *settings)
110 {
111 if (command.*member != Command().*member)
112 saveValue(key, command.*member, settings);
113 }
114
saveCommand(const Command & c,QSettings * settings)115 void saveCommand(const Command &c, QSettings *settings)
116 {
117 saveNewValue("Name", c, &Command::name, settings);
118 saveNewValue("Match", c, &Command::re, settings);
119 saveNewValue("Window", c, &Command::wndre, settings);
120 saveNewValue("MatchCommand", c, &Command::matchCmd, settings);
121 saveNewValue("Command", c, &Command::cmd, settings);
122 saveNewValue("Input", c, &Command::input, settings);
123 saveNewValue("Output", c, &Command::output, settings);
124
125 // Separator for new command is set to '\n' for convenience.
126 // But this value shouldn't be saved if output format is not set.
127 if ( c.sep != "\\n" || !c.output.isEmpty() )
128 saveNewValue("Separator", c, &Command::sep, settings);
129
130 saveNewValue("Wait", c, &Command::wait, settings);
131 saveNewValue("Automatic", c, &Command::automatic, settings);
132 saveNewValue("Display", c, &Command::display, settings);
133 saveNewValue("InMenu", c, &Command::inMenu, settings);
134 saveNewValue("IsGlobalShortcut", c, &Command::isGlobalShortcut, settings);
135 saveNewValue("IsScript", c, &Command::isScript, settings);
136 saveNewValue("Transform", c, &Command::transform, settings);
137 saveNewValue("Remove", c, &Command::remove, settings);
138 saveNewValue("HideWindow", c, &Command::hideWindow, settings);
139 saveNewValue("Enable", c, &Command::enable, settings);
140 saveNewValue("Icon", c, &Command::icon, settings);
141 saveNewValue("Shortcut", c, &Command::shortcuts, settings);
142 saveNewValue("GlobalShortcut", c, &Command::globalShortcuts, settings);
143 saveNewValue("Tab", c, &Command::tab, settings);
144 saveNewValue("OutputTab", c, &Command::outputTab, settings);
145 }
146
importCommands(QSettings * settings)147 Commands importCommands(QSettings *settings)
148 {
149 auto commands = loadCommands(settings);
150
151 for (auto &command : commands) {
152 normalizeLineBreaks(command.cmd);
153 normalizeLineBreaks(command.matchCmd);
154 }
155
156 return commands;
157 }
158
159 } // namespace
160
loadAllCommands()161 Commands loadAllCommands()
162 {
163 const QString commandConfigPath = getConfigurationFilePath("-commands.ini");
164 return importCommandsFromFile(commandConfigPath);
165 }
166
saveCommands(const Commands & commands)167 void saveCommands(const Commands &commands)
168 {
169 Settings settings(getConfigurationFilePath("-commands.ini"));
170 saveCommands(commands, settings.settingsData());
171 }
172
loadCommands(QSettings * settings)173 Commands loadCommands(QSettings *settings)
174 {
175 Commands commands;
176
177 const QStringList groups = settings->childGroups();
178
179 if ( groups.contains("Command") ) {
180 settings->beginGroup("Command");
181 loadCommand(*settings, &commands);
182 settings->endGroup();
183 }
184
185 const int size = settings->beginReadArray("Commands");
186 // Allow undefined "size" for the array in settings.
187 if (size == 0) {
188 for (int i = 0; ; ++i) {
189 settings->setArrayIndex(i);
190 if ( settings->childKeys().isEmpty() )
191 break;
192 loadCommand(*settings, &commands);
193 }
194 } else {
195 for (int i = 0; i < size; ++i) {
196 settings->setArrayIndex(i);
197 loadCommand(*settings, &commands);
198 }
199 }
200
201 settings->endArray();
202
203 return commands;
204 }
205
saveCommands(const Commands & commands,QSettings * settings)206 void saveCommands(const Commands &commands, QSettings *settings)
207 {
208 settings->remove("Commands");
209 settings->remove("Command");
210
211 if (commands.size() == 1) {
212 settings->beginGroup("Command");
213 saveCommand(commands[0], settings);
214 settings->endGroup();
215 } else {
216 settings->beginWriteArray("Commands");
217 int i = 0;
218 for (const auto &c : commands) {
219 settings->setArrayIndex(i++);
220 saveCommand(c, settings);
221 }
222 settings->endArray();
223 }
224 }
225
importCommandsFromFile(const QString & filePath)226 Commands importCommandsFromFile(const QString &filePath)
227 {
228 QSettings commandsSettings(filePath, QSettings::IniFormat);
229 return importCommands(&commandsSettings);
230 }
231
importCommandsFromText(const QString & commands)232 Commands importCommandsFromText(const QString &commands)
233 {
234 TemporarySettings temporarySettings(commands.toUtf8());
235 return importCommands( temporarySettings.settings() );
236 }
237
exportCommands(const Commands & commands)238 QString exportCommands(const Commands &commands)
239 {
240 TemporarySettings temporarySettings;
241 saveCommands( commands, temporarySettings.settings() );
242
243 // Replace ugly '\n' with indented lines.
244 const QString data = getTextData( temporarySettings.content() );
245 QString commandData;
246 QRegularExpression re(R"(^(\d+\\)?(Command|MatchCommand)="?)");
247
248 for (const auto &line : data.split('\n')) {
249 const auto m = re.match(line);
250 if (m.hasMatch()) {
251 int i = m.capturedLength();
252 commandData.append(line.leftRef(i));
253
254 const bool addQuotes = !commandData.endsWith('"');
255 if (addQuotes)
256 commandData.append('"');
257
258 commandData.append('\n');
259 const QLatin1String indent(" ");
260 bool escape = false;
261
262 for (; i < line.size(); ++i) {
263 const QChar c = line[i];
264
265 if (escape) {
266 escape = false;
267
268 if (c == 'n') {
269 commandData.append('\n');
270 } else {
271 if ( commandData.endsWith('\n') )
272 commandData.append(indent);
273 commandData.append('\\');
274 commandData.append(c);
275 }
276 } else if (c == '\\') {
277 escape = !escape;
278 } else {
279 if ( commandData.endsWith('\n') )
280 commandData.append(indent);
281 commandData.append(c);
282 }
283 }
284
285 if (addQuotes)
286 commandData.append('"');
287 } else {
288 commandData.append(line);
289 }
290
291 commandData.append('\n');
292 }
293
294 return commandData.trimmed();
295 }
296