refactor(gui): make theme override declarative
This commit is contained in:
parent
bca53679f1
commit
29dc0d9877
7 changed files with 150 additions and 23 deletions
|
|
@ -51,9 +51,9 @@ type Service struct {
|
|||
events *WSEventManager
|
||||
}
|
||||
|
||||
// New is the constructor for the display service.
|
||||
// Use: svc, err := display.New()
|
||||
func New() (*Service, error) {
|
||||
// NewService creates a display service with empty config sections.
|
||||
// Use: svc, err := display.NewService()
|
||||
func NewService() (*Service, error) {
|
||||
return &Service{
|
||||
configData: map[string]map[string]any{
|
||||
"window": {},
|
||||
|
|
@ -63,12 +63,17 @@ func New() (*Service, error) {
|
|||
}, nil
|
||||
}
|
||||
|
||||
// Deprecated: use NewService.
|
||||
func New() (*Service, error) {
|
||||
return NewService()
|
||||
}
|
||||
|
||||
// Register creates a factory closure that captures the Wails app.
|
||||
// Use: core.WithService(display.Register(app))
|
||||
// Pass nil for testing without a Wails runtime.
|
||||
func Register(wailsApp *application.App) func(*core.Core) (any, error) {
|
||||
return func(c *core.Core) (any, error) {
|
||||
s, err := New()
|
||||
s, err := NewService()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -1028,6 +1033,10 @@ func (s *Service) handleWSMessage(msg WSMessage) (any, bool, error) {
|
|||
case "theme:system":
|
||||
result, handled, err = s.GetSystemTheme(), true, nil
|
||||
case "theme:set":
|
||||
if theme, ok := msg.Data["theme"].(string); ok && theme != "" {
|
||||
result, handled, err = nil, true, s.SetThemeMode(theme)
|
||||
break
|
||||
}
|
||||
isDark, _ := msg.Data["isDark"].(bool)
|
||||
result, handled, err = nil, true, s.SetTheme(isDark)
|
||||
case "dialog:open-file":
|
||||
|
|
@ -2148,7 +2157,16 @@ func (s *Service) GetSystemTheme() string {
|
|||
// SetTheme overrides the application theme.
|
||||
// Use: _ = svc.SetTheme(true)
|
||||
func (s *Service) SetTheme(isDark bool) error {
|
||||
_, handled, err := s.Core().PERFORM(environment.TaskSetTheme{IsDark: isDark})
|
||||
if isDark {
|
||||
return s.SetThemeMode("dark")
|
||||
}
|
||||
return s.SetThemeMode("light")
|
||||
}
|
||||
|
||||
// SetThemeMode overrides the application theme using a declarative mode string.
|
||||
// Use: _ = svc.SetThemeMode("system")
|
||||
func (s *Service) SetThemeMode(theme string) error {
|
||||
_, handled, err := s.Core().PERFORM(environment.TaskSetTheme{Theme: theme})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1146,6 +1146,19 @@ func TestHandleWSMessage_Extended_Good(t *testing.T) {
|
|||
assert.True(t, handled)
|
||||
})
|
||||
|
||||
t.Run("theme set", func(t *testing.T) {
|
||||
_, handled, err := svc.handleWSMessage(WSMessage{
|
||||
Action: "theme:set",
|
||||
Data: map[string]any{"theme": "light"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, handled)
|
||||
|
||||
theme := svc.GetTheme()
|
||||
require.NotNil(t, theme)
|
||||
assert.False(t, theme.IsDark)
|
||||
})
|
||||
|
||||
t.Run("webview devtools", func(t *testing.T) {
|
||||
_, handled, err := svc.handleWSMessage(WSMessage{
|
||||
Action: "webview:devtools-open",
|
||||
|
|
|
|||
|
|
@ -17,8 +17,10 @@ type TaskOpenFileManager struct {
|
|||
}
|
||||
|
||||
// TaskSetTheme applies an application theme override when supported.
|
||||
// Theme values: "dark", "light", or "system".
|
||||
type TaskSetTheme struct {
|
||||
IsDark bool `json:"isDark"`
|
||||
Theme string `json:"theme,omitempty"`
|
||||
IsDark bool `json:"isDark,omitempty"`
|
||||
}
|
||||
|
||||
// ActionThemeChanged is broadcast when the system theme changes.
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package environment
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"forge.lthn.ai/core/go/pkg/core"
|
||||
)
|
||||
|
|
@ -56,10 +57,7 @@ func (s *Service) HandleIPCEvents(c *core.Core, msg core.Message) error {
|
|||
func (s *Service) handleQuery(c *core.Core, q core.Query) (any, bool, error) {
|
||||
switch q.(type) {
|
||||
case QueryTheme:
|
||||
isDark := s.platform.IsDarkMode()
|
||||
if s.overrideDark != nil {
|
||||
isDark = *s.overrideDark
|
||||
}
|
||||
isDark := s.currentTheme()
|
||||
theme := "light"
|
||||
if isDark {
|
||||
theme = "dark"
|
||||
|
|
@ -79,16 +77,51 @@ func (s *Service) handleTask(c *core.Core, t core.Task) (any, bool, error) {
|
|||
case TaskOpenFileManager:
|
||||
return nil, true, s.platform.OpenFileManager(t.Path, t.Select)
|
||||
case TaskSetTheme:
|
||||
isDark := t.IsDark
|
||||
s.overrideDark = &isDark
|
||||
if setter, ok := s.platform.(interface{ SetTheme(bool) error }); ok {
|
||||
if err := setter.SetTheme(isDark); err != nil {
|
||||
return nil, true, err
|
||||
}
|
||||
if err := s.taskSetTheme(t); err != nil {
|
||||
return nil, true, err
|
||||
}
|
||||
_ = s.Core().ACTION(ActionThemeChanged{IsDark: isDark})
|
||||
return nil, true, nil
|
||||
default:
|
||||
return nil, false, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) taskSetTheme(task TaskSetTheme) error {
|
||||
shouldApplyTheme := false
|
||||
switch task.Theme {
|
||||
case "dark":
|
||||
isDark := true
|
||||
s.overrideDark = &isDark
|
||||
shouldApplyTheme = true
|
||||
case "light":
|
||||
isDark := false
|
||||
s.overrideDark = &isDark
|
||||
shouldApplyTheme = true
|
||||
case "system":
|
||||
s.overrideDark = nil
|
||||
case "":
|
||||
isDark := task.IsDark
|
||||
s.overrideDark = &isDark
|
||||
shouldApplyTheme = true
|
||||
default:
|
||||
return fmt.Errorf("invalid theme mode: %s", task.Theme)
|
||||
}
|
||||
|
||||
if shouldApplyTheme {
|
||||
if setter, ok := s.platform.(interface{ SetTheme(bool) error }); ok {
|
||||
if err := setter.SetTheme(s.currentTheme()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ = s.Core().ACTION(ActionThemeChanged{IsDark: s.currentTheme()})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) currentTheme() bool {
|
||||
if s.overrideDark != nil {
|
||||
return *s.overrideDark
|
||||
}
|
||||
return s.platform.IsDarkMode()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -142,7 +142,7 @@ func TestThemeChange_ActionBroadcast_Good(t *testing.T) {
|
|||
|
||||
func TestTaskSetTheme_Good(t *testing.T) {
|
||||
mock, c := newTestService(t)
|
||||
_, handled, err := c.PERFORM(TaskSetTheme{IsDark: false})
|
||||
_, handled, err := c.PERFORM(TaskSetTheme{Theme: "light"})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, handled)
|
||||
assert.True(t, mock.setThemeSeen)
|
||||
|
|
@ -154,3 +154,18 @@ func TestTaskSetTheme_Good(t *testing.T) {
|
|||
assert.False(t, theme.IsDark)
|
||||
assert.Equal(t, "light", theme.Theme)
|
||||
}
|
||||
|
||||
func TestTaskSetTheme_Compatibility_Good(t *testing.T) {
|
||||
mock, c := newTestService(t)
|
||||
_, handled, err := c.PERFORM(TaskSetTheme{IsDark: true})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, handled)
|
||||
assert.True(t, mock.setThemeSeen)
|
||||
|
||||
result, handled, err := c.QUERY(QueryTheme{})
|
||||
require.NoError(t, err)
|
||||
assert.True(t, handled)
|
||||
theme := result.(ThemeInfo)
|
||||
assert.True(t, theme.IsDark)
|
||||
assert.Equal(t, "dark", theme.Theme)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
"forge.lthn.ai/core/go/pkg/core"
|
||||
"forge.lthn.ai/core/gui/pkg/clipboard"
|
||||
"forge.lthn.ai/core/gui/pkg/display"
|
||||
"forge.lthn.ai/core/gui/pkg/environment"
|
||||
"forge.lthn.ai/core/gui/pkg/notification"
|
||||
"forge.lthn.ai/core/gui/pkg/screen"
|
||||
"forge.lthn.ai/core/gui/pkg/webview"
|
||||
|
|
@ -54,6 +55,26 @@ func (m *mockNotificationPlatform) Send(opts notification.NotificationOptions) e
|
|||
func (m *mockNotificationPlatform) RequestPermission() (bool, error) { return true, nil }
|
||||
func (m *mockNotificationPlatform) CheckPermission() (bool, error) { return true, nil }
|
||||
|
||||
type mockEnvironmentPlatform struct {
|
||||
isDark bool
|
||||
}
|
||||
|
||||
func (m *mockEnvironmentPlatform) IsDarkMode() bool { return m.isDark }
|
||||
func (m *mockEnvironmentPlatform) Info() environment.EnvironmentInfo {
|
||||
return environment.EnvironmentInfo{}
|
||||
}
|
||||
func (m *mockEnvironmentPlatform) AccentColour() string { return "" }
|
||||
func (m *mockEnvironmentPlatform) OpenFileManager(path string, selectFile bool) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockEnvironmentPlatform) OnThemeChange(handler func(isDark bool)) func() {
|
||||
return func() {}
|
||||
}
|
||||
func (m *mockEnvironmentPlatform) SetTheme(isDark bool) error {
|
||||
m.isDark = isDark
|
||||
return nil
|
||||
}
|
||||
|
||||
type mockScreenPlatform struct {
|
||||
screens []screen.Screen
|
||||
}
|
||||
|
|
@ -109,6 +130,23 @@ func TestMCP_Good_DialogMessage(t *testing.T) {
|
|||
assert.Equal(t, notification.SeverityError, mock.lastOpts.Severity)
|
||||
}
|
||||
|
||||
func TestMCP_Good_ThemeSetString(t *testing.T) {
|
||||
mock := &mockEnvironmentPlatform{isDark: true}
|
||||
c, err := core.New(
|
||||
core.WithService(environment.Register(mock)),
|
||||
core.WithServiceLock(),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, c.ServiceStartup(context.Background(), nil))
|
||||
|
||||
sub := New(c)
|
||||
_, result, err := sub.themeSet(context.Background(), nil, ThemeSetInput{Theme: "light"})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "light", result.Theme.Theme)
|
||||
assert.False(t, result.Theme.IsDark)
|
||||
assert.False(t, mock.isDark)
|
||||
}
|
||||
|
||||
func TestMCP_Good_WindowTitleSetAlias(t *testing.T) {
|
||||
c, err := core.New(
|
||||
core.WithService(window.Register(window.NewMockPlatform())),
|
||||
|
|
|
|||
|
|
@ -50,18 +50,26 @@ func (s *Subsystem) themeSystem(_ context.Context, _ *mcp.CallToolRequest, _ The
|
|||
// --- theme_set ---
|
||||
|
||||
type ThemeSetInput struct {
|
||||
IsDark bool `json:"isDark"`
|
||||
Theme string `json:"theme"`
|
||||
}
|
||||
type ThemeSetOutput struct {
|
||||
Success bool `json:"success"`
|
||||
Theme environment.ThemeInfo `json:"theme"`
|
||||
}
|
||||
|
||||
func (s *Subsystem) themeSet(_ context.Context, _ *mcp.CallToolRequest, input ThemeSetInput) (*mcp.CallToolResult, ThemeSetOutput, error) {
|
||||
_, _, err := s.core.PERFORM(environment.TaskSetTheme{IsDark: input.IsDark})
|
||||
_, _, err := s.core.PERFORM(environment.TaskSetTheme{Theme: input.Theme})
|
||||
if err != nil {
|
||||
return nil, ThemeSetOutput{}, err
|
||||
}
|
||||
return nil, ThemeSetOutput{Success: true}, nil
|
||||
result, _, err := s.core.QUERY(environment.QueryTheme{})
|
||||
if err != nil {
|
||||
return nil, ThemeSetOutput{}, err
|
||||
}
|
||||
theme, ok := result.(environment.ThemeInfo)
|
||||
if !ok {
|
||||
return nil, ThemeSetOutput{}, fmt.Errorf("unexpected result type from theme query")
|
||||
}
|
||||
return nil, ThemeSetOutput{Theme: theme}, nil
|
||||
}
|
||||
|
||||
// --- Registration ---
|
||||
|
|
@ -69,5 +77,5 @@ func (s *Subsystem) themeSet(_ context.Context, _ *mcp.CallToolRequest, input Th
|
|||
func (s *Subsystem) registerEnvironmentTools(server *mcp.Server) {
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "theme_get", Description: "Get the current application theme"}, s.themeGet)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "theme_system", Description: "Get system environment and theme information"}, s.themeSystem)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "theme_set", Description: "Set or override the application theme"}, s.themeSet)
|
||||
mcp.AddTool(server, &mcp.Tool{Name: "theme_set", Description: "Set the application theme override"}, s.themeSet)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue