Full Code of 0xeb/ida-qscripts for AI

main e754bfa2de81 cached
73 files
116.2 KB
29.4k tokens
121 symbols
1 requests
Download .txt
Repository: 0xeb/ida-qscripts
Branch: main
Commit: e754bfa2de81
Files: 73
Total size: 116.2 KB

Directory structure:
gitextract_tqzl_grq/

├── .gitattributes
├── .github/
│   └── workflows/
│       └── build.yml
├── .gitignore
├── CLAUDE.md
├── CMakeLists.txt
├── LICENSE
├── README.md
├── TODO.md
├── ida-plugin.json
├── ida.h
├── prep-cmake.bat
├── qscripts.cpp
├── script.hpp
├── snippet_manager/
│   ├── README.md
│   ├── pseudocode.cpp
│   └── snippetmanager.py
├── test_addons/
│   ├── README.md
│   ├── loader_template/
│   │   ├── .gitignore
│   │   ├── CMakeLists.txt
│   │   ├── driver.cpp
│   │   ├── idasdk.h
│   │   └── main.cpp
│   ├── plugin_template/
│   │   ├── .claude/
│   │   │   └── agents/
│   │   │       └── ida-cmake.md
│   │   ├── .gitignore
│   │   ├── CLAUDE.md
│   │   ├── CMakeLists.txt
│   │   ├── driver.cpp
│   │   ├── idasdk.h
│   │   └── main.cpp
│   ├── plugin_triton/
│   │   ├── .gitignore
│   │   ├── CMakeLists.txt
│   │   ├── README.md
│   │   ├── driver.cpp
│   │   ├── idasdk.h
│   │   ├── junk64.bin.i64
│   │   ├── main.cpp
│   │   ├── qscripts_native_triton.py
│   │   └── qscripts_native_triton.py.deps.qscripts
│   ├── qscripts_native.py
│   └── qscripts_native.py.deps.qscripts
├── test_scripts/
│   ├── dependency-test/
│   │   ├── README.md
│   │   ├── subdir/
│   │   │   ├── t4.py
│   │   │   ├── t4.py.deps.qscripts
│   │   │   └── t5.py
│   │   ├── t1.py
│   │   ├── t1.py.deps.qscripts
│   │   ├── t2.py
│   │   ├── t2.py.deps.qscripts
│   │   └── t3.py
│   ├── hello.idc
│   ├── hello.py
│   ├── notebooks/
│   │   ├── 0000 Imports and Init.py
│   │   ├── 0000 Imports and Init.py.deps.qscripts
│   │   ├── 0010 List functions.py
│   │   ├── 0020 List segments.py
│   │   ├── 0030 Show info.py
│   │   ├── 0040 Set name.py
│   │   ├── 0050 Set name.py
│   │   ├── A0000 Init.py
│   │   ├── A0000 Init.py.deps.qscripts
│   │   ├── A0010 Cell 1.py
│   │   ├── A0020 Cell 2.py
│   │   ├── B0000 Init.py
│   │   ├── B0000 Init.py.deps.qscripts
│   │   ├── B0010 Cell 1.py
│   │   ├── B0020 Cell 2.py
│   │   └── README.md
│   ├── pkg-dependency/
│   │   ├── README.md
│   │   └── example.py.deps.qscripts
│   └── trigger-file/
│       ├── dep.py
│       ├── trigger.py
│       └── trigger.py.deps.qscripts
└── utils_impl.cpp

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitattributes
================================================
* -crlf

================================================
FILE: .github/workflows/build.yml
================================================
name: build

on:
  push:
    branches: ["**"]
    tags: ["v*", "V*"]
  pull_request:
    branches: ["**"]
  workflow_dispatch:

permissions:
  contents: write

concurrency:
  group: ci-${{ github.ref }}
  cancel-in-progress: true

jobs:
  build-linux:
    name: Build for Linux
    runs-on: ubuntu-latest
    env:
      IDASDK: ${{ github.workspace }}/ida-sdk
    steps:
      - name: Checkout project
        uses: actions/checkout@v4

      - name: Prepare IDA SDK
        run: git clone --depth 1 --recurse-submodules https://github.com/HexRaysSA/ida-sdk ida-sdk

      - name: Configure (Linux)
        run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Release

      - name: Build (Linux)
        run: cmake --build build -- -j2

      - name: Upload Linux artifact (qscripts.so)
        uses: actions/upload-artifact@v4
        with:
          name: qscripts-linux
          path: ${{ env.IDASDK }}/src/bin/plugins/qscripts.so

  build-macos:
    name: Build for macOS
    runs-on: macos-latest
    env:
      IDASDK: ${{ github.workspace }}/ida-sdk
    steps:
      - name: Checkout project
        uses: actions/checkout@v4

      - name: Prepare IDA SDK
        run: git clone --depth 1 --recurse-submodules https://github.com/HexRaysSA/ida-sdk ida-sdk

      - name: Configure (macOS)
        run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Release

      - name: Build (macOS)
        run: cmake --build build -- -j2

      - name: Upload macOS artifact (qscripts.dylib)
        uses: actions/upload-artifact@v4
        with:
          name: qscripts-macos
          path: ${{ env.IDASDK }}/src/bin/plugins/qscripts.dylib

  build-windows:
    name: Build for Windows
    runs-on: windows-latest
    env:
      IDASDK: ${{ github.workspace }}/ida-sdk
    steps:
      - name: Checkout project
        uses: actions/checkout@v4

      - name: Prepare IDA SDK
        run: git clone --depth 1 --recurse-submodules https://github.com/HexRaysSA/ida-sdk ida-sdk

      - name: Configure (Windows)
        run: cmake -S . -B build -A x64

      - name: Build (Windows)
        run: cmake --build build --config Release -- /m

      - name: Upload Windows artifact (qscripts.dll)
        uses: actions/upload-artifact@v4
        with:
          name: qscripts-windows
          path: ${{ env.IDASDK }}/src/bin/plugins/qscripts.dll

  publish-release:
    name: Publish Release
    runs-on: ubuntu-latest
    needs: [ build-linux, build-macos, build-windows ]
    if: startsWith(github.ref, 'refs/tags/')
    steps:
      - name: Checkout project
        uses: actions/checkout@v4

      - name: Download all artifacts
        uses: actions/download-artifact@v4
        with:
          pattern: qscripts-*
          path: release
          merge-multiple: true

      - name: Copy metadata to release folder
        run: |
          cp ida-plugin.json release/ || echo "No ida-plugin.json found"

      - name: List release files
        run: |
          ls -la release || true

      - name: Create zip package
        run: |
          cd release
          if [ -f ida-plugin.json ]; then
            zip -9 qscripts-${{ github.ref_name }}.zip qscripts.dll qscripts.so qscripts.dylib ida-plugin.json
          else
            zip -9 qscripts-${{ github.ref_name }}.zip qscripts.dll qscripts.so qscripts.dylib
          fi
          ls -lh *.zip

      - name: Create GitHub Release and upload assets
        uses: softprops/action-gh-release@v2
        with:
          tag_name: ${{ github.ref_name }}
          name: QScripts ${{ github.ref_name }}
          draft: false
          prerelease: false
          generate_release_notes: true
          files: |
            release/qscripts-${{ github.ref_name }}.zip
            release/qscripts.dll
            release/qscripts.so
            release/qscripts.dylib
            release/ida-plugin.json


================================================
FILE: .gitignore
================================================
.vscode/
obj/
__pycache__/
build*/
.vs/
Debug/
Release/
x64/
BINARIES/
.claude/settings.local.json
.claude/agents/ida-cmake.md


================================================
FILE: CLAUDE.md
================================================
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

# QScripts - IDA Pro Productivity Plugin

QScripts is an IDA Pro plugin that enables automatic script execution upon file changes, supporting hot-reload development workflows with Python, IDC, and compiled plugins.

## Build Commands

### Prerequisites
- **IDASDK**: Environment variable must be set to IDA SDK path
- **ida-cmake**: Must be installed at `$IDASDK/ida-cmake/`
- **idacpp**: Optional - will be automatically fetched if not found at `$IDASDK/include/idacpp/`

### Build with ida-cmake agent
Always use the ida-cmake agent for building:
```
Task with subagent_type="ida-cmake"
```

### Manual build (Windows)
```bash
# Configure CMake
prep-cmake.bat

# Build
prep-cmake.bat build

# Clean build
prep-cmake.bat clean
```

## Architecture

### Core Components

**Main Plugin (`qscripts.cpp`)**
- Implements `qscripts_chooser_t`: Non-modal chooser UI for script management
- File monitoring system with configurable intervals (default 500ms)
- Dependency tracking and automatic reloading
- Integration with IDA's recent scripts system (shares same list)

**Script Management (`script.hpp`)**
- `fileinfo_t`: File metadata and modification tracking
- `script_info_t`: Script state management (active/inactive/dependency)
- `active_script_info_t`: Extends script_info with dependency handling
- Dependency resolution with recursive parsing and cycle detection

### Key Features Implementation

**Dependency System**
- Dependencies defined in `.deps.qscripts` files
- Supports `/reload` directive for Python module reloading
- `/triggerfile` directive for custom trigger conditions
- `/notebook` mode for cell-based execution
- Variable expansion: `$basename$`, `$env:VAR$`, `$pkgbase$`, `$ext$`

**File Monitoring**
- Uses qtimer-based polling (not OS-specific watchers yet)
- Monitors active script and all dependencies
- Trigger file support for compiled plugin development

## IDA SDK and API Resources

When answering SDK/API questions, search and read from:
- **SDK Headers**: `$IDASDK/include` - All headers have docstrings
- **SDK Examples**: `$IDASDK/plugins`, `$IDASDK/loaders`, `$IDASDK/module`

## Testing

Test scripts are located in `test_scripts/`:
- `dependency-test/` - Simple dependency examples
- `pkg-dependency/` - Package dependency examples
- `notebooks/` - Notebook mode examples
- `trigger-native/` - Compiled plugin hot-reload examples

## Plugin States

Scripts can be in three states:
1. **Normal**: Shown in regular font
2. **Active** (bold): Currently monitored for changes
3. **Inactive** (italics): Previously active but monitoring disabled

## Important Implementation Notes

- Plugin uses idacpp wrapper library for enhanced C++ API
- Shares script list with IDA's built-in "Recent Scripts" (Alt-F9)
- Maximum 512 scripts in list (IDA_MAX_RECENT_SCRIPTS)
- Special unload function: `__quick_unload_script` called before reload
- Supports undo via IDA's undo system when enabled in options

## CI/CD and Releases

### Automated Builds
- GitHub Actions builds for Linux (.so), macOS (.dylib), and Windows (.dll)
- Runs on every push and pull request
- CMake automatically fetches idacpp if not available in IDASDK

### Creating Releases
To create a new release with pre-built binaries:
```bash
git tag v1.0.0              # Create version tag
git push origin v1.0.0      # Push tag to trigger release
```
This will automatically:
1. Build qscripts for all 3 platforms
2. Create a GitHub Release page
3. Upload all platform binaries (.dll, .so, .dylib)
4. Generate release notes from commits

================================================
FILE: CMakeLists.txt
================================================
cmake_minimum_required(VERSION 3.27)
project(qscripts)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

# Include IDA SDK bootstrap
include($ENV{IDASDK}/src/cmake/bootstrap.cmake)
find_package(idasdk REQUIRED)

# Add idacpp library with fallback
# First, try to use idacpp from IDASDK
set(IDACPP_SDK_PATH "$ENV{IDASDK}/include/idacpp")
set(IDACPP_LOCAL_PATH "${CMAKE_CURRENT_BINARY_DIR}/idacpp")

if(EXISTS "${IDACPP_SDK_PATH}/CMakeLists.txt")
    message(STATUS "Using idacpp from IDASDK: ${IDACPP_SDK_PATH}")
    add_subdirectory(${IDACPP_SDK_PATH} ${CMAKE_CURRENT_BINARY_DIR}/idacpp)
else()
    message(STATUS "idacpp not found in IDASDK, fetching to build directory...")

    # Use FetchContent to download idacpp into build directory
    # NOTE: The header-only idacpp wrapper is in the allthingsida/libidacpp repo
    include(FetchContent)
    FetchContent_Declare(
        idacpp
        GIT_REPOSITORY https://github.com/allthingsida/libidacpp.git
        GIT_TAG main
        SOURCE_DIR ${IDACPP_LOCAL_PATH}
    )
    FetchContent_MakeAvailable(idacpp)
endif()

# Convert paths to native format
set(SAMPLE_IDB "${IDA_CMAKE_DIR}/samples/wizmo32.exe.i64")
file(TO_NATIVE_PATH "${SAMPLE_IDB}" SAMPLE_IDB)

# Disabled sources
list(APPEND DISABLED_SOURCES utils_impl.cpp)
set_source_files_properties(${DISABLED_SOURCES} PROPERTIES LANGUAGE "")

# Add plugin
ida_add_plugin(qscripts
    SOURCES
        qscripts.cpp
        ida.h
        script.hpp
        ${DISABLED_SOURCES}
    DEBUG_ARGS
        "${SAMPLE_IDB}"
)

target_link_libraries(qscripts PRIVATE idacpp::idacpp)


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2019 Elias Bachaalany

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
# What is QScripts?

QScripts 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.

![Quick introduction](docs/_resources/qscripts-vid-1.gif)

Video tutorials on the [AllThingsIDA](https://www.youtube.com/@allthingsida) YouTube channel:

- [Boost your IDA programming efficiency tenfold using the ida-qscripts productivity plugin](https://youtu.be/1UEoLAgEGMc?si=YMieIKHEY0AXgMHU)
- [Scripting concepts and productivity tips for IDAPython & IDC](https://youtu.be/RgHmwHN0NLk?si=OCnLMhcAmHAQPgNI)
- [An exercise in netnodes with the snippet manager plugin](https://youtu.be/yhVdLYzFJW0?si=z3xMqCEFOU89gAkI)

# Usage

Invoke QScripts from the plugins menu, or its default hotkey Alt-Shift-F9.
When 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.
QScripts shares the same scripts list as IDA's `Recent Scripts` window.

To execute a script, just press `ENTER` or double-click it. After running a script once, it will become the active script (shown in **bold**).

An 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.

To 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*.

## Keyboard shortcuts

Inside QScripts window:
* `Alt-Shift-F9`: Open QScripts window
* `ENTER` or double-click: Activate and execute selected script
* `Shift-Enter`: Execute selected script without activating it
* `Ins`: Add a new script to the list
* `Del`: Remove a script from the list
* `Ctrl-E`: Open options dialog
* `Ctrl-D`: Deactivate script monitor

From anywhere in IDA:
* `Alt-Shift-X`: Re-execute the last active script or notebook cell

## Configuration options

Press `Ctrl+E` or right-click and select `Options` to configure QScripts:

* 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.
* Show file name when execution: display the name of the file that is automatically executed
* 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)
* 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.
* Allow QScripts execution to be undo-able: The executed script's side effects can be reverted with IDA's Undo.

# Managing Dependencies in QScripts

QScripts offers a feature that allows automatic re-execution of the active script when any of its dependent scripts, undergo modifications.

## Setting Up Automatic Dependencies

To 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.

Alternatively, you can place a `.deps` file (without the `.qscripts` suffix) within a `.qscripts` subfolder, located alongside your active script.

**Example locations for `script.py`:**
* `script.py.deps.qscripts` (same directory)
* `.qscripts/script.py.deps.qscripts` (local folder)
* `.qscripts/script.py.deps` (local folder, alternate name)

## Dependency Index File Syntax

The dependency index file supports comments and directives:

```txt
# Python-style comments
; Semicolon comments
// C-style comments

/reload import importlib; import $basename$; importlib.reload($basename$);
dependency1.py
dependency2.py
```

## Integrating Python Scripts

For 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.

### Example `.deps.qscripts` file for `t1.py`:

```txt
/reload import importlib; import $basename$; importlib.reload($basename$);
t2.py
# This is a comment
t3.py
```

The `t1.py.deps.qscripts` configuration enables the following behavior:

1. **Script Auto-Execution**: Changes to `t1.py` trigger its automatic re-execution within the IDA environment.
2. **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.
3. **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.

**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.

See also:

* [Simple dependency example](test_scripts/dependency-test/README.md)
* [Package dependency example](test_scripts/pkg-dependency/README.md)

## Directive Reference

Directives must appear at the beginning of a line and start with `/`. Arguments follow the directive name.

### `/reload <code>`

Executes 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.

```txt
/reload import importlib; import $basename$; importlib.reload($basename$);
```

### `/pkgbase <path>`

Specifies a package base directory for Python package dependencies. This path is used in conjunction with `$pkgmodname$` and `$pkgparentmodname$` variables.

```txt
/pkgbase C:\projects\mypackage
```

### `/triggerfile [/keep] <filepath>`

Configures 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.

Options:
* `/keep`: Preserve the trigger file after execution (default behavior is to delete it)

```txt
/triggerfile /keep C:\temp\build_done.flag
```

### `/notebook [<title>]`

Enables 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.

```txt
/notebook My Analysis Notebook
```

### `/notebook.cells_re <regex>`

Specifies a regular expression pattern to identify notebook cell files. The default pattern is `\d{4}.*\.py$`.

```txt
/notebook.cells_re ^\d{4}_.+\.py$
```

### `/notebook.activate <action>`

Controls the behavior when the notebook is activated:
* `exec_none`: Display the notebook title but do not execute any scripts
* `exec_main`: Execute the main script file
* `exec_all`: Execute all notebook cells in order

```txt
/notebook.activate exec_all
```

## Special Variables

Variables are expanded when encountered in file paths or reload directives. Use the syntax `$variable$`.

* `$basename$`: The base name (without extension) of the current dependency file
* `$env:VariableName$`: The value of the environment variable `VariableName`
* `$pkgbase$`: The package base directory (set via `/pkgbase`)
* `$pkgmodname$`: The module name derived from the dependency file path relative to `$pkgbase$`, with path separators replaced by dots (e.g., `pkg.submodule.file`)
* `$pkgparentmodname$`: The parent module name (e.g., `pkg.submodule`)
* `$ext$`: The platform-specific plugin extension (e.g., `64.dll`, `.so`, `64.dylib`)

### Examples

```txt
# Reload a Python module
/reload import importlib; import $basename$; importlib.reload($basename$);

# Use environment variable in path
$env:SCRIPTS_DIR$/helper.py

# Package reloading
/pkgbase C:\myproject\src
/reload import importlib; import $pkgmodname$; importlib.reload($pkgmodname$);
src/utils/helper.py
```

# Using QScripts like a Jupyter notebook

QScripts 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.

**Example notebook configuration for `notebook.py.deps.qscripts`:**

```txt
/notebook Data Analysis Notebook
/notebook.cells_re ^\d{4}_.+\.py$
/notebook.activate exec_none
/reload import importlib; import $basename$; importlib.reload($basename$);
shared_utils.py
```

With this configuration:
* Files like `0010_load_data.py`, `0020_process.py`, `0030_visualize.py` are recognized as cells
* Saving any cell executes only that cell
* The notebook title is displayed when activated
* Changes to `shared_utils.py` cause the last-executed cell to re-run

See also:

* [Notebooks dependency example](test_scripts/notebooks/README.md)

# Using QScripts with trigger files

Sometimes you don't want to trigger QScripts when your scripts are saved, instead you want your own trigger condition.
One way to achieve a custom trigger is by using the `/triggerfile` directive:

```
/triggerfile createme.tmp

; Dependencies...
dep1.py
```

This 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.

You may pass the `/keep` option so QScripts does not delete your trigger file:

```
/triggerfile /keep dont_del_me.info
```

# Using QScripts programmatically

QScripts can be controlled programmatically from scripts or other plugins.

## Plugin Arguments

```python
# Open QScripts window
idaapi.load_and_run_plugin("qscripts", 0)

# Execute the last selected script
idaapi.load_and_run_plugin("qscripts", 1)

# Activate the script monitor
idaapi.load_and_run_plugin("qscripts", 2)

# Deactivate the script monitor
idaapi.load_and_run_plugin("qscripts", 3)
```

## Action IDs

QScripts registers the following actions that can be invoked programmatically:

* `qscripts:deactivatemonitor` - Deactivate the script monitor
* `qscripts:execselscript` - Execute the selected script without activating it
* `qscripts:execscriptwithundo` - Re-execute the last active script or notebook cell
* `qscripts:executenotebook` - Execute all cells in the active notebook

```python
# Re-execute the active script
idaapi.process_ui_action("qscripts:execscriptwithundo")

# Execute all notebook cells
idaapi.process_ui_action("qscripts:executenotebook")
```

# Hot-Reloading Native Plugins

QScripts 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.

![Compiled code](docs/_resources/trigger_native.gif)

## Requirements

The native plugin must be designed to support unloading:
* **Plugins**: Set the `PLUGIN_UNL` flag to allow IDA to unload the plugin after each invocation
* **Loaders**: Loaders are naturally unloadable
* **Processor modules**: Use appropriate cleanup in module termination

## Workflow

The hot-reload workflow uses trigger files to detect when a new binary is available:

1. Create a loader script that invokes your native plugin
2. Configure a dependency file with `/triggerfile` pointing to the compiled binary
3. Build your native plugin
4. QScripts detects the binary change and executes the loader script
5. The loader script invokes the newly compiled plugin

## Example: Hot-Reloading a Plugin

For a plugin with the `PLUGIN_UNL` flag (like the IDA SDK `hello` sample):

**Step 1**: Create a loader script `load_hello.py`:

```python
# Optionally clear the screen
idaapi.msg_clear()

# Load your plugin and pass any arg value you want
idaapi.load_and_run_plugin('hello', 0)

# Optionally, do post work, etc.
# ...
```

**Step 2**: Create the dependency file `load_hello.py.deps.qscripts`:

```
/triggerfile /keep C:\<ida_dir>\plugins\hello$ext$
```

**Step 3**: Activate `load_hello.py` in QScripts (press `ENTER` on it)

**Step 4**: Build or rebuild the plugin in your IDE

The 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.

## Example: Hot-Reloading a Loader

For loaders, the workflow is similar but uses `load_file()` instead:

**Loader script `test_loader.py`:**

```python
idaapi.msg_clear()

# Unload the old loader if needed
# (IDA handles this automatically for loaders)

# Load a test file with your loader
idaapi.load_file("C:\\testfiles\\myformat.bin", 0)
```

**Dependency file `test_loader.py.deps.qscripts`:**

```
/triggerfile /keep C:\<ida_dir>\loaders\myloader$ext$
```

## Using $ext$ Variable

The `$ext$` variable automatically expands to the platform-specific plugin extension:
* Windows 64-bit: `64.dll`
* Windows 32-bit: `.dll`
* Linux 64-bit: `64.so`
* Linux 32-bit: `.so`
* macOS: `64.dylib` or `.dylib`

This allows your dependency files to work across platforms without modification.

## Additional Examples

Please check the native addons examples in [test_addons](test_addons/) for complete working examples of hot-reloading plugins, loaders, and processor modules.

# Building

QScripts uses [idacpp](https://github.com/allthingsida/idacpp) and is built using [ida-cmake](https://github.com/allthingsida/ida-cmake).

If you don't want to build from sources, then there are release pre-built for MS Windows.

# Installation

QScripts 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:

* `<IDA_install_folder>/plugins`
* `%APPDATA%\Hex-Rays/plugins`

Since 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).

# BONUS

## Snippet Manager

QScripts ships with a simple [Snippet Manager](snippet_manager/README.md) plugin to allow you to manage script snippets.


================================================
FILE: TODO.md
================================================
# Ideas

## Allow dot folders

- Check for $(pwd)/.qscripts/<sourcefilename.depsfiles> first before checking the current directory
- This allows less pollution in the current folder

# TODO

- Automatically select the previous active script when the UI launches
- Restore the QScripts window layout when closed and re-opened
- Use OS specific file change monitors

- It is time to re-write this, now that we have all the specs and features

# Monitor code and refactoring

- Isolate the timer code from the UI logic
    - Refactor filemonitor into a utility class
    - Perhaps implement per OS on Win32, and as is for Linux/Mac
    - For Linux, perhaps call an external process like 'inotify-wait' ?




================================================
FILE: ida-plugin.json
================================================
{
  "IDAMetadataDescriptorVersion": 1,
  "plugin": {
    "name": "QScripts",
    "version": "1.2.6",
    "entryPoint": "qscripts",
    "description": "Develop IDA scripts faster with hot-reload and automatic execution on file changes. Supports Python, IDC, and compiled plugins with dependency tracking.",
    "urls": {
      "repository": "https://github.com/allthingsida/qscripts"
    },
    "authors": [
      {
        "name": "Elias Bachaalany",
        "email": "elias.bachaalany@gmail.com"
      }
    ],
    "categories": [
      "api-scripting-and-automation",
      "collaboration-and-productivity"
    ],
    "idaVersions": ">=9.0",
    "platforms": [
      "windows-x86_64",
      "linux-x86_64",
      "macos-aarch64"
    ],
    "license": "MIT"
  }
}


================================================
FILE: ida.h
================================================
#pragma once

#pragma warning(push)
#pragma warning(disable: 4267 4244 4146)
#include <loader.hpp>
#include <idp.hpp>
#include <expr.hpp>
#include <prodir.h>
#include <kernwin.hpp>
#include <diskio.hpp>
#include <registry.hpp>
#include <idacpp/kernwin/kernwin.hpp>

using namespace idacpp::kernwin;
#pragma warning(pop)

// IDA 8.3
#ifndef CH_NOIDB
    #define CH_NOIDB CH_UNUSED
#endif


================================================
FILE: prep-cmake.bat
================================================
@echo off

:: checkout the Batchography book

setlocal

if not defined IDASDK (
    echo IDASDK environment variable not set.
    goto :eof
)

if not exist %IDASDK%\src\cmake\bootstrap.cmake (
    echo IDA SDK cmake not found at %IDASDK%\src\cmake\bootstrap.cmake
    goto :eof
)

if "%1"=="clean" (
    if exist build rmdir /s /q build
    goto :eof
)

if not exist build cmake -B build -S . -A x64

if "%1"=="build" cmake --build build --config Release

echo.
echo All done!
echo.

================================================
FILE: qscripts.cpp
================================================
/*
Quick execute script: a plugin to speedup IDA scripts development.

This plugin replaces the regular "Recent scripts" and "Execute Script" dialogs and allows you to develop
scripts in your favorite editor and execute them directly in IDA.

(c) Elias Bachaalany <elias.bachaalany@gmail.com>
*/
#include <unordered_map>
#include <string>
#include <regex>
#include <filesystem>
#include <unordered_set>
#include "ida.h"

#include "utils_impl.cpp"
#include "script.hpp"

//-------------------------------------------------------------------------
// Some constants
static constexpr int  IDA_MAX_RECENT_SCRIPTS    = 512;
static constexpr char IDAREG_RECENT_SCRIPTS[]   = "RecentScripts";

//-------------------------------------------------------------------------
// Non-modal scripts chooser
struct qscripts_chooser_t: public plugmod_t, public chooser_t
{
    using chooser_t::operator delete;
    using chooser_t::operator new;

private:
    action_manager_t am;

    bool m_b_filemon_timer_active = false;
    qtimer_t m_filemon_timer = nullptr;
    const std::regex RE_EXPANDER = std::regex(R"(\$(.+?)\$)");

    int opt_change_interval  = 500;
    int opt_clear_log        = 0;
    int opt_show_filename    = 0;
    int opt_exec_unload_func = 0;
    int opt_with_undo        = 0;

    active_script_info_t selected_script;
    script_info_t* action_active_script = nullptr;

    struct expand_ctx_t
    {
        // input
        qstring script_file;
        bool    main_file;

        // working
        qstring base_dir;
        qstring pkg_base;
        qstring reload_cmd;
    };

    inline int normalize_filemon_interval(const int change_interval) const
    {
        return qmax(300, change_interval);
    }

    const char *get_selected_script_file()
    {
        return selected_script.file_path.c_str();
    }

    bool make_meta_filename(
        const char* filename,
        const char* extension,
        qstring& out,
        bool local_only = false)
    {
        // Check the .qscripts folder
        char dir[2048];
        if (qdirname(dir, sizeof(dir), filename))
        {
            out.sprnt("%s" SDIRCHAR QSCRIPTS_LOCAL SDIRCHAR "%s.%s", dir, qbasename(filename), extension);
            if (qfileexist(out.c_str()))
                return true;
        }

        if (local_only)
            return false;

        // Check the actual script folder
        out.sprnt("%s.%s", filename, extension);
        return qfileexist(out.c_str());
    }

    bool find_deps_file(
        const char* filename,
        qstring& out)
    {
        return      make_meta_filename(filename, "deps", out, true)
                ||  make_meta_filename(filename, "deps.qscripts", out);

    }
    
    bool parse_deps_for_script(expand_ctx_t &ctx)
    {
        // Parse the dependency index file
        qstring dep_file;
        if (!find_deps_file(ctx.script_file.c_str(), dep_file))
            return false;

        FILE *fp = qfopen(dep_file.c_str(), "r");
        if (fp == nullptr)
            return false;

        // Get the dependency file directory
        ctx.base_dir.resize(ctx.script_file.size());
        qdirname(ctx.base_dir.begin(), ctx.base_dir.size(), ctx.script_file.c_str());
        ctx.base_dir.resize(strlen(ctx.base_dir.c_str()));

        // Add the dependency file to the active script
        selected_script.add_dep_index(dep_file.c_str());

        static auto get_value = [](const char* str, const char* key, int key_len) -> const char *
        {
            if (strncmp(str, key, key_len) != 0)
                return nullptr;
            // Empty value?
            if (str[key_len] == '\0')
                return "";
            else
                return str + key_len + 1;
        };

        // Parse each line
        for (qstring line = dep_file; qgetline(&line, fp) != -1;)
        {
            line.trim2();

            // Skip comment lines (';', '//' and '#')
            if (line.empty() || strncmp(line.c_str(), "//", 2) == 0 || line[0] == '#' || line[0] == ';')
                continue;

            // Parse special directives (some apply only for the main selected script)
            if (auto val = get_value(line.c_str(), "/pkgbase", 8))
            {
                if (ctx.main_file)
                {
                    ctx.pkg_base = val;
                    expand_file_name(ctx.pkg_base, ctx);
                    make_abs_path(ctx.pkg_base, ctx.base_dir.c_str(), true);
                }
                continue;
            }
            else if (auto val = get_value(line.c_str(), "/notebook.cells_re", 18))
            {
                if (ctx.main_file)
                {
                    selected_script.notebook.cells_re = std::regex(val);
                    continue;
                }
            }
            else if (auto val = get_value(line.c_str(), "/notebook.activate", 18))
            {
                if (ctx.main_file)
                {
                    int act;
                    if (qstrcmp(val, "exec_main") == 0)
                        act = notebook_ctx_t::act_exec_main;
                    else if (qstrcmp(val, "exec_all") == 0)
                        act = notebook_ctx_t::act_exec_all;
                    else
                        act = notebook_ctx_t::act_exec_none;

                    selected_script.notebook.activation_action = act;
                    continue;
                }
            }
            else if (auto val = get_value(line.c_str(), "/notebook", 9))
            {
                if (ctx.main_file)
                {
                    selected_script.b_is_notebook = true;
                    selected_script.notebook.title = val;
                    continue;
                }
            }
            else if (auto val = get_value(line.c_str(), "/reload", 7))
            {
                if (ctx.main_file)
                    ctx.reload_cmd = val;
                continue;
            }
            else if (auto trigger_file = get_value(line.c_str(), "/triggerfile", 12))
            {
                if (auto keep = get_value(trigger_file, "/keep", 5))
                {
                    trigger_file = keep;
                    selected_script.b_keep_trigger_file = true;
                }

                if (ctx.main_file)
                {
                    selected_script.trigger_file.refresh(trigger_file);
                    expand_file_name(selected_script.trigger_file.file_path, ctx);
                }
                continue;
            }

            // From here on, the *line* variable is an expandable string leading to a script file
            ctx.script_file = line;
            expand_file_name(line, ctx);
            normalize_path_sep(line);

            // Skip dependency scripts that (do not|no longer) exist
            script_info_t dep_script;
            if (!get_file_modification_time(line, &dep_script.modified_time))
                continue;

            // Add script
            dep_script.file_path  = line.c_str();
            dep_script.reload_cmd = ctx.reload_cmd;
            dep_script.pkg_base   = ctx.pkg_base;

            selected_script.dep_scripts[line.c_str()] = std::move(dep_script);

            expand_ctx_t sub_ctx = ctx;
            sub_ctx.script_file  = line;
            sub_ctx.main_file    = false;
            parse_deps_for_script(sub_ctx);
        }
        qfclose(fp);

        return true;
    }

    void expand_file_name(qstring &filename, const expand_ctx_t &ctx)
    {
        expand_string(filename, filename, ctx);
        make_abs_path(filename, ctx.base_dir.c_str(), true);
    }

    void populate_initial_notebook_cells()
    {
        auto& cell_files = selected_script.notebook.cell_files;
        auto current_path = std::filesystem::path(selected_script.file_path.c_str()).parent_path();
        selected_script.notebook.base_path = current_path.string();

        enumerate_files(
            current_path, 
            selected_script.notebook.cells_re, 
            [&cell_files](const std::string& filename)
            {
                qtime64_t mtime;
                get_file_modification_time(filename, &mtime);
                cell_files[filename] = mtime;
                return true;
            }
        );
    }
    
    void set_selected_script(script_info_t &script)
    {
        // Activate a new script
        selected_script.clear();
        selected_script.refresh(script.file_path.c_str());

        // Recursively parse the dependencies and the index files
        expand_ctx_t main_ctx = { script.file_path.c_str(), true };
        parse_deps_for_script(main_ctx);

        // If a notebook is selected, let's capture all the cell files
        if (selected_script.is_notebook())
            populate_initial_notebook_cells();
    }

    void clear_selected_script()
    {
        action_active_script = nullptr;
        selected_script.clear();
        // ...and deactivate the monitor
        activate_monitor(false);
    }

    const bool has_selected_script()
    {
        return !selected_script.file_path.empty();
    }

    bool is_monitor_active()          const { return m_b_filemon_timer_active; }
    bool is_filemon_timer_installed() const { return m_filemon_timer != nullptr; }

    std::string expand_pkgmodname(const expand_ctx_t& ctx)
    {
        auto dep_file = selected_script.has_dep(ctx.script_file.c_str());
        qstring pkg_base = dep_file == nullptr ? selected_script.pkg_base : dep_file->pkg_base;

        // If the script file is in the package base, then replace the path separators with '.'
        if (strncmp(ctx.script_file.c_str(), pkg_base.c_str(), pkg_base.length()) == 0)
        {
            qstring s = ctx.script_file.c_str() + pkg_base.length() + 1;
            s.replace(SDIRCHAR, ".");
            // Drop the extension too
            auto idx = s.rfind('.');
            if (idx != -1)
                s.resize(idx);

            return s.c_str();
        }
        return "";
    }
    
    // Dynamic string expansion   Description
    // ------------------------   -----------
    // basename                   Returns the basename of the input file
    // env:Variable_Name          Expands the 'Variable_Name'
    // pkgbase                    Sets the current pkgbase path
    // pkgmodname                 Expands the file name using the pkgbase into the form: 'module.submodule1.submodule2'
    // pkgparentmodname           Expands the file name using the pkgbase into the form up to the parent module: 'module.submodule1'
    // ext                        Add-on suffix including bitness and extension (example: 64.dll, .so, 64.so, .dylib, etc.)
    void expand_string(
        qstring &input, 
        qstring &output, 
        const expand_ctx_t& ctx)
    {
        output = std::regex_replace(
            input.c_str(),
            RE_EXPANDER,
            [this, ctx](auto &m) -> std::string
            {
                qstring match1 = m.str(1).c_str();

                if (strncmp(match1.c_str(), "pkgmodname", 10) == 0)
                {
					return expand_pkgmodname(ctx);
                }
                else if (strncmp(match1.c_str(), "pkgparentmodname", 16) == 0)
                {
                    std::string pkgmodname = expand_pkgmodname(ctx);
                    size_t pos = pkgmodname.rfind('.');
                    return pos == std::string::npos ? pkgmodname : pkgmodname.substr(0, pos);
                }
                else if (strncmp(match1.c_str(), "ext", 3) == 0)
                {
                    static_assert(LOADER_DLL[0] == '*');
                    return LOADER_DLL + 1;
                }
                else if (strncmp(match1.c_str(), "pkgbase", 7) == 0)
                {
                    return ctx.pkg_base.c_str();
                }
                else if (strncmp(match1.c_str(), "basename", 8) == 0)
                {
                    char *basename, *ext;
                    qstring wrk_str;
                    get_basename_and_ext(ctx.script_file.c_str(), &basename, &ext, wrk_str);
                    return basename;
                }
                else if (strncmp(match1.c_str(), "env:", 4) == 0)
                {
                    qstring env;
                    if (qgetenv(match1.begin() + 4, &env))
                        return env.c_str();
                }
                return m.str(1);
            }
        ).c_str();
    }

    bool execute_reload_directive(
        script_info_t &dep_script_file,
        qstring &err,
        bool silent=true)
    {
        const char *script_file = dep_script_file.file_path.c_str();

        do
        {
            auto ext = get_file_ext(script_file);
            extlang_object_t elang(find_extlang_by_ext(ext == nullptr ? "" : ext));
            if (elang == nullptr)
            {
                err.sprnt("unknown script language detected for '%s'!\n", script_file);
                break;
            }

            qstring reload_cmd;
            expand_ctx_t ctx;
            ctx.script_file = script_file;
            ctx.pkg_base = dep_script_file.pkg_base;
            expand_string(dep_script_file.reload_cmd, reload_cmd, ctx);

            if (!elang->eval_snippet(reload_cmd.c_str(), &err))
            {
                err.sprnt(
                    "QScripts failed to reload script file: '%s'\n"
                    "Reload command used: %s\n"
                    "Error: %s\n",                  
                    script_file, reload_cmd.c_str(), err.c_str());
                break;
            }
            return true;
        } while (false);

        if (!silent)
			msg("%s", err.c_str());

        return false;
    }

    bool execute_script(script_info_t *script_info, bool with_undo)
    {
        if (with_undo)
        {
            action_active_script = script_info;
            auto r = process_ui_action(ACTION_EXECUTE_SCRIPT_WITH_UNDO_ID);
            action_active_script = nullptr;
            return r;
        }
        return execute_script_sync(script_info);
    }

    // Executes a script file
    bool execute_script_sync(script_info_t *script_info)
    {
        bool exec_ok = false;

        // Pause the file monitor timer while executing a script
        bool old_state = activate_monitor(false);
        do
        {
            auto script_file = script_info->file_path.c_str();

            // First things first: always take the file's modification time-stamp first so not to visit it again in the file monitor timer
            if (!get_file_modification_time(script_file, &script_info->modified_time))
            {
                msg("Script file '%s' not found!\n", script_file);
                break;
            }

            const char *script_ext = get_file_ext(script_file);
            extlang_object_t elang(nullptr);
            if (script_ext == nullptr || (elang = find_extlang_by_ext(script_ext)) == nullptr)
            {
                msg("Unknown script language detected for '%s'!\n", script_file);
                break;
            }

            if (opt_clear_log)
                msg_clear();

            // Silently call the unload script function
            qstring errbuf;
            if (opt_exec_unload_func)
            {
                idc_value_t result;
                elang->call_func(&result, UNLOAD_SCRIPT_FUNC_NAME, &result, 0, &errbuf);
            }

            if (opt_show_filename)
                msg("QScripts executing %s...\n", script_file);

            exec_ok = elang->compile_file(
                script_file, 
#if IDA_SDK_VERSION >= 850
                nullptr,  // requested_namespace
#endif
                &errbuf);
            if (!exec_ok)
            {
                msg("QScripts failed to compile script file: '%s':\n%s", script_file, errbuf.c_str());
                break;
            }

            // Special case for IDC scripts: we have to call 'main'
            if (elang->is_idc())
            {
                idc_value_t result;
                exec_ok = elang->call_func(&result, "main", &result, 0, &errbuf);
                if (!exec_ok)
                {
                    msg("QScripts failed to run the IDC main() of file '%s':\n%s", script_file, errbuf.c_str());
                    break;
                }
            }
        } while (false);
        activate_monitor(old_state);

        return exec_ok;
    }

    enum 
    {
        OPTID_INTERVAL       = 0x0001,
        OPTID_CLEARLOG       = 0x0002,
        OPTID_SHOWNAME       = 0x0004,
        OPTID_UNLOADEXEC     = 0x0008,
        OPTID_SELSCRIPT      = 0x0010,
        OPTID_WITHUNDO       = 0x0020,

        OPTID_ONLY_SCRIPT    = OPTID_SELSCRIPT,
        OPTID_ALL_BUT_SCRIPT = 0xffff & ~OPTID_ONLY_SCRIPT,
        OPTID_ALL            = 0xffff,
    };

    // Save or load the options
    void saveload_options(bool bsave, int what_ids = OPTID_ALL)
    {
        enum { QSTR = 1000 };
        struct options_t
        {
            int id;
            const char *name;
            int vtype;
            void *pval;
        } int_options [] =
        {
            {OPTID_INTERVAL,   "QScripts_interval",             VT_LONG, &opt_change_interval},
            {OPTID_CLEARLOG,   "QScripts_clearlog",             VT_LONG, &opt_clear_log},
            {OPTID_SHOWNAME,   "QScripts_showscriptname",       VT_LONG, &opt_show_filename},
            {OPTID_UNLOADEXEC, "QScripts_exec_unload_func",     VT_LONG, &opt_exec_unload_func},
            {OPTID_SELSCRIPT,  "QScripts_selected_script_name", QSTR, &selected_script.file_path},
            {OPTID_WITHUNDO,   "QScripts_with_undo",            VT_LONG, &opt_with_undo}
        };

        for (auto &opt: int_options)
        {
            if ((what_ids & opt.id) == 0)
                continue;

            if (opt.vtype == VT_LONG)
            {
                if (bsave)
                    reg_write_int(opt.name, *(int *)opt.pval);
                else
                    *(int *)opt.pval = reg_read_int(opt.name, *(int *)opt.pval);
            }
            else if (opt.vtype == VT_STR)
            {
                if (bsave)
                    reg_write_string(opt.name, ((qstring *)opt.pval)->c_str());
                else
                    reg_read_string(((qstring *)opt.pval), opt.name);
            }
            else if (opt.vtype == QSTR)
            {
                if (bsave)
                {
                    reg_write_string(opt.name, ((qstring *)opt.pval)->c_str());
                }
                else
                {
                    qstring tmp;
                    reg_read_string(&tmp, opt.name);
                    *((qstring *)opt.pval) = tmp.c_str();
                }
            }
        }

        if (!bsave)
            opt_change_interval = normalize_filemon_interval(opt_change_interval);
    }

    static int idaapi s_filemon_timer_cb(void *ud)
    {
        return ((qscripts_chooser_t *)ud)->filemon_timer_cb();
    }

    // Monitor callback
    int filemon_timer_cb()
    {
        do
        {
            // No active script, do nothing
            if (!is_monitor_active() || !has_selected_script())
                break;

            std::unique_ptr<active_script_info_t> notebook_cell_script;
            active_script_info_t* work_script = &selected_script;

            //
            // Handle dependencies first
            // 

            // Check if the active script or its dependencies are changed:
            // 1. Dependency file --> repopulate it and execute active script
            // 2. Any dependencies --> reload if needed and //
            // 3. Active script --> execute it again
            auto& dep_scripts = selected_script.dep_scripts;

            // Let's check the dependencies index files first
            auto mod_stat = selected_script.is_any_dep_index_modified();
            if (mod_stat == filemod_status_e::modified)
            {
                // Force re-parsing of the index file
                dep_scripts.clear();
                set_selected_script(selected_script);

                // Let's invalidate all the scripts time stamps so we ensure they are re-interpreted again
                selected_script.invalidate_all_scripts();

                // Refresh the UI
                refresh_chooser(QSCRIPTS_TITLE);

                // Just leave and come back fast so we get a chance to re-evaluate everything
                return 1; // (1 ms)
            }
            // Dependency index file is gone
            else if (mod_stat == filemod_status_e::not_found && !dep_scripts.empty())
            {
                // Let's just check the active script
                dep_scripts.clear();
            }

            //
            // Check the dependency scripts
            //
            bool dep_script_changed = false;
            bool brk = false;
            for (auto& kv : dep_scripts)
            {
                auto& dep_script = kv.second;
                if (dep_script.get_modification_status() == filemod_status_e::modified)
                {
                    qstring err;
                    dep_script_changed = true;
                    if (dep_script.has_reload_directive()
                        && !execute_reload_directive(dep_script, err, false))
                    {
                        brk = true;
                        break;
                    }
                }
            }
            if (brk)
                break;

            //
            // Notebook mode
            //
            if (selected_script.is_notebook())
            {
                auto& last_active_cell = selected_script.notebook.last_active_cell;
                auto& cell_files = selected_script.notebook.cell_files;
                auto current_path = std::filesystem::path(selected_script.file_path.c_str()).parent_path();
                std::unordered_set<std::string> present_files;

                std::string active_cell;
                enumerate_files(
                    current_path, 
                    selected_script.notebook.cells_re, 
                    [&present_files, &last_active_cell, &active_cell, &cell_files](const std::string& filename)
                    {
                        present_files.insert(filename);
                        auto p = cell_files.find(filename);

                        qtime64_t mtime;
                        get_file_modification_time(filename.c_str(), &mtime);

                        // New file?
                        if (p == cell_files.end())
                        {
                            cell_files[filename] = mtime;
                        }
                        // File was modified?
                        else if (p->second != mtime)
                        {
                            last_active_cell = active_cell = filename;
                            p->second = mtime;
                            // Stop enumeration; next interval we pick up the rest
                            return false;
                        }
                        return true;
                    }
                );

                // Remove missing files from cell_files
                for (auto it = cell_files.begin(); it != cell_files.end();)
                {
                    if (present_files.find(it->first) == present_files.end())
                        it = cell_files.erase(it);
                    else
                        ++it;
                }

                // We have to always execute a script when a dependency changes:
                // - If a dependency has changed, but no active cells changedthen attempt to use the last active cell.
                if (dep_script_changed && active_cell.empty())
                    active_cell = selected_script.notebook.last_active_cell;

                // If no modified cell files, then do nothing
                if (!active_cell.empty())
                {
                    // ...use the same metadata as the notebook main script, but just execute the given cell
                    notebook_cell_script.reset(new active_script_info_t(selected_script));
                    work_script = notebook_cell_script.get();
                    work_script->file_path = active_cell.c_str();
                }
            }
            //
            // Trigger mode
            // 
            // In trigger file mode, just wait for the trigger file to be created
            else if (selected_script.trigger_based())
            {
                // The monitor waits until the trigger file is created or modified
                auto trigger_status = selected_script.trigger_file.get_modification_status(true);
                if (trigger_status != filemod_status_e::modified)
                    break;

                // Delete the trigger file
                if (!selected_script.b_keep_trigger_file)
                    qunlink(selected_script.trigger_file.c_str());

                // Always execute the main script even if it was not changed
                selected_script.invalidate();
                // ...and proceed with QScript logic
            }

            // Check the main script
            mod_stat = work_script->get_modification_status();
            if (mod_stat == filemod_status_e::not_found)
            {
                // Script no longer exists
                msg(
                    "QScripts detected that the active script '%s' no longer exists!\n", 
                    work_script->file_path.c_str());
                clear_selected_script();
                break;
            }

            // Script or its dependencies changed?
            if (dep_script_changed || mod_stat == filemod_status_e::modified)
                execute_script(work_script, opt_with_undo);
        } while (false);
        return opt_change_interval;
    }

protected:
    static constexpr uint32 flags_ =
        CH_KEEP    | CH_RESTORE  | CH_ATTRS   | CH_NOIDB |
        CH_CAN_DEL | CH_CAN_EDIT | CH_CAN_INS | CH_CAN_REFRESH;

    static constexpr int widths_[2]               = { 20, 70 };
    static constexpr const char *const header_[2] = { "Script", "Path" };

    static constexpr const char *ACTION_DEACTIVATE_MONITOR_ID        = "qscripts:deactivatemonitor";
    static constexpr const char *ACTION_EXECUTE_SELECTED_SCRIPT_ID   = "qscripts:execselscript";
    static constexpr const char *ACTION_EXECUTE_SCRIPT_WITH_UNDO_ID  = "qscripts:execscriptwithundo";
    static constexpr const char *ACTION_EXECUTE_NOTEBOOK_ID          = "qscripts:executenotebook";

    scripts_info_t m_scripts;
    ssize_t m_nselected = NO_SELECTION;

    static bool is_correct_widget(action_update_ctx_t* ctx)
    {
        return ctx->widget_title == QSCRIPTS_TITLE;
    }


    // Add a new script file and properly populate its script info object
    // and returns a borrowed reference
    const script_info_t *add_script(
        const char *script_file,
        bool silent = false,
        bool unique = true)
    {
        if (unique)
        {
            auto p = m_scripts.find({ script_file });
            if (p != m_scripts.end())
                return &*p;
        }

        qtime64_t mtime;
        if (!get_file_modification_time(script_file, &mtime))
        {
            if (!silent)
                msg("Script file not found: '%s'\n", script_file);
            return nullptr;
        }

        auto &si         = m_scripts.push_back();
        si.file_path     = script_file;
        si.modified_time = mtime;
        return &si;
    }

    bool config_dialog()
    {
        static const char form[] =
            "Options\n"
            "\n"
            "<#Controls the refresh rate of the script change monitor#Script monitor ~i~nterval:D:100:10::>\n"
            "<#Clear the output window before re-running the script#C~l~ear the output window:C>\n"
            "<#Display the name of the file that is automatically executed#Show ~f~ile name when execution:C>\n"
            "<#Execute a function called '__quick_unload_script' before reloading the script#Execute the u~n~load script function:C>\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";

        // Copy values to the dialog
        union
        {
            ushort n;
            struct
            {
                ushort b_clear_log        : 1;
                ushort b_show_filename    : 1;
                ushort b_exec_unload_func : 1;
                ushort b_with_undo        : 1;
            };
        } chk_opts;
        // Load previous options first (account for multiple instances of IDA)
        saveload_options(false);

        chk_opts.n = 0;
        chk_opts.b_clear_log        = opt_clear_log;
        chk_opts.b_show_filename    = opt_show_filename;
        chk_opts.b_exec_unload_func = opt_exec_unload_func;
        chk_opts.b_with_undo        = opt_with_undo;
        sval_t interval             = opt_change_interval;

        if (ask_form(form, &interval, &chk_opts.n) > 0)
        {
            // Copy values from the dialog
            opt_change_interval  = normalize_filemon_interval(int(interval));
            opt_clear_log        = chk_opts.b_clear_log;
            opt_show_filename    = chk_opts.b_show_filename;
            opt_exec_unload_func = chk_opts.b_exec_unload_func;
            opt_with_undo        = chk_opts.b_with_undo;

            // Save the options directly
            saveload_options(true);
            return true;
        }
        return false;
    }

    const void *get_obj_id(size_t *len) const override
    {
        // Allow a single instance
        *len = sizeof(this);
        return (const void *)this;
    }

    size_t idaapi get_count() const override
    {
        return m_scripts.size();
    }

    void idaapi get_row(
        qstrvec_t *cols,
        int *icon,
        chooser_item_attrs_t *attrs,
        size_t n) const override
    {
        auto si = &m_scripts[n];
        auto path = si->file_path.c_str();
        auto name = strrchr(path, DIRCHAR);
        cols->at(0) = name == nullptr ? path : name + 1;
        cols->at(1) = path;
        if (n == m_nselected)
        {
            if (is_monitor_active())
            {
                attrs->flags = CHITEM_BOLD;
                *icon = selected_script.is_notebook() ? IDAICONS::NOTEPAD_1 : IDAICONS::KEYBOARD_GRAY;
            }
            else
            {
                attrs->flags = CHITEM_ITALIC;
                *icon = IDAICONS::RED_DOT;
            }
        }
        else if (is_monitor_active() && selected_script.has_dep(si->file_path) != nullptr)
        {
            // Mark as a dependency
            *icon = IDAICONS::EYE_GLASSES_EDIT;
        }
        else
        {
            // Mark as an inactive file
            *icon = IDAICONS::GRAY_X_CIRCLE;
        }
    }

    // Activate a script and execute it
    cbret_t idaapi enter(size_t n) override
    {
        bool exec_ok = false;

        m_nselected = n;

        // Set as the selected script and execute it
        set_selected_script(m_scripts[n]);

        if (   selected_script.is_notebook() 
            && selected_script.notebook.activation_action == notebook_ctx_t::act_exec_none)
        {
            // Notebook, execute nothing on activation
            msg("Selected notebook: %s\n",
                selected_script.notebook.title.c_str());
        }
        else if (   selected_script.is_notebook()
                 && selected_script.notebook.activation_action == notebook_ctx_t::act_exec_all)
        {
            // Notebook, execute all scripts on activation
            msg("Executing all scripts for notebook: %s\n", 
                selected_script.notebook.title.c_str());

            execute_notebook_cells(&selected_script);
        }
        else
        {
            exec_ok = execute_script(&selected_script, opt_with_undo);
        }

        if (exec_ok)
            saveload_options(true, OPTID_ONLY_SCRIPT);

        // ...and activate the monitor even if the script fails
        activate_monitor();

        return cbret_t(n, chooser_base_t::ALL_CHANGED);
    }

    // Add a new script
    cbret_t idaapi ins(ssize_t) override
    {
        qstring filter;
        get_browse_scripts_filter(filter);
        const char *script_file = ask_file(false, "", "%s", filter.c_str());
        if (script_file == nullptr)
            return {};

        reg_update_strlist(IDAREG_RECENT_SCRIPTS, script_file, IDA_MAX_RECENT_SCRIPTS);
        ssize_t idx = build_scripts_list(script_file);
        return cbret_t(qmax(idx, 0), chooser_base_t::ALL_CHANGED);
    }

    // Remove a script from the list
    cbret_t idaapi del(size_t n) override
    {
        auto &script_file = m_scripts[n].file_path;
        reg_update_strlist(IDAREG_RECENT_SCRIPTS, nullptr, IDA_MAX_RECENT_SCRIPTS, script_file.c_str());
        build_scripts_list();

        // Active script removed?
        if (m_nselected == NO_SELECTION)
            clear_selected_script();

        return adjust_last_item(n);
    }

    // Use it to show the configuration dialog
    cbret_t idaapi edit(size_t n) override
    {
        config_dialog();
        return cbret_t(n, chooser_base_t::NOTHING_CHANGED);
    }

    void idaapi closed() override
    {
        saveload_options(true);
    }

    static void get_browse_scripts_filter(qstring &filter)
    {
        // Collect all installed external languages
        extlangs_t langs;
        collect_extlangs(&langs, false);

        // Build the filter
        filter = "FILTER Script files|";

        for (auto lang: langs)
            filter.cat_sprnt("*.%s;", lang->fileext);

        filter.remove_last();
        filter.append("|");

        // Language specific filters
        for (auto lang: langs)
            filter.cat_sprnt("%s scripts|*.%s|", lang->name, lang->fileext);

        filter.remove_last();
        filter.append("\nSelect script file to load");
    }

    void setup_ui()
    {
        am.add_action(
            AMAHF_NONE,
            ACTION_DEACTIVATE_MONITOR_ID,
            "QScripts: Deactivate script monitor",
            "Ctrl-D",
            FO_ACTION_UPDATE([this],
                return this->is_correct_widget(ctx) ? AST_ENABLE_FOR_WIDGET : AST_DISABLE_FOR_WIDGET;
            ),
            FO_ACTION_ACTIVATE([this]) {
                if (this->is_monitor_active())
                {
                    this->clear_selected_script();
                    refresh_chooser(QSCRIPTS_TITLE);
                }
                return 1;
            },
            nullptr,
            IDAICONS::DISABLED);

        am.add_action(
            AMAHF_NONE,
            ACTION_EXECUTE_SELECTED_SCRIPT_ID,
            "QScripts: Execute selected script",
            "Shift-Enter",
            FO_ACTION_UPDATE([this],
                return this->is_correct_widget(ctx) ? AST_ENABLE_FOR_WIDGET : AST_DISABLE_FOR_WIDGET;
            ),
            FO_ACTION_ACTIVATE([this]) {
                if (!ctx->chooser_selection.empty())
                    this->execute_script_at(ctx->chooser_selection.at(0));
                return 1;
            },
            "Execute script without activating it",
            IDAICONS::FLASH);

        am.add_action(
            AMAHF_NONE,
            ACTION_EXECUTE_SCRIPT_WITH_UNDO_ID,
            "QScripts: Execute last active script",
            "Alt-Shift-X",
            FO_ACTION_UPDATE([this],
                return AST_ENABLE_ALWAYS;
            ),
            FO_ACTION_ACTIVATE([this]) 
            {
                if (is_monitor_active())
                {
                    script_info_t* work_script = nullptr;
                    std::unique_ptr<active_script_info_t> cell_script;
                    if (selected_script.is_notebook())
                    {
                        if (!selected_script.notebook.last_active_cell.empty())
                        {
                            cell_script.reset(new active_script_info_t(selected_script));
                            if (work_script = cell_script.get())
                                work_script->file_path = selected_script.notebook.last_active_cell.c_str();
                        }
                    }
                    else if (action_active_script != nullptr)
                        work_script = action_active_script;
                    else if (this->has_selected_script())
                        work_script = &selected_script;

                    if (work_script != nullptr)
                        this->execute_script_sync(work_script);
                }
                return 1;
            },
            "An action to programmatically execute the active script",
            IDAICONS::FLASH);

        am.add_action(
            AMAHF_NONE,
            ACTION_EXECUTE_NOTEBOOK_ID,
            "QScripts: Execute all notebook cells",
            "",
            FO_ACTION_UPDATE([this],
                return AST_ENABLE_ALWAYS;
            ),
            FO_ACTION_ACTIVATE([this]) 
            {
                if (this->has_selected_script() && selected_script.is_notebook())
                    this->execute_notebook_cells(&selected_script);
                return 1;
            },
            "An action to programmatically execute the active script",
            IDAICONS::NOTEPAD_1);
    }

public:
    static constexpr const char *QSCRIPTS_TITLE = "QScripts";

    qscripts_chooser_t(const char *title_ = QSCRIPTS_TITLE)
        : chooser_t(flags_, qnumber(widths_), widths_, header_, title_), am(this)
    {
        popup_names[POPUP_EDIT] = "~O~ptions";
        setup_ui();
        saveload_options(false);
    }

    bool activate_monitor(bool activate = true)
    {
        bool old = m_b_filemon_timer_active;
        m_b_filemon_timer_active = activate;
        return old;
    }

    // Rebuilds the scripts list and returns the index of the `find_script` if needed
    ssize_t build_scripts_list(const char *find_script = nullptr)
    {
        // Remember active script and invalidate its index
        bool b_has_selected_script = has_selected_script();
        qstring selected_script;
        if (b_has_selected_script)
            selected_script = get_selected_script_file();

        // De-selected the current script in the hope of finding it again in the list
        m_nselected = NO_SELECTION;

        // Read all scripts
        qstrvec_t scripts_list;
        reg_read_strlist(&scripts_list, IDAREG_RECENT_SCRIPTS);

        // Rebuild the list
        ssize_t idx = 0, find_idx = NO_SELECTION;
        m_scripts.qclear();
        for (auto &script_file: scripts_list)
        {
            // Restore active script
            if (b_has_selected_script && selected_script == script_file)
                m_nselected = idx;

            // Optionally, find the index of a script by name
            if (find_script != nullptr && streq(script_file.c_str(), find_script))
                find_idx = idx;

            // We skip non-existent scripts
            if (add_script(script_file.c_str(), true) != nullptr)
                ++idx;
        }
        return find_idx;
    }

    void execute_last_selected_script(bool with_undo=false)
    {
        if (has_selected_script())
            execute_script(&selected_script, with_undo);
    }

    void execute_script_at(ssize_t n)
    {
        if (n >=0 && n < ssize_t(m_scripts.size()))
            execute_script(&m_scripts[n], opt_with_undo);
    }

    void execute_notebook_cells(active_script_info_t *script)
    {
        auto& cell_files = script->notebook.cell_files;

        active_script_info_t cell_script = *script;
        enumerate_files(
            script->notebook.base_path,
            script->notebook.cells_re,
            [this, &cell_script, &cell_files](const std::string& filename)
            {
                qtime64_t mtime;
                get_file_modification_time(filename, &mtime);
                cell_files[filename] = mtime;
                cell_script.file_path = filename.c_str();
                // Execute script and stop enumeration if one cell fails to execute
                return this->execute_script_sync(&cell_script);
            }
        );
    }

    void show()
    {
        build_scripts_list();

        auto r = choose(m_nselected);

        TWidget *widget;
        if (r == 0 && (widget = find_widget(QSCRIPTS_TITLE)) != nullptr)
        {
            attach_action_to_popup(
                widget,
                nullptr,
                ACTION_DEACTIVATE_MONITOR_ID);
            attach_action_to_popup(
                widget,
                nullptr,
                ACTION_EXECUTE_SELECTED_SCRIPT_ID);
            attach_action_to_popup(
                widget,
                nullptr,
                ACTION_EXECUTE_SCRIPT_WITH_UNDO_ID);
            attach_action_to_popup(
                widget,
                nullptr,
                ACTION_EXECUTE_NOTEBOOK_ID);
        }
    }

    bool install_filemon_timer()
    {
        m_filemon_timer = register_timer(
            opt_change_interval,
            s_filemon_timer_cb,
            this);
        return is_filemon_timer_installed();
    }

    void uninstall_filemon_timer()
    {
        if (is_filemon_timer_installed())
        {
            unregister_timer(m_filemon_timer);
            m_filemon_timer = nullptr;
        }
        activate_monitor(false);
    }

    bool idaapi run(size_t arg) override
    {
        switch (arg)
        {
            // Full UI run
            case 0:
            {
                if (!is_filemon_timer_installed())
                {
                    if (!install_filemon_timer())
                        msg("QScripts: failed to start the file monitor.\n");
                    else
                        msg("QScripts: file monitor started successfully.\n");
                }

                show();
                break;
            }
            // Execute the selected script
            case 1:
            {
                execute_last_selected_script();
                break;
            }
            // Activate the scripts monitor
            case 2:
            {
                activate_monitor(true);
                refresh_chooser(QSCRIPTS_TITLE);
                break;
            }
            // Deactivate the scripts monitor
            case 3:
            {
                activate_monitor(false);
                refresh_chooser(QSCRIPTS_TITLE);
                break;
            }
        }

        return true;
    }

    virtual ~qscripts_chooser_t()
    {
        uninstall_filemon_timer();
    }
};

//-------------------------------------------------------------------------
plugmod_t *idaapi init(void)
{
    auto plg = new qscripts_chooser_t();
    if (!plg->install_filemon_timer())
    {
        if (::debug & IDA_DEBUG_PLUGIN)
            msg("QScripts: failed to install the file monitor on startup. Please invoke the UI once to attempt try again!\n");
    }

    return plg;
}

//--------------------------------------------------------------------------
static const char help[] =
    "An alternative scripts manager that lets you develop in an external editor and run them fast in IDA\n"
    "\n"
    "Just press ENTER on the script to activate it and then go back to your editor to continue development.\n"
    "\n"
    "Each time you update your script, it will be automatically invoked in IDA\n\n"
    "\n"
    "QScripts is developed by Elias Bachaalany. Please see https://github.com/0xeb/ida-qscripts for more information\n"
    "\n"
    "\0"
    __DATE__ " " __TIME__ "\n"
    "\n";

//--------------------------------------------------------------------------
//
//      PLUGIN DESCRIPTION BLOCK
//
//--------------------------------------------------------------------------
plugin_t PLUGIN =
{
    IDP_INTERFACE_VERSION,
    PLUGIN_MULTI | PLUGIN_FIX,
    init,
    nullptr,
    nullptr,
    "QScripts: Develop IDA scripts faster in your favorite text editor",
    help,
    qscripts_chooser_t::QSCRIPTS_TITLE,
#ifdef _DEBUG
    "Alt-Shift-A"
#else
    "Alt-Shift-F9"
#endif
};


================================================
FILE: script.hpp
================================================
#pragma once

#define QSCRIPTS_LOCAL ".qscripts"
static constexpr char UNLOAD_SCRIPT_FUNC_NAME[] = "__quick_unload_script";
static constexpr auto DEFAULT_CELLS_RE = R"(\d{4}.*\.py$)";

//-------------------------------------------------------------------------
// File modification state
enum class filemod_status_e
{
    not_found,
    not_modified,
    modified
};

// Structure to describe a file and its metadata
struct fileinfo_t
{
    qstring file_path;
    qtime64_t modified_time;

    fileinfo_t(const char* file_path = nullptr): modified_time(0)
    {
        if (file_path != nullptr)
            this->file_path = file_path;
    }

    inline const bool empty() const
    {
        return file_path.empty();
    }

    inline const char* c_str()
    {
        return file_path.c_str();
    }

    bool operator==(const fileinfo_t &rhs) const
    {
        return file_path == rhs.file_path;
    }

    virtual void clear()
    {
        file_path.clear();
        modified_time = 0;
    }

    bool refresh(const char *file_path = nullptr)
    {
        if (file_path != nullptr)
            this->file_path = file_path;

        return get_file_modification_time(this->file_path, &modified_time);
    }

    // Checks if the current script has been modified
    // Optionally updates the time stamp to the latest one if modified
    filemod_status_e get_modification_status(bool update_mtime=true)
    {
        qtime64_t cur_mtime;
        const char *script_file = this->file_path.c_str();
        if (!get_file_modification_time(script_file, &cur_mtime))
        {
            if (update_mtime)
                modified_time = 0;
            return filemod_status_e::not_found;
        }

        // Script is up to date, no need to execute it again
        if (cur_mtime == modified_time)
            return filemod_status_e::not_modified;

        if (update_mtime)
            modified_time = cur_mtime;

        return filemod_status_e::modified;
    }

    void invalidate()
    {
        modified_time = 0;
    }
};

//-------------------------------------------------------------------------
// Dependency script info
struct script_info_t: fileinfo_t
{
    using fileinfo_t::fileinfo_t;

    // Each dependency script can have its own reload command
    qstring reload_cmd;

    // Base path if this dependency is part of a package
    qstring pkg_base;

    const bool has_reload_directive() const { return !reload_cmd.empty(); }
};

// Script files
using scripts_info_t = qvector<script_info_t>;

//-------------------------------------------------------------------------
// Notebook context
struct notebook_ctx_t
{
    enum activate_action_e
    {
        act_exec_none,
        act_exec_main,
        act_exec_all
    };
    std::string base_path;
    std::string title;
    std::regex cells_re = std::regex(DEFAULT_CELLS_RE);
    std::map<std::string, qtime64_t> cell_files;
    std::string last_active_cell;

    int activation_action = act_exec_none;

    void clear()
    {
        title.clear();
        cell_files.clear();
        last_active_cell.clear();
        cells_re = std::regex(DEFAULT_CELLS_RE);
    }
};

//-------------------------------------------------------------------------
// Active script information along with its dependencies
struct active_script_info_t : script_info_t
{
    // Notebook options
    bool b_is_notebook = false;

    const bool is_notebook() const {
        return b_is_notebook;
    }

    notebook_ctx_t notebook;

    // Trigger file options
    fileinfo_t trigger_file;
    bool b_keep_trigger_file;

    // The dependencies index files. First entry is for the main script's deps
    qvector<fileinfo_t> dep_indices;

    // The list of dependency scripts
    std::unordered_map<std::string, script_info_t> dep_scripts;

    // Checks to see if we have a dependency on a given file
    const script_info_t* has_dep(const qstring& dep_file) const
    {
        auto p = dep_scripts.find(dep_file.c_str());
        return p == dep_scripts.end() ? nullptr : &p->second;
    }

    // Is this trigger based or dependency based?
    const bool trigger_based() const { return !trigger_file.empty(); }

    // If no dependency index files have been modified, return 0.
    // Return 1 if one of them has been modified or -1 if one of them has gone missing.
    // In both latter cases, we have to recompute our dependencies
    filemod_status_e is_any_dep_index_modified(bool update_mtime = true)
    {
        filemod_status_e r = filemod_status_e::not_modified;
        for (auto& dep_file : dep_indices)
        {
            r = dep_file.get_modification_status(update_mtime);
            if (r != filemod_status_e::not_modified)
                break;
        }
        return r;
    }

    bool add_dep_index(const char* dep_file)
    {
        fileinfo_t fi;
        if (!get_file_modification_time(dep_file, &fi.modified_time))
            return false;

        fi.file_path = dep_file;
        dep_indices.push_back(std::move(fi));
        return true;
    }

    void clear() override
    {
        script_info_t::clear();
        dep_indices.qclear();
        dep_scripts.clear();
        trigger_file.clear();
        b_keep_trigger_file = false;
        b_is_notebook = false;
        notebook.clear();
        reload_cmd.clear();
        pkg_base.clear();
    }

    void invalidate_all_scripts()
    {
        invalidate();

        // Invalidate all but the index file itself
        for (auto& kv : dep_scripts)
            kv.second.invalidate();
    }
};


================================================
FILE: snippet_manager/README.md
================================================
# Snippet Manager Plugin for IDA

This plugin for IDA provides a set of functionality for importing, exporting or deleting all snippets from IDA (Shift-F2 window).

## Functions

Type the following functions from IDA's CLI or just call them directly from Python:

- `idaapi.ext.snippets.save(output_folder='')`: Save snippets to a specified output folder.
- `idaapi.ext.snippets.load(input_folder='')`: Load snippets from a specified input folder.
- `idaapi.ext.snippets.delete()`: Delete all existing snippets.
- `idaapi.ext.snippets.man`: Direct access to the snippet manager object.

By 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.

## Installation

Copy the `snippetmanager.py` file to your IDA plugin folder of your choice.

Alternatively, for example on Windows, just make a symbolic link directly:

```batch
C:\Projects\ida-qscripts\snippet_manager>mklink c:\ida\plugins\snippetmanager.py %cd%\snippetmanager.py
```

## Very important

Due 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.

================================================
FILE: snippet_manager/pseudocode.cpp
================================================
// The following snippet / pseudo-code has been kindly shared by  Arnaud Diederen from Hex-Rays

////
//// save snippets + rebuild index
////

void save_all_snippets()
{ 
  netnode main_node;
  main_node.create("$ scriptsnippets");
  
  atag = 'A'
  main_node.altdel_all(atag);
  
  // save each, plus add in index
  for i in range(snippets):
      netnode snippet_node = snippet(i).save(); // see below
      main_node.altset(i, snippet_node+1);
}
 
netnode snippet_t::save()
{
   xtag = 'X'
   if ( node == BADNODE )
     node.create();
   node.supset(0, name.c_str());
   node.supset(1, lang->name);
   node.setblob(body.c_str(), body.size(), 0, xtag); // body
   return node;
}


================================================
FILE: snippet_manager/snippetmanager.py
================================================
"""
Snippet loader/saver for IDA Pro

by Elias Bachaalany / @allthingsida
"""

# TODO:
# - snippetmanager: run snippets in order (optional prefix RE; comes with stock REs); save_combined()
# - - save combined()
# - save_clean() --> delete previous snippets and save
# - stock regular expressions: \d+

import os
from typing import Union
import idaapi, idc

LANG_EXT_MAP = {
    "Python": "py",
    "IDC": "idc",
}

R_LANG_EXT_MAP = {v: k for k, v in LANG_EXT_MAP.items()}

# --------------------------------------------------------------
class snippet_t:
    def __init__(self, 
                 lang: str, 
                 name: str, 
                 body: str, /, 
                 netnode_idx: int = idaapi.BADNODE, 
                 index: int =-1):
        self.lang : str = lang
        self.name : str = name
        self.body : str = body
        self.netnode_idx : int = netnode_idx
        """Underlying netnode index"""
        self.index : int = index
        """Position in the index table"""

    def __repr__(self) -> str:
        return f"snippet_t({self.lang}, {self.name}, {self.body}, {self.netnode_idx}, {self.index})"

    def __str__(self) -> str:
        return f"lang: {self.lang}; title: {self.name}; index: {self.index}"

    def save(self, index: int) -> None:
        """Saves a node and updates the netnode index that was used to save it"""
        node = idaapi.netnode()
        node.create()

        node.supset(1, self.lang)
        node.supset(0, self.name)
        node.setblob(self.body.encode('utf-8'), 0, 'X')

        self.index = index
        self.netnode_idx = node.index()

    @staticmethod
    def from_file(file_name: str) -> Union[None, 'snippet_t']:
        """Constructs a snippet from a file.
        Its slot index and netnode index are not known yet.
        Call save() to save it to the database and get the netnode index
        """
        if not os.path.exists(file_name):
            return None

        basename = os.path.basename(file_name)
        ext = os.path.splitext(basename)[1].lstrip('.').lower()
        if not (lang := R_LANG_EXT_MAP.get(ext)):
            return None

        with open(file_name, 'r') as f:
            body = f.read()

        name = os.path.splitext(basename)[0]
        return snippet_t(lang, name, body)

    @staticmethod
    def from_netnode(netnode_idx: int, slot_idx: int, fast: bool = False):
        node = idaapi.netnode(netnode_idx)
        body = None if fast else node.getblob(0, 'X')
        return snippet_t(node.supstr(1),
                         node.supstr(0),
                         body.decode('utf-8').rstrip('\x00') if body else "",
                         netnode_idx=netnode_idx,
                         index=slot_idx)

# --------------------------------------------------------------
class snippet_manager_t:
    def __init__(self):
        self.index = idaapi.netnode()
        self.index.create("$ scriptsnippets")

    def delete(self, snippet: snippet_t) -> bool:
        if snippet.netnode_idx == idaapi.BADNODE:
            return False

        self.index.altdel(snippet.index)
        idaapi.netnode(snippet.netnode_idx).kill()
        snippet.netnode_idx = idaapi.BADNODE
        return True

    def delete_all(self) -> None:
        snippets = self.retrieve_snippets(fast=True)
        for snip in snippets:
            self.delete(snip)

        self.index.altdel_all()

    def load_from_folder(self, folder : str = '') -> bool:
        """Imports snippets from a folder."""
        # If no input directory is specified, use the default one
        if not folder:
            folder = os.path.join(os.path.dirname(idc.get_idb_path()), '.snippets')
            # For the default directory, create it if it doesn't exist
            if not os.path.exists(folder):
                os.makedirs(folder)

        # For a custom directory, check if it exists
        if not os.path.exists(folder):
            return False

        # Delete previous snippets (and reset index)
        self.delete_all()

        # Enumerate files
        snippets = []
        for file in os.listdir(folder):
            file = os.path.join(folder, file)
            if snippet := snippet_t.from_file(file):
                snippets.append(snippet)

        # Sort by snippet name
        snippets.sort(key=lambda x: x.name)

        for isnippet, snippet in enumerate(snippets):
            snippet.save(isnippet)
            self.index.altset(isnippet, snippet.netnode_idx + 1)

        return True

    def save_to_folder(self, folder: str = '') -> tuple[bool, str]:
        if not folder:
            folder = os.path.join(os.path.dirname(idc.get_idb_path()), '.snippets')

        if not os.path.exists(folder):
            os.makedirs(folder)

        try:
            snippets = self.retrieve_snippets()
            for snip in snippets:
                outfile_name = os.path.join(folder, "%s.%s" % (snip.name, LANG_EXT_MAP[snip.lang]))
                with open(outfile_name, 'w') as outfile:
                    outfile.write(snip.body)
        except Exception as e:
            return (False, f'Failed to save: {e!s}')
        return (True, f'Saved {len(snippets)} snippets to {folder}')


    def retrieve_snippets(self, fast: bool = False) -> list[snippet_t]:
        """Load all snippets from the database"""
        snip_idx = self.index.altfirst()
        if snip_idx == idaapi.BADNODE:
            return []

        snippets = []
        while snip_idx != idaapi.BADNODE:
            snip_node_idx = self.index.altval(snip_idx) - 1
            snippet = snippet_t.from_netnode(snip_node_idx, snip_idx, fast=fast)
            snippets.append(snippet)

            snip_idx = self.index.altnext(snip_idx)

        return snippets


# --------------------------------------------------------------
# Register some functions into the extension part of the IDA API
ext = getattr(idaapi, 'ext', None)
if not ext:
    ext = idaapi.ext = idaapi.object_t()

_sm = snippet_manager_t()

def save_snippets(output_folder: str =''):
    """
    Save snippets to a specified output folder. If no folder is specified, 
    the function uses the current directory by default.

    Args:
    output_folder (str): The folder where the snippets will be saved. 
                         Defaults to current directory if not specified.

    Returns: 
    Result of the operation performed by the save_to_folder method.
    """
    return _sm.save_to_folder(output_folder)

def load_snippets(input_folder: str =''):
    """
    Load snippets from a specified input folder. If no folder is specified, 
    the function uses the current directory by default.

    Args:
    input_folder (str): The folder from where the snippets will be loaded. 
                        Defaults to current directory if not specified.

    Returns: 
    Result of the operation performed by the load_from_folder method.
    """
    return _sm.load_from_folder(input_folder)

def delete_snippets():
    """
    Delete all existing snippets.

    Returns: 
    Result of the operation performed by the delete_all method.
    """
    return _sm.delete_all()

ext.snippets = idaapi.object_t(
    save=save_snippets,
    load=load_snippets,
    delete=delete_snippets,
    man=_sm,
)

# --------------------------------------------------------------
def _test_load(with_body=False):
    idaapi.msg_clear()
    global sm
    sm = _sm
    snippets = sm.retrieve_snippets()
    for isnip, snip in enumerate(snippets):
        print(f"#{isnip:03d} {snip!s}")
        if with_body:
            print("<body>\n%s" % snip.body)
            print("</body>\n------")

# --------------------------------------------------------------
class snippetman_plugmod_t(idaapi.plugmod_t):
    def run(self, _):
        print("""Please use the following methods to:
    - save snippets: idaapi.ext.snippets.save()
    - load snippets: idaapi.ext.snippets.load()
    - delete snippets: idaapi.ext.snippets.delete()
    - access the snippet manager object: idaapi.ext.snippets.man
""")
        return 0

class snippetman_plugin_t(idaapi.plugin_t):
    flags = idaapi.PLUGIN_MULTI
    comment = "Snippet manager plugin"
    help = "Run the plugin to get the full help message"
    wanted_name = "Snippet manager"
    wanted_hotkey = ""

    def init(self):
        return snippetman_plugmod_t()

def PLUGIN_ENTRY() -> idaapi.plugin_t:
    return snippetman_plugin_t()

# --------------------------------------------------------------
if __name__ == "__main__":
    _test_load(False)


================================================
FILE: test_addons/README.md
================================================
# QScripts Test Add-ons

QScripts 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.

This directory provides example native add-ons (plugins, loaders, processors) show casing QScripts' hot-reload workflow for rapid iteration and testing.


## Available Templates

### [plugin_template](./plugin_template)
Basic plugin template for general IDA Pro plugin development.

### [plugin_triton](./plugin_triton)
Advanced plugin integrating the [Triton](https://github.com/JonathanSalwan/Triton) dynamic binary analysis framework.

### [loader_template](./loader_template)
Template for developing custom file format loaders (with mock `accept_file` and `load_file` implementations).

## Building Add-ons

### Standard Build Process

1. **Navigate to the add-on directory:**
   ```bash
   cd test_addons/<addon_name>
   ```

2. **Configure with CMake:**
   ```bash
   cmake -B build -A x64
   ```

3. **Build the add-on:**
   ```bash
   cmake --build build --config RelWithDebInfo
   ```

The compiled binary will be automatically installed to:
- Plugins: `%IDASDK%/bin/plugins/`
- Loaders: `%IDASDK%/bin/loaders/`
- Processors: `%IDASDK%/bin/procs/`

## Hot-Reload Development Workflow

### 1. Create a Python Script
```python
import time
import idaapi

# Give the linker time to finish
time.sleep(1)

# Load your plugin (adjust name and arguments as needed)
idaapi.load_and_run_plugin('your_addon_name', 0)
```

### 2. Configure Dependencies
Create a `.deps.qscripts` file to specify what triggers reloading:

```
# Monitor the compiled plugin DLL
/triggerfile /keep $env:IDASDK$/bin/plugins/your_addon$ext$
```

### 3. Activate Monitoring
1. Open QScripts chooser
2. Navigate to your Python script
3. Press `Enter` to activate monitoring
4. The script name appears in **bold** when active

### 4. Development Cycle
1. Modify your C++ code
2. Rebuild the add-on
3. QScripts automatically detects changes and reloads
4. No IDA restart required!


================================================
FILE: test_addons/loader_template/.gitignore
================================================
build*/

================================================
FILE: test_addons/loader_template/CMakeLists.txt
================================================
cmake_minimum_required(VERSION 3.27)
project(qscripts_native)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# Include IDA SDK bootstrap
include($ENV{IDASDK}/ida-cmake/bootstrap.cmake)
find_package(idasdk REQUIRED)

# Add loader
ida_add_loader(qscripts_native
    SOURCES
        driver.cpp
        main.cpp
        idasdk.h
    DEBUG_ARGS
        "-t"
)


================================================
FILE: test_addons/loader_template/driver.cpp
================================================
#include "idasdk.h"

extern bool main();

//--------------------------------------------------------------------------
struct plugin_ctx_t : public plugmod_t
{
    bool idaapi run(size_t) override
    {
        return main();
    }
};

//--------------------------------------------------------------------------
plugin_t PLUGIN =
{
  IDP_INTERFACE_VERSION,
  PLUGIN_UNL | PLUGIN_MULTI,
  []()->plugmod_t* {return new plugin_ctx_t; },
  nullptr,
  nullptr,
  nullptr,
  nullptr,
  "QScripts native plugin driver",
  nullptr,
};


================================================
FILE: test_addons/loader_template/idasdk.h
================================================
#pragma warning(push)
#pragma warning(disable: 4244 4267)

#include <ida.hpp>
#include <idp.hpp>
#include <loader.hpp>
#include <kernwin.hpp>
#include <name.hpp>
#include <funcs.hpp>
#include <hexrays.hpp>
#include <diskio.hpp>
#include <entry.hpp>

#pragma warning(pop)


================================================
FILE: test_addons/loader_template/main.cpp
================================================
#include "idasdk.h"

#pragma pack(push, 1)
struct file_header_t
{
    char sig[4];      // Signature == "CHNK"
    char cpuname[10]; // Processor name (for set_processor_type())
    int32 nchunks;    // Number of chunks
    int32 entrypoint; // The entry point address
};

struct chunk_t
{
    int32 base;     // base address
    int32 sz;       // size
};
#pragma pack(pop)

static int idaapi accept_file(
    qstring* fileformatname,
    qstring* processor,
    linput_t* li,
    const char* filename)
{
    file_header_t fh;
    lread(li, &fh, sizeof(file_header_t));
    if (strncmp(fh.sig, "CHNK", 4) != 0)
        return 0;

    *fileformatname = "Chunk file loader";
    *processor = fh.cpuname;

    return 1;
}

//--------------------------------------------------------------------------
//
//      load file into the database.
//
void idaapi load_file(linput_t* li, ushort neflag, const char* fileformatname)
{
    // Read the header to detect the number of chunks, proc name, etc.
    file_header_t fh;
    lread(li, &fh, sizeof(file_header_t));

    set_processor_type(fh.cpuname, SETPROC_USER);

    for (int32 i = 0; i < fh.nchunks; ++i)
    {
        qstring seg_name;
        seg_name.sprnt("chunk%d", i);

        chunk_t chkinfo;
        lread(li, &chkinfo, sizeof(chkinfo));

        ea_t start_ea = chkinfo.base;
        ea_t end_ea = chkinfo.base + chkinfo.sz;
        add_segm(
            0,
            start_ea, 
            end_ea, 
            seg_name.c_str(),
            "CODE",
            0);

        // Now read the actual data
        file2base(li, qltell(li), start_ea, end_ea, 1);
    }
    inf_set_start_ea(fh.entrypoint);
    inf_set_start_ip(fh.entrypoint);
    inf_set_start_cs(0);
    add_entry(0, fh.entrypoint, "start", 1, 0);
}

bool test_accept_file(linput_t *li, const char *fname)
{
    qstring format_name, procname;
    if (accept_file(&format_name, &procname, li, fname))
    {
        msg("Recognized format name: %s\n", format_name.c_str());
        msg("Suggest proc module   : %s\n", procname.c_str());
        return true;
    }
    else
    {
        msg("Not recognized!\n");
        return false;
    }
}

bool main()
{
    msg_clear();

    auto fname = R"(C:\Users\elias\Projects\github\ida-qscripts\samples\chunk1.bin)";
    auto li = open_linput(fname, false);

    if (!test_accept_file(li, fname))
        return false;
    
    load_file(li, 0, fname);
    close_linput(li);
}

================================================
FILE: test_addons/plugin_template/.claude/agents/ida-cmake.md
================================================
---
name: ida-cmake
description: 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.
model: sonnet
color: orange
---

IDA Pro addons usually refer to: plugins, processor modules, file loaders, or idalib standalone applications.
You are an expert in building these addons using the ida-cmake CMake scripts located in `$IDASDK/ida-cmake`.

- Refer to `$IDASDK/ida-cmake/README.md` for documentation on how to use `ida-cmake`.
- Refer to `$IDASDK/ida-cmake/templates/plugin/` for example CMakeLists.txt file and plugin
- Refer to `$IDASDK/ida-cmake/templates/loader` for example CMakeLists.txt file and loader
- Refer to `$IDASDK/ida-cmake/templates/procmod` for example CMakeLists.txt file and processor module
- Refer to `$IDASDK/ida-cmake/templates/idalib` for example CMakeLists.txt file and idalib application
- Refer to `$IDASDK/include` for SDK headers. All headers have docstrings, use them to explain SDK usage and lookup APIs.
- Refer to `$IDASDK/plugins`, `$IDASDK/loaders`, `$IDASDK/module` for SDK example and how APIs are used in practice.

You must open and read those files above to answer the user correctly to answer all things IDA compiling and building related questions.

## CMake Usage

The ida-cmake provides clean interface libraries:
- `idasdk::plugin` - For IDA plugins
- `idasdk::loader` - For file loaders
- `idasdk::procmod` - For processor modules
- `idasdk::idalib` - For standalone idalib applications

## Example Usage

For quick reference, here's a skeleton CMakeLists.txt begining:

```cmake
cmake_minimum_required(VERSION 3.27)
project(myplugin)
set(CMAKE_CXX_STANDARD 17)

# Include IDA SDK bootstrap
include($ENV{IDASDK}/ida-cmake/bootstrap.cmake)
find_package(idasdk REQUIRED)
```

Now, for a plugin, use the `ida_add_plugin` function:

```cmake
ida_add_plugin(myplugin
    SOURCES
        main.cpp
    DEBUG_ARGS
        "-t -z10000"
)```

...and for a file loader, use the `ida_add_loader` function:

```cmake
ida_add_loader(myloader
    SOURCES
        loader.cpp
    DEBUG_ARGS
        "-c -A"  # Common loader debug args
)```

...and for a standalone idalib application, use the following CMakeLists.txt:

```cmake
ida_add_idalib_exe(myidalib
    SOURCES
        main.cpp
    DEBUG_ARGS
        "${IDASDK}/ida-cmake/samples/wizmo32.exe.i64"
)```

To build an addon, just use something like from the addon source folder:

```bash
cmake -B build
cmake --build build --config RelWithDebInfo
```


================================================
FILE: test_addons/plugin_template/.gitignore
================================================
build*/

================================================
FILE: test_addons/plugin_template/CLAUDE.md
================================================
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

# qscripts_native - IDA Pro Plugin Template

This is a template for developing IDA Pro plugins using the QScripts hot-reload workflow. The plugin lists non-library functions in the current database.

## Build Commands

Always use the ida-cmake agent for building:
```
Task with subagent_type="ida-cmake"
```

## Prerequisites

- **IDASDK**: Environment variable must be set to IDA SDK path
- **IDAX**: Must be installed at `$IDASDK/include/idax/`
- **ida-cmake**: Must be installed at `$IDASDK/ida-cmake/`

## Architecture

### File Structure
- `driver.cpp`: IDA plugin interface boilerplate (PLUGIN structure and plugmod_t implementation)
- `main.cpp`: Core plugin logic - implement your functionality here
- `idasdk.h`: Consolidated IDA SDK header includes with warning suppression
- `CMakeLists.txt`: Build configuration using ida-cmake

### Plugin Behavior
- **Type**: PLUGIN_UNL | PLUGIN_MULTI (unloadable, multi-instance)
- **Entry Point**: `main()` function in main.cpp receives size_t argument
- **Current Function**: Enumerates all non-library functions and prints their names

## Development Workflow

This template is designed for use with QScripts hot-reload:
1. Parent directory contains `qscripts_native.py.deps.qscripts` with trigger file configuration
2. Built plugin outputs to `$IDASDK/bin/plugins/qscripts_native[.dll/.so/.dylib]`
3. QScripts monitors the output file and automatically reloads on changes

## SDK and IDA API Resources

When answering SDK/API questions, search and read from:
- **SDK Headers**: `$IDASDK/include` - All headers have docstrings
- **SDK Examples**: `$IDASDK/plugins`, `$IDASDK/loaders`, `$IDASDK/module`

================================================
FILE: test_addons/plugin_template/CMakeLists.txt
================================================
cmake_minimum_required(VERSION 3.27)
project(qscripts_native)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# Include IDA SDK bootstrap
include($ENV{IDASDK}/ida-cmake/bootstrap.cmake)
find_package(idasdk REQUIRED)

# Add plugin
ida_add_plugin(qscripts_native
    SOURCES
        driver.cpp
        main.cpp
        idasdk.h
    DEBUG_ARGS
        "-t"
)


================================================
FILE: test_addons/plugin_template/driver.cpp
================================================
#include "idasdk.h"

extern bool main(size_t);

//--------------------------------------------------------------------------
struct plugin_ctx_t : public plugmod_t
{
    bool idaapi run(size_t arg) override
    {
        return main(arg);
    }
};

//--------------------------------------------------------------------------
plugin_t PLUGIN =
{
  IDP_INTERFACE_VERSION,
  PLUGIN_UNL | PLUGIN_MULTI,
  []()->plugmod_t* {return new plugin_ctx_t; },
  nullptr,
  nullptr,
  nullptr,
  nullptr,
  "QScripts native plugin driver",
  nullptr,
};


================================================
FILE: test_addons/plugin_template/idasdk.h
================================================
#pragma warning(push)
#pragma warning(disable: 4244 4267)

#include <ida.hpp>
#include <idp.hpp>
#include <loader.hpp>
#include <kernwin.hpp>
#include <name.hpp>
#include <funcs.hpp>
// Include more headers here...
#pragma warning(pop)


================================================
FILE: test_addons/plugin_template/main.cpp
================================================
#include "idasdk.h"

bool main(size_t)
{
    auto nfuncs = get_func_qty();

    size_t c = 0;
    for (size_t i = 0; i < nfuncs; ++i)
    {
        func_t* func = getn_func(i);
        if (func->flags & FUNC_LIB)
            continue;

        ++c;
        qstring name;
        get_func_name(&name, func->start_ea);
        msg("%a: %s\n", func->start_ea, name.c_str());
    }

    msg("%zu function(s)\n", c);
	return true;
}

================================================
FILE: test_addons/plugin_triton/.gitignore
================================================
build*/
.z3-trace
junk64.bin.id?
junk64.bin.til
junk64.bin.nam

================================================
FILE: test_addons/plugin_triton/CMakeLists.txt
================================================
cmake_minimum_required(VERSION 3.27)
project(qscripts_native_triton)

# Make sure 'tritonenv.bat' script has been called and the proper environment variables are set

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# Include IDA SDK bootstrap
include($ENV{IDASDK}/ida-cmake/bootstrap.cmake)
find_package(idasdk REQUIRED)

# Extract vcpkg include path from TRITON_LIB environment variable
string(REPLACE ";" ";" TRITON_LIB_LIST "$ENV{TRITON_LIB}")
foreach(LIB_PATH IN LISTS TRITON_LIB_LIST)
    # Handle both Windows backslashes and Unix forward slashes
    if(LIB_PATH MATCHES "(.+)[/\\\\]vcpkg_installed[/\\\\]([^/\\\\]+)[/\\\\]lib[/\\\\]")
        set(VCPKG_INCLUDE_DIR "${CMAKE_MATCH_1}/vcpkg_installed/${CMAKE_MATCH_2}/include")
        # Normalize path separators for consistency
        string(REPLACE "\\" "/" VCPKG_INCLUDE_DIR "${VCPKG_INCLUDE_DIR}")
        break()
    endif()
endforeach()

# Add plugin with Triton support
ida_add_plugin(qscripts_native_triton
    SOURCES
        driver.cpp
        main.cpp
    INCLUDES
        $ENV{TRITON_INCLUDES}
        ${VCPKG_INCLUDE_DIR}
    LIBRARIES
        $ENV{TRITON_LIB}
    DEBUG_ARGS
        ${CMAKE_CURRENT_LIST_DIR}/junk64.bin.i64
)

# Disable specific warnings
target_compile_options(qscripts_native_triton PRIVATE /wd4267 /wd4244)


================================================
FILE: test_addons/plugin_triton/README.md
================================================
# Triton Plugin for IDA Pro with QScripts

A 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.

## Quick Start

### Installing Triton on Windows

Let's clone to `C:\tools` for this example:

1. **Install vcpkg** (if not already installed):
   ```batch
   cd C:\tools
   git clone https://github.com/Microsoft/vcpkg.git
   cd vcpkg
   bootstrap-vcpkg.bat
   ```

2. **Clone Triton**:
   ```batch
   cd C:\tools
   git clone https://github.com/JonathanSalwan/Triton.git
   cd Triton
   ```

3. **Build with static runtime** (required for IDA plugins):
   ```batch
   cmake -B build-static -A x64 ^
         -DCMAKE_TOOLCHAIN_FILE="c:\tools\vcpkg\scripts\buildsystems\vcpkg.cmake" ^
         -DVCPKG_TARGET_TRIPLET=x64-windows-static ^
         -DBUILD_SHARED_LIBS=OFF ^
         -DPYTHON_BINDINGS=OFF ^
         -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded

   cmake --build build-static --config Release
   ```

### Building the Plugin

1. **Set up environment variables** (required):
   ```batch
   set TRITON_INCLUDES=C:\tools\Triton\src\libtriton\includes;C:\tools\Triton\build-static\src\libtriton\includes
   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
   ```

2. **Build the plugin**:
   ```batch
   cmake -B build -A x64
   cmake --build build --config RelWithDebInfo
   ```

3. **Plugin is automatically installed** to `%IDASDK%\bin\plugins\qscripts_native_triton.dll`

4. Activate the `qscripts_native_triton.py` to load the plugin in IDA and start monitoring.


================================================
FILE: test_addons/plugin_triton/driver.cpp
================================================
#include "idasdk.h"

extern bool main(size_t);

//--------------------------------------------------------------------------
struct plugin_ctx_t : public plugmod_t
{
    bool idaapi run(size_t arg) override
    {
        return main(arg);
    }
};

//--------------------------------------------------------------------------
plugin_t PLUGIN =
{
    IDP_INTERFACE_VERSION,
    PLUGIN_UNL | PLUGIN_MULTI,
    []()->plugmod_t* { return new plugin_ctx_t; },
    nullptr,
    nullptr,
    nullptr,
    nullptr,
    "QScripts native Triton plugin driver",
    "Ctrl-Shift-R",
};


================================================
FILE: test_addons/plugin_triton/idasdk.h
================================================
#pragma warning(push)
#pragma warning(disable: 4244 4267)

#define USE_DANGEROUS_FUNCTIONS
#include <ida.hpp>
#include <idp.hpp>
#include <loader.hpp>
#include <kernwin.hpp>
#include <name.hpp>
#include <funcs.hpp>
// Include more headers here...
#pragma warning(pop)


================================================
FILE: test_addons/plugin_triton/main.cpp
================================================
#include "idasdk.h"

#include <sstream>
#include <triton/context.hpp>
#include <triton/basicBlock.hpp>
#include <triton/x86Specifications.hpp>

using namespace triton;
using namespace triton::arch;

bool main(size_t)
{
    constexpr const ea_t FUNC_EA = 0x400000;
    msg_clear();

    auto f = get_func(FUNC_EA);
    if (f == nullptr)
    {
        msg("Can't find test function @ %a!\n", FUNC_EA);
        return false;
    }

    /* Init the triton context */
    triton::Context ctx;
    ctx.setArchitecture(ARCH_X86_64);

    func_item_iterator_t ffi;
    BasicBlock bb;
    for (bool ok = ffi.set(f); ok; ok = ffi.next_code())
    {
        auto ea = ffi.current();
        insn_t insn;
        if (decode_insn(&insn, ea) == 0)
        {
            msg("Failed to decode at %a\n", ea);
            return false;
        }

        unsigned char buf[32];
        get_bytes(buf, qmin(sizeof(buf), insn.size), ea);

        // Setup opcode
        Instruction inst;
        inst.setOpcode(buf, insn.size);
        inst.setAddress(triton::uint64(ea));
        bb.add(inst);
    }

    ctx.disassembly(bb, FUNC_EA);

    std::ostringstream ostr;
    ostr << bb;
    msg("----------------\n"
        "Original:\n"
        "----------------\n"
        "%s\n", ostr.str().c_str());

    ostr.str("");
    auto simplified_bb = ctx.simplify(bb);
    ostr << simplified_bb;
    msg("----------------\n"
        "Simplified:\n"
        "----------------\n"
        "%s\n", ostr.str().c_str());

	return true;
}


================================================
FILE: test_addons/plugin_triton/qscripts_native_triton.py
================================================
import time
import idaapi

# Give the linker time to finish flushing the binary
time.sleep(1)

# Optionally clear the screen:
#idaapi.msg_clear()

# Load your plugin and pass any arg value you want
idaapi.load_and_run_plugin('qscripts_native_triton', 0)

# Optionally, do post work, etc.

================================================
FILE: test_addons/plugin_triton/qscripts_native_triton.py.deps.qscripts
================================================
/triggerfile /keep $env:IDASDK$\bin\plugins\qscripts_native_triton$ext$

================================================
FILE: test_addons/qscripts_native.py
================================================
import time
import idaapi

# Give the linker time to finish flushing the binary
time.sleep(1)

# Optionally clear the screen:
#idaapi.msg_clear()

# Load your plugin and pass any arg value you want
idaapi.load_and_run_plugin('qscripts_native', 0)

# Optionally, do post work, etc.

================================================
FILE: test_addons/qscripts_native.py.deps.qscripts
================================================
/triggerfile /keep $env:IDASDK$\bin\plugins\qscripts_native$ext$

================================================
FILE: test_scripts/dependency-test/README.md
================================================
This is a dependency test folder.

The script `t1.py` has its dependency index file which describes the reload directive and one dependency on `t2.py`:
```
/reload import importlib;import $basename$;importlib.reload($basename$)
t2.py
```

OTOH, `t2.py` has a relative dependency on `t3.py` (in the same folder) and on `subdir/t4.py` in a sub-folder:
```
t3.py
subdir/t4.py
```

The script `t4.py` has a single relative dependency in its index file:
```
t5.py
```

If 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.

================================================
FILE: test_scripts/dependency-test/subdir/t4.py
================================================
print("f4, This is %s" % __file__)

def f4():
    print("t4.f4() called!")


================================================
FILE: test_scripts/dependency-test/subdir/t4.py.deps.qscripts
================================================
t5.py

================================================
FILE: test_scripts/dependency-test/subdir/t5.py
================================================
print("f5, This is %s" % __file__)

def f5():
    print("t5.f5() called!")


================================================
FILE: test_scripts/dependency-test/t1.py
================================================
import sys

subdir = os.path.join(os.path.dirname(__file__), 'subdir')
if subdir not in sys.path:
    print("-->adding to path: %s" % subdir)
    sys.path.append(subdir)

import datetime
import t2, t3, t4, t5

print("--- %s; this is %s.." % (datetime.datetime.now(), __file__))

t2.f2()
t3.f3()
t4.f4()
t5.f5()

================================================
FILE: test_scripts/dependency-test/t1.py.deps.qscripts
================================================
/reload import importlib;importlib.reload($basename$)
t2.py

================================================
FILE: test_scripts/dependency-test/t2.py
================================================
print("t2 changed, This is %s" % __file__)
def f2():
    print("t2.f2() called!")

================================================
FILE: test_scripts/dependency-test/t2.py.deps.qscripts
================================================
t3.py
subdir/t4.py

================================================
FILE: test_scripts/dependency-test/t3.py
================================================
print("t3., This is %s" % __file__)
def f3():
    print("t3.f3() called!")

================================================
FILE: test_scripts/hello.idc
================================================
#include <idc.idc>

static main()
{
    Message("Welcome to QScripts!\n");
}

static __fast_unload_script()
{
    Message("IDC: aha! unloaded!\n");
}


================================================
FILE: test_scripts/hello.py
================================================
try:
    var1 += 1
except:
    var1 = 1

def __quick_unload_script():
    print("Unloaded: %s" % str(var1))

print(f"Just edit and save! {var1}")



================================================
FILE: test_scripts/notebooks/0000 Imports and Init.py
================================================
import os
import sys
import idaapi
import idautils
import idc

VERSION = globals().get('VERSION', 0) + 1

print(f"Initialized; version: {VERSION}")

================================================
FILE: test_scripts/notebooks/0000 Imports and Init.py.deps.qscripts
================================================
# A simple notebook
/notebook Test notebook
# On activation, just execute the main script
/notebook.activate exec_main
# Uses default cells matcher (pattern: \d{4}.*\.py$)


================================================
FILE: test_scripts/notebooks/0010 List functions.py
================================================
# Enumerating functions

if idaapi.get_func_qty() != 0:
    for func_ea in idautils.Functions():
        print(f"Function at 0x{func_ea:#08X}")
else:
    print("No functions found!")

================================================
FILE: test_scripts/notebooks/0020 List segments.py
================================================
# Enumerating segments

if idaapi.get_segm_qty() > 0:
    for seg_ea in idautils.Segments():
        seg = idaapi.getseg(seg_ea)
        print(f"Segment at {seg.start_ea:#08x} - {seg.end_ea:#08x}")

else:
    print("No segments found!")



================================================
FILE: test_scripts/notebooks/0030 Show info.py
================================================
# Enumerating segments

print(f"min_ea={idaapi.inf_get_min_ea():#x}, max_ea={idaapi.inf_get_max_ea():#x}")


================================================
FILE: test_scripts/notebooks/0040 Set name.py
================================================
# Rename setuff
idaapi.set_name(idc.here(), "my_function")


================================================
FILE: test_scripts/notebooks/0050 Set name.py
================================================
# Rename setuff
z = globals().get('z', 0) + 1
idaapi.set_name(idc.here(), f"my_function_{z}")


================================================
FILE: test_scripts/notebooks/A0000 Init.py
================================================
import os
import sys
import idaapi
import idautils
import idc

VERSION_A = globals().get('VERSION_A', 0) + 1

print(f"A)Initialized; version: {VERSION_A}")


================================================
FILE: test_scripts/notebooks/A0000 Init.py.deps.qscripts
================================================
/notebook Test notebook #A
# This notebook looks for "Annnn" prefixed cells
/notebook.cells_re A\d{4}.*\.py$
# On notebook execution, execute all cells
/notebook.activate exec_all


================================================
FILE: test_scripts/notebooks/A0010 Cell 1.py
================================================
print('This is Cell A.1')


================================================
FILE: test_scripts/notebooks/A0020 Cell 2.py
================================================
print('This is Cell A.2')

================================================
FILE: test_scripts/notebooks/B0000 Init.py
================================================
import os
import sys
import idaapi
import idautils
import idc

VERSION_B = globals().get('VERSION_B', 0) + 1

print(f"B)Initialized; version: {VERSION_B}")

================================================
FILE: test_scripts/notebooks/B0000 Init.py.deps.qscripts
================================================
/notebook Test notebook #B
/notebook.cells_re B\d{4}.*\.py$
# On notebook activation, just execute this main script
/notebook.activate exec_main

================================================
FILE: test_scripts/notebooks/B0010 Cell 1.py
================================================
print('This is Cell B.1')

================================================
FILE: test_scripts/notebooks/B0020 Cell 2.py
================================================
print('This is Cell B.2')

================================================
FILE: test_scripts/notebooks/README.md
================================================
# Notebook dependency example

## Quick start

To define a notebook, just add the `/notebook [title]` directive as such:

```
/notebook Test notebook #B
```

Then, to select the files to be monitored, use the `/notebook.cells_re` directive, specifying a regular expression pattern to match the desired files:

```
/notebook.cells_re B\d{4}.*\.py$
```

In 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.

Now, when a notebook is activated, you have options to:

- Execute the main script (`exec_main`)
- All scripts (`exec_all`) 
- or no scripts (`exec_none`)

Using the `/notebook.activate` directive:

```
/notebook.activate exec_main
```

## Provided notebooks examples

- `0000 Imports and Init.py` - This notebook has its own dependency file that looks for the "nnnn *.py" Python files
- `A0000 Init.py` - This notebook has its own dependency file that looks for the "Annn *.py" Python files
- `B0000 Init.py` - This notebook has its own dependency file that looks for the "Bnnnn *.py" Python files

As 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.


================================================
FILE: test_scripts/pkg-dependency/README.md
================================================
# Package Dependency Example

In 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.

For this purpose, we can either explicitly specify the package's full path or refer to it through an environment variable, as demonstrated below:

```plaintext
# Define package base folder
/pkgbase $env:idapyx$
# Automatically reload the package's modules when they change
/reload import importlib;from $pkgparentmodname$ import $basename$ as __qscripts_autoreload__; importlib.reload(__qscripts_autoreload__)
# Specify the paths to the package modules that need to be reloaded if they change
$pkgbase$/idapyx/bin/pe/rtfuncs.py
$pkgbase$/idapyx/bin/pe/types.py
```

- The `/pkgbase` directive specifies the base path of the package, aiding in the module reload process.
- The reload directive employs the `pkgmodparentname` variable to derive the Python parent module name based on the package directory.
- Similarly, the reload directive utilizes the `basename` variable to determine the Python module name, again using the package directory.
- Dependencies are specified relative to their package base folder, facilitated by the `pkgbase` variable.


================================================
FILE: test_scripts/pkg-dependency/example.py.deps.qscripts
================================================
# Package dependencies (here, we use the environment variable 'IDAPYX' that contains the package path
/pkgbase $env:idapyx$
/reload import importlib;from $pkgparentmodname$ import $basename$ as __qscripts_autoreload__;importlib.reload(__qscripts_autoreload__)
$pkgbase$/idapyx/bin/pe/rtfuncs.py
$pkgbase$/idapyx/bin/pe/types.py



================================================
FILE: test_scripts/trigger-file/dep.py
================================================
try:
    trigger_ver += 1
except:
    trigger_ver = 1

print(f"dep file version {trigger_ver}: Even if you save this file, this script won't re-execute. Instead, create the trigger file.")

================================================
FILE: test_scripts/trigger-file/trigger.py
================================================
import idaapi
import dep

try:
    trigger_ver += 1
except:
    trigger_ver = 1

print(f"version {trigger_ver}: Even if you save this file, this script won't re-execute. Instead, create the trigger file.")

print("----------------------")

================================================
FILE: test_scripts/trigger-file/trigger.py.deps.qscripts
================================================
/reload import importlib;importlib.reload($basename$);
/triggerfile createme.tmp
dep.py

================================================
FILE: utils_impl.cpp
================================================
//-------------------------------------------------------------------------
struct collect_extlangs: extlang_visitor_t
{
    extlangs_t *langs;

    virtual ssize_t idaapi visit_extlang(extlang_t *extlang) override
    {
        langs->push_back(extlang);
        return 0;
    }

    collect_extlangs(extlangs_t *langs, bool select)
    {
        langs->qclear();
        this->langs = langs;
        for_all_extlangs(*this, select);
    }
};

//-------------------------------------------------------------------------
// Utility function to return a file's last modification timestamp
bool get_file_modification_time(
    const char *filename,
    qtime64_t *mtime = nullptr)
{
    qstatbuf stat_buf;
    if (qstat(filename, &stat_buf) != 0)
        return false;

    if (mtime != nullptr)
        *mtime = stat_buf.qst_mtime;
    return true;
}

template <class STRING>
bool get_file_modification_time(
    const STRING &filename,
    qtime64_t *mtime = nullptr)
{
    return get_file_modification_time(filename.c_str(), mtime);
}

//-------------------------------------------------------------------------
void normalize_path_sep(qstring &path)
{
#ifdef __FAT__
    path.replace("/", SDIRCHAR);
#else
    path.replace("\\", SDIRCHAR);
#endif
}

//-------------------------------------------------------------------------
void make_abs_path(qstring& path, const char* base_dir = nullptr, bool normalize = false)
{
    if (qisabspath(path.c_str()))
        return;

    auto old_cwd = std::filesystem::current_path();
    if (base_dir == nullptr)
    {
        path = old_cwd.string().c_str();
    }
    else
    {
        std::filesystem::current_path(base_dir);
        auto abs = std::filesystem::absolute(path.c_str());
        path = abs.string().c_str();
        std::filesystem::current_path(old_cwd);
    }
    if (normalize)
        normalize_path_sep(path);
}

//-------------------------------------------------------------------------
bool get_basename_and_ext(
    const char *path, 
    char **basename,
    char **ext,
    qstring &wrk_buf)
{
    wrk_buf = path;
    qsplitfile(wrk_buf.begin(), basename, ext);
    if ((*basename = qstrrchr(wrk_buf.begin(), DIRCHAR)) != nullptr)
        return ++(*basename), true;
    else
        return false;
}

//-------------------------------------------------------------------------
inline void get_current_directory(qstring &dir)
{
    dir = std::filesystem::current_path().string().c_str();
}

//-------------------------------------------------------------------------
// Utility function similar to Python's re.sub().
// Based on https://stackoverflow.com/a/37516316
namespace std
{
    template<class BidirIt, class Traits, class CharT, class UnaryFunction>
    std::basic_string<CharT> regex_replace(BidirIt first, BidirIt last,
        const std::basic_regex<CharT, Traits> &re, UnaryFunction f)
    {
        std::basic_string<CharT> s;

        typename std::match_results<BidirIt>::difference_type positionOfLastMatch = 0;
        auto endOfLastMatch = first;

        auto callback = [&](const std::match_results<BidirIt> &match)
        {
            auto positionOfThisMatch = match.position(0);
            auto diff = positionOfThisMatch - positionOfLastMatch;

            auto startOfThisMatch = endOfLastMatch;
            std::advance(startOfThisMatch, diff);

            s.append(endOfLastMatch, startOfThisMatch);
            s.append(f(match));

            auto lengthOfMatch = match.length(0);

            positionOfLastMatch = positionOfThisMatch + lengthOfMatch;

            endOfLastMatch = startOfThisMatch;
            std::advance(endOfLastMatch, lengthOfMatch);
        };

        std::regex_iterator<BidirIt> begin(first, last, re), end;
        std::for_each(begin, end, callback);

        s.append(endOfLastMatch, last);

        return s;
    }

    template<class Traits, class CharT, class UnaryFunction>
    std::string regex_replace(const std::string &s,
        const std::basic_regex<CharT, Traits> &re, UnaryFunction f)
    {
        return regex_replace(s.cbegin(), s.cend(), re, f);
    }
}

//-------------------------------------------------------------------------
void enumerate_files(
    const std::filesystem::path& path, 
    const std::regex& filter, 
    std::function<bool(const std::string&)> callback)
{
    for (const auto& entry : std::filesystem::directory_iterator(path)) 
    {
        if (entry.is_regular_file()) 
        {
            if (!std::regex_match(entry.path().filename().string(), filter)) 
                continue;
            if (!callback(entry.path().string()))
                break;
        }
    }
}

Download .txt
gitextract_tqzl_grq/

├── .gitattributes
├── .github/
│   └── workflows/
│       └── build.yml
├── .gitignore
├── CLAUDE.md
├── CMakeLists.txt
├── LICENSE
├── README.md
├── TODO.md
├── ida-plugin.json
├── ida.h
├── prep-cmake.bat
├── qscripts.cpp
├── script.hpp
├── snippet_manager/
│   ├── README.md
│   ├── pseudocode.cpp
│   └── snippetmanager.py
├── test_addons/
│   ├── README.md
│   ├── loader_template/
│   │   ├── .gitignore
│   │   ├── CMakeLists.txt
│   │   ├── driver.cpp
│   │   ├── idasdk.h
│   │   └── main.cpp
│   ├── plugin_template/
│   │   ├── .claude/
│   │   │   └── agents/
│   │   │       └── ida-cmake.md
│   │   ├── .gitignore
│   │   ├── CLAUDE.md
│   │   ├── CMakeLists.txt
│   │   ├── driver.cpp
│   │   ├── idasdk.h
│   │   └── main.cpp
│   ├── plugin_triton/
│   │   ├── .gitignore
│   │   ├── CMakeLists.txt
│   │   ├── README.md
│   │   ├── driver.cpp
│   │   ├── idasdk.h
│   │   ├── junk64.bin.i64
│   │   ├── main.cpp
│   │   ├── qscripts_native_triton.py
│   │   └── qscripts_native_triton.py.deps.qscripts
│   ├── qscripts_native.py
│   └── qscripts_native.py.deps.qscripts
├── test_scripts/
│   ├── dependency-test/
│   │   ├── README.md
│   │   ├── subdir/
│   │   │   ├── t4.py
│   │   │   ├── t4.py.deps.qscripts
│   │   │   └── t5.py
│   │   ├── t1.py
│   │   ├── t1.py.deps.qscripts
│   │   ├── t2.py
│   │   ├── t2.py.deps.qscripts
│   │   └── t3.py
│   ├── hello.idc
│   ├── hello.py
│   ├── notebooks/
│   │   ├── 0000 Imports and Init.py
│   │   ├── 0000 Imports and Init.py.deps.qscripts
│   │   ├── 0010 List functions.py
│   │   ├── 0020 List segments.py
│   │   ├── 0030 Show info.py
│   │   ├── 0040 Set name.py
│   │   ├── 0050 Set name.py
│   │   ├── A0000 Init.py
│   │   ├── A0000 Init.py.deps.qscripts
│   │   ├── A0010 Cell 1.py
│   │   ├── A0020 Cell 2.py
│   │   ├── B0000 Init.py
│   │   ├── B0000 Init.py.deps.qscripts
│   │   ├── B0010 Cell 1.py
│   │   ├── B0020 Cell 2.py
│   │   └── README.md
│   ├── pkg-dependency/
│   │   ├── README.md
│   │   └── example.py.deps.qscripts
│   └── trigger-file/
│       ├── dep.py
│       ├── trigger.py
│       └── trigger.py.deps.qscripts
└── utils_impl.cpp
Download .txt
SYMBOL INDEX (121 symbols across 16 files)

FILE: qscripts.cpp
  type qscripts_chooser_t (line 26) | struct qscripts_chooser_t: public plugmod_t, public chooser_t
    type expand_ctx_t (line 47) | struct expand_ctx_t
    method normalize_filemon_interval (line 59) | inline int normalize_filemon_interval(const int change_interval) const
    method make_meta_filename (line 69) | bool make_meta_filename(
    method find_deps_file (line 92) | bool find_deps_file(
    method parse_deps_for_script (line 101) | bool parse_deps_for_script(expand_ctx_t &ctx)
    method expand_file_name (line 233) | void expand_file_name(qstring &filename, const expand_ctx_t &ctx)
    method populate_initial_notebook_cells (line 239) | void populate_initial_notebook_cells()
    method set_selected_script (line 258) | void set_selected_script(script_info_t &script)
    method clear_selected_script (line 273) | void clear_selected_script()
    method has_selected_script (line 281) | const bool has_selected_script()
    method is_monitor_active (line 286) | bool is_monitor_active()          const { return m_b_filemon_timer_act...
    method is_filemon_timer_installed (line 287) | bool is_filemon_timer_installed() const { return m_filemon_timer != nu...
    method expand_pkgmodname (line 289) | std::string expand_pkgmodname(const expand_ctx_t& ctx)
    method expand_string (line 317) | void expand_string(
    method execute_reload_directive (line 366) | bool execute_reload_directive(
    method execute_script (line 407) | bool execute_script(script_info_t *script_info, bool with_undo)
    method execute_script_sync (line 420) | bool execute_script_sync(script_info_t *script_info)
    method saveload_options (line 503) | void saveload_options(bool bsave, int what_ids = OPTID_ALL)
    method s_filemon_timer_cb (line 560) | static int idaapi s_filemon_timer_cb(void *ud)
    method filemon_timer_cb (line 566) | int filemon_timer_cb()
    method is_correct_widget (line 751) | static bool is_correct_widget(action_update_ctx_t* ctx)
    method script_info_t (line 759) | const script_info_t *add_script(
    method config_dialog (line 785) | bool config_dialog()
    method get_count (line 844) | size_t idaapi get_count() const override
    method get_row (line 849) | void idaapi get_row(
    method cbret_t (line 886) | cbret_t idaapi enter(size_t n) override
    method cbret_t (line 926) | cbret_t idaapi ins(ssize_t) override
    method cbret_t (line 940) | cbret_t idaapi del(size_t n) override
    method cbret_t (line 954) | cbret_t idaapi edit(size_t n) override
    method closed (line 960) | void idaapi closed() override
    method get_browse_scripts_filter (line 965) | static void get_browse_scripts_filter(qstring &filter)
    method setup_ui (line 988) | void setup_ui()
    method qscripts_chooser_t (line 1082) | qscripts_chooser_t(const char *title_ = QSCRIPTS_TITLE)
    method activate_monitor (line 1090) | bool activate_monitor(bool activate = true)
    method build_scripts_list (line 1098) | ssize_t build_scripts_list(const char *find_script = nullptr)
    method execute_last_selected_script (line 1133) | void execute_last_selected_script(bool with_undo=false)
    method execute_script_at (line 1139) | void execute_script_at(ssize_t n)
    method execute_notebook_cells (line 1145) | void execute_notebook_cells(active_script_info_t *script)
    method show (line 1165) | void show()
    method install_filemon_timer (line 1193) | bool install_filemon_timer()
    method uninstall_filemon_timer (line 1202) | void uninstall_filemon_timer()
    method run (line 1212) | bool idaapi run(size_t arg) override

FILE: script.hpp
  type filemod_status_e (line 9) | enum class filemod_status_e
  type fileinfo_t (line 17) | struct fileinfo_t
    method fileinfo_t (line 22) | fileinfo_t(const char* file_path = nullptr): modified_time(0)
    method empty (line 28) | inline const bool empty() const
    method clear (line 43) | virtual void clear()
    method refresh (line 49) | bool refresh(const char *file_path = nullptr)
    method filemod_status_e (line 59) | filemod_status_e get_modification_status(bool update_mtime=true)
    method invalidate (line 80) | void invalidate()
  type script_info_t (line 88) | struct script_info_t: fileinfo_t
    method has_reload_directive (line 98) | const bool has_reload_directive() const { return !reload_cmd.empty(); }
  type notebook_ctx_t (line 106) | struct notebook_ctx_t
    type activate_action_e (line 108) | enum activate_action_e
    method clear (line 122) | void clear()
  type active_script_info_t (line 133) | struct active_script_info_t : script_info_t
    method is_notebook (line 138) | const bool is_notebook() const {
    method script_info_t (line 155) | const script_info_t* has_dep(const qstring& dep_file) const
    method trigger_based (line 162) | const bool trigger_based() const { return !trigger_file.empty(); }
    method filemod_status_e (line 167) | filemod_status_e is_any_dep_index_modified(bool update_mtime = true)
    method add_dep_index (line 179) | bool add_dep_index(const char* dep_file)
    method clear (line 190) | void clear() override
    method invalidate_all_scripts (line 203) | void invalidate_all_scripts()

FILE: snippet_manager/pseudocode.cpp
  function save_all_snippets (line 7) | void save_all_snippets()
  function netnode (line 21) | netnode snippet_t::save()

FILE: snippet_manager/snippetmanager.py
  class snippet_t (line 25) | class snippet_t:
    method __init__ (line 26) | def __init__(self,
    method __repr__ (line 40) | def __repr__(self) -> str:
    method __str__ (line 43) | def __str__(self) -> str:
    method save (line 46) | def save(self, index: int) -> None:
    method from_file (line 59) | def from_file(file_name: str) -> Union[None, 'snippet_t']:
    method from_netnode (line 79) | def from_netnode(netnode_idx: int, slot_idx: int, fast: bool = False):
  class snippet_manager_t (line 89) | class snippet_manager_t:
    method __init__ (line 90) | def __init__(self):
    method delete (line 94) | def delete(self, snippet: snippet_t) -> bool:
    method delete_all (line 103) | def delete_all(self) -> None:
    method load_from_folder (line 110) | def load_from_folder(self, folder : str = '') -> bool:
    method save_to_folder (line 142) | def save_to_folder(self, folder: str = '') -> tuple[bool, str]:
    method retrieve_snippets (line 160) | def retrieve_snippets(self, fast: bool = False) -> list[snippet_t]:
  function save_snippets (line 185) | def save_snippets(output_folder: str =''):
  function load_snippets (line 199) | def load_snippets(input_folder: str =''):
  function delete_snippets (line 213) | def delete_snippets():
  function _test_load (line 230) | def _test_load(with_body=False):
  class snippetman_plugmod_t (line 242) | class snippetman_plugmod_t(idaapi.plugmod_t):
    method run (line 243) | def run(self, _):
  class snippetman_plugin_t (line 252) | class snippetman_plugin_t(idaapi.plugin_t):
    method init (line 259) | def init(self):
  function PLUGIN_ENTRY (line 262) | def PLUGIN_ENTRY() -> idaapi.plugin_t:

FILE: test_addons/loader_template/driver.cpp
  type plugin_ctx_t (line 6) | struct plugin_ctx_t : public plugmod_t
    method run (line 8) | bool idaapi run(size_t) override

FILE: test_addons/loader_template/main.cpp
  type file_header_t (line 4) | struct file_header_t
  type chunk_t (line 12) | struct chunk_t
  function accept_file (line 19) | static int idaapi accept_file(
  function load_file (line 40) | void idaapi load_file(linput_t* li, ushort neflag, const char* fileforma...
  function test_accept_file (line 75) | bool test_accept_file(linput_t *li, const char *fname)
  function main (line 91) | bool main()

FILE: test_addons/plugin_template/driver.cpp
  type plugin_ctx_t (line 6) | struct plugin_ctx_t : public plugmod_t
    method run (line 8) | bool idaapi run(size_t arg) override

FILE: test_addons/plugin_template/main.cpp
  function main (line 3) | bool main(size_t)

FILE: test_addons/plugin_triton/driver.cpp
  type plugin_ctx_t (line 6) | struct plugin_ctx_t : public plugmod_t
    method run (line 8) | bool idaapi run(size_t arg) override

FILE: test_addons/plugin_triton/main.cpp
  function main (line 11) | bool main(size_t)

FILE: test_scripts/dependency-test/subdir/t4.py
  function f4 (line 3) | def f4():

FILE: test_scripts/dependency-test/subdir/t5.py
  function f5 (line 3) | def f5():

FILE: test_scripts/dependency-test/t2.py
  function f2 (line 2) | def f2():

FILE: test_scripts/dependency-test/t3.py
  function f3 (line 2) | def f3():

FILE: test_scripts/hello.py
  function __quick_unload_script (line 6) | def __quick_unload_script():

FILE: utils_impl.cpp
  type collect_extlangs (line 2) | struct collect_extlangs: extlang_visitor_t
    method visit_extlang (line 6) | virtual ssize_t idaapi visit_extlang(extlang_t *extlang) override
    method collect_extlangs (line 12) | collect_extlangs(extlangs_t *langs, bool select)
  function get_file_modification_time (line 22) | bool get_file_modification_time(
  function get_file_modification_time (line 36) | bool get_file_modification_time(
  function normalize_path_sep (line 44) | void normalize_path_sep(qstring &path)
  function make_abs_path (line 54) | void make_abs_path(qstring& path, const char* base_dir = nullptr, bool n...
  function get_basename_and_ext (line 76) | bool get_basename_and_ext(
  function get_current_directory (line 91) | inline void get_current_directory(qstring &dir)
  type std (line 99) | namespace std
    function regex_replace (line 102) | std::basic_string<CharT> regex_replace(BidirIt first, BidirIt last,
    function regex_replace (line 138) | std::string regex_replace(const std::string &s,
  function enumerate_files (line 146) | void enumerate_files(
Condensed preview — 73 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (130K chars).
[
  {
    "path": ".gitattributes",
    "chars": 7,
    "preview": "* -crlf"
  },
  {
    "path": ".github/workflows/build.yml",
    "chars": 3846,
    "preview": "name: build\n\non:\n  push:\n    branches: [\"**\"]\n    tags: [\"v*\", \"V*\"]\n  pull_request:\n    branches: [\"**\"]\n  workflow_dis"
  },
  {
    "path": ".gitignore",
    "chars": 127,
    "preview": ".vscode/\nobj/\n__pycache__/\nbuild*/\n.vs/\nDebug/\nRelease/\nx64/\nBINARIES/\n.claude/settings.local.json\n.claude/agents/ida-cm"
  },
  {
    "path": "CLAUDE.md",
    "chars": 3754,
    "preview": "# 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"
  },
  {
    "path": "CMakeLists.txt",
    "chars": 1685,
    "preview": "cmake_minimum_required(VERSION 3.27)\r\nproject(qscripts)\r\n\r\nset(CMAKE_CXX_STANDARD 20)\r\nset(CMAKE_CXX_STANDARD_REQUIRED O"
  },
  {
    "path": "LICENSE",
    "chars": 1073,
    "preview": "MIT License\n\nCopyright (c) 2019 Elias Bachaalany\n\nPermission is hereby granted, free of charge, to any person obtaining "
  },
  {
    "path": "README.md",
    "chars": 14879,
    "preview": "# What is QScripts?\n\nQScripts is productivity tool and an alternative to IDA's \"Recent scripts\" (Alt-F9) and \"Execute Sc"
  },
  {
    "path": "TODO.md",
    "chars": 726,
    "preview": "# Ideas\r\n\r\n## Allow dot folders\r\n\r\n- Check for $(pwd)/.qscripts/<sourcefilename.depsfiles> first before checking the cur"
  },
  {
    "path": "ida-plugin.json",
    "chars": 794,
    "preview": "{\r\n  \"IDAMetadataDescriptorVersion\": 1,\r\n  \"plugin\": {\r\n    \"name\": \"QScripts\",\r\n    \"version\": \"1.2.6\",\r\n    \"entryPoin"
  },
  {
    "path": "ida.h",
    "chars": 387,
    "preview": "#pragma once\n\n#pragma warning(push)\n#pragma warning(disable: 4267 4244 4146)\n#include <loader.hpp>\n#include <idp.hpp>\n#i"
  },
  {
    "path": "prep-cmake.bat",
    "chars": 509,
    "preview": "@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 varia"
  },
  {
    "path": "qscripts.cpp",
    "chars": 44333,
    "preview": "/*\nQuick execute script: a plugin to speedup IDA scripts development.\n\nThis plugin replaces the regular \"Recent scripts\""
  },
  {
    "path": "script.hpp",
    "chars": 5756,
    "preview": "#pragma once\r\n\r\n#define QSCRIPTS_LOCAL \".qscripts\"\r\nstatic constexpr char UNLOAD_SCRIPT_FUNC_NAME[] = \"__quick_unload_sc"
  },
  {
    "path": "snippet_manager/README.md",
    "chars": 1310,
    "preview": "# Snippet Manager Plugin for IDA\n\nThis plugin for IDA provides a set of functionality for importing, exporting or deleti"
  },
  {
    "path": "snippet_manager/pseudocode.cpp",
    "chars": 712,
    "preview": "// The following snippet / pseudo-code has been kindly shared by  Arnaud Diederen from Hex-Rays\r\n\r\n////\r\n//// save snipp"
  },
  {
    "path": "snippet_manager/snippetmanager.py",
    "chars": 8805,
    "preview": "\"\"\"\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 sn"
  },
  {
    "path": "test_addons/README.md",
    "chars": 2197,
    "preview": "# QScripts Test Add-ons\r\n\r\nQScripts streamlines IDA Pro add-on development by automatically reloading plugins, loaders, "
  },
  {
    "path": "test_addons/loader_template/.gitignore",
    "chars": 7,
    "preview": "build*/"
  },
  {
    "path": "test_addons/loader_template/CMakeLists.txt",
    "chars": 392,
    "preview": "cmake_minimum_required(VERSION 3.27)\r\nproject(qscripts_native)\r\n\r\nset(CMAKE_CXX_STANDARD 17)\r\nset(CMAKE_CXX_STANDARD_REQ"
  },
  {
    "path": "test_addons/loader_template/driver.cpp",
    "chars": 528,
    "preview": "#include \"idasdk.h\"\n\nextern bool main();\n\n//--------------------------------------------------------------------------\ns"
  },
  {
    "path": "test_addons/loader_template/idasdk.h",
    "chars": 285,
    "preview": "#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.h"
  },
  {
    "path": "test_addons/loader_template/main.cpp",
    "chars": 2545,
    "preview": "#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   "
  },
  {
    "path": "test_addons/plugin_template/.claude/agents/ida-cmake.md",
    "chars": 2803,
    "preview": "---\r\nname: ida-cmake\r\ndescription: Use this agent to compile/build IDA Pro extensions including plugins, idalib applicat"
  },
  {
    "path": "test_addons/plugin_template/.gitignore",
    "chars": 7,
    "preview": "build*/"
  },
  {
    "path": "test_addons/plugin_template/CLAUDE.md",
    "chars": 1814,
    "preview": "# 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"
  },
  {
    "path": "test_addons/plugin_template/CMakeLists.txt",
    "chars": 392,
    "preview": "cmake_minimum_required(VERSION 3.27)\r\nproject(qscripts_native)\r\n\r\nset(CMAKE_CXX_STANDARD 17)\r\nset(CMAKE_CXX_STANDARD_REQ"
  },
  {
    "path": "test_addons/plugin_template/driver.cpp",
    "chars": 541,
    "preview": "#include \"idasdk.h\"\n\nextern bool main(size_t);\n\n//----------------------------------------------------------------------"
  },
  {
    "path": "test_addons/plugin_template/idasdk.h",
    "chars": 247,
    "preview": "#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.h"
  },
  {
    "path": "test_addons/plugin_template/main.cpp",
    "chars": 448,
    "preview": "#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 ="
  },
  {
    "path": "test_addons/plugin_triton/.gitignore",
    "chars": 66,
    "preview": "build*/\r\n.z3-trace\r\njunk64.bin.id?\r\njunk64.bin.til\r\njunk64.bin.nam"
  },
  {
    "path": "test_addons/plugin_triton/CMakeLists.txt",
    "chars": 1353,
    "preview": "cmake_minimum_required(VERSION 3.27)\r\nproject(qscripts_native_triton)\r\n\r\n# Make sure 'tritonenv.bat' script has been cal"
  },
  {
    "path": "test_addons/plugin_triton/README.md",
    "chars": 1869,
    "preview": "# Triton Plugin for IDA Pro with QScripts\r\n\r\nA native IDA Pro plugin that integrates the [Triton](https://github.com/Jon"
  },
  {
    "path": "test_addons/plugin_triton/driver.cpp",
    "chars": 574,
    "preview": "#include \"idasdk.h\"\n\nextern bool main(size_t);\n\n//----------------------------------------------------------------------"
  },
  {
    "path": "test_addons/plugin_triton/idasdk.h",
    "chars": 280,
    "preview": "#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#incl"
  },
  {
    "path": "test_addons/plugin_triton/main.cpp",
    "chars": 1573,
    "preview": "#include \"idasdk.h\"\r\n\r\n#include <sstream>\r\n#include <triton/context.hpp>\r\n#include <triton/basicBlock.hpp>\r\n#include <tr"
  },
  {
    "path": "test_addons/plugin_triton/qscripts_native_triton.py",
    "chars": 299,
    "preview": "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 "
  },
  {
    "path": "test_addons/plugin_triton/qscripts_native_triton.py.deps.qscripts",
    "chars": 71,
    "preview": "/triggerfile /keep $env:IDASDK$\\bin\\plugins\\qscripts_native_triton$ext$"
  },
  {
    "path": "test_addons/qscripts_native.py",
    "chars": 292,
    "preview": "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 "
  },
  {
    "path": "test_addons/qscripts_native.py.deps.qscripts",
    "chars": 64,
    "preview": "/triggerfile /keep $env:IDASDK$\\bin\\plugins\\qscripts_native$ext$"
  },
  {
    "path": "test_scripts/dependency-test/README.md",
    "chars": 700,
    "preview": "This is a dependency test folder.\r\n\r\nThe script `t1.py` has its dependency index file which describes the reload directi"
  },
  {
    "path": "test_scripts/dependency-test/subdir/t4.py",
    "chars": 79,
    "preview": "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",
    "chars": 5,
    "preview": "t5.py"
  },
  {
    "path": "test_scripts/dependency-test/subdir/t5.py",
    "chars": 79,
    "preview": "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",
    "chars": 325,
    "preview": "import sys\r\n\r\nsubdir = os.path.join(os.path.dirname(__file__), 'subdir')\r\nif subdir not in sys.path:\r\n    print(\"-->addi"
  },
  {
    "path": "test_scripts/dependency-test/t1.py.deps.qscripts",
    "chars": 60,
    "preview": "/reload import importlib;importlib.reload($basename$)\r\nt2.py"
  },
  {
    "path": "test_scripts/dependency-test/t2.py",
    "chars": 83,
    "preview": "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",
    "chars": 19,
    "preview": "t3.py\r\nsubdir/t4.py"
  },
  {
    "path": "test_scripts/dependency-test/t3.py",
    "chars": 76,
    "preview": "print(\"t3., This is %s\" % __file__)\r\ndef f3():\r\n    print(\"t3.f3() called!\")"
  },
  {
    "path": "test_scripts/hello.idc",
    "chars": 150,
    "preview": "#include <idc.idc>\n\nstatic main()\n{\n    Message(\"Welcome to QScripts!\\n\");\n}\n\nstatic __fast_unload_script()\n{\n    Messag"
  },
  {
    "path": "test_scripts/hello.py",
    "chars": 147,
    "preview": "try:\n    var1 += 1\nexcept:\n    var1 = 1\n\ndef __quick_unload_script():\n    print(\"Unloaded: %s\" % str(var1))\n\nprint(f\"Jus"
  },
  {
    "path": "test_scripts/notebooks/0000 Imports and Init.py",
    "chars": 155,
    "preview": "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("
  },
  {
    "path": "test_scripts/notebooks/0000 Imports and Init.py.deps.qscripts",
    "chars": 177,
    "preview": "# A simple notebook\r\n/notebook Test notebook\r\n# On activation, just execute the main script\r\n/notebook.activate exec_mai"
  },
  {
    "path": "test_scripts/notebooks/0010 List functions.py",
    "chars": 188,
    "preview": "# Enumerating functions\r\n\r\nif idaapi.get_func_qty() != 0:\r\n    for func_ea in idautils.Functions():\r\n        print(f\"Fun"
  },
  {
    "path": "test_scripts/notebooks/0020 List segments.py",
    "chars": 248,
    "preview": "# Enumerating segments\r\n\r\nif idaapi.get_segm_qty() > 0:\r\n    for seg_ea in idautils.Segments():\r\n        seg = idaapi.ge"
  },
  {
    "path": "test_scripts/notebooks/0030 Show info.py",
    "chars": 110,
    "preview": "# 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",
    "chars": 61,
    "preview": "# Rename setuff\r\nidaapi.set_name(idc.here(), \"my_function\")\r\n"
  },
  {
    "path": "test_scripts/notebooks/0050 Set name.py",
    "chars": 97,
    "preview": "# 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",
    "chars": 165,
    "preview": "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\npr"
  },
  {
    "path": "test_scripts/notebooks/A0000 Init.py.deps.qscripts",
    "chars": 185,
    "preview": "/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 not"
  },
  {
    "path": "test_scripts/notebooks/A0010 Cell 1.py",
    "chars": 27,
    "preview": "print('This is Cell A.1')\r\n"
  },
  {
    "path": "test_scripts/notebooks/A0020 Cell 2.py",
    "chars": 25,
    "preview": "print('This is Cell A.2')"
  },
  {
    "path": "test_scripts/notebooks/B0000 Init.py",
    "chars": 163,
    "preview": "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\npr"
  },
  {
    "path": "test_scripts/notebooks/B0000 Init.py.deps.qscripts",
    "chars": 147,
    "preview": "/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/"
  },
  {
    "path": "test_scripts/notebooks/B0010 Cell 1.py",
    "chars": 25,
    "preview": "print('This is Cell B.1')"
  },
  {
    "path": "test_scripts/notebooks/B0020 Cell 2.py",
    "chars": 25,
    "preview": "print('This is Cell B.2')"
  },
  {
    "path": "test_scripts/notebooks/README.md",
    "chars": 1306,
    "preview": "# Notebook dependency example\r\n\r\n## Quick start\r\n\r\nTo define a notebook, just add the `/notebook [title]` directive as s"
  },
  {
    "path": "test_scripts/pkg-dependency/README.md",
    "chars": 1340,
    "preview": "# Package Dependency Example\r\n\r\nIn this example, we're dealing with a dependency file for a package that's currently bei"
  },
  {
    "path": "test_scripts/pkg-dependency/example.py.deps.qscripts",
    "chars": 335,
    "preview": "# Package dependencies (here, we use the environment variable 'IDAPYX' that contains the package path\r\n/pkgbase $env:ida"
  },
  {
    "path": "test_scripts/trigger-file/dep.py",
    "chars": 193,
    "preview": "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 thi"
  },
  {
    "path": "test_scripts/trigger-file/trigger.py",
    "chars": 248,
    "preview": "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}: "
  },
  {
    "path": "test_scripts/trigger-file/trigger.py.deps.qscripts",
    "chars": 89,
    "preview": "/reload import importlib;importlib.reload($basename$);\r\n/triggerfile createme.tmp\r\ndep.py"
  },
  {
    "path": "utils_impl.cpp",
    "chars": 4652,
    "preview": "//-------------------------------------------------------------------------\nstruct collect_extlangs: extlang_visitor_t\n{"
  }
]

// ... and 1 more files (download for full content)

About this extraction

This page contains the full source code of the 0xeb/ida-qscripts GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 73 files (116.2 KB), approximately 29.4k tokens, and a symbol index with 121 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!