public function testInOut() { $tests = array('1d' => '1d', '+2d' => '+2d', '3w' => '3w', '+4w' => '+4w', '5m' => '5m', '+6m' => '+6m', '7y' => '7y', '+8y' => '+8y', '9' => '9d', '+9' => '+9d', '11d b' => '11db', '+12d b' => '+12db', '13w b' => '13wb', '+14w b' => '+14wb', '15m b' => '15mb', '+16m b' => '+16mb', '17y b' => '17yb', '+18y b' => '+18yb', '19 b' => '19db', '+19 b' => '+19db', '11db' => '11db', '+12db' => '+12db', '13wb' => '13wb', '+14wb' => '+14wb', '15mb' => '15mb', '+16mb' => '+16mb', '17yb' => '17yb', '+18yb' => '+18yb', '19b' => '19db', '+19b' => '+19db'); foreach ($tests as $in => $out) { $r = new Recurrent($in); $this->assertEquals($r->toString(), $out); } }
public function start() { $this->readLine = new ReadLine(); $this->readLine->setCompletitionCallback(function ($input) { return $this->readlineCompletion($input); }); try { $this->readLine->historyLoad(Config::$config['gui']['history_file']); } catch (HistoryLoadException $hle) { echo $hle->getMessage() . PHP_EOL; exit(-1); } // Clear screen echo "c"; while (true) { $this->loadIfChanged(); $search = $this->search; if ($search === null) { $this->filteredTodos = $this->todos; } else { $this->filteredTodos = $this->todos->array_filter(function ($todo) use($search) { foreach ($search as $s) { if ($s['not'] && stripos($todo->text, $s['text']) !== false) { return false; } } foreach ($search as $s) { if (!$s['not'] && stripos($todo->text, $s['text']) !== false) { return true; } } foreach ($search as $s) { if (!$s['not']) { return false; } } return true; }); } // Screen height $maxTodos = $this->getTerminalHeight() - 15; if ($maxTodos <= 0) { echo "c"; echo 'Too small terminal, make it taller' . PHP_EOL; sleep(Config::$config['gui']['reload_timeout']); continue; } // Detect max length of every column $columns = Config::$config['gui']['columns']; $lengths = array(); $minLengths = array(); foreach ($columns as $column) { if (!isset($this->columns[$column])) { echo 'Unknow column: ' . $column . ', check configuration gui.columns!' . PHP_EOL; exit(-1); } $lengths[$column] = mb_strlen($this->columns[$column]['title']); $minLengths[$column] = $lengths[$column]; } $pos = 0; foreach ($this->filteredTodos as $k => $todo) { $pos++; foreach ($columns as $column) { $len = mb_strlen($this->columnValue($k, $column)); if ($len > $lengths[$column]) { $lengths[$column] = $len; } } if ($pos >= $maxTodos) { break; } } // Screen width $maxWidth = $this->getTerminalWidth(); $width = -1; foreach ($lengths as $length) { $width += $length + 3; } $needShorten = $width - $maxWidth; if ($needShorten > 0) { // Try to shorten few columns $posShortens = array(); $posShortenSum = 0; foreach ($columns as $column) { if ($this->columns[$column]['shorten'] && $lengths[$column] > $minLengths[$column]) { $posShortens[$column] = $lengths[$column] - $minLengths[$column]; $posShortenSum += $posShortens[$column]; } } $shortened = 0; foreach ($posShortens as $column => $posShorten) { $shorten = floor((double) $posShorten / (double) $posShortenSum * (double) $needShorten); $lengths[$column] -= $shorten; $posShortens[$column] -= $shorten; $shortened += $shorten; } // Fix rounding problem foreach ($posShortens as $column => $posShorten) { if ($shortened >= $needShorten) { break; } if ($posShorten <= 0) { continue; } $shorten = 1; $lengths[$column] -= $shorten; $posShortens[$column] -= $shorten; $shortened += $shorten; } // Nothing more to shorten if ($shortened < $needShorten) { echo "c"; echo 'Too small terminal, make it wider' . PHP_EOL; sleep(Config::$config['gui']['reload_timeout']); continue; } } // Got to the begging of screen echo "[0;0H"; // Show columns title $first = true; foreach ($columns as $column) { // Clear line echo "[K"; if ($first) { $first = false; } else { echo '|'; } echo $this->config2color(Config::$config['color']['title']); $this->printColumn($this->columns[$column]['title'], $lengths[$column], $this->columns[$column]['padTitle']); echo $this->config2color(Config::$config['color']['default']); } echo PHP_EOL; // Show todos $pos = 0; foreach ($this->filteredTodos as $k => $todo) { // Clear line echo "[K"; if ($pos++ % 2 == 0) { echo $this->config2color(Config::$config['color']['todo_odd']); } else { echo $this->config2color(Config::$config['color']['todo_even']); } $now = new DateTime('today'); if (!$todo->done && $todo->due !== null) { $diff = $todo->due->diff($now); if ($diff->days == 0) { echo $this->config2color(Config::$config['color']['todo_due_today']); } else { if (!$diff->invert) { echo $this->config2color(Config::$config['color']['todo_after_due']); } } } if ($todo->priority !== null) { if (isset(Config::$config['color']['todo_prio_' . $todo->priority])) { echo $this->config2color(Config::$config['color']['todo_prio_' . $todo->priority]); } elseif (isset(Config::$config['color']['todo_prio'])) { echo $this->config2color(Config::$config['color']['todo_prio']); } } $first = true; foreach ($columns as $column) { if ($first) { $first = false; } else { echo '|'; } $this->printColumn($this->columnValue($k, $column), $lengths[$column], $this->columns[$column]['padValue']); } echo $this->config2color(Config::$config['color']['default']); echo PHP_EOL; if ($pos >= $maxTodos) { echo "[K..." . PHP_EOL; break; } } // Clear rest of screen echo "[0J"; echo PHP_EOL; echo $this->message . PHP_EOL; echo 'c Create e Edit' . PHP_EOL; echo 'r Remove a Archive' . PHP_EOL; echo 'x Mark as done X Unmark as done' . PHP_EOL; echo 'd Set due date D Unset due date' . PHP_EOL; echo 'g Set recurrent G Unset recurrent' . PHP_EOL; echo 'p Priority P Unset priority' . PHP_EOL; echo 's Sort: '; $first = true; foreach ($this->sort as $col => $asc) { if ($first) { $first = false; } else { echo ', '; } if (!$asc) { echo '!'; } echo $col; } echo PHP_EOL; echo '/ Search'; if ($this->search !== null) { echo ': '; $first = true; foreach ($this->search as $s) { if ($first) { $first = false; } else { echo ', '; } if ($s['not']) { echo '!'; } echo $s['text']; } } echo PHP_EOL; echo 'q Quit' . PHP_EOL; $cmd = $this->readLine->read('> ', '', Config::$config['gui']['reload_timeout']); if ($this->readLine->timeout) { continue; } $this->message = ''; $this->readLine->historyAdd($cmd); $this->readLine->historySave(Config::$config['gui']['history_file']); if ($cmd === '') { continue; } switch ($cmd[0]) { // Create new task case 'c': $text = trim(substr($cmd, 1)); if (empty($text)) { $text = $this->readLine->read('Text: '); } if ($text === '') { $this->error('Need text'); } else { $t = new TodoEx($this->todos); $t->text = $text; $t->creationDate = new DateTime('today'); // Detect duplicity $dup = $this->todos->searchSimilar($t); if ($dup) { echo 'Duplicity found: ' . $dup->text . PHP_EOL; $confirm = $this->readLine->read('Really add (y/n)? ', 'y'); if ($confirm !== 'y') { $this->error('Todo not added'); break; } } $this->todos[] = $t; $this->lastLineNumber = array_pop($this->todos->array_keys()); if ($this->changed()) { $this->notice('Todo added'); } } break; // Edit task // Edit task case 'e': $num = $this->getLineNumber($cmd); if ($num === null) { break; } $text = $this->readLine->read('Text: ', $this->todos[$num]->text); if ($text === '') { $this->error('Need text'); } else { $this->todos[$num]->text = $text; if ($this->changed()) { $this->notice('Todo ' . $num . ' changed'); } } break; // Remove task // Remove task case 'r': $num = $this->getLineNumber($cmd); if ($num === null) { break; } $confirm = $this->readLine->read('Do you really want to remove todo ' . $num . ' (y/n)? ', 'y'); if ($confirm === 'y') { unset($this->todos[$num]); if ($this->changed()) { $this->notice('Todo ' . $num . ' removed'); } } else { $this->error('Todo ' . $num . ' NOT removed'); } break; // Archive // Archive case 'a': $count = $this->todos->archive(Config::$config['core']['archive_file']); if ($count) { if ($this->changed()) { $this->todos->sort($this->sort); $this->notice($count . ' todo(s) archived'); } } else { $this->notice('No todos to archive'); } break; // Mark as done // Mark as done case 'x': $num = $this->getLineNumber($cmd); if ($num === null) { break; } $this->todos[$num]->markDone(); if ($this->changed()) { $this->notice('Todo ' . $num . ' marked done'); } break; // Unmark as done // Unmark as done case 'X': $num = $this->getLineNumber($cmd); if ($num === null) { break; } $this->todos[$num]->unmarkDone(); if ($this->changed()) { $this->notice('Todo ' . $num . ' unmarked done'); } break; // Set due date // Set due date case 'd': $num = $this->getLineNumber($cmd); if ($num === null) { break; } $due = null; if ($this->todos[$num]->due) { $due = $this->todos[$num]->due->format(Config::$config['gui']['date_format_out']); } $str = $this->readLine->read('Due date: ', $due); if ($str === '') { $this->todos[$num]->due = null; if ($this->changed()) { $this->notice('Due date unset for todo ' . $num); } } else { try { $dt = $this->parseDate($str); $this->todos[$num]->due = $dt; if ($this->changed()) { $this->notice('Due date set to ' . $dt->format('Y-m-d') . ' for todo ' . $num); } } catch (DateParseException $dpe) { $this->error('Don\'t understand ' . $str); } } break; // Unset due date // Unset due date case 'D': $num = $this->getLineNumber($cmd); if ($num === null) { break; } $this->todos[$num]->due = null; if ($this->changed()) { $this->notice('Due date unset for todo ' . $num); } break; // Recurrent // Recurrent case 'g': $num = $this->getLineNumber($cmd); if ($num === null) { break; } $recurrent = null; if ($this->todos[$num]->recurrent) { $recurrent = $this->todos[$num]->recurrent->toString(); } $str = $this->readLine->read('Recurrent: ', $recurrent); if ($str === '') { $this->todos[$num]->recurrent = null; if ($this->changed()) { $this->notice('Todo ' . $num . ' set not recurrent'); } } else { try { $r = new Recurrent($str); $this->todos[$num]->recurrent = $r; if ($this->changed()) { $this->notice('Todo ' . $num . ' set recurrent ' . $r->toString()); } } catch (RecurrentParseException $rpe) { $this->error('Don\'t understand ' . $str); } } break; // Unset recurrent // Unset recurrent case 'G': $num = $this->getLineNumber($cmd); if ($num === null) { break; } $this->todos[$num]->recurrent = null; if ($this->changed()) { $this->notice('Todo ' . $num . ' set not recurrent'); } break; // Sort // Sort case 's': case 'S': $this->nextSort(); $this->notice('Sorting changed'); break; // Search // Search case '/': $search = trim(substr($cmd, 1)); if (empty($search)) { $search = $this->readLine->read('Search: '); } if ($search === '') { $this->search = null; } else { $search = explode(',', $search); $this->search = array(); foreach ($search as $w) { $not = false; if ($w[0] === '!') { $w = substr($w, 1); $not = true; } $this->search[] = array('not' => $not, 'text' => trim($w)); } } break; // Priority // Priority case 'p': $num = $this->getLineNumber($cmd); if ($num === null) { break; } $str = $this->readLine->read('Priority: ', $this->todos[$num]->priority); if ($str === '') { $this->todos[$num]->priority = null; if ($this->changed()) { $this->notice('Priority unset for todo ' . $num); } } else { if (preg_match('/^[a-zA-Z]$/', $str)) { $prio = strtoupper($str); $this->todos[$num]->priority = $prio; if ($this->changed()) { $this->notice('Priority set to ' . $prio . ' for todo ' . $num); } } else { $this->error('Wrong priority ' . $str); } } break; // Unset priority // Unset priority case 'P': $num = $this->getLineNumber($cmd); if ($num === null) { break; } $this->todos[$num]->priority = null; if ($this->changed()) { $this->notice('Priority unset for todo ' . $num); } break; // Quit // Quit case 'q': case 'Q': exit; break; } } }