Skip to content

Commit 63b54eb

Browse files
committed
Add GNOME appearance and sound management functionality
- Implemented DBus service for managing GNOME appearance settings, including theme, dark mode, background, accent color, and icon theme. - Added commands to get and set various appearance properties using gsettings. - Introduced sound management capabilities, allowing control over system volume, mute state, input volume, and sound devices. - Integrated fallback methods using amixer for systems without pactl. - Enhanced device management by listing available sound devices and setting default devices.
1 parent 6f3d0fa commit 63b54eb

10 files changed

Lines changed: 1524 additions & 0 deletions

File tree

dbus/gnome/appearance.go

Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
package gnome
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
"os/exec"
8+
"path/filepath"
9+
"strings"
10+
11+
"github.com/agnath18K/lumo/internal/core"
12+
)
13+
14+
// GNOME appearance-related DBus service names and interfaces
15+
const (
16+
// GSettings is the GSettings service
17+
GSettings = "org.gnome.Settings"
18+
// GSettingsPath is the GSettings object path
19+
GSettingsPath = "/org/gnome/Settings"
20+
// GSettingsInterface is the GSettings interface
21+
GSettingsInterface = "org.gnome.Settings"
22+
23+
// GSettingsSchemaDesktopInterface is the schema for desktop interface settings
24+
GSettingsSchemaDesktopInterface = "org.gnome.desktop.interface"
25+
// GSettingsSchemaDesktopBackground is the schema for desktop background settings
26+
GSettingsSchemaDesktopBackground = "org.gnome.desktop.background"
27+
)
28+
29+
// executeAppearanceCommand executes an appearance management command
30+
func (e *Environment) executeAppearanceCommand(ctx context.Context, cmd *core.Command) (*core.Result, error) {
31+
switch cmd.Action {
32+
case "set-theme":
33+
theme := cmd.Target
34+
if theme == "" {
35+
return nil, fmt.Errorf("theme name is required")
36+
}
37+
if err := e.SetGtkTheme(ctx, theme); err != nil {
38+
return nil, err
39+
}
40+
return &core.Result{
41+
Output: fmt.Sprintf("Set GTK theme to: %s", theme),
42+
Success: true,
43+
}, nil
44+
case "set-dark-mode":
45+
// Convert target to boolean
46+
enable := true
47+
if cmd.Target == "false" || cmd.Target == "off" || cmd.Target == "0" {
48+
enable = false
49+
}
50+
51+
// Set color scheme based on dark mode preference
52+
colorScheme := "prefer-dark"
53+
if !enable {
54+
colorScheme = "prefer-light"
55+
}
56+
57+
// Use gsettings to set the color scheme
58+
if err := e.setGSetting(GSettingsSchemaDesktopInterface, "color-scheme", colorScheme); err != nil {
59+
return nil, err
60+
}
61+
62+
return &core.Result{
63+
Output: fmt.Sprintf("Set dark mode to: %v", enable),
64+
Success: true,
65+
}, nil
66+
case "set-background":
67+
imagePath := cmd.Target
68+
if imagePath == "" {
69+
return nil, fmt.Errorf("background image path is required")
70+
}
71+
if err := e.SetDesktopBackground(ctx, imagePath); err != nil {
72+
return nil, err
73+
}
74+
return &core.Result{
75+
Output: fmt.Sprintf("Set desktop background to: %s", imagePath),
76+
Success: true,
77+
}, nil
78+
case "set-accent-color":
79+
color := cmd.Target
80+
if color == "" {
81+
return nil, fmt.Errorf("accent color is required")
82+
}
83+
if err := e.SetAccentColor(ctx, color); err != nil {
84+
return nil, err
85+
}
86+
return &core.Result{
87+
Output: fmt.Sprintf("Set accent color to: %s", color),
88+
Success: true,
89+
}, nil
90+
case "set-icon-theme":
91+
theme := cmd.Target
92+
if theme == "" {
93+
return nil, fmt.Errorf("icon theme name is required")
94+
}
95+
if err := e.SetIconTheme(ctx, theme); err != nil {
96+
return nil, err
97+
}
98+
return &core.Result{
99+
Output: fmt.Sprintf("Set icon theme to: %s", theme),
100+
Success: true,
101+
}, nil
102+
case "get-theme":
103+
theme, err := e.GetCurrentTheme(ctx)
104+
if err != nil {
105+
return nil, err
106+
}
107+
return &core.Result{
108+
Output: fmt.Sprintf("Current GTK theme: %s", theme),
109+
Success: true,
110+
Data: map[string]any{
111+
"theme": theme,
112+
},
113+
}, nil
114+
case "get-background":
115+
background, err := e.GetCurrentBackground(ctx)
116+
if err != nil {
117+
return nil, err
118+
}
119+
return &core.Result{
120+
Output: fmt.Sprintf("Current desktop background: %s", background),
121+
Success: true,
122+
Data: map[string]any{
123+
"background": background,
124+
},
125+
}, nil
126+
case "get-icon-theme":
127+
theme, err := e.GetCurrentIconTheme(ctx)
128+
if err != nil {
129+
return nil, err
130+
}
131+
return &core.Result{
132+
Output: fmt.Sprintf("Current icon theme: %s", theme),
133+
Success: true,
134+
Data: map[string]any{
135+
"icon_theme": theme,
136+
},
137+
}, nil
138+
default:
139+
return nil, fmt.Errorf("unsupported appearance action: %s", cmd.Action)
140+
}
141+
}
142+
143+
// SetGtkTheme sets the GTK theme
144+
func (e *Environment) SetGtkTheme(ctx context.Context, theme string) error {
145+
// Use gsettings to set the GTK theme
146+
if err := e.setGSetting(GSettingsSchemaDesktopInterface, "gtk-theme", theme); err != nil {
147+
return fmt.Errorf("failed to set GTK theme: %w", err)
148+
}
149+
return nil
150+
}
151+
152+
// SetDesktopBackground sets the desktop background image
153+
func (e *Environment) SetDesktopBackground(ctx context.Context, imagePath string) error {
154+
// Verify the image file exists
155+
if _, err := os.Stat(imagePath); os.IsNotExist(err) {
156+
return fmt.Errorf("background image does not exist: %s", imagePath)
157+
}
158+
159+
// Convert to absolute path if needed
160+
if !filepath.IsAbs(imagePath) {
161+
absPath, err := filepath.Abs(imagePath)
162+
if err != nil {
163+
return fmt.Errorf("failed to get absolute path: %w", err)
164+
}
165+
imagePath = absPath
166+
}
167+
168+
// Format the path as a URI
169+
imageURI := fmt.Sprintf("file://%s", imagePath)
170+
171+
// Use gsettings to set the desktop background
172+
if err := e.setGSetting(GSettingsSchemaDesktopBackground, "picture-uri", imageURI); err != nil {
173+
return fmt.Errorf("failed to set desktop background: %w", err)
174+
}
175+
176+
// Also set the dark mode background for GNOME 42+
177+
if err := e.setGSetting(GSettingsSchemaDesktopBackground, "picture-uri-dark", imageURI); err != nil {
178+
// This might fail on older GNOME versions, so we'll just log it
179+
fmt.Printf("Warning: Failed to set dark mode background: %v\n", err)
180+
}
181+
182+
return nil
183+
}
184+
185+
// SetAccentColor sets the accent color
186+
func (e *Environment) SetAccentColor(ctx context.Context, color string) error {
187+
// Use gsettings to set the accent color (GNOME 42+)
188+
if err := e.setGSetting(GSettingsSchemaDesktopInterface, "accent-color", color); err != nil {
189+
return fmt.Errorf("failed to set accent color (this may not be supported in your GNOME version): %w", err)
190+
}
191+
return nil
192+
}
193+
194+
// SetIconTheme sets the icon theme
195+
func (e *Environment) SetIconTheme(ctx context.Context, theme string) error {
196+
// Use gsettings to set the icon theme
197+
if err := e.setGSetting(GSettingsSchemaDesktopInterface, "icon-theme", theme); err != nil {
198+
return fmt.Errorf("failed to set icon theme: %w", err)
199+
}
200+
return nil
201+
}
202+
203+
// GetCurrentTheme gets the current GTK theme
204+
func (e *Environment) GetCurrentTheme(ctx context.Context) (string, error) {
205+
// Use gsettings to get the current GTK theme
206+
theme, err := e.getGSetting(GSettingsSchemaDesktopInterface, "gtk-theme")
207+
if err != nil {
208+
return "", fmt.Errorf("failed to get current GTK theme: %w", err)
209+
}
210+
return theme, nil
211+
}
212+
213+
// GetCurrentBackground gets the current desktop background
214+
func (e *Environment) GetCurrentBackground(ctx context.Context) (string, error) {
215+
// Use gsettings to get the current desktop background
216+
background, err := e.getGSetting(GSettingsSchemaDesktopBackground, "picture-uri")
217+
if err != nil {
218+
return "", fmt.Errorf("failed to get current desktop background: %w", err)
219+
}
220+
221+
// Convert from URI format to path
222+
background = strings.TrimPrefix(background, "file://")
223+
224+
return background, nil
225+
}
226+
227+
// GetCurrentIconTheme gets the current icon theme
228+
func (e *Environment) GetCurrentIconTheme(ctx context.Context) (string, error) {
229+
// Use gsettings to get the current icon theme
230+
theme, err := e.getGSetting(GSettingsSchemaDesktopInterface, "icon-theme")
231+
if err != nil {
232+
return "", fmt.Errorf("failed to get current icon theme: %w", err)
233+
}
234+
return theme, nil
235+
}
236+
237+
// setGSetting sets a GSettings value
238+
func (e *Environment) setGSetting(schema, key, value string) error {
239+
// Use the gsettings command-line tool to set the value
240+
cmd := fmt.Sprintf("gsettings set %s %s '%s'", schema, key, value)
241+
output, err := e.runCommand(cmd)
242+
if err != nil {
243+
return fmt.Errorf("failed to set gsettings value: %w (output: %s)", err, output)
244+
}
245+
return nil
246+
}
247+
248+
// getGSetting gets a GSettings value
249+
func (e *Environment) getGSetting(schema, key string) (string, error) {
250+
// Use the gsettings command-line tool to get the value
251+
cmd := fmt.Sprintf("gsettings get %s %s", schema, key)
252+
output, err := e.runCommand(cmd)
253+
if err != nil {
254+
return "", fmt.Errorf("failed to get gsettings value: %w", err)
255+
}
256+
257+
// Clean up the output (remove quotes, newlines, etc.)
258+
output = strings.TrimSpace(output)
259+
output = strings.Trim(output, "'\"")
260+
261+
return output, nil
262+
}
263+
264+
// runCommand runs a shell command and returns its output
265+
func (e *Environment) runCommand(cmd string) (string, error) {
266+
// Run the command using the system's shell
267+
command := exec.Command("sh", "-c", cmd)
268+
output, err := command.CombinedOutput()
269+
if err != nil {
270+
return string(output), err
271+
}
272+
return string(output), nil
273+
}

dbus/gnome/gnome.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ func NewEnvironment() (*Environment, error) {
4949
core.CapabilityMediaControl,
5050
core.CapabilityScreenshot,
5151
core.CapabilityClipboard,
52+
core.CapabilityAppearanceManagement,
53+
core.CapabilitySoundManagement,
5254
}
5355

5456
// Create base environment
@@ -103,6 +105,10 @@ func (e *Environment) ExecuteCommand(ctx context.Context, cmd *core.Command) (*c
103105
return e.executeNotificationCommand(ctx, cmd)
104106
case core.CommandTypeMedia:
105107
return e.executeMediaCommand(ctx, cmd)
108+
case core.CommandTypeAppearance:
109+
return e.executeAppearanceCommand(ctx, cmd)
110+
case core.CommandTypeSound:
111+
return e.executeSoundCommand(ctx, cmd)
106112
default:
107113
return nil, fmt.Errorf("unsupported command type: %s", cmd.Type)
108114
}

0 commit comments

Comments
 (0)