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 }