1 module dinu.commandBuilder; 2 3 4 import dinu; 5 6 7 __gshared: 8 9 10 immutable(Command)[] output; 11 12 13 unittest { 14 string text = "€äüö"; 15 assert(text.to!dstring == text.toUTF32); 16 } 17 18 19 bool isText(dchar c){ 20 return !c.isWhite && c != '/'; 21 } 22 23 void delChar(ref dstring text, size_t cursor){ 24 if(cursor < text.length) 25 text = text[0..cursor] ~ text[cursor+1..$]; 26 } 27 28 void delBackChar(ref dstring text, ref size_t cursor){ 29 if(cursor){ 30 text = text[0..cursor-1] ~ text[cursor..$]; 31 cursor--; 32 } 33 } 34 35 void deleteWordLeft(ref dstring text, ref size_t cursor){ 36 if(!text.length || !cursor) 37 return; 38 text.delBackChar(cursor); 39 if(!cursor) 40 return; 41 bool mode = text[cursor-1].isText; 42 while(cursor && mode == text[cursor-1].isText){ 43 text = text[0..cursor-1] ~ text[cursor..$]; 44 cursor--; 45 } 46 } 47 48 void deleteWordRight(ref dstring text, size_t cursor){ 49 text.delChar(cursor); 50 if(cursor >= text.length) 51 return; 52 bool mode = text[cursor].isText; 53 while(cursor < text.length && mode == text[cursor].isText){ 54 text = text[0..cursor] ~ text[cursor+1..$]; 55 } 56 } 57 58 59 60 class CommandBuilder { 61 62 FuzzyFilter!Command choiceFilter; 63 64 dstring[] command; 65 size_t editing; 66 size_t cursorStart; 67 bool shiftDown; 68 size_t cursor; 69 70 dstring filterText; 71 72 immutable(Command)[] commandSelected; 73 int logIdx=1; 74 string[] scannedDirs; 75 immutable(Command)[] bashCompletions; 76 long selected; 77 78 OutputLoader outputLoader; 79 ExecutablesLoader execLoader; 80 TalkProcessLoader processLoader; 81 FilesLoader filesLoader; 82 WindowsLoader windowsLoader; 83 84 immutable(Command)[] history; 85 86 this(){ 87 88 choiceFilter = new FuzzyFilter!Command((c){ 89 if(toString.length && toString[0] == '@' && editing == 0){ 90 return c.type == Type.processInfo; 91 }else if(!bashCompletions.length){ 92 auto filter = [Type.file, Type.directory]; 93 if(editing == 0) 94 filter ~= [Type.script, Type.desktop, Type.special, Type.history, Type.window]; 95 return filter.canFind(c.type); 96 }else if(bashCompletions.length){ 97 return c.type == Type.bashCompletion; 98 }else 99 return false; 100 }); 101 102 outputLoader = new OutputLoader; 103 outputLoader.each((c){ 104 if(c.type == Type.history){ 105 synchronized(this) 106 history = c ~ history; 107 choiceFilter.add(c); 108 }else{ 109 synchronized(this){ 110 if(c.score >= 10000*999) 111 output = c ~ output; 112 else 113 output ~= c; 114 } 115 } 116 }); 117 118 reset; 119 resetChoices; 120 resetFilter; 121 } 122 123 immutable(Match!Command)[] results(){ 124 return choiceFilter.res; 125 } 126 127 void cleanup(){ 128 if(execLoader) 129 execLoader.stop; 130 if(processLoader) 131 processLoader.stop; 132 if(filesLoader) 133 filesLoader.stop; 134 } 135 136 void destroy(){ 137 cleanup; 138 outputLoader.stop; 139 windowsLoader.stop; 140 choiceFilter.stop; 141 } 142 143 void reset(){ 144 command = [""]; 145 editing = 0; 146 cursor = 0; 147 cursorStart = 0; 148 filterText = ""; 149 commandSelected = null; 150 choiceFilter.reset(""); 151 } 152 153 void resetFilter(){ 154 choiceFilter.reset(text.to!string); 155 selected = -1; 156 } 157 158 void resetChoices(){ 159 bashCompletions = []; 160 synchronized(this) 161 choiceFilter.set(history.idup); 162 163 if(execLoader) 164 execLoader.stop; 165 execLoader = new ExecutablesLoader; 166 execLoader.each(&choiceFilter.add); 167 168 if(processLoader) 169 processLoader.stop; 170 processLoader = new TalkProcessLoader; 171 processLoader.each(&choiceFilter.add); 172 173 scannedDirs = []; 174 if(filesLoader) 175 filesLoader.stop; 176 filesLoader = new FilesLoader(getcwd, options.directoryDepth); 177 filesLoader.each((c){ 178 if(c.type == Type.directory){ 179 synchronized(this){ 180 if(scannedDirs.canFind(c.text)) 181 return; 182 else 183 scannedDirs ~= c.text; 184 } 185 } 186 choiceFilter.add(c); 187 }); 188 189 if(windowsLoader) 190 windowsLoader.stop; 191 windowsLoader = new WindowsLoader; 192 windowsLoader.each(&choiceFilter.add); 193 194 choiceFilter.reset(text.to!string); 195 } 196 197 void resetState(bool force=false){ 198 if(!force && editing == 0) 199 commandSelected = null; 200 if(force || editing != 0 || !text.length){ 201 filterText = ""; 202 } 203 } 204 205 void checkNativeCompletions(){ 206 choiceFilter.remove(bashCompletions); 207 resetFilter; 208 bashCompletions = []; 209 if(command.length > 1){ 210 task({ 211 l:foreach(c; loadBashCompletion(toString)){ 212 foreach(e; bashCompletions) 213 if(e.text == c) 214 continue l; 215 auto completion = new immutable CommandBashCompletion(c); 216 bashCompletions ~= completion; 217 choiceFilter.add(completion); 218 resetFilter; 219 } 220 }).executeInNewThread; 221 } 222 } 223 224 string finishedPart(){ 225 return command[0..editing] 226 .fold!"a ~ ' ' ~ b"(""d) 227 .to!string; 228 } 229 230 string cursorPart(){ 231 return finishedPart ~ text[0..cursor].to!string; 232 } 233 234 void clearOutput(){ 235 std.file.write(options.configPath ~ ".log", ""); 236 output = []; 237 history = []; 238 } 239 240 void run(bool r=true){ 241 /+ 242 if(!command[0].length && !commandHistory) 243 return; 244 +/ 245 if(!commandSelected){ 246 auto res = choiceFilter.res; 247 if(res.length && selected >= -1) 248 commandSelected = res[cast(size_t)(selected<0 ? 0 : selected)].data; 249 /+ 250 else 251 commandSelected = new immutable CommandFile("http://" ~ command[0].to!string); 252 +/ 253 } 254 auto params = ""; 255 if(command.length > 1) 256 params = command[1..$].reduce!"a ~ ' ' ~ b".to!string; 257 commandSelected[0].run(params); 258 if(r){ 259 deleteLeft; 260 } 261 } 262 263 // Choice selection 264 265 void select(long selected){ 266 auto res = choiceFilter.res; 267 if(selected == -1){ 268 if(filterText.length){ 269 text = filterText[0..$-1]; 270 cursor = text.length; 271 cursorStart = cursor; 272 filterText = ""; 273 if(editing == 0) 274 commandSelected = null; 275 } 276 this.selected = -1; 277 }else if(selected > -1){ 278 selectChoice(selected); 279 }else{ 280 selectOutput(-selected-2); 281 } 282 } 283 284 void selectChoice(long selected){ 285 if(!filterText.length) 286 filterText = text ~ ' '; 287 auto res = choiceFilter.res; 288 selected = selected.min(res.length-1).max(0); 289 auto sel = res[cast(size_t)selected].data[0]; 290 if(editing == 0){ 291 if(sel.type == Type.history) 292 commandSelected = [(cast(CommandHistory)sel).command]; 293 else 294 commandSelected = [sel]; 295 if(sel.parameter.length){ 296 command = [commandSelected[0].text.to!dstring]; 297 command ~= sel.parameter.to!dstring; 298 editing = 0; 299 }else{ 300 command[0] = commandSelected[0].text.to!dstring; 301 } 302 }else{ 303 text = sel.text.to!dstring; 304 } 305 cursor = text.length; 306 cursorStart = cursor; 307 this.selected = selected; 308 } 309 310 void selectOutput(long selected){ 311 selected = selected.max(0).min(output.length-1); 312 if(!filterText.length) 313 filterText = text ~ ' '; 314 auto c = output[cast(size_t)selected]; 315 if(c.parameter.length) 316 text = c.parameter.to!dstring; 317 else 318 text = ('\'' ~ c.text ~ '\'').to!dstring; 319 cursor = text.length; 320 cursorStart = cursor; 321 this.selected = -selected-2; 322 } 323 324 // Text functions 325 326 ref dstring text(){ 327 return command[editing]; 328 } 329 330 void selectAll(){ 331 cursorStart = 0; 332 cursor = text.length; 333 } 334 335 void moveLeft(bool word=false){ 336 if(editing && cursor == 0){ 337 editing--; 338 cursor = command[editing].length; 339 cursorStart = cursor; 340 if(editing == 0){ 341 commandSelected = null; 342 } 343 resetFilter; 344 }else if(!word) 345 cursor = cast(size_t)max(0, cast(long)cursor-1); 346 if(!shiftDown) 347 cursorStart = cursor; 348 } 349 350 void moveRight(bool word=false){ 351 if(cursor == text.length && text.length && editing+1 < command.length){ 352 resetState; 353 if(editing == 0 && !commandSelected) 354 select(0); 355 editing++; 356 cursor = 0; 357 cursorStart = 0; 358 selected = -1; 359 resetFilter; 360 }else if(!word) 361 cursor = min(cursor+1, text.length); 362 if(!shiftDown) 363 cursorStart = cursor; 364 } 365 366 // Text altering 367 368 void insert(dstring s){ 369 if(cursorStart != cursor) 370 deleteSelection; 371 if(cursor == text.length && s == " " && (cursor<2 || text[cursor-2] != '\\')){ 372 if(!commandSelected && editing == 0){ 373 auto res = choiceFilter.res; 374 if(res.length && selected >= -1) 375 commandSelected = res[cast(size_t)(selected<0 ? 0 : selected)].data; 376 else { 377 auto c = new immutable CommandExec(command[0].to!string); 378 commandSelected = [c]; 379 } 380 text = commandSelected[0].text.to!dstring; 381 } 382 resetState(true); 383 command ~= ""; 384 editing++; 385 cursor = 0; 386 cursorStart = 0; 387 resetFilter; 388 checkNativeCompletions; 389 return; 390 } 391 392 if(editing == 0){ 393 commandSelected = null; 394 } 395 if(!text.length) 396 choiceFilter.reset(""); 397 text = text[0..cursor] ~ s ~ text[cursor..$]; 398 cursor += s.length; 399 cursorStart = cursor; 400 choiceFilter.narrow(s.to!string); 401 402 if(filterText.length){ 403 filterText = ""; 404 resetFilter; 405 } 406 select(-1); 407 408 if(s.endsWith("-", "=")) 409 checkNativeCompletions; 410 411 filesLoader.update(text); 412 413 } 414 415 void deleteLeft(){ 416 reset; 417 resetChoices; 418 select(-1); 419 checkNativeCompletions; 420 filesLoader.update(text); 421 } 422 423 void deleteSelection(){ 424 text = text[0..min(cursorStart,cursor)] ~ text[max(cursorStart,cursor)..$]; 425 cursor = cursorStart = min(cursorStart,cursor); 426 } 427 428 void delChar(){ 429 resetState; 430 if(cursorStart == cursor) 431 text.delChar(cursor); 432 else 433 deleteSelection; 434 resetFilter; 435 checkNativeCompletions; 436 filesLoader.update(text); 437 } 438 439 void delBackChar(){ 440 resetState; 441 if(cursorStart == cursor){ 442 if(cursor == 0 && command.length && editing > 0){ 443 command = command[0..editing] ~ command[editing+1..$]; 444 moveLeft; 445 return; 446 } 447 text.delBackChar(cursor); 448 }else{ 449 deleteSelection; 450 } 451 cursorStart = cursor; 452 resetFilter; 453 checkNativeCompletions; 454 filesLoader.update(text); 455 } 456 457 void deleteWordLeft(){ 458 resetState; 459 if(cursor == 0 && command.length && editing > 0){ 460 command = command[0..editing] ~ command[editing+1..$]; 461 moveLeft; 462 } 463 auto oldLength = text.length; 464 text.deleteWordLeft(cursor); 465 cursorStart = cursor; 466 resetFilter; 467 checkNativeCompletions; 468 filesLoader.update(text); 469 } 470 471 void deleteWordRight(){ 472 resetState; 473 text.deleteWordRight(cursor); 474 resetFilter; 475 checkNativeCompletions; 476 filesLoader.update(text); 477 } 478 479 override string toString(){ 480 if(commandSelected){ 481 if(command.length > 1) 482 return commandSelected[0].text ~ command[1..$].fold!"a ~ ' ' ~ b"(""d).to!string; 483 else 484 return commandSelected[0].text; 485 } 486 if(command.length) 487 return command.fold!"a ~ ' ' ~ b".to!string; 488 return ""; 489 } 490 491 }