1 /*
2 	 -------------------------------------------------------------------
3 
4 	 Copyright (C) 2014, Edwin van Leeuwen
5 
6 	 This file is part of todod todo list manager.
7 
8 	 Todod is free software; you can redistribute it and/or modify
9 	 it under the terms of the GNU General Public License as published by
10 	 the Free Software Foundation; either version 3 of the License, or
11 	 (at your option) any later version.
12 
13 	 Todod is distributed in the hope that it will be useful,
14 	 but WITHOUT ANY WARRANTY; without even the implied warranty of
15 	 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 	 GNU General Public License for more details.
17 
18 	 You should have received a copy of the GNU General Public License
19 	 along with Todod. If not, see <http://www.gnu.org/licenses/>.
20 
21 	 -------------------------------------------------------------------
22 	 */
23 
24 module todod.shell;
25 
26 import std.stdio;
27 import std.array;
28 import std.range;
29 
30 import std.string;
31 import std.regex;
32 import std.container;
33 import std.algorithm;
34 import std.conv;
35 
36 import colorize;
37 
38 import todod.commandline;
39 import todod.date;
40 import todod.dependency;
41 import todod.random;
42 import todod.state;
43 import todod.tag;
44 import todod.todo;
45 
46 auto addTagRegex = regex(r"(?:^|\s)\+(\w+)");
47 auto delTagRegex = regex(r"(?:^|\s)\-(\w+)");
48 auto allTagRegex = regex(r"(?:^|\s)[+-](\w+)");
49 
50 unittest {
51 	assert( match( "+tag", addTagRegex ) );
52 	assert( !match( "-tag", addTagRegex ) );
53 	assert( match( "bla +tag bla", addTagRegex ) );
54 	assert( !match( "bla+tag", addTagRegex ) );
55 
56 	assert( !match( "+tag", delTagRegex ) );
57 	assert( match( "-tag", delTagRegex ) );
58 	assert( match( "bla -tag blaat", delTagRegex ) );
59 	assert( !match( "bla-tag", delTagRegex ) );
60 
61 	assert( match( "+tag", allTagRegex ) );
62 	assert( match( "-tag", allTagRegex ) );
63 	assert( match( "bla -tag", allTagRegex ) );
64 	assert( !match( "bla-tag", allTagRegex ) );
65 }
66 
67 /// parse a string and return a tuple with TagDelta and the rest of the string
68 auto parseAndRemoveTags( string str ) {
69 	TagDelta td;
70 	auto m = matchAll( str, addTagRegex );
71 	foreach ( hits ; m ) {
72 		td.add_tags.add( new Tag( hits[1] ) );
73 	}
74 	m = matchAll( str, delTagRegex );
75 	foreach ( hits ; m ) {
76 		td.delete_tags.add( new Tag( hits[1] ) );
77 	}
78 	
79 	// Should be possible to do matching 
80 	// and replacing with one call to replaceAll!( dg ) but didn't
81 	// work for me
82 	str = replaceAll( str, allTagRegex, "" );
83 	// Replace multiple spaces
84 	str = replaceAll( str, regex(r"(?:^|\s) +"), "" );
85 	return tuple(td, str);
86 }
87 
88 
89 
90 TagDelta parseTags( string str ) {
91 	TagDelta td;
92 	auto m = matchAll( str, addTagRegex );
93 	foreach ( hits ; m ) {
94 		td.add_tags.add( new Tag( hits[1] ) );
95 	}
96 	m = matchAll( str, delTagRegex );
97 	foreach ( hits ; m ) {
98 		td.delete_tags.add( new Tag( hits[1] ) );
99 	}
100 	return td;
101 }
102 
103 unittest {
104 	auto td = parseTags( "+tag1" );
105 	assert( std.algorithm.equal(td.add_tags.array, [new Tag("tag1")]) );
106 	td = parseTags( "+tag1 +tag2" );
107 	assert( std.algorithm.equal(td.add_tags.array, [new Tag("tag1"),
108 				new Tag("tag2")]) );
109 	td = parseTags( "+tag1+tag2" );
110 	assert( std.algorithm.equal(td.add_tags.array, [new Tag("tag1")]) ); 
111 
112 	// Same for negative tags
113 	td = parseTags( "-tag1" );
114 	assert( std.algorithm.equal(td.delete_tags.array, [new Tag("tag1")]) );
115 	td = parseTags( "-tag1 -tag2" );
116 	assert( std.algorithm.equal(td.delete_tags.array, [new Tag("tag1"), 
117 				new Tag("tag2")]) );
118 	td = parseTags( "-tag1-tag2" );
119 	assert( std.algorithm.equal(td.delete_tags.array, [new Tag("tag1")]) );
120 
121 	td = parseTags( "-tag1+tag2" );
122 	assert( std.algorithm.equal(td.delete_tags.array, [new Tag("tag1")]) );
123 	assert( td.add_tags.length == 0 );
124 	td = parseTags( "+tag1-tag2" );
125 	assert( std.algorithm.equal(td.add_tags.array, [new Tag("tag1")]) );
126 	assert( td.delete_tags.length == 0 );
127 
128 	td = parseTags( "-tag1 +tag2" );
129 	assert( std.algorithm.equal(td.delete_tags.array, [new Tag("tag1")]) );
130 	assert( std.algorithm.equal(td.add_tags.array, [new Tag("tag2")]) ); 
131 	td = parseTags( "+tag1 -tag2" );
132 	assert( std.algorithm.equal(td.delete_tags.array, [new Tag("tag2")]) );
133 	assert( std.algorithm.equal(td.add_tags.array, [new Tag("tag1")]) ); 
134 }
135 
136 /// Return tuple witch string with due date removed. Due date is something along
137 /// D2014-01-12
138 auto parseAndRemoveDueDate( string str ) {
139 	// Due dates
140 	auto date_regex = regex( r"D(\d\d\d\d-\d\d-\d\d)" );
141 	Date dt;
142 	auto due_m = matchFirst( str, date_regex );
143 	if (due_m) {
144 		dt = Date( due_m.captures[1] );
145 		str = replaceAll( str, date_regex, "" );
146 	} else {
147 		date_regex = regex( r"(?:^|\s)D\+(\d+)" );
148 		due_m = matchFirst( str, date_regex );
149 		if (due_m) {
150 			dt = Date.now;
151 			dt.addDays( to!long( due_m.captures[1] ) );
152 			str = replaceAll( str, date_regex, "" );
153 		}
154 	}
155 	// Replace multiple spaces
156 	str = replaceAll( str, regex(r"(?:^|\s) +"), "" );
157 	return tuple( dt, str );
158 }
159 
160 unittest {
161 	auto tup = parseAndRemoveDueDate( "D2014-01-12" );
162 	assert( tup[0].substract( Date( "2014-01-08" ) ) == 4 );
163 	assert( tup[1] == "" );
164 
165 	tup = parseAndRemoveDueDate( "Bla D2014-01-12" );
166 	assert( tup[0].substract( Date( "2014-01-08" ) ) == 4 );
167 	assert( tup[1] == "Bla " );
168 
169 	tup = parseAndRemoveDueDate( "Bla" );
170 	assert( !tup[0] );
171 	assert( tup[1] == "Bla" );
172 }
173 
174 /// Return date from string. Date is something along 2014-01-12
175 auto parseDate( string str, Date from = Date.now ) {
176 	// Due dates
177 	auto date_regex = regex( r"(\d\d\d\d-\d\d-\d\d)" );
178 	Date dt;
179 	auto due_m = matchFirst( str, date_regex );
180 	if (due_m) {
181 		dt = Date( due_m.captures[1] );
182 		return dt;
183 	} else {
184 		due_m = matchFirst( str, r"(?:^|\s|D)\+(\d+)" );
185 		if (due_m) {
186 			from.addDays( to!long( due_m.captures[1] ) );
187 		}
188 		return from.dup;
189 	}
190 }
191 
192 unittest {
193 	auto fromDate = Date("2014-01-16");
194 	auto dt = parseDate( "Bla +4 Bla", fromDate );
195 	assert( dt.substract( fromDate ) == 4 );
196 
197 	dt = parseDate( "Bla D+31 Bla", fromDate );
198 	assert( dt.substract( fromDate ) == 31 );
199 
200 	dt = parseDate( "+11 Bla", fromDate );
201 	assert( dt.substract( fromDate ) == 11 );
202 }
203 
204 
205 
206 Todo applyTags( Todo td, TagDelta delta ) {
207 	td.tags.add( delta.add_tags );
208 	td.tags.remove( delta.delete_tags );
209 	return td;
210 }
211 
212 unittest {
213 	TagDelta delta;
214 	delta.add_tags.add( [new Tag("tag2"), new Tag("tag1")] );
215 	Todo td = new Todo("Todo1"); 
216 	td = applyTags( td, delta );
217 	assert( equal( td.tags.array, [new Tag("tag1"), new Tag("tag2")] ) );
218 	td = applyTags( td, delta );
219 	assert( equal( td.tags.array, [new Tag("tag1"), new Tag("tag2")] ) );
220 	delta.delete_tags.add( [new Tag("tag3"), new Tag("tag1")] );
221 	td = applyTags( td, delta );
222 	assert( equal( td.tags.array, [new Tag("tag2")] ) );
223 }
224 
225 /// Wrap string in color used for tags
226 string tagColor( string str ) {
227 	return color( str, fg.red );
228 }
229 
230 /// Wrap string in color used for emphasizing titles 
231 string titleEmphasize( string str ) {
232 	return color( str, fg.green );
233 }
234 
235 /// Produce colored string from tags
236 string prettyStringTags( Tags tags ) {
237 	string line;
238 	foreach( tag; tags ) {
239 		line ~= tag.name ~ " ";
240 	}
241 	return tagColor( line );
242 }
243 
244 string prettyStringTodo( Todo t ) {
245 	Date currentDate = Date.now;
246 	string description = titleEmphasize(t.title) ~ "\n";
247 	description ~= "\t  Tags:       " ~ prettyStringTags( t.tags ) ~"\n";
248 	description ~= "\t  " ~ "Last progress    " 
249 		~ tagColor(to!string( lastProgress( t ) ).rightJustify( 4 )) ~
250 		" days ago\n";
251 
252 	if ( t.due_date )
253 		description ~= "\t  " ~ "Due in           " ~ tagColor( 
254 				to!string( t.due_date.substract( currentDate ) ).rightJustify( 4 ) ) 
255 			~ " days\n";
256 
257 	return description;
258 }
259 
260 string prettyStringTodos(RANGE)( RANGE ts, Todos allTodos, Tags allTags,
261 		TagDelta selectedTags, string searchString, in Dependencies deps, in double[string] defaultWeights,
262 		bool showWeight = false ) {
263 	string str;
264 	size_t id = 0;
265 	foreach( t; ts ) {
266 		str = str ~ to!string( id ) ~ "\t" ~ prettyStringTodo( t );
267 		if (showWeight)
268 			str ~= "Weight: " ~ to!string( weight( t, selectedTags, searchString,
269 						allTodos.length, allTodos.tagOccurence( allTags ), deps, 
270 						defaultWeights ) ) ~ "\n";
271 		str ~= "\n";
272 		id++;
273 	}
274 	return str;
275 }
276 
277 string prettyStringState( State state, bool showWeight = false ) {
278 	string str = "Tags and number of todos associated with that tag.\n";
279 	auto tags = state.todos.tagOccurence( state.tags );
280 	foreach( tag, count; tags ) {
281 		if (!state.selectedTags.delete_tags.canFind( tag )) {
282 			if (state.selectedTags.add_tags.canFind( tag ))
283 				str ~= tagColor(tag.name) ~ " (" ~ count.to!string ~ "),  ";
284 			else
285 				str ~= tag.name ~ " (" ~ count.to!string ~ "),  ";
286 		}
287 	}
288 	str ~= "\n\n";
289 	str ~= prettyStringTodos( state.selectedTodos, state.todos, state.tags, 
290 				state.selectedTags, state.searchString, state.dependencies, 
291 				state.defaultWeights, showWeight );
292 	debug {
293 		str ~= "Debug: Selected " ~ state.selectedTags.add_tags.to!string ~ "\n";
294 		str ~= "Debug: Deselected " ~ state.selectedTags.delete_tags.to!string ~ "\n";
295 	}
296 	return str;
297 }
298 
299 Commands!( State delegate( State, string) ) addShowCommands( 
300 		ref Commands!( State delegate( State, string) ) main ) {
301 	auto showCommands = Commands!( State delegate( State, string) )(
302 			"Show different views. When called without parameters shows a (randomly) selected list of Todos.");
303 
304 	showCommands.add( 
305 			"tags", delegate( State state, string parameter ) {
306 				writeln( prettyStringTags( state.tags ) );
307 				return state;
308 			}, "List of tags" );
309 
310 	showCommands.add( 
311 			"dependencies", delegate( State state, string parameter ) {
312 				auto groups = groupByChild( state.dependencies );
313 				foreach ( child, parents; groups ) {
314 					auto childT = state.todos.find!( (a) => a.id == child );
315 					writeln( prettyStringTodo(childT[0]) );
316 					writeln( "depends on:" );
317 					foreach( parent; parents ) {
318 						auto parentT = state.todos.find!( (a) => a.id == parent );
319 						writeln( prettyStringTodo(parentT[0]) );
320 					}
321 					writeln("");
322 				}
323 			return state;
324 		}, "Show list of current dependencies." );
325 
326 	showCommands.add( 
327 			"help", delegate( State state, string parameter ) {
328 			state = main["clear"]( state, "" ); 
329 			writeln( showCommands.toString );
330 			return state;
331 		}, "Print this help message" );
332 
333 	showCommands.add(
334 			"weight", delegate( State state, string parameter ) {
335 			// Weight is actually captured in main["show"].
336 			// This is a dummy for help and tabcompletion
337 			return state; }, "Show weights for selected Todos" );
338 
339 	main.add( 
340 			"show", delegate( State state, string parameter ) {
341 				state = main["clear"]( state, "" ); 
342 				if ( parameter == "" ) {
343 					writeln( prettyStringState( state ) );
344 				} else if (parameter == "weight") {
345 					writeln( prettyStringState( state, true ) );
346 				} else {
347 					auto split = parameter.findSplit( " " );
348 					state = showCommands[split[0]]( state, split[2] );
349 				}
350 				return state;
351 			}, "Show different views. When called without parameters shows a (randomly) selected list of Todos. See show help for more options" );
352 
353 	main.addCompletion( "show",
354 		delegate( string cmd, string parameter ) {
355 			string[] results;
356 			auto m = match( parameter, "^([A-z]*)$" );
357 			if (m) {
358 				// Main commands
359 				string[] command_keys = showCommands.commands;
360 				auto matchingCommands =
361 					filter!( a => match( a, m.captures[1] ))( command_keys );
362 				foreach ( com; matchingCommands ) {
363 					results ~= [cmd ~ " " ~ com];
364 				}
365 			}
366 			return results;
367 		}
368 	);
369 	return main;
370 }
371 
372 /// Range that either returns elements from the array targets or returns infinitively increasing range (when all is set)
373 struct Targets {
374 	bool all = false;
375 	int[] targets;
376 	int count = 0;
377 
378 	@property bool empty() const
379 	{
380 		if (all)
381 			return false;
382 		else 
383 			return targets.empty;
384 	}
385 
386 	@property int front()  const
387 	{
388 		if (all)
389 			return count;
390 		else
391 			return targets.front();
392 	}
393 	void popFront() 
394 	{
395 		if (all)
396 			count++;
397 		else
398 			targets.popFront;
399 	}
400 
401 	/// Apply a delegate to all todos specified by targets
402 	void apply( void delegate( ref Todo ) dg, Todo[] selectedTodos  ) {
403 		auto first = front;
404 		size_t count = 0;
405 		popFront;
406 		foreach ( ref t; selectedTodos ) {
407 			if (count == first) {
408 				dg( t );
409 				if ( empty )
410 					break;
411 				else {
412 					first = front;
413 					popFront;
414 				}
415 			}
416 			count++;
417 		}
418 	}
419 }
420 
421 /// Convert all or 1,.. into Targets
422 Targets parseTarget( string target ) {
423 	int[] targets;
424 	Targets ts;
425 	auto last_term = matchFirst( target, r"\S+$" )[0];
426 	if (match(last_term, r"(\d+,*){1,}$")) {
427 		auto map_result = (map!(a => to!int( a ))( split( last_term, regex(",")) ));
428 		foreach( a; map_result )
429 			targets ~= a;
430 		targets.sort;
431 	} else if ( last_term == "all" ) {
432 		ts.all = true;
433 		return ts;
434 	}
435 	ts.targets = targets;
436 	return ts;
437 }
438 
439 unittest {
440 	auto targets = parseTarget( "bla 2" );
441 	assert( equal( targets.take(2), [2] ) );
442 	targets = parseTarget( "bla 2,1" );
443 	assert( equal( targets, [1,2] ) );
444 
445 	targets = parseTarget( "bla 2,a" );
446 	assert( targets.walkLength == 0 );
447 	targets = parseTarget( "bla all" );
448 	assert( equal(targets.take(5), [0,1,2,3,4]) );
449 
450 	targets = parseTarget( "bla" );
451 	assert( targets.take(5).walkLength == 0 );
452 }
453 
454