Skip to content
Merged
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: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ require (
github.com/bnema/zerowrap v1.4.0
github.com/charmbracelet/x/ansi v0.11.7
github.com/cli/go-gh/v2 v2.13.0
github.com/fsnotify/fsnotify v1.9.0
github.com/go-git/go-git/v5 v5.19.1
github.com/godbus/dbus/v5 v5.2.2
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/pelletier/go-toml/v2 v2.2.4
github.com/spf13/cobra v1.10.2
Expand Down Expand Up @@ -43,7 +45,6 @@ require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dlclark/regexp2 v1.12.0 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.9.0 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ github.com/go-git/go-git/v5 v5.19.1/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhE
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
Expand Down
9 changes: 9 additions & 0 deletions internal/adapters/in/cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,15 @@ func NewRootCommand(cfg *viper.Viper, run RunFunc) (*cobra.Command, error) {
flags.Int("context-lines", 3, "Number of unchanged context lines to keep around changes")
flags.String("log-level", "info", "Log level (trace, debug, info, warn, error, disabled)")
flags.String("log-file", "", "Write logs to this file instead of the default XDG state log")
flags.String("theme", "auto", "Theme mode: auto, dark, or light")
flags.String("config", "", "Path to a config file (defaults to XDG config when present)")
flags.Duration("provider-sync-interval", 2*time.Minute, "Interval for active review provider background sync")
cfg.SetDefault("repo-path", ".")
cfg.SetDefault("context-lines", 3)
cfg.SetDefault("diff-mode", string(core.DiffModeBranch))
cfg.SetDefault("startup-detect", true)
cfg.SetDefault("log-level", "info")
cfg.SetDefault("theme", "auto")
cfg.SetDefault("provider-sync-interval", 2*time.Minute)
if err := cfg.BindPFlag("repo-path", flags.Lookup("repo-path")); err != nil {
return nil, fmt.Errorf("bind repo-path flag: %w", err)
Expand All @@ -63,6 +66,12 @@ func NewRootCommand(cfg *viper.Viper, run RunFunc) (*cobra.Command, error) {
if err := cfg.BindPFlag("log-file", flags.Lookup("log-file")); err != nil {
return nil, fmt.Errorf("bind log-file flag: %w", err)
}
if err := cfg.BindPFlag("theme", flags.Lookup("theme")); err != nil {
return nil, fmt.Errorf("bind theme flag: %w", err)
}
if err := cfg.BindPFlag("config", flags.Lookup("config")); err != nil {
return nil, fmt.Errorf("bind config flag: %w", err)
}
if err := cfg.BindPFlag("provider-sync-interval", flags.Lookup("provider-sync-interval")); err != nil {
return nil, fmt.Errorf("bind provider-sync-interval flag: %w", err)
}
Expand Down
7 changes: 5 additions & 2 deletions internal/adapters/in/cli/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@ func TestNewRootCommandExecutesRunFuncAndBindsConfig(t *testing.T) {
args []string
expectRepo string
expectContext int
expectTheme string
}{
{
name: "binds repo path and context lines",
args: []string{"--repo-path", "/tmp/repo", "--context-lines", "2"},
name: "binds repo path, context lines, and theme",
args: []string{"--repo-path", "/tmp/repo", "--context-lines", "2", "--theme", "light"},
expectRepo: "/tmp/repo",
expectContext: 2,
expectTheme: "light",
},
}

Expand Down Expand Up @@ -52,6 +54,7 @@ func TestNewRootCommandExecutesRunFuncAndBindsConfig(t *testing.T) {
assert.True(t, called)
assert.Equal(t, tt.expectRepo, cfg.GetString("repo-path"))
assert.Equal(t, tt.expectContext, cfg.GetInt("context-lines"))
assert.Equal(t, tt.expectTheme, cfg.GetString("theme"))
assert.Equal(t, string(core.DiffModeBranch), cfg.GetString("diff-mode"))
})
}
Expand Down
98 changes: 98 additions & 0 deletions internal/adapters/in/systemtheme/portal.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package systemtheme

import (
"context"
"errors"
"fmt"

"github.com/godbus/dbus/v5"

"ero/internal/core"
)

const (
portalBusName = "org.freedesktop.portal.Desktop"
portalObject = "/org/freedesktop/portal/desktop"
portalInterface = "org.freedesktop.portal.Settings"
portalNamespace = "org.freedesktop.appearance"
portalKey = "color-scheme"
)

// PortalReader reads the desktop light/dark preference from the XDG Desktop
// Portal settings API.
type PortalReader struct {
Connect func() (*dbus.Conn, error)
}

func (r PortalReader) CurrentPreference(ctx context.Context) (core.SystemThemePreference, error) {
conn, err := r.connect()
if err != nil {
return core.SystemThemeUnknown, err
}
defer func() { _ = conn.Close() }()
value, err := readPortalColorScheme(ctx, conn.Object(portalBusName, dbus.ObjectPath(portalObject)))
if err != nil {
return core.SystemThemeUnknown, err
}
return core.ParseSystemThemePreference(value), nil
}

func (r PortalReader) connect() (*dbus.Conn, error) {
if r.Connect != nil {
return r.Connect()
}
return dbus.ConnectSessionBus()
}

type portalCaller interface {
CallWithContext(ctx context.Context, method string, flags dbus.Flags, args ...any) *dbus.Call
}

func readPortalColorScheme(ctx context.Context, obj portalCaller) (uint32, error) {
var value dbus.Variant
err := obj.CallWithContext(ctx, portalInterface+".ReadOne", 0, portalNamespace, portalKey).Store(&value)
if err == nil {
return variantUint32(value)
}
if isUnknownMethodError(err) {
if err := obj.CallWithContext(ctx, portalInterface+".Read", 0, portalNamespace, portalKey).Store(&value); err != nil {
return 0, err
}
return variantUint32(value)
}
return 0, err
}

func isUnknownMethodError(err error) bool {
var dbusErr dbus.Error
if !errors.As(err, &dbusErr) {
return false
}
return dbusErr.Name == "org.freedesktop.DBus.Error.UnknownMethod"
}

func variantUint32(variant dbus.Variant) (uint32, error) {
value := variant.Value()
for {
switch typed := value.(type) {
case uint32:
return typed, nil
case dbus.Variant:
value = typed.Value()
case uint:
return uint32(typed), nil
case int32:
if typed < 0 {
return 0, fmt.Errorf("portal color-scheme variant was negative: %d", typed)
}
return uint32(typed), nil
case int:
if typed < 0 {
return 0, fmt.Errorf("portal color-scheme variant was negative: %d", typed)
}
return uint32(typed), nil
default:
return 0, fmt.Errorf("portal color-scheme variant carried %T, want uint32", value)
}
}
}
17 changes: 11 additions & 6 deletions internal/adapters/in/tui/comment_editor.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,18 @@ import (
"charm.land/bubbles/v2/textarea"
tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
)

var (
commentEditorStyle = lipgloss.NewStyle().Border(lipgloss.RoundedBorder()).BorderForeground(lipgloss.Color("62")).Padding(0, 1)
commentEditorTitleStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("81"))
"ero/internal/adapters/in/tui/theme"
)

func commentEditorStyle() lipgloss.Style {
return theme.SearchPaneStyle
}

func commentEditorTitleStyle() lipgloss.Style {
return theme.SearchPaneTitleStyle
}

type CommentEditorAction string

const (
Expand Down Expand Up @@ -76,9 +81,9 @@ func (e CommentEditor) ViewWithWidth(availableWidth int) string {
input := e.input
input.SetWidth(max(width-4, 20))
lines := []string{
commentEditorTitleStyle.Render("Add review comment"),
commentEditorTitleStyle().Render("Add review comment"),
input.View(),
renderKeyHints([]KeyHint{{Key: commentSubmitKeyLabel(), Label: "submit"}, {Key: "esc", Label: "cancel"}}),
}
return commentEditorStyle.Width(width).Render(strings.Join(lines, "\n"))
return commentEditorStyle().Width(width).Render(strings.Join(lines, "\n"))
}
14 changes: 7 additions & 7 deletions internal/adapters/in/tui/component/statusbar.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,12 +211,12 @@ func renderNerdFontProviderSync(model StatusModel) string {
}
var b strings.Builder
b.WriteString(theme.StatusBaseStyle.Render(" "))
b.WriteString(theme.StatusBaseStyle.Foreground(lipgloss.Color("248")).Render(providerGlyph(provider)))
b.WriteString(theme.StatusBaseStyle.Foreground(lipgloss.Color(theme.ColorStatusInfo)).Render(providerGlyph(provider)))
b.WriteString(theme.StatusBaseStyle.Render(" "))
b.WriteString(theme.StatusBaseStyle.Foreground(providerStatusDotColor(model.ProviderSync.Status)).Render(nerdFontSyncDot))
for _, part := range nerdFontProviderSyncTextParts(model) {
b.WriteString(theme.StatusBaseStyle.Render(" "))
b.WriteString(theme.StatusBaseStyle.Foreground(lipgloss.Color("248")).Render(part))
b.WriteString(theme.StatusBaseStyle.Foreground(lipgloss.Color(theme.ColorStatusInfo)).Render(part))
}
b.WriteString(theme.StatusBaseStyle.Render(" "))
return b.String()
Expand Down Expand Up @@ -250,15 +250,15 @@ func providerGlyph(provider string) string {
func providerStatusDotColor(status core.ProviderSyncStatus) color.Color {
switch status {
case core.ProviderSyncStatusSynced:
return lipgloss.Color("#3fb950")
return lipgloss.Color(theme.CurrentPalette().AddedMarkerFg)
case core.ProviderSyncStatusFailed:
return lipgloss.Color("#ff7b72")
return lipgloss.Color(theme.CurrentPalette().DeletedMarkerFg)
case core.ProviderSyncStatusBackingOff:
return lipgloss.Color("#ffa657")
return lipgloss.Color(theme.ColorWarning)
case core.ProviderSyncStatusLoadingCache, core.ProviderSyncStatusSyncing:
return lipgloss.Color("#58a6ff")
return lipgloss.Color(theme.ColorAccent)
default:
return lipgloss.Color("81")
return lipgloss.Color(theme.ColorAccent)
}
}

Expand Down
35 changes: 26 additions & 9 deletions internal/adapters/in/tui/markdown_renderer.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
glamouransi "charm.land/glamour/v2/ansi"

"ero/internal/adapters/in/tui/theme"
"ero/internal/core"
)

type MarkdownTheme string
Expand All @@ -19,6 +20,13 @@ const (
MarkdownThemeLight MarkdownTheme = "light"
)

func markdownThemeForAppearance(appearance core.ThemeAppearance) MarkdownTheme {
if appearance == core.ThemeAppearanceLight {
return MarkdownThemeLight
}
return MarkdownThemeDark
}

type markdownTermRenderer interface {
Render(markdown string) (string, error)
}
Expand Down Expand Up @@ -57,6 +65,14 @@ func NewMarkdownRendererWithFactory(factory markdownRendererFactory) *MarkdownRe
}
}

func (r *MarkdownRenderer) Clear() {
if r == nil {
return
}
r.renderers = map[markdownRendererConfig]markdownTermRenderer{}
r.entries = map[markdownRendererCacheKey]string{}
}

func (r *MarkdownRenderer) Render(markdown string, width int, theme MarkdownTheme) string {
if r == nil {
return safeMarkdownFallback(markdown)
Expand Down Expand Up @@ -113,16 +129,17 @@ func newGlamourTermRenderer(width int, theme MarkdownTheme) (markdownTermRendere
}

func eroMarkdownStyle(markdownTheme MarkdownTheme) glamouransi.StyleConfig {
text := theme.ColorStatusInfo
muted := theme.ColorMutedText
heading := theme.ColorAccent
section := theme.ColorWarning
codeBackground := theme.ColorCodeBg
appearance := core.ThemeAppearanceDark
if markdownTheme == MarkdownThemeLight {
text = theme.ColorStatusBase
muted = "244"
codeBackground = "#f6f8fa"
appearance = core.ThemeAppearanceLight
}
palette := theme.PaletteForAppearance(appearance)
text := palette.ColorStatusInfo
muted := palette.ColorMutedText
heading := palette.ColorAccent
section := palette.ColorWarning
codeBackground := palette.ColorCodeBg
codeTheme := palette.MarkdownCodeTheme
bold := true
italic := true
underline := true
Expand All @@ -144,7 +161,7 @@ func eroMarkdownStyle(markdownTheme MarkdownTheme) glamouransi.StyleConfig {
Link: glamouransi.StylePrimitive{Color: &heading, Underline: &underline},
LinkText: glamouransi.StylePrimitive{Color: &heading, Underline: &underline},
Code: glamouransi.StyleBlock{StylePrimitive: glamouransi.StylePrimitive{Color: &section, BackgroundColor: &codeBackground}},
CodeBlock: glamouransi.StyleCodeBlock{StyleBlock: glamouransi.StyleBlock{StylePrimitive: glamouransi.StylePrimitive{Color: &text, BackgroundColor: &codeBackground}, Margin: &zeroIndent}, Theme: "github-dark"},
CodeBlock: glamouransi.StyleCodeBlock{StyleBlock: glamouransi.StyleBlock{StylePrimitive: glamouransi.StylePrimitive{Color: &text, BackgroundColor: &codeBackground}, Margin: &zeroIndent}, Theme: codeTheme},
BlockQuote: glamouransi.StyleBlock{StylePrimitive: glamouransi.StylePrimitive{Color: &muted}, Indent: &quoteIndent, IndentToken: &quoteToken},
}
}
Expand Down
16 changes: 16 additions & 0 deletions internal/adapters/in/tui/markdown_renderer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,22 @@ func TestMarkdownRendererColorsHeadingsAndFencedCodeBlocks(t *testing.T) {
}
}

func TestMarkdownRendererUsesLightCodeTheme(t *testing.T) {
renderer := NewMarkdownRenderer()

got := renderer.Render("```go\nfmt.Println(\"hi\")\n```", 80, MarkdownThemeLight)
plain := regexp.MustCompile(`\x1b\[[0-9;?]*[ -/]*[@-~]`).ReplaceAllString(got, "")

if !strings.Contains(plain, "fmt.Println") {
t.Fatalf("expected rendered fenced code block to include code, got %q", got)
}
// These ANSI colors come from the current light Chroma style; update them if
// the light palette's MarkdownCodeTheme changes.
if !strings.Contains(got, "38;5;61") || !strings.Contains(got, "38;5;23") {
t.Fatalf("expected light code highlighting in rendered markdown, got %q", got)
}
}

func TestMarkdownRendererRendersFencedCodeBlocks(t *testing.T) {
renderer := NewMarkdownRenderer()

Expand Down
Loading
Loading