Plugins

This conceptual guide covers plugin packaging and integration. For signatures generated directly from the XML-documented contracts, see the Plugin API Reference.

Built-in Features

wtmux includes built-in equivalents of popular tmux plugins:

  • Prefix highlight (#{prefix_highlight}) — Shows an inverse-video indicator when the prefix key is active
  • Session save/restore — Save and restore window/pane layouts (tmux-resurrect equivalent)
  • Conditional format (#{?client_prefix,yes,no}) — Conditional text based on terminal state

COM Plugin System

External plugins use COM interfaces and MSIX AppExtensions for a fully AOT-compatible, language-agnostic plugin architecture. Plugins are discovered automatically when installed as MSIX packages that declare the com.wtmux.plugin extension contract.

Plugin Package Requirements

  1. Implement the IWtmuxComPlugin COM interface ({3A2F8E01-7B4C-4D5A-9E1F-0A2B3C4D5E01})
  2. Register as a COM server in the package manifest
  3. Declare the com.wtmux.plugin AppExtension with a CLSID property

MSIX Manifest Example

<Extensions>
  <!-- Register the COM server -->
  <com:Extension Category="windows.comServer">
    <com:ComServer>
      <com:ExeServer Executable="MyPlugin.exe">
        <com:Class Id="{YOUR-PLUGIN-CLSID}" />
      </com:ExeServer>
    </com:ComServer>
  </com:Extension>

  <!-- Declare as a wtmux plugin extension -->
  <uap3:Extension Category="windows.appExtension">
    <uap3:AppExtension Name="com.wtmux.plugin"
                        DisplayName="My Plugin"
                        Description="Description of my plugin"
                        PublicFolder="Public">
      <uap3:Properties>
        <CLSID>{YOUR-PLUGIN-CLSID}</CLSID>
      </uap3:Properties>
    </uap3:AppExtension>
  </uap3:Extension>
</Extensions>

COM Interface (C#)

The plugin contract consists of two interfaces. Declare them with the same GUIDs (until a contract package is available):

using System.Runtime.InteropServices;
using System.Runtime.InteropServices.Marshalling;

[GeneratedComInterface(StringMarshalling = StringMarshalling.Utf16)]
[Guid("3A2F8E01-7B4C-4D5A-9E1F-0A2B3C4D5E01")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public partial interface IWtmuxComPlugin
{
    void GetName(out string name);
    void Initialize(IWtmuxComPluginHost host);
    void OnSessionCreated(string sessionName, int sessionId);
    void OnSessionDestroyed(string sessionName, int sessionId);
    void ExpandVariable(string variableName, string sessionName,
        int windowIndex, string windowName, int paneIndex,
        bool prefixActive, out string result);
    void Shutdown();
}

[GeneratedComInterface(StringMarshalling = StringMarshalling.Utf16)]
[Guid("3A2F8E01-7B4C-4D5A-9E1F-0A2B3C4D5E02")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public partial interface IWtmuxComPluginHost
{
    void RegisterFormatVariable(string name);
    void BindKey(byte key, string command);
    void UnbindKey(byte key);
    void SetOption(string option, string value);
    void Log(string message);
}

// Additive v2 interface for mouse and root-table (no-prefix) key bindings.
// Query the host for it; the original IWtmuxComPluginHost is unchanged.
[GeneratedComInterface(StringMarshalling = StringMarshalling.Utf16)]
[Guid("3A2F8E01-7B4C-4D5A-9E1F-0A2B3C4D5E03")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public partial interface IWtmuxComPluginHost2
{
    void BindMouse(string mouseKey, string command);   // e.g. "WheelUpPane", "MouseDown1Pane"
    void UnbindMouse(string mouseKey);
    void BindRootKey(byte key, string command);         // no-prefix keyboard binding
    void UnbindRootKey(byte key);
}

// Additive v3 interface for running wtmux commands from a plugin.
// Query the host for it; the earlier host interfaces are unchanged.
[GeneratedComInterface(StringMarshalling = StringMarshalling.Utf16)]
[Guid("3A2F8E01-7B4C-4D5A-9E1F-0A2B3C4D5E04")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public partial interface IWtmuxComPluginHost3
{
    // Runs a wtmux command in the server's global context (use -t for targeting).
    void RunCommand(string command, out string output, out bool success);
}

To bind mouse actions or no-prefix keys, cast (query) the host to IWtmuxComPluginHost2:

public void Initialize(IWtmuxComPluginHost host)
{
    host.BindKey((byte)'T', "new-window");
    if (host is IWtmuxComPluginHost2 host2)
    {
        host2.BindMouse("MouseDown2Pane", "new-window"); // middle-click opens a window
        host2.BindRootKey((byte)0x0F, "copy-mode");       // Ctrl+O (no prefix)
    }
}

To run wtmux commands, cast (query) the host to IWtmuxComPluginHost3:

public void Initialize(IWtmuxComPluginHost host)
{
    if (host is IWtmuxComPluginHost3 host3)
    {
        host3.RunCommand("set-option -g status-right \"#{my_var}\"", out var output, out var ok);
        if (!ok) host.Log($"command failed: {output}");
    }
}

Plugin-issued commands run in the server's global context — there is no attached client — so client-independent commands work: global option changes (set-option -g), config commands (source-file/reload-config), key bindings (bind/unbind), and queries (show-option). Commands that act on the current client/session (new-window, split-window, display-message, send-keys, …) are not supported from this context yet and return a failure. Avoid calling RunCommand reentrantly from inside a host event callback.

A minimal implementation:

[GeneratedComClass]
public partial class MyPlugin : IWtmuxComPlugin
{
    public void GetName(out string name) => name = "my-plugin";

    public void Initialize(IWtmuxComPluginHost host)
    {
        host.RegisterFormatVariable("my_var");
        host.BindKey((byte)'T', "new-window");
        host.Log("My plugin loaded!");
    }

    public void ExpandVariable(string variableName, string sessionName,
        int windowIndex, string windowName, int paneIndex,
        bool prefixActive, out string result)
    {
        result = variableName == "my_var"
            ? (prefixActive ? "[PREFIX]" : "[NORMAL]")
            : "";
    }

    public void OnSessionCreated(string sessionName, int sessionId) { }
    public void OnSessionDestroyed(string sessionName, int sessionId) { }
    public void Shutdown() { }
}

Architecture Benefits

  • AOT-compatible — No reflection or assembly loading; uses source-generated COM interop
  • Language-agnostic — Plugins can be written in C#, C++, Rust, or any COM-capable language
  • Process isolation — Out-of-process COM servers prevent plugin crashes from affecting wtmux
  • Automatic discovery — MSIX AppExtensions handle plugin enumeration; no manual folder scanning
  • Secure — MSIX packages are signed and sandboxed
  • Store-distributable — Plugins can be published and updated via the Microsoft Store

Format Variable Context

When wtmux renders a plugin's #{variable}, it provides the context of the pane/window being rendered. Two levels are available:

  • IWtmuxComPlugin.ExpandVariable receives the session name, window index/name, pane index, and prefix state.
  • IWtmuxComPlugin2.ExpandVariable2 (additive) receives the full context — window flags, pane title, pane pid, pane current path, session window count, window activity, and host name — so a plugin can build real status segments (a git indicator from paneCurrentPath, a process indicator from panePid/paneTitle, and so on). Implement IWtmuxComPlugin2 in addition to IWtmuxComPlugin; the host calls ExpandVariable2 when present and falls back to ExpandVariable otherwise, so plugins that implement only IWtmuxComPlugin keep working.
[GeneratedComInterface(StringMarshalling = StringMarshalling.Utf16)]
[Guid("3A2F8E01-7B4C-4D5A-9E1F-0A2B3C4D5E05")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public partial interface IWtmuxComPlugin2
{
    void ExpandVariable2(string variableName, string sessionName, int windowIndex,
        string windowName, string windowFlags, int paneIndex, string paneTitle, int panePid,
        string paneCurrentPath, int sessionWindows, string windowActivity, string host,
        bool prefixActive, out string result);
}

In-process built-in providers registered via RegisterFormatVariable(name, ctx => …) receive the same enriched FormatContext (with PaneCurrentPath, PanePid, PaneTitle, WindowFlags, SessionWindows, WindowActivity, and Host).

Plugin Configuration (@ User Options)

Plugins are configured through tmux-style @ user options. A user sets, for example, set -g @my_plugin_theme dark in their config (or wtmux set-option -g @my_plugin_theme dark at runtime). A plugin reads its value through the in-process host's GetUserOption("@my_plugin_theme"), from a format string as #{@my_plugin_theme}, and can write one with SetOption("@my_plugin_theme", "dark"). See the user options section of the configuration reference.

PowerShell Config Scripts

Use run-shell in your config to execute PowerShell scripts that output wtmux commands:

# In ~/.wtmux.conf
run 'Write-Output "set -g status-right $(Get-Date -Format HH:mm)"'