|
|
@@ -132,21 +132,34 @@ func profileFilePath(name string) string {
|
|
|
}
|
|
|
|
|
|
// Save writes the profile into the per-user directory under its own name, which is
|
|
|
-// what every caller that has a name and no path means.
|
|
|
+// what every caller that has a name and no path means. It creates that directory,
|
|
|
+// which is the one directory a save may create: it is this tool's own, and a user
|
|
|
+// who has never saved a profile does not have it, so the first `save lava` is
|
|
|
+// exactly the case that needs it.
|
|
|
func (p *Profile) Save() error {
|
|
|
+ if _, err := ensureDataDir(profilesPath()); err != nil {
|
|
|
+ return fmt.Errorf("create profiles directory: %w", err)
|
|
|
+ }
|
|
|
return p.saveTo(profileFilePath(p.Name))
|
|
|
}
|
|
|
|
|
|
-// saveTo writes the profile to one file and creates the directory it is in. It
|
|
|
-// resolves nothing: the caller decides where the file goes, so that the rule that
|
|
|
-// decides is the only one there is.
|
|
|
+// saveTo writes the profile to one file, in a directory that has to be there
|
|
|
+// already. A caller that names a path names a directory the tool did not create,
|
|
|
+// and building it turns `save profiles/neu/x.json` with a typo in it into a tree of
|
|
|
+// empty ones that nothing lists and nothing cleans up — reported, meanwhile, as a
|
|
|
+// save. It resolves nothing either: the caller decides where the file goes, so
|
|
|
+// that the rule that decides is the only one there is.
|
|
|
func (p *Profile) saveTo(path string) error {
|
|
|
if p.Name == "" {
|
|
|
return fmt.Errorf("profile name is required")
|
|
|
}
|
|
|
|
|
|
- if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
|
- return fmt.Errorf("create profiles directory: %w", err)
|
|
|
+ dir := filepath.Dir(path)
|
|
|
+ if _, err := os.Stat(dir); err != nil {
|
|
|
+ if os.IsNotExist(err) {
|
|
|
+ return fmt.Errorf("profile directory %s does not exist; create it first, or save a name", dir)
|
|
|
+ }
|
|
|
+ return fmt.Errorf("profile directory %s: %w", dir, err)
|
|
|
}
|
|
|
|
|
|
data, err := json.MarshalIndent(p, "", " ")
|
|
|
@@ -271,9 +284,10 @@ func applyProfileToProfile(proto rgbProtocol, channels []via.Channel, display ma
|
|
|
// loadProfileFromDevice reads RGB state from device and saves it. Every channel
|
|
|
// is recorded, so the profile a `load` applies later is the whole keyboard and
|
|
|
// not the part of it that happened to be named. Warnings go to warn, which is the
|
|
|
-// command's stderr. The file is written where path says and the profile is named
|
|
|
-// after name, so a `save` of a file writes that file and names the profile for it.
|
|
|
-func loadProfileFromDevice(path, name string, warn io.Writer) error {
|
|
|
+// command's stderr. Where the file goes is the caller's decision: isPath is what
|
|
|
+// resolveProfileTarget said about the argument, and a name is the only form that
|
|
|
+// brings a directory with it.
|
|
|
+func loadProfileFromDevice(path, name string, isPath bool, warn io.Writer) error {
|
|
|
proto, target, channels, err := openTarget("")
|
|
|
if err != nil {
|
|
|
return err
|
|
|
@@ -303,7 +317,10 @@ func loadProfileFromDevice(path, name string, warn io.Writer) error {
|
|
|
if err := applyProfileToProfile(proto, channels, target.Display, catalog, p); err != nil {
|
|
|
return err
|
|
|
}
|
|
|
- return p.saveTo(path)
|
|
|
+ if isPath {
|
|
|
+ return p.saveTo(path)
|
|
|
+ }
|
|
|
+ return p.Save()
|
|
|
}
|
|
|
|
|
|
// savedProfileLine is what a save says it did. The path is annotated the way every
|
|
|
@@ -329,8 +346,9 @@ func NewProfileSaveCmd() *cobra.Command {
|
|
|
Short: "Save current RGB state to a profile",
|
|
|
Long: "Read the current RGB settings from every channel of the keyboard and save\n" +
|
|
|
"them as a JSON profile. A name is written as <name>.json in the per-user\n" +
|
|
|
- "profiles/ directory; an argument ending in .json is a path, and that file is\n" +
|
|
|
- "written where it says. It reports the file it wrote.\n" +
|
|
|
+ "profiles/ directory, which is created if it is not there yet; an argument\n" +
|
|
|
+ "ending in .json is a path, and that file is written where it says, in a\n" +
|
|
|
+ "directory that has to exist already. It reports the file it wrote.\n" +
|
|
|
"\n" +
|
|
|
"A profile is always complete. Recording only some of the channels would let a\n" +
|
|
|
"later `load` apply them and leave the rest as they were, which reads as a\n" +
|
|
|
@@ -341,8 +359,8 @@ func NewProfileSaveCmd() *cobra.Command {
|
|
|
if len(args) > 0 {
|
|
|
arg = args[0]
|
|
|
}
|
|
|
- path, name, _ := resolveProfileTarget(arg)
|
|
|
- if err := loadProfileFromDevice(path, name, cmd.ErrOrStderr()); err != nil {
|
|
|
+ path, name, isPath := resolveProfileTarget(arg)
|
|
|
+ if err := loadProfileFromDevice(path, name, isPath, cmd.ErrOrStderr()); err != nil {
|
|
|
return err
|
|
|
}
|
|
|
fmt.Fprint(cmd.OutOrStdout(), savedProfileLine(name, path))
|