1 /** 2 * Handling of interaction with users via standard input. 3 * 4 * Provides functions for simple and common interactions with users in 5 * the form of question and answer. 6 * 7 * Copyright: Copyright Jesse Phillips 2010 8 * License: $(LINK2 https://github.com/Abscissa/scriptlike/blob/master/LICENSE.txt, zlib/libpng) 9 * Authors: Jesse Phillips 10 * 11 * Synopsis: 12 * 13 * -------- 14 * import scriptlike.interact; 15 * 16 * auto age = userInput!int("Please Enter your age"); 17 * 18 * if(userInput!bool("Do you want to continue?")) 19 * { 20 * auto outputFolder = pathLocation("Where you do want to place the output?"); 21 * auto color = menu!string("What color would you like to use?", ["Blue", "Green"]); 22 * } 23 * 24 * auto num = require!(int, "a > 0 && a <= 10")("Enter a number from 1 to 10"); 25 * -------- 26 */ 27 module scriptlike.interact; 28 29 import std.conv; 30 import std.file; 31 import std.functional; 32 import std.range; 33 import std.stdio; 34 import std..string; 35 import std.traits; 36 37 /** 38 * The $(D userInput) function provides a means to accessing a single 39 * value from the user. Each invocation outputs a provided 40 * statement/question and takes an entire line of input. The result is then 41 * converted to the requested type; default is a string. 42 * 43 * -------- 44 * auto name = userInput("What is your name"); 45 * //or 46 * string name; 47 * userInput("What is your name", name); 48 * -------- 49 * 50 * Returns: User response as type T. 51 * 52 * Where type is bool: 53 * 54 * true on "ok", "continue", 55 * and if the response starts with 'y' or 'Y'. 56 * 57 * false on all other input, include no response (will not throw). 58 * 59 * Throws: $(D NoInputException) if the user does not enter anything. 60 * $(D ConvError) when the string could not be converted to the desired type. 61 */ 62 T userInput(T = string)(string question = "") 63 { 64 write(question ~ "\n> "); 65 stdout.flush; 66 auto ans = readln(); 67 68 static if(is(T == bool)) 69 { 70 switch(ans.front) 71 { 72 case 'y', 'Y': 73 return true; 74 default: 75 } 76 switch(ans.strip()) 77 { 78 case "continue": 79 case "ok": 80 return true; 81 default: 82 return false; 83 } 84 } else 85 { 86 if(ans == "\x0a") 87 throw new NoInputException("Value required, "~ 88 "cannot continue operation."); 89 static if(isSomeChar!T) 90 { 91 return to!(T)(ans[0]); 92 } else 93 return to!(T)(ans.strip()); 94 } 95 } 96 97 ///ditto 98 void userInput(T = string)(string question, ref T result) 99 { 100 result = userInput!T(question); 101 } 102 103 version(unittest_scriptlike_d) 104 unittest 105 { 106 mixin(selfCom(["10PM", "9PM"])); 107 mixin(selfCom()); 108 auto s = userInput("What time is it?"); 109 assert(s == "10PM", "Expected 10PM got" ~ s); 110 outfile.rewind; 111 assert(outfile.readln().strip == "What time is it?"); 112 113 outfile.rewind; 114 userInput("What time?", s); 115 assert(s == "9PM", "Expected 9PM got" ~ s); 116 outfile.rewind; 117 assert(outfile.readln().strip == "What time?"); 118 } 119 120 /** 121 * Pauses and prompts the user to press Enter (or "Return" on OSX). 122 * 123 * This is similar to the Windows command line's PAUSE command. 124 * 125 * -------- 126 * pause(); 127 * pause("Thanks. Please press Enter again..."); 128 * -------- 129 */ 130 void pause(string prompt = defaultPausePrompt) 131 { 132 //TODO: This works, but needs a little work. Currently, it echoes all 133 // input until Enter is pressed. Fixing that requires some low-level 134 // os-specific work. 135 // 136 // For reference: 137 // http://stackoverflow.com/questions/6856635/hide-password-input-on-terminal 138 // http://linux.die.net/man/3/termios 139 140 write(prompt); 141 stdout.flush(); 142 getchar(); 143 } 144 145 version(OSX) 146 enum defaultPausePrompt = "Press Return to continue..."; /// 147 else 148 enum defaultPausePrompt = "Press Enter to continue..."; /// 149 150 151 /** 152 * Gets a valid path folder from the user. The string will not contain 153 * quotes, if you are using in a system call and the path contain spaces 154 * wrapping in quotes may be required. 155 * 156 * -------- 157 * auto confFile = pathLocation("Where is the configuration file?"); 158 * -------- 159 * 160 * Throws: NoInputException if the user does not provide a path. 161 */ 162 string pathLocation(string action) 163 { 164 import std.algorithm; 165 import std.utf : byChar; 166 string ans; 167 168 do 169 { 170 if(ans !is null) 171 writeln("Could not locate that file."); 172 ans = userInput(action); 173 // Quotations will generally cause problems when 174 // using the path with std.file and Windows. This removes the quotes. 175 ans = std..string.strip( ans.byChar.filter!(a => a != '"' && a != ';').array ); 176 //ans = ans.removechars("\";").strip(); 177 //ans = ans[0] == '"' ? ans[1..$] : ans; // removechars skips first char 178 } while(!exists(ans)); 179 180 return ans; 181 } 182 183 /** 184 * Creates a menu from a Range of strings. 185 * 186 * It will require that a number is selected within the number of options. 187 * 188 * If the the return type is a string, the string in the options parameter will 189 * be returned. 190 * 191 * Throws: NoInputException if the user wants to quit. 192 */ 193 T menu(T = ElementType!(Range), Range) (string question, Range options) 194 if((is(T==ElementType!(Range)) || is(T==int)) && 195 isForwardRange!(Range)) 196 { 197 string ans; 198 int maxI; 199 int i; 200 201 while(true) 202 { 203 writeln(question); 204 i = 0; 205 foreach(str; options) 206 { 207 writefln("%8s. %s", i+1, str); 208 i++; 209 } 210 maxI = i+1; 211 212 writefln("%8s. %s", "No Input", "Quit"); 213 ans = userInput!(string)("").strip(); 214 int ians; 215 216 try 217 { 218 ians = to!(int)(ans); 219 } catch(ConvException ce) 220 { 221 bool found; 222 i = 0; 223 foreach(o; options) 224 { 225 if(ans.toLower() == to!string(o).toLower()) 226 { 227 found = true; 228 ians = i+1; 229 break; 230 } 231 i++; 232 } 233 if(!found) 234 throw ce; 235 236 } 237 238 if(ians > 0 && ians <= maxI) 239 static if(is(T==ElementType!(Range))) 240 static if(isRandomAccessRange!(Range)) 241 return options[ians-1]; 242 else 243 { 244 take!(ians-1)(options); 245 return options.front; 246 } 247 else 248 return ians; 249 else 250 writeln("You did not select a valid entry."); 251 } 252 } 253 254 version(unittest_scriptlike_d) 255 unittest 256 { 257 mixin(selfCom(["1","Green", "green","2"])); 258 mixin(selfCom()); 259 auto color = menu!string("What color?", ["Blue", "Green"]); 260 assert(color == "Blue", "Expected Blue got " ~ color); 261 262 auto ic = menu!int("What color?", ["Blue", "Green"]); 263 assert(ic == 2, "Expected 2 got " ~ ic.to!string); 264 265 color = menu!string("What color?", ["Blue", "Green"]); 266 assert(color == "Green", "Expected Green got " ~ color); 267 268 color = menu!string("What color?", ["Blue", "Green"]); 269 assert(color == "Green", "Expected Green got " ~ color); 270 outfile.rewind; 271 assert(outfile.readln().strip == "What color?"); 272 } 273 274 275 /** 276 * Requires that a value be provided and valid based on 277 * the delegate passed in. It must also check against null input. 278 * 279 * -------- 280 * auto num = require!(int, "a > 0 && a <= 10")("Enter a number from 1 to 10"); 281 * -------- 282 * 283 * Throws: NoInputException if the user does not provide any value. 284 * ConvError if the user does not provide any value. 285 */ 286 T require(T, alias cond)(in string question, in string failure = null) 287 { 288 alias unaryFun!(cond) call; 289 T ans; 290 while(1) 291 { 292 ans = userInput!T(question); 293 if(call(ans)) 294 break; 295 if(failure) 296 writeln(failure); 297 } 298 299 return ans; 300 } 301 302 version(unittest_scriptlike_d) 303 unittest 304 { 305 mixin(selfCom(["1","11","3"])); 306 mixin(selfCom()); 307 auto num = require!(int, "a > 0 && a <= 10")("Enter a number from 1 to 10"); 308 assert(num == 1, "Expected 1 got" ~ num.to!string); 309 num = require!(int, "a > 0 && a <= 10")("Enter a number from 1 to 10"); 310 assert(num == 3, "Expected 1 got" ~ num.to!string); 311 outfile.rewind; 312 assert(outfile.readln().strip == "Enter a number from 1 to 10"); 313 } 314 315 316 /** 317 * Used when input was not provided. 318 */ 319 class NoInputException: Exception 320 { 321 this(string msg) 322 { 323 super(msg); 324 } 325 } 326 327 version(unittest_scriptlike_d) 328 private string selfCom() 329 { 330 string ans = q{ 331 auto outfile = File.tmpfile(); 332 auto origstdout = stdout; 333 scope(exit) stdout = origstdout; 334 stdout = outfile;}; 335 336 return ans; 337 } 338 339 version(unittest_scriptlike_d) 340 private string selfCom(string[] input) 341 { 342 string ans = q{ 343 auto infile = File.tmpfile(); 344 auto origstdin = stdin; 345 scope(exit) stdin = origstdin; 346 stdin = infile;}; 347 348 foreach(i; input) 349 ans ~= "infile.writeln(`"~i~"`);"; 350 ans ~= "infile.rewind;"; 351 352 return ans; 353 }