Compare commits

..

6 commits

16 changed files with 193 additions and 1038 deletions

View file

@ -1,20 +1,12 @@
[package] [package]
name = "remux" name = "remux"
version = "0.4.0" version = "0.3.0"
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"
homepage = "https://git.vwolfe.io/valerie/remux" homepage = "https://git.vwolfe.io/valerie/remux"
repository = "https://git.vwolfe.io/valerie/remux" repository = "https://git.vwolfe.io/valerie/remux"
license = "MIT" license = "MIT"
categories = [ "command-line-utilities" ]
keywords = [ "tmux", "remux" ]
[package.metadata.aur]
name = "remux"
type = "source"
archive = "archive/v$pkgver.tar.gz"
depends = [ "tmux>=3.0" ]
[[bin]] [[bin]]
name = "remux" name = "remux"

View file

@ -17,23 +17,24 @@ shorter than its equivalent tmux command:
```sh ```sh
# new session # new session
tmux new-s -t foo tmux new-session -t foo
remux n foo remux n foo
# list sessions # lists
tmux ls tmux ls
remux l remux l
remux
# attach # attach
tmux a -t foo tmux a -t foo
remux a foo remux a foo
# has # has
tmux h -t foo tmux has -t foo
remux h foo remux has foo
# detach # detach
tmux det -t foo tmux detach-client -t foo
remux d foo remux d foo
# nesting sessions with '-n' flag # nesting sessions with '-n' flag
@ -42,14 +43,6 @@ remux a -n foo
TMUX='' tmux new-session -t foo TMUX='' tmux new-session -t foo
remux n -n foo remux n -n foo
# switch to another session
tmux swi -t foo
rmux s foo
# cd to session path
cd `tmux display-mes -p "#{session_path}"`
cd `rmux p`
``` ```
## Dependencies ## Dependencies
@ -92,19 +85,6 @@ 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
<details>
<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>
## Configuration ## Configuration
The pretty-print attached symbol (default: `*`) can be set manually by setting `REMUX_ATTACH_SYMBOL`. The pretty-print attached symbol (default: `*`) can be set manually by setting `REMUX_ATTACH_SYMBOL`.

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,4 +0,0 @@
[licenses]
allow = [ "MIT" ]

View file

@ -1,179 +0,0 @@
.Dd $Mdocdate$
.Dt REMUX 1
.Sh NAME
.Nm remux
.Nd a command shortener for
.Xr tmux 1
.Sh SYNOPSIS
.Nm remux
.Op Fl dhnqrtv
.Op Fl D Ar path
.Op Ar 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 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
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 rd
.Op 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 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. If blank, the previous session will be used.
.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)
.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
.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

260
sbom.xml
View file

@ -1,260 +0,0 @@
{
"SPDXID": "SPDXRef-DOCUMENT",
"creationInfo": {
"created": "2024-03-14T20:41:35.559Z",
"creators": [
"Tool: cargo-sbom-v0.8.4"
]
},
"dataLicense": "CC0-1.0",
"documentNamespace": "https://spdx.org/spdxdocs/remux-eb288ebb-1cdd-412b-87c2-b15fc96cd8bd",
"files": [
{
"SPDXID": "SPDXRef-File-remux",
"checksums": [],
"fileName": "remux",
"fileTypes": [
"BINARY"
]
}
],
"name": "remux",
"packages": [
{
"SPDXID": "SPDXRef-Package-libredox-0.0.2",
"description": "Redox stable ABI",
"downloadLocation": "registry+https://github.com/rust-lang/crates.io-index",
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceLocator": "pkg:cargo/libredox@0.0.2",
"referenceType": "purl"
}
],
"licenseConcluded": "MIT",
"licenseDeclared": "MIT",
"name": "libredox",
"versionInfo": "0.0.2"
},
{
"SPDXID": "SPDXRef-Package-libc-0.2.153",
"description": "Raw FFI bindings to platform libraries like libc.\n",
"downloadLocation": "registry+https://github.com/rust-lang/crates.io-index",
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceLocator": "pkg:cargo/libc@0.2.153",
"referenceType": "purl"
}
],
"homepage": "https://github.com/rust-lang/libc",
"licenseConcluded": "MIT OR Apache-2.0",
"licenseDeclared": "MIT OR Apache-2.0",
"name": "libc",
"versionInfo": "0.2.153"
},
{
"SPDXID": "SPDXRef-Package-numtoa-0.1.0",
"description": "Convert numbers into stack-allocated byte arrays",
"downloadLocation": "registry+https://github.com/rust-lang/crates.io-index",
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceLocator": "pkg:cargo/numtoa@0.1.0",
"referenceType": "purl"
}
],
"licenseConcluded": "MIT OR Apache-2.0",
"licenseDeclared": "MIT OR Apache-2.0",
"name": "numtoa",
"versionInfo": "0.1.0"
},
{
"SPDXID": "SPDXRef-Package-termion-2.0.3",
"description": "A bindless library for manipulating terminals.",
"downloadLocation": "registry+https://github.com/rust-lang/crates.io-index",
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceLocator": "pkg:cargo/termion@2.0.3",
"referenceType": "purl"
}
],
"licenseConcluded": "MIT",
"licenseDeclared": "MIT",
"name": "termion",
"versionInfo": "2.0.3"
},
{
"SPDXID": "SPDXRef-Package-pico-args-0.5.0",
"description": "An ultra simple CLI arguments parser.",
"downloadLocation": "registry+https://github.com/rust-lang/crates.io-index",
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceLocator": "pkg:cargo/pico-args@0.5.0",
"referenceType": "purl"
}
],
"licenseConcluded": "MIT",
"licenseDeclared": "MIT",
"name": "pico-args",
"versionInfo": "0.5.0"
},
{
"SPDXID": "SPDXRef-Package-redox_termios-0.1.3",
"description": "A Rust library to access Redox termios functions",
"downloadLocation": "registry+https://github.com/rust-lang/crates.io-index",
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceLocator": "pkg:cargo/redox_termios@0.1.3",
"referenceType": "purl"
}
],
"licenseConcluded": "MIT",
"licenseDeclared": "MIT",
"name": "redox_termios",
"versionInfo": "0.1.3"
},
{
"SPDXID": "SPDXRef-Package-remux-0.2.1",
"description": "A friendly command shortener for tmux",
"downloadLocation": "NONE",
"homepage": "https://git.vwolfe.io/valerie/remux",
"licenseConcluded": "MIT",
"licenseDeclared": "MIT",
"name": "remux",
"versionInfo": "0.2.1"
},
{
"SPDXID": "SPDXRef-Package-bitflags-2.4.2",
"description": "A macro to generate structures which behave like bitflags.\n",
"downloadLocation": "registry+https://github.com/rust-lang/crates.io-index",
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceLocator": "pkg:cargo/bitflags@2.4.2",
"referenceType": "purl"
}
],
"homepage": "https://github.com/bitflags/bitflags",
"licenseConcluded": "MIT OR Apache-2.0",
"licenseDeclared": "MIT OR Apache-2.0",
"name": "bitflags",
"versionInfo": "2.4.2"
},
{
"SPDXID": "SPDXRef-Package-bitflags-1.3.2",
"description": "A macro to generate structures which behave like bitflags.\n",
"downloadLocation": "registry+https://github.com/rust-lang/crates.io-index",
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceLocator": "pkg:cargo/bitflags@1.3.2",
"referenceType": "purl"
}
],
"homepage": "https://github.com/bitflags/bitflags",
"licenseConcluded": "MIT OR Apache-2.0",
"licenseDeclared": "MIT/Apache-2.0",
"name": "bitflags",
"versionInfo": "1.3.2"
},
{
"SPDXID": "SPDXRef-Package-redox_syscall-0.4.1",
"description": "A Rust library to access raw Redox system calls",
"downloadLocation": "registry+https://github.com/rust-lang/crates.io-index",
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceLocator": "pkg:cargo/redox_syscall@0.4.1",
"referenceType": "purl"
}
],
"licenseConcluded": "MIT",
"licenseDeclared": "MIT",
"name": "redox_syscall",
"versionInfo": "0.4.1"
},
{
"SPDXID": "SPDXRef-Package-tmux_interface-0.2.1",
"description": "Rust language library for communication with TMUX via CLI",
"downloadLocation": "registry+https://github.com/rust-lang/crates.io-index",
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceLocator": "pkg:cargo/tmux_interface@0.2.1",
"referenceType": "purl"
}
],
"licenseConcluded": "MIT",
"licenseDeclared": "MIT",
"name": "tmux_interface",
"versionInfo": "0.2.1"
}
],
"relationships": [
{
"relatedSpdxElement": "SPDXRef-Package-libc-0.2.153",
"relationshipType": "DEPENDS_ON",
"spdxElementId": "SPDXRef-Package-termion-2.0.3"
},
{
"relatedSpdxElement": "SPDXRef-Package-termion-2.0.3",
"relationshipType": "DEPENDS_ON",
"spdxElementId": "SPDXRef-Package-remux-0.2.1"
},
{
"relatedSpdxElement": "SPDXRef-Package-redox_syscall-0.4.1",
"relationshipType": "DEPENDS_ON",
"spdxElementId": "SPDXRef-Package-libredox-0.0.2"
},
{
"relatedSpdxElement": "SPDXRef-Package-numtoa-0.1.0",
"relationshipType": "DEPENDS_ON",
"spdxElementId": "SPDXRef-Package-termion-2.0.3"
},
{
"relatedSpdxElement": "SPDXRef-Package-tmux_interface-0.2.1",
"relationshipType": "DEPENDS_ON",
"spdxElementId": "SPDXRef-Package-remux-0.2.1"
},
{
"relatedSpdxElement": "SPDXRef-Package-pico-args-0.5.0",
"relationshipType": "DEPENDS_ON",
"spdxElementId": "SPDXRef-Package-remux-0.2.1"
},
{
"relatedSpdxElement": "SPDXRef-Package-redox_termios-0.1.3",
"relationshipType": "DEPENDS_ON",
"spdxElementId": "SPDXRef-Package-termion-2.0.3"
},
{
"relatedSpdxElement": "SPDXRef-Package-libc-0.2.153",
"relationshipType": "DEPENDS_ON",
"spdxElementId": "SPDXRef-Package-libredox-0.0.2"
},
{
"relatedSpdxElement": "SPDXRef-Package-bitflags-1.3.2",
"relationshipType": "DEPENDS_ON",
"spdxElementId": "SPDXRef-Package-redox_syscall-0.4.1"
},
{
"relatedSpdxElement": "SPDXRef-Package-remux-0.2.1",
"relationshipType": "GENERATED_FROM",
"spdxElementId": "SPDXRef-File-remux"
},
{
"relatedSpdxElement": "SPDXRef-Package-bitflags-2.4.2",
"relationshipType": "DEPENDS_ON",
"spdxElementId": "SPDXRef-Package-libredox-0.0.2"
},
{
"relatedSpdxElement": "SPDXRef-Package-libredox-0.0.2",
"relationshipType": "DEPENDS_ON",
"spdxElementId": "SPDXRef-Package-termion-2.0.3"
}
],
"spdxVersion": "SPDX-2.3"
}

View file

@ -1,4 +1,4 @@
pub mod share; pub mod share;
pub mod session; //pub mod session;

View file

@ -1,67 +0,0 @@
//! commands accessible from within a session
use tmux_interface::{
Tmux,
commands
};
use crate::{
error,
state::State,
util::{
self,
message,
MSG_PREVIOUS, MSG_SESSION_PATH, NULL
}
};
pub fn path(state: &mut State) {
state.session_enforce("path");
if let Some(message) = message(MSG_SESSION_PATH) {
println!("{message}");
}
}
pub fn switch(state: &mut State) {
util::terminal_enforce();
// refuse to run outside a session
state.session_enforce("switch");
// consume optional flags
let read_only = state.flags.read_only;
let detach_other = state.flags.detached;
let args = state.args.clone().finish();
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);
if read_only { switch.read_only = true; }
tmux.add_command(switch)
.stderr(NULL).output().ok();
}
pub fn title(state: State) {
state.session_enforce("title");
if let Some(title) = state.title { println!("{title}"); }
}

View file

@ -1,5 +1,8 @@
//! globally available tmux commands. //! globally available tmux commands.
use std::process::exit; use std::{
ffi::OsString,
process::exit
};
use pico_args::{ Arguments, Error }; use pico_args::{ Arguments, Error };
use termion::{ color, style }; use termion::{ color, style };
@ -9,35 +12,32 @@ 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, util
util::{
self,
message,
MSG_PREVIOUS, NULL
}
}; };
pub fn attach(state: &mut State) { pub fn attach(pargs: &mut Arguments) {
util::terminal_enforce(); // don't allow unflagged nests
state.nest_init(); util::prevent_nest();
// consume optional flags // consume optional flags
let read_only = state.flags.read_only; let read_only = pargs.contains(flag::READ_ONLY);
let detach_other = state.flags.detached; let detach_other = pargs.contains(flag::DETACH);
// consume arguments let args = pargs.clone().finish();
let target = state.target_title().unwrap(); let target: String;
let window = state.target(); let window: Option<&OsString>;
if args.len() < 1 {
// do not allow attaching to the same session // missing name will attempt to fall back to repository
if state.session && target == state.title.clone().unwrap() { error::same_session(); } target = util::repo_fallback();
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());
@ -52,41 +52,25 @@ pub fn attach(state: &mut State) {
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.into()); command.target_window = Some(window.to_string_lossy());
select_window = Some(command); select_window = Some(command);
} else { select_window = None; } } else { select_window = None; }
// build dispatch // build dispatch
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.stderr(NULL).output().ok(); tmux.output().ok();
state.nest_deinit();
} }
pub fn context_action(state: &mut State) { pub fn detach(pargs: &mut Arguments) {
if !state.session { // get target or fallback
if let Some(repository) = &state.repository { let args = pargs.clone().finish();
let target = repository.name.clone(); let target: String;
let mut args = Arguments::from_vec( vec![(&target).into()] ); if args.len() < 1 {
let mut substate = State::new(&mut args); target = util::repo_fallback();
substate.flags = state.flags.clone(); } else {
if util::session_exists(&target) { target = args.get(0).unwrap().to_string_lossy().to_string();
attach(&mut substate);
} else {
new(&mut substate);
}
return;
}
} }
// fallback behavior is list
list(state);
}
pub fn detach(state: &mut State) {
util::terminal_enforce();
let target = state.target_title().unwrap();
// make sure the session exists // make sure the session exists
let exists = util::session_exists(target.clone()); let exists = util::session_exists(target.clone());
@ -97,15 +81,21 @@ pub fn detach(state: &mut State) {
.target_session(target); .target_session(target);
Tmux::new() Tmux::new()
.add_command(detach) .add_command(detach)
.disable_echo().output().ok(); .output().ok();
} }
pub fn has(state: &mut State) { pub fn has(pargs: &mut Arguments) {
// consume optional flags // consume optional flags
let quiet = state.flags.quiet; let quiet = pargs.contains(flag::QUIET);
// get target // get target or fallback
let target = state.target_title().unwrap(); let args = pargs.clone().finish();
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());
@ -122,83 +112,66 @@ 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() {
// 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 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 group = session.group.unwrap_or("[untitled]".to_string());
let id = session.id.unwrap();
let attached = session.attached.unwrap_or(0) > 0;
if search.is_some() && !name.starts_with(search.as_ref().unwrap()) { continue; } println!(
" {group} ({bold}{blue}{id}{reset}) {bold}{green}{attach}{reset}",
if !state.flags.quiet { // values
let id = session.id.unwrap(); attach = if attached { attach_symbol.clone() } else { "".to_string() },
// formatting
let attached = session.attached.unwrap_or(0) > 0; bold = style::Bold,
blue = color::Fg(color::Blue),
let compare = Some(name.clone()); green = color::Fg(color::LightGreen),
let marker = reset = style::Reset
if compare == state.title { current_symbol.clone() } );
else if state.session && compare == previous { prev_symbol.clone() }
else { " ".to_string() };
println!(
" {marker} {name}{reset} ({bold}{blue}{id}{reset}) {bold}{green}{attach}{reset}",
// values
attach = if attached { attach_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} ");
}
} }
} }
pub fn new(state: &mut State) { pub fn new(pargs: &mut Arguments) {
util::terminal_enforce(); // don't allow unflagged nesting
util::prevent_nest();
// get optional flags // get optional flags
let detached = state.flags.detached; let detached = pargs.contains(flag::DETACH);
let target_dir: Result<String, Error> = state.args.value_from_str(flag::TARGET); let target_dir: Result<String, Error> = pargs.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);
// consume arguments // get target or fallback
let title = state.target_title().unwrap(); let args = pargs.clone().finish();
let command = state.target(); let title: String;
let command: Option<&OsString>;
// don't allow duplicate names if args.len() < 1 {
let exists = util::session_exists(title.clone()); // attempt repo fallback
if exists { error::target_exists(title.clone()); } title = util::repo_fallback();
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.session_name(title); new = new.group_name(title);
if let Some(command) = command { new.shell_command = Some(command.into()); } if let Some(command) = command { new.shell_command = Some(command.to_string_lossy()); }
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); }
@ -211,6 +184,6 @@ pub fn new(state: &mut State) {
tmux = tmux.add_command(auto_name); tmux = tmux.add_command(auto_name);
} }
tmux.stderr(NULL).output().ok(); tmux.output().ok();
} }

View file

@ -2,16 +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 NEW_WINDOW_NAME: EnvVar = ("REMUX_NEW_WINDOW", "");
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 const 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() }

View file

@ -1,84 +1,39 @@
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}\""); println!("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"); println!("remux: no session \"{target}\" exists");
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) {
eprintln!("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() {
eprintln!("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() -> ! {
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) -> ! {
let target = target.into();
eprintln!("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() {
eprintln!("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() {
eprintln!("remux: inappropriate nesting flag (-n); not in a session"); println!("remux: cannot use nesting flag outside a TMUX session");
exit(6); exit(6);
} }
/// operation requires nesting flag; code 6
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 { 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) -> ! {
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);
}

View file

@ -1,56 +1,11 @@
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"];
pub static HELP: Flag = [ "-h", "--help" ]; pub static HELP: Flag = ["-h", "--help"];
pub static NEST: Flag = [ "-n", "--nest" ]; pub static NEST: Flag = ["-n", "--nest"];
pub static QUIET: Flag = [ "-q", "--quiet" ]; 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 detached: bool,
pub nested: bool,
pub quiet: bool,
pub read_only: bool,
pub target: Option<String>,
pub working_dir: 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();
let working_dir = args.value_from_str(WORKING_DIR).ok();
Flags {
detached,
nested,
quiet,
read_only,
target,
working_dir
}
}
pub fn clone(&self) -> Flags {
Flags {
detached: self.detached,
nested: self.nested,
quiet: self.quiet,
read_only: self.read_only,
target: self.target.clone(),
working_dir: self.working_dir.clone()
}
}
}

View file

@ -13,29 +13,19 @@ println!("remux v{VERSION}
Valerie Wolfe <sleeplessval@gmail.com> Valerie Wolfe <sleeplessval@gmail.com>
A command wrapper for tmux written in Rust. A command wrapper for tmux written in Rust.
usage: remux [command] [<args>] usage: remux <command> [<args>]
commands: commands:
help Show help text for remux, a command, or a help topic. help Show help text for remux or a specific command
attach Attach to an existing tmux session attach Attach to an existing tmux session
detach Detach clients from a tmux session detach Detach clients from a tmux session
has Check if a tmux session exists has Check if a tmux session exists
list Pretty-print all tmux sessions list Pretty-print all tmux sessions
new Create a new tmux session new Create a new tmux session
path print session path (session) Use 'remux help <command>' to see detailed help text for each command."),
switch switch to another session (session)
title print session title (session)
Use 'remux help <command>' to see detailed help text for each command. Some("a" | "attach")
help topics:
env Environment variables"),
// COMMAND HELP
Some("a" | "attach")
=> =>
println!("remux attach println!("remux attach
Attach to an existing session. Attach to an existing session.
@ -63,13 +53,12 @@ usage: remux detach <session>
args: args:
<session> The session name to detach clients from"), <session> The session name to detach clients from"),
Some("h" | "has") Some("has")
=> =>
println!("remux has println!("remux has
Check if the target session exists. Check if the target session exists.
usage: remux has [flags] <session> usage: remux has [flags] <session>
rmux h [flags] session
args: args:
<session> The session to check for <session> The session to check for
@ -102,63 +91,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("p" | "path") // not found
=>
println!("remux path
Print the session path (#{{session_path}}) to standard output.
Must be run from inside a session.
usage: remux path
remux p"),
Some("s" | "switch")
=>
println!("remux switch
Switch to a different tmux session.
Must be run from inside a session.
usage: remux switch [flags] <title>
remux s [flags] <title>
args:
<title> The title of the session to switch to.
flags:
-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
Some("env" | "vars")
=>
println!("remux environment variables
REMUX_ATTACH_SYMBOL
Changes the symbol displayed for attached sessions displayed
by the 'list' command.
Default: '*'
REMUX_CURRENT_SYMBOL
Changes the symbol displayed to denote the current session
in the 'list' command.
Default: '>'
REMUX_NEW_WINDOW
Provides a default window name when creating a session with
the 'new' command, if not empty.
Default: (unset)"),
// not found
_ => error::no_help(topic.unwrap()) _ => error::no_help(topic.unwrap())
} }
} }

View file

@ -1,3 +1,7 @@
use std::{
env::{ set_var, var },
io::{ stdout, IsTerminal }
};
use pico_args::Arguments; use pico_args::Arguments;
@ -6,11 +10,9 @@ 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");
@ -29,44 +31,47 @@ fn main() {
return; return;
} }
let mut state = State::new(&mut args); let nesting = args.contains(flag::NEST);
let tmux_var = var("TMUX").ok();
let target = state.target(); if nesting {
if tmux_var.is_none() {
// invoke subcommand function error::not_nesting();
match target.as_deref() { }
Some("help") set_var("TMUX", "");
=> help(&mut args),
None
=> command::share::context_action(&mut state),
Some("a" | "attach")
=> command::share::attach(&mut state),
Some("d" | "detach")
=> command::share::detach(&mut state),
Some("h" | "has")
=> command::share::has(&mut state),
Some("l" | "ls" | "list")
=> command::share::list(&mut state),
Some("n" | "new")
=> command::share::new(&mut state),
Some("p" | "path")
=> command::session::path(&mut state),
Some("s" | "switch")
=> command::session::switch(&mut state),
Some("t" | "title" | "which")
=> command::session::title(state),
_
=> error::no_subcommand(target.unwrap())
} }
if !stdout().is_terminal() { error::not_terminal(); }
let subcommand = args.subcommand().unwrap();
// invoke subcommand function
match subcommand.as_deref() {
Some("h" | "help")
=> help(&mut args),
Some("a" | "attach")
=> command::share::attach(&mut args),
Some("d" | "detach")
=> command::share::detach(&mut args),
Some("has")
=> command::share::has(&mut args),
None |
Some("l" | "ls" | "list")
=> command::share::list(),
Some("n" | "new")
=> command::share::new(&mut args),
_
=> error::no_subcommand(subcommand.unwrap())
}
// re-set TMUX var if we unset it for nest mode
if nesting {
set_var("TMUX", tmux_var.unwrap());
}
} }

View file

@ -1,103 +0,0 @@
use std::{
env,
path::PathBuf
};
use pico_args::Arguments;
use crate::{
env::{ env_var, REPO_FILE, TMUX },
error,
flag::Flags,
util::{ find, message, MSG_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();
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 {
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); }
}
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() }
}
}
pub struct Repository {
pub path: PathBuf,
pub name: String
}
impl Repository {
pub fn find() -> Option<Repository> {
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 {
path,
name
};
Some(inner)
} else { None }
}
}

View file

@ -1,36 +1,20 @@
use std::{ use std::{
io::{ stdout, IsTerminal }, env::current_dir,
path::PathBuf path::PathBuf,
process::exit
}; };
use tmux_interface::{ use tmux_interface::{
Session, StdIO, Tmux, Session, Tmux,
commands, commands,
variables::session::SessionsCtl variables::session::SessionsCtl
}; };
use crate::error; use crate::{
env,
pub const NULL: Option<StdIO> = Some(StdIO::Null); error
};
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 {
if title.len() > 0 { Some(title[0..title.len() - 1].to_owned()) }
else { None }
} 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>> {
@ -40,28 +24,42 @@ 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!("Sessions should be nested with care; unset TMUX or use the '-n' flag to allow.");
exit(6);
}
}
/// 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()
.target_session(target.into()); .target_session(target.into());
Tmux::new().add_command(has_session) Tmux::new().add_command(has_session)
.stderr(NULL) .disable_echo()
.status() .status()
.unwrap() .unwrap()
.success() .success()
} }
/// enforce a command is being run in a terminal /// attempt to return the repo name or exit
pub fn terminal_enforce() { pub fn repo_fallback() -> String {
if !stdout().is_terminal() { error::not_terminal(); } let repo = repo_root(current_dir().unwrap());
if repo.is_none() { error::missing_target(); }
let target = repo.unwrap().file_name().unwrap().to_string_lossy().to_string();
target
} }
/// recursively propagate up directories to find a child /// recursively attempt to find a git root directory
pub fn find(target: &str, path: PathBuf) -> Option<PathBuf> { pub fn repo_root(path: PathBuf) -> Option<PathBuf> {
if path.join(target).exists() { return Some(path); } // 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 { return find(target, parent.to_path_buf()) } if let Some(parent) = parent { repo_root(parent.to_path_buf()) }
else { None } else { None }
} }