Skip to content

Introduce plugin system support (done right)#8

Open
Toni500github wants to merge 42 commits into
mainfrom
plugins
Open

Introduce plugin system support (done right)#8
Toni500github wants to merge 42 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
Refactor plugin manager into composable services, wire it into oshot GUI.
This shi took for real 5 looong days to rewritte :wilted_rose:

refactor(oshotpm):
- Split PluginManager into GitClient (git subprocess calls), PluginBuilder
  (platform check + conflict check + build_steps), PluginInstaller (moves
  built libraries into config dir), and PluginManager as a thin orchestrator
  that composes them. BuildPlugins is now a linear TRY() chain through
  ConfirmTrustDisclaimer, ConfirmDependencies, BuildAllPlugins,
  FinalizeRepoDirectory, InstallAllPlugins instead of one long function.
- Replace free-standing success()/status() CLI printers and the
  cli_only_logging flag with a PluginCallbacks struct (on_status, on_success,
  on_warning, on_error, on_info, confirm). PluginManager and its helpers no
  longer know whether they're driven by a CLI or a GUI; oshotpm/main.cpp
  supplies ANSI-colored terminal callbacks, oshot's ScreenshotTool will
  supply ImGui-backed ones.
- Add WorkingDirCleanupGuard to replace the repeated fs::remove_all(working_dir)
  calls on every early-return path in the old BuildPlugins.
- StateManager::insert_or_assign_at_plugin renamed to UpdatePlugin and now
  returns Result<> instead of calling die() on failure. SaveState() moved
  out of the header and made public so PluginManager controls when state
  is persisted.
- TomlAPI::GetValue (table+key overload) renamed to GetValueFromTable to
  stop colliding with the plain GetValue(key) overload.
- Rename oshot_plugman target to liboshotpm; build it from oshot_common
  directly instead of depending on oshot_plugin, and link the CLI against
  it PRIVATE. oshot_common switched from an OBJECT library to STATIC so it
  can be linked into both the main executable and liboshotpm without ODR
  duplication. toml++ and tinyfiledialogs pulled out into their own static
  library targets instead of being compiled straight into oshot_common.

feat(oshotpm):
- Add Manifest::IsValidID, separate from IsValidName, that allows dots so
  reverse-domain plugin IDs (e.g. dev.test-plugin) validate correctly.
  ParseManifest now rejects a plugin whose ID fails this check instead of
  silently accepting it.
- oshot's ScreenshotTool now owns a PluginManager and StateManager, and
  load_plugins() takes the resolved repo list from state.toml instead of
  blindly walking the plugins directory. Plugin discovery is now driven by
  the `libraries` array oshotpm writes per plugin, not directory_iterator
  guesswork.
- switch_plugin in oshotpm CLI can now enable/disable a plugin by ID alone
  (no repo/ prefix required), by searching every repository's plugin list;
  the per-library rename logic was pulled into switch_plugin_path so both
  code paths share it.
- CLI logging output goes through the new colored PluginCallbacks lambdas
  in main.cpp instead of nvdialog popups; nvd_set_error(NVD_NOT_INITIALIZED)
  is now called unconditionally so util.hpp's logging helpers never try to
  spawn a dialog from the CLI.

fix(oshotpm):
- UpdateRepos' recursive AddPluginRepo call now explicitly passes
  is_update=true instead of relying on a file-scope `static bool is_update`
  global, which meant plugins from a freshly re-cloned repo were incorrectly
  flagged as conflicting with the state entries that same repo already owned.
- TRY_MSG no longer double-wraps the error through fmt::format before
  returning Err(), which was mangling the `{}` placeholder for the inner
  error.
- StateManager's constructor now creates its parent config directory before
  checking whether state.toml exists, fixing first-run failures.
- Dropped the unused -D/--dialogs CLI flag and cli_only_logging option now
  that logging is routed through callbacks.
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