diff --git a/cmd/root.go b/cmd/root.go index 7889fb6e..42fe4bb7 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -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)") diff --git a/internal/config/config.go b/internal/config/config.go index 466c7b41..b5f5062f 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -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 +} diff --git a/internal/ui/styles.go b/internal/ui/styles.go index 0131329e..17b0c622 100644 --- a/internal/ui/styles.go +++ b/internal/ui/styles.go @@ -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