@amrqhz/todo
Version:
A beautiful terminal-based todo application with timestamps and progress tracking
327 lines (286 loc) • 16 kB
JavaScript
const fs = require('fs');
const path = require('path');
const os = require('os');
// Configuration
const TODO_FILE = path.join(os.homedir(), '.todos.json');
// Helper functions
function loadTodos() {
try {
if (fs.existsSync(TODO_FILE)) {
const data = fs.readFileSync(TODO_FILE, 'utf8');
return JSON.parse(data);
}
} catch (error) {
console.error('Error loading todos:', error.message);
}
return [];
}
function saveTodos(todos) {
try {
fs.writeFileSync(TODO_FILE, JSON.stringify(todos, null, 2));
} catch (error) {
console.error('Error saving todos:', error.message);
}
}
function formatDate(dateString) {
const date = new Date(dateString);
const now = new Date();
const diffMs = now - date;
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
const diffDays = Math.floor(diffHours / 24);
if (diffHours < 1) {
const diffMins = Math.floor(diffMs / (1000 * 60));
return diffMins < 1 ? 'just now' : `${diffMins}m ago`;
} else if (diffHours < 24) {
return `${diffHours}h ago`;
} else if (diffDays < 7) {
return `${diffDays}d ago`;
} else {
return date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: date.getFullYear() !== now.getFullYear() ? 'numeric' : undefined
});
}
}
function showBanner() {
console.log('\x1b[36m');
console.log('████████╗ ██████╗ ██████╗ ██████╗ ');
console.log('╚══██╔══╝██╔═══██╗██╔══██╗██╔═══██╗');
console.log(' ██║ ██║ ██║██║ ██║██║ ██║');
console.log(' ██║ ██║ ██║██║ ██║██║ ██║');
console.log(' ██║ ╚██████╔╝██████╔╝╚██████╔╝');
console.log(' ╚═╝ ╚═════╝ ╚═════╝ ╚═════╝ ');
console.log('\x1b[0m');
console.log('\x1b[2m by AMRQHZ\x1b[0m');
console.log('\x1b[2m v1.1.0\x1b[0m');
console.log('');
// Social links
console.log('\x1b[2m 🐙 GitHub: https://github.com/amrqhz\x1b[0m');
console.log('\x1b[2m 🐦 Twitter: https://twitter.com/amrqhz\x1b[0m');
console.log('\x1b[2m 📱 Telegram: https://t.me/amrqhz\x1b[0m');
console.log('');
}
function displayTodos(todos) {
showBanner();
if (todos.length === 0) {
console.log('┌─────────────────────────────────────────────┐');
console.log('│ │');
console.log('│ \x1b[36m📝 No todos found!\x1b[0m │');
console.log('│ │');
console.log('│ \x1b[2mGet started with:\x1b[0m │');
console.log('│ \x1b[33mtodo add "Your first task"\x1b[0m │');
console.log('│ │');
console.log('└─────────────────────────────────────────────┘\n');
return;
}
// Header
console.log('┌─────────────────────────────────────────────┐');
console.log('│ │');
console.log('│ \x1b[1m\x1b[36m📝 Your Todo List\x1b[0m │');
console.log('│ │');
console.log('└─────────────────────────────────────────────┘');
// Statistics
const completed = todos.filter(t => t.completed).length;
const pending = todos.length - completed;
console.log(`\x1b[2m 📊 Total: ${todos.length} | ✅ Done: ${completed} | ⏳ Pending: ${pending}\x1b[0m`);
console.log('');
// Todo items
todos.forEach((todo, index) => {
const num = `${index + 1}`.padStart(2, ' ');
const timeAgo = formatDate(todo.createdAt);
if (todo.completed) {
// Completed todo - green checkmark, strikethrough text
console.log(` \x1b[32m✓\x1b[0m \x1b[2m${num}.\x1b[0m \x1b[9m\x1b[90m${todo.text}\x1b[0m`);
console.log(` \x1b[90m\x1b[2m📅 Created ${timeAgo}${todo.completedAt ? ` • ✅ Completed ${formatDate(todo.completedAt)}` : ''}\x1b[0m`);
} else {
// Pending todo - yellow circle, normal text
console.log(` \x1b[33m○\x1b[0m \x1b[1m${num}.\x1b[0m ${todo.text}`);
console.log(` \x1b[90m\x1b[2m📅 Created ${timeAgo}\x1b[0m`);
}
console.log('');
});
console.log('\x1b[2m 💡 Use "todo check <number>" to toggle completion\x1b[0m');
console.log('\x1b[2m 💡 Use "todo help" for more commands\x1b[0m\n');
}
function addTodo(text) {
const todos = loadTodos();
const now = new Date().toISOString();
todos.push({
id: Date.now(),
text: text,
completed: false,
createdAt: now
});
saveTodos(todos);
console.log('\n┌─────────────────────────────────────────────┐');
console.log('│ \x1b[32m✨ Todo Added Successfully!\x1b[0m │');
console.log('│ │');
console.log(`│ \x1b[1m"${text}"\x1b[0m${' '.repeat(Math.max(0, 41 - text.length))}│`);
console.log(`│ \x1b[2m📅 ${new Date().toLocaleString()}\x1b[0m${' '.repeat(Math.max(0, 41 - new Date().toLocaleString().length))}│`);
console.log('│ │');
console.log('└─────────────────────────────────────────────┘\n');
}
function toggleTodo(index) {
const todos = loadTodos();
const todoIndex = parseInt(index) - 1;
if (todoIndex < 0 || todoIndex >= todos.length) {
console.log('\n\x1b[31m❌ Invalid todo number\x1b[0m');
console.log('\x1b[2m Use "todo" to see your list\x1b[0m\n');
return;
}
todos[todoIndex].completed = !todos[todoIndex].completed;
// Add completion timestamp
if (todos[todoIndex].completed) {
todos[todoIndex].completedAt = new Date().toISOString();
} else {
delete todos[todoIndex].completedAt;
}
saveTodos(todos);
if (todos[todoIndex].completed) {
console.log('\n┌─────────────────────────────────────────────┐');
console.log('│ \x1b[32m🎉 Todo Completed!\x1b[0m │');
console.log('│ │');
console.log(`│ \x1b[9m\x1b[90m"${todos[todoIndex].text}"\x1b[0m${' '.repeat(Math.max(0, 41 - todos[todoIndex].text.length))}│`);
console.log(`│ \x1b[2m✅ ${new Date().toLocaleString()}\x1b[0m${' '.repeat(Math.max(0, 41 - new Date().toLocaleString().length))}│`);
console.log('│ │');
console.log('└─────────────────────────────────────────────┘\n');
} else {
console.log('\n┌─────────────────────────────────────────────┐');
console.log('│ \x1b[33m🔄 Todo Reopened\x1b[0m │');
console.log('│ │');
console.log(`│ \x1b[1m"${todos[todoIndex].text}"\x1b[0m${' '.repeat(Math.max(0, 41 - todos[todoIndex].text.length))}│`);
console.log(`│ \x1b[2m🔄 ${new Date().toLocaleString()}\x1b[0m${' '.repeat(Math.max(0, 41 - new Date().toLocaleString().length))}│`);
console.log('│ │');
console.log('└─────────────────────────────────────────────┘\n');
}
}
function removeTodo(index) {
const todos = loadTodos();
const todoIndex = parseInt(index) - 1;
if (todoIndex < 0 || todoIndex >= todos.length) {
console.log('\n\x1b[31m❌ Invalid todo number\x1b[0m');
console.log('\x1b[2m Use "todo" to see your list\x1b[0m\n');
return;
}
const removed = todos.splice(todoIndex, 1)[0];
saveTodos(todos);
console.log('\n┌─────────────────────────────────────────────┐');
console.log('│ \x1b[31m🗑️ Todo Removed\x1b[0m │');
console.log('│ │');
console.log(`│ \x1b[9m"${removed.text}"\x1b[0m${' '.repeat(Math.max(0, 41 - removed.text.length))}│`);
console.log(`│ \x1b[2m📅 Created ${formatDate(removed.createdAt)}\x1b[0m${' '.repeat(Math.max(0, 41 - `Created ${formatDate(removed.createdAt)}`.length))}│`);
console.log('│ │');
console.log('└─────────────────────────────────────────────┘\n');
}
function clearCompleted() {
const todos = loadTodos();
const remaining = todos.filter(todo => !todo.completed);
const removed = todos.length - remaining.length;
if (removed === 0) {
console.log('\n\x1b[33m💭 No completed todos to clear\x1b[0m\n');
return;
}
saveTodos(remaining);
console.log('\n┌─────────────────────────────────────────────┐');
console.log('│ \x1b[32m🧹 Cleanup Complete!\x1b[0m │');
console.log('│ │');
console.log(`│ \x1b[1mCleared ${removed} completed todo${removed === 1 ? '' : 's'}\x1b[0m${' '.repeat(Math.max(0, 30 - removed.toString().length))}│`);
console.log(`│ \x1b[2m🗑️ ${new Date().toLocaleString()}\x1b[0m${' '.repeat(Math.max(0, 41 - new Date().toLocaleString().length))}│`);
console.log('│ │');
console.log('└─────────────────────────────────────────────┘\n');
}
function showHelp() {
showBanner();
console.log(`
╔═══════════════════════════════════════════════════════╗
║ ║
║ \x1b[1m\x1b[36m📝 Todo CLI - Command Reference\x1b[0m ║
║ ║
╠═══════════════════════════════════════════════════════╣
║ ║
║ \x1b[1mCOMMANDS:\x1b[0m ║
║ ║
║ \x1b[33mtodo\x1b[0m Show all todos ║
║ \x1b[33mtodo add "task"\x1b[0m Add a new todo ║
║ \x1b[33mtodo check <number>\x1b[0m Toggle todo completion ║
║ \x1b[33mtodo remove <number>\x1b[0m Remove a todo ║
║ \x1b[33mtodo clear\x1b[0m Clear completed todos ║
║ \x1b[33mtodo help\x1b[0m Show this help ║
║ ║
╠═══════════════════════════════════════════════════════╣
║ ║
║ \x1b[1mEXAMPLES:\x1b[0m ║
║ ║
║ \x1b[32m$\x1b[0m todo add "Buy groceries" ║
║ \x1b[32m$\x1b[0m todo add "Finish the report" ║
║ \x1b[32m$\x1b[0m todo check 1 ║
║ \x1b[32m$\x1b[0m todo remove 2 ║
║ \x1b[32m$\x1b[0m todo clear ║
║ ║
╠═══════════════════════════════════════════════════════╣
║ ║
║ \x1b[1mFEATURES:\x1b[0m ║
║ ║
║ 📅 Timestamps for creation and completion ║
║ 🕒 Smart relative time display (2h ago, 3d ago) ║
║ 📊 Statistics and progress tracking ║
║ 🎨 Beautiful CLI interface with colors ║
║ ║
╚═══════════════════════════════════════════════════════╝
`);
}
// Main CLI logic
function main() {
const args = process.argv.slice(2);
const command = args[0];
switch (command) {
case 'add':
if (args.length < 2) {
console.log('\n\x1b[31m❌ Missing todo text\x1b[0m');
console.log('\x1b[2m Usage: todo add "Your task"\x1b[0m\n');
return;
}
const todoText = args.slice(1).join(' ').replace(/^["']|["']$/g, '');
addTodo(todoText);
break;
case 'check':
case 'toggle':
if (args.length < 2) {
console.log('\n\x1b[31m❌ Missing todo number\x1b[0m');
console.log('\x1b[2m Usage: todo check <number>\x1b[0m\n');
return;
}
toggleTodo(args[1]);
break;
case 'remove':
case 'delete':
case 'rm':
if (args.length < 2) {
console.log('\n\x1b[31m❌ Missing todo number\x1b[0m');
console.log('\x1b[2m Usage: todo remove <number>\x1b[0m\n');
return;
}
removeTodo(args[1]);
break;
case 'clear':
clearCompleted();
break;
case 'help':
case '--help':
case '-h':
showHelp();
break;
case undefined:
// Show all todos when no command is provided
const todos = loadTodos();
displayTodos(todos);
break;
default:
console.log(`\n\x1b[31m❌ Unknown command: \x1b[1m${command}\x1b[0m`);
console.log('\x1b[2m Run "todo help" for available commands\x1b[0m\n');
}
}
// Run the CLI
main();