Skip to content

Instantly share code, notes, and snippets.

@Mrfiregem
Last active January 13, 2025 19:11
Show Gist options
  • Save Mrfiregem/90c432f3611ec69dab73cf6c85a38d7b to your computer and use it in GitHub Desktop.
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`.
# 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