Seems to work!
This commit is contained in:
parent
81eed730aa
commit
264c4402ec
6 changed files with 2111 additions and 13 deletions
1918
Cargo.lock
generated
1918
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -4,3 +4,8 @@ version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
anyhow = "1.0.98"
|
||||||
|
clap = { version = "4.5.38", features = ["derive"] }
|
||||||
|
sqlx = { version = "0.8.6", features = ["runtime-tokio", "sqlite"] }
|
||||||
|
time = { version = "0.3.41", features = ["macros"] }
|
||||||
|
tokio = { version = "1.45.1", features = ["full", "macros", "tokio-macros"] }
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
# More Structured Bookmark Manager
|
# More Structured Bookmark Manager
|
||||||
|
|
||||||
- TODO make CLI stuff
|
- PROG make CLI stuff
|
||||||
|
- TODO create sqlite integration to ensure invariants
|
||||||
|
- keep all history, but only show hidden history when requested?
|
||||||
|
|
|
||||||
64
src/cli.rs
Normal file
64
src/cli.rs
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
use clap::{self, Parser, Subcommand};
|
||||||
|
use crate::{db::{Bookmark, Id, Store}, Result};
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
pub struct Cli {
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: Command,
|
||||||
|
#[arg(short='r')]
|
||||||
|
/// Whether the command should be applied to the reading list instead
|
||||||
|
/// of the bookmark list
|
||||||
|
pub(crate) is_reading_list: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
enum Command {
|
||||||
|
/// Add an entry to the selected list
|
||||||
|
Add {
|
||||||
|
link: String,
|
||||||
|
#[arg(default_value = "")]
|
||||||
|
comment: String,
|
||||||
|
},
|
||||||
|
/// Fetch the link from an entry in the selected list.
|
||||||
|
/// If fetching from reading list, the item will be removed as well.
|
||||||
|
Get {
|
||||||
|
id: Id,
|
||||||
|
},
|
||||||
|
/// List entries from the selected list.
|
||||||
|
/// The format is ID LINK # COMMENT
|
||||||
|
List,
|
||||||
|
/// Remove an entry, printing out its link in the process.
|
||||||
|
Rm {
|
||||||
|
id: Id,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Cli {
|
||||||
|
pub async fn run(self, mut store: Store) -> Result<()> {
|
||||||
|
match self.command {
|
||||||
|
Command::Add { link, comment } => {
|
||||||
|
let bookmark = Bookmark::new(link, comment);
|
||||||
|
store.add(bookmark).await?;
|
||||||
|
},
|
||||||
|
Command::Get { id } => {
|
||||||
|
let bookmark = store.get(id).await?;
|
||||||
|
if self.is_reading_list {
|
||||||
|
store.rm(id).await?;
|
||||||
|
}
|
||||||
|
println!("{}", bookmark.link);
|
||||||
|
},
|
||||||
|
Command::List => {
|
||||||
|
let bookmarks = store.list().await?;
|
||||||
|
for Bookmark { id, link, comment } in bookmarks.iter() {
|
||||||
|
println!("{id} {link} # {comment}");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Command::Rm { id } => {
|
||||||
|
let bookmark = store.get(id).await?;
|
||||||
|
store.rm(id).await?;
|
||||||
|
println!("{}", bookmark.link);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
103
src/db.rs
Normal file
103
src/db.rs
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
use crate::Result;
|
||||||
|
|
||||||
|
use anyhow::anyhow;
|
||||||
|
use sqlx::{self, ConnectOptions, SqliteConnection};
|
||||||
|
use sqlx::sqlite::SqliteConnectOptions;
|
||||||
|
|
||||||
|
pub(crate) type Id = i64;
|
||||||
|
|
||||||
|
#[derive(sqlx::FromRow)]
|
||||||
|
pub struct Bookmark {
|
||||||
|
#[sqlx(rename="rowid")]
|
||||||
|
pub id: Id,
|
||||||
|
pub link: String,
|
||||||
|
pub comment: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Bookmark {
|
||||||
|
pub fn new(link: String, comment: String) -> Self {
|
||||||
|
Bookmark { id: 0, link, comment }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Store {
|
||||||
|
conn: SqliteConnection,
|
||||||
|
table: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Store {
|
||||||
|
pub async fn new(location: &str, is_reading_list: bool) -> Result<Self> {
|
||||||
|
let conn = location.parse::<SqliteConnectOptions>()?
|
||||||
|
.connect().await?;
|
||||||
|
let table = {
|
||||||
|
if is_reading_list { "reading_list" } else { "bookmarks" }
|
||||||
|
};
|
||||||
|
let mut result = Self { conn, table };
|
||||||
|
result.create_tables().await?;
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn list(&mut self) -> Result<Vec<Bookmark>> {
|
||||||
|
let query = format!(
|
||||||
|
"SELECT rowid, link, comment
|
||||||
|
FROM {}
|
||||||
|
ORDER BY timestamp",
|
||||||
|
self.table
|
||||||
|
);
|
||||||
|
Ok(sqlx::query_as(&query)
|
||||||
|
.fetch_all(&mut self.conn)
|
||||||
|
.await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn add(&mut self, bookmark: Bookmark) -> Result<()> {
|
||||||
|
let query = format!(
|
||||||
|
"INSERT INTO {} VALUES
|
||||||
|
($1, $2, unixepoch('now'))",
|
||||||
|
self.table
|
||||||
|
);
|
||||||
|
sqlx::query(&query)
|
||||||
|
.bind(bookmark.link)
|
||||||
|
.bind(bookmark.comment)
|
||||||
|
.execute(&mut self.conn).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub async fn get(&mut self, id: Id)
|
||||||
|
-> Result<Bookmark> {
|
||||||
|
let query = format!(
|
||||||
|
"SELECT rowid, link, comment
|
||||||
|
FROM {}
|
||||||
|
WHERE rowid = $1",
|
||||||
|
self.table
|
||||||
|
);
|
||||||
|
let result = sqlx::query_as(&query)
|
||||||
|
.bind(id)
|
||||||
|
.fetch_optional(&mut self.conn)
|
||||||
|
.await?
|
||||||
|
.ok_or(anyhow!("Could not find a bookmark with that ID!"))?;
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn rm(&mut self, id: Id) -> Result<()> {
|
||||||
|
let query = format!(
|
||||||
|
"DELETE FROM {}
|
||||||
|
WHERE rowid = $1",
|
||||||
|
self.table
|
||||||
|
);
|
||||||
|
sqlx::query(&query).bind(id).execute(&mut self.conn).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_tables(&mut self) -> Result<()> {
|
||||||
|
for table in ["bookmarks", "reading_list"] {
|
||||||
|
let query = format!(
|
||||||
|
"CREATE TABLE IF NOT EXISTS {}
|
||||||
|
(link TEXT, comment TEXT, timestamp DATETIME)",
|
||||||
|
table
|
||||||
|
);
|
||||||
|
sqlx::query(&query).execute(&mut self.conn).await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
30
src/main.rs
30
src/main.rs
|
|
@ -1,15 +1,21 @@
|
||||||
struct Bookmark {
|
mod cli;
|
||||||
link: String,
|
mod db;
|
||||||
comment: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Desired actions:
|
use anyhow::anyhow;
|
||||||
* - add new bookmark/reading list item
|
use clap::Parser;
|
||||||
* - get bookmark
|
use cli::Cli;
|
||||||
* - get RL item and remove/delist
|
|
||||||
* - ensure nothing is lost accidentally
|
|
||||||
*/
|
|
||||||
|
|
||||||
fn main() {
|
pub(crate) type Result<T> = anyhow::Result<T>;
|
||||||
println!("Hello, world!");
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> Result<()> {
|
||||||
|
let cli = Cli::parse();
|
||||||
|
let location = std::env::var("BOOKMARK_DB_PATH")
|
||||||
|
.map_err(|_| anyhow!(
|
||||||
|
"No database path set! \
|
||||||
|
Please provide one with the BOOKMARK_DB_PATH environment \
|
||||||
|
variable.".to_string()
|
||||||
|
))?;
|
||||||
|
let store = db::Store::new(&location, cli.is_reading_list).await?;
|
||||||
|
cli.run(store).await
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue