11 Commits

11 changed files with 360 additions and 107 deletions

View File

@@ -2,12 +2,79 @@
[![Go Reference](https://pkg.go.dev/badge/git.tebibyte.media/tomo/nasin.svg)](https://pkg.go.dev/git.tebibyte.media/tomo/nasin) [![Go Reference](https://pkg.go.dev/badge/git.tebibyte.media/tomo/nasin.svg)](https://pkg.go.dev/git.tebibyte.media/tomo/nasin)
Nasin provides an easy way to write applications with Tomo. To get started, take Nasin builds an application framework on top of Tomo to ease and encourage the
a look at the [examples](examples) directory and the development of consistent and stable application software. It has these
wonderful features, and more:
- Use the Application interface to create applications with relatively low
boilerplate
- CLI argument parsing and URI opening
- Automatic setup/teardown of the backend
- Advanced configuration system that can watch config files for changes
- Default style and icon set, as well as a fully featured stylesheet language
for creating custom styles, and support for XDG icon themes
## Getting Started
Here is a basic "hello world" application, with explanations as comments:
```go
package main
import "image"
import "git.tebibyte.media/tomo/nasin"
import "git.tebibyte.media/tomo/objects"
import "git.tebibyte.media/tomo/objects/layouts"
func main () {
nasin.RunApplication(new(Application))
}
type Application struct { }
// Describe returns the application's name and ID, and optionally what type of
// application it is.
func (this *Application) Describe () nasin.ApplicationDescription {
return nasin.ApplicationDescription {
// This is the name of the application. New application windows
// will have this as their title by default.
Name: "Example",
// This is a "well-known" name, which typically is a domain name
// owned by the application author.
ID: "com.example.Example",
}
}
// Init performs initial setup of the application. Since this is a single-window
// application that doesn't open any files, we create the window here.
func (this *Application) Init () error {
// Passing an empty rectangle when creating a new window will cause it
// to auto-expand to fit the minimum size of its contents.
window, err := nasin.NewApplicationWindow(this, image.Rectangle { })
if err != nil { return err }
// Here we create a new container with a basic vertical layout, place a
// text label that says "Hello world!" in it, and set it as the root
// object of the window.
window.SetRoot(objects.NewOuterContainer (
layouts.ContractVertical,
objects.NewLabel("Hello world!")))
window.SetVisible(true)
// Nasin will not exit until all windows it is "waiting for" have
// been closed.
nasin.WaitFor(window)
return nil
}
// Stop cleanly closes things like system resources or background tasks. We do
// not have any here, so nothing is done.
func (this *Application) Stop () { }
```
To learn more, take a look at the [examples](examples) directory and the
[online documentation](https://pkg.go.dev/git.tebibyte.media/tomo/nasin). [online documentation](https://pkg.go.dev/git.tebibyte.media/tomo/nasin).
Related repositories: ## Related Repositories
- [Tomo API](https://git.tebibyte.media/tomo/tomo): The API that all other parts - [Tomo API](https://git.tebibyte.media/tomo/tomo): The API that all other parts
of the toolkit agree on of the toolkit agree on
- [Objects](https://git.tebibyte.media/tomo/objects): A standard collection of - [Objects](https://git.tebibyte.media/tomo/objects): A standard collection of
re-usable objects and other GUI components re-usable objects and other GUI components
- [Backend](https://git.tebibyte.media/tomo/backend): The software responsible
for managing and rendering things behind the scenes

View File

@@ -144,15 +144,47 @@ func RunApplication (application Application) {
} }
flag.Parse() flag.Parse()
// open config
globalConfig, err := ApplicationConfig(GlobalApplicationDescription())
if err != nil { log.Fatalln("nasin: could not open config:", err) }
currentGlobalConfig = globalConfig
defer func () {
globalConfig.Close()
currentGlobalConfig = nil
} ()
styleConfigKey := "Style"
iconSetConfigKey := "IconSet"
// registry
// TODO: rebuild registry around the config
reg := new(registrar.Registrar) reg := new(registrar.Registrar)
backend, err := reg.SetBackend() backend, err := reg.SetBackend()
if err != nil { log.Fatalln("nasin: could not register backend:", err) } if err != nil { log.Fatalln("nasin: could not register backend:", err) }
err = reg.SetTheme()
if err != nil { log.Fatalln("nasin: could not set theme:", err) }
err = reg.SetIconSet()
if err != nil { log.Fatalln("nasin: could not set icon set:", err) }
err = reg.SetFaceSet() err = reg.SetFaceSet()
if err != nil { log.Fatalln("nasin: could not set face set:", err) } if err != nil { log.Fatalln("nasin: could not set face set:", err) }
updateStyle := func () {
value, err := globalConfig.GetString(styleConfigKey, "")
if err != nil { log.Fatalln("nasin: could not set theme:", err) }
err = reg.SetStyle(value)
if err != nil { log.Fatalln("nasin: could not set theme:", err) }
}
updateIconSet := func () {
value, err := globalConfig.GetString(iconSetConfigKey, "")
if err != nil { log.Fatalln("nasin: could not set icon set:", err) }
err = reg.SetIconSet(value)
if err != nil { log.Fatalln("nasin: could not set icon set:", err) }
}
updateStyle()
updateIconSet()
globalConfig.OnChange(func (key string) {
switch key {
case styleConfigKey: updateStyle()
case iconSetConfigKey: updateIconSet()
}
})
// init application
err = application.Init() err = application.Init()
if err != nil { log.Fatalln("nasin: could not run application:", err) } if err != nil { log.Fatalln("nasin: could not run application:", err) }
@@ -196,6 +228,14 @@ func ApplicationConfig (app ApplicationDescription) (config.ConfigCloser, error)
return config.NewConfig(user, system...) return config.NewConfig(user, system...)
} }
var currentGlobalConfig config.Config
// GlobalConfig returns the global config. It contains options that apply to
// Tomo/Nasin itself, such as the style sheet and the icon set. This is managed
// by Nasin and must not be closed by the application.
func GlobalConfig () config.Config {
return currentGlobalConfig
}
func errorPopupf (title, format string, v ...any) func (func ()) { func errorPopupf (title, format string, v ...any) func (func ()) {
return func (callback func ()) { return func (callback func ()) {
dialog, err := objects.NewDialogOk ( dialog, err := objects.NewDialogOk (

78
config/README.md Normal file
View File

@@ -0,0 +1,78 @@
# config
Package config provides a configuration system for applications.
## Config File Location
Config files are stored in standard operating system locations. Each application
has exactly one user-level config directory within the user-level config
location, and a system-level subdirectory in each of the system-level config
locations (if applicable). Each subdirectory bears the application's well-known
name as specified in ApplicationDescription. Each subdirectory contains a file
called config.conf, which is where the actual config data is stored.
The user-level configuration file takes precendence over system configuration
files, and system configuration files take precedence over eachother depending
on what order they are specified in. How they are specified depends on the
operating system.
### Linux, Most Unixes
In terms of the XDG Base Directory Specification, an application with the
well-known name com.example.Example would have its config files stored at
`$XDG_CONFIG_DIRS/com.example.Example/config.conf`. On most systems where this
specification is applicable, this will result in a file at
`/etc/xdg/com.example.Example/config.conf` and another at
`$HOME/.config/com.example.Example/config.conf`. The location for config files
on systems that do not make use of this specification is yet to be determined.
## Config File Format
The general format of the file is as follows:
- Encoded in UTF-8
- Consists of lines, separated by \n, or \r\n
- Lines can be any of these:
- Blank line: has only whitespace
- Comment: begins with a '#'
- Entry: a key/value pair separated by an '=' sign
### Entries
For entries, all whitespace on either side of the '=' sign, the key, or the
value is ignored. The key may contain any letter or digit, as well as '-' and
'.'. The value is always identified by its first rune (after the preliminary
whitespace of course) and can be one of:
- String
- Number
- Bool
#### String
A string can be either double-quoted, or any string of runes not identifiable
as any other kind of value. Quoted strings are always unquoted when they are
read. Either way, these escape sequences are supported, and resolved when they
are read:
- '\\\\': a literal backslash
- '\a': alert, bell
- '\b': backspace
- '\t': horizontal tab
- '\n': line feed
- '\v': vertical tab
- '\f': form feed
- '\r': carriage return
- '\\"': double quote
Be aware that some unquoted strings, within reason, are subject to being read
as some other value in the future. For example, if there were suddenly a
third boolean value called glorble, the unquoted string glorble would be read
as a boolean value instead of a string.
#### Number
A number is a floating point value. It can be of the form:
- Inf: positive infinity
- -Inf: negative infinity
- NaN: "not a number"
- [0-9]+: a whole number
- [0-9]+\.[0-9]*: a fractional number
#### Bool
A bool is a boolean value. It can be one of:
- true
- false

View File

@@ -54,11 +54,15 @@ type Config interface {
// not a boolean, it will return fallback. // not a boolean, it will return fallback.
GetBool (key string, fallback bool) (bool, error) GetBool (key string, fallback bool) (bool, error)
// Set sets a value in the user-level config file. If the key is // Set sets a value in the user-level config file. If the key is
// invalid, it returns ErrMalformedKey. // invalid, it returns ErrMalformedKey. Note that calling this behavior
// *will* cause a write to disk, and a read from disk for whatever is
// watching the user file.
Set (key string, value Value) error Set (key string, value Value) error
// Reset removes the value from the user-level config file, resetting it // Reset removes the value from the user-level config file, resetting it
// to what is described by the system-level config files. If the key is // to what is described by the system-level config files. If the key is
// invalid, it returns ErrMalformedKey. // invalid, it returns ErrMalformedKey. Note that calling this behavior
// *will* cause a write to disk if successful , and a read from disk for
// whatever is watching the user file.
Reset (key string) error Reset (key string) error
// OnChange specifies a function to be called whenever a value is // OnChange specifies a function to be called whenever a value is
// changed. The callback is always run within the backend's event loop // changed. The callback is always run within the backend's event loop

View File

@@ -13,6 +13,8 @@ type line any
type comment string type comment string
type entry struct { key string; value Value } type entry struct { key string; value Value }
var _ io.WriterTo = new(File)
// File represents a config file. It preserves the order of the lines, as well // File represents a config file. It preserves the order of the lines, as well
// as blank lines and comments. // as blank lines and comments.
type File struct { type File struct {
@@ -29,47 +31,8 @@ func NewFile () *File {
// Parse parses a config file from a reader. This function operates on a // Parse parses a config file from a reader. This function operates on a
// best-effort basis: A file will always be returned, and any errors encountered // best-effort basis: A file will always be returned, and any errors encountered
// will be joined together. // will be joined together. For a description of the format, see the README.md
// // of this package.
// The general format of the file is as follows:
// - Encoded in UTF-8
// - Consists of lines, separated by \n, or \r\n
// - Lines can be any of these:
// - Blank line: has only whitespace
// - Comment: begins with a '#'
// - Entry: a key/value pair separated by an '=' sign
//
// For entries, all whitespace on either side of the '=' sign, the key, or the
// value is ignored. The key may contain any letter or digit, as well as '-'
// and '.'. The value is always identified by its first rune (after the
// preliminary whitespace of course) and can be one of:
// - String: either a double-quoted string, or any string of runes not
// identifiable as any other kind of value. The quoted string is always
// unquoted when it is read. Either way, these escape sequences are
// supported, and resolved when they are read:
// - '\\': a literal backslash
// - '\a': alert, bell
// - '\b': backspace
// - '\t': horizontal tab
// - '\n': line feed
// - '\v': vertical tab
// - '\f': form feed
// - '\r': carriage return
// - '\"': double quote
// - Number: a floating point value. It can be of the form:
// - Inf
// - -Inf
// - NaN
// - [0-9]+
// - [0-9]+\.[0-9]*
// - Bool: a boolean value. It can be one of:
// - true
// - false
//
// Be aware that some unquoted strings, within reason, are subject to being read
// as some other value in the future. For example, if there were suddenly a
// third boolean value called glorble, the unquoted string glorble would be read
// as a boolean value instead of a string.
func Parse (reader io.Reader) (*File, error) { func Parse (reader io.Reader) (*File, error) {
file := &File { file := &File {
keys: make(map[string] int), keys: make(map[string] int),
@@ -147,8 +110,12 @@ func ParseValue (str string) (Value, error) {
// ErrMalformedKey. // ErrMalformedKey.
func (this *File) Has (key string) (bool, error) { func (this *File) Has (key string) (bool, error) {
if !KeyValid(key) { return false, ErrMalformedKey } if !KeyValid(key) { return false, ErrMalformedKey }
_, ok := this.keys[key] if index, ok := this.keys[key]; ok {
return ok, nil if _, ok := this.lines[index].(entry); ok {
return true, nil
}
}
return false, nil
} }
// Get gets the keyed value. If the value is unspecified, it returns nil, // Get gets the keyed value. If the value is unspecified, it returns nil,
@@ -156,7 +123,7 @@ func (this *File) Has (key string) (bool, error) {
func (this *File) Get (key string) (Value, error) { func (this *File) Get (key string) (Value, error) {
if !KeyValid(key) { return nil, ErrMalformedKey } if !KeyValid(key) { return nil, ErrMalformedKey }
if index, ok := this.keys[key]; ok { if index, ok := this.keys[key]; ok {
if lin := this.lines[index].(entry); ok { if lin, ok := this.lines[index].(entry); ok {
return lin.value, nil return lin.value, nil
} }
} }
@@ -166,14 +133,16 @@ func (this *File) Get (key string) (Value, error) {
// Set sets a value. If the key is invalid, it returns ErrMalformedKey. // Set sets a value. If the key is invalid, it returns ErrMalformedKey.
func (this *File) Set (key string, value Value) error { func (this *File) Set (key string, value Value) error {
if !KeyValid(key) { return ErrMalformedKey } if !KeyValid(key) { return ErrMalformedKey }
ent := entry {
key: key,
value: value,
}
if index, ok := this.keys[key]; ok { if index, ok := this.keys[key]; ok {
ent := this.lines[index].(entry)
ent.value = value
this.lines[index] = ent this.lines[index] = ent
return nil return nil
} }
this.keys[key] = len(this.lines) this.keys[key] = len(this.lines)
this.lines = append(this.lines, value) this.lines = append(this.lines, ent)
return nil return nil
} }
@@ -212,19 +181,31 @@ func (this *File) Diff (other *File) map[string] struct { } {
// - keys only we have // - keys only we have
// - keys we both have, but are different // - keys we both have, but are different
for key, index := range this.keys { for key, index := range this.keys {
thisEntry, ok := this.lines[index].(entry)
if !ok { continue }
otherIndex, ok := other.keys[key] otherIndex, ok := other.keys[key]
if !ok { if !ok {
diff[key] = struct { } { } diff[key] = struct { } { }
continue continue
} }
if !this.lines[index].(entry).value.Equals(other.lines[otherIndex].(entry).value) { otherEntry, ok := other.lines[otherIndex].(entry)
if !ok {
diff[key] = struct { } { }
continue
}
if !thisEntry.value.Equals(otherEntry.value) {
diff[key] = struct { } { } diff[key] = struct { } { }
} }
} }
// - keys only they have // - keys only they have
for key := range other.keys { for key := range other.keys {
if _, has := this.keys[key]; !has { if otherHas, _ := other.Has(key); !otherHas {
continue
}
if thisHas, _ := this.Has(key); !thisHas {
diff[key] = struct { } { } diff[key] = struct { } { }
} }
} }
@@ -233,8 +214,8 @@ func (this *File) Diff (other *File) map[string] struct { } {
} }
// WriteTo writes the data in this file to an io.Writer. // WriteTo writes the data in this file to an io.Writer.
func (file *File) WriteTo (writer io.Writer) (n int64, err error) { func (this *File) WriteTo (writer io.Writer) (n int64, err error) {
for _, lin := range file.lines { for _, lin := range this.lines {
nint := 0 nint := 0
switch lin := lin.(type) { switch lin := lin.(type) {
case comment: case comment:

View File

@@ -123,6 +123,36 @@ yetAnotherThing = 0.23498
} }
} }
func TestDiffReset (test *testing.T) {
file1 := parseFileString(test,
`key4=0
key1=value1
keyToDelete=true
# comment
key2=34`)
file2 := parseFileString(test,
`key1=value2
key2=34
anotherKeyToDelete=false
# comment
key3=0.2`)
file1.Reset("keyToDelete")
file2.Reset("anotherKeyToDelete")
diff := file1.Diff(file2)
correct := map[string] struct { } {
"key1": struct { } { },
"key3": struct { } { },
"key4": struct { } { },
}
if !maps.Equal(diff, correct) {
test.Error("diffs do not match")
test.Errorf("EXPECTED:\n%v", correct)
test.Errorf("GOT:\n%v", diff)
test.Fail()
}
}
func TestDiff (test *testing.T) { func TestDiff (test *testing.T) {
file1 := parseFileString(test, file1 := parseFileString(test,
`key4=0 `key4=0

View File

@@ -19,6 +19,7 @@ type config struct {
open bool open bool
watcher *fsnotify.Watcher watcher *fsnotify.Watcher
lock sync.RWMutex lock sync.RWMutex
ignoreNextUserUpdate bool
paths struct { paths struct {
user string user string
@@ -68,10 +69,13 @@ func (this *config) lockAndProcessEvent (event fsnotify.Event) {
if _, ok := this.paths.watching[event.Name]; !ok { return } if _, ok := this.paths.watching[event.Name]; !ok { return }
if event.Name == this.paths.user { if event.Name == this.paths.user {
if !this.ignoreNextUserUpdate {
previousUser := this.data.user previousUser := this.data.user
this.reloadUser() this.reloadUser()
newUser := this.data.user newUser := this.data.user
this.processUserDiff(newUser.Diff(previousUser)) this.processUserDiff(newUser.Diff(previousUser))
}
this.ignoreNextUserUpdate = false
} else { } else {
index := slices.Index(this.paths.system, event.Name) index := slices.Index(this.paths.system, event.Name)
if index > 0 { if index > 0 {
@@ -148,6 +152,7 @@ func (this *config) saveUser () error {
defer file.Close() defer file.Close()
_, err = this.data.user.WriteTo(file) _, err = this.data.user.WriteTo(file)
if err != nil { return err } if err != nil { return err }
this.ignoreNextUserUpdate = true
return nil return nil
} }
@@ -262,7 +267,10 @@ func (this *config) Set (key string, value Value) error {
if this.data.user == nil { this.data.user = NewFile() } if this.data.user == nil { this.data.user = NewFile() }
err := this.data.user.Set(key, value) err := this.data.user.Set(key, value)
if err != nil { return err } if err != nil { return err }
return this.saveUser() err = this.saveUser()
if err != nil { return err }
this.broadcastChange(key)
return nil
} }
func (this *config) Reset (key string) error { func (this *config) Reset (key string) error {
@@ -271,7 +279,10 @@ func (this *config) Reset (key string) error {
if this.data.user == nil { this.data.user = NewFile() } if this.data.user == nil { this.data.user = NewFile() }
err := this.data.user.Reset(key) err := this.data.user.Reset(key)
if err != nil { return err } if err != nil { return err }
return this.saveUser() err = this.saveUser()
if err != nil { return err }
this.broadcastChange(key)
return nil
} }
func (this *config) OnChange (callback func (string)) event.Cookie { func (this *config) OnChange (callback func (string)) event.Cookie {

View File

@@ -6,9 +6,10 @@ import _ "embed"
import _ "image/png" import _ "image/png"
import "git.tebibyte.media/tomo/tomo" import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/tomo/data" import "git.tebibyte.media/tomo/tomo/data"
import "git.tebibyte.media/tomo/nasin/internal/util" import "git.tebibyte.media/tomo/tomo/event"
import "git.tebibyte.media/tomo/tomo/canvas" import "git.tebibyte.media/tomo/tomo/canvas"
import "git.tebibyte.media/tomo/backend/style" import "git.tebibyte.media/tomo/backend/style"
import "git.tebibyte.media/tomo/nasin/internal/util"
//go:embed assets/icons-small.png //go:embed assets/icons-small.png
var atlasSmallBytes []byte var atlasSmallBytes []byte
@@ -35,7 +36,7 @@ const (
iconXOfficeSpreadsheet = tomo.Icon("x-office-spreadsheet") iconXOfficeSpreadsheet = tomo.Icon("x-office-spreadsheet")
) )
func generateSource (data []byte, width int) map[tomo.Icon] canvas.Texture { func generateSource (data []byte, width int) (canvas.TextureCloser, map[tomo.Icon] canvas.Texture) {
atlasImage, _, err := image.Decode(bytes.NewReader(data)) atlasImage, _, err := image.Decode(bytes.NewReader(data))
if err != nil { panic(err) } if err != nil { panic(err) }
atlasTexture := tomo.NewTexture(atlasImage) atlasTexture := tomo.NewTexture(atlasImage)
@@ -448,23 +449,26 @@ func generateSource (data []byte, width int) map[tomo.Icon] canvas.Texture {
col(tomo.IconWeatherSnow) col(tomo.IconWeatherSnow)
col(tomo.IconWeatherStorm) col(tomo.IconWeatherStorm)
return source return atlasTexture, source
} }
type iconSet struct { type iconSet struct {
atlasSmall canvas.TextureCloser
atlasLarge canvas.TextureCloser
texturesSmall map[tomo.Icon] canvas.Texture texturesSmall map[tomo.Icon] canvas.Texture
texturesLarge map[tomo.Icon] canvas.Texture texturesLarge map[tomo.Icon] canvas.Texture
} }
// New creates a new fallback icon set. // New creates a new fallback icon set.
func New () style.IconSet { func New () (style.IconSet, event.Cookie) {
return new(iconSet) iconSet := new(iconSet)
return iconSet, iconSet
} }
func (this *iconSet) ensure () { func (this *iconSet) ensure () {
if this.texturesSmall != nil { return } if this.texturesSmall != nil { return }
this.texturesSmall = generateSource(atlasSmallBytes, 16) this.atlasSmall, this.texturesSmall = generateSource(atlasSmallBytes, 16)
this.texturesLarge = generateSource(atlasLargeBytes, 32) this.atlasLarge, this.texturesLarge = generateSource(atlasLargeBytes, 32)
} }
func (this *iconSet) selectSource (size tomo.IconSize) map[tomo.Icon] canvas.Texture { func (this *iconSet) selectSource (size tomo.IconSize) map[tomo.Icon] canvas.Texture {
@@ -500,3 +504,12 @@ func (this *iconSet) MimeIcon (mime data.Mime, size tomo.IconSize) canvas.Textur
return source[tomo.Icon(iconApplicationXGeneric)] return source[tomo.Icon(iconApplicationXGeneric)]
} }
} }
func (this *iconSet) Close () {
if this.atlasSmall != nil {
this.atlasSmall.Close()
}
if this.atlasLarge != nil {
this.atlasLarge.Close()
}
}

View File

@@ -9,6 +9,7 @@ import "strings"
import _ "image/png" import _ "image/png"
import "git.tebibyte.media/tomo/tomo" import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/tomo/data" import "git.tebibyte.media/tomo/tomo/data"
import "git.tebibyte.media/tomo/tomo/event"
import "git.tebibyte.media/tomo/tomo/canvas" import "git.tebibyte.media/tomo/tomo/canvas"
import "git.tebibyte.media/tomo/backend/style" import "git.tebibyte.media/tomo/backend/style"
import "git.tebibyte.media/tomo/nasin/internal/util" import "git.tebibyte.media/tomo/nasin/internal/util"
@@ -17,27 +18,27 @@ import xdgIconTheme "git.tebibyte.media/tomo/xdg/icon-theme"
type iconTheme struct { type iconTheme struct {
xdg xdgIconTheme.Theme xdg xdgIconTheme.Theme
fallback style.IconSet fallback style.IconSet
texturesSmall map[tomo.Icon] canvas.Texture texturesSmall map[tomo.Icon] canvas.TextureCloser
texturesMedium map[tomo.Icon] canvas.Texture texturesMedium map[tomo.Icon] canvas.TextureCloser
texturesLarge map[tomo.Icon] canvas.Texture texturesLarge map[tomo.Icon] canvas.TextureCloser
} }
func FindThemeWarn (name string, fallback style.IconSet, path ...string) (style.IconSet, error) { func FindThemeWarn (name string, fallback style.IconSet, path ...string) (style.IconSet, event.Cookie, error) {
this := &iconTheme { this := &iconTheme {
fallback: fallback, fallback: fallback,
texturesLarge: make(map[tomo.Icon] canvas.Texture), texturesLarge: make(map[tomo.Icon] canvas.TextureCloser),
texturesMedium: make(map[tomo.Icon] canvas.Texture), texturesMedium: make(map[tomo.Icon] canvas.TextureCloser),
texturesSmall: make(map[tomo.Icon] canvas.Texture), texturesSmall: make(map[tomo.Icon] canvas.TextureCloser),
} }
xdg, err := xdgIconTheme.FindThemeWarn(name, path...) xdg, err := xdgIconTheme.FindThemeWarn(name, path...)
if err != nil { return nil, err } if err != nil { return nil, nil, err }
this.xdg = xdg this.xdg = xdg
return this, nil return this, this, nil
} }
func (this *iconTheme) selectSource (size tomo.IconSize) map[tomo.Icon] canvas.Texture { func (this *iconTheme) selectSource (size tomo.IconSize) map[tomo.Icon] canvas.TextureCloser {
switch size { switch size {
case tomo.IconSizeMedium: return this.texturesMedium case tomo.IconSizeMedium: return this.texturesMedium
case tomo.IconSizeLarge: return this.texturesLarge case tomo.IconSizeLarge: return this.texturesLarge
@@ -45,7 +46,7 @@ func (this *iconTheme) selectSource (size tomo.IconSize) map[tomo.Icon] canvas.T
} }
} }
func (this *iconTheme) xdgIcon (name string, size tomo.IconSize) (canvas.Texture, bool) { func (this *iconTheme) xdgIcon (name string, size tomo.IconSize) (canvas.TextureCloser, bool) {
// TODO use scaling factor instead of 1 // TODO use scaling factor instead of 1
// find icon file // find icon file
icon, err := this.xdg.FindIcon(name, iconSizePixels(size), 1, xdgIconTheme.PNG) icon, err := this.xdg.FindIcon(name, iconSizePixels(size), 1, xdgIconTheme.PNG)
@@ -100,7 +101,20 @@ func (this *iconTheme) MimeIcon (mime data.Mime, size tomo.IconSize) canvas.Text
} }
} }
func (this *iconTheme) icon (icon tomo.Icon, size tomo.IconSize) canvas.Texture { func (this *iconTheme) Close () {
closeAllIn := func (mp map[tomo.Icon] canvas.TextureCloser) {
for _, texture := range mp {
if texture != nil {
texture.Close()
}
}
}
closeAllIn(this.texturesSmall)
closeAllIn(this.texturesMedium)
closeAllIn(this.texturesLarge)
}
func (this *iconTheme) icon (icon tomo.Icon, size tomo.IconSize) canvas.TextureCloser {
if texture, ok := this.xdgIcon(XdgIconName(icon), size); ok { if texture, ok := this.xdgIcon(XdgIconName(icon), size); ok {
return texture return texture
} }
@@ -110,7 +124,7 @@ func (this *iconTheme) icon (icon tomo.Icon, size tomo.IconSize) canvas.Texture
return nil return nil
} }
func (this *iconTheme) mimeIcon (mime data.Mime, size tomo.IconSize) canvas.Texture { func (this *iconTheme) mimeIcon (mime data.Mime, size tomo.IconSize) canvas.TextureCloser {
if texture, ok := this.xdgIcon(xdgFormatMime(mime), size); ok { if texture, ok := this.xdgIcon(xdgFormatMime(mime), size); ok {
return texture return texture
} }

View File

@@ -1,10 +1,10 @@
//go:build unix && (!darwin) //go:build unix && (!darwin)
package registrar package registrar
import "os"
import "log" import "log"
import "git.tebibyte.media/tomo/tomo" import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/backend/x" import "git.tebibyte.media/tomo/backend/x"
import "git.tebibyte.media/tomo/tomo/event"
import "git.tebibyte.media/sashakoshka/goparse" import "git.tebibyte.media/sashakoshka/goparse"
import "git.tebibyte.media/tomo/nasin/internal/icons/xdg" import "git.tebibyte.media/tomo/nasin/internal/icons/xdg"
import "git.tebibyte.media/tomo/nasin/internal/styles/tss" import "git.tebibyte.media/tomo/nasin/internal/styles/tss"
@@ -14,6 +14,8 @@ import "git.tebibyte.media/tomo/nasin/internal/faces/fallback"
type Registrar struct { type Registrar struct {
backend *x.Backend backend *x.Backend
iconSetCookie event.Cookie
styleCookie event.Cookie
} }
func (this *Registrar) SetBackend () (tomo.Backend, error) { func (this *Registrar) SetBackend () (tomo.Backend, error) {
@@ -24,38 +26,50 @@ func (this *Registrar) SetBackend () (tomo.Backend, error) {
return backend, nil return backend, nil
} }
func (this *Registrar) SetTheme () error { func (this *Registrar) SetStyle (name string) error {
styleSheetName := os.Getenv("TOMO_STYLE_SHEET") if this.styleCookie != nil {
if styleSheetName != "" { this.styleCookie.Close()
styl, _, err := tss.LoadFile(styleSheetName) this.styleCookie = nil
}
if name != "" {
styl, cookie, err := tss.LoadFile(name)
if err == nil { if err == nil {
this.backend.SetStyle(styl) this.backend.SetStyle(styl)
this.styleCookie = cookie
return nil return nil
} else { } else {
log.Printf ( log.Printf (
"nasin: could not load style sheet '%s'\n%v", "nasin: could not load style sheet '%s'\n%v",
styleSheetName, parse.Format(err)) name, parse.Format(err))
} }
} }
styl, _ := fallbackStyle.New() styl, cookie := fallbackStyle.New()
this.styleCookie = cookie
this.backend.SetStyle(styl) this.backend.SetStyle(styl)
return nil return nil
} }
func (this *Registrar) SetIconSet () error { func (this *Registrar) SetIconSet (name string) error {
iconSet := fallbackIcons.New() if this.iconSetCookie != nil {
iconSetName := os.Getenv("TOMO_XDG_ICON_THEME") this.iconSetCookie.Close()
if iconSetName != "" { this.iconSetCookie = nil
xdgIconSet, err := xdgIcons.FindThemeWarn(iconSetName, iconSet) }
iconSet, cookie := fallbackIcons.New()
if name != "" {
xdgIconSet, xdgCookie, err := xdgIcons.FindThemeWarn(name, iconSet)
cookie = event.MultiCookie(cookie, xdgCookie)
if err == nil { if err == nil {
iconSet = xdgIconSet iconSet = xdgIconSet
} else { } else {
log.Printf("nasin: could not load icon theme '%s': %v", iconSetName, err) log.Printf("nasin: could not load icon theme '%s': %v", name, err)
} }
} }
this.backend.SetIconSet(iconSet) this.backend.SetIconSet(iconSet)
this.iconSetCookie = cookie
return nil return nil
} }

11
path.go
View File

@@ -11,8 +11,9 @@ func ApplicationUserDataDir (app ApplicationDescription) (string, error) {
} }
// ApplicationSystemDataDirs returns a list of directory paths where an // ApplicationSystemDataDirs returns a list of directory paths where an
// application can look for its system-level data files. These directories may // application can look for its system-level data files. Directories returned
// or may not exist. This function may return an empty slice on some platforms. // by this function may or may not actually exist. This function may return an
// empty slice on some platforms.
func ApplicationSystemDataDirs (app ApplicationDescription) ([]string, error) { func ApplicationSystemDataDirs (app ApplicationDescription) ([]string, error) {
return systemDirs(app.ID, systemDataDirs) return systemDirs(app.ID, systemDataDirs)
} }
@@ -24,9 +25,9 @@ func ApplicationUserConfigDir (app ApplicationDescription) (string, error) {
} }
// ApplicationSystemDataDirs returns a list of directory paths where an // ApplicationSystemDataDirs returns a list of directory paths where an
// application can look for its system-level configuration files. These // application can look for its system-level configuration files. Directories
// directories may or may not exist. This function may return an empty slice on // returned by this function may or may not actually exist. This function may
// some platforms. // return an empty slice on some platforms.
func ApplicationSystemConfigDirs (app ApplicationDescription) ([]string, error) { func ApplicationSystemConfigDirs (app ApplicationDescription) ([]string, error) {
return systemDirs(app.ID, systemConfigDirs) return systemDirs(app.ID, systemConfigDirs)
} }