Plugin tutorials
Hands-on walkthroughs for building a Peach Commander plugin. They assume you have
read the SDK overview. All paths are relative to a checkout of
the repository; the SDK headers live in Plugins/SDK/ and the bundled sample
plugins in Plugins/ are working references.
1. Your first plugin — a command¶
The smallest useful plugin adds a command that appears in a menu and does
something with the selected file. This uses only the contributions ABI, so it works
regardless of plugin type; declare the type as ptx (a pure tool/action plugin).
a. The manifest (Contents/Info.plist):
<key>PCPluginType</key> <string>ptx</string>
<key>PCPluginName</key> <string>Word Count</string>
<key>PCPluginAPIVersion</key> <integer>1</integer>
<key>PCContributions</key>
<dict>
<key>commands</key>
<array>
<dict>
<key>id</key><string>plugin.wordcount.count</string>
<key>title</key><string>Count Words</string>
<key>needsLocalPath</key><true/>
</dict>
</array>
<key>contextMenus</key>
<array>
<dict>
<key>command</key><string>plugin.wordcount.count</string>
<key>surface</key><string>panel.item</string>
<key>when</key><string>cursorPath</string>
</dict>
</array>
</dict>
b. The behavior (wordcount.swift):
import CPeachCommanderPlugin
import Foundation
@_cdecl("PcGetApiVersion")
public func PcGetApiVersion() -> Int32 { 1 }
@_cdecl("PcRunCommand")
public func PcRunCommand(_ commandId: UnsafePointer<CChar>?,
_ services: UnsafePointer<PcHostServices>?) {
guard let services = services?.pointee, let host = services.host else { return }
var buf = [CChar](repeating: 0, count: 4096)
guard services.localCursorPath?(host, &buf, 4096) == 1 else { return }
let path = String(cString: buf)
let words = (try? String(contentsOfFile: path, encoding: .utf8))?
.split(whereSeparator: { $0.isWhitespace }).count ?? 0
services.presentInfo?(host, "Word Count", "\(words) words in \(path)")
}
needsLocalPath + localCursorPath give you a real on-disk path even when the
cursor is inside an archive or on a remote file system (the host materializes it to
a temp file). presentInfo shows a native alert.
2. Add a menu item, toolbar button, or keybinding¶
You already added a context-menu item above. To also put the command in the menu
bar, in a container, or on a key, add more entries to PCContributions:
<key>menus</key>
<array>
<dict>
<key>command</key><string>plugin.wordcount.count</string>
<key>menu</key><string>Commands</string>
<key>group</key><string>9_plugins</string>
</dict>
</array>
<key>keybindings</key>
<array>
<dict>
<key>command</key><string>plugin.wordcount.count</string>
<key>key</key><string>cmd+shift+w</string>
</dict>
</array>
Button-bar buttons run any command by id, so users can drop your command onto the button bar themselves — nothing extra is required from the plugin.
3. Add a panel or sidebar view¶
Declare a views contribution and implement PcMakeView, returning an NSView*.
The Treemap ("Disk Map") plugin is a full example: it declares a sidebar view and
reads its root folder from getContext("sidebarViewRoot").
@_cdecl("PcMakeView")
public func PcMakeView(_ viewId: UnsafePointer<CChar>?,
_ containerId: UnsafePointer<CChar>?,
_ services: UnsafePointer<PcHostServices>?) -> UnsafeMutableRawPointer? {
let view = MyView(frame: .zero) // an NSView subclass
return Unmanaged.passRetained(view).toOpaque()
}
The host calls PcCloseView when it tears the view down; balance your retain there.
4. Access selected files and run operations¶
- Cursor file:
services.cursorPath(display path) orlocalCursorPath(on disk). - Selection:
services.selectionCount+services.selectionPath(host, i, …). - Act on files through the host op engine so it participates in undo/progress:
services.moveToTrash,services.deletePermanently, thenservices.reloadActivePanel. Navigate withopenPath/openPathInPanel.
For a file-system plugin (PFX) or a packer (PCX), you implement the file-op ABI instead — see the API reference and the SampleFS / SamplePacker plugins.
5. Settings, events, and logging¶
- Settings — resolve your config directory from
-ConfigRoot/PEACHCMD_CONFIG_ROOT(see the architecture guide) and store JSON there. Never useUserDefaults. - Events — the host pushes context changes to an embedded view via
PcNotifyView(view, key, value); read live context withgetContext. - Logging — use
os.Logger/NSLog; keep a plugin quiet by default. The bundled Log Viewer plugin is a good pattern for a tool window with its own menus.
6. Localize¶
Add Resources/en.lproj/Localizable.strings and translations, and look strings up
through your own bundle:
// PluginLoc.swift (copy from Plugins/SDK/)
let title = L("Count Words") // English literal is the key; falls back if untranslated
Do not call NSLocalizedString — it resolves in the host bundle, not yours.
7. Package the plugin¶
Build a .<type>plugin bundle whose Contents/MacOS/<name> matches the bundle
name. The Tools/build-*-plugin.sh scripts show the exact swiftc/clang
invocations, Info.plist, and .lproj copying used for the bundled plugins — copy
one as a template. Ship the bundle as-is, or zip it with a pluginst.inf so users
can install it from Configuration ▸ Plugins.
8. Test the plugin¶
- Headless — the host's test suite loads plugins through
PCPluginHost; the SampleFS / SampleLister / SamplePacker plugins expose test hooks and are driven byPCPluginHostTests. Model your own tests on those. - In the app — drop the bundle in the plugins folder, enable it in
Configuration ▸ Plugins, and drive the app with the DEBUG
-AutomationScripthook (or the VM screenshot harness) for repeatable checks. - Because plugins are crash-guarded, a bug shows up as a quarantined plugin rather than an app crash — check the logs to see why it was quarantined.
9. Publish¶
Distribute the .zip (bundle + optional pluginst.inf) however you like — there
is no central registry yet. Users install it via Configuration ▸ Plugins. State
your required PCPluginAPIVersion so users on an older host get a clear message.
Porting a Total Commander plugin¶
WCX/WFX/WLX/WDX plugins map directly onto PCX/PFX/PLX/PDX. Plugins/SDK/PORTING.md
is a full walkthrough: the type and entry-point maps, replacing Win32 types and
time formats, and a worked example. In most cases you keep your logic and swap the
header and a handful of signatures.