Skip to main content
The Fresh editor provides a plugin utilities library (plugins/lib/) with reusable components that abstract common plugin patterns. These utilities help you build plugins faster with less boilerplate.

Installation

Import utilities from the @plugins/lib module:
import { PanelManager, NavigationController, VirtualBufferFactory } from "@plugins/lib";
import type { RGB, Location, PanelOptions } from "@plugins/lib";

PanelManager

Manages the lifecycle of result panels (open, close, update, toggle). Simplifies creating persistent panels that can be shown, hidden, and updated.

Creating a Panel

import { PanelManager } from "@plugins/lib";

const panel = new PanelManager({
  name: "*Search Results*",
  mode: "search-results",
  panelId: "search",
  ratio: 0.3,
  keybindings: [
    ["Return", "search_goto"],
    ["q", "close_buffer"]
  ]
});

PanelOptions

interface PanelOptions {
  name: string;              // Panel name (e.g., "*Search*")
  mode: string;              // Mode for keybindings
  panelId: string;           // Unique panel ID
  ratio: number;             // Split ratio (0.0-1.0)
  keybindings: [string, string][];  // Key bindings
  direction?: string;        // "horizontal" or "vertical"
}

Methods

open

Open the panel with initial content.
await panel.open(entries: TextPropertyEntry[]): Promise<void>
Example:
await panel.open([
  { text: "Result 1\n", properties: { id: 1 } },
  { text: "Result 2\n", properties: { id: 2 } }
]);

update

Update panel content (panel must already be open).
await panel.update(entries: TextPropertyEntry[]): Promise<void>
Example:
await panel.update([
  { text: "Updated result 1\n", properties: { id: 1 } },
  { text: "New result 3\n", properties: { id: 3 } }
]);

toggle

Toggle panel visibility (open if closed, close if open).
await panel.toggle(entries: TextPropertyEntry[]): Promise<void>

close

Close the panel.
panel.close(): void

isOpen

Check if panel is currently open.
panel.isOpen(): boolean

Complete Example

import { PanelManager } from "@plugins/lib";

// Create panel manager
const searchPanel = new PanelManager({
  name: "*Search Results*",
  mode: "search-results",
  panelId: "search",
  ratio: 0.3,
  keybindings: [
    ["Return", "search_goto"],
    ["n", "search_next"],
    ["q", "close_buffer"]
  ]
});

// Perform search and show results
async function search(query: string) {
  const results = await performSearch(query);
  
  const entries = results.map(r => ({
    text: `${r.file}:${r.line}: ${r.text}\n`,
    properties: { file: r.file, line: r.line }
  }));
  
  if (searchPanel.isOpen()) {
    await searchPanel.update(entries);
  } else {
    await searchPanel.open(entries);
  }
}
Handles list navigation with selection tracking and visual highlighting. Perfect for implementing navigable result lists.

Creating a Navigator

import { NavigationController } from "@plugins/lib";

const nav = new NavigationController({
  bufferId: myBufferId,
  highlightPrefix: "search",
  color: { r: 100, g: 100, b: 255 }
});
interface NavigationOptions {
  bufferId: number;          // Buffer to navigate in
  highlightPrefix: string;   // Prefix for highlight namespace
  color: RGB;                // Highlight color
}

interface RGB {
  r: number;  // Red (0-255)
  g: number;  // Green (0-255)
  b: number;  // Blue (0-255)
}

Methods

moveUp

Move selection up one line.
nav.moveUp(): void

moveDown

Move selection down one line.
nav.moveDown(): void

moveToTop

Move selection to first line.
nav.moveToTop(): void

moveToBottom

Move selection to last line.
nav.moveToBottom(): void

getSelectedIndex

Get current selection index (0-based).
nav.getSelectedIndex(): number

getSelectedLocation

Get text properties at current selection.
nav.getSelectedLocation(): Record<string, unknown> | null

clearHighlights

Clear all navigation highlights.
nav.clearHighlights(): void

Complete Example

import { NavigationController } from "@plugins/lib";

// Create navigator
const nav = new NavigationController({
  bufferId: resultsBufferId,
  highlightPrefix: "search",
  color: { r: 100, g: 150, b: 255 }
});

// Register navigation commands
globalThis.search_next = () => {
  nav.moveDown();
};

globalThis.search_prev = () => {
  nav.moveUp();
};

globalThis.search_goto = () => {
  const location = nav.getSelectedLocation();
  if (location?.file && location?.line) {
    editor.openFile(location.file as string, location.line as number, 0);
  }
};

VirtualBufferFactory

Simplified creation of virtual buffers with less boilerplate.

Creating a Virtual Buffer

import { VirtualBufferFactory } from "@plugins/lib";

const bufferId = await VirtualBufferFactory.create({
  name: "*Output*",
  mode: "output-mode",
  entries: [
    { text: "Line 1\n", properties: { id: 1 } },
    { text: "Line 2\n", properties: { id: 2 } }
  ],
  readOnly: true,
  ratio: 0.25,
  panelId: "output"
});

VirtualBufferOptions

interface VirtualBufferOptions {
  name: string;              // Buffer name
  mode: string;              // Mode for keybindings
  entries: TextPropertyEntry[];  // Buffer content
  readOnly?: boolean;        // Read-only flag
  ratio?: number;            // Split ratio
  panelId?: string;          // Panel ID for updates
  direction?: string;        // Split direction
}

Type Definitions

The library exports common types for use in your plugins:
import type {
  RGB,
  Location,
  PanelOptions,
  NavigationOptions,
  VirtualBufferOptions
} from "@plugins/lib";

RGB

interface RGB {
  r: number;  // Red component (0-255)
  g: number;  // Green component (0-255)
  b: number;  // Blue component (0-255)
}

Location

interface Location {
  file: string;   // File path
  line: number;   // Line number
  column?: number;  // Optional column number
}

Complete Plugin Example

Here’s a complete plugin using all three utilities:
import {
  PanelManager,
  NavigationController,
  VirtualBufferFactory
} from "@plugins/lib";
import type { RGB } from "@plugins/lib";

// Define mode
editor.defineMode("file-search", "special", [
  ["Return", "file_search_open"],
  ["j", "file_search_next"],
  ["k", "file_search_prev"],
  ["q", "close_buffer"]
], true);

// Create panel manager
const panel = new PanelManager({
  name: "*File Search*",
  mode: "file-search",
  panelId: "file-search",
  ratio: 0.3,
  keybindings: [
    ["Return", "file_search_open"],
    ["j", "file_search_next"],
    ["k", "file_search_prev"],
    ["q", "close_buffer"]
  ]
});

let navigator: NavigationController | null = null;

// Search for files
async function searchFiles(pattern: string) {
  const result = await editor.spawnProcess("find", [
    ".",
    "-name",
    `*${pattern}*`,
    "-type",
    "f"
  ]);
  
  if (result.exit_code !== 0) {
    editor.setStatus("No files found");
    return;
  }
  
  const files = result.stdout
    .split("\n")
    .filter(f => f.length > 0);
  
  const entries = files.map(file => ({
    text: `${file}\n`,
    properties: { file: file, line: 1 }
  }));
  
  await panel.open(entries);
  
  // Create navigator
  const bufferId = editor.getActiveBufferId();
  navigator = new NavigationController({
    bufferId: bufferId,
    highlightPrefix: "file-search",
    color: { r: 100, g: 150, b: 255 }
  });
  
  editor.setStatus(`Found ${files.length} files`);
}

// Navigation commands
globalThis.file_search_next = () => {
  navigator?.moveDown();
};

globalThis.file_search_prev = () => {
  navigator?.moveUp();
};

globalThis.file_search_open = () => {
  const location = navigator?.getSelectedLocation();
  if (location?.file) {
    editor.openFile(location.file as string, 1, 0);
  }
};

// Register command
editor.registerCommand(
  "File: Search",
  "Search for files by name",
  "file_search",
  "normal",
  "plugin"
);

globalThis.file_search = () => {
  editor.startPrompt("File search: ", "file-search-input");
};

// Handle prompt
globalThis.handleFileSearchPrompt = async (data) => {
  if (data.prompt_type === "file-search-input" && data.confirmed) {
    await searchFiles(data.value);
  }
};

editor.on("prompt_submit", "handleFileSearchPrompt");

Best Practices

PanelManager handles panel lifecycle automatically, including reusing panels with the same panelId.
For navigable result lists, use PanelManager to manage the panel and NavigationController for selection highlighting.
Use theme-appropriate colors for highlights. Consider using theme colors from the editor config.
Call clearHighlights() when closing panels to remove visual decorations.

Source Files

For full API details, see the source files in plugins/lib/:
  • plugins/lib/panel-manager.ts
  • plugins/lib/navigation-controller.ts
  • plugins/lib/virtual-buffer-factory.ts
  • plugins/lib/types.ts

Build docs developers (and LLMs) love