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