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