Pustakam Library

Free Programming learning guide

Learn Rust Programming: A Beginner's Guide

Learn Rust Programming: A Beginner's Guide — a free beginner-level guide covering learn rust programming for beginners. Learn with clear explanations,...

125 min read13 chaptersbeginner

What you will learn

  1. Getting Started with Rust
  2. Variables, Data Types & Operators
  3. Control Flow & Functions
  4. Ownership & Borrowing
  5. Structs & Enums
  6. Collections & Strings
  7. Option, Result & Error Handling
  8. Traits & Generics
  9. Modules & Code Organization
  10. Iterators & Closures
  11. Testing & Documentation
  12. Concurrency Basics
  13. Capstone Project

1. Getting Started with Rust

Why Rust, and Why Start Here? Imagine you are building a web browser. You need a language that is fast enough to render complex graphics at sixty frames per second, but safe enough that a single malformed webpage doesn't crash the entire application or expose a critical security vulnerability. For decades, developers faced a stark choice: use a language like C or C++ for maximum performance at the cost of safety, or use a language like Python or JavaScript for safety at the cost of raw speed. Rust was built to shatter that compromise. It provides low-level control over hardware and memory, comparable to C++, but enforces strict safety rules during compilation that prevent entire classes of bugs before your program ever runs. But before we can build a web browser, a game engine, or an operating system, we need the tools to write, compile, and run a single line of code. Every Rust programmer—whether building a tiny script or a massive enterprise application—begins the exact same way: by installing the Rust toolchain and creating a "Hello, World!" program. Installing the Rust Toolchain To write and run Rust code, you need a collection of tools known as a toolchain. This includes the Rust compiler (the program that translates your human-readable code into machine code), standard libraries, and utility programs. Rust uses a fantastic tool called rustup to manage this toolchain. Rustup acts as an installer and version manager, ensuring you always have the latest stable version of Rust and allowing you to easily switch between different versions if needed. Installing on Windows If you are using Windows, you will need the C++ build tools. The easiest way to get them is by downloading the "Build Tools for Visual Studio" from Microsoft's official website, which includes the MSVC compiler required to build Rust programs on Windows. Once the C++ build tools are installed, go to the official Rust website (rustup.rs) and download the rustup-init.exe file. Run the executable in your terminal or command prompt, and it will guide you through the installation process. Installing on macOS and Linux For macOS and Linux, the installation is a single command run in your terminal. Open your terminal and paste the following command: This command downloads a script and runs it to install rustup. The script will ask how you want to install Rust; for beginners, simply pressing "Enter" to accept the default installation is the best choice. Verifying the Installation Once the installation finishes, you need to verify that your computer recognizes the new commands. Close your current terminal window and open a new one (this ensures the terminal reloads its list of available commands). Type the following command and press Enter: If …

2. Variables, Data Types & Operators

Storing and Changing Data Every program you write will manipulate data. Whether you are calculating the total price of a shopping cart, checking if a user is logged in, or processing text from a file, you need ways to store and manipulate information. In Rust, we store data in variables. In the previous chapter, we used Cargo to build and run a program that printed "Hello, world!". We did this using the println! macro. But what if we want to reuse a piece of data multiple times, or change it before printing? Let's look at a simple scenario. Imagine you are building a program to track the lives of a character in a game. You start by creating a variable named health and assigning it a value of 100. Here, we use the let keyword to declare (create) a variable. We name it health, use the equals sign = to assign it the value 100, and end the statement with a semicolon ;. Finally, we use the {} syntax inside the println! macro to insert the value of our variable into the text. If you run this code using cargo run, it will output Player health: 100. The Power of Immutability If you write a lot of Python, JavaScript, or C++, you might expect to be able to change the value of health simply by assigning a new value to it. Let's try subtracting 20 points of damage when the player gets hit: If you try to compile this, the Rust compiler will stop you in your tracks with an error: This is one of the most defining features of Rust: variables are immutable by default. When a variable is immutable, once a value is bound to a name, you cannot change that value. Rust enforces this rule at compile time. Why? Because human error is the root of countless bugs. If a variable can be changed anywhere in a large program, it becomes incredibly difficult to track how and when its value changes. By forcing variables to be immutable by default, Rust allows the compiler to guarantee that a specific piece of data will not change unexpectedly, making your code inherently safer and easier to reason about. Making Variables Mutable Of course, sometimes you do need to change a value. In a game, a player's health fluctuates constantly. To tell the Rust compiler that a variable is allowed to change, you use the mut keyword. Now the program compiles successfully and outputs Player health: 80. By requiring you to explicitly write mut, Rust forces you to declare your intent. If you read through a block of Rust code and see a variable declared without mut, you can immediately breathe …

3. Control Flow & Functions

Making Decisions with Conditionals Imagine you are building an automated climate control system for a greenhouse. If the temperature rises above 30°C, the system needs to turn on the ventilation fans. Otherwise, the fans should remain off to conserve energy. In the previous chapter, you learned how to store this temperature reading in an immutable variable. But a variable holding a number doesn't do anything on its own. To make your software react to the world, you need control flow—the ability to execute different code depending on certain conditions. The most fundamental building block for decision-making is the if expression. The if Expression In Rust, an if expression allows you to branch your code based on a condition. Let’s look at the basic syntax using our greenhouse example: The condition here is temperature 30. Because temperature is 32, the condition evaluates to true, and the code inside the curly braces { } runs. If the temperature had been 25, the condition would evaluate to false, and the program would simply skip that block of code. A critical rule in Rust is that the condition in an if statement must evaluate to a Boolean (bool). Unlike languages such as C or JavaScript, Rust will not automatically convert numbers or strings to a boolean. For example, if temperature { ... } will cause a compiler error. You must be explicit: if temperature != 0 { ... }. Handling Multiple Branches with else and else if Often, a single branch isn't enough. You might want to perform one action if the condition is true, and a different action if it is false. You can do this with an else block: What if you have more than two possible scenarios? You can chain conditions together using else if. Let's expand the greenhouse logic to handle a freezing scenario as well: When this code runs, Rust checks each condition sequentially from top to bottom. Once it finds a condition that evaluates to true, it executes that corresponding block of code and then skips the rest of the if/else if/else chain. Repeating Code with Loops Making a decision once is useful, but what if you need to check the greenhouse temperature continuously? Writing the same if expression over and over is impractical. Loops allow you to execute a block of code more than once. Rust provides three primary loop constructs: loop, while, and for. Unconditional Repetition with loop The loop keyword tells Rust to execute a block of code over and over again forever, until you explicitly tell it to stop. Because loop runs indefinitely, you must use the break keyword to exit the loop. You can also use continue to skip the rest of the …

4. Ownership & Borrowing

The Memory Management Problem In languages like Python or JavaScript, a background process called a garbage collector periodically scans your program's memory, finds data that is no longer being used, and cleans it up for you. This is convenient, but it requires computational overhead and can cause unpredictable performance hiccups. In older languages like C and C++, you are responsible for manually allocating memory when you create data and explicitly freeing it when you are done. If you forget to free it, your program will leak memory. If you accidentally free it twice or try to use it after freeing it, your program will crash or behave unpredictably. Rust aims for the best of both worlds: memory safety without a garbage collector, and high performance without manual memory management. It achieves this through a system of rules checked at compile time called ownership. Because Rust is statically typed, the compiler already knows exactly what your variables and data types are. The ownership system leverages this by enforcing rules about which variable "owns" a piece of data. If you break these rules, your code simply won't compile. This means memory bugs are caught before your program ever runs. The Three Rules of Ownership Ownership is built on three foundational rules: 1. Each value in Rust has a variable that is its owner. 2. There can only be one owner at a time. 3. When the owner goes out of scope, the value is dropped (deleted). Let’s break down what this means. A scope is the range within a program where a variable is valid. In Rust, scopes are marked by curly braces { }. When a variable comes into scope, it is created. When it goes out of scope, Rust automatically cleans up the memory associated with it. In this example, s is the owner of the String. When the inner scope ends, s goes out of scope, and the memory is immediately returned to the operating system. No garbage collector required. Moving Values The second rule states there can only be one owner at a time. To understand how this works in practice, we need to distinguish between two types of data we've seen so far: basic data types and complex ones. Copying Basic Types Remember from the variables chapter that integers, floating-point numbers, and booleans are basic, fixed-size types. Because they are small and live entirely on the stack, assigning them to a new variable simply copies the value. Here, x remains valid after let y = x. There are now two independent values of 5 on the stack. Moving Complex Types Complex types like String behave differently. A String is made up of three parts: a pointer to …

5. Structs & Enums

Beyond the Basics: Modeling Real-World Data Imagine you are building an application for a library. So far, you have learned how to store a single piece of data—like a book's title as a string literal, or its publication year as an integer. But a real book is a collection of related properties: it has a title, an author, a page count, and an ISBN. In Chapter 2: Variables, Data Types & Operators, you met Rust’s basic data types like i32 and bool. While useful, these single values don't let us group related data together. If you wanted to pass a book around your program, passing four separate variables for every book would quickly become unmanageable. Rust solves this by letting you define your own custom data types. The two primary tools for this are structs and enums. A struct (short for "structure") groups multiple related values under a single name. If a struct is a logical "AND"—a Book has a title AND an author AND a page count—an enum (short for "enumeration") is a logical "OR". It defines a type that can be one of several distinct variants: a Payment can be Cash OR Credit OR Crypto. By the end of this chapter, you will be able to model complex, real-world concepts in Rust, attach custom behavior to them, and safely handle every possible state your data might take. Defining Structs A struct is defined using the struct keyword, followed by a name (which should follow Rust's naming convention of CamelCase), and a set of curly braces containing its fields. Each field is given a name and a data type. Let's create a struct to represent a book in our library system: Instantiating a Struct Defining a struct is like creating a blueprint. To actually use it, we must create an instance of that struct. We do this by providing concrete values for each field. Notice that we don't need to write the data types when instantiating the struct; Rust infers them from the blueprint. Once an instance is created, you can access specific fields using dot notation (e.g., mybook.pagecount). If a struct instance is mutable (declared with the mut keyword, as covered in Chapter 2), you can also update its fields. Remember, Rust variables are immutable by default, so you must opt into mutability for the entire instance—you cannot mark just a single field as mutable. Struct Update Syntax Sometimes, you want to create a new struct instance based on an old one, changing only a few fields. Rust provides a shortcut called the struct update syntax using the .. operator. Here, unavailablebook will have the exact same title, author, and pagecount as mybook, but isavailable will be set …

6. Collections & Strings

Storing Lists of Data with Vectors Imagine you are building a command-line application that reads a log file. You don't know in advance how many lines the file will contain. It could be ten lines, or it could be ten thousand. If you relied strictly on the fixed-size arrays we touched on earlier, you would have to guess the maximum size upfront, wasting memory if the file is small, or crashing the program if the file is larger than your guess. To handle groups of data that can grow and shrink while your program runs, Rust provides collections. Unlike arrays and tuples, which store data on the stack and have a fixed size determined at compile time, collections store their data on the heap. As a reminder from our Ownership & Borrowing chapter, the heap is used for data whose size might change or whose lifetime we need to manage dynamically. The most common and versatile collection in Rust is the vector, written as Vec<T. The <T is a placeholder for the type of data the vector will hold. A vector of integers is Vec<i32, and a vector of characters is Vec<char. Creating and Updating Vectors To create a new, empty vector, we use the Vec::new() function. Because we haven't inserted any values yet, Rust doesn't know what type of data we intend to store. We must add an explicit type annotation. Notice the mut keyword. Just like standard variables, vectors are immutable by default. If we want to add or remove elements, we must make the binding mutable. More often, you will want to create a vector initialized with specific values. Rust provides a convenient macro, vec!, for exactly this purpose. When you use vec!, Rust can infer the type from the values you provide, so the type annotation is no longer strictly necessary. To add elements to an existing vector, we use the .push() method. This appends the new value to the end of the list. Reading from Vectors Once you have data in a vector, you need a way to access it. There are two primary ways to read an element from a vector: by index, or by using the .get() method. Accessing by index works exactly like it does with arrays. You provide the index number in square brackets. Remember that Rust, like most programming languages, uses zero-based indexing, so the first element is at index 0. The second way is using the .get() method, which returns an Option type. While we will dive deep into Option in the next chapter, for now you just need to know that it means the method returns either Some(value) if the index exists, or None if it doesn't. Why …

7. Option, Result & Error Handling

The Problem with Nothing Imagine you are writing a function to find a user's email address in a database. If the user exists, you return their email as a string. But what if the user doesn't exist? What do you return? In many older programming languages, you might return a special "null" or "nil" value. The calling code then has to remember to check for this null before trying to use it. If a programmer forgets that check, the program crashes at runtime with a "null pointer exception"—a mistake so common that a famous computer scientist once called it his "billion-dollar mistake." Rust refuses to allow this vulnerability. In Rust, there is no null. Instead, Rust forces you to explicitly acknowledge that a value might be absent by using a built-in enum called Option<T. Representing Absence with Option<T In the previous chapter on Structs & Enums, we saw how to build our own custom enums. The Rust standard library provides an enum that is so useful it is built right into the core language. It is called Option<T, and it looks exactly like this: The <T syntax means this enum is generic over a type. It simply means that Option can hold any type T inside its Some variant. If you have an Option<String, it can either be Some("hello@example.com") or None. Because Option is so fundamental, you don't even need to bring it into scope with a use statement. Both Option, Some, and None are available everywhere in your Rust code. Handling Option with match Because Option is just an enum, you can handle it using the match control flow construct we learned earlier. match forces you to account for every possibility, which means the compiler guarantees you will never accidentally try to use a value that isn't there. If the function returns Some, we extract the inner String and bind it to the variable address. If it returns None, we handle that case gracefully. The compiler ensures both arms are present. Representing Failure with Result<T, E Option<T is perfect for when a value is simply missing. But what about operations that can fail? If you try to read a file from the hard drive, the file might not exist. If you try to parse text into a number, the text might contain letters. In these cases, just knowing the operation failed isn't enough—you usually want to know why it failed. Rust handles this with another built-in enum: Result<T, E. Result has two generic types: T for the success type, and E for the error type. If the operation succeeds, it returns Ok(T) holding the successful value. If it fails, it returns Err(E) holding an error detail. Let's look at …

8. Traits & Generics

The Problem of Duplication Imagine you are building an application that processes geometric shapes. You have a Circle struct and a Square struct, and you want to calculate the area for both. In previous chapters, we learned how to define methods on structs using impl blocks. You might write a method called area() for Circle, and another method called area() for Square. But what happens when you want to write a function that prints the area of any shape? If you write fn printarea(shape: Circle), it will only accept circles. If you want to support squares, you have to write a second function: fn printareasquare(shape: Square). If you add a Triangle later, you need a third function. This violates a core principle of good software design: Don't Repeat Yourself (DRY). We want to write a single printarea function that works with any type that knows how to calculate its own area. Rust solves this problem using two interconnected features: traits and generics. Defining Traits A trait is a way to define a shared contract or behavior. If you are familiar with object-oriented languages, it is similar to an interface. A trait tells the compiler, "Any type that implements this trait must provide these specific methods." Let's define a trait called HasArea: This definition says: "There is a behavior called HasArea. Any type that claims to have this behavior must provide a method called area that takes an immutable reference to itself (&self) and returns an f64." Notice that the trait body only contains the function signature. It ends with a semicolon, not curly braces. There is no implementation here. The trait just dictates the rules. Implementing Traits for Custom Types Now that we have our contract, we need our custom types to sign it. We do this using an implementation block (impl). Let's bring in our Circle and Square structs from previous chapters and make them obey the HasArea trait. By writing impl HasArea for Circle, we are telling Rust that Circle fulfills the contract of the HasArea trait. We then provide the actual logic inside the block. Now, both Circle and Square are guaranteed to have an area() method. Writing Generic Functions Now we can solve our original problem. We want a single function that accepts any type that has an area. A generic type parameter allows us to write a placeholder for a type instead of committing to a specific one. By convention, generics are often named using a single uppercase letter like T, though you can use longer names like Shape. Here is our first generic function: The angle brackets <T right after the function name tell Rust: "This function is generic over some type T. The …

9. Modules & Code Organization

The Problem with a Single File Imagine you are building an application to manage a space exploration company. You need to track rockets, calculate orbital trajectories, manage employee records, and handle customer invoices. If you write all of this in a single main.rs file, you will soon be staring at a 5,000-line scrolling nightmare. Finding the right function becomes a scavenger hunt, and naming things gets complicated—do you name your function calculatetrajectory or calculaterockettrajectory to avoid clashing with an accounting function? To solve this, software developers organize code into smaller, logical pieces. Rust provides a powerful system for splitting code into multiple files and namespaces. In this chapter, we will explore how Rust organizes code using packages, crates, and modules, and how you can use these tools to keep your projects clean, readable, and maintainable. Packages, Crates, and Modules Before we write any code, we need to define some Rust-specific vocabulary. Rust uses a strict hierarchy for code organization: - Package: A collection of one or more crates. When you run cargo new myproject, Cargo creates a package. A package contains a Cargo.toml file (which describes the package) and a src directory. - Crate: The fundamental unit of compilation in Rust. A crate can be a binary (an executable program) or a library (code meant to be used by other programs). Every crate has a crate root, which is the source file the Rust compiler starts at. - Module: A way to organize code within a crate. Modules let you group related functions, structs, and enums together, controlling their visibility and namespacing them. Think of a package as a shipping container. Inside that container, you might have a truck (the binary crate that drives the application) and a toolbox (the library crate that holds shared tools). Inside the toolbox, modules act like the drawers and compartments keeping your tools organized. Binary Crates vs. Library Crates So far in this book, we have mostly worked with binary crates. A binary crate is a program that compiles down to an executable file. The entry point for a binary crate is the main function, usually located in src/main.rs. A library crate, on the other hand, does not have a main function. It is a collection of code meant to be shared and used by other crates. When you build a library crate, Rust generates a .rlib (Rust library) file or a standard dynamic/static library, not an executable. Often, a package will contain both. Cargo has a convention for this: if you have both src/main.rs and src/lib.rs in your package, Cargo treats them as two separate crates within the same package. The binary crate (main.rs) can use the library crate (lib.rs), effectively separating your …

10. Iterators & Closures

Capturing Behavior with Closures Imagine you are building a web server and you need a background task to run every 60 seconds. You want to pass a piece of code to the timer that the timer can execute later. In many languages, you would use a "callback" or an "anonymous function." In Rust, we use closures. A closure is essentially a function that can "remember" the variables from the environment where it was created. We covered functions back in Chapter 3, but standard Rust functions cannot capture variables from their surrounding scope. Closures fill this gap. Let's look at the syntax. Closures are defined using vertical pipes || to enclose their parameters, followed by curly braces {} for the body (which can be omitted for single expressions). In this example, greet is a closure. It takes no parameters (hence the empty ||), but it reaches outside of itself to capture the username variable. How Closures Capture Their Environment Because closures can capture outside variables, Rust needs to manage the ownership and borrowing of those variables. Back in Chapter 4, we learned that Rust has strict rules for borrowing (shared & references) and moving ownership. Closures use these exact same rules to capture variables, falling into three categories: 1. Borrowing immutably: The closure only needs to read the value. 2. Borrowing mutably: The closure needs to modify the value. 3. Taking ownership: The closure needs to take full ownership of the value (often done so the closure can be moved to a new thread, which we'll see in Chapter 12). Rust is smart enough to figure out which method to use based on how the closure uses the variable. It will always choose the least restrictive option. Notice the move keyword in the third example. If you want to force a closure to take ownership of every variable it captures—regardless of whether it strictly needs to—you add move before the parameter pipes. This is highly useful when you are passing a closure to a new thread so it doesn't rely on memory that might be cleaned up by the original thread. The Fn, FnMut, and FnOnce Traits In Chapter 8, we learned about Traits and Generics. Closures are so useful that Rust defines special traits to allow functions to accept closures as arguments. But because closures capture environments differently, there isn't just one trait for closures—there are three: - FnOnce: Applies to all closures. The closure captures its environment by moving ownership. It can only be called exactly once because calling it consumes the captured variables. - FnMut: Applies to closures that don't move ownership out of their environment, but might mutate the captured variables. They can be called multiple times. …

11. Testing & Documentation

Why Test and Document Your Code? Imagine you write a function to calculate the final price of a shopping cart, applying a 10% discount to items over $50. You run it once, it looks right, and you move on. Three weeks later, you add a new feature for tax calculation. You deploy your updated application, and suddenly, customers are being charged $0 for their entire carts. What happened? Without tests, every change to your codebase is a leap of faith. As your Rust programs grow—incorporating structs, enums, traits, and complex ownership patterns—the mental overhead of verifying that every piece still works becomes impossible. Testing is the practice of writing code that checks your other code. By writing automated tests, you create a safety net. If you change a function and break something, your tests will tell you immediately. Documentation, on the other hand, is how you communicate with your future self, your teammates, or anyone else who wants to use your code. Rust treats both testing and documentation as first-class citizens. The Rust compiler (rustc) and build tool (Cargo) have built-in support for running tests and generating HTML documentation directly from your source code. Writing Your First Unit Test A unit test is a small test that checks a single, isolated piece of code—usually one function. In Rust, unit tests live in the same file as the code they are testing. Let’s look at a practical example. Suppose we are building a simple arithmetic module and want to test an addition function. There are a few new concepts here: - [cfg(test)]: This is an attribute (a piece of metadata attached to a module). It tells the Rust compiler to only compile this tests module when you run cargo test, not when you are building your final application for release. This keeps your final binary small and fast. - mod tests: We use the module system (covered in Chapter 9) to neatly tuck our tests away. - use super::: super refers to the parent module. This line brings the add function into the test module's scope so we can use it without typing super::add every time. - [test]: This attribute marks the function below it as a test function. When you run cargo test, Rust will look for every function with this attribute and run it. - asserteq!: This is a macro (like println!) that asserts that two values are equal. If result is not equal to 4, the test fails. The Assertion Macros Rust provides three primary macros for making assertions in your tests: 1. assert!(expression): Passes if the expression evaluates to true. Fails if it evaluates to false. 2. asserteq!(left, right): Passes if left equals right. 3. assertne!(left, right): …

12. Concurrency Basics

Why Concurrency? Imagine you are cooking a multi-course dinner. If you prepare the salad, bake the bread, roast the vegetables, and simmer the soup one after another, the meal will take hours. Instead, you turn on the oven, chop vegetables while the bread bakes, and monitor the soup while the salad chills. By doing multiple things at the same time, you finish much faster. Software works the same way. Modern computers have multiple processor cores, meaning they can genuinely execute multiple instructions simultaneously. When we write concurrent code, we break our program into separate tasks that can run independently and at the same time. However, concurrency introduces a notorious problem: what happens when two tasks try to change the exact same piece of data at the exact same time? In many programming languages, this leads to data races—unpredictable bugs that cause crashes or corrupt data. Rust was designed from the ground up to make concurrency safe. Thanks to the ownership and borrowing rules you learned in Module 4, Rust can catch concurrency bugs at compile time, before your program ever runs. Spawning Threads In Rust, the standard unit of concurrent execution is a thread. You can think of a thread as a sequence of instructions that runs independently. Every Rust program starts with one main thread (which executes your main function). To do things concurrently, we can spawn new threads. We use the std::thread::spawn function to create a new thread. It takes a closure (introduced in Module 10) containing the code we want to run. If you run this code, you might notice something surprising: the background task might not finish! When the main thread finishes its loop, the main function ends, and the program shuts down immediately, killing any background threads still running. To fix this, we need to wait for the spawned thread to finish. We do this by capturing the JoinHandle returned by thread::spawn and calling the join method on it. Now, the program will wait at handle.join().unwrap() until the background task completes before the program exits. The move Keyword In the previous example, our thread didn't use any data from the outside environment. But what if we want to pass data into the thread? Because threads can outlive the function that creates them, Rust must ensure that any data a thread uses lives as long as the thread does. If we try to borrow a variable from the main thread, Rust's borrow checker will stop us, fearing the main thread might delete the data while the background thread is still using it. To solve this, we use the move keyword before the closure. This forces the closure to take ownership of the variables it uses, moving …

13. Capstone Project

Imagine having a digital sticky note for every brilliant idea, grocery list, and meeting reminder you’ve ever had—scattered across your computer in random text files. What if you could replace that chaos with a single, fast command-line tool built entirely by you? Throughout this journey, you’ve gathered an impressive toolkit. You started with println! and basic data types, moved through Ownership & Borrowing, mastered Option, Result & Error Handling, and learned how to organize code using Modules & Code Organization. Now, it’s time to assemble those tools into a complete, working application. This capstone project walks you through building taskr, a command-line to-do list manager. Along the way, you will design a project structure, parse user input safely, combine file I/O with collections to save your data, write tests to ensure it works, and package it for sharing. Designing the taskr Project Real-world applications are rarely written in a single main.rs file. To build taskr, we will use Cargo to set up a binary project and immediately divide our logic into modules. Start by creating a new project in your terminal: This generates your Cargo.toml file and the src/ directory containing main.rs. For this project, we will split our code into three main areas of responsibility: 1. main.rs: The entry point. It will parse command-line arguments and route them to the correct actions. 2. task.rs: A module to define what a "task" is and how to display it. 3. storage.rs: A module to handle reading and writing our tasks to a file. To keep things simple and avoid introducing complex database systems, taskr will save tasks as plain text in a file called tasks.txt. Defining the Data Model Before we can manage tasks, we need to define what a task is. Create a new file in your src/ directory called task.rs. We will use a Struct to represent our task and an Enum to represent its status. Because we eventually want to save tasks to a file and read them back, we will derive Debug (to print it easily) and Clone (to create copies when needed). Here, we use a Trait (fmt::Display) to control how a task prints to the console. Instead of the default debug output, a pending task with ID 1 will print cleanly as 1 [ ] - Buy milk. Handling File I/O and Collections Now that we have our data model, we need a way to store and retrieve it. Create src/storage.rs. This module will use Collections (specifically Vec<Task) to hold our tasks in memory, and standard library file I/O to persist them. We will represent each line in tasks.txt as a task. To make parsing easier, we'll separate the status and description with a pipe …

Continue learning