Implementation — Menu-Driven CLI App¶
Build the task manager step by step. Each step adds a working feature.
Step 1 — Scaffold and Config¶
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONFIG="${SCRIPT_DIR}/taskman.conf"
DB_FILE="${SCRIPT_DIR}/tasks.db"
DEFAULT_PRIORITY="normal"
SHOW_DONE=true
[[ -f "$CONFIG" ]] && source "$CONFIG"
# ANSI colors (disabled when not a terminal)
if [[ -t 1 ]]; then
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
CYAN='\033[0;36m'; BOLD='\033[1m'; DIM='\033[2m'; RESET='\033[0m'
else
RED=''; GREEN=''; YELLOW=''; CYAN=''; BOLD=''; DIM=''; RESET=''
fi
Step 2 — Initialize the Database¶
Step 3 — Next ID¶
next_id() {
local max=0
while IFS=: read -r id _rest; do
[[ "$id" =~ ^[0-9]+$ ]] && (( id > max )) && max=$id
done < "$DB_FILE"
echo $(( max + 1 ))
}
Step 4 — List Tasks¶
cmd_list() {
init_db
local has_tasks=false
printf "${BOLD}%-4s %-6s %-8s %s${RESET}\n" "ID" "Status" "Priority" "Task"
printf "%s\n" "---- ------ -------- ----"
while IFS=: read -r id status priority desc; do
[[ -z "$id" || "$id" =~ ^# ]] && continue
[[ "$status" == "done" && "$SHOW_DONE" != "true" ]] && continue
local status_sym color
if [[ "$status" == "done" ]]; then
status_sym="${GREEN}✓${RESET}"
color="$DIM"
else
status_sym="${YELLOW}○${RESET}"
color=""
fi
local pri_color=""
[[ "$priority" == "high" ]] && pri_color="$RED"
[[ "$priority" == "low" ]] && pri_color="$DIM"
printf "${color}%-4s %b ${pri_color}%-8s${RESET}${color} %s${RESET}\n" \
"$id" "$status_sym" "$priority" "$desc"
has_tasks=true
done < "$DB_FILE"
[[ "$has_tasks" == false ]] && echo "(no tasks yet — use 'add' to create one)"
}
Step 5 — Add Task¶
cmd_add() {
local desc="${1:-}"
local priority="${DEFAULT_PRIORITY}"
# Parse --priority flag
shift || true
while [[ "${1:-}" =~ ^- ]]; do
case "$1" in
--priority|-p) priority="${2:?--priority requires low|normal|high}"; shift ;;
*) echo "Unknown option: $1" >&2; return 1 ;;
esac
shift
done
# Prompt if no description given
if [[ -z "$desc" ]]; then
printf "Task description: "
read -r desc
fi
[[ -z "$desc" ]] && { echo "Error: description cannot be empty" >&2; return 1; }
# Validate priority
case "$priority" in
low|normal|high) ;;
*) echo "Error: priority must be low, normal, or high" >&2; return 1 ;;
esac
init_db
local id
id=$(next_id)
echo "${id}:pending:${priority}:${desc}" >> "$DB_FILE"
printf "${GREEN}[OK]${RESET} Task added: #%s %s [%s]\n" "$id" "$desc" "$priority"
}
Step 6 — Mark Done and Delete¶
cmd_done() {
local id="${1:?done requires a task ID}"
_update_task "$id" "done"
}
cmd_delete() {
local id="${1:?delete requires a task ID}"
local tmp
tmp=$(mktemp)
if grep -v "^${id}:" "$DB_FILE" > "$tmp"; then
mv "$tmp" "$DB_FILE"
printf "${GREEN}[OK]${RESET} Task #%s deleted\n" "$id"
else
rm -f "$tmp"
echo "Error: task #$id not found" >&2
return 1
fi
}
_update_task() {
local id="$1" new_status="$2"
local tmp found=false
tmp=$(mktemp)
while IFS=: read -r tid status priority desc; do
if [[ "$tid" == "$id" ]]; then
echo "${tid}:${new_status}:${priority}:${desc}" >> "$tmp"
found=true
else
echo "${tid}:${status}:${priority}:${desc}" >> "$tmp"
fi
done < "$DB_FILE"
if [[ "$found" == true ]]; then
mv "$tmp" "$DB_FILE"
printf "${GREEN}[OK]${RESET} Task #%s marked %s\n" "$id" "$new_status"
else
rm -f "$tmp"
echo "Error: task #$id not found" >&2
return 1
fi
}
Step 7 — Interactive Menu¶
show_header() {
echo ""
printf "${CYAN}${BOLD}╔══════════════════════════════╗${RESET}\n"
printf "${CYAN}${BOLD}║ Task Manager v1.0 ║${RESET}\n"
printf "${CYAN}${BOLD}╚══════════════════════════════╝${RESET}\n"
echo ""
}
interactive_menu() {
show_header
while true; do
echo ""
PS3=$'\n'"${BOLD}Choice:${RESET} "
local options=("List tasks" "Add task" "Complete task" "Delete task" "Settings" "Quit")
select choice in "${options[@]}"; do
case "$choice" in
"List tasks") cmd_list; break ;;
"Add task") cmd_add; break ;;
"Complete task")
printf "Task ID to complete: "
read -r id
cmd_done "$id"
break ;;
"Delete task")
printf "Task ID to delete: "
read -r id
cmd_delete "$id"
break ;;
"Settings") cmd_settings; break ;;
"Quit") echo "Goodbye!"; exit 0 ;;
*) echo "Invalid choice: $REPLY" ;;
esac
done
done
}
Step 8 — Settings and main()¶
cmd_settings() {
echo ""
echo "Current settings:"
echo " DB file: $DB_FILE"
echo " Default priority: $DEFAULT_PRIORITY"
echo " Show done tasks: $SHOW_DONE"
echo ""
echo "(Edit $CONFIG to change settings)"
}
main() {
local command="${1:-}"
case "$command" in
"") interactive_menu ;;
list) shift; cmd_list "$@" ;;
add) shift; cmd_add "$@" ;;
done) shift; cmd_done "$@" ;;
delete|rm) shift; cmd_delete "$@" ;;
settings) cmd_settings ;;
--help|-h)
echo "Usage: $(basename "$0") [list|add|done|delete|settings]"
echo "Run without arguments for interactive menu."
;;
*) echo "Unknown command: $command" >&2; exit 1 ;;
esac
}
[[ "${BASH_SOURCE[0]}" == "$0" ]] && main "$@"