aboutsummaryrefslogtreecommitdiff
path: root/lldb/examples/python/fzf_history.py
blob: d70789c8c0259354577d018e73857f3d489daf66 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
import os
import re
import sys
import subprocess
import tempfile

import lldb


@lldb.command()
def fzf_history(debugger, cmdstr, ctx, result, _):
    """Use fzf to search and select from lldb command history."""
    history_file = os.path.expanduser("~/.lldb/lldb-widehistory")
    if not os.path.exists(history_file):
        result.SetError("history file does not exist")
        return
    history = _load_history(debugger, history_file)

    if sys.platform != "darwin":
        # The ability to integrate fzf's result into lldb uses copy and paste.
        # In absense of copy and paste, run the selected command directly.
        temp_file = tempfile.NamedTemporaryFile("r")
        fzf_command = (
            "fzf",
            "--no-sort",
            f"--query={cmdstr}",
            f"--bind=enter:execute-silent(echo -n {{}} > {temp_file.name})+accept",
        )
        subprocess.run(fzf_command, input=history, text=True)
        command = temp_file.read()
        debugger.HandleCommand(command)
        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",
    )
    completed = subprocess.run(fzf_command, input=history, text=True)
    # 130 is used for CTRL-C or ESC.
    if completed.returncode not in (0, 130):
        result.SetError("fzf failed")
        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)


# `session history` example formatting:
#    1: first command
#    2: penultimate command
#    3: latest command
_HISTORY_PREFIX = re.compile(r"^\s+\d+:\s+")


def _load_session_history(debugger):
    """Load and parse lldb session history."""
    result = lldb.SBCommandReturnObject()
    interp = debugger.GetCommandInterpreter()
    interp.HandleCommand("session history", result)
    history = result.GetOutput()
    commands = []
    for line in history.splitlines():
        # Strip the prefix.
        command = _HISTORY_PREFIX.sub("", line)
        commands.append(command)
    return commands


def _load_persisted_history(history_file):
    """Load and decode lldb persisted history."""
    with open(history_file) as f:
        history_contents = f.read()

    # Some characters (ex spaces and newlines) are encoded as octal values, but
    # as _characters_ (not bytes). Space is the string r"\\040".
    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]
    return history_lines


def _load_history(debugger, history_file):
    """Load, decode, parse, and prepare lldb history for fzf."""
    # Persisted history is older (earlier).
    history_lines = _load_persisted_history(history_file)
    # Session history is newer (later).
    history_lines.extend(_load_session_history(debugger))

    # 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 "\n".join(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)