diff options
author | Toby Vincent <tobyv13@gmail.com> | 2023-03-18 19:01:16 -0500 |
---|---|---|
committer | Toby Vincent <tobyv13@gmail.com> | 2023-03-18 19:01:16 -0500 |
commit | b9890b578d8bcdb0d0a9c12ba3901b4e34514286 (patch) | |
tree | 6efbe30d3dfb5a26c7ea45d29bf357b60eb63015 /src | |
parent | 9b4f0bb63002a2657d89c7519697aabe515282b4 (diff) |
feat: impl history file and sorting
Diffstat (limited to 'src')
-rw-r--r-- | src/config.rs | 57 | ||||
-rw-r--r-- | src/history.rs | 55 | ||||
-rw-r--r-- | src/lib.rs | 51 | ||||
-rw-r--r-- | src/main.rs | 63 | ||||
-rw-r--r-- | src/tmux.rs | 50 | ||||
-rw-r--r-- | src/tmux/error.rs | 11 |
6 files changed, 244 insertions, 43 deletions
diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..7745e0b --- /dev/null +++ b/src/config.rs @@ -0,0 +1,57 @@ +use std::path::PathBuf; + +use clap::{Args, Parser}; +use tracing::{metadata::LevelFilter, Level}; + +#[derive(Debug, Clone, Parser)] +pub struct Config { + pub target: Option<String>, + + #[arg(short, long)] + pub list: bool, + + /// tmux socket-name, equivelent to `tmux -L <socket-name>` + #[arg(short = 'L', long, default_value = "ssh")] + pub socket_name: String, + + /// path to history file [default: $XDG_DATA_HOME/sshr/history] + #[arg(short = 'f', long)] + pub history_file: Option<PathBuf>, + + #[command(flatten)] + pub verbosity: Verbosity, +} + +#[derive(Debug, Default, Clone, Args)] +pub struct Verbosity { + /// Print additional information per occurrence. + /// + /// Conflicts with `--quiet`. + #[arg(short, long, global = true, action = clap::ArgAction::Count, conflicts_with = "quiet")] + pub verbose: u8, + + /// Suppress all output. + /// + /// Conflicts with `--verbose`. + #[arg(short, long, global = true, conflicts_with = "verbose")] + pub quiet: bool, +} + +impl From<&Verbosity> for Option<Level> { + fn from(value: &Verbosity) -> Self { + match 1 + value.verbose - u8::from(value.quiet) { + 0 => None, + 1 => Some(Level::ERROR), + 2 => Some(Level::WARN), + 3 => Some(Level::INFO), + 4 => Some(Level::DEBUG), + _ => Some(Level::TRACE), + } + } +} + +impl From<&Verbosity> for LevelFilter { + fn from(value: &Verbosity) -> Self { + Option::<Level>::from(value).into() + } +} diff --git a/src/history.rs b/src/history.rs new file mode 100644 index 0000000..9d29468 --- /dev/null +++ b/src/history.rs @@ -0,0 +1,55 @@ +use std::{ + fs::File, + io::{BufRead, BufReader}, + path::PathBuf, +}; + +use directories::ProjectDirs; + +use crate::Session; + +use super::SessionSource; + +#[derive(Debug)] +pub struct History(PathBuf); + +impl History { + pub fn new(history_file: PathBuf) -> Self { + Self(history_file) + } + + pub fn default_path() -> Option<PathBuf> { + ProjectDirs::from("", "", env!("CARGO_CRATE_NAME"))? + .state_dir()? + .join("history") + .into() + } +} + +impl SessionSource for History { + type Error = std::io::Error; + + type Iter = Vec<Session>; + + fn sessions(&self) -> Result<Self::Iter, Self::Error> { + let mut sessions = Vec::new(); + + let file = File::open(&self.0)?; + for res in BufReader::new(file).lines() { + let entry = match res { + Ok(entry) => entry, + Err(err) => { + tracing::warn!(?err, "Failed to read line from history file"); + continue; + } + }; + + match ron::from_str(&entry) { + Ok(session) => sessions.push(session), + Err(err) => tracing::warn!(?err, "Invalid history entry"), + } + } + + Ok(sessions) + } +} @@ -1,13 +1,50 @@ +use std::{collections::HashMap, fmt::Display, iter::IntoIterator}; + +use serde::Deserialize; + pub use config::Config; +pub use history::History; +pub use tmux::Tmux; + +mod config; +mod history; +mod tmux; + +pub type Timestamp = Option<u64>; + +pub trait SessionSource { + type Error: std::error::Error; -mod config { - use clap::Parser; + type Iter: IntoIterator<Item = Session>; - #[derive(Debug, Clone, Parser)] - pub struct Config { - pub target: Option<String>, + fn sessions(&self) -> Result<Self::Iter, Self::Error>; + + fn update( + &self, + mut hash_map: HashMap<String, Timestamp>, + ) -> Result<HashMap<String, Timestamp>, Self::Error> { + for session in self.sessions()? { + hash_map + .entry(session.name) + .and_modify(|o| { + if let Some(timestamp) = session.timestamp { + *o = o.map(|t| timestamp.max(t)); + } + }) + .or_insert(session.timestamp); + } + Ok(hash_map) + } +} + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Deserialize)] +pub struct Session { + pub timestamp: Timestamp, + pub name: String, +} - #[arg(short, long)] - pub list: bool, +impl Display for Session { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.name) } } diff --git a/src/main.rs b/src/main.rs index ebd0526..7534bb1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,57 +1,48 @@ -use std::{process::Command, time::Duration}; +use std::collections::HashMap; use clap::Parser; -use serde::Deserialize; -use sshr::Config; + +use sshr::{Config, History, Session, SessionSource, Timestamp, Tmux}; fn main() -> anyhow::Result<()> { let config = Config::parse(); + tracing_subscriber::fmt::fmt() + .with_max_level(&config.verbosity) + .without_time() + .init(); + if config.list { - list() + list_sessions(config) } else { switch(config.target) } } -#[derive(Debug, Deserialize)] -struct Session { - pub name: String, - pub created: u64, - pub attached: u64, -} +fn list_sessions(config: Config) -> anyhow::Result<()> { + let mut sessions: HashMap<String, Timestamp> = HashMap::new(); -fn list() -> anyhow::Result<()> { - let sessions = tmux_sessions()?; + let tmux = Tmux::new(config.socket_name); + sessions = tmux.update(sessions)?; - for session in sessions { - println!("{session:?}"); + if let Some(history_file) = config.history_file.or_else(History::default_path) { + if history_file.exists() { + sessions = History::new(history_file).update(sessions)?; + } } - Ok(()) -} + let mut sessions: Vec<Session> = sessions + .into_iter() + .map(|(name, timestamp)| Session { name, timestamp }) + .collect(); + + sessions.sort(); -fn tmux_sessions() -> anyhow::Result<Vec<Session>> { - let format = indoc::indoc! {r##"[ - #{S: Session( - name: "#S", - created: #{session_created}, - attached: #{session_last_attached} - ),} - ]"##}; - - let output = Command::new("tmux") - .arg("-L") - .arg("ssh") - .arg("display") - .arg("-p") - .arg(format) - .output()?; - - match std::str::from_utf8(&output.stdout)? { - "" => Ok(Vec::new()), - s => ron::from_str(s).map_err(Into::into), + for session in sessions { + println!("{session}"); } + + Ok(()) } fn switch(_target: Option<String>) -> anyhow::Result<()> { diff --git a/src/tmux.rs b/src/tmux.rs new file mode 100644 index 0000000..c6e83b1 --- /dev/null +++ b/src/tmux.rs @@ -0,0 +1,50 @@ +use std::process::Command; + +use crate::{Session, SessionSource}; + +pub use error::Error; + +mod error; + +#[derive(Debug)] +pub struct Tmux { + socket_name: String, +} + +impl Tmux { + const SESSION_FORMAT: &str = r##"Session( name: "#S", timestamp: Some(#{?session_last_attached,#{session_last_attached},#{session_created}}))"##; + + pub fn new(socket_name: String) -> Self { + Self { socket_name } + } +} + +impl SessionSource for Tmux { + type Error = Error; + + type Iter = Vec<Session>; + + fn sessions(&self) -> Result<Self::Iter, Self::Error> { + let output = Command::new("tmux") + .arg("-L") + .arg(&self.socket_name) + .arg("list-sessions") + .arg("-F") + .arg(Self::SESSION_FORMAT) + .output()? + .stdout; + + let sessions = std::str::from_utf8(&output)? + .lines() + .filter_map(|s| match ron::from_str(s) { + Ok(session) => Some(session), + Err(err) => { + tracing::warn!(?err, "Invalid session format"); + None + } + }) + .collect(); + + Ok(sessions) + } +} diff --git a/src/tmux/error.rs b/src/tmux/error.rs new file mode 100644 index 0000000..6da5830 --- /dev/null +++ b/src/tmux/error.rs @@ -0,0 +1,11 @@ +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("IO error: {0}")] + IO(#[from] std::io::Error), + + #[error(transparent)] + Format(#[from] ron::error::SpannedError), + + #[error("Parsing error: {0}")] + Parse(#[from] std::str::Utf8Error), +} |