Skip to content

Startup Script (init.ts)

Fresh auto-loads ~/.config/fresh/init.ts on startup. It's a TypeScript file that runs once with full access to the plugin API, and it complements the declarative config — it doesn't replace it.

When to use which

The declarative config (config.json, the Settings UI, keybindings editor, theme selector) and init.ts cover different needs. Reach for the declarative side first:

  • Static preferences (tab size, line numbers, default theme) — config.json / Settings UI.
  • Key bindings — Keybinding editor.
  • A reusable feature you'd want on more than one machine, or want to share — publish a plugin package.

Reach for init.ts when the decision depends on where or how Fresh is starting, and can't be baked into a shared config file without lying to the next person who opens it:

  • Different settings when launched over SSH vs. locally.
  • Host-specific tool paths (e.g. rust-analyzer lives in a different prefix on your work laptop).
  • Environment-driven profiles (FRESH_PROFILE=writing fresh → wrap at 80, turn off diagnostics).
  • Extending a bundled plugin with your own data — e.g. adding a custom section to the Dashboard.
  • One-off startup effects — e.g. fade in to your theme on launch.

Complementary with plugins

If you're building something reusable, prefer a plugin — it's installable, shareable, and gets the plugin lifecycle for free.

Sometimes a plugin is the right unit, but part of its behavior only makes sense at startup or depends on the environment. In that case expose the knobs as a plugin API and call them from init.ts. Plugin APIs are typed automatically — editor.getPluginApi("dashboard") returns the right interface or null, no as-cast needed:

ts
// In init.ts — plug the parts together for this machine.
editor.on("plugins_loaded", () => {
  // Add a custom section to the Dashboard plugin.
  const dash = editor.getPluginApi("dashboard");
  if (dash) {
    dash.registerSection("todo", async (ctx) => {
      ctx.kv("open", "3", "warn");
      ctx.newline();
    });
  }

  // Configure another plugin for this environment.
  const todo = editor.getPluginApi("todo-highlighter");
  if (todo) todo.configure({ tags: ["TODO", "FIXME", "HACK"] });
});

That way the plugin stays declarative and shareable, and the environment-specific glue lives in the one file that isn't meant to be shared.

Small examples

ts
// Calmer UI over SSH. setSetting writes to a runtime layer — nothing
// is persisted, so removing this file is a complete undo.
if (editor.getEnv("SSH_TTY")) {
  editor.setSetting("editor.diagnostics_inline_text", false);
  editor.setSetting("terminal.mouse", false);
}

// Host-specific LSP path.
if (editor.getEnv("HOSTNAME") === "work-mac") {
  editor.registerLspServer("rust", {
    command: "/opt/homebrew/bin/rust-analyzer",
    args: [],
    autoStart: true,
  });
}

// Env-driven profile: FRESH_PROFILE=writing fresh
if (editor.getEnv("FRESH_PROFILE") === "writing") {
  editor.setSetting("editor.line_wrap", true);
  editor.setSetting("editor.wrap_column", 80);
}

Editing and reloading

  • init: Edit init.ts from the command palette opens (or creates) ~/.config/fresh/init.ts with a starter template. The same command also refreshes types/fresh.d.ts, writes types/plugins.d.ts (so editor.getPluginApi("dashboard") and friends are typed), and creates a tsconfig.json on first run.
  • init: Reload init.ts re-runs the file without restarting Fresh.
  • init: Check init.ts runs a syntax check (oxc parser) and reports parse errors. It does not run a full TypeScript type-check.
  • fresh --no-init (alias --safe) skips loading for a single launch — useful if the file errors out.
  • Crash fuse: if init.ts fails to evaluate three times in a row within five minutes, the next launch auto-skips it until you fix or remove the file. A successful evaluation clears the counter.

The full API surface is the same as plugins — see the Plugin API reference.

See it in action: What's New in 0.3.0 → init.ts.

Released under the Apache 2.0 License