personal fork of atuin without AI stuff, sync stuff, script management stuff
0

Configure Feed

Select the types of activity you want to include in your feed.

remove more utils

Signed-off-by: oppiliappan <me@oppi.li>

author
oppiliappan
date (Jul 18, 2026, 11:28 AM +0100) commit ec8806b3 parent 592dadca change-id ywrwvwrn
+1 -504
-16
crates/atuin-client/record-migrations/20230531212437_create-records.sql
··· 1 - -- Add migration script here 2 - create table if not exists records ( 3 - id text primary key, 4 - parent text unique, -- null if this is the first one 5 - host text not null, 6 - 7 - timestamp integer not null, 8 - tag text not null, 9 - version text not null, 10 - data blob not null, 11 - cek blob not null 12 - ); 13 - 14 - create index host_idx on records (host); 15 - create index tag_idx on records (tag); 16 - create index host_tag_idx on records (host, tag);
-15
crates/atuin-client/record-migrations/20231127090831_create-store.sql
··· 1 - -- Add migration script here 2 - create table if not exists store ( 3 - id text primary key, -- globally unique ID 4 - 5 - idx integer, -- incrementing integer ID unique per (host, tag) 6 - host text not null, -- references the host row 7 - tag text not null, 8 - 9 - timestamp integer not null, 10 - version text not null, 11 - data blob not null, 12 - cek blob not null 13 - ); 14 - 15 - create unique index record_uniq ON store(host, tag, idx);
+1 -273
crates/atuin-client/src/history.rs
··· 1 - use rmp::decode::{self, Bytes}; 2 - use rmp::encode; 3 1 use std::env; 4 2 5 - use atuin_common::record::DecryptedData; 6 3 use atuin_common::utils::{normalize_optional_string, uuid_v7}; 7 4 8 - use eyre::{Result, bail}; 9 - 10 5 use crate::secrets::SECRET_PATTERNS_RE; 11 6 use crate::settings::Settings; 12 7 use crate::utils::get_host_user; 13 - use crate::utils::rmp::{DecodeError, EncodeError, read_optional, read_string, write_optional}; 14 8 use time::OffsetDateTime; 15 9 16 10 pub(crate) mod builder; ··· 203 197 } 204 198 } 205 199 206 - /// Serializes a history entry in the V2 format. 207 - /// 208 - /// Differences from V1: 209 - /// 210 - /// * `intent` is always written; if `None`, nil is written to the output. 211 - /// * Added new field `shell`. 212 - /// 213 - /// V2 is designed to allow new fields to be added without incrementing the version. V1 cannot 214 - /// accommodate this because its deserialization routine errors if more than 11 fields are 215 - /// provided. 216 - pub fn serialize(&self) -> Result<DecryptedData, EncodeError> { 217 - let mut output = vec![]; 218 - 219 - // write the version 220 - encode::write_u16(&mut output, Version::LATEST.as_int())?; 221 - encode::write_array_len(&mut output, Version::LATEST.min_fields())?; 222 - 223 - encode::write_str(&mut output, &self.id.0)?; 224 - encode::write_u64(&mut output, self.timestamp.unix_timestamp_nanos() as u64)?; 225 - encode::write_sint(&mut output, self.duration)?; 226 - encode::write_sint(&mut output, self.exit)?; 227 - encode::write_str(&mut output, &self.command)?; 228 - encode::write_str(&mut output, &self.cwd)?; 229 - encode::write_str(&mut output, &self.session)?; 230 - encode::write_str(&mut output, &self.hostname)?; 231 - 232 - write_optional( 233 - &mut output, 234 - self.deleted_at.map(|d| d.unix_timestamp_nanos() as u64), 235 - encode::write_u64, 236 - )?; 237 - encode::write_str(&mut output, self.author.as_str())?; 238 - write_optional(&mut output, self.intent.as_deref(), encode::write_str)?; 239 - write_optional(&mut output, self.shell.as_deref(), encode::write_str)?; 240 - Ok(DecryptedData(output)) 241 - } 242 - 243 - pub fn deserialize(bytes: &[u8], version: &str) -> Result<History> { 244 - let Some(version) = Version::from_name(version) else { 245 - bail!("unknown version {version:?}"); 246 - }; 247 - 248 - let mut bytes = Bytes::new(bytes); 249 - 250 - let real_version = decode::read_u16(&mut bytes).map_err(DecodeError::from)?; 251 - if real_version != version.as_int() { 252 - bail!("expected to decode {version} record, found v{real_version}"); 253 - } 254 - 255 - let nfields = decode::read_array_len(&mut bytes).map_err(DecodeError::from)?; 256 - let min_fields = version.min_fields(); 257 - if nfields < min_fields || version.max_fields().is_some_and(|max| nfields > max) { 258 - bail!("unexpected number of fields ({nfields}) for history version {version}"); 259 - } 260 - 261 - let id = read_string(&mut bytes)?; 262 - let timestamp = decode::read_u64(&mut bytes).map_err(DecodeError::from)?; 263 - let duration = decode::read_int(&mut bytes).map_err(DecodeError::from)?; 264 - let exit = decode::read_int(&mut bytes).map_err(DecodeError::from)?; 265 - 266 - let command = read_string(&mut bytes)?; 267 - let cwd = read_string(&mut bytes)?; 268 - let session = read_string(&mut bytes)?; 269 - let hostname = read_string(&mut bytes)?; 270 - let deleted_at = read_optional(&mut bytes, decode::read_u64)?; 271 - 272 - let author = if version >= Version::One { 273 - read_optional(&mut bytes, read_string)? 274 - } else { 275 - None 276 - }; 277 - 278 - let intent = if match version { 279 - Version::Zero => false, 280 - Version::One => nfields > min_fields, 281 - Version::Two => true, 282 - } { 283 - read_optional(&mut bytes, read_string)? 284 - } else { 285 - None 286 - }; 287 - 288 - let shell = if version >= Version::Two { 289 - read_optional(&mut bytes, read_string)? 290 - } else { 291 - None 292 - }; 293 - 294 - if version < Version::Two && !bytes.remaining_slice().is_empty() { 295 - bail!("trailing bytes in encoded history. malformed") 296 - } 297 - 298 - Ok(History { 299 - id: id.into(), 300 - timestamp: OffsetDateTime::from_unix_timestamp_nanos(i128::from(timestamp))?, 301 - duration, 302 - exit, 303 - command, 304 - cwd, 305 - session, 306 - author: author.unwrap_or_else(|| Self::author_from_hostname(&hostname)), 307 - hostname, 308 - intent, 309 - deleted_at: deleted_at 310 - .map(|t| OffsetDateTime::from_unix_timestamp_nanos(i128::from(t))) 311 - .transpose()?, 312 - shell, 313 - }) 314 - } 315 - 316 200 /// Builder for a history entry that is imported from shell history. 317 201 /// 318 202 /// The only two required fields are `timestamp` and `command`. ··· 477 361 #[cfg(test)] 478 362 mod tests { 479 363 use regex::RegexSet; 480 - use time::macros::datetime; 481 364 482 365 use crate::{ 483 - history::{AUTHOR_FILTER_ALL_AGENT, AUTHOR_FILTER_ALL_USER, Version}, 366 + history::{AUTHOR_FILTER_ALL_AGENT, AUTHOR_FILTER_ALL_USER}, 484 367 settings::Settings, 485 368 }; 486 369 ··· 573 456 .into(); 574 457 575 458 assert!(stripe_key.should_save(&settings)); 576 - } 577 - 578 - #[test] 579 - fn test_serialize_deserialize() { 580 - let history = History { 581 - id: "66d16cbee7cd47538e5c5b8b44e9006e".to_owned().into(), 582 - timestamp: datetime!(2023-05-28 18:35:40.633872 +00:00), 583 - duration: 49206000, 584 - exit: 0, 585 - command: "git status".to_owned(), 586 - cwd: "/Users/conrad.ludgate/Documents/code/atuin".to_owned(), 587 - session: "b97d9a306f274473a203d2eba41f9457".to_owned(), 588 - hostname: "fvfg936c0kpf:conrad.ludgate".to_owned(), 589 - author: "conrad.ludgate".to_owned(), 590 - intent: None, 591 - deleted_at: None, 592 - shell: None, 593 - }; 594 - 595 - let serialized = history.serialize().expect("failed to serialize history"); 596 - assert_eq!( 597 - &serialized.0[0..3], 598 - [205, 0, 2], 599 - "should encode as history v2" 600 - ); 601 - 602 - let deserialized = History::deserialize(&serialized.0, Version::LATEST.name()) 603 - .expect("failed to deserialize history"); 604 - assert_eq!(history, deserialized); 605 - } 606 - 607 - #[test] 608 - fn test_serialize_deserialize_deleted() { 609 - let history = History { 610 - id: "66d16cbee7cd47538e5c5b8b44e9006e".to_owned().into(), 611 - timestamp: datetime!(2023-05-28 18:35:40.633872 +00:00), 612 - duration: 49206000, 613 - exit: 0, 614 - command: "git status".to_owned(), 615 - cwd: "/Users/conrad.ludgate/Documents/code/atuin".to_owned(), 616 - session: "b97d9a306f274473a203d2eba41f9457".to_owned(), 617 - hostname: "fvfg936c0kpf:conrad.ludgate".to_owned(), 618 - author: "conrad.ludgate".to_owned(), 619 - intent: None, 620 - deleted_at: Some(datetime!(2023-11-19 20:18 +00:00)), 621 - shell: Some("bash".into()), 622 - }; 623 - 624 - let serialized = history.serialize().expect("failed to serialize history"); 625 - 626 - let deserialized = History::deserialize(&serialized.0, Version::LATEST.name()) 627 - .expect("failed to deserialize history"); 628 - 629 - assert_eq!(history, deserialized); 630 - } 631 - 632 - #[test] 633 - fn test_serialize_deserialize_with_author_and_intent() { 634 - let history = History { 635 - id: "66d16cbee7cd47538e5c5b8b44e9006e".to_owned().into(), 636 - timestamp: datetime!(2023-05-28 18:35:40.633872 +00:00), 637 - duration: 49206000, 638 - exit: 0, 639 - command: "git status".to_owned(), 640 - cwd: "/Users/conrad.ludgate/Documents/code/atuin".to_owned(), 641 - session: "b97d9a306f274473a203d2eba41f9457".to_owned(), 642 - hostname: "fvfg936c0kpf:conrad.ludgate".to_owned(), 643 - author: "claude".to_owned(), 644 - intent: Some("check repository status".to_owned()), 645 - deleted_at: None, 646 - shell: Some("fish".into()), 647 - }; 648 - 649 - let serialized = history.serialize().expect("failed to serialize history"); 650 - let deserialized = History::deserialize(&serialized.0, Version::LATEST.name()) 651 - .expect("failed to deserialize history"); 652 - 653 - assert_eq!(history, deserialized); 654 - } 655 - 656 - #[test] 657 - fn test_serialize_deserialize_version() { 658 - let bytes_v0 = [ 659 - 205, 0, 0, 153, 217, 32, 54, 54, 100, 49, 54, 99, 98, 101, 101, 55, 99, 100, 52, 55, 660 - 53, 51, 56, 101, 53, 99, 53, 98, 56, 98, 52, 52, 101, 57, 48, 48, 54, 101, 207, 23, 99, 661 - 98, 117, 24, 210, 246, 128, 206, 2, 238, 210, 240, 0, 170, 103, 105, 116, 32, 115, 116, 662 - 97, 116, 117, 115, 217, 42, 47, 85, 115, 101, 114, 115, 47, 99, 111, 110, 114, 97, 100, 663 - 46, 108, 117, 100, 103, 97, 116, 101, 47, 68, 111, 99, 117, 109, 101, 110, 116, 115, 664 - 47, 99, 111, 100, 101, 47, 97, 116, 117, 105, 110, 217, 32, 98, 57, 55, 100, 57, 97, 665 - 51, 48, 54, 102, 50, 55, 52, 52, 55, 51, 97, 50, 48, 51, 100, 50, 101, 98, 97, 52, 49, 666 - 102, 57, 52, 53, 55, 187, 102, 118, 102, 103, 57, 51, 54, 99, 48, 107, 112, 102, 58, 667 - 99, 111, 110, 114, 97, 100, 46, 108, 117, 100, 103, 97, 116, 101, 192, 668 - ]; 669 - 670 - let bytes_v1 = [ 671 - 205, 0, 1, 155, 217, 32, 54, 54, 100, 49, 54, 99, 98, 101, 101, 55, 99, 100, 52, 55, 672 - 53, 51, 56, 101, 53, 99, 53, 98, 56, 98, 52, 52, 101, 57, 48, 48, 54, 101, 207, 23, 99, 673 - 98, 117, 24, 210, 246, 128, 206, 2, 238, 210, 240, 0, 170, 103, 105, 116, 32, 115, 116, 674 - 97, 116, 117, 115, 217, 42, 47, 85, 115, 101, 114, 115, 47, 99, 111, 110, 114, 97, 100, 675 - 46, 108, 117, 100, 103, 97, 116, 101, 47, 68, 111, 99, 117, 109, 101, 110, 116, 115, 676 - 47, 99, 111, 100, 101, 47, 97, 116, 117, 105, 110, 217, 32, 98, 57, 55, 100, 57, 97, 677 - 51, 48, 54, 102, 50, 55, 52, 52, 55, 51, 97, 50, 48, 51, 100, 50, 101, 98, 97, 52, 49, 678 - 102, 57, 52, 53, 55, 187, 102, 118, 102, 103, 57, 51, 54, 99, 48, 107, 112, 102, 58, 679 - 99, 111, 110, 114, 97, 100, 46, 108, 117, 100, 103, 97, 116, 101, 207, 24, 194, 83, 680 - 235, 108, 206, 10, 0, 174, 99, 111, 110, 114, 97, 100, 46, 108, 117, 100, 103, 97, 116, 681 - 101, 173, 115, 97, 109, 112, 108, 101, 32, 105, 110, 116, 101, 110, 116, 682 - ]; 683 - 684 - let expected_v2 = History { 685 - id: "66d16cbee7cd47538e5c5b8b44e9006e".to_owned().into(), 686 - timestamp: datetime!(2023-05-28 18:35:40.633872 +00:00), 687 - duration: 49206000, 688 - exit: 0, 689 - command: "git status".to_owned(), 690 - cwd: "/Users/conrad.ludgate/Documents/code/atuin".to_owned(), 691 - session: "b97d9a306f274473a203d2eba41f9457".to_owned(), 692 - hostname: "fvfg936c0kpf:conrad.ludgate".to_owned(), 693 - author: "conrad.ludgate".to_owned(), 694 - intent: Some("sample intent".to_owned()), 695 - deleted_at: Some(time::OffsetDateTime::from_unix_timestamp(1784080673).unwrap()), 696 - shell: Some("zsh".into()), 697 - }; 698 - let bytes_v2 = expected_v2 699 - .serialize() 700 - .expect("failed to serialize history"); 701 - 702 - let mut expected_v1 = expected_v2.clone(); 703 - expected_v1.shell = None; 704 - 705 - let mut expected_v0 = expected_v1.clone(); 706 - expected_v0.intent = None; 707 - expected_v0.deleted_at = None; 708 - 709 - let cases = [ 710 - (bytes_v0.as_slice(), expected_v0), 711 - (&bytes_v1, expected_v1), 712 - (&bytes_v2, expected_v2), 713 - ]; 714 - 715 - for (i, (bytes, expected)) in cases.into_iter().enumerate() { 716 - for version in Version::VARIANTS { 717 - let deserialized = History::deserialize(bytes, version.name()); 718 - if usize::from(version.as_int()) == i { 719 - let Ok(deserialized) = deserialized else { 720 - panic!("failed to deserialize {version}"); 721 - }; 722 - assert_eq!(deserialized, expected, "{version}"); 723 - } else { 724 - assert!( 725 - deserialized.is_err(), 726 - "unexpected success deserializing as {version}" 727 - ); 728 - } 729 - } 730 - } 731 459 } 732 460 }
-2
crates/atuin-client/src/utils.rs
··· 1 - pub(crate) mod rmp; 2 - 3 1 pub(crate) fn get_hostname() -> String { 4 2 std::env::var("ATUIN_HOST_NAME") 5 3 .unwrap_or_else(|_| whoami::hostname().unwrap_or_else(|_| "unknown-host".to_string()))
-123
crates/atuin-client/src/utils/rmp.rs
··· 1 - use rmp::Marker; 2 - use rmp::decode::bytes::{Bytes, BytesReadError}; 3 - use rmp::decode::{ 4 - self, DecodeStringError, NumValueReadError, RmpRead, RmpReadErr, ValueReadError, 5 - }; 6 - use rmp::encode::{self, RmpWrite, RmpWriteErr, ValueWriteError}; 7 - 8 - /// An error encountered while trying to encode a message with [`rmp`]. 9 - /// 10 - /// This is currently just a wrapper around [`ValueWriteError`] with a better error message. 11 - /// [`rmp`]'s error message does not indicate which variant the error is (`InvalidMarkerWrite` or 12 - /// `InvalidDataWrite`) and does not print anything about the inner I/O error of type `E`. 13 - #[derive(Debug, derive_more::Display, derive_more::From, thiserror::Error)] 14 - #[display("could not write MessagePack value: {_0:?}")] 15 - pub struct EncodeError<E: RmpWriteErr = std::io::Error>(ValueWriteError<E>); 16 - 17 - /// An error encountered while trying to decode a message with [`rmp`]. 18 - /// 19 - /// This is a wrapper the various types of errors that can be returned by [`rmp`]'s decoding 20 - /// functions. Unlike those types, this type implements [`Display`] with an error message that 21 - /// indicates which variant the error is ([`rmp`]'s error types are enums; some unconditionally 22 - /// print a static string and others don't even implement [`Display`] for all `E`). 23 - /// 24 - /// Conversion to [`eyre::Report`] is supported. This cannot be done by implementing 25 - /// [`std::error::Error`] because this type is not, in general, `'static`, so a manual 26 - /// implementation is provided. 27 - #[derive(Debug, derive_more::Display, derive_more::From)] 28 - #[display("could not decode MessagePack value: {_0:?}")] 29 - pub enum DecodeError<'a, E: RmpReadErr = BytesReadError> { 30 - DecodeString(DecodeStringError<'a, E>), 31 - NumValueRead(NumValueReadError<E>), 32 - ValueRead(ValueReadError<E>), 33 - } 34 - 35 - impl<E: RmpReadErr> DecodeError<'_, E> { 36 - pub fn type_mismatch(&self) -> Option<Marker> { 37 - match self { 38 - Self::DecodeString(DecodeStringError::TypeMismatch(m)) => Some(*m), 39 - Self::NumValueRead(NumValueReadError::TypeMismatch(m)) => Some(*m), 40 - Self::ValueRead(ValueReadError::TypeMismatch(m)) => Some(*m), 41 - _ => None, 42 - } 43 - } 44 - } 45 - 46 - impl<E: RmpReadErr> From<DecodeError<'_, E>> for eyre::Report { 47 - fn from(e: DecodeError<'_, E>) -> Self { 48 - eyre::eyre!("{e}") 49 - } 50 - } 51 - 52 - /// Read an owned string from a [`Bytes`] object. 53 - /// 54 - /// If you need an owned [`String`], this function is more convenient than using 55 - /// [`read_str_from_slice`] and converting the resulting [`str`], as you don't need to 56 - /// keep unwrapping and re-creating the [`Bytes`] object. 57 - /// 58 - /// [`read_str_from_slice`]: decode::read_str_from_slice 59 - pub fn read_string<'a>(bytes: &mut Bytes<'a>) -> Result<String, DecodeError<'a>> { 60 - let slice = bytes.remaining_slice(); 61 - let (string, rest) = match decode::read_str_from_slice(slice) { 62 - Ok(pair) => pair, 63 - Err(e) => { 64 - if let DecodeStringError::TypeMismatch(_) = e { 65 - // The decode functions in `rmp::decode` consume the marker byte when there's a 66 - // type mismatch; make sure we do that too, as `read_optional` depends on it. 67 - bytes 68 - .read_u8() 69 - .expect("TypeMismatch implies stream contains a marker byte"); 70 - } 71 - return Err(e.into()); 72 - } 73 - }; 74 - *bytes = Bytes::new(rest); 75 - Ok(string.into()) 76 - } 77 - 78 - /// Read an optional value from the stream. 79 - /// 80 - /// This function calls `read`, which should try to decode a value of type `T` from the stream. If 81 - /// that function returns an error indicating [`Marker::Null`] was encountered instead, this 82 - /// function returns [`None`]. All other errors are forwarded as-is. 83 - pub fn read_optional<'a, R, F, T, E>( 84 - input: &mut R, 85 - read: F, 86 - ) -> Result<Option<T>, DecodeError<'a, R::Error>> 87 - where 88 - R: RmpRead, 89 - R::Error: Send + Sync, 90 - F: FnOnce(&mut R) -> Result<T, E>, 91 - E: Into<DecodeError<'a, R::Error>>, 92 - { 93 - let err = match read(input) { 94 - Ok(v) => return Ok(Some(v)), 95 - Err(e) => e.into(), 96 - }; 97 - 98 - if let Some(Marker::Null) = err.type_mismatch() { 99 - Ok(None) 100 - } else { 101 - Err(err) 102 - } 103 - } 104 - 105 - /// Write an optional value to the stream. 106 - /// 107 - /// If `value` is [`Some`], this function calls `write` with the value, which should encode a value 108 - /// of type `T` to the stream. Otherwise, this function writes [`Marker::Null`]. 109 - pub fn write_optional<W, T, F>( 110 - output: &mut W, 111 - value: Option<T>, 112 - write: F, 113 - ) -> Result<(), ValueWriteError<W::Error>> 114 - where 115 - W: RmpWrite, 116 - F: FnOnce(&mut W, T) -> Result<(), ValueWriteError<W::Error>>, 117 - W::Error: Send + Sync, 118 - { 119 - match value { 120 - Some(v) => write(output, v), 121 - None => encode::write_nil(output).map_err(ValueWriteError::InvalidMarkerWrite), 122 - } 123 - }
-60
crates/atuin-common/src/lib.rs
··· 1 1 #![deny(unsafe_code)] 2 2 3 - /// Defines a new UUID type wrapper 4 - macro_rules! new_uuid { 5 - ($name:ident) => { 6 - #[derive( 7 - Debug, 8 - Copy, 9 - Clone, 10 - PartialEq, 11 - Eq, 12 - Hash, 13 - PartialOrd, 14 - Ord, 15 - serde::Serialize, 16 - serde::Deserialize, 17 - derive_more::Display, 18 - derive_more::From, 19 - derive_more::Deref, 20 - )] 21 - #[serde(transparent)] 22 - #[display("{_0}")] 23 - pub struct $name(pub Uuid); 24 - 25 - impl<DB: sqlx::Database> sqlx::Type<DB> for $name 26 - where 27 - Uuid: sqlx::Type<DB>, 28 - { 29 - fn type_info() -> <DB as sqlx::Database>::TypeInfo { 30 - Uuid::type_info() 31 - } 32 - } 33 - 34 - impl<'r, DB: sqlx::Database> sqlx::Decode<'r, DB> for $name 35 - where 36 - Uuid: sqlx::Decode<'r, DB>, 37 - { 38 - fn decode( 39 - value: DB::ValueRef<'r>, 40 - ) -> std::result::Result<Self, sqlx::error::BoxDynError> { 41 - Uuid::decode(value).map(Self) 42 - } 43 - } 44 - 45 - impl<'q, DB: sqlx::Database> sqlx::Encode<'q, DB> for $name 46 - where 47 - Uuid: sqlx::Encode<'q, DB>, 48 - { 49 - fn encode_by_ref( 50 - &self, 51 - buf: &mut DB::ArgumentBuffer<'q>, 52 - ) -> Result<sqlx::encode::IsNull, Box<dyn std::error::Error + Send + Sync + 'static>> 53 - { 54 - self.0.encode_by_ref(buf) 55 - } 56 - } 57 - }; 58 - } 59 - 60 3 pub mod api; 61 4 pub mod logs; 62 - pub mod record; 63 5 pub mod shell; 64 6 pub mod string; 65 7 #[cfg(feature = "test-utils")] 66 8 pub mod test_utils; 67 - pub mod tls; 68 - pub mod url; 69 9 pub mod utils;
-15
crates/atuin-common/src/tls.rs
··· 1 - use std::sync::Once; 2 - 3 - static INIT: Once = Once::new(); 4 - 5 - /// Ensure the rustls crypto provider (ring) is installed. 6 - /// 7 - /// Must be called before creating any reqwest clients. Safe to call 8 - /// multiple times — only the first call installs the provider. 9 - pub fn ensure_crypto_provider() { 10 - INIT.call_once(|| { 11 - rustls::crypto::ring::default_provider() 12 - .install_default() 13 - .expect("Failed to install rustls crypto provider"); 14 - }); 15 - }