Compare commits

..

29 commits

Author SHA1 Message Date
222f929fd4 merged in changes from 'switch' branch 2024-06-17 13:54:05 -04:00
0fc6cd66c6 renamed 'root' command to 'path' 2024-06-10 12:03:40 -04:00
1320b6f122 'h' is now shortened version of 'has' instead of help 2024-06-10 12:02:57 -04:00
26f6fd5dd0 made nest prevent message more helpful 2024-06-10 12:02:57 -04:00
19b12c682d updated README 2024-06-10 12:02:57 -04:00
28a7fc44f9 switched session enforce to new tmux detection helper 2024-06-10 12:02:57 -04:00
21eda33624 default window name now sets correctly 2024-06-10 12:02:57 -04:00
d776129b3e added help text for REMUX_NEW_WINDOW env var 2024-06-10 12:02:57 -04:00
62d72735da added REMUX_WINDOW_NAME var for add command 2024-06-10 12:02:57 -04:00
681e7427ba updated help text to include topics 2024-06-10 12:02:57 -04:00
d8aaf7feed corrected name of ATTACH_SYMBOL env var 2024-06-10 12:02:57 -04:00
b3403ffa36 wrote help topic for env vars 2024-06-10 12:02:57 -04:00
a07ae193b5 moved environment variable code to a new module 2024-06-10 12:02:02 -04:00
c8800a7f53 created env module 2024-06-10 12:01:15 -04:00
92d0010186 renamed detach flag and added detach support to 'new' 2024-06-10 12:01:15 -04:00
21f7b72298 removed incompatible disable_echo from session exist check 2024-06-10 12:01:15 -04:00
2e28acc3c3 version bump 2024-06-10 12:01:15 -04:00
e017d57a53 initial implementation of root command and made some commands allow being thrown through pipes 2024-06-10 12:01:15 -04:00
6b7c84b673 merged in changes from dep-upgrade 2024-03-08 13:53:26 -05:00
782fb694d0 removed disable_echo flag from commands that don't support it 2024-03-08 13:50:27 -05:00
882e130121 fixed tmux dispatch echoing command information 2024-03-08 10:26:00 -05:00
4615777cae added help topic for switch 2024-03-08 10:23:02 -05:00
b00e15a037 switch command now must run in-session 2024-03-08 10:04:24 -05:00
5c3fb7df3f created switch command 2024-03-07 17:10:45 -05:00
41a64f039f revised incorrect help text for 'new' and 'attach' commands 2024-03-07 16:54:16 -05:00
be593c82e7 list attach symbol is now configurable 2024-03-06 16:12:54 -05:00
9c814c43f4 updated README 2024-03-06 16:05:53 -05:00
63335916d9 updated README 2024-03-06 15:59:40 -05:00
aba802e77d upgraded tmux_interface to 0.3.2 and restructured command module 2024-03-06 15:54:41 -05:00
12 changed files with 412 additions and 221 deletions

View file

@ -1,6 +1,6 @@
[package]
name = "remux"
version = "0.2.1"
version = "0.3.1"
edition = "2021"
authors = [ "Valerie Wolfe <sleeplessval@gmail.com>" ]
description = "A friendly command shortener for tmux"
@ -23,7 +23,7 @@ path = "src/main.rs"
[dependencies]
pico-args = { version = "0.5.0", features = [ "combined-flags", "eq-separator" ] }
termion = "2.0.1"
tmux_interface = "0.2.1"
tmux_interface = "0.3.2"
[profile.release]
opt-level = 's'

View file

@ -7,16 +7,20 @@ A tmux wrapper and command shortener written in Rust. ReMux's
goal is to wrap tmux commands to be both shorter, and oriented
around session names instead of session IDs.
To further simplify developer usage, the `attach`, `detach`, `has`, and `new`
commands can be used without a target field, and will default to the name of
the Git repository root directory, if one is found.
In their shortest forms, *every* ReMux command is as short or
shorter than its equivalent tmux command:
```sh
# new session
tmux new-session -t foo
tmux new-s -t foo
remux n foo
# lists
# list sessions
tmux ls
remux l
remux
@ -26,11 +30,11 @@ tmux a -t foo
remux a foo
# has
tmux has -t foo
remux has foo
tmux h -t foo
remux h foo
# detach
tmux detach-client -t foo
tmux det -t foo
remux d foo
# nesting sessions with '-n' flag
@ -39,11 +43,16 @@ remux a -n foo
TMUX='' tmux new-session -t foo
remux n -n foo
```
# switch to another session
tmux swi -t foo
rmux s foo
If you're working in a git repository, the `attach`, `has`, and `new` commands
can be used without a session title, and the repository directory name will be
used instead.
# cd to session path
tmux run 'printf "#{session_path}" > /tmp/tmux_path'
cd `cat /tmp/tmux_path`
cd `rmux p`
```
## Dependencies
@ -85,6 +94,10 @@ using an AUR package manager such as <a href="https://github.com/Morganamilo/par
Install the package using Cargo with the command <code>cargo install tmux-remux</code>.
</details>
## Configuration
The pretty-print attached symbol (default: `*`) can be set manually by setting `REMUX_ATTACH_SYMBOL`.
## Libraries
- [pico-args](https://crates.io/crates/pico_args) — argument parsing

View file

@ -1,170 +0,0 @@
use std::{
env::current_dir,
ffi::OsString,
process::exit
};
use pico_args::Arguments;
use termion::{ color, style };
use tmux_interface::TmuxCommand;
use crate::error;
use crate::util;
pub fn attach(pargs: &mut Arguments) {
util::prevent_nest();
// get optional flags
let read_only = pargs.contains(["-r", "--readonly"]);
let detach_other = pargs.contains(["-d", "--detach"]);
// collect target and window arguments
let args = pargs.clone().finish();
let target: String;
let window: Option<&OsString>;
if args.len() < 1 {
// missing name will attempt to fall back to repository
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);
}
// focus window if provided
if window.is_some() {
let target = window.unwrap().to_string_lossy();
TmuxCommand::new()
.select_window()
.target_window(target)
.output().ok();
}
// make sure the target session exists
let exists = util::session_exists(target.clone());
if !exists { error::no_target(target.clone()); }
// build command
let mut attach = TmuxCommand::new().attach_session();
// handle optional flags
if read_only { attach.read_only(); }
if detach_other { attach.detach_other(); }
// run command
attach
.target_session(target)
.output().ok();
}
pub fn detach(pargs: &mut Arguments) {
// get target and error out if not provided
let args = pargs.clone().finish();
if args.len() < 1 { error::missing_target(); }
let target = args.get(0).unwrap().to_string_lossy();
// make sure the target session exists
let exists = util::session_exists(target.clone());
if !exists { error::no_target(target.clone()); }
// build command and run
TmuxCommand::new()
.detach_client()
.target_session(target)
.output().ok();
}
pub fn has(pargs: &mut Arguments) {
// get optional flag
let quiet = pargs.contains(["-q", "--quiet"]);
// collect target argument
let args = pargs.clone().finish();
let target: String;
if args.len() < 1 {
// missing name will attempt to fall back to repository
target = util::repo_fallback();
} else {
target = args.get(0).unwrap().to_string_lossy().to_string();
}
// run command
let success = util::session_exists(target.clone());
// handle optional flag
// inverted; print text if NOT quiet
if !quiet { println!("session \"{target}\" {}.", if success { "exists" } else { "does not exist" }); }
// exit codes for scripts to use
exit( if success { 0 } else { 1 });
}
pub fn list() {
// get session list
let sessions = util::get_sessions().unwrap_or(Vec::new());
// handle empty case
if sessions.len() == 0 {
println!("no sessions");
return;
}
// iterate over pretty print
println!("sessions:");
for session in sessions.into_iter() {
let group = session.group.unwrap_or("[untitled]".to_string());
let id = session.id.unwrap();
let attached = session.attached.unwrap_or(0) > 0;
println!(
" {group} ({bold}{blue}{id}{reset}) {bold}{green}{attach_sym}{reset}",
// values
attach_sym = if attached { "󰌹" } else {""},
// formatting
bold = style::Bold,
blue = color::Fg(color::Blue),
green = color::Fg(color::LightGreen),
reset = style::Reset
);
}
}
pub fn new(pargs: &mut Arguments) {
use pico_args::Error;
// show nest message
util::prevent_nest();
// get optional flag
let target_dir: Result<String, Error> = pargs.value_from_str(["-t", "--target"]);
// get target and error out if not provided
let args = pargs.clone().finish();
// collect name and command arguments
let title: String;
let command: Option<&OsString>;
if args.len() < 1 {
// missing name will attempt to fall back to repository
title = util::repo_fallback();
command = None;
} else {
title = args.get(0).unwrap().to_string_lossy().to_string();
command = args.get(1);
}
// build command
let mut new = TmuxCommand::new().new_session();
// handle shell command argument
if command.is_some() { new.shell_command(command.unwrap().to_string_lossy()); }
// run command
new
.session_name(title)
.attach()
.start_directory(target_dir.unwrap_or(current_dir().unwrap().to_string_lossy().to_string()))
.output().ok();
}

4
src/command/mod.rs Normal file
View file

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

52
src/command/session.rs Normal file
View file

@ -0,0 +1,52 @@
//! commands accessible from within a session
use std::fs::read_to_string;
use pico_args::Arguments;
use tmux_interface::{
Tmux,
commands
};
use crate::{ error, flag, util };
const TMP_ROOT: &str = "/tmp/remux_path";
pub fn switch(pargs: &mut Arguments) {
util::terminal_enforce();
// refuse to run outside a session
util::session_enforce("switch");
// consume optional flags
let read_only = pargs.contains(flag::READ_ONLY);
//TODO: -d flag handling needs to be done manually
let args = pargs.clone().finish();
if args.len() < 1 { error::missing_target(); }
let target = args.get(0).unwrap().to_string_lossy().to_string();
let exists = util::session_exists(target.clone());
if !exists { error::no_target(target.clone()); }
let mut switch = commands::SwitchClient::new();
switch = switch.target_session(target);
if read_only { switch.read_only = true; }
Tmux::new()
.add_command(switch)
.output().ok();
}
pub fn path() {
util::session_enforce("path");
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();
}
}

193
src/command/share.rs Normal file
View file

@ -0,0 +1,193 @@
//! globally available tmux commands.
use std::{
ffi::OsString,
process::exit
};
use pico_args::{ Arguments, Error };
use termion::{ color, style };
use tmux_interface::{
Tmux,
commands
};
use crate::{
env::{ self, env_var },
error,
flag,
util
};
pub fn attach(pargs: &mut Arguments) {
// must be run from terminal
util::terminal_enforce();
// don't allow unflagged nests
util::prevent_nest();
// consume optional flags
let read_only = pargs.contains(flag::READ_ONLY);
let detach_other = pargs.contains(flag::DETACH);
let args = pargs.clone().finish();
let target: String;
let window: Option<&OsString>;
if args.len() < 1 {
// missing name will attempt to fall back to repository
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
let exists = util::session_exists(target.clone());
if !exists { error::no_target(target.clone()); }
// build attach command
let mut attach = commands::AttachSession::new();
attach = attach.target_session(target);
if read_only { attach.read_only = true; }
if detach_other { attach.detach_other = true; }
let select_window: Option<commands::SelectWindow>;
if let Some(window) = window {
let mut command = commands::SelectWindow::new();
command.target_window = Some(window.to_string_lossy());
select_window = Some(command);
} else { select_window = None; }
// build dispatch
let mut tmux = Tmux::new().add_command(attach);
if let Some(select_window) = select_window { tmux = tmux.add_command(select_window); }
tmux.output().ok();
}
pub fn detach(pargs: &mut Arguments) {
util::terminal_enforce();
// get target or fallback
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();
}
// make sure the session exists
let exists = util::session_exists(target.clone());
if !exists { error::no_target(target.clone()); }
// build and dispatch
let detach = commands::DetachClient::new()
.target_session(target);
Tmux::new()
.add_command(detach)
.disable_echo().output().ok();
}
pub fn has(pargs: &mut Arguments) {
// consume optional flags
let quiet = pargs.contains(flag::QUIET);
// get target or fallback
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
let success = util::session_exists(target.clone());
// print if not quiet
if !quiet {
println!("session \"{target}\" {}.",
if success { "exists" }
else { "does not exist" }
);
}
// emit exit code
exit( if success { 0 } else { 1 });
}
pub fn list() {
// get session list
let sessions = util::get_sessions().unwrap_or(Vec::new());
// handle empty case
if sessions.len() == 0 {
println!("no sessions");
return;
}
// get attached session symbol
let attach_symbol = env_var(env::ATTACH_SYMBOL);
// pretty print session list
println!("sessions:");
for session in sessions.into_iter() {
let group = session.group.unwrap_or("[untitled]".to_string());
let id = session.id.unwrap();
let attached = session.attached.unwrap_or(0) > 0;
println!(
" {group} ({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
);
}
}
pub fn new(pargs: &mut Arguments) {
util::terminal_enforce();
// don't allow unflagged nesting
util::prevent_nest();
// get optional flags
let detached = pargs.contains(flag::DETACH);
let target_dir: Result<String, Error> = pargs.value_from_str(flag::TARGET);
// get environment variables
let window_name = env_var(env::NEW_WINDOW_NAME);
// get target or fallback
let args = pargs.clone().finish();
let title: String;
let command: Option<&OsString>;
if args.len() < 1 {
// attempt repo fallback
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();
new = new.group_name(title);
if let Some(command) = command { new.shell_command = Some(command.to_string_lossy()); }
if detached { new.detached = true; }
if let Ok(target_dir) = target_dir { new = new.start_directory(target_dir); }
let mut tmux = Tmux::new().add_command(new);
// rename window if var not empty
if !window_name.is_empty() {
let auto_name = commands::RenameWindow::new()
.new_name(window_name);
tmux = tmux.add_command(auto_name);
}
tmux.output().ok();
}

13
src/env.rs Normal file
View file

@ -0,0 +1,13 @@
use std::env::var;
pub type EnvVar = (&'static str, &'static str);
pub static ATTACH_SYMBOL: EnvVar = ("REMUX_ATTACH_SYMBOL", "*");
pub static NEW_WINDOW_NAME: EnvVar = ("REMUX_NEW_WINDOW", "");
pub fn env_var(envvar: EnvVar) -> String {
var(envvar.0).unwrap_or(envvar.1.to_string())
}
pub fn tmux() -> bool { !var("TMUX").unwrap_or("".to_string()).is_empty() }

View file

@ -37,3 +37,9 @@ pub fn not_nesting() {
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");
exit(7);
}

11
src/flag.rs Normal file
View file

@ -0,0 +1,11 @@
type Flag = [&'static str;2];
pub static DETACH: Flag = ["-d", "--detach"];
pub static HELP: Flag = ["-h", "--help"];
pub static NEST: Flag = ["-n", "--nest"];
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"];

View file

@ -16,16 +16,25 @@ A command wrapper for tmux written in Rust.
usage: remux <command> [<args>]
commands:
help Show help text for remux or a specific command
help Show help text for remux, a command, or a help topic.
attach Attach to an existing tmux session
detach Detach clients from a tmux session
has Check if a tmux session exists
list Pretty-print all tmux sessions
new Create a new tmux session
Use 'remux help <command>' to see detailed help text for each command."),
path print session path (session)
switch switch to another session (session)
Some("a" | "attach")
Use 'remux help <command>' to see detailed help text for each command.
help topics:
env Environment variables"),
// COMMAND HELP
Some("a" | "attach")
=>
println!("remux attach
Attach to an existing session.
@ -40,7 +49,7 @@ args:
flags:
-d, --detach Detach other attached clients from the session
-n, --nest Attach the session inside another session.
-r, --readonly Attach the session as read-only"),
-r, --read-only Attach the session as read-only"),
Some("d" | "detach")
=>
@ -53,12 +62,13 @@ usage: remux detach <session>
args:
<session> The session name to detach clients from"),
Some("has")
Some("h" | "has")
=>
println!("remux has
Check if the target session exists.
usage: remux has [flags] <session>
rmux h [flags] session
args:
<session> The session to check for
@ -81,6 +91,7 @@ println!("remux new
Create a new tmux session.
usage: remux new [flags] <title> [command]
remux n [flags] <title> [command]
args:
<title> The title of the new session
@ -90,7 +101,48 @@ flags:
-n, --nest Create the session inside another session.
-t, --target <dir> Sets the target directory for the new session."),
// not found
Some("root")
=>
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."),
// 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_NEW_WINDOW
Provides a default window name when creating a session with
the 'new' command, if not empty.
Default: ''"),
// not found
_ => error::no_help(topic.unwrap())
}
}

View file

@ -1,12 +1,11 @@
use std::{
env::{ set_var, var },
io::{ stdout, IsTerminal }
};
use std::env::{ set_var, var };
use pico_args::Arguments;
mod command;
mod env;
mod error;
mod flag;
mod help;
mod util;
@ -19,17 +18,17 @@ fn main() {
let mut args = Arguments::from_env();
// consume flags
if args.contains(["-h", "--help"]) {
if args.contains(flag::HELP) {
help(&mut args);
return;
}
if args.contains(["-v", "--version"]) {
if args.contains(flag::VERSION) {
version();
return;
}
let nesting = args.contains(["-n", "--nest"]);
let nesting = args.contains(flag::NEST);
let tmux_var = var("TMUX").ok();
if nesting {
if tmux_var.is_none() {
@ -38,30 +37,34 @@ fn main() {
set_var("TMUX", "");
}
if !stdout().is_terminal() { error::not_terminal(); }
let subcommand = args.subcommand().unwrap();
// invoke subcommand function
match subcommand.as_deref() {
Some("h" | "help")
Some("help")
=> help(&mut args),
Some("a" | "attach")
=> command::attach(&mut args),
=> command::share::attach(&mut args),
Some("d" | "detach")
=> command::detach(&mut args),
=> command::share::detach(&mut args),
Some("has")
=> command::has(&mut args),
Some("h" | "has")
=> command::share::has(&mut args),
None |
Some("l" | "ls" | "list")
=> command::list(),
=> command::share::list(),
Some("n" | "new")
=> command::new(&mut args),
=> command::share::new(&mut args),
Some("p" | "path")
=> command::session::path(),
Some("s" | "switch")
=> command::session::switch(&mut args),
_
=> error::no_subcommand(subcommand.unwrap())

View file

@ -1,44 +1,58 @@
use std::{
env::{ current_dir, var },
env::current_dir,
io::{ stdout, IsTerminal },
path::PathBuf,
process::exit
};
use tmux_interface::{
Session, Sessions, TmuxCommand,
variables::session::session::SESSION_ALL
Session, Tmux,
commands,
variables::session::SessionsCtl
};
use crate::error;
use crate::{
env,
error
};
/// return a Vec of all sessions or None
pub fn get_sessions() -> Option<Vec<Session>> {
let i_sessions = Sessions::get(SESSION_ALL);
if i_sessions.is_err() { return None; }
let sessions = i_sessions.ok();
if sessions.is_none() { return None; }
Some(sessions.unwrap().0)
let sessions = SessionsCtl::new().get_all();
if let Ok(sessions) = sessions {
return Some(sessions.0);
} else { return None; }
}
/// show the tmux nest text if env var is not unset
pub fn prevent_nest() {
let tmux = var("TMUX").ok();
if tmux.is_some() && tmux.unwrap() != "" {
println!("Sessions should be nested with care; unset TMUX or use the '-n' flag to allow.");
exit(1);
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
pub fn session_exists<S: Into<String>>(target: S) -> bool {
TmuxCommand::new()
.has_session()
.target_session(target.into())
.output().unwrap()
let has_session = commands::HasSession::new()
.target_session(target.into());
Tmux::new().add_command(has_session)
.status()
.unwrap()
.success()
}
/// enforce a command is being run in a terminal
pub fn terminal_enforce() {
if !stdout().is_terminal() { error::not_terminal(); }
}
/// attempt to return the repo name or exit
pub fn repo_fallback() -> String {
let repo = repo_root(current_dir().unwrap());