jaclu / tmux-menus

Tmux plugin, Popup menus to help with managing your environment
MIT License
322 stars 11 forks source link

Have menus adapt to user's customized bindings #48

Open thomastthai opened 1 month ago

thomastthai commented 1 month ago

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 commented 1 month ago

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 commented 1 month ago

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 commented 1 month ago

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 commented 1 month ago

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 commented 1 month ago

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 commented 1 month ago

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 commented 1 month ago

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 commented 1 month ago

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

jaclu commented 1 month ago

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 commented 1 month ago

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 commented 1 month ago

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 commented 1 month ago

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

thomastthai commented 1 month ago

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

Have a safe flight!

thomastthai commented 1 month ago

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