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 you 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 auto ans = readln(); 66 67 static if(is(T == bool)) 68 { 69 switch(ans.front) 70 { 71 case 'y', 'Y': 72 return true; 73 default: 74 } 75 switch(ans.strip()) 76 { 77 case "continue": 78 case "ok": 79 return true; 80 default: 81 return false; 82 } 83 } else 84 { 85 if(ans == "\x0a") 86 throw new NoInputException("Value required, " 87 "cannot continue operation."); 88 static if(isSomeChar!T) 89 { 90 return to!(T)(ans[0]); 91 } else 92 return to!(T)(ans.strip()); 93 } 94 } 95 96 ///ditto 97 void userInput(T = string)(string question, ref T result) 98 { 99 result = userInput!T(question); 100 } 101 102 version(unittest_scriptlike_d) 103 unittest 104 { 105 mixin(selfCom(["10PM", "9PM"])); 106 mixin(selfCom()); 107 auto s = userInput("What time is it?"); 108 assert(s == "10PM", "Expected 10PM got" ~ s); 109 outfile.rewind; 110 assert(outfile.readln().strip == "What time is it?"); 111 112 outfile.rewind; 113 userInput("What time?", s); 114 assert(s == "9PM", "Expected 9PM got" ~ s); 115 outfile.rewind; 116 assert(outfile.readln().strip == "What time?"); 117 } 118 119 /** 120 * Pauses and prompts the user to press Enter (or "Return" on OSX). 121 * 122 * This is similar to the Windows command line's PAUSE command. 123 * 124 * -------- 125 * pause(); 126 * pause("Thanks. Please press Enter again..."); 127 * -------- 128 */ 129 void pause(string prompt = defaultPausePrompt) 130 { 131 //TODO: This works, but needs a little work. Currently, it echoes all 132 // input until Enter is pressed. Fixing that requires some low-level 133 // os-specific work. 134 // 135 // For reference: 136 // http://stackoverflow.com/questions/6856635/hide-password-input-on-terminal 137 // http://linux.die.net/man/3/termios 138 139 write(prompt); 140 stdout.flush(); 141 getchar(); 142 } 143 144 version(OSX) 145 enum defaultPausePrompt = "Press Return to continue..."; /// 146 else 147 enum defaultPausePrompt = "Press Enter to continue..."; /// 148 149 150 /** 151 * Gets a valid path folder from the user. The string will not contain 152 * quotes, if you are using in a system call and the path contain spaces 153 * wrapping in quotes may be required. 154 * 155 * -------- 156 * auto confFile = pathLocation("Where is the configuration file?"); 157 * -------- 158 * 159 * Throws: NoInputException if the user does not provide a path. 160 */ 161 string pathLocation(string action) 162 { 163 string ans; 164 165 do 166 { 167 if(ans !is null) 168 writeln("Could not locate that file."); 169 ans = userInput(action); 170 // Quotations will generally cause problems when 171 // using the path with std.file and Windows. This removes the quotes. 172 ans = ans.removechars("\";").strip(); 173 ans = ans[0] == '"' ? ans[1..$] : ans; // removechars skips first char 174 } while(!exists(ans)); 175 176 return ans; 177 } 178 179 /** 180 * Creates a menu from a Range of strings. 181 * 182 * It will require that a number is selected within the number of options. 183 * 184 * If the the return type is a string, the string in the options parameter will 185 * be returned. 186 * 187 * Throws: NoInputException if the user wants to quit. 188 */ 189 T menu(T = ElementType!(Range), Range) (string question, Range options) 190 if((is(T==ElementType!(Range)) || is(T==int)) && 191 isForwardRange!(Range)) 192 { 193 string ans; 194 int maxI; 195 int i; 196 197 while(true) 198 { 199 writeln(question); 200 i = 0; 201 foreach(str; options) 202 { 203 writefln("%8s. %s", i+1, str); 204 i++; 205 } 206 maxI = i+1; 207 208 writefln("%8s. %s", "No Input", "Quit"); 209 ans = userInput!(string)("").strip(); 210 int ians; 211 212 try 213 { 214 ians = to!(int)(ans); 215 } catch(ConvException ce) 216 { 217 bool found; 218 i = 0; 219 foreach(o; options) 220 { 221 if(ans.toLower() == to!string(o).toLower()) 222 { 223 found = true; 224 ians = i+1; 225 break; 226 } 227 i++; 228 } 229 if(!found) 230 throw ce; 231 232 } 233 234 if(ians > 0 && ians <= maxI) 235 static if(is(T==ElementType!(Range))) 236 static if(isRandomAccessRange!(Range)) 237 return options[ians-1]; 238 else 239 { 240 take!(ians-1)(options); 241 return options.front; 242 } 243 else 244 return ians; 245 else 246 writeln("You did not select a valid entry."); 247 } 248 } 249 250 version(unittest_scriptlike_d) 251 unittest 252 { 253 mixin(selfCom(["1","Green", "green","2"])); 254 mixin(selfCom()); 255 auto color = menu!string("What color?", ["Blue", "Green"]); 256 assert(color == "Blue", "Expected Blue got " ~ color); 257 258 auto ic = menu!int("What color?", ["Blue", "Green"]); 259 assert(ic == 2, "Expected 2 got " ~ ic.to!string); 260 261 color = menu!string("What color?", ["Blue", "Green"]); 262 assert(color == "Green", "Expected Green got " ~ color); 263 264 color = menu!string("What color?", ["Blue", "Green"]); 265 assert(color == "Green", "Expected Green got " ~ color); 266 outfile.rewind; 267 assert(outfile.readln().strip == "What color?"); 268 } 269 270 271 /** 272 * Requires that a value be provided and valid based on 273 * the delegate passed in. It must also check against null input. 274 * 275 * -------- 276 * auto num = require!(int, "a > 0 && a <= 10")("Enter a number from 1 to 10"); 277 * -------- 278 * 279 * Throws: NoInputException if the user does not provide any value. 280 * ConvError if the user does not provide any value. 281 */ 282 T require(T, alias cond)(in string question, in string failure = null) 283 { 284 alias unaryFun!(cond) call; 285 T ans; 286 while(1) 287 { 288 ans = userInput!T(question); 289 if(call(ans)) 290 break; 291 if(failure) 292 writeln(failure); 293 } 294 295 return ans; 296 } 297 298 version(unittest_scriptlike_d) 299 unittest 300 { 301 mixin(selfCom(["1","11","3"])); 302 mixin(selfCom()); 303 auto num = require!(int, "a > 0 && a <= 10")("Enter a number from 1 to 10"); 304 assert(num == 1, "Expected 1 got" ~ num.to!string); 305 num = require!(int, "a > 0 && a <= 10")("Enter a number from 1 to 10"); 306 assert(num == 3, "Expected 1 got" ~ num.to!string); 307 outfile.rewind; 308 assert(outfile.readln().strip == "Enter a number from 1 to 10"); 309 } 310 311 312 /** 313 * Used when input was not provided. 314 */ 315 class NoInputException: Exception 316 { 317 this(string msg) 318 { 319 super(msg); 320 } 321 } 322 323 version(unittest_scriptlike_d) 324 private string selfCom() 325 { 326 string ans = q{ 327 auto outfile = File.tmpfile(); 328 auto origstdout = stdout; 329 scope(exit) stdout = origstdout; 330 stdout = outfile;}; 331 332 return ans; 333 } 334 335 version(unittest_scriptlike_d) 336 private string selfCom(string[] input) 337 { 338 string ans = q{ 339 auto infile = File.tmpfile(); 340 auto origstdin = stdin; 341 scope(exit) stdin = origstdin; 342 stdin = infile;}; 343 344 foreach(i; input) 345 ans ~= "infile.writeln(`"~i~"`);"; 346 ans ~= "infile.rewind;"; 347 348 return ans; 349 }