summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorToby Vincent <tobyv13@gmail.com>2023-03-18 19:01:16 -0500
committerToby Vincent <tobyv13@gmail.com>2023-03-18 19:01:16 -0500
commitb9890b578d8bcdb0d0a9c12ba3901b4e34514286 (patch)
tree6efbe30d3dfb5a26c7ea45d29bf357b60eb63015 /src
parent9b4f0bb63002a2657d89c7519697aabe515282b4 (diff)
feat: impl history file and sorting
Diffstat (limited to 'src')
-rw-r--r--src/config.rs57
-rw-r--r--src/history.rs55
-rw-r--r--src/lib.rs51
-rw-r--r--src/main.rs63
-rw-r--r--src/tmux.rs50
-rw-r--r--src/tmux/error.rs11
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)
+ }
+}
diff --git a/src/lib.rs b/src/lib.rs
index d465d78..8d33eac 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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),
+}