Introduction
In this tutorial, we'll build a console-based todo list manager that allows you to:
- Add, remove, and complete tasks
- List all, pending, or completed tasks
- Save tasks to a JSON file and load them later
This project will demonstrate several important ShitRust features:
- Defining and using custom structs with methods
- Working with collections (Vector)
- File I/O operations
- JSON serialization and deserialization
- Error handling with the
result type
- Control flow with
match expressions
By the end of this tutorial, you'll have a fully functional task management application that persists data between sessions.
Task Structure
First, let's import the required modules and define a Task struct to represent a todo item.
// Import required modules
import { stdin, stdout, File, Path } from "std/io";
import { Vector } from "std/collections";
import { DateTime } from "std/time";
import { JsonValue, parse_json, to_json } from "std/json";
// Define a Task struct to represent a todo item
struct Task {
id: int,
description: string,
completed: bool,
due_date: string, // ISO date format (YYYY-MM-DD)
// Create a new task with the given description and due date
static fn new(id: int, description: string, due_date: string) -> Task {
return Task {
id: id,
description: description,
completed: false,
due_date: due_date
};
}
// Mark the task as completed
fn complete() -> void {
self.completed = true;
}
// Convert the task to a string representation
fn to_string() -> string {
let status = if self.completed { "[x]" } else { "[ ]" };
return self.id.to_string() + ". " + status + " " + self.description +
" (Due: " + self.due_date + ")";
}
}
The Task struct has four fields:
id: A unique identifier for the task
description: The task description
completed: A boolean flag indicating whether the task is done
due_date: The date by which the task should be completed
We've also defined three methods:
new: A static method (constructor) to create a new task
complete: A method to mark the task as completed
to_string: A method to convert the task to a string representation
JSON Serialization
To save and load tasks, we need to convert them to and from JSON format. Let's add these methods to our Task struct:
// Convert task to JSON
fn to_json() -> JsonValue {
let mut map = HashMap::new();
map.insert("id", JsonValue::Number(self.id));
map.insert("description", JsonValue::String(self.description));
map.insert("completed", JsonValue::Boolean(self.completed));
map.insert("due_date", JsonValue::String(self.due_date));
return JsonValue::Object(map);
}
// Create a task from JSON
static fn from_json(json: JsonValue) -> result {
if !json.is_object() {
return err("JSON value is not an object");
}
let obj = json.as_object();
if !obj.contains_key("id") || !obj["id"].is_number() {
return err("Missing or invalid 'id' field");
}
if !obj.contains_key("description") || !obj["description"].is_string() {
return err("Missing or invalid 'description' field");
}
if !obj.contains_key("completed") || !obj["completed"].is_boolean() {
return err("Missing or invalid 'completed' field");
}
if !obj.contains_key("due_date") || !obj["due_date"].is_string() {
return err("Missing or invalid 'due_date' field");
}
return ok(Task {
id: obj["id"].as_number().to_int(),
description: obj["description"].as_string(),
completed: obj["completed"].as_boolean(),
due_date: obj["due_date"].as_string()
});
}
These methods allow us to:
- Convert a task to a JSON object (
to_json)
- Create a task from a JSON object (
from_json), with proper validation
Task Manager
Next, let's create a TaskManager struct to manage a collection of tasks:
// TaskManager to handle the collection of tasks
struct TaskManager {
tasks: Vector,
next_id: int,
filename: string,
// Create a new task manager with the given filename for storage
static fn new(filename: string) -> TaskManager {
return TaskManager {
tasks: Vector::new(),
next_id: 1,
filename: filename
};
}
// Add a new task
fn add_task(description: string, due_date: string) -> Task {
let task = Task::new(self.next_id, description, due_date);
self.tasks.push(task);
self.next_id = self.next_id + 1;
return task;
}
// Remove a task by ID
fn remove_task(id: int) -> result {
let index = self.find_task_index(id);
if index == -1 {
return err("Task with ID " + id.to_string() + " not found");
}
self.tasks.remove(index);
return ok(());
}
// Mark a task as completed
fn complete_task(id: int) -> result {
let index = self.find_task_index(id);
if index == -1 {
return err("Task with ID " + id.to_string() + " not found");
}
self.tasks[index].complete();
return ok(());
}
// Find the index of a task by ID
fn find_task_index(id: int) -> int {
for i in 0..self.tasks.length() {
if self.tasks[i].id == id {
return i;
}
}
return -1;
}
}
The TaskManager struct handles the collection of tasks and provides methods to:
- Add a new task with an automatically assigned ID
- Remove a task by ID
- Mark a task as completed
- Find a task's index by its ID
Task Listing
Now, let's add methods to list tasks:
// List all tasks
fn list_tasks() -> void {
if self.tasks.length() == 0 {
println("No tasks found!");
return;
}
println("Tasks:");
for task in self.tasks {
println(task.to_string());
}
}
// List only completed tasks
fn list_completed_tasks() -> void {
let completed_tasks = self.tasks.filter(|task| -> bool { return task.completed; });
if completed_tasks.length() == 0 {
println("No completed tasks found!");
return;
}
println("Completed Tasks:");
for task in completed_tasks {
println(task.to_string());
}
}
// List only pending tasks
fn list_pending_tasks() -> void {
let pending_tasks = self.tasks.filter(|task| -> bool { return !task.completed; });
if pending_tasks.length() == 0 {
println("No pending tasks found!");
return;
}
println("Pending Tasks:");
for task in pending_tasks {
println(task.to_string());
}
}
These methods allow us to list:
- All tasks
- Only completed tasks
- Only pending tasks
Note how we use the filter method with lambda functions to filter the tasks based on their completed status.
File Operations
Let's add methods to save and load tasks from a file:
// Save tasks to a file
fn save() -> result {
let json_tasks = Vector::new();
for task in self.tasks {
json_tasks.push(task.to_json());
}
let json_data = JsonValue::Object(HashMap::from([
("tasks", JsonValue::Array(json_tasks)),
("next_id", JsonValue::Number(self.next_id))
]));
let json_str = to_json(json_data);
try {
let file = File::open(self.filename, "w");
file.write(json_str);
file.close();
return ok(());
} catch e {
return err("Failed to save tasks: " + e.to_string());
}
}
// Load tasks from a file
fn load() -> result {
let path = Path::new(self.filename);
if !path.exists() {
return ok(()); // File doesn't exist yet, this is fine
}
try {
let file = File::open(self.filename, "r");
let content = file.read_to_string();
file.close();
let json_data = parse_json(content);
if !json_data.is_object() {
return err("Invalid JSON format in file");
}
let json_obj = json_data.as_object();
if !json_obj.contains_key("tasks") || !json_obj["tasks"].is_array() {
return err("Missing or invalid 'tasks' field in JSON");
}
if !json_obj.contains_key("next_id") || !json_obj["next_id"].is_number() {
return err("Missing or invalid 'next_id' field in JSON");
}
self.next_id = json_obj["next_id"].as_number().to_int();
self.tasks.clear();
let json_tasks = json_obj["tasks"].as_array();
for json_task in json_tasks {
let task_result = Task::from_json(json_task);
if task_result.is_ok() {
self.tasks.push(task_result.unwrap());
} else {
println("Warning: Skipped invalid task: " + task_result.unwrap_err());
}
}
return ok(());
} catch e {
return err("Failed to load tasks: " + e.to_string());
}
}
These methods:
- Save the tasks to a JSON file, including the next ID to use
- Load tasks from a JSON file, with proper error handling
We use ShitRust's try/catch blocks for exception handling during file operations.
User Interface
Now, let's create functions to display a menu and get today's date:
// Function to display the menu
fn show_menu() -> void {
println("\n===== ShitRust Todo List Manager =====");
println("1. Add a new task");
println("2. List all tasks");
println("3. List pending tasks");
println("4. List completed tasks");
println("5. Mark task as completed");
println("6. Remove a task");
println("7. Save tasks");
println("8. Exit");
println("======================================");
print("Enter your choice (1-8): ");
}
// Function to get today's date in YYYY-MM-DD format
fn get_today() -> string {
let today = DateTime::now();
return today.format("%Y-%m-%d");
}
The show_menu function displays the application's menu options, and get_today returns today's date in YYYY-MM-DD format for setting default due dates.
Main Program
Finally, let's implement the main function to tie everything together:
// Main function
fn main() -> void {
let task_manager = TaskManager::new("tasks.json");
// Try to load existing tasks
let load_result = task_manager.load();
if load_result.is_err() {
println("Error loading tasks: " + load_result.unwrap_err());
println("Starting with an empty task list.");
} else {
println("Tasks loaded successfully.");
}
let mut running = true;
while (running) {
show_menu();
let choice = stdin.read_line().trim();
match choice {
"1" => {
print("Enter task description: ");
let description = stdin.read_line().trim();
print("Enter due date (YYYY-MM-DD) or leave empty for today: ");
let due_date_input = stdin.read_line().trim();
let due_date = if due_date_input == "" { get_today() } else { due_date_input };
let task = task_manager.add_task(description, due_date);
println("Task added: " + task.to_string());
},
"2" => {
task_manager.list_tasks();
},
"3" => {
task_manager.list_pending_tasks();
},
"4" => {
task_manager.list_completed_tasks();
},
"5" => {
print("Enter task ID to mark as completed: ");
let id_str = stdin.read_line().trim();
let id_result = id_str.parse_int();
if id_result.is_err() {
println("Invalid ID: " + id_str);
continue;
}
let complete_result = task_manager.complete_task(id_result.unwrap());
if complete_result.is_err() {
println("Error: " + complete_result.unwrap_err());
} else {
println("Task marked as completed.");
}
},
"6" => {
print("Enter task ID to remove: ");
let id_str = stdin.read_line().trim();
let id_result = id_str.parse_int();
if id_result.is_err() {
println("Invalid ID: " + id_str);
continue;
}
let remove_result = task_manager.remove_task(id_result.unwrap());
if remove_result.is_err() {
println("Error: " + remove_result.unwrap_err());
} else {
println("Task removed successfully.");
}
},
"7" => {
let save_result = task_manager.save();
if save_result.is_err() {
println("Error saving tasks: " + save_result.unwrap_err());
} else {
println("Tasks saved successfully.");
}
},
"8" => {
println("Saving tasks before exit...");
task_manager.save();
println("Goodbye!");
running = false;
},
_ => {
println("Invalid choice. Please enter a number from 1 to 8.");
}
}
}
}
The main function:
- Creates a new
TaskManager
- Loads any existing tasks from the file
- Enters a loop to display the menu and process user choices
- Handles different user actions using a
match expression
- Saves all tasks before exiting
Complete Code
The complete code for the Todo List Manager is available in the todo_list.sr file.
Running the Program
To run the Todo List Manager:
- Save the code in a file named
todo_list.sr
- Open a terminal and navigate to the directory containing your file
- Compile and run the program with the following command:
shitrust run todo_list.sr
You should see the menu displayed, and you can start adding and managing tasks. The tasks will be saved to a file named tasks.json in the same directory.
Sample Session
Tasks loaded successfully.
===== ShitRust Todo List Manager =====
1. Add a new task
2. List all tasks
3. List pending tasks
4. List completed tasks
5. Mark task as completed
6. Remove a task
7. Save tasks
8. Exit
======================================
Enter your choice (1-8): 1
Enter task description: Finish ShitRust tutorial
Enter due date (YYYY-MM-DD) or leave empty for today:
Task added: 1. [ ] Finish ShitRust tutorial (Due: 2023-06-10)
===== ShitRust Todo List Manager =====
1. Add a new task
2. List all tasks
3. List pending tasks
4. List completed tasks
5. Mark task as completed
6. Remove a task
7. Save tasks
8. Exit
======================================
Enter your choice (1-8): 1
Enter task description: Build a web app
Enter due date (YYYY-MM-DD) or leave empty for today: 2023-06-20
Task added: 2. [ ] Build a web app (Due: 2023-06-20)
===== ShitRust Todo List Manager =====
1. Add a new task
2. List all tasks
3. List pending tasks
4. List completed tasks
5. Mark task as completed
6. Remove a task
7. Save tasks
8. Exit
======================================
Enter your choice (1-8): 2
Tasks:
1. [ ] Finish ShitRust tutorial (Due: 2023-06-10)
2. [ ] Build a web app (Due: 2023-06-20)
===== ShitRust Todo List Manager =====
1. Add a new task
2. List all tasks
3. List pending tasks
4. List completed tasks
5. Mark task as completed
6. Remove a task
7. Save tasks
8. Exit
======================================
Enter your choice (1-8): 5
Enter task ID to mark as completed: 1
Task marked as completed.
===== ShitRust Todo List Manager =====
1. Add a new task
2. List all tasks
3. List pending tasks
4. List completed tasks
5. Mark task as completed
6. Remove a task
7. Save tasks
8. Exit
======================================
Enter your choice (1-8): 2
Tasks:
1. [x] Finish ShitRust tutorial (Due: 2023-06-10)
2. [ ] Build a web app (Due: 2023-06-20)
===== ShitRust Todo List Manager =====
1. Add a new task
2. List all tasks
3. List pending tasks
4. List completed tasks
5. Mark task as completed
6. Remove a task
7. Save tasks
8. Exit
======================================
Enter your choice (1-8): 7
Tasks saved successfully.
===== ShitRust Todo List Manager =====
1. Add a new task
2. List all tasks
3. List pending tasks
4. List completed tasks
5. Mark task as completed
6. Remove a task
7. Save tasks
8. Exit
======================================
Enter your choice (1-8): 8
Saving tasks before exit...
Goodbye!
Extensions and Challenges
Now that you've built a basic Todo List Manager, here are some ideas to extend and improve it:
- Add task priorities (high, medium, low)
- Implement task categories or tags
- Add the ability to sort tasks by due date, priority, etc.
- Implement task reminders or notifications
- Add a search function to find tasks by keywords
- Create a simple GUI version using a ShitRust GUI library
These extensions will give you more practice with ShitRust and help you build a more powerful task management application.
Next Steps
Congratulations on completing this tutorial! You've built a functional Todo List Manager and learned about several key features of ShitRust.
Check out our other tutorials to continue your ShitRust journey:
Or explore the Language Reference to learn more about ShitRust's features.