ultra fancy cli for diesel-demo
cargo devel CI / cargo CI (push) Successful in 3m25s
Details
cargo devel CI / cargo CI (push) Successful in 3m25s
Details
This commit is contained in:
parent
c84672cf78
commit
5ad5cd39ed
|
@ -358,6 +358,19 @@ dependencies = [
|
|||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "comfy-table"
|
||||
version = "7.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b34115915337defe99b2aff5c2ce6771e5fbc4079f4b506301f5cf394c8452f7"
|
||||
dependencies = [
|
||||
"console",
|
||||
"crossterm",
|
||||
"strum",
|
||||
"strum_macros",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "console"
|
||||
version = "0.15.8"
|
||||
|
@ -463,6 +476,28 @@ version = "0.8.20"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
|
||||
|
||||
[[package]]
|
||||
name = "crossterm"
|
||||
version = "0.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"crossterm_winapi",
|
||||
"libc",
|
||||
"parking_lot",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossterm_winapi"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crunchy"
|
||||
version = "0.2.2"
|
||||
|
@ -610,6 +645,19 @@ dependencies = [
|
|||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dialoguer"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "658bce805d770f407bc62102fca7c2c64ceef2fbcb2b8bd19d2765ce093980de"
|
||||
dependencies = [
|
||||
"console",
|
||||
"shell-words",
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "diesel"
|
||||
version = "2.2.1"
|
||||
|
@ -629,8 +677,12 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"anyhow",
|
||||
"colored",
|
||||
"comfy-table",
|
||||
"console",
|
||||
"dialoguer",
|
||||
"diesel",
|
||||
"dotenvy",
|
||||
"indicatif",
|
||||
"libpt",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
@ -759,6 +811,12 @@ version = "0.1.9"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a"
|
||||
|
||||
[[package]]
|
||||
name = "fluent"
|
||||
version = "0.16.1"
|
||||
|
@ -2035,6 +2093,12 @@ dependencies = [
|
|||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.18"
|
||||
|
@ -2185,6 +2249,12 @@ dependencies = [
|
|||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shell-words"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde"
|
||||
|
||||
[[package]]
|
||||
name = "shortc"
|
||||
version = "0.1.0"
|
||||
|
@ -2275,6 +2345,25 @@ version = "0.11.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.26.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
|
||||
|
||||
[[package]]
|
||||
name = "strum_macros"
|
||||
version = "0.26.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustversion",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "subtle"
|
||||
version = "2.6.1"
|
||||
|
@ -2325,6 +2414,18 @@ dependencies = [
|
|||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"fastrand",
|
||||
"rustix",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "terminal_size"
|
||||
version = "0.3.0"
|
||||
|
@ -3078,3 +3179,9 @@ dependencies = [
|
|||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
|
||||
|
|
|
@ -6,8 +6,12 @@ edition = "2021"
|
|||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
colored = "2.1.0"
|
||||
comfy-table = { version = "7.1.1", features = ["console"] }
|
||||
console = "0.15.8"
|
||||
dialoguer = { version = "0.11.0", features = ["completion", "history"] }
|
||||
diesel = { version = "2.2.1", features = ["serde_json", "sqlite", "uuid", "returning_clauses_for_sqlite_3_35"] }
|
||||
dotenvy = "0.15.7"
|
||||
indicatif = "0.17.8"
|
||||
libpt.workspace = true
|
||||
serde = { workspace = true, features = ["serde_derive"] }
|
||||
serde_json.workspace = true
|
||||
|
|
|
@ -0,0 +1,128 @@
|
|||
use std::fmt::Display;
|
||||
|
||||
use comfy_table::presets::UTF8_FULL_CONDENSED;
|
||||
use comfy_table::Table;
|
||||
use console::{style, Color};
|
||||
use dialoguer::{theme::ColorfulTheme, Completion, Input};
|
||||
use dialoguer::{BasicHistory, History};
|
||||
use libpt::log::{info, warn};
|
||||
|
||||
use crate::models::{self, Post};
|
||||
|
||||
const HELP_TEXT: &str = "\
|
||||
help|? - show this menu\n\
|
||||
exit - exit the application\n\
|
||||
list|ls - list all posts\n\
|
||||
publish [id] - publish the post with the id [id]\n\
|
||||
unpublish [id] - make the post with the id [id] a draft\n\
|
||||
delete [id] - delete the post with the id [id]\n\
|
||||
read|show [id] - display the post with the id [id]\n\
|
||||
new - create a new post";
|
||||
const USAGE_TEXT: &str = "Bad input: try 'help'";
|
||||
|
||||
pub struct MyCompletion {
|
||||
options: Vec<String>,
|
||||
}
|
||||
impl Default for MyCompletion {
|
||||
fn default() -> Self {
|
||||
MyCompletion {
|
||||
options: vec![
|
||||
"help".to_string(),
|
||||
"?".to_string(),
|
||||
"list".to_string(),
|
||||
"publish".to_string(),
|
||||
"unpublish".to_string(),
|
||||
"delete".to_string(),
|
||||
"read".to_string(),
|
||||
"show".to_string(),
|
||||
"new".to_string(),
|
||||
"ls".to_string(),
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Completion for MyCompletion {
|
||||
/// Simple completion implementation based on substring
|
||||
fn get(&self, input: &str) -> Option<String> {
|
||||
let matches = self
|
||||
.options
|
||||
.iter()
|
||||
.filter(|option| option.starts_with(input))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if matches.len() == 1 {
|
||||
Some(matches[0].to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn table_posts(posts_to_print: &Vec<models::Post>) {
|
||||
let mut table = Table::new();
|
||||
table
|
||||
.load_preset(UTF8_FULL_CONDENSED)
|
||||
.set_content_arrangement(comfy_table::ContentArrangement::DynamicFullWidth)
|
||||
.set_header(vec!["id", "title", "published?", "body"]);
|
||||
for post in posts_to_print {
|
||||
let mut stitle = post.title.clone();
|
||||
stitle.truncate(40);
|
||||
let mut sbody = post.body.clone();
|
||||
sbody.truncate(40);
|
||||
|
||||
table.add_row(vec![
|
||||
post.id.to_string(),
|
||||
stitle,
|
||||
post.published.to_string(),
|
||||
sbody,
|
||||
]);
|
||||
}
|
||||
println!("{}", style(table).dim());
|
||||
}
|
||||
|
||||
impl Display for Post {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let mut table = Table::new();
|
||||
table
|
||||
.load_preset(UTF8_FULL_CONDENSED)
|
||||
.set_content_arrangement(comfy_table::ContentArrangement::DynamicFullWidth)
|
||||
.set_header(vec![format!("{:<6} {}", self.id, self.title)])
|
||||
.add_row(vec![self.body.clone()]);
|
||||
writeln!(f, "{table}")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_buf_interactive(
|
||||
buf: &mut String,
|
||||
completion: &impl Completion,
|
||||
history: &mut BasicHistory,
|
||||
) -> anyhow::Result<()> {
|
||||
buf.clear();
|
||||
|
||||
*buf = Input::with_theme(&ColorfulTheme::default())
|
||||
.completion_with(completion)
|
||||
.history_with(history)
|
||||
.interact_text()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn usage() {
|
||||
borderprint(USAGE_TEXT, Some(Color::Red));
|
||||
}
|
||||
|
||||
pub fn help() {
|
||||
borderprint(HELP_TEXT, Some(Color::Cyan));
|
||||
}
|
||||
|
||||
pub(crate) fn borderprint(content: impl ToString, color: Option<Color>) {
|
||||
let mut table = Table::new();
|
||||
table
|
||||
.load_preset(UTF8_FULL_CONDENSED)
|
||||
.set_content_arrangement(comfy_table::ContentArrangement::DynamicFullWidth)
|
||||
.add_row(vec![content.to_string()]);
|
||||
match color {
|
||||
Some(c) => println!("{}", style(table).fg(c)),
|
||||
None => println!("{table}")
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
pub mod models;
|
||||
pub mod schema;
|
||||
pub mod cli;
|
||||
|
||||
use self::schema::posts::dsl::*;
|
||||
|
||||
|
@ -7,6 +8,7 @@ use std::io::Write;
|
|||
use std::{env, io};
|
||||
|
||||
use colored::Colorize;
|
||||
use dialoguer::Input;
|
||||
use diesel::prelude::*;
|
||||
use diesel::sqlite::SqliteConnection;
|
||||
|
||||
|
@ -34,45 +36,3 @@ pub fn load_relevant_posts(conn: &mut SqliteConnection) -> anyhow::Result<Vec<mo
|
|||
.load(conn)?)
|
||||
}
|
||||
|
||||
// NOTE: formatting breaks when you use japanese fullwidth (or probably other longer chars too)
|
||||
// characters. Works well for the regular alphabet
|
||||
pub fn print_posts(posts_to_print: &Vec<models::Post>) {
|
||||
if !posts_to_print.is_empty() {
|
||||
println!(
|
||||
"{: <12}| {: <30} | {: <40}[...] | {: <12} | {: <5}",
|
||||
"id", "title", "body (truncated)", "body len", "is published?"
|
||||
);
|
||||
println!("{:=^140}", "");
|
||||
for post in posts_to_print {
|
||||
let mut short_title = post.body.clone();
|
||||
short_title.truncate(30);
|
||||
let mut short_body = post.body.clone();
|
||||
short_body.truncate(40);
|
||||
println!(
|
||||
"{: <12}| {: <30} | {: <40}[...] | {: <12} | {: <5}",
|
||||
post.id,
|
||||
short_title,
|
||||
short_body,
|
||||
post.body.len(),
|
||||
post.published
|
||||
);
|
||||
}
|
||||
info!("total: {}", posts_to_print.len());
|
||||
} else {
|
||||
warn!("Tried to display posts, but there are no posts stored in the database");
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: this can't handle unicode stuff like 春 and I don't really care
|
||||
pub fn read_buf_interactive(buf: &mut String) -> anyhow::Result<()> {
|
||||
buf.clear();
|
||||
let stdin = io::stdin();
|
||||
let mut stdout = io::stdout();
|
||||
|
||||
print!("{}", "> ".green().bold());
|
||||
stdout.flush()?;
|
||||
stdin.read_line(buf)?;
|
||||
*buf = buf.trim().to_string();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1,19 +1,9 @@
|
|||
use dialoguer::BasicHistory;
|
||||
use diesel::SqliteConnection;
|
||||
use diesel_demo::models::{Post, PostDraft};
|
||||
use libpt::log::{self, debug, error, trace, warn};
|
||||
|
||||
const HELP_TEXT: &str = "\
|
||||
help|? - show this menu\n\
|
||||
exit - exit the application\n\
|
||||
list - list all posts\n\
|
||||
publish [id] - publish the post with the id [id]\n\
|
||||
unpublish [id] - make the post with the id [id] a draft\n\
|
||||
delete [id] - delete the post with the id [id]\n\
|
||||
read|show [id] - display the post with the id [id]\n\
|
||||
new - create a new post";
|
||||
const USAGE_TEXT: &str = "Bad input: try 'help'";
|
||||
|
||||
use colored::*;
|
||||
use lib::cli::*;
|
||||
|
||||
use diesel_demo as lib;
|
||||
|
||||
|
@ -37,12 +27,14 @@ fn main() -> anyhow::Result<()> {
|
|||
|
||||
fn repl(conn: &mut SqliteConnection) -> anyhow::Result<()> {
|
||||
let mut buf = String::new();
|
||||
let completion = MyCompletion::default();
|
||||
let mut history = BasicHistory::new();
|
||||
|
||||
loop {
|
||||
lib::read_buf_interactive(&mut buf)?;
|
||||
read_buf_interactive(&mut buf, &completion, &mut history)?;
|
||||
buf = buf.to_uppercase();
|
||||
if buf.starts_with("HELP") || buf.starts_with('?') {
|
||||
println!("{}", HELP_TEXT.bright_blue())
|
||||
help()
|
||||
} else if buf.starts_with("EXIT") || buf.is_empty() {
|
||||
break;
|
||||
} else if buf.starts_with("UNPUBLISH") {
|
||||
|
@ -101,10 +93,10 @@ fn repl(conn: &mut SqliteConnection) -> anyhow::Result<()> {
|
|||
}
|
||||
}
|
||||
};
|
||||
} else if buf.starts_with("LIST") {
|
||||
} else if buf.starts_with("LIST") || buf.starts_with("LS") {
|
||||
let posts = lib::load_all_posts(conn)?;
|
||||
trace!("loaded posts for display: {posts:#?}");
|
||||
lib::print_posts(&posts);
|
||||
table_posts(&posts);
|
||||
} else if buf.starts_with("NEW") {
|
||||
let post = PostDraft::interactive_create()?;
|
||||
let _ = post.post(conn).inspect_err(|e| {
|
||||
|
@ -118,10 +110,6 @@ fn repl(conn: &mut SqliteConnection) -> anyhow::Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn usage() {
|
||||
println!("{}", USAGE_TEXT.red().bold());
|
||||
}
|
||||
|
||||
fn get_id(buf: &str) -> Option<i32> {
|
||||
match buf.split(' ').nth(1) {
|
||||
Some(s) => match s.parse() {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use std::fmt::Display;
|
||||
use std::io::{self, Read, Write};
|
||||
|
||||
use dialoguer::Confirm;
|
||||
use diesel::prelude::*;
|
||||
use libpt::log::{info, trace};
|
||||
|
||||
|
@ -40,6 +41,13 @@ impl Post {
|
|||
Ok(())
|
||||
}
|
||||
pub fn delete(conn: &mut SqliteConnection, id: i32) -> anyhow::Result<()> {
|
||||
let confirmation = Confirm::new()
|
||||
.with_prompt(format!("You are about to delete post {id}, continue?"))
|
||||
.interact()?;
|
||||
if !confirmation {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
use crate::schema::posts::dsl::posts;
|
||||
|
||||
let post = diesel::delete(posts.find(id))
|
||||
|
@ -50,20 +58,6 @@ impl Post {
|
|||
}
|
||||
}
|
||||
|
||||
impl Display for Post {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
if !self.published {
|
||||
writeln!(f, "this post has not yet been published!")
|
||||
} else {
|
||||
writeln!(
|
||||
f,
|
||||
"\n{:<60} | published: {:<5}\n{:=^140}\n\n{}",
|
||||
self.title, self.published, "", self.body
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Insertable, Debug)]
|
||||
#[diesel(table_name = crate::schema::posts)]
|
||||
#[diesel(check_for_backend(diesel::sqlite::Sqlite))] // optional but improves generated compiler errors
|
||||
|
|
Loading…
Reference in New Issue