diff --git a/.gitignore b/.gitignore index eb5a316..b83d222 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1 @@ -target +/target/ diff --git a/Cargo.lock b/Cargo.lock index 4a03f2b..506111b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -172,62 +172,6 @@ dependencies = [ "syn", ] -[[package]] -name = "deranged" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" -dependencies = [ - "powerfmt", -] - -[[package]] -name = "diesel" -version = "2.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a917a9209950404d5be011c81d081a2692a822f73c3d6af586f0cab5ff50f614" -dependencies = [ - "diesel_derives", - "libsqlite3-sys", - "time", -] - -[[package]] -name = "diesel_derives" -version = "2.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52841e97814f407b895d836fa0012091dff79c6268f39ad8155d384c21ae0d26" -dependencies = [ - "diesel_table_macro_syntax", - "dsl_auto_type", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "diesel_table_macro_syntax" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "209c735641a413bc68c4923a9d6ad4bcb3ca306b794edaa7eb0b3228a99ffb25" -dependencies = [ - "syn", -] - -[[package]] -name = "dsl_auto_type" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139ae9aca7527f85f26dd76483eb38533fd84bd571065da1739656ef71c5ff5b" -dependencies = [ - "darling", - "either", - "heck", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "either" version = "1.15.0" @@ -353,16 +297,6 @@ version = "0.2.173" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8cfeafaffdbc32176b64fb251369d52ea9f0a8fbc6f8759edffef7b525d64bb" -[[package]] -name = "libsqlite3-sys" -version = "0.33.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "947e6816f7825b2b45027c2c32e7085da9934defa535de4a6a46b10a4d5257fa" -dependencies = [ - "pkg-config", - "vcpkg", -] - [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -421,12 +355,6 @@ dependencies = [ "windows-sys", ] -[[package]] -name = "num-conv" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" - [[package]] name = "object" version = "0.36.7" @@ -483,18 +411,6 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" -[[package]] -name = "pkg-config" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - [[package]] name = "proc-macro2" version = "1.0.95" @@ -580,26 +496,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "serde" -version = "1.0.219" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.219" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "sharded-slab" version = "0.1.7" @@ -699,44 +595,12 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "time" -version = "0.3.41" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" -dependencies = [ - "deranged", - "itoa", - "num-conv", - "powerfmt", - "serde", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" - -[[package]] -name = "time-macros" -version = "0.2.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" -dependencies = [ - "num-conv", - "time-core", -] - [[package]] name = "todo" version = "0.1.0" dependencies = [ "color-eyre", "crossterm", - "diesel", "ratatui", ] @@ -822,12 +686,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" diff --git a/Cargo.toml b/Cargo.toml index b2f7bec..201dd34 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,5 +10,3 @@ edition = "2024" crossterm = "0.28.1" ratatui = "0.29.0" color-eyre = "0.6.3" - -diesel = { version = "2.2", features = ["sqlite", "returning_clauses_for_sqlite_3_35"] } diff --git a/diesel.toml b/diesel.toml deleted file mode 100644 index ff0dac5..0000000 --- a/diesel.toml +++ /dev/null @@ -1,9 +0,0 @@ -# For documentation on how to configure this file, -# see https://diesel.rs/guides/configuring-diesel-cli - -[print_schema] -file = "src/schema.rs" -custom_type_derives = ["diesel::query_builder::QueryId", "Clone"] - -[migrations_directory] -dir = "/home/ffreling/code/todo/migrations" diff --git a/flake.nix b/flake.nix index 8df85e2..9595d88 100644 --- a/flake.nix +++ b/flake.nix @@ -17,10 +17,9 @@ nativeBuildInputs = [ rust-toolchain cargo-generate - diesel-cli + # rust-analyzer-nightly just - lazysql ]; }; }; diff --git a/justfile b/justfile index b342642..e4a4530 100644 --- a/justfile +++ b/justfile @@ -1,5 +1,3 @@ -set dotenv-load - alias b := build build: cargo build @@ -7,16 +5,3 @@ build: alias r := run run: cargo run - -db-setup: - diesel setup - -db-update desc: - diesel migration generate --diff-schema {{ desc }} - -db-migrate: - diesel migration run - diesel migration redo - -db-inspect: - lazysql $DATABASE_URL diff --git a/migrations/.keep b/migrations/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/migrations/2025-06-20-111801_create_tasks/down.sql b/migrations/2025-06-20-111801_create_tasks/down.sql deleted file mode 100644 index 39667d2..0000000 --- a/migrations/2025-06-20-111801_create_tasks/down.sql +++ /dev/null @@ -1,2 +0,0 @@ --- This file should undo anything in `up.sql` -DROP TABLE IF EXISTS `tasks`; diff --git a/migrations/2025-06-20-111801_create_tasks/up.sql b/migrations/2025-06-20-111801_create_tasks/up.sql deleted file mode 100644 index 09e14e7..0000000 --- a/migrations/2025-06-20-111801_create_tasks/up.sql +++ /dev/null @@ -1,7 +0,0 @@ --- Your SQL goes here -CREATE TABLE `tasks`( - `id` INT4 NOT NULL PRIMARY KEY, - `title` VARCHAR NOT NULL, - `description` TEXT NOT NULL -); - diff --git a/src/main.rs b/src/main.rs index 7e94155..87d0063 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,288 +1,92 @@ use color_eyre::Result; +use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers}; use ratatui::{ - DefaultTerminal, - buffer::Buffer, - crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind}, - layout::{Constraint, Layout, Rect}, - style::{ - Color, Modifier, Style, Stylize, - palette::tailwind::{BLUE, GREEN, SLATE}, - }, - symbols, + DefaultTerminal, Frame, + style::Stylize, text::Line, - widgets::{ - Block, Borders, HighlightSpacing, List, ListItem, ListState, Padding, Paragraph, - StatefulWidget, Widget, Wrap, - }, + widgets::{Block, Paragraph}, }; -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; - -fn main() -> Result<()> { +fn main() -> color_eyre::Result<()> { color_eyre::install()?; let terminal = ratatui::init(); - let app_result = App::default().run(terminal); + let result = App::new().run(terminal); ratatui::restore(); - app_result + result } -struct App { - should_exit: bool, - todo_list: TodoList, -} - -struct TodoList { - items: Vec, - 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 { - fn default() -> Self { - Self { - should_exit: false, - todo_list: TodoList::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!", - ), - ]), - } - } -} - -impl FromIterator<(Status, &'static str, &'static str)> for TodoList { - fn from_iter>(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(), - } - } +/// The main application which holds the state and logic of the application. +#[derive(Debug, Default)] +pub struct App { + /// Is the application running? + running: bool, } impl App { - fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> { - while !self.should_exit { - terminal.draw(|frame| frame.render_widget(&mut self, frame.area()))?; - if let Event::Key(key) = event::read()? { - self.handle_key(key); - }; + /// Construct a new instance of [`App`]. + pub fn new() -> Self { + Self::default() + } + + /// Run the application's main loop. + pub fn run(mut self, mut terminal: DefaultTerminal) -> Result<()> { + self.running = true; + while self.running { + terminal.draw(|frame| self.render(frame))?; + self.handle_crossterm_events()?; } Ok(()) } - fn handle_key(&mut self, key: KeyEvent) { - if key.kind != KeyEventKind::Press { - return; + /// Renders the user interface. + /// + /// This is where you add new widgets. See the following resources for more information: + /// + /// - + /// - + fn render(&mut self, frame: &mut Frame) { + let title = Line::from("Ratatui Simple Template") + .bold() + .blue() + .centered(); + let text = "Hello, Ratatui!\n\n\ + Created using https://github.com/ratatui/templates\n\ + Press `Esc`, `Ctrl-C` or `q` to stop running."; + frame.render_widget( + Paragraph::new(text) + .block(Block::bordered().title(title)) + .centered(), + frame.area(), + ) + } + + /// Reads the crossterm events and updates the state of [`App`]. + /// + /// If your application needs to perform work in between handling events, you can use the + /// [`event::poll`] function to check if there are any events available with a timeout. + fn handle_crossterm_events(&mut self) -> Result<()> { + match event::read()? { + // it's important to check KeyEventKind::Press to avoid handling key release events + Event::Key(key) if key.kind == KeyEventKind::Press => self.on_key_event(key), + Event::Mouse(_) => {} + Event::Resize(_, _) => {} + _ => {} } - match key.code { - KeyCode::Char('q') | KeyCode::Esc => self.should_exit = true, - KeyCode::Char('h') | KeyCode::Left => self.select_none(), - KeyCode::Char('j') | KeyCode::Down => self.select_next(), - KeyCode::Char('k') | KeyCode::Up => self.select_previous(), - KeyCode::Char('g') | KeyCode::Home => self.select_first(), - KeyCode::Char('G') | KeyCode::End => self.select_last(), - KeyCode::Enter | KeyCode::Char(' ') => self.toggle_status(), + Ok(()) + } + + /// Handles the key events and updates the state of [`App`]. + fn on_key_event(&mut self, key: KeyEvent) { + match (key.modifiers, key.code) { + (_, KeyCode::Esc | KeyCode::Char('q')) + | (KeyModifiers::CONTROL, KeyCode::Char('c') | KeyCode::Char('C')) => self.quit(), + // Add other key handlers here. _ => {} } } - fn select_none(&mut self) { - self.todo_list.state.select(None); - } - - fn select_next(&mut self) { - self.todo_list.state.select_next(); - } - fn select_previous(&mut self) { - self.todo_list.state.select_previous(); - } - - fn select_first(&mut self) { - self.todo_list.state.select_first(); - } - - fn select_last(&mut self) { - self.todo_list.state.select_last(); - } - - /// Changes the status of the selected list item - fn toggle_status(&mut self) { - if let Some(i) = self.todo_list.state.selected() { - self.todo_list.items[i].status = match self.todo_list.items[i].status { - Status::Completed => Status::Todo, - Status::Todo => Status::Completed, - } - } - } -} - -impl Widget for &mut App { - fn render(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); - - App::render_header(header_area, buf); - App::render_footer(footer_area, buf); - self.render_list(list_area, buf); - self.render_selected_item(item_area, buf); - } -} - -/// Rendering logic for the app -impl App { - fn render_header(area: Rect, buf: &mut Buffer) { - Paragraph::new("Ratatui List Example") - .bold() - .centered() - .render(area, buf); - } - - fn render_footer(area: Rect, buf: &mut Buffer) { - Paragraph::new("Use ↓↑ to move, ← to unselect, → to change status, g/G to go top/bottom.") - .centered() - .render(area, buf); - } - - 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 = self - .todo_list - .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.todo_list.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(i) = self.todo_list.state.selected() { - match self.todo_list.items[i].status { - Status::Completed => format!("✓ DONE: {}", self.todo_list.items[i].info), - Status::Todo => format!("☐ TODO: {}", self.todo_list.items[i].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) + /// Set running to false to quit the application. + fn quit(&mut self) { + self.running = false; } } diff --git a/src/schema.rs b/src/schema.rs deleted file mode 100644 index 4cc919d..0000000 --- a/src/schema.rs +++ /dev/null @@ -1,9 +0,0 @@ -// @generated automatically by Diesel CLI. - -diesel::table! { - tasks (id) { - id -> Integer, - title -> Text, - description -> Text, - } -}