diff --git a/docs/presentation.md b/docs/presentation.md index 8e415c5..055c4e2 100644 --- a/docs/presentation.md +++ b/docs/presentation.md @@ -3,6 +3,7 @@ title: Welcome image_backend: docs style: border: hidden + theme: dark --- ![img|43x10](kyma_logo.png) @@ -90,6 +91,7 @@ style: title: Style usage style: border: hidden + theme: dracula transition: swipeLeft --- @@ -110,7 +112,8 @@ style: ---- --- title: Config -preset: dark +style: + theme: dracula --- # Configuration @@ -143,6 +146,8 @@ presets: ---- --- title: Global styles +style: + theme: dracula --- # Global styles @@ -175,6 +180,8 @@ presets: ---- --- title: Presets +style: + theme: dracula --- # Presets @@ -207,6 +214,8 @@ presets: ---- --- title: More ways to navigate +style: + theme: dracula --- # More ways to navigate @@ -221,6 +230,8 @@ title: More ways to navigate title: Achievements transition: swipeLeft image_backend: docs +style: + theme: dracula --- # Achievements diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 7a1bf3a..21212fa 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -5,7 +5,12 @@ import ( "path/filepath" "testing" + glamourStyles "github.com/charmbracelet/glamour/styles" + "github.com/charmbracelet/lipgloss" + "github.com/goccy/go-yaml" "github.com/spf13/viper" + + "github.com/museslabs/kyma/internal/tui/transitions" ) func TestLoad(t *testing.T) { @@ -234,3 +239,282 @@ func TestCreateDefaultConfig(t *testing.T) { ) } } + +func TestPrecedence(t *testing.T) { + tmpDir := t.TempDir() + + testConfig := `global: + style: + border: rounded + border_color: "#FF0000" + layout: center + theme: dark + +presets: + test: + style: + border: hidden + theme: notty + border_color: "#fff" + layout: center +` + testConfigPath := filepath.Join(tmpDir, "kyma.yaml") + if err := os.WriteFile(testConfigPath, []byte(testConfig), 0644); err != nil { + t.Fatalf("Failed to write test config: %v", err) + } + + if err := Load(testConfigPath); err != nil { + t.Fatalf("Load() error = %v", err) + } + + tests := []struct { + name string + properties string + want Properties + }{ + { + name: "slide properties should override global ones", + properties: `style: +border: hidden +border_color: "#000" +layout: left +theme: dracula`, + want: Properties{ + Title: "", + Style: StyleConfig{ + Layout: func() *lipgloss.Style { + s := lipgloss.NewStyle().Align(lipgloss.Left, lipgloss.Left) + return &s + }(), + Border: func() *lipgloss.Border { + b := lipgloss.HiddenBorder() + return &b + }(), + BorderColor: "#000", + Theme: &GlamourTheme{ + Style: *glamourStyles.DefaultStyles["dracula"], + Name: "dracula", + }, + }, + Transition: transitions.Get("none", transitions.Fps), + Notes: "", + ImageBackend: "chafa", + }, + }, + { + name: "border from default styles", + properties: `style: +border_color: "#000" +layout: left +theme: dracula`, + want: Properties{ + Title: "", + Style: StyleConfig{ + Layout: func() *lipgloss.Style { + s := lipgloss.NewStyle().Align(lipgloss.Left, lipgloss.Left) + return &s + }(), + Border: func() *lipgloss.Border { + b := lipgloss.RoundedBorder() + return &b + }(), + BorderColor: "#000", + Theme: &GlamourTheme{ + Style: *glamourStyles.DefaultStyles["dracula"], + Name: "dracula", + }, + }, + Transition: transitions.Get("none", transitions.Fps), + Notes: "", + ImageBackend: "chafa", + }, + }, + { + name: "border color from default styles", + properties: `style: +border: hidden +layout: left +theme: dracula`, + want: Properties{ + Title: "", + Style: StyleConfig{ + Layout: func() *lipgloss.Style { + s := lipgloss.NewStyle().Align(lipgloss.Left, lipgloss.Left) + return &s + }(), + Border: func() *lipgloss.Border { + b := lipgloss.HiddenBorder() + return &b + }(), + BorderColor: "#FF0000", + Theme: &GlamourTheme{ + Style: *glamourStyles.DefaultStyles["dracula"], + Name: "dracula", + }, + }, + Transition: transitions.Get("none", transitions.Fps), + Notes: "", + ImageBackend: "chafa", + }, + }, + { + name: "layout from default styles", + properties: `style: +border: hidden +border_color: "#000" +theme: dracula`, + want: Properties{ + Title: "", + Style: StyleConfig{ + Layout: func() *lipgloss.Style { + s := lipgloss.NewStyle().Align(lipgloss.Center, lipgloss.Center) + return &s + }(), + Border: func() *lipgloss.Border { + b := lipgloss.HiddenBorder() + return &b + }(), + BorderColor: "#000", + Theme: &GlamourTheme{ + Style: *glamourStyles.DefaultStyles["dracula"], + Name: "dracula", + }, + }, + Transition: transitions.Get("none", transitions.Fps), + Notes: "", + ImageBackend: "chafa", + }, + }, + { + name: "theme from default styles", + properties: `style: +border: hidden +border_color: "#000" +layout: left`, + want: Properties{ + Title: "", + Style: StyleConfig{ + Layout: func() *lipgloss.Style { + s := lipgloss.NewStyle().Align(lipgloss.Left, lipgloss.Left) + return &s + }(), + Border: func() *lipgloss.Border { + b := lipgloss.HiddenBorder() + return &b + }(), + BorderColor: "#000", + Theme: &GlamourTheme{ + Style: *glamourStyles.DefaultStyles["dark"], + Name: "dark", + }, + }, + Transition: transitions.Get("none", transitions.Fps), + Notes: "", + ImageBackend: "chafa", + }, + }, + { + name: "use a preset", + properties: `preset: test`, + want: Properties{ + Title: "", + Style: StyleConfig{ + Layout: func() *lipgloss.Style { + s := lipgloss.NewStyle().Align(lipgloss.Center, lipgloss.Center) + return &s + }(), + Border: func() *lipgloss.Border { + b := lipgloss.HiddenBorder() + return &b + }(), + BorderColor: "#fff", + Theme: &GlamourTheme{ + Style: *glamourStyles.DefaultStyles["notty"], + Name: "notty", + }, + }, + Transition: transitions.Get("none", transitions.Fps), + Notes: "", + ImageBackend: "chafa", + }, + }, + { + name: "use a preset and override border", + properties: `preset: test +style: + border: rounded`, + want: Properties{ + Title: "", + Style: StyleConfig{ + Layout: func() *lipgloss.Style { + s := lipgloss.NewStyle().Align(lipgloss.Center, lipgloss.Center) + return &s + }(), + Border: func() *lipgloss.Border { + b := lipgloss.RoundedBorder() + return &b + }(), + BorderColor: "#fff", + Theme: &GlamourTheme{ + Style: *glamourStyles.DefaultStyles["notty"], + Name: "notty", + }, + }, + Transition: transitions.Get("none", transitions.Fps), + Notes: "", + ImageBackend: "chafa", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var p Properties + if err := yaml.Unmarshal([]byte(tt.properties), &p); err != nil { + t.Fatalf("yaml.Unmarshal() error = %v", err) + } + + if p.Title != tt.want.Title { + t.Errorf("p.Title = %s, want = %s", p.Title, tt.want.Title) + } + + if p.Transition != tt.want.Transition { + t.Errorf("p.Transition = %s, want = %s", p.Transition, tt.want.Transition) + } + + if p.Notes != tt.want.Notes { + t.Errorf("p.Notes = %s, want = %s", p.Notes, tt.want.Notes) + } + + if p.ImageBackend != tt.want.ImageBackend { + t.Errorf("p.ImageBackend = %s, want = %s", p.ImageBackend, tt.want.ImageBackend) + } + + if p.Style.BorderColor != tt.want.Style.BorderColor { + t.Errorf( + "p.Style.BorderColor = %s, want = %s", + p.Style.BorderColor, + tt.want.Style.BorderColor, + ) + } + + if *p.Style.Border != *tt.want.Style.Border { + t.Errorf("p.Style.Border = %v, want = %v", p.Style.Border, tt.want.Style.Border) + } + + if *p.Style.Theme != *tt.want.Style.Theme { + t.Errorf("p.Style.Theme = %v, want = %v", p.Style.Theme, tt.want.Style.Theme) + } + + if p.Style.Layout.GetAlignHorizontal() != tt.want.Style.Layout.GetAlignHorizontal() || + p.Style.Layout.GetAlignVertical() != tt.want.Style.Layout.GetAlignVertical() { + t.Errorf( + "p.Style.Layout = %f, %f, want = %f, %f", + p.Style.Layout.GetAlignHorizontal(), + p.Style.Layout.GetAlignVertical(), + tt.want.Style.Layout.GetAlignHorizontal(), + tt.want.Style.Layout.GetAlignVertical(), + ) + } + }) + } +} diff --git a/internal/config/style.go b/internal/config/style.go index 6e69bd9..fb7c252 100644 --- a/internal/config/style.go +++ b/internal/config/style.go @@ -41,10 +41,25 @@ type GlamourTheme struct { } type StyleConfig struct { - Layout lipgloss.Style `yaml:"layout"` - Border lipgloss.Border `yaml:"border"` - BorderColor string `yaml:"border_color"` - Theme GlamourTheme `yaml:"theme"` + Layout *lipgloss.Style `yaml:"layout"` + Border *lipgloss.Border `yaml:"border"` + BorderColor string `yaml:"border_color"` + Theme *GlamourTheme `yaml:"theme"` +} + +func (s *StyleConfig) Merge(style StyleConfig) { + if style.Layout != nil { + s.Layout = style.Layout + } + if style.Border != nil { + s.Border = style.Border + } + if style.BorderColor != "" { + s.BorderColor = style.BorderColor + } + if style.Theme != nil { + s.Theme = style.Theme + } } func (s *StyleConfig) DecodeMap(input map[string]any) error { @@ -59,12 +74,18 @@ func (s *StyleConfig) DecodeMap(input map[string]any) error { return err } - var err error - s.Layout, err = getLayout(aux.Layout) + layout, err, ok := getLayout(aux.Layout) if err != nil { return err } - s.Border = getBorder(aux.Border) + if ok { + s.Layout = &layout + } + + if border, ok := getBorder(aux.Border); ok { + s.Border = &border + } + s.BorderColor = aux.BorderColor s.Theme = getTheme(aux.Theme) @@ -85,12 +106,18 @@ func (s *StyleConfig) UnmarshalYAML(bytes []byte) error { return err } - s.Layout, err = getLayout(aux.Layout) + layout, err, ok := getLayout(aux.Layout) if err != nil { return err } + if ok { + s.Layout = &layout + } + + if border, ok := getBorder(aux.Border); ok { + s.Border = &border + } - s.Border = getBorder(aux.Border) s.BorderColor = aux.BorderColor s.Theme = getTheme(aux.Theme) @@ -100,8 +127,13 @@ func (s *StyleConfig) UnmarshalYAML(bytes []byte) error { func (s StyleConfig) Apply(width, height int) SlideStyle { borderColor := DefaultBorderColor - if s.Theme.Style.H1.BackgroundColor != nil { - borderColor = *s.Theme.Style.H1.BackgroundColor + theme := GlamourTheme{Style: glamourStyles.DarkStyleConfig, Name: "dark"} + if s.Theme != nil { + theme = *s.Theme + } + + if theme.Style.H1.BackgroundColor != nil { + borderColor = *theme.Style.H1.BackgroundColor } if s.BorderColor != "" { @@ -112,88 +144,96 @@ func (s StyleConfig) Apply(width, height int) SlideStyle { borderColor = DefaultBorderColor } - style := s.Layout. - Border(s.Border). + layout := lipgloss.NewStyle() + if s.Layout != nil { + layout = *s.Layout + } + + border := lipgloss.RoundedBorder() + if s.Border != nil { + border = *s.Border + } + + style := layout. + Border(border). BorderForeground(lipgloss.Color(borderColor)). Width(width - 2). Height(height - 2) return SlideStyle{ LipGlossStyle: style, - Theme: s.Theme, + Theme: theme, } } -func getBorder(border string) lipgloss.Border { +func getBorder(border string) (lipgloss.Border, bool) { switch border { case "rounded": - return lipgloss.RoundedBorder() + return lipgloss.RoundedBorder(), true case "double": - return lipgloss.DoubleBorder() + return lipgloss.DoubleBorder(), true case "thick": - return lipgloss.ThickBorder() + return lipgloss.ThickBorder(), true case "hidden": - return lipgloss.HiddenBorder() + return lipgloss.HiddenBorder(), true case "block": - return lipgloss.BlockBorder() + return lipgloss.BlockBorder(), true case "innerHalfBlock": - return lipgloss.InnerHalfBlockBorder() + return lipgloss.InnerHalfBlockBorder(), true case "outerHalfBlock": - return lipgloss.OuterHalfBlockBorder() + return lipgloss.OuterHalfBlockBorder(), true case "normal": - return lipgloss.NormalBorder() + return lipgloss.NormalBorder(), true default: - return lipgloss.Border{} + return lipgloss.Border{}, false } } -func getLayout(layout string) (lipgloss.Style, error) { - style := lipgloss.NewStyle() - +func getLayout(layout string) (lipgloss.Style, error, bool) { layout = strings.TrimSpace(layout) if layout == "" { - return style, nil + return lipgloss.Style{}, nil, false } positions := strings.Split(layout, ",") if len(positions) > 2 { - return style, fmt.Errorf("invalid layout configuration: %s", layout) + return lipgloss.Style{}, fmt.Errorf("invalid layout configuration: %s", layout), false } p1, err := getLayoutPosition(positions[0]) if err != nil { - return style, err + return lipgloss.Style{}, err, false } if len(positions) == 1 { - return style.Align(p1, p1), nil + return lipgloss.NewStyle().Align(p1, p1), nil, true } p2, err := getLayoutPosition(positions[1]) if err != nil { - return style, err + return lipgloss.Style{}, err, false } - return style.Align(p1, p2), nil + return lipgloss.NewStyle().Align(p1, p2), nil, true } -func getTheme(theme string) GlamourTheme { +func getTheme(theme string) *GlamourTheme { style, ok := glamourStyles.DefaultStyles[theme] if !ok { jsonBytes, err := os.ReadFile(theme) if err != nil { - return GlamourTheme{Style: glamourStyles.DarkStyleConfig, Name: "dark"} + return nil } var customStyle ansi.StyleConfig if err := json.Unmarshal(jsonBytes, &customStyle); err != nil { - return GlamourTheme{Style: glamourStyles.DarkStyleConfig, Name: "dark"} + return nil } - return GlamourTheme{Style: customStyle, Name: theme} + return &GlamourTheme{Style: customStyle, Name: theme} } - return GlamourTheme{Style: *style, Name: theme} + return &GlamourTheme{Style: *style, Name: theme} } func getLayoutPosition(p string) (lipgloss.Position, error) { @@ -223,6 +263,9 @@ func (p *Properties) UnmarshalYAML(bytes []byte) error { ImageBackend string `yaml:"image_backend"` }{} + if err := aux.Style.UnmarshalYAML(bytes); err != nil { + return err + } if err := yaml.Unmarshal(bytes, &aux); err != nil { return err } @@ -236,26 +279,16 @@ func (p *Properties) UnmarshalYAML(bytes []byte) error { if !ok { return fmt.Errorf("preset %s does not exist", aux.Preset) } + preset.Style.Merge(aux.Style) p.Style = preset.Style p.Transition = preset.Transition } else { - p.Style = aux.Style + style := GlobalConfig.Global.Style + style.Merge(aux.Style) + p.Style = style p.Transition = transitions.Get(aux.Transition, transitions.Fps) } - if p.Style.Layout.GetAlignHorizontal() == lipgloss.Left || - p.Style.Layout.GetAlignVertical() == lipgloss.Top { // The default - p.Style.Layout = GlobalConfig.Global.Style.Layout - } - if p.Style.Border == (lipgloss.Border{}) { - p.Style.Border = GlobalConfig.Global.Style.Border - } - if p.Style.BorderColor == "" { - p.Style.BorderColor = GlobalConfig.Global.Style.BorderColor - } - if p.Style.Theme.Name == "" { - p.Style.Theme = GlobalConfig.Global.Style.Theme - } if p.Transition == nil { p.Transition = GlobalConfig.Global.Transition } @@ -327,7 +360,11 @@ func GetChromaStyle(themeName string) *chroma.Style { return chromaStyle } - styleConfig := getTheme(themeName) + styleConfig := GlamourTheme{Style: glamourStyles.DarkStyleConfig, Name: "dark"} + if theme := getTheme(themeName); theme != nil { + styleConfig = *theme + } + style := styleConfig.Style if style.CodeBlock.Chroma != nil { diff --git a/internal/tui/slide.go b/internal/tui/slide.go index 6fc0e74..8494fd8 100644 --- a/internal/tui/slide.go +++ b/internal/tui/slide.go @@ -28,7 +28,7 @@ type UpdateSlidesMsg struct { func NewSlide(data string, props config.Properties) (*Slide, error) { themeName := "dark" - if props.Style.Theme.Name != "" { + if props.Style.Theme != nil && props.Style.Theme.Name != "" { themeName = props.Style.Theme.Name }