Skip to content

Commit 6e73b3f

Browse files
committed
feat: add sync command
1 parent 567e0d5 commit 6e73b3f

File tree

2 files changed

+185
-35
lines changed

2 files changed

+185
-35
lines changed

main.go

Lines changed: 50 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -6,45 +6,61 @@ import (
66
"os"
77
)
88

9+
10+
var applyCmd = &cobra.Command{
11+
Use: "apply [path]",
12+
Short: "Apply changes from a profile.json",
13+
Args: cobra.ExactArgs(1),
14+
Run: func(cmd *cobra.Command, args []string) {
15+
err := applyProfile(args[0])
16+
if err != nil {
17+
fmt.Printf("Error: %s\n", err)
18+
}
19+
},
20+
}
21+
22+
var revertCmd = &cobra.Command{
23+
Use: "revert [path]",
24+
Short: "Revert changes from a profile.json",
25+
Args: cobra.ExactArgs(1),
26+
Run: func(cmd *cobra.Command, args []string) {
27+
err := revertProfile(args[0])
28+
if err != nil {
29+
fmt.Printf("Error: %s\n", err)
30+
}
31+
},
32+
}
33+
34+
var recordCmd = &cobra.Command{
35+
Use: "record",
36+
Short: "Record changes to xsettings and dump them as a profile on SIGINT",
37+
Run: func(cmd *cobra.Command, args []string) {
38+
recordProfile()
39+
},
40+
}
41+
42+
var syncCmd = &cobra.Command{
43+
Use: "sync",
44+
Short: "Sync user profile with distribution's recommended profile",
45+
Run: func(cmd *cobra.Command, args []string) {
46+
distProfile, _ := cmd.Flags().GetString("profile")
47+
if err := syncProfile(distProfile); err != nil {
48+
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
49+
os.Exit(1)
50+
}
51+
},
52+
}
53+
54+
func init() {
55+
syncCmd.Flags().StringP("profile", "p", "/usr/share/xfconf-profile/default.json", "Path to the distribution's recommended profile")
56+
}
57+
958
func main() {
1059
var rootCmd = &cobra.Command{
1160
Use: "xfconf-profile",
1261
Short: "Tool for applying, reverting and managing Xfce profiles",
1362
}
14-
15-
var applyCmd = &cobra.Command{
16-
Use: "apply [path]",
17-
Short: "Apply changes from a profile.json",
18-
Args: cobra.ExactArgs(1),
19-
Run: func(cmd *cobra.Command, args []string) {
20-
err := applyProfile(args[0])
21-
if err != nil {
22-
fmt.Printf("Error: %s\n", err)
23-
}
24-
},
25-
}
26-
27-
var revertCmd = &cobra.Command{
28-
Use: "revert [path]",
29-
Short: "Revert changes from a profile.json",
30-
Args: cobra.ExactArgs(1),
31-
Run: func(cmd *cobra.Command, args []string) {
32-
err := revertProfile(args[0])
33-
if err != nil {
34-
fmt.Printf("Error: %s\n", err)
35-
}
36-
},
37-
}
38-
39-
var recordCmd = &cobra.Command{
40-
Use: "record",
41-
Short: "Record changes to xsettings and dump them as a profile on SIGINT",
42-
Run: func(cmd *cobra.Command, args []string) {
43-
recordProfile()
44-
},
45-
}
46-
47-
rootCmd.AddCommand(applyCmd, revertCmd, recordCmd)
63+
rootCmd.AddCommand(applyCmd, revertCmd, recordCmd, syncCmd)
4864

4965
if err := rootCmd.Execute(); err != nil {
5066
fmt.Println(err)

profile.go

Lines changed: 135 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@ package main
22

33
import (
44
"encoding/json"
5+
"errors"
56
"fmt"
67
"io/ioutil"
8+
"os"
79
"os/exec"
10+
"path/filepath"
811
"strings"
912
"github.com/fatih/color"
1013
)
@@ -30,7 +33,7 @@ func applyProfile(profilePath string) error {
3033
}
3134
for property, value := range properties {
3235
fmt.Printf("%s Setting %s::%s ➔ %s\n", blue("•"), channel, property, value)
33-
cmd := exec.Command("xfconf-query", "-c", channel, "--property", property, "--set", fmt.Sprintf("%v", value))
36+
cmd := exec.Command("xfconf-query", "-c", channel, "--property", property, "--type", "string", "--create", "--set", fmt.Sprintf("%v", value))
3437

3538
output, err := cmd.CombinedOutput()
3639
if err != nil {
@@ -75,3 +78,134 @@ func revertProfile(profilePath string) error {
7578

7679
return nil
7780
}
81+
82+
83+
// Create $XDG_STATE_HOME/xfconf-profile/sync if needed
84+
func ensureStateDir() (string, error) {
85+
xdgStateHome := os.Getenv("XDG_STATE_HOME")
86+
var stateDirPath string
87+
88+
if xdgStateHome != "" {
89+
stateDirPath = filepath.Join(xdgStateHome, "xfconf-profile", "sync")
90+
} else {
91+
homeDir, err := os.UserHomeDir()
92+
if err != nil {
93+
return "", fmt.Errorf("failed to get user home directory: %v", err)
94+
}
95+
stateDirPath = filepath.Join(homeDir, ".local", "state", "xfconf-profile", "sync")
96+
}
97+
98+
if err := os.MkdirAll(stateDirPath, 0755); err != nil {
99+
return "", fmt.Errorf("failed to create state directory: %v", err)
100+
}
101+
102+
return stateDirPath, nil
103+
}
104+
105+
func copyDistConfig(distConfig string, currentDir string) error {
106+
defaultConfigData, err := ioutil.ReadFile(distConfig)
107+
if err != nil {
108+
return fmt.Errorf("failed to read config: %v", err)
109+
}
110+
111+
currentConfigPath := filepath.Join(currentDir, "profile.json")
112+
if err := ioutil.WriteFile(currentConfigPath, defaultConfigData, 0644); err != nil {
113+
return fmt.Errorf("failed to write current config: %v", err)
114+
}
115+
116+
return nil
117+
}
118+
119+
func compareFiles(file1, file2 string) (bool, error) {
120+
data1, err := ioutil.ReadFile(file1)
121+
if err != nil {
122+
return false, fmt.Errorf("failed to read %s: %v", file1, err)
123+
}
124+
125+
data2, err := ioutil.ReadFile(file2)
126+
if err != nil {
127+
return false, fmt.Errorf("failed to read %s: %v", file2, err)
128+
}
129+
130+
return string(data1) == string(data2), nil
131+
}
132+
133+
func syncProfile(distConfig string) error {
134+
stateDirPath, err := ensureStateDir()
135+
if err != nil {
136+
return err
137+
}
138+
139+
_, err = os.Stat(distConfig)
140+
if err != nil {
141+
return err
142+
}
143+
144+
currentDir := filepath.Join(stateDirPath, "current")
145+
previousDir := filepath.Join(stateDirPath, "previous")
146+
147+
// Abnormal case: reset state directory if it's invalid
148+
if _, err := os.Stat(currentDir); errors.Is(err, os.ErrNotExist) {
149+
if _, err := os.Stat(previousDir); err == nil {
150+
fmt.Println("Invalid state: resetting data")
151+
if err := os.RemoveAll(stateDirPath); err != nil {
152+
return fmt.Errorf("failed to reset state directory: %v", err)
153+
}
154+
if _, err := ensureStateDir(); err != nil {
155+
return err
156+
}
157+
}
158+
}
159+
160+
// First run: initialize current directory
161+
if _, err := os.Stat(currentDir); errors.Is(err, os.ErrNotExist) {
162+
fmt.Println("Empty state")
163+
if err := applyProfile(distConfig); err != nil {
164+
return err
165+
}
166+
if err := os.MkdirAll(currentDir, 0755); err != nil {
167+
return fmt.Errorf("failed to create current directory: %v", err)
168+
}
169+
if err := copyDistConfig(distConfig, currentDir); err != nil {
170+
return err
171+
}
172+
return nil
173+
}
174+
175+
// Steady run: move current to previous and apply new config
176+
fmt.Println("Steady state")
177+
if err := os.RemoveAll(previousDir); err != nil {
178+
return fmt.Errorf("failed to remove previous directory: %v", err)
179+
}
180+
if err := os.Rename(currentDir, previousDir); err != nil {
181+
return fmt.Errorf("failed to move current to previous: %v", err)
182+
}
183+
if err := os.MkdirAll(currentDir, 0755); err != nil {
184+
return fmt.Errorf("failed to create current directory: %v", err)
185+
}
186+
if err := copyDistConfig(distConfig, currentDir); err != nil {
187+
return err
188+
}
189+
190+
// Check if configurations differ
191+
currentConfig := filepath.Join(currentDir, "profile.json")
192+
previousConfig := filepath.Join(previousDir, "profile.json")
193+
identical, err := compareFiles(currentConfig, previousConfig)
194+
if err != nil {
195+
return err
196+
}
197+
198+
if !identical {
199+
fmt.Println("Configurations differ -- reverting old and applying new")
200+
if err := revertProfile(previousConfig); err != nil {
201+
return err
202+
}
203+
if err := applyProfile(currentConfig); err != nil {
204+
return err
205+
}
206+
} else {
207+
fmt.Println("Configurations identical -- no changes required")
208+
}
209+
210+
return nil
211+
}

0 commit comments

Comments
 (0)