diff --git a/cmd/root.go b/cmd/root.go index b758dfcc..9b0f670e 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -936,6 +936,28 @@ func runNormalMode(ctx context.Context) error { SetActiveTools: func(names []string) { kitInstance.SetExtensionActiveTools(names) }, + RegisterTheme: func(name string, config extensions.ThemeColorConfig) { + tc := func(c extensions.ThemeColor) [2]string { return [2]string{c.Light, c.Dark} } + ui.RegisterThemeFromConfig(name, + tc(config.Primary), tc(config.Secondary), + tc(config.Success), tc(config.Warning), + tc(config.Error), tc(config.Info), + tc(config.Text), tc(config.Muted), + tc(config.VeryMuted), tc(config.Background), + tc(config.Border), tc(config.MutedBorder), + tc(config.System), tc(config.Tool), + tc(config.Accent), tc(config.Highlight), + tc(config.MdHeading), tc(config.MdLink), + tc(config.MdKeyword), tc(config.MdString), + tc(config.MdNumber), tc(config.MdComment), + ) + }, + SetTheme: func(name string) error { + return ui.ApplyTheme(name) + }, + ListThemes: func() []string { + return ui.ListThemes() + }, ShowOverlay: func(config extensions.OverlayConfig) extensions.OverlayResult { ch := make(chan app.OverlayResponse, 1) appInstance.SendOverlayRequest(app.OverlayRequestEvent{ diff --git a/examples/extensions/neon-theme.go b/examples/extensions/neon-theme.go new file mode 100644 index 00000000..56eb556f --- /dev/null +++ b/examples/extensions/neon-theme.go @@ -0,0 +1,42 @@ +//go:build ignore + +package main + +import "kit/ext" + +// Init registers a "neon" theme and a /neon slash command to apply it. +// Demonstrates how extensions can create and set themes programmatically. +// +// Usage: kit -e examples/extensions/neon-theme.go +func Init(api ext.API) { + api.OnSessionStart(func(_ ext.SessionStartEvent, ctx ext.Context) { + // Register a cyberpunk neon theme at startup. + ctx.RegisterTheme("neon", ext.ThemeColorConfig{ + Primary: ext.ThemeColor{Light: "#CC00FF", Dark: "#FF00FF"}, + Secondary: ext.ThemeColor{Light: "#0088CC", Dark: "#00FFFF"}, + Success: ext.ThemeColor{Light: "#00CC44", Dark: "#00FF66"}, + Warning: ext.ThemeColor{Light: "#CCAA00", Dark: "#FFFF00"}, + Error: ext.ThemeColor{Light: "#CC0033", Dark: "#FF0055"}, + Info: ext.ThemeColor{Light: "#0088CC", Dark: "#00CCFF"}, + Text: ext.ThemeColor{Light: "#111111", Dark: "#F0F0F0"}, + Background: ext.ThemeColor{Light: "#F0F0F0", Dark: "#0A0A14"}, + MdKeyword: ext.ThemeColor{Light: "#CC00FF", Dark: "#FF00FF"}, + MdString: ext.ThemeColor{Light: "#00CC44", Dark: "#00FF66"}, + MdComment: ext.ThemeColor{Light: "#888888", Dark: "#555555"}, + }) + + ctx.PrintInfo("Neon theme registered! Use /theme neon to activate.") + }) + + // Also register a /neon slash command as a shortcut. + api.RegisterCommand(ext.CommandDef{ + Name: "neon", + Description: "Switch to the neon cyberpunk theme", + Execute: func(args string, ctx ext.Context) (string, error) { + if err := ctx.SetTheme("neon"); err != nil { + return "", err + } + return "Neon theme activated!", nil + }, + }) +} diff --git a/internal/extensions/api.go b/internal/extensions/api.go index bef0d52a..6e5009c4 100644 --- a/internal/extensions/api.go +++ b/internal/extensions/api.go @@ -485,6 +485,36 @@ type Context struct { // ctx.RenderMessage("build-status", "All 42 tests passed.") RenderMessage func(rendererName string, content string) + // RegisterTheme adds a named theme to the runtime theme registry. + // If a theme with the same name already exists it is replaced. + // The theme becomes available via /theme and ctx.SetTheme(). + // + // Example: + // + // ctx.RegisterTheme("neon", ext.ThemeColorConfig{ + // Primary: ext.ThemeColor{Dark: "#FF00FF"}, + // Secondary: ext.ThemeColor{Dark: "#00FFFF"}, + // Success: ext.ThemeColor{Dark: "#00FF00"}, + // Warning: ext.ThemeColor{Dark: "#FFFF00"}, + // Error: ext.ThemeColor{Dark: "#FF0000"}, + // Info: ext.ThemeColor{Dark: "#00FFFF"}, + // Text: ext.ThemeColor{Dark: "#FFFFFF"}, + // Background: ext.ThemeColor{Dark: "#000000"}, + // }) + RegisterTheme func(name string, config ThemeColorConfig) + + // SetTheme switches the active color theme by name. The name must + // match a built-in theme, a user/project theme file, or a theme + // registered via RegisterTheme. Returns an error if not found. + // + // Example: + // + // err := ctx.SetTheme("neon") + SetTheme func(name string) error + + // ListThemes returns the names of all available themes. + ListThemes func() []string + // ReloadExtensions hot-reloads all extensions from disk. Existing // extensions receive a SessionShutdown event, then new code is loaded // and receives a SessionStart event. Event handlers, commands, @@ -1723,3 +1753,44 @@ type BeforeCompactResult struct { } func (BeforeCompactResult) isResult() {} + +// --------------------------------------------------------------------------- +// Theme types (exposed to Yaegi — concrete structs, string hex colors) +// --------------------------------------------------------------------------- + +// ThemeColor is an adaptive color pair with light and dark hex values. +// Either field may be empty to inherit from the default theme. +type ThemeColor struct { + Light string + Dark string +} + +// ThemeColorConfig defines a complete color theme that extensions can register +// programmatically via ctx.RegisterTheme(). Uses plain hex strings (not +// color.Color) so the type is safe to pass across the Yaegi boundary. +type ThemeColorConfig struct { + Primary ThemeColor + Secondary ThemeColor + Success ThemeColor + Warning ThemeColor + Error ThemeColor + Info ThemeColor + Text ThemeColor + Muted ThemeColor + VeryMuted ThemeColor + Background ThemeColor + Border ThemeColor + MutedBorder ThemeColor + System ThemeColor + Tool ThemeColor + Accent ThemeColor + Highlight ThemeColor + + // Markdown/syntax highlighting overrides. + MdHeading ThemeColor + MdLink ThemeColor + MdKeyword ThemeColor + MdString ThemeColor + MdNumber ThemeColor + MdComment ThemeColor +} diff --git a/internal/extensions/symbols.go b/internal/extensions/symbols.go index d4677903..afb16c7b 100644 --- a/internal/extensions/symbols.go +++ b/internal/extensions/symbols.go @@ -119,6 +119,10 @@ func Symbols() interp.Exports { "SubagentHandle": reflect.ValueOf((*SubagentHandle)(nil)), "SubagentEvent": reflect.ValueOf((*SubagentEvent)(nil)), + // Theme types + "ThemeColor": reflect.ValueOf((*ThemeColor)(nil)), + "ThemeColorConfig": reflect.ValueOf((*ThemeColorConfig)(nil)), + // Event structs "ToolCallEvent": reflect.ValueOf((*ToolCallEvent)(nil)), "ToolCallResult": reflect.ValueOf((*ToolCallResult)(nil)), diff --git a/internal/ui/themes.go b/internal/ui/themes.go index 1478ac06..2206d2ad 100644 --- a/internal/ui/themes.go +++ b/internal/ui/themes.go @@ -454,6 +454,37 @@ func RefreshThemeRegistry() { initThemeRegistry() } +// RegisterThemeFromConfig adds a theme to the runtime registry from an +// extension's ThemeColorConfig (string hex pairs). Replaces any existing +// entry with the same name. The theme is immediately available via +// ListThemes, LoadThemeByName, and ApplyTheme. +func RegisterThemeFromConfig(name string, primary, secondary, success, warning, error_, info, text, muted, veryMuted, background, border, mutedBorder, system, tool, accent, highlight, mdHeading, mdLink, mdKeyword, mdString, mdNumber, mdComment [2]string) { + if themeRegistry == nil { + initThemeRegistry() + } + t := makeTheme(presetColors{ + primary: primary, secondary: secondary, + success: success, warning: warning, + error_: error_, info: info, + text: text, muted: muted, + veryMuted: veryMuted, background: background, + border: border, mutedBorder: mutedBorder, + system: system, tool: tool, + accent: accent, highlight: highlight, + mdHeading: mdHeading, mdLink: mdLink, + mdKeyword: mdKeyword, mdString: mdString, + mdNumber: mdNumber, mdComment: mdComment, + }) + removeFromRegistry(name) + themeRegistry = append(themeRegistry, ThemeEntry{ + Name: name, + Source: "extension", + theme: t, + loaded: true, + }) + sortRegistry() +} + // ActiveThemeName returns the name of the currently active theme by comparing // against known entries. Returns "custom" if no match is found. func ActiveThemeName() string {