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 string ans; 165 166 do 167 { 168 if(ans !is null) 169 writeln("Could not locate that file."); 170 ans = userInput(action); 171 // Quotations will generally cause problems when 172 // using the path with std.file and Windows. This removes the quotes. 173 ans = ans.removechars("\";").strip(); 174 ans = ans[0] == '"' ? ans[1..$] : ans; // removechars skips first char 175 } while(!exists(ans)); 176 177 return ans; 178 } 179 180 /** 181 * Creates a menu from a Range of strings. 182 * 183 * It will require that a number is selected within the number of options. 184 * 185 * If the the return type is a string, the string in the options parameter will 186 * be returned. 187 * 188 * Throws: NoInputException if the user wants to quit. 189 */ 190 T menu(T = ElementType!(Range), Range) (string question, Range options) 191 if((is(T==ElementType!(Range)) || is(T==int)) && 192 isForwardRange!(Range)) 193 { 194 string ans; 195 int maxI; 196 int i; 197 198 while(true) 199 { 200 writeln(question); 201 i = 0; 202 foreach(str; options) 203 { 204 writefln("%8s. %s", i+1, str); 205 i++; 206 } 207 maxI = i+1; 208 209 writefln("%8s. %s", "No Input", "Quit"); 210 ans = userInput!(string)("").strip(); 211 int ians; 212 213 try 214 { 215 ians = to!(int)(ans); 216 } catch(ConvException ce) 217 { 218 bool found; 219 i = 0; 220 foreach(o; options) 221 { 222 if(ans.toLower() == to!string(o).toLower()) 223 { 224 found = true; 225 ians = i+1; 226 break; 227 } 228 i++; 229 } 230 if(!found) 231 throw ce; 232 233 } 234 235 if(ians > 0 && ians <= maxI) 236 static if(is(T==ElementType!(Range))) 237 static if(isRandomAccessRange!(Range)) 238 return options[ians-1]; 239 else 240 { 241 take!(ians-1)(options); 242 return options.front; 243 } 244 else 245 return ians; 246 else 247 writeln("You did not select a valid entry."); 248 } 249 } 250 251 version(unittest_scriptlike_d) 252 unittest 253 { 254 mixin(selfCom(["1","Green", "green","2"])); 255 mixin(selfCom()); 256 auto color = menu!string("What color?", ["Blue", "Green"]); 257 assert(color == "Blue", "Expected Blue got " ~ color); 258 259 auto ic = menu!int("What color?", ["Blue", "Green"]); 260 assert(ic == 2, "Expected 2 got " ~ ic.to!string); 261 262 color = menu!string("What color?", ["Blue", "Green"]); 263 assert(color == "Green", "Expected Green got " ~ color); 264 265 color = menu!string("What color?", ["Blue", "Green"]); 266 assert(color == "Green", "Expected Green got " ~ color); 267 outfile.rewind; 268 assert(outfile.readln().strip == "What color?"); 269 } 270 271 272 /** 273 * Requires that a value be provided and valid based on 274 * the delegate passed in. It must also check against null input. 275 * 276 * -------- 277 * auto num = require!(int, "a > 0 && a <= 10")("Enter a number from 1 to 10"); 278 * -------- 279 * 280 * Throws: NoInputException if the user does not provide any value. 281 * ConvError if the user does not provide any value. 282 */ 283 T require(T, alias cond)(in string question, in string failure = null) 284 { 285 alias unaryFun!(cond) call; 286 T ans; 287 while(1) 288 { 289 ans = userInput!T(question); 290 if(call(ans)) 291 break; 292 if(failure) 293 writeln(failure); 294 } 295 296 return ans; 297 } 298 299 version(unittest_scriptlike_d) 300 unittest 301 { 302 mixin(selfCom(["1","11","3"])); 303 mixin(selfCom()); 304 auto num = require!(int, "a > 0 && a <= 10")("Enter a number from 1 to 10"); 305 assert(num == 1, "Expected 1 got" ~ num.to!string); 306 num = require!(int, "a > 0 && a <= 10")("Enter a number from 1 to 10"); 307 assert(num == 3, "Expected 1 got" ~ num.to!string); 308 outfile.rewind; 309 assert(outfile.readln().strip == "Enter a number from 1 to 10"); 310 } 311 312 313 /** 314 * Used when input was not provided. 315 */ 316 class NoInputException: Exception 317 { 318 this(string msg) 319 { 320 super(msg); 321 } 322 } 323 324 version(unittest_scriptlike_d) 325 private string selfCom() 326 { 327 string ans = q{ 328 auto outfile = File.tmpfile(); 329 auto origstdout = stdout; 330 scope(exit) stdout = origstdout; 331 stdout = outfile;}; 332 333 return ans; 334 } 335 336 version(unittest_scriptlike_d) 337 private string selfCom(string[] input) 338 { 339 string ans = q{ 340 auto infile = File.tmpfile(); 341 auto origstdin = stdin; 342 scope(exit) stdin = origstdin; 343 stdin = infile;}; 344 345 foreach(i; input) 346 ans ~= "infile.writeln(`"~i~"`);"; 347 ans ~= "infile.rewind;"; 348 349 return ans; 350 }