Skip to content

Commit

Permalink
Feat: Add API for tracking mouse movements (#136)
Browse files Browse the repository at this point in the history
  • Loading branch information
mahaloz authored Nov 12, 2024
1 parent a1fc519 commit 669215d
Show file tree
Hide file tree
Showing 5 changed files with 56 additions and 11 deletions.
2 changes: 1 addition & 1 deletion libbs/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
__version__ = "2.6.0"
__version__ = "2.7.0"


import logging
Expand Down
2 changes: 2 additions & 0 deletions libbs/api/decompiler_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ def __init__(
decompiler_closed_callbacks: Optional[List[Callable]] = None,
thread_artifact_callbacks: bool = True,
force_click_recording: bool = False,
track_mouse_moves: bool = False,
):
self.name = name
self.art_lifter = artifact_lifter
Expand All @@ -86,6 +87,7 @@ def __init__(
self.gui_plugin = None
self.artifact_watchers_started = False
self.force_click_recording = force_click_recording
self.track_mouse_moves = track_mouse_moves

# locks
self.artifact_write_lock = threading.Lock()
Expand Down
22 changes: 21 additions & 1 deletion libbs/artifacts/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,42 @@


class Context(Artifact):
ACT_VIEW_OPEN = "view_open"
ACT_MOUSE_CLICK = "mouse_click"
ACT_MOUSE_MOVE = "mouse_move"
ACT_UNKNOWN = "unknown"

__slots__ = Artifact.__slots__ + (
"addr",
"func_addr",
"line_number",
"col_number",
"screen_name",
"variable"
"variable",
"action",
"extras",
)

def __init__(
self,
addr: Optional[int] = None,
func_addr: Optional[int] = None,
line_number: Optional[int] = None,
col_number: Optional[int] = None,
screen_name: Optional[str] = None,
variable: Optional[str] = None,
action: Optional[str] = None,
extras: Optional[dict] = None,
**kwargs
):
self.addr = addr
self.func_addr = func_addr
self.line_number = line_number
self.col_number = col_number
self.screen_name = screen_name
self.variable = variable
self.action: str = action or self.ACT_UNKNOWN
self.extras = extras or {}
super().__init__(**kwargs)

def __str__(self):
Expand All @@ -37,5 +51,11 @@ def __str__(self):
post_text = hex(self.addr) + post_text
if self.line_number is not None:
post_text += f" line={self.line_number}"
if self.col_number is not None:
post_text += f" col={self.col_number}"
if self.action != self.ACT_UNKNOWN:
post_text += f" action={self.action}"
if self.extras:
post_text += f" extras={self.extras}"

return f"<Context {post_text}>"
14 changes: 10 additions & 4 deletions libbs/decompilers/ida/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -1671,27 +1671,33 @@ def get_decompiler_version() -> typing.Optional[Version]:
return vers


def view_to_bs_context(view, get_var=True) -> typing.Optional[Context]:
def view_to_bs_context(view, get_var=True, action: str = Context.ACT_UNKNOWN) -> typing.Optional[Context]:
form_type = idaapi.get_widget_type(view)
if form_type is None:
return None

form_to_type_name = get_form_to_type_name()
view_name = form_to_type_name.get(form_type, "unknown")
ctx = Context(screen_name=view_name)
ctx = Context(screen_name=view_name, action=action)
if view_name in FUNC_FORMS:
ctx.addr = idaapi.get_screen_ea()
func = idaapi.get_func(ctx.addr)
if func is not None:
ctx.func_addr = func.start_ea
if get_var and view_name == "decompilation":
# exit early when we are still rendering the screen (no real click info)
if action == Context.ACT_MOUSE_MOVE:
return ctx

if view_name == "decompilation" and get_var:
# get lvar info at cursor
vu = idaapi.get_widget_vdui(view)
if vu and vu.item:
lvar = vu.item.get_lvar()
if lvar:
ctx.variable = lvar.name
ctx.line_number = vu.cpos.lnnum if vu.cpos else None
if vu.cpos is not None:
ctx.line_number = vu.cpos.lnnum
ctx.col_number = vu.cpos.x

return ctx

Expand Down
27 changes: 22 additions & 5 deletions libbs/decompilers/ida/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,26 +89,43 @@ def __init__(self, interface: "IDAInterface"):
super(ScreenHook, self).__init__()

def view_click(self, view, event):
self._handle_view_event(view, click=True)
self._handle_view_event(view, action_type=Context.ACT_MOUSE_CLICK)

def view_activated(self, view: "TWidget *"):
self._handle_view_event(view)
self._handle_view_event(view, action_type=Context.ACT_VIEW_OPEN)

def _handle_view_event(self, view, click=False):
def view_mouse_moved(self, view: "TWidget *", event: "view_mouse_event_t"):
if self.interface.track_mouse_moves:
self._handle_view_event(view, ida_event=event, action_type=Context.ACT_MOUSE_MOVE)

def _handle_view_event(self, view, action_type=Context.ACT_UNKNOWN, ida_event=None):
if self.interface.force_click_recording or self.interface.artifact_watchers_started:
# drop ctx for speed when the artifact watches have not been officially started, and we are not clicking
if (self.interface.force_click_recording and not self.interface.artifact_watchers_started) and not click:
if (self.interface.force_click_recording and not self.interface.artifact_watchers_started) and \
action_type != Context.ACT_MOUSE_CLICK:
return

ctx = compat.view_to_bs_context(view)
ctx = compat.view_to_bs_context(view, action=action_type)
if ctx is None:
return

# handle special case of mouse move
if action_type == Context.ACT_MOUSE_MOVE and ida_event is not None:
ctx.line_number = ida_event.renderer_pos.cy
ctx.col_number = ida_event.renderer_pos.cx
if ctx.screen_name == "disassembly" and ida_event.renderer_pos.node != -1:
# TODO: this is not an addr, but the node number in graph view
ctx.extras['node'] = ida_event.renderer_pos.node
elif ctx.screen_name == "decompilation":
# TODO: the address is useless here!
ctx.addr = ctx.func_addr

ctx = self.interface.art_lifter.lift(ctx)
self.interface._gui_active_context = ctx

self.interface.gui_context_changed(ctx)


class IDAHotkeyHook(ida_kernwin.UI_Hooks):
def __init__(self, keys_to_pass, uiptr):
super().__init__()
Expand Down

0 comments on commit 669215d

Please sign in to comment.