<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ Stephen Emmanuel - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ Stephen Emmanuel - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Mon, 25 May 2026 05:05:18 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/author/stephcrown/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How to Build a Local-First CLI Financial Tracker with Rust [Full Handbook] ]]>
                </title>
                <description>
                    <![CDATA[ Most financial apps store your sensitive data on remote servers. This requires you to trust a company with your records and rely on their service staying online. But if you build a local-first application, you can keep your data on your own machine i... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-build-a-local-first-cli-financial-tracker-with-rust/</link>
                <guid isPermaLink="false">696183e6f3839d7c9a8133ff</guid>
                
                    <category>
                        <![CDATA[ Rust ]]>
                    </category>
                
                    <category>
                        <![CDATA[ handbook ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Stephen Emmanuel ]]>
                </dc:creator>
                <pubDate>Fri, 09 Jan 2026 22:40:38 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1767998415383/82c48f39-cd5e-4f66-af83-2b65bafccd65.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Most financial apps store your sensitive data on remote servers. This requires you to trust a company with your records and rely on their service staying online. But if you build a local-first application, you can keep your data on your own machine in a format you can actually read.</p>
<p>In this guide, you’ll learn how to create a financial tracker that runs entirely in your terminal. You’ll use Rust to build a system that saves transactions to a local JSON file, ensuring that you have total ownership of your information.</p>
<p>Along the way, you’ll learn how to use the Rust type system to validate financial data and handle file errors gracefully. You’ll also use the Clap library to create a professional command line interface. By the time you finish, you’ll understand how to manage local state, serialize data with Serde, and structure a modular Rust application.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ol>
<li><p><a class="post-section-overview" href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-commands-youll-build">Commands You’ll Build</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-1-set-up-the-project">Step 1: Set Up the Project</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-2-design-the-data-model">Step 2: Design the Data Model</a></p>
<ul>
<li><a class="post-section-overview" href="#heading-add-methods-to-the-trackerdata">Add Methods to the TrackerData</a></li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-step-3-handle-errors-properly">Step 3: Handle Errors Properly</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-map-system-errors-to-custom-errors">Map System Errors to Custom Errors</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-prepare-for-error-output">Prepare for Error Output</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-step-4-create-file-operations">Step 4: Create File Operations</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-add-json-utility-functions">Add JSON Utility Functions</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-register-the-utilities">Register the Utilities</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-step-5-set-up-the-cli-structure">Step 5: Set Up the CLI Structure</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-the-command-architecture">The Command Architecture</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-manage-paths-with-global-context">Manage Paths with Global Context</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-register-the-command-system">Register the Command System</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-step-6-create-response-types">Step 6: Create Response Types</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-define-the-response-structures">Define the Response Structures</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-implement-the-output-module">Implement the Output Module</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-update-the-library-registration">Update the Library Registration</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-step-7-create-argument-parsing-helpers">Step 7: Create Argument Parsing Helpers</a></p>
<ul>
<li><a class="post-section-overview" href="#heading-implement-custom-data-parsers">Implement Custom Data Parsers</a></li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-step-8-implement-the-init-command">Step 8: Implement the Init Command</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-9-implement-the-add-command">Step 9: Implement the Add Command</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-10-implement-the-list-command">Step 10: Implement the List Command</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-11-implement-the-update-command">Step 11: Implement the Update Command</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-12-implement-the-delete-command">Step 12: Implement the Delete Command</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-13-implement-subcategory-commands">Step 13: Implement Subcategory Commands</a></p>
<ul>
<li><p><a class="post-section-overview" href="#heading-list-subcategories">List Subcategories</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-add-subcategories">Add Subcategories</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-delete-subcategories">Delete Subcategories</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-rename-subcategories">Rename Subcategories</a></p>
</li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-step-14-implement-the-total-command">Step 14: Implement the Total Command</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-15-wire-up-the-main-function">Step 15: Wire Up the Main Function</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-test-your-application">Test Your Application</a></p>
<ul>
<li><a class="post-section-overview" href="#heading-install-the-binary">Install the Binary</a></li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-whats-next-and-advanced-features">What's Next and Advanced Features</a></p>
<ul>
<li><a class="post-section-overview" href="#heading-advanced-features-to-explore">Advanced Features to Explore</a></li>
</ul>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ol>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>To follow along with this tutorial, you should have a basic comfort level with Rust syntax. You don’t need to be an expert, but you should understand how to use variables, functions, and structs.</p>
<p>You’ll also need the following tools and knowledge:</p>
<ul>
<li><p>Rust installed (version 1.70 or later). If you don't have Rust installed, follow the <a target="_blank" href="https://rust-book.cs.brown.edu/ch01-01-installation.html">official installation guide</a>. You can verify your installation by running <code>rustc --version</code> in your terminal.</p>
</li>
<li><p>Familiarity with command-line tools and terminal usage.</p>
</li>
<li><p>Basic knowledge of the JSON format.</p>
</li>
</ul>
<h2 id="heading-commands-youll-build">Commands You’ll Build</h2>
<p>This tutorial will guide you on how to implement these commands step-by-step:</p>
<ul>
<li><p><code>init</code>: Initializes a new tracker and creates your storage file.</p>
</li>
<li><p><code>add</code>: Saves new income or expense records to your data.</p>
</li>
<li><p><code>list</code>: Allows you to view and filter your saved transactions.</p>
</li>
<li><p><code>update</code>: Modifies existing records in your storage.</p>
</li>
<li><p><code>delete</code>: Removes specific records from your history.</p>
</li>
<li><p><code>subcategory</code>: Manages custom subcategories (list, add, delete, rename)</p>
</li>
<li><p><code>total</code>: Calculates your financial totals and net balance.</p>
</li>
</ul>
<h2 id="heading-step-1-set-up-the-project">Step 1: Set Up the Project</h2>
<p>To start, you need to create a new Rust project. Open your terminal and run these commands:</p>
<pre><code class="lang-bash">cargo new fintrack
<span class="hljs-built_in">cd</span> fintrack
</code></pre>
<p>This creates a new directory called <code>fintrack</code> with a basic Rust project structure. <code>cargo</code> is Rust's package manager and build tool. It handles dependencies, compilation, and project management.</p>
<p>Now, open <code>Cargo.toml</code> in your editor. This file defines the metadata and libraries for your project. Add the following dependencies that your application will need:</p>
<pre><code class="lang-toml"><span class="hljs-section">[package]</span>
<span class="hljs-attr">name</span> = <span class="hljs-string">"fintrack"</span>
<span class="hljs-attr">version</span> = <span class="hljs-string">"1.0.0"</span>
<span class="hljs-attr">edition</span> = <span class="hljs-string">"2021"</span>

<span class="hljs-section">[dependencies]</span>
<span class="hljs-attr">chrono</span> = <span class="hljs-string">"0.4.42"</span>
<span class="hljs-attr">clap</span> = { version = <span class="hljs-string">"4.5.53"</span>, features = [<span class="hljs-string">"derive"</span>] }
<span class="hljs-attr">dirs</span> = <span class="hljs-string">"6.0.0"</span>
<span class="hljs-attr">serde</span> = { version = <span class="hljs-string">"1.0.228"</span>, features = [<span class="hljs-string">"derive"</span>] }
<span class="hljs-attr">serde_json</span> = <span class="hljs-string">"1.0.148"</span>
<span class="hljs-attr">strum</span> = { version = <span class="hljs-string">"0.26"</span>, features = [<span class="hljs-string">"derive"</span>] }
</code></pre>
<p>Here’s what each dependency does in your project:</p>
<ul>
<li><p><code>chrono</code>: Handles dates and times. You'll use it to parse dates from user input and format them for display.</p>
</li>
<li><p><code>clap</code>: A library for building command-line interfaces. It manages the process of parsing and validating the arguments you type into the terminal.</p>
</li>
<li><p><code>dirs</code>: Provides a cross-platform way to find the user's home directory, where you'll store the tracker data.</p>
</li>
<li><p><code>serde</code> and <code>serde_json</code>: <code>serde</code> is Rust's serialization framework. Combined with <code>serde_json</code>, it lets you convert Rust structs to JSON and back. This is how you'll save and load your tracker data.</p>
</li>
<li><p><code>strum</code>: Provides macros to automatically generate useful code for enums, like converting them to strings and parsing strings into enums.</p>
</li>
</ul>
<p>The <code>features = ["derive"]</code> for <code>clap</code> and <code>serde</code> enables their derive macros, which will let you use attributes like <code>#[derive(...)]</code> to automatically generate the code needed for parsing and data conversion.</p>
<h2 id="heading-step-2-design-the-data-model">Step 2: Design the Data Model</h2>
<p>Before writing any command logic, you’ll want to define the structure of the data your tracker will store. In Rust, you use structs to group related data much like a record in a database, and <strong>enums</strong> to represent values that can only be one of several fixed variants.</p>
<p>Create a new file src/models.rs and add the code to define a record:</p>
<pre><code class="lang-rust"><span class="hljs-keyword">use</span> chrono::NaiveDate;
<span class="hljs-keyword">use</span> serde::{Deserialize, Serialize};
<span class="hljs-keyword">use</span> std::collections::HashMap;

<span class="hljs-meta">#[derive(Debug, Clone, Serialize, Deserialize)]</span>
<span class="hljs-keyword">pub</span> <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">Record</span></span> {
    <span class="hljs-keyword">pub</span> id: <span class="hljs-built_in">usize</span>,
    <span class="hljs-keyword">pub</span> category: <span class="hljs-built_in">usize</span>,
    <span class="hljs-keyword">pub</span> amount: <span class="hljs-built_in">f64</span>,
    <span class="hljs-keyword">pub</span> subcategory: <span class="hljs-built_in">usize</span>,
    <span class="hljs-keyword">pub</span> description: <span class="hljs-built_in">String</span>,
    <span class="hljs-keyword">pub</span> date: <span class="hljs-built_in">String</span>,
}
</code></pre>
<p>This Record struct represents a single income or expense transaction. The <code>#[derive(...)]</code> attribute automatically implements traits that allow you to print the struct for debugging, copy it, and convert it to or from JSON. The <code>pub</code> keyword ensures that these fields are accessible to the other modules you will build.</p>
<p>Next, add the main data structure to the <code>src/models.rs</code> file:</p>
<pre><code class="lang-rust"><span class="hljs-meta">#[derive(Debug, Clone, Serialize, Deserialize)]</span>
<span class="hljs-keyword">pub</span> <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">TrackerData</span></span> {
    <span class="hljs-keyword">pub</span> version: <span class="hljs-built_in">u32</span>,
    <span class="hljs-keyword">pub</span> currency: <span class="hljs-built_in">String</span>,
    <span class="hljs-keyword">pub</span> created_at: <span class="hljs-built_in">String</span>,
    <span class="hljs-keyword">pub</span> last_modified: <span class="hljs-built_in">String</span>,
    <span class="hljs-keyword">pub</span> opening_balance: <span class="hljs-built_in">f64</span>,
    <span class="hljs-keyword">pub</span> categories: HashMap&lt;<span class="hljs-built_in">String</span>, <span class="hljs-built_in">usize</span>&gt;,
    <span class="hljs-keyword">pub</span> subcategories_by_id: HashMap&lt;<span class="hljs-built_in">usize</span>, <span class="hljs-built_in">String</span>&gt;,
    <span class="hljs-keyword">pub</span> subcategories_by_name: HashMap&lt;<span class="hljs-built_in">String</span>, <span class="hljs-built_in">usize</span>&gt;,
    <span class="hljs-keyword">pub</span> next_subcategory_id: <span class="hljs-built_in">u32</span>,
    <span class="hljs-keyword">pub</span> records: <span class="hljs-built_in">Vec</span>&lt;Record&gt;,
    <span class="hljs-keyword">pub</span> next_record_id: <span class="hljs-built_in">usize</span>,
}
</code></pre>
<p>This struct holds the state of the entire application. It uses a HashMap for categories and subcategories to allow for fast lookups by name or ID. All individual transactions are stored in the <code>records</code> vector, which can grow dynamically as you add more data.</p>
<p>Now, add enums to handle your fixed categories and supported currencies:</p>
<pre><code class="lang-rust"><span class="hljs-meta">#[derive(clap::ValueEnum, Clone, Debug, strum::Display, strum::EnumString)]</span>
<span class="hljs-meta">#[strum(serialize_all = <span class="hljs-meta-string">"lowercase"</span>, ascii_case_insensitive)]</span>
<span class="hljs-keyword">pub</span> <span class="hljs-class"><span class="hljs-keyword">enum</span> <span class="hljs-title">Category</span></span> {
    Income,
    Expenses,
}

<span class="hljs-meta">#[derive(clap::ValueEnum, Clone, Debug, strum::Display, strum::EnumString)]</span>
<span class="hljs-meta">#[strum(serialize_all = <span class="hljs-meta-string">"UPPERCASE"</span>, ascii_case_insensitive)]</span>
<span class="hljs-keyword">pub</span> <span class="hljs-class"><span class="hljs-keyword">enum</span> <span class="hljs-title">Currency</span></span> {
    NGN,
    USD,
    GBP,
    EUR,
    CAD,
    AUD,
    JPY,
}
</code></pre>
<p>These enums ensure the user can only input valid categories or currencies. The strum attributes handle the conversion between terminal input strings and your Rust code, while <code>clap::ValueEnum</code> allows these types to work directly with your command-line arguments.</p>
<h3 id="heading-add-methods-to-the-trackerdata">Add Methods to the TrackerData</h3>
<p>To interact with this data in the <code>TrackerData</code> struct, you need to add methods using an <code>impl</code> block. These methods will handle adding records and calculating totals:</p>
<pre><code class="lang-rust"><span class="hljs-keyword">impl</span> TrackerData {
    <span class="hljs-keyword">pub</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">push_record</span></span>(&amp;<span class="hljs-keyword">mut</span> <span class="hljs-keyword">self</span>, record: Record) -&gt; &amp;<span class="hljs-keyword">Self</span> {
        <span class="hljs-keyword">self</span>.records.push(record);
        <span class="hljs-keyword">self</span>
    }

    <span class="hljs-keyword">pub</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">category_id</span></span>(&amp;<span class="hljs-keyword">self</span>, category: &amp;<span class="hljs-built_in">str</span>) -&gt; <span class="hljs-built_in">usize</span> {
        <span class="hljs-keyword">self</span>.categories[category]
    }

    <span class="hljs-keyword">pub</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">miscellaneous_subcategory_id</span></span>(&amp;<span class="hljs-keyword">self</span>) -&gt; <span class="hljs-built_in">Option</span>&lt;<span class="hljs-built_in">usize</span>&gt; {
        <span class="hljs-keyword">self</span>.subcategories_by_name.get(<span class="hljs-string">"miscellaneous"</span>).copied()
    }

    <span class="hljs-keyword">pub</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">subcategory_id</span></span>(&amp;<span class="hljs-keyword">self</span>, name: &amp;<span class="hljs-built_in">str</span>) -&gt; <span class="hljs-built_in">Option</span>&lt;<span class="hljs-built_in">usize</span>&gt; {
        <span class="hljs-keyword">self</span>.subcategories_by_name.get(&amp;name.to_lowercase()).copied()
    }

    <span class="hljs-keyword">pub</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">category_name</span></span>(&amp;<span class="hljs-keyword">self</span>, id: <span class="hljs-built_in">usize</span>) -&gt; <span class="hljs-built_in">Option</span>&lt;&amp;<span class="hljs-built_in">String</span>&gt; {
        <span class="hljs-keyword">self</span>.categories.iter().find(|(_, v)| **v == id).map(|(k, _)| k)
    }

    <span class="hljs-keyword">pub</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">subcategory_name</span></span>(&amp;<span class="hljs-keyword">self</span>, id: <span class="hljs-built_in">usize</span>) -&gt; <span class="hljs-built_in">Option</span>&lt;&amp;<span class="hljs-built_in">String</span>&gt; {
        <span class="hljs-keyword">self</span>.subcategories_by_id.get(&amp;id)
    }

    <span class="hljs-keyword">pub</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">totals</span></span>(&amp;<span class="hljs-keyword">self</span>) -&gt; (<span class="hljs-built_in">f64</span>, <span class="hljs-built_in">f64</span>) {
        <span class="hljs-keyword">self</span>.records.iter().fold((<span class="hljs-number">0.0</span>, <span class="hljs-number">0.0</span>), |<span class="hljs-keyword">mut</span> acc, r| {
            <span class="hljs-keyword">if</span> r.category == <span class="hljs-number">1</span> {
                acc.<span class="hljs-number">0</span> += r.amount;
            } <span class="hljs-keyword">else</span> {
                acc.<span class="hljs-number">1</span> += r.amount;
            }
            acc
        })
    }
}
</code></pre>
<p>These methods utilize key Rust patterns to manage the tracker's state:</p>
<ul>
<li><p><code>&amp;mut self</code> is used when you need to modify the data, such as pushing a new record into the vector.</p>
</li>
<li><p><code>Option</code> handles cases where a value might not exist, returning <code>Some(value)</code> or <code>None</code>.</p>
</li>
<li><p><code>iter()</code> and <code>fold</code> are used in the <code>totals()</code> method to process all records and accumulate the total income and expenses into a single tuple <code>(f64, f64)</code> representing total income and total expenses.</p>
</li>
</ul>
<p>Finally, add a helper function to create the default tracker JSON structure. Add this to <code>src/models.rs</code>:</p>
<pre><code class="lang-rust"><span class="hljs-keyword">pub</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">default_tracker_json</span></span>(currency: &amp;Currency, opening_balance: <span class="hljs-built_in">f64</span>) -&gt; serde_json::Value {
    serde_json::json!({
        <span class="hljs-string">"version"</span>: <span class="hljs-number">1</span>,
        <span class="hljs-string">"currency"</span>: currency.to_string(),
        <span class="hljs-string">"opening_balance"</span>: opening_balance,
        <span class="hljs-string">"created_at"</span>: chrono::Utc::now().to_rfc3339(),
        <span class="hljs-string">"last_modified"</span>: chrono::Utc::now().to_rfc3339(),
        <span class="hljs-string">"categories"</span>: {
            <span class="hljs-string">"income"</span>: <span class="hljs-number">1</span>,
            <span class="hljs-string">"expenses"</span>: <span class="hljs-number">2</span>
        },
        <span class="hljs-string">"subcategories_by_id"</span>: {
            <span class="hljs-string">"1"</span>: <span class="hljs-string">"miscellaneous"</span>
        },
        <span class="hljs-string">"subcategories_by_name"</span>: {
            <span class="hljs-string">"miscellaneous"</span>: <span class="hljs-number">1</span>
        },
        <span class="hljs-string">"records"</span>: [],
        <span class="hljs-string">"next_record_id"</span>: <span class="hljs-number">1</span>,
        <span class="hljs-string">"next_subcategory_id"</span>: <span class="hljs-number">2</span>
    })
}
</code></pre>
<p>Then, register this module in your <code>src/lib.rs</code> file so the rest of your application can use it:</p>
<pre><code class="lang-rust"><span class="hljs-keyword">pub</span> <span class="hljs-keyword">mod</span> models;
</code></pre>
<h2 id="heading-step-3-handle-errors-properly">Step 3: Handle Errors Properly</h2>
<p>In a financial application, error handling is critical to ensure you don’t lose or corrupt your data. Rust uses a <code>Result</code> type to handle operations that might fail. A <code>Result</code> is either an <code>Ok</code> containing the successful value or an <code>Err</code> containing the error details. This structure forces you to address potential failures explicitly before your code will compile.</p>
<p>Create a new file named <code>src/error.rs</code> and start with the necessary imports:</p>
<pre><code class="lang-rust"><span class="hljs-keyword">use</span> std::io;
</code></pre>
<p>Now, define your custom error types using enums:</p>
<pre><code class="lang-rust"><span class="hljs-meta">#[derive(Debug)]</span>
<span class="hljs-keyword">pub</span> <span class="hljs-class"><span class="hljs-keyword">enum</span> <span class="hljs-title">ValidationErrorKind</span></span> {
    AmountTooSmall { amount: <span class="hljs-built_in">f64</span> },
    InvalidDate { provided: <span class="hljs-built_in">String</span>, expected_format: <span class="hljs-built_in">String</span> },
    SubcategoryNotFound { name: <span class="hljs-built_in">String</span> },
    SubcategoryAlreadyExists { name: <span class="hljs-built_in">String</span> },
    RecordNotFound { id: <span class="hljs-built_in">usize</span> },
    SubcategoryHasRecords { name: <span class="hljs-built_in">String</span>, count: <span class="hljs-built_in">usize</span> },
    CannotDeleteMiscellaneous,
    CategoryImmutable { category: <span class="hljs-built_in">usize</span> },
    InvalidCategoryName { name: <span class="hljs-built_in">String</span>, reason: <span class="hljs-built_in">String</span> },
    InvalidName { name: <span class="hljs-built_in">String</span>, reason: <span class="hljs-built_in">String</span> },
    InvalidAmount { reason: <span class="hljs-built_in">String</span> },
    TrackerAlreadyInitialized,
    InvalidSubcommand { subcommand: <span class="hljs-built_in">String</span> },
}

<span class="hljs-meta">#[derive(Debug)]</span>
<span class="hljs-keyword">pub</span> <span class="hljs-class"><span class="hljs-keyword">enum</span> <span class="hljs-title">CliError</span></span> {
    FileNotFound(<span class="hljs-built_in">String</span>),
    InvalidJson(<span class="hljs-built_in">String</span>),
    ValidationError(ValidationErrorKind),
    PermissionDenied(<span class="hljs-built_in">String</span>),
    CorruptedData { backup_restored: <span class="hljs-built_in">bool</span>, timestamp: <span class="hljs-built_in">String</span> },
    FileAlreadyExists,
    Other(<span class="hljs-built_in">String</span>),
}
</code></pre>
<p>This nested structure allows you to categorize every possible failure that can occur during the execution of your program. The CliError enum acts as the top-level container for all errors in the application. It handles errors like missing files, denied permissions, validation errors, file existence conflicts, and so on.</p>
<p>One specific variant, <code>ValidationError</code>, carries a <code>ValidationErrorKind</code> as its payload. This allows you to group all validation-specific failures (such as invalid date formats, duplicate subcategory names, or attempts to delete protected system categories) under a single error type while still preserving the specific details of what went wrong.</p>
<p>Structuring your errors this way allows you to report exactly what caused a failure alongside the specific data that triggered it. For example, a validation error can include the exact amount or date that failed your rules, while a system error can pinpoint the specific file path or permission issue that stopped the program.</p>
<h3 id="heading-map-system-errors-to-custom-errors">Map System Errors to Custom Errors</h3>
<p>To keep your application code clean, you can use the <code>From</code> trait to automatically convert low-level system errors into your custom <code>CliError</code>. This allows you to use the <code>?</code> operator later in your logic to propagate errors gracefully.</p>
<p>Add these implementations to <code>src/error.rs</code>:</p>
<pre><code class="lang-rust"><span class="hljs-keyword">impl</span> <span class="hljs-built_in">From</span>&lt;std::io::Error&gt; <span class="hljs-keyword">for</span> CliError {
    <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">from</span></span>(err: std::io::Error) -&gt; <span class="hljs-keyword">Self</span> {
        <span class="hljs-keyword">match</span> err.kind() {
            std::io::ErrorKind::NotFound =&gt; CliError::FileNotFound(err.to_string()),
            std::io::ErrorKind::PermissionDenied =&gt; CliError::PermissionDenied(err.to_string()),
            std::io::ErrorKind::AlreadyExists =&gt; CliError::FileAlreadyExists,
            <span class="hljs-comment">// ... add more here as is required.</span>
            _ =&gt; CliError::Other(<span class="hljs-built_in">format!</span>(<span class="hljs-string">"IO error: {}"</span>, err)),
        }
    }
}

<span class="hljs-keyword">impl</span> <span class="hljs-built_in">From</span>&lt;serde_json::Error&gt; <span class="hljs-keyword">for</span> CliError {
    <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">from</span></span>(err: serde_json::Error) -&gt; <span class="hljs-keyword">Self</span> {
        CliError::InvalidJson(err.to_string())
    }
}
</code></pre>
<p>The <code>match</code> block inside the <code>std::io::Error</code> implementation allows you to inspect the system error and categorize it correctly. If the system reports a "NotFound" error, your application transforms it into a <code>CliError::FileNotFound</code>. This ensures that your user-facing messages remain consistent.</p>
<h3 id="heading-prepare-for-error-output">Prepare for Error Output</h3>
<p>Finally, add a method signature to the <code>CliError</code> block. This will later connect your error logic to a dedicated output module that formats these errors for the terminal:</p>
<pre><code class="lang-rust"><span class="hljs-keyword">impl</span> CliError {
    <span class="hljs-keyword">pub</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">write_to</span></span>(&amp;<span class="hljs-keyword">self</span>, writer: &amp;<span class="hljs-keyword">mut</span> <span class="hljs-keyword">impl</span> std::io::Write) -&gt; io::<span class="hljs-built_in">Result</span>&lt;()&gt; {
        crate::output::write_error(<span class="hljs-keyword">self</span>, writer)
    }
}
</code></pre>
<p>The <code>&amp;mut impl std::io::Write</code> parameter is a flexible way to say this method can write to any output stream, whether it’s the standard error stream in the terminal or a log file.</p>
<p>Register the error module in your <code>src/lib.rs</code> file so it’s available to the rest of your project:</p>
<pre><code class="lang-rust"><span class="hljs-keyword">pub</span> <span class="hljs-keyword">mod</span> models;
<span class="hljs-keyword">pub</span> <span class="hljs-keyword">mod</span> error;
</code></pre>
<h2 id="heading-step-4-create-file-operations">Step 4: Create File Operations</h2>
<p>To manage your tracker data, you need a reliable way to read and write JSON files. Instead of repeating file logic in every command, you’ll create a trait. In Rust, traits allow you to add new methods to existing types. Here, you’ll add custom file-handling methods directly to <code>Path</code> and <code>PathBuf</code>.</p>
<p>First, create a new directory named <code>src/utils</code> and create a file inside it called <code>src/utils/file.rs</code>. Start with the necessary imports:</p>
<pre><code class="lang-rust"><span class="hljs-keyword">use</span> std::{
  fs::{<span class="hljs-keyword">self</span>, File},
  io::{<span class="hljs-keyword">self</span>, prelude::*},
  path::Path,
};

<span class="hljs-keyword">use</span> serde_json::Value;

<span class="hljs-keyword">use</span> crate::CliError;
</code></pre>
<p>Now, define and implement the FilePath trait:</p>
<pre><code class="lang-rust"><span class="hljs-keyword">pub</span> <span class="hljs-class"><span class="hljs-keyword">trait</span> <span class="hljs-title">FilePath</span></span>: <span class="hljs-built_in">AsRef</span>&lt;Path&gt; {
    <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">create_file_if_not_exists</span></span>(&amp;<span class="hljs-keyword">self</span>) -&gt; io::<span class="hljs-built_in">Result</span>&lt;File&gt; {
        <span class="hljs-keyword">let</span> path = <span class="hljs-keyword">self</span>.as_ref();
        <span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> <span class="hljs-literal">Some</span>(parent) = path.parent() {
            fs::create_dir_all(parent)?;
        }
        File::options().write(<span class="hljs-literal">true</span>).create_new(<span class="hljs-literal">true</span>).open(path)
    }

    <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">read_file</span></span>(&amp;<span class="hljs-keyword">self</span>) -&gt; io::<span class="hljs-built_in">Result</span>&lt;File&gt; {
        File::options().read(<span class="hljs-literal">true</span>).open(<span class="hljs-keyword">self</span>.as_ref())
    }

    <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">open_read_write</span></span>(&amp;<span class="hljs-keyword">self</span>) -&gt; io::<span class="hljs-built_in">Result</span>&lt;File&gt; {
        File::options().read(<span class="hljs-literal">true</span>).write(<span class="hljs-literal">true</span>).open(<span class="hljs-keyword">self</span>.as_ref())
    }

    <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">open_read</span></span>(&amp;<span class="hljs-keyword">self</span>) -&gt; io::<span class="hljs-built_in">Result</span>&lt;File&gt; {
        File::options().read(<span class="hljs-literal">true</span>).open(<span class="hljs-keyword">self</span>.as_ref())
    }

    <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">delete_if_exists</span></span>(&amp;<span class="hljs-keyword">self</span>) -&gt; io::<span class="hljs-built_in">Result</span>&lt;()&gt; {
        <span class="hljs-keyword">let</span> path = <span class="hljs-keyword">self</span>.as_ref();
        <span class="hljs-keyword">if</span> !path.exists() {
            <span class="hljs-keyword">return</span> <span class="hljs-literal">Ok</span>(());
        }
        <span class="hljs-keyword">if</span> path.is_dir() {
            fs::remove_dir_all(path)?;
        } <span class="hljs-keyword">else</span> {
            fs::remove_file(path)?;
        }
        <span class="hljs-literal">Ok</span>(())
    }
}

<span class="hljs-keyword">impl</span>&lt;P: <span class="hljs-built_in">AsRef</span>&lt;Path&gt;&gt; FilePath <span class="hljs-keyword">for</span> P {}
</code></pre>
<p>This "blanket implementation" at the end is powerful. It ensures that any type capable of representing a file path, like a <code>PathBuf</code> or a standard <code>String</code>, automatically gains these methods.</p>
<p>Throughout these methods, you use the <code>?</code> operator. This is Rust’s shorthand for error propagation. If an operation like <code>create_dir_all fails</code>, the ? immediately returns the error from the function. If it succeeds, the program continues to the next line. This keeps your logic flat and readable without nested error checks.</p>
<h3 id="heading-add-json-utility-functions">Add JSON Utility Functions</h3>
<p>Writing financial data to a file requires precision. You must ensure that you are completely overwriting the old data rather than just appending to it. Add this helper function to <code>src/utils/file.rs</code>:</p>
<pre><code class="lang-rust"><span class="hljs-keyword">pub</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">write_json_to_file</span></span>(json: &amp;Value, file: &amp;<span class="hljs-keyword">mut</span> File) -&gt; <span class="hljs-built_in">Result</span>&lt;(), CliError&gt; {
    <span class="hljs-keyword">let</span> json_string = serde_json::to_string_pretty(&amp;json)?;

    file.seek(io::SeekFrom::Start(<span class="hljs-number">0</span>))?;
    file.set_len(<span class="hljs-number">0</span>)?;
    file.write_all(json_string.as_bytes())?;

    <span class="hljs-literal">Ok</span>(())
}
</code></pre>
<p>The <code>seek</code> call moves the file pointer back to the very beginning, and <code>set_len(0)</code> truncates the file to zero bytes. Using <code>to_string_pretty</code> ensures your JSON file is human-readable, which fits the local-first goal of keeping your data accessible.</p>
<h3 id="heading-register-the-utilities">Register the Utilities</h3>
<p>To make these tools available to the rest of your application, you need to set up the module tree. Create <code>src/utils.rs</code> and add this line:</p>
<pre><code class="lang-rust"><span class="hljs-keyword">pub</span> <span class="hljs-keyword">mod</span> file;
</code></pre>
<p>Then, update your <code>src/lib.rs</code> file to include the new utils module and export the types you've built so far:</p>
<pre><code class="lang-rust"><span class="hljs-keyword">pub</span> <span class="hljs-keyword">mod</span> models;
<span class="hljs-keyword">pub</span> <span class="hljs-keyword">mod</span> error;
<span class="hljs-keyword">pub</span> <span class="hljs-keyword">mod</span> utils;

<span class="hljs-keyword">pub</span> <span class="hljs-keyword">use</span> error::{CliError, ValidationErrorKind};
<span class="hljs-keyword">pub</span> <span class="hljs-keyword">use</span> models::{Category, Currency, Record, TrackerData};
</code></pre>
<h2 id="heading-step-5-set-up-the-cli-structure">Step 5: Set Up the CLI Structure</h2>
<p>In this step, you will organize the interface that allows users to interact with your code. Building a CLI is more than just reading strings. It involves mapping specific terminal commands to the internal logic of your application.</p>
<h3 id="heading-the-command-architecture">The Command Architecture</h3>
<p>You’ll follow a modular pattern where each command has its own definition and execution logic. This separation ensures that adding a new feature in the future doesn’t break your existing commands.</p>
<p>Create a file named src/commands.rs. This file acts as a central dispatcher that declares your command modules and routes terminal input to the correct function:</p>
<pre><code class="lang-rust"><span class="hljs-keyword">use</span> crate::{CliResult, command_prelude::*};
<span class="hljs-keyword">use</span> clap::{ArgMatches, Command};

<span class="hljs-keyword">pub</span> <span class="hljs-class"><span class="hljs-keyword">type</span> <span class="hljs-title">Exec</span></span> = <span class="hljs-function"><span class="hljs-keyword">fn</span></span>(&amp;<span class="hljs-keyword">mut</span> GlobalContext, &amp;ArgMatches) -&gt; CliResult;

<span class="hljs-keyword">pub</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">cli</span></span>() -&gt; <span class="hljs-built_in">Vec</span>&lt;Command&gt; {
    <span class="hljs-built_in">vec!</span>[
        init::cli(),
        add::cli(),
        list::cli(),
        update::cli(),
        delete::cli(),
        subcategory::cli(),
        total::cli(),
    ]
}

<span class="hljs-keyword">pub</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">build_exec</span></span>(cmd: &amp;<span class="hljs-built_in">str</span>) -&gt; <span class="hljs-built_in">Option</span>&lt;Exec&gt; {
    <span class="hljs-keyword">match</span> cmd {
        <span class="hljs-string">"init"</span> =&gt; <span class="hljs-literal">Some</span>(init::exec),
        <span class="hljs-string">"add"</span> =&gt; <span class="hljs-literal">Some</span>(add::exec),
        <span class="hljs-string">"list"</span> =&gt; <span class="hljs-literal">Some</span>(list::exec),
        <span class="hljs-string">"update"</span> =&gt; <span class="hljs-literal">Some</span>(update::exec),
        <span class="hljs-string">"delete"</span> =&gt; <span class="hljs-literal">Some</span>(delete::exec),
        <span class="hljs-string">"subcategory"</span> =&gt; <span class="hljs-literal">Some</span>(subcategory::exec),
        <span class="hljs-string">"total"</span> =&gt; <span class="hljs-literal">Some</span>(total::exec),
        _ =&gt; <span class="hljs-literal">None</span>,
    }
}

<span class="hljs-keyword">pub</span> <span class="hljs-keyword">mod</span> init;
<span class="hljs-keyword">pub</span> <span class="hljs-keyword">mod</span> add;
<span class="hljs-keyword">pub</span> <span class="hljs-keyword">mod</span> list;
<span class="hljs-keyword">pub</span> <span class="hljs-keyword">mod</span> update;
<span class="hljs-keyword">pub</span> <span class="hljs-keyword">mod</span> delete;
<span class="hljs-keyword">pub</span> <span class="hljs-keyword">mod</span> subcategory;
<span class="hljs-keyword">pub</span> <span class="hljs-keyword">mod</span> total;
</code></pre>
<p>The <code>Exec</code> type alias defines a standard signature for all your command functions. Every command will receive the global context and the arguments parsed by clap, and every command will return a CliResult.</p>
<p>The <code>Exec</code> type alias defines a standard signature for all your command functions. Every command will receive the global context and the arguments parsed by <code>clap</code>. The <code>build_exec</code> function then uses pattern matching to return the specific execution logic associated with the user's input.</p>
<h3 id="heading-manage-paths-with-global-context">Manage Paths with Global Context</h3>
<p>Since your application is local-first, it needs to know exactly where to find the data directory on different operating systems. You will create a <code>GlobalContext</code> struct to centralize these paths so you don’t have to rebuild them manually in every command module.</p>
<p>Now create <code>src/utils/context.rs</code> for managing file paths:</p>
<pre><code class="lang-rust"><span class="hljs-keyword">use</span> std::path::PathBuf;

<span class="hljs-meta">#[derive(Debug)]</span>
<span class="hljs-keyword">pub</span> <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">GlobalContext</span></span> {
    home_path: PathBuf,
    base_path: PathBuf,
    tracker_path: PathBuf,
}

<span class="hljs-keyword">impl</span> GlobalContext {
    <span class="hljs-keyword">pub</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">new</span></span>(home_dir: PathBuf) -&gt; <span class="hljs-keyword">Self</span> {
        <span class="hljs-keyword">let</span> base_path = home_dir.join(<span class="hljs-string">".fintrack"</span>);
        <span class="hljs-keyword">let</span> tracker_path = base_path.join(<span class="hljs-string">"tracker.json"</span>);

        GlobalContext {
            home_path: home_dir,
            base_path,
            tracker_path,
        }
    }

    <span class="hljs-keyword">pub</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">tracker_path</span></span>(&amp;<span class="hljs-keyword">self</span>) -&gt; &amp;PathBuf {
        &amp;<span class="hljs-keyword">self</span>.tracker_path
    }

    <span class="hljs-keyword">pub</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">home_path</span></span>(&amp;<span class="hljs-keyword">self</span>) -&gt; &amp;PathBuf {
        &amp;<span class="hljs-keyword">self</span>.home_path
    }

    <span class="hljs-keyword">pub</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">base_path</span></span>(&amp;<span class="hljs-keyword">self</span>) -&gt; &amp;PathBuf {
        &amp;<span class="hljs-keyword">self</span>.base_path
    }
}
</code></pre>
<p>The <code>join()</code> method is a cross-platform way to combine paths. It automatically uses the correct separator for your operating system, such as a backslash on Windows or a forward slash on Linux.</p>
<h3 id="heading-register-the-command-system">Register the Command System</h3>
<p>To tie these components together, update your utility and library files. In <code>src/utils.rs</code>, add the context module:</p>
<pre><code class="lang-rust"><span class="hljs-keyword">pub</span> <span class="hljs-keyword">mod</span> file;
<span class="hljs-keyword">pub</span> <span class="hljs-keyword">mod</span> context;
</code></pre>
<p>Finally, update <code>src/lib.rs</code> to expose the command structures and the new context type. You’ll also define a <code>CliResult</code> type alias to keep your function signatures consistent throughout the project:</p>
<pre><code class="lang-rust"><span class="hljs-keyword">pub</span> <span class="hljs-keyword">mod</span> models;
<span class="hljs-keyword">pub</span> <span class="hljs-keyword">mod</span> error;
<span class="hljs-keyword">pub</span> <span class="hljs-keyword">mod</span> utils;
<span class="hljs-keyword">pub</span> <span class="hljs-keyword">mod</span> commands;

<span class="hljs-keyword">pub</span> <span class="hljs-keyword">use</span> error::*;
<span class="hljs-keyword">pub</span> <span class="hljs-keyword">use</span> models::*;
<span class="hljs-keyword">pub</span> <span class="hljs-keyword">use</span> utils::command_prelude;
<span class="hljs-keyword">pub</span> <span class="hljs-keyword">use</span> utils::context::GlobalContext;
<span class="hljs-keyword">pub</span> <span class="hljs-keyword">use</span> utils::parsers;
</code></pre>
<p>By defining the result type here, you ensure that every command follows the same error-handling and response rules you established in previous steps.</p>
<h2 id="heading-step-6-create-response-types">Step 6: Create Response Types</h2>
<p>Commands in your tracker do more than just execute logic. They return data that must be formatted and displayed to the user.</p>
<p>In a command-line tool, your "user interface" is the text printed to the terminal, so you need a structured way to handle various results. You’ll create a <code>ResponseContent</code> enum to categorize these different outputs, such as single records, transaction lists, or financial totals. This ensures that your application communicates both successful results and informative error messages clearly.</p>
<h3 id="heading-define-the-response-structures">Define the Response Structures</h3>
<p>Open your <code>src/models.rs</code> file and add these structures to manage how the application packages its data:</p>
<p>Add to <code>src/models.rs</code>:</p>
<pre><code class="lang-rust"><span class="hljs-meta">#[derive(Debug)]</span>
<span class="hljs-keyword">pub</span> <span class="hljs-class"><span class="hljs-keyword">enum</span> <span class="hljs-title">ResponseContent</span></span> {
    Message(<span class="hljs-built_in">String</span>),
    Record {
        record: Record,
        tracker_data: TrackerData,
        is_update: <span class="hljs-built_in">bool</span>,
    },
    List {
        records: <span class="hljs-built_in">Vec</span>&lt;Record&gt;,
        tracker_data: TrackerData,
    },
    TrackerData(TrackerData),
    Total(Total),
    Categories(<span class="hljs-built_in">Vec</span>&lt;(<span class="hljs-built_in">usize</span>, <span class="hljs-built_in">String</span>)&gt;),
    Subcategories(<span class="hljs-built_in">Vec</span>&lt;(<span class="hljs-built_in">usize</span>, <span class="hljs-built_in">String</span>)&gt;),
}

<span class="hljs-meta">#[derive(Debug, Clone)]</span>
<span class="hljs-keyword">pub</span> <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">Total</span></span> {
    <span class="hljs-keyword">pub</span> currency: Currency,
    <span class="hljs-keyword">pub</span> opening_balance: <span class="hljs-built_in">f64</span>,
    <span class="hljs-keyword">pub</span> income_total: <span class="hljs-built_in">f64</span>,
    <span class="hljs-keyword">pub</span> expenses_total: <span class="hljs-built_in">f64</span>,
}

<span class="hljs-meta">#[derive(Debug)]</span>
<span class="hljs-keyword">pub</span> <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">CliResponse</span></span> {
    content: <span class="hljs-built_in">Option</span>&lt;ResponseContent&gt;,
}

<span class="hljs-keyword">impl</span> CliResponse {
    <span class="hljs-keyword">pub</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">new</span></span>(content: ResponseContent) -&gt; <span class="hljs-keyword">Self</span> {
        CliResponse {
            content: <span class="hljs-literal">Some</span>(content),
        }
    }

    <span class="hljs-keyword">pub</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">success</span></span>() -&gt; <span class="hljs-keyword">Self</span> {
        CliResponse { content: <span class="hljs-literal">None</span> }
    }

    <span class="hljs-keyword">pub</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">content</span></span>(&amp;<span class="hljs-keyword">self</span>) -&gt; <span class="hljs-built_in">Option</span>&lt;&amp;ResponseContent&gt; {
        <span class="hljs-keyword">self</span>.content.as_ref()
    }

    <span class="hljs-keyword">pub</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">write_to</span></span>(&amp;<span class="hljs-keyword">self</span>, writer: &amp;<span class="hljs-keyword">mut</span> <span class="hljs-keyword">impl</span> std::io::Write) -&gt; std::io::<span class="hljs-built_in">Result</span>&lt;()&gt; {
        crate::output::write_response(<span class="hljs-keyword">self</span>, writer)
    }
}

<span class="hljs-keyword">pub</span> <span class="hljs-class"><span class="hljs-keyword">type</span> <span class="hljs-title">CliResult</span></span> = <span class="hljs-built_in">Result</span>&lt;CliResponse, CliError&gt;;
</code></pre>
<p>The <code>CliResponse</code> struct acts as a container for your output. By using an <code>Option&lt;ResponseContent&gt;</code>, you can represent a simple success message when the content is <code>None</code>, or provide more complex data like a <code>Total</code> struct when needed. This approach keeps your command logic consistent because every operation will return the same response type.</p>
<h3 id="heading-implement-the-output-module">Implement the Output Module</h3>
<p>Next, you need a central place to turn these Rust types into formatted text for the terminal. Create a new file named <code>src/output.rs</code>. This module will handle the printing logic for both successful responses and the errors you defined earlier.</p>
<pre><code class="lang-rust"><span class="hljs-keyword">use</span> crate::{CliError, CliResponse, ResponseContent};

<span class="hljs-keyword">pub</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">write_response</span></span>(res: &amp;CliResponse, writer: &amp;<span class="hljs-keyword">mut</span> <span class="hljs-keyword">impl</span> std::io::Write) -&gt; std::io::<span class="hljs-built_in">Result</span>&lt;()&gt; {
    <span class="hljs-keyword">let</span> <span class="hljs-literal">Some</span>(content) = res.content() <span class="hljs-keyword">else</span> {
        <span class="hljs-built_in">writeln!</span>(writer, <span class="hljs-string">"✓ Success"</span>)?;
        <span class="hljs-keyword">return</span> <span class="hljs-literal">Ok</span>(());
    };

    <span class="hljs-keyword">match</span> content {
        ResponseContent::Message(msg) =&gt; {
            <span class="hljs-built_in">writeln!</span>(writer, <span class="hljs-string">"✓ {}"</span>, msg)?;
        }
        ResponseContent::Record { record, .. } =&gt; {
            <span class="hljs-built_in">writeln!</span>(writer, <span class="hljs-string">"✓ Record created:"</span>)?;
            <span class="hljs-built_in">writeln!</span>(writer, <span class="hljs-string">"  ID: {}"</span>, record.id)?;
            <span class="hljs-built_in">writeln!</span>(writer, <span class="hljs-string">"  Amount: {}"</span>, record.amount)?;
            <span class="hljs-comment">// More formatting later</span>
        }
        ResponseContent::List { records, .. } =&gt; {
            <span class="hljs-keyword">for</span> record <span class="hljs-keyword">in</span> records {
                <span class="hljs-built_in">writeln!</span>(writer, <span class="hljs-string">"{:?}"</span>, record)?;
            }
        }
        ResponseContent::Total(total) =&gt; {
            <span class="hljs-built_in">writeln!</span>(writer, <span class="hljs-string">"Opening Balance: {} {}"</span>, total.opening_balance, total.currency)?;
            <span class="hljs-built_in">writeln!</span>(writer, <span class="hljs-string">"Total Income: {} {}"</span>, total.income_total, total.currency)?;
            <span class="hljs-built_in">writeln!</span>(writer, <span class="hljs-string">"Total Expenses: {} {}"</span>, total.expenses_total, total.currency)?;
            <span class="hljs-keyword">let</span> net_balance = total.opening_balance + total.income_total - total.expenses_total;
            <span class="hljs-built_in">writeln!</span>(writer, <span class="hljs-string">"Net Balance: {} {}"</span>, net_balance, total.currency)?;
        }
        _ =&gt; {}
    }
    <span class="hljs-literal">Ok</span>(())
}

<span class="hljs-keyword">pub</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">write_error</span></span>(err: &amp;CliError, writer: &amp;<span class="hljs-keyword">mut</span> <span class="hljs-keyword">impl</span> std::io::Write) -&gt; std::io::<span class="hljs-built_in">Result</span>&lt;()&gt; {
    <span class="hljs-keyword">match</span> err {
        CliError::FileNotFound(msg) =&gt; <span class="hljs-built_in">writeln!</span>(writer, <span class="hljs-string">"Error: File not found: {}"</span>, msg),
        CliError::InvalidJson(msg) =&gt; <span class="hljs-built_in">writeln!</span>(writer, <span class="hljs-string">"Error: Invalid JSON: {}"</span>, msg),
        CliError::ValidationError(kind) =&gt; {
            <span class="hljs-keyword">match</span> kind {
                crate::ValidationErrorKind::AmountTooSmall { amount } =&gt; {
                    <span class="hljs-built_in">writeln!</span>(writer, <span class="hljs-string">"Error: Amount must be greater than 0, got {}"</span>, amount)
                }
                crate::ValidationErrorKind::SubcategoryNotFound { name } =&gt; {
                    <span class="hljs-built_in">writeln!</span>(writer, <span class="hljs-string">"Error: Subcategory '{}' not found"</span>, name)
                }
                crate::ValidationErrorKind::RecordNotFound { id } =&gt; {
                    <span class="hljs-built_in">writeln!</span>(writer, <span class="hljs-string">"Error: Record with ID {} not found"</span>, id)
                }
                _ =&gt; <span class="hljs-built_in">writeln!</span>(writer, <span class="hljs-string">"Error: Validation failed"</span>),
            }
        }
        CliError::FileAlreadyExists =&gt; {
            <span class="hljs-built_in">writeln!</span>(writer, <span class="hljs-string">"Error: Tracker already initialized. Use 'fintrack clear' to start fresh."</span>)
        }
        _ =&gt; <span class="hljs-built_in">writeln!</span>(writer, <span class="hljs-string">"Error: {}"</span>, err),
    }
}
</code></pre>
<p>By centralizing the output logic in this module, you fulfill the goal of reporting the exact data that caused a failure alongside the error message itself. If a user enters an invalid amount, the error output clearly identifies the problematic value.</p>
<h3 id="heading-update-the-library-registration">Update the Library Registration</h3>
<p>To finalize this step, register the output module and export the new response types in your <code>src/lib.rs</code> file:</p>
<pre><code class="lang-rust"><span class="hljs-keyword">pub</span> <span class="hljs-keyword">mod</span> commands;
<span class="hljs-keyword">pub</span> <span class="hljs-keyword">mod</span> error;
<span class="hljs-keyword">pub</span> <span class="hljs-keyword">mod</span> models;
<span class="hljs-keyword">pub</span> <span class="hljs-keyword">mod</span> output;
<span class="hljs-keyword">pub</span> <span class="hljs-keyword">mod</span> utils;

<span class="hljs-keyword">pub</span> <span class="hljs-keyword">use</span> error::*;
<span class="hljs-keyword">pub</span> <span class="hljs-keyword">use</span> models::*;
<span class="hljs-keyword">pub</span> <span class="hljs-keyword">use</span> utils::command_prelude;
<span class="hljs-keyword">pub</span> <span class="hljs-keyword">use</span> utils::context::GlobalContext;
<span class="hljs-keyword">pub</span> <span class="hljs-keyword">use</span> utils::parsers;
</code></pre>
<h2 id="heading-step-7-create-argument-parsing-helpers">Step 7: Create Argument Parsing Helpers</h2>
<p>Extracting specific values like transaction amounts, dates, or categories from raw command-line input can quickly lead to repetitive code. While <code>clap</code> provides the basic parsing, you need a streamlined way to convert those inputs into the specific types used by your tracker. By creating a custom trait to extend <code>clap</code>, you handle type conversion and error reporting in a single, consistent place.</p>
<p>Create a new file named <code>src/utils/cli.rs</code> and add the following implementation:</p>
<pre><code class="lang-rust"><span class="hljs-keyword">use</span> chrono::NaiveDate;
<span class="hljs-keyword">use</span> clap::ArgMatches;
<span class="hljs-keyword">use</span> crate::{Category, CliError, Currency};

<span class="hljs-keyword">const</span> DEFAULT_F64: <span class="hljs-built_in">f64</span> = <span class="hljs-number">0.0</span>;
<span class="hljs-keyword">const</span> DEFAULT_USIZE: <span class="hljs-built_in">usize</span> = <span class="hljs-number">0</span>;
<span class="hljs-keyword">const</span> DEFAULT_SUBCATEGORY: &amp;<span class="hljs-built_in">str</span> = <span class="hljs-string">"miscellaneous"</span>;

<span class="hljs-keyword">pub</span> <span class="hljs-class"><span class="hljs-keyword">trait</span> <span class="hljs-title">ArgMatchesExt</span></span> {
    <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">get_category</span></span>(&amp;<span class="hljs-keyword">self</span>, id: &amp;<span class="hljs-built_in">str</span>) -&gt; <span class="hljs-built_in">Result</span>&lt;&amp;Category, CliError&gt;;
    <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">get_usize</span></span>(&amp;<span class="hljs-keyword">self</span>, id: &amp;<span class="hljs-built_in">str</span>) -&gt; <span class="hljs-built_in">Result</span>&lt;<span class="hljs-built_in">usize</span>, CliError&gt;;
    <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">get_category_opt</span></span>(&amp;<span class="hljs-keyword">self</span>, id: &amp;<span class="hljs-built_in">str</span>) -&gt; <span class="hljs-built_in">Option</span>&lt;&amp;Category&gt;;
    <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">get_f64_opt</span></span>(&amp;<span class="hljs-keyword">self</span>, id: &amp;<span class="hljs-built_in">str</span>) -&gt; <span class="hljs-built_in">Option</span>&lt;<span class="hljs-built_in">f64</span>&gt;;
    <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">get_usize_opt</span></span>(&amp;<span class="hljs-keyword">self</span>, id: &amp;<span class="hljs-built_in">str</span>) -&gt; <span class="hljs-built_in">Option</span>&lt;<span class="hljs-built_in">usize</span>&gt;;
    <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">get_string_opt</span></span>(&amp;<span class="hljs-keyword">self</span>, id: &amp;<span class="hljs-built_in">str</span>) -&gt; <span class="hljs-built_in">Option</span>&lt;<span class="hljs-built_in">String</span>&gt;;
    <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">get_subcategory_opt</span></span>(&amp;<span class="hljs-keyword">self</span>, id: &amp;<span class="hljs-built_in">str</span>) -&gt; <span class="hljs-built_in">Option</span>&lt;<span class="hljs-built_in">String</span>&gt;;
    <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">get_date_opt</span></span>(&amp;<span class="hljs-keyword">self</span>, id: &amp;<span class="hljs-built_in">str</span>) -&gt; <span class="hljs-built_in">Option</span>&lt;NaiveDate&gt;;
    <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">get_currency_opt</span></span>(&amp;<span class="hljs-keyword">self</span>, id: &amp;<span class="hljs-built_in">str</span>) -&gt; <span class="hljs-built_in">Option</span>&lt;&amp;Currency&gt;;
    <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">get_f64_or_default</span></span>(&amp;<span class="hljs-keyword">self</span>, id: &amp;<span class="hljs-built_in">str</span>) -&gt; <span class="hljs-built_in">f64</span>;
    <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">get_usize_or_default</span></span>(&amp;<span class="hljs-keyword">self</span>, id: &amp;<span class="hljs-built_in">str</span>) -&gt; <span class="hljs-built_in">usize</span>;
    <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">get_string_or_default</span></span>(&amp;<span class="hljs-keyword">self</span>, id: &amp;<span class="hljs-built_in">str</span>) -&gt; <span class="hljs-built_in">String</span>;
    <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">get_subcategory_or_default</span></span>(&amp;<span class="hljs-keyword">self</span>, id: &amp;<span class="hljs-built_in">str</span>) -&gt; <span class="hljs-built_in">String</span>;
    <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">get_currency_or_default</span></span>(&amp;<span class="hljs-keyword">self</span>, id: &amp;<span class="hljs-built_in">str</span>) -&gt; &amp;Currency;
    <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">get_vec</span></span>&lt;T: <span class="hljs-built_in">Clone</span> + <span class="hljs-built_in">Send</span> + <span class="hljs-built_in">Sync</span> + <span class="hljs-symbol">'static</span>&gt;(&amp;<span class="hljs-keyword">self</span>, id: &amp;<span class="hljs-built_in">str</span>) -&gt; <span class="hljs-built_in">Vec</span>&lt;T&gt;;
    <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">contains_id</span></span>(&amp;<span class="hljs-keyword">self</span>, id: &amp;<span class="hljs-built_in">str</span>) -&gt; <span class="hljs-built_in">bool</span>;
}

<span class="hljs-keyword">impl</span> ArgMatchesExt <span class="hljs-keyword">for</span> ArgMatches {
    <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">get_category</span></span>(&amp;<span class="hljs-keyword">self</span>, id: &amp;<span class="hljs-built_in">str</span>) -&gt; <span class="hljs-built_in">Result</span>&lt;&amp;Category, CliError&gt; {
        <span class="hljs-keyword">self</span>.get_one::&lt;Category&gt;(id).ok_or_else(|| {
            CliError::ValidationError(crate::ValidationErrorKind::InvalidCategoryName {
                name: id.to_string(),
                reason: <span class="hljs-string">"Category not provided"</span>.to_string(),
            })
        })
    }

    <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">get_usize</span></span>(&amp;<span class="hljs-keyword">self</span>, id: &amp;<span class="hljs-built_in">str</span>) -&gt; <span class="hljs-built_in">Result</span>&lt;<span class="hljs-built_in">usize</span>, CliError&gt; {
        <span class="hljs-keyword">self</span>.get_one::&lt;<span class="hljs-built_in">usize</span>&gt;(id).copied().ok_or_else(|| {
            CliError::Other(<span class="hljs-built_in">format!</span>(<span class="hljs-string">"Required argument '{}' not provided"</span>, id))
        })
    }

    <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">get_category_opt</span></span>(&amp;<span class="hljs-keyword">self</span>, id: &amp;<span class="hljs-built_in">str</span>) -&gt; <span class="hljs-built_in">Option</span>&lt;&amp;Category&gt; {
        <span class="hljs-keyword">self</span>.get_one::&lt;Category&gt;(id)
    }

    <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">get_f64_opt</span></span>(&amp;<span class="hljs-keyword">self</span>, id: &amp;<span class="hljs-built_in">str</span>) -&gt; <span class="hljs-built_in">Option</span>&lt;<span class="hljs-built_in">f64</span>&gt; {
        <span class="hljs-keyword">self</span>.get_one::&lt;<span class="hljs-built_in">f64</span>&gt;(id).copied()
    }

    <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">get_usize_opt</span></span>(&amp;<span class="hljs-keyword">self</span>, id: &amp;<span class="hljs-built_in">str</span>) -&gt; <span class="hljs-built_in">Option</span>&lt;<span class="hljs-built_in">usize</span>&gt; {
        <span class="hljs-keyword">self</span>.get_one::&lt;<span class="hljs-built_in">usize</span>&gt;(id).copied()
    }

    <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">get_string_opt</span></span>(&amp;<span class="hljs-keyword">self</span>, id: &amp;<span class="hljs-built_in">str</span>) -&gt; <span class="hljs-built_in">Option</span>&lt;<span class="hljs-built_in">String</span>&gt; {
        <span class="hljs-keyword">self</span>.get_one::&lt;<span class="hljs-built_in">String</span>&gt;(id).cloned()
    }

    <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">get_subcategory_opt</span></span>(&amp;<span class="hljs-keyword">self</span>, id: &amp;<span class="hljs-built_in">str</span>) -&gt; <span class="hljs-built_in">Option</span>&lt;<span class="hljs-built_in">String</span>&gt; {
        <span class="hljs-keyword">self</span>.get_one::&lt;<span class="hljs-built_in">String</span>&gt;(id).cloned()
    }

    <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">get_date_opt</span></span>(&amp;<span class="hljs-keyword">self</span>, id: &amp;<span class="hljs-built_in">str</span>) -&gt; <span class="hljs-built_in">Option</span>&lt;NaiveDate&gt; {
        <span class="hljs-keyword">self</span>.get_one::&lt;NaiveDate&gt;(id).copied()
    }

    <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">get_currency_opt</span></span>(&amp;<span class="hljs-keyword">self</span>, id: &amp;<span class="hljs-built_in">str</span>) -&gt; <span class="hljs-built_in">Option</span>&lt;&amp;Currency&gt; {
        <span class="hljs-keyword">self</span>.get_one::&lt;Currency&gt;(id)
    }

    <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">get_f64_or_default</span></span>(&amp;<span class="hljs-keyword">self</span>, id: &amp;<span class="hljs-built_in">str</span>) -&gt; <span class="hljs-built_in">f64</span> {
        <span class="hljs-keyword">self</span>.get_one::&lt;<span class="hljs-built_in">f64</span>&gt;(id).copied().unwrap_or(DEFAULT_F64)
    }

    <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">get_usize_or_default</span></span>(&amp;<span class="hljs-keyword">self</span>, id: &amp;<span class="hljs-built_in">str</span>) -&gt; <span class="hljs-built_in">usize</span> {
        <span class="hljs-keyword">self</span>.get_one::&lt;<span class="hljs-built_in">usize</span>&gt;(id).copied().unwrap_or(DEFAULT_USIZE)
    }

    <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">get_string_or_default</span></span>(&amp;<span class="hljs-keyword">self</span>, id: &amp;<span class="hljs-built_in">str</span>) -&gt; <span class="hljs-built_in">String</span> {
        <span class="hljs-keyword">self</span>.get_one::&lt;<span class="hljs-built_in">String</span>&gt;(id).cloned().unwrap_or_default()
    }

    <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">get_subcategory_or_default</span></span>(&amp;<span class="hljs-keyword">self</span>, id: &amp;<span class="hljs-built_in">str</span>) -&gt; <span class="hljs-built_in">String</span> {
        <span class="hljs-keyword">self</span>.get_one::&lt;<span class="hljs-built_in">String</span>&gt;(id)
            .cloned()
            .unwrap_or_else(|| DEFAULT_SUBCATEGORY.to_string())
    }

    <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">get_currency_or_default</span></span>(&amp;<span class="hljs-keyword">self</span>, id: &amp;<span class="hljs-built_in">str</span>) -&gt; &amp;Currency {
        <span class="hljs-keyword">self</span>.get_one::&lt;Currency&gt;(id).unwrap_or(&amp;Currency::NGN)
    }

    <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">get_vec</span></span>&lt;T: <span class="hljs-built_in">Clone</span> + <span class="hljs-built_in">Send</span> + <span class="hljs-built_in">Sync</span> + <span class="hljs-symbol">'static</span>&gt;(&amp;<span class="hljs-keyword">self</span>, id: &amp;<span class="hljs-built_in">str</span>) -&gt; <span class="hljs-built_in">Vec</span>&lt;T&gt; {
        <span class="hljs-keyword">self</span>.get_many::&lt;T&gt;(id)
            .map(|iter| iter.cloned().collect())
            .unwrap_or_default()
    }

    <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">contains_id</span></span>(&amp;<span class="hljs-keyword">self</span>, id: &amp;<span class="hljs-built_in">str</span>) -&gt; <span class="hljs-built_in">bool</span> {
        ArgMatches::contains_id(<span class="hljs-keyword">self</span>, id)
    }
}
</code></pre>
<p>These helper methods allow you to decide exactly how to handle missing data. Methods like <code>ok_or_else</code> convert an empty input into a specific <code>CliError</code> that informs the user exactly which argument is missing. In contrast, <code>unwrap_or_else</code> allows the application to provide sensible fallbacks, such as defaulting to the "miscellaneous" subcategory if the user does not specify one.</p>
<h3 id="heading-implement-custom-data-parsers">Implement Custom Data Parsers</h3>
<p>Standard command-line arguments are received as strings. To turn them into useful data types like dates or categories, you need specific parsing logic. Create a new file <code>src/utils/parsers.rs</code>:</p>
<pre><code class="lang-rust"><span class="hljs-keyword">use</span> chrono::NaiveDate;
<span class="hljs-keyword">use</span> crate::Category;

<span class="hljs-keyword">pub</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">parse_date</span></span>(s: &amp;<span class="hljs-built_in">str</span>) -&gt; <span class="hljs-built_in">Result</span>&lt;NaiveDate, <span class="hljs-built_in">String</span>&gt; {
    NaiveDate::parse_from_str(s, <span class="hljs-string">"%d-%m-%Y"</span>)
        .map_err(|_| <span class="hljs-built_in">format!</span>(<span class="hljs-string">"'{}' is not in the format DD-MM-YYYY"</span>, s))
}

<span class="hljs-keyword">pub</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">parse_category</span></span>(s: &amp;<span class="hljs-built_in">str</span>) -&gt; <span class="hljs-built_in">Result</span>&lt;Category, <span class="hljs-built_in">String</span>&gt; {
    s.parse::&lt;Category&gt;().map_err(|_| {
        <span class="hljs-built_in">format!</span>(<span class="hljs-string">"'{}' is not a valid category. Use 'income' or 'expenses'"</span>, s)
    })
}
</code></pre>
<p>We'll use these parsers to ensure that any input that doesn’t match your expected format is caught immediately with a clear error message before it ever reaches your core logic.</p>
<p>To finalize this setup, update <code>src/utils.rs</code> to include the new helper files:</p>
<pre><code class="lang-rust"><span class="hljs-keyword">pub</span> <span class="hljs-keyword">mod</span> cli;
<span class="hljs-keyword">pub</span> <span class="hljs-keyword">mod</span> command_prelude;
<span class="hljs-keyword">pub</span> <span class="hljs-keyword">mod</span> context;
<span class="hljs-keyword">pub</span> <span class="hljs-keyword">mod</span> file;
<span class="hljs-keyword">pub</span> <span class="hljs-keyword">mod</span> parsers;
</code></pre>
<p>This infrastructure ensures that when you begin implementing the actual financial commands, you can focus on the business logic instead of fighting with string conversions and repeating argument validation.</p>
<p>Next we'll begin implementing the business logic for the commands, starting with the <code>init</code> command.</p>
<h2 id="heading-step-8-implement-the-init-command">Step 8: Implement the Init Command</h2>
<p>The init command will set up the workspace for the financial tracker. It will handle the creation of the hidden directory structure in your home folder and generate the initial JSON file with default settings like the currency and starting balance.</p>
<p>Create the <code>src/commands/init.rs</code> file and add this code:</p>
<pre><code class="lang-rust"><span class="hljs-keyword">use</span> clap::{Arg, ArgMatches, Command};

<span class="hljs-keyword">use</span> crate::command_prelude::ArgMatchesExt;
<span class="hljs-keyword">use</span> crate::utils::file::{FilePath, write_json_to_file};
<span class="hljs-keyword">use</span> crate::{CliResponse, CliResult, Currency, GlobalContext, default_tracker_json};

<span class="hljs-keyword">pub</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">cli</span></span>() -&gt; Command {
    Command::new(<span class="hljs-string">"init"</span>)
        .about(<span class="hljs-string">"Initialize a new financial tracker"</span>)
        .arg(
            Arg::new(<span class="hljs-string">"currency"</span>)
                .short(<span class="hljs-string">'c'</span>)
                .value_parser(clap::value_parser!(Currency))
                .default_value(<span class="hljs-string">"ngn"</span>),
        )
        .arg(
            Arg::new(<span class="hljs-string">"opening"</span>)
                .short(<span class="hljs-string">'o'</span>)
                .value_parser(clap::value_parser!(<span class="hljs-built_in">f64</span>)),
        )
}

<span class="hljs-keyword">pub</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">exec</span></span>(gctx: &amp;<span class="hljs-keyword">mut</span> GlobalContext, args: &amp;ArgMatches) -&gt; CliResult {
    <span class="hljs-keyword">let</span> currency = args.get_currency_or_default(<span class="hljs-string">"currency"</span>);
    <span class="hljs-keyword">let</span> opening_balance = args.get_f64_or_default(<span class="hljs-string">"opening"</span>);

    <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut</span> file = gctx.tracker_path().create_file_if_not_exists()?;

    <span class="hljs-keyword">let</span> default_json = default_tracker_json(currency, opening_balance);
    write_json_to_file(&amp;default_json, &amp;<span class="hljs-keyword">mut</span> file)?;

    <span class="hljs-literal">Ok</span>(CliResponse::success())
}
</code></pre>
<p>The <code>cli</code> function defines the command's interface. <code>Command::new("init")</code> sets the name of the subcommand the user types. Within the <code>.arg()</code> blocks, <code>.short('c')</code> and <code>.long("currency")</code> allow for two different ways to provide the same data. A user can choose the concise short form or the more descriptive long form:</p>
<pre><code class="lang-bash">fintrack init -c usd -o 5000
</code></pre>
<p>OR</p>
<pre><code class="lang-bash">fintrack init --currency usd --opening 5000
</code></pre>
<p>Both commands map to the same internal <code>"currency"</code> and <code>"opening"</code> arguments.</p>
<p>The <code>exec</code> function performs the actual initialization of the tracker. It uses the helpers built in previous steps to keep the logic concise. Specifically, it uses <code>get_currency_or_default</code> and <code>get_f64_or_default</code> from the <code>ArgMatchesExt</code> trait you created in <code>src/utils/cli.rs</code>.</p>
<p>When attempting to create the tracker file, it calls <code>create_file_if_not_exists</code>. This method belongs to the <code>FilePath</code> trait you implemented in <code>src/utils/file.rs</code>. Because that method was built using <code>create_new(true)</code>, it acts as a guard that fails if a tracker already exists with an error <code>std::io::ErrorKind::AlreadyExists</code>. This failure is caught and converted into a <code>CliError::FileAlreadyExists</code> message, which was defined in your error handling logic in <code>src/error.rs</code>.</p>
<p>The <code>default_tracker_json</code> function builds the initial state for the application. It packages the base currency, opening balance, and default "miscellaneous" subcategory into a JSON structure. Finally, the <code>write_json_to_file</code> helper from <code>src/utils/file.rs</code> writes this data to the disk.</p>
<h2 id="heading-step-9-implement-the-add-command">Step 9: Implement the Add Command</h2>
<p>The <code>add</code> command will add a new record. To do this, you’ll implement code that will read the existing data from the JSON file, validate the new input, and save the updated record back to the JSON file.</p>
<p>Create <code>src/commands/add.rs</code> and insert this code:</p>
<pre><code class="lang-rust"><span class="hljs-keyword">use</span> chrono::Local;
<span class="hljs-keyword">use</span> clap::{Arg, ArgMatches, Command};

<span class="hljs-keyword">use</span> crate::command_prelude::ArgMatchesExt;
<span class="hljs-keyword">use</span> crate::utils::file::{FilePath, write_json_to_file};
<span class="hljs-keyword">use</span> crate::utils::parsers::{parse_category, parse_date};
<span class="hljs-keyword">use</span> crate::{
    CliError, CliResponse, CliResult, GlobalContext, Record, ResponseContent, TrackerData,
};

<span class="hljs-keyword">pub</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">cli</span></span>() -&gt; Command {
    Command::new(<span class="hljs-string">"add"</span>)
        .about(<span class="hljs-string">"Record a new income or expense transaction"</span>)
        .arg(
            Arg::new(<span class="hljs-string">"category"</span>)
                .index(<span class="hljs-number">1</span>)
                .required(<span class="hljs-literal">true</span>)
                .value_parser(parse_category),
        )
        .arg(
            Arg::new(<span class="hljs-string">"amount"</span>)
                .index(<span class="hljs-number">2</span>)
                .required(<span class="hljs-literal">true</span>)
                .value_parser(clap::value_parser!(<span class="hljs-built_in">f64</span>)),
        )
        .arg(
            Arg::new(<span class="hljs-string">"subcategory"</span>)
                .short(<span class="hljs-string">'s'</span>)
                .long(<span class="hljs-string">"subcategory"</span>)
                .default_value(<span class="hljs-string">"miscellaneous"</span>),
        )
        .arg(
            Arg::new(<span class="hljs-string">"description"</span>)
                .short(<span class="hljs-string">'d'</span>)
                .long(<span class="hljs-string">"description"</span>),
        )
        .arg(
            Arg::new(<span class="hljs-string">"date"</span>)
                .short(<span class="hljs-string">'D'</span>)
                .long(<span class="hljs-string">"date"</span>)
                .value_parser(parse_date),
        )
}

<span class="hljs-keyword">pub</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">exec</span></span>(gctx: &amp;<span class="hljs-keyword">mut</span> GlobalContext, args: &amp;ArgMatches) -&gt; CliResult {
    <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut</span> file = gctx.tracker_path().open_read_write()?;
    <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut</span> tracker_data: TrackerData = serde_json::from_reader(&amp;file)?;

    <span class="hljs-keyword">let</span> category = args.get_category(<span class="hljs-string">"category"</span>)?;
    <span class="hljs-keyword">let</span> amount = args.get_f64_or_default(<span class="hljs-string">"amount"</span>);

    <span class="hljs-keyword">if</span> amount &lt;= <span class="hljs-number">0.0</span> {
        <span class="hljs-keyword">return</span> <span class="hljs-literal">Err</span>(CliError::ValidationError(
            crate::ValidationErrorKind::AmountTooSmall { amount },
        ));
    }

    <span class="hljs-keyword">let</span> subcategory_name = args.get_subcategory_or_default(<span class="hljs-string">"subcategory"</span>);
    <span class="hljs-keyword">let</span> description = args.get_string_or_default(<span class="hljs-string">"description"</span>);

    <span class="hljs-keyword">let</span> category_str = category.to_string();
    <span class="hljs-keyword">let</span> category_id = tracker_data.category_id(&amp;category_str);

    <span class="hljs-keyword">let</span> subcategory_id = tracker_data
        .subcategory_id(&amp;subcategory_name)
        .ok_or_else(|| {
            CliError::ValidationError(crate::ValidationErrorKind::SubcategoryNotFound {
                name: subcategory_name,
            })
        })?;

    <span class="hljs-keyword">let</span> date = args
        .get_date_opt(<span class="hljs-string">"date"</span>)
        .map(|d| d.format(<span class="hljs-string">"%d-%m-%Y"</span>).to_string())
        .unwrap_or_else(|| Local::now().format(<span class="hljs-string">"%d-%m-%Y"</span>).to_string());

    <span class="hljs-keyword">let</span> record_id = tracker_data.next_record_id;
    <span class="hljs-keyword">let</span> record = Record {
        id: record_id,
        category: category_id,
        amount,
        subcategory: subcategory_id,
        description,
        date,
    };

    tracker_data.next_record_id += <span class="hljs-number">1</span>;
    tracker_data.last_modified = chrono::Utc::now().to_rfc3339();
    tracker_data.push_record(record.clone());

    <span class="hljs-keyword">let</span> tracker_json = serde_json::json!(tracker_data);
    write_json_to_file(&amp;tracker_json, &amp;<span class="hljs-keyword">mut</span> file)?;

    <span class="hljs-literal">Ok</span>(CliResponse::new(ResponseContent::Record {
        record,
        tracker_data,
        is_update: <span class="hljs-literal">false</span>,
    }))
}
</code></pre>
<p>The <code>cli</code> function here defines positional arguments using <code>.index(1)</code> and <code>.index(2)</code>. This means users can provide the category and amount without specific flags. An example usage looks like this:</p>
<pre><code class="lang-bash">fintrack add income 1500 -s salary -d <span class="hljs-string">"Monthly pay"</span>
</code></pre>
<p>In this command, <code>"income"</code> maps to the <code>"category"</code> and 1500 maps to the <code>"amount"</code>. The parser logic for these inputs makes use of the <code>parse_category</code> and <code>parse_date</code> functions created in <code>src/utils/parsers.rs</code>.</p>
<p>The <code>exec</code> function here opens the data file with <code>open_read_write</code> from the <code>FilePath</code> trait (<code>src/utils/file.rs</code>) and extracts user input using the <code>ArgMatchesExt</code> trait (<code>src/utils/cli.rs</code>).</p>
<p>The date logic handles optional input through a chain of methods. <code>get_date_opt</code> returns an <code>Option</code>, such that when a date exists, <code>.map</code> transforms it into the required string format. When a date doesn’t exist, <code>.unwrap_or_else</code> provides the current system date as a default.</p>
<p>Once the <code>Record</code> struct is populated, the code updates the <code>TrackerData</code> state and saves the result using the <code>write_json_to_file</code> helper. The final <code>CliResponse</code> contains the record details for the output module in <code>src/output.rs</code> to display.</p>
<h2 id="heading-step-10-implement-the-list-command">Step 10: Implement the List Command</h2>
<p>The <code>list</code> command will provide a way to view and filter records. This logic involves loading the data file, applying criteria like date ranges or categories, and sorting the results chronologically.</p>
<p>Create <code>src/commands/list.rs</code> and add the following code:</p>
<pre><code class="lang-rust"><span class="hljs-keyword">use</span> chrono::NaiveDate;
<span class="hljs-keyword">use</span> clap::{Arg, ArgGroup, ArgMatches, Command};

<span class="hljs-keyword">use</span> crate::command_prelude::ArgMatchesExt;
<span class="hljs-keyword">use</span> crate::utils::file::FilePath;
<span class="hljs-keyword">use</span> crate::utils::parsers::{parse_category, parse_date};
<span class="hljs-keyword">use</span> crate::{CliResponse, CliResult, GlobalContext, Record, ResponseContent, TrackerData};

<span class="hljs-keyword">pub</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">cli</span></span>() -&gt; Command {
    Command::new(<span class="hljs-string">"list"</span>)
        .about(<span class="hljs-string">"View and filter your transaction records"</span>)
        .arg(
            Arg::new(<span class="hljs-string">"first"</span>)
                .short(<span class="hljs-string">'f'</span>)
                .long(<span class="hljs-string">"first"</span>)
                .value_parser(clap::value_parser!(<span class="hljs-built_in">usize</span>)),
        )
        .arg(
            Arg::new(<span class="hljs-string">"last"</span>)
                .short(<span class="hljs-string">'l'</span>)
                .long(<span class="hljs-string">"last"</span>)
                .value_parser(clap::value_parser!(<span class="hljs-built_in">usize</span>)),
        )
        .group(
            ArgGroup::new(<span class="hljs-string">"first_or_last"</span>)
                .args([<span class="hljs-string">"first"</span>, <span class="hljs-string">"last"</span>])
                .multiple(<span class="hljs-literal">false</span>),
        )
        .arg(
            Arg::new(<span class="hljs-string">"start"</span>)
                .short(<span class="hljs-string">'S'</span>)
                .long(<span class="hljs-string">"start"</span>)
                .value_parser(parse_date),
        )
        .arg(
            Arg::new(<span class="hljs-string">"end"</span>)
                .short(<span class="hljs-string">'E'</span>)
                .long(<span class="hljs-string">"end"</span>)
                .value_parser(parse_date),
        )
        .arg(
            Arg::new(<span class="hljs-string">"category"</span>)
                .short(<span class="hljs-string">'c'</span>)
                .long(<span class="hljs-string">"category"</span>)
                .value_parser(parse_category),
        )
        .arg(
            Arg::new(<span class="hljs-string">"subcategory"</span>)
                .short(<span class="hljs-string">'s'</span>)
                .long(<span class="hljs-string">"subcategory"</span>),
        )
}

<span class="hljs-keyword">pub</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">exec</span></span>(gctx: &amp;<span class="hljs-keyword">mut</span> GlobalContext, args: &amp;ArgMatches) -&gt; CliResult {
    <span class="hljs-keyword">let</span> file = gctx.tracker_path().open_read()?;
    <span class="hljs-keyword">let</span> tracker_data: TrackerData = serde_json::from_reader(&amp;file)?;

    <span class="hljs-keyword">let</span> start_date = args.get_date_opt(<span class="hljs-string">"start"</span>);
    <span class="hljs-keyword">let</span> end_date = args.get_date_opt(<span class="hljs-string">"end"</span>);

    <span class="hljs-keyword">let</span> category_filter = args
        .get_category_opt(<span class="hljs-string">"category"</span>)
        .map(|cat| tracker_data.category_id(&amp;cat.to_string()));

    <span class="hljs-keyword">let</span> subcategory_filter = args
        .get_subcategory_opt(<span class="hljs-string">"subcategory"</span>)
        .and_then(|name| tracker_data.subcategory_id(&amp;name));

    <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut</span> filtered_data: <span class="hljs-built_in">Vec</span>&lt;Record&gt; = tracker_data
        .records
        .iter()
        .filter(|r| {
            <span class="hljs-keyword">let</span> matches_category = category_filter
                .map(|expected_id| r.category == expected_id)
                .unwrap_or(<span class="hljs-literal">true</span>);

            <span class="hljs-keyword">let</span> matches_subcategory = subcategory_filter
                .map(|expected_id| r.subcategory == expected_id)
                .unwrap_or(<span class="hljs-literal">true</span>);

            <span class="hljs-keyword">let</span> matches_date = NaiveDate::parse_from_str(&amp;r.date, <span class="hljs-string">"%d-%m-%Y"</span>)
                .map(|record_date| {
                    <span class="hljs-keyword">let</span> after_start = start_date.map_or(<span class="hljs-literal">true</span>, |start| record_date &gt;= start);
                    <span class="hljs-keyword">let</span> before_end = end_date.map_or(<span class="hljs-literal">true</span>, |end| record_date &lt;= end);
                    after_start &amp;&amp; before_end
                })
                .unwrap_or(<span class="hljs-literal">false</span>);

            matches_category &amp;&amp; matches_subcategory &amp;&amp; matches_date
        })
        .cloned()
        .collect();

    filtered_data.sort_by(|a, b| {
        <span class="hljs-keyword">let</span> date_a = NaiveDate::parse_from_str(&amp;a.date, <span class="hljs-string">"%d-%m-%Y"</span>).unwrap_or(NaiveDate::MIN);
        <span class="hljs-keyword">let</span> date_b = NaiveDate::parse_from_str(&amp;b.date, <span class="hljs-string">"%d-%m-%Y"</span>).unwrap_or(NaiveDate::MIN);
        date_a.cmp(&amp;date_b)
    });

    <span class="hljs-keyword">if</span> args.contains_id(<span class="hljs-string">"first"</span>) {
        <span class="hljs-keyword">let</span> first = args.get_usize_or_default(<span class="hljs-string">"first"</span>);
        <span class="hljs-keyword">if</span> first &gt; <span class="hljs-number">0</span> {
            filtered_data.truncate(first);
        }
    } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> args.contains_id(<span class="hljs-string">"last"</span>) {
        <span class="hljs-keyword">let</span> last = args.get_usize_or_default(<span class="hljs-string">"last"</span>);
        <span class="hljs-keyword">if</span> last &gt; <span class="hljs-number">0</span> &amp;&amp; filtered_data.len() &gt; last {
            <span class="hljs-keyword">let</span> start_idx = filtered_data.len() - last;
            filtered_data = filtered_data.into_iter().skip(start_idx).collect();
        }
    }

    <span class="hljs-literal">Ok</span>(CliResponse::new(ResponseContent::List {
        records: filtered_data,
        tracker_data,
    }))
}
</code></pre>
<p>The <code>cli</code> function utilizes an <code>ArgGroup</code> named <code>"first_or_last"</code>. This ensures the user cannot request both the first N and last N records at the same time. The command supports multiple filtering flags, which allows a user to run queries like:</p>
<pre><code class="lang-bash">fintrack list -c expenses -S 01-01-2024 -E 31-01-2024
</code></pre>
<p>The command above filters for "expenses" specifically within the month of January 2024.</p>
<p>The <code>exec</code> function uses <code>open_read</code> from the <code>FilePath</code> trait (<code>src/utils/file.rs</code>) to access the tracker file without write permissions. The filtering logic makes use of methods like <code>and_then</code> and <code>map_or</code> to handle optional criteria. For example, the date filter uses <code>map_or(true, ...)</code> to include a record if no specific start or end date was provided.</p>
<p>The record sorting uses <code>sort_by</code> to compare record dates. Since dates are stored as strings in the JSON file, they are parsed into <code>NaiveDate</code> objects temporarily for an accurate chronological comparison. Finally, the function uses <code>truncate</code> or <code>skip</code> to limit the results based on the <code>"first"</code> or <code>"last"</code> arguments before returning a <code>ResponseContent::List</code> for the output module in <code>src/output.rs</code> to process.</p>
<h2 id="heading-step-11-implement-the-update-command">Step 11: Implement the Update Command</h2>
<p>The <code>update</code> command will allow the user to modify specific fields in an existing record. It will accept similar arguments as the <code>add</code> command but unlike the <code>add</code> command, every argument except the ID will be optional, enabling the user to change only what is necessary.</p>
<p>Create <code>src/commands/update.rs</code> and add the following code:</p>
<pre><code class="lang-rust"><span class="hljs-keyword">use</span> clap::{Arg, ArgMatches, Command};

<span class="hljs-keyword">use</span> crate::command_prelude::ArgMatchesExt;
<span class="hljs-keyword">use</span> crate::utils::file::{FilePath, write_json_to_file};
<span class="hljs-keyword">use</span> crate::utils::parsers::{parse_category, parse_date};
<span class="hljs-keyword">use</span> crate::{CliError, CliResponse, CliResult, GlobalContext, ResponseContent, TrackerData};

<span class="hljs-keyword">pub</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">cli</span></span>() -&gt; Command {
    Command::new(<span class="hljs-string">"update"</span>)
        .about(<span class="hljs-string">"Modify an existing transaction record"</span>)
        .arg(
            Arg::new(<span class="hljs-string">"record_id"</span>)
                .index(<span class="hljs-number">1</span>)
                .required(<span class="hljs-literal">true</span>)
                .value_parser(clap::value_parser!(<span class="hljs-built_in">usize</span>)),
        )
        .arg(
            Arg::new(<span class="hljs-string">"category"</span>)
                .short(<span class="hljs-string">'c'</span>)
                .long(<span class="hljs-string">"category"</span>)
                .value_parser(parse_category),
        )
        .arg(
            Arg::new(<span class="hljs-string">"amount"</span>)
                .short(<span class="hljs-string">'a'</span>)
                .long(<span class="hljs-string">"amount"</span>)
                .value_parser(clap::value_parser!(<span class="hljs-built_in">f64</span>)),
        )
        .arg(
            Arg::new(<span class="hljs-string">"subcategory"</span>)
                .short(<span class="hljs-string">'s'</span>)
                .long(<span class="hljs-string">"subcategory"</span>),
        )
        .arg(
            Arg::new(<span class="hljs-string">"description"</span>)
                .short(<span class="hljs-string">'d'</span>)
                .long(<span class="hljs-string">"description"</span>),
        )
        .arg(
            Arg::new(<span class="hljs-string">"date"</span>)
                .short(<span class="hljs-string">'D'</span>)
                .long(<span class="hljs-string">"date"</span>)
                .value_parser(parse_date),
        )
}

<span class="hljs-keyword">pub</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">exec</span></span>(gctx: &amp;<span class="hljs-keyword">mut</span> GlobalContext, args: &amp;ArgMatches) -&gt; CliResult {
    <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut</span> file = gctx.tracker_path().open_read_write()?;
    <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut</span> tracker_data: TrackerData = serde_json::from_reader(&amp;file)?;

    <span class="hljs-keyword">let</span> record_id = args
        .get_usize(<span class="hljs-string">"record_id"</span>)
        .map_err(|_| CliError::ValidationError(crate::ValidationErrorKind::RecordNotFound { id: <span class="hljs-number">0</span> }))?;

    <span class="hljs-keyword">let</span> category_id = args.get_category_opt(<span class="hljs-string">"category"</span>).map(|category| {
        <span class="hljs-keyword">let</span> category_str = category.to_string();
        tracker_data.category_id(&amp;category_str)
    });

    <span class="hljs-keyword">let</span> subcategory_id = args
        .get_subcategory_opt(<span class="hljs-string">"subcategory"</span>)
        .map(|name| {
            tracker_data.subcategory_id(&amp;name).ok_or_else(|| {
                CliError::ValidationError(crate::ValidationErrorKind::SubcategoryNotFound { name })
            })
        })
        .transpose()?;

    <span class="hljs-keyword">let</span> record = tracker_data
        .records
        .iter_mut()
        .find(|r| r.id == record_id)
        .ok_or_else(|| {
            CliError::ValidationError(crate::ValidationErrorKind::RecordNotFound { id: record_id })
        })?;

    <span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> <span class="hljs-literal">Some</span>(cat_id) = category_id {
        record.category = cat_id;
    }

    <span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> <span class="hljs-literal">Some</span>(amount) = args.get_f64_opt(<span class="hljs-string">"amount"</span>) {
        <span class="hljs-keyword">if</span> amount &lt;= <span class="hljs-number">0.0</span> {
            <span class="hljs-keyword">return</span> <span class="hljs-literal">Err</span>(CliError::ValidationError(
                crate::ValidationErrorKind::AmountTooSmall { amount },
            ));
        }
        record.amount = amount;
    }

    <span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> <span class="hljs-literal">Some</span>(subcat_id) = subcategory_id {
        record.subcategory = subcat_id;
    }

    <span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> <span class="hljs-literal">Some</span>(description) = args.get_string_opt(<span class="hljs-string">"description"</span>) {
        record.description = description;
    }

    <span class="hljs-keyword">if</span> <span class="hljs-keyword">let</span> <span class="hljs-literal">Some</span>(date) = args.get_date_opt(<span class="hljs-string">"date"</span>) {
        record.date = date.format(<span class="hljs-string">"%d-%m-%Y"</span>).to_string();
    }

    tracker_data.last_modified = chrono::Utc::now().to_rfc3339();

    <span class="hljs-keyword">let</span> updated_record = record.clone();

    <span class="hljs-keyword">let</span> tracker_json = serde_json::json!(tracker_data);
    write_json_to_file(&amp;tracker_json, &amp;<span class="hljs-keyword">mut</span> file)?;

    <span class="hljs-literal">Ok</span>(CliResponse::new(ResponseContent::Record {
        record: updated_record,
        tracker_data,
        is_update: <span class="hljs-literal">true</span>,
    }))
}
</code></pre>
<p>The <code>cli</code> function requires a positional <code>"record_id"</code> so the program knows which record to target. Users can find this ID by running the <code>list</code> command. An <code>update</code> command looks like this:</p>
<pre><code class="lang-bash">fintrack update 5 -a 2000 -d <span class="hljs-string">"Updated price"</span>
</code></pre>
<p>The command above specifically updates the amount and description for record number 5, leaving all other fields unchanged.</p>
<p>The <code>exec</code> function uses <code>iter_mut()</code> and <code>find()</code> to locate the specific record in your data list. Because <code>iter_mut()</code> provides a mutable reference, any changes made to the record variable directly update the object inside <code>tracker_data.records</code>.</p>
<p>To handle the optional subcategory update, the code uses <code>transpose()</code>. This method is essential here because looking up a subcategory name is optional. But if a name is provided and it doesn't exist, the program should stop and return an error. <code>transpose()</code> flips the <code>Option&lt;Result&gt;</code> into a <code>Result&lt;Option&gt;</code>, allowing the <code>?</code> operator to handle the error while still giving you an <code>Option</code> to work with.</p>
<p>The final state is saved back to the file using the <code>write_json_to_file</code> helper from <code>src/utils/file.rs</code>. The <code>CliResponse</code> indicates that an update occurred by setting <code>is_update: true</code>, which the <code>output</code> module uses to format the success message appropriately.</p>
<h2 id="heading-step-12-implement-the-delete-command">Step 12: Implement the Delete Command</h2>
<p>The <code>delete</code> command will remove specific records from the tracker. This implementation will support multiple deletion strategies: targeting individual IDs, removing an entire category, or clearing a specific subcategory.</p>
<pre><code class="lang-rust"><span class="hljs-keyword">use</span> std::collections::HashSet;

<span class="hljs-keyword">use</span> clap::{Arg, ArgAction, ArgGroup, ArgMatches, Command};

<span class="hljs-keyword">use</span> crate::{
    CliResponse, CliResult, GlobalContext, TrackerData,
    command_prelude::ArgMatchesExt,
    utils::file::{FilePath, write_json_to_file},
    utils::parsers::parse_category,
};

<span class="hljs-keyword">pub</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">cli</span></span>() -&gt; Command {
    Command::new(<span class="hljs-string">"delete"</span>)
        .about(<span class="hljs-string">"Delete transaction records"</span>)
        .arg(
            Arg::new(<span class="hljs-string">"ids"</span>)
                .short(<span class="hljs-string">'i'</span>)
                .long(<span class="hljs-string">"ids"</span>)
                .value_parser(clap::value_parser!(<span class="hljs-built_in">usize</span>))
                .action(ArgAction::Append)
                .value_delimiter(<span class="hljs-string">','</span>),
        )
        .arg(
            Arg::new(<span class="hljs-string">"by-cat"</span>)
                .short(<span class="hljs-string">'c'</span>)
                .long(<span class="hljs-string">"by-cat"</span>)
                .value_parser(parse_category),
        )
        .arg(
            Arg::new(<span class="hljs-string">"by-subcat"</span>)
                .short(<span class="hljs-string">'s'</span>)
                .long(<span class="hljs-string">"by-subcat"</span>),
        )
        .group(
            ArgGroup::new(<span class="hljs-string">"delete_by"</span>)
                .args([<span class="hljs-string">"ids"</span>, <span class="hljs-string">"by-cat"</span>, <span class="hljs-string">"by-subcat"</span>])
                .multiple(<span class="hljs-literal">false</span>)
                .required(<span class="hljs-literal">true</span>),
        )
}

<span class="hljs-keyword">pub</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">exec</span></span>(gctx: &amp;<span class="hljs-keyword">mut</span> GlobalContext, args: &amp;ArgMatches) -&gt; CliResult {
    <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut</span> file = gctx.tracker_path().open_read_write()?;
    <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut</span> tracker_data: TrackerData = serde_json::from_reader(&amp;file)?;

    <span class="hljs-keyword">if</span> args.contains_id(<span class="hljs-string">"ids"</span>) {
        <span class="hljs-keyword">let</span> ids: <span class="hljs-built_in">Vec</span>&lt;<span class="hljs-built_in">usize</span>&gt; = args.get_vec::&lt;<span class="hljs-built_in">usize</span>&gt;(<span class="hljs-string">"ids"</span>);
        <span class="hljs-keyword">let</span> ids_set: HashSet&lt;<span class="hljs-built_in">usize</span>&gt; = ids.into_iter().collect();

        tracker_data.records.retain(|r| !ids_set.contains(&amp;r.id));
    } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> args.contains_id(<span class="hljs-string">"by-cat"</span>) {
        <span class="hljs-keyword">let</span> category = args.get_category(<span class="hljs-string">"by-cat"</span>)?;
        <span class="hljs-keyword">let</span> category_str = category.to_string();
        <span class="hljs-keyword">let</span> category_id = tracker_data.category_id(&amp;category_str);

        tracker_data.records.retain(|r| r.category != category_id);
    } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> args.contains_id(<span class="hljs-string">"by-subcat"</span>) {
        <span class="hljs-keyword">let</span> subcategory_name = args
            .get_subcategory_opt(<span class="hljs-string">"by-subcat"</span>)
            .ok_or_else(|| crate::CliError::Other(<span class="hljs-string">"Subcategory not provided"</span>.to_string()))?;

        <span class="hljs-keyword">let</span> subcategory_id = tracker_data
            .subcategory_id(subcategory_name.as_str())
            .ok_or_else(|| {
                crate::CliError::ValidationError(crate::ValidationErrorKind::SubcategoryNotFound {
                    name: subcategory_name.clone(),
                })
            })?;

        tracker_data
            .records
            .retain(|r| r.subcategory != subcategory_id);
    }

    tracker_data.last_modified = chrono::Utc::now().to_rfc3339();

    <span class="hljs-keyword">let</span> tracker_json = serde_json::json!(tracker_data);
    write_json_to_file(&amp;tracker_json, &amp;<span class="hljs-keyword">mut</span> file)?;

    <span class="hljs-literal">Ok</span>(CliResponse::success())
}
</code></pre>
<p>The <code>cli</code> function makes use of an <code>ArgGroup</code> to enforce that only one deletion method is used at a time (<code>"ids</code>, <code>"by-cat"</code>, or <code>"by-subcat"</code>). The <code>"ids"</code> argument uses <code>value_delimiter(',')</code>, allowing a user to pass multiple IDs separated by a comma (','). For example:</p>
<pre><code class="lang-bash">fintrack delete --ids 1,4,7
</code></pre>
<p>Also, a user can clear all records of a particular category or subcategory using the <code>"by-cat"</code> or <code>"by-subcat"</code> flag. For example:</p>
<pre><code class="lang-bash">fintrack delete --by-cat expenses
</code></pre>
<p>The <code>exec</code> function determines which records to target based on three possible inputs. If <code>--ids</code> is used, it collects the provided values directly into a HashSet. If <code>--by-cat</code> or <code>--by-subcat</code> is used, the code iterates through existing records and gathers the IDs of every record that matches that specific category or subcategory and stores it in a HashSet. Regardless of the flag used, the logic converges on a HashSet containing all IDs scheduled for removal.</p>
<p>Using a HashSet makes the final cleanup highly efficient because it allows the program to check if an ID exists in the "delete list" almost instantly. The retain method then keeps only the records whose IDs are not in that set, effectively pruning the data in place.</p>
<p>After the removal, the code calculates the difference between the initial and final record counts. If no records were removed, it returns a <code>RecordNotFound</code> error. Otherwise, it updates the <code>last_modified</code> timestamp and saves the updated JSON using the <code>write_json_to_file</code> helper from <code>src/utils/file.rs</code>.</p>
<h2 id="heading-step-13-implement-subcategory-commands">Step 13: Implement Subcategory Commands</h2>
<p>The <code>subcategory</code> command will serve as a parent for several nested subcommands, allowing users to organize their records beyond the basic "income" and "expenses" categories. This structure will use a modular approach, where each management task will live in its own dedicated file.</p>
<p>Create the entry point file at <code>src/commands/subcategory.rs</code>:</p>
<pre><code class="lang-rust"><span class="hljs-keyword">use</span> clap::{ArgMatches, Command};

<span class="hljs-keyword">use</span> crate::{CliResult, GlobalContext, commands::Exec, invalid_subcommand_error};

<span class="hljs-keyword">pub</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">cli</span></span>() -&gt; Command {
    Command::new(<span class="hljs-string">"subcategory"</span>)
        .about(<span class="hljs-string">"Manage your subcategories"</span>)
        .subcommand_required(<span class="hljs-literal">true</span>)
        .subcommands(build_cli())
}

<span class="hljs-keyword">pub</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">exec</span></span>(gctx: &amp;<span class="hljs-keyword">mut</span> GlobalContext, args: &amp;ArgMatches) -&gt; CliResult {
    <span class="hljs-keyword">match</span> args.subcommand() {
        <span class="hljs-literal">Some</span>((cmd, sub_args)) =&gt; {
            <span class="hljs-keyword">let</span> exec_fn = build_exec(cmd).ok_or_else(|| invalid_subcommand_error(cmd))?;

            exec_fn(gctx, sub_args)
        }
        <span class="hljs-literal">None</span> =&gt; <span class="hljs-literal">Err</span>(invalid_subcommand_error(<span class="hljs-string">""</span>)),
    }
}

<span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">build_cli</span></span>() -&gt; <span class="hljs-built_in">Vec</span>&lt;Command&gt; {
    <span class="hljs-built_in">vec!</span>[add::cli(), delete::cli(), list::cli(), rename::cli()]
}

<span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">build_exec</span></span>(cmd: &amp;<span class="hljs-built_in">str</span>) -&gt; <span class="hljs-built_in">Option</span>&lt;Exec&gt; {
    <span class="hljs-keyword">match</span> cmd {
        <span class="hljs-string">"add"</span> =&gt; <span class="hljs-literal">Some</span>(add::exec),
        <span class="hljs-string">"delete"</span> =&gt; <span class="hljs-literal">Some</span>(delete::exec),
        <span class="hljs-string">"list"</span> =&gt; <span class="hljs-literal">Some</span>(list::exec),
        <span class="hljs-string">"rename"</span> =&gt; <span class="hljs-literal">Some</span>(rename::exec),
        <span class="hljs-string">"update"</span> =&gt; <span class="hljs-literal">Some</span>(rename::exec),
        _ =&gt; <span class="hljs-literal">None</span>,
    }
}

<span class="hljs-keyword">pub</span> <span class="hljs-keyword">mod</span> list;
<span class="hljs-keyword">pub</span> <span class="hljs-keyword">mod</span> add;
<span class="hljs-keyword">pub</span> <span class="hljs-keyword">mod</span> delete;
<span class="hljs-keyword">pub</span> <span class="hljs-keyword">mod</span> rename;
</code></pre>
<p>The <code>cli</code> function here sets <code>subcommand_required(true)</code>. This means the user must specify an action. The <code>exec</code> function uses a <code>match</code> statement to delegate the logic to the appropriate module.</p>
<h3 id="heading-list-subcategories">List Subcategories</h3>
<p>Create <code>src/commands/subcategory/list.rs</code>:</p>
<pre><code class="lang-rust"><span class="hljs-keyword">use</span> clap::{ArgMatches, Command};

<span class="hljs-keyword">use</span> crate::{CliResponse, CliResult, GlobalContext, ResponseContent, TrackerData, utils::file::FilePath};

<span class="hljs-keyword">pub</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">cli</span></span>() -&gt; Command {
    Command::new(<span class="hljs-string">"list"</span>)
        .about(<span class="hljs-string">"View all available subcategories"</span>)
}

<span class="hljs-keyword">pub</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">exec</span></span>(gctx: &amp;<span class="hljs-keyword">mut</span> GlobalContext, _args: &amp;ArgMatches) -&gt; CliResult {
    <span class="hljs-keyword">let</span> file = gctx.tracker_path().open_read()?;
    <span class="hljs-keyword">let</span> tracker_data: TrackerData = serde_json::from_reader(&amp;file)?;

    <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut</span> subcategories: <span class="hljs-built_in">Vec</span>&lt;(<span class="hljs-built_in">usize</span>, <span class="hljs-built_in">String</span>)&gt; = tracker_data
        .subcategories_by_id
        .iter()
        .map(|(&amp;id, name)| (id, name.clone()))
        .collect();

    subcategories.sort_by_key(|(id, _)| *id);

    <span class="hljs-literal">Ok</span>(CliResponse::new(ResponseContent::Subcategories(subcategories)))
}
</code></pre>
<p>The <code>exec</code> function first accesses the data file using the <code>open_read</code> helper method defined earlier in <code>src/utils/file.rs</code>. Once the file is open, it reads the JSON content into the <code>TrackerData</code> struct. The logic then pulls the specific list of IDs and names from the <code>subcategories_by_id</code> map found in <code>src/models.rs</code> and converts them into a simple list for the user to view.</p>
<h3 id="heading-add-subcategories">Add Subcategories</h3>
<p>Create <code>src/commands/subcategory/add.rs</code>:</p>
<pre><code class="lang-rust"><span class="hljs-keyword">use</span> clap::{Arg, ArgMatches, Command};

<span class="hljs-keyword">use</span> crate::{
    CliError, CliResponse, CliResult, GlobalContext, TrackerData,
    utils::file::{FilePath, write_json_to_file},
    utils::parsers::parse_label,
};

<span class="hljs-keyword">pub</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">cli</span></span>() -&gt; Command {
    Command::new(<span class="hljs-string">"add"</span>)
        .about(<span class="hljs-string">"Create a new subcategory"</span>)
        .arg(
            Arg::new(<span class="hljs-string">"name"</span>)
                .index(<span class="hljs-number">1</span>)
                .required(<span class="hljs-literal">true</span>)
                .value_parser(parse_label),
        )
}

<span class="hljs-keyword">pub</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">exec</span></span>(gctx: &amp;<span class="hljs-keyword">mut</span> GlobalContext, args: &amp;ArgMatches) -&gt; CliResult {
    <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut</span> file = gctx.tracker_path().open_read_write()?;
    <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut</span> tracker_data: TrackerData = serde_json::from_reader(&amp;file)?;

    <span class="hljs-keyword">let</span> name = args
        .get_one::&lt;<span class="hljs-built_in">String</span>&gt;(<span class="hljs-string">"name"</span>)
        .ok_or_else(|| CliError::Other(<span class="hljs-string">"Subcategory name not provided"</span>.to_string()))?;

    <span class="hljs-keyword">let</span> name_lower = name.to_lowercase();
    <span class="hljs-keyword">let</span> name_title = {
        <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut</span> chars = name_lower.chars();
        <span class="hljs-keyword">match</span> chars.next() {
            <span class="hljs-literal">None</span> =&gt; <span class="hljs-keyword">return</span> <span class="hljs-literal">Err</span>(CliError::Other(<span class="hljs-string">"Invalid name"</span>.to_string())),
            <span class="hljs-literal">Some</span>(first) =&gt; first.to_uppercase().collect::&lt;<span class="hljs-built_in">String</span>&gt;() + &amp;chars.as_str().to_lowercase(),
        }
    };

    <span class="hljs-keyword">if</span> name_lower == <span class="hljs-string">"miscellaneous"</span> {
        <span class="hljs-keyword">return</span> <span class="hljs-literal">Err</span>(CliError::ValidationError(
            crate::ValidationErrorKind::CannotDeleteMiscellaneous,
        ));
    }

    <span class="hljs-keyword">if</span> tracker_data.subcategories_by_name.contains_key(&amp;name_lower) {
        <span class="hljs-keyword">return</span> <span class="hljs-literal">Err</span>(CliError::ValidationError(
            crate::ValidationErrorKind::SubcategoryAlreadyExists {
                name: name_title.clone(),
            },
        ));
    }

    <span class="hljs-keyword">let</span> subcategory_id = tracker_data.next_subcategory_id <span class="hljs-keyword">as</span> <span class="hljs-built_in">usize</span>;
    tracker_data.subcategories_by_id.insert(subcategory_id, name_title.clone());
    tracker_data.subcategories_by_name.insert(name_lower, subcategory_id);
    tracker_data.next_subcategory_id += <span class="hljs-number">1</span>;
    tracker_data.last_modified = chrono::Utc::now().to_rfc3339();

    <span class="hljs-keyword">let</span> tracker_json = serde_json::json!(tracker_data);
    write_json_to_file(&amp;tracker_json, &amp;<span class="hljs-keyword">mut</span> file)?;

    <span class="hljs-literal">Ok</span>(CliResponse::new(crate::ResponseContent::Message(<span class="hljs-built_in">format!</span>(
        <span class="hljs-string">"Subcategory '{}' added (ID: {})"</span>,
        name_title, subcategory_id
    ))))
}
</code></pre>
<p>The <code>exec</code> function here makes use of <code>open_read_write</code> to load the data for modification. It retrieves the user's input through the <code>get_string_opt</code> helper from the <code>ArgMatchesExt</code> trait.</p>
<p>To maintain consistency, the <code>normalize_name</code> function ensures all names follow a standard title case format. Before saving, the logic checks the <code>subcategories_by_name</code> map from <code>src/models.rs</code> to ensure the name is unique.</p>
<p>Once validated, it updates the <code>next_subcategory_id</code> and writes the changes back to disk using <code>write_json_to_file</code>.</p>
<h3 id="heading-delete-subcategories">Delete Subcategories</h3>
<p>Create <code>src/commands/subcategory/delete.rs</code>:</p>
<pre><code class="lang-rust"><span class="hljs-keyword">use</span> clap::{Arg, ArgMatches, Command};

<span class="hljs-keyword">use</span> crate::{
    CliError, CliResponse, CliResult, GlobalContext, TrackerData,
    utils::file::{FilePath, write_json_to_file},
    utils::parsers::parse_label,
};

<span class="hljs-keyword">pub</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">cli</span></span>() -&gt; Command {
    Command::new(<span class="hljs-string">"delete"</span>)
        .about(<span class="hljs-string">"Delete a subcategory"</span>)
        .arg(
            Arg::new(<span class="hljs-string">"name"</span>)
                .index(<span class="hljs-number">1</span>)
                .required(<span class="hljs-literal">true</span>)
                .value_parser(parse_label),
        )
}

<span class="hljs-keyword">pub</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">exec</span></span>(gctx: &amp;<span class="hljs-keyword">mut</span> GlobalContext, args: &amp;ArgMatches) -&gt; CliResult {
    <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut</span> file = gctx.tracker_path().open_read_write()?;
    <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut</span> tracker_data: TrackerData = serde_json::from_reader(&amp;file)?;

    <span class="hljs-keyword">let</span> name = args
        .get_one::&lt;<span class="hljs-built_in">String</span>&gt;(<span class="hljs-string">"name"</span>)
        .ok_or_else(|| CliError::Other(<span class="hljs-string">"Subcategory name not provided"</span>.to_string()))?;

    <span class="hljs-keyword">let</span> name_lower = name.to_lowercase();

    <span class="hljs-keyword">if</span> name_lower == <span class="hljs-string">"miscellaneous"</span> {
        <span class="hljs-keyword">return</span> <span class="hljs-literal">Err</span>(CliError::ValidationError(
            crate::ValidationErrorKind::CannotDeleteMiscellaneous,
        ));
    }

    <span class="hljs-keyword">let</span> subcategory_id = tracker_data
        .subcategory_id(&amp;name_lower)
        .ok_or_else(|| {
            CliError::ValidationError(crate::ValidationErrorKind::SubcategoryNotFound {
                name: name.to_string(),
            })
        })?;

    <span class="hljs-keyword">let</span> record_count = tracker_data
        .records
        .iter()
        .filter(|r| r.subcategory == subcategory_id)
        .count();

    <span class="hljs-keyword">if</span> record_count &gt; <span class="hljs-number">0</span> {
        <span class="hljs-keyword">return</span> <span class="hljs-literal">Err</span>(CliError::ValidationError(
            crate::ValidationErrorKind::SubcategoryHasRecords {
                name: name.to_string(),
                count: record_count,
            },
        ));
    }

    tracker_data.subcategories_by_id.remove(&amp;subcategory_id);
    tracker_data.subcategories_by_name.remove(&amp;name_lower);
    tracker_data.last_modified = chrono::Utc::now().to_rfc3339();

    <span class="hljs-keyword">let</span> tracker_json = serde_json::json!(tracker_data);
    write_json_to_file(&amp;tracker_json, &amp;<span class="hljs-keyword">mut</span> file)?;

    <span class="hljs-literal">Ok</span>(CliResponse::new(crate::ResponseContent::Message(<span class="hljs-built_in">format!</span>(
        <span class="hljs-string">"Subcategory '{}' deleted"</span>,
        name
    ))))
}
</code></pre>
<p>The <code>exec</code> function performs a safety check before removing any data. It first locates the ID of the target subcategory using the name provided by the user. Then, it iterates through the records vector in the tracker data to count if any records are currently linked to that ID. If the count is greater than zero, the operation stops and returns a <code>SubcategoryHasRecords</code> error, preventing you from accidentally creating "orphaned" records that point to a missing subcategory. If the check passes, the subcategory is removed from both HashMaps in <code>src/models.rs</code>.</p>
<h3 id="heading-rename-subcategories">Rename Subcategories</h3>
<p>Create <code>src/commands/subcategory/rename.rs</code>:</p>
<pre><code class="lang-rust"><span class="hljs-keyword">use</span> clap::{Arg, ArgMatches, Command};

<span class="hljs-keyword">use</span> crate::{
    CliError, CliResponse, CliResult, GlobalContext, TrackerData,
    utils::file::{FilePath, write_json_to_file},
    utils::parsers::parse_label,
};

<span class="hljs-keyword">pub</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">cli</span></span>() -&gt; Command {
    Command::new(<span class="hljs-string">"rename"</span>)
        .about(<span class="hljs-string">"Rename an existing subcategory"</span>)
        .arg(
            Arg::new(<span class="hljs-string">"old"</span>)
                .index(<span class="hljs-number">1</span>)
                .required(<span class="hljs-literal">true</span>)
                .value_parser(parse_label),
        )
        .arg(
            Arg::new(<span class="hljs-string">"new"</span>)
                .index(<span class="hljs-number">2</span>)
                .required(<span class="hljs-literal">true</span>)
                .value_parser(parse_label),
        )
}

<span class="hljs-keyword">pub</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">exec</span></span>(gctx: &amp;<span class="hljs-keyword">mut</span> GlobalContext, args: &amp;ArgMatches) -&gt; CliResult {
    <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut</span> file = gctx.tracker_path().open_read_write()?;
    <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut</span> tracker_data: TrackerData = serde_json::from_reader(&amp;file)?;

    <span class="hljs-keyword">let</span> old_name = args
        .get_one::&lt;<span class="hljs-built_in">String</span>&gt;(<span class="hljs-string">"old"</span>)
        .ok_or_else(|| CliError::Other(<span class="hljs-string">"Old subcategory name not provided"</span>.to_string()))?;
    <span class="hljs-keyword">let</span> new_name = args
        .get_one::&lt;<span class="hljs-built_in">String</span>&gt;(<span class="hljs-string">"new"</span>)
        .ok_or_else(|| CliError::Other(<span class="hljs-string">"New subcategory name not provided"</span>.to_string()))?;

    <span class="hljs-keyword">let</span> old_name_lower = old_name.to_lowercase();
    <span class="hljs-keyword">let</span> new_name_lower = new_name.to_lowercase();
    <span class="hljs-keyword">let</span> new_name_title = {
        <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut</span> chars = new_name_lower.chars();
        <span class="hljs-keyword">match</span> chars.next() {
            <span class="hljs-literal">None</span> =&gt; <span class="hljs-keyword">return</span> <span class="hljs-literal">Err</span>(CliError::Other(<span class="hljs-string">"Invalid new name"</span>.to_string())),
            <span class="hljs-literal">Some</span>(first) =&gt; first.to_uppercase().collect::&lt;<span class="hljs-built_in">String</span>&gt;() + &amp;chars.as_str().to_lowercase(),
        }
    };

    <span class="hljs-keyword">let</span> subcategory_id = tracker_data
        .subcategory_id(&amp;old_name_lower)
        .ok_or_else(|| {
            CliError::ValidationError(crate::ValidationErrorKind::SubcategoryNotFound {
                name: old_name.to_string(),
            })
        })?;

    <span class="hljs-keyword">if</span> tracker_data.subcategories_by_name.contains_key(&amp;new_name_lower) {
        <span class="hljs-keyword">return</span> <span class="hljs-literal">Err</span>(CliError::ValidationError(
            crate::ValidationErrorKind::SubcategoryAlreadyExists {
                name: new_name_title.clone(),
            },
        ));
    }

    tracker_data
        .subcategories_by_id
        .insert(subcategory_id, new_name_title.clone());
    tracker_data.subcategories_by_name.remove(&amp;old_name_lower);
    tracker_data
        .subcategories_by_name
        .insert(new_name_lower, subcategory_id);
    tracker_data.last_modified = chrono::Utc::now().to_rfc3339();

    <span class="hljs-keyword">let</span> tracker_json = serde_json::json!(tracker_data);
    write_json_to_file(&amp;tracker_json, &amp;<span class="hljs-keyword">mut</span> file)?;

    <span class="hljs-literal">Ok</span>(CliResponse::new(crate::ResponseContent::Message(<span class="hljs-built_in">format!</span>(
        <span class="hljs-string">"Subcategory renamed: '{}' → '{}'"</span>,
        old_name, new_name_title
    ))))
}
</code></pre>
<p>The <code>exec</code> function implements a "swap" logic to preserve record history. It first finds the numeric ID associated with the current name. Instead of changing any individual record, it simply removes the old name from the <code>subcategories_by_name</code> HashMap and inserts the new name with the same ID. This ensures that all existing records in <code>src/models.rs</code> immediately reflect the new name because they reference the subcategory by ID rather than by a string.</p>
<h2 id="heading-step-14-implement-the-total-command">Step 14: Implement the Total Command</h2>
<p>The <code>total</code> command will aggregate every transaction record in your JSON file to provide a clear view of your ledger's standing. It will sum up all income and expenses to show you exactly how your balance has changed since you initialized the tracker.</p>
<p>Create <code>src/commands/total.rs</code> and add the following code:</p>
<pre><code class="lang-rust"><span class="hljs-keyword">use</span> clap::{ArgMatches, Command};

<span class="hljs-keyword">use</span> crate::{
    CliError, CliResponse, CliResult, Currency, GlobalContext, Total, TrackerData,
    utils::file::FilePath,
};

<span class="hljs-keyword">pub</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">cli</span></span>() -&gt; Command {
    Command::new(<span class="hljs-string">"total"</span>)
        .about(<span class="hljs-string">"Display financial summary with totals"</span>)
}


<span class="hljs-keyword">pub</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">exec</span></span>(gctx: &amp;<span class="hljs-keyword">mut</span> GlobalContext, _args: &amp;ArgMatches) -&gt; CliResult {
  <span class="hljs-keyword">let</span> file = gctx.tracker_path().open_read()?;
  <span class="hljs-keyword">let</span> tracker_data: TrackerData = serde_json::from_reader(&amp;file)?;

  <span class="hljs-keyword">let</span> opening_balance = tracker_data.opening_balance;

  <span class="hljs-keyword">let</span> currency = tracker_data
    .currency
    .parse::&lt;Currency&gt;()
    .map_err(|e| CliError::Other(<span class="hljs-built_in">format!</span>(<span class="hljs-string">"Invalid currency in tracker data: {}"</span>, e)))?;

  <span class="hljs-keyword">let</span> (income_total, expenses_total) = tracker_data.totals();

  <span class="hljs-literal">Ok</span>(CliResponse::new(crate::ResponseContent::Total(Total {
    currency,
    opening_balance,
    income_total,
    expenses_total,
  })))
}
</code></pre>
<p>The <code>cli</code> function defines a straightforward interface without extra flags. It focuses entirely on processing the complete dataset.</p>
<p>The <code>exec</code> function first accesses the data file using the <code>open_read</code> helper. After parsing the JSON into the <code>TrackerData</code> struct, the logic calls the <code>totals()</code> method you implemented in <code>src/models.rs</code>. That method iterates through your records to return the raw sums of all income and expenses.</p>
<p>The <code>Total</code> struct contains the opening balance, income total, and expenses total. The net balance is calculated in the output module by adding <code>income_total</code> to the <code>opening_balance</code> and subtracting <code>expenses_total</code>. Finally you return a <code>CliResponse</code> which allows the output module to take these raw numbers and render them in the terminal.</p>
<h2 id="heading-step-15-wire-up-the-main-function">Step 15: Wire Up the Main Function</h2>
<p>This step brings every separate module back to the source. Until now, the models, error handling, and command logic existed as isolated parts. You will now create the <code>main.rs</code> file to establish the central entry point that connects these pieces, allowing the application to function as a unified binary.</p>
<p>First, update <code>src/lib.rs</code> to expose the internal modules:</p>
<pre><code class="lang-rust"><span class="hljs-keyword">pub</span> <span class="hljs-keyword">mod</span> commands;
<span class="hljs-keyword">pub</span> <span class="hljs-keyword">mod</span> error;
<span class="hljs-keyword">pub</span> <span class="hljs-keyword">mod</span> models;
<span class="hljs-keyword">pub</span> <span class="hljs-keyword">mod</span> output;
<span class="hljs-keyword">pub</span> <span class="hljs-keyword">mod</span> utils;

<span class="hljs-keyword">pub</span> <span class="hljs-keyword">use</span> error::*;
<span class="hljs-keyword">pub</span> <span class="hljs-keyword">use</span> models::*;
<span class="hljs-keyword">pub</span> <span class="hljs-keyword">use</span> utils::command_prelude;
<span class="hljs-keyword">pub</span> <span class="hljs-keyword">use</span> utils::context::GlobalContext;
<span class="hljs-keyword">pub</span> <span class="hljs-keyword">use</span> utils::parsers;
</code></pre>
<p>Next, create <code>src/main.rs</code>:</p>
<pre><code class="lang-rust"><span class="hljs-keyword">use</span> std::io;

<span class="hljs-keyword">use</span> clap::Command;
<span class="hljs-keyword">use</span> fintrack::{GlobalContext, commands};

<span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">main</span></span>() {
    <span class="hljs-keyword">let</span> exit_code = <span class="hljs-keyword">match</span> run() {
        <span class="hljs-literal">Ok</span>(_) =&gt; <span class="hljs-number">0</span>,
        <span class="hljs-literal">Err</span>(e) =&gt; {
            eprintln!(<span class="hljs-string">"Error: {}"</span>, e);
            <span class="hljs-number">1</span>
        }
    };
    std::process::exit(exit_code);
}

<span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">run</span></span>() -&gt; <span class="hljs-built_in">Result</span>&lt;(), <span class="hljs-built_in">String</span>&gt; {
    <span class="hljs-keyword">let</span> home_dir = dirs::home_dir()
        .ok_or_else(|| <span class="hljs-string">"Failed to determine home directory"</span>.to_string())?;

    <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut</span> gctx = GlobalContext::new(home_dir);

    <span class="hljs-keyword">let</span> matches = Command::new(<span class="hljs-string">"fintrack"</span>)
        .bin_name(<span class="hljs-string">"fintrack"</span>)
        .about(<span class="hljs-string">"A local-first CLI financial tracker for managing income and expenses"</span>)
        .version(<span class="hljs-built_in">env!</span>(<span class="hljs-string">"CARGO_PKG_VERSION"</span>))
        .subcommand_required(<span class="hljs-literal">true</span>)
        .subcommands(commands::cli())
        .get_matches();

    <span class="hljs-keyword">let</span> (cmd, args) = matches
        .subcommand()
        .expect(<span class="hljs-string">"subcommand required but not found"</span>);

    <span class="hljs-keyword">let</span> exec_fn = commands::build_exec(cmd)
        .ok_or_else(|| <span class="hljs-built_in">format!</span>(<span class="hljs-string">"Unknown command: {}"</span>, cmd))?;

    <span class="hljs-keyword">let</span> exec_result = exec_fn(&amp;<span class="hljs-keyword">mut</span> gctx, args);
    process_result(&amp;exec_result).expect(<span class="hljs-string">"An error occurred displaying response"</span>);

    <span class="hljs-literal">Ok</span>(())
}

<span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">process_result</span></span>(result: &amp;fintrack::CliResult) -&gt; io::<span class="hljs-built_in">Result</span>&lt;()&gt; {
    <span class="hljs-keyword">match</span> result {
        <span class="hljs-literal">Ok</span>(res) =&gt; res.write_to(&amp;<span class="hljs-keyword">mut</span> std::io::stdout()),
        <span class="hljs-literal">Err</span>(err) =&gt; err.write_to(&amp;<span class="hljs-keyword">mut</span> std::io::stderr()),
    }
}
</code></pre>
<p>The <code>main</code> function serves as the supervisor for the entire process. It triggers the <code>run</code> function and maps the final outcome to a standard system exit code. This informs the terminal whether the operation succeeded or encountered a failure.</p>
<p>The <code>run</code> function initiates a complete roundtrip through the architecture you built in previous steps. It starts by determining the user's home directory and passing it into <code>GlobalContext::new(home_dir)</code>. This instantiation creates the <code>gctx</code> object from Step 5, which determines the cross platform paths for the .fintrack folder and the tracker.json file.</p>
<p>When a user types a command like <code>fintrack add</code> in the terminal, the process begins by calling <code>commands::cli()</code>. This function, which you have defined in your central dispatcher <code>src/commands.rs</code> from Step 5, collects the list of all available subcommands (<code>init</code>, <code>add</code>, <code>list</code>, etc). It pulls the specific configuration for each command into a single clap instance so the terminal can understand the user intent.</p>
<p>If the user provides the correct input and arguments and <code>clap</code>'s validation is passed, it calls <code>commands::build_exec(cmd)</code> which uses the pattern matching logic also defined in Step 5. This function returns a pointer to the specific <code>exec</code> function for that command. For example, if the user typed <code>fintrack add ...</code>, it retrieves the exec function from <code>src/commands/add.rs</code>. The code then executes this function using a mutable reference to the <code>gctx</code> you just instantiated. This grants the command access to the file paths and data it needs.</p>
<p>The final execution phase happens in <code>process_result</code>. This function takes the <code>CliResult</code> returned by the command and calls the <code>write_to</code> method you defined earlier in the Step 6 output logic. Providing mutable references to <code>std::io::stdout()</code> for successes or <code>std::io::stderr()</code> for errors ensures the application prints the result or error message to the terminal.</p>
<h2 id="heading-test-your-application">Test Your Application</h2>
<p>You can build and test your application using cargo run. The double dash <code>--</code> tells Cargo to pass the following flags directly to your fintrack binary rather than interpreting them as Cargo arguments:</p>
<pre><code class="lang-bash">cargo build
cargo run -- init --currency USD --opening 1000
cargo run -- add income 500 --subcategory salary
cargo run -- add expenses 50 --subcategory groceries
cargo run -- list
cargo run -- total
</code></pre>
<h3 id="heading-install-the-binary">Install the Binary</h3>
<p>Running with <code>cargo run</code> is useful during development, but you can install the binary directly to your system to use the <code>fintrack</code> command globally. It will use <code>fintrack</code> because that's the value in the <code>name</code> field in your Cargo.toml file, which you set in the first step when you ran <code>cargo new fintrack</code>.</p>
<p>Run this to install <code>fintack</code> as a command:</p>
<pre><code class="lang-bash">cargo install --path .
</code></pre>
<p>When you run the installation command, Cargo compiles your code in release mode and places the executable in your Cargo bin folder (typically <code>~/.cargo/bin</code>). Once installed, the operating system recognizes <code>fintrack</code> as a standalone command. You can now call your application from any directory without prefixing it with <code>cargo</code>:</p>
<pre><code class="lang-bash">fintrack total
</code></pre>
<h2 id="heading-whats-next-and-advanced-features">What's Next and Advanced Features</h2>
<p>Congratulations! You've built a complete local-first CLI financial tracker. The application you've created includes:</p>
<ul>
<li><p>Data persistence in JSON format</p>
</li>
<li><p>Full CRUD operations for financial records</p>
</li>
<li><p>Subcategory management</p>
</li>
<li><p>Financial calculations</p>
</li>
<li><p>Comprehensive error handling</p>
</li>
<li><p>Type-safe command-line argument parsing</p>
</li>
</ul>
<h3 id="heading-advanced-features-to-explore">Advanced Features to Explore</h3>
<p>The modular architecture you built makes this tool easily extensible. To add new commands, you simply follow the pattern established in previous steps: create a new command module, define its logic, and register it within the <code>cli()</code> and <code>build_exec</code> functions in <code>src/commands.rs</code>.</p>
<p>Consider implementing these features to enhance the tool:</p>
<ul>
<li><p><strong>Export</strong>: Add an <code>export.rs</code> module to convert your JSON data into CSV format for analysis in spreadsheet applications.</p>
</li>
<li><p><strong>Describe</strong>: Create a command that uses terminal plotting libraries to generate visual charts of your spending patterns.</p>
</li>
<li><p><strong>Enhanced Output Formatting</strong>: Update <code>src/output.rs</code> with libraries like <code>colored</code> or <code>tabled</code> to add colors and professional borders to your terminal summaries.</p>
</li>
</ul>
<p>You can find the complete implementation of <code>fintrack</code> with all features, including advanced output formatting, export functionality, and more, in the <a target="_blank" href="https://github.com/steph-crown/fintrack">GitHub repository</a>. The repository also includes installation instructions for downloading the binary or installing via Cargo.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this tutorial, you've learned how to:</p>
<ul>
<li><p>Structure a Rust CLI application with proper error handling</p>
</li>
<li><p>Use traits to extend functionality</p>
</li>
<li><p>Work with JSON serialization</p>
</li>
<li><p>Parse and validate command-line arguments</p>
</li>
<li><p>Manage file I/O operations</p>
</li>
<li><p>Implement a complete data model with relationships</p>
</li>
</ul>
<p>The patterns you've learned here apply to many Rust applications. Traits, error handling with <code>Result</code>, and the ownership system are fundamental to writing idiomatic Rust code. These techniques ensure that as the application grows, the code remains maintainable and safe.</p>
<p>The modular nature of this tracker also means that the source code is now a template for any local-first tool. By swapping out the financial models for other data types, this same architecture can power a task manager, a personal wiki, or a time tracker.</p>
<p>Keep building, and happy tracking!</p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
