Scarab Plugin Development Quick Start
This guide will get you writing Scarab plugins in 5 minutes.
Prerequisites
- Scarab terminal emulator installed
- Basic F# syntax knowledge (or willingness to learn - it’s quite readable!)
- Text editor
Your First Plugin (3 Steps)
Step 1: Create the plugin file
Create ~/.config/scarab/plugins/my-first-plugin.fsx:
open Scarab.PluginApi
let metadata = {
Name = "my-first-plugin"
Version = "1.0.0"
Description = "My first Scarab plugin"
Author = "Your Name"
Homepage = None
ApiVersion = "0.1.0"
MinScarabVersion = "0.1.0"
}
let on_load (ctx: PluginContext) : Async<Result<unit, string>> =
async {
ctx.Log(LogLevel.Info, "Hello from my first plugin!")
return Ok ()
}
Plugin.Register {
Metadata = metadata
OnLoad = on_load
OnUnload = None
OnOutput = None
OnInput = None
OnResize = None
OnAttach = None
OnDetach = None
OnPreCommand = None
OnPostCommand = None
OnRemoteCommand = None
GetCommands = fun () -> []
}Step 2: Enable the plugin
Add to ~/.config/scarab/config.toml:
[plugins]
enabled = ["my-first-plugin.fsx"]Step 3: Test it
Start Scarab and check the logs - you should see “Hello from my first plugin!”
Common Plugin Patterns
Pattern 1: Intercept Output (Highlight Errors)
let on_output (line: string) (ctx: PluginContext) : Async<Result<Action, string>> =
async {
if line.Contains("ERROR") then
ctx.Log(LogLevel.Warn, sprintf "Found error: %s" line)
// Draw red indicator
let (_, y) = ctx.GetCursor()
ctx.QueueCommand(RemoteCommand.DrawOverlay {
Id = 1UL
X = 0us
Y = y
Text = "[!]"
Style = { Fg = 0xFFFFFFFFu; Bg = 0xFF0000FFu; ZIndex = 50.0f }
})
return Ok Action.Continue
}Pattern 2: Custom Keyboard Shortcut
let on_input (input: byte[]) (ctx: PluginContext) : Async<Result<Action, string>> =
async {
// Ctrl+X (0x18)
if input = [|0x18uy|] then
ctx.Notify("You pressed Ctrl+X!")
return Ok (Action.Modify [||]) // Consume the key
else
return Ok Action.Continue // Pass through
}Pattern 3: Track Command Duration
let mutable startTime = DateTime.Now
let on_pre_command (cmd: string) (ctx: PluginContext) : Async<Result<Action, string>> =
async {
startTime <- DateTime.Now
return Ok Action.Continue
}
let on_post_command (cmd: string) (exitCode: int) (ctx: PluginContext) : Async<Result<unit, string>> =
async {
let duration = DateTime.Now - startTime
if duration.TotalSeconds > 5.0 then
ctx.Notify(sprintf "Command took %.1f seconds" duration.TotalSeconds)
return Ok ()
}Pattern 4: Add Command Palette Entry
let getCommands () : ModalItem list =
[
{
Id = "my-plugin.action"
Label = "Do Something Cool"
Description = Some "Press this to trigger an action"
}
]
let on_remote_command (id: string) (ctx: PluginContext) : Async<Result<unit, string>> =
async {
if id = "my-plugin.action" then
ctx.Notify("Action triggered!")
return Ok ()
}Pattern 5: Configuration
In your plugin:
let on_load (ctx: PluginContext) : Async<Result<unit, string>> =
async {
match ctx.Config.GetOpt<string>("greeting") with
| Some msg -> ctx.Log(LogLevel.Info, msg)
| None -> ctx.Log(LogLevel.Info, "No greeting configured")
return Ok ()
}In ~/.config/scarab/plugins.toml:
[plugin.my-first-plugin]
greeting = "Hello, configured world!"Plugin Template
Save this as a starting point for new plugins:
(*
* template-plugin.fsx
*
* Name: Template Plugin
* Version: 1.0.0
* Description: Template for new plugins
* Author: Your Name
* API Version: 0.1.0
*)
open Scarab.PluginApi
// Metadata
let metadata = {
Name = "template-plugin"
Version = "1.0.0"
Description = "Template for new plugins"
Author = "Your Name"
Homepage = None
ApiVersion = "0.1.0"
MinScarabVersion = "0.1.0"
}
// Hooks
let on_load (ctx: PluginContext) : Async<Result<unit, string>> =
async {
ctx.Log(LogLevel.Info, "Template plugin loaded")
return Ok ()
}
let on_unload (ctx: PluginContext) : Async<Result<unit, string>> =
async {
ctx.Log(LogLevel.Info, "Template plugin unloaded")
return Ok ()
}
// Register
Plugin.Register {
Metadata = metadata
OnLoad = on_load
OnUnload = on_unload
OnOutput = None
OnInput = None
OnResize = None
OnAttach = None
OnDetach = None
OnPreCommand = None
OnPostCommand = None
OnRemoteCommand = None
GetCommands = fun () -> []
}Debugging Tips
Enable Debug Logging
In ~/.config/scarab/config.toml:
[plugins.global]
log_level = "debug"
log_file = "~/.local/share/scarab/plugins.log"Then tail the log:
tail -f ~/.local/share/scarab/plugins.logTest Hook Execution
Add logging to every hook:
let on_input (input: byte[]) (ctx: PluginContext) : Async<Result<Action, string>> =
async {
ctx.Log(LogLevel.Debug, sprintf "Input: %A" input)
// Your logic here...
return Ok Action.Continue
}Inspect Terminal State
let inspect (ctx: PluginContext) =
let (cols, rows) = ctx.GetSize()
let (x, y) = ctx.GetCursor()
ctx.Log(LogLevel.Debug, sprintf "Terminal: %dx%d, Cursor: (%d,%d)" cols rows x y)
// Dump first 5 lines
for i in 0us .. 4us do
match ctx.GetLine(i) with
| Some line -> ctx.Log(LogLevel.Debug, sprintf "Line %d: %s" i line)
| None -> ()Key Bindings Quick Reference
Common control characters:
- Ctrl+A = 0x01
- Ctrl+B = 0x02
- …
- Ctrl+Z = 0x1A
- Escape = 0x1B
- Ctrl+P = 0x10
Alt combinations (Escape + key):
- Alt+F = [0x1B, byte ‘f’]
- Alt+1 = [0x1B, byte ‘1’]
Next Steps
- Read the full README.md for comprehensive API documentation
- Study the example plugins in this directory
- Join the Scarab community for questions and sharing
- Check out Fusabi language docs for advanced F# features
Common Gotchas
- Always return a Result: Hooks return
Result<T, string>, not justT - Use Async: All hooks are async - wrap your code in
async { ... } - Unique overlay IDs: Use a consistent range for your plugin (e.g., 5000-5999)
- Clean up on unload: Clear overlays, timers, and resources
- Handle None cases: Many API functions return
Option<T>
Getting Help
- Examples:
/examples/plugins/ - API Reference:
/crates/scarab-plugin-api/src/ - Core Plugins:
/crates/scarab-{palette,nav,platform}/ - Issues: GitHub Issues
- Community: Discord/Forum
Happy plugin development!