Fresh plugins are written in TypeScript and run in a sandboxed QuickJS environment . This guide covers everything you need to build your own plugins.
Quick Start
Create your first plugin in under 2 minutes:
Create Plugin File
Create a new TypeScript file in the plugins directory: touch ~/.config/fresh/plugins/my_plugin.ts
Add Plugin Code
/// < reference path = "../types/fresh.d.ts" />
// Register a command that inserts text at the cursor
globalThis . my_plugin_say_hello = function () : void {
editor . insertAtCursor ( "Hello from my new plugin! \n " );
editor . setStatus ( "My plugin says hello!" );
};
editor . registerCommand (
"My Plugin: Say Hello" ,
"Inserts a greeting from my plugin" ,
"my_plugin_say_hello" ,
"normal"
);
editor . setStatus ( "My first plugin loaded!" );
Restart Fresh
Restart Fresh to load your plugin.
Test Your Plugin
Press Ctrl+P > and search for “My Plugin: Say Hello”.
All .ts files in the plugins/ directory are automatically loaded when Fresh starts.
Plugin Architecture
Runtime Environment
Plugins run in a sandboxed QuickJS JavaScript runtime:
Transpilation TypeScript is transpiled to JavaScript using oxc_transformer (a fast, Rust-based compiler).
Sandboxing Each plugin runs in an isolated QuickJS environment, preventing interference between plugins.
Async Support Full async/await support for non-blocking I/O, process spawning, and LSP requests.
Type Safety TypeScript definitions provide autocomplete and type checking in your editor.
The editor Global Object
The editor object is the main API surface:
/// < reference path = "../types/fresh.d.ts" />
// The editor global is always available
editor . setStatus ( "Plugin initialized" );
editor . debug ( "This goes to the debug log" );
The editor object provides access to:
Buffer operations (read, write, modify)
Command registration
Event handling
Process spawning
File system operations
LSP integration
Visual overlays and decorations
Core Concepts
Commands
Commands are actions that appear in the command palette and can be bound to keys.
Registering Commands
// Define the command handler
globalThis . my_action = function () : void {
editor . setStatus ( "Command executed!" );
};
// Register it with the editor
editor . registerCommand (
"My Custom Command" , // Name shown in command palette
"Does something useful" , // Description
"my_action" , // Global function name to call
"normal" // Context: "normal", "insert", "prompt", etc.
);
Command handlers must be attached to globalThis because the editor calls them by name.
Command Contexts
Contexts control when commands are available:
Context Description "normal"Normal editing mode "insert"Insert mode "prompt"When a prompt is active "" (empty)All contexts Custom Your custom mode/context
Async Operations
Many API calls return Promises. Use async/await:
globalThis . search_files = async function () : Promise < void > {
editor . setStatus ( "Searching..." );
const result = await editor . spawnProcess ( "rg" , [ "TODO" , "." ]);
if ( result . exit_code === 0 ) {
const lines = result . stdout . split ( " \n " ). filter ( l => l . trim ());
editor . setStatus ( `Found ${ lines . length } TODOs` );
} else {
editor . setStatus ( "Search failed" );
}
};
Event Handlers
Subscribe to editor events with editor.on():
globalThis . onSave = function ( data : { buffer_id : number , path : string }) : void {
editor . debug ( `Saved: ${ data . path } ` );
editor . setStatus ( `File saved: ${ data . path } ` );
};
editor . on ( "buffer_save" , "onSave" );
Available Events:
buffer_save - After a buffer is saved to disk
buffer_closed - When a buffer is closed
cursor_moved - When cursor position changes
render_start - Before screen renders (for overlays)
lines_changed - When visible lines change
prompt_confirmed - When user confirms a prompt
prompt_cancelled - When user cancels a prompt
Buffers
Buffers hold text content. Each buffer has a unique numeric ID.
Querying Buffers
const bufferId = editor . getActiveBufferId ();
const path = editor . getBufferPath ( bufferId );
const length = editor . getBufferLength ( bufferId );
const modified = editor . isBufferModified ( bufferId );
const cursorPos = editor . getCursorPosition ();
Reading Buffer Content
const bufferId = editor . getActiveBufferId ();
const text = await editor . getBufferText ( bufferId , 0 , 100 );
editor . debug ( `First 100 bytes: ${ text } ` );
Modifying Buffers
const bufferId = editor . getActiveBufferId ();
const pos = editor . getCursorPosition ();
// Insert text at a position
editor . insertText ( bufferId , pos , "Hello, world! \n " );
// Delete a range
editor . deleteRange ( bufferId , 0 , 10 );
// Insert at cursor (convenience method)
editor . insertAtCursor ( "Quick insert \n " );
Virtual Buffers
Create special buffers for displaying structured data like search results, diagnostics, or logs.
editor . defineMode (
"my-results" , // Mode name
null , // No parent mode
[
[ "Return" , "my_goto_result" ],
[ "q" , "close_buffer" ]
],
true // Read-only
);
await editor . createVirtualBufferInSplit ({
name: "*Search Results*" ,
mode: "my-results" ,
readOnly: true ,
entries: [
{
text: "src/main.rs:42: found match \n " ,
properties: { file: "src/main.rs" , line: 42 , column: 10 }
},
{
text: "src/lib.rs:100: another match \n " ,
properties: { file: "src/lib.rs" , line: 100 , column: 5 }
}
],
ratio: 0.3 , // Takes 30% of height
panelId: "my-search-results" // Reuse panel if exists
});
Accessing Properties
globalThis . my_goto_result = function () : void {
const bufferId = editor . getActiveBufferId ();
const props = editor . getTextPropertiesAtCursor ( bufferId );
if ( props . length > 0 && props [ 0 ]. file ) {
editor . openFile ( props [ 0 ]. file , props [ 0 ]. line , props [ 0 ]. column || 0 );
}
};
Overlays
Add visual decorations without modifying buffer content:
const bufferId = editor . getActiveBufferId ();
const start = 100 ;
const end = 150 ;
// Highlight with yellow background
editor . addOverlay (
bufferId ,
"my_highlight" , // Namespace (for batch removal)
start ,
end ,
{
fg: [ 255 , 255 , 0 ], // Yellow foreground
underline: true
}
);
// Later, clear all overlays in namespace
editor . clearNamespace ( bufferId , "my_highlight" );
Process Spawning
Run external commands and tools:
globalThis . run_tests = async function () : Promise < void > {
editor . setStatus ( "Running tests..." );
const result = await editor . spawnProcess (
"cargo" , // Command
[ "test" ], // Arguments
null // Working directory (null = editor's cwd)
);
if ( result . exit_code === 0 ) {
editor . setStatus ( "Tests passed!" );
} else {
const firstError = result . stderr . split ( ' \n ' )[ 0 ];
editor . setStatus ( `Tests failed: ${ firstError } ` );
}
};
LSP Integration
Invoke language server requests:
globalThis . switch_header = async function () : Promise < void > {
const bufferId = editor . getActiveBufferId ();
const path = editor . getBufferPath ( bufferId );
const uri = `file:// ${ path } ` ;
const result = await editor . sendLspRequest (
"cpp" ,
"textDocument/switchSourceHeader" ,
{ textDocument: { uri } }
);
if ( result && typeof result === "string" ) {
editor . openFile ( result . replace ( "file://" , "" ), 0 , 0 );
}
};
File System Operations
Read and write files:
globalThis . process_file = async function () : Promise < void > {
const path = "/path/to/file.txt" ;
if ( editor . fileExists ( path )) {
const content = await editor . readFile ( path );
const modified = content . replace ( /TODO/ g , "DONE" );
await editor . writeFile ( path + ".processed" , modified );
editor . setStatus ( "File processed!" );
}
};
Common Patterns
Highlighting Text
globalThis . highlight_todos = function () : void {
const bufferId = editor . getActiveBufferId ();
const length = editor . getBufferLength ( bufferId );
// Read entire buffer
editor . getBufferText ( bufferId , 0 , length ). then ( text => {
const regex = /TODO | FIXME | HACK/ g ;
let match ;
while (( match = regex . exec ( text )) !== null ) {
editor . addOverlay (
bufferId ,
"todo_highlight" ,
match . index ,
match . index + match [ 0 ]. length ,
{ fg: [ 255 , 165 , 0 ], underline: true }
);
}
});
};
Interactive Prompts
globalThis . git_grep = function () : void {
editor . startPrompt ( "Git grep: " , "git-grep" );
};
globalThis . onGitGrepConfirm = async function ( args : {
prompt_type : string ;
input : string ;
}) : Promise < boolean > {
if ( args . prompt_type !== "git-grep" ) return true ;
const result = await editor . spawnProcess (
"git" ,
[ "grep" , "-n" , args . input ]
);
if ( result . exit_code === 0 ) {
// Parse results and create virtual buffer
const lines = result . stdout . split ( " \n " ). filter ( l => l . trim ());
editor . setStatus ( `Found ${ lines . length } matches` );
}
return true ;
};
editor . on ( "prompt_confirmed" , "onGitGrepConfirm" );
Custom Modes
editor . defineMode (
"my-mode" ,
"normal" , // Parent mode
[
[ "j" , "move_line_down" ],
[ "k" , "move_line_up" ],
[ "Return" , "my_custom_action" ],
[ "q" , "close_buffer" ]
],
false // Not read-only
);
Internationalization
Plugins can support multiple languages using i18n:
// Use plugin translations
const message = editor . pluginTranslate (
"my_plugin" ,
"welcome_message" ,
{ name: "World" }
);
editor . setStatus ( message );
Create a .i18n.json file next to your plugin:
{
"en" : {
"welcome_message" : "Hello, {name}!"
},
"es" : {
"welcome_message" : "¡Hola, {name}!"
}
}
Debugging
Debug Logging
editor . debug ( "Debug message" );
editor . info ( "Info message" );
editor . warn ( "Warning message" );
editor . error ( "Error message" );
Run Fresh with debug logging:
Status Messages
editor . setStatus ( "Operation complete!" );
Status messages appear in the status bar.
Best Practices
Use TypeScript Type Definitions
Always include the type reference at the top of your plugin: /// < reference path = "../types/fresh.d.ts" />
Always wrap async operations in try/catch: try {
const result = await editor . spawnProcess ( "cmd" , []);
} catch ( e ) {
editor . error ( `Failed: ${ e } ` );
}
Use Namespaces for Overlays
Use consistent namespaces to enable batch removal: editor . addOverlay ( bufferId , "plugin:feature" , start , end , {});
// Later:
editor . clearNamespace ( bufferId , "plugin:feature" );
Debounce Expensive Operations
Use editor.delay() to debounce rapid events: await editor . delay ( 300 ); // Wait 300ms
Subscribe to buffer_closed to clean up resources: globalThis . onBufferClose = function ( data : { buffer_id : number }) {
editor . clearNamespace ( data . buffer_id , "my_plugin" );
};
editor . on ( "buffer_closed" , "onBufferClose" );
Publishing Your Plugin
To share your plugin with others:
Create Git Repository
Initialize a git repository for your plugin: git init
git add .
git commit -m "Initial plugin version"
Add package.json
Create a package.json with metadata: {
"name" : "my-awesome-plugin" ,
"version" : "1.0.0" ,
"description" : "Does something awesome" ,
"fresh" : {
"type" : "plugin" ,
"entry" : "my_plugin.ts"
}
}
Push to GitHub
Push your repository to GitHub or any git hosting service.
Share the URL
Users can install your plugin with: pkg: Install from URL
https://github.com/yourusername/my-awesome-plugin
Next Steps
Plugin Examples Explore real plugin examples with complete code
API Reference Complete API documentation with all methods
Plugin Overview Learn about the plugin system architecture
Getting Started Install and manage plugins