mirror of
https://github.com/atuinsh/atuin.git
synced 2024-11-22 08:13:57 +01:00
add updates
This commit is contained in:
parent
9347eedbe3
commit
214e3c337c
22
Cargo.lock
generated
22
Cargo.lock
generated
@ -75,6 +75,12 @@ dependencies = [
|
||||
"password-hash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arrayvec"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711"
|
||||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.58"
|
||||
@ -270,6 +276,7 @@ dependencies = [
|
||||
"async-trait",
|
||||
"atuin-client",
|
||||
"atuin-common",
|
||||
"chrono",
|
||||
"clap",
|
||||
"eyre",
|
||||
"fs-err",
|
||||
@ -278,6 +285,8 @@ dependencies = [
|
||||
"serde_json",
|
||||
"tantivy",
|
||||
"tokio",
|
||||
"type-safe-id",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3507,6 +3516,19 @@ version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
|
||||
|
||||
[[package]]
|
||||
name = "type-safe-id"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb1211a53644c6a5ae26c45bdd520d1533b07dab50dc6af1a15063772f9061ad"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
"thiserror",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typed-builder"
|
||||
version = "0.14.0"
|
||||
|
@ -11,6 +11,7 @@ atuin-client = { path = "../atuin-client", version = "15.0.0" }
|
||||
|
||||
eyre = { workspace = true }
|
||||
async-trait = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
clap = { workspace = true }
|
||||
fs-err = { workspace = true }
|
||||
rmp = { version = "0.8.11" }
|
||||
@ -18,3 +19,5 @@ serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
tantivy = "0.20.2"
|
||||
tokio = { workspace = true }
|
||||
type-safe-id = { version = "0.2.1", features = ["serde"] }
|
||||
uuid = { workspace = true }
|
||||
|
@ -8,13 +8,23 @@ use atuin_client::{
|
||||
settings::Settings,
|
||||
};
|
||||
use atuin_common::record::HostId;
|
||||
use chrono::{Local, TimeZone};
|
||||
use clap::Parser;
|
||||
use eyre::{bail, Context, Result};
|
||||
use record::TodoStore;
|
||||
use record::{TodoRecord, TodoStore};
|
||||
use type_safe_id::{StaticType, TypeSafeId};
|
||||
use uuid::Uuid;
|
||||
|
||||
mod record;
|
||||
mod search;
|
||||
|
||||
#[derive(Default, Copy, Clone, PartialEq, Eq)]
|
||||
pub struct Todo;
|
||||
impl StaticType for Todo {
|
||||
const TYPE: &'static str = "todo";
|
||||
}
|
||||
pub type TodoId = TypeSafeId<Todo>;
|
||||
|
||||
#[derive(clap::Parser)]
|
||||
#[command(author = "Conrad Ludgate <conradludgate@gmail.com>")]
|
||||
enum Cmd {
|
||||
@ -30,6 +40,25 @@ enum Cmd {
|
||||
/// todo text
|
||||
text: String,
|
||||
},
|
||||
Update {
|
||||
#[arg(long)]
|
||||
id: TodoId,
|
||||
|
||||
/// The state this todo item is in. eg "todo", "in progress", etc
|
||||
#[arg(short = 's', long = "state")]
|
||||
state: Option<String>,
|
||||
|
||||
/// Whether to append the new tags or overwrite the tags
|
||||
#[arg(short, long)]
|
||||
append: bool,
|
||||
|
||||
/// tags for this todo item
|
||||
#[arg(short = 't', long = "tag")]
|
||||
tags: Vec<String>,
|
||||
|
||||
/// todo text
|
||||
text: Option<String>,
|
||||
},
|
||||
Search {
|
||||
#[arg(long, default_value = "20")]
|
||||
limit: usize,
|
||||
@ -37,6 +66,10 @@ enum Cmd {
|
||||
/// search query
|
||||
query: String,
|
||||
},
|
||||
View {
|
||||
#[arg(long)]
|
||||
id: TodoId,
|
||||
},
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
@ -66,15 +99,74 @@ async fn main() -> Result<()> {
|
||||
|
||||
match cmd {
|
||||
Cmd::Push { state, tags, text } => {
|
||||
todo_store
|
||||
.create_item(&mut store, &encryption_key, state, text, tags)
|
||||
let record = TodoRecord {
|
||||
tags,
|
||||
state,
|
||||
text,
|
||||
updates: TodoId::from_uuid(Uuid::nil()),
|
||||
};
|
||||
let record = todo_store
|
||||
.create_item(&mut store, &encryption_key, record)
|
||||
.await?;
|
||||
|
||||
println!("created {}", TodoId::from_uuid(record.id.0));
|
||||
}
|
||||
Cmd::Update {
|
||||
id,
|
||||
state,
|
||||
append,
|
||||
mut tags,
|
||||
text,
|
||||
} => {
|
||||
let mut record = todo_store.get(&mut store, &encryption_key, id).await?.data;
|
||||
record.updates = id;
|
||||
if append {
|
||||
record.tags.append(&mut tags)
|
||||
} else if !tags.is_empty() {
|
||||
record.tags = tags;
|
||||
}
|
||||
if let Some(state) = state {
|
||||
record.state = state;
|
||||
}
|
||||
if let Some(text) = text {
|
||||
record.text = text;
|
||||
}
|
||||
|
||||
let record = todo_store
|
||||
.create_item(&mut store, &encryption_key, record)
|
||||
.await?;
|
||||
|
||||
println!("updated from {} to {}", id, TodoId::from_uuid(record.id.0));
|
||||
}
|
||||
Cmd::Search { limit, query } => {
|
||||
let ids = todo_store.search(&query, limit).await?;
|
||||
for id in ids {
|
||||
let record = todo_store.get(&mut store, &encryption_key, id).await?;
|
||||
println!("{}", serde_json::to_string(&record).unwrap());
|
||||
println!("{id} - [{}] {}", record.data.state, record.data.text);
|
||||
if !record.data.tags.is_empty() {
|
||||
print!("\t");
|
||||
for tag in record.data.tags {
|
||||
print!("#{tag} ")
|
||||
}
|
||||
println!()
|
||||
}
|
||||
}
|
||||
}
|
||||
Cmd::View { mut id } => {
|
||||
while !id.uuid().is_nil() {
|
||||
let record = todo_store.get(&mut store, &encryption_key, id).await?;
|
||||
id = record.data.updates;
|
||||
let ts = Local
|
||||
.timestamp_nanos(record.timestamp as i64)
|
||||
.format("%Y %B %-d %R");
|
||||
println!("{ts} - [{}] {}", record.data.state, record.data.text);
|
||||
if !record.data.tags.is_empty() {
|
||||
print!("\t");
|
||||
for tag in record.data.tags {
|
||||
print!("#{tag} ")
|
||||
}
|
||||
println!()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,19 +21,24 @@
|
||||
//! state,
|
||||
//! text,
|
||||
//! [tag],
|
||||
//! updates,
|
||||
//! ]
|
||||
//! ```
|
||||
|
||||
use atuin_common::record::{DecryptedData, Record, RecordId};
|
||||
use eyre::{bail, ensure, eyre, Result};
|
||||
use atuin_common::record::{DecryptedData, EncryptedData, Record, RecordId};
|
||||
use eyre::{bail, ensure, eyre, Context, ContextCompat, Result};
|
||||
|
||||
use atuin_client::record::encryption::paseto_v4::PASETO_V4;
|
||||
use atuin_client::record::store::Store;
|
||||
use atuin_client::settings::Settings;
|
||||
use serde::Serialize;
|
||||
use tantivy::{collector::TopDocs, query::QueryParser, Index};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::search::{self, TodoSchema};
|
||||
use crate::{
|
||||
search::{self, TodoSchema},
|
||||
TodoId,
|
||||
};
|
||||
|
||||
const TODO_VERSION: &str = "v0";
|
||||
const TODO_TAG: &str = "todo";
|
||||
@ -43,6 +48,7 @@ pub struct TodoRecord {
|
||||
pub state: String,
|
||||
pub text: String,
|
||||
pub tags: Vec<String>,
|
||||
pub updates: TodoId,
|
||||
}
|
||||
|
||||
impl TodoRecord {
|
||||
@ -52,7 +58,7 @@ impl TodoRecord {
|
||||
let mut output = vec![];
|
||||
|
||||
// INFO: ensure this is updated when adding new fields
|
||||
encode::write_array_len(&mut output, 3)?;
|
||||
encode::write_array_len(&mut output, 4)?;
|
||||
|
||||
encode::write_str(&mut output, &self.state)?;
|
||||
encode::write_str(&mut output, &self.text)?;
|
||||
@ -62,6 +68,8 @@ impl TodoRecord {
|
||||
encode::write_str(&mut output, tag)?;
|
||||
}
|
||||
|
||||
encode::write_bin(&mut output, self.updates.uuid().as_bytes())?;
|
||||
|
||||
Ok(DecryptedData(output))
|
||||
}
|
||||
|
||||
@ -74,32 +82,39 @@ impl TodoRecord {
|
||||
|
||||
match version {
|
||||
TODO_VERSION => {
|
||||
let mut bytes = decode::Bytes::new(&data.0);
|
||||
// let mut rd = decode::Bytes::new(&data.0);
|
||||
let mut data = &*data.0;
|
||||
|
||||
let nfields = decode::read_array_len(&mut bytes).map_err(error_report)?;
|
||||
ensure!(nfields == 3, "too many entries in v0 todo record");
|
||||
let nfields = decode::read_array_len(&mut data)?;
|
||||
ensure!(
|
||||
nfields == 4,
|
||||
"incorrect number of entries in v0 todo record"
|
||||
);
|
||||
|
||||
let bytes = bytes.remaining_slice();
|
||||
let (state, data) = decode::read_str_from_slice(data).map_err(error_report)?;
|
||||
let (text, mut data) = decode::read_str_from_slice(data).map_err(error_report)?;
|
||||
|
||||
let (state, bytes) = decode::read_str_from_slice(bytes).map_err(error_report)?;
|
||||
let (text, mut bytes) = decode::read_str_from_slice(bytes).map_err(error_report)?;
|
||||
|
||||
let ntags = decode::read_array_len(&mut bytes).map_err(error_report)?;
|
||||
let ntags = decode::read_array_len(&mut data)?;
|
||||
let mut tags = Vec::with_capacity(ntags as usize);
|
||||
for _ in 0..ntags {
|
||||
let (value, b) = decode::read_str_from_slice(bytes).map_err(error_report)?;
|
||||
bytes = b;
|
||||
let (value, b) = decode::read_str_from_slice(data).map_err(error_report)?;
|
||||
data = b;
|
||||
tags.push(value.to_owned())
|
||||
}
|
||||
|
||||
if !bytes.is_empty() {
|
||||
bail!("trailing bytes in encoded todo record. malformed")
|
||||
}
|
||||
let updates_len = decode::read_bin_len(&mut data)?;
|
||||
ensure!(updates_len == 16, "incorrect UUID encoding in todo record");
|
||||
let updates: [u8; 16] = data
|
||||
.try_into()
|
||||
.context("incorrect UUID encoding in todo record")?;
|
||||
|
||||
let updates = TodoId::from_uuid(Uuid::from_bytes(updates));
|
||||
|
||||
Ok(TodoRecord {
|
||||
state: state.to_owned(),
|
||||
text: text.to_owned(),
|
||||
tags,
|
||||
updates,
|
||||
})
|
||||
}
|
||||
_ => {
|
||||
@ -132,14 +147,10 @@ impl TodoStore {
|
||||
&self,
|
||||
store: &mut (impl Store + Send + Sync),
|
||||
encryption_key: &[u8; 32],
|
||||
state: String,
|
||||
text: String,
|
||||
tags: Vec<String>,
|
||||
) -> Result<()> {
|
||||
record: TodoRecord,
|
||||
) -> Result<Record<EncryptedData>> {
|
||||
let host_id = Settings::host_id().expect("failed to get host_id");
|
||||
|
||||
let todo = TodoRecord { state, text, tags };
|
||||
|
||||
let parent = store.tail(host_id, TODO_TAG).await?.map(|entry| entry.id);
|
||||
|
||||
let record = Record::builder()
|
||||
@ -147,7 +158,7 @@ impl TodoStore {
|
||||
.version(TODO_VERSION.to_string())
|
||||
.tag(TODO_TAG.to_owned())
|
||||
.parent(parent)
|
||||
.data(todo)
|
||||
.data(record)
|
||||
.build();
|
||||
|
||||
let mut writer = self.index.writer(3_000_000)?;
|
||||
@ -162,16 +173,16 @@ impl TodoStore {
|
||||
let record = record.encrypt::<PASETO_V4>(encryption_key);
|
||||
store.push(&record).await?;
|
||||
|
||||
Ok(())
|
||||
Ok(record)
|
||||
}
|
||||
|
||||
pub async fn get(
|
||||
&self,
|
||||
store: &mut (impl Store + Send + Sync),
|
||||
encryption_key: &[u8; 32],
|
||||
id: RecordId,
|
||||
id: TodoId,
|
||||
) -> Result<Record<TodoRecord>> {
|
||||
let record = store.get(id).await?;
|
||||
let record = store.get(RecordId(id.uuid())).await?;
|
||||
match &*record.version {
|
||||
"v0" => {
|
||||
let record = record.decrypt::<PASETO_V4>(encryption_key)?;
|
||||
@ -182,7 +193,7 @@ impl TodoStore {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn search(&self, query: &str, limit: usize) -> Result<Vec<RecordId>> {
|
||||
pub async fn search(&self, query: &str, limit: usize) -> Result<Vec<TodoId>> {
|
||||
let query_parser = QueryParser::new(
|
||||
self.index.schema(),
|
||||
vec![self.schema.text, self.schema.tag],
|
||||
@ -197,7 +208,12 @@ impl TodoStore {
|
||||
for (_, doc) in docs {
|
||||
let doc = searcher.doc(doc)?;
|
||||
let id = doc.get_first(self.schema.id);
|
||||
output.push(RecordId(id.unwrap().as_text().unwrap().parse().unwrap()))
|
||||
output.push(
|
||||
id.context("missing id")?
|
||||
.as_text()
|
||||
.context("invalid id")?
|
||||
.parse()?,
|
||||
)
|
||||
}
|
||||
|
||||
Ok(output)
|
||||
@ -206,6 +222,10 @@ impl TodoStore {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::TodoId;
|
||||
|
||||
use super::{TodoRecord, TODO_VERSION};
|
||||
|
||||
#[test]
|
||||
@ -214,6 +234,7 @@ mod tests {
|
||||
state: "todo".to_owned(),
|
||||
text: "implement todo".to_owned(),
|
||||
tags: vec!["atuin".to_owned(), "rust".to_owned()],
|
||||
updates: TodoId::from_uuid(Uuid::nil()),
|
||||
};
|
||||
|
||||
let encoded = kv.serialize().unwrap();
|
||||
|
@ -4,10 +4,10 @@ use tantivy::{
|
||||
directory::MmapDirectory,
|
||||
doc,
|
||||
schema::{Field, Schema, STORED, STRING, TEXT},
|
||||
DateTime, Index, IndexWriter,
|
||||
DateTime, Index, IndexWriter, Term,
|
||||
};
|
||||
|
||||
use crate::record::TodoRecord;
|
||||
use crate::{record::TodoRecord, TodoId};
|
||||
|
||||
pub fn schema() -> (TodoSchema, Schema) {
|
||||
let mut schema_builder = Schema::builder();
|
||||
@ -47,7 +47,7 @@ pub fn write_record(
|
||||
) -> Result<()> {
|
||||
let timestamp = DateTime::from_timestamp_nanos(record.timestamp as i64);
|
||||
let mut doc = doc!(
|
||||
schema.id => record.id.0.to_string(),
|
||||
schema.id => TodoId::from_uuid(record.id.0).to_string(),
|
||||
schema.text => record.data.text.clone(),
|
||||
schema.timestamp => timestamp,
|
||||
);
|
||||
@ -56,5 +56,12 @@ pub fn write_record(
|
||||
}
|
||||
writer.add_document(doc)?;
|
||||
|
||||
if !record.data.updates.uuid().is_nil() {
|
||||
writer.delete_term(Term::from_field_text(
|
||||
schema.id,
|
||||
&record.data.updates.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user