mirror of
https://codeberg.org/JasterV/teatui.git
synced 2026-04-26 18:10:03 +00:00
feat: add new example for working with lists
This commit is contained in:
parent
92118d929e
commit
4d7ba57f96
7 changed files with 635 additions and 0 deletions
10
Cargo.lock
generated
10
Cargo.lock
generated
|
|
@ -1304,6 +1304,16 @@ version = "0.1.8"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
|
||||
|
||||
[[package]]
|
||||
name = "todo_list"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"crossterm",
|
||||
"ratatui",
|
||||
"strum",
|
||||
"teatui",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.49.0"
|
||||
|
|
|
|||
12
examples/todo-list/Cargo.toml
Normal file
12
examples/todo-list/Cargo.toml
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
[package]
|
||||
name = "todo_list"
|
||||
publish = false
|
||||
version = "0.0.0"
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
|
||||
[dependencies]
|
||||
crossterm.workspace = true
|
||||
ratatui.workspace = true
|
||||
strum = "0.27.2"
|
||||
teatui = { path = "../../teatui" }
|
||||
18
examples/todo-list/src/main.rs
Normal file
18
examples/todo-list/src/main.rs
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
//! # [TeaTui] List example
|
||||
|
||||
use crate::model::Model;
|
||||
use message::Message;
|
||||
use teatui::ProgramError;
|
||||
|
||||
mod message;
|
||||
mod model;
|
||||
mod update;
|
||||
mod view;
|
||||
|
||||
fn main() -> Result<(), ProgramError<Model, Message, ()>> {
|
||||
teatui::start(init, update::update, view::view, |_, _| None)
|
||||
}
|
||||
|
||||
fn init() -> (Model, Option<()>) {
|
||||
(Model::default(), None)
|
||||
}
|
||||
75
examples/todo-list/src/message.rs
Normal file
75
examples/todo-list/src/message.rs
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Message {
|
||||
NoOp,
|
||||
Exit,
|
||||
SelectNext,
|
||||
SelectNone,
|
||||
SelectPrevious,
|
||||
SelectFirst,
|
||||
SelectLast,
|
||||
ToggleStatus,
|
||||
}
|
||||
|
||||
impl From<crossterm::event::Event> for Message {
|
||||
fn from(value: crossterm::event::Event) -> Self {
|
||||
match value {
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Esc | KeyCode::Char('q'),
|
||||
kind: KeyEventKind::Press,
|
||||
state: _,
|
||||
modifiers: _,
|
||||
}) => Self::Exit,
|
||||
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('l') | KeyCode::Char(' ') | KeyCode::Right,
|
||||
kind: KeyEventKind::Press,
|
||||
state: _,
|
||||
modifiers: _,
|
||||
}) => Self::ToggleStatus,
|
||||
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('h') | KeyCode::Left,
|
||||
kind: KeyEventKind::Press,
|
||||
state: _,
|
||||
modifiers: _,
|
||||
}) => Self::SelectNone,
|
||||
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('j') | KeyCode::Down,
|
||||
kind: KeyEventKind::Press,
|
||||
state: _,
|
||||
modifiers: _,
|
||||
}) => Self::SelectNext,
|
||||
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('k') | KeyCode::Up,
|
||||
kind: KeyEventKind::Press,
|
||||
state: _,
|
||||
modifiers: _,
|
||||
}) => Self::SelectPrevious,
|
||||
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('g') | KeyCode::Home,
|
||||
kind: KeyEventKind::Press,
|
||||
state: _,
|
||||
modifiers: _,
|
||||
}) => Self::SelectFirst,
|
||||
|
||||
Event::Key(KeyEvent {
|
||||
code: KeyCode::Char('G') | KeyCode::End,
|
||||
kind: KeyEventKind::Press,
|
||||
state: _,
|
||||
modifiers: _,
|
||||
}) => Self::SelectLast,
|
||||
|
||||
Event::FocusGained
|
||||
| Event::FocusLost
|
||||
| Event::Key(_)
|
||||
| Event::Mouse(_)
|
||||
| Event::Paste(_)
|
||||
| Event::Resize(_, _) => Self::NoOp,
|
||||
}
|
||||
}
|
||||
}
|
||||
364
examples/todo-list/src/model.rs
Normal file
364
examples/todo-list/src/model.rs
Normal file
|
|
@ -0,0 +1,364 @@
|
|||
use ratatui::widgets::ListState;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Model {
|
||||
pub items: Vec<TodoItem>,
|
||||
pub state: ListState,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct TodoItem {
|
||||
pub todo: String,
|
||||
pub info: String,
|
||||
pub status: Status,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum Status {
|
||||
Todo,
|
||||
Completed,
|
||||
}
|
||||
|
||||
impl Default for Model {
|
||||
fn default() -> Self {
|
||||
Model::from_iter([
|
||||
(
|
||||
Status::Todo,
|
||||
"Rewrite everything with Rust!",
|
||||
"I can't hold my inner voice. He tells me to rewrite the complete universe with Rust",
|
||||
),
|
||||
(
|
||||
Status::Completed,
|
||||
"Rewrite all of your tui apps with Ratatui",
|
||||
"Yes, you heard that right. Go and replace your tui with Ratatui.",
|
||||
),
|
||||
(
|
||||
Status::Todo,
|
||||
"Pet your cat",
|
||||
"Minnak loves to be pet by you! Don't forget to pet and give some treats!",
|
||||
),
|
||||
(
|
||||
Status::Todo,
|
||||
"Walk with your dog",
|
||||
"Max is bored, go walk with him!",
|
||||
),
|
||||
(
|
||||
Status::Completed,
|
||||
"Pay the bills",
|
||||
"Pay the train subscription!!!",
|
||||
),
|
||||
(
|
||||
Status::Completed,
|
||||
"Refactor list example",
|
||||
"If you see this info that means I completed this task!",
|
||||
),
|
||||
(
|
||||
Status::Todo,
|
||||
"Implement a neural network in pure Rust",
|
||||
"Because why use Python when you can have zero-cost abstractions and borrow checker headaches?",
|
||||
),
|
||||
(
|
||||
Status::Todo,
|
||||
"Buy more coffee",
|
||||
"The compile times are getting longer, or maybe I'm just getting slower. Either way, caffeine is required.",
|
||||
),
|
||||
(
|
||||
Status::Completed,
|
||||
"Fix that one annoying lifetime error",
|
||||
"It only took three days and a sacrifice to the crab god, but it finally compiles!",
|
||||
),
|
||||
(
|
||||
Status::Todo,
|
||||
"Clean the mechanical keyboard",
|
||||
"There is a non-zero amount of crumbs between the blue switches. It’s affecting my WPM.",
|
||||
),
|
||||
(
|
||||
Status::Todo,
|
||||
"Read the 'Rustonomicon'",
|
||||
"Into the dark depths of unsafe Rust we go. May the pointer aliasing rules have mercy.",
|
||||
),
|
||||
(
|
||||
Status::Completed,
|
||||
"Hydrate",
|
||||
"Drank a full glass of water. Look at me, practicing self-care while writing low-level code.",
|
||||
),
|
||||
(
|
||||
Status::Todo,
|
||||
"Argue about memory safety on the internet",
|
||||
"Someone said C is 'fine.' I must cordially explain why they are mistaken.",
|
||||
),
|
||||
(
|
||||
Status::Todo,
|
||||
"Organize the .dotfiles",
|
||||
"Spend 4 hours configuring Neovim instead of actually working on the project.",
|
||||
),
|
||||
(
|
||||
Status::Completed,
|
||||
"Update dependencies",
|
||||
"Ran 'cargo update'. Only 14 breaking changes in the ecosystem today. A new record!",
|
||||
),
|
||||
(
|
||||
Status::Todo,
|
||||
"Research async-trait internals",
|
||||
"Why is it a macro? Why is life a macro? I need to know.",
|
||||
),
|
||||
(
|
||||
Status::Todo,
|
||||
"Go to the gym",
|
||||
"Strong body, strong memory management. Don't let your muscles leak like a C++ string.",
|
||||
),
|
||||
(
|
||||
Status::Completed,
|
||||
"Explain ownership to a rubber duck",
|
||||
"The duck didn't get it at first, but after a few squeaks, it finally understood move semantics.",
|
||||
),
|
||||
(
|
||||
Status::Todo,
|
||||
"Check for compiler updates",
|
||||
"Is there a new nightly? I need those experimental features for no reason at all.",
|
||||
),
|
||||
(
|
||||
Status::Todo,
|
||||
"Debug the deadlocked thread",
|
||||
"It's not a bug, it's just a very dedicated pause in execution.",
|
||||
),
|
||||
(
|
||||
Status::Completed,
|
||||
"Star every crate I use on GitHub",
|
||||
"Showing love to the maintainers who keep my 'cargo build' alive.",
|
||||
),
|
||||
(
|
||||
Status::Todo,
|
||||
"Write a custom proc-macro",
|
||||
"I want to generate code that generates code. We need to go deeper.",
|
||||
),
|
||||
(
|
||||
Status::Todo,
|
||||
"Finally learn how Pin works",
|
||||
"I've read the docs five times. Maybe the sixth time is the charm?",
|
||||
),
|
||||
(
|
||||
Status::Completed,
|
||||
"Delete the 'node_modules' folder",
|
||||
"Regained 40GB of disk space. Nature is healing.",
|
||||
),
|
||||
(
|
||||
Status::Todo,
|
||||
"Optimize the Docker image size",
|
||||
"From 1.2GB to 10MB using multi-stage builds and Alpine. Efficiency feels good.",
|
||||
),
|
||||
(
|
||||
Status::Todo,
|
||||
"Fix the CI/CD pipeline",
|
||||
"It works on my machine, but GitHub Actions thinks otherwise.",
|
||||
),
|
||||
(
|
||||
Status::Completed,
|
||||
"Replace a 5-line bash script with 200 lines of Rust",
|
||||
"Was it worth it? Yes. It's type-safe now.",
|
||||
),
|
||||
(
|
||||
Status::Todo,
|
||||
"Actually write documentation",
|
||||
"The code is the documentation... said no one ever (and lived).",
|
||||
),
|
||||
(
|
||||
Status::Todo,
|
||||
"Try a different terminal emulator",
|
||||
"Is Alacritty faster than Kitty? Let the benchmarking begin.",
|
||||
),
|
||||
(
|
||||
Status::Completed,
|
||||
"Nap for 20 minutes",
|
||||
"Brain was stuck in a recursion loop. Had to reboot the system.",
|
||||
),
|
||||
(
|
||||
Status::Todo,
|
||||
"Configure a status bar for the TWM",
|
||||
"I need to see my CPU temperature in 16-bit color at all times.",
|
||||
),
|
||||
(
|
||||
Status::Todo,
|
||||
"Rewrite the parser using Nom",
|
||||
"Regex is great, but combinators are cooler.",
|
||||
),
|
||||
(
|
||||
Status::Completed,
|
||||
"Find a cool Ferris sticker",
|
||||
"My laptop lid still has 2 square inches of empty space.",
|
||||
),
|
||||
(
|
||||
Status::Todo,
|
||||
"Master Vim motions",
|
||||
"H, J, K, L... why did I just delete the entire main function?",
|
||||
),
|
||||
(
|
||||
Status::Todo,
|
||||
"Experiment with WebAssembly",
|
||||
"Rust in the browser is the future, and the future is now.",
|
||||
),
|
||||
(
|
||||
Status::Completed,
|
||||
"Add a dark mode to the UI",
|
||||
"My eyes are no longer burning. Success.",
|
||||
),
|
||||
(
|
||||
Status::Todo,
|
||||
"Submit a PR to an open-source project",
|
||||
"Fixing a typo counts as a contribution, right?",
|
||||
),
|
||||
(
|
||||
Status::Todo,
|
||||
"Explain Cow to a non-programmer",
|
||||
"No, not the animal. It's 'Clone On Write'. Why are you walking away?",
|
||||
),
|
||||
(
|
||||
Status::Completed,
|
||||
"Organize the physical desk",
|
||||
"Found a missing USB drive and three half-empty coffee mugs.",
|
||||
),
|
||||
(
|
||||
Status::Todo,
|
||||
"Write unit tests for the edge cases",
|
||||
"What happens if the input is an emoji and a null byte? Let's find out.",
|
||||
),
|
||||
(
|
||||
Status::Todo,
|
||||
"Learn a new functional programming language",
|
||||
"Just to see how it feels. Don't worry Rust, I'm not leaving you.",
|
||||
),
|
||||
(
|
||||
Status::Completed,
|
||||
"Solve a LeetCode hard in Rust",
|
||||
"The borrow checker was the real final boss.",
|
||||
),
|
||||
(
|
||||
Status::Todo,
|
||||
"Set up a Raspberry Pi server",
|
||||
"It will sit in the corner and run a Telegram bot for two days before I forget about it.",
|
||||
),
|
||||
(
|
||||
Status::Todo,
|
||||
"Refactor the error handling",
|
||||
"Switching from 'unwrap()' to 'thiserror' because I'm a professional now.",
|
||||
),
|
||||
(
|
||||
Status::Completed,
|
||||
"Explain the orphan rule to a coworker",
|
||||
"I think I confused them more, but I feel smarter.",
|
||||
),
|
||||
(
|
||||
Status::Todo,
|
||||
"Benchmark the hot path",
|
||||
"Flamegraphs are like modern art, but for nerds.",
|
||||
),
|
||||
(
|
||||
Status::Todo,
|
||||
"Buy a more ergonomic chair",
|
||||
"My back is screaming in a language I don't understand.",
|
||||
),
|
||||
(
|
||||
Status::Completed,
|
||||
"Finally finish this list",
|
||||
"36 items in total. Now I just have to actually do them.",
|
||||
),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
impl FromIterator<(Status, &'static str, &'static str)> for Model {
|
||||
fn from_iter<I: IntoIterator<Item = (Status, &'static str, &'static str)>>(iter: I) -> Self {
|
||||
let items: Vec<_> = iter
|
||||
.into_iter()
|
||||
.map(|(status, todo, info)| TodoItem::new(status, todo, info))
|
||||
.collect();
|
||||
|
||||
let state = ListState::default().with_selected(Some(0));
|
||||
|
||||
Self { items, state }
|
||||
}
|
||||
}
|
||||
|
||||
impl TodoItem {
|
||||
fn new(status: Status, todo: &str, info: &str) -> Self {
|
||||
Self {
|
||||
status,
|
||||
todo: todo.to_string(),
|
||||
info: info.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Model {
|
||||
pub fn selected(&self) -> Option<&TodoItem> {
|
||||
self.state
|
||||
.selected()
|
||||
.and_then(|index| self.items.get(index))
|
||||
}
|
||||
|
||||
pub fn select_none(self) -> Self {
|
||||
let state = ListState::default().with_selected(None);
|
||||
|
||||
Self {
|
||||
items: self.items,
|
||||
state,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_first(self) -> Self {
|
||||
let state = ListState::default().with_selected(Some(0));
|
||||
|
||||
Model {
|
||||
items: self.items,
|
||||
state,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_last(self) -> Self {
|
||||
let state = ListState::default().with_selected(Some(self.items.len() - 1));
|
||||
|
||||
Model {
|
||||
items: self.items,
|
||||
state,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_next(self) -> Self {
|
||||
match self.state.selected() {
|
||||
Some(index) => {
|
||||
let state =
|
||||
ListState::default().with_selected(Some((index + 1) % self.items.len()));
|
||||
|
||||
Model { state, ..self }
|
||||
}
|
||||
None => self.select_first(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_previous(self) -> Self {
|
||||
match self.state.selected() {
|
||||
Some(0) => self.select_last(),
|
||||
Some(index) => {
|
||||
let state = ListState::default().with_selected(Some(index.saturating_sub(1)));
|
||||
Model { state, ..self }
|
||||
}
|
||||
None => self.select_last(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_status(mut self) -> Self {
|
||||
let selected_index = self.state.selected();
|
||||
|
||||
match selected_index {
|
||||
Some(index) => {
|
||||
self.items[index].status = match self.items[index].status {
|
||||
Status::Todo => Status::Completed,
|
||||
Status::Completed => Status::Todo,
|
||||
};
|
||||
|
||||
Model { ..self }
|
||||
}
|
||||
None => Model { ..self },
|
||||
}
|
||||
}
|
||||
}
|
||||
16
examples/todo-list/src/update.rs
Normal file
16
examples/todo-list/src/update.rs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
use teatui::update::Update;
|
||||
|
||||
use crate::{message::Message, model::Model};
|
||||
|
||||
pub fn update(model: Model, msg: Message) -> Update<Model, ()> {
|
||||
match msg {
|
||||
Message::NoOp => Update::Next(model, None),
|
||||
Message::Exit => Update::Exit,
|
||||
Message::SelectNext => Update::Next(model.select_next(), None),
|
||||
Message::SelectNone => Update::Next(model.select_none(), None),
|
||||
Message::SelectPrevious => Update::Next(model.select_previous(), None),
|
||||
Message::SelectFirst => Update::Next(model.select_first(), None),
|
||||
Message::SelectLast => Update::Next(model.select_last(), None),
|
||||
Message::ToggleStatus => Update::Next(model.toggle_status(), None),
|
||||
}
|
||||
}
|
||||
140
examples/todo-list/src/view.rs
Normal file
140
examples/todo-list/src/view.rs
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::{Constraint, Layout, Rect},
|
||||
style::{
|
||||
Color, Modifier, Style, Stylize,
|
||||
palette::tailwind::{BLUE, GREEN, SLATE},
|
||||
},
|
||||
symbols,
|
||||
text::Line,
|
||||
widgets::{
|
||||
Block, Borders, HighlightSpacing, List, ListItem, Padding, Paragraph, StatefulWidget,
|
||||
Widget, Wrap,
|
||||
},
|
||||
};
|
||||
|
||||
use crate::model::{Model, Status, TodoItem};
|
||||
|
||||
const TODO_HEADER_STYLE: Style = Style::new().fg(SLATE.c100).bg(BLUE.c800);
|
||||
const NORMAL_ROW_BG: Color = SLATE.c950;
|
||||
const ALT_ROW_BG_COLOR: Color = SLATE.c900;
|
||||
const SELECTED_STYLE: Style = Style::new().bg(SLATE.c800).add_modifier(Modifier::BOLD);
|
||||
const TEXT_FG_COLOR: Color = SLATE.c200;
|
||||
const COMPLETED_TEXT_FG_COLOR: Color = GREEN.c500;
|
||||
|
||||
pub fn view(model: Model) -> AppWidget {
|
||||
AppWidget { model }
|
||||
}
|
||||
|
||||
pub struct AppWidget {
|
||||
model: Model,
|
||||
}
|
||||
|
||||
impl Widget for AppWidget {
|
||||
fn render(mut self, area: Rect, buf: &mut Buffer) {
|
||||
let [header_area, main_area, footer_area] = Layout::vertical([
|
||||
Constraint::Length(2),
|
||||
Constraint::Fill(1),
|
||||
Constraint::Length(1),
|
||||
])
|
||||
.areas(area);
|
||||
|
||||
let [list_area, item_area] =
|
||||
Layout::vertical([Constraint::Fill(1), Constraint::Fill(1)]).areas(main_area);
|
||||
|
||||
Paragraph::new("Ratatui List Example")
|
||||
.bold()
|
||||
.centered()
|
||||
.render(header_area, buf);
|
||||
|
||||
Paragraph::new("Use ↓↑ to move, ← to unselect, → to change status, g/G to go top/bottom.")
|
||||
.centered()
|
||||
.render(footer_area, buf);
|
||||
|
||||
self.render_list(list_area, buf);
|
||||
self.render_selected_item(item_area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
/// Rendering logic for the app
|
||||
impl AppWidget {
|
||||
fn render_list(&mut self, area: Rect, buf: &mut Buffer) {
|
||||
let block = Block::new()
|
||||
.title(Line::raw("TODO List").centered())
|
||||
.borders(Borders::TOP)
|
||||
.border_set(symbols::border::EMPTY)
|
||||
.border_style(TODO_HEADER_STYLE)
|
||||
.bg(NORMAL_ROW_BG);
|
||||
|
||||
// Iterate through all elements in the `items` and stylize them.
|
||||
let items: Vec<ListItem> = self
|
||||
.model
|
||||
.items
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, todo_item)| {
|
||||
let color = alternate_colors(i);
|
||||
ListItem::from(todo_item).bg(color)
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Create a List from all list items and highlight the currently selected one
|
||||
let list = List::new(items)
|
||||
.block(block)
|
||||
.highlight_style(SELECTED_STYLE)
|
||||
.highlight_symbol(">")
|
||||
.highlight_spacing(HighlightSpacing::Always);
|
||||
|
||||
// We need to disambiguate this trait method as both `Widget` and `StatefulWidget` share the
|
||||
// same method name `render`.
|
||||
StatefulWidget::render(list, area, buf, &mut self.model.state);
|
||||
}
|
||||
|
||||
fn render_selected_item(&self, area: Rect, buf: &mut Buffer) {
|
||||
// We get the info depending on the item's state.
|
||||
let info = if let Some(item) = self.model.selected() {
|
||||
match item.status {
|
||||
Status::Completed => format!("✓ DONE: {}", item.info),
|
||||
Status::Todo => format!("☐ TODO: {}", item.info),
|
||||
}
|
||||
} else {
|
||||
"Nothing selected...".to_string()
|
||||
};
|
||||
|
||||
// We show the list item's info under the list in this paragraph
|
||||
let block = Block::new()
|
||||
.title(Line::raw("TODO Info").centered())
|
||||
.borders(Borders::TOP)
|
||||
.border_set(symbols::border::EMPTY)
|
||||
.border_style(TODO_HEADER_STYLE)
|
||||
.bg(NORMAL_ROW_BG)
|
||||
.padding(Padding::horizontal(1));
|
||||
|
||||
// We can now render the item info
|
||||
Paragraph::new(info)
|
||||
.block(block)
|
||||
.fg(TEXT_FG_COLOR)
|
||||
.wrap(Wrap { trim: false })
|
||||
.render(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
const fn alternate_colors(i: usize) -> Color {
|
||||
if i % 2 == 0 {
|
||||
NORMAL_ROW_BG
|
||||
} else {
|
||||
ALT_ROW_BG_COLOR
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&TodoItem> for ListItem<'_> {
|
||||
fn from(value: &TodoItem) -> Self {
|
||||
let line = match value.status {
|
||||
Status::Todo => Line::styled(format!(" ☐ {}", value.todo), TEXT_FG_COLOR),
|
||||
Status::Completed => {
|
||||
Line::styled(format!(" ✓ {}", value.todo), COMPLETED_TEXT_FG_COLOR)
|
||||
}
|
||||
};
|
||||
ListItem::new(line)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue