wailsapp / wails

Create beautiful applications using Go
https://wails.io
MIT License
25.25k stars 1.21k forks source link

User settings store #1956

Open avengerweb opened 2 years ago

avengerweb commented 2 years ago

Is your feature request related to a problem? Please describe.

Almost every app development starts with "Where I will store user preferences/settings?" And if you look at stack overflow such question is very popular. I understand that there is many-many-3rd-party-libs/ways to implement it but I have business logic to implement and implementing user settings storage and interface over and over again is not where I want to start.

Describe the solution you'd like

Implement wails lib or integrate into framework small system to work with settings that will stored in some "good" place like user directory in OS. And expose to developer some basic methods to access read/write/delete settings. It could be struct or some straight forward API like Get/Set(String|Boolean|Number)/Remove/Contains

Describe alternatives you've considered

No response

Additional context

No response

ValentinTrinque commented 2 years ago

In the software we build, we decided to use the XDG standard (it gets more and more traction), super charged by the library adrg/xdg, that exposes OS equivalent.

Building an access layer is pretty trivial, you just have to wrap this library so you can:

store := NewFileStore()

cfg := store.GetConfig()

store.UpdateConfig(cfg)

store.DeleteConfig(cfg)

The Config struct is best to be strongly typed, instead of having generic struct with GetKey/SetKey.

I don't think wails would benefit of having such generic feature.

stffabi commented 2 years ago

I think this would be a good use case for an upcoming plugin architecture. I would rather see this in a community driven plugin or a plugin developed by the Wails Team, and not being part of Wails core.

avengerweb commented 2 years ago

You can always put it as core and after plugin system will be finished, separate it.

misitebao commented 2 years ago

Maybe we can provide a "store" interface and a default storage scheme. Users can use this scheme or implement their own storage scheme according to the interface, just like "Logger".

Note: Although front-end storage solutions can be used, I think other work other than UI interaction should be left to Go.

ValentinTrinque commented 2 years ago

You can always put it as core and after plugin system will be finished, separate it.

@avengerweb : That would create a breaking change and Wails 2.0 is now in a stable state. Not sure it's a good thing.

Maybe we can provide a "store" interface and a default storage scheme. Users can use this scheme or implement their own storage scheme according to the interface, just like "Logger".

@misitebao : It makes sense for the Logger because the core relies on it in some places. And since we have multiple tools for logging, customization makes sense to unify the logging format. That said, I hardly see how wails core would use / benefit from user preferences file in its core. Two possibilities:

  1. Wails start to rely on some content of the file, which no longer make it a user preference file, but an "technical" file with some user preferences in it, which is going to actually make custom implementation harder, defeating the initial purpose of such feature.
  2. Or, we would provide an interface for something that is not used internally, and we would just result in defining some arbitrary contract for something we have no use case for. It will be difficult to provide a good interface without having any idea of what it should be.

IMHO, having plugins (or helpers) is the most flexible solution, so you don't have to deal with something if you don't need it.

Example of implementation:

package main

import (
    "encoding/json"
    "fmt"
    "io/fs"
    "os"
    "path/filepath"

    "github.com/adrg/xdg"
)

type Config struct {
    ServerURL string `json:"serverUrl"`
}

func DefaultConfig() Config {
    return Config{
        ServerURL: "https://api.example.com",
    }
}

type ConfigStore struct {
    configPath string
}

func NewConfigStore() (*ConfigStore, error) {
    configFilePath, err := xdg.ConfigFile("appname/config.json")
    if err != nil {
        return nil, fmt.Errorf("could not resolve path for config file: %w", err)
    }

    return &ConfigStore{
        configPath: configFilePath,
    }, nil
}

func (s *ConfigStore) Config() (Config, error) {
    _, err := os.Stat(s.configPath)
    if os.IsNotExist(err) {
        return DefaultConfig(), nil
    }

    dir, fileName := filepath.Split(s.configPath)
    if len(dir) == 0 {
        dir = "."
    }

    buf, err := fs.ReadFile(os.DirFS(dir), fileName)
    if err != nil {
        return Config{}, fmt.Errorf("could not read the configuration file: %w", err)
    }

    if len(buf) == 0 {
        return DefaultConfig(), nil
    }

    cfg := Config{}
    if err := json.Unmarshal(buf, &cfg); err != nil {
        return Config{}, fmt.Errorf("configuration file does not have a valid format: %w", err)
    }

    return cfg, nil

}

func main() {
    store, err := NewConfigStore()
    if err != nil {
        fmt.Printf("could not initialize the config store: %v\n", err)
        return
    }

    fmt.Println(store.configPath)

    cfg, err := store.Config()
    if err != nil {
        fmt.Printf("could not retrieve the configuration: %v\n", err)
        return
    }
    fmt.Printf("config: %v\n", cfg)

}

Go Playground link

You just have to implement the SaveConfig(Config) error method and you are good to go.

avengerweb commented 2 years ago

You can always put it as core and after plugin system will be finished, separate it.

@avengerweb : That would create a breaking change and Wails 2.0 is now in a stable state. Not sure it's a good thing.

Maybe we can provide a "store" interface and a default storage scheme. Users can use this scheme or implement their own storage scheme according to the interface, just like "Logger".

@misitebao : It makes sense for the Logger because the core relies on it in some places. And since we have multiple tools for logging, customization makes sense to unify the logging format. That said, I hardly see how wails core would use / benefit from user preferences file in its core. Two possibilities:

  1. Wails start to rely on some content of the file, which no longer make it a user preference file, but an "technical" file with some user preferences in it, which is going to actually make custom implementation harder, defeating the initial purpose of such feature.
  2. Or, we would provide an interface for something that is not used internally, and we would just result in defining some arbitrary contract for something we have no use case for. It will be difficult to provide a good interface without having any idea of what it should be.

IMHO, having plugins (or helpers) is the most flexible solution, so you don't have to deal with something if you don't need it.

Example of implementation:

package main

import (
  "encoding/json"
  "fmt"
  "io/fs"
  "os"
  "path/filepath"

  "github.com/adrg/xdg"
)

type Config struct {
  ServerURL string `json:"serverUrl"`
}

func DefaultConfig() Config {
  return Config{
      ServerURL: "https://api.example.com",
  }
}

type ConfigStore struct {
  configPath string
}

func NewConfigStore() (*ConfigStore, error) {
  configFilePath, err := xdg.ConfigFile("appname/config.json")
  if err != nil {
      return nil, fmt.Errorf("could not resolve path for config file: %w", err)
  }

  return &ConfigStore{
      configPath: configFilePath,
  }, nil
}

func (s *ConfigStore) Config() (Config, error) {
  _, err := os.Stat(s.configPath)
  if os.IsNotExist(err) {
      return DefaultConfig(), nil
  }

  dir, fileName := filepath.Split(s.configPath)
  if len(dir) == 0 {
      dir = "."
  }

  buf, err := fs.ReadFile(os.DirFS(dir), fileName)
  if err != nil {
      return Config{}, fmt.Errorf("could not read the configuration file: %w", err)
  }

  if len(buf) == 0 {
      return DefaultConfig(), nil
  }

  cfg := Config{}
  if err := json.Unmarshal(buf, &cfg); err != nil {
      return Config{}, fmt.Errorf("configuration file does not have a valid format: %w", err)
  }

  return cfg, nil

}

func main() {
  store, err := NewConfigStore()
  if err != nil {
      fmt.Printf("could not initialize the config store: %v\n", err)
      return
  }

  fmt.Println(store.configPath)

  cfg, err := store.Config()
  if err != nil {
      fmt.Printf("could not retrieve the configuration: %v\n", err)
      return
  }
  fmt.Printf("config: %v\n", cfg)

}

Go Playground link

You just have to implement the SaveConfig(Config) error method and you are good to go.

A feature is not a breaking change; it is normal to divide features into 3rd package/add-on, even in the same major release, as soon as it doesn't break the existing API. For example, you can remove it from the core, move to the plugin, add the plugin as default in configuration for 2.1 and remove it from defaults in 3.0. I feel it is not a very productive point of discussion.

Going back for benefits of common functionality, it is very beneficial to Wails. If I can choose framework and this framework will save me few days - to not thinking about "common" functionality (How many desktop apps need user configuration? 99%?). Well, most of developers choose it and advertise it. But if I need to spent few days to spin up "common" functionality before write a single line of business logic... That is difference, I can give you an example to be more objective about common functionality and "benefits" to framework. In PHP world there is most popular Laravel framework that not only became most popular in short time but also play large role in reviving PHP today. They did a simple thing - they did developer ALL common functionality (name it, they have it; yes, a lot of just seamless integrated and documented 3rd party OS packages), so developer can focus on business logic.

So, by my subjective opinion having common functionality like auto-update, configs, logs, crash-reports, even opinionated package for Http request is very beneficial for Wails on this stage - it could attract developers and companies to Wails.

ValentinTrinque commented 2 years ago

I hear your opinion and it's perfectly fine.

It really depends what Wails is aiming for.

For vega, the company did select wails because of its openness, flexibility, slimness and because it's not an opinionated framework.

A framework can make you very productive but it can also lock you in or out based on your needs.

I have a fair amount of experience with framework like Ruby on Rails, and today I favour the flexibility offered by tools like Wails. I can just use whatever I want and keep the core down to something minimal.

It's not for me to decide anything about this project as I am not a maintainer, and merely a contributor. I just wanted to express my opinion on the subject.

iberflow commented 1 year ago

Hi! My application contains a settings.json, then various runtime-generated files (images extracted from locations on the filesystem, file caches, indexes, etc. I've not tested it yet, but I will need to display images not packed in the app and living on the FS so this might be slightly related).

My 2ct: Reading the conversation here, I agree with Wails not necessarily needing a built-in config/directory handling, however, I would love to see filesystem-related documentation and guidelines that could include "official" suggestions and sort of best practices, such as using the XDG standard and its Go lib with some basic examples in relation to Wails. That would be enough for me to understand how to implement this as a "standardized" cross-platform feature.

On the other hand Electron (which I've used in the past) has this: app.getPath, which does its job and leaves further implementation to the devs, who no longer need to think about dealing with this in 3 OS.

Both approaches work for me though :)

AndreiTelteu commented 9 months ago

I searched for a plugin like this for wails and this thread is all I found. I built a plugin with the example implementation that @avengerweb wrote. https://github.com/AndreiTelteu/wails-configstore I think it's very useful for frontend js devs to have simple plugins like this available and easily searchable without having to know golang. And it ok to not be included in core. Look at react-native-async-storage, intially it was in core, then moved to react-native-community org, then moved to a separated org.

leaanthony commented 9 months ago

V3 will have plugins, but it's not a goal of the project to provide a way to write apps in just JavaScript.