Reading User Input

Rust standard library provides an easy way of reading from and writing to the command-line commonly known as stdout. First, create a file in the src folder called user_input.rs.

File: src/user_input.rs

use crate::{
    format_todos, MemDB, ADD_COMMAND, DONE_COMMAND, EDIT_COMMAND, EXIT_COMMAND, NUMBER, TITLE,
    UNDO_COMMAND,
};
use async_std::io;

pub async fn read_line(
    buffer: &mut String,
    fruits_list: &Vec<String>,
    memdb: &MemDB,
    //todo_list: &Vec<String>,
) -> anyhow::Result<String> {
    crate::clear_terminal();
    buffer.clear();
    println!("+--------------------------+");
    println!("+ {:^5}{:17}+", "COMMANDS", " ");
    println!("+{:26}+", " ");
    println!("→   {ADD_COMMAND:5}{:18}+", " ");
    println!("→   {DONE_COMMAND:23}+");
    println!("→   {UNDO_COMMAND:23}+");
    println!("→   {EDIT_COMMAND:23}+");
    println!("→   {EXIT_COMMAND:23}+");
    println!("+{:26}+", " ");
    println!("+--------------------------+");

    println!("{NUMBER}| {TITLE:10}");
    println!("----------------");
    for (mut index, item) in fruits_list.iter().enumerate() {
        index += 1;
        println!("{index:2} | {item:10}");
    }
    println!("--------------------------------------------");
    format_todos(&memdb).await;

    println!("Enter a fruit that is available.",);
    let stdin = io::stdin(); // We get `Stdin` here.
    stdin.read_line(buffer).await?;

    Ok(buffer.to_owned())
}

read_line() is responsible for reading stdout for the user input and returning the user input as a String. It always clears the terminal using utils::clear_terminal(); before the next input, clears the buffer to prevent stale commands using buffer.clear(), lists the list of fruits that the user can add and formats the TODOs printing the sorted TODO list and a set of commands that the user can input to interact with the client.

User Input Handler

To handle the input create a file in the src directory called handler.rs

File: src/handler.rs

use crate::{
    convert_case, create_new_user, done, edit, get_fruits, get_user_remote_storage,
    load_sqlite_cache, loading, read_line, split_words, store, synching, undo,
    update_remote_storage, MemDB,
};
use async_std::io;
use sea_orm::DatabaseConnection;
use std::collections::HashMap;

pub async fn input_handler(db: &DatabaseConnection) -> anyhow::Result<()> {
    let mut username_buffer = String::default();
    println!("What is Your Username...",);
    let stdin = io::stdin(); // We get `Stdin` here.
    stdin.read_line(&mut username_buffer).await?;
    let username = username_buffer.trim().to_string();

    let fruits_list: Vec<String> = get_fruits().await?;

    let mut buffer = String::new();
    let mut text_buffer: String;
    let mut memdb = MemDB::new(HashMap::default());
    loading();
    load_sqlite_cache(db, &mut memdb).await?;

    let remote_result = get_user_remote_storage(&username).await?;
    if let Some(result_data) = remote_result {
        if result_data == "USER_NOT_FOUND" {
            create_new_user(&username).await?;
        }
    }

    loop {
        read_line(&mut buffer, fruits_list.as_ref(), &memdb).await?;
        buffer = buffer.trim().to_owned();
        let words = split_words(buffer.clone());
        let command = words[0].to_lowercase().to_string();
        let mut quantity: &str = "";
        if command.as_str() == "done" || command.as_str() == "undo" {
            text_buffer = convert_case(&words[1]);
        } else if command.as_str() == "exit" {
            update_remote_storage(&memdb, &username).await?;
            println!("SYNCED SUCCESSFULLY.");
            println!("Bye! :)");
            break;
        } else {
            quantity = &words[1];
            text_buffer = convert_case(&words[2]);
        }

        if !text_buffer.is_empty() {
            match fruits_list.iter().find(|&fruit| *fruit == text_buffer) {
                None => {
                    if !text_buffer.is_empty() {
                        println!("The fruit `{buffer}` is not available.\n",);
                    }
                    continue;
                }
                Some(_) => {
                    if command.as_str() == "add" {
                        if memdb.lock().await.contains_key(&text_buffer) {
                            continue;
                            //TODO
                        } else {
                            synching();
                            store(&db, quantity, &text_buffer).await?;
                            load_sqlite_cache(&db, &mut memdb).await?;
                        }
                    } else if command.as_str() == "edit" {
                        if let Some(mut todo_model) = memdb.lock().await.get_mut(&text_buffer) {
                            if todo_model.status != 1 {
                                synching();
                                edit(&db, todo_model, quantity.to_owned()).await?;
                                todo_model.quantity = quantity.to_owned();
                            }
                        } else {
                            continue;
                        }
                    } else if command.as_str() == "done" {
                        if let Some(todo_model) = memdb.lock().await.get_mut(&text_buffer) {
                            if todo_model.status == 0 {
                                synching();
                                let updated_model = done(&db, todo_model).await?;
                                *todo_model = updated_model;
                            }
                            continue;
                        } else {
                            continue;
                        }
                    } else if command.as_str() == "undo" {
                        if let Some(todo_model) = memdb.lock().await.get_mut(&text_buffer) {
                            if todo_model.status == 1 {
                                synching();
                                let updated_model = undo(&db, todo_model).await?;
                                *todo_model = updated_model;
                            }
                            continue;
                        } else {
                            continue;
                        }
                    } else {
                        dbg!("Unsupported Command");
                        break;
                    }
                }
            }
        }
    }

    Ok(())
}

The code block above is nested and there are comments to help understanding it. Simply, it:

1. reads the `username`
1. looks up the `username` from the remote PostgreSQL database
1. Loads the local TODO list cache from the local SQLite database
1. Stores the loaded local TODO list cache into `MemDB` in-memory database
1. reads `stdin` for user input into a `buffer`
1. splits the buffer into individual constituents and stores them in an array
1. reads the first index of the array to get the command
1. performs conditional operations on the command and performs the necessary database operations
1. If the command it not available it exits the program
1. If the fruit provided is not available, it clears the buffer and reads `stdin` again
1. if the command is `EXIT` , it syncs the local SQLite cache with the remote PostgreSQL database and exits.

Lastly, import the modules into src/main.rs

File: src/main.rs

  mod common;
  mod db_ops;
+ mod handler;
  mod todo_list_table;
+ mod user_input;
  mod utils;

  pub use common::*;
  pub use db_ops::*;
+ pub use handler::*;
  pub use todo_list_table::prelude::*;
+ pub use user_input::*;
  pub use utils::*;

#[async_std::main]
async fn main() -> anyhow::Result<()> {
    let db = database_config().await?;
    create_todo_table(&db).await?;

+	input_handler(&db).await?;

    Ok(())
}

Running the Client and Server

Running both the todo-server in the TODO-Server directory prints

$ ../target/debug/todo-server
`CREATE TABLE fruits` "Operation Successful"
`CREATE TABLE todos` "Operation Successful"
Listening on 127.0.0.1:8080

Running the todo-client in the current directory prints.

$ Running `target/debug/todo_client`
`CREATE TABLE todo_list` "Operation Successful"
What is Your Username...

Enter a username like user001

This creates a new user in the PostgreSQL database since the user currently does not exist. Querying the PostgreSQL database prints

fruits_market=# SELECT * FROM todos;
 todo_id | username | todo_list 
---------+----------+-----------
       2 | user001  | 
(1 row)

The client then prints a list of fruits, commands and a TODO section:

+--------------------------+
+ COMMANDS                 +
+                          +
→   ADD                    +
→   DONE                   +
→   UNDO                   +
→   EDIT                   +
→   EXIT                   +
+                          +
+--------------------------+
No.| FRUITS AVAILABLE
----------------
 1 | Apple     
 2 | Orange    
 3 | Mango     
 4 | Pineapple 
--------------------------------------------




Oh My! There are no TODOs
Enter a fruit that is available.

Adding a fruit, like ADD 5kg Apple prints:

+--------------------------+
+ COMMANDS                 +
+                          +
→   ADD                    +
→   DONE                   +
→   UNDO                   +
→   EDIT                   +
→   EXIT                   +
+                          +
+--------------------------+
No.| FRUITS AVAILABLE
----------------
 1 | Apple     
 2 | Orange    
 3 | Mango     
 4 | Pineapple 
--------------------------------------------




QUANTITY | NOT DONE  
----------------
     5kg | Apple     
----------------

----------------
Bummer :( You Have Not Completed Any TODOs!
----------------


Enter a fruit that is available.

A NOT DONE table is added and below that the statement Bummer :( You Have Not Completed Any TODOs! is printed showing that we have TODOs that are not done yet.

Add another fruit like ADD 1kg OraNGe will print:

+--------------------------+
+ COMMANDS                 +
+                          +
→   ADD                    +
→   DONE                   +
→   UNDO                   +
→   EDIT                   +
→   EXIT                   +
+                          +
+--------------------------+
No.| FRUITS AVAILABLE
----------------
 1 | Apple     
 2 | Orange    
 3 | Mango     
 4 | Pineapple 
--------------------------------------------




QUANTITY | NOT DONE  
----------------
     5kg | Apple     
     1kg | Orange    
----------------

----------------
Bummer :( You Have Not Completed Any TODOs!
----------------


Enter a fruit that is available.

Here, even though the fruit Orange is typed as OraNGe, it is still added since we handle this in the code using convert_case() function.

Now, edit the orange from 1Kg to 3kg with EDIT 3kg Orange. This prints:

+--------------------------+
+ COMMANDS                 +
+                          +
→   ADD                    +
→   DONE                   +
→   UNDO                   +
→   EDIT                   +
→   EXIT                   +
+                          +
+--------------------------+
No.| FRUITS AVAILABLE
----------------
 1 | Apple     
 2 | Orange    
 3 | Mango     
 4 | Pineapple 
--------------------------------------------




QUANTITY | NOT DONE  
----------------
     5kg | Apple     
     3kg | Orange    
----------------

----------------
Bummer :( You Have Not Completed Any TODOs!
----------------


Enter a fruit that is available.

Next, mark the Apple TODO as done using DONE apple. This prints:

+--------------------------+
+ COMMANDS                 +
+                          +
→   ADD                    +
→   DONE                   +
→   UNDO                   +
→   EDIT                   +
→   EXIT                   +
+                          +
+--------------------------+
No.| FRUITS AVAILABLE
----------------
 1 | Apple     
 2 | Orange    
 3 | Mango     
 4 | Pineapple 
--------------------------------------------




QUANTITY | NOT DONE  
----------------
     3kg | Orange    
----------------

QUANTITY | DONE TODOS
----------------
     5kg | Apple     
----------------

Enter a fruit that is available.

A DONE TODOS table is created with the Apple as a member.

Next, mark the Apple as undone with UNDO Apple. This prints:

+--------------------------+
+ COMMANDS                 +
+                          +
→   ADD                    +
→   DONE                   +
→   UNDO                   +
→   EDIT                   +
→   EXIT                   +
+                          +
+--------------------------+
No.| FRUITS AVAILABLE
----------------
 1 | Apple     
 2 | Orange    
 3 | Mango     
 4 | Pineapple 
--------------------------------------------




QUANTITY | NOT DONE  
----------------
     5kg | Apple     
     3kg | Orange    
----------------

----------------
Bummer :( You Have Not Completed Any TODOs!
----------------


Enter a fruit that is available.


The Apple is moved back to the NOT DONE table and since there are no DONE TODOs, the DONE TODO table is replaced by Bummer :( You Have Not Completed Any TODOs! .

Next, complete all TODOs by marking both the Orange and Apple as done with:

1. `DONE Apple`
1. `DONE orange`

This prints:

+--------------------------+
+ COMMANDS                 +
+                          +
→   ADD                    +
→   DONE                   +
→   UNDO                   +
→   EDIT                   +
→   EXIT                   +
+                          +
+--------------------------+
No.| FRUITS AVAILABLE
----------------
 1 | Apple     
 2 | Orange    
 3 | Mango     
 4 | Pineapple 
--------------------------------------------




Wohooo! All TODOs are Completed.
QUANTITY | DONE TODOS
----------------
     5kg | Apple     
     3kg | Orange    
----------------

Enter a fruit that is available.



All TODOs are moved to the DONE TODOS table and the NOT DONE table is replaced by Wohooo! All TODOs are Completed. since all TODOs are done. This proves that our logic works.

Lastly, exit the todo-client gracefully with the command EXIT. This syncs the in-memory database to the remote PostgreSQL server and then exits the program. It prints:

SYNCING TO SERVER...
SYNCED SUCCESSFULLY.
Bye! :)

The state of the SQLite cache is:

sqlite> SELECT * FROM todo_list ;
1|Apple|5kg|1
2|Orange|3kg|1
sqlite> 

The state of the PostgreSQL server is:

fruits_market=# SELECT * FROM todos;
 todo_id | username |                                                                        todo_list                                                                         
---------+----------+----------------------------------------------------------------------------------------------------------------------------------------------------------
       2 | user001  | {"queued":[],"completed":[{"todo_id":2,"todo_name":"Orange","quantity":"3kg","status":1},{"todo_id":1,"todo_name":"Apple","quantity":"5kg","status":1}]}
(1 row)

This shows that the TODO list has been successfully synced to remote storage. Running the client again with the same username user001 should print the DONE TODOS from the persisted SQLite cache:

+--------------------------+
+ COMMANDS                 +
+                          +
→   ADD                    +
→   DONE                   +
→   UNDO                   +
→   EDIT                   +
→   EXIT                   +
+                          +
+--------------------------+
No.| FRUITS AVAILABLE
----------------
 1 | Apple     
 2 | Orange    
 3 | Mango     
 4 | Pineapple 
--------------------------------------------




Wohooo! All TODOs are Completed.
QUANTITY | DONE TODOS
----------------
     5kg | Apple     
     3kg | Orange    
----------------

Enter a fruit that is available.


That's it for this tutorial. :)