Compare commits
15 commits
Author | SHA1 | Date | |
---|---|---|---|
4f76590930 | |||
d3a408ad33 | |||
8449ae00d6 | |||
ff30bc1052 | |||
8e2826b110 | |||
0fe3906578 | |||
fdf3114c04 | |||
8ad16ad825 | |||
a9a73314af | |||
af33e82415 | |||
eaf72847b1 | |||
beb880ed43 | |||
449c460bbb | |||
1b51633d4f | |||
b7b893d55c |
12 changed files with 193 additions and 97 deletions
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "remux"
|
||||
version = "0.3.5"
|
||||
version = "0.4.0"
|
||||
edition = "2021"
|
||||
authors = [ "Valerie Wolfe <sleeplessval@gmail.com>" ]
|
||||
description = "A friendly command shortener for tmux"
|
||||
|
|
10
README.md
10
README.md
|
@ -92,10 +92,16 @@ 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>.
|
||||
</details>
|
||||
|
||||
### Man Page
|
||||
### Supplemental
|
||||
|
||||
<details>
|
||||
<summary>Section 1</summary>
|
||||
<summary>Bash Completions</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>.
|
||||
</details>
|
||||
|
||||
|
|
19
bash-completion/remux
Normal file
19
bash-completion/remux
Normal file
|
@ -0,0 +1,19 @@
|
|||
_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
|
||||
|
20
man/remux.1
20
man/remux.1
|
@ -1,6 +1,5 @@
|
|||
.Dd $Mdocdate$
|
||||
.Dt REMUX 1
|
||||
.Os
|
||||
.Sh NAME
|
||||
.Nm remux
|
||||
.Nd a command shortener for
|
||||
|
@ -8,6 +7,7 @@
|
|||
.Sh SYNOPSIS
|
||||
.Nm remux
|
||||
.Op Fl dhnqrtv
|
||||
.Op Fl D Ar path
|
||||
.Op Ar command
|
||||
.Op args...
|
||||
.Sh DESCRIPTION
|
||||
|
@ -35,6 +35,8 @@ Attaches to an existing session.
|
|||
.Bl -tag -width Ds -compact
|
||||
.It Fl d , Fl -detach
|
||||
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
|
||||
Allow nesting (attaching a session from inside another session).
|
||||
.It Fl r , Fl -read-only
|
||||
|
@ -101,8 +103,8 @@ aliases: p
|
|||
Prints the session path.
|
||||
.Ed
|
||||
.It Xo Ic switch
|
||||
.Op Fl r , Fl -read-only
|
||||
.Ar title
|
||||
.Op Fl rd
|
||||
.Op Ar title
|
||||
.Xc
|
||||
.Bd -literal -compact
|
||||
aliases: s
|
||||
|
@ -110,10 +112,12 @@ Switches from the current session to the target.
|
|||
.Ed
|
||||
.Pp
|
||||
.Bl -tag -width Ds -compact
|
||||
.It Fl d , Fl -detach
|
||||
Detaches other clients from the target session.
|
||||
.It Fl r , Fl -read-only
|
||||
Switch to the target session in read-only mode.
|
||||
.It Ar title
|
||||
The title of the session to switch to.
|
||||
The title of the session to switch to. If blank, the previous session will be used.
|
||||
.El
|
||||
.It Ic title
|
||||
.Bd -literal -compact
|
||||
|
@ -135,6 +139,14 @@ Default: '>'
|
|||
.It Ev REMUX_NEW_WINDOW
|
||||
Provides a default windows name when creating a new session. Unused if empty.
|
||||
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
|
||||
The filename to match on when trying to find the root of a repository.
|
||||
Default: '.git'
|
||||
.El
|
||||
.Sh EXIT STATUS
|
||||
.Bl -tag -Width Ds
|
||||
|
|
|
@ -8,21 +8,18 @@ use tmux_interface::{
|
|||
use crate::{
|
||||
error,
|
||||
state::State,
|
||||
util::{ self, NULL }
|
||||
util::{
|
||||
self,
|
||||
message,
|
||||
MSG_PREVIOUS, MSG_SESSION_PATH, NULL
|
||||
}
|
||||
};
|
||||
|
||||
pub fn path(state: &mut State) {
|
||||
state.session_enforce("path");
|
||||
|
||||
let message = commands::DisplayMessage::new().print().message("#{session_path}");
|
||||
|
||||
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]);
|
||||
if let Some(message) = message(MSG_SESSION_PATH) {
|
||||
println!("{message}");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -33,21 +30,33 @@ pub fn switch(state: &mut State) {
|
|||
|
||||
// consume optional flags
|
||||
let read_only = state.flags.read_only;
|
||||
//TODO: -d flag handling needs to be done manually
|
||||
let detach_other = state.flags.detached;
|
||||
|
||||
let args = state.args.clone().finish();
|
||||
if args.len() < 1 { error::missing_target(); }
|
||||
let target = args.get(0).unwrap().to_string_lossy().to_string();
|
||||
let target: String = match if let Some(inner) = args.get(0) { inner.to_str() } else { None } {
|
||||
None |
|
||||
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());
|
||||
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();
|
||||
switch = switch.target_session(target);
|
||||
switch = switch.target_session(&target);
|
||||
if read_only { switch.read_only = true; }
|
||||
|
||||
Tmux::new()
|
||||
.add_command(switch)
|
||||
tmux.add_command(switch)
|
||||
.stderr(NULL).output().ok();
|
||||
}
|
||||
|
||||
|
|
|
@ -9,11 +9,19 @@ use tmux_interface::{
|
|||
};
|
||||
|
||||
use crate::{
|
||||
env::{ self, env_var },
|
||||
env::{
|
||||
self,
|
||||
env_var,
|
||||
SYMBOL_ATTACH, SYMBOL_CURRENT, SYMBOL_PREV
|
||||
},
|
||||
error,
|
||||
flag,
|
||||
state::State,
|
||||
util::{ self, NULL }
|
||||
util::{
|
||||
self,
|
||||
message,
|
||||
MSG_PREVIOUS, NULL
|
||||
}
|
||||
};
|
||||
|
||||
pub fn attach(state: &mut State) {
|
||||
|
@ -56,7 +64,7 @@ pub fn attach(state: &mut State) {
|
|||
state.nest_deinit();
|
||||
}
|
||||
|
||||
pub fn context_action(state: &State) {
|
||||
pub fn context_action(state: &mut State) {
|
||||
if !state.session {
|
||||
if let Some(repository) = &state.repository {
|
||||
let target = repository.name.clone();
|
||||
|
@ -72,7 +80,7 @@ pub fn context_action(state: &State) {
|
|||
}
|
||||
}
|
||||
// fallback behavior is list
|
||||
list(&state);
|
||||
list(state);
|
||||
}
|
||||
|
||||
pub fn detach(state: &mut State) {
|
||||
|
@ -114,40 +122,55 @@ pub fn has(state: &mut State) {
|
|||
exit( if success { 0 } else { 1 });
|
||||
}
|
||||
|
||||
pub fn list(state: &State) {
|
||||
pub fn list(state: &mut State) {
|
||||
// get session list
|
||||
let sessions = util::get_sessions().unwrap_or(Vec::new());
|
||||
|
||||
let search = state.target();
|
||||
let previous = message(MSG_PREVIOUS);
|
||||
|
||||
// handle empty case
|
||||
if sessions.len() == 0 {
|
||||
println!("no sessions");
|
||||
if !state.flags.quiet { println!("no sessions"); }
|
||||
return;
|
||||
}
|
||||
|
||||
// get attached session symbol
|
||||
let attach_symbol = env_var(env::ATTACH_SYMBOL);
|
||||
let current_symbol = env_var(env::CURRENT_SYMBOL);
|
||||
let attach_symbol = env_var(SYMBOL_ATTACH);
|
||||
let current_symbol = env_var(SYMBOL_CURRENT);
|
||||
let prev_symbol = env_var(SYMBOL_PREV);
|
||||
|
||||
// pretty print session list
|
||||
println!("sessions:");
|
||||
for session in sessions.into_iter() {
|
||||
if !state.flags.quiet { println!("sessions:"); }
|
||||
for session in sessions {
|
||||
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 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!(
|
||||
" {current} {name}{reset} ({bold}{blue}{id}{reset}) {bold}{green}{attach}{reset}",
|
||||
" {marker} {name}{reset} ({bold}{blue}{id}{reset}) {bold}{green}{attach}{reset}",
|
||||
// values
|
||||
attach = if attached { attach_symbol.clone() } else { "".to_string() },
|
||||
current = if current { current_symbol.clone() } else { " ".to_string() },
|
||||
// formatting
|
||||
bold = style::Bold,
|
||||
blue = color::Fg(color::Blue),
|
||||
green = color::Fg(color::LightGreen),
|
||||
reset = style::Reset,
|
||||
);
|
||||
} else {
|
||||
print!("{name} ");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
10
src/env.rs
10
src/env.rs
|
@ -2,11 +2,13 @@ use std::env::var;
|
|||
|
||||
pub type EnvVar = (&'static str, &'static str);
|
||||
|
||||
pub static ATTACH_SYMBOL: EnvVar = ("REMUX_ATTACH_SYMBOL", "*");
|
||||
pub static CURRENT_SYMBOL: EnvVar = ("REMUX_CURRENT_SYMBOL", ">");
|
||||
pub static NEW_WINDOW_NAME: EnvVar = ("REMUX_NEW_WINDOW", "");
|
||||
pub const NEW_WINDOW_NAME: EnvVar = ("REMUX_NEW_WINDOW", "");
|
||||
pub const REPO_FILE: EnvVar = ("REMUX_REPO_FILE", ".git");
|
||||
pub const SYMBOL_ATTACH: EnvVar = ("REMUX_ATTACH_SYMBOL", "*");
|
||||
pub const SYMBOL_CURRENT: EnvVar = ("REMUX_CURRENT_SYMBOL", ">");
|
||||
pub const SYMBOL_PREV: EnvVar = ("REMUX_PREVIOUS_SYMBOL", "-");
|
||||
|
||||
pub static TMUX: &str = "TMUX";
|
||||
pub const TMUX: &str = "TMUX";
|
||||
|
||||
/// get or default an environment variable
|
||||
pub fn env_var(envvar: EnvVar) -> String {
|
||||
|
|
53
src/error.rs
53
src/error.rs
|
@ -1,77 +1,84 @@
|
|||
use std::process::exit;
|
||||
|
||||
/// no subcommand that matches user input; code 1
|
||||
pub fn no_subcommand(subcommand: String) {
|
||||
println!("remux: no command match for \"{subcommand}\"");
|
||||
pub fn no_subcommand(subcommand: String) -> ! {
|
||||
eprintln!("remux: no command match for \"{subcommand}\"");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
|
||||
/// 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();
|
||||
println!("remux: no session \"{target}\" exists");
|
||||
eprintln!("remux: no session \"{target}\" exists");
|
||||
exit(2);
|
||||
}
|
||||
|
||||
|
||||
/// help topic doesn't exist; code 3
|
||||
pub fn no_help(topic: String) {
|
||||
println!("remux: no help for \"{topic}\"");
|
||||
pub fn no_help(topic: String) -> ! {
|
||||
eprintln!("remux: no help for \"{topic}\"");
|
||||
exit(3);
|
||||
}
|
||||
|
||||
|
||||
/// user provided no target; code 4
|
||||
pub fn missing_target() {
|
||||
println!("remux: no target provided");
|
||||
pub fn missing_target() -> ! {
|
||||
eprintln!("remux: no target provided");
|
||||
exit(4);
|
||||
}
|
||||
|
||||
/// refuse to attach to current session; code 4
|
||||
pub fn same_session() {
|
||||
println!("remux: cannot attach to same session");
|
||||
pub fn same_session() -> ! {
|
||||
eprintln!("remux: cannot attach to same session");
|
||||
exit(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();
|
||||
println!("remux: session \"{target}\" already exists");
|
||||
eprintln!("remux: session \"{target}\" already exists");
|
||||
exit(4);
|
||||
}
|
||||
|
||||
|
||||
/// non-terminal environment prevention; code 5
|
||||
pub fn not_terminal() {
|
||||
println!("remux: not running from a terminal");
|
||||
pub fn not_terminal() -> ! {
|
||||
eprintln!("remux: not running from a terminal");
|
||||
exit(5);
|
||||
}
|
||||
|
||||
|
||||
/// tried to nest while not in a session; code 6
|
||||
pub fn not_nesting() {
|
||||
println!("remux: inappropriate nesting flag (-n); not in a session");
|
||||
pub fn not_nesting() -> ! {
|
||||
eprintln!("remux: inappropriate nesting flag (-n); not in a session");
|
||||
exit(6);
|
||||
}
|
||||
|
||||
/// operation requires nesting flag; code 6
|
||||
pub fn prevent_nest() {
|
||||
println!("remux: the nesting flag (-n) is required for nesting operation");
|
||||
pub fn prevent_nest() -> ! {
|
||||
eprintln!("remux: the nesting flag (-n) is required for nesting operation");
|
||||
exit(6);
|
||||
}
|
||||
|
||||
/// operation conflicts with nesting flag; code 6
|
||||
pub fn conflict_nest(reason: Option<&'static str>) {
|
||||
if let Some(reason) = reason { println!("remux: inappropriate nesting flag (-n): {reason}"); }
|
||||
else { println!("remux: nesting flag (-n) is inappropriate for this operation."); }
|
||||
pub fn conflict_nest(reason: Option<&'static str>) -> ! {
|
||||
if let Some(reason) = reason { eprintln!("remux: inappropriate nesting flag (-n): {reason}"); }
|
||||
else { eprintln!("remux: nesting flag (-n) is inappropriate for this operation."); }
|
||||
exit(6);
|
||||
}
|
||||
|
||||
|
||||
/// tried to run a session command outside a session; code 7
|
||||
pub fn not_in_session(cmd: &'static str) {
|
||||
println!("remux: '{cmd}' must be run from within a session");
|
||||
pub fn not_in_session(cmd: &'static str) -> ! {
|
||||
eprintln!("remux: '{cmd}' must be run from within a session");
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ pub static QUIET: Flag = ["-q", "--quiet"];
|
|||
pub static READ_ONLY: Flag = [ "-r", "--read-only" ];
|
||||
pub static TARGET: Flag = [ "-t", "--target" ];
|
||||
pub static VERSION: Flag = [ "-v", "--version" ];
|
||||
pub static WORKING_DIR: Flag = [ "-D", "--dir" ];
|
||||
|
||||
pub struct Flags {
|
||||
pub detached: bool,
|
||||
|
@ -17,6 +18,7 @@ pub struct Flags {
|
|||
pub quiet: bool,
|
||||
pub read_only: bool,
|
||||
pub target: Option<String>,
|
||||
pub working_dir: Option<String>
|
||||
}
|
||||
|
||||
impl Flags {
|
||||
|
@ -27,13 +29,15 @@ impl Flags {
|
|||
let quiet = args.contains(QUIET);
|
||||
let read_only = args.contains(READ_ONLY);
|
||||
let target = args.value_from_str(TARGET).ok();
|
||||
let working_dir = args.value_from_str(WORKING_DIR).ok();
|
||||
|
||||
Flags {
|
||||
detached,
|
||||
nested,
|
||||
quiet,
|
||||
read_only,
|
||||
target
|
||||
target,
|
||||
working_dir
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -43,7 +47,8 @@ impl Flags {
|
|||
nested: self.nested,
|
||||
quiet: self.quiet,
|
||||
read_only: self.read_only,
|
||||
target: None
|
||||
target: self.target.clone(),
|
||||
working_dir: self.working_dir.clone()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -38,7 +38,7 @@ fn main() {
|
|||
Some("help")
|
||||
=> help(&mut args),
|
||||
None
|
||||
=> command::share::context_action(&state),
|
||||
=> command::share::context_action(&mut state),
|
||||
|
||||
Some("a" | "attach")
|
||||
=> command::share::attach(&mut state),
|
||||
|
@ -50,7 +50,7 @@ fn main() {
|
|||
=> command::share::has(&mut state),
|
||||
|
||||
Some("l" | "ls" | "list")
|
||||
=> command::share::list(&state),
|
||||
=> command::share::list(&mut state),
|
||||
|
||||
Some("n" | "new")
|
||||
=> command::share::new(&mut state),
|
||||
|
|
23
src/state.rs
23
src/state.rs
|
@ -6,10 +6,10 @@ use std::{
|
|||
use pico_args::Arguments;
|
||||
|
||||
use crate::{
|
||||
env::TMUX,
|
||||
env::{ env_var, REPO_FILE, TMUX },
|
||||
error,
|
||||
flag::Flags,
|
||||
util::{ find, session_name }
|
||||
util::{ find, message, MSG_SESSION_NAME }
|
||||
};
|
||||
|
||||
pub struct State<'a> {
|
||||
|
@ -29,7 +29,10 @@ impl State<'_> {
|
|||
let flags = Flags::from(args);
|
||||
let tmux_var = env::var(TMUX).ok();
|
||||
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();
|
||||
|
||||
State {
|
||||
|
@ -62,15 +65,19 @@ impl State<'_> {
|
|||
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_title(&mut self) -> Option<String> {
|
||||
let from_args = self.target();
|
||||
if from_args.is_some() { return from_args; }
|
||||
else if let Some(repository) = &self.repository { Some(repository.name.clone()) }
|
||||
else {
|
||||
error::missing_target();
|
||||
None
|
||||
}
|
||||
else { error::missing_target() }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -82,7 +89,7 @@ pub struct Repository {
|
|||
|
||||
impl Repository {
|
||||
pub fn find() -> Option<Repository> {
|
||||
let path = find(".git", env::current_dir().unwrap());
|
||||
let path = find(&env_var(REPO_FILE), env::current_dir().unwrap());
|
||||
if let Some(path) = path {
|
||||
let name = path.file_name().unwrap().to_string_lossy().to_string();
|
||||
let inner = Repository {
|
||||
|
|
14
src/util.rs
14
src/util.rs
|
@ -14,14 +14,20 @@ use crate::error;
|
|||
|
||||
pub const NULL: Option<StdIO> = Some(StdIO::Null);
|
||||
|
||||
pub fn session_name() -> Option<String> {
|
||||
let message = commands::DisplayMessage::new().print().message("#{session_name}");
|
||||
pub const MSG_PREVIOUS: &str = "#{client_last_session}";
|
||||
pub const MSG_SESSION_NAME: &str = "#S";
|
||||
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();
|
||||
if let Ok(output) = result {
|
||||
let text = String::from_utf8(output.0.stdout);
|
||||
if let Ok(title) = text {
|
||||
Some(title[0..title.len() - 1].to_owned())
|
||||
if title.len() > 0 { Some(title[0..title.len() - 1].to_owned()) }
|
||||
else { None }
|
||||
} else { None }
|
||||
} else { None }
|
||||
}
|
||||
|
@ -51,7 +57,7 @@ pub fn terminal_enforce() {
|
|||
}
|
||||
|
||||
/// recursively propagate up directories to find a child
|
||||
pub fn find(target: &'static str, path: PathBuf) -> Option<PathBuf> {
|
||||
pub fn find(target: &str, path: PathBuf) -> Option<PathBuf> {
|
||||
if path.join(target).exists() { return Some(path); }
|
||||
|
||||
let parent = path.parent();
|
||||
|
|
Loading…
Reference in a new issue