Theme config (#127)

* add theme configuration

* add path for themes (broken)

* md theme relative path not working

* cleanup malpractice

* mid commit

* fix logical error
This commit is contained in:
matscalia
2025-09-02 09:30:20 +02:00
committed by GitHub
parent 4f2f61c673
commit 4efc2e271c
3 changed files with 166 additions and 8 deletions
+31
View File
@@ -11,6 +11,7 @@ import (
"strings"
"time"
"github.com/charmbracelet/lipgloss"
"github.com/cloudwego/eino/schema"
"github.com/mark3labs/mcphost/internal/agent"
"github.com/mark3labs/mcphost/internal/config"
@@ -199,6 +200,7 @@ func initConfig() {
viper.Set("hooks", hooksConfig)
}
}
}
// loadConfigWithEnvSubstitution loads a config file with environment variable substitution
@@ -222,13 +224,42 @@ func loadConfigWithEnvSubstitution(configPath string) error {
configType = "json"
}
config.SetConfigPath(configPath)
// Use viper to parse the processed content
viper.SetConfigType(configType)
return viper.ReadConfig(strings.NewReader(processedContent))
}
func configToUiTheme(theme config.Theme) ui.Theme {
return ui.Theme{
Primary: lipgloss.AdaptiveColor(theme.Primary),
Secondary: lipgloss.AdaptiveColor(theme.Secondary),
Success: lipgloss.AdaptiveColor(theme.Success),
Warning: lipgloss.AdaptiveColor(theme.Warning),
Error: lipgloss.AdaptiveColor(theme.Error),
Info: lipgloss.AdaptiveColor(theme.Info),
Text: lipgloss.AdaptiveColor(theme.Text),
Muted: lipgloss.AdaptiveColor(theme.Muted),
VeryMuted: lipgloss.AdaptiveColor(theme.VeryMuted),
Background: lipgloss.AdaptiveColor(theme.Background),
Border: lipgloss.AdaptiveColor(theme.Border),
MutedBorder: lipgloss.AdaptiveColor(theme.MutedBorder),
System: lipgloss.AdaptiveColor(theme.System),
Tool: lipgloss.AdaptiveColor(theme.Tool),
Accent: lipgloss.AdaptiveColor(theme.Accent),
Highlight: lipgloss.AdaptiveColor(theme.Highlight),
}
}
func init() {
cobra.OnInitialize(initConfig)
var theme config.Theme
err := config.FilepathOr("theme", &theme)
if err == nil && viper.InConfig("theme") {
uiTheme := configToUiTheme(theme)
ui.SetTheme(uiTheme)
}
rootCmd.PersistentFlags().
StringVar(&configFile, "config", "", "config file (default is $HOME/.mcp.json)")
+98
View File
@@ -6,6 +6,9 @@ import (
"os"
"path/filepath"
"strings"
"github.com/spf13/viper"
"gopkg.in/yaml.v3"
)
// MCPServerConfig represents configuration for an MCP server
@@ -97,6 +100,45 @@ func (s *MCPServerConfig) UnmarshalJSON(data []byte) error {
return nil
}
type AdaptiveColor struct {
Light string `json:"light,omitempty" yaml:"light,omitempty"`
Dark string `json:"dark,omitempty" yaml:"dark,omitempty"`
}
type Theme struct {
Primary AdaptiveColor `json:"primary" yaml:"primary"`
Secondary AdaptiveColor `json:"secondary" yaml:"secondary"`
Success AdaptiveColor `json:"success" yaml:"success"`
Warning AdaptiveColor `json:"warning" yaml:"warning"`
Error AdaptiveColor `json:"error" yaml:"error"`
Info AdaptiveColor `json:"info" yaml:"info"`
Text AdaptiveColor `json:"text" yaml:"text"`
Muted AdaptiveColor `json:"muted" yaml:"muted"`
VeryMuted AdaptiveColor `json:"very-muted" yaml:"very-muted"`
Background AdaptiveColor `json:"background" yaml:"background"`
Border AdaptiveColor `json:"border" yaml:"border"`
MutedBorder AdaptiveColor `json:"muted-border" yaml:"muted-border"`
System AdaptiveColor `json:"system" yaml:"system"`
Tool AdaptiveColor `json:"tool" yaml:"tool"`
Accent AdaptiveColor `json:"accent" yaml:"accent"`
Highlight AdaptiveColor `json:"highlight" yaml:"highlight"`
}
type MarkdownTheme struct {
Text AdaptiveColor `json:"text" yaml:"text"`
Muted AdaptiveColor `json:"muted" yaml:"muted"`
Heading AdaptiveColor `json:"heading" yaml:"heading"`
Emph AdaptiveColor `json:"emph" yaml:"emph"`
Strong AdaptiveColor `json:"strong" yaml:"strong"`
Link AdaptiveColor `json:"link" yaml:"link"`
Code AdaptiveColor `json:"code" yaml:"code"`
Error AdaptiveColor `json:"error" yaml:"error"`
Keyword AdaptiveColor `json:"keyword" yaml:"keyword"`
String AdaptiveColor `json:"string" yaml:"string"`
Number AdaptiveColor `json:"number" yaml:"number"`
Comment AdaptiveColor `json:"comment" yaml:"comment"`
}
// Config represents the application configuration
type Config struct {
MCPServers map[string]MCPServerConfig `json:"mcpServers" yaml:"mcpServers"`
@@ -110,6 +152,8 @@ type Config struct {
Prompt string `json:"prompt,omitempty" yaml:"prompt,omitempty"`
NoExit bool `json:"no-exit,omitempty" yaml:"no-exit,omitempty"`
Stream *bool `json:"stream,omitempty" yaml:"stream,omitempty"`
Theme any `json:"theme" yaml:"theme"`
MarkdownTheme any `json:"markdown-theme" yaml:"markdown-theme"`
// Model generation parameters
MaxTokens int `json:"max-tokens,omitempty" yaml:"max-tokens,omitempty"`
@@ -333,3 +377,57 @@ mcpServers:
return nil
}
func FilepathOr[T any](key string, value *T) error {
var field any
err := viper.UnmarshalKey(key, &field)
if err != nil {
value = nil
return err
}
switch f := field.(type) {
case string:
{
absPath := f
if strings.HasPrefix(absPath, "~/") {
home, err := os.UserHomeDir()
if err != nil {
return err
}
filepath.Join(home, absPath[2:])
}
if !filepath.IsAbs(absPath) {
// base := GetConfigPath()
base := configPath
if base == "" {
fmt.Fprintf(os.Stderr, "unable to build relative path to config.")
os.Exit(1)
}
absPath = filepath.Join(filepath.Dir(base), absPath)
}
b, err := os.ReadFile(absPath)
if err != nil {
fmt.Fprintf(os.Stderr, "%q", err)
os.Exit(1)
}
if filepath.Ext(absPath) == ".json" {
return json.Unmarshal(b, value)
}
if filepath.Ext(absPath) == ".yaml" {
return yaml.Unmarshal(b, value)
}
}
case map[string]any:
return viper.UnmarshalKey(key, value)
default:
return fmt.Errorf("invalid type for field %q", key)
}
return nil
}
var configPath string
func SetConfigPath(path string) {
configPath = path
}
+37 -8
View File
@@ -4,6 +4,8 @@ import (
"github.com/charmbracelet/glamour"
"github.com/charmbracelet/glamour/ansi"
"github.com/charmbracelet/lipgloss"
"github.com/mark3labs/mcphost/internal/config"
"github.com/spf13/viper"
)
const defaultMargin = 1
@@ -29,17 +31,42 @@ func GetMarkdownRenderer(width int) *glamour.TermRenderer {
// generateMarkdownStyleConfig creates an ansi.StyleConfig for markdown rendering
func generateMarkdownStyleConfig() ansi.StyleConfig {
// Define adaptive colors based on terminal background
var textColor, mutedColor string
if lipgloss.HasDarkBackground() {
var headingColor, emphColor, strongColor, linkColor, codeColor, errorColor, keywordColor, stringColor, numberColor, commentColor string
var mdTheme config.MarkdownTheme
err := config.FilepathOr("markdown-theme", &mdTheme)
fromConfig := err == nil && viper.InConfig("markdown-theme")
if fromConfig && lipgloss.HasDarkBackground() {
textColor = mdTheme.Text.Light
mutedColor = mdTheme.Muted.Light
headingColor = mdTheme.Heading.Light
emphColor = mdTheme.Emph.Light
strongColor = mdTheme.Strong.Light
linkColor = mdTheme.Link.Light
codeColor = mdTheme.Code.Light
errorColor = mdTheme.Error.Light
keywordColor = mdTheme.Keyword.Light
stringColor = mdTheme.String.Light
numberColor = mdTheme.Number.Light
commentColor = mdTheme.Comment.Light
} else if fromConfig {
textColor = mdTheme.Text.Dark
mutedColor = mdTheme.Muted.Dark
headingColor = mdTheme.Heading.Dark
emphColor = mdTheme.Emph.Dark
strongColor = mdTheme.Strong.Dark
linkColor = mdTheme.Link.Dark
codeColor = mdTheme.Code.Dark
errorColor = mdTheme.Error.Dark
keywordColor = mdTheme.Keyword.Dark
stringColor = mdTheme.String.Dark
numberColor = mdTheme.Number.Dark
commentColor = mdTheme.Comment.Dark
} else if lipgloss.HasDarkBackground() {
textColor = "#F9FAFB" // Light text for dark backgrounds
mutedColor = "#9CA3AF" // Light muted for dark backgrounds
} else {
textColor = "#1F2937" // Dark text for light backgrounds
mutedColor = "#6B7280" // Dark muted for light backgrounds
}
var headingColor, emphColor, strongColor, linkColor, codeColor, errorColor, keywordColor, stringColor, numberColor, commentColor string
if lipgloss.HasDarkBackground() {
// Dark background colors
headingColor = "#22D3EE" // Cyan
emphColor = "#FDE047" // Yellow
@@ -52,6 +79,8 @@ func generateMarkdownStyleConfig() ansi.StyleConfig {
numberColor = "#FBBF24" // Orange
commentColor = "#9CA3AF" // Muted gray
} else {
textColor = "#1F2937" // Dark text for light backgrounds
mutedColor = "#6B7280" // Dark muted for light backgrounds
// Light background colors
headingColor = "#0891B2" // Dark cyan
emphColor = "#D97706" // Orange