diff --git a/.gitmodules b/.gitmodules old mode 100644 new mode 100755 index ea4702b..76ccef3 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,6 @@ [submodule "palette/ida-cmake"] path = palette/ida-cmake - url = https://github.com/Jinmo/ida-cmake.git + url = https://github.com/blue-devil/ida-cmake.git [submodule "palette/pybind11"] path = palette/pybind11 url = https://github.com/pybind/pybind11.git diff --git a/CHANGELOG.md b/CHANGELOG.md index 57eeb4e..7a249bd 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,55 @@ # Changelog -## [2025-12-10] +## [2026-09-03][2026.09.03] + +### Added + +* IDA Pro v9.4 support - rebuilt against the IDA Pro SDK v9.4 (hotfix 1) +* Windows on ARM64 (aarch64) support +* `Edit > Plugins > ifred` menu with **Settings** and **About** entries +* Settings window with two tabs: a **Theme Selector** and an in-place + **config.json** editor +* Light theme, and a gitkraken-light theme (gitkraken is now split into + gitkraken-dark / gitkraken-light) +* Version information embedded in the built binaries, stamped with the build + date (`YYYY.MM.DD`): a Windows VERSIONINFO resource, the Mach-O + `current_version` on macOS, and a greppable `ifred version ...` string on all + platforms + +### Fixed + +* Replaced the IDA SDK notification functions deprecated in 9.4 + (`hook_to_notification_point` / `unhook_from_notification_point`) with the new + event-listener API: dedicated `event_listener_t` listener classes registered + via `hook_event_listener` / `unhook_event_listener`. The plugin no longer + calls any deprecated SDK function. +* gitkraken theme: list rows were unreadable (text drawn in the background + colour); reworked and split into gitkraken-dark / gitkraken-light +* ayudark theme: black/invisible text, wrong sizing and a missing search icon +* Search-box placeholder ("Enter action or option name...") was invisible on + the ayudark / ayumirage / ayuwhite themes +* macOS arm64 binary is now stripped at link time (`-Wl,-x`), which keeps + `LC_ID_DYLIB` intact so the plugin loads - a post-build `strip` removed it +* Silenced a signed/unsigned comparison warning and fixed undefined behaviour + when reading an enum back through `va_arg` in the type enumeration + +### Changed + +* Every theme now lives in its own folder under `theme/`, so users can add or + remove themes and switch between them live from Settings while working in + IDA Pro +* The default (built-in) theme is now a real `dark/` folder; ifred ships + dark, light, solarized-dark and solarized-light (the remaining themes stay in + the repository for manual installation) +* The palette font is now resolved per-OS and set with a single-value CSS + `font-family` (Qt does not reliably honour multi-family lists in the palette's + rich-text rows) + +## [2026-02-16][2026.02.16] + +This release is just a rebuilt for IDA Pro v9.3 + +## [2025-12-10][2025.12.10] ### Added @@ -23,7 +72,7 @@ palette by using shortcut, then IDA pro crashes. Now it opens and empty palette if no binary have loaded to prevent crash. -## [2025-09-14] +## [2025-09-14][2025.09.14] ### Added @@ -35,13 +84,15 @@ * Qt5 support removed * Created a new branch for IDA pro v9.1 and Qt5 -## [2024-10-29][2024-10-29] +## [2024-10-29][2024.10.29] ### Added * IDA Pro v9.0 support * Prebuilt binaries provided -[2025-12-10]: https://github.com/blue-devil/ifred/compare/v2025.09.14...HEAD -[2025-09-14]: https://github.com/blue-devil/ifred/compare/v2024.10.29...v2025.09.14 -[2024-10-29]: https://github.com/blue-devil/ifred/releases/tag/v2024.10.29 +[2026.09.03]: https://github.com/blue-devil/ifred/compare/v2026.02.16...HEAD +[2026.02.16]: https://github.com/blue-devil/ifred/compare/v2025.12.10...v2026.02.16 +[2025.12.10]: https://github.com/blue-devil/ifred/compare/v2025.09.14...v2025.12.10 +[2025.09.14]: https://github.com/blue-devil/ifred/compare/v2024.10.29...v2025.09.14 +[2024.10.29]: https://github.com/blue-devil/ifred/releases/tag/v2024.10.29 diff --git a/README.md b/README.md index a4f1561..5fcaed9 100644 --- a/README.md +++ b/README.md @@ -9,12 +9,14 @@ Tested on all operating systems that run IDA Pro. -| OS | Arch | Status | -| ------- | ------ | ------ | -| Linux | x86_64 | ✅ | -| macOS | x86_64 | ✅ | -| macOS | arm64 | ✅ | -| Windows | x86_64 | ✅ | +| OS | Arch | Status | +| ------- | ------- | ------ | +| Linux | x86_64 | ✅ | +| Linux | aarch64 | ✅ | +| macOS | x86_64 | ✅ | +| macOS | aarch64 | ✅ | +| Windows | x86_64 | ✅ | +| Windows | aarch64 | ✅ | Currently this repo supports IDA Pro with Qt6. For IDA Pro v9.1 and earlier with Qt5 support head to [qt5 branch][04]. To build you need IDA Pro SDK. @@ -103,9 +105,18 @@ solarized dark: ![screenshot3][03] -[01]: screenshots/1.png -[02]: screenshots/2.png -[03]: screenshots/3.png +## TODO + +* [ ] Support HCLI (Will think about this) +* [ ] Proper versioning (HCLI cries for date versioning) +* [ ] Palette translation map + * [x] Map ready +* [ ] Is `standalone` working, check this +* [ ] Custom palette using IDAPython does it still working? + +[01]: ./assets/screenshots/1.png +[02]: ./assets/screenshots/2.png +[03]: ./assets/screenshots/3.png [04]: https://github.com/Jinmo/ifred/tree/qt5 [05]: https://github.com/Vector35/qt-artifacts/releases [06]: https://github.com/blue-devil/ifred/releases diff --git a/assets/cover-og-1280x640.jpg b/assets/cover-og-1280x640.jpg new file mode 100644 index 0000000..dbe5c9d Binary files /dev/null and b/assets/cover-og-1280x640.jpg differ diff --git a/assets/ifred-banner-896x280.png b/assets/ifred-banner-896x280.png new file mode 100644 index 0000000..efca152 Binary files /dev/null and b/assets/ifred-banner-896x280.png differ diff --git a/assets/ifred-logo-square-640x640.png b/assets/ifred-logo-square-640x640.png new file mode 100644 index 0000000..7b0ffd0 Binary files /dev/null and b/assets/ifred-logo-square-640x640.png differ diff --git a/screenshots/1.png b/assets/screenshots/1.png similarity index 100% rename from screenshots/1.png rename to assets/screenshots/1.png diff --git a/screenshots/2.png b/assets/screenshots/2.png similarity index 100% rename from screenshots/2.png rename to assets/screenshots/2.png diff --git a/screenshots/3.png b/assets/screenshots/3.png similarity index 100% rename from screenshots/3.png rename to assets/screenshots/3.png diff --git a/ida-plugin.json b/ida-plugin.json old mode 100644 new mode 100755 index 501afcd..cb46391 --- a/ida-plugin.json +++ b/ida-plugin.json @@ -2,7 +2,7 @@ "IDAMetadataDescriptorVersion": 2, "plugin": { "name": "ifred", - "version": "0.0.5", + "version": "2026.09.03", "description": "IDA command palette & more", "entryPoint": "ida_palette64", "authors": [ @@ -17,10 +17,13 @@ "license": "MIT", "platforms": [ "windows-x86_64", + "windows-aarch64", "linux-x86_64", + "linux-aarch64", + "macos-x86_64", "macos-aarch64" ], - "idaVersions": ["9.2"], + "idaVersions": ["9.4"], "keywords": ["palette", "command", "launcher", "fuzzy"] } } diff --git a/palette/ida-cmake b/palette/ida-cmake index 0c95425..dbc9a72 160000 --- a/palette/ida-cmake +++ b/palette/ida-cmake @@ -1 +1 @@ -Subproject commit 0c954255e59a02407a986cb3276a191704094a86 +Subproject commit dbc9a7221771c990d0225fb32dbb1f2f547b7473 diff --git a/palette/include/palette/utils.h b/palette/include/palette/utils.h old mode 100644 new mode 100755 index 9d3a2c8..5dd72e2 --- a/palette/include/palette/utils.h +++ b/palette/include/palette/utils.h @@ -14,4 +14,21 @@ QString loadFile(const char* filename, bool force_update = false, // File handler typedef QString (*pathhandler_t)(char const* path); -extern pathhandler_t pluginPath; \ No newline at end of file +extern pathhandler_t pluginPath; + +// Themes. +// +// A theme is a directory /theme// holding window.css (required) +// plus optional styles.json and images. Which one is active is the "theme" key +// of /config.json; with no key at all, PALETTE_DEFAULT_THEME is used. +// A theme file that does not exist on disk falls back to the bundled copy, so a +// partial theme still works. The whole theme/ tree is folders only - there are +// no top-level theme files. +#define PALETTE_DEFAULT_THEME "dark" + +// Every theme directory on disk (one with a window.css), sorted. +QStringList availableThemes(); +QString currentTheme(); +// Persists the choice to config.json and makes it effective for the next +// palette shown. Returns false if config.json could not be written. +bool setCurrentTheme(const QString& name); diff --git a/palette/res/theme/ayudark/bg.png b/palette/res/theme/ayudark/bg.png deleted file mode 100644 index c64302d..0000000 Binary files a/palette/res/theme/ayudark/bg.png and /dev/null differ diff --git a/palette/res/theme/ayudark/input-search.png b/palette/res/theme/ayudark/input-search.png new file mode 100755 index 0000000..541548d Binary files /dev/null and b/palette/res/theme/ayudark/input-search.png differ diff --git a/palette/res/theme/ayudark/item.css b/palette/res/theme/ayudark/item.css deleted file mode 100644 index 7057895..0000000 --- a/palette/res/theme/ayudark/item.css +++ /dev/null @@ -1,19 +0,0 @@ -div { - font-size: 18px; - font-family: Segoe UI; - color: rgb(191, 186, 176); - color: rgb(71, 83, 89); -} - -em { - font-weight: bold; - font-style: normal; - color: #ff8f40; -} - -span { - font-size: 13px; - font-family: Segoe UI; - color: transparent; - display: none; -} \ No newline at end of file diff --git a/palette/res/theme/ayudark/searchbox.css b/palette/res/theme/ayudark/searchbox.css deleted file mode 100644 index c54449c..0000000 --- a/palette/res/theme/ayudark/searchbox.css +++ /dev/null @@ -1,11 +0,0 @@ -QLineEdit { - font-family: Segoe UI; - height: 62px !important; - font-size: 25px; - color: rgb(191, 186, 176); - border: none; - width: 500px; - padding-left: 60px; - background: url(C:/Users/berry/take/theme/bg.png) no-repeat; - background-color: rgb(19, 24, 29); -} \ No newline at end of file diff --git a/palette/res/theme/ayudark/styles.json b/palette/res/theme/ayudark/styles.json index ab1c313..a2f9f19 100644 --- a/palette/res/theme/ayudark/styles.json +++ b/palette/res/theme/ayudark/styles.json @@ -1,7 +1,3 @@ -{ - "itemHeight": 47, - "itemMarginLeft": 25, - "itemMarginTop": 8, - "itemHoverBackground": "#0c1014", - "shadow": 5 -} \ No newline at end of file +{ + "shadow": 15 +} diff --git a/palette/res/theme/ayudark/window.css b/palette/res/theme/ayudark/window.css old mode 100644 new mode 100755 index 72b185a..6b4ce54 --- a/palette/res/theme/ayudark/window.css +++ b/palette/res/theme/ayudark/window.css @@ -1,44 +1,161 @@ +/* + Ayu Dark Theme for ifred + Author : Jinmo + Repo : github.com/jinmo + Palette : https://github.com/ayu-theme/ayu-colors +*/ + * { - font-family: Segoe UI; - font-size: 15px; - background: rgb(19, 24, 29); - border-radius: 6px; + font-family: "Segoe UI", "Ubuntu", "DejaVu Sans", sans-serif; + font-size: 18px; + background: #0b0e14; + border-radius: 6px; +} + +QMainWindow, +QLineEdit, +QListView { + width: 700px; +} + +QFrame { + background: #0b0e14; } +/* Searchbox with the magnifier icon */ + +QLineEdit { + font-family: "Segoe UI", "Ubuntu", "DejaVu Sans", sans-serif; + height: 62px !important; + font-size: 25px; + color: #bfbdb6; + border: none; + padding-left: 80px; + /* + input-search.png is a gray magnifier on a transparent background, so the + field colour must be set explicitly - otherwise Qt paints the default + white QLineEdit base through the transparent areas. Use the longhand + background-* properties: the "background: url() ..." shorthand is + rejected by Qt's stylesheet parser, and one bad rule makes Qt drop the + whole stylesheet (everything reverts to the default white). + */ + background-color: #0b0e14; + background-image: url(theme:input-search.png); + background-repeat: no-repeat; +} + +/* Scrollbar */ + QScrollBar:vertical * { - background-color: #000; + background-color: #0b0e14; } QScrollBar:vertical { - border: none; - background: none; - background-color: #000; - width: 12px; - border-radius: 6px; + border: none; + width: 12px; + border-radius: 6px; + background: transparent; +} + +QScrollBar::add-page:vertical, +QScrollBar::sub-page:vertical { + background: #11151c; } QScrollBar::handle:vertical { - background: rgba(255,255,255,0.3); - min-height: 20px; - border-radius: 6px; + background: rgba(191, 189, 182, 0.28); + min-height: 20px; + border-radius: 6px; } QScrollBar::add-line:vertical { - width: 0; - height: 0; - subcontrol-position: bottom; - subcontrol-origin: margin; + width: 0; + height: 0; + subcontrol-position: bottom; + subcontrol-origin: margin; } QScrollBar::sub-line:vertical { - width: 0; - height: 0; - subcontrol-position: top; - subcontrol-origin: margin; + width: 0; + height: 0; + subcontrol-position: top; + subcontrol-origin: margin; } +/* List view and items */ + QListView { - border: none; - min-height: 300px; - max-height: 300px; -} \ No newline at end of file + border: none; + min-height: 300px; + max-height: 300px; +} + +QListView::item { + padding-top: 8px; + padding-left: 25px; +} + +QListView::item:hover { + background: #0b0e14; +} + +QListView::item:selected { + background: #1b2433; +} + +/* Item text (rendered as HTML in a QTextDocument) */ + +table, +td, +div, +p, +span, +em { + background: transparent; +} + +table { + color: #bfbdb6; + font-size: 18px; + font-family: "Segoe UI", "Ubuntu", "DejaVu Sans", sans-serif; + margin-bottom: 18px; +} + +table.selected { + color: #ffffff; +} + +td.placeholder { + font-size: 1px; + width: 700px; +} + +td { + padding: 0; + margin: 0; + vertical-align: middle; +} + +td.shortcut { + float: right; + padding-right: 32px; +} + +td.description { + color: #565b66; + font-size: 14px; +} + +em { + /* highlighted (matched) characters */ + font-weight: bold; + color: #ff8f40; + font-style: normal; +} + +span { + /* action id text - kept invisible */ + color: transparent; + font-family: "Segoe UI", "Ubuntu", "DejaVu Sans", sans-serif; + padding-left: 20px; +} diff --git a/palette/res/theme/ayumirage/window.css b/palette/res/theme/ayumirage/window.css index 9e0d7a3..06eaa35 100644 --- a/palette/res/theme/ayumirage/window.css +++ b/palette/res/theme/ayumirage/window.css @@ -1,131 +1,140 @@ +/* + Ayu Mirage Theme for ifred + Author : Jinmo + Repo : github.com/jinmo + Palette : https://github.com/ayu-theme/ayu-colors +*/ * { - font-family: Segoe UI; - font-size: 18px; - background-color: #232834; - border-radius: 6px; + font-family: Segoe UI; + font-size: 18px; + background-color: #232834; + border-radius: 6px; } -QMainWindow, QLineEdit, QListView { - width: 700px; +QMainWindow, +QLineEdit, +QListView { + width: 700px; } QLineEdit { - font-family: Segoe UI; - height: 62px !important; - font-size: 25px; - color: #cbccc6; - border: none; - padding-left: 80px; - background: url(theme:input-search.png) no-repeat; - background-color: #232834; -} - -QLineEdit[text=""] { - color: #232834; + font-family: Segoe UI; + height: 62px !important; + font-size: 25px; + color: #cbccc6; + border: none; + padding-left: 80px; + background: url(theme:input-search.png) no-repeat; + background-color: #232834; } QScrollBar:vertical * { - background-color: #fff; + background-color: #fff; } QScrollBar:vertical { - border: none; - width: 12px; - border-radius: 6px; - background: transparent; + border: none; + width: 12px; + border-radius: 6px; + background: transparent; } -QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { - background: rgb(245,246,246); +QScrollBar::add-page:vertical, +QScrollBar::sub-page:vertical { + background: rgb(245, 246, 246); } QScrollBar::handle:vertical { - background: rgb(219,220,221); - min-height: 20px; - border-radius: 6px; + background: rgb(219, 220, 221); + min-height: 20px; + border-radius: 6px; } QScrollBar::add-line:vertical { - width: 0; - height: 0; - subcontrol-position: bottom; - subcontrol-origin: margin; + width: 0; + height: 0; + subcontrol-position: bottom; + subcontrol-origin: margin; } QScrollBar::sub-line:vertical { - width: 0; - height: 0; - subcontrol-position: top; - subcontrol-origin: margin; + width: 0; + height: 0; + subcontrol-position: top; + subcontrol-origin: margin; } QListView { - border: none; - min-height: 300px; - max-height: 300px; + border: none; + min-height: 300px; + max-height: 300px; } QListView::item { - padding-top: 8px; - padding-left: 25px; + padding-top: 8px; + padding-left: 25px; } QListView::item:hover { - background: transparent; + background: transparent; } QListView::item:selected { - background: #191e2a; + background: #191e2a; } -table, td, div, p, span, em { - background: transparent; +table, +td, +div, +p, +span, +em { + background: transparent; } table { - color: rgb(80, 87, 97); - font-size: 18px; - font-family: Segoe UI; - margin-bottom: 18px; + color: rgb(80, 87, 97); + font-size: 18px; + font-family: Segoe UI; + margin-bottom: 18px; } table.selected { - color: #cbccc6; + color: #cbccc6; } td.placeholder { - font-size: 1px; - width: 700px; + font-size: 1px; + width: 700px; } td { - padding: 0; - margin: 0; - vertical-align: middle; + padding: 0; + margin: 0; + vertical-align: middle; } td.shortcut { - float: right; - padding-right: 32px; + float: right; + padding-right: 32px; } td.description { - color: rgb(185, 190, 195); - font-size: 14px; + color: rgb(185, 190, 195); + font-size: 14px; } em { - /* Highlighted Text */ - font-weight: bold; - color: rgb(255, 204, 102); - font-style: normal; + /* Highlighted Text */ + font-weight: bold; + color: rgb(255, 204, 102); + font-style: normal; } span { - color: transparent; - /* Action ID text */ - font-family: Segoe UI; - padding-left: 20px; - color: transparent; + color: transparent; + /* Action ID text */ + font-family: Segoe UI; + padding-left: 20px; + color: transparent; } - diff --git a/palette/res/theme/ayuwhite/window.css b/palette/res/theme/ayuwhite/window.css index a8ed200..545137d 100644 --- a/palette/res/theme/ayuwhite/window.css +++ b/palette/res/theme/ayuwhite/window.css @@ -1,134 +1,143 @@ +/* + Ayu White Theme for ifred + Author : Jinmo + Repo : github.com/jinmo + Palette : https://github.com/ayu-theme/ayu-colors +*/ * { - font-family: Segoe UI; - font-size: 18px; - background: #fff; - border-radius: 6px; + font-family: Segoe UI; + font-size: 18px; + background: #fff; + border-radius: 6px; } -QMainWindow, QLineEdit, QListView { - width: 700px; +QMainWindow, +QLineEdit, +QListView { + width: 700px; } QFrame { - background: #fff; + background: #fff; } QLineEdit { - font-family: Segoe UI; - height: 62px !important; - font-size: 25px; - color: rgb(97, 103, 107); - border: none; - padding-left: 80px; - background: url(theme:input-search.png) no-repeat; -} - -QLineEdit[text=""] { - color: #fff; + font-family: Segoe UI; + height: 62px !important; + font-size: 25px; + color: rgb(97, 103, 107); + border: none; + padding-left: 80px; + background: url(theme:input-search.png) no-repeat; } QScrollBar:vertical * { - background-color: #fff; + background-color: #fff; } QScrollBar:vertical { - border: none; - width: 12px; - border-radius: 6px; - background: transparent; + border: none; + width: 12px; + border-radius: 6px; + background: transparent; } -QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { - background: rgb(245,246,246); +QScrollBar::add-page:vertical, +QScrollBar::sub-page:vertical { + background: rgb(245, 246, 246); } QScrollBar::handle:vertical { - background: rgb(219,220,221); - min-height: 20px; - border-radius: 6px; + background: rgb(219, 220, 221); + min-height: 20px; + border-radius: 6px; } QScrollBar::add-line:vertical { - width: 0; - height: 0; - subcontrol-position: bottom; - subcontrol-origin: margin; + width: 0; + height: 0; + subcontrol-position: bottom; + subcontrol-origin: margin; } QScrollBar::sub-line:vertical { - width: 0; - height: 0; - subcontrol-position: top; - subcontrol-origin: margin; + width: 0; + height: 0; + subcontrol-position: top; + subcontrol-origin: margin; } QListView { - border: none; - min-height: 300px; - max-height: 300px; + border: none; + min-height: 300px; + max-height: 300px; } QListView::item { - padding-top: 8px; - padding-left: 25px; + padding-top: 8px; + padding-left: 25px; } QListView::item:hover { - background: #fff; + background: #fff; } QListView::item:selected { - background: #f5f5f5; + background: #f5f5f5; } -table, td, div, p, span, em { - background: transparent; +table, +td, +div, +p, +span, +em { + background: transparent; } table { - color: rgb(149, 157, 166); - font-size: 18px; - font-family: Segoe UI; - margin-bottom: 18px; + color: rgb(149, 157, 166); + font-size: 18px; + font-family: Segoe UI; + margin-bottom: 18px; } table.selected { - color: rgb(107, 118, 128); + color: rgb(107, 118, 128); } td.placeholder { - font-size: 1px; - width: 700px; + font-size: 1px; + width: 700px; } td { - padding: 0; - margin: 0; - vertical-align: middle; + padding: 0; + margin: 0; + vertical-align: middle; } td.shortcut { - float: right; - padding-right: 32px; + float: right; + padding-right: 32px; } td.description { - color: rgb(185, 190, 195); - font-size: 14px; + color: rgb(185, 190, 195); + font-size: 14px; } em { - /* Highlighted Text */ - font-weight: bold; - color: #ff8f40; - font-style: normal; + /* Highlighted Text */ + font-weight: bold; + color: #ff8f40; + font-style: normal; } span { - color: transparent; - /* Action ID text */ - font-family: Segoe UI; - padding-left: 20px; - color: transparent; + color: transparent; + /* Action ID text */ + font-family: Segoe UI; + padding-left: 20px; + color: transparent; } - diff --git a/palette/res/theme/styles.json b/palette/res/theme/dark/styles.json similarity index 100% rename from palette/res/theme/styles.json rename to palette/res/theme/dark/styles.json diff --git a/palette/res/theme/dark/window.css b/palette/res/theme/dark/window.css new file mode 100644 index 0000000..ed1be48 --- /dev/null +++ b/palette/res/theme/dark/window.css @@ -0,0 +1,141 @@ +/* + Dark Theme for ifred + Author : Jinmo + Repo : github.com/jinmo +*/ + +* { + /* Font: set ONE family name (a comma-separated list is unreliable in + Qt's rich-text renderer and can fall back to IDA's default font). + Change this to any font installed on your system. */ + font-family: "Segoe UI"; + font-size: 16px; + background: rgb(37, 37, 38); + color: rgb(204, 204, 204); +} + +/* Searchbox */ + +QLineEdit, +QLineEdit:hover, +QLineEdit:active { + font-size: 16px; + width: 720px; + height: 29px; + margin: 7px 7px 8px 8px; + padding: 0px 7px; + border: 1px solid rgb(23, 91, 137); + background: rgb(60, 60, 60); +} + +/* List view and items */ + +QListView { + border: none; + padding: 0; + margin: 0; + min-height: 350px; +} + +QListView::item { + padding-left: 14px; + padding-top: 3px; +} + +QListView::item:hover { + background: rgb(42, 45, 46); + border: 0; +} + +QListView::item:checked { + padding-bottom: 1px; + border-bottom: 1px solid #3f3f46; +} + +QListView::item:selected { + background: #062f4a !important; +} + +table, +td, +div, +p, +span, +em { + background: transparent; +} + +table { + color: #fff; + margin-bottom: 5px; +} + +td.placeholder { + font-size: 1px; + width: 700px; +} + +td { + padding: 0; + margin: 0; + vertical-align: middle; +} + +td.shortcut { + float: right; + padding-right: 18px; +} + +td.description { + color: #555; +} + +em { + /* Highlighted Text */ + font-weight: bold; + color: rgb(0, 151, 251); + font-style: normal; +} + +span { + /* Action ID text */ + padding-left: 20px; + color: transparent; +} + +/* List scrollbar */ + +QScrollBar:vertical * { + background-color: #000; +} + +QScrollBar:vertical { + border: none; + width: 12px; + background: rgb(37, 37, 38); +} + +QScrollBar::add-page:vertical, +QScrollBar::sub-page:vertical { + background: rgba(0, 0, 0, 0.1); +} + +QScrollBar::handle:vertical { + background: rgba(255, 255, 255, 0.3); + min-height: 20px; + border-radius: 6px; +} + +QScrollBar::add-line:vertical { + width: 0; + height: 0; + subcontrol-position: bottom; + subcontrol-origin: margin; +} + +QScrollBar::sub-line:vertical { + width: 0; + height: 0; + subcontrol-position: top; + subcontrol-origin: margin; +} diff --git a/palette/res/theme/gitkraken-dark/window.css b/palette/res/theme/gitkraken-dark/window.css new file mode 100755 index 0000000..6b00bc4 --- /dev/null +++ b/palette/res/theme/gitkraken-dark/window.css @@ -0,0 +1,154 @@ +/* + Dark Theme for ifred + Author : Jinmo + Repo : github.com/jinmo +*/ + +* { + font-family: "Segoe UI", "Ubuntu", "DejaVu Sans", sans-serif; + font-size: 18px; + background: #272a31; + border-radius: 6px; +} + +QMainWindow, +QLineEdit, +QListView { + width: 700px; +} + +QFrame { + background: #272a31; +} + +/* Searchbox - GitKraken's blue border, no icon */ + +QLineEdit { + font-family: "Segoe UI", "Ubuntu", "DejaVu Sans", sans-serif; + height: 34px; + font-size: 20px; + color: rgb(204, 204, 204); + border: 4px solid #4d88ff; + border-radius: 5px; + background: #33363f; + padding: 4px 12px; +} + +/* Scrollbar */ + +QScrollBar:vertical * { + background-color: #272a31; +} + +QScrollBar:vertical { + border: none; + width: 12px; + border-radius: 6px; + background: transparent; +} + +QScrollBar::add-page:vertical, +QScrollBar::sub-page:vertical { + background: rgb(40, 43, 50); +} + +QScrollBar::handle:vertical { + background: rgb(62, 66, 78); + min-height: 20px; + border-radius: 6px; +} + +QScrollBar::add-line:vertical { + width: 0; + height: 0; + subcontrol-position: bottom; + subcontrol-origin: margin; +} + +QScrollBar::sub-line:vertical { + width: 0; + height: 0; + subcontrol-position: top; + subcontrol-origin: margin; +} + +/* List view and items */ + +QListView { + border: none; + min-height: 300px; + max-height: 300px; +} + +QListView::item { + background: #272a31; + padding-top: 8px; + padding-bottom: 10px; + padding-left: 25px; + border-bottom: 1px solid #000000; +} + +QListView::item:hover { + background: #2b3446; + border-bottom: 1px solid #000000; +} + +QListView::item:selected { + background: #2e3d5a; +} + +/* Item text (rendered as HTML in a QTextDocument) */ + +table, +td, +div, +p, +span, +em { + background: transparent; +} + +table { + color: rgb(204, 204, 204); + font-size: 18px; + margin-bottom: 18px; +} + +table.selected { + color: #ffffff; +} + +td.placeholder { + font-size: 1px; + width: 700px; +} + +td { + padding: 0; + margin: 0; + vertical-align: middle; +} + +td.shortcut { + float: right; + padding-right: 32px; + color: rgb(140, 145, 155); +} + +td.description { + color: rgb(140, 145, 155); + font-size: 14px; +} + +em { + /* Highlighted (matched) characters */ + font-weight: bold; + color: #4d88ff; + font-style: normal; +} + +span { + /* Action ID text - kept invisible */ + color: transparent; + padding-left: 20px; +} diff --git a/palette/res/theme/gitkraken-light/window.css b/palette/res/theme/gitkraken-light/window.css new file mode 100755 index 0000000..fe1ae27 --- /dev/null +++ b/palette/res/theme/gitkraken-light/window.css @@ -0,0 +1,158 @@ +/* + GitKraken Light Theme for ifred + Author : Blue DeviL // SCT + Masto : @bluedevil@infosec.exchange + Repo : github.com/blue-devil + Repo : gitlab.com/bluedevil + Date : 13.09.2023 + Palette : https://github.com/JonBunator/gitkraken-custom-themes +*/ + +* { + font-family: "Segoe UI", "Ubuntu", "DejaVu Sans", sans-serif; + font-size: 18px; + background: #ffffff; + border-radius: 6px; +} + +QMainWindow, +QLineEdit, +QListView { + width: 700px; +} + +QFrame { + background: #ffffff; +} + +/* Searchbox - GitKraken's blue border, no icon */ + +QLineEdit { + font-family: "Segoe UI", "Ubuntu", "DejaVu Sans", sans-serif; + height: 34px; + font-size: 20px; + color: rgb(26, 26, 26); + border: 4px solid #4d88ff; + border-radius: 5px; + background: #ffffff; + padding: 4px 12px; +} + +/* Scrollbar */ + +QScrollBar:vertical * { + background-color: #ffffff; +} + +QScrollBar:vertical { + border: none; + width: 12px; + border-radius: 6px; + background: transparent; +} + +QScrollBar::add-page:vertical, +QScrollBar::sub-page:vertical { + background: rgb(240, 240, 240); +} + +QScrollBar::handle:vertical { + background: rgb(195, 199, 209); + min-height: 20px; + border-radius: 6px; +} + +QScrollBar::add-line:vertical { + width: 0; + height: 0; + subcontrol-position: bottom; + subcontrol-origin: margin; +} + +QScrollBar::sub-line:vertical { + width: 0; + height: 0; + subcontrol-position: top; + subcontrol-origin: margin; +} + +/* List view and items */ + +QListView { + border: none; + min-height: 300px; + max-height: 300px; +} + +QListView::item { + background: #ffffff; + padding-top: 8px; + padding-bottom: 10px; + padding-left: 25px; + border-bottom: 1px solid #e6e6e6; +} + +QListView::item:hover { + background: #eef3fd; + border-bottom: 1px solid #e6e6e6; +} + +QListView::item:selected { + background: #e0e9fc; +} + +/* Item text (rendered as HTML in a QTextDocument) */ + +table, +td, +div, +p, +span, +em { + background: transparent; +} + +table { + color: rgb(26, 26, 26); + font-size: 18px; + margin-bottom: 18px; +} + +table.selected { + color: #000000; +} + +td.placeholder { + font-size: 1px; + width: 700px; +} + +td { + padding: 0; + margin: 0; + vertical-align: middle; +} + +td.shortcut { + float: right; + padding-right: 32px; + color: rgb(120, 120, 120); +} + +td.description { + color: rgb(120, 120, 120); + font-size: 14px; +} + +em { + /* Highlighted (matched) characters */ + font-weight: bold; + color: #395bbf; + font-style: normal; +} + +span { + /* Action ID text - kept invisible */ + color: transparent; + padding-left: 20px; +} diff --git a/palette/res/theme/gitkraken/window.css b/palette/res/theme/gitkraken/window.css deleted file mode 100644 index 66c26a6..0000000 --- a/palette/res/theme/gitkraken/window.css +++ /dev/null @@ -1,106 +0,0 @@ -* { - font-family: Segoe UI; - font-size: 20px; - color: rgb(204, 204, 204); - background: transparent; -} - - -/* Searchbox */ - -QLineEdit, -QLineEdit:hover, -QLineEdit:active { - width: 720px; - height: 29px; - border: 4px solid rgb(77, 136, 255); - border-radius: 5px; - border-bottom-left-radius: 0px; - border-bottom-right-radius: 0px; - font-size: 18px; - background: rgb(51, 54, 63); - padding: 6px 0px 6px 12px; -} - - -/* List view and items */ - -QListView { - background-color: transparent; - border: none; -} - - QListView::item { - background: rgb(39, 42, 49); - padding-left: 14px; - padding-top: 6px; - padding-bottom: 13px; - border-bottom: 1px solid #000; - } - - QListView::item:hover { - background: rgb(43, 52, 70); - border: 0; - border-bottom: 1px solid #000; - } - - QListView::item:selected { - background: rgb(46, 61, 90) !important; - } - -div, -span, -em { - background: transparent; -} - -em { - /* Highlighted Text */ - font-weight: bold; - color: rgb(77, 136, 255); - font-style: normal; -} - -span { - /* Action ID text */ - color: transparent; -} - - -/* List scrollbar */ - -QScrollBar:vertical * { - background-color: rgb(56, 59, 65); -} - -QScrollBar:vertical { - border: none; - width: 12px; - background: rgb(56, 59, 65); -} - -QScrollBar::add-page:vertical, -QScrollBar::sub-page:vertical { - background: rgba(0, 0, 0, 0.1); -} - -QScrollBar::handle:vertical { - background: rgba(62, 66, 78); - border: 1px solid #4d515c; - min-height: 20px; - border-radius: 6px; -} - -QScrollBar::add-line:vertical { - width: 0; - height: 0; - subcontrol-position: bottom; - subcontrol-origin: margin; -} - -QScrollBar::sub-line:vertical { - width: 0; - height: 0; - subcontrol-position: top; - subcontrol-origin: margin; -} diff --git a/palette/res/theme/light/styles.json b/palette/res/theme/light/styles.json new file mode 100755 index 0000000..cc3ba3e --- /dev/null +++ b/palette/res/theme/light/styles.json @@ -0,0 +1,3 @@ +{ + "shadow": 30 +} diff --git a/palette/res/theme/light/window.css b/palette/res/theme/light/window.css new file mode 100755 index 0000000..3e25cd5 --- /dev/null +++ b/palette/res/theme/light/window.css @@ -0,0 +1,146 @@ +/* + Light Theme for ifred + Author : Blue DeviL // SCT + Masto : @bluedevil@infosec.exchange + Repo : github.com/blue-devil + Repo : gitlab.com/bluedevil + Date : 02.09.2026 + Palette : Inspired from VSCode light theme +*/ + +* { + /* Font: set ONE family name (a comma-separated list is unreliable in + Qt's rich-text renderer and can fall back to IDA's default font). + Change this to any font installed on your system. */ + font-family: "Segoe UI"; + font-size: 16px; + background: #f3f3f3; + color: rgb(51, 51, 51); +} + +/* Searchbox */ + +QLineEdit, +QLineEdit:hover, +QLineEdit:active { + font-size: 16px; + width: 720px; + height: 29px; + margin: 7px 7px 8px 8px; + padding: 0px 7px; + color: rgb(51, 51, 51); + border: 1px solid rgb(0, 122, 204); + background: #ffffff; +} + +/* List view and items */ + +QListView { + border: none; + padding: 0; + margin: 0; + min-height: 350px; +} + +QListView::item { + padding-left: 14px; + padding-top: 3px; +} + +QListView::item:hover { + background: rgb(232, 232, 232); + border: 0; +} + +QListView::item:checked { + padding-bottom: 1px; + border-bottom: 1px solid #c8c8c8; +} + +QListView::item:selected { + background: #cfe6ff !important; +} + +table, +td, +div, +p, +span, +em { + background: transparent; +} + +table { + color: #1e1e1e; + margin-bottom: 5px; +} + +td.placeholder { + font-size: 1px; + width: 700px; +} + +td { + padding: 0; + margin: 0; + vertical-align: middle; +} + +td.shortcut { + float: right; + padding-right: 18px; +} + +td.description { + color: #999; +} + +em { + /* Highlighted Text */ + font-weight: bold; + color: #005fb8; + font-style: normal; +} + +span { + /* Action ID text */ + padding-left: 20px; + color: transparent; +} + +/* List scrollbar */ + +QScrollBar:vertical * { + background-color: #f3f3f3; +} + +QScrollBar:vertical { + border: none; + width: 12px; + background: #f3f3f3; +} + +QScrollBar::add-page:vertical, +QScrollBar::sub-page:vertical { + background: rgba(0, 0, 0, 0.05); +} + +QScrollBar::handle:vertical { + background: rgba(0, 0, 0, 0.25); + min-height: 20px; + border-radius: 6px; +} + +QScrollBar::add-line:vertical { + width: 0; + height: 0; + subcontrol-position: bottom; + subcontrol-origin: margin; +} + +QScrollBar::sub-line:vertical { + width: 0; + height: 0; + subcontrol-position: top; + subcontrol-origin: margin; +} diff --git a/palette/res/theme/solarized-dark/window.css b/palette/res/theme/solarized-dark/window.css index f1ba434..a0637f8 100644 --- a/palette/res/theme/solarized-dark/window.css +++ b/palette/res/theme/solarized-dark/window.css @@ -1,132 +1,145 @@ -/* - Author: Blue DeviL // SCT - Masto : @bluedevil@infosec.exchange - Repo : github.com/blue-devil - Repo : gitlab.com/bluedevil - Date : 13.09.2023 -*/ -* { - font-family: Segoe UI; - font-size: 16px; - background: rgb(0, 43, 54); - color: rgb(238, 232, 213); -} - -/* Searchbox */ - -QLineEdit, QLineEdit:hover, QLineEdit:active { - font-size: 16px; - width: 720px; - height: 29px; - margin: 7px 7px 8px 8px; - padding: 0px 7px; - border: 1px solid rgb(42, 161, 152); - background: rgb(7, 54, 66); -} - -/* List view and items */ - -QListView { - border: none; - padding: 0; - margin: 0; - min-height: 350px; -} - -QListView::item { - padding-left: 14px; - padding-top: 3px; -} - -QListView::item:hover { - background: rgb(7, 54, 66); - border: 0; -} - -QListView::item:checked { - padding-bottom: 1px; - border-bottom: 1px solid #3f3f46; -} - -QListView::item:selected { - background: #07514a !important; -} - -table, td, div, p, span, em { - background: transparent; -} - -table { - color: #fff; - margin-bottom: 5px; -} - -td.placeholder { - font-size: 1px; - width: 700px; -} - -td { - padding: 0; - margin: 0; - vertical-align: middle; -} - -td.shortcut { - float: right; - padding-right: 18px; -} - -td.description { - color: #555; -} - -em { - /* Highlighted Text */ - font-weight: bold; - color: rgb(42, 161, 152); - font-style: normal; -} - -span { - /* Action ID text */ - font-family: Segoe UI; - padding-left: 20px; - color: transparent; -} - -/* List scrollbar */ - -QScrollBar:vertical * { - background-color: #000; -} - -QScrollBar:vertical { - border: none; - width: 12px; - background: rgb(5, 33, 40); -} - -QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { - background: rgba(0, 0, 0, 0.1); -} - -QScrollBar::handle:vertical { - background: rgba(2, 255, 255, 0.3); - min-height: 20px; - border-radius: 6px; -} - -QScrollBar::add-line:vertical { - width: 0; - height: 0; - subcontrol-position: bottom; - subcontrol-origin: margin; -} - -QScrollBar::sub-line:vertical { - width: 0; - height: 0; - subcontrol-position: top; - subcontrol-origin: margin; -} +/* + Solarized Dark Theme for ifred + Author : Blue DeviL // SCT + Masto : @bluedevil@infosec.exchange + Repo : github.com/blue-devil + Repo : gitlab.com/bluedevil + Date : 13.09.2023 + Palette : https://github.com/altercation/solarized +*/ + +* { + /* Font: set ONE family name (a comma-separated list is unreliable in + Qt's rich-text renderer and can fall back to IDA's default font). + Change this to any font installed on your system. */ + font-family: "Segoe UI"; + font-size: 16px; + background: rgb(0, 43, 54); + color: rgb(238, 232, 213); +} + +/* Searchbox */ + +QLineEdit, +QLineEdit:hover, +QLineEdit:active { + font-size: 16px; + width: 720px; + height: 29px; + margin: 7px 7px 8px 8px; + padding: 0px 7px; + border: 1px solid rgb(42, 161, 152); + background: rgb(7, 54, 66); +} + +/* List view and items */ + +QListView { + border: none; + padding: 0; + margin: 0; + min-height: 350px; +} + +QListView::item { + padding-left: 14px; + padding-top: 3px; +} + +QListView::item:hover { + background: rgb(7, 54, 66); + border: 0; +} + +QListView::item:checked { + padding-bottom: 1px; + border-bottom: 1px solid #3f3f46; +} + +QListView::item:selected { + background: #07514a !important; +} + +table, +td, +div, +p, +span, +em { + background: transparent; +} + +table { + color: #fff; + margin-bottom: 5px; +} + +td.placeholder { + font-size: 1px; + width: 700px; +} + +td { + padding: 0; + margin: 0; + vertical-align: middle; +} + +td.shortcut { + float: right; + padding-right: 18px; +} + +td.description { + color: #555; +} + +em { + /* Highlighted Text */ + font-weight: bold; + color: rgb(42, 161, 152); + font-style: normal; +} + +span { + /* Action ID text */ + padding-left: 20px; + color: transparent; +} + +/* List scrollbar */ + +QScrollBar:vertical * { + background-color: #000; +} + +QScrollBar:vertical { + border: none; + width: 12px; + background: rgb(5, 33, 40); +} + +QScrollBar::add-page:vertical, +QScrollBar::sub-page:vertical { + background: rgba(0, 0, 0, 0.1); +} + +QScrollBar::handle:vertical { + background: rgba(2, 255, 255, 0.3); + min-height: 20px; + border-radius: 6px; +} + +QScrollBar::add-line:vertical { + width: 0; + height: 0; + subcontrol-position: bottom; + subcontrol-origin: margin; +} + +QScrollBar::sub-line:vertical { + width: 0; + height: 0; + subcontrol-position: top; + subcontrol-origin: margin; +} diff --git a/palette/res/theme/solarized-light/window.css b/palette/res/theme/solarized-light/window.css index 5ff5d11..2638c24 100644 --- a/palette/res/theme/solarized-light/window.css +++ b/palette/res/theme/solarized-light/window.css @@ -1,132 +1,145 @@ -/* - Author: Blue DeviL // SCT - Masto : @bluedevil@infosec.exchange - Repo : github.com/blue-devil - Repo : gitlab.com/bluedevil - Date : 13.09.2023 -*/ -* { - font-family: Segoe UI; - font-size: 16px; - background: rgb(147, 161, 161); - color: rgb(0, 43, 54); -} - -/* Searchbox */ - -QLineEdit, QLineEdit:hover, QLineEdit:active { - font-size: 16px; - width: 720px; - height: 29px; - margin: 7px 7px 8px 8px; - padding: 0px 7px; - border: 1px solid rgb(7, 54, 66); - background: rgb(238, 232, 213); -} - -/* List view and items */ - -QListView { - border: none; - padding: 0; - margin: 0; - min-height: 350px; -} - -QListView::item { - padding-left: 14px; - padding-top: 3px; -} - -QListView::item:hover { - background: rgb(238, 232, 213); - border: 0; -} - -QListView::item:checked { - padding-bottom: 1px; - border-bottom: 1px solid #3f3f46; -} - -QListView::item:selected { - background: #a5dfd9 !important; -} - -table, td, div, p, span, em { - background: transparent; -} - -table { - color: #fff; - margin-bottom: 5px; -} - -td.placeholder { - font-size: 1px; - width: 700px; -} - -td { - padding: 0; - margin: 0; - vertical-align: middle; -} - -td.shortcut { - float: right; - padding-right: 18px; -} - -td.description { - color: #555; -} - -em { - /* Highlighted Text */ - font-weight: bold; - color: rgb(13, 64, 60); - font-style: normal; -} - -span { - /* Action ID text */ - font-family: Segoe UI; - padding-left: 20px; - color: transparent; -} - -/* List scrollbar */ - -QScrollBar:vertical * { - background-color: #000; -} - -QScrollBar:vertical { - border: none; - width: 12px; - background: rgb(88, 110, 117); -} - -QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { - background: rgba(0, 0, 0, 0.1); -} - -QScrollBar::handle:vertical { - background: rgba(165, 223, 217, 0.7); - min-height: 20px; - border-radius: 6px; -} - -QScrollBar::add-line:vertical { - width: 0; - height: 0; - subcontrol-position: bottom; - subcontrol-origin: margin; -} - -QScrollBar::sub-line:vertical { - width: 0; - height: 0; - subcontrol-position: top; - subcontrol-origin: margin; -} +/* + Solarized Light Theme for ifred + Author : Blue DeviL // SCT + Masto : @bluedevil@infosec.exchange + Repo : github.com/blue-devil + Repo : gitlab.com/bluedevil + Date : 13.09.2023 + Palette : https://github.com/altercation/solarized +*/ + +* { + /* Font: set ONE family name (a comma-separated list is unreliable in + Qt's rich-text renderer and can fall back to IDA's default font). + Change this to any font installed on your system. */ + font-family: "Segoe UI"; + font-size: 16px; + background: rgb(147, 161, 161); + color: rgb(0, 43, 54); +} + +/* Searchbox */ + +QLineEdit, +QLineEdit:hover, +QLineEdit:active { + font-size: 16px; + width: 720px; + height: 29px; + margin: 7px 7px 8px 8px; + padding: 0px 7px; + border: 1px solid rgb(7, 54, 66); + background: rgb(238, 232, 213); +} + +/* List view and items */ + +QListView { + border: none; + padding: 0; + margin: 0; + min-height: 350px; +} + +QListView::item { + padding-left: 14px; + padding-top: 3px; +} + +QListView::item:hover { + background: rgb(238, 232, 213); + border: 0; +} + +QListView::item:checked { + padding-bottom: 1px; + border-bottom: 1px solid #3f3f46; +} + +QListView::item:selected { + background: #a5dfd9 !important; +} + +table, +td, +div, +p, +span, +em { + background: transparent; +} + +table { + color: #fff; + margin-bottom: 5px; +} + +td.placeholder { + font-size: 1px; + width: 700px; +} + +td { + padding: 0; + margin: 0; + vertical-align: middle; +} + +td.shortcut { + float: right; + padding-right: 18px; +} + +td.description { + color: #555; +} + +em { + /* Highlighted Text */ + font-weight: bold; + color: rgb(13, 64, 60); + font-style: normal; +} + +span { + /* Action ID text */ + padding-left: 20px; + color: transparent; +} + +/* List scrollbar */ + +QScrollBar:vertical * { + background-color: #000; +} + +QScrollBar:vertical { + border: none; + width: 12px; + background: rgb(88, 110, 117); +} + +QScrollBar::add-page:vertical, +QScrollBar::sub-page:vertical { + background: rgba(0, 0, 0, 0.1); +} + +QScrollBar::handle:vertical { + background: rgba(165, 223, 217, 0.7); + min-height: 20px; + border-radius: 6px; +} + +QScrollBar::add-line:vertical { + width: 0; + height: 0; + subcontrol-position: bottom; + subcontrol-origin: margin; +} + +QScrollBar::sub-line:vertical { + width: 0; + height: 0; + subcontrol-position: top; + subcontrol-origin: margin; +} diff --git a/palette/res/theme/window.css b/palette/res/theme/window.css deleted file mode 100644 index bed25c7..0000000 --- a/palette/res/theme/window.css +++ /dev/null @@ -1,125 +0,0 @@ -* { - font-family: Segoe UI; - font-size: 16px; - background: rgb(37, 37, 38); - color: rgb(204, 204, 204); -} - -/* Searchbox */ - -QLineEdit, QLineEdit:hover, QLineEdit:active { - font-size: 16px; - width: 720px; - height: 29px; - margin: 7px 7px 8px 8px; - padding: 0px 7px; - border: 1px solid rgb(23, 91, 137); - background: rgb(60, 60, 60); -} - -/* List view and items */ - -QListView { - border: none; - padding: 0; - margin: 0; - min-height: 350px; -} - -QListView::item { - padding-left: 14px; - padding-top: 3px; -} - -QListView::item:hover { - background: rgb(42, 45, 46); - border: 0; -} - -QListView::item:checked { - padding-bottom: 1px; - border-bottom: 1px solid #3f3f46; -} - -QListView::item:selected { - background: #062f4a !important; -} - -table, td, div, p, span, em { - background: transparent; -} - -table { - color: #fff; - margin-bottom: 5px; -} - -td.placeholder { - font-size: 1px; - width: 700px; -} - -td { - padding: 0; - margin: 0; - vertical-align: middle; -} - -td.shortcut { - float: right; - padding-right: 18px; -} - -td.description { - color: #555; -} - -em { - /* Highlighted Text */ - font-weight: bold; - color: rgb(0, 151, 251); - font-style: normal; -} - -span { - /* Action ID text */ - font-family: Segoe UI; - padding-left: 20px; - color: transparent; -} - -/* List scrollbar */ - -QScrollBar:vertical * { - background-color: #000; -} - -QScrollBar:vertical { - border: none; - width: 12px; - background: rgb(37, 37, 38); -} - -QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { - background: rgba(0, 0, 0, 0.1); -} - -QScrollBar::handle:vertical { - background: rgba(255, 255, 255, 0.3); - min-height: 20px; - border-radius: 6px; -} - -QScrollBar::add-line:vertical { - width: 0; - height: 0; - subcontrol-position: bottom; - subcontrol-origin: margin; -} - -QScrollBar::sub-line:vertical { - width: 0; - height: 0; - subcontrol-position: top; - subcontrol-origin: margin; -} \ No newline at end of file diff --git a/palette/res/theme_bundle.qrc b/palette/res/theme_bundle.qrc old mode 100644 new mode 100755 index a3296dc..7c1c4a1 --- a/palette/res/theme_bundle.qrc +++ b/palette/res/theme_bundle.qrc @@ -1,7 +1,24 @@ - theme/styles.json - theme/window.css config.json + + ../../assets/ifred-banner-896x280.png + + theme/dark/styles.json + theme/dark/window.css + theme/light/styles.json + theme/light/window.css + theme/solarized-dark/styles.json + theme/solarized-dark/window.css + theme/solarized-light/styles.json + theme/solarized-light/window.css diff --git a/palette/src/ida/CMakeLists.txt b/palette/src/ida/CMakeLists.txt index 19eab3d..63f4859 100755 --- a/palette/src/ida/CMakeLists.txt +++ b/palette/src/ida/CMakeLists.txt @@ -1,9 +1,42 @@ include("../../ida-cmake/cmake/IDA.cmake") -set(sources plugin.cpp python.cpp) +set(sources plugin.cpp python.cpp dialogs.cpp) add_ida_plugin(ida_palette ${sources}) +# Date-stamped version, refreshed on every configure. Deliberate: releases are +# stamped with their build date. Shown in the About dialog and embedded into +# the binary's version metadata below. +string(TIMESTAMP IFRED_VERSION "%Y.%m.%d") +target_compile_definitions(ida_palette PRIVATE "IFRED_VERSION=\"${IFRED_VERSION}\"") + +# Numeric date components for the platform version fields. Leading zeros must +# go: rc.exe would parse 09 as (invalid) octal, and Mach-O current_version +# wants plain integers. +string(TIMESTAMP IFRED_VER_YEAR "%Y") +string(TIMESTAMP IFRED_VER_MONTH "%m") +string(TIMESTAMP IFRED_VER_DAY "%d") +string(REGEX REPLACE "^0" "" IFRED_VER_MONTH "${IFRED_VER_MONTH}") +string(REGEX REPLACE "^0" "" IFRED_VER_DAY "${IFRED_VER_DAY}") + +if (WIN32) + # VERSIONINFO resource: what Explorer shows under Properties > Details. + if (IDA_EA_64) + set(IFRED_DLL_NAME "ida_palette64.dll") + else () + set(IFRED_DLL_NAME "ida_palette.dll") + endif () + configure_file(version.rc.in version.rc @ONLY) + target_sources(ida_palette PRIVATE "${CMAKE_CURRENT_BINARY_DIR}/version.rc") +elseif (APPLE) + # Mach-O dylibs carry a native current_version (X.Y.Z, Y/Z <= 255 - month + # and day fit). Readable with: otool -L ida_palette64.dylib + target_link_options(ida_palette PRIVATE + "-Wl,-current_version,${IFRED_VER_YEAR}.${IFRED_VER_MONTH}.${IFRED_VER_DAY}") +endif () +# Linux ELF has no version resource; the greppable ifred_version_tag string +# in dialogs.cpp is the cross-platform fallback there. + if(APPLE AND IDA_INSTALL_DIR) target_compile_definitions(ida_palette PUBLIC "QT_NAMESPACE=QT") endif() @@ -59,32 +92,19 @@ endif() if(NOT CMAKE_BUILD_TYPE STREQUAL "Debug") if(APPLE) - if(CMAKE_OSX_ARCHITECTURES) - set(MAC_ARCH "${CMAKE_OSX_ARCHITECTURES}") - add_custom_command(TARGET ida_palette POST_BUILD - COMMENT "1 ${MAC_ARCH}" - ) - else() - set(MAC_ARCH "${CMAKE_SYSTEM_PROCESSOR}") - add_custom_command(TARGET ida_palette POST_BUILD - COMMENT "2 ${MAC_ARCH}" - ) - endif() - if(MAC_ARCH STREQUAL "x86_64") - add_custom_command(TARGET ida_palette POST_BUILD - COMMENT "Stripping ${MAC_ARCH} binary" - COMMAND strip -x $ - ) - elseif(MAC_ARCH STREQUAL "arm64") - add_custom_command(TARGET ida_palette POST_BUILD - COMMENT "TODO: Find proper way to strip ${MAC_ARCH} binary" - #COMMAND strip -x $ - ) - endif() + # Strip local symbols at LINK time (-Wl,-x = "no non-global symbols in + # the output"). This is the proper way on macOS, Apple Silicon in + # particular: the linker emits a well-formed, ad-hoc-signed dylib with + # its LC_ID_DYLIB intact. Running a post-build `strip` instead removes + # LC_ID_DYLIB and invalidates the signature, and dyld then refuses to + # load the plugin ("MH_DYLIB is missing LC_ID_DYLIB"). External symbols + # (the exported PLUGIN that IDA looks up) are kept. + target_link_options(ida_palette PRIVATE "-Wl,-x") elseif(UNIX) add_custom_command(TARGET ida_palette POST_BUILD COMMENT "Stripping binary" COMMAND strip -s $ + VERBATIM ) endif() endif() diff --git a/palette/src/ida/dialogs.cpp b/palette/src/ida/dialogs.cpp new file mode 100755 index 0000000..2a8f367 --- /dev/null +++ b/palette/src/ida/dialogs.cpp @@ -0,0 +1,324 @@ +#include "dialogs.h" + +#include + +#include +#include + +#include + +// Set from CMake as YYYY.MM.DD at configure time. +#ifndef IFRED_VERSION +#define IFRED_VERSION "0.0.0" +#endif + +// Greppable build stamp, embedded as plain ASCII in the binary on every OS - +// on Linux this is the only version marker an ELF .so can carry: +// strings ida_palette64.* | grep "ifred version" +// (extern "C" gives it external linkage so it cannot be optimized away.) +extern "C" const char ifred_version_tag[] = "ifred version " IFRED_VERSION; + +namespace { + +// Both dialogs are small and fixed-size, but must not be narrow: a single +// column of short lines is hard to read. This is the content width. +constexpr int kDialogMinWidth = 480; + +// Same idiom as api.cpp: IDA's main window is the only QMainWindow around. +// Parenting to it keeps the dialog centered on IDA, correctly modal, and - +// important here - inside the scope of IDA's theme stylesheet. +QWidget* mainWindow() { + for (auto& widget : qApp->topLevelWidgets()) { + if (qobject_cast(widget)) { + return widget; + } + } + return nullptr; +} + +// WCAG relative luminance / contrast ratio, used to judge a candidate link +// colour against the dialog background. +double luminance(const QColor& c) { + auto lin = [](double v) { + return v <= 0.03928 ? v / 12.92 : std::pow((v + 0.055) / 1.055, 2.4); + }; + return 0.2126 * lin(c.redF()) + 0.7152 * lin(c.greenF()) + + 0.0722 * lin(c.blueF()); +} + +double contrast(const QColor& a, const QColor& b) { + double hi = luminance(a), lo = luminance(b); + if (hi < lo) std::swap(hi, lo); + return (hi + 0.05) / (lo + 0.05); +} + +// Pick a link colour that is readable under whatever theme IDA has applied. +// +// IDA themes are Qt stylesheets. They restyle text and backgrounds, but +// hyperlinks inside rich-text labels are drawn with QPalette::Link, which no +// IDA theme sets - so links come out in Qt's default blue, invisible on +// solarized-dark and friends. The stylesheet's colours do reach the widget's +// palette once it has been polished, so read them from there: +// 1. If the palette's window/text pair is self-consistent (the stylesheet +// updated both), accept Link or Highlight when it contrasts well enough +// against that background. +// 2. Otherwise use the text colour itself: it is, by definition, what the +// theme made readable here. Underlining still marks it as a link. +QColor linkColor(QWidget* w) { + w->ensurePolished(); + const QPalette pal = w->palette(); + const QColor bg = pal.color(QPalette::Window); + const QColor fg = pal.color(QPalette::WindowText); + + if (contrast(fg, bg) >= 3.0) { + for (auto role : {QPalette::Link, QPalette::Highlight}) { + QColor c = pal.color(role); + if (contrast(c, bg) >= 4.5) return c; + } + } + return fg; +} + +QString link(const QString& url, const QString& text, const QColor& color) { + return QStringLiteral("%3") + .arg(url, color.name(), text); +} + +// A rich-text label whose links open in the browser (QDesktopServices). +// TextBrowserInteraction is what makes them clickable and gives the hover +// cursor; the colour is baked into the HTML, see linkColor(). +QLabel* richLabel(const QString& html, QWidget* parent) { + auto* label = new QLabel(html, parent); + label->setTextFormat(Qt::RichText); + label->setWordWrap(true); + label->setOpenExternalLinks(true); + label->setTextInteractionFlags(Qt::TextBrowserInteraction); + return label; +} + +// A centered OK button, wired to close the dialog. +QDialogButtonBox* okButton(QDialog* dialog) { + auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok, dialog); + buttons->setCenterButtons(true); + QObject::connect(buttons, &QDialogButtonBox::accepted, dialog, + &QDialog::accept); + return buttons; +} + +} // namespace + +void show_settings_dialog() { + QDialog dialog(mainWindow()); + dialog.setWindowTitle(QStringLiteral("IDA Palette Settings")); + dialog.resize(680, 540); + + const QString configPath = pluginPath("config.json"); + const QString themesDir = pluginPath("theme/"); + + auto* layout = new QVBoxLayout(&dialog); + layout->setContentsMargins(16, 16, 16, 16); + + auto* tabs = new QTabWidget(&dialog); + + // --- Tab 1: Theme Selector ------------------------------------------------ + auto* themeTab = new QWidget(tabs); + auto* themeLayout = new QVBoxLayout(themeTab); + + auto* combo = new QComboBox(themeTab); + auto* themeStatus = new QLabel(themeTab); + themeStatus->setWordWrap(true); + + auto* themeHint = new QLabel( + QStringLiteral( + "A theme is a directory under
%1
" + "holding a window.css (plus an optional styles.json " + "and images). Each directory is one entry above; " + "" PALETTE_DEFAULT_THEME " is the default.
" + "The choice is stored as \"theme\" in config.json " + "and takes effect the next time a palette opens.") + .arg(themesDir.toHtmlEscaped()), + themeTab); + themeHint->setTextFormat(Qt::RichText); + themeHint->setWordWrap(true); + + themeLayout->addWidget(new QLabel(QStringLiteral("Theme:"), themeTab)); + themeLayout->addWidget(combo); + themeLayout->addWidget(themeStatus); + themeLayout->addSpacing(12); + themeLayout->addWidget(themeHint); + themeLayout->addStretch(1); + + // --- Tab 2: Edit config.json --------------------------------------------- + auto* configTab = new QWidget(tabs); + auto* configLayout = new QVBoxLayout(configTab); + + auto* pathLabel = new QLabel(configPath, configTab); + pathLabel->setWordWrap(true); + + auto* editor = new QPlainTextEdit(configTab); + QFont mono = QFontDatabase::systemFont(QFontDatabase::FixedFont); + editor->setFont(mono); + editor->setLineWrapMode(QPlainTextEdit::NoWrap); + editor->setTabStopDistance( + 4 * QFontMetricsF(mono).horizontalAdvance(QLatin1Char(' '))); + + auto* configStatus = new QLabel(configTab); + configStatus->setWordWrap(true); + auto* save = new QPushButton(QStringLiteral("Save"), configTab); + + auto* saveRow = new QHBoxLayout(); + saveRow->addWidget(configStatus, 1); + saveRow->addWidget(save); + + configLayout->addWidget(pathLabel); + configLayout->addWidget(editor, 1); + configLayout->addLayout(saveRow); + + tabs->addTab(themeTab, QStringLiteral("Theme Selector")); + tabs->addTab(configTab, QStringLiteral("Edit config.json")); + + // config.json is the single source of truth: the selector writes it, the + // editor shows it, and after either changes it the other one re-reads it. + auto reloadEditor = [&]() { + editor->setPlainText(loadFile("config.json")); + editor->document()->setModified(false); + }; + auto reloadThemes = [&]() { + QSignalBlocker blocker(combo); + combo->clear(); + combo->addItems(availableThemes()); + int index = combo->findText(currentTheme()); + combo->setCurrentIndex(index >= 0 ? index : 0); + }; + + QObject::connect( + combo, &QComboBox::currentTextChanged, &dialog, + [&](const QString& name) { + if (name.isEmpty()) return; // clear() during reload + if (!setCurrentTheme(name)) { + themeStatus->setText( + QStringLiteral("Could not write %1").arg(configPath)); + return; + } + themeStatus->setText( + QStringLiteral("Theme \"%1\" selected.").arg(name)); + // Don't clobber edits in progress; Save will re-sync the selector. + if (!editor->document()->isModified()) reloadEditor(); + }); + + QObject::connect(save, &QPushButton::clicked, &dialog, [&]() { + const QByteArray text = editor->toPlainText().toUtf8(); + + QJsonParseError error{}; + auto doc = QJsonDocument::fromJson(text, &error); + if (error.error != QJsonParseError::NoError) { + configStatus->setText(QStringLiteral("Not saved - invalid JSON: %1 (at %2)") + .arg(error.errorString()) + .arg(error.offset)); + return; + } + if (!doc.isObject()) { + configStatus->setText( + QStringLiteral("Not saved - the top level must be a JSON object")); + return; + } + + QFile file(configPath); + if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) { + configStatus->setText( + QStringLiteral("Not saved - could not write %1").arg(configPath)); + return; + } + file.write(text); + file.close(); + + editor->document()->setModified(false); + configStatus->setText(QStringLiteral("Saved.")); + reloadThemes(); // the file may have changed "theme" + }); + + reloadThemes(); + reloadEditor(); + + layout->addWidget(tabs, 1); + layout->addSpacing(8); + layout->addWidget(okButton(&dialog)); + + dialog.exec(); +} + +void show_about_dialog() { + QDialog dialog(mainWindow()); + dialog.setWindowTitle(QStringLiteral("IDA Palette About")); + dialog.setMinimumWidth(kDialogMinWidth); + + const QColor lc = linkColor(&dialog); + + auto* layout = new QVBoxLayout(&dialog); + layout->setContentsMargins(16, 16, 16, 16); + layout->setSizeConstraint(QLayout::SetFixedSize); + + // Banner across the top. The 896x280 asset is tagged as 2x, so it renders at + // 448x140 logical points - sharp on both normal and HiDPI screens. + Q_INIT_RESOURCE(theme_bundle); // resource lives in a static lib; ensure init + QPixmap banner(QStringLiteral(":/bundle/ifred-banner.png")); + if (!banner.isNull()) { + banner.setDevicePixelRatio(2.0); + auto* bannerLabel = new QLabel(&dialog); + bannerLabel->setPixmap(banner); + bannerLabel->setAlignment(Qt::AlignCenter); + layout->addWidget(bannerLabel); + layout->addSpacing(12); + } + + auto* title = new QLabel( + QStringLiteral("ifred - IDA Palette
Version: %1") + .arg(QStringLiteral(IFRED_VERSION)), + &dialog); + title->setTextFormat(Qt::RichText); + title->setMinimumWidth(kDialogMinWidth - 32); + layout->addWidget(title); + layout->addSpacing(12); + + // Two columns: a right-aligned key, then the value(s). Reads far better + // than one narrow column of alternating headings and links. + auto* grid = new QGridLayout(); + grid->setHorizontalSpacing(16); + grid->setVerticalSpacing(8); + grid->setColumnStretch(1, 1); + + const struct { + const char* key; + QString value; + } rows[] = { + {"Author:", link("https://github.com/Jinmo", "jinmo", lc)}, + {"Contributors:", + link("https://github.com/blue-devil", "BlueDeviL", lc) + "
" + + link("https://github.com/nyx0", "nyx0", lc) + "
" + + link("https://github.com/yrp604", "_yrp", lc)}, + {"Homepage:", + link("https://github.com/Jinmo/ifred", "https://github.com/Jinmo/ifred", + lc)}, + {"Themes:", + link("https://github.com/Jinmo/ifred/tree/master/palette/res/theme", + "IDA Palette Themes", + lc)}, + {"License:", + link("https://github.com/Jinmo/ifred/blob/master/LICENSE", "MIT", lc)}, + }; + + int row = 0; + for (auto& r : rows) { + auto* key = new QLabel(QString::fromUtf8(r.key), &dialog); + key->setAlignment(Qt::AlignRight | Qt::AlignTop); + grid->addWidget(key, row, 0); + grid->addWidget(richLabel(r.value, &dialog), row, 1); + row++; + } + layout->addLayout(grid); + + layout->addSpacing(16); + layout->addWidget(okButton(&dialog)); + + dialog.exec(); +} diff --git a/palette/src/ida/dialogs.h b/palette/src/ida/dialogs.h new file mode 100755 index 0000000..edf4fc7 --- /dev/null +++ b/palette/src/ida/dialogs.h @@ -0,0 +1,7 @@ +#pragma once + +// Modal dialogs reachable from the Edit > Plugins > ifred submenu. +// Both must be called on the UI thread; IDA action handlers already run there. + +void show_settings_dialog(); +void show_about_dialog(); diff --git a/palette/src/ida/plugin.cpp b/palette/src/ida/plugin.cpp index 16660c4..412e67c 100755 --- a/palette/src/ida/plugin.cpp +++ b/palette/src/ida/plugin.cpp @@ -3,6 +3,8 @@ #include #include +#include "dialogs.h" + #include #include @@ -197,7 +199,7 @@ void addTypes(QVector &result) { return; } auto count = get_ordinal_count(til); - for (auto ordinal = 0; ordinal < count; ordinal++) { + for (decltype(count) ordinal = 0; ordinal < count; ordinal++) { // NOTE: this shouldn'be freed I think? It's managed by IDA and doesn't always return the start of allocation const char *name = get_numbered_type_name(til, ordinal); if (!name) { @@ -228,10 +230,30 @@ class NamesManager { QHash address_to_struct; QVector result; + // SDK 9.4 replaces the deprecated hook_to_notification_point() callbacks with + // event_listener_t objects. HT_IDB and HT_IDP are separate event groups and + // event_listener_t exposes a single on_event(), so we install one listener + // per group, each forwarding to a member handler. The &PLUGIN owner lets the + // kernel remove them automatically when ifred is unloaded. + struct idb_listener_t : event_listener_t { + NamesManager* mgr; + ssize_t idaapi on_event(ssize_t code, va_list va) override { + return mgr->on_idb_event(int(code), va); + } + } idb_listener_; + struct idp_listener_t : event_listener_t { + NamesManager* mgr; + ssize_t idaapi on_event(ssize_t code, va_list va) override { + return mgr->on_idp_event(int(code), va); + } + } idp_listener_; + public: NamesManager() { - hook_to_notification_point(HT_IDB, idb_hooks, this); - hook_to_notification_point(HT_IDP, idp_hooks, this); + idb_listener_.mgr = this; + idp_listener_.mgr = this; + hook_event_listener(HT_IDB, &idb_listener_, &PLUGIN); + hook_event_listener(HT_IDP, &idp_listener_, &PLUGIN); } void init(QVector* names) { @@ -326,32 +348,31 @@ class NamesManager { return result; } - static ssize_t idaapi idb_hooks(void* user_data, int notification_code, - va_list va) { - auto manager = reinterpret_cast(user_data); - + ssize_t on_idb_event(int notification_code, va_list va) { switch (notification_code) { case idb_event::allsegs_moved: { auto info = va_arg(va, segm_move_infos_t*); - manager->rebase(*info); + rebase(*info); break; } case idb_event::renamed: { auto ea = va_arg(va, ea_t); auto new_name = va_arg(va, const char*); - manager->rename(ea, new_name); + rename(ea, new_name); break; } #if SHOULD_USE_TYPEINF && IDA_SDK_VERSION >= 900 case idb_event::local_types_changed: { - auto ltc = va_arg(va, local_type_change_t); + // An enum is promoted to int when passed through '...', so it must be + // read back as int and cast - reading it as the enum type is UB. + auto ltc = static_cast(va_arg(va, int)); auto ordinal = va_arg(va, uint32_t); auto name = va_arg(va, const char *); switch (ltc) { case LTC_ADDED: case LTC_ALIASED: case LTC_EDITED: - manager->update_struct(ordinal, get_nice_struc_name(ordinal).c_str()); + update_struct(ordinal, get_nice_struc_name(ordinal).c_str()); break; case LTC_COMPILER: case LTC_DELETED: @@ -369,7 +390,7 @@ class NamesManager { auto struc = va_arg(va, struc_t*); if (struc) { auto tid = struc->id; - manager->update_struct(tid, get_nice_struc_name(tid).c_str()); + update_struct(tid, get_nice_struc_name(tid).c_str()); } break; } @@ -377,7 +398,7 @@ class NamesManager { case idb_event::enum_created: case idb_event::enum_renamed: { auto tid = va_arg(va, tid_t); - manager->update_struct(tid, get_nice_struc_name(tid).c_str()); + update_struct(tid, get_nice_struc_name(tid).c_str()); } #endif default: @@ -386,13 +407,10 @@ class NamesManager { return 0; } - static ssize_t idaapi idp_hooks(void* user_data, int notification_code, - va_list va) { - auto manager = reinterpret_cast(user_data); - + ssize_t on_idp_event(int notification_code, va_list va) { switch (notification_code) { case processor_t::ev_term: - manager->clear(); + clear(); } return 0; } @@ -490,9 +508,33 @@ class toggle_local_types_handler : public action_handler_t { } }; +class settings_handler : public action_handler_t { + int idaapi activate(action_activation_ctx_t*) override { + show_settings_dialog(); + return 1; + } + + action_state_t idaapi update(action_update_ctx_t*) override { + return AST_ENABLE_ALWAYS; + } +}; + +class about_handler : public action_handler_t { + int idaapi activate(action_activation_ctx_t*) override { + show_about_dialog(); + return 1; + } + + action_state_t idaapi update(action_update_ctx_t*) override { + return AST_ENABLE_ALWAYS; + } +}; + static command_palette_handler command_palette_handler_; static name_palette_handler name_palette_handler_; static toggle_local_types_handler toggle_local_types_handler_; +static settings_handler settings_handler_; +static about_handler about_handler_; static action_desc_t command_palette_action = ACTION_DESC_LITERAL( "ifred:command_palette", "ifred: Command Palette", @@ -507,6 +549,120 @@ static action_desc_t toggle_local_types_action = &toggle_local_types_handler_, "", "toggle local types or struct/enum view", -1); +// These two are the entries of the Edit > Plugins > ifred submenu, so their +// labels are bare: the submenu already says "ifred". +static action_desc_t settings_action = + ACTION_DESC_LITERAL("ifred:settings", "Settings", &settings_handler_, "", + "ifred settings", -1); + +static action_desc_t about_action = ACTION_DESC_LITERAL( + "ifred:about", "About", &about_handler_, "", "about ifred", -1); + +// Name of our submenu, as passed to create_menu()/delete_menu(). Must be unique +// across IDA, hence the prefix. Note that the *path* used to attach actions is +// built from the menu's LABEL, not this name - see the SDK's own +// plugins/idapython/examples/ui/misc/add_menus.py. +#define IFRED_MENU_NAME "ifred:menu" +#define IFRED_MENU_LABEL "ifred" +#define IFRED_MENU_PATH "Edit/Plugins/" IFRED_MENU_LABEL "/" + +static bool g_menu_installed = false; +static qtimer_t g_menu_timer = nullptr; + +// (Re)build Edit > Plugins > ifred > {Settings, About}. +// +// PLUGIN_HIDE stays set on purpose: it suppresses the automatic "ifred" entry +// IDA would otherwise add to Edit > Plugins, which just calls run() and does +// nothing. This submenu replaces it. +// +// Timing is the whole difficulty here, and it is subtle: +// +// * ifred is a PLUGIN_FIX plugin, so init() runs before the Edit > Plugins +// menu exists. create_menu()/attach_action_to_menu() do NOT fail that +// early: IDA records the path in its menu registry and returns true (the +// path even resolves afterwards), but nothing is ever rendered. +// * IDA (re)generates the Plugins menu from its plugin list on its own +// schedule - at least once between ui_database_inited and ui_ready_to_run +// during startup, and around plugin load/unload waves (IDA loads every +// plugin at startup, then unloads the non-KEEP ones right after +// ui_ready_to_run; opening a database does it again). Anything we attached +// before such a regeneration silently disappears from the rendered menu, +// while the registry still claims it exists - so "already installed" can +// not be trusted, and a retry must start from delete_menu(). +// +// Hence: never install from init(). Instead, every event that can precede or +// cause a regeneration (ui_ready_to_run, ui_plugin_loaded, ui_plugin_unloading) +// schedules a one-shot timer, and the timer tears down and rebuilds the +// submenu once the event loop is idle again - i.e. after IDA's own rebuild. +// The timer is debounced, so a wave of 30 plugin unloads costs one rebuild. +// Because IDA fires ui_plugin_loaded for ifred itself, this also covers a +// plugin loaded long after startup. +// +// Failure is not fatal: the palettes and their shortcuts work regardless. +static void install_ifred_menu() { + if (g_menu_installed) { + delete_menu(IFRED_MENU_NAME); + g_menu_installed = false; + } + + if (!create_menu(IFRED_MENU_NAME, IFRED_MENU_LABEL, "Edit/Plugins/")) { + msg("ifred: create_menu(\"Edit/Plugins/\") failed\n"); + return; + } + + // SETMENU_INS, not SETMENU_APP: the submenu is empty at this point, so there + // is nothing for "append after" to anchor to. + bool ok = true; + if (!attach_action_to_menu(IFRED_MENU_PATH, settings_action.name, + SETMENU_INS)) { + msg("ifred: attach Settings to \"%s\" failed\n", IFRED_MENU_PATH); + ok = false; + } + if (!attach_action_to_menu(IFRED_MENU_PATH, about_action.name, SETMENU_INS)) { + msg("ifred: attach About to \"%s\" failed\n", IFRED_MENU_PATH); + ok = false; + } + + if (!ok) { + // Leave no half-built menu behind; the name must be free for a retry. + delete_menu(IFRED_MENU_NAME); + return; + } + + g_menu_installed = true; +} + +static int idaapi menu_timer_cb(void*) { + g_menu_timer = nullptr; + install_ifred_menu(); + return -1; // one-shot +} + +static void schedule_menu_install() { + if (g_menu_timer) return; // already pending; the last event of a wave wins + g_menu_timer = register_timer(50, menu_timer_cb, nullptr); + if (!g_menu_timer) install_ifred_menu(); // no timers (headless): try now +} + +// SDK 9.4 event listener replacing the deprecated HT_UI notification callback. +// &PLUGIN as owner lets the kernel auto-remove it on unload; we also unhook it +// explicitly in term(). +struct ui_listener_t : event_listener_t { + ssize_t idaapi on_event(ssize_t code, va_list) override { + switch (code) { + case ui_ready_to_run: + case ui_plugin_loaded: + case ui_plugin_unloading: + schedule_menu_install(); + break; + default: + break; + } + return 0; + } +}; +static ui_listener_t g_ui_listener; + //-------------------------------------------------------------------------- bool idaapi run(size_t) { return true; } @@ -575,6 +731,21 @@ INIT_RETURN_TYPE idaapi init() { return PLUGIN_SKIP; }; + if (!register_action(settings_action)) { + msg("settings action loading error\n"); + return PLUGIN_SKIP; + }; + + if (!register_action(about_action)) { + msg("about action loading error\n"); + return PLUGIN_SKIP; + }; + + // The Edit > Plugins submenu is installed from the UI listener, once the UI + // is up. Do NOT install it here - see install_ifred_menu() for why that + // breaks. + hook_event_listener(HT_UI, &g_ui_listener, &PLUGIN); + #ifdef __MAC__ if (!mac_dlopen_workaround()) { msg("ifred mac dlopen workaround error\n"); @@ -602,7 +773,17 @@ INIT_RETURN_TYPE idaapi init() { } //-------------------------------------------------------------------------- -void idaapi term() { cleanup_palettes(); } +void idaapi term() { + unhook_event_listener(HT_UI, &g_ui_listener); + if (g_menu_timer) { + // A pending rebuild must not fire into an unloaded module. + unregister_timer(g_menu_timer); + g_menu_timer = nullptr; + } + delete_menu(IFRED_MENU_NAME); + g_menu_installed = false; + cleanup_palettes(); +} //-------------------------------------------------------------------------- // diff --git a/palette/src/ida/python.cpp b/palette/src/ida/python.cpp old mode 100644 new mode 100755 index 0afba38..cfe27a0 --- a/palette/src/ida/python.cpp +++ b/palette/src/ida/python.cpp @@ -21,21 +21,27 @@ class gil_scoped_acquire { } }; -ssize_t idaapi on_python_loaded(void*, int notification_code, va_list va) { - auto info = va_arg(va, plugin_info_t*); - - if (notification_code == ui_plugin_loaded && - !strcmp(info->org_name, "IDAPython")) { - initpy(); - unhook_from_notification_point(HT_UI, on_python_loaded, nullptr); +// SDK 9.4 event listener replacing the deprecated HT_UI notification callback. +// It waits for the IDAPython plugin to load, installs the module, then unhooks +// itself. va is only read for the ui_plugin_loaded event it carries a +// plugin_info_t for. +struct python_loaded_listener_t : event_listener_t { + ssize_t idaapi on_event(ssize_t code, va_list va) override { + if (code == ui_plugin_loaded) { + auto info = va_arg(va, plugin_info_t*); + if (info && !strcmp(info->org_name, "IDAPython")) { + initpy(); + unhook_event_listener(HT_UI, this); + } + } + return 0; } - - return 0; -} +}; +static python_loaded_listener_t g_python_loaded_listener; void initpy() { if (!Py_IsInitialized()) - hook_to_notification_point(HT_UI, on_python_loaded, nullptr); + hook_event_listener(HT_UI, &g_python_loaded_listener, &PLUGIN); else { gil_scoped_acquire gil; init_python_module(); diff --git a/palette/src/ida/version.rc.in b/palette/src/ida/version.rc.in new file mode 100755 index 0000000..d7841ce --- /dev/null +++ b/palette/src/ida/version.rc.in @@ -0,0 +1,36 @@ +// Configured by CMake from version.rc.in - do not edit the generated file. +// +// Deliberately include-free (no winres.h): plain numeric literals keep this +// compilable by rc.exe, llvm-rc and windres alike. +// +// The binary FILEVERSION fields are four 16-bit integers, so the build date +// becomes YYYY,M,D,0 (leading zeros must be stripped: rc parses 09 as octal). +// The string block carries the human-readable YYYY.MM.DD form. + +1 VERSIONINFO +FILEVERSION @IFRED_VER_YEAR@,@IFRED_VER_MONTH@,@IFRED_VER_DAY@,0 +PRODUCTVERSION @IFRED_VER_YEAR@,@IFRED_VER_MONTH@,@IFRED_VER_DAY@,0 +FILEFLAGSMASK 0x3fL +FILEFLAGS 0x0L +FILEOS 0x40004L // VOS_NT_WINDOWS32 +FILETYPE 0x2L // VFT_DLL +FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904b0" // en-US, Unicode + BEGIN + VALUE "ProductName", "ifred" + VALUE "FileDescription", "ifred - IDA Pro command palette plugin" + VALUE "FileVersion", "@IFRED_VERSION@" + VALUE "ProductVersion", "@IFRED_VERSION@" + VALUE "InternalName", "ida_palette" + VALUE "OriginalFilename", "@IFRED_DLL_NAME@" + VALUE "LegalCopyright", "MIT License - https://github.com/Jinmo/ifred" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1200 + END +END diff --git a/palette/src/utils.cpp b/palette/src/utils.cpp index 07eff58..ff0e2a9 100755 --- a/palette/src/utils.cpp +++ b/palette/src/utils.cpp @@ -2,17 +2,64 @@ #include #include -QString loadFileFromBundle(const char* filename, QFile& file, bool& updated) { - static bool resource_initialized; +static QString themesRoot() { return pluginPath("theme/"); } + +// Copy every theme directory bundled in the resources to disk, unless a +// directory of that name already exists - user edits are never overwritten. +// Files copied out of Qt resources come out read-only; fix that up so the +// user can edit them. +static void seedBundledThemes() { + QDir bundle(":/bundle/theme"); + for (auto& name : bundle.entryList(QDir::Dirs | QDir::NoDotAndDotDot)) { + QDir dst(themesRoot() + name); + if (dst.exists()) continue; + if (!dst.mkpath(".")) continue; + + QDir src(bundle.filePath(name)); + for (auto& file : src.entryList(QDir::Files)) { + QString target = dst.filePath(file); + if (QFile::copy(src.filePath(file), target)) + QFile::setPermissions(target, QFileDevice::ReadOwner | + QFileDevice::WriteOwner | + QFileDevice::ReadUser | + QFileDevice::WriteUser | + QFileDevice::ReadGroup | + QFileDevice::ReadOther); + } + } +} - if (!resource_initialized) { - Q_INIT_RESOURCE(theme_bundle); - resource_initialized = true; +static void ensureThemeInit() { + static bool done; + if (done) return; + done = true; // before anything below reads config.json through loadFile() - QDir::addSearchPath("theme", pluginPath("theme/")); - } + Q_INIT_RESOURCE(theme_bundle); + seedBundledThemes(); +} - QFile resFile(":/bundle/" + QString(filename)); +// Rewrite a "theme/" request into the active theme's folder, e.g. +// "theme/window.css" -> "theme/dark/window.css". Every theme - including the +// default - is a real folder now; there are no top-level theme files. Non-theme +// paths (config.json) pass through unchanged. +// +// This is also where the "theme:" search path used by url(theme:...) in the css +// files is refreshed, so it always follows whatever config.json says by the time +// a theme file is loaded, with the themes root as a fallback. +static QString themeRelative(const QString& filename) { + const QString prefix("theme/"); + if (!filename.startsWith(prefix)) return filename; + + const QString theme = currentTheme(); + QDir::setSearchPaths("theme", + QStringList() << (themesRoot() + theme) << themesRoot()); + return prefix + theme + "/" + filename.mid(prefix.size()); +} + +// Reads the bundled copy of (":/bundle/") and, as a side effect, +// caches it to on disk so later loads read from disk. +QString loadFileFromBundle(const QString& rel, QFile& file, bool& updated) { + QFile resFile(":/bundle/" + rel); updated = false; @@ -37,14 +84,16 @@ QString loadFileFromBundle(const char* filename, QFile& file, bool& updated) { } QString loadFile(const char* filename, bool force_update, bool& updated) { - auto absolutePath = pluginPath(filename); - QFile file(absolutePath); + ensureThemeInit(); + + const QString rel = themeRelative(QString::fromUtf8(filename)); + QFile file(pluginPath(rel.toUtf8().constData())); updated = false; if (!file.exists()) { - // Check if it exists in bundle resource - return loadFileFromBundle(filename, file, updated); + // Not on disk yet - read (and cache) the bundled copy. + return loadFileFromBundle(rel, file, updated); } if (!file.open(QIODevice::ReadOnly)) return QString(); @@ -69,3 +118,35 @@ QJsonObject json(const char* filename, bool force_update) { return json.object(); } + +QStringList availableThemes() { + ensureThemeInit(); + + QStringList themes; + QDir root(themesRoot()); + for (auto& name : root.entryList(QDir::Dirs | QDir::NoDotAndDotDot, + QDir::Name | QDir::IgnoreCase)) { + if (QFile::exists(root.filePath(name + "/window.css"))) themes << name; + } + return themes; +} + +QString currentTheme() { + auto theme = json("config.json")["theme"].toString(); + return theme.isEmpty() ? QString(PALETTE_DEFAULT_THEME) : theme; +} + +bool setCurrentTheme(const QString& name) { + // Rewrite config.json with the new "theme" key and everything else intact. + bool updated; + auto doc = QJsonDocument::fromJson( + loadFile("config.json", false, updated).toUtf8()); + auto config = doc.object(); + config["theme"] = name.isEmpty() ? QString(PALETTE_DEFAULT_THEME) : name; + + QFile file(pluginPath("config.json")); + if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) return false; + file.write(QJsonDocument(config).toJson(QJsonDocument::Indented)); + file.close(); + return true; +} diff --git a/palette/src/widgets/item.cpp b/palette/src/widgets/item.cpp old mode 100644 new mode 100755