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