Skip to content

Introduce plugin system support (done right)#8

Open
Toni500github wants to merge 40 commits into
mainfrom
plugins
Open

Introduce plugin system support (done right)#8
Toni500github wants to merge 40 commits into
mainfrom
plugins

Conversation

@Toni500github

@Toni500github Toni500github commented Jul 15, 2026

Copy link
Copy Markdown
Owner

oshot started as a straightforward screenshot & annotation tool with a systray icon and an OCR pipeline bolted on for pulling text out of a selection. That already covers a lot of ground, but the moment you want oshot to do something it wasn't built for (a different OCR backend, a custom exporter, whatever), you're stuck forking the whole app just to scratch your own itch.

So here we are: plugins.

The idea is simple: give oshot a stable C ABI that anyone can link against, and let plugins hook into the parts of the app that actually matter - the init/render/destroy lifecycle, config and cache storage, logging, and (for OCR-adjacent plugins) the on_ocr_done result callback.

C ABI specifically, not C++: no name mangling, no vtable layout or STL ABI to keep in lockstep with whatever compiler/standard-library version oshot itself was built with, and no risk of a plugin silently breaking because it was built against a slightly different libstdc++/MSVC runtime. Practically, that also means plugins aren't locked into C++ at all, thus anything that can export a C-compatible interface (C/C++, Rust, Zig, even Python via ctypes/cffi) can load into oshot the same way, which is the whole point since the goal is to let people extend the app in whatever language they're comfortable with. Each plugin gets its own config.toml, backed by the same TomlAPI base oshot already uses for its own Config/Cache, so nobody has to hand-roll a bespoke serialization format per plugin, unless they want to use a different configuration and similair, they are free to do so completely.
cimgui (a C-ABI wrapper for ImGui) got pulled in specifically so plugin UI code isn't stuck writing raw ImGui calls from C++ across the ABI boundary.

There's also a managing window now, so plugins can be toggled on/off without hand-editing a config file, and each plugin can register its own tab in the preferences window.


This is still WIP. The plugin API surface (oshot_plugin.h) isn't frozen yet and will keep shifting until there's an actual manifest standard behind it. Right now "does this thing even have a unique id/name" is enforced at load time and not a whole lot else.

TODO:

  • First working end-to-end plugin load path
  • Per-plugin runtime (kill the g_api singleton misattribution bug)
  • Config/cache getter + setter API for plugins
  • Preferences tab support for plugins
  • Plugin managing window (enable/disable)
  • Proper manifest standard
  • Freeze/stabilize the plugin ABI

Un-related commits that should be in main

  • 737ddad tool: fix regression about OCR model selection
  • 79c3ba5 tool: improve a bit the scan error UX
  • b2a7df0 tool: don't scan OCR path while editing
  • 887010f build: fix incremental rebuilds from generated version header
  • c1739d4 disable asan for the sake of macos+windows builds
  • 836be33 fix windows build, but macos packaging is screwed
  • 1b5e2f8 annotation(menu): separate color picker settings into preferences
  • 184b764 build: separate oshot common code into its own liboshot_common.a
  • 020de27 build: try to fix macos/windows/debian package builds
  • 26c7ed6 misc: suppress some dialogs to just logs
  • 1735fff tool: ditch booleans states for GeneralContext+enums
  • ba89d2d build: replace hand-rolled Makefile with a thin CMake wrapper
  • 7d98b01 macos: fix spam of libimgui.1.dylib not being found when bundling the app
  • 6d0dd60 make: refactor src/libs/*/Makefile's to use common.mk
  • a937f80 misc: fix compilation from obselete functions
  • 9b93d12 libs: updated fmt, toml++, spdlog, imgui, stb

Toni500github and others added 30 commits June 21, 2026 17:19
The old Makefile and mk/common.mk rules had no header dependency
tracking (no -MMD/-MP), so editing imgui.h during the 1.92.9 WIP
migration didn't trigger a rebuild of liboshot_imgui.so. The result
was a stale .so linked against fresh app code, with mismatched
ImGuiIO layouts between the two. This is what IMGUI_CHECKVERSION()
caught as an assertion failure in debug builds, and very likely also
explains the earlier "toolbar click resets selection" bug, since
ui_blocks_selection()/HandleSelectionInput() are unchanged between
the working and broken commits.

CMakeLists.txt already builds the same targets with proper compiler-
driven dependency tracking and never showed either symptom. All CI
already builds via CMake, so there's no remaining reason to keep a
second, manually-synced build description around.

Replace the root Makefile with a thin wrapper around `cmake --build`,
keeping the existing CLI (make, clean, distclean, dist,
DEBUG=0/1 -> build/release or build/debug). mk/common.mk and the
per-lib Makefiles under src/libs/ are now deleted.

Keeping it on a separate commit in case I want to roll this back.
g_api was a single global struct, so with more than one plugin loaded,
config reads/writes and log lines could get misattributed to whichever
plugin last touched it.

Replace it with plugin_runtime_t, one instance per loaded plugin, keyed
by a unique id in g_plugins. ScopedActivePlugin (RAII) sets
g_current_plugin around every call into plugin code (init, render,
on_ocr_done, destroy), so host-side oshot_* calls always resolve
against the correct plugin, including reentrant calls.

Also:
- oshot_get_abi_version() now returns the host's constant instead of
  echoing back the active plugin's own version
- config lookups now key off plugin id (unique) instead of display
  name (not guaranteed unique), fixing a settings collision between
  same-named plugins; ImGui PushID switched to id for the same reason
- plugin id/name are validated at load time; duplicate ids are
  rejected instead of silently overwriting the existing entry
- init() now runs after the runtime entry exists and is scoped active,
  so config reads during init() see correct values
- on_ocr_done now receives a real result struct (text, confidence,
  psm) instead of requiring a re-fetch through the text-buffer side
  channel; allocated once per call and freed once, not leaked per plugin
- plugin_entry_t / g_plugin_entries removed; shutdown destroy() now
  happens via plugin_runtime_t's destructor
also separate some plugins code away from screenshot_tool.cpp to plugins.cpp
let's avoid annoying the user sometimes
hope this fixes all macos and windows builds 😭
finally pushed this damn commit :wilted_rose:
Each plugin now gets its own TomlAPI-backed config.toml,
loaded at plugin load and persisted via the new SaveFile()
after on_save_preferences commits changes.
Plugins can still manage their own file/format via oshot_get_plugin_data_dir().
also make `mk_cache_entries` constexpr and array for, "perfomances"
Avoid full rebuilds by preserving the generated version header when
unchanged and fix its CMake dependency tracking.
it was expensive plus forgot to check if the dir existed so it was kinda crashing
this kinda sucks and needs to be created a manifest standard ASAP
from sizeof(oshot_plugin_t) == 88 to 80
also don't bleed the combo list with error color
Ported from cufetchpm (customfetch) because apparently
reinventing a package manager for every project I write is now a
personality trait. And because starting a new codebase sucks ass.

Adds a new oshotpm/ subtree with a shared lib (oshot_plugman) and
its executable (oshotpm), wired into CMakeLists.txt behind the
existing DISABLE_PLUGINS switch.

Manifest format (oshot-plugin.toml) so far:
  - repo: name, git url, tracked git-hash
  - plugins[]: name, id, description, output_dir, licenses,
    authors, build_steps, platforms
  - top-level dependencies list (declared, not consumed yet)

Management commands wired up in main.cpp:
  install, update, list [-v], enable/disable, uninstall,
  gen-manifest, help. Install accepts either a repo url/path
  (clone + build) or a local directory already on disk.

State tracking:
  - StateManager persists installed repos/plugins to an
    auto-generated TOML state file
  - enable/disable works by renaming built libraries to/from a
    .disabled suffix, which is delightfully hacky and also fine

Still missing / rough edges:
  - is_valid_name() is referenced in manifest.hpp comments but
    doesn't exist yet, so right now any name goes
  - the `dependencies` field in manifest_t is parsed into nothing,
    it's aspirational at best
  - no actual plugin conflict resolution beyond a boolean check
  - no signature/checksum verification on downloaded repos, so
    trust your sources
  - gen-manifest just dumps a canned template, no interactive mode

Also, unrelated drive-by cleanup:
  - CMakeLists: -pedantic -> -Wpedantic, one-line generate_version
    target because vertical space is precious
  - moved the giant default config/theme TOML strings out of
    config.hpp into texts.hpp, config.hpp was starting to look
    like a novel
  - screenshot_tool.cpp: actually check the Result from
    CopyText() instead of silently launching it into the void
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant