[
  {
    "path": ".gitattributes",
    "content": "* -crlf"
  },
  {
    "path": ".github/workflows/build.yml",
    "content": "name: build\n\non:\n  push:\n    branches: [\"**\"]\n    tags: [\"v*\", \"V*\"]\n  pull_request:\n    branches: [\"**\"]\n  workflow_dispatch:\n\npermissions:\n  contents: write\n\nconcurrency:\n  group: ci-${{ github.ref }}\n  cancel-in-progress: true\n\njobs:\n  build-linux:\n    name: Build for Linux\n    runs-on: ubuntu-latest\n    env:\n      IDASDK: ${{ github.workspace }}/ida-sdk\n    steps:\n      - name: Checkout project\n        uses: actions/checkout@v4\n\n      - name: Prepare IDA SDK\n        run: git clone --depth 1 --recurse-submodules https://github.com/HexRaysSA/ida-sdk ida-sdk\n\n      - name: Configure (Linux)\n        run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Release\n\n      - name: Build (Linux)\n        run: cmake --build build -- -j2\n\n      - name: Upload Linux artifact (qscripts.so)\n        uses: actions/upload-artifact@v4\n        with:\n          name: qscripts-linux\n          path: ${{ env.IDASDK }}/src/bin/plugins/qscripts.so\n\n  build-macos:\n    name: Build for macOS\n    runs-on: macos-latest\n    env:\n      IDASDK: ${{ github.workspace }}/ida-sdk\n    steps:\n      - name: Checkout project\n        uses: actions/checkout@v4\n\n      - name: Prepare IDA SDK\n        run: git clone --depth 1 --recurse-submodules https://github.com/HexRaysSA/ida-sdk ida-sdk\n\n      - name: Configure (macOS)\n        run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Release\n\n      - name: Build (macOS)\n        run: cmake --build build -- -j2\n\n      - name: Upload macOS artifact (qscripts.dylib)\n        uses: actions/upload-artifact@v4\n        with:\n          name: qscripts-macos\n          path: ${{ env.IDASDK }}/src/bin/plugins/qscripts.dylib\n\n  build-windows:\n    name: Build for Windows\n    runs-on: windows-latest\n    env:\n      IDASDK: ${{ github.workspace }}/ida-sdk\n    steps:\n      - name: Checkout project\n        uses: actions/checkout@v4\n\n      - name: Prepare IDA SDK\n        run: git clone --depth 1 --recurse-submodules https://github.com/HexRaysSA/ida-sdk ida-sdk\n\n      - name: Configure (Windows)\n        run: cmake -S . -B build -A x64\n\n      - name: Build (Windows)\n        run: cmake --build build --config Release -- /m\n\n      - name: Upload Windows artifact (qscripts.dll)\n        uses: actions/upload-artifact@v4\n        with:\n          name: qscripts-windows\n          path: ${{ env.IDASDK }}/src/bin/plugins/qscripts.dll\n\n  publish-release:\n    name: Publish Release\n    runs-on: ubuntu-latest\n    needs: [ build-linux, build-macos, build-windows ]\n    if: startsWith(github.ref, 'refs/tags/')\n    steps:\n      - name: Checkout project\n        uses: actions/checkout@v4\n\n      - name: Download all artifacts\n        uses: actions/download-artifact@v4\n        with:\n          pattern: qscripts-*\n          path: release\n          merge-multiple: true\n\n      - name: Copy metadata to release folder\n        run: |\n          cp ida-plugin.json release/ || echo \"No ida-plugin.json found\"\n\n      - name: List release files\n        run: |\n          ls -la release || true\n\n      - name: Create zip package\n        run: |\n          cd release\n          if [ -f ida-plugin.json ]; then\n            zip -9 qscripts-${{ github.ref_name }}.zip qscripts.dll qscripts.so qscripts.dylib ida-plugin.json\n          else\n            zip -9 qscripts-${{ github.ref_name }}.zip qscripts.dll qscripts.so qscripts.dylib\n          fi\n          ls -lh *.zip\n\n      - name: Create GitHub Release and upload assets\n        uses: softprops/action-gh-release@v2\n        with:\n          tag_name: ${{ github.ref_name }}\n          name: QScripts ${{ github.ref_name }}\n          draft: false\n          prerelease: false\n          generate_release_notes: true\n          files: |\n            release/qscripts-${{ github.ref_name }}.zip\n            release/qscripts.dll\n            release/qscripts.so\n            release/qscripts.dylib\n            release/ida-plugin.json\n"
  },
  {
    "path": ".gitignore",
    "content": ".vscode/\nobj/\n__pycache__/\nbuild*/\n.vs/\nDebug/\nRelease/\nx64/\nBINARIES/\n.claude/settings.local.json\n.claude/agents/ida-cmake.md\n"
  },
  {
    "path": "CLAUDE.md",
    "content": "# CLAUDE.md\r\n\r\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\r\n\r\n# QScripts - IDA Pro Productivity Plugin\r\n\r\nQScripts is an IDA Pro plugin that enables automatic script execution upon file changes, supporting hot-reload development workflows with Python, IDC, and compiled plugins.\r\n\r\n## Build Commands\r\n\r\n### Prerequisites\r\n- **IDASDK**: Environment variable must be set to IDA SDK path\r\n- **ida-cmake**: Must be installed at `$IDASDK/ida-cmake/`\r\n- **idacpp**: Optional - will be automatically fetched if not found at `$IDASDK/include/idacpp/`\r\n\r\n### Build with ida-cmake agent\r\nAlways use the ida-cmake agent for building:\r\n```\r\nTask with subagent_type=\"ida-cmake\"\r\n```\r\n\r\n### Manual build (Windows)\r\n```bash\r\n# Configure CMake\r\nprep-cmake.bat\r\n\r\n# Build\r\nprep-cmake.bat build\r\n\r\n# Clean build\r\nprep-cmake.bat clean\r\n```\r\n\r\n## Architecture\r\n\r\n### Core Components\r\n\r\n**Main Plugin (`qscripts.cpp`)**\r\n- Implements `qscripts_chooser_t`: Non-modal chooser UI for script management\r\n- File monitoring system with configurable intervals (default 500ms)\r\n- Dependency tracking and automatic reloading\r\n- Integration with IDA's recent scripts system (shares same list)\r\n\r\n**Script Management (`script.hpp`)**\r\n- `fileinfo_t`: File metadata and modification tracking\r\n- `script_info_t`: Script state management (active/inactive/dependency)\r\n- `active_script_info_t`: Extends script_info with dependency handling\r\n- Dependency resolution with recursive parsing and cycle detection\r\n\r\n### Key Features Implementation\r\n\r\n**Dependency System**\r\n- Dependencies defined in `.deps.qscripts` files\r\n- Supports `/reload` directive for Python module reloading\r\n- `/triggerfile` directive for custom trigger conditions\r\n- `/notebook` mode for cell-based execution\r\n- Variable expansion: `$basename$`, `$env:VAR$`, `$pkgbase$`, `$ext$`\r\n\r\n**File Monitoring**\r\n- Uses qtimer-based polling (not OS-specific watchers yet)\r\n- Monitors active script and all dependencies\r\n- Trigger file support for compiled plugin development\r\n\r\n## IDA SDK and API Resources\r\n\r\nWhen answering SDK/API questions, search and read from:\r\n- **SDK Headers**: `$IDASDK/include` - All headers have docstrings\r\n- **SDK Examples**: `$IDASDK/plugins`, `$IDASDK/loaders`, `$IDASDK/module`\r\n\r\n## Testing\r\n\r\nTest scripts are located in `test_scripts/`:\r\n- `dependency-test/` - Simple dependency examples\r\n- `pkg-dependency/` - Package dependency examples\r\n- `notebooks/` - Notebook mode examples\r\n- `trigger-native/` - Compiled plugin hot-reload examples\r\n\r\n## Plugin States\r\n\r\nScripts can be in three states:\r\n1. **Normal**: Shown in regular font\r\n2. **Active** (bold): Currently monitored for changes\r\n3. **Inactive** (italics): Previously active but monitoring disabled\r\n\r\n## Important Implementation Notes\r\n\r\n- Plugin uses idacpp wrapper library for enhanced C++ API\r\n- Shares script list with IDA's built-in \"Recent Scripts\" (Alt-F9)\r\n- Maximum 512 scripts in list (IDA_MAX_RECENT_SCRIPTS)\r\n- Special unload function: `__quick_unload_script` called before reload\r\n- Supports undo via IDA's undo system when enabled in options\r\n\r\n## CI/CD and Releases\r\n\r\n### Automated Builds\r\n- GitHub Actions builds for Linux (.so), macOS (.dylib), and Windows (.dll)\r\n- Runs on every push and pull request\r\n- CMake automatically fetches idacpp if not available in IDASDK\r\n\r\n### Creating Releases\r\nTo create a new release with pre-built binaries:\r\n```bash\r\ngit tag v1.0.0              # Create version tag\r\ngit push origin v1.0.0      # Push tag to trigger release\r\n```\r\nThis will automatically:\r\n1. Build qscripts for all 3 platforms\r\n2. Create a GitHub Release page\r\n3. Upload all platform binaries (.dll, .so, .dylib)\r\n4. Generate release notes from commits"
  },
  {
    "path": "CMakeLists.txt",
    "content": "cmake_minimum_required(VERSION 3.27)\r\nproject(qscripts)\r\n\r\nset(CMAKE_CXX_STANDARD 20)\r\nset(CMAKE_CXX_STANDARD_REQUIRED ON)\r\nset(CMAKE_EXPORT_COMPILE_COMMANDS ON)\r\n\r\n# Include IDA SDK bootstrap\r\ninclude($ENV{IDASDK}/src/cmake/bootstrap.cmake)\r\nfind_package(idasdk REQUIRED)\r\n\r\n# Add idacpp library with fallback\r\n# First, try to use idacpp from IDASDK\r\nset(IDACPP_SDK_PATH \"$ENV{IDASDK}/include/idacpp\")\r\nset(IDACPP_LOCAL_PATH \"${CMAKE_CURRENT_BINARY_DIR}/idacpp\")\r\n\r\nif(EXISTS \"${IDACPP_SDK_PATH}/CMakeLists.txt\")\r\n    message(STATUS \"Using idacpp from IDASDK: ${IDACPP_SDK_PATH}\")\r\n    add_subdirectory(${IDACPP_SDK_PATH} ${CMAKE_CURRENT_BINARY_DIR}/idacpp)\r\nelse()\r\n    message(STATUS \"idacpp not found in IDASDK, fetching to build directory...\")\r\n\r\n    # Use FetchContent to download idacpp into build directory\r\n    # NOTE: The header-only idacpp wrapper is in the allthingsida/libidacpp repo\r\n    include(FetchContent)\r\n    FetchContent_Declare(\r\n        idacpp\r\n        GIT_REPOSITORY https://github.com/allthingsida/libidacpp.git\r\n        GIT_TAG main\r\n        SOURCE_DIR ${IDACPP_LOCAL_PATH}\r\n    )\r\n    FetchContent_MakeAvailable(idacpp)\r\nendif()\r\n\r\n# Convert paths to native format\r\nset(SAMPLE_IDB \"${IDA_CMAKE_DIR}/samples/wizmo32.exe.i64\")\r\nfile(TO_NATIVE_PATH \"${SAMPLE_IDB}\" SAMPLE_IDB)\r\n\r\n# Disabled sources\r\nlist(APPEND DISABLED_SOURCES utils_impl.cpp)\r\nset_source_files_properties(${DISABLED_SOURCES} PROPERTIES LANGUAGE \"\")\r\n\r\n# Add plugin\r\nida_add_plugin(qscripts\r\n    SOURCES\r\n        qscripts.cpp\r\n        ida.h\r\n        script.hpp\r\n        ${DISABLED_SOURCES}\r\n    DEBUG_ARGS\r\n        \"${SAMPLE_IDB}\"\r\n)\r\n\r\ntarget_link_libraries(qscripts PRIVATE idacpp::idacpp)\r\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2019 Elias Bachaalany\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# What is QScripts?\n\nQScripts is productivity tool and an alternative to IDA's \"Recent scripts\" (Alt-F9) and \"Execute Scripts\" (Shift-F2) facilities. QScripts allows you to develop and run any supported scripting language (\\*.py; \\*.idc, etc.) from the comfort of your own favorite text editor as soon as you save the active script, the trigger file or any of its dependencies. QScripts also supports hot-reloading of native plugins (loaders, processor modules, and plugins) using trigger files, enabling rapid development of compiled IDA addons.\n\n![Quick introduction](docs/_resources/qscripts-vid-1.gif)\n\nVideo tutorials on the [AllThingsIDA](https://www.youtube.com/@allthingsida) YouTube channel:\n\n- [Boost your IDA programming efficiency tenfold using the ida-qscripts productivity plugin](https://youtu.be/1UEoLAgEGMc?si=YMieIKHEY0AXgMHU)\n- [Scripting concepts and productivity tips for IDAPython & IDC](https://youtu.be/RgHmwHN0NLk?si=OCnLMhcAmHAQPgNI)\n- [An exercise in netnodes with the snippet manager plugin](https://youtu.be/yhVdLYzFJW0?si=z3xMqCEFOU89gAkI)\n\n# Usage\n\nInvoke QScripts from the plugins menu, or its default hotkey Alt-Shift-F9.\nWhen it runs, the scripts list might be empty. Just press `Ins` and select a script to add, or press `Del` to delete a script from the list.\nQScripts shares the same scripts list as IDA's `Recent Scripts` window.\n\nTo execute a script, just press `ENTER` or double-click it. After running a script once, it will become the active script (shown in **bold**).\n\nAn active script will then be monitored for changes. If you modify the script in your favorite text editor and save it, then QScripts will execute the script for you automatically in IDA.\n\nTo deactivate the script monitor, just press `Ctrl-D` or right-click and choose `Deactivate script monitor` from the QScripts window. When an active script becomes inactive, it will be shown in *italics*.\n\n## Keyboard shortcuts\n\nInside QScripts window:\n* `Alt-Shift-F9`: Open QScripts window\n* `ENTER` or double-click: Activate and execute selected script\n* `Shift-Enter`: Execute selected script without activating it\n* `Ins`: Add a new script to the list\n* `Del`: Remove a script from the list\n* `Ctrl-E`: Open options dialog\n* `Ctrl-D`: Deactivate script monitor\n\nFrom anywhere in IDA:\n* `Alt-Shift-X`: Re-execute the last active script or notebook cell\n\n## Configuration options\n\nPress `Ctrl+E` or right-click and select `Options` to configure QScripts:\n\n* Clear message window before execution: clear the message log before re-running the script. Very handy if you to have a fresh output log each time.\n* Show file name when execution: display the name of the file that is automatically executed\n* Execute the unload script function: A special function, if defined in the global scope (usually by your active script), called `__quick_unload_script` will be invoked before reloading the script. This gives your script a chance to do some cleanup (for example to unregister some hotkeys)\n* Script monitor interval: controls the refresh rate of the script change monitor. Ideally 500ms is a good amount of time to pick up script changes.\n* Allow QScripts execution to be undo-able: The executed script's side effects can be reverted with IDA's Undo.\n\n# Managing Dependencies in QScripts\n\nQScripts offers a feature that allows automatic re-execution of the active script when any of its dependent scripts, undergo modifications.\n\n## Setting Up Automatic Dependencies\n\nTo leverage the automatic dependency tracking feature, create a file named identically to your active script, appending `.deps.qscripts` to its name. This file should contain paths to dependent scripts, along with any necessary reload directives.\n\nAlternatively, you can place a `.deps` file (without the `.qscripts` suffix) within a `.qscripts` subfolder, located alongside your active script.\n\n**Example locations for `script.py`:**\n* `script.py.deps.qscripts` (same directory)\n* `.qscripts/script.py.deps.qscripts` (local folder)\n* `.qscripts/script.py.deps` (local folder, alternate name)\n\n## Dependency Index File Syntax\n\nThe dependency index file supports comments and directives:\n\n```txt\n# Python-style comments\n; Semicolon comments\n// C-style comments\n\n/reload import importlib; import $basename$; importlib.reload($basename$);\ndependency1.py\ndependency2.py\n```\n\n## Integrating Python Scripts\n\nFor projects involving Python, QScripts can automatically [reload](https://docs.python.org/3/library/importlib.html#importlib.reload) any changed dependent Python scripts. Include a `/reload` directive in your `.deps.qscripts` file, followed by the appropriate Python reload syntax.\n\n### Example `.deps.qscripts` file for `t1.py`:\n\n```txt\n/reload import importlib; import $basename$; importlib.reload($basename$);\nt2.py\n# This is a comment\nt3.py\n```\n\nThe `t1.py.deps.qscripts` configuration enables the following behavior:\n\n1. **Script Auto-Execution**: Changes to `t1.py` trigger its automatic re-execution within the IDA environment.\n2. **Dependency Reload**: Modifications to the dependency index file (`t1.py.deps.qscripts`) lead to the reloading of specified dependencies, followed by the re-execution of the active script.\n3. **Dependency Script Changes**: Any alteration in a dependency script file causes the active script to re-execute. If a reload directive is present, the modified dependency files are also reloaded. In our cases, if either or both of `t2.py` and `t3.py` are modified, `t1.py` is re-executed and the modified dependencies are reloaded as well.\n\n**Note**: If a dependent script possesses its own `.deps.qscripts` file, QScripts recursively integrates all linked dependencies into the active script's dependencies. However, specific directives (e.g., `reload`) within these recursive dependencies are disregarded.\n\nSee also:\n\n* [Simple dependency example](test_scripts/dependency-test/README.md)\n* [Package dependency example](test_scripts/pkg-dependency/README.md)\n\n## Directive Reference\n\nDirectives must appear at the beginning of a line and start with `/`. Arguments follow the directive name.\n\n### `/reload <code>`\n\nExecutes the specified code snippet when a dependency is modified, before re-executing the main script. The code is executed in the context of the dependency's language interpreter.\n\n```txt\n/reload import importlib; import $basename$; importlib.reload($basename$);\n```\n\n### `/pkgbase <path>`\n\nSpecifies a package base directory for Python package dependencies. This path is used in conjunction with `$pkgmodname$` and `$pkgparentmodname$` variables.\n\n```txt\n/pkgbase C:\\projects\\mypackage\n```\n\n### `/triggerfile [/keep] <filepath>`\n\nConfigures QScripts to execute the script when the specified trigger file is created or modified, rather than when the script itself changes. This is particularly useful for hot-reloading compiled native plugins.\n\nOptions:\n* `/keep`: Preserve the trigger file after execution (default behavior is to delete it)\n\n```txt\n/triggerfile /keep C:\\temp\\build_done.flag\n```\n\n### `/notebook [<title>]`\n\nEnables notebook mode where a directory of scripts is treated as a collection of cells. When any file matching the cell pattern is saved, that cell is executed.\n\n```txt\n/notebook My Analysis Notebook\n```\n\n### `/notebook.cells_re <regex>`\n\nSpecifies a regular expression pattern to identify notebook cell files. The default pattern is `\\d{4}.*\\.py$`.\n\n```txt\n/notebook.cells_re ^\\d{4}_.+\\.py$\n```\n\n### `/notebook.activate <action>`\n\nControls the behavior when the notebook is activated:\n* `exec_none`: Display the notebook title but do not execute any scripts\n* `exec_main`: Execute the main script file\n* `exec_all`: Execute all notebook cells in order\n\n```txt\n/notebook.activate exec_all\n```\n\n## Special Variables\n\nVariables are expanded when encountered in file paths or reload directives. Use the syntax `$variable$`.\n\n* `$basename$`: The base name (without extension) of the current dependency file\n* `$env:VariableName$`: The value of the environment variable `VariableName`\n* `$pkgbase$`: The package base directory (set via `/pkgbase`)\n* `$pkgmodname$`: The module name derived from the dependency file path relative to `$pkgbase$`, with path separators replaced by dots (e.g., `pkg.submodule.file`)\n* `$pkgparentmodname$`: The parent module name (e.g., `pkg.submodule`)\n* `$ext$`: The platform-specific plugin extension (e.g., `64.dll`, `.so`, `64.dylib`)\n\n### Examples\n\n```txt\n# Reload a Python module\n/reload import importlib; import $basename$; importlib.reload($basename$);\n\n# Use environment variable in path\n$env:SCRIPTS_DIR$/helper.py\n\n# Package reloading\n/pkgbase C:\\myproject\\src\n/reload import importlib; import $pkgmodname$; importlib.reload($pkgmodname$);\nsrc/utils/helper.py\n```\n\n# Using QScripts like a Jupyter notebook\n\nQScripts can monitor a directory of script files as if they were notebook cells. When you save any file matching the cell pattern, that cell is executed.\n\n**Example notebook configuration for `notebook.py.deps.qscripts`:**\n\n```txt\n/notebook Data Analysis Notebook\n/notebook.cells_re ^\\d{4}_.+\\.py$\n/notebook.activate exec_none\n/reload import importlib; import $basename$; importlib.reload($basename$);\nshared_utils.py\n```\n\nWith this configuration:\n* Files like `0010_load_data.py`, `0020_process.py`, `0030_visualize.py` are recognized as cells\n* Saving any cell executes only that cell\n* The notebook title is displayed when activated\n* Changes to `shared_utils.py` cause the last-executed cell to re-run\n\nSee also:\n\n* [Notebooks dependency example](test_scripts/notebooks/README.md)\n\n# Using QScripts with trigger files\n\nSometimes you don't want to trigger QScripts when your scripts are saved, instead you want your own trigger condition.\nOne way to achieve a custom trigger is by using the `/triggerfile` directive:\n\n```\n/triggerfile createme.tmp\n\n; Dependencies...\ndep1.py\n```\n\nThis tells QScripts to wait until the trigger file `createme.tmp` is created (or modified) before executing your script. Now, any time you want to execute the active script, just create (or modify) the trigger file.\n\nYou may pass the `/keep` option so QScripts does not delete your trigger file:\n\n```\n/triggerfile /keep dont_del_me.info\n```\n\n# Using QScripts programmatically\n\nQScripts can be controlled programmatically from scripts or other plugins.\n\n## Plugin Arguments\n\n```python\n# Open QScripts window\nidaapi.load_and_run_plugin(\"qscripts\", 0)\n\n# Execute the last selected script\nidaapi.load_and_run_plugin(\"qscripts\", 1)\n\n# Activate the script monitor\nidaapi.load_and_run_plugin(\"qscripts\", 2)\n\n# Deactivate the script monitor\nidaapi.load_and_run_plugin(\"qscripts\", 3)\n```\n\n## Action IDs\n\nQScripts registers the following actions that can be invoked programmatically:\n\n* `qscripts:deactivatemonitor` - Deactivate the script monitor\n* `qscripts:execselscript` - Execute the selected script without activating it\n* `qscripts:execscriptwithundo` - Re-execute the last active script or notebook cell\n* `qscripts:executenotebook` - Execute all cells in the active notebook\n\n```python\n# Re-execute the active script\nidaapi.process_ui_action(\"qscripts:execscriptwithundo\")\n\n# Execute all notebook cells\nidaapi.process_ui_action(\"qscripts:executenotebook\")\n```\n\n# Hot-Reloading Native Plugins\n\nQScripts supports hot-reloading of compiled native plugins (IDA plugins, loaders, and processor modules) using trigger files. This enables rapid iterative development of native IDA addons without restarting IDA.\n\n![Compiled code](docs/_resources/trigger_native.gif)\n\n## Requirements\n\nThe native plugin must be designed to support unloading:\n* **Plugins**: Set the `PLUGIN_UNL` flag to allow IDA to unload the plugin after each invocation\n* **Loaders**: Loaders are naturally unloadable\n* **Processor modules**: Use appropriate cleanup in module termination\n\n## Workflow\n\nThe hot-reload workflow uses trigger files to detect when a new binary is available:\n\n1. Create a loader script that invokes your native plugin\n2. Configure a dependency file with `/triggerfile` pointing to the compiled binary\n3. Build your native plugin\n4. QScripts detects the binary change and executes the loader script\n5. The loader script invokes the newly compiled plugin\n\n## Example: Hot-Reloading a Plugin\n\nFor a plugin with the `PLUGIN_UNL` flag (like the IDA SDK `hello` sample):\n\n**Step 1**: Create a loader script `load_hello.py`:\n\n```python\n# Optionally clear the screen\nidaapi.msg_clear()\n\n# Load your plugin and pass any arg value you want\nidaapi.load_and_run_plugin('hello', 0)\n\n# Optionally, do post work, etc.\n# ...\n```\n\n**Step 2**: Create the dependency file `load_hello.py.deps.qscripts`:\n\n```\n/triggerfile /keep C:\\<ida_dir>\\plugins\\hello$ext$\n```\n\n**Step 3**: Activate `load_hello.py` in QScripts (press `ENTER` on it)\n\n**Step 4**: Build or rebuild the plugin in your IDE\n\nThe moment the compilation succeeds, the new binary will be detected (since it is the trigger file) and your loader script will use IDA's `load_and_run_plugin()` to run the plugin again.\n\n## Example: Hot-Reloading a Loader\n\nFor loaders, the workflow is similar but uses `load_file()` instead:\n\n**Loader script `test_loader.py`:**\n\n```python\nidaapi.msg_clear()\n\n# Unload the old loader if needed\n# (IDA handles this automatically for loaders)\n\n# Load a test file with your loader\nidaapi.load_file(\"C:\\\\testfiles\\\\myformat.bin\", 0)\n```\n\n**Dependency file `test_loader.py.deps.qscripts`:**\n\n```\n/triggerfile /keep C:\\<ida_dir>\\loaders\\myloader$ext$\n```\n\n## Using $ext$ Variable\n\nThe `$ext$` variable automatically expands to the platform-specific plugin extension:\n* Windows 64-bit: `64.dll`\n* Windows 32-bit: `.dll`\n* Linux 64-bit: `64.so`\n* Linux 32-bit: `.so`\n* macOS: `64.dylib` or `.dylib`\n\nThis allows your dependency files to work across platforms without modification.\n\n## Additional Examples\n\nPlease check the native addons examples in [test_addons](test_addons/) for complete working examples of hot-reloading plugins, loaders, and processor modules.\n\n# Building\n\nQScripts uses [idacpp](https://github.com/allthingsida/idacpp) and is built using [ida-cmake](https://github.com/allthingsida/ida-cmake).\n\nIf you don't want to build from sources, then there are release pre-built for MS Windows.\n\n# Installation\n\nQScripts is written in C++ with IDA's SDK and therefore it should be deployed like a regular plugin. Copy the plugin binaries to either of those locations:\n\n* `<IDA_install_folder>/plugins`\n* `%APPDATA%\\Hex-Rays/plugins`\n\nSince the plugin uses IDA's SDK and no other OS specific functions, the plugin should be compilable for macOS and Linux just fine. I only provide MS Windows binaries. Please check the [releases page](https://github.com/allthingsida/ida-qscripts/releases).\n\n# BONUS\n\n## Snippet Manager\n\nQScripts ships with a simple [Snippet Manager](snippet_manager/README.md) plugin to allow you to manage script snippets.\n"
  },
  {
    "path": "TODO.md",
    "content": "# Ideas\r\n\r\n## Allow dot folders\r\n\r\n- Check for $(pwd)/.qscripts/<sourcefilename.depsfiles> first before checking the current directory\r\n- This allows less pollution in the current folder\r\n\r\n# TODO\r\n\r\n- Automatically select the previous active script when the UI launches\r\n- Restore the QScripts window layout when closed and re-opened\r\n- Use OS specific file change monitors\r\n\r\n- It is time to re-write this, now that we have all the specs and features\r\n\r\n# Monitor code and refactoring\r\n\r\n- Isolate the timer code from the UI logic\r\n    - Refactor filemonitor into a utility class\r\n    - Perhaps implement per OS on Win32, and as is for Linux/Mac\r\n    - For Linux, perhaps call an external process like 'inotify-wait' ?\r\n\r\n\r\n"
  },
  {
    "path": "ida-plugin.json",
    "content": "{\r\n  \"IDAMetadataDescriptorVersion\": 1,\r\n  \"plugin\": {\r\n    \"name\": \"QScripts\",\r\n    \"version\": \"1.2.6\",\r\n    \"entryPoint\": \"qscripts\",\r\n    \"description\": \"Develop IDA scripts faster with hot-reload and automatic execution on file changes. Supports Python, IDC, and compiled plugins with dependency tracking.\",\r\n    \"urls\": {\r\n      \"repository\": \"https://github.com/allthingsida/qscripts\"\r\n    },\r\n    \"authors\": [\r\n      {\r\n        \"name\": \"Elias Bachaalany\",\r\n        \"email\": \"elias.bachaalany@gmail.com\"\r\n      }\r\n    ],\r\n    \"categories\": [\r\n      \"api-scripting-and-automation\",\r\n      \"collaboration-and-productivity\"\r\n    ],\r\n    \"idaVersions\": \">=9.0\",\r\n    \"platforms\": [\r\n      \"windows-x86_64\",\r\n      \"linux-x86_64\",\r\n      \"macos-aarch64\"\r\n    ],\r\n    \"license\": \"MIT\"\r\n  }\r\n}\r\n"
  },
  {
    "path": "ida.h",
    "content": "#pragma once\n\n#pragma warning(push)\n#pragma warning(disable: 4267 4244 4146)\n#include <loader.hpp>\n#include <idp.hpp>\n#include <expr.hpp>\n#include <prodir.h>\n#include <kernwin.hpp>\n#include <diskio.hpp>\n#include <registry.hpp>\n#include <idacpp/kernwin/kernwin.hpp>\n\nusing namespace idacpp::kernwin;\n#pragma warning(pop)\n\n// IDA 8.3\n#ifndef CH_NOIDB\n    #define CH_NOIDB CH_UNUSED\n#endif\n"
  },
  {
    "path": "prep-cmake.bat",
    "content": "@echo off\r\n\r\n:: checkout the Batchography book\r\n\r\nsetlocal\r\n\r\nif not defined IDASDK (\r\n    echo IDASDK environment variable not set.\r\n    goto :eof\r\n)\r\n\r\nif not exist %IDASDK%\\src\\cmake\\bootstrap.cmake (\r\n    echo IDA SDK cmake not found at %IDASDK%\\src\\cmake\\bootstrap.cmake\r\n    goto :eof\r\n)\r\n\r\nif \"%1\"==\"clean\" (\r\n    if exist build rmdir /s /q build\r\n    goto :eof\r\n)\r\n\r\nif not exist build cmake -B build -S . -A x64\r\n\r\nif \"%1\"==\"build\" cmake --build build --config Release\r\n\r\necho.\r\necho All done!\r\necho."
  },
  {
    "path": "qscripts.cpp",
    "content": "/*\nQuick execute script: a plugin to speedup IDA scripts development.\n\nThis plugin replaces the regular \"Recent scripts\" and \"Execute Script\" dialogs and allows you to develop\nscripts in your favorite editor and execute them directly in IDA.\n\n(c) Elias Bachaalany <elias.bachaalany@gmail.com>\n*/\n#include <unordered_map>\n#include <string>\n#include <regex>\n#include <filesystem>\n#include <unordered_set>\n#include \"ida.h\"\n\n#include \"utils_impl.cpp\"\n#include \"script.hpp\"\n\n//-------------------------------------------------------------------------\n// Some constants\nstatic constexpr int  IDA_MAX_RECENT_SCRIPTS    = 512;\nstatic constexpr char IDAREG_RECENT_SCRIPTS[]   = \"RecentScripts\";\n\n//-------------------------------------------------------------------------\n// Non-modal scripts chooser\nstruct qscripts_chooser_t: public plugmod_t, public chooser_t\n{\n    using chooser_t::operator delete;\n    using chooser_t::operator new;\n\nprivate:\n    action_manager_t am;\n\n    bool m_b_filemon_timer_active = false;\n    qtimer_t m_filemon_timer = nullptr;\n    const std::regex RE_EXPANDER = std::regex(R\"(\\$(.+?)\\$)\");\n\n    int opt_change_interval  = 500;\n    int opt_clear_log        = 0;\n    int opt_show_filename    = 0;\n    int opt_exec_unload_func = 0;\n    int opt_with_undo        = 0;\n\n    active_script_info_t selected_script;\n    script_info_t* action_active_script = nullptr;\n\n    struct expand_ctx_t\n    {\n        // input\n        qstring script_file;\n        bool    main_file;\n\n        // working\n        qstring base_dir;\n        qstring pkg_base;\n        qstring reload_cmd;\n    };\n\n    inline int normalize_filemon_interval(const int change_interval) const\n    {\n        return qmax(300, change_interval);\n    }\n\n    const char *get_selected_script_file()\n    {\n        return selected_script.file_path.c_str();\n    }\n\n    bool make_meta_filename(\n        const char* filename,\n        const char* extension,\n        qstring& out,\n        bool local_only = false)\n    {\n        // Check the .qscripts folder\n        char dir[2048];\n        if (qdirname(dir, sizeof(dir), filename))\n        {\n            out.sprnt(\"%s\" SDIRCHAR QSCRIPTS_LOCAL SDIRCHAR \"%s.%s\", dir, qbasename(filename), extension);\n            if (qfileexist(out.c_str()))\n                return true;\n        }\n\n        if (local_only)\n            return false;\n\n        // Check the actual script folder\n        out.sprnt(\"%s.%s\", filename, extension);\n        return qfileexist(out.c_str());\n    }\n\n    bool find_deps_file(\n        const char* filename,\n        qstring& out)\n    {\n        return      make_meta_filename(filename, \"deps\", out, true)\n                ||  make_meta_filename(filename, \"deps.qscripts\", out);\n\n    }\n    \n    bool parse_deps_for_script(expand_ctx_t &ctx)\n    {\n        // Parse the dependency index file\n        qstring dep_file;\n        if (!find_deps_file(ctx.script_file.c_str(), dep_file))\n            return false;\n\n        FILE *fp = qfopen(dep_file.c_str(), \"r\");\n        if (fp == nullptr)\n            return false;\n\n        // Get the dependency file directory\n        ctx.base_dir.resize(ctx.script_file.size());\n        qdirname(ctx.base_dir.begin(), ctx.base_dir.size(), ctx.script_file.c_str());\n        ctx.base_dir.resize(strlen(ctx.base_dir.c_str()));\n\n        // Add the dependency file to the active script\n        selected_script.add_dep_index(dep_file.c_str());\n\n        static auto get_value = [](const char* str, const char* key, int key_len) -> const char *\n        {\n            if (strncmp(str, key, key_len) != 0)\n                return nullptr;\n            // Empty value?\n            if (str[key_len] == '\\0')\n                return \"\";\n            else\n                return str + key_len + 1;\n        };\n\n        // Parse each line\n        for (qstring line = dep_file; qgetline(&line, fp) != -1;)\n        {\n            line.trim2();\n\n            // Skip comment lines (';', '//' and '#')\n            if (line.empty() || strncmp(line.c_str(), \"//\", 2) == 0 || line[0] == '#' || line[0] == ';')\n                continue;\n\n            // Parse special directives (some apply only for the main selected script)\n            if (auto val = get_value(line.c_str(), \"/pkgbase\", 8))\n            {\n                if (ctx.main_file)\n                {\n                    ctx.pkg_base = val;\n                    expand_file_name(ctx.pkg_base, ctx);\n                    make_abs_path(ctx.pkg_base, ctx.base_dir.c_str(), true);\n                }\n                continue;\n            }\n            else if (auto val = get_value(line.c_str(), \"/notebook.cells_re\", 18))\n            {\n                if (ctx.main_file)\n                {\n                    selected_script.notebook.cells_re = std::regex(val);\n                    continue;\n                }\n            }\n            else if (auto val = get_value(line.c_str(), \"/notebook.activate\", 18))\n            {\n                if (ctx.main_file)\n                {\n                    int act;\n                    if (qstrcmp(val, \"exec_main\") == 0)\n                        act = notebook_ctx_t::act_exec_main;\n                    else if (qstrcmp(val, \"exec_all\") == 0)\n                        act = notebook_ctx_t::act_exec_all;\n                    else\n                        act = notebook_ctx_t::act_exec_none;\n\n                    selected_script.notebook.activation_action = act;\n                    continue;\n                }\n            }\n            else if (auto val = get_value(line.c_str(), \"/notebook\", 9))\n            {\n                if (ctx.main_file)\n                {\n                    selected_script.b_is_notebook = true;\n                    selected_script.notebook.title = val;\n                    continue;\n                }\n            }\n            else if (auto val = get_value(line.c_str(), \"/reload\", 7))\n            {\n                if (ctx.main_file)\n                    ctx.reload_cmd = val;\n                continue;\n            }\n            else if (auto trigger_file = get_value(line.c_str(), \"/triggerfile\", 12))\n            {\n                if (auto keep = get_value(trigger_file, \"/keep\", 5))\n                {\n                    trigger_file = keep;\n                    selected_script.b_keep_trigger_file = true;\n                }\n\n                if (ctx.main_file)\n                {\n                    selected_script.trigger_file.refresh(trigger_file);\n                    expand_file_name(selected_script.trigger_file.file_path, ctx);\n                }\n                continue;\n            }\n\n            // From here on, the *line* variable is an expandable string leading to a script file\n            ctx.script_file = line;\n            expand_file_name(line, ctx);\n            normalize_path_sep(line);\n\n            // Skip dependency scripts that (do not|no longer) exist\n            script_info_t dep_script;\n            if (!get_file_modification_time(line, &dep_script.modified_time))\n                continue;\n\n            // Add script\n            dep_script.file_path  = line.c_str();\n            dep_script.reload_cmd = ctx.reload_cmd;\n            dep_script.pkg_base   = ctx.pkg_base;\n\n            selected_script.dep_scripts[line.c_str()] = std::move(dep_script);\n\n            expand_ctx_t sub_ctx = ctx;\n            sub_ctx.script_file  = line;\n            sub_ctx.main_file    = false;\n            parse_deps_for_script(sub_ctx);\n        }\n        qfclose(fp);\n\n        return true;\n    }\n\n    void expand_file_name(qstring &filename, const expand_ctx_t &ctx)\n    {\n        expand_string(filename, filename, ctx);\n        make_abs_path(filename, ctx.base_dir.c_str(), true);\n    }\n\n    void populate_initial_notebook_cells()\n    {\n        auto& cell_files = selected_script.notebook.cell_files;\n        auto current_path = std::filesystem::path(selected_script.file_path.c_str()).parent_path();\n        selected_script.notebook.base_path = current_path.string();\n\n        enumerate_files(\n            current_path, \n            selected_script.notebook.cells_re, \n            [&cell_files](const std::string& filename)\n            {\n                qtime64_t mtime;\n                get_file_modification_time(filename, &mtime);\n                cell_files[filename] = mtime;\n                return true;\n            }\n        );\n    }\n    \n    void set_selected_script(script_info_t &script)\n    {\n        // Activate a new script\n        selected_script.clear();\n        selected_script.refresh(script.file_path.c_str());\n\n        // Recursively parse the dependencies and the index files\n        expand_ctx_t main_ctx = { script.file_path.c_str(), true };\n        parse_deps_for_script(main_ctx);\n\n        // If a notebook is selected, let's capture all the cell files\n        if (selected_script.is_notebook())\n            populate_initial_notebook_cells();\n    }\n\n    void clear_selected_script()\n    {\n        action_active_script = nullptr;\n        selected_script.clear();\n        // ...and deactivate the monitor\n        activate_monitor(false);\n    }\n\n    const bool has_selected_script()\n    {\n        return !selected_script.file_path.empty();\n    }\n\n    bool is_monitor_active()          const { return m_b_filemon_timer_active; }\n    bool is_filemon_timer_installed() const { return m_filemon_timer != nullptr; }\n\n    std::string expand_pkgmodname(const expand_ctx_t& ctx)\n    {\n        auto dep_file = selected_script.has_dep(ctx.script_file.c_str());\n        qstring pkg_base = dep_file == nullptr ? selected_script.pkg_base : dep_file->pkg_base;\n\n        // If the script file is in the package base, then replace the path separators with '.'\n        if (strncmp(ctx.script_file.c_str(), pkg_base.c_str(), pkg_base.length()) == 0)\n        {\n            qstring s = ctx.script_file.c_str() + pkg_base.length() + 1;\n            s.replace(SDIRCHAR, \".\");\n            // Drop the extension too\n            auto idx = s.rfind('.');\n            if (idx != -1)\n                s.resize(idx);\n\n            return s.c_str();\n        }\n        return \"\";\n    }\n    \n    // Dynamic string expansion   Description\n    // ------------------------   -----------\n    // basename                   Returns the basename of the input file\n    // env:Variable_Name          Expands the 'Variable_Name'\n    // pkgbase                    Sets the current pkgbase path\n    // pkgmodname                 Expands the file name using the pkgbase into the form: 'module.submodule1.submodule2'\n    // pkgparentmodname           Expands the file name using the pkgbase into the form up to the parent module: 'module.submodule1'\n    // ext                        Add-on suffix including bitness and extension (example: 64.dll, .so, 64.so, .dylib, etc.)\n    void expand_string(\n        qstring &input, \n        qstring &output, \n        const expand_ctx_t& ctx)\n    {\n        output = std::regex_replace(\n            input.c_str(),\n            RE_EXPANDER,\n            [this, ctx](auto &m) -> std::string\n            {\n                qstring match1 = m.str(1).c_str();\n\n                if (strncmp(match1.c_str(), \"pkgmodname\", 10) == 0)\n                {\n\t\t\t\t\treturn expand_pkgmodname(ctx);\n                }\n                else if (strncmp(match1.c_str(), \"pkgparentmodname\", 16) == 0)\n                {\n                    std::string pkgmodname = expand_pkgmodname(ctx);\n                    size_t pos = pkgmodname.rfind('.');\n                    return pos == std::string::npos ? pkgmodname : pkgmodname.substr(0, pos);\n                }\n                else if (strncmp(match1.c_str(), \"ext\", 3) == 0)\n                {\n                    static_assert(LOADER_DLL[0] == '*');\n                    return LOADER_DLL + 1;\n                }\n                else if (strncmp(match1.c_str(), \"pkgbase\", 7) == 0)\n                {\n                    return ctx.pkg_base.c_str();\n                }\n                else if (strncmp(match1.c_str(), \"basename\", 8) == 0)\n                {\n                    char *basename, *ext;\n                    qstring wrk_str;\n                    get_basename_and_ext(ctx.script_file.c_str(), &basename, &ext, wrk_str);\n                    return basename;\n                }\n                else if (strncmp(match1.c_str(), \"env:\", 4) == 0)\n                {\n                    qstring env;\n                    if (qgetenv(match1.begin() + 4, &env))\n                        return env.c_str();\n                }\n                return m.str(1);\n            }\n        ).c_str();\n    }\n\n    bool execute_reload_directive(\n        script_info_t &dep_script_file,\n        qstring &err,\n        bool silent=true)\n    {\n        const char *script_file = dep_script_file.file_path.c_str();\n\n        do\n        {\n            auto ext = get_file_ext(script_file);\n            extlang_object_t elang(find_extlang_by_ext(ext == nullptr ? \"\" : ext));\n            if (elang == nullptr)\n            {\n                err.sprnt(\"unknown script language detected for '%s'!\\n\", script_file);\n                break;\n            }\n\n            qstring reload_cmd;\n            expand_ctx_t ctx;\n            ctx.script_file = script_file;\n            ctx.pkg_base = dep_script_file.pkg_base;\n            expand_string(dep_script_file.reload_cmd, reload_cmd, ctx);\n\n            if (!elang->eval_snippet(reload_cmd.c_str(), &err))\n            {\n                err.sprnt(\n                    \"QScripts failed to reload script file: '%s'\\n\"\n                    \"Reload command used: %s\\n\"\n                    \"Error: %s\\n\",                  \n                    script_file, reload_cmd.c_str(), err.c_str());\n                break;\n            }\n            return true;\n        } while (false);\n\n        if (!silent)\n\t\t\tmsg(\"%s\", err.c_str());\n\n        return false;\n    }\n\n    bool execute_script(script_info_t *script_info, bool with_undo)\n    {\n        if (with_undo)\n        {\n            action_active_script = script_info;\n            auto r = process_ui_action(ACTION_EXECUTE_SCRIPT_WITH_UNDO_ID);\n            action_active_script = nullptr;\n            return r;\n        }\n        return execute_script_sync(script_info);\n    }\n\n    // Executes a script file\n    bool execute_script_sync(script_info_t *script_info)\n    {\n        bool exec_ok = false;\n\n        // Pause the file monitor timer while executing a script\n        bool old_state = activate_monitor(false);\n        do\n        {\n            auto script_file = script_info->file_path.c_str();\n\n            // First things first: always take the file's modification time-stamp first so not to visit it again in the file monitor timer\n            if (!get_file_modification_time(script_file, &script_info->modified_time))\n            {\n                msg(\"Script file '%s' not found!\\n\", script_file);\n                break;\n            }\n\n            const char *script_ext = get_file_ext(script_file);\n            extlang_object_t elang(nullptr);\n            if (script_ext == nullptr || (elang = find_extlang_by_ext(script_ext)) == nullptr)\n            {\n                msg(\"Unknown script language detected for '%s'!\\n\", script_file);\n                break;\n            }\n\n            if (opt_clear_log)\n                msg_clear();\n\n            // Silently call the unload script function\n            qstring errbuf;\n            if (opt_exec_unload_func)\n            {\n                idc_value_t result;\n                elang->call_func(&result, UNLOAD_SCRIPT_FUNC_NAME, &result, 0, &errbuf);\n            }\n\n            if (opt_show_filename)\n                msg(\"QScripts executing %s...\\n\", script_file);\n\n            exec_ok = elang->compile_file(\n                script_file, \n#if IDA_SDK_VERSION >= 850\n                nullptr,  // requested_namespace\n#endif\n                &errbuf);\n            if (!exec_ok)\n            {\n                msg(\"QScripts failed to compile script file: '%s':\\n%s\", script_file, errbuf.c_str());\n                break;\n            }\n\n            // Special case for IDC scripts: we have to call 'main'\n            if (elang->is_idc())\n            {\n                idc_value_t result;\n                exec_ok = elang->call_func(&result, \"main\", &result, 0, &errbuf);\n                if (!exec_ok)\n                {\n                    msg(\"QScripts failed to run the IDC main() of file '%s':\\n%s\", script_file, errbuf.c_str());\n                    break;\n                }\n            }\n        } while (false);\n        activate_monitor(old_state);\n\n        return exec_ok;\n    }\n\n    enum \n    {\n        OPTID_INTERVAL       = 0x0001,\n        OPTID_CLEARLOG       = 0x0002,\n        OPTID_SHOWNAME       = 0x0004,\n        OPTID_UNLOADEXEC     = 0x0008,\n        OPTID_SELSCRIPT      = 0x0010,\n        OPTID_WITHUNDO       = 0x0020,\n\n        OPTID_ONLY_SCRIPT    = OPTID_SELSCRIPT,\n        OPTID_ALL_BUT_SCRIPT = 0xffff & ~OPTID_ONLY_SCRIPT,\n        OPTID_ALL            = 0xffff,\n    };\n\n    // Save or load the options\n    void saveload_options(bool bsave, int what_ids = OPTID_ALL)\n    {\n        enum { QSTR = 1000 };\n        struct options_t\n        {\n            int id;\n            const char *name;\n            int vtype;\n            void *pval;\n        } int_options [] =\n        {\n            {OPTID_INTERVAL,   \"QScripts_interval\",             VT_LONG, &opt_change_interval},\n            {OPTID_CLEARLOG,   \"QScripts_clearlog\",             VT_LONG, &opt_clear_log},\n            {OPTID_SHOWNAME,   \"QScripts_showscriptname\",       VT_LONG, &opt_show_filename},\n            {OPTID_UNLOADEXEC, \"QScripts_exec_unload_func\",     VT_LONG, &opt_exec_unload_func},\n            {OPTID_SELSCRIPT,  \"QScripts_selected_script_name\", QSTR, &selected_script.file_path},\n            {OPTID_WITHUNDO,   \"QScripts_with_undo\",            VT_LONG, &opt_with_undo}\n        };\n\n        for (auto &opt: int_options)\n        {\n            if ((what_ids & opt.id) == 0)\n                continue;\n\n            if (opt.vtype == VT_LONG)\n            {\n                if (bsave)\n                    reg_write_int(opt.name, *(int *)opt.pval);\n                else\n                    *(int *)opt.pval = reg_read_int(opt.name, *(int *)opt.pval);\n            }\n            else if (opt.vtype == VT_STR)\n            {\n                if (bsave)\n                    reg_write_string(opt.name, ((qstring *)opt.pval)->c_str());\n                else\n                    reg_read_string(((qstring *)opt.pval), opt.name);\n            }\n            else if (opt.vtype == QSTR)\n            {\n                if (bsave)\n                {\n                    reg_write_string(opt.name, ((qstring *)opt.pval)->c_str());\n                }\n                else\n                {\n                    qstring tmp;\n                    reg_read_string(&tmp, opt.name);\n                    *((qstring *)opt.pval) = tmp.c_str();\n                }\n            }\n        }\n\n        if (!bsave)\n            opt_change_interval = normalize_filemon_interval(opt_change_interval);\n    }\n\n    static int idaapi s_filemon_timer_cb(void *ud)\n    {\n        return ((qscripts_chooser_t *)ud)->filemon_timer_cb();\n    }\n\n    // Monitor callback\n    int filemon_timer_cb()\n    {\n        do\n        {\n            // No active script, do nothing\n            if (!is_monitor_active() || !has_selected_script())\n                break;\n\n            std::unique_ptr<active_script_info_t> notebook_cell_script;\n            active_script_info_t* work_script = &selected_script;\n\n            //\n            // Handle dependencies first\n            // \n\n            // Check if the active script or its dependencies are changed:\n            // 1. Dependency file --> repopulate it and execute active script\n            // 2. Any dependencies --> reload if needed and //\n            // 3. Active script --> execute it again\n            auto& dep_scripts = selected_script.dep_scripts;\n\n            // Let's check the dependencies index files first\n            auto mod_stat = selected_script.is_any_dep_index_modified();\n            if (mod_stat == filemod_status_e::modified)\n            {\n                // Force re-parsing of the index file\n                dep_scripts.clear();\n                set_selected_script(selected_script);\n\n                // Let's invalidate all the scripts time stamps so we ensure they are re-interpreted again\n                selected_script.invalidate_all_scripts();\n\n                // Refresh the UI\n                refresh_chooser(QSCRIPTS_TITLE);\n\n                // Just leave and come back fast so we get a chance to re-evaluate everything\n                return 1; // (1 ms)\n            }\n            // Dependency index file is gone\n            else if (mod_stat == filemod_status_e::not_found && !dep_scripts.empty())\n            {\n                // Let's just check the active script\n                dep_scripts.clear();\n            }\n\n            //\n            // Check the dependency scripts\n            //\n            bool dep_script_changed = false;\n            bool brk = false;\n            for (auto& kv : dep_scripts)\n            {\n                auto& dep_script = kv.second;\n                if (dep_script.get_modification_status() == filemod_status_e::modified)\n                {\n                    qstring err;\n                    dep_script_changed = true;\n                    if (dep_script.has_reload_directive()\n                        && !execute_reload_directive(dep_script, err, false))\n                    {\n                        brk = true;\n                        break;\n                    }\n                }\n            }\n            if (brk)\n                break;\n\n            //\n            // Notebook mode\n            //\n            if (selected_script.is_notebook())\n            {\n                auto& last_active_cell = selected_script.notebook.last_active_cell;\n                auto& cell_files = selected_script.notebook.cell_files;\n                auto current_path = std::filesystem::path(selected_script.file_path.c_str()).parent_path();\n                std::unordered_set<std::string> present_files;\n\n                std::string active_cell;\n                enumerate_files(\n                    current_path, \n                    selected_script.notebook.cells_re, \n                    [&present_files, &last_active_cell, &active_cell, &cell_files](const std::string& filename)\n                    {\n                        present_files.insert(filename);\n                        auto p = cell_files.find(filename);\n\n                        qtime64_t mtime;\n                        get_file_modification_time(filename.c_str(), &mtime);\n\n                        // New file?\n                        if (p == cell_files.end())\n                        {\n                            cell_files[filename] = mtime;\n                        }\n                        // File was modified?\n                        else if (p->second != mtime)\n                        {\n                            last_active_cell = active_cell = filename;\n                            p->second = mtime;\n                            // Stop enumeration; next interval we pick up the rest\n                            return false;\n                        }\n                        return true;\n                    }\n                );\n\n                // Remove missing files from cell_files\n                for (auto it = cell_files.begin(); it != cell_files.end();)\n                {\n                    if (present_files.find(it->first) == present_files.end())\n                        it = cell_files.erase(it);\n                    else\n                        ++it;\n                }\n\n                // We have to always execute a script when a dependency changes:\n                // - If a dependency has changed, but no active cells changedthen attempt to use the last active cell.\n                if (dep_script_changed && active_cell.empty())\n                    active_cell = selected_script.notebook.last_active_cell;\n\n                // If no modified cell files, then do nothing\n                if (!active_cell.empty())\n                {\n                    // ...use the same metadata as the notebook main script, but just execute the given cell\n                    notebook_cell_script.reset(new active_script_info_t(selected_script));\n                    work_script = notebook_cell_script.get();\n                    work_script->file_path = active_cell.c_str();\n                }\n            }\n            //\n            // Trigger mode\n            // \n            // In trigger file mode, just wait for the trigger file to be created\n            else if (selected_script.trigger_based())\n            {\n                // The monitor waits until the trigger file is created or modified\n                auto trigger_status = selected_script.trigger_file.get_modification_status(true);\n                if (trigger_status != filemod_status_e::modified)\n                    break;\n\n                // Delete the trigger file\n                if (!selected_script.b_keep_trigger_file)\n                    qunlink(selected_script.trigger_file.c_str());\n\n                // Always execute the main script even if it was not changed\n                selected_script.invalidate();\n                // ...and proceed with QScript logic\n            }\n\n            // Check the main script\n            mod_stat = work_script->get_modification_status();\n            if (mod_stat == filemod_status_e::not_found)\n            {\n                // Script no longer exists\n                msg(\n                    \"QScripts detected that the active script '%s' no longer exists!\\n\", \n                    work_script->file_path.c_str());\n                clear_selected_script();\n                break;\n            }\n\n            // Script or its dependencies changed?\n            if (dep_script_changed || mod_stat == filemod_status_e::modified)\n                execute_script(work_script, opt_with_undo);\n        } while (false);\n        return opt_change_interval;\n    }\n\nprotected:\n    static constexpr uint32 flags_ =\n        CH_KEEP    | CH_RESTORE  | CH_ATTRS   | CH_NOIDB |\n        CH_CAN_DEL | CH_CAN_EDIT | CH_CAN_INS | CH_CAN_REFRESH;\n\n    static constexpr int widths_[2]               = { 20, 70 };\n    static constexpr const char *const header_[2] = { \"Script\", \"Path\" };\n\n    static constexpr const char *ACTION_DEACTIVATE_MONITOR_ID        = \"qscripts:deactivatemonitor\";\n    static constexpr const char *ACTION_EXECUTE_SELECTED_SCRIPT_ID   = \"qscripts:execselscript\";\n    static constexpr const char *ACTION_EXECUTE_SCRIPT_WITH_UNDO_ID  = \"qscripts:execscriptwithundo\";\n    static constexpr const char *ACTION_EXECUTE_NOTEBOOK_ID          = \"qscripts:executenotebook\";\n\n    scripts_info_t m_scripts;\n    ssize_t m_nselected = NO_SELECTION;\n\n    static bool is_correct_widget(action_update_ctx_t* ctx)\n    {\n        return ctx->widget_title == QSCRIPTS_TITLE;\n    }\n\n\n    // Add a new script file and properly populate its script info object\n    // and returns a borrowed reference\n    const script_info_t *add_script(\n        const char *script_file,\n        bool silent = false,\n        bool unique = true)\n    {\n        if (unique)\n        {\n            auto p = m_scripts.find({ script_file });\n            if (p != m_scripts.end())\n                return &*p;\n        }\n\n        qtime64_t mtime;\n        if (!get_file_modification_time(script_file, &mtime))\n        {\n            if (!silent)\n                msg(\"Script file not found: '%s'\\n\", script_file);\n            return nullptr;\n        }\n\n        auto &si         = m_scripts.push_back();\n        si.file_path     = script_file;\n        si.modified_time = mtime;\n        return &si;\n    }\n\n    bool config_dialog()\n    {\n        static const char form[] =\n            \"Options\\n\"\n            \"\\n\"\n            \"<#Controls the refresh rate of the script change monitor#Script monitor ~i~nterval:D:100:10::>\\n\"\n            \"<#Clear the output window before re-running the script#C~l~ear the output window:C>\\n\"\n            \"<#Display the name of the file that is automatically executed#Show ~f~ile name when execution:C>\\n\"\n            \"<#Execute a function called '__quick_unload_script' before reloading the script#Execute the u~n~load script function:C>\\n\"\n            \"<#The executed scripts' side effects can be reverted with IDA's Undo#Allow QScripts execution to be ~u~ndo-able:C>>\\n\"\n\n            \"\\n\"\n            \"\\n\";\n\n        // Copy values to the dialog\n        union\n        {\n            ushort n;\n            struct\n            {\n                ushort b_clear_log        : 1;\n                ushort b_show_filename    : 1;\n                ushort b_exec_unload_func : 1;\n                ushort b_with_undo        : 1;\n            };\n        } chk_opts;\n        // Load previous options first (account for multiple instances of IDA)\n        saveload_options(false);\n\n        chk_opts.n = 0;\n        chk_opts.b_clear_log        = opt_clear_log;\n        chk_opts.b_show_filename    = opt_show_filename;\n        chk_opts.b_exec_unload_func = opt_exec_unload_func;\n        chk_opts.b_with_undo        = opt_with_undo;\n        sval_t interval             = opt_change_interval;\n\n        if (ask_form(form, &interval, &chk_opts.n) > 0)\n        {\n            // Copy values from the dialog\n            opt_change_interval  = normalize_filemon_interval(int(interval));\n            opt_clear_log        = chk_opts.b_clear_log;\n            opt_show_filename    = chk_opts.b_show_filename;\n            opt_exec_unload_func = chk_opts.b_exec_unload_func;\n            opt_with_undo        = chk_opts.b_with_undo;\n\n            // Save the options directly\n            saveload_options(true);\n            return true;\n        }\n        return false;\n    }\n\n    const void *get_obj_id(size_t *len) const override\n    {\n        // Allow a single instance\n        *len = sizeof(this);\n        return (const void *)this;\n    }\n\n    size_t idaapi get_count() const override\n    {\n        return m_scripts.size();\n    }\n\n    void idaapi get_row(\n        qstrvec_t *cols,\n        int *icon,\n        chooser_item_attrs_t *attrs,\n        size_t n) const override\n    {\n        auto si = &m_scripts[n];\n        auto path = si->file_path.c_str();\n        auto name = strrchr(path, DIRCHAR);\n        cols->at(0) = name == nullptr ? path : name + 1;\n        cols->at(1) = path;\n        if (n == m_nselected)\n        {\n            if (is_monitor_active())\n            {\n                attrs->flags = CHITEM_BOLD;\n                *icon = selected_script.is_notebook() ? IDAICONS::NOTEPAD_1 : IDAICONS::KEYBOARD_GRAY;\n            }\n            else\n            {\n                attrs->flags = CHITEM_ITALIC;\n                *icon = IDAICONS::RED_DOT;\n            }\n        }\n        else if (is_monitor_active() && selected_script.has_dep(si->file_path) != nullptr)\n        {\n            // Mark as a dependency\n            *icon = IDAICONS::EYE_GLASSES_EDIT;\n        }\n        else\n        {\n            // Mark as an inactive file\n            *icon = IDAICONS::GRAY_X_CIRCLE;\n        }\n    }\n\n    // Activate a script and execute it\n    cbret_t idaapi enter(size_t n) override\n    {\n        bool exec_ok = false;\n\n        m_nselected = n;\n\n        // Set as the selected script and execute it\n        set_selected_script(m_scripts[n]);\n\n        if (   selected_script.is_notebook() \n            && selected_script.notebook.activation_action == notebook_ctx_t::act_exec_none)\n        {\n            // Notebook, execute nothing on activation\n            msg(\"Selected notebook: %s\\n\",\n                selected_script.notebook.title.c_str());\n        }\n        else if (   selected_script.is_notebook()\n                 && selected_script.notebook.activation_action == notebook_ctx_t::act_exec_all)\n        {\n            // Notebook, execute all scripts on activation\n            msg(\"Executing all scripts for notebook: %s\\n\", \n                selected_script.notebook.title.c_str());\n\n            execute_notebook_cells(&selected_script);\n        }\n        else\n        {\n            exec_ok = execute_script(&selected_script, opt_with_undo);\n        }\n\n        if (exec_ok)\n            saveload_options(true, OPTID_ONLY_SCRIPT);\n\n        // ...and activate the monitor even if the script fails\n        activate_monitor();\n\n        return cbret_t(n, chooser_base_t::ALL_CHANGED);\n    }\n\n    // Add a new script\n    cbret_t idaapi ins(ssize_t) override\n    {\n        qstring filter;\n        get_browse_scripts_filter(filter);\n        const char *script_file = ask_file(false, \"\", \"%s\", filter.c_str());\n        if (script_file == nullptr)\n            return {};\n\n        reg_update_strlist(IDAREG_RECENT_SCRIPTS, script_file, IDA_MAX_RECENT_SCRIPTS);\n        ssize_t idx = build_scripts_list(script_file);\n        return cbret_t(qmax(idx, 0), chooser_base_t::ALL_CHANGED);\n    }\n\n    // Remove a script from the list\n    cbret_t idaapi del(size_t n) override\n    {\n        auto &script_file = m_scripts[n].file_path;\n        reg_update_strlist(IDAREG_RECENT_SCRIPTS, nullptr, IDA_MAX_RECENT_SCRIPTS, script_file.c_str());\n        build_scripts_list();\n\n        // Active script removed?\n        if (m_nselected == NO_SELECTION)\n            clear_selected_script();\n\n        return adjust_last_item(n);\n    }\n\n    // Use it to show the configuration dialog\n    cbret_t idaapi edit(size_t n) override\n    {\n        config_dialog();\n        return cbret_t(n, chooser_base_t::NOTHING_CHANGED);\n    }\n\n    void idaapi closed() override\n    {\n        saveload_options(true);\n    }\n\n    static void get_browse_scripts_filter(qstring &filter)\n    {\n        // Collect all installed external languages\n        extlangs_t langs;\n        collect_extlangs(&langs, false);\n\n        // Build the filter\n        filter = \"FILTER Script files|\";\n\n        for (auto lang: langs)\n            filter.cat_sprnt(\"*.%s;\", lang->fileext);\n\n        filter.remove_last();\n        filter.append(\"|\");\n\n        // Language specific filters\n        for (auto lang: langs)\n            filter.cat_sprnt(\"%s scripts|*.%s|\", lang->name, lang->fileext);\n\n        filter.remove_last();\n        filter.append(\"\\nSelect script file to load\");\n    }\n\n    void setup_ui()\n    {\n        am.add_action(\n            AMAHF_NONE,\n            ACTION_DEACTIVATE_MONITOR_ID,\n            \"QScripts: Deactivate script monitor\",\n            \"Ctrl-D\",\n            FO_ACTION_UPDATE([this],\n                return this->is_correct_widget(ctx) ? AST_ENABLE_FOR_WIDGET : AST_DISABLE_FOR_WIDGET;\n            ),\n            FO_ACTION_ACTIVATE([this]) {\n                if (this->is_monitor_active())\n                {\n                    this->clear_selected_script();\n                    refresh_chooser(QSCRIPTS_TITLE);\n                }\n                return 1;\n            },\n            nullptr,\n            IDAICONS::DISABLED);\n\n        am.add_action(\n            AMAHF_NONE,\n            ACTION_EXECUTE_SELECTED_SCRIPT_ID,\n            \"QScripts: Execute selected script\",\n            \"Shift-Enter\",\n            FO_ACTION_UPDATE([this],\n                return this->is_correct_widget(ctx) ? AST_ENABLE_FOR_WIDGET : AST_DISABLE_FOR_WIDGET;\n            ),\n            FO_ACTION_ACTIVATE([this]) {\n                if (!ctx->chooser_selection.empty())\n                    this->execute_script_at(ctx->chooser_selection.at(0));\n                return 1;\n            },\n            \"Execute script without activating it\",\n            IDAICONS::FLASH);\n\n        am.add_action(\n            AMAHF_NONE,\n            ACTION_EXECUTE_SCRIPT_WITH_UNDO_ID,\n            \"QScripts: Execute last active script\",\n            \"Alt-Shift-X\",\n            FO_ACTION_UPDATE([this],\n                return AST_ENABLE_ALWAYS;\n            ),\n            FO_ACTION_ACTIVATE([this]) \n            {\n                if (is_monitor_active())\n                {\n                    script_info_t* work_script = nullptr;\n                    std::unique_ptr<active_script_info_t> cell_script;\n                    if (selected_script.is_notebook())\n                    {\n                        if (!selected_script.notebook.last_active_cell.empty())\n                        {\n                            cell_script.reset(new active_script_info_t(selected_script));\n                            if (work_script = cell_script.get())\n                                work_script->file_path = selected_script.notebook.last_active_cell.c_str();\n                        }\n                    }\n                    else if (action_active_script != nullptr)\n                        work_script = action_active_script;\n                    else if (this->has_selected_script())\n                        work_script = &selected_script;\n\n                    if (work_script != nullptr)\n                        this->execute_script_sync(work_script);\n                }\n                return 1;\n            },\n            \"An action to programmatically execute the active script\",\n            IDAICONS::FLASH);\n\n        am.add_action(\n            AMAHF_NONE,\n            ACTION_EXECUTE_NOTEBOOK_ID,\n            \"QScripts: Execute all notebook cells\",\n            \"\",\n            FO_ACTION_UPDATE([this],\n                return AST_ENABLE_ALWAYS;\n            ),\n            FO_ACTION_ACTIVATE([this]) \n            {\n                if (this->has_selected_script() && selected_script.is_notebook())\n                    this->execute_notebook_cells(&selected_script);\n                return 1;\n            },\n            \"An action to programmatically execute the active script\",\n            IDAICONS::NOTEPAD_1);\n    }\n\npublic:\n    static constexpr const char *QSCRIPTS_TITLE = \"QScripts\";\n\n    qscripts_chooser_t(const char *title_ = QSCRIPTS_TITLE)\n        : chooser_t(flags_, qnumber(widths_), widths_, header_, title_), am(this)\n    {\n        popup_names[POPUP_EDIT] = \"~O~ptions\";\n        setup_ui();\n        saveload_options(false);\n    }\n\n    bool activate_monitor(bool activate = true)\n    {\n        bool old = m_b_filemon_timer_active;\n        m_b_filemon_timer_active = activate;\n        return old;\n    }\n\n    // Rebuilds the scripts list and returns the index of the `find_script` if needed\n    ssize_t build_scripts_list(const char *find_script = nullptr)\n    {\n        // Remember active script and invalidate its index\n        bool b_has_selected_script = has_selected_script();\n        qstring selected_script;\n        if (b_has_selected_script)\n            selected_script = get_selected_script_file();\n\n        // De-selected the current script in the hope of finding it again in the list\n        m_nselected = NO_SELECTION;\n\n        // Read all scripts\n        qstrvec_t scripts_list;\n        reg_read_strlist(&scripts_list, IDAREG_RECENT_SCRIPTS);\n\n        // Rebuild the list\n        ssize_t idx = 0, find_idx = NO_SELECTION;\n        m_scripts.qclear();\n        for (auto &script_file: scripts_list)\n        {\n            // Restore active script\n            if (b_has_selected_script && selected_script == script_file)\n                m_nselected = idx;\n\n            // Optionally, find the index of a script by name\n            if (find_script != nullptr && streq(script_file.c_str(), find_script))\n                find_idx = idx;\n\n            // We skip non-existent scripts\n            if (add_script(script_file.c_str(), true) != nullptr)\n                ++idx;\n        }\n        return find_idx;\n    }\n\n    void execute_last_selected_script(bool with_undo=false)\n    {\n        if (has_selected_script())\n            execute_script(&selected_script, with_undo);\n    }\n\n    void execute_script_at(ssize_t n)\n    {\n        if (n >=0 && n < ssize_t(m_scripts.size()))\n            execute_script(&m_scripts[n], opt_with_undo);\n    }\n\n    void execute_notebook_cells(active_script_info_t *script)\n    {\n        auto& cell_files = script->notebook.cell_files;\n\n        active_script_info_t cell_script = *script;\n        enumerate_files(\n            script->notebook.base_path,\n            script->notebook.cells_re,\n            [this, &cell_script, &cell_files](const std::string& filename)\n            {\n                qtime64_t mtime;\n                get_file_modification_time(filename, &mtime);\n                cell_files[filename] = mtime;\n                cell_script.file_path = filename.c_str();\n                // Execute script and stop enumeration if one cell fails to execute\n                return this->execute_script_sync(&cell_script);\n            }\n        );\n    }\n\n    void show()\n    {\n        build_scripts_list();\n\n        auto r = choose(m_nselected);\n\n        TWidget *widget;\n        if (r == 0 && (widget = find_widget(QSCRIPTS_TITLE)) != nullptr)\n        {\n            attach_action_to_popup(\n                widget,\n                nullptr,\n                ACTION_DEACTIVATE_MONITOR_ID);\n            attach_action_to_popup(\n                widget,\n                nullptr,\n                ACTION_EXECUTE_SELECTED_SCRIPT_ID);\n            attach_action_to_popup(\n                widget,\n                nullptr,\n                ACTION_EXECUTE_SCRIPT_WITH_UNDO_ID);\n            attach_action_to_popup(\n                widget,\n                nullptr,\n                ACTION_EXECUTE_NOTEBOOK_ID);\n        }\n    }\n\n    bool install_filemon_timer()\n    {\n        m_filemon_timer = register_timer(\n            opt_change_interval,\n            s_filemon_timer_cb,\n            this);\n        return is_filemon_timer_installed();\n    }\n\n    void uninstall_filemon_timer()\n    {\n        if (is_filemon_timer_installed())\n        {\n            unregister_timer(m_filemon_timer);\n            m_filemon_timer = nullptr;\n        }\n        activate_monitor(false);\n    }\n\n    bool idaapi run(size_t arg) override\n    {\n        switch (arg)\n        {\n            // Full UI run\n            case 0:\n            {\n                if (!is_filemon_timer_installed())\n                {\n                    if (!install_filemon_timer())\n                        msg(\"QScripts: failed to start the file monitor.\\n\");\n                    else\n                        msg(\"QScripts: file monitor started successfully.\\n\");\n                }\n\n                show();\n                break;\n            }\n            // Execute the selected script\n            case 1:\n            {\n                execute_last_selected_script();\n                break;\n            }\n            // Activate the scripts monitor\n            case 2:\n            {\n                activate_monitor(true);\n                refresh_chooser(QSCRIPTS_TITLE);\n                break;\n            }\n            // Deactivate the scripts monitor\n            case 3:\n            {\n                activate_monitor(false);\n                refresh_chooser(QSCRIPTS_TITLE);\n                break;\n            }\n        }\n\n        return true;\n    }\n\n    virtual ~qscripts_chooser_t()\n    {\n        uninstall_filemon_timer();\n    }\n};\n\n//-------------------------------------------------------------------------\nplugmod_t *idaapi init(void)\n{\n    auto plg = new qscripts_chooser_t();\n    if (!plg->install_filemon_timer())\n    {\n        if (::debug & IDA_DEBUG_PLUGIN)\n            msg(\"QScripts: failed to install the file monitor on startup. Please invoke the UI once to attempt try again!\\n\");\n    }\n\n    return plg;\n}\n\n//--------------------------------------------------------------------------\nstatic const char help[] =\n    \"An alternative scripts manager that lets you develop in an external editor and run them fast in IDA\\n\"\n    \"\\n\"\n    \"Just press ENTER on the script to activate it and then go back to your editor to continue development.\\n\"\n    \"\\n\"\n    \"Each time you update your script, it will be automatically invoked in IDA\\n\\n\"\n    \"\\n\"\n    \"QScripts is developed by Elias Bachaalany. Please see https://github.com/0xeb/ida-qscripts for more information\\n\"\n    \"\\n\"\n    \"\\0\"\n    __DATE__ \" \" __TIME__ \"\\n\"\n    \"\\n\";\n\n//--------------------------------------------------------------------------\n//\n//      PLUGIN DESCRIPTION BLOCK\n//\n//--------------------------------------------------------------------------\nplugin_t PLUGIN =\n{\n    IDP_INTERFACE_VERSION,\n    PLUGIN_MULTI | PLUGIN_FIX,\n    init,\n    nullptr,\n    nullptr,\n    \"QScripts: Develop IDA scripts faster in your favorite text editor\",\n    help,\n    qscripts_chooser_t::QSCRIPTS_TITLE,\n#ifdef _DEBUG\n    \"Alt-Shift-A\"\n#else\n    \"Alt-Shift-F9\"\n#endif\n};\n"
  },
  {
    "path": "script.hpp",
    "content": "#pragma once\r\n\r\n#define QSCRIPTS_LOCAL \".qscripts\"\r\nstatic constexpr char UNLOAD_SCRIPT_FUNC_NAME[] = \"__quick_unload_script\";\r\nstatic constexpr auto DEFAULT_CELLS_RE = R\"(\\d{4}.*\\.py$)\";\r\n\r\n//-------------------------------------------------------------------------\r\n// File modification state\r\nenum class filemod_status_e\r\n{\r\n    not_found,\r\n    not_modified,\r\n    modified\r\n};\r\n\r\n// Structure to describe a file and its metadata\r\nstruct fileinfo_t\r\n{\r\n    qstring file_path;\r\n    qtime64_t modified_time;\r\n\r\n    fileinfo_t(const char* file_path = nullptr): modified_time(0)\r\n    {\r\n        if (file_path != nullptr)\r\n            this->file_path = file_path;\r\n    }\r\n\r\n    inline const bool empty() const\r\n    {\r\n        return file_path.empty();\r\n    }\r\n\r\n    inline const char* c_str()\r\n    {\r\n        return file_path.c_str();\r\n    }\r\n\r\n    bool operator==(const fileinfo_t &rhs) const\r\n    {\r\n        return file_path == rhs.file_path;\r\n    }\r\n\r\n    virtual void clear()\r\n    {\r\n        file_path.clear();\r\n        modified_time = 0;\r\n    }\r\n\r\n    bool refresh(const char *file_path = nullptr)\r\n    {\r\n        if (file_path != nullptr)\r\n            this->file_path = file_path;\r\n\r\n        return get_file_modification_time(this->file_path, &modified_time);\r\n    }\r\n\r\n    // Checks if the current script has been modified\r\n    // Optionally updates the time stamp to the latest one if modified\r\n    filemod_status_e get_modification_status(bool update_mtime=true)\r\n    {\r\n        qtime64_t cur_mtime;\r\n        const char *script_file = this->file_path.c_str();\r\n        if (!get_file_modification_time(script_file, &cur_mtime))\r\n        {\r\n            if (update_mtime)\r\n                modified_time = 0;\r\n            return filemod_status_e::not_found;\r\n        }\r\n\r\n        // Script is up to date, no need to execute it again\r\n        if (cur_mtime == modified_time)\r\n            return filemod_status_e::not_modified;\r\n\r\n        if (update_mtime)\r\n            modified_time = cur_mtime;\r\n\r\n        return filemod_status_e::modified;\r\n    }\r\n\r\n    void invalidate()\r\n    {\r\n        modified_time = 0;\r\n    }\r\n};\r\n\r\n//-------------------------------------------------------------------------\r\n// Dependency script info\r\nstruct script_info_t: fileinfo_t\r\n{\r\n    using fileinfo_t::fileinfo_t;\r\n\r\n    // Each dependency script can have its own reload command\r\n    qstring reload_cmd;\r\n\r\n    // Base path if this dependency is part of a package\r\n    qstring pkg_base;\r\n\r\n    const bool has_reload_directive() const { return !reload_cmd.empty(); }\r\n};\r\n\r\n// Script files\r\nusing scripts_info_t = qvector<script_info_t>;\r\n\r\n//-------------------------------------------------------------------------\r\n// Notebook context\r\nstruct notebook_ctx_t\r\n{\r\n    enum activate_action_e\r\n    {\r\n        act_exec_none,\r\n        act_exec_main,\r\n        act_exec_all\r\n    };\r\n    std::string base_path;\r\n    std::string title;\r\n    std::regex cells_re = std::regex(DEFAULT_CELLS_RE);\r\n    std::map<std::string, qtime64_t> cell_files;\r\n    std::string last_active_cell;\r\n\r\n    int activation_action = act_exec_none;\r\n\r\n    void clear()\r\n    {\r\n        title.clear();\r\n        cell_files.clear();\r\n        last_active_cell.clear();\r\n        cells_re = std::regex(DEFAULT_CELLS_RE);\r\n    }\r\n};\r\n\r\n//-------------------------------------------------------------------------\r\n// Active script information along with its dependencies\r\nstruct active_script_info_t : script_info_t\r\n{\r\n    // Notebook options\r\n    bool b_is_notebook = false;\r\n\r\n    const bool is_notebook() const {\r\n        return b_is_notebook;\r\n    }\r\n\r\n    notebook_ctx_t notebook;\r\n\r\n    // Trigger file options\r\n    fileinfo_t trigger_file;\r\n    bool b_keep_trigger_file;\r\n\r\n    // The dependencies index files. First entry is for the main script's deps\r\n    qvector<fileinfo_t> dep_indices;\r\n\r\n    // The list of dependency scripts\r\n    std::unordered_map<std::string, script_info_t> dep_scripts;\r\n\r\n    // Checks to see if we have a dependency on a given file\r\n    const script_info_t* has_dep(const qstring& dep_file) const\r\n    {\r\n        auto p = dep_scripts.find(dep_file.c_str());\r\n        return p == dep_scripts.end() ? nullptr : &p->second;\r\n    }\r\n\r\n    // Is this trigger based or dependency based?\r\n    const bool trigger_based() const { return !trigger_file.empty(); }\r\n\r\n    // If no dependency index files have been modified, return 0.\r\n    // Return 1 if one of them has been modified or -1 if one of them has gone missing.\r\n    // In both latter cases, we have to recompute our dependencies\r\n    filemod_status_e is_any_dep_index_modified(bool update_mtime = true)\r\n    {\r\n        filemod_status_e r = filemod_status_e::not_modified;\r\n        for (auto& dep_file : dep_indices)\r\n        {\r\n            r = dep_file.get_modification_status(update_mtime);\r\n            if (r != filemod_status_e::not_modified)\r\n                break;\r\n        }\r\n        return r;\r\n    }\r\n\r\n    bool add_dep_index(const char* dep_file)\r\n    {\r\n        fileinfo_t fi;\r\n        if (!get_file_modification_time(dep_file, &fi.modified_time))\r\n            return false;\r\n\r\n        fi.file_path = dep_file;\r\n        dep_indices.push_back(std::move(fi));\r\n        return true;\r\n    }\r\n\r\n    void clear() override\r\n    {\r\n        script_info_t::clear();\r\n        dep_indices.qclear();\r\n        dep_scripts.clear();\r\n        trigger_file.clear();\r\n        b_keep_trigger_file = false;\r\n        b_is_notebook = false;\r\n        notebook.clear();\r\n        reload_cmd.clear();\r\n        pkg_base.clear();\r\n    }\r\n\r\n    void invalidate_all_scripts()\r\n    {\r\n        invalidate();\r\n\r\n        // Invalidate all but the index file itself\r\n        for (auto& kv : dep_scripts)\r\n            kv.second.invalidate();\r\n    }\r\n};\r\n"
  },
  {
    "path": "snippet_manager/README.md",
    "content": "# Snippet Manager Plugin for IDA\n\nThis plugin for IDA provides a set of functionality for importing, exporting or deleting all snippets from IDA (Shift-F2 window).\n\n## Functions\n\nType the following functions from IDA's CLI or just call them directly from Python:\n\n- `idaapi.ext.snippets.save(output_folder='')`: Save snippets to a specified output folder.\n- `idaapi.ext.snippets.load(input_folder='')`: Load snippets from a specified input folder.\n- `idaapi.ext.snippets.delete()`: Delete all existing snippets.\n- `idaapi.ext.snippets.man`: Direct access to the snippet manager object.\n\nBy default, if no input or output folders are specified, then the plugin will default to the `os.path.join(os.path.dirname(idc.get_idb_path()), '.snippets')` folder.\n\n## Installation\n\nCopy the `snippetmanager.py` file to your IDA plugin folder of your choice.\n\nAlternatively, for example on Windows, just make a symbolic link directly:\n\n```batch\nC:\\Projects\\ida-qscripts\\snippet_manager>mklink c:\\ida\\plugins\\snippetmanager.py %cd%\\snippetmanager.py\n```\n\n## Very important\n\nDue to the IDA UI's caching of snippets in memory, changes will not be immediately visible. Therefore, upon executing any of the functions, it is obligatory to close, save, and then reopen the database manually in order to observe the modifications."
  },
  {
    "path": "snippet_manager/pseudocode.cpp",
    "content": "// The following snippet / pseudo-code has been kindly shared by  Arnaud Diederen from Hex-Rays\r\n\r\n////\r\n//// save snippets + rebuild index\r\n////\r\n\r\nvoid save_all_snippets()\r\n{ \r\n  netnode main_node;\r\n  main_node.create(\"$ scriptsnippets\");\r\n  \r\n  atag = 'A'\r\n  main_node.altdel_all(atag);\r\n  \r\n  // save each, plus add in index\r\n  for i in range(snippets):\r\n      netnode snippet_node = snippet(i).save(); // see below\r\n      main_node.altset(i, snippet_node+1);\r\n}\r\n \r\nnetnode snippet_t::save()\r\n{\r\n   xtag = 'X'\r\n   if ( node == BADNODE )\r\n     node.create();\r\n   node.supset(0, name.c_str());\r\n   node.supset(1, lang->name);\r\n   node.setblob(body.c_str(), body.size(), 0, xtag); // body\r\n   return node;\r\n}\r\n"
  },
  {
    "path": "snippet_manager/snippetmanager.py",
    "content": "\"\"\"\r\nSnippet loader/saver for IDA Pro\r\n\r\nby Elias Bachaalany / @allthingsida\r\n\"\"\"\r\n\r\n# TODO:\r\n# - snippetmanager: run snippets in order (optional prefix RE; comes with stock REs); save_combined()\r\n# - - save combined()\r\n# - save_clean() --> delete previous snippets and save\r\n# - stock regular expressions: \\d+\r\n\r\nimport os\r\nfrom typing import Union\r\nimport idaapi, idc\r\n\r\nLANG_EXT_MAP = {\r\n    \"Python\": \"py\",\r\n    \"IDC\": \"idc\",\r\n}\r\n\r\nR_LANG_EXT_MAP = {v: k for k, v in LANG_EXT_MAP.items()}\r\n\r\n# --------------------------------------------------------------\r\nclass snippet_t:\r\n    def __init__(self, \r\n                 lang: str, \r\n                 name: str, \r\n                 body: str, /, \r\n                 netnode_idx: int = idaapi.BADNODE, \r\n                 index: int =-1):\r\n        self.lang : str = lang\r\n        self.name : str = name\r\n        self.body : str = body\r\n        self.netnode_idx : int = netnode_idx\r\n        \"\"\"Underlying netnode index\"\"\"\r\n        self.index : int = index\r\n        \"\"\"Position in the index table\"\"\"\r\n\r\n    def __repr__(self) -> str:\r\n        return f\"snippet_t({self.lang}, {self.name}, {self.body}, {self.netnode_idx}, {self.index})\"\r\n\r\n    def __str__(self) -> str:\r\n        return f\"lang: {self.lang}; title: {self.name}; index: {self.index}\"\r\n\r\n    def save(self, index: int) -> None:\r\n        \"\"\"Saves a node and updates the netnode index that was used to save it\"\"\"\r\n        node = idaapi.netnode()\r\n        node.create()\r\n\r\n        node.supset(1, self.lang)\r\n        node.supset(0, self.name)\r\n        node.setblob(self.body.encode('utf-8'), 0, 'X')\r\n\r\n        self.index = index\r\n        self.netnode_idx = node.index()\r\n\r\n    @staticmethod\r\n    def from_file(file_name: str) -> Union[None, 'snippet_t']:\r\n        \"\"\"Constructs a snippet from a file.\r\n        Its slot index and netnode index are not known yet.\r\n        Call save() to save it to the database and get the netnode index\r\n        \"\"\"\r\n        if not os.path.exists(file_name):\r\n            return None\r\n\r\n        basename = os.path.basename(file_name)\r\n        ext = os.path.splitext(basename)[1].lstrip('.').lower()\r\n        if not (lang := R_LANG_EXT_MAP.get(ext)):\r\n            return None\r\n\r\n        with open(file_name, 'r') as f:\r\n            body = f.read()\r\n\r\n        name = os.path.splitext(basename)[0]\r\n        return snippet_t(lang, name, body)\r\n\r\n    @staticmethod\r\n    def from_netnode(netnode_idx: int, slot_idx: int, fast: bool = False):\r\n        node = idaapi.netnode(netnode_idx)\r\n        body = None if fast else node.getblob(0, 'X')\r\n        return snippet_t(node.supstr(1),\r\n                         node.supstr(0),\r\n                         body.decode('utf-8').rstrip('\\x00') if body else \"\",\r\n                         netnode_idx=netnode_idx,\r\n                         index=slot_idx)\r\n\r\n# --------------------------------------------------------------\r\nclass snippet_manager_t:\r\n    def __init__(self):\r\n        self.index = idaapi.netnode()\r\n        self.index.create(\"$ scriptsnippets\")\r\n\r\n    def delete(self, snippet: snippet_t) -> bool:\n        if snippet.netnode_idx == idaapi.BADNODE:\n            return False\n\n        self.index.altdel(snippet.index)\n        idaapi.netnode(snippet.netnode_idx).kill()\n        snippet.netnode_idx = idaapi.BADNODE\n        return True\n\r\n    def delete_all(self) -> None:\r\n        snippets = self.retrieve_snippets(fast=True)\r\n        for snip in snippets:\r\n            self.delete(snip)\r\n\r\n        self.index.altdel_all()\r\n\r\n    def load_from_folder(self, folder : str = '') -> bool:\r\n        \"\"\"Imports snippets from a folder.\"\"\"\r\n        # If no input directory is specified, use the default one\r\n        if not folder:\r\n            folder = os.path.join(os.path.dirname(idc.get_idb_path()), '.snippets')\r\n            # For the default directory, create it if it doesn't exist\r\n            if not os.path.exists(folder):\r\n                os.makedirs(folder)\r\n\r\n        # For a custom directory, check if it exists\r\n        if not os.path.exists(folder):\r\n            return False\r\n\r\n        # Delete previous snippets (and reset index)\r\n        self.delete_all()\r\n\r\n        # Enumerate files\r\n        snippets = []\r\n        for file in os.listdir(folder):\r\n            file = os.path.join(folder, file)\r\n            if snippet := snippet_t.from_file(file):\r\n                snippets.append(snippet)\r\n\r\n        # Sort by snippet name\r\n        snippets.sort(key=lambda x: x.name)\r\n\r\n        for isnippet, snippet in enumerate(snippets):\r\n            snippet.save(isnippet)\r\n            self.index.altset(isnippet, snippet.netnode_idx + 1)\r\n\r\n        return True\r\n\r\n    def save_to_folder(self, folder: str = '') -> tuple[bool, str]:\r\n        if not folder:\r\n            folder = os.path.join(os.path.dirname(idc.get_idb_path()), '.snippets')\r\n\r\n        if not os.path.exists(folder):\r\n            os.makedirs(folder)\r\n\r\n        try:\r\n            snippets = self.retrieve_snippets()\r\n            for snip in snippets:\r\n                outfile_name = os.path.join(folder, \"%s.%s\" % (snip.name, LANG_EXT_MAP[snip.lang]))\r\n                with open(outfile_name, 'w') as outfile:\r\n                    outfile.write(snip.body)\r\n        except Exception as e:\r\n            return (False, f'Failed to save: {e!s}')\r\n        return (True, f'Saved {len(snippets)} snippets to {folder}')\r\n\r\n\r\n    def retrieve_snippets(self, fast: bool = False) -> list[snippet_t]:\r\n        \"\"\"Load all snippets from the database\"\"\"\r\n        snip_idx = self.index.altfirst()\r\n        if snip_idx == idaapi.BADNODE:\r\n            return []\r\n\r\n        snippets = []\r\n        while snip_idx != idaapi.BADNODE:\r\n            snip_node_idx = self.index.altval(snip_idx) - 1\r\n            snippet = snippet_t.from_netnode(snip_node_idx, snip_idx, fast=fast)\r\n            snippets.append(snippet)\r\n\r\n            snip_idx = self.index.altnext(snip_idx)\r\n\r\n        return snippets\r\n\r\n\r\n# --------------------------------------------------------------\r\n# Register some functions into the extension part of the IDA API\r\next = getattr(idaapi, 'ext', None)\r\nif not ext:\r\n    ext = idaapi.ext = idaapi.object_t()\r\n\r\n_sm = snippet_manager_t()\r\n\r\ndef save_snippets(output_folder: str =''):\r\n    \"\"\"\r\n    Save snippets to a specified output folder. If no folder is specified, \r\n    the function uses the current directory by default.\r\n\r\n    Args:\r\n    output_folder (str): The folder where the snippets will be saved. \r\n                         Defaults to current directory if not specified.\r\n\r\n    Returns: \r\n    Result of the operation performed by the save_to_folder method.\r\n    \"\"\"\r\n    return _sm.save_to_folder(output_folder)\r\n\r\ndef load_snippets(input_folder: str =''):\r\n    \"\"\"\r\n    Load snippets from a specified input folder. If no folder is specified, \r\n    the function uses the current directory by default.\r\n\r\n    Args:\r\n    input_folder (str): The folder from where the snippets will be loaded. \r\n                        Defaults to current directory if not specified.\r\n\r\n    Returns: \r\n    Result of the operation performed by the load_from_folder method.\r\n    \"\"\"\r\n    return _sm.load_from_folder(input_folder)\r\n\r\ndef delete_snippets():\r\n    \"\"\"\r\n    Delete all existing snippets.\r\n\r\n    Returns: \r\n    Result of the operation performed by the delete_all method.\r\n    \"\"\"\r\n    return _sm.delete_all()\r\n\r\next.snippets = idaapi.object_t(\r\n    save=save_snippets,\r\n    load=load_snippets,\r\n    delete=delete_snippets,\r\n    man=_sm,\r\n)\r\n\r\n# --------------------------------------------------------------\r\ndef _test_load(with_body=False):\r\n    idaapi.msg_clear()\r\n    global sm\r\n    sm = _sm\r\n    snippets = sm.retrieve_snippets()\r\n    for isnip, snip in enumerate(snippets):\r\n        print(f\"#{isnip:03d} {snip!s}\")\r\n        if with_body:\r\n            print(\"<body>\\n%s\" % snip.body)\r\n            print(\"</body>\\n------\")\r\n\r\n# --------------------------------------------------------------\r\nclass snippetman_plugmod_t(idaapi.plugmod_t):\r\n    def run(self, _):\r\n        print(\"\"\"Please use the following methods to:\r\n    - save snippets: idaapi.ext.snippets.save()\r\n    - load snippets: idaapi.ext.snippets.load()\r\n    - delete snippets: idaapi.ext.snippets.delete()\r\n    - access the snippet manager object: idaapi.ext.snippets.man\r\n\"\"\")\r\n        return 0\r\n\r\nclass snippetman_plugin_t(idaapi.plugin_t):\r\n    flags = idaapi.PLUGIN_MULTI\r\n    comment = \"Snippet manager plugin\"\r\n    help = \"Run the plugin to get the full help message\"\r\n    wanted_name = \"Snippet manager\"\r\n    wanted_hotkey = \"\"\r\n\r\n    def init(self):\r\n        return snippetman_plugmod_t()\r\n\r\ndef PLUGIN_ENTRY() -> idaapi.plugin_t:\r\n    return snippetman_plugin_t()\r\n\r\n# --------------------------------------------------------------\r\nif __name__ == \"__main__\":\r\n    _test_load(False)\r\n"
  },
  {
    "path": "test_addons/README.md",
    "content": "# QScripts Test Add-ons\r\n\r\nQScripts streamlines IDA Pro add-on development by automatically reloading plugins, loaders, and processors when their compiled binaries change—no need to restart IDA Pro. This greatly accelerates the development cycle and boosts productivity.\r\n\r\nThis directory provides example native add-ons (plugins, loaders, processors) show casing QScripts' hot-reload workflow for rapid iteration and testing.\r\n\r\n\r\n## Available Templates\r\n\r\n### [plugin_template](./plugin_template)\r\nBasic plugin template for general IDA Pro plugin development.\r\n\r\n### [plugin_triton](./plugin_triton)\r\nAdvanced plugin integrating the [Triton](https://github.com/JonathanSalwan/Triton) dynamic binary analysis framework.\r\n\r\n### [loader_template](./loader_template)\r\nTemplate for developing custom file format loaders (with mock `accept_file` and `load_file` implementations).\r\n\r\n## Building Add-ons\r\n\r\n### Standard Build Process\r\n\r\n1. **Navigate to the add-on directory:**\r\n   ```bash\r\n   cd test_addons/<addon_name>\r\n   ```\r\n\r\n2. **Configure with CMake:**\r\n   ```bash\r\n   cmake -B build -A x64\r\n   ```\r\n\r\n3. **Build the add-on:**\r\n   ```bash\r\n   cmake --build build --config RelWithDebInfo\r\n   ```\r\n\r\nThe compiled binary will be automatically installed to:\r\n- Plugins: `%IDASDK%/bin/plugins/`\r\n- Loaders: `%IDASDK%/bin/loaders/`\r\n- Processors: `%IDASDK%/bin/procs/`\r\n\r\n## Hot-Reload Development Workflow\r\n\r\n### 1. Create a Python Script\r\n```python\r\nimport time\r\nimport idaapi\r\n\r\n# Give the linker time to finish\r\ntime.sleep(1)\r\n\r\n# Load your plugin (adjust name and arguments as needed)\r\nidaapi.load_and_run_plugin('your_addon_name', 0)\r\n```\r\n\r\n### 2. Configure Dependencies\r\nCreate a `.deps.qscripts` file to specify what triggers reloading:\r\n\r\n```\r\n# Monitor the compiled plugin DLL\r\n/triggerfile /keep $env:IDASDK$/bin/plugins/your_addon$ext$\r\n```\r\n\r\n### 3. Activate Monitoring\r\n1. Open QScripts chooser\r\n2. Navigate to your Python script\r\n3. Press `Enter` to activate monitoring\r\n4. The script name appears in **bold** when active\r\n\r\n### 4. Development Cycle\r\n1. Modify your C++ code\r\n2. Rebuild the add-on\r\n3. QScripts automatically detects changes and reloads\r\n4. No IDA restart required!\r\n"
  },
  {
    "path": "test_addons/loader_template/.gitignore",
    "content": "build*/"
  },
  {
    "path": "test_addons/loader_template/CMakeLists.txt",
    "content": "cmake_minimum_required(VERSION 3.27)\r\nproject(qscripts_native)\r\n\r\nset(CMAKE_CXX_STANDARD 17)\r\nset(CMAKE_CXX_STANDARD_REQUIRED ON)\r\n\r\n# Include IDA SDK bootstrap\r\ninclude($ENV{IDASDK}/ida-cmake/bootstrap.cmake)\r\nfind_package(idasdk REQUIRED)\r\n\r\n# Add loader\r\nida_add_loader(qscripts_native\r\n    SOURCES\r\n        driver.cpp\r\n        main.cpp\r\n        idasdk.h\r\n    DEBUG_ARGS\r\n        \"-t\"\r\n)\r\n"
  },
  {
    "path": "test_addons/loader_template/driver.cpp",
    "content": "#include \"idasdk.h\"\n\nextern bool main();\n\n//--------------------------------------------------------------------------\nstruct plugin_ctx_t : public plugmod_t\n{\n    bool idaapi run(size_t) override\n    {\n        return main();\n    }\n};\n\n//--------------------------------------------------------------------------\nplugin_t PLUGIN =\n{\n  IDP_INTERFACE_VERSION,\n  PLUGIN_UNL | PLUGIN_MULTI,\n  []()->plugmod_t* {return new plugin_ctx_t; },\n  nullptr,\n  nullptr,\n  nullptr,\n  nullptr,\n  \"QScripts native plugin driver\",\n  nullptr,\n};\n"
  },
  {
    "path": "test_addons/loader_template/idasdk.h",
    "content": "#pragma warning(push)\r\n#pragma warning(disable: 4244 4267)\r\n\r\n#include <ida.hpp>\r\n#include <idp.hpp>\r\n#include <loader.hpp>\r\n#include <kernwin.hpp>\r\n#include <name.hpp>\r\n#include <funcs.hpp>\r\n#include <hexrays.hpp>\r\n#include <diskio.hpp>\r\n#include <entry.hpp>\r\n\r\n#pragma warning(pop)\r\n"
  },
  {
    "path": "test_addons/loader_template/main.cpp",
    "content": "#include \"idasdk.h\"\r\n\r\n#pragma pack(push, 1)\r\nstruct file_header_t\r\n{\r\n    char sig[4];      // Signature == \"CHNK\"\r\n    char cpuname[10]; // Processor name (for set_processor_type())\r\n    int32 nchunks;    // Number of chunks\r\n    int32 entrypoint; // The entry point address\r\n};\r\n\r\nstruct chunk_t\r\n{\r\n    int32 base;     // base address\r\n    int32 sz;       // size\r\n};\r\n#pragma pack(pop)\r\n\r\nstatic int idaapi accept_file(\r\n    qstring* fileformatname,\r\n    qstring* processor,\r\n    linput_t* li,\r\n    const char* filename)\r\n{\r\n    file_header_t fh;\r\n    lread(li, &fh, sizeof(file_header_t));\r\n    if (strncmp(fh.sig, \"CHNK\", 4) != 0)\r\n        return 0;\r\n\r\n    *fileformatname = \"Chunk file loader\";\r\n    *processor = fh.cpuname;\r\n\r\n    return 1;\r\n}\r\n\r\n//--------------------------------------------------------------------------\r\n//\r\n//      load file into the database.\r\n//\r\nvoid idaapi load_file(linput_t* li, ushort neflag, const char* fileformatname)\r\n{\r\n    // Read the header to detect the number of chunks, proc name, etc.\r\n    file_header_t fh;\r\n    lread(li, &fh, sizeof(file_header_t));\r\n\r\n    set_processor_type(fh.cpuname, SETPROC_USER);\r\n\r\n    for (int32 i = 0; i < fh.nchunks; ++i)\r\n    {\r\n        qstring seg_name;\r\n        seg_name.sprnt(\"chunk%d\", i);\r\n\r\n        chunk_t chkinfo;\r\n        lread(li, &chkinfo, sizeof(chkinfo));\r\n\r\n        ea_t start_ea = chkinfo.base;\r\n        ea_t end_ea = chkinfo.base + chkinfo.sz;\r\n        add_segm(\r\n            0,\r\n            start_ea, \r\n            end_ea, \r\n            seg_name.c_str(),\r\n            \"CODE\",\r\n            0);\r\n\r\n        // Now read the actual data\r\n        file2base(li, qltell(li), start_ea, end_ea, 1);\r\n    }\r\n    inf_set_start_ea(fh.entrypoint);\r\n    inf_set_start_ip(fh.entrypoint);\r\n    inf_set_start_cs(0);\r\n    add_entry(0, fh.entrypoint, \"start\", 1, 0);\r\n}\r\n\r\nbool test_accept_file(linput_t *li, const char *fname)\r\n{\r\n    qstring format_name, procname;\r\n    if (accept_file(&format_name, &procname, li, fname))\r\n    {\r\n        msg(\"Recognized format name: %s\\n\", format_name.c_str());\r\n        msg(\"Suggest proc module   : %s\\n\", procname.c_str());\r\n        return true;\r\n    }\r\n    else\r\n    {\r\n        msg(\"Not recognized!\\n\");\r\n        return false;\r\n    }\r\n}\r\n\r\nbool main()\r\n{\r\n    msg_clear();\r\n\r\n    auto fname = R\"(C:\\Users\\elias\\Projects\\github\\ida-qscripts\\samples\\chunk1.bin)\";\r\n    auto li = open_linput(fname, false);\r\n\r\n    if (!test_accept_file(li, fname))\r\n        return false;\r\n    \r\n    load_file(li, 0, fname);\r\n    close_linput(li);\r\n}"
  },
  {
    "path": "test_addons/plugin_template/.claude/agents/ida-cmake.md",
    "content": "---\r\nname: ida-cmake\r\ndescription: Use this agent to compile/build IDA Pro extensions including plugins, idalib applications, processor modules, or file loaders (all known as IDA addons) using the IDA C++ SDK. Used this agent to create a new addon template or for troubleshooting build issues, configuring the IDASDK environment, or integrating ida-cmake into existing projects.\r\nmodel: sonnet\r\ncolor: orange\r\n---\r\n\r\nIDA Pro addons usually refer to: plugins, processor modules, file loaders, or idalib standalone applications.\r\nYou are an expert in building these addons using the ida-cmake CMake scripts located in `$IDASDK/ida-cmake`.\r\n\r\n- Refer to `$IDASDK/ida-cmake/README.md` for documentation on how to use `ida-cmake`.\r\n- Refer to `$IDASDK/ida-cmake/templates/plugin/` for example CMakeLists.txt file and plugin\r\n- Refer to `$IDASDK/ida-cmake/templates/loader` for example CMakeLists.txt file and loader\r\n- Refer to `$IDASDK/ida-cmake/templates/procmod` for example CMakeLists.txt file and processor module\r\n- Refer to `$IDASDK/ida-cmake/templates/idalib` for example CMakeLists.txt file and idalib application\r\n- Refer to `$IDASDK/include` for SDK headers. All headers have docstrings, use them to explain SDK usage and lookup APIs.\r\n- Refer to `$IDASDK/plugins`, `$IDASDK/loaders`, `$IDASDK/module` for SDK example and how APIs are used in practice.\r\n\r\nYou must open and read those files above to answer the user correctly to answer all things IDA compiling and building related questions.\r\n\r\n## CMake Usage\r\n\r\nThe ida-cmake provides clean interface libraries:\r\n- `idasdk::plugin` - For IDA plugins\r\n- `idasdk::loader` - For file loaders\r\n- `idasdk::procmod` - For processor modules\r\n- `idasdk::idalib` - For standalone idalib applications\r\n\r\n## Example Usage\r\n\r\nFor quick reference, here's a skeleton CMakeLists.txt begining:\r\n\r\n```cmake\r\ncmake_minimum_required(VERSION 3.27)\r\nproject(myplugin)\r\nset(CMAKE_CXX_STANDARD 17)\r\n\r\n# Include IDA SDK bootstrap\r\ninclude($ENV{IDASDK}/ida-cmake/bootstrap.cmake)\r\nfind_package(idasdk REQUIRED)\r\n```\r\n\r\nNow, for a plugin, use the `ida_add_plugin` function:\r\n\r\n```cmake\r\nida_add_plugin(myplugin\r\n    SOURCES\r\n        main.cpp\r\n    DEBUG_ARGS\r\n        \"-t -z10000\"\r\n)```\r\n\r\n...and for a file loader, use the `ida_add_loader` function:\r\n\r\n```cmake\r\nida_add_loader(myloader\r\n    SOURCES\r\n        loader.cpp\r\n    DEBUG_ARGS\r\n        \"-c -A\"  # Common loader debug args\r\n)```\r\n\r\n...and for a standalone idalib application, use the following CMakeLists.txt:\r\n\r\n```cmake\r\nida_add_idalib_exe(myidalib\r\n    SOURCES\r\n        main.cpp\r\n    DEBUG_ARGS\r\n        \"${IDASDK}/ida-cmake/samples/wizmo32.exe.i64\"\r\n)```\r\n\r\nTo build an addon, just use something like from the addon source folder:\r\n\r\n```bash\r\ncmake -B build\r\ncmake --build build --config RelWithDebInfo\r\n```\r\n"
  },
  {
    "path": "test_addons/plugin_template/.gitignore",
    "content": "build*/"
  },
  {
    "path": "test_addons/plugin_template/CLAUDE.md",
    "content": "# CLAUDE.md\r\n\r\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\r\n\r\n# qscripts_native - IDA Pro Plugin Template\r\n\r\nThis is a template for developing IDA Pro plugins using the QScripts hot-reload workflow. The plugin lists non-library functions in the current database.\r\n\r\n## Build Commands\r\n\r\nAlways use the ida-cmake agent for building:\r\n```\r\nTask with subagent_type=\"ida-cmake\"\r\n```\r\n\r\n## Prerequisites\r\n\r\n- **IDASDK**: Environment variable must be set to IDA SDK path\r\n- **IDAX**: Must be installed at `$IDASDK/include/idax/`\r\n- **ida-cmake**: Must be installed at `$IDASDK/ida-cmake/`\r\n\r\n## Architecture\r\n\r\n### File Structure\r\n- `driver.cpp`: IDA plugin interface boilerplate (PLUGIN structure and plugmod_t implementation)\r\n- `main.cpp`: Core plugin logic - implement your functionality here\r\n- `idasdk.h`: Consolidated IDA SDK header includes with warning suppression\r\n- `CMakeLists.txt`: Build configuration using ida-cmake\r\n\r\n### Plugin Behavior\r\n- **Type**: PLUGIN_UNL | PLUGIN_MULTI (unloadable, multi-instance)\r\n- **Entry Point**: `main()` function in main.cpp receives size_t argument\r\n- **Current Function**: Enumerates all non-library functions and prints their names\r\n\r\n## Development Workflow\r\n\r\nThis template is designed for use with QScripts hot-reload:\r\n1. Parent directory contains `qscripts_native.py.deps.qscripts` with trigger file configuration\r\n2. Built plugin outputs to `$IDASDK/bin/plugins/qscripts_native[.dll/.so/.dylib]`\r\n3. QScripts monitors the output file and automatically reloads on changes\r\n\r\n## SDK and IDA API Resources\r\n\r\nWhen answering SDK/API questions, search and read from:\r\n- **SDK Headers**: `$IDASDK/include` - All headers have docstrings\r\n- **SDK Examples**: `$IDASDK/plugins`, `$IDASDK/loaders`, `$IDASDK/module`"
  },
  {
    "path": "test_addons/plugin_template/CMakeLists.txt",
    "content": "cmake_minimum_required(VERSION 3.27)\r\nproject(qscripts_native)\r\n\r\nset(CMAKE_CXX_STANDARD 17)\r\nset(CMAKE_CXX_STANDARD_REQUIRED ON)\r\n\r\n# Include IDA SDK bootstrap\r\ninclude($ENV{IDASDK}/ida-cmake/bootstrap.cmake)\r\nfind_package(idasdk REQUIRED)\r\n\r\n# Add plugin\r\nida_add_plugin(qscripts_native\r\n    SOURCES\r\n        driver.cpp\r\n        main.cpp\r\n        idasdk.h\r\n    DEBUG_ARGS\r\n        \"-t\"\r\n)\r\n"
  },
  {
    "path": "test_addons/plugin_template/driver.cpp",
    "content": "#include \"idasdk.h\"\n\nextern bool main(size_t);\n\n//--------------------------------------------------------------------------\nstruct plugin_ctx_t : public plugmod_t\n{\n    bool idaapi run(size_t arg) override\n    {\n        return main(arg);\n    }\n};\n\n//--------------------------------------------------------------------------\nplugin_t PLUGIN =\n{\n  IDP_INTERFACE_VERSION,\n  PLUGIN_UNL | PLUGIN_MULTI,\n  []()->plugmod_t* {return new plugin_ctx_t; },\n  nullptr,\n  nullptr,\n  nullptr,\n  nullptr,\n  \"QScripts native plugin driver\",\n  nullptr,\n};\n"
  },
  {
    "path": "test_addons/plugin_template/idasdk.h",
    "content": "#pragma warning(push)\r\n#pragma warning(disable: 4244 4267)\r\n\r\n#include <ida.hpp>\r\n#include <idp.hpp>\r\n#include <loader.hpp>\r\n#include <kernwin.hpp>\r\n#include <name.hpp>\r\n#include <funcs.hpp>\r\n// Include more headers here...\r\n#pragma warning(pop)\r\n"
  },
  {
    "path": "test_addons/plugin_template/main.cpp",
    "content": "#include \"idasdk.h\"\r\n\r\nbool main(size_t)\r\n{\r\n    auto nfuncs = get_func_qty();\r\n\r\n    size_t c = 0;\r\n    for (size_t i = 0; i < nfuncs; ++i)\r\n    {\r\n        func_t* func = getn_func(i);\r\n        if (func->flags & FUNC_LIB)\r\n            continue;\r\n\r\n        ++c;\r\n        qstring name;\r\n        get_func_name(&name, func->start_ea);\r\n        msg(\"%a: %s\\n\", func->start_ea, name.c_str());\r\n    }\r\n\r\n    msg(\"%zu function(s)\\n\", c);\r\n\treturn true;\r\n}"
  },
  {
    "path": "test_addons/plugin_triton/.gitignore",
    "content": "build*/\r\n.z3-trace\r\njunk64.bin.id?\r\njunk64.bin.til\r\njunk64.bin.nam"
  },
  {
    "path": "test_addons/plugin_triton/CMakeLists.txt",
    "content": "cmake_minimum_required(VERSION 3.27)\r\nproject(qscripts_native_triton)\r\n\r\n# Make sure 'tritonenv.bat' script has been called and the proper environment variables are set\r\n\r\nset(CMAKE_CXX_STANDARD 17)\r\nset(CMAKE_CXX_STANDARD_REQUIRED ON)\r\n\r\n# Include IDA SDK bootstrap\r\ninclude($ENV{IDASDK}/ida-cmake/bootstrap.cmake)\r\nfind_package(idasdk REQUIRED)\r\n\r\n# Extract vcpkg include path from TRITON_LIB environment variable\r\nstring(REPLACE \";\" \";\" TRITON_LIB_LIST \"$ENV{TRITON_LIB}\")\r\nforeach(LIB_PATH IN LISTS TRITON_LIB_LIST)\r\n    # Handle both Windows backslashes and Unix forward slashes\r\n    if(LIB_PATH MATCHES \"(.+)[/\\\\\\\\]vcpkg_installed[/\\\\\\\\]([^/\\\\\\\\]+)[/\\\\\\\\]lib[/\\\\\\\\]\")\r\n        set(VCPKG_INCLUDE_DIR \"${CMAKE_MATCH_1}/vcpkg_installed/${CMAKE_MATCH_2}/include\")\r\n        # Normalize path separators for consistency\r\n        string(REPLACE \"\\\\\" \"/\" VCPKG_INCLUDE_DIR \"${VCPKG_INCLUDE_DIR}\")\r\n        break()\r\n    endif()\r\nendforeach()\r\n\r\n# Add plugin with Triton support\r\nida_add_plugin(qscripts_native_triton\r\n    SOURCES\r\n        driver.cpp\r\n        main.cpp\r\n    INCLUDES\r\n        $ENV{TRITON_INCLUDES}\r\n        ${VCPKG_INCLUDE_DIR}\r\n    LIBRARIES\r\n        $ENV{TRITON_LIB}\r\n    DEBUG_ARGS\r\n        ${CMAKE_CURRENT_LIST_DIR}/junk64.bin.i64\r\n)\r\n\r\n# Disable specific warnings\r\ntarget_compile_options(qscripts_native_triton PRIVATE /wd4267 /wd4244)\r\n"
  },
  {
    "path": "test_addons/plugin_triton/README.md",
    "content": "# Triton Plugin for IDA Pro with QScripts\r\n\r\nA native IDA Pro plugin that integrates the [Triton](https://github.com/JonathanSalwan/Triton) dynamic binary analysis framework, demonstrating QScripts' hot-reload capabilities for rapid plugin development.\r\n\r\n## Quick Start\r\n\r\n### Installing Triton on Windows\r\n\r\nLet's clone to `C:\\tools` for this example:\r\n\r\n1. **Install vcpkg** (if not already installed):\r\n   ```batch\r\n   cd C:\\tools\r\n   git clone https://github.com/Microsoft/vcpkg.git\r\n   cd vcpkg\r\n   bootstrap-vcpkg.bat\r\n   ```\r\n\r\n2. **Clone Triton**:\r\n   ```batch\r\n   cd C:\\tools\r\n   git clone https://github.com/JonathanSalwan/Triton.git\r\n   cd Triton\r\n   ```\r\n\r\n3. **Build with static runtime** (required for IDA plugins):\r\n   ```batch\r\n   cmake -B build-static -A x64 ^\r\n         -DCMAKE_TOOLCHAIN_FILE=\"c:\\tools\\vcpkg\\scripts\\buildsystems\\vcpkg.cmake\" ^\r\n         -DVCPKG_TARGET_TRIPLET=x64-windows-static ^\r\n         -DBUILD_SHARED_LIBS=OFF ^\r\n         -DPYTHON_BINDINGS=OFF ^\r\n         -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded\r\n\r\n   cmake --build build-static --config Release\r\n   ```\r\n\r\n### Building the Plugin\r\n\r\n1. **Set up environment variables** (required):\r\n   ```batch\r\n   set TRITON_INCLUDES=C:\\tools\\Triton\\src\\libtriton\\includes;C:\\tools\\Triton\\build-static\\src\\libtriton\\includes\r\n   set TRITON_LIB=C:\\tools\\Triton\\build-static\\src\\libtriton\\Release\\triton.lib;C:\\tools\\Triton\\build-static\\vcpkg_installed\\x64-windows-static\\lib\\libz3.lib;C:\\tools\\Triton\\build-static\\vcpkg_installed\\x64-windows-static\\lib\\capstone.lib\r\n   ```\r\n\r\n2. **Build the plugin**:\r\n   ```batch\r\n   cmake -B build -A x64\r\n   cmake --build build --config RelWithDebInfo\r\n   ```\r\n\r\n3. **Plugin is automatically installed** to `%IDASDK%\\bin\\plugins\\qscripts_native_triton.dll`\r\n\r\n4. Activate the `qscripts_native_triton.py` to load the plugin in IDA and start monitoring.\r\n"
  },
  {
    "path": "test_addons/plugin_triton/driver.cpp",
    "content": "#include \"idasdk.h\"\n\nextern bool main(size_t);\n\n//--------------------------------------------------------------------------\nstruct plugin_ctx_t : public plugmod_t\n{\n    bool idaapi run(size_t arg) override\n    {\n        return main(arg);\n    }\n};\n\n//--------------------------------------------------------------------------\nplugin_t PLUGIN =\n{\n    IDP_INTERFACE_VERSION,\n    PLUGIN_UNL | PLUGIN_MULTI,\n    []()->plugmod_t* { return new plugin_ctx_t; },\n    nullptr,\n    nullptr,\n    nullptr,\n    nullptr,\n    \"QScripts native Triton plugin driver\",\n    \"Ctrl-Shift-R\",\n};\n"
  },
  {
    "path": "test_addons/plugin_triton/idasdk.h",
    "content": "#pragma warning(push)\r\n#pragma warning(disable: 4244 4267)\r\n\r\n#define USE_DANGEROUS_FUNCTIONS\r\n#include <ida.hpp>\r\n#include <idp.hpp>\r\n#include <loader.hpp>\r\n#include <kernwin.hpp>\r\n#include <name.hpp>\r\n#include <funcs.hpp>\r\n// Include more headers here...\r\n#pragma warning(pop)\r\n"
  },
  {
    "path": "test_addons/plugin_triton/main.cpp",
    "content": "#include \"idasdk.h\"\r\n\r\n#include <sstream>\r\n#include <triton/context.hpp>\r\n#include <triton/basicBlock.hpp>\r\n#include <triton/x86Specifications.hpp>\r\n\r\nusing namespace triton;\r\nusing namespace triton::arch;\r\n\r\nbool main(size_t)\r\n{\r\n    constexpr const ea_t FUNC_EA = 0x400000;\r\n    msg_clear();\r\n\r\n    auto f = get_func(FUNC_EA);\r\n    if (f == nullptr)\r\n    {\r\n        msg(\"Can't find test function @ %a!\\n\", FUNC_EA);\r\n        return false;\r\n    }\r\n\r\n    /* Init the triton context */\r\n    triton::Context ctx;\r\n    ctx.setArchitecture(ARCH_X86_64);\r\n\r\n    func_item_iterator_t ffi;\r\n    BasicBlock bb;\r\n    for (bool ok = ffi.set(f); ok; ok = ffi.next_code())\r\n    {\r\n        auto ea = ffi.current();\r\n        insn_t insn;\r\n        if (decode_insn(&insn, ea) == 0)\r\n        {\r\n            msg(\"Failed to decode at %a\\n\", ea);\r\n            return false;\r\n        }\r\n\r\n        unsigned char buf[32];\r\n        get_bytes(buf, qmin(sizeof(buf), insn.size), ea);\r\n\r\n        // Setup opcode\r\n        Instruction inst;\r\n        inst.setOpcode(buf, insn.size);\r\n        inst.setAddress(triton::uint64(ea));\r\n        bb.add(inst);\r\n    }\r\n\r\n    ctx.disassembly(bb, FUNC_EA);\r\n\r\n    std::ostringstream ostr;\r\n    ostr << bb;\r\n    msg(\"----------------\\n\"\r\n        \"Original:\\n\"\r\n        \"----------------\\n\"\r\n        \"%s\\n\", ostr.str().c_str());\r\n\r\n    ostr.str(\"\");\r\n    auto simplified_bb = ctx.simplify(bb);\r\n    ostr << simplified_bb;\r\n    msg(\"----------------\\n\"\r\n        \"Simplified:\\n\"\r\n        \"----------------\\n\"\r\n        \"%s\\n\", ostr.str().c_str());\r\n\r\n\treturn true;\r\n}\r\n"
  },
  {
    "path": "test_addons/plugin_triton/qscripts_native_triton.py",
    "content": "import time\r\nimport idaapi\r\n\r\n# Give the linker time to finish flushing the binary\r\ntime.sleep(1)\r\n\r\n# Optionally clear the screen:\r\n#idaapi.msg_clear()\r\n\r\n# Load your plugin and pass any arg value you want\r\nidaapi.load_and_run_plugin('qscripts_native_triton', 0)\r\n\r\n# Optionally, do post work, etc."
  },
  {
    "path": "test_addons/plugin_triton/qscripts_native_triton.py.deps.qscripts",
    "content": "/triggerfile /keep $env:IDASDK$\\bin\\plugins\\qscripts_native_triton$ext$"
  },
  {
    "path": "test_addons/qscripts_native.py",
    "content": "import time\r\nimport idaapi\r\n\r\n# Give the linker time to finish flushing the binary\r\ntime.sleep(1)\r\n\r\n# Optionally clear the screen:\r\n#idaapi.msg_clear()\r\n\r\n# Load your plugin and pass any arg value you want\r\nidaapi.load_and_run_plugin('qscripts_native', 0)\r\n\r\n# Optionally, do post work, etc."
  },
  {
    "path": "test_addons/qscripts_native.py.deps.qscripts",
    "content": "/triggerfile /keep $env:IDASDK$\\bin\\plugins\\qscripts_native$ext$"
  },
  {
    "path": "test_scripts/dependency-test/README.md",
    "content": "This is a dependency test folder.\r\n\r\nThe script `t1.py` has its dependency index file which describes the reload directive and one dependency on `t2.py`:\r\n```\r\n/reload import importlib;import $basename$;importlib.reload($basename$)\r\nt2.py\r\n```\r\n\r\nOTOH, `t2.py` has a relative dependency on `t3.py` (in the same folder) and on `subdir/t4.py` in a sub-folder:\r\n```\r\nt3.py\r\nsubdir/t4.py\r\n```\r\n\r\nThe script `t4.py` has a single relative dependency in its index file:\r\n```\r\nt5.py\r\n```\r\n\r\nIf you activate `t1.py` from QScripts in IDA, then changing any of the files or their dependency files, then the changed dependencies will be reloaded (using the reload syntax) and the active script is executed again."
  },
  {
    "path": "test_scripts/dependency-test/subdir/t4.py",
    "content": "print(\"f4, This is %s\" % __file__)\r\n\r\ndef f4():\r\n    print(\"t4.f4() called!\")\r\n"
  },
  {
    "path": "test_scripts/dependency-test/subdir/t4.py.deps.qscripts",
    "content": "t5.py"
  },
  {
    "path": "test_scripts/dependency-test/subdir/t5.py",
    "content": "print(\"f5, This is %s\" % __file__)\r\n\r\ndef f5():\r\n    print(\"t5.f5() called!\")\r\n"
  },
  {
    "path": "test_scripts/dependency-test/t1.py",
    "content": "import sys\r\n\r\nsubdir = os.path.join(os.path.dirname(__file__), 'subdir')\r\nif subdir not in sys.path:\r\n    print(\"-->adding to path: %s\" % subdir)\r\n    sys.path.append(subdir)\r\n\r\nimport datetime\r\nimport t2, t3, t4, t5\r\n\r\nprint(\"--- %s; this is %s..\" % (datetime.datetime.now(), __file__))\r\n\r\nt2.f2()\r\nt3.f3()\r\nt4.f4()\r\nt5.f5()"
  },
  {
    "path": "test_scripts/dependency-test/t1.py.deps.qscripts",
    "content": "/reload import importlib;importlib.reload($basename$)\r\nt2.py"
  },
  {
    "path": "test_scripts/dependency-test/t2.py",
    "content": "print(\"t2 changed, This is %s\" % __file__)\r\ndef f2():\r\n    print(\"t2.f2() called!\")"
  },
  {
    "path": "test_scripts/dependency-test/t2.py.deps.qscripts",
    "content": "t3.py\r\nsubdir/t4.py"
  },
  {
    "path": "test_scripts/dependency-test/t3.py",
    "content": "print(\"t3., This is %s\" % __file__)\r\ndef f3():\r\n    print(\"t3.f3() called!\")"
  },
  {
    "path": "test_scripts/hello.idc",
    "content": "#include <idc.idc>\n\nstatic main()\n{\n    Message(\"Welcome to QScripts!\\n\");\n}\n\nstatic __fast_unload_script()\n{\n    Message(\"IDC: aha! unloaded!\\n\");\n}\n"
  },
  {
    "path": "test_scripts/hello.py",
    "content": "try:\n    var1 += 1\nexcept:\n    var1 = 1\n\ndef __quick_unload_script():\n    print(\"Unloaded: %s\" % str(var1))\n\nprint(f\"Just edit and save! {var1}\")\n\n"
  },
  {
    "path": "test_scripts/notebooks/0000 Imports and Init.py",
    "content": "import os\r\nimport sys\r\nimport idaapi\r\nimport idautils\r\nimport idc\r\n\r\nVERSION = globals().get('VERSION', 0) + 1\r\n\r\nprint(f\"Initialized; version: {VERSION}\")"
  },
  {
    "path": "test_scripts/notebooks/0000 Imports and Init.py.deps.qscripts",
    "content": "# A simple notebook\r\n/notebook Test notebook\r\n# On activation, just execute the main script\r\n/notebook.activate exec_main\r\n# Uses default cells matcher (pattern: \\d{4}.*\\.py$)\r\n"
  },
  {
    "path": "test_scripts/notebooks/0010 List functions.py",
    "content": "# Enumerating functions\r\n\r\nif idaapi.get_func_qty() != 0:\r\n    for func_ea in idautils.Functions():\r\n        print(f\"Function at 0x{func_ea:#08X}\")\r\nelse:\r\n    print(\"No functions found!\")"
  },
  {
    "path": "test_scripts/notebooks/0020 List segments.py",
    "content": "# Enumerating segments\r\n\r\nif idaapi.get_segm_qty() > 0:\r\n    for seg_ea in idautils.Segments():\r\n        seg = idaapi.getseg(seg_ea)\r\n        print(f\"Segment at {seg.start_ea:#08x} - {seg.end_ea:#08x}\")\r\n\r\nelse:\r\n    print(\"No segments found!\")\r\n\r\n"
  },
  {
    "path": "test_scripts/notebooks/0030 Show info.py",
    "content": "# Enumerating segments\r\n\r\nprint(f\"min_ea={idaapi.inf_get_min_ea():#x}, max_ea={idaapi.inf_get_max_ea():#x}\")\r\n"
  },
  {
    "path": "test_scripts/notebooks/0040 Set name.py",
    "content": "# Rename setuff\r\nidaapi.set_name(idc.here(), \"my_function\")\r\n"
  },
  {
    "path": "test_scripts/notebooks/0050 Set name.py",
    "content": "# Rename setuff\r\nz = globals().get('z', 0) + 1\r\nidaapi.set_name(idc.here(), f\"my_function_{z}\")\r\n"
  },
  {
    "path": "test_scripts/notebooks/A0000 Init.py",
    "content": "import os\r\nimport sys\r\nimport idaapi\r\nimport idautils\r\nimport idc\r\n\r\nVERSION_A = globals().get('VERSION_A', 0) + 1\r\n\r\nprint(f\"A)Initialized; version: {VERSION_A}\")\r\n"
  },
  {
    "path": "test_scripts/notebooks/A0000 Init.py.deps.qscripts",
    "content": "/notebook Test notebook #A\r\n# This notebook looks for \"Annnn\" prefixed cells\r\n/notebook.cells_re A\\d{4}.*\\.py$\r\n# On notebook execution, execute all cells\r\n/notebook.activate exec_all\r\n"
  },
  {
    "path": "test_scripts/notebooks/A0010 Cell 1.py",
    "content": "print('This is Cell A.1')\r\n"
  },
  {
    "path": "test_scripts/notebooks/A0020 Cell 2.py",
    "content": "print('This is Cell A.2')"
  },
  {
    "path": "test_scripts/notebooks/B0000 Init.py",
    "content": "import os\r\nimport sys\r\nimport idaapi\r\nimport idautils\r\nimport idc\r\n\r\nVERSION_B = globals().get('VERSION_B', 0) + 1\r\n\r\nprint(f\"B)Initialized; version: {VERSION_B}\")"
  },
  {
    "path": "test_scripts/notebooks/B0000 Init.py.deps.qscripts",
    "content": "/notebook Test notebook #B\r\n/notebook.cells_re B\\d{4}.*\\.py$\r\n# On notebook activation, just execute this main script\r\n/notebook.activate exec_main"
  },
  {
    "path": "test_scripts/notebooks/B0010 Cell 1.py",
    "content": "print('This is Cell B.1')"
  },
  {
    "path": "test_scripts/notebooks/B0020 Cell 2.py",
    "content": "print('This is Cell B.2')"
  },
  {
    "path": "test_scripts/notebooks/README.md",
    "content": "# Notebook dependency example\r\n\r\n## Quick start\r\n\r\nTo define a notebook, just add the `/notebook [title]` directive as such:\r\n\r\n```\r\n/notebook Test notebook #B\r\n```\r\n\r\nThen, to select the files to be monitored, use the `/notebook.cells_re` directive, specifying a regular expression pattern to match the desired files:\r\n\r\n```\r\n/notebook.cells_re B\\d{4}.*\\.py$\r\n```\r\n\r\nIn this example, the notebook will monitor all files that match the pattern `B\\d{4}.*\\.py$`, for example `B0001_test.py`, `B0002_test.py`, etc.\r\n\r\nNow, when a notebook is activated, you have options to:\r\n\r\n- Execute the main script (`exec_main`)\r\n- All scripts (`exec_all`) \r\n- or no scripts (`exec_none`)\r\n\r\nUsing the `/notebook.activate` directive:\r\n\r\n```\r\n/notebook.activate exec_main\r\n```\r\n\r\n## Provided notebooks examples\r\n\r\n- `0000 Imports and Init.py` - This notebook has its own dependency file that looks for the \"nnnn *.py\" Python files\r\n- `A0000 Init.py` - This notebook has its own dependency file that looks for the \"Annn *.py\" Python files\r\n- `B0000 Init.py` - This notebook has its own dependency file that looks for the \"Bnnnn *.py\" Python files\r\n\r\nAs you can see, it is possible to have various notebooks in the same folder, each with its own dependency file, as long as their `cells_re` configuration does not overlap.\r\n"
  },
  {
    "path": "test_scripts/pkg-dependency/README.md",
    "content": "# Package Dependency Example\r\n\r\nIn this example, we're dealing with a dependency file for a package that's currently being developed. This setup is designed to ensure that modules from the package, `idapyx` in this instance, are automatically reloaded upon any changes.\r\n\r\nFor this purpose, we can either explicitly specify the package's full path or refer to it through an environment variable, as demonstrated below:\r\n\r\n```plaintext\r\n# Define package base folder\r\n/pkgbase $env:idapyx$\r\n# Automatically reload the package's modules when they change\r\n/reload import importlib;from $pkgparentmodname$ import $basename$ as __qscripts_autoreload__; importlib.reload(__qscripts_autoreload__)\r\n# Specify the paths to the package modules that need to be reloaded if they change\r\n$pkgbase$/idapyx/bin/pe/rtfuncs.py\r\n$pkgbase$/idapyx/bin/pe/types.py\r\n```\r\n\r\n- The `/pkgbase` directive specifies the base path of the package, aiding in the module reload process.\r\n- The reload directive employs the `pkgmodparentname` variable to derive the Python parent module name based on the package directory.\r\n- Similarly, the reload directive utilizes the `basename` variable to determine the Python module name, again using the package directory.\r\n- Dependencies are specified relative to their package base folder, facilitated by the `pkgbase` variable.\r\n"
  },
  {
    "path": "test_scripts/pkg-dependency/example.py.deps.qscripts",
    "content": "# Package dependencies (here, we use the environment variable 'IDAPYX' that contains the package path\r\n/pkgbase $env:idapyx$\r\n/reload import importlib;from $pkgparentmodname$ import $basename$ as __qscripts_autoreload__;importlib.reload(__qscripts_autoreload__)\r\n$pkgbase$/idapyx/bin/pe/rtfuncs.py\r\n$pkgbase$/idapyx/bin/pe/types.py\r\n\r\n"
  },
  {
    "path": "test_scripts/trigger-file/dep.py",
    "content": "try:\r\n    trigger_ver += 1\r\nexcept:\r\n    trigger_ver = 1\r\n\r\nprint(f\"dep file version {trigger_ver}: Even if you save this file, this script won't re-execute. Instead, create the trigger file.\")"
  },
  {
    "path": "test_scripts/trigger-file/trigger.py",
    "content": "import idaapi\r\nimport dep\r\n\r\ntry:\r\n    trigger_ver += 1\r\nexcept:\r\n    trigger_ver = 1\r\n\r\nprint(f\"version {trigger_ver}: Even if you save this file, this script won't re-execute. Instead, create the trigger file.\")\r\n\r\nprint(\"----------------------\")"
  },
  {
    "path": "test_scripts/trigger-file/trigger.py.deps.qscripts",
    "content": "/reload import importlib;importlib.reload($basename$);\r\n/triggerfile createme.tmp\r\ndep.py"
  },
  {
    "path": "utils_impl.cpp",
    "content": "//-------------------------------------------------------------------------\nstruct collect_extlangs: extlang_visitor_t\n{\n    extlangs_t *langs;\n\n    virtual ssize_t idaapi visit_extlang(extlang_t *extlang) override\n    {\n        langs->push_back(extlang);\n        return 0;\n    }\n\n    collect_extlangs(extlangs_t *langs, bool select)\n    {\n        langs->qclear();\n        this->langs = langs;\n        for_all_extlangs(*this, select);\n    }\n};\n\n//-------------------------------------------------------------------------\n// Utility function to return a file's last modification timestamp\nbool get_file_modification_time(\n    const char *filename,\n    qtime64_t *mtime = nullptr)\n{\n    qstatbuf stat_buf;\n    if (qstat(filename, &stat_buf) != 0)\n        return false;\n\n    if (mtime != nullptr)\n        *mtime = stat_buf.qst_mtime;\n    return true;\n}\n\ntemplate <class STRING>\nbool get_file_modification_time(\n    const STRING &filename,\n    qtime64_t *mtime = nullptr)\n{\n    return get_file_modification_time(filename.c_str(), mtime);\n}\n\n//-------------------------------------------------------------------------\nvoid normalize_path_sep(qstring &path)\n{\n#ifdef __FAT__\n    path.replace(\"/\", SDIRCHAR);\n#else\n    path.replace(\"\\\\\", SDIRCHAR);\n#endif\n}\n\n//-------------------------------------------------------------------------\nvoid make_abs_path(qstring& path, const char* base_dir = nullptr, bool normalize = false)\n{\n    if (qisabspath(path.c_str()))\n        return;\n\n    auto old_cwd = std::filesystem::current_path();\n    if (base_dir == nullptr)\n    {\n        path = old_cwd.string().c_str();\n    }\n    else\n    {\n        std::filesystem::current_path(base_dir);\n        auto abs = std::filesystem::absolute(path.c_str());\n        path = abs.string().c_str();\n        std::filesystem::current_path(old_cwd);\n    }\n    if (normalize)\n        normalize_path_sep(path);\n}\n\n//-------------------------------------------------------------------------\nbool get_basename_and_ext(\n    const char *path, \n    char **basename,\n    char **ext,\n    qstring &wrk_buf)\n{\n    wrk_buf = path;\n    qsplitfile(wrk_buf.begin(), basename, ext);\n    if ((*basename = qstrrchr(wrk_buf.begin(), DIRCHAR)) != nullptr)\n        return ++(*basename), true;\n    else\n        return false;\n}\n\n//-------------------------------------------------------------------------\ninline void get_current_directory(qstring &dir)\n{\n    dir = std::filesystem::current_path().string().c_str();\n}\n\n//-------------------------------------------------------------------------\n// Utility function similar to Python's re.sub().\n// Based on https://stackoverflow.com/a/37516316\nnamespace std\n{\n    template<class BidirIt, class Traits, class CharT, class UnaryFunction>\n    std::basic_string<CharT> regex_replace(BidirIt first, BidirIt last,\n        const std::basic_regex<CharT, Traits> &re, UnaryFunction f)\n    {\n        std::basic_string<CharT> s;\n\n        typename std::match_results<BidirIt>::difference_type positionOfLastMatch = 0;\n        auto endOfLastMatch = first;\n\n        auto callback = [&](const std::match_results<BidirIt> &match)\n        {\n            auto positionOfThisMatch = match.position(0);\n            auto diff = positionOfThisMatch - positionOfLastMatch;\n\n            auto startOfThisMatch = endOfLastMatch;\n            std::advance(startOfThisMatch, diff);\n\n            s.append(endOfLastMatch, startOfThisMatch);\n            s.append(f(match));\n\n            auto lengthOfMatch = match.length(0);\n\n            positionOfLastMatch = positionOfThisMatch + lengthOfMatch;\n\n            endOfLastMatch = startOfThisMatch;\n            std::advance(endOfLastMatch, lengthOfMatch);\n        };\n\n        std::regex_iterator<BidirIt> begin(first, last, re), end;\n        std::for_each(begin, end, callback);\n\n        s.append(endOfLastMatch, last);\n\n        return s;\n    }\n\n    template<class Traits, class CharT, class UnaryFunction>\n    std::string regex_replace(const std::string &s,\n        const std::basic_regex<CharT, Traits> &re, UnaryFunction f)\n    {\n        return regex_replace(s.cbegin(), s.cend(), re, f);\n    }\n}\n\n//-------------------------------------------------------------------------\nvoid enumerate_files(\n    const std::filesystem::path& path, \n    const std::regex& filter, \n    std::function<bool(const std::string&)> callback)\n{\n    for (const auto& entry : std::filesystem::directory_iterator(path)) \n    {\n        if (entry.is_regular_file()) \n        {\n            if (!std::regex_match(entry.path().filename().string(), filter)) \n                continue;\n            if (!callback(entry.path().string()))\n                break;\n        }\n    }\n}\n\n"
  }
]