AntonGepting / tmux-interface-rs

Rust language library for communication with TMUX via CLI
MIT License
51 stars 5 forks source link

Running multiple commands in one invocation #7

Closed ypoluektovich closed 1 year ago

ypoluektovich commented 3 years ago

I need to run tmux show-options -g -v default-shell. There's a catch, however: it doesn't work if a tmux server hasn't already been started. In command line, you solve that by running tmux start-server \; show-options -g -v default-shell.

In tmux_interface there can be (at least) two — well, three approaches to accomplishing that.

The first one is to manually assemble the command and then also manually call SessionOptions.from_str to parse the output, but that's boring :)

The second approach would be to add some kind of "append command" mechanism to TmuxCommand. To preserve the ability to parse output (of the last command) (which would be an optional feature — the most basic thing to do would probably be just a way to assemble a sequence of commands into an std::process::Command), I'd probably also add an analogue of From<TmuxCommand> implementations for (all? some?) command structs so that they preserve cmd/cmd_args of previously added commands. It looks like a lot of work though, and it'll probably require modifying the internals of TmuxCommand from what I can see.

The third approach is to add a special "run in a new server" flag to (all? some?) commands that would prepend "start-server", ";" to the command. Not sure if that's the direction you want to develop the API in.

What do you think?

ypoluektovich commented 3 years ago

Here's what I'm doing right now with 0.2.0:

pub fn get_default_shell() -> Result<Option<String>> {
    let option_name = SESSION_OPTIONS.iter().find_map(|t| if t.3 == DEFAULT_SHELL { Some(t.0) } else { None }).unwrap();
    let mut command = ShowOptions::new();
    command.global().option(option_name);
    // dirty hack
    command.0.bin_args = Some(vec![START_SERVER.into(), ";".into()]);
    let output: SessionOptions = command
        .output()
        .wrap_err("failed to run tmux to get default shell")?
        .to_string()
        .parse()
        .wrap_err("failed to parse show-options output")?;
    Ok(output.default_shell)
}
AntonGepting commented 3 years ago

Before I explain my thoughts about architecture of the library.

I’m not sure, is this suitable for your purposes, but still. Examples below are something I would write if I had to get options using current version of the lib (excluding any error processing, just as an example):

  1. Variant, direct access to tmux command show-options

  2. Variant, getting all available server/session/window/pane options (ServerOptions::, SessionOptions::, WindowOptions::, PaneOptions::get_all()) returns the structure, with all available fields filled with information

  3. Variant, selectively getting variables (ServerOptions::, SessionOptions::, WindowOptions::, PaneOptions::get(VARIABLE1_BITFLAG | VARIABLE2_BITFLAG)) returns the structure, with only given fields filled with information, others stay None

use tmux_interface::{SessionOptions, TmuxCommand, DEFAULT_SHELL};
use std::io::Result;

// 1. Variant
pub fn get_default_shell1() -> Result<Option<String>> {
    let tmux = TmuxCommand::new();

    tmux.start_server().output().unwrap();

    // 1. Variant
    let output = tmux
        .show_options()
        .global()
        .value()
        .option("default-shell")
        .output()
        .unwrap()
        .to_string();

    Ok(Some(output))
}

// 2. Variant
pub fn get_default_shell2() -> Result<Option<String>> {
    let tmux = TmuxCommand::new();

    tmux.start_server().output().unwrap();

    let output = SessionOptions::get_all().unwrap();
    Ok(output.default_shell)
}

// 3. Variant
pub fn get_default_shell3() -> Result<Option<String>> {
    let tmux = TmuxCommand::new();

    tmux.start_server().output().unwrap();

    let output = SessionOptions::get(DEFAULT_SHELL).unwrap();
    Ok(output.default_shell)
}

fn main() {
    let default_shell = get_default_shell1();
    dbg!(default_shell);

    let default_shell = get_default_shell2();
    dbg!(default_shell);

    let default_shell = get_default_shell3();
    dbg!(default_shell);
}

But the existence of multiple servers is not handled, not taken into account, just saw it now.

ypoluektovich commented 3 years ago

You can't execute start-server separately from show-options, because (unless you specify some more options) the server will be immediately stopped, as it doesn't have any sessions attached to it (see the documentation for start-server command in man tmux). And I don't want to keep the new server running after I get the option value, because there's a chance I won't need it after all.

Of the variants you propose, I'd love to use the third one, but I can't hack start-server into the command invocation with it, because both the creation and execution of the command happens inside output().

AntonGepting commented 3 years ago

Now I've got your point. Sorry, was thinking about shell related command execution with that semicolon.

tmux manual:

PARSING SYNTAX ... Each command is terminated by a newline or a semicolon (;). Commands separated by semicolons together form a ‘command sequence’

  • if a command in the sequence encounters an error, no subsequent commands are executed. ...

Give me some time to think about possible implementation of this feature.

AntonGepting commented 3 years ago

First of all thank you for all your suggestions. I wasn't even familiar with this great feature of tmux. Talking about the ways to solve it, you proposed:

The first one is to manually assemble the command ... but that's boring :)

Agree, why does some user need to use the library at all then.

The second approach would be to add some kind of "append command" mechanism to TmuxCommand

Best approach of proposed in my opinion.

The third approach is to add a special "run in a new server" flag

Only as an ad-hoc and specific solution of only this problem, but it's not so elegant, because it doesn’t allow to take the full advantage of this tmux feature.

Great thing would be to implement some universal method:

What I am thinking of is some kind of an array to hold all the commands. Array members can be joined and the resulting command line (command array) can be passed through to std::process::Command.

Something like (simplified pseudo code):

let tmux = TmuxCommand::new();
let start_server = tmux.start_server();
let get_options = show_options().global().value().option("default-shell");

let cmds: TmuxCommands(Vec<TmuxCommand>) = TmuxCommands::new();
let output = cmds.push(start_server.0)
.push(get_options.0)
.output()
.unwrap();

Or using method as you described like .append() (simplified pseudo code):

let cmds = TmuxCommands::new();
let output = cmds
.start_server().append()
.show_options().global().value().option("default-shell")
.output().unwrap();

From current point of view I would say these concepts could be good solutions, but of course few related side problems must be analyzed and solved. It takes time to check it and implement some proof of concept.

AntonGepting commented 3 years ago

In addition some drafts of graphical representation, of my mindset about architecture of the library. They are incomplete, may have inaccuracies etc, not everything matches the current state of the code base, but anyway probably they can give some additional information about the whole vision and illustrate it.

Abstraction Levels: 1

Tmux Modes: 2

Structures and functions call trace: 3

ypoluektovich commented 3 years ago

The way I envisioned it, we have: 1) the Command as a backend; 2) TmuxCommand to represent a single invocation of tmux binary, providing methods to set global flags for the invocation, to add "raw" commands (by just specifying sequences of Cow<OsStr> or something like that), to create command builders (like ShowOptions) attached to this command, and to wrap the (last sub-)command in a high-level interface (like SessionOptions); 3) the command builders, for example, taking &mut TmuxCommand and manipulating it; 4) the high-level interfaces that provide native idiomatic access to the commands and their output to Rust code.

let mut cmd = TmuxCommand::new(); // command: tmux

cmd.start_server() // -> StartServer; invocation: tmux start-server
    // here you can interact with the StartServer instance
    .end_command(); // -> &mut TmuxCommand; invocation: tmux start-server \;
// ^ alternatively: cmd.append_full_command([START_SERVER])
// ^ alternatively: StartServer::append_to(&mut cmd).end_command();

let shell: Option<String> = SessionOptions::get(cmd) // fn get(_: TmuxCommand) -> GetSessionOptions
    .global()
    .single(DEFAULT_SHELL); // invokes the command

// alternatively:
let shell: Option<String> = SessionOptions::get(cmd) // fn get(_: TmuxCommand) -> GetSessionOptions
    .global()
    .all() // invokes the command
    .get(DEFAULT_SHELL);

This roughly covers things up to Level 3 in your terms; I'm not quite sure how Level 4 is supposed to work.

AntonGepting commented 1 year ago

new mechanisms proposed in v0.3.0

thank you for the suggestion

multiple commands can be chained and executed in one call of tmux binary executable

Exmaple

    let output = Tmux::new()
        .add_command(StartServer::new())
        .add_command(ShowOptions::new().value().option("default-terminal"))
        .output();
    dbg!(output);

I hope this improvement is still relevant and it can help you solve your problems in convenient way. I apologize very much for the very long waiting time for implementing this.

ypoluektovich commented 1 year ago

Thanks! My dirty hack has been serving me well, but I'll look into upgrading to v0.3 as time permits :)

ypoluektovich commented 1 year ago

Reporting back: I migrated my code to 0.3 and it's great. I think we can consider this particular issue solved.

AntonGepting commented 1 year ago

thx for your feedback, alright i'm closing this as solved. Please feel free to reopen or create a new one in case of newly discovered problems.