1 /**************************************************************************
2  **                                                                      **
3  ** Copyright (C) 2018 Lukas Spies                                       **
4  ** Contact: http://photoqt.org                                          **
5  **                                                                      **
6  ** This file is part of PhotoQt.                                        **
7  **                                                                      **
8  ** PhotoQt is free software: you can redistribute it and/or modify      **
9  ** it under the terms of the GNU General Public License as published by **
10  ** the Free Software Foundation, either version 2 of the License, or    **
11  ** (at your option) any later version.                                  **
12  **                                                                      **
13  ** PhotoQt is distributed in the hope that it will be useful,           **
14  ** but WITHOUT ANY WARRANTY; without even the implied warranty of       **
15  ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the        **
16  ** GNU General Public License for more details.                         **
17  **                                                                      **
18  ** You should have received a copy of the GNU General Public License    **
19  ** along with PhotoQt. If not, see <http://www.gnu.org/licenses/>.      **
20  **                                                                      **
21  **************************************************************************/
22 
23 #ifndef COMMANDLINEPARSER_H
24 #define COMMANDLINEPARSER_H
25 
26 #include <QCoreApplication>
27 #include <iomanip>
28 #include "../logger.h"
29 
30 // A custom command line parser
31 // Looks very much like QCommandLineParser, but allows grouping entries
32 class CommandLineParser : public QObject {
33 
34     Q_OBJECT
35 
36 public:
37 
CommandLineParser(QCoreApplication * app)38     explicit CommandLineParser(QCoreApplication *app) : QObject() {
39 
40         // This will hold the maximum width of the options to align everything nicely
41         maxEntriesWidth = 0;
42 
43         // These 4 options should always be the first four!
44         validOptions << "h" << "help" << "v" << "version";
45 
46         // First category
47         QString cat = "Interaction with PhotoQt";
48         addOption(cat, {"o", "open"}, "Make PhotoQt ask for a new File.");
49         addOption(cat, {"s", "show"}, "Shows PhotoQt (does nothing if already shown).");
50         addOption(cat, "hide", "Hides PhotoQt to system tray (does nothing if already hidden).");
51         addOption(cat, {"t", "toggle"}, "Toggle PhotoQt - hides PhotoQt if visible, shows if hidden.");
52         addOption(cat, {"thumbs", "no-thumbs"}, "Enable/Disable thumbnails.");
53 
54         // Second category
55         cat = "Start-up-only options";
56         addOption(cat, "start-in-tray", "Start PhotoQt hidden to the system tray.");
57         addOption(cat, "standalone", "Create standalone PhotoQt, multiple instances but no remote interaction possible.");
58 
59         // Third category
60         cat = "General Options";
61         addOption(cat, {"debug", "no-debug"}, "Switch on/off debug messages.");
62         addOption(cat, "export", "Export configuration to given filename.", "filename");
63         addOption(cat, "import", "Import configuration from given filename.", "filename");
64 
65         // Process the command line
66         process(app);
67 
68     }
69 
70     // Convenience function, allows to pass a single option as string
71     void addOption(QString cat, QString option, QString description, QString valueName = "") {
72         addOption(cat, QStringList() << option, description, valueName);
73     }
74 
75     // Add a command line option
76     void addOption(QString cat, QStringList option, QString description, QString valueName = "") {
77 
78         // Store a new category
79         if(!categories.contains(cat))
80             categories.append(cat);
81 
82         // Compose the option string that is to be displayed
83         QString optionString = "";
84         // Loop over all options
85         for(QString o : option) {
86 
87             // Store this as a valid options the user can use
88             validOptions << o;
89 
90             // If this flag comes with a value, store that
91             if(valueName != "")
92                 optionsWithValue << o;
93 
94             // Multiple options are displayed separated by comma
95             if(optionString != "")
96                 optionString += ", ";
97 
98             // Add on the option to the string.
99             // 1-character flags are called with a single '-'
100             // 2+-character flags are called with a double '-'
101             optionString += (o.length()==1 ? "-" : "--") + o;
102 
103         }
104 
105         // If option comes with a value, displayed after options in angled brackets
106         if(valueName.length() > 0)
107             optionString += " <" + valueName + ">";
108 
109         // Find largets width necessary
110         if(optionString.length() > maxEntriesWidth)
111             maxEntriesWidth = optionString.length();
112 
113         // Store entry
114         allEntries << (QStringList() << cat << optionString << description);
115 
116     }
117 
118 private:
119 
120     // Show the help message and quit
showHelp()121     void showHelp() {
122 
123         // the max width gets two added on the left (indented by 2) and on the right (spacing between option and description)
124         int entryWidth = 2+maxEntriesWidth+2;
125 
126         int maxWidth = 78;
127 
128         // Header
129         std::cout << "Usage: photoqt [options] [filename]" << std::endl;
130         std::cout << "PhotoQt Image Viewer" << std::endl << std::endl;
131 
132         // help and version option
133         std::cout << std::setfill(' ') << std::setw(entryWidth) << std::left << "  -h, --help" << "Displays this help." << std::endl;
134         std::cout << std::setfill(' ') << std::setw(entryWidth) << std::left << "  -v, --version" << "Displays version information." << std::endl;
135 
136         // Loop over all categories
137         for(QString cat : categories) {
138 
139             // Display category followed by colon
140             std::cout << std::endl << cat.toStdString() << ":" << std::endl;
141 
142             // Loop over all entries to find the ones matching this category
143             for(QStringList entry : allEntries) {
144 
145                 // If categories match, output option
146                 if(entry.at(0) == cat) {
147 
148                     // Output category
149                     std::cout << std::setfill(' ') << std::setw(entryWidth) << std::left << ("  " + entry.at(1).toStdString());
150 
151                     // The description. This will be reduced by whatever is outputted
152                     QString desc = entry.at(2);
153 
154                     // Keep as going as long as there is something to output
155                     while(desc.length() > 0) {
156 
157                         // The maximum width that is available for the description
158                         int maxDescLength = std::min(maxWidth-entryWidth, desc.length()-1);
159 
160                         // Make sure that, if we are not at the end of the string, we break the string at a space
161                         while(desc.at(maxDescLength) != ' ' && maxDescLength < desc.length()-1)
162                             --maxDescLength;
163 
164                         // Error checking, this should never really happen, but just to be safe
165                         if(maxDescLength < 1)
166                             break;
167 
168                         // If this is the second line, we need to pad the output on the left for proper indentation
169                         if(desc.length() < entry.at(2).length())
170                             std::cout << std::setfill(' ') << std::setw(entryWidth) << " ";
171 
172                         // Output the description (sub-)string
173                         std::cout << desc.left(maxDescLength+1).trimmed().toStdString() << std::endl;
174 
175                         // Remove whatever part of the description is displayed
176                         desc = desc.remove(0, maxDescLength+1);
177 
178                     }
179 
180                 }
181 
182             }
183 
184         }
185 
186         // Output the positional argument at end
187         std::cout << std::endl;
188         std::cout << "Arguments:" << std::endl;
189         std::cout << std::setfill(' ') << std::setw(entryWidth) << std::left << "  filename" << "File to open with PhotoQt." << std::endl;
190 
191         // And quit application
192         qApp->quit();
193         exit(0);
194 
195     }
196 
197     // When an error was detected, display error and quit
showError(QString err)198     void showError(QString err) {
199         std::cout << err.toStdString() << std::endl;
200         qApp->quit();
201         exit(0);
202     }
203 
204     // Process the command line
process(QCoreApplication * app)205     void process(QCoreApplication *app) {
206 
207         // Get all arguments
208         QStringList args = app->arguments();
209 
210         // If help flag is set, show help and quit
211         if(args.contains("-h") || args.contains("--help")) {
212             showHelp();
213             return;
214         }
215 
216         // Set the found filename to empty
217         foundFilename = "";
218 
219         // Loop over all arguments
220         for(int i = 1; i < args.length(); ++i) {
221 
222             // quick access for current flag
223             QString a = args.at(i);
224 
225             // If flag doesn't start with a '-', then this is the positional flag, i.e., the filename
226             if(!a.startsWith("-")) {
227 
228                 // If this is the first filename, store that
229                 if(foundFilename.trimmed() == "")
230                     foundFilename = a;
231                 // Else show error as only one filename is allowed
232                 else {
233                     showError("Only one filename can be passed on!");
234                     return;
235                 }
236 
237             // 'Normal' flags
238             } else {
239 
240                 // Remove any '-' from the beginning of the flag
241                 while(a.startsWith("-"))
242                     a = a.remove(0,1);
243 
244                 // If option is not a valid one, show error and quit
245                 if(!validOptions.contains(a)) {
246                     showError("Unknown option '" + a + "'.");
247                     return;
248                 }
249 
250                 // If option comes with a value
251                 if(optionsWithValue.contains(a)) {
252 
253                     // If no value was provided, show error and quit
254                     if(i == args.length()-1 || args.at(i+1).startsWith("-")) {
255                         showError("Filename required for option '" + a + "'.");
256                         return;
257                     // Store value
258                     } else {
259                         foundValues.insert(a, args.at(++i));
260                         // This is a simple list holding all passed on flags
261                         // This makes it much easier to find out what has been set or not
262                         foundOptions.append(a);
263                     }
264 
265                 // A normal flag without value
266                 } else
267                     foundOptions.append(a);
268 
269             }
270 
271         }
272 
273     }
274 
275 public:
276     // All options that have been found
277     QStringList foundOptions;
278     // All option/value pairs that have been found
279     QMap<QString,QString> foundValues;
280     // The found filename (if any)
281     QString foundFilename;
282 
283 private:
284     // Hold max width required for options
285     int maxEntriesWidth;
286     // All categories that have been set
287     QStringList categories;
288     // All entries
289     QList<QStringList> allEntries;
290     // All options that are set as one list
291     QStringList validOptions;
292     // All options that are set with values as one list
293     QStringList optionsWithValue;
294 
295 };
296 
297 
298 #endif
299