Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,6 @@ bun.lockb
dist
.amp/
.worktrees/

# macOS Finder metadata
.DS_Store
43 changes: 43 additions & 0 deletions crates/fresh-core/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2572,6 +2572,38 @@ pub enum PluginCommand {
namespace: String,
},

/// Place an inline raster image (kitty graphics protocol) anchored to a
/// buffer position. The core reserves `rows` virtual lines × `cols`
/// columns and renders kitty Unicode placeholders over them, so the image
/// scrolls with the text. No-op (the plugin keeps its text fallback) on
/// terminals without graphics support. A generic rendering primitive:
/// the core doesn't know what the image shows — plugins use it to
/// render any file's content (diagrams, pictures, plots, previews, …).
PlaceImage {
buffer_id: BufferId,
/// Content key for dedup/reuse across re-renders (e.g. a hash of the
/// image bytes). Re-placing the same key reuses the transmitted image.
key: String,
/// Absolute path to a PNG the terminal can read.
source: String,
/// Byte position whose line the image is anchored to.
position: usize,
/// Placement width / height in terminal cells.
cols: u16,
rows: u16,
/// true = above the anchored line, false = below.
above: bool,
/// Namespace for bulk removal via [`PluginCommand::ClearImages`].
namespace: String,
},

/// Remove all images (and their reserved placeholder rows) placed under a
/// namespace, and free the terminal-side pixel data.
ClearImages {
buffer_id: BufferId,
namespace: String,
},

/// Add a conceal range that hides or replaces a byte range during rendering.
/// Used for Typora-style seamless markdown: hiding syntax markers like `**`, `[](url)`, etc.
AddConceal {
Expand Down Expand Up @@ -2599,6 +2631,17 @@ pub enum PluginCommand {
end: usize,
},

/// Remove conceal ranges in a byte range, restricted to one namespace.
/// Lets a plugin rebuild its own conceals for a line without wiping
/// ranges other plugins placed there (same motivation as
/// `ClearOverlaysInRangeForNamespace`).
ClearConcealsInRangeForNamespace {
buffer_id: BufferId,
namespace: OverlayNamespace,
start: usize,
end: usize,
},

/// Add a collapsed fold range. Hides the byte range
/// `[start, end)` from rendering — the line containing `start - 1`
/// (the fold's "header") stays visible while the lines covered by
Expand Down
129 changes: 129 additions & 0 deletions crates/fresh-core/src/graphics.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
//! Terminal raster-graphics capability detection.
//!
//! Shared by the editor (which gates inline-image placement on it) and the
//! plugin runtime (which exposes it to plugins via
//! `editor.getGraphicsCapability()`), so both sides always agree on what the
//! terminal supports. Detection is purely environment-based.

/// Terminal raster-graphics capability.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GraphicsCapability {
/// No known raster-graphics support; images fall back to text.
None,
/// Kitty graphics protocol (kitty, WezTerm, Ghostty, recent Konsole).
Kitty,
}

impl GraphicsCapability {
/// Detect graphics capability from the environment. Override with the
/// `FRESH_GRAPHICS` env var (`kitty` / `none`).
pub fn detect() -> Self {
Self::detect_from(|name| std::env::var(name).ok())
}

/// Detection core, parameterized over an environment lookup so it can be
/// unit-tested without touching the process environment.
pub fn detect_from(get: impl Fn(&str) -> Option<String>) -> Self {
if let Some(v) = get("FRESH_GRAPHICS") {
match v.to_lowercase().as_str() {
"kitty" | "on" | "1" | "true" => return GraphicsCapability::Kitty,
"none" | "off" | "0" | "false" => return GraphicsCapability::None,
_ => {}
}
}

// kitty sets KITTY_WINDOW_ID for every window.
if get("KITTY_WINDOW_ID").is_some() {
return GraphicsCapability::Kitty;
}

// WezTerm / Ghostty advertise themselves and both speak the kitty
// graphics protocol.
if let Some(tp) = get("TERM_PROGRAM") {
let t = tp.to_lowercase();
if t.contains("wezterm") || t.contains("ghostty") {
return GraphicsCapability::Kitty;
}
}
if get("GHOSTTY_RESOURCES_DIR").is_some() || get("GHOSTTY_BIN_DIR").is_some() {
return GraphicsCapability::Kitty;
}

if let Some(term) = get("TERM") {
let t = term.to_lowercase();
if t.contains("kitty") || t.contains("ghostty") || t.contains("wezterm") {
return GraphicsCapability::Kitty;
}
}

GraphicsCapability::None
}

pub fn supports_images(self) -> bool {
matches!(self, GraphicsCapability::Kitty)
}

/// Stable string form exposed to plugins (`"kitty"` / `"none"`).
pub fn as_str(self) -> &'static str {
match self {
GraphicsCapability::None => "none",
GraphicsCapability::Kitty => "kitty",
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn detect_respects_override() {
let kitty = GraphicsCapability::detect_from(|n| {
if n == "FRESH_GRAPHICS" {
Some("kitty".to_string())
} else {
None
}
});
assert_eq!(kitty, GraphicsCapability::Kitty);

let off = GraphicsCapability::detect_from(|n| {
if n == "FRESH_GRAPHICS" {
Some("none".to_string())
} else {
None
}
});
assert_eq!(off, GraphicsCapability::None);
}

#[test]
fn detect_recognizes_kitty_and_wezterm() {
let kitty = GraphicsCapability::detect_from(|n| {
if n == "KITTY_WINDOW_ID" {
Some("1".to_string())
} else {
None
}
});
assert_eq!(kitty, GraphicsCapability::Kitty);

let wez = GraphicsCapability::detect_from(|n| {
if n == "TERM_PROGRAM" {
Some("WezTerm".to_string())
} else {
None
}
});
assert_eq!(wez, GraphicsCapability::Kitty);

let plain = GraphicsCapability::detect_from(|_| None);
assert_eq!(plain, GraphicsCapability::None);
}

#[test]
fn as_str_round_trips() {
assert_eq!(GraphicsCapability::Kitty.as_str(), "kitty");
assert_eq!(GraphicsCapability::None.as_str(), "none");
}
}
1 change: 1 addition & 0 deletions crates/fresh-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ impl std::fmt::Display for WindowTerminalId {
pub mod config;
pub mod file_explorer;
pub mod file_uri;
pub mod graphics;
pub mod menu;
pub mod overlay;
pub mod plugin_schemas;
Expand Down
115 changes: 76 additions & 39 deletions crates/fresh-editor/plugins/lib/fresh.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -603,15 +603,39 @@ type FileExplorerDecoration = {
*/
priority: number;
};
type FileExplorerTooltip = {
type FileExplorerSlotEntry = {
/**
* Tooltip title shown in the popup border.
* File or directory path to override.
*/
title: string;
path: string;
/**
* Body lines shown inside the popup.
* Optional leading-slot override.
*/
lines: Array<string>;
leading: FileExplorerLeadingSlot | null;
/**
* Explicitly suppress the compatibility leading slot for this path.
*/
suppressLeading: boolean;
/**
* Optional trailing-slot override.
*/
trailing: FileExplorerTrailingSlot | null;
/**
* Explicitly suppress the compatibility trailing slot for this path.
*/
suppressTrailing: boolean;
/**
* Optional filename colour override.
*/
nameColor: OverlayColorSpec | null;
/**
* Explicitly suppress compatibility filename colouring for this path.
*/
suppressNameColor: boolean;
/**
* Priority for display when multiple overrides exist (higher wins).
*/
priority: number;
};
type FileExplorerLeadingSlot = {
/**
Expand All @@ -625,7 +649,7 @@ type FileExplorerLeadingSlot = {
/**
* Minimum display width reserved for the leading slot.
*/
minWidth?: number;
minWidth: number;
};
type FileExplorerTrailingSlot = {
/**
Expand All @@ -639,41 +663,17 @@ type FileExplorerTrailingSlot = {
/**
* Optional tooltip shown when hovering the trailing slot.
*/
tooltip?: FileExplorerTooltip | null;
tooltip: FileExplorerTooltip | null;
};
type FileExplorerSlotEntry = {
/**
* File or directory path to override.
*/
path: string;
/**
* Optional leading-slot override.
*/
leading?: FileExplorerLeadingSlot | null;
/**
* Explicitly suppress the compatibility leading slot for this path.
*/
suppressLeading?: boolean;
/**
* Optional trailing-slot override.
*/
trailing?: FileExplorerTrailingSlot | null;
/**
* Explicitly suppress the compatibility trailing slot for this path.
*/
suppressTrailing?: boolean;
/**
* Optional filename colour override.
*/
nameColor?: OverlayColorSpec | null;
type FileExplorerTooltip = {
/**
* Explicitly suppress compatibility filename colouring for this path.
* Tooltip title shown in the popup border.
*/
suppressNameColor?: boolean;
title: string;
/**
* Priority for display when multiple overrides exist (higher wins).
* Body lines shown inside the popup.
*/
priority?: number;
lines: Array<string>;
};
type FormatterPackConfig = {
/**
Expand Down Expand Up @@ -2157,6 +2157,14 @@ interface EditorAPI {
*/
readFile(path: string): string | null;
/**
* Read up to `max_len` bytes from a file starting at byte `offset`,
* returned as an array of byte values (0-255). Returns `null` if the
* file can't be opened or read. Intended for lightweight binary
* inspection — e.g. parsing an image header for its dimensions — not
* for bulk reads: `max_len` is capped at 1 MiB.
*/
readFileBytes(path: string, offset: number, maxLen: number): number[] | null;
/**
* Write file contents
*/
writeFile(path: string, content: string): boolean;
Expand Down Expand Up @@ -2502,6 +2510,11 @@ interface EditorAPI {
*/
clearConcealsInRange(bufferId: number, start: number, end: number): boolean;
/**
* Clear conceal ranges overlapping a byte range, restricted to one
* namespace — other plugins' conceals in the range are untouched.
*/
clearConcealsInRangeForNamespace(bufferId: number, namespace: string, start: number, end: number): boolean;
/**
* Add a collapsed fold range. Hides bytes [start, end) from
* rendering — the line containing `start - 1` (the fold "header")
* stays visible, while subsequent lines covered by the range are
Expand Down Expand Up @@ -2567,10 +2580,9 @@ interface EditorAPI {
*/
clearFileExplorerDecorations(namespace: string): boolean;
/**
* Set file explorer slot overrides for a namespace. Any omitted fields
* fall back to the editor's default file-explorer providers.
* Set file explorer slot overrides for a namespace
*/
setFileExplorerSlots(namespace: string, slots: FileExplorerSlotEntry[]): boolean;
setFileExplorerSlots(namespace: string, slots: Record<string, unknown>[]): boolean;
/**
* Clear file explorer slot overrides for a namespace
*/
Expand Down Expand Up @@ -2621,6 +2633,31 @@ interface EditorAPI {
*/
addVirtualLine(bufferId: number, position: number, text: string, options: Record<string, unknown>, above: boolean, namespace: string, priority: number): boolean;
/**
* Place an inline image (kitty graphics protocol) anchored to a buffer
* position. `source` is an absolute path to a PNG; `cols`/`rows` are the
* placement size in terminal cells. Returns true if the command was
* dispatched. On terminals without graphics support the core treats it
* as a no-op, so the caller should keep a visible text fallback.
*/
placeImage(bufferId: number, key: string, source: string, position: number, cols: number, rows: number, above: boolean, namespace: string): boolean;
/**
* Remove all images placed under `namespace` (and their reserved rows)
* and free the terminal-side image data.
*/
clearImages(bufferId: number, namespace: string): boolean;
/**
* The terminal's raster-graphics capability: `"kitty"` if inline images
* placed via `placeImage` will actually render (kitty graphics
* protocol — kitty, WezTerm, Ghostty, …), `"none"` otherwise.
*
* Check this before doing expensive rendering work (rasterizing
* diagrams, converting image formats): on `"none"` terminals
* `placeImage` is a no-op in the core, so skip the work and keep a text
* fallback instead. Overridable by the user with the `FRESH_GRAPHICS`
* env var (`kitty` / `none`).
*/
getGraphicsCapability(): string;
/**
* Show a prompt and wait for user input (async)
* Returns the user input or null if cancelled
*/
Expand Down
Loading
Loading