Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 20 additions & 79 deletions src/commands/list.rs
Original file line number Diff line number Diff line change
@@ -1,84 +1,37 @@
use super::RunnableCommand;
use crate::errors::ReplayResult;
use crate::session::MetaData;
use crate::session::DisplayMeta;
use crate::session::Session;
use chrono::Utc;
use clap::Args;

#[derive(Args, PartialEq, Eq, Debug)]
pub struct ListCommand {}

impl RunnableCommand for ListCommand {
fn run(&self) -> ReplayResult<()> {
let list_lines = Self::list()?;
for line in list_lines {
let line = line?;
println!("{}", line);
for session_infos in Self::list()? {
println!("{}", session_infos?)
}
Ok(())
}
}

impl ListCommand {
pub fn list() -> ReplayResult<impl Iterator<Item = ReplayResult<String>>> {
let iter = Session::iter_session_ids_rev()?;
Ok(iter.enumerate().map(|(i, line_res)| {
let line = line_res?;
let session_metadata = Session::load_metadata_by_index(&line)?;
Ok(format!(
"replay@{{{}}}: {}",
i,
Self::display_metadata(session_metadata)
))
}))
}
fn truncate_description(line: &str, max_len: usize) -> String {
let truncated: String = line.chars().take(max_len).collect();
if line.chars().count() > max_len {
truncated + "..."
} else {
truncated
}
}

fn display_metadata(metadata: MetaData) -> String {
if let Some(dess) = metadata.description {
let list_message = format!(
"{}, message: {}",
Self::adapt_date_metadata(metadata.timestamp),
dess,
);
Self::truncate_description(&list_message, 50)
} else {
let first_commands_stylized = metadata.first_commands.join(" | ");
let list_message = format!(
"{}, commands: {}",
Self::adapt_date_metadata(metadata.timestamp),
first_commands_stylized,
);
Self::truncate_description(&list_message, 50)
}
}

fn adapt_date_metadata(timestamp: chrono::DateTime<Utc>) -> String {
let duration = Utc::now().signed_duration_since(timestamp);

if duration.num_days() > 0 {
format!("{} days ago", duration.num_days())
} else if duration.num_hours() > 0 {
format!("{} hours ago", duration.num_hours())
} else if duration.num_minutes() > 0 {
format!("{} minutes ago", duration.num_minutes())
} else {
format!("{} seconds ago", duration.num_seconds())
}
fn list() -> ReplayResult<impl Iterator<Item = ReplayResult<String>>> {
Ok(Session::get_all_session_metadata()?.enumerate().map(
|(i, metadata)| -> ReplayResult<String> {
let md = metadata?;
Ok(DisplayMeta { index: i, meta: md }.to_string())
},
))
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::session::tests::setup;
use regex::Regex;
use serial_test::serial;

#[test]
Expand All @@ -100,26 +53,14 @@ mod tests {
.unwrap()
.collect::<Result<Vec<_>, _>>()
.unwrap();
assert_eq!(
format!(
"replay@{{0}}: {}, message: session message is too lon...",
ListCommand::adapt_date_metadata(session_3.timestamp),
),
list_output[0]
);
assert_eq!(
format!(
"replay@{{1}}: {}, message: test session 2",
ListCommand::adapt_date_metadata(session_2.timestamp)
),
list_output[1]
);
assert_eq!(
format!(
"replay@{{2}}: {}, commands: ls | echo test",
ListCommand::adapt_date_metadata(session_1.timestamp)
),
list_output[2]
);
let re1 = Regex::new(
r"^replay@\{0\}: \d+ seconds ago, message: session message is too lon\.\.\.$",
)
.unwrap();
assert!(re1.is_match(&list_output[0]));
let re2 = Regex::new(r"^replay@\{1\}: \d+ seconds ago, message: test session 2$").unwrap();
assert!(re2.is_match(&list_output[1]));
let re3 = Regex::new(r"^replay@\{2\}: \d+ seconds ago, commands: ls | echo test$").unwrap();
assert!(re3.is_match(&list_output[2]));
}
}
66 changes: 66 additions & 0 deletions src/session/display.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
use super::MetaData;
use chrono::Utc;

/// Small owned wrapper that knows how to format a MetaData for display.
/// We keep it *owned* to simplify usage where metadata comes from an iterator.
pub struct DisplayMeta {
pub index: usize,
pub meta: MetaData,
}

impl std::fmt::Display for DisplayMeta {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(desc) = &self.meta.description {
let list_message = format!(
"{}, message: {}",
Self::format_time_ago(self.meta.timestamp),
desc,
);
write!(
f,
"replay@{{{}}}: {}",
self.index,
Self::truncate_description(&list_message, 50)
)
} else {
let first_commands_stylized = self.meta.first_commands.join(" | ");
let list_message = format!(
"{}, commands: {}",
Self::format_time_ago(self.meta.timestamp),
first_commands_stylized,
);
write!(
f,
"replay@{{{}}}: {}",
self.index,
Self::truncate_description(&list_message, 50)
)
}
}
}

/// Helpers (kept here for locality).
impl DisplayMeta {
fn format_time_ago(timestamp: chrono::DateTime<Utc>) -> String {
let duration = Utc::now().signed_duration_since(timestamp);

if duration.num_days() > 0 {
format!("{} days ago", duration.num_days())
} else if duration.num_hours() > 0 {
format!("{} hours ago", duration.num_hours())
} else if duration.num_minutes() > 0 {
format!("{} minutes ago", duration.num_minutes())
} else {
format!("{} seconds ago", duration.num_seconds())
}
}

fn truncate_description(line: &str, max_len: usize) -> String {
let truncated: String = line.chars().take(max_len).collect();
if line.chars().count() > max_len {
truncated + "..."
} else {
truncated
}
}
}
126 changes: 126 additions & 0 deletions src/session/index.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
use crate::errors::{ReplayError, ReplayResult};
use crate::paths;
use std::fs::File;
use std::io::{Read, Seek, SeekFrom, Write};
use std::path::PathBuf;

const INDEX_SIZE: u64 = 64;
pub struct SessionIndexFile;

struct RevIndexIter {
file: File,
index_size: u64,
current: u64,
}

impl RevIndexIter {
pub fn new(file: File, index_size: u64) -> ReplayResult<Self> {
let metadata = file.metadata()?;
let total_records = metadata.len() / index_size;
Ok(Self {
file,
index_size,
current: total_records,
})
}
}

impl Iterator for RevIndexIter {
type Item = ReplayResult<String>;

fn next(&mut self) -> Option<Self::Item> {
if self.current == 0 {
return None;
}
self.current -= 1;
let pos = self.current * self.index_size;
let result = (|| -> ReplayResult<String> {
self.file.seek(SeekFrom::Start(pos))?;
let mut buf = vec![0u8; self.index_size as usize];
self.file.read_exact(&mut buf)?;
Ok(String::from_utf8_lossy(&buf).to_string())
})();

Some(result)
}
}

impl SessionIndexFile {
pub(super) fn get_path() -> PathBuf {
paths::replay_dir().join("session_idx")
}

fn open_file() -> ReplayResult<std::fs::File> {
Ok(std::fs::OpenOptions::new()
.read(true)
.create(true)
.append(true)
.open(Self::get_path())?)
}

pub fn push_session(session_id: &str) -> ReplayResult<()> {
let mut file = Self::open_file()?;
write!(file, "{}", session_id)?;
Ok(())
}

fn get_id_offset_by_index(n: u32) -> ReplayResult<u64> {
let file = Self::open_file()?;
let file_size = file.metadata()?.len();
if file_size == 0 {
return Err(ReplayError::SessionError("No replay entries found".into()));
}

let total_lines = file_size / INDEX_SIZE;
if n as u64 >= total_lines {
return Err(ReplayError::SessionError(
"Replay index out of range".into(),
));
}

Ok(file_size - (n as u64 + 1) * INDEX_SIZE)
}

fn read_id_at(offset: u64) -> Result<String, ReplayError> {
let mut file = Self::open_file()?;
file.seek(SeekFrom::Start(offset))?;
let mut buf = vec![0u8; INDEX_SIZE as usize];
file.read_exact(&mut buf)?;

Ok(String::from_utf8_lossy(&buf).to_string())
}

/// Get the nth session id and remove it from the file
pub fn remove_session_id(n: u32) -> ReplayResult<String> {
let mut file = Self::open_file()?;
let id_offset = Self::get_id_offset_by_index(n)?;

let session_id = Self::read_id_at(id_offset)?;

// Calculate the position of next id
let next_id_offset = id_offset + INDEX_SIZE;

// Read the end of the file after this line
let mut rest = Vec::new();
file.seek(SeekFrom::Start(next_id_offset))?;
file.read_to_end(&mut rest)?;

// Truncate and rewrite the rest
file.set_len(id_offset)?;
file.seek(SeekFrom::Start(id_offset))?;
file.write_all(&rest)?;
file.flush()?;

Ok(session_id)
}

pub fn get_session_id(index: u32) -> ReplayResult<String> {
let line_offset = Self::get_id_offset_by_index(index)?;
Self::read_id_at(line_offset)
}
pub fn iter_session_ids_rev() -> ReplayResult<impl Iterator<Item = ReplayResult<String>>> {
let file = SessionIndexFile::open_file()?;
let iter = RevIndexIter::new(file, INDEX_SIZE)?;
Ok(iter)
}
}
Loading