altar 0.1.0

A TUI library in the style of SwiftUI
Documentation
use altar::*;
use tokio::sync::mpsc;
use uuid::*;

#[derive(Debug, Clone, PartialEq, Eq)]
struct Todo {
    id: Uuid,
    name: String,
    is_complete: bool,
}

impl Todo {
    fn new(name: &str) -> Self {
        Self {
            id: Uuid::new_v4(),
            name: name.to_string(),
            is_complete: false,
        }
    }
}

struct TodoApp {
    todos: Vec<Todo>,
    input: String,
    mode: AppMode,
    todo_index: usize,
}

#[derive(Debug, Clone, PartialEq, Eq)]
enum AppMode {
    Viewing,
    Adding,
}

enum Message {}

impl TodoApp {
    fn handle_key_event(
        &mut self,
        key_event: KeyEvent,
        tx: &mpsc::UnboundedSender<Message>,
    ) -> bool {
        match self.mode {
            AppMode::Viewing => self.handle_viewing_mode(key_event, tx),
            AppMode::Adding => self.handle_adding_mode(key_event),
        }
    }

    fn handle_viewing_mode(
        &mut self,
        key_event: KeyEvent,
        _tx: &mpsc::UnboundedSender<Message>,
    ) -> bool {
        match key_event.code {
            KeyCode::Char('q') => return false,
            KeyCode::Char('n') => self.mode = AppMode::Adding,
            KeyCode::Up => {
                if self.todo_index > 0 {
                    self.todo_index -= 1;
                }
            }
            KeyCode::Down => {
                if self.todo_index < self.todos.len().saturating_sub(1) {
                    self.todo_index += 1;
                }
            }
            KeyCode::Char(' ') => {
                if !self.todos.is_empty() {
                    let todo = &mut self.todos[self.todo_index];
                    todo.is_complete = !todo.is_complete;
                }
            }
            _ => {}
        }
        true
    }

    fn handle_adding_mode(&mut self, key_event: KeyEvent) -> bool {
        match key_event.code {
            KeyCode::Char(c) => self.input.push(c),
            KeyCode::Backspace => {
                self.input.pop();
            }
            KeyCode::Enter => {
                if !self.input.is_empty() {
                    self.todos.insert(0, Todo::new(&self.input));
                    self.input.clear();
                    self.mode = AppMode::Viewing;
                }
            }
            KeyCode::Esc => {
                self.input.clear();
                self.mode = AppMode::Viewing;
            }
            _ => {}
        }
        true
    }

    fn render_todo(&self, index: usize, todo: &Todo) -> impl View {
        let is_selected = index == self.todo_index && self.mode == AppMode::Viewing;

        let cursor = if_then_view(
            is_selected,
            RenderCounter {}.blue(),
            RenderCounter {}.green(),
        );

        hstack((
            cursor,
            text(todo.name.clone()).strikethrough_when(todo.is_complete),
        ))
        .dim_when(todo.is_complete)
        .bold_when(is_selected)
    }

    fn input_view(&self) -> impl View {
        hstack((
            text("+"), //
            text(self.input.clone()),
        ))
        .green()
        .bold()
        .visible(self.mode == AppMode::Adding)
    }

    fn render_todos(&self) -> impl View {
        vstack((
            self.input_view(),
            vstack(
                self.todos
                    .iter()
                    .enumerate()
                    .map(|(index, todo)| self.render_todo(index, todo).id(todo.id))
                    .collect::<Vec<_>>(),
            ),
        ))
    }
}

impl AsyncTerminalApp for TodoApp {
    type Message = Message;

    fn render(&self) -> impl View {
        let todos = self.render_todos();

        fn shortcut_text(key: &str, description: &str) -> impl View {
            hstack((
                text(key).bold(), //
                text(description).dim(),
            ))
            .blue()
        }

        let commands_view = hstack((
            shortcut_text("n", "new"),
            shortcut_text("space", "toggle"),
            shortcut_text("Up/Down", "navigate"),
            shortcut_text("q", "quit"),
        ))
        .spacing(2);

        vstack((
            altar::view::RenderCounter {},
            todos.fill(),
            commands_view,
        ))
        .border()
        .title(" TODOS ")
    }

    fn update(
        &mut self,
        event: Event<Self::Message>,
        tx: &mpsc::UnboundedSender<Self::Message>,
    ) -> bool {
        match event {
            Event::Key(key_event) => self.handle_key_event(key_event, tx),
            Event::Message(_) => true,
        }
    }

    fn handle_exit(&self) -> Option<impl View> {
        Some(text("You quit the app!"))
    }
}

#[tokio::main]
async fn main() {
    let mut app = TodoApp {
        todos: vec![Todo::new("Buy Milk"), Todo::new("Buy Bread")],
        input: String::new(),
        mode: AppMode::Viewing,
        todo_index: 0,
    };

    app.run(true).await;
}