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 }