Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Have menus adapt to user's customized bindings #48

Open
thomastthai opened this issue Oct 3, 2024 · 15 comments
Open

Have menus adapt to user's customized bindings #48

thomastthai opened this issue Oct 3, 2024 · 15 comments

Comments

@thomastthai
Copy link
Contributor

This plugin is already nicely over-engineered. Why not take it to the next level of over-engineering? :)

Currently, the menus are hardcoded with certain keybindings that could be different than the user's settings. Having the menu "read" (tmux list-keys, tmux show-keys, and/or other ways) the user key bindings and display those in the menus would help reinforce memory and give them the menu as another access point. This is a major reason for nvim's which-key success.

For example, part of my tmux.conf include:

bind | split-window -h -c "#{pane_current_path}"
bind \\ split-window -fh -c "#{pane_current_path}"
bind '-' split-window -v -c "#{pane_current_path}"
bind '_' split-window -fv -c "#{pane_current_path}"
@jaclu
Copy link
Owner

jaclu commented Oct 3, 2024

I always apreciate suggestions, but Im not sure what you mean in this case. Previously I tried to always list the default key-bindings for items when there was one, but in the end it made the menus a bit bloated, so I recently dropped it.
Is your suggestion to go all in and display the current bindings where appicable? In that case I do agree it would be a nice feature to be remainded of the current config. But it would raise the bar considerably, since then each other shortcut in the menu would have to be checked so that there are no duplicates

@thomastthai
Copy link
Contributor Author

thomastthai commented Oct 3, 2024

Including all the default key bindings would bloat the menus for sure! In real-world use, most people use about 20% of the keybindings 80% of the time. Which key bindings vary between people. Around 10% of those keybindings are common across all people. People's workflows are different. Their preferred custom bindings are different for the same functions.

There are potentially three ways to build the menus taking people's preferences into consideration:

  1. The plugin would read the key bindings from tmux.conf (or the pointer to their config file). Also scan tmux list-keys and tmux show-keys. This could capture the key bindings they customized. Then show that key binding for a particular action in the menu instead of the default key binding. For example, if I customize the key binding | to horizontally split the window instead of the default tmux % (not for sure if that's the default), then display `|' next to the menu for the Horizontal Split Window menu. This helps reinforce the user's muscle memory for their chosen preference.
  2. Use a config file like in YAML for this plugin where they can easily define their custom menus and possibly retype their custom key bindings already in their tmux.conf file. This file can allow them to build the structure of their menus. If someone is starting fresh and doesn't have a lot of custom key bindings in tmux.conf, then using another config file may work.
  3. A hybrid of No. 1 and No. 2.

IMO, No. 1 is important to cater to people's custom key bindings already in place, but they could use a visual menu to remind them. No. 1 reduces friction from the user to use the plugin, i.e., it just works and fits in with their current workflow.

@jaclu
Copy link
Owner

jaclu commented Oct 3, 2024

Scenario 1 seems to trigger the issue I mentioned, if the user defined shortcut is used as the menu shortcut it would indeed make sense, but sooner or later somebody will happen to use a shortcut colliding with something already used in that menu, adding a pre-parse stage to first extract user preferences, then ensure all other items get unused shortcuts - sure not to hard to do, but adds processing time, so will make the menus slower to appear on slower systems

@thomastthai
Copy link
Contributor Author

thomastthai commented Oct 3, 2024

Agreed on the additional processing time. One option is to cache the menu and give the user the option, i.e., a menu item to refresh the cache. For most people, unless they are experimenting, their custom key bindings won't change much.

@jaclu
Copy link
Owner

jaclu commented Oct 3, 2024

Regarding using yaml to define menus, sure could be done, but not sure if it would make things more convenient when defining dynamic items like the Panes menu, using shell script gives a fairly simplistic menu design, and allows normal script logic when examining states deciding dynamic items. I have mostly used yaml when doing docker and ansible, so perhaps scripting can be included without too much over head?

@thomastthai
Copy link
Contributor Author

The YAML idea was a brainstorming suggestion. Most people who are smart enough to use tmux and nvim will likely be able to modify shell scripts and mess up those shell scripts as well :) From the customer experience perspective, people enjoy a solution that offers low friction, is easy to use, and provides value and "magic."

@jaclu
Copy link
Owner

jaclu commented Oct 3, 2024

Im not arguing against the low friction perspective, and I would agree that yaml would make sense from a pure menu design perspective.
I was more thinking that in the case of dynamic menus, that needs to scan some tmux state as they are generated, if that would imply both a yaml file defining the menu, and a shell script defining the dynamic action thus spreading it out to two files, most of the the yaml advantage would be gone, and since I "hopefully" have made my menu design aproach reasonably easy to grasp, using my custom menu design lingo and having the asociated dynamic checks for states in a single file would make sense in keeping it in a single file and minimising the need for starting additional processes.
Remember im tryinng to make this usable even on iSH, a ridiculously slow linux env for iOS. That was why i implemented caching in the first place. On any normal env, if the menu is generated in 0.03 seconds or 0.07 doesnt matter, but in iSH its about having the main menu arrive in 1.1s vs 2.5...

@jaclu
Copy link
Owner

jaclu commented Oct 3, 2024

but sure i you think my menu design concept is hard to understand, that might need to be adressed

@jaclu
Copy link
Owner

jaclu commented Oct 3, 2024

GPT suggested that tmux conditionals could be done something like this:

menus:
  - name: "My Tmux Menu"
    items:
      - label: "Do Action If Pane Marked"
        command: |
          if-shell 'tmux list-panes -F "#{?pane_marked,1,0}" | grep -q 1' \
            'tmux display-message "A pane is marked!"' \
            'tmux display-message "No pane is marked."'
      - label: "Other Action"
        command: "tmux split-window"

No entirely sure that could be seen as less confusing :) - any thoughts?

@thomastthai
Copy link
Contributor Author

Another idea is to set this plugin's config and layout the menu within tmux.conf right below the line to load this plugin. This is how some tmux plugin does it. For example, Catppuccin settings:

# Use catppuccin theme
set -g @plugin 'catppuccin/tmux'
set -g @catppuccin_flavor 'mocha'
set -g @catppuccin_borders 'rounded'  # Pane border options: 'rounded', 'sharp', or 'none'
set -g @catppuccin_italics '1'  # set to '0' to disable italics
set -g @catppuccin_powerline 'true' # Show Powerline Separators (if using powerline fonts)
#set -g @catppuccin_accent 'lavender'
#set -g @catppuccin_bg_color '#1E1E2E'
#set -g @catppuccin_fg_color '#D9E0EE'

set -g @catppuccin_window_current_color "#{thm_blue}" # text color
set -g @catppuccin_window_current_background "#{thm_bg}"
set -g @catppuccin_window_default_color "#{thm_bg}" # text color
set -g @catppuccin_window_default_background "#{thm_blue}"
set -g @catppuccin_window_left_separator "█"
set -g @catppuccin_window_right_separator "█ "
#set -g @catppuccin_window_middle_separator "█"
set -g @catppuccin_window_middle_separator " "
set -g @catppuccin_window_number_position "left"
set -g @catppuccin_window_default_fill "all"
set -g @catppuccin_window_default_text "#W"
set -g @catppuccin_window_current_fill "all"
set -g @catppuccin_window_current_text "#W"
...

@thomastthai
Copy link
Contributor Author

GPT suggested that tmux conditionals could be done something like this:

menus:
  - name: "My Tmux Menu"
    items:
      - label: "Do Action If Pane Marked"
        command: |
          if-shell 'tmux list-panes -F "#{?pane_marked,1,0}" | grep -q 1' \
            'tmux display-message "A pane is marked!"' \
            'tmux display-message "No pane is marked."'
      - label: "Other Action"
        command: "tmux split-window"

No entirely sure that could be seen as less confusing :) - any thoughts?

It's slightly less confusing but the difference is negligible :)

@jaclu
Copy link
Owner

jaclu commented Oct 3, 2024

about to board a plane, you had some interesting inputs - much apreciated, will look into it more tomorrow!

@thomastthai
Copy link
Contributor Author

about to board a plane, you had some interesting inputs - much apreciated, will look into it more tomorrow!

Have a safe flight!

@thomastthai
Copy link
Contributor Author

thomastthai commented Oct 3, 2024

set -g @catppuccin_flavor 'mocha'

Having this plugin's configuration settings and menus defined like above in the tmux.conf file, allows this plugin's shell scripts to access those variables and values. For example, to use catppuccin_flavor in a shell script:

   #!/bin/bash

   # Extract the value of @catppuccin_flavor from tmux
   catppuccin_flavor=$(tmux show-options -g | grep '@catppuccin_flavor' | awk '{print $2}')

   # Use the extracted value
   echo "The current Catppuccin flavor is: $catppuccin_flavor"

   # Example usage: Apply the theme based on the flavor
   if [ "$catppuccin_flavor" == "mocha" ]; then
       echo "Applying the Mocha theme..."
       # Add commands to apply the Mocha theme
   else
       echo "Unknown Catppuccin flavor: $catppuccin_flavor"
   fi

Your shell scripts to handle the menus look good. You could use the existing code for that and leverage the set -g @whatever "value" and allow users to define their key bindings that correspond with their bind ... ... in tmux.conf.

Take the menu item "Rename window" for example. Your default setup uses r as the key binding. In tmux.conf:

set -g @tmux_menus_rename_window 'r'

Now if I happen to prefer to use , instead of r, I can easily change that in tmux.conf:

set -g @tmux_menus_rename_window ','

or

use the value of the variable as shell commands to pull the custom keybinding:

set -g @tmux_menus_rename_window "tmux list-keys -N | rg '^[a-zA-Z0-9-]+\s+(\S+)\s+Rename current window$' -r '$1''' # uses ripgrep so it's consistent across all OSes

or

use the description of each key binding from tmux list-keys -N as the value:

set -g @tmux_menus_rename_window "Rename current window"

Then in your shell scripts to build the menu, use "Rename current window" as a key to access the bash hash map to get the custom keybinding.

One way to build that key binding hash map is to use rg (ripgrep) to maintain consistency across all OSes since sed and grep can vary:

#!/bin/bash

# Ensure we're using an associative array
declare -A my_map

# Capture the output and check if rg processes it correctly
output=$(tmux list-keys -N | rg '^([a-zA-Z0-9-]+)\s+(\S+)\s+(.*)$' -r $'$3\t$2')

# Use a here string to avoid subshells
while IFS=$'\t' read -r key value; do
    # Strip leading and trailing whitespace
    key=$(echo "$key" | xargs)
    value=$(echo "$value" | xargs)

    # Set the key-value pair in the hash map
    if [[ -n "$key" && -n "$value" ]]; then
        my_map["$key"]="$value"
        
        # Print the key and value immediately after adding to the hash map
        echo "Added to my_map: Key: '$key', Value: '${my_map[$key]}'"
    fi
done <<< "$output"

# Check if my_map is populated and print the contents
if [[ ${#my_map[@]} -eq 0 ]]; then
    echo "my_map is empty."
else
    echo -e "\nFinal key-value pairs in my_map:"
    for key in "${!my_map[@]}"; do
        echo "Key: '$key', Value: '${my_map[$key]}'"
    done
fi

That key binding hash map can be loaded once for all the submenus to find their keys/values.

Here is a sample output from the script above:

Final key-value pairs in my_map:
Key: 'Toggle the marked pane', Value: 'm'
Key: 'Resize the pane right by 5', Value: 'M-Right'
Key: 'List all paste buffers', Value: '#'
Key: 'Rotate through the panes in reverse', Value: 'M-o'
Key: 'Spread panes out evenly', Value: 'E'
Key: 'Select the previous window with an alert', Value: 'M-p'
Key: 'Customize options', Value: 'C'
Key: 'Display pane numbers', Value: 'q'
Key: 'Resize the pane left by 5', Value: 'M-Left'
Key: 'Show messages', Value: '~'
Key: 'Select the pane below the active pane', Value: 'Down'
Key: 'Zoom the active pane', Value: 'z'
Key: 'Switch to the last client', Value: 'L'
Key: 'Choose a session from a list', Value: 's'
Key: 'Set the main-vertical layout', Value: 'M-4'
Key: 'Set the even-horizontal layout', Value: 'M-1'
...

The prefix keys can be retrieved using the rp command above and the $1 match group per line or:

❯ tmux show-options -g prefix | rg '^prefix (.+)$' -r '$1'
C-Space

❯ tmux show-options -g prefix2 | rg '^prefix2 (.+)$' -r '$1'
None

@jaclu
Copy link
Owner

jaclu commented Dec 14, 2024

By using a variation of your bash script where i instead filter using

tmux list-keys | grep 'prefix' | sed 's/.*prefix *//'

and

tmux list-keys | grep 'root' | sed 's/.*root *//'

I could get two lists defininng user binds.

But it doesnt really work out in practice, for instance the menu items/split_view.sh uses several versions of split-window, most of them with multiple options.

Checking for a matching user bind via list-keys quickly collapses, for instance "Split Window-Left" internally uses split-window -fhb, but there is no guarantee the user bind would use the same notation for a bind doing the same, there it might be defined as -bfh or -f -b -h etc

So this makes finding a user bind that matches pretty resource intensive, since first all options would need to be expanded into single option items in alphabetical order, and then all options in each menu would also need the same option expansion treatment to ensure source and dest is using the same notation. Finally all options that can be ignored in a specific context needs to be filtered out.

And after all that is done, there is still the risk of when overriding the menu shortcut with the user defined bind, it would collide with an already used shortcut. Each such override needs to be cross checked with all other shortcuts in that menu, and change any found duplicate in the menu definition to something else.

I certainly agree that it would be a desirable feature, but I just cant come up with a reasonable way to implement such a feature.

If you can come up with a solution that can both handle identical but differently formulated options, and find a way to remap predefined colliding menu shortcuts that would be epic and I would gladly accept any such PR!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants