Canop / broot

A new way to see and navigate directory trees : https://dystroy.org/broot
MIT License
10.74k stars 235 forks source link

[Feature proposal] Single instance #225

Open SRGOM opened 4 years ago

SRGOM commented 4 years ago

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?

Canop commented 4 years ago

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).

SRGOM commented 4 years ago

No worries, thank you.

SRGOM commented 4 years ago

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" )
            ;
        },
        _ => {}
    }

}
Canop commented 4 years ago

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.

SRGOM commented 4 years ago

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.

Canop commented 4 years ago

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.

ralt commented 4 years ago

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".

asdf8dfafjk commented 4 years ago

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 ...

Canop commented 4 years ago

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

asdf8dfafjk commented 4 years ago

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.

Canop commented 4 years ago

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 commented 4 years ago

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

Canop commented 4 years ago

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.

asdf8dfafjk commented 4 years ago

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