Skip to content

[lldb] Add fzf_history command to examples #128571

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

Merged

Conversation

kastiglione
Copy link
Contributor

@kastiglione kastiglione commented Feb 24, 2025

Adds a fzf_history to the examples directory.

This python command invokes fzf to select from lldb's command history.

Tighter integration is available on macOS, via commands for copy and paste. The user's chosen history entry back is pasted into the lldb console (via AppleScript). By pasting it, users have the opportunity to edit it before running it. This matches how fzf's history search works.

Without copy and paste, the user's chosen history entry is printed to screen and then run automatically.

@llvmbot
Copy link
Member

llvmbot commented Feb 24, 2025

@llvm/pr-subscribers-lldb

Author: Dave Lee (kastiglione)

Changes

Full diff: https://github.com/llvm/llvm-project/pull/128571.diff

1 Files Affected:

  • (added) lldb/examples/python/fzf_history.py (+99)
diff --git a/lldb/examples/python/fzf_history.py b/lldb/examples/python/fzf_history.py
new file mode 100644
index 0000000000000..4647a3532b0df
--- /dev/null
+++ b/lldb/examples/python/fzf_history.py
@@ -0,0 +1,99 @@
+import os
+import re
+import sys
+import subprocess
+
+import lldb
+
+
+@lldb.command()
+def fzf_history(debugger, cmdstr, ctx, result, _):
+    if sys.platform != 'darwin':
+        result.SetError("fzf_history supports macOS only")
+        return
+
+    # Capture the current pasteboard contents to restore after overwriting.
+    paste_snapshot = subprocess.run("pbpaste", text=True, capture_output=True).stdout
+
+    # On enter, copy the selected history entry into the pasteboard.
+    fzf_command = (
+        "fzf",
+        "--no-sort",
+        f"--query={cmdstr}",
+        "--bind=enter:execute-silent(echo -n {} | pbcopy)+close",
+    )
+
+    history_file = os.path.expanduser("~/.lldb/lldb-widehistory")
+    if not os.path.exists(history_file):
+        result.SetError("history file does not exist")
+        return
+
+    history_commands = _load_history(history_file)
+    fzf_input = "\n".join(history_commands)
+    completed = subprocess.run(fzf_command, input=fzf_input, text=True)
+    # 130 is used for CTRL-C or ESC.
+    if completed.returncode not in (0, 130):
+        result.SetError(f"fzf failed: {completed.stderr}")
+        return
+
+    # Get the user's selected history entry.
+    selected_command = subprocess.run("pbpaste", text=True, capture_output=True).stdout
+    if selected_command == paste_snapshot:
+        # Nothing was selected, no cleanup needed.
+        return
+
+    _handle_command(debugger, selected_command)
+
+    # Restore the pasteboard's contents.
+    subprocess.run("pbcopy", input=paste_snapshot, text=True)
+
+
+def _handle_command(debugger, command):
+    """Try pasting the command, and failing that, run it directly."""
+    if not command:
+        return
+
+    # Use applescript to paste the selected result into lldb's console.
+    paste_command = (
+        "osascript",
+        "-e",
+        'tell application "System Events" to keystroke "v" using command down',
+    )
+    completed = subprocess.run(paste_command, capture_output=True)
+
+    if completed.returncode != 0:
+        # The above applescript requires the "control your computer" permission.
+        #     Settings > Private & Security > Accessibility
+        # If not enabled, fallback to running the command.
+        debugger.HandleCommand(command)
+
+
+def _load_history(history_file):
+    """Load, decode, and parse an lldb history file."""
+    with open(history_file) as f:
+        history_contents = f.read()
+
+    history_decoded = re.sub(r"\\0([0-7][0-7])", _decode_char, history_contents)
+    history_lines = history_decoded.splitlines()
+
+    # Skip the header line (_HiStOrY_V2_)
+    del history_lines[0]
+    # Reverse to show latest first.
+    history_lines.reverse()
+
+    history_commands = []
+    history_seen = set()
+    for line in history_lines:
+        line = line.strip()
+        # Skip empty lines, single character commands, and duplicates.
+        if line and len(line) > 1 and line not in history_seen:
+            history_commands.append(line)
+            history_seen.add(line)
+
+    return history_commands
+
+
+def _decode_char(match):
+    """Decode octal strings ('\0NN') into a single character string."""
+    code = int(match.group(1), base=8)
+    return chr(code)

Copy link

github-actions bot commented Feb 24, 2025

✅ With the latest revision this PR passed the Python code formatter.

Copy link
Member

@JDevlieghere JDevlieghere left a comment

Choose a reason for hiding this comment

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

Instead of using pbcopy/pbpaste, which makes this macOS only, could you use a Python temp file and read/write from that?

def fzf_history(debugger, cmdstr, ctx, result, _):
"""Use fzf to search and select from lldb command history."""
if sys.platform != "darwin":
result.SetError("fzf_history supports macOS only")
Copy link
Contributor

Choose a reason for hiding this comment

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

I know that fzf_history here is using pbcopy/pbpaste and applescript here for the clipboard operations but is it possible to just say that "clipboard operations are only supported on macOS" and still run the script?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@kastiglione
Copy link
Contributor Author

Instead of using pbcopy/pbpaste, which makes this macOS only, could you use a Python temp file and read/write from that?

@JDevlieghere I didn't do that because it would be an implementation I wouldn't even want to use, for platforms I don't use.

Without a way to do copy and paste, the command could skip writing to write to a temp file, since fzf's default behavior is to print the selected line to stdout.

@kastiglione
Copy link
Contributor Author

I added a featureless invocation of fzf for when copy and paste support isn't available.

@JDevlieghere
Copy link
Member

Instead of using pbcopy/pbpaste, which makes this macOS only, could you use a Python temp file and read/write from that?

@JDevlieghere I didn't do that because it would be an implementation I wouldn't even want to use, for platforms I don't use.

What I have in mind seems significantly simpler than what you currently have, wouldn't require special permissions and would make this work on other POSIX platforms (but admittedly not Windows). I'm not sure I understand the pushback.

Maybe I'm missing something? Or what I'm suggesting isn't possible? What I had in mind was to pipe the output to a temporary file like this:

"--bind=enter:execute-silent(echo -n {} > /tmp/xyz_unique_file)+close"

and then instead of doing the dance with osascript, you just open that file and read its contents.

@kastiglione
Copy link
Contributor Author

and then instead [...] open that file and read its contents.

@JDevlieghere and then… run the command directly? When I read your first comment, I think I must have not realized the implication (because see below).

I'm not sure I understand the pushback.
What I have in mind seems significantly simpler than what you currently have

I want it to work as close to the same way as it works with fzf's shell integration. I don't want the selected command to run automatically, and I don't want to have to manually copy and paste the command. The only reason I went forward with this is because I saw an approach that would achieve those.

I also thought someone else might be motivated to figure out how to achieve those on other platforms.

@kastiglione
Copy link
Contributor Author

kastiglione commented Feb 25, 2025

@JDevlieghere the latest commit makes fzf write to a temp file, and then those contents are run as a command (for non-Darwin platforms).

For Darwin, the pasting is still used to achieve parity with fzf history search in shells.

@kastiglione
Copy link
Contributor Author

A thought, would it make sense to add a function (to SBDebugger or SBCommandInterpreter) that sets the string of the lldb command line? If such a function existed, pasting wouldn't be needed.

Copy link
Member

@JDevlieghere JDevlieghere left a comment

Choose a reason for hiding this comment

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

Thanks, I missed the part where it pastes the command but doesn't actually run it. Setting the string on the command line would be a very IOHandler specific thing and that's not something we expose. Sure, you could add it to the CommandInterpeter but that's definitely above and beyond for an example. I think this looks good!

@kastiglione kastiglione merged commit 83c6b1a into llvm:main Feb 25, 2025
10 checks passed
@kastiglione kastiglione deleted the lldb-Add-fzf_history-command-to-examples branch February 25, 2025 16:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants