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 - Conditional format (
#{?client_prefix,yes,no}) — Conditional text based on terminal state
Session save/restore is not currently exposed as a command or key binding; it is not a shipped tmux-resurrect equivalent.
COM Plugin System
External plugins use COM interfaces and an MSIX AppExtension contract for an
AOT-compatible, language-agnostic plugin architecture. Automatic discovery of
installed com.wtmux.plugin extensions is not yet enabled in server startup;
installing a plugin package alone does not currently load it. The contracts and
packaging examples below describe the integration surface, not a working
automatic installation-to-initialization path.
A packaged server can retain its package identity without discovering extensions. Discovery and plugin lifecycle support are separate from server launch identity.
Plugin Package Requirements
- Implement the
IWtmuxComPluginCOM interface ({3A2F8E01-7B4C-4D5A-9E1F-0A2B3C4D5E01}) - Register as a COM server in the package manifest
- Declare the
com.wtmux.pluginAppExtension with aCLSIDproperty
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 has two base interfaces plus additive host/plugin interfaces for newer capabilities. Declare them with the same GUIDs — this is manual today; a Shmuelie.WindowsTmux.Plugin package that supplies them for you is planned but not yet published:
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 supported client-independent command in the server's global context.
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
- Hosting choice — COM supports an in-process DLL or an out-of-process EXE. An EXE separates the plugin's process from wtmux, but COM failures and blocking calls still need error handling.
- Discovery contract — MSIX AppExtensions describe installed plugins; automatic server-startup discovery is planned
- Package identity — MSIX provides signed package identity; it does not by itself sandbox a full-trust COM plugin. Install only plugins you trust.
- 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.ExpandVariablereceives 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 frompaneCurrentPath, a process indicator frompanePid/paneTitle, and so on). ImplementIWtmuxComPlugin2in addition toIWtmuxComPlugin; the host callsExpandVariable2when present and falls back toExpandVariableotherwise, so plugins that implement onlyIWtmuxComPluginkeep 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)"'