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