Rust๋กœ todo CLI ๋งŒ๋“ค๊ธฐ ๐Ÿ”ฅ

34538 ๋‹จ์–ด beginnersprogrammingrusttutorial
์—ฌ๊ธฐ์š”! ์ด ๊ธฐ์‚ฌ์—์„œ๋Š” Rust๋กœ to-do CLI ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๋นŒ๋“œํ•  ๊ฒƒ์ž…๋‹ˆ๋‹ค. ๋กœ์ปฌ JSON ํŒŒ์ผ์€ ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. ์•ฑ ๋ฏธ๋ฆฌ๋ณด๊ธฐ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.



์‹œ์ž‘ํ•˜์ž!

์‹œ์ž‘ํ•˜๊ธฐ



๋จผ์ € Cargo๋กœ ์ƒˆ ํ”„๋กœ์ ํŠธ๋ฅผ ๋งŒ๋“ญ๋‹ˆ๋‹ค.

cargo new todo


๊ทธ๋Ÿฐ ๋‹ค์Œ Cargo.toml ํŒŒ์ผ์— ๋‹ค์Œ ์ข…์†์„ฑ์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

chrono = "0.4.22"
colorize = "0.1.0"
rand = "0.8.5"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.85"


๊ฐ ์ข…์†์„ฑ์ด ์ˆ˜ํ–‰ํ•˜๋Š” ์ž‘์—…์€ ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.
  • chrono๋Š” ํ˜„์žฌ ๋‚ ์งœ์™€ ์‹œ๊ฐ„์„ ๊ฐ€์ ธ์˜ค๋Š” ๋ฐ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.
  • colorize๋Š” ์ถœ๋ ฅ ์ƒ‰์ƒ์„ ์ง€์ •ํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.
  • rand๋Š” ์ž„์˜์˜ ID๋ฅผ ์ƒ์„ฑํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.
  • serde ๋ฐ serde_json๋Š” JSON ํŒŒ์ผ์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๋ฐ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค.

  • ํด๋” ๊ตฌ์กฐ ๋งŒ๋“ค๊ธฐ


    src ํด๋”๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์Šต๋‹ˆ๋‹ค.

    src
     โ”ฃ app
     โ”ƒ โ”— mod.rs # The app module
     โ”ฃ structs
     โ”ƒ โ”— mod.rs # The structs
     โ”ฃ todo
     โ”ƒ โ”— mod.rs # Todo related functions
     โ”ฃ utils
     โ”ƒ โ”— mod.rs # Utility functions
     โ”— main.rs # The main file
    


    ์ด์ œ main.rs ํŒŒ์ผ์—์„œ ๋ชจ๋“  ๋ชจ๋“ˆ์„ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.

    mod utils;
    mod structs;
    mod todo;
    mod app;
    
    fn main() {
        // ...
    }
    


    ๊ตฌ์กฐ์ฒด ๋งŒ๋“ค๊ธฐ



    ์‹œ์ž‘ํ•˜๊ธฐ ์ „์— ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜๋Š” ๋ฐ ์‚ฌ์šฉํ•  ๊ตฌ์กฐ์ฒด๋ฅผ ๋งŒ๋“ค์–ด ๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค. structs/mod.rs ํŒŒ์ผ์—์„œ ๋‹ค์Œ ๊ตฌ์กฐ์ฒด๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.

    use serde::{Deserialize, Serialize};
    
    #[derive(Serialize, Deserialize, Debug, Clone)]
    pub struct Todo {
        pub created_at: String,
        pub title: String,
        pub done: bool,
        pub id: u32,
        pub updated_at: String,
    }
    
    #[derive(Serialize, Deserialize, Debug)]
    pub struct ConfigFile {
        pub data: Vec<Todo>,
    }
    


    ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜ ์ƒ์„ฑ


    utils/mod.rs ํŒŒ์ผ์—์„œ ์ข…์†์„ฑ์„ ๊ฐ€์ ธ์˜ค๊ณ  ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.

    use crate::structs;
    use chrono;
    use colorize::*;
    use rand::prelude::*;
    use serde_json::from_str;
    use serde_json::Result;
    use std::{fs, io::Write};
    


    ์ฒซ ๋ฒˆ์งธ ๊ธฐ๋Šฅ์€ ๊ธ€๋กœ๋ฒŒ ๋ฐ์ดํ„ฐ ํŒŒ์ผ์ด ์กด์žฌํ•˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ ์ƒ์„ฑํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

    pub fn init() {
        // Check if folder exists
        if !fs::metadata("C:\\.todobook").is_ok() {
            fs::create_dir("C:\\.todobook").unwrap(); // Create folder
    
            // Create file
            let mut file = fs::File::create(DATA_FILE).unwrap();
    
            // Write to file
            file.write_all(b"{\"data\":[]}").unwrap();
    
            println!("{} {}", "Created folder and file".green(), DATA_FILE);
        }
    
        // Check if file exists
        else if !fs::metadata(DATA_FILE).is_ok() {
            // Create file
            let mut file = fs::File::create(DATA_FILE).unwrap();
    
            // Write to file
            file.write_all(b"{\"data\":[]}").unwrap();
    
            println!("{} {}", "Created file".green(), DATA_FILE);
        }
    }
    


    ๋‹ค์Œ ๊ธฐ๋Šฅ์€ ๋ช…๋ น์ค„์—์„œ ์ธ์ˆ˜๋ฅผ ์ฝ๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ํ•จ์ˆ˜๋ฅผ ์ƒ์„ฑํ•˜๊ธฐ ์ „์— Command ๋ชจ๋“ˆ์—์„œ structs๋ผ๋Š” ๊ตฌ์กฐ์ฒด๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค.

    pub struct Command {
        pub command: String,
        pub arguments: String,
    }
    


    ์ด์ œ get_args ํ•จ์ˆ˜๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    pub fn get_args() -> structs::Command {
        let args = std::env::args().collect::<Vec<String>>(); // Get arguments and collect them into a vector
    
        let command = args.get(1).unwrap_or(&"".to_string()).to_string(); // Get command or set it to an empty string
        let arguments = args.get(2).unwrap_or(&"".to_string()).to_string(); // "" arguments or ""
    
        structs::Command { command, arguments } // Return the command and arguments
    }
    


    ๋‹ค์Œ ํ•จ์ˆ˜๋Š” ํƒ€์ž„์Šคํƒฌํ”„๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.

    pub fn get_timestamp() -> String {
        let now = chrono::Local::now();
        let timestamp = now.format("%m-%d %H:%M").to_string();
    
        timestamp
    }
    


    ๊ทธ๋Ÿฐ ๋‹ค์Œ ์ž„์˜์˜ ID๋ฅผ ์ƒ์„ฑํ•˜๋Š” ํ•จ์ˆ˜๋ฅผ ๋งŒ๋“ญ๋‹ˆ๋‹ค.

    pub fn get_id() -> u32 {
        // Genrate number between 1 and 1000
        let mut rng = rand::thread_rng();
        let id: u32 = rng.gen_range(1..1000);
    
        id + rng.gen_range(1..1000)
    }
    


    ๋‹ค์Œ ํ•จ์ˆ˜๋Š” JSON ํŒŒ์ผ์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์ฝ๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

    pub fn get_todos() -> Result<Vec<structs::Todo>> {
        let data = fs::read_to_string(DATA_FILE).unwrap();
        let todos: structs::ConfigFile = from_str(&data)?;
    
        Ok(todos.data)
    }
    


    ๋งˆ์ง€๋ง‰ ๊ธฐ๋Šฅ์€ ๋ฐ์ดํ„ฐ๋ฅผ JSON ํŒŒ์ผ์— ์“ฐ๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

    pub fn save_todos(todos: Vec<structs::Todo>) {
        let config_file = structs::ConfigFile { data: todos };
        let json = serde_json::to_string(&config_file).unwrap();
    
        let mut file = fs::File::create(DATA_FILE).unwrap();
        file.write_all(json.as_bytes()).unwrap();
    }
    


    ๊ทธ๋ฆฌ๊ณ  ์ด๊ฒƒ์ด ์œ ํ‹ธ๋ฆฌํ‹ฐ ๊ธฐ๋Šฅ์— ๋Œ€ํ•œ ๊ฒƒ์ž…๋‹ˆ๋‹ค.

    todo ํ•จ์ˆ˜ ๋งŒ๋“ค๊ธฐ



    ์ด์ œ ํ•  ์ผ์„ ์ถ”๊ฐ€, ์ œ๊ฑฐ ๋ฐ ๋‚˜์—ดํ•˜๋Š” ๋ฐ ์‚ฌ์šฉํ•  ํ•จ์ˆ˜๋ฅผ ๋งŒ๋“ญ๋‹ˆ๋‹ค. todo/mod.rs ํŒŒ์ผ์—์„œ ์ข…์†์„ฑ์„ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.

    use crate::structs::Todo;
    use crate::utils;
    use colorize::*;
    


    ์ฒซ ๋ฒˆ์งธ ๊ธฐ๋Šฅ์€ todo๋ฅผ ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

    pub fn add(title: String) {
        if title.len() < 1 { // Check if title is empty
            println!("{}", "No title provided".red());
    
            return;
        }
    
        let mut todos = utils::get_todos().unwrap(); // Get todos
    
        let todo = Todo {
            created_at: utils::get_timestamp(),
            title,
            done: false,
            id: utils::get_id(),
            updated_at: utils::get_timestamp(),
        };
    
        todos.push(todo); // Push todo to todos
    
        utils::save_todos(todos); // Save todos
    
        println!("{}", "Added todo".green());
    }
    


    ๋‹ค์Œ ๊ธฐ๋Šฅ์€ todos๋ฅผ ๋‚˜์—ดํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

    pub fn list() {
        let todos = utils::get_todos().unwrap();
    
        if todos.len() == 0 {
            println!("{}", "No todos".red());
            return;
        }
    
        println!(
            "{0: <5} | {1: <20} | {2: <20} | {3: <20} | {4: <20}",
            "ID", "Title", "Created at", "Updated at", "Done"
        );
    
        println!();
    
        for todo in todos {
            println!(
                "{0: <5} | {1: <20} | {2: <20} | {3: <20} | {4: <20}",
                todo.id,
                todo.title,
                todo.created_at,
                todo.updated_at,
                if todo.done { "Completed ๐Ÿ˜ธ".green() } else { "No ๐Ÿ˜ฟ".red() }
            );
        }
    }
    


    ๊ทธ๋Ÿฐ ๋‹ค์Œ ํ•  ์ผ์„ ์™„๋ฃŒ๋กœ ํ‘œ์‹œํ•˜๋Š” ํ•จ์ˆ˜๋ฅผ ๋งŒ๋“ญ๋‹ˆ๋‹ค.

    pub fn done(id: String) {
        let mut todos = utils::get_todos().unwrap();
        let id = id.parse::<u32>().unwrap_or(0);
    
        let exists = todos.iter().any(|todo| todo.id == id);
    
        if !exists {
            println!("{}", "Todo not found".red());
            return;
        }
    
        for todo in &mut todos {
            if todo.id == id {
                todo.done = true;
                todo.updated_at = utils::get_timestamp();
            }
        }
    
        utils::save_todos(todos);
    
        println!("{}", "Marked todo as done".green());
    }
    


    ๋‹ค์Œ ๊ธฐ๋Šฅ์€ todo๋ฅผ ์ œ๊ฑฐํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.

    pub fn remove(id: String) {
        let mut todos = utils::get_todos().unwrap();
        let id = id.parse::<u32>().unwrap_or(0);
    
        let exists = todos.iter().any(|todo| todo.id == id);
    
        if !exists {
            println!("{}", "Todo not found".red());
            return;
        }
    
        todos.retain(|todo| todo.id != id);
    
        utils::save_todos(todos);
    
        println!("{}", "Removed todo".green());
    }
    


    ์ด์ œ todo ์•ฑ์„ ๋งŒ๋“œ๋Š” ๋ฐ ํ•„์š”ํ•œ ๋ชจ๋“  ๊ธฐ๋Šฅ์ด ์žˆ์Šต๋‹ˆ๋‹ค. ํ†ตํ•ฉํ•˜๊ณ  ์ž‘๋™ํ•˜๋„๋ก ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

    ๊ธฐ๋Šฅ ํ†ตํ•ฉ


    app/mod.rs ํŒŒ์ผ์—์„œ ์ข…์†์„ฑ์„ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.

    use crate::todo::*;
    use crate::utils;
    use colorize::*;
    

    main.rs ํŒŒ์ผ์—์„œ ํ˜ธ์ถœ๋  ์‹œ์ž‘ ํ•จ์ˆ˜๋ฅผ ๋‚ด๋ณด๋ƒ…๋‹ˆ๋‹ค.

    pub fn start() {
        // ...
    }
    


    ์กด์žฌํ•˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ ๋จผ์ € ๋ฐ์ดํ„ฐ ํŒŒ์ผ์„ ํ™•์ธํ•˜๊ณ  ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.

    utils::init();
    


    ๊ทธ๋Ÿฐ ๋‹ค์Œ ๋ช…๋ น๊ณผ ์ธ์ˆ˜๋ฅผ ์–ป์Šต๋‹ˆ๋‹ค.

    let args = utils::get_args();
    


    ๊ทธ๋Ÿฐ ๋‹ค์Œ ๋ช…๋ น์„ ์ผ์น˜์‹œํ‚ค๊ณ  ์ ์ ˆํ•œ ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค.

    match args.command.as_str() {
        "a" => add(args.arguments),
        "l" => list(),
        "d" => done(args.arguments),
        "r" => remove(args.arguments),
        "q" => std::process::exit(0),
        _ => {
            /// SHOW HELP
        }
    }
    


    ๋„์›€์€ ์ด๋ ‡๊ฒŒ ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค.

    println!("{}", "            No command found - Showing help".black());
    
    let help = format!(
                    "
                {} {}
                {}
                -----
    
                Help:
    
                Command   | Arguments | Description
                {}           text        Add a new todo
                {}                       List all todos
                {}           id          Mark a todo as done
                {}           id          Delete a todo
            ",
            "Welcome to".grey(),
            "TodoBook".cyan(),
            "Simple todo app written in Rust".black(),
            "a".cyan(),
            "l".blue(),
            "d".green(),
            "r".red()
    );
    
    println!("{help}");
    


    ์ด์ œ main.rs ํŒŒ์ผ์—์„œ ๋‹ค์Œ ํ•จ์ˆ˜ ํ˜ธ์ถœ์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค.

    fn main() {
        app::start();
    }
    


    ์ด์ œ cargo run๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์•ฑ์„ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์Œ๊ณผ ๊ฐ™์€ ๋‚ด์šฉ์ด ํ‘œ์‹œ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.



    ๊ฒฐ๋ก 



    ์ฝ์–ด ์ฃผ์…”์„œ ๊ฐ์‚ฌํ•ฉ๋‹ˆ๋‹ค. ์ด ํŠœํ† ๋ฆฌ์–ผ์„ ์ฆ๊ฒผ๊ธฐ๋ฅผ ๋ฐ”๋ž๋‹ˆ๋‹ค. ์งˆ๋ฌธ์ด ์žˆ์œผ์‹œ๋ฉด ์˜๊ฒฌ์— ์ž์œ ๋กญ๊ฒŒ ์งˆ๋ฌธํ•˜์‹ญ์‹œ์˜ค. ์†Œ์Šค ์ฝ”๋“œhere๋„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

    ์ข‹์€ ์›นํŽ˜์ด์ง€ ์ฆ๊ฒจ์ฐพ๊ธฐ