Compare commits
10 commits
a915bfd9c5
...
b93d814f6f
Author | SHA1 | Date | |
---|---|---|---|
b93d814f6f | |||
b24778113a | |||
21bd6f40ca | |||
1f143f00ad | |||
e930af1027 | |||
97f546a69c | |||
44bad997cb | |||
441f1d3ef0 | |||
0232a9c650 | |||
ace9627b79 |
12 changed files with 483 additions and 166 deletions
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "remux"
|
name = "remux"
|
||||||
version = "0.3.2"
|
version = "0.3.4"
|
||||||
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"
|
||||||
|
|
|
@ -47,8 +47,7 @@ tmux swi -t foo
|
||||||
rmux s foo
|
rmux s foo
|
||||||
|
|
||||||
# cd to session path
|
# cd to session path
|
||||||
tmux run 'printf "#{session_path}" > /tmp/tmux_path'
|
cd `tmux display-mes -p "#{session_path}"`
|
||||||
cd `cat /tmp/tmux_path`
|
|
||||||
cd `rmux p`
|
cd `rmux p`
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
167
man/remux.1
Normal file
167
man/remux.1
Normal file
|
@ -0,0 +1,167 @@
|
||||||
|
.Dd $Mdocdate$
|
||||||
|
.Dt REMUX 1
|
||||||
|
.Os
|
||||||
|
.Sh NAME
|
||||||
|
.Nm remux
|
||||||
|
.Nd a command shortener for
|
||||||
|
.Xr tmux 1
|
||||||
|
.Sh SYNOPSIS
|
||||||
|
.Nm remux
|
||||||
|
.Op Fl dhnqrtv
|
||||||
|
.Op command
|
||||||
|
.Op args...
|
||||||
|
.Sh DESCRIPTION
|
||||||
|
.Nm
|
||||||
|
is a wrapper and command shortener for
|
||||||
|
.Xr tmux 1 ,
|
||||||
|
primarily focused on improving the ergonomics of using named sessions.
|
||||||
|
.Pp
|
||||||
|
If no command is provided, remux will use the context action. Inside a repository, remux will attach or create. If in a session or outside a repository, remux will list sessions.
|
||||||
|
.Sh COMMANDS
|
||||||
|
.Nm remux
|
||||||
|
commands are split into two categories: global commands, which can be used anywhere, and session commands, which can only be used from inside a session.
|
||||||
|
.Ss GLOBAL COMMANDS
|
||||||
|
.Bl -tag -width Ds
|
||||||
|
.It Xo Ic attach
|
||||||
|
.Op Fl dnr
|
||||||
|
.Op Ar title
|
||||||
|
.Op Ar window
|
||||||
|
.Xc
|
||||||
|
.Bd -literal -compact
|
||||||
|
aliases: a
|
||||||
|
Attaches to an existing session.
|
||||||
|
.Ed
|
||||||
|
.Pp
|
||||||
|
.Bl -tag -width Ds -compact
|
||||||
|
.It Fl d , Fl -detach
|
||||||
|
Detach all other connections to the session.
|
||||||
|
.It Fl n , Fl -nest
|
||||||
|
Allow nesting (attaching a session from inside another session).
|
||||||
|
.It Fl r , Fl -read-only
|
||||||
|
Attach the session in read-only mode.
|
||||||
|
.It Ar title
|
||||||
|
The title of the target session. If not given, remux will try to use the name of the repository containing the current directory.
|
||||||
|
.It Ar window
|
||||||
|
The name of the window to attach to.
|
||||||
|
.El
|
||||||
|
.It Xo Ic detach
|
||||||
|
.Op Ar title
|
||||||
|
.Xc
|
||||||
|
.Bd -literal -compact
|
||||||
|
aliases: d
|
||||||
|
Detaches all clients from the target session.
|
||||||
|
.Ed
|
||||||
|
.It Xo Ic has
|
||||||
|
.Op Fl q
|
||||||
|
.Op Ar title
|
||||||
|
.Xc
|
||||||
|
.Bd -literal -compact
|
||||||
|
aliases: h
|
||||||
|
Checks whether or not a session exists.
|
||||||
|
.Ed
|
||||||
|
.Pp
|
||||||
|
.Bl -tag -width Ds -compact
|
||||||
|
.It Fl q , Fl -quiet
|
||||||
|
Run silently without printing to standard output.
|
||||||
|
.It Ar title
|
||||||
|
The title of the target session. If not given, remux will attempt to use the name of the repository containing the current directory.
|
||||||
|
.El
|
||||||
|
.It Ic list
|
||||||
|
.Bd -literal -compact
|
||||||
|
aliases: l, ls
|
||||||
|
Pretty-prints a list of tmux sessions.
|
||||||
|
.Ed
|
||||||
|
.It Xo Ic new
|
||||||
|
.Op Fl dn
|
||||||
|
.Op Fl t\ |\ --target Ar path
|
||||||
|
.Op Ar title
|
||||||
|
.Op Ar command
|
||||||
|
.Xc
|
||||||
|
.Bd -literal -compact
|
||||||
|
aliases: n
|
||||||
|
Creates a new session.
|
||||||
|
.Ed
|
||||||
|
.Pp
|
||||||
|
.Bl -tag -width Ds -compact
|
||||||
|
.It Fl d , Fl -detach
|
||||||
|
Creates the session without attaching.
|
||||||
|
.It Fl n , Fl -nest
|
||||||
|
Allow nesting (attaching a session from inside another session).
|
||||||
|
.It Fl t , Fl -target Ar path
|
||||||
|
Sets the session path to the provided directory.
|
||||||
|
.It Ar title
|
||||||
|
Create the session with the given title. If not given, remux will attempt to use the name of the repository containing the current directory.
|
||||||
|
.El
|
||||||
|
.El
|
||||||
|
.Ss SESSION COMMANDS
|
||||||
|
.Bl -tag -width Ds
|
||||||
|
.It Ic path
|
||||||
|
.Bd -literal -compact
|
||||||
|
aliases: p
|
||||||
|
Prints the session path.
|
||||||
|
.Ed
|
||||||
|
.It Xo Ic switch
|
||||||
|
.Op Fl r , Fl -read-only
|
||||||
|
.Ar title
|
||||||
|
.Xc
|
||||||
|
.Bd -literal -compact
|
||||||
|
aliases: s
|
||||||
|
Switches from the current session to the target.
|
||||||
|
.Ed
|
||||||
|
.Pp
|
||||||
|
.Bl -tag -width Ds -compact
|
||||||
|
.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.
|
||||||
|
.El
|
||||||
|
.It Ic title
|
||||||
|
.Bd -literal -compact
|
||||||
|
aliases: t, which
|
||||||
|
Prints the session title.
|
||||||
|
.El
|
||||||
|
.Sh ENVIRONMENT
|
||||||
|
.Bl -tag -width Ds
|
||||||
|
.It Ev REMUX_ATTACH_SYMBOL
|
||||||
|
Changes the symbol displayed for attached sessions in the
|
||||||
|
.Ic list
|
||||||
|
command.
|
||||||
|
Default: '*'
|
||||||
|
.It Ev REMUX_CURRENT_SYMBOL
|
||||||
|
Changes the symbol displayed for the current session in the
|
||||||
|
.Ic list
|
||||||
|
command.
|
||||||
|
Default: '>'
|
||||||
|
.It Ev REMUX_NEW_WINDOW
|
||||||
|
Provides a default windows name when creating a new session. Unused if empty.
|
||||||
|
Default: (unset)
|
||||||
|
.El
|
||||||
|
.Sh EXIT STATUS
|
||||||
|
.Bl -tag -Width Ds
|
||||||
|
.It 1
|
||||||
|
Unmatched command name.
|
||||||
|
.It 2
|
||||||
|
Unmatched target session.
|
||||||
|
.It 3
|
||||||
|
Unmatched help topic.
|
||||||
|
.It 4
|
||||||
|
Missing or invalid target; target was not given or is the same as the current session.
|
||||||
|
.It 5
|
||||||
|
.Nm remux
|
||||||
|
is not running from within a terminal.
|
||||||
|
.It 6
|
||||||
|
Nesting error; nest flag is missing or inappropriate.
|
||||||
|
.It 7
|
||||||
|
A session command was attempted outside a session.
|
||||||
|
.Sh EXAMPLES
|
||||||
|
Use
|
||||||
|
.Ic path
|
||||||
|
to navigate to the session path:
|
||||||
|
.Pp
|
||||||
|
.Dl $ cd `remux p`
|
||||||
|
.Pp
|
||||||
|
.Sh SEE ALSO
|
||||||
|
.Xr tmux 1
|
||||||
|
.Sh AUTHORS
|
||||||
|
.An -nosplit
|
||||||
|
.An Valerie Wolfe Aq Mt sleeplessval@gmail.com
|
|
@ -1,26 +1,41 @@
|
||||||
//! commands accessible from within a session
|
//! commands accessible from within a session
|
||||||
use std::fs::read_to_string;
|
|
||||||
|
|
||||||
use pico_args::Arguments;
|
|
||||||
use tmux_interface::{
|
use tmux_interface::{
|
||||||
Tmux,
|
Tmux,
|
||||||
commands
|
commands
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{ error, flag, util };
|
use crate::{
|
||||||
|
error,
|
||||||
|
state::State,
|
||||||
|
util
|
||||||
|
};
|
||||||
|
|
||||||
const TMP_ROOT: &str = "/tmp/remux_path";
|
pub fn path(state: &mut State) {
|
||||||
|
state.session_enforce("path");
|
||||||
|
|
||||||
pub fn switch(pargs: &mut Arguments) {
|
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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn switch(state: &mut State) {
|
||||||
util::terminal_enforce();
|
util::terminal_enforce();
|
||||||
// refuse to run outside a session
|
// refuse to run outside a session
|
||||||
util::session_enforce("switch");
|
state.session_enforce("switch");
|
||||||
|
|
||||||
// consume optional flags
|
// consume optional flags
|
||||||
let read_only = pargs.contains(flag::READ_ONLY);
|
let read_only = state.flags.read_only;
|
||||||
//TODO: -d flag handling needs to be done manually
|
//TODO: -d flag handling needs to be done manually
|
||||||
|
|
||||||
let args = pargs.clone().finish();
|
let args = state.args.clone().finish();
|
||||||
if args.len() < 1 { error::missing_target(); }
|
if args.len() < 1 { error::missing_target(); }
|
||||||
let target = args.get(0).unwrap().to_string_lossy().to_string();
|
let target = args.get(0).unwrap().to_string_lossy().to_string();
|
||||||
|
|
||||||
|
@ -36,17 +51,8 @@ pub fn switch(pargs: &mut Arguments) {
|
||||||
.output().ok();
|
.output().ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn path() {
|
pub fn title(state: State) {
|
||||||
util::session_enforce("path");
|
state.session_enforce("title");
|
||||||
|
if let Some(title) = state.title { println!("{title}"); }
|
||||||
let exec = commands::Run::new().shell_command("printf '#{session_path}' > ".to_string() + TMP_ROOT);
|
|
||||||
Tmux::new()
|
|
||||||
.add_command(exec)
|
|
||||||
.output().ok();
|
|
||||||
|
|
||||||
if let Ok(text) = read_to_string(TMP_ROOT) {
|
|
||||||
println!("{text}");
|
|
||||||
std::fs::remove_file(TMP_ROOT).ok();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,5 @@
|
||||||
//! globally available tmux commands.
|
//! globally available tmux commands.
|
||||||
use std::{
|
use std::process::exit;
|
||||||
ffi::OsString,
|
|
||||||
process::exit
|
|
||||||
};
|
|
||||||
|
|
||||||
use pico_args::{ Arguments, Error };
|
use pico_args::{ Arguments, Error };
|
||||||
use termion::{ color, style };
|
use termion::{ color, style };
|
||||||
|
@ -15,31 +12,24 @@ use crate::{
|
||||||
env::{ self, env_var },
|
env::{ self, env_var },
|
||||||
error,
|
error,
|
||||||
flag,
|
flag,
|
||||||
|
state::State,
|
||||||
util
|
util
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn attach(pargs: &mut Arguments) {
|
pub fn attach(state: &mut State) {
|
||||||
// must be run from terminal
|
|
||||||
util::terminal_enforce();
|
util::terminal_enforce();
|
||||||
// don't allow unflagged nests
|
state.nest_init();
|
||||||
util::prevent_nest();
|
|
||||||
|
|
||||||
// consume optional flags
|
// consume optional flags
|
||||||
let read_only = pargs.contains(flag::READ_ONLY);
|
let read_only = state.flags.read_only;
|
||||||
let detach_other = pargs.contains(flag::DETACH);
|
let detach_other = state.flags.detached;
|
||||||
|
|
||||||
let args = pargs.clone().finish();
|
// consume arguments
|
||||||
let target: String;
|
let target = state.target_title().unwrap();
|
||||||
let window: Option<&OsString>;
|
let window = state.target();
|
||||||
if args.len() < 1 {
|
|
||||||
// missing name will attempt to fall back to repository
|
// do not allow attaching to the same session
|
||||||
target = util::repo_fallback();
|
if state.session && target == state.title.clone().unwrap() { error::same_session(); }
|
||||||
if !util::session_exists(target.clone()) { error::missing_target(); }
|
|
||||||
window = None;
|
|
||||||
} else {
|
|
||||||
target = args.get(0).unwrap().to_string_lossy().to_string();
|
|
||||||
window = args.get(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// make sure the session exists
|
// make sure the session exists
|
||||||
let exists = util::session_exists(target.clone());
|
let exists = util::session_exists(target.clone());
|
||||||
|
@ -54,7 +44,7 @@ pub fn attach(pargs: &mut Arguments) {
|
||||||
let select_window: Option<commands::SelectWindow>;
|
let select_window: Option<commands::SelectWindow>;
|
||||||
if let Some(window) = window {
|
if let Some(window) = window {
|
||||||
let mut command = commands::SelectWindow::new();
|
let mut command = commands::SelectWindow::new();
|
||||||
command.target_window = Some(window.to_string_lossy());
|
command.target_window = Some(window.into());
|
||||||
select_window = Some(command);
|
select_window = Some(command);
|
||||||
} else { select_window = None; }
|
} else { select_window = None; }
|
||||||
|
|
||||||
|
@ -62,35 +52,33 @@ pub fn attach(pargs: &mut Arguments) {
|
||||||
let mut tmux = Tmux::new().add_command(attach);
|
let mut tmux = Tmux::new().add_command(attach);
|
||||||
if let Some(select_window) = select_window { tmux = tmux.add_command(select_window); }
|
if let Some(select_window) = select_window { tmux = tmux.add_command(select_window); }
|
||||||
tmux.output().ok();
|
tmux.output().ok();
|
||||||
|
|
||||||
|
state.nest_deinit();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn context_action() {
|
pub fn context_action(state: &State) {
|
||||||
let repo = util::repo_root(std::env::current_dir().unwrap());
|
if !state.session {
|
||||||
if !env::tmux() && repo.is_some() {
|
if let Some(repository) = &state.repository {
|
||||||
let target = util::repo_fallback();
|
let target = repository.name.clone();
|
||||||
let mut args = Arguments::from_vec( vec![(&target).into()] );
|
let mut args = Arguments::from_vec( vec![(&target).into()] );
|
||||||
|
let mut substate = State::new(&mut args);
|
||||||
|
substate.flags = state.flags.clone();
|
||||||
if util::session_exists(&target) {
|
if util::session_exists(&target) {
|
||||||
attach(&mut args);
|
attach(&mut substate);
|
||||||
} else {
|
} else {
|
||||||
new(&mut args);
|
new(&mut substate);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
} else {
|
|
||||||
// fallback behavior is list
|
|
||||||
list();
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
// fallback behavior is list
|
||||||
|
list(&state);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn detach(pargs: &mut Arguments) {
|
pub fn detach(state: &mut State) {
|
||||||
util::terminal_enforce();
|
util::terminal_enforce();
|
||||||
// get target or fallback
|
|
||||||
let args = pargs.clone().finish();
|
let target = state.target_title().unwrap();
|
||||||
let target: String;
|
|
||||||
if args.len() < 1 {
|
|
||||||
target = util::repo_fallback();
|
|
||||||
} else {
|
|
||||||
target = args.get(0).unwrap().to_string_lossy().to_string();
|
|
||||||
}
|
|
||||||
|
|
||||||
// make sure the session exists
|
// make sure the session exists
|
||||||
let exists = util::session_exists(target.clone());
|
let exists = util::session_exists(target.clone());
|
||||||
|
@ -104,18 +92,12 @@ pub fn detach(pargs: &mut Arguments) {
|
||||||
.disable_echo().output().ok();
|
.disable_echo().output().ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn has(pargs: &mut Arguments) {
|
pub fn has(state: &mut State) {
|
||||||
// consume optional flags
|
// consume optional flags
|
||||||
let quiet = pargs.contains(flag::QUIET);
|
let quiet = state.flags.quiet;
|
||||||
|
|
||||||
// get target or fallback
|
// get target
|
||||||
let args = pargs.clone().finish();
|
let target = state.target_title().unwrap();
|
||||||
let target: String;
|
|
||||||
if args.len() < 1 {
|
|
||||||
target = util::repo_fallback();
|
|
||||||
} else {
|
|
||||||
target = args.get(0).unwrap().to_string_lossy().to_string();
|
|
||||||
}
|
|
||||||
|
|
||||||
// run command
|
// run command
|
||||||
let success = util::session_exists(target.clone());
|
let success = util::session_exists(target.clone());
|
||||||
|
@ -132,7 +114,7 @@ pub fn has(pargs: &mut Arguments) {
|
||||||
exit( if success { 0 } else { 1 });
|
exit( if success { 0 } else { 1 });
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn list() {
|
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());
|
||||||
|
|
||||||
|
@ -144,55 +126,56 @@ pub fn list() {
|
||||||
|
|
||||||
// get attached session symbol
|
// get attached session symbol
|
||||||
let attach_symbol = env_var(env::ATTACH_SYMBOL);
|
let attach_symbol = env_var(env::ATTACH_SYMBOL);
|
||||||
|
let current_symbol = env_var(env::CURRENT_SYMBOL);
|
||||||
|
|
||||||
// pretty print session list
|
// pretty print session list
|
||||||
println!("sessions:");
|
println!("sessions:");
|
||||||
for session in sessions.into_iter() {
|
for session in sessions.into_iter() {
|
||||||
let group = session.group.unwrap_or("[untitled]".to_string());
|
let name = session.name.unwrap_or("[untitled]".to_string());
|
||||||
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;
|
||||||
|
|
||||||
println!(
|
println!(
|
||||||
" {group} ({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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new(pargs: &mut Arguments) {
|
pub fn new(state: &mut State) {
|
||||||
util::terminal_enforce();
|
util::terminal_enforce();
|
||||||
// don't allow unflagged nesting
|
|
||||||
util::prevent_nest();
|
|
||||||
|
|
||||||
// get optional flags
|
// get optional flags
|
||||||
let detached = pargs.contains(flag::DETACH);
|
let detached = state.flags.detached;
|
||||||
let target_dir: Result<String, Error> = pargs.value_from_str(flag::TARGET);
|
let target_dir: Result<String, Error> = state.args.value_from_str(flag::TARGET);
|
||||||
|
|
||||||
|
// delayed nest_init; detached behavior conflicts with nest
|
||||||
|
if !detached { state.nest_init(); }
|
||||||
|
else if state.flags.nested { error::conflict_nest(Some("detached session is not nesting")); }
|
||||||
|
|
||||||
// get environment variables
|
// get environment variables
|
||||||
let window_name = env_var(env::NEW_WINDOW_NAME);
|
let window_name = env_var(env::NEW_WINDOW_NAME);
|
||||||
|
|
||||||
// get target or fallback
|
// consume arguments
|
||||||
let args = pargs.clone().finish();
|
let title = state.target_title().unwrap();
|
||||||
let title: String;
|
let command = state.target();
|
||||||
let command: Option<&OsString>;
|
|
||||||
if args.len() < 1 {
|
// don't allow duplicate names
|
||||||
// attempt repo fallback
|
let exists = util::session_exists(title.clone());
|
||||||
title = util::repo_fallback();
|
if exists { error::target_exists(title.clone()); }
|
||||||
command = None;
|
|
||||||
} else {
|
|
||||||
title = args.get(0).unwrap().to_string_lossy().to_string();
|
|
||||||
command = args.get(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut new = commands::NewSession::new();
|
let mut new = commands::NewSession::new();
|
||||||
new = new.group_name(title);
|
new = new.session_name(title);
|
||||||
if let Some(command) = command { new.shell_command = Some(command.to_string_lossy()); }
|
if let Some(command) = command { new.shell_command = Some(command.into()); }
|
||||||
if detached { new.detached = true; }
|
if detached { new.detached = true; }
|
||||||
if let Ok(target_dir) = target_dir { new = new.start_directory(target_dir); }
|
if let Ok(target_dir) = target_dir { new = new.start_directory(target_dir); }
|
||||||
|
|
||||||
|
|
|
@ -3,11 +3,13 @@ use std::env::var;
|
||||||
pub type EnvVar = (&'static str, &'static str);
|
pub type EnvVar = (&'static str, &'static str);
|
||||||
|
|
||||||
pub static ATTACH_SYMBOL: EnvVar = ("REMUX_ATTACH_SYMBOL", "*");
|
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 static NEW_WINDOW_NAME: EnvVar = ("REMUX_NEW_WINDOW", "");
|
||||||
|
|
||||||
|
pub static TMUX: &str = "TMUX";
|
||||||
|
|
||||||
|
/// get or default an environment variable
|
||||||
pub fn env_var(envvar: EnvVar) -> String {
|
pub fn env_var(envvar: EnvVar) -> String {
|
||||||
var(envvar.0).unwrap_or(envvar.1.to_string())
|
var(envvar.0).unwrap_or(envvar.1.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn tmux() -> bool { !var("TMUX").unwrap_or("".to_string()).is_empty() }
|
|
||||||
|
|
||||||
|
|
34
src/error.rs
34
src/error.rs
|
@ -6,6 +6,7 @@ pub fn no_subcommand(subcommand: String) {
|
||||||
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();
|
||||||
|
@ -13,30 +14,61 @@ pub fn no_target<S: Into<String>>(target: S) {
|
||||||
exit(2);
|
exit(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// 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) {
|
||||||
println!("remux: no help for \"{topic}\"");
|
println!("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() {
|
||||||
println!("remux: no target provided");
|
println!("remux: no target provided");
|
||||||
exit(4);
|
exit(4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// refuse to attach to current session; code 4
|
||||||
|
pub fn same_session() {
|
||||||
|
println!("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) {
|
||||||
|
let target = target.into();
|
||||||
|
println!("remux: session \"{target}\" already exists");
|
||||||
|
exit(4);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/// non-terminal environment prevention; code 5
|
/// non-terminal environment prevention; code 5
|
||||||
pub fn not_terminal() {
|
pub fn not_terminal() {
|
||||||
println!("remux: not running from a terminal");
|
println!("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() {
|
||||||
println!("remux: cannot use nesting flag outside a TMUX session");
|
println!("remux: inappropriate nesting flag (-n); not in a session");
|
||||||
exit(6);
|
exit(6);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// operation requires nesting flag; code 6
|
||||||
|
pub fn prevent_nest() {
|
||||||
|
println!("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."); }
|
||||||
|
exit(6);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/// 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) {
|
||||||
println!("remux: '{cmd}' must be run from within a session");
|
println!("remux: '{cmd}' must be run from within a session");
|
||||||
|
|
40
src/flag.rs
40
src/flag.rs
|
@ -1,4 +1,6 @@
|
||||||
|
|
||||||
|
use pico_args::Arguments;
|
||||||
|
|
||||||
type Flag = [&'static str;2];
|
type Flag = [&'static str;2];
|
||||||
|
|
||||||
pub static DETACH: Flag = ["-d", "--detach"];
|
pub static DETACH: Flag = ["-d", "--detach"];
|
||||||
|
@ -9,3 +11,41 @@ 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 struct Flags {
|
||||||
|
pub detached: bool,
|
||||||
|
pub nested: bool,
|
||||||
|
pub quiet: bool,
|
||||||
|
pub read_only: bool,
|
||||||
|
pub target: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Flags {
|
||||||
|
|
||||||
|
pub fn from(args: &mut Arguments) -> Flags {
|
||||||
|
let detached = args.contains(DETACH);
|
||||||
|
let nested = args.contains(NEST);
|
||||||
|
let quiet = args.contains(QUIET);
|
||||||
|
let read_only = args.contains(READ_ONLY);
|
||||||
|
let target = args.value_from_str(TARGET).ok();
|
||||||
|
|
||||||
|
Flags {
|
||||||
|
detached,
|
||||||
|
nested,
|
||||||
|
quiet,
|
||||||
|
read_only,
|
||||||
|
target
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clone(&self) -> Flags {
|
||||||
|
Flags {
|
||||||
|
detached: self.detached,
|
||||||
|
nested: self.nested,
|
||||||
|
quiet: self.quiet,
|
||||||
|
read_only: self.read_only,
|
||||||
|
target: None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
20
src/help.rs
20
src/help.rs
|
@ -25,6 +25,7 @@ commands:
|
||||||
|
|
||||||
path print session path (session)
|
path print session path (session)
|
||||||
switch switch to another session (session)
|
switch switch to another session (session)
|
||||||
|
title print session title (session)
|
||||||
|
|
||||||
Use 'remux help <command>' to see detailed help text for each command.
|
Use 'remux help <command>' to see detailed help text for each command.
|
||||||
|
|
||||||
|
@ -101,7 +102,7 @@ flags:
|
||||||
-n, --nest Create the session inside another session.
|
-n, --nest Create the session inside another session.
|
||||||
-t, --target <dir> Sets the target directory for the new session."),
|
-t, --target <dir> Sets the target directory for the new session."),
|
||||||
|
|
||||||
Some("root")
|
Some("p" | "path")
|
||||||
=>
|
=>
|
||||||
println!("remux path
|
println!("remux path
|
||||||
Print the session path (#{{session_path}}) to standard output.
|
Print the session path (#{{session_path}}) to standard output.
|
||||||
|
@ -126,6 +127,16 @@ flags:
|
||||||
-r, --read-only Attach the target session as read-only."),
|
-r, --read-only Attach the target session as read-only."),
|
||||||
|
|
||||||
|
|
||||||
|
Some("w" | "which" | "title")
|
||||||
|
=>
|
||||||
|
println!("remux which
|
||||||
|
Print the title of the current session.
|
||||||
|
|
||||||
|
usage: remux which
|
||||||
|
remux w
|
||||||
|
remux title"),
|
||||||
|
|
||||||
|
|
||||||
// TOPIC HELP
|
// TOPIC HELP
|
||||||
|
|
||||||
Some("env" | "vars")
|
Some("env" | "vars")
|
||||||
|
@ -137,10 +148,15 @@ REMUX_ATTACH_SYMBOL
|
||||||
by the 'list' command.
|
by the 'list' command.
|
||||||
Default: '*'
|
Default: '*'
|
||||||
|
|
||||||
|
REMUX_CURRENT_SYMBOL
|
||||||
|
Changes the symbol displayed to denote the current session
|
||||||
|
in the 'list' command.
|
||||||
|
Default: '>'
|
||||||
|
|
||||||
REMUX_NEW_WINDOW
|
REMUX_NEW_WINDOW
|
||||||
Provides a default window name when creating a session with
|
Provides a default window name when creating a session with
|
||||||
the 'new' command, if not empty.
|
the 'new' command, if not empty.
|
||||||
Default: ''"),
|
Default: (unset)"),
|
||||||
|
|
||||||
// not found
|
// not found
|
||||||
_ => error::no_help(topic.unwrap())
|
_ => error::no_help(topic.unwrap())
|
||||||
|
|
41
src/main.rs
41
src/main.rs
|
@ -1,4 +1,3 @@
|
||||||
use std::env::{ set_var, var };
|
|
||||||
|
|
||||||
use pico_args::Arguments;
|
use pico_args::Arguments;
|
||||||
|
|
||||||
|
@ -7,9 +6,11 @@ mod env;
|
||||||
mod error;
|
mod error;
|
||||||
mod flag;
|
mod flag;
|
||||||
mod help;
|
mod help;
|
||||||
|
mod state;
|
||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
use help::{ help, version };
|
use help::{ help, version };
|
||||||
|
use state::State;
|
||||||
|
|
||||||
static VERSION: &str = env!("CARGO_PKG_VERSION");
|
static VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||||
|
|
||||||
|
@ -28,52 +29,44 @@ fn main() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let nesting = args.contains(flag::NEST);
|
let mut state = State::new(&mut args);
|
||||||
let tmux_var = var("TMUX").ok();
|
|
||||||
if nesting {
|
|
||||||
if tmux_var.is_none() {
|
|
||||||
error::not_nesting();
|
|
||||||
}
|
|
||||||
set_var("TMUX", "");
|
|
||||||
}
|
|
||||||
|
|
||||||
let subcommand = args.subcommand().unwrap();
|
let target = state.target();
|
||||||
|
|
||||||
// invoke subcommand function
|
// invoke subcommand function
|
||||||
match subcommand.as_deref() {
|
match target.as_deref() {
|
||||||
Some("help")
|
Some("help")
|
||||||
=> help(&mut args),
|
=> help(&mut args),
|
||||||
None
|
None
|
||||||
=> command::share::context_action(),
|
=> command::share::context_action(&state),
|
||||||
|
|
||||||
Some("a" | "attach")
|
Some("a" | "attach")
|
||||||
=> command::share::attach(&mut args),
|
=> command::share::attach(&mut state),
|
||||||
|
|
||||||
Some("d" | "detach")
|
Some("d" | "detach")
|
||||||
=> command::share::detach(&mut args),
|
=> command::share::detach(&mut state),
|
||||||
|
|
||||||
Some("h" | "has")
|
Some("h" | "has")
|
||||||
=> command::share::has(&mut args),
|
=> command::share::has(&mut state),
|
||||||
|
|
||||||
Some("l" | "ls" | "list")
|
Some("l" | "ls" | "list")
|
||||||
=> command::share::list(),
|
=> command::share::list(&state),
|
||||||
|
|
||||||
Some("n" | "new")
|
Some("n" | "new")
|
||||||
=> command::share::new(&mut args),
|
=> command::share::new(&mut state),
|
||||||
|
|
||||||
Some("p" | "path")
|
Some("p" | "path")
|
||||||
=> command::session::path(),
|
=> command::session::path(&mut state),
|
||||||
|
|
||||||
Some("s" | "switch")
|
Some("s" | "switch")
|
||||||
=> command::session::switch(&mut args),
|
=> command::session::switch(&mut state),
|
||||||
|
|
||||||
|
Some("t" | "title" | "which")
|
||||||
|
=> command::session::title(state),
|
||||||
|
|
||||||
_
|
_
|
||||||
=> error::no_subcommand(subcommand.unwrap())
|
=> error::no_subcommand(target.unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
// re-set TMUX var if we unset it for nest mode
|
|
||||||
if nesting {
|
|
||||||
set_var("TMUX", tmux_var.unwrap());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
96
src/state.rs
Normal file
96
src/state.rs
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
use std::{
|
||||||
|
env,
|
||||||
|
path::PathBuf
|
||||||
|
};
|
||||||
|
|
||||||
|
use pico_args::Arguments;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
env::TMUX,
|
||||||
|
error,
|
||||||
|
flag::Flags,
|
||||||
|
util::{ find, session_name }
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct State<'a> {
|
||||||
|
pub args: &'a mut Arguments,
|
||||||
|
pub flags: Flags,
|
||||||
|
|
||||||
|
pub session: bool,
|
||||||
|
tmux_var: Option<String>,
|
||||||
|
pub title: Option<String>,
|
||||||
|
|
||||||
|
pub repository: Option<Repository>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl State<'_> {
|
||||||
|
|
||||||
|
pub fn new(args: &mut Arguments) -> 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 };
|
||||||
|
let repository = Repository::find();
|
||||||
|
|
||||||
|
State {
|
||||||
|
args,
|
||||||
|
flags,
|
||||||
|
|
||||||
|
session,
|
||||||
|
tmux_var,
|
||||||
|
title,
|
||||||
|
|
||||||
|
repository
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn nest_init(&self) {
|
||||||
|
if self.flags.nested {
|
||||||
|
if self.session { env::set_var(TMUX, ""); } // nesting & session => ok
|
||||||
|
else { error::not_nesting(); } // nesting & !session => error
|
||||||
|
} else if self.session { error::prevent_nest(); } // !nesting & session => error
|
||||||
|
// !nesting & !session => ok
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn nest_deinit(&self) {
|
||||||
|
if self.flags.nested && self.session {
|
||||||
|
env::set_var(TMUX, self.tmux_var.as_ref().unwrap());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn session_enforce(&self, cmd: &'static str) {
|
||||||
|
if !self.session { error::not_in_session(cmd); }
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
pub struct Repository {
|
||||||
|
pub path: PathBuf,
|
||||||
|
pub name: String
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Repository {
|
||||||
|
pub fn find() -> Option<Repository> {
|
||||||
|
let path = find(".git", env::current_dir().unwrap());
|
||||||
|
if let Some(path) = path {
|
||||||
|
let name = path.file_name().unwrap().to_string_lossy().to_string();
|
||||||
|
let inner = Repository {
|
||||||
|
path,
|
||||||
|
name
|
||||||
|
};
|
||||||
|
Some(inner)
|
||||||
|
} else { None }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
53
src/util.rs
53
src/util.rs
|
@ -1,8 +1,6 @@
|
||||||
use std::{
|
use std::{
|
||||||
env::current_dir,
|
|
||||||
io::{ stdout, IsTerminal },
|
io::{ stdout, IsTerminal },
|
||||||
path::PathBuf,
|
path::PathBuf
|
||||||
process::exit
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use tmux_interface::{
|
use tmux_interface::{
|
||||||
|
@ -12,10 +10,19 @@ use tmux_interface::{
|
||||||
variables::session::SessionsCtl
|
variables::session::SessionsCtl
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::error;
|
||||||
env,
|
|
||||||
error
|
pub fn session_name() -> Option<String> {
|
||||||
};
|
let message = commands::DisplayMessage::new().print().message("#{session_name}");
|
||||||
|
|
||||||
|
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())
|
||||||
|
} else { None }
|
||||||
|
} else { None }
|
||||||
|
}
|
||||||
|
|
||||||
/// return a Vec of all sessions or None
|
/// return a Vec of all sessions or None
|
||||||
pub fn get_sessions() -> Option<Vec<Session>> {
|
pub fn get_sessions() -> Option<Vec<Session>> {
|
||||||
|
@ -25,19 +32,6 @@ pub fn get_sessions() -> Option<Vec<Session>> {
|
||||||
} else { return None; }
|
} else { return None; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// show the tmux nest text if env var is not unset
|
|
||||||
pub fn prevent_nest() {
|
|
||||||
if env::tmux() {
|
|
||||||
println!("To nest sessions, use the -n flag.");
|
|
||||||
exit(6);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// enforce a command is being used in-session
|
|
||||||
pub fn session_enforce(cmd: &'static str) {
|
|
||||||
if !env::tmux() { error::not_in_session(cmd); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// check whether a target session exists
|
/// check whether a target session exists
|
||||||
pub fn session_exists<S: Into<String>>(target: S) -> bool {
|
pub fn session_exists<S: Into<String>>(target: S) -> bool {
|
||||||
let has_session = commands::HasSession::new()
|
let has_session = commands::HasSession::new()
|
||||||
|
@ -53,23 +47,12 @@ pub fn terminal_enforce() {
|
||||||
if !stdout().is_terminal() { error::not_terminal(); }
|
if !stdout().is_terminal() { error::not_terminal(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// attempt to return the repo name or exit
|
/// recursively propagate up directories to find a child
|
||||||
pub fn repo_fallback() -> String {
|
pub fn find(target: &'static str, path: PathBuf) -> Option<PathBuf> {
|
||||||
let repo = repo_root(current_dir().unwrap());
|
if path.join(target).exists() { return Some(path); }
|
||||||
if repo.is_none() { error::missing_target(); }
|
|
||||||
|
|
||||||
let target = repo.unwrap().file_name().unwrap().to_string_lossy().to_string();
|
|
||||||
target
|
|
||||||
}
|
|
||||||
|
|
||||||
/// recursively attempt to find a git root directory
|
|
||||||
pub fn repo_root(path: PathBuf) -> Option<PathBuf> {
|
|
||||||
// if .git dir is found, return
|
|
||||||
if path.join(".git").exists() { return Some(path); }
|
|
||||||
|
|
||||||
// otherwise, attempt to traverse
|
|
||||||
let parent = path.parent();
|
let parent = path.parent();
|
||||||
if let Some(parent) = parent { repo_root(parent.to_path_buf()) }
|
if let Some(parent) = parent { return find(target, parent.to_path_buf()) }
|
||||||
else { None }
|
else { None }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue