use custom Task type

This commit is contained in:
Fabien Freling 2025-06-23 13:05:23 +02:00
parent 01566042b3
commit 5450884bb0
6 changed files with 79 additions and 100 deletions

7
Cargo.lock generated
View file

@ -214,6 +214,12 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "dotenvy"
version = "0.15.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
[[package]] [[package]]
name = "dsl_auto_type" name = "dsl_auto_type"
version = "0.1.3" version = "0.1.3"
@ -737,6 +743,7 @@ dependencies = [
"color-eyre", "color-eyre",
"crossterm", "crossterm",
"diesel", "diesel",
"dotenvy",
"ratatui", "ratatui",
] ]

View file

@ -12,3 +12,4 @@ ratatui = "0.29.0"
color-eyre = "0.6.3" color-eyre = "0.6.3"
diesel = { version = "2.2", features = ["sqlite", "returning_clauses_for_sqlite_3_35"] } diesel = { version = "2.2", features = ["sqlite", "returning_clauses_for_sqlite_3_35"] }
dotenvy = "0.15"

View file

@ -18,6 +18,7 @@
rust-toolchain rust-toolchain
cargo-generate cargo-generate
diesel-cli diesel-cli
sqlite
just just
lazysql lazysql

14
src/lib.rs Normal file
View file

@ -0,0 +1,14 @@
pub mod models;
pub mod schema;
use diesel::prelude::*;
use dotenvy::dotenv;
use std::env;
pub fn establish_connection() -> SqliteConnection {
dotenv().ok();
let database_url = env::var("DATABASE_URL").expect("DATABASE_URL must be set");
SqliteConnection::establish(&database_url)
.unwrap_or_else(|_| panic!("Error connecting to {}", database_url))
}

View file

@ -1,4 +1,5 @@
use color_eyre::Result; use color_eyre::Result;
use diesel::prelude::*;
use ratatui::{ use ratatui::{
DefaultTerminal, DefaultTerminal,
buffer::Buffer, buffer::Buffer,
@ -15,6 +16,8 @@ use ratatui::{
StatefulWidget, Widget, Wrap, StatefulWidget, Widget, Wrap,
}, },
}; };
use todo::establish_connection;
use todo::models::*;
const TODO_HEADER_STYLE: Style = Style::new().fg(SLATE.c100).bg(BLUE.c800); const TODO_HEADER_STYLE: Style = Style::new().fg(SLATE.c100).bg(BLUE.c800);
const NORMAL_ROW_BG: Color = SLATE.c950; const NORMAL_ROW_BG: Color = SLATE.c950;
@ -26,91 +29,32 @@ const COMPLETED_TEXT_FG_COLOR: Color = GREEN.c500;
fn main() -> Result<()> { fn main() -> Result<()> {
color_eyre::install()?; color_eyre::install()?;
let terminal = ratatui::init(); let terminal = ratatui::init();
let app_result = App::default().run(terminal); let app = App::default().load_db();
let app_result = app.run(terminal);
ratatui::restore(); ratatui::restore();
app_result app_result
} }
struct App { struct App {
should_exit: bool, should_exit: bool,
todo_list: TodoList, tasks: TaskList,
sqlite: SqliteConnection,
} }
struct TodoList { struct TaskList {
items: Vec<TodoItem>, items: Vec<Task>,
state: ListState, state: ListState,
} }
#[derive(Debug)]
struct TodoItem {
todo: String,
info: String,
status: Status,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Status {
Todo,
Completed,
}
impl Default for App { impl Default for App {
fn default() -> Self { fn default() -> Self {
Self { Self {
should_exit: false, should_exit: false,
todo_list: TodoList::from_iter([ tasks: TaskList {
( items: Vec::new(),
Status::Todo, state: ListState::default(),
"Rewrite everything with Rust!", },
"I can't hold my inner voice. He tells me to rewrite the complete universe with Rust", sqlite: establish_connection(),
),
(
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!",
),
]),
}
}
}
impl FromIterator<(Status, &'static str, &'static str)> for TodoList {
fn from_iter<I: IntoIterator<Item = (Status, &'static str, &'static str)>>(iter: I) -> Self {
let items = iter
.into_iter()
.map(|(status, todo, info)| TodoItem::new(status, todo, info))
.collect();
let state = ListState::default();
Self { items, state }
}
}
impl TodoItem {
fn new(status: Status, todo: &str, info: &str) -> Self {
Self {
status,
todo: todo.to_string(),
info: info.to_string(),
} }
} }
} }
@ -126,6 +70,17 @@ impl App {
Ok(()) Ok(())
} }
fn load_db(mut self) -> Self {
use todo::schema::tasks::dsl::*;
self.tasks.items = tasks
.select(Task::as_select())
.load(&mut self.sqlite)
.expect("Error loading posts");
self
}
fn handle_key(&mut self, key: KeyEvent) { fn handle_key(&mut self, key: KeyEvent) {
if key.kind != KeyEventKind::Press { if key.kind != KeyEventKind::Press {
return; return;
@ -143,32 +98,32 @@ impl App {
} }
fn select_none(&mut self) { fn select_none(&mut self) {
self.todo_list.state.select(None); self.tasks.state.select(None);
} }
fn select_next(&mut self) { fn select_next(&mut self) {
self.todo_list.state.select_next(); self.tasks.state.select_next();
} }
fn select_previous(&mut self) { fn select_previous(&mut self) {
self.todo_list.state.select_previous(); self.tasks.state.select_previous();
} }
fn select_first(&mut self) { fn select_first(&mut self) {
self.todo_list.state.select_first(); self.tasks.state.select_first();
} }
fn select_last(&mut self) { fn select_last(&mut self) {
self.todo_list.state.select_last(); self.tasks.state.select_last();
} }
/// Changes the status of the selected list item /// Changes the status of the selected list item
fn toggle_status(&mut self) { fn toggle_status(&mut self) {
if let Some(i) = self.todo_list.state.selected() { // if let Some(i) = self.tasks.state.selected() {
self.todo_list.items[i].status = match self.todo_list.items[i].status { // self.tasks.items[i].status = match self.tasks.items[i].status {
Status::Completed => Status::Todo, // Status::Completed => Status::Todo,
Status::Todo => Status::Completed, // Status::Todo => Status::Completed,
} // }
} // }
} }
} }
@ -216,13 +171,14 @@ impl App {
// Iterate through all elements in the `items` and stylize them. // Iterate through all elements in the `items` and stylize them.
let items: Vec<ListItem> = self let items: Vec<ListItem> = self
.todo_list .tasks
.items .items
.iter() .iter()
.enumerate() .enumerate()
.map(|(i, todo_item)| { .map(|(i, task)| {
let color = alternate_colors(i); let color = alternate_colors(i);
ListItem::from(todo_item).bg(color) let item = item_from_task(task);
item.bg(color)
}) })
.collect(); .collect();
@ -235,16 +191,13 @@ impl App {
// We need to disambiguate this trait method as both `Widget` and `StatefulWidget` share the // We need to disambiguate this trait method as both `Widget` and `StatefulWidget` share the
// same method name `render`. // same method name `render`.
StatefulWidget::render(list, area, buf, &mut self.todo_list.state); StatefulWidget::render(list, area, buf, &mut self.tasks.state);
} }
fn render_selected_item(&self, area: Rect, buf: &mut Buffer) { fn render_selected_item(&self, area: Rect, buf: &mut Buffer) {
// We get the info depending on the item's state. // We get the info depending on the item's state.
let info = if let Some(i) = self.todo_list.state.selected() { let info = if let Some(i) = self.tasks.state.selected() {
match self.todo_list.items[i].status { format!("☐ TODO: {}", self.tasks.items[i].title)
Status::Completed => format!("✓ DONE: {}", self.todo_list.items[i].info),
Status::Todo => format!("☐ TODO: {}", self.todo_list.items[i].info),
}
} else { } else {
"Nothing selected...".to_string() "Nothing selected...".to_string()
}; };
@ -275,14 +228,7 @@ const fn alternate_colors(i: usize) -> Color {
} }
} }
impl From<&TodoItem> for ListItem<'_> { fn item_from_task(task: &Task) -> ListItem<'_> {
fn from(value: &TodoItem) -> Self { let line = Line::styled(format!("{}", task.title), TEXT_FG_COLOR);
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) ListItem::new(line)
} }
}

10
src/models.rs Normal file
View file

@ -0,0 +1,10 @@
use diesel::prelude::*;
#[derive(Queryable, Selectable)]
#[diesel(table_name = crate::schema::tasks)]
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
pub struct Task {
pub id: i32,
pub title: String,
pub description: String,
}