Compare commits

..

No commits in common. "main" and "repo-var" have entirely different histories.

12 changed files with 83 additions and 175 deletions

View file

@ -1,6 +1,6 @@
[package] [package]
name = "remux" name = "remux"
version = "0.4.0" version = "0.3.5"
edition = "2021" edition = "2021"
authors = [ "Valerie Wolfe <sleeplessval@gmail.com>" ] authors = [ "Valerie Wolfe <sleeplessval@gmail.com>" ]
description = "A friendly command shortener for tmux" description = "A friendly command shortener for tmux"

View file

@ -92,16 +92,10 @@ using an AUR package manager such as <a href="https://github.com/Morganamilo/par
Install the package using Cargo with the command <code>cargo install tmux-remux</code>. Install the package using Cargo with the command <code>cargo install tmux-remux</code>.
</details> </details>
### Supplemental ### Man Page
<details> <details>
<summary>Bash Completions</summary> <summary>Section 1</summary>
Copy <code>bash-completion/remux</code> to the appropriate directory, typically
<code>/usr/share/bash-completion</code>.
</details>
<details>
<summary>Man Page: Section 1</summary>
Copy <code>man/remux.1</code> into <code>/usr/share/man/man1/</code>. Copy <code>man/remux.1</code> into <code>/usr/share/man/man1/</code>.
</details> </details>

View file

@ -1,19 +0,0 @@
_remux() {
local word
COMPREPLY=()
word="${COMP_WORDS[COMP_CWORD]}"
case $COMP_CWORD in
1)
COMPREPLY=( `compgen -W 'attach detach has help list new path switch title' -- "$word"` )
;;
2)
COMPREPLY=( `compgen -W "$(remux l -q $word)"` )
;;
esac
return 0
}
complete -F _remux remux

View file

@ -1,5 +1,6 @@
.Dd $Mdocdate$ .Dd $Mdocdate$
.Dt REMUX 1 .Dt REMUX 1
.Os
.Sh NAME .Sh NAME
.Nm remux .Nm remux
.Nd a command shortener for .Nd a command shortener for
@ -7,7 +8,6 @@
.Sh SYNOPSIS .Sh SYNOPSIS
.Nm remux .Nm remux
.Op Fl dhnqrtv .Op Fl dhnqrtv
.Op Fl D Ar path
.Op Ar command .Op Ar command
.Op args... .Op args...
.Sh DESCRIPTION .Sh DESCRIPTION
@ -35,8 +35,6 @@ Attaches to an existing session.
.Bl -tag -width Ds -compact .Bl -tag -width Ds -compact
.It Fl d , Fl -detach .It Fl d , Fl -detach
Detach all other connections to the session. Detach all other connections to the session.
.It Fl D , Fl -dir Ar path
Sets the working directory for the given command.
.It Fl n , Fl -nest .It Fl n , Fl -nest
Allow nesting (attaching a session from inside another session). Allow nesting (attaching a session from inside another session).
.It Fl r , Fl -read-only .It Fl r , Fl -read-only
@ -103,8 +101,8 @@ aliases: p
Prints the session path. Prints the session path.
.Ed .Ed
.It Xo Ic switch .It Xo Ic switch
.Op Fl rd .Op Fl r , Fl -read-only
.Op Ar title .Ar title
.Xc .Xc
.Bd -literal -compact .Bd -literal -compact
aliases: s aliases: s
@ -112,12 +110,10 @@ Switches from the current session to the target.
.Ed .Ed
.Pp .Pp
.Bl -tag -width Ds -compact .Bl -tag -width Ds -compact
.It Fl d , Fl -detach
Detaches other clients from the target session.
.It Fl r , Fl -read-only .It Fl r , Fl -read-only
Switch to the target session in read-only mode. Switch to the target session in read-only mode.
.It Ar title .It Ar title
The title of the session to switch to. If blank, the previous session will be used. The title of the session to switch to.
.El .El
.It Ic title .It Ic title
.Bd -literal -compact .Bd -literal -compact
@ -139,11 +135,6 @@ Default: '>'
.It Ev REMUX_NEW_WINDOW .It Ev REMUX_NEW_WINDOW
Provides a default windows name when creating a new session. Unused if empty. Provides a default windows name when creating a new session. Unused if empty.
Default: (unset) Default: (unset)
.It Ev REMUX_PREVIOUS_SYMBOL
Changes the symbol displayed for the previous session in the
.Ic list
command.
Default: '-'
.It Ev REMUX_REPO_FILE .It Ev REMUX_REPO_FILE
The filename to match on when trying to find the root of a repository. The filename to match on when trying to find the root of a repository.
Default: '.git' Default: '.git'

View file

@ -8,18 +8,21 @@ use tmux_interface::{
use crate::{ use crate::{
error, error,
state::State, state::State,
util::{ util::{ self, NULL }
self,
message,
MSG_PREVIOUS, MSG_SESSION_PATH, NULL
}
}; };
pub fn path(state: &mut State) { pub fn path(state: &mut State) {
state.session_enforce("path"); state.session_enforce("path");
if let Some(message) = message(MSG_SESSION_PATH) { let message = commands::DisplayMessage::new().print().message("#{session_path}");
println!("{message}");
let result = Tmux::new().add_command(message).output().unwrap();
let text = String::from_utf8(result.0.stdout);
if let Ok(output) = text {
// trim the trailing line break
let target = output.len() - 1;
println!("{}", &output[0..target]);
} }
} }
@ -30,33 +33,21 @@ pub fn switch(state: &mut State) {
// consume optional flags // consume optional flags
let read_only = state.flags.read_only; let read_only = state.flags.read_only;
let detach_other = state.flags.detached; //TODO: -d flag handling needs to be done manually
let args = state.args.clone().finish(); let args = state.args.clone().finish();
let target: String = match if let Some(inner) = args.get(0) { inner.to_str() } else { None } { if args.len() < 1 { error::missing_target(); }
None | let target = args.get(0).unwrap().to_string_lossy().to_string();
Some("-") => if let Some(prev) = message(MSG_PREVIOUS) { prev }
else { error::missing_target() },
Some(inner) => inner.to_owned()
};
let exists = util::session_exists(target.clone()); let exists = util::session_exists(target.clone());
if !exists { error::no_target(target.clone()); } if !exists { error::no_target(target.clone()); }
let mut tmux = Tmux::new();
if detach_other {
let detach = commands::DetachClient::new()
.target_session(&target);
tmux = tmux.add_command(detach);
}
let mut switch = commands::SwitchClient::new(); let mut switch = commands::SwitchClient::new();
switch = switch.target_session(&target); switch = switch.target_session(target);
if read_only { switch.read_only = true; } if read_only { switch.read_only = true; }
tmux.add_command(switch) Tmux::new()
.add_command(switch)
.stderr(NULL).output().ok(); .stderr(NULL).output().ok();
} }

View file

@ -9,19 +9,11 @@ use tmux_interface::{
}; };
use crate::{ use crate::{
env::{ env::{ self, env_var },
self,
env_var,
SYMBOL_ATTACH, SYMBOL_CURRENT, SYMBOL_PREV
},
error, error,
flag, flag,
state::State, state::State,
util::{ util::{ self, NULL }
self,
message,
MSG_PREVIOUS, NULL
}
}; };
pub fn attach(state: &mut State) { pub fn attach(state: &mut State) {
@ -64,7 +56,7 @@ pub fn attach(state: &mut State) {
state.nest_deinit(); state.nest_deinit();
} }
pub fn context_action(state: &mut State) { pub fn context_action(state: &State) {
if !state.session { if !state.session {
if let Some(repository) = &state.repository { if let Some(repository) = &state.repository {
let target = repository.name.clone(); let target = repository.name.clone();
@ -80,7 +72,7 @@ pub fn context_action(state: &mut State) {
} }
} }
// fallback behavior is list // fallback behavior is list
list(state); list(&state);
} }
pub fn detach(state: &mut State) { pub fn detach(state: &mut State) {
@ -122,55 +114,40 @@ pub fn has(state: &mut State) {
exit( if success { 0 } else { 1 }); exit( if success { 0 } else { 1 });
} }
pub fn list(state: &mut State) { pub fn list(state: &State) {
// get session list // get session list
let sessions = util::get_sessions().unwrap_or(Vec::new()); let sessions = util::get_sessions().unwrap_or(Vec::new());
let search = state.target();
let previous = message(MSG_PREVIOUS);
// handle empty case // handle empty case
if sessions.len() == 0 { if sessions.len() == 0 {
if !state.flags.quiet { println!("no sessions"); } println!("no sessions");
return; return;
} }
// get attached session symbol // get attached session symbol
let attach_symbol = env_var(SYMBOL_ATTACH); let attach_symbol = env_var(env::ATTACH_SYMBOL);
let current_symbol = env_var(SYMBOL_CURRENT); let current_symbol = env_var(env::CURRENT_SYMBOL);
let prev_symbol = env_var(SYMBOL_PREV);
// pretty print session list // pretty print session list
if !state.flags.quiet { println!("sessions:"); } println!("sessions:");
for session in sessions { for session in sessions.into_iter() {
let name = session.name.unwrap_or("[untitled]".to_string()); let name = session.name.unwrap_or("[untitled]".to_string());
if search.is_some() && !name.starts_with(search.as_ref().unwrap()) { continue; }
if !state.flags.quiet {
let id = session.id.unwrap(); let id = session.id.unwrap();
let attached = session.attached.unwrap_or(0) > 0; let attached = session.attached.unwrap_or(0) > 0;
let current = Some(name.clone()) == state.title;
let compare = Some(name.clone());
let marker =
if compare == state.title { current_symbol.clone() }
else if state.session && compare == previous { prev_symbol.clone() }
else { " ".to_string() };
println!( println!(
" {marker} {name}{reset} ({bold}{blue}{id}{reset}) {bold}{green}{attach}{reset}", " {current} {name}{reset} ({bold}{blue}{id}{reset}) {bold}{green}{attach}{reset}",
// values // values
attach = if attached { attach_symbol.clone() } else { "".to_string() }, attach = if attached { attach_symbol.clone() } else { "".to_string() },
current = if current { current_symbol.clone() } else { " ".to_string() },
// formatting // formatting
bold = style::Bold, bold = style::Bold,
blue = color::Fg(color::Blue), blue = color::Fg(color::Blue),
green = color::Fg(color::LightGreen), green = color::Fg(color::LightGreen),
reset = style::Reset, reset = style::Reset,
); );
} else {
print!("{name} ");
}
} }
} }

View file

@ -2,13 +2,12 @@ use std::env::var;
pub type EnvVar = (&'static str, &'static str); pub type EnvVar = (&'static str, &'static str);
pub const NEW_WINDOW_NAME: EnvVar = ("REMUX_NEW_WINDOW", ""); pub static ATTACH_SYMBOL: EnvVar = ("REMUX_ATTACH_SYMBOL", "*");
pub const REPO_FILE: EnvVar = ("REMUX_REPO_FILE", ".git"); pub static CURRENT_SYMBOL: EnvVar = ("REMUX_CURRENT_SYMBOL", ">");
pub const SYMBOL_ATTACH: EnvVar = ("REMUX_ATTACH_SYMBOL", "*"); pub static NEW_WINDOW_NAME: EnvVar = ("REMUX_NEW_WINDOW", "");
pub const SYMBOL_CURRENT: EnvVar = ("REMUX_CURRENT_SYMBOL", ">"); pub static REPO_FILE: EnvVar = ("REMUX_REPO_FILE", ".git");
pub const SYMBOL_PREV: EnvVar = ("REMUX_PREVIOUS_SYMBOL", "-");
pub const TMUX: &str = "TMUX"; pub static TMUX: &str = "TMUX";
/// get or default an environment variable /// get or default an environment variable
pub fn env_var(envvar: EnvVar) -> String { pub fn env_var(envvar: EnvVar) -> String {

View file

@ -1,14 +1,14 @@
use std::process::exit; use std::process::exit;
/// no subcommand that matches user input; code 1 /// no subcommand that matches user input; code 1
pub fn no_subcommand(subcommand: String) -> ! { pub fn no_subcommand(subcommand: String) {
eprintln!("remux: no command match for \"{subcommand}\""); eprintln!("remux: no command match for \"{subcommand}\"");
exit(1); exit(1);
} }
/// target session not found; code 2 /// target session not found; code 2
pub fn no_target<S: Into<String>>(target: S) -> ! { pub fn no_target<S: Into<String>>(target: S) {
let target = target.into(); let target = target.into();
eprintln!("remux: no session \"{target}\" exists"); eprintln!("remux: no session \"{target}\" exists");
exit(2); exit(2);
@ -16,26 +16,26 @@ pub fn no_target<S: Into<String>>(target: S) -> ! {
/// help topic doesn't exist; code 3 /// help topic doesn't exist; code 3
pub fn no_help(topic: String) -> ! { pub fn no_help(topic: String) {
eprintln!("remux: no help for \"{topic}\""); eprintln!("remux: no help for \"{topic}\"");
exit(3); exit(3);
} }
/// user provided no target; code 4 /// user provided no target; code 4
pub fn missing_target() -> ! { pub fn missing_target() {
eprintln!("remux: no target provided"); eprintln!("remux: no target provided");
exit(4); exit(4);
} }
/// refuse to attach to current session; code 4 /// refuse to attach to current session; code 4
pub fn same_session() -> ! { pub fn same_session() {
eprintln!("remux: cannot attach to same session"); eprintln!("remux: cannot attach to same session");
exit(4); exit(4);
} }
/// a session with the target name already exists; code 4 /// a session with the target name already exists; code 4
pub fn target_exists<S: Into<String>>(target: S) -> ! { pub fn target_exists<S: Into<String>>(target: S) {
let target = target.into(); let target = target.into();
eprintln!("remux: session \"{target}\" already exists"); eprintln!("remux: session \"{target}\" already exists");
exit(4); exit(4);
@ -43,26 +43,26 @@ pub fn target_exists<S: Into<String>>(target: S) -> ! {
/// non-terminal environment prevention; code 5 /// non-terminal environment prevention; code 5
pub fn not_terminal() -> ! { pub fn not_terminal() {
eprintln!("remux: not running from a terminal"); eprintln!("remux: not running from a terminal");
exit(5); exit(5);
} }
/// tried to nest while not in a session; code 6 /// tried to nest while not in a session; code 6
pub fn not_nesting() -> ! { pub fn not_nesting() {
eprintln!("remux: inappropriate nesting flag (-n); not in a session"); eprintln!("remux: inappropriate nesting flag (-n); not in a session");
exit(6); exit(6);
} }
/// operation requires nesting flag; code 6 /// operation requires nesting flag; code 6
pub fn prevent_nest() -> ! { pub fn prevent_nest() {
eprintln!("remux: the nesting flag (-n) is required for nesting operation"); eprintln!("remux: the nesting flag (-n) is required for nesting operation");
exit(6); exit(6);
} }
/// operation conflicts with nesting flag; code 6 /// operation conflicts with nesting flag; code 6
pub fn conflict_nest(reason: Option<&'static str>) -> ! { pub fn conflict_nest(reason: Option<&'static str>) {
if let Some(reason) = reason { eprintln!("remux: inappropriate nesting flag (-n): {reason}"); } if let Some(reason) = reason { eprintln!("remux: inappropriate nesting flag (-n): {reason}"); }
else { eprintln!("remux: nesting flag (-n) is inappropriate for this operation."); } else { eprintln!("remux: nesting flag (-n) is inappropriate for this operation."); }
exit(6); exit(6);
@ -70,15 +70,8 @@ pub fn conflict_nest(reason: Option<&'static str>) -> ! {
/// tried to run a session command outside a session; code 7 /// tried to run a session command outside a session; code 7
pub fn not_in_session(cmd: &'static str) -> ! { pub fn not_in_session(cmd: &'static str) {
eprintln!("remux: '{cmd}' must be run from within a session"); eprintln!("remux: '{cmd}' must be run from within a session");
exit(7); exit(7);
} }
/// failed to set working directory; code 8
pub fn working_dir_fail(working_dir: &str) -> ! {
eprintln!("remux: failed to set working directory to '{working_dir}'");
exit(8);
}

View file

@ -10,7 +10,6 @@ pub static QUIET: Flag = [ "-q", "--quiet" ];
pub static READ_ONLY: Flag = ["-r", "--read-only"]; pub static READ_ONLY: Flag = ["-r", "--read-only"];
pub static TARGET: Flag = ["-t", "--target"]; pub static TARGET: Flag = ["-t", "--target"];
pub static VERSION: Flag = ["-v", "--version"]; pub static VERSION: Flag = ["-v", "--version"];
pub static WORKING_DIR: Flag = [ "-D", "--dir" ];
pub struct Flags { pub struct Flags {
pub detached: bool, pub detached: bool,
@ -18,7 +17,6 @@ pub struct Flags {
pub quiet: bool, pub quiet: bool,
pub read_only: bool, pub read_only: bool,
pub target: Option<String>, pub target: Option<String>,
pub working_dir: Option<String>
} }
impl Flags { impl Flags {
@ -29,15 +27,13 @@ impl Flags {
let quiet = args.contains(QUIET); let quiet = args.contains(QUIET);
let read_only = args.contains(READ_ONLY); let read_only = args.contains(READ_ONLY);
let target = args.value_from_str(TARGET).ok(); let target = args.value_from_str(TARGET).ok();
let working_dir = args.value_from_str(WORKING_DIR).ok();
Flags { Flags {
detached, detached,
nested, nested,
quiet, quiet,
read_only, read_only,
target, target
working_dir
} }
} }
@ -47,8 +43,7 @@ impl Flags {
nested: self.nested, nested: self.nested,
quiet: self.quiet, quiet: self.quiet,
read_only: self.read_only, read_only: self.read_only,
target: self.target.clone(), target: None
working_dir: self.working_dir.clone()
} }
} }

View file

@ -38,7 +38,7 @@ fn main() {
Some("help") Some("help")
=> help(&mut args), => help(&mut args),
None None
=> command::share::context_action(&mut state), => command::share::context_action(&state),
Some("a" | "attach") Some("a" | "attach")
=> command::share::attach(&mut state), => command::share::attach(&mut state),
@ -50,7 +50,7 @@ fn main() {
=> command::share::has(&mut state), => command::share::has(&mut state),
Some("l" | "ls" | "list") Some("l" | "ls" | "list")
=> command::share::list(&mut state), => command::share::list(&state),
Some("n" | "new") Some("n" | "new")
=> command::share::new(&mut state), => command::share::new(&mut state),

View file

@ -9,7 +9,7 @@ use crate::{
env::{ env_var, REPO_FILE, TMUX }, env::{ env_var, REPO_FILE, TMUX },
error, error,
flag::Flags, flag::Flags,
util::{ find, message, MSG_SESSION_NAME } util::{ find, session_name }
}; };
pub struct State<'a> { pub struct State<'a> {
@ -29,10 +29,7 @@ impl State<'_> {
let flags = Flags::from(args); let flags = Flags::from(args);
let tmux_var = env::var(TMUX).ok(); let tmux_var = env::var(TMUX).ok();
let session = tmux_var.is_some(); let session = tmux_var.is_some();
let title = if session { session_name() } else { None };
if let Some(ref path) = flags.working_dir { State::set_working_dir(&path); }
let title = if session { message(MSG_SESSION_NAME) } else { None };
let repository = Repository::find(); let repository = Repository::find();
State { State {
@ -65,19 +62,15 @@ impl State<'_> {
if !self.session { error::not_in_session(cmd); } if !self.session { error::not_in_session(cmd); }
} }
fn set_working_dir(path: &str) {
let result = env::set_current_dir(path);
if result.is_err() {
error::working_dir_fail(path);
}
}
pub fn target(&mut self) -> Option<String> { self.args.subcommand().unwrap_or(None) } pub fn target(&mut self) -> Option<String> { self.args.subcommand().unwrap_or(None) }
pub fn target_title(&mut self) -> Option<String> { pub fn target_title(&mut self) -> Option<String> {
let from_args = self.target(); let from_args = self.target();
if from_args.is_some() { return from_args; } if from_args.is_some() { return from_args; }
else if let Some(repository) = &self.repository { Some(repository.name.clone()) } else if let Some(repository) = &self.repository { Some(repository.name.clone()) }
else { error::missing_target() } else {
error::missing_target();
None
}
} }
} }

View file

@ -14,20 +14,14 @@ use crate::error;
pub const NULL: Option<StdIO> = Some(StdIO::Null); pub const NULL: Option<StdIO> = Some(StdIO::Null);
pub const MSG_PREVIOUS: &str = "#{client_last_session}"; pub fn session_name() -> Option<String> {
pub const MSG_SESSION_NAME: &str = "#S"; let message = commands::DisplayMessage::new().print().message("#{session_name}");
pub const MSG_SESSION_PATH: &str = "#{session_path}";
pub const MSG_WINDOW_NAME: &str = "#{window_name}";
pub fn message(fstr: &str) -> Option<String> {
let message = commands::DisplayMessage::new().print().message(fstr);
let result = Tmux::new().add_command(message).output(); let result = Tmux::new().add_command(message).output();
if let Ok(output) = result { if let Ok(output) = result {
let text = String::from_utf8(output.0.stdout); let text = String::from_utf8(output.0.stdout);
if let Ok(title) = text { if let Ok(title) = text {
if title.len() > 0 { Some(title[0..title.len() - 1].to_owned()) } Some(title[0..title.len() - 1].to_owned())
else { None }
} else { None } } else { None }
} else { None } } else { None }
} }