Compare commits

...

17 commits
state ... main

Author SHA1 Message Date
8449ae00d6 error functions now return a never type 2024-07-24 20:59:48 -04:00
ff30bc1052 minor 'list' optimization 2024-07-22 10:18:35 -04:00
8e2826b110 message helper now handles empty responses more gracefully 2024-07-22 10:18:10 -04:00
0fe3906578 'switch' command now supports detach flag, and updated manual documentation for 'switch' 2024-07-22 09:26:33 -04:00
fdf3114c04 'list' command now shows a symbol for the previous session 2024-07-17 11:30:50 -04:00
8ad16ad825 'switch' now handles empty case correctly 2024-07-17 11:21:05 -04:00
a9a73314af 'switch' command now defaults to previous session if present 2024-07-17 11:07:14 -04:00
af33e82415 renamed bash completion folder to be more appropriate 2024-07-17 09:24:31 -04:00
eaf72847b1 updated README 2024-07-17 09:23:34 -04:00
beb880ed43 Merge branch 'repo-var' into completion 2024-07-17 09:17:35 -04:00
449c460bbb repository search now tries to match the filename set by the REMUX_REPO_NAME environment variable 2024-07-17 09:15:11 -04:00
1b51633d4f added search and quiet mode to 'list' command and wrote a completion function 2024-07-16 11:46:56 -04:00
b7b893d55c errors now print to stderr instead of stdout 2024-07-12 10:48:03 -04:00
cdc68986fa inner tmux errors are now hidden 2024-07-06 21:10:39 -04:00
1acc7aeb7b updated README 2024-07-06 20:48:45 -04:00
449921656c added man instructions to README 2024-07-06 18:33:28 -04:00
638fce2331 minor man correction 2024-07-06 17:01:20 -04:00
11 changed files with 171 additions and 89 deletions

View file

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

View file

@ -92,6 +92,19 @@ 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>
### 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
The pretty-print attached symbol (default: `*`) can be set manually by setting `REMUX_ATTACH_SYMBOL`.

19
bash-completion/remux Normal file
View file

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

View file

@ -8,7 +8,7 @@
.Sh SYNOPSIS
.Nm remux
.Op Fl dhnqrtv
.Op command
.Op Ar command
.Op args...
.Sh DESCRIPTION
.Nm
@ -101,8 +101,8 @@ aliases: p
Prints the session path.
.Ed
.It Xo Ic switch
.Op Fl r , Fl -read-only
.Ar title
.Op Fl rd
.Op Ar title
.Xc
.Bd -literal -compact
aliases: s
@ -110,10 +110,12 @@ Switches from the current session to the target.
.Ed
.Pp
.Bl -tag -width Ds -compact
.It Fl d , Fl -detach
Detaches other clients from the target session.
.It Fl r , Fl -read-only
Switch to the target session in read-only mode.
.It Ar title
The title of the session to switch to.
The title of the session to switch to. If blank, the previous session will be used.
.El
.It Ic title
.Bd -literal -compact
@ -135,6 +137,14 @@ Default: '>'
.It Ev REMUX_NEW_WINDOW
Provides a default windows name when creating a new session. Unused if empty.
Default: (unset)
.It Ev REMUX_PREVIOUS_SYMBOL
Changes the symbol displayed for the previous session in the
.Ic list
command.
Default: '-'
.It Ev REMUX_REPO_FILE
The filename to match on when trying to find the root of a repository.
Default: '.git'
.El
.Sh EXIT STATUS
.Bl -tag -Width Ds

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -6,10 +6,10 @@ use std::{
use pico_args::Arguments;
use crate::{
env::TMUX,
env::{ env_var, REPO_FILE, TMUX },
error,
flag::Flags,
util::{ find, session_name }
util::{ find, message, MSG_SESSION_NAME }
};
pub struct State<'a> {
@ -29,7 +29,7 @@ impl State<'_> {
let flags = Flags::from(args);
let tmux_var = env::var(TMUX).ok();
let session = tmux_var.is_some();
let title = if session { session_name() } else { None };
let title = if session { message(MSG_SESSION_NAME) } else { None };
let repository = Repository::find();
State {
@ -67,10 +67,7 @@ impl State<'_> {
let from_args = self.target();
if from_args.is_some() { return from_args; }
else if let Some(repository) = &self.repository { Some(repository.name.clone()) }
else {
error::missing_target();
None
}
else { error::missing_target() }
}
}
@ -82,7 +79,7 @@ pub struct Repository {
impl Repository {
pub fn find() -> Option<Repository> {
let path = find(".git", env::current_dir().unwrap());
let path = find(&env_var(REPO_FILE), env::current_dir().unwrap());
if let Some(path) = path {
let name = path.file_name().unwrap().to_string_lossy().to_string();
let inner = Repository {

View file

@ -4,7 +4,7 @@ use std::{
};
use tmux_interface::{
Session, Tmux,
Session, StdIO, Tmux,
commands,
variables::session::SessionsCtl
@ -12,14 +12,22 @@ use tmux_interface::{
use crate::error;
pub fn session_name() -> Option<String> {
let message = commands::DisplayMessage::new().print().message("#{session_name}");
pub const NULL: Option<StdIO> = Some(StdIO::Null);
pub const MSG_PREVIOUS: &str = "#{client_last_session}";
pub const MSG_SESSION_NAME: &str = "#S";
pub const MSG_SESSION_PATH: &str = "#{session_path}";
pub const MSG_WINDOW_NAME: &str = "#{window_name}";
pub fn message(fstr: &str) -> Option<String> {
let message = commands::DisplayMessage::new().print().message(fstr);
let result = Tmux::new().add_command(message).output();
if let Ok(output) = result {
let text = String::from_utf8(output.0.stdout);
if let Ok(title) = text {
Some(title[0..title.len() - 1].to_owned())
if title.len() > 0 { Some(title[0..title.len() - 1].to_owned()) }
else { None }
} else { None }
} else { None }
}
@ -37,6 +45,7 @@ pub fn session_exists<S: Into<String>>(target: S) -> bool {
let has_session = commands::HasSession::new()
.target_session(target.into());
Tmux::new().add_command(has_session)
.stderr(NULL)
.status()
.unwrap()
.success()
@ -48,7 +57,7 @@ pub fn terminal_enforce() {
}
/// recursively propagate up directories to find a child
pub fn find(target: &'static str, path: PathBuf) -> Option<PathBuf> {
pub fn find(target: &str, path: PathBuf) -> Option<PathBuf> {
if path.join(target).exists() { return Some(path); }
let parent = path.parent();