Skip to main content
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:
1

Create Plugin File

Create a new TypeScript file in the plugins directory:
touch ~/.config/fresh/plugins/my_plugin.ts
2

Add Plugin Code

my_plugin.ts
/// <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!");
3

Restart Fresh

Restart Fresh to load your plugin.
4

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:
ContextDescription
"normal"Normal editing mode
"insert"Insert mode
"prompt"When a prompt is active
"" (empty)All contexts
CustomYour 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:
my_plugin.i18n.json
{
  "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:
RUST_LOG=debug fresh

Status Messages

editor.setStatus("Operation complete!");
Status messages appear in the status bar.

Best Practices

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 consistent namespaces to enable batch removal:
editor.addOverlay(bufferId, "plugin:feature", start, end, {});
// Later:
editor.clearNamespace(bufferId, "plugin:feature");
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:
1

Create Git Repository

Initialize a git repository for your plugin:
git init
git add .
git commit -m "Initial plugin version"
2

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"
  }
}
3

Push to GitHub

Push your repository to GitHub or any git hosting service.
4

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

Build docs developers (and LLMs) love