Skip to content

Conversation

@tonynajjar
Copy link

@tonynajjar tonynajjar commented Dec 21, 2025

Description

This PR adds optional fuzzy search functionality to ros2cli commands using fzf, inspired by the ros2-aliases project. When arguments are omitted from commands, an interactive fzf selector is launched instead of throwing an error.

Commands enhanced:

  • ros2 topic echo - Select topic interactively
  • ros2 topic hz - Select topic interactively
  • ros2 topic info - Select topic interactively
  • ros2 topic bw - Select topic interactively
  • ros2 topic pub - Select topic interactively
  • ros2 node info - Select node interactively
  • ros2 service call - Select service interactively
  • ros2 param get - Cascading selection (node → parameter)
  • ros2 interface show - Select interface interactively

Implementation:

  • Added interactive_select() helper function in ros2cli/helpers.py
  • Made positional arguments optional (nargs='?') in affected verbs
  • Added fzf as an execution dependency in ros2cli/package.xml
  • Maintains 100% backward compatibility - all existing command usage still works

Is this user-facing behavior change?

Yes. Users can now omit arguments and use fuzzy search instead:

Before:

ros2 topic echo /my_topic_name  # Must know exact topic name

After:

ros2 topic echo                  # Launches fzf to select from available topics
ros2 topic echo /my_topic_name   # Still works as before

If fzf is not installed, users get a clear error message directing them to install via rosdep.

Did you use Generative AI?

Yes, GitHub Copilot was used

Additional Information

  • Requires fzf: Added as dependency in package.xml but dependent on Add fzf ros/rosdistro#49148
  • No breaking changes: All existing CLI usage remains unchanged
  • Graceful degradation: Clear error message if fzf is missing

Signed-off-by: Tony Najjar <tony.najjar@dexory.com>
Signed-off-by: Tony Najjar <tony.najjar@dexory.com>
Signed-off-by: Tony Najjar <tony.najjar@dexory.com>
Signed-off-by: Tony Najjar <tony.najjar@dexory.com>
@tonynajjar
Copy link
Author

tonynajjar commented Dec 22, 2025

This PR still needs some testing and refinement but before I iterate further I'd like to know if you would merge this idea in. Thanks
@ahcorde @fujitatomoya
You can see a gif demo in https://github.com/tonynajjar/ros2-aliases

Copy link
Collaborator

@fujitatomoya fujitatomoya left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tonynajjar generally i like this enhancement, thanks 🚀

there are several comments that i think we need to address but overall i think this is gonna be better user-experience for ROS 2 users and developers.

we already have type completion via shell command prompt but tab completion requires us to know the prefix and type sequentially from the beginning, while fzf lets us match any part of the name in any order (e.g., typing "wrist depth" finds /arm/wrist_camera/depth/image_raw). fzf also provides visual browsing of all available options with real-time filtering, making it ideal for exploring unfamiliar systems or when you only remember fragments of a name.

before fixing up my comments, let's hear out more feedback on this 👍

Comment on lines +141 to +144
def interactive_select(
items: List[str],
prompt: str = 'Select an item:'
) -> Optional[str]:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does it miss tty detection? no check for whether stdin/stdout is a tty? i think this will cause scripts and CI/CD pipelines to hang, if it meets this case. related to this issue, fzf reads keyboard input from /dev/tty, not stdin. however, when ros2cli commands are piped or redirected, fzf may not be able to access the tty properly?

the followings are pseudo code,

    # Check if we're in an interactive terminal
    if not sys.stdin.isatty() or not sys.stdout.isatty():
        print('Error: Interactive selection requires a TTY terminal.', 
              file=sys.stderr)
        return None

and

try:
    # fzf needs direct access to /dev/tty for keyboard input
    tty = open('/dev/tty', 'r')
    process = subprocess.Popen(
        ['fzf', '--prompt', prompt + ' ', '--height', '40%', '--reverse'],
        stdin=subprocess.PIPE,
        stdout=subprocess.PIPE,
        stderr=tty,  # fzf uses stderr for display
        text=True
    )
    # ...
finally:
    tty.close()

)

# Send items to fzf
stdout, _ = process.communicate(input='\n'.join(items))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do not need to have any signal control here when opening the child process? If the user presses Ctrl+C, this may leave the terminal in a bad state or not clean up properly... maybe we can add try/except statement to catch the KeyboardInterrupt to call process.terminate()?

import signal

try:
    stdout, _ = process.communicate(input='\n'.join(items))
except KeyboardInterrupt:
    process.terminate()
    process.wait()
    return None

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

besides that, i would add the timeout just in case if fzf hangs for some reason, the entire command hangs indefinitely.

Comment on lines +156 to +171
try:
# Check if fzf is available
result = subprocess.run(
['fzf', '--version'],
capture_output=True,
text=True,
timeout=1
)
if result.returncode != 0:
raise FileNotFoundError()
except (FileNotFoundError, subprocess.TimeoutExpired):
print(
'Error: fzf is not installed but is a dependency for this package. You can install it with rosdep',
file=sys.stderr
)
return None
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i believe this is redundant. i would do the following instead to check fzf availability.

import shutil

if shutil.which('fzf') is None:
    print('Error: fzf is not installed...', file=sys.stderr)
    return None

Comment on lines +193 to +195
except Exception as e:
print(f'Error during interactive selection: {e}', file=sys.stderr)
return None
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Catching all exceptions hides bugs and makes debugging harder.

Suggested change
except Exception as e:
print(f'Error during interactive selection: {e}', file=sys.stderr)
return None
except (OSError, subprocess.SubprocessError) as e:
print(f'Error during interactive selection: {e}', file=sys.stderr)
return None

@tonynajjar
Copy link
Author

Thanks for the feedback @fujitatomoya . Like you said I will start working on your comments once I get another opinion on the general idea, to make sure this can ultimately get merged in

@tonynajjar
Copy link
Author

tonynajjar commented Dec 29, 2025

Maybe @ahcorde you can give your general opinion on the feature and I can continue to iterate with @fujitatomoya? Thanks!

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

Successfully merging this pull request may close these issues.

2 participants