Need a config system #3

Closed
opened 2024-06-08 15:30:22 -06:00 by sashakoshka · 4 comments
Owner

It could just be a wrapper around dconf, but dconf is owned by GNOME. Not only is this not cross platform (we'd need another implementation for other systems), but depending on anything GNOME related is a horrible idea. Additionally, holding all settings within a single file is kind of a bad idea.

The configuration system should:

  • Categorize application configurations by application ID
  • Have system and user-level configurations for each application
    • System level configurations specify default values
    • User level configurations override system level values
  • Be able to handle multiple locations for system-level config dirs to support XDG base dirs
    • Just one location for the user-level config
    • Each dir contains a single config file
  • Have a user-readable, editable file format
  • Preserve unused values
  • Store values losslessly in all cases
  • Be primarily key/value structured
  • Alert the application when a setting has changed, either by use of an API, or by the user or an external program
    • Watch all files
    • When a file gets changed, reload it and find out what value(s) were altered
  • Have some global Nasin settings that apply to all applications, possibly with the ID xyz.holanet.Nasin
    • This would be read by Nasin itself and used to read things like the theme, scaling, etc.
    • The handle to this must be made available through a function
  • Ideally not depend on dbus
It could just be a wrapper around dconf, but dconf is owned by GNOME. Not only is this not cross platform (we'd need another implementation for other systems), but depending on anything GNOME related is a horrible idea. Additionally, holding all settings within a single file is kind of a bad idea. The configuration system should: - Categorize application configurations by application ID - Have system and user-level configurations for each application - System level configurations specify default values - User level configurations override system level values - Be able to handle multiple locations for system-level config dirs to support XDG base dirs - Just one location for the user-level config - Each dir contains a single config file - Have a user-readable, editable file format - Preserve unused values - Store values losslessly in all cases - Be primarily key/value structured - Alert the application when a setting has changed, either by use of an API, or by the user or an external program - Watch all files - When a file gets changed, reload it and find out what value(s) were altered - Have some global Nasin settings that apply to all applications, possibly with the ID xyz.holanet.Nasin - This would be read by Nasin itself and used to read things like the theme, scaling, etc. - The handle to this must be made available through a function - Ideally not depend on dbus
Author
Owner

The API could look like:

type Config struct { ... }

// Set sets a value in the user-level config file.
func (*Config) Set (key string, value any) error
// Get gets a value, considering all config files.
func (*Config) Get (key string, fallback any) (any, error)
// Reset removes the value from the user-level config file, resetting it to
// what is described by the system-level config files.
func (*Config) Reset (key string) error
// OnChange specifies a function to be called whenever a value is changed.
// The callback is always run within the backend's event loop using tomo.Do.
func (*Config) OnChange (func (key string))
// Close closes the config, causing it to stop watching for changes. Reads
// or writes to the config after this will return an error.
func (*Config) Close () error

// ApplicationConfig returns a new config for the application in question.
// It must be closed after use unless it is supposed to remain open for the
// entire duration the application is running.
func ApplicationConfig (ApplicationDescription) *Config
// ApplicationConfig returns the global config. This should not be closed.
func GlobalConfig () *Config
The API could look like: ```go type Config struct { ... } // Set sets a value in the user-level config file. func (*Config) Set (key string, value any) error // Get gets a value, considering all config files. func (*Config) Get (key string, fallback any) (any, error) // Reset removes the value from the user-level config file, resetting it to // what is described by the system-level config files. func (*Config) Reset (key string) error // OnChange specifies a function to be called whenever a value is changed. // The callback is always run within the backend's event loop using tomo.Do. func (*Config) OnChange (func (key string)) // Close closes the config, causing it to stop watching for changes. Reads // or writes to the config after this will return an error. func (*Config) Close () error // ApplicationConfig returns a new config for the application in question. // It must be closed after use unless it is supposed to remain open for the // entire duration the application is running. func ApplicationConfig (ApplicationDescription) *Config // ApplicationConfig returns the global config. This should not be closed. func GlobalConfig () *Config ```
Author
Owner

Alternatively, the pattern used for canvases and textures could be used, providing more explicit ownership of resources (in this case a goroutine):

type Config interface {
        Set (key string, value any) error
        Get (key string, fallback any) (any, error)
        Reset (key string) error
        OnChange (func (key string))
}

type ConfigCloser interface {
        Config
        io.Closer
}

func ApplicationConfig (ApplicationDescription) ConfigCloser
func GlobalConfig () Config
Alternatively, the pattern used for canvases and textures could be used, providing more explicit ownership of resources (in this case a goroutine): ```go type Config interface { Set (key string, value any) error Get (key string, fallback any) (any, error) Reset (key string) error OnChange (func (key string)) } type ConfigCloser interface { Config io.Closer } func ApplicationConfig (ApplicationDescription) ConfigCloser func GlobalConfig () Config ```
Author
Owner

One thing I didn't consider is that any callback given in OnChange will run in the backend's event loop. Now, this is the Tomo application framework, but having a system running in another goroutine magically be able to run callbacks in the main event loop is ridiculous and will cause problems. What if the backend is not running? It should not depend on Tomo at all, ideally. Perhaps OnChange could be replaced with a Changes function that returns a channel of names:

Changes () <- chan string

Each time a value is modified, the name of that value would be sent along the channel. Each time this function is called, a new channel will be returned, and all of them will get the same information. When a receiver closes a channel, the Config will remove it from the list and stop sending to it. When the Config is stopped, all returned channels are closed.

One thing I didn't consider is that any callback given in OnChange will run in the backend's event loop. Now, this is the Tomo application framework, but having a system running in another goroutine magically be able to run callbacks in the main event loop is ridiculous and will cause problems. What if the backend is not running? It should not depend on Tomo at all, ideally. Perhaps OnChange could be replaced with a Changes function that returns a channel of names: ```go Changes () <- chan string ``` Each time a value is modified, the name of that value would be sent along the channel. Each time this function is called, a new channel will be returned, and all of them will get the same information. When a receiver closes a channel, the Config will remove it from the list and stop sending to it. When the Config is stopped, all returned channels are closed.
Author
Owner

Never mind that last comment. When there is no backend, tomo.Do simply does nothing. The application ought not to be running while there is no backend anyways. The simplicity gains from having it call tomo.Do outweigh the cursedness. What gosh darned application developer wants to manage goroutines just to stay updated on config values?

Never mind that last comment. When there is no backend, tomo.Do simply does nothing. The application ought not to be running while there is no backend anyways. The simplicity gains from having it call tomo.Do outweigh the cursedness. What gosh darned application developer wants to manage goroutines just to stay updated on config values?
Sign in to join this conversation.
No Label
No Milestone
No project
No Assignees
1 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: tomo/nasin#3
No description provided.