[Feature proposal] Single instance
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?
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).
No worries, thank you.
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)
navigatewould take the "server" instance to that directory.get-cwd- the client program gets from server and prints the current dir on stdoutget-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" )
;
},
_ => {}
}
}
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.
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.
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:
-
A
--listen 1234launch argument telling broot that it should register as server on port1234(or fail if there's already one). If it goes well, broot would work as usual. -
A
--send 1234launch argument which would mean broot needs to connect to an existing server (or fail if there's none) and send the arguments given to--cmdto 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:focuscommand (i.e.br --as-client-to 1234 -c "c/test" ~/dev/trucwould be equivalent tobr --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.
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".
Hi, sorry my old account is having trouble so I missed your email-
-
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. -
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
--listenand--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 ...
You can either come to the broot room or to the more lively Code&Croissants room
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.
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.
Related: https://github.com/Canop/broot/blob/master/client-server.md
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.
FWIW, there are currently 86 stars for https://github.com/cshuaimin/scd
@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
}