broot icon indicating copy to clipboard operation
broot copied to clipboard

[Feature proposal] Single instance

Open SRGOM opened this issue 5 years ago • 15 comments

Context (not important): https://old.reddit.com/r/rust/comments/gasc1f/i_wrote_a_file_manager_that_syncs_its_current/fp2e0bq/?context=3

Hi, is there a way to achieve a "single instance" of broot open? Say i say broot --go <dir> then it just opens that dir in the currently opened instance?

SRGOM avatar Apr 30 '20 16:04 SRGOM

This could be easily developed, at least on unix, but it's not currently a feature of broot (you can change this question into a request if you want).

Canop avatar Apr 30 '20 16:04 Canop

No worries, thank you.

SRGOM avatar Apr 30 '20 16:04 SRGOM

Hello, I took a bit of a stab at trying, as a proof of concept, to do what I was proposing. I've managed to get the following code working as a PoC standalone program. If you agree with the following way of implementing what I was proposing, you think you could accept a "drive-by" PR?, where you could help me by roughly pointing out the places where I would place this code and I would add it, basically trying to save me time understanding the codebase, which unfortunately I cannot afford to. (I know I'm asking you to read my code but this is really small).

By the way, while I've only tested on Linux, this code seems cross-platform to me.

I would defer to you on the exact command names but I had 3 in mind (probably with a -- in front)

  1. navigate would take the "server" instance to that directory.
  2. get-cwd - the client program gets from server and prints the current dir on stdout
  3. get-selected - the client program gets from server and prints on stdout the value of current selection.
use std::net::{
	TcpStream,
	TcpListener,
};

use std::fs::File;
use std::io::prelude::*;

enum Connection
{
	StandAlone,
	Listener( std::net::TcpListener ),
	Sender( std::net::TcpStream ),
}

fn get_file_name_from_server<S: AsRef<str>>( server: S ) -> String { 
	String::from( "/tmp/broot-server-" ) + server.as_ref()
}

fn no_server_file_or_port_error<S: AsRef<str>>( e: S, server: S ) -> String
{
	String::from( 
		"Could not connect to existing server. " 
	) 
		+ e.as_ref()
		+ " Try deleting " 
		+ & get_file_name_from_server( server.as_ref() ) 
	
}

fn bind_to_unused_port() -> ( TcpListener, u16 )
{
	for attemped_port in 10001 .. std::u16::MAX  
	{
		if let Ok( listener ) = TcpListener::bind( ( "0.0.0.0", attemped_port ) ) 
		{
			return ( listener, attemped_port )
		}
	}
	panic!( "Could not start server on an unused port" );
}

fn main()
{
	let args: std::vec::Vec<String>  = std::env::args().collect();

	let server_nest = args
		.split( |a| a == "-S" || a == "--server" )
		.collect::< std::vec::Vec< _ > >()
	;

	let connection = 
		if server_nest.len() < 2 
		{
			// carry on
			Connection::StandAlone
		}
		else
		{
			let server_name = &server_nest[ 1 ][ 0 ];
			let file_name = get_file_name_from_server( server_name );

			let file_handle_result = File::open( & file_name  );
			let mut purported_port_string = String::new();

			let connection = match file_handle_result{
				Ok( mut f ) => 
				{
					f.read_to_string( & mut purported_port_string )
						.expect( & no_server_file_or_port_error( "Could not read server file.", server_name ) )
					;

					let port = purported_port_string.parse::<u16>()
						.expect(  & no_server_file_or_port_error( "Could not read port number.", server_name ) )
					;
					
					Connection::Sender( 
						TcpStream::connect( ( "0", port ) )
							.expect( "Could not connect to server" )
					)
				},
				_ => 
				{
					// File doesn't exist => Server doesn't exist.
					// So start 
					let ( listener, port ) = bind_to_unused_port();
					
					let mut file = File::create( &file_name )
						.expect( & ( String::from( "Failed to create or not allowed to read file " ) + &file_name ) )
					;

					file.write_all( port.to_string().as_bytes() )
						.expect( & ( String::from( "Failed to write to file " ) + &file_name ) )
					;

					Connection::Listener( listener )
				}
			};
			connection
		}
	;

	match connection
	{
		Connection::Listener( me ) => {
			let server_name = &server_nest[ 1 ][ 0 ];
			let file_name = get_file_name_from_server( server_name );

			for client in me.incoming() {
				dbg!( client );
				//handle_client( client.unwrap()) ;
			}
			// Delete file here, to be handled in the "quitting" thread.
			// along with closing port.
			// std::fs::remove_file( file_name )
			// 	.expect( String::from( "Failed to delete, please manually delete" ) + &file_name )
		},
		Connection::Sender( mut server ) => {
			let mut server_response = std::vec::Vec::new();

			// Depending on the command line param, 
			// send PWD and maybe commands
			// Two major functions- 
			// 1) is to ask server to navigate in the interface
			// 2) Get server to return the currrent directory
			server.write( b"$PWD + all the args and maybe command" )
				.expect( "Could not send message to server." )
			;

			// Read from server
			server.read( & mut server_response )
				.expect( "Could not get server response" )
			;
		},
		_ => {}
	}

}

SRGOM avatar May 01 '20 15:05 SRGOM

I can't currently accept any PR as I'm rewriting broot (see the "panels" branch) for a big new set of features.

The idea's still interesting, I'll have a loot at it later.

Canop avatar May 01 '20 15:05 Canop

Alright, thank you. Whenever you do, please do let me know and I'll do my part. I'm keen to help out with regards to this feature.

I will change the title and keep the issue open if that's alright.

SRGOM avatar May 01 '20 16:05 SRGOM

I don't think it would be wise to have an automatic port grab: it would prevent having hardcoded commands in your editors or scripts. It could also be messy when you execute broot many times with the wrong arguments.

Here's a draft of how I think the API could be:

  1. A --listen 1234 launch argument telling broot that it should register as server on port 1234 (or fail if there's already one). If it goes well, broot would work as usual.

  2. A --send 1234 launch argument which would mean broot needs to connect to an existing server (or fail if there's none) and send the arguments given to --cmd to that server which would then execute them while the client process immediately quits. With this syntax, the optional final launch argument would be converted to a :focus command (i.e. br --as-client-to 1234 -c "c/test" ~/dev/truc would be equivalent to br --as-client-to 1234 -c ":focus ~/dev/truc;c/test").

Broot's event loop would probably be changed to accept incoming commands as events.

note: now seems the right time for implementing this. I can do it (your prototype would help me not lose too much time looking in the docs) or we could do it together.

Canop avatar Jul 01 '20 10:07 Canop

I would recommend against ports and favor Unix sockets, for the simple reason that you can reach out to local ports from a browser. An example of what you could inspire yourself from would be screen/tmux, which have the concepts of "sessions".

ralt avatar Jul 01 '20 20:07 ralt

Hi, sorry my old account is having trouble so I missed your email-

  1. RE: Auto port grab, I think you have objection to fn bind_to_unused_port()? If so, port is attached to a servername, so everybody would only specify (and hardcode) a servername. This allows additional flexibility, a script could hardcode part of servername and get other part from its environment. A servername uniquely identifies a port. That said, I'm happy with whatever approach you prefer. I haven't explored the way @ralt suggested (does sound better technically) and might not like to invest time there but if you're keen I'll go in that direction too.

  2. The API would be uniform for both sender and receiver as far as connecting to a server is concerned. If no server exists, the caller become a server. (Maybe this behavior could be ammended and the API could be distinctly --listen and --send

I have to confess I'm not a very advanced broot user, I only use it for quick grokking (which is, to my beginner self, its distinctive feature). I mention this so that you know that I would need plenty of help from you.

Do you have a room on https://miaou.dystroy.org/ ? If you give it to me I will contact you there ...

asdf8dfafjk avatar Jul 09 '20 08:07 asdf8dfafjk

You can either come to the broot room or to the more lively Code&Croissants room

Canop avatar Jul 09 '20 08:07 Canop

I messaged you there (but I don't see my own messages so not sure if you received them). I'm using the same github account.

asdf8dfafjk avatar Jul 09 '20 11:07 asdf8dfafjk

Note to readers of this issue: the discussion regarding this feature specification and implementation is done in the chat. Feel free to come and ask if you're interested.

Canop avatar Jul 10 '20 08:07 Canop

Related: https://github.com/Canop/broot/blob/master/client-server.md

Canop avatar Jul 20 '20 18:07 Canop

I added the --get-root launch argument. This feature seems complete... but might stay behind a compilation flag until I get more demands for it.

Canop avatar Aug 17 '20 19:08 Canop

FWIW, there are currently 86 stars for https://github.com/cshuaimin/scd

asdf8dfafjk avatar Aug 23 '20 15:08 asdf8dfafjk

@asdf8dfafjk I only noticed --get-root today, but have something similar to scd going with broot+tmux+Zsh now.

It syncs the broot folder to match the shell, but does not sync the other way automatically. I'm not sure how or if I should do that, so for now it adds a Zsh function gob which jumps to the broot-root folder.

# -- Pinned view in another tmux pane --
broot-pin () {
  emulate -L zsh
  local name=$RANDOM

  eval '
    update_pinned_broot_$name () {
      if ! { broot --send '$name' -c ":focus $PWD" 2>/dev/null } {
        add-zsh-hook -d precmd $0
        unfunction gob
      }
    }

    gob () {
      emulate -L zsh
      cd "$(broot --send '$name' --get-root)"
    }
  '

  add-zsh-hook precmd update_pinned_broot_$name
  tmux splitw -d -h -l 70 broot --listen $name
}

AndydeCleyre avatar Oct 02 '25 18:10 AndydeCleyre