Last active
January 13, 2025 19:11
-
-
Save Mrfiregem/90c432f3611ec69dab73cf6c85a38d7b to your computer and use it in GitHub Desktop.
Simple todo.txt cli in pure Nu. Meant to be loaded with `overlay use`.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Ask user to create todo.txt file if it doesn't exist | |
def "todo ensure-file" [ | |
file: path = ~/todo.txt # Path to the todo file | |
]: nothing -> bool { | |
if not ($file | path exists) { | |
print "The todo file does not exist. Would you like to create it? [y/N]: " | |
let response = input -sn 1 --default 'n' | str downcase | |
if $response == 'y' { | |
print $"Creating todo.txt file at ($file | path expand)." | |
char nl | save ($file | path expand) | |
return true | |
} else { | |
print "Aborted." | |
return false | |
} | |
} | |
return true | |
} | |
# Parse todo.txt files into tables | |
export def "from todo" []: string -> any { | |
lines | str trim | compact -e | |
| each { split row -r '\s+' } | |
| par-each -k { | |
mut tokens = $in | |
mut result = {} | |
$result.complete = if $tokens.0 == 'x' { | |
$tokens = $tokens | skip 1 | |
true | |
} else { false } | |
$result.priority = if $tokens.0 =~ '^\([A-Z]\)$' { | |
let p = $tokens.0 | str replace -ra '[()]' '' | |
$tokens = $tokens | skip 1; $p | |
} else { null } | |
if $tokens.0 =~ '^\d{4}-\d{2}-\d{2}$' { | |
if $tokens.1 =~ '^\d{4}-\d{2}-\d{2}$' { | |
$result.created = $tokens.1 | |
$result.completed = $tokens.0 | |
$tokens = $tokens | skip 2 | |
} else { | |
$result.created = $tokens.0 | |
$result.completed = null | |
$tokens = $tokens | skip 1 | |
} | |
} else { | |
$result.created = null | |
$result.completed = null | |
} | |
$result.description = $tokens | str join ' ' | |
$result.projects = $tokens | where $it =~ '^\+' | str trim -lc '+' | |
$result.contexts = $tokens | where $it =~ '^@' | str trim -lc '@' | |
$result.tags = $tokens | parse '{key}:{value}' | transpose -rd | |
echo $result | |
} | update tags {|rc| if ($rc.tags | is-empty) { {} } else { $rc.tags } } | |
| update cells -c [created, completed] {|t| if $t != null { $t | into datetime } else { $t } } | |
} | |
# Convert a table in "from todo" format to a todo.txt file format | |
export def "to todo" []: table -> string { | |
each {|rc| | |
mut result = '' | |
if $rc.complete { | |
$result += 'x ' | |
} | |
if ($rc.priority | is-not-empty) { | |
$result += '(' + $rc.priority + ') ' | |
} | |
if ($rc.completed | is-not-empty) { | |
$result += ($rc.completed | format date '%F') + ' ' + ($rc.created | format date '%F') + ' ' | |
} else if ($rc.created | is-not-empty) { | |
$result += ($rc.created | format date '%F') + ' ' | |
} | |
$result += $rc.description | |
echo $result | |
} | to text | |
} | |
# List tasks in a todo.txt file. | |
# The --as-table flag contains the following columns: | |
# complete: bool - True if task is complete | |
# priority: char? - Null by default, or a priority rank A-Z | |
# created: date? - The date the task was created, if available | |
# completed: date? - The date the task was completed, if available | |
# description: string - The body of the task, including tags, projects, and contexts | |
# projects: list[string] - A list of +projects extracted from the description | |
# contexts: list[string] - A list of @contexts extracted from the description | |
# tags: record | [] - A record of key:value pairs extracted from description | |
export def "todo list" [ | |
--file (-f): path = ~/todo.txt # Path to the todo file | |
--show-all (-a) # Show all tasks | |
--as-table (-t) # Output as a table | |
] { | |
if (todo ensure-file $file) == false { | |
return null | |
} | |
open ($file | path expand) | from todo | |
| if not $show_all { where complete == false } else {} | |
| sort-by complete priority created | |
| if not $as_table { to todo } else {} | |
} | |
# List outstanding todos with a format string; useful for greeter functions. | |
# This command adds the following extra columns to the normal `todo list` table: | |
# created_f - The "created" column formatted using --date-format | |
# completed_f - The "completed" column formatted using --date-format | |
# desc_clean - the "description" column with custom tags removed and (+/@) prefixes trimmed | |
export def "todo pretty" [ | |
format: string = $'(ansi i)From (ansi mi){created_f}(ansi def): (ansi n)(ansi ub){desc_clean}' # String to format todo items | |
--date-format(-d): string = '%F' # Use the {created_f} and {completed_f} columns to access | |
] { | |
todo list -t | |
| insert created_f {|rc| if ($rc.created | is-not-empty) { $rc.created | format date $date_format} else { '' } } | |
| insert completed_f {|rc| if ($rc.completed | is-not-empty) { $rc.completed | format date $date_format} else { '' } } | |
| insert desc_clean {|rc| $rc.description | split row -r '\s+' | str replace -r '^[\+|@]' '' | filter { $in not-like '^[^:]+:[^:]+$' } | str join ' ' } | |
| format pattern $format | |
| to text | |
} | |
# List tasks in a todo.txt file with a filter | |
export def "todo filter" [ | |
--file (-f): path = ~/todo.txt # Path to the todo file | |
filter: closure # Filter to apply to the todo list | |
] { | |
open ($file | path expand) | from todo | |
| filter $filter | to todo | |
} | |
# Remove completed tasks from a todo.txt file | |
export def "todo clean" [ | |
--file (-f): path = ~/todo.txt | |
--no-confirm (-y) # Do not ask for confirmation | |
] { | |
while true { | |
print -n "This will remove all completed tasks. Are you sure you want to continue? [y/N]: " | |
let response = input -sn 1 --default 'n' | str downcase | |
if $response == 'y' { | |
break | |
} else if $response == 'n' { | |
print "\nAborted." | |
return | |
} | |
print "\nInvalid response." | |
} | |
open ($file | path expand) | from todo | |
| where complete == false | |
| to todo | |
| save -f ($file | path expand) | |
} | |
# Remove all tasks from a todo.txt file | |
export def "todo clean all" [ | |
--file (-f): path = ~/todo.txt | |
] { | |
char nl | save -f ($file | path expand) | |
} | |
# Add a task to a todo.txt file | |
export def "todo add" [ | |
--file (-f): path = ~/todo.txt | |
--priority (-p): string = '' # Priority of the task | |
task: string # Task to add | |
] { | |
if (todo ensure-file $file) == false { | |
return | |
} | |
open ($file | path expand) | from todo | |
| append { | |
complete: false | |
priority: (if $priority =~ '^[A-Z]$' { $priority } else { null }) | |
created: (date now | format date '%F') | |
completed: null | |
description: $task | |
} | to todo | |
| collect { save -f ($file | path expand) } | |
} | |
# Open the todo.txt file in the default editor | |
export def "todo edit" [ | |
--file (-f): path = ~/todo.txt | |
] { | |
let editor = $env.config.buffer_editor? | default $env.VISUAL? | default $env.EDITOR? | default 'vi' | |
^$editor ($file | path expand) | |
} | |
export def "todo rm" [ | |
--file (-f): path = ~/todo.txt | |
] { | |
if not ($file | path exists) { | |
print "The todo file does not exist. Check the path and try again." | |
return | |
} | |
let choice = todo list -ta --file $file | enumerate | |
| insert display {|rc| $"($rc.index):(char tab)($rc.item | to todo)" } | |
| input list -d display | |
if ($choice | is-empty) { | |
print "No task selected." | |
return | |
} | |
todo list -ta --file $file | |
| drop nth $choice.index | |
| to todo | |
| collect { save -f ($file | path expand) } | |
} | |
# Mark a task as complete in a todo.txt file | |
export def "todo complete" [ | |
--file (-f): path = ~/todo.txt | |
] { | |
let choice = todo list -ta --file $file | enumerate | |
| insert display {|rc| $"($rc.index):(char tab)($rc.item | to todo)" } | |
| input list -d display | |
if ($choice | is-empty) { | |
print "No task selected." | |
return | |
} | |
todo list -ta --file $file | |
| update ([$choice.index, complete] | into cell-path) { not $in } | |
| update ([$choice.index, completed] | into cell-path) { date now | format date '%F' } | |
| to todo | |
| collect { save -f ($file | path expand) } | |
} | |
export def main [] { | |
print "Usage: todo <command> [args]" | |
print "\nCommands:" | |
print " pretty [-f <file>] [-d <format>] [<format>] - List tasks using `format pattern`" | |
print " list [-f <file>] [-a] [-t] - List tasks in the todo.txt file format" | |
print " filter [-f <file>] <filter> - List tasks in a todo.txt file with a filter" | |
print " clean [-f <file>] [-y] - Remove completed tasks from a todo.txt file" | |
print " clean all [-f <file>] - Replace todo.txt with an empty file (NO CONFIRMATION)" | |
print " add [-f <file>] [-p <priority>] <task> - Add a task to a todo.txt file" | |
print " edit [-f <file>] - Open the todo.txt file in the default editor" | |
print " rm [-f <file>] - Remove a task from a todo.txt file" | |
print " complete [-f <file>] - Mark a task as complete in a todo.txt file" | |
print "\nBy default, the todo.txt file is located at ~/todo.txt." | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment