├── README.md ├── api_palette.py └── assets └── image-20200525213153865.png /README.md: -------------------------------------------------------------------------------- 1 | # api_palette 2 | 3 | > Originally developed by [Milan Bohacek](mailto:milan.bohacek+apipalette@gmail.com). 4 | > 5 | > This plugin wins the IDA [*Plug-In Contest 2017: Hall Of Fame*](https://www.hex-rays.com/contests_details/contest2017/). 6 | 7 | api_palette.py will be useful for those who write scripts for IDA (in the CLI or the script snippets window). 8 | 9 | 10 | 11 | ## Changes 12 | 13 | - Compatible with Python 3 and IDA 7.5 14 | - Only show/search the first line of doc 15 | - Only search api name and doc 16 | 17 | 18 | 19 | ## Usage 20 | 21 | The default shortcut is set to Shift + P. 22 | 23 | 24 | 25 | ## Screenshot 26 | 27 | ![image-20200525213153865](assets/image-20200525213153865.png) -------------------------------------------------------------------------------- /api_palette.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2016-2017 2 | # Milan Bohacek 3 | # All rights reserved. 4 | # 5 | # ============================================================================== 6 | # 7 | # This file is part of API Palette. 8 | # 9 | # API Palette is free software: you can redistribute it and/or modify it 10 | # under the terms of the GNU General Public License as published by 11 | # the Free Software Foundation, either version 3 of the License, or 12 | # (at your option) any later version. 13 | # 14 | # This program is distributed in the hope that it will be useful, but 15 | # WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 17 | # General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU General Public License 20 | # along with this program. If not, see . 21 | # 22 | # ============================================================================== 23 | 24 | import idaapi 25 | import idautils 26 | import idc 27 | from idaapi import PluginForm 28 | from PyQt5 import QtCore, QtGui, QtWidgets 29 | import PyQt5 30 | import re 31 | import inspect 32 | 33 | last_api = "" 34 | last_api_search = "" 35 | 36 | # timing = 0 37 | 38 | # -------------------------------------------------------------------------- 39 | 40 | 41 | def list_api(): 42 | api_list = [] 43 | for module in [idaapi, idautils, idc]: 44 | for i in inspect.getmembers(module, inspect.isfunction): 45 | # api_name api_content module_name 46 | api_list.append((i[0], i[1], module.__name__)) 47 | return sorted(api_list, key=lambda x: x[0]) 48 | 49 | 50 | # -------------------------------------------------------------------------- 51 | 52 | 53 | class MyEdit(QtWidgets.QLineEdit): 54 | def keyPressEvent(self, event): 55 | if event.key() in [QtCore.Qt.Key_Down, QtCore.Qt.Key_Up, QtCore.Qt.Key_PageDown, QtCore.Qt.Key_PageUp]: 56 | QtWidgets.QApplication.sendEvent(self.lst, event) 57 | 58 | QtWidgets.QLineEdit.keyPressEvent(self, event) 59 | 60 | 61 | # -------------------------------------------------------------------------- 62 | 63 | 64 | class MyApiList(QtWidgets.QListView): 65 | def keyPressEvent(self, event): 66 | super(MyApiList, self).keyPressEvent(event) 67 | 68 | def moveCursor(self, cursorAction, modifiers): 69 | idx = self.currentIndex() 70 | row = idx.row() 71 | cnt = idx.model().rowCount() 72 | 73 | if cursorAction in [QtWidgets.QAbstractItemView.MoveUp, QtWidgets.QAbstractItemView.MovePrevious]: 74 | if row == 0: 75 | cursorAction = QtWidgets.QAbstractItemView.MoveEnd 76 | 77 | if cursorAction in [QtWidgets.QAbstractItemView.MoveDown, QtWidgets.QAbstractItemView.MoveNext]: 78 | if row + 1 == cnt: 79 | cursorAction = QtWidgets.QAbstractItemView.MoveHome 80 | 81 | return super(MyApiList, self).moveCursor(cursorAction, modifiers) 82 | 83 | 84 | # -------------------------------------------------------------------------- 85 | 86 | 87 | class api_delegate(QtWidgets.QStyledItemDelegate): 88 | def __init__(self, parent): 89 | self.cached_size = None 90 | super(api_delegate, self).__init__(parent) 91 | self.template_str = "
{}{}{}
" 92 | # self.template_str = "
{}{}{}{}
" 93 | 94 | def paint(self, painter, option, index): 95 | model = index.model() 96 | row = index.row() 97 | action = model.data(model.index(row, 0, QtCore.QModelIndex())) 98 | api = model.data(model.index(row, 1, QtCore.QModelIndex())) 99 | module = model.data(model.index(row, 2, QtCore.QModelIndex())) 100 | 101 | doc = QtGui.QTextDocument() 102 | 103 | global ApiForm 104 | 105 | if len(ApiForm.regex_pattern) > 1: 106 | try: 107 | action = ApiForm.regex.sub(r'\1', action) 108 | except: 109 | pass 110 | try: 111 | api = ApiForm.regex.sub(r'\1', api) 112 | except: 113 | pass 114 | if api: 115 | api = api.lstrip("\t \n\r").split("\n")[0] 116 | 117 | document = self.template_str.format(action, api, module) 118 | doc.setHtml(document) 119 | 120 | painter.save() 121 | option.widget.style().drawControl(QtWidgets.QStyle.CE_ItemViewItem, option, painter) 122 | 123 | painter.translate(option.rect.left(), option.rect.top()) 124 | clip = QtCore.QRectF(0, 0, option.rect.width(), option.rect.height()) 125 | doc.drawContents(painter, clip) 126 | painter.restore() 127 | 128 | def sizeHint(self, option, index): 129 | if self.cached_size is None: 130 | self.initStyleOption(option, index) 131 | doc = QtGui.QTextDocument() 132 | document = self.template_str.format("action", "api", "description") 133 | doc.setHtml(document) 134 | doc.setTextWidth(option.rect.width()) 135 | self.cached_size = QtCore.QSize(int(doc.idealWidth()), int(doc.size().height())) 136 | return self.cached_size 137 | 138 | 139 | # -------------------------------------------------------------------------- 140 | 141 | 142 | class ApiFilter(QtCore.QSortFilterProxyModel): 143 | def filterAcceptsRow__(self, sourceRow, sourceParent): 144 | # t1 = time.clock() 145 | r = self.filterAcceptsRow_(sourceRow, sourceParent) 146 | # t2 = time.clock() 147 | # global timing 148 | # timing += t2-t1 149 | # return r 150 | 151 | def filterAcceptsRow(self, sourceRow, sourceParent): 152 | regex = self.filterRegExp() 153 | if len(regex.pattern()) == 0: 154 | return True 155 | 156 | m = self.sourceModel() 157 | 158 | for i in range(2): # search api name and doc 159 | if regex.indexIn(m.data(m.index(sourceRow, i, sourceParent))) != -1: 160 | return True 161 | return False 162 | 163 | 164 | # -------------------------------------------------------------------------- 165 | 166 | 167 | class ApiPaletteForm_t(QtWidgets.QDialog): 168 | def mousePressEvent(self, event): 169 | event.ignore() 170 | event.accept() 171 | if not self.rect().contains(event.pos()): 172 | close() 173 | 174 | def select(self, row): 175 | idx = self.proxyModel.index(row, 0, QtCore.QModelIndex()) 176 | self.lst.setCurrentIndex(idx) 177 | 178 | def on_text_changed(self): 179 | filter = self.filter.text() 180 | self.regex = re.compile("(%s)" % (re.escape(filter)), flags=re.IGNORECASE) 181 | self.regex_pattern = filter 182 | self.proxyModel.setFilterRegExp( 183 | QtCore.QRegExp( 184 | filter, QtCore.Qt.CaseInsensitive, QtCore.QRegExp.FixedString 185 | ) 186 | ) 187 | 188 | # self.lst.currentIndex() 189 | self.select(0) 190 | self.lst.viewport().update() 191 | 192 | def on_enter(self): 193 | self.report_action(self.lst.currentIndex()) 194 | 195 | def report_action(self, index): 196 | if not index.isValid(): 197 | return 198 | self.setResult(1) 199 | m = index.model() 200 | row = index.row() 201 | self.action_name = "%s.%s" % (m.data(m.index(row, 2)), m.data(m.index(row, 0))) 202 | global last_api_search 203 | last_api_search = self.filter.text() 204 | self.done(1) 205 | 206 | def focusOutEvent(self, event): 207 | pass 208 | 209 | # def event(self, event): 210 | # return super(ApiPaletteForm_t, self).event(event) 211 | def __init__(self, parent=None, flags=None): 212 | """ 213 | Called when the plugin form is created 214 | """ 215 | # Get parent widget 216 | # self.parent = idaapi.PluginForm.FormToPyQtWidget(form) 217 | # super(ApiPaletteForm_t, self).__init__( parent, QtCore.Qt.WindowSystemMenuHint | QtCore.Qt.WindowTitleHint ) 218 | # super(ApiPaletteForm_t, self).__init__( parent, QtCore.Qt.Popup ) 219 | # super(ApiPaletteForm_t, self).__init__( parent, QtCore.Qt.Window | QtCore.Qt.FramelessWindowHint ) 220 | super(ApiPaletteForm_t, self).__init__(parent, QtCore.Qt.Window | QtCore.Qt.FramelessWindowHint) 221 | 222 | self.setFocusPolicy(QtCore.Qt.ClickFocus) 223 | 224 | self.setWindowTitle("API Palette") 225 | self.resize(800, 500) 226 | 227 | # Create tree control 228 | self.lst = MyApiList() 229 | self.actions = list_api() 230 | self.action_name = None 231 | 232 | self.model = QtGui.QStandardItemModel(len(self.actions), 3) 233 | 234 | self.proxyModel = ApiFilter() 235 | self.proxyModel.setDynamicSortFilter(True) 236 | 237 | self.model.setHeaderData(0, QtCore.Qt.Horizontal, "api") 238 | self.model.setHeaderData(1, QtCore.Qt.Horizontal, "doc") 239 | self.model.setHeaderData(2, QtCore.Qt.Horizontal, "module") 240 | 241 | for row, i in enumerate(self.actions): 242 | self.model.setData(self.model.index(row, 0, QtCore.QModelIndex()), i[0]) # api name 243 | # first line of the doc or function definition 244 | if i[1].__doc__: 245 | doc = i[1].__doc__.lstrip().split('\n', 1)[0] 246 | else: 247 | # XXX: add try-except if we add more modules 248 | doc = inspect.getsource(i[1]).partition('\n')[0] 249 | self.model.setData(self.model.index(row, 1, QtCore.QModelIndex()), doc) 250 | self.model.setData(self.model.index(row, 2, QtCore.QModelIndex()), i[2]) # module name 251 | 252 | self.proxyModel.setSourceModel(self.model) 253 | self.lst.setModel(self.proxyModel) 254 | 255 | global last_api_search 256 | 257 | self.filter = MyEdit(last_api_search) 258 | self.regex = re.compile("(%s)" % re.escape(last_api_search), flags=re.IGNORECASE) 259 | self.regex_pattern = last_api_search 260 | 261 | self.proxyModel.setFilterRegExp( 262 | QtCore.QRegExp(self.regex_pattern, QtCore.Qt.CaseInsensitive, QtCore.QRegExp.FixedString)) 263 | 264 | self.filter.setMaximumHeight(30) 265 | self.filter.textChanged.connect(self.on_text_changed) 266 | self.filter.returnPressed.connect(self.on_enter) 267 | 268 | self.lst.clicked.connect(self.on_clicked) 269 | # self.lst.activated.connect(self.on_activated) 270 | 271 | self.lst.setSelectionMode(1) # QtSingleSelection 272 | 273 | self.lst.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) 274 | self.lst.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows) 275 | self.lst.setItemDelegate(api_delegate(self.lst)) 276 | 277 | # self.lst.setSectionResizeMode( QtWidgets.QHeaderView.Fixed ) 278 | 279 | self.filter.lst = self.lst 280 | self.lst.filter = self.filter 281 | self.filter.setStyleSheet('border: 0px solid black; border-bottom:0px;') 282 | self.lst.setStyleSheet('QListView{border: 0px solid black; background-color: #F0F0F0;}; ') 283 | 284 | # self.completer = QtWidgets.QCompleter(self.model) 285 | # self.completer.setCompletionMode(QtWidgets.QCompleter.InlineCompletion) 286 | # self.filter.setCompleter(self.completer) 287 | 288 | # Create layout 289 | layout = QtWidgets.QVBoxLayout() 290 | layout.setSpacing(0) 291 | layout.addWidget(self.filter) 292 | layout.addWidget(self.lst) 293 | 294 | # Populate PluginForm 295 | self.setLayout(layout) 296 | self.filter.setFocus() 297 | self.filter.selectAll() 298 | 299 | found = False 300 | if len(last_api) > 0: 301 | for row in range(self.proxyModel.rowCount()): 302 | idx = self.proxyModel.index(row, 0) 303 | if self.proxyModel.data(idx) == last_api: 304 | self.lst.setCurrentIndex(idx) 305 | found = True 306 | break 307 | if not found: 308 | self.lst.setCurrentIndex(self.proxyModel.index(0, 0)) 309 | 310 | def on_clicked(self, item): 311 | self.report_action(item) 312 | 313 | def on_activated(self, item): 314 | self.report_action(item) 315 | 316 | 317 | # -------------------------------------------------------------------------- 318 | def AskForAPI(): 319 | global ApiForm 320 | main_window = [ 321 | x 322 | for x in QtWidgets.QApplication.topLevelWidgets() 323 | if isinstance(x, QtWidgets.QMainWindow) 324 | ][0] 325 | ApiForm = ApiPaletteForm_t(main_window) 326 | ApiForm.setModal(True) 327 | idaapi.disable_script_timeout() 328 | 329 | # ApiForm.setStyleSheet("background:transparent;"); 330 | ApiForm.setAttribute(QtCore.Qt.WA_DeleteOnClose, True) 331 | # ApiForm.setAttribute(QtCore.Qt.WA_TranslucentBackground, True); 332 | 333 | result = None 334 | 335 | if ApiForm.exec_() == 1: 336 | global last_api 337 | last_api = ApiForm.action_name 338 | result = last_api 339 | 340 | del ApiForm 341 | return result 342 | 343 | 344 | # -------------------------------------------------------------------------- 345 | 346 | 347 | # https://stackoverflow.com/questions/35762856/how-can-i-move-the-keyboard-cursor-focus-to-a-qlineedit/43383330#43383330 348 | # QPoint pos(line_edit->width()-5, 5); 349 | # QMouseEvent e(QEvent::MouseButtonPress, pos, Qt::LeftButton, Qt::LeftButton, 0); 350 | # qApp->sendEvent(line_edit, &e); 351 | # QMouseEvent f(QEvent::MouseButtonRelease, pos, Qt::LeftButton, Qt::LeftButton, 0); 352 | # qApp->sendEvent(line_edit, &f); 353 | def set_focus_on_qplaintextedit(control): 354 | r = control.cursorRect() 355 | pos = QtCore.QPoint(r.left(), r.top()) 356 | # pos = control.mapToGlobal(pos) 357 | ev = QtGui.QMouseEvent( 358 | PyQt5.QtGui.QMouseEvent.MouseButtonPress, 359 | pos, 360 | QtCore.Qt.LeftButton, 361 | QtCore.Qt.LeftButton, 362 | QtCore.Qt.NoModifier, 363 | ) 364 | ev2 = QtGui.QMouseEvent( 365 | PyQt5.QtGui.QMouseEvent.MouseButtonRelease, 366 | pos, 367 | QtCore.Qt.LeftButton, 368 | QtCore.Qt.LeftButton, 369 | QtCore.Qt.NoModifier, 370 | ) 371 | control.mousePressEvent(ev) 372 | control.mouseReleaseEvent(ev2) 373 | control.activateWindow() 374 | 375 | 376 | class api_palette_ah(idaapi.action_handler_t): 377 | def __init__(self): 378 | idaapi.action_handler_t.__init__(self) 379 | 380 | def activate(self, ctx): 381 | global control 382 | control = QtWidgets.QApplication.focusWidget() 383 | action = AskForAPI() 384 | 385 | if action: 386 | r = repr(control) 387 | if "QPlainTextEdit" in r: 388 | control.insertPlainText(action + "(") 389 | set_focus_on_qplaintextedit(control) 390 | 391 | elif "QLineEdit" in r: 392 | control.insert(action + "(") 393 | control.setFocus() 394 | else: 395 | CLI_append(action) 396 | # control.insert(action + "(") 397 | return 1 398 | 399 | def update(self, ctx): 400 | return idaapi.AST_ENABLE_ALWAYS 401 | 402 | 403 | api_palette_action_desc = idaapi.action_desc_t( 404 | "mb:api_palette", 405 | "API Palette", 406 | api_palette_ah(), 407 | "Shift+P", 408 | "Opens Sublime-like api palette.", 409 | -1, 410 | ) 411 | 412 | 413 | def api_register_actions(): 414 | idaapi.register_action(api_palette_action_desc) 415 | 416 | 417 | def api_unregister_actions(): 418 | idaapi.unregister_action(api_palette_action_desc.name) 419 | 420 | 421 | def CLI_append(text): 422 | main_window = [ 423 | x 424 | for x in QtWidgets.QApplication.topLevelWidgets() 425 | if isinstance(x, QtWidgets.QMainWindow) 426 | ][0] 427 | # Locate Output window and insert text 428 | # Here we rely on only one QGroupBox in IDA interface, and it's in the Output window. 429 | output = main_window.findChild(QtWidgets.QGroupBox) 430 | ed = output.findChild(QtWidgets.QLineEdit) 431 | ed.insert(text + "(") 432 | ed.setFocus() 433 | 434 | 435 | class APIPalettePlugin(idaapi.plugin_t): 436 | flags = idaapi.PLUGIN_FIX | idaapi.PLUGIN_HIDE 437 | comment = "Sublime-like api palette for IDA" 438 | help = "Sublime-like api palette" 439 | wanted_name = "api palette" 440 | wanted_hotkey = "" 441 | 442 | def init(self): 443 | addon = idaapi.addon_info_t() 444 | addon.id = "milan.bohacek.api_palette" 445 | addon.name = "API Palette" 446 | addon.producer = "Milan Bohacek" 447 | addon.url = "milan.bohacek+apipalette@gmail.com" 448 | addon.version = "7.00" 449 | idaapi.register_addon(addon) 450 | api_register_actions() 451 | 452 | return idaapi.PLUGIN_KEEP 453 | 454 | def term(self): 455 | api_unregister_actions() 456 | pass 457 | 458 | def run(self, arg): 459 | pass 460 | 461 | 462 | def PLUGIN_ENTRY(): 463 | return APIPalettePlugin() 464 | -------------------------------------------------------------------------------- /assets/image-20200525213153865.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xKira/api_palette/542ccc7ab9b675b005c5cf1f0a9172995e8a48db/assets/image-20200525213153865.png --------------------------------------------------------------------------------