duncs / clusterssh

Cluster SSH - Cluster Admin Via SSH
https://github.com/duncs/clusterssh/wiki
889 stars 79 forks source link

Maximize use of screen real estate #117

Open jaybeers opened 5 years ago

jaybeers commented 5 years ago

I would prefer that the window placement behaviour use as much of the available space as possible (I know plenty of other people probably prefer the default, but I can only speak for my own preferences :) ).

First, a little background to make sure I'm explaining myself properly. As I understand it from observing its behaviour, ClusterSSH figures out the geometry of the area where it can draw windows (starting with the area of the entire workspace and subtracting the "$screenreserve*" amounts) and then tiles as many $terminal_size windows as possible in that space, decreasing the height of each window if necessary to fit them all in.

What I would like is for the window allocation to be "greedy" and use up as much of the screen as possible; in other words, make the windows bigger if there's space for it. These are my proposals:

Horizontal expansion only

Do the same thing you're doing now, but expand the windows horizontally (i.e. increase $COLUMNS) to fill the available space. Anything "left over" (e.g. if you've got an odd number of windows) should go to the first host on the command line...so you'd wind up with something like this if you had five windows, for example:

-----------------
|     host1     |
-----------------
| host2 | host3 |
-----------------
| host4 | host5 |
-----------------

Horizontal and vertical expansion

Take the above and go one step further: also expand the windows vertically (i.e. increase $ROWS) to fill the available space; the first host also gets these leftovers.

Perhaps a bit silly

What if you could specify a different font based on how many total hosts you were connecting to? In other words, maybe if I only open four hosts I want a nice big font 'cause it's easier to read, but if things snowball into a 37-host situation I want to back it down to something more suited, like good ol' 5x8. I'm not sure how you'd express this in the config file...maybe something like this?

terminal_font = 1:6x13 4:6x10 8:5x8

The semantics are "6x13 is used if there are less than four windows. 6x10 is used if there are at least four windows, but less than eight. 5x8 is used if there are eight or more." You could also presumably drop the 1: since the first group has no lower limit of window count. On the other hand, maybe having the 1: does the parser a favor, in that it can immediately differentiate this new syntax from the existing "one size fits all" value.

sharkbit commented 3 years ago

I really want this! I constantly have multiple server (6++) open on a large display and I am constantly resizing them to use more of my display. When I have 10+ I am spending a lot of time just resizing windows.

jaybeers commented 3 years ago

I had forgotten about this feature request; I wound up solving the issue for myself in a different way (since I always open my ClusterSSH windows on portrait-orientation display, I just increased terminal_size to the entire width (177x24) and that actually worked out for me just fine).

Buuut, as I was reading over my previous post again, I had an idea that I'm surprised I didn't have before. Instead of trying to pick multiple values for terminal_font, why not just do it with terminal_size? ClusterSSH knows how many hosts are on its command line, so it seems relatively straightforward to have a switch() statement of some kind that dynamically sets the terminal size based on that, and then just proceed with the rest of the window placement logic as normal? (I know I'm skirting very close to "if it's so easy, why don't you do it?" territory...I'm just saying it seems relatively straightforward. :slightly_smiling_face:)

terminal_size = 1:177x24 4:132x24 8:80x24

Then, the user can decide which size values make sense for their layout, and ClusterSSH can automatically (re-)arrange them using that logic.

I don't know enough Perl to do this in real code (and not pseudo-code), but it seems like if you put something like this right before https://github.com/duncs/clusterssh/blob/master/lib/App/ClusterSSH/Window/Tk.pm#L867-L881 it should do the trick?

Select terminal size based on number of terminals requested

  1. Determine how many terminals are requested. Initially this is the number of hosts on the command line, but that may be different if some windows have been closed, so this count should reflect that so that "re-tile windows" works properly.
  2. Figure out which terminal_size value to use based on window count:
    1. Split $self-config->{terminal_size} into an array ts on (space, or any other delimiter you prefer)
    2. If ts has exactly 1 element then use that value directly (this preserves the current behaviour, for backwards compatibility)
    3. If ts has more than 1 element, for each element of ts:
      1. Split the element on :. If there is no :, then implicitly prepend 0 as the first element and the rest as the second
      2. If final terminal size hasn't been set yet or the number of terminals is >= element[0], set terminal_size to element[1]
      3. If the number of windows is < element[0], break the loop early

Example of how this logic works, based on the following hypothetical config setting, with sharkbit's 6++ (i.e. 7 :smile:) terminals. Ideally the first element would be 1:177x24, but maybe enforcing that isn't necessary. What is necessary is that the terminal counts be in ascending order (in this example, that's <implied 0>, 4, 8) for the loop logic to work properly.

terminal_size = 177x24 4:132x24 8:80x24

This is how I would implement it in Python (with the comments stripped out so you can more easily get a feel for the complexity):

def pick_terminal_size(number_of_terminals=7, config_value="177x24 4:132x24 8:80x24"):
    size_options = config_value.split(" ")
    if len(size_options) == 1:
        return(size_options[0].split(":")[1])
    else:
        final_terminal_size = None
        for size_option in size_options:
            size_properties = size_option.split(":")
            if len(size_properties) == 1:
                size_properties = [ 0, size_properties[0] ]
            if final_terminal_size is None or number_of_terminals >= int(size_properties[0]):
                final_terminal_size = size_properties[1]
            elif number_of_terminals < int(size_properties[0]):
                break
        if final_terminal_size is None:
            final_terminal_size = "80x24"
        else:
            return(final_terminal_size)

Here's the exact same code, with the comments. :)

def pick_terminal_size(number_of_terminals=7, config_value="177x24 4:132x24 8:80x24"):

    ## This is where you can change the field delimiter for the config value
    size_options = config_value.split(" ")

    ## `size_options` is now: [ "177x24", "4:132x24", "8:80x24" ]

    if len(size_options) == 1:
        ## This doesn't execute because we have three elements, not just one
        ## Grab the single choice, strip off any colon and whatever's before
        ## it (shouldn't be there, but just for safety) and return it.
        return(size_options[0].split(":")[1])
    else:
        ## Initialize this variable so we know if we've set it yet
        final_terminal_size = None

        for size_option in size_options:
            ## Iterate through the choices and find the right one

            ## `size_option` is now:
            ##     First item:  "177x24"
            ##     Second item: "4:132x24"
            ##     Third item:  "8:80x24"

            size_properties = size_option.split(":")

            ## `size_properties` is now:
            ##     First item:  [ "177x24" ]
            ##     Second item: [ "4", "132x24" ]
            ##     Third item:  [ "8", "80x24" ]

            if len(size_properties) == 1:
                ## This turns `[ "177x24" ]` into `[ 0, "177x24" ]`
                size_properties = [ 0, size_properties[0] ]

            ## `size_properties` is now:
            ##     First item:  [ 0, "177x24" ]
            ##     Second item: [ "4", "132x24" ]
            ##     Third item:  [ "8", "80x24" ]
            ##
            ## You'll notice 0 is an integer while "4" and "8" are strings.  I use on-the-fly
            ## casting to deal with that later; if this were real code I'd normalize the data
            ## properly but I don't know what the equivalent syntax is in Perl. :)

            if final_terminal_size is None or number_of_terminals >= int(size_properties[0]):
                ## We either don't have a final size yet, so take whatever is there, or the
                ## number of terminals for this size is at least as many as we're dealing with.
                ## Future iterations of the loop may push this number larger.
                final_terminal_size = size_properties[1]
            elif number_of_terminals < int(size_properties[0]):
                ## This choice is for a larger number of terminals than we have.
                break

        if final_terminal_size is None:
            ## This is after the loop.  If we _still_ don't have a size, there's something
            ## wrong with the config value, so just put in a hardcoded default for sanity.
            final_terminal_size = "80x24"
        else:
            ## In this example, though, we _did_ have a valid value.  It got set to "177x24"
            ## on the first pass, because we had more than zero terminals.  Then it got updated
            ## to "132x24" on the second pass, because we had more than 4 terminals.  When we
            ## did the third pass, it was for 8 (or more) terminals, which is higher than our
            ## count of 7, so we broke the loop, leaving "132x24" as the value, which we now
            ## return to our hypothetical calling context.
            return(final_terminal_size)