1 /// Contains functions for getting input, and sending output to the user.
2 module jaster.cli.userio;
3 
4 import jaster.cli.ansi, jaster.cli.binder;
5 import std.experimental.logger : LogLevel;
6 import std.traits : isInstanceOf;
7 
8 /++
9  + Provides various utilities:
10  +  - Program-wide configuration via `UserIO.configure`
11  +  - Logging, including debug-only and verbose-only logging via `logf`, `debugf`, and `verbosef`
12  +  - Logging helpers, for example `logTracef`, `debugInfof`, and `verboseErrorf`.
13  +  - Easily getting input from the user via `getInput`, `getInputNonEmptyString`, `getInputFromList`, and more.
14  + ++/
15 final static class UserIO
16 {
17     /++++++++++++++++
18      +++   VARS   +++
19      ++++++++++++++++/
20     private static
21     {
22         UserIOConfig _config;
23     }
24 
25     public static
26     {
27         /++
28          + Configure the settings for `UserIO`, can be called multiple times.
29          +
30          + Returns:
31          +  A `UserIOConfigBuilder`, which is a fluent-builder based struct used to set configuration options.
32          + ++/
33         UserIOConfigBuilder configure()
34         {
35             return UserIOConfigBuilder();
36         }
37     }
38 
39     /+++++++++++++++++
40      +++  LOGGING  +++
41      +++++++++++++++++/
42     public static
43     {
44         /++
45          + Logs the given `output` to the console, as long as `level` is >= the configured minimum log level.
46          +
47          + Configuration:
48          +  If `UserIOConfigBuilder.useColouredText` (see `UserIO.configure`) is set to `true`, then the text will be coloured
49          +  according to its log level.
50          +
51          +  trace - gray;
52          +  info - default;
53          +  warning - yellow;
54          +  error - red;
55          +  critical & fatal - bright red.
56          +
57          +  If `level` is lower than `UserIOConfigBuilder.useMinimumLogLevel`, then no output is logged.
58          +
59          + Params:
60          +  output = The output to display.
61          +  level  = The log level of this log.
62          + ++/
63         void log(const char[] output, LogLevel level)
64         {
65             import std.stdio : writeln;
66 
67             if(cast(int)level < UserIO._config.global.minLogLevel)
68                 return;
69 
70             if(!UserIO._config.global.useColouredText)
71             {
72                 writeln(output);
73                 return;
74             }
75 
76             AnsiText colouredOutput;
77             switch(level) with(LogLevel)
78             {
79                 case trace:     colouredOutput = output.ansi.fg(Ansi4BitColour.brightBlack); break;
80                 case warning:   colouredOutput = output.ansi.fg(Ansi4BitColour.yellow);      break;
81                 case error:     colouredOutput = output.ansi.fg(Ansi4BitColour.red);         break;
82                 case critical:  
83                 case fatal:     colouredOutput = output.ansi.fg(Ansi4BitColour.brightRed);   break;
84 
85                 default: break;
86             }
87 
88             if(colouredOutput == colouredOutput.init)
89                 colouredOutput = output.ansi;
90 
91             writeln(colouredOutput);
92         }
93 
94         /// Variant of `UserIO.log` that uses `std.format.format` to format the final output.
95         void logf(Args...)(const char[] fmt, LogLevel level, Args args)
96         {
97             import std.format : format;
98 
99             UserIO.log(format(fmt, args), level);
100         }
101 
102         /// Variant of `UserIO.logf` that only shows output in non-release builds.
103         void debugf(Args...)(const char[] format, LogLevel level, Args args)
104         {
105             debug UserIO.logf(format, level, args);
106         }
107 
108         /// Variant of `UserIO.logf` that only shows output if `UserIOConfigBuilder.useVerboseLogging` is set to `true`.
109         void verbosef(Args...)(const char[] format, LogLevel level, Args args)
110         {
111             if(UserIO._config.global.useVerboseLogging)
112                 UserIO.logf(format, level, args);
113         }
114 
115         /// Logs an exception, using the given `LogFunc`, as an error.
116         ///
117         /// Prefer the use of `logException`, `debugException`, and `verboseException`.
118         void exception(alias LogFunc)(Exception ex)
119         {
120             LogFunc(
121                 "----EXCEPTION----\nFile: %s\nLine: %s\nType: %s\nMessage: '%s'\nTrace: %s",
122                 ex.file,
123                 ex.line,
124                 ex.classinfo,
125                 ex.msg,
126                 ex.info
127             );
128         }
129 
130         // I'm not auto-generating these, as I want autocomplete (e.g. vscode) to be able to pick these up.
131 
132         /// Helper functions for `logf`, to easily use a specific log level.
133         void logTracef   (Args...)(const char[] format, Args args){ UserIO.logf(format, LogLevel.trace, args);    }
134         /// ditto
135         void logInfof    (Args...)(const char[] format, Args args){ UserIO.logf(format, LogLevel.info, args);     }
136         /// ditto
137         void logWarningf (Args...)(const char[] format, Args args){ UserIO.logf(format, LogLevel.warning, args);  }
138         /// ditto
139         void logErrorf   (Args...)(const char[] format, Args args){ UserIO.logf(format, LogLevel.error, args);    }
140         /// ditto
141         void logCriticalf(Args...)(const char[] format, Args args){ UserIO.logf(format, LogLevel.critical, args); }
142         /// ditto
143         void logFatalf   (Args...)(const char[] format, Args args){ UserIO.logf(format, LogLevel.fatal, args);    }
144         /// ditto
145         alias logException = exception!logErrorf;
146 
147         /// Helper functions for `debugf`, to easily use a specific log level.
148         void debugTracef   (Args...)(const char[] format, Args args){ UserIO.debugf(format, LogLevel.trace, args);    }
149         /// ditto
150         void debugInfof    (Args...)(const char[] format, Args args){ UserIO.debugf(format, LogLevel.info, args);     }
151         /// ditto
152         void debugWarningf (Args...)(const char[] format, Args args){ UserIO.debugf(format, LogLevel.warning, args);  }
153         /// ditto
154         void debugErrorf   (Args...)(const char[] format, Args args){ UserIO.debugf(format, LogLevel.error, args);    }
155         /// ditto
156         void debugCriticalf(Args...)(const char[] format, Args args){ UserIO.debugf(format, LogLevel.critical, args); }
157         /// ditto
158         void debugFatalf   (Args...)(const char[] format, Args args){ UserIO.debugf(format, LogLevel.fatal, args);    }
159         /// ditto
160         alias debugException = exception!debugErrorf;
161 
162         /// Helper functions for `verbosef`, to easily use a specific log level.
163         void verboseTracef   (Args...)(const char[] format, Args args){ UserIO.verbosef(format, LogLevel.trace, args);    }
164         /// ditto
165         void verboseInfof    (Args...)(const char[] format, Args args){ UserIO.verbosef(format, LogLevel.info, args);     }
166         /// ditto
167         void verboseWarningf (Args...)(const char[] format, Args args){ UserIO.verbosef(format, LogLevel.warning, args);  }
168         /// ditto
169         void verboseErrorf   (Args...)(const char[] format, Args args){ UserIO.verbosef(format, LogLevel.error, args);    }
170         /// ditto
171         void verboseCriticalf(Args...)(const char[] format, Args args){ UserIO.verbosef(format, LogLevel.critical, args); }
172         /// ditto
173         void verboseFatalf   (Args...)(const char[] format, Args args){ UserIO.verbosef(format, LogLevel.fatal, args);    }
174         /// ditto
175         alias verboseException = exception!verboseErrorf;
176     }
177 
178     /+++++++++++++++++
179      +++  CURSOR   +++
180      +++++++++++++++++/
181     public static
182     {
183         @safe
184         private void singleArgCsiCommand(char command)(size_t n)
185         {
186             import std.conv   : to;
187             import std.stdio  : write;
188             import std.format : sformat;
189 
190             enum FORMAT_STRING = "\033[%s"~command;
191             enum SIZET_LENGTH  = size_t.max.to!string.length;
192 
193             char[SIZET_LENGTH] buffer;
194             const used = sformat!FORMAT_STRING(buffer, n);
195 
196             // Pretty sure this is safe right? It copies the buffer, right?
197             write(used);
198         }
199 
200         // Again, not auto generated since I don't trust autocomplete to pick up aliases properly.
201 
202         /++
203          + Moves the console's cursor down and moves the cursor to the start of that line.
204          +
205          + Params:
206          +  lineCount = The amount of lines to move down.
207          + ++/
208         @safe
209         void moveCursorDownByLines(size_t lineCount) { UserIO.singleArgCsiCommand!'E'(lineCount); }
210 
211         /++
212          + Moves the console's cursor up and moves the cursor to the start of that line.
213          +
214          + Params:
215          +  lineCount = The amount of lines to move up.
216          + ++/
217         @safe
218         void moveCursorUpByLines(size_t lineCount) { UserIO.singleArgCsiCommand!'F'(lineCount); }
219     }
220 
221     /+++++++++++++++
222      +++  INPUT  +++
223      +++++++++++++++/
224     public static
225     {
226         /++
227          + Gets input from the user, and uses the given `ArgBinder` (or the default one, if one isn't passed) to
228          + convert the string to a `T`.
229          +
230          + Notes:
231          +  Because `ArgBinder` is responsible for the conversion, if for example you wanted `T` to be a custom struct,
232          +  then you could create an `@ArgBinderFunc` to perform the conversion, and then this function (and all `UserIO.getInput` variants)
233          +  will be able to convert the user's input to that type.
234          +
235          +  See also the documentation for `ArgBinder`.
236          +
237          + Params:
238          +  T       = The type to conver the string to, via `Binder`.
239          +  Binder  = The `ArgBinder` that knows how to convert a string -> `T`.
240          +  prompt  = The prompt to display to the user, note that no extra characters or spaces are added, the prompt is shown as-is.
241          +
242          + Returns:
243          +  A `T` that was created by the user's input given to `Binder`.
244          + ++/
245         T getInput(T, Binder = ArgBinder!())(string prompt)
246         if(isInstanceOf!(ArgBinder, Binder))
247         {
248             import std.string : chomp;
249             import std.stdio  : readln, write;
250             
251             write(prompt);
252 
253             T value;
254             Binder.bind(readln().chomp, value);
255 
256             return value;
257         }
258 
259         /++
260          + A variant of `UserIO.getInput` that'll constantly prompt the user until they enter a non-null, non-whitespace-only string.
261          +
262          + Notes:
263          +  The `Binder` is only used to convert a string to a string, in case there's some weird voodoo you want to do with it.
264          + ++/
265         string getInputNonEmptyString(Binder = ArgBinder!())(string prompt)
266         {
267             import std.algorithm : all;
268             import std.ascii     : isWhite;
269 
270             string value;
271             while(value.length == 0 || value.all!isWhite)
272                 value = UserIO.getInput!(string, Binder)(prompt);
273 
274             return value;
275         }
276 
277         /++
278          + A variant of `UserIO.getInput` that'll constantly prompt the user until they enter a value that doesn't cause an
279          + exception (of type `Ex`) to be thrown by the `Binder`.
280          + ++/
281         T getInputCatchExceptions(T, Ex: Exception = Exception, Binder = ArgBinder!())(string prompt, void delegate(Ex) onException = null)
282         {
283             while(true)
284             {
285                 try return UserIO.getInput!(T, Binder)(prompt);
286                 catch(Ex ex)
287                 {
288                     if(onException !is null)
289                         onException(ex);
290                 }
291             }
292         }
293 
294         /++
295          + A variant of `UserIO.getInput` that'll constantly prompt the user until they enter a value from the given `list`.
296          +
297          + Behaviour:
298          +  All items of `list` are converted to a string (via `std.conv.to`), and the user must enter the *exact* value of one of these
299          +  strings for this function to return, so if you're wanting to use a struct then ensure you make `toString` provide a user-friendly
300          +  value.
301          +
302          +  This function $(B does not) use `Binder` to provide the final value, it will instead simply return the appropriate
303          +  item from `list`. This is because the value already exists (inside of `list`) so there's no reason to perform a conversion.
304          +
305          +  The `Binder` is only used to convert the user's input from a string into another string, in case there's any transformations
306          +  you'd like to perform on it.
307          +
308          + Prompt:
309          +  The prompt layout for this variant is a bit different than other variants.
310          +
311          +  `$prompt[$list[0], $list[1], ...]$promptPostfix`
312          +
313          +  For example `Choose colour[red, blue, green]: `
314          + ++/
315         T getInputFromList(T, Binder = ArgBinder!())(string prompt, T[] list, string promptPostfix = ": ")
316         {
317             import std.stdio     : write;
318             import std.conv      : to;
319             import std.exception : assumeUnique;
320 
321             auto listAsStrings = new string[list.length];
322             foreach(i, item; list)
323                 listAsStrings[i] = item.to!string();
324 
325             // 2 is for the "[" and "]", list.length * 2 is for the ", " added between each item.
326             // list.length * 10 is just to try and overallocate a little bit.
327             char[] promptBuilder;
328             promptBuilder.reserve(prompt.length + 2 + (list.length * 2) + (list.length * 10) + promptPostfix.length);
329 
330             promptBuilder ~= prompt;
331             promptBuilder ~= "[";
332             foreach(i, item; list)
333             {
334                 promptBuilder ~= listAsStrings[i];
335                 if(i != list.length - 1)
336                     promptBuilder ~= ", ";
337             }
338             promptBuilder ~= "]";
339             promptBuilder ~= promptPostfix;
340 
341             prompt = promptBuilder.assumeUnique;
342             while(true)
343             {
344                 const input = UserIO.getInput!(string, Binder)(prompt);
345                 foreach(i, str; listAsStrings)
346                 {
347                     if(input == str)
348                         return list[i];
349                 }
350             }
351         }
352     }
353 }
354 
355 private struct UserIOConfigScope
356 {
357     bool useVerboseLogging;
358     bool useColouredText = true;
359     LogLevel minLogLevel;
360 }
361 
362 private struct UserIOConfig
363 {
364     UserIOConfigScope global;
365 }
366 
367 /++
368  + A struct that provides an easy and fluent way to configure how `UserIO` works.
369  + ++/
370 struct UserIOConfigBuilder
371 {
372     private ref UserIOConfigScope getScope()
373     {
374         // For future purposes.
375         return UserIO._config.global;
376     }
377 
378     /++
379      + Determines whether `UserIO.log` uses coloured output based on log level.
380      + ++/
381     UserIOConfigBuilder useColouredText(bool value = true)
382     {
383         this.getScope().useColouredText = value;
384         return this;
385     }
386 
387     /++
388      + Determines whether `UserIO.verbosef` and friends are allowed to output anything at all.
389      + ++/
390     UserIOConfigBuilder useVerboseLogging(bool value = true)
391     {
392         this.getScope().useVerboseLogging = value;
393         return this;
394     }
395 
396     /++
397      + Sets the minimum log level. Any logs must be >= this `level` in order to be printed out on screen.
398      + ++/
399     UserIOConfigBuilder useMinimumLogLevel(LogLevel level)
400     {
401         this.getScope().minLogLevel = level;
402         return this;
403     }
404 }