mirror of
https://github.com/atuinsh/atuin.git
synced 2024-11-25 17:54:55 +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",
|
"password-hash",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "arrayvec"
|
||||||
|
version = "0.7.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-trait"
|
name = "async-trait"
|
||||||
version = "0.1.58"
|
version = "0.1.58"
|
||||||
@ -270,6 +276,7 @@ dependencies = [
|
|||||||
"async-trait",
|
"async-trait",
|
||||||
"atuin-client",
|
"atuin-client",
|
||||||
"atuin-common",
|
"atuin-common",
|
||||||
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
"eyre",
|
"eyre",
|
||||||
"fs-err",
|
"fs-err",
|
||||||
@ -278,6 +285,8 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"tantivy",
|
"tantivy",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"type-safe-id",
|
||||||
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -3507,6 +3516,19 @@ version = "0.2.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
|
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]]
|
[[package]]
|
||||||
name = "typed-builder"
|
name = "typed-builder"
|
||||||
version = "0.14.0"
|
version = "0.14.0"
|
||||||
|
@ -11,6 +11,7 @@ atuin-client = { path = "../atuin-client", version = "15.0.0" }
|
|||||||
|
|
||||||
eyre = { workspace = true }
|
eyre = { workspace = true }
|
||||||
async-trait = { workspace = true }
|
async-trait = { workspace = true }
|
||||||
|
chrono = { workspace = true }
|
||||||
clap = { workspace = true }
|
clap = { workspace = true }
|
||||||
fs-err = { workspace = true }
|
fs-err = { workspace = true }
|
||||||
rmp = { version = "0.8.11" }
|
rmp = { version = "0.8.11" }
|
||||||
@ -18,3 +19,5 @@ serde = { workspace = true }
|
|||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
tantivy = "0.20.2"
|
tantivy = "0.20.2"
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
|
type-safe-id = { version = "0.2.1", features = ["serde"] }
|
||||||
|
uuid = { workspace = true }
|
||||||
|
@ -8,13 +8,23 @@ use atuin_client::{
|
|||||||
settings::Settings,
|
settings::Settings,
|
||||||
};
|
};
|
||||||
use atuin_common::record::HostId;
|
use atuin_common::record::HostId;
|
||||||
|
use chrono::{Local, TimeZone};
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use eyre::{bail, Context, Result};
|
use eyre::{bail, Context, Result};
|
||||||
use record::TodoStore;
|
use record::{TodoRecord, TodoStore};
|
||||||
|
use type_safe_id::{StaticType, TypeSafeId};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
mod record;
|
mod record;
|
||||||
mod search;
|
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)]
|
#[derive(clap::Parser)]
|
||||||
#[command(author = "Conrad Ludgate <conradludgate@gmail.com>")]
|
#[command(author = "Conrad Ludgate <conradludgate@gmail.com>")]
|
||||||
enum Cmd {
|
enum Cmd {
|
||||||
@ -30,6 +40,25 @@ enum Cmd {
|
|||||||
/// todo text
|
/// todo text
|
||||||
text: String,
|
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 {
|
Search {
|
||||||
#[arg(long, default_value = "20")]
|
#[arg(long, default_value = "20")]
|
||||||
limit: usize,
|
limit: usize,
|
||||||
@ -37,6 +66,10 @@ enum Cmd {
|
|||||||
/// search query
|
/// search query
|
||||||
query: String,
|
query: String,
|
||||||
},
|
},
|
||||||
|
View {
|
||||||
|
#[arg(long)]
|
||||||
|
id: TodoId,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
@ -66,15 +99,74 @@ async fn main() -> Result<()> {
|
|||||||
|
|
||||||
match cmd {
|
match cmd {
|
||||||
Cmd::Push { state, tags, text } => {
|
Cmd::Push { state, tags, text } => {
|
||||||
todo_store
|
let record = TodoRecord {
|
||||||
.create_item(&mut store, &encryption_key, state, text, tags)
|
tags,
|
||||||
|
state,
|
||||||
|
text,
|
||||||
|
updates: TodoId::from_uuid(Uuid::nil()),
|
||||||
|
};
|
||||||
|
let record = todo_store
|
||||||
|
.create_item(&mut store, &encryption_key, record)
|
||||||
.await?;
|
.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 } => {
|
Cmd::Search { limit, query } => {
|
||||||
let ids = todo_store.search(&query, limit).await?;
|
let ids = todo_store.search(&query, limit).await?;
|
||||||
for id in ids {
|
for id in ids {
|
||||||
let record = todo_store.get(&mut store, &encryption_key, id).await?;
|
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,
|
//! state,
|
||||||
//! text,
|
//! text,
|
||||||
//! [tag],
|
//! [tag],
|
||||||
|
//! updates,
|
||||||
//! ]
|
//! ]
|
||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
use atuin_common::record::{DecryptedData, Record, RecordId};
|
use atuin_common::record::{DecryptedData, EncryptedData, Record, RecordId};
|
||||||
use eyre::{bail, ensure, eyre, Result};
|
use eyre::{bail, ensure, eyre, Context, ContextCompat, Result};
|
||||||
|
|
||||||
use atuin_client::record::encryption::paseto_v4::PASETO_V4;
|
use atuin_client::record::encryption::paseto_v4::PASETO_V4;
|
||||||
use atuin_client::record::store::Store;
|
use atuin_client::record::store::Store;
|
||||||
use atuin_client::settings::Settings;
|
use atuin_client::settings::Settings;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use tantivy::{collector::TopDocs, query::QueryParser, Index};
|
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_VERSION: &str = "v0";
|
||||||
const TODO_TAG: &str = "todo";
|
const TODO_TAG: &str = "todo";
|
||||||
@ -43,6 +48,7 @@ pub struct TodoRecord {
|
|||||||
pub state: String,
|
pub state: String,
|
||||||
pub text: String,
|
pub text: String,
|
||||||
pub tags: Vec<String>,
|
pub tags: Vec<String>,
|
||||||
|
pub updates: TodoId,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TodoRecord {
|
impl TodoRecord {
|
||||||
@ -52,7 +58,7 @@ impl TodoRecord {
|
|||||||
let mut output = vec![];
|
let mut output = vec![];
|
||||||
|
|
||||||
// INFO: ensure this is updated when adding new fields
|
// 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.state)?;
|
||||||
encode::write_str(&mut output, &self.text)?;
|
encode::write_str(&mut output, &self.text)?;
|
||||||
@ -62,6 +68,8 @@ impl TodoRecord {
|
|||||||
encode::write_str(&mut output, tag)?;
|
encode::write_str(&mut output, tag)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
encode::write_bin(&mut output, self.updates.uuid().as_bytes())?;
|
||||||
|
|
||||||
Ok(DecryptedData(output))
|
Ok(DecryptedData(output))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,32 +82,39 @@ impl TodoRecord {
|
|||||||
|
|
||||||
match version {
|
match version {
|
||||||
TODO_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)?;
|
let nfields = decode::read_array_len(&mut data)?;
|
||||||
ensure!(nfields == 3, "too many entries in v0 todo record");
|
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 ntags = decode::read_array_len(&mut data)?;
|
||||||
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 mut tags = Vec::with_capacity(ntags as usize);
|
let mut tags = Vec::with_capacity(ntags as usize);
|
||||||
for _ in 0..ntags {
|
for _ in 0..ntags {
|
||||||
let (value, b) = decode::read_str_from_slice(bytes).map_err(error_report)?;
|
let (value, b) = decode::read_str_from_slice(data).map_err(error_report)?;
|
||||||
bytes = b;
|
data = b;
|
||||||
tags.push(value.to_owned())
|
tags.push(value.to_owned())
|
||||||
}
|
}
|
||||||
|
|
||||||
if !bytes.is_empty() {
|
let updates_len = decode::read_bin_len(&mut data)?;
|
||||||
bail!("trailing bytes in encoded todo record. malformed")
|
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 {
|
Ok(TodoRecord {
|
||||||
state: state.to_owned(),
|
state: state.to_owned(),
|
||||||
text: text.to_owned(),
|
text: text.to_owned(),
|
||||||
tags,
|
tags,
|
||||||
|
updates,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
@ -132,14 +147,10 @@ impl TodoStore {
|
|||||||
&self,
|
&self,
|
||||||
store: &mut (impl Store + Send + Sync),
|
store: &mut (impl Store + Send + Sync),
|
||||||
encryption_key: &[u8; 32],
|
encryption_key: &[u8; 32],
|
||||||
state: String,
|
record: TodoRecord,
|
||||||
text: String,
|
) -> Result<Record<EncryptedData>> {
|
||||||
tags: Vec<String>,
|
|
||||||
) -> Result<()> {
|
|
||||||
let host_id = Settings::host_id().expect("failed to get host_id");
|
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 parent = store.tail(host_id, TODO_TAG).await?.map(|entry| entry.id);
|
||||||
|
|
||||||
let record = Record::builder()
|
let record = Record::builder()
|
||||||
@ -147,7 +158,7 @@ impl TodoStore {
|
|||||||
.version(TODO_VERSION.to_string())
|
.version(TODO_VERSION.to_string())
|
||||||
.tag(TODO_TAG.to_owned())
|
.tag(TODO_TAG.to_owned())
|
||||||
.parent(parent)
|
.parent(parent)
|
||||||
.data(todo)
|
.data(record)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
let mut writer = self.index.writer(3_000_000)?;
|
let mut writer = self.index.writer(3_000_000)?;
|
||||||
@ -162,16 +173,16 @@ impl TodoStore {
|
|||||||
let record = record.encrypt::<PASETO_V4>(encryption_key);
|
let record = record.encrypt::<PASETO_V4>(encryption_key);
|
||||||
store.push(&record).await?;
|
store.push(&record).await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(record)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get(
|
pub async fn get(
|
||||||
&self,
|
&self,
|
||||||
store: &mut (impl Store + Send + Sync),
|
store: &mut (impl Store + Send + Sync),
|
||||||
encryption_key: &[u8; 32],
|
encryption_key: &[u8; 32],
|
||||||
id: RecordId,
|
id: TodoId,
|
||||||
) -> Result<Record<TodoRecord>> {
|
) -> Result<Record<TodoRecord>> {
|
||||||
let record = store.get(id).await?;
|
let record = store.get(RecordId(id.uuid())).await?;
|
||||||
match &*record.version {
|
match &*record.version {
|
||||||
"v0" => {
|
"v0" => {
|
||||||
let record = record.decrypt::<PASETO_V4>(encryption_key)?;
|
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(
|
let query_parser = QueryParser::new(
|
||||||
self.index.schema(),
|
self.index.schema(),
|
||||||
vec![self.schema.text, self.schema.tag],
|
vec![self.schema.text, self.schema.tag],
|
||||||
@ -197,7 +208,12 @@ impl TodoStore {
|
|||||||
for (_, doc) in docs {
|
for (_, doc) in docs {
|
||||||
let doc = searcher.doc(doc)?;
|
let doc = searcher.doc(doc)?;
|
||||||
let id = doc.get_first(self.schema.id);
|
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)
|
Ok(output)
|
||||||
@ -206,6 +222,10 @@ impl TodoStore {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::TodoId;
|
||||||
|
|
||||||
use super::{TodoRecord, TODO_VERSION};
|
use super::{TodoRecord, TODO_VERSION};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -214,6 +234,7 @@ mod tests {
|
|||||||
state: "todo".to_owned(),
|
state: "todo".to_owned(),
|
||||||
text: "implement todo".to_owned(),
|
text: "implement todo".to_owned(),
|
||||||
tags: vec!["atuin".to_owned(), "rust".to_owned()],
|
tags: vec!["atuin".to_owned(), "rust".to_owned()],
|
||||||
|
updates: TodoId::from_uuid(Uuid::nil()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let encoded = kv.serialize().unwrap();
|
let encoded = kv.serialize().unwrap();
|
||||||
|
@ -4,10 +4,10 @@ use tantivy::{
|
|||||||
directory::MmapDirectory,
|
directory::MmapDirectory,
|
||||||
doc,
|
doc,
|
||||||
schema::{Field, Schema, STORED, STRING, TEXT},
|
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) {
|
pub fn schema() -> (TodoSchema, Schema) {
|
||||||
let mut schema_builder = Schema::builder();
|
let mut schema_builder = Schema::builder();
|
||||||
@ -47,7 +47,7 @@ pub fn write_record(
|
|||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let timestamp = DateTime::from_timestamp_nanos(record.timestamp as i64);
|
let timestamp = DateTime::from_timestamp_nanos(record.timestamp as i64);
|
||||||
let mut doc = doc!(
|
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.text => record.data.text.clone(),
|
||||||
schema.timestamp => timestamp,
|
schema.timestamp => timestamp,
|
||||||
);
|
);
|
||||||
@ -56,5 +56,12 @@ pub fn write_record(
|
|||||||
}
|
}
|
||||||
writer.add_document(doc)?;
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user