Compare commits

..

65 Commits
v0.4.0 ... main

Author SHA1 Message Date
c9328e9585 Actually make use of ApplicationURLOpener
Progress on #4
2024-07-04 22:45:09 -04:00
d38fc8274c Remove examples and dependency on objects
We ought to put examples somewhere else
2024-07-04 20:42:22 -04:00
52330f2941 Upgrade backend, objects 2024-06-26 10:53:54 -04:00
26179d8ff7 Add styling for tabs to Wintergreen 2024-06-26 10:46:57 -04:00
fb3d3b0919 Update objects 2024-06-19 00:43:36 -04:00
8e2fb26ab5 Add styling for calendars 2024-06-19 00:31:50 -04:00
45b2bee72d Update examples 2024-06-19 00:17:44 -04:00
99be133432 Update backend 2024-06-18 19:46:52 -04:00
1fe74c8e69 Update objects 2024-06-18 19:45:18 -04:00
e96ca7f7d6 Update icons example (kind of) 2024-06-14 02:33:10 -04:00
046556cce3 Update registrar 2024-06-12 00:27:34 -04:00
03ca852475 Restructure internal theme 2024-06-12 00:19:12 -04:00
86fb87c7f6 Update Tomo API 2024-06-12 00:19:06 -04:00
157d617ffd Remove debug messaging when getting application icon 2024-06-07 18:16:16 -04:00
4fff592a70 Add styling for tear line 2024-06-07 17:36:10 -04:00
880904d5fa Make changes to application.go to account for new API 2024-06-07 02:01:00 -04:00
593a74924d Update dependency versions 2024-06-07 02:00:36 -04:00
bf50e8c27a Change file naming in aluminum theme 2024-06-07 01:56:31 -04:00
a5f7feb5eb Remove the FS thing and use normal paths 2024-06-06 22:55:14 -04:00
b87f3445e4 Revert aluminum styling changes for TextBox 2024-06-06 22:42:37 -04:00
1bc08bcfe4 Add ApplicationURLOpener interface
Eventually we can have nasin parse cli args and figure out what
files to open, instructing the application to open those files.
We will also be able to have nasin connect to dbus using the
application ID and open files in an already running instance of the
application.
2024-06-06 22:38:51 -04:00
d5d9f3abfb NewApplicationWindow automatically sets an application icon 2024-06-06 20:36:25 -04:00
6cb908ea6e Tweaks to the aluminum theme 2024-06-03 22:49:02 -04:00
2db501e66c Update clock example 2024-06-03 22:03:19 -04:00
8bd6fac8a8 Update internal theme to use Roles stored within Boxes 2024-06-03 21:59:28 -04:00
4bb7539718 Update dependencies 2024-06-03 21:59:08 -04:00
7511262309 Update backend version 2024-06-03 03:48:25 -04:00
19b71a7cec Add aluminum style 2024-06-03 03:48:13 -04:00
f9432efc82 Stop using the monolithic backend 2024-06-03 03:04:47 -04:00
8d9bdd5cb8 Restructure internal/theme 2024-06-03 03:04:29 -04:00
cf3b7ca651 XDG icon theme loader now uses Wintergreen icons as fallback 2024-05-28 22:13:42 -04:00
fc41696b5e Wintergreen icon theme returns nil if there is no suitable icon 2024-05-28 22:00:06 -04:00
bea78be331 Update Wintergreen style 2024-05-28 21:57:48 -04:00
0eced435a0 Tweak icons example 2024-05-28 21:57:34 -04:00
072eaa6029 XDG icon themes can be loaded by setting $TOMO_XDG_ICON_THEME 2024-05-28 21:56:52 -04:00
7d9d93fa3f Wintergreen theme constructor returns *dataTheme.Theme 2024-05-28 21:56:20 -04:00
fc8da2abd5 Add XDG icon theme loader 2024-05-28 21:55:51 -04:00
b9163ffe39 Update internal registrar to use new API 2024-05-27 16:03:11 -04:00
908dbd0bad Update Wintergreen theme to use new API 2024-05-27 16:02:57 -04:00
9e2d1ecf01 Update internal/theme to use new API 2024-05-27 16:02:39 -04:00
1142cb7ab6 Update examples to use new API 2024-05-27 16:01:50 -04:00
a52a703ec1 Update dependencies, Tomo API 2024-05-27 16:01:34 -04:00
4557769cb4 Icon example now shows icon details when one is clicked 2024-05-20 13:58:07 -04:00
a514e99d51 Update X backend 2024-05-20 13:08:36 -04:00
6ad413ee48 Add license viewing example 2024-05-18 14:30:18 -04:00
311eb0ecd2 Add styling for TextView 2024-05-18 14:29:57 -04:00
d559cfe9da Update objects 2024-05-18 14:29:49 -04:00
1166850fe2 Icons example uses new flow layout 2024-05-17 15:22:05 -04:00
fcac15ac4d Update objects, x backend 2024-05-17 15:20:05 -04:00
0be749c8bd Update go.sum 2024-05-15 02:07:13 -04:00
93de3f4e36 Update X backend 2024-05-15 02:05:34 -04:00
5c3a0d26f8 More README tweaks 2024-05-14 12:50:49 -04:00
57f73fb585 Fix syntax error in README 2024-05-14 12:49:22 -04:00
a08b68b881 Add more info to README 2024-05-14 12:48:44 -04:00
8aff032ebb Icon example scrolls! 2024-05-13 20:11:24 -04:00
ac51d3dc9f Upgrade objects, x 2024-05-13 19:56:07 -04:00
573ab6bcdc Update Wintergreen theme 2024-05-13 19:55:57 -04:00
c1686e336b Upgrade X version 2024-05-07 20:25:15 -04:00
dfd566b23d Include new objects in Wintergreen theme 2024-05-07 20:22:53 -04:00
da4af8d240 Add examples 2024-05-07 20:22:39 -04:00
d90fb327db Add icons example 2024-05-07 01:30:12 -04:00
2b9f17b612 Add clock example 2024-05-07 01:30:06 -04:00
2cde2cc6d5 Update X backend 2024-05-07 00:46:52 -04:00
69576c7aca Add basic icons to wintergreen theme 2024-05-06 23:25:53 -04:00
a11cc2cb89 Rename registry to registrar 2024-05-06 23:25:44 -04:00
28 changed files with 2009 additions and 501 deletions

View File

@ -2,5 +2,12 @@
[![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. Parts of Tomo that Nasin provides an easy way to write applications with Tomo. To get started, take
aren't the GUI toolkit may be found here. a look at the [examples](examples) directory and the
[online documentation](https://pkg.go.dev/git.tebibyte.media/tomo/nasin).
Related repositories:
- [Tomo API](https://git.tebibyte.media/tomo/tomo): The API that all other parts
of the toolkit agree on
- [Objects](https://git.tebibyte.media/tomo/objects): A standard collection of
re-usable objects and other GUI components

View File

@ -1,9 +1,15 @@
package nasin package nasin
import "fmt"
import "log" import "log"
import "flag"
import "image" import "image"
import "strings"
import "net/url"
import "git.tebibyte.media/tomo/tomo" import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/nasin/internal/registry" import "git.tebibyte.media/tomo/objects"
import "git.tebibyte.media/tomo/tomo/canvas"
import "git.tebibyte.media/tomo/nasin/internal/registrar"
// Application represents an application object. // Application represents an application object.
type Application interface { type Application interface {
@ -14,6 +20,35 @@ type Application interface {
Init () error Init () error
} }
// ApplicationURLOpener is an application that can open a URL.
type ApplicationURLOpener interface {
Application
// OpenURL opens a new window with the contents of the given URL. If the
// given URL is unsupported, it returns an error (for example, an image
// viewer is not expected to open a text file).
//
// Applications should support the file:// scheme at the very least, and
// should also support others like http:// and https:// if possible.
OpenURL (*url.URL) error
// OpenNone is called when the application is launched without any URLs
// to open. The application may create some sort of default starting
// window, or call tomo.Stop().
OpenNone ()
}
// ApplicationFlagAdder is an application that supports reading command line
// flags.
type ApplicationFlagAdder interface {
Application
// AddFlags is called before Init and given the default flag set that
// Nasin uses to parse command line arguments. Note that when this
// method is called, Tomo will not yet be initialized.
AddFlags (*flag.FlagSet)
}
// ApplicationDescription describes the name and type of an application. // ApplicationDescription describes the name and type of an application.
type ApplicationDescription struct { type ApplicationDescription struct {
// The name of the application. // The name of the application.
@ -76,24 +111,111 @@ type ApplicationRole string; const (
RoleChecklist ApplicationRole = "Checklist" RoleChecklist ApplicationRole = "Checklist"
) )
// Icon returns the icon ID for this role.
func (role ApplicationRole) Icon () tomo.Icon {
if role == "" {
return tomo.IconApplication
} else {
return tomo.Icon("Application" + strings.ReplaceAll(string(role), " ", ""))
}
}
// RunApplication is like tomo.Run, but runs an application. If something fails // RunApplication is like tomo.Run, but runs an application. If something fails
// to initialize, an error is written to the standard logger. // to initialize, an error is written to the standard logger.
func RunApplication (application Application) { func RunApplication (application Application) {
err := registry.Init() // TODO: see #4
if err != nil { log.Fatal("nasin: could not init registry:", err) }
if application, ok := application.(ApplicationFlagAdder); ok {
application.AddFlags(flag.CommandLine)
}
flag.Parse()
err := registrar.RegisterBackend()
if err != nil { log.Fatalln("nasin: could not register backend:", err) }
err = tomo.Run(func () { err = tomo.Run(func () {
err := application.Init() err := registrar.SetTheme()
if err != nil { log.Fatal("nasin: could not run application:", err) } if err != nil { log.Fatalln("nasin: could not set theme:", err) }
err = application.Init()
if err != nil { log.Fatalln("nasin: could not run application:", err) }
// open URLs
args := flag.Args()
applicationOpenUrls(application, args...)
}) })
if err != nil { log.Fatal("nasin: could not run application:", err) } if err != nil { log.Fatalln("nasin: could not run application:", err) }
} }
// NewApplicationWindow creates a window for an application. It will // NewApplicationWindow creates a window for an application. It will
// automatically set window information to signal to the OS that the window is // automatically set window information to signal to the OS that the window is
// owned by the application. // owned by the application. The window's icon will be automatically set by
func NewApplicationWindow (application Application, bounds image.Rectangle) (tomo.MainWindow, error) { // looking for an icon with the name of the application's ID. If that is not
// found, the default icon for the application's ApplicationRole will used.
func NewApplicationWindow (application Application, bounds image.Rectangle) (tomo.Window, error) {
window, err := tomo.NewWindow(bounds) window, err := tomo.NewWindow(bounds)
if err != nil { return nil, err } if err != nil { return nil, err }
window.SetTitle(application.Describe().String()) description := application.Describe()
window.SetTitle(description.Name)
setApplicationWindowIcon(window, description)
return window, nil return window, nil
} }
func applicationOpenUrls (application Application, args ...string) {
if application, ok := application.(ApplicationURLOpener); ok {
if len(args) <= 0 {
application.OpenNone()
}
openedAny := false
for _, arg := range flag.Args() {
ur, err := url.Parse(arg)
if err != nil {
log.Fatalf (
"nasin: invalid URL %v: %v",
arg, err)
}
if ur.Scheme == "" {
ur.Scheme = "file"
}
err = application.OpenURL(ur)
if err != nil {
dialog, err := objects.NewDialogOk (
objects.DialogError, nil,
"Could Not Open URL",
fmt.Sprintf (
"Could not open %v: %v",
arg, err),
func () {
if !openedAny {
application.OpenNone()
}
})
if err != nil { log.Fatal(err) }
dialog.SetVisible(true)
}
}
} else {
log.Fatal("nasin: this application cannot open URLs")
}
}
func setApplicationWindowIcon (window tomo.Window, description ApplicationDescription) {
allSizes := func (icon tomo.Icon) (sizes []canvas.Texture) {
small := icon.Texture(tomo.IconSizeSmall)
medium := icon.Texture(tomo.IconSizeMedium)
large := icon.Texture(tomo.IconSizeLarge)
if small != nil { sizes = append(sizes, small) }
if medium != nil { sizes = append(sizes, medium) }
if large != nil { sizes = append(sizes, large) }
return sizes
}
if sizes := allSizes(tomo.Icon(description.ID)); len(sizes) > 0 {
window.SetIcon(sizes...)
return
}
if sizes := allSizes(description.Role.Icon()); len(sizes) > 0 {
window.SetIcon(sizes...)
return
}
}

13
go.mod
View File

@ -3,17 +3,18 @@ module git.tebibyte.media/tomo/nasin
go 1.20 go 1.20
require ( require (
git.tebibyte.media/tomo/tomo v0.31.0 git.tebibyte.media/tomo/backend v0.4.1
git.tebibyte.media/tomo/x v0.7.0 git.tebibyte.media/tomo/objects v0.19.0
git.tebibyte.media/tomo/tomo v0.38.0
git.tebibyte.media/tomo/xdg v0.1.0 git.tebibyte.media/tomo/xdg v0.1.0
golang.org/x/image v0.11.0
) )
require ( require (
git.tebibyte.media/tomo/typeset v0.7.0 // indirect git.tebibyte.media/tomo/typeset v0.7.1 // indirect
git.tebibyte.media/tomo/xgbkb v1.0.1 // indirect git.tebibyte.media/tomo/xgbkb v1.0.1 // indirect
github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298 // indirect github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298 // indirect
github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966 // indirect github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966 // indirect
github.com/jezek/xgb v1.1.0 // indirect github.com/jezek/xgb v1.1.1 // indirect
github.com/jezek/xgbutil v0.0.0-20230603163917-04188eb39cf0 // indirect github.com/jezek/xgbutil v0.0.0-20231116234834-47f30c120111 // indirect
golang.org/x/image v0.11.0 // indirect
) )

20
go.sum
View File

@ -1,10 +1,12 @@
git.tebibyte.media/sashakoshka/xgbkb v1.0.0/go.mod h1:pNcE6TRO93vHd6q42SdwLSTTj25L0Yzggz7yLe0JV6Q= git.tebibyte.media/sashakoshka/xgbkb v1.0.0/go.mod h1:pNcE6TRO93vHd6q42SdwLSTTj25L0Yzggz7yLe0JV6Q=
git.tebibyte.media/tomo/tomo v0.31.0 h1:LHPpj3AWycochnC8F441aaRNS6Tq6w6WnBrp/LGjyhM= git.tebibyte.media/tomo/backend v0.4.1 h1:rpkyTAfp/PE56aSqKNxtSN+D/aBSW8wjJ/vTtSgqQhE=
git.tebibyte.media/tomo/tomo v0.31.0/go.mod h1:C9EzepS9wjkTJjnZaPBh22YvVPyA4hbBAJVU20Rdmps= git.tebibyte.media/tomo/backend v0.4.1/go.mod h1:2G04zmKbzf5fA/xzsjoQRrN55BWZkB+vknFxd3ialZ0=
git.tebibyte.media/tomo/typeset v0.7.0 h1:JFpEuGmN6R2XSCvkINYxpH0AyYUqqs+dZYr6OSd91y0= git.tebibyte.media/tomo/objects v0.19.0 h1:ibnAoPvkPTiuJGlqQDnoGVZBQiUT/GZo1qqvDDUCK4w=
git.tebibyte.media/tomo/typeset v0.7.0/go.mod h1:PwDpSdBF3l/EzoIsa2ME7QffVVajnTHZN6l3MHEGe1g= git.tebibyte.media/tomo/objects v0.19.0/go.mod h1:GP5BZ6lc2F2D0OxERfr9r46PIk7jFlpq2/BWOpoQDqc=
git.tebibyte.media/tomo/x v0.7.0 h1:HiLbKRWwwR+D1lYruhK3Z63JPiMlcISBe9TtCkZTeBI= git.tebibyte.media/tomo/tomo v0.38.0 h1:K5TP67RxnszudeNfmGZiU5cFTRjFueXiI3NCsgw+05U=
git.tebibyte.media/tomo/x v0.7.0/go.mod h1:h4vXFU+ZQETr7hxr/ydHqM1xFzHKvV2uKnmGzagWgnY= git.tebibyte.media/tomo/tomo v0.38.0/go.mod h1:C9EzepS9wjkTJjnZaPBh22YvVPyA4hbBAJVU20Rdmps=
git.tebibyte.media/tomo/typeset v0.7.1 h1:aZrsHwCG5ZB4f5CruRFsxLv5ezJUCFUFsQJJso2sXQ8=
git.tebibyte.media/tomo/typeset v0.7.1/go.mod h1:PwDpSdBF3l/EzoIsa2ME7QffVVajnTHZN6l3MHEGe1g=
git.tebibyte.media/tomo/xdg v0.1.0 h1:6G2WYPPiM2IXleCpKKHuJA34BxumwNWuLsUoX3yu5zA= git.tebibyte.media/tomo/xdg v0.1.0 h1:6G2WYPPiM2IXleCpKKHuJA34BxumwNWuLsUoX3yu5zA=
git.tebibyte.media/tomo/xdg v0.1.0/go.mod h1:tuaRwRkyYW7mqlxA7P2+V+e10KzcamNoUzcOgaIYKAY= git.tebibyte.media/tomo/xdg v0.1.0/go.mod h1:tuaRwRkyYW7mqlxA7P2+V+e10KzcamNoUzcOgaIYKAY=
git.tebibyte.media/tomo/xgbkb v1.0.1 h1:b3HDUopjdQp1MZrb5Vpil4bOtk3NnNXtfQW27Blw2kE= git.tebibyte.media/tomo/xgbkb v1.0.1 h1:b3HDUopjdQp1MZrb5Vpil4bOtk3NnNXtfQW27Blw2kE=
@ -13,10 +15,12 @@ github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298 h1:1qlsVAQJ
github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298/go.mod h1:D+QujdIlUNfa0igpNMk6UIvlb6C252URs4yupRUV4lQ= github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298/go.mod h1:D+QujdIlUNfa0igpNMk6UIvlb6C252URs4yupRUV4lQ=
github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966 h1:lTG4HQym5oPKjL7nGs+csTgiDna685ZXjxijkne828g= github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966 h1:lTG4HQym5oPKjL7nGs+csTgiDna685ZXjxijkne828g=
github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966/go.mod h1:Mid70uvE93zn9wgF92A/r5ixgnvX8Lh68fxp9KQBaI0= github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966/go.mod h1:Mid70uvE93zn9wgF92A/r5ixgnvX8Lh68fxp9KQBaI0=
github.com/jezek/xgb v1.1.0 h1:wnpxJzP1+rkbGclEkmwpVFQWpuE2PUGNUzP8SbfFobk=
github.com/jezek/xgb v1.1.0/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk= github.com/jezek/xgb v1.1.0/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk=
github.com/jezek/xgbutil v0.0.0-20230603163917-04188eb39cf0 h1:Pf/0BAbppEOq4azPH6fnvUX2dycAwZdGkdxFn25j44c= github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4=
github.com/jezek/xgb v1.1.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk=
github.com/jezek/xgbutil v0.0.0-20230603163917-04188eb39cf0/go.mod h1:AHecLyFNy6AN9f/+0AH/h1MI7X1+JL5bmCz4XlVZk7Y= github.com/jezek/xgbutil v0.0.0-20230603163917-04188eb39cf0/go.mod h1:AHecLyFNy6AN9f/+0AH/h1MI7X1+JL5bmCz4XlVZk7Y=
github.com/jezek/xgbutil v0.0.0-20231116234834-47f30c120111 h1:cX/mTy4LgFtWqr5dCadtdJ4zdh/KtPco5yFLsliaFyU=
github.com/jezek/xgbutil v0.0.0-20231116234834-47f30c120111/go.mod h1:AHecLyFNy6AN9f/+0AH/h1MI7X1+JL5bmCz4XlVZk7Y=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

View File

@ -0,0 +1,455 @@
package fallbackIcons
import "bytes"
import "image"
import _ "embed"
import _ "image/png"
import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/tomo/data"
import "git.tebibyte.media/tomo/tomo/canvas"
//go:embed assets/icons-small.png
var atlasSmallBytes []byte
//go:embed assets/icons-large.png
var atlasLargeBytes []byte
func generateSource (data []byte, width int) map[tomo.Icon] canvas.Texture {
atlasImage, _, err := image.Decode(bytes.NewReader(data))
if err != nil { panic(err) }
atlasTexture := tomo.NewTexture(atlasImage)
source := make(map[tomo.Icon] canvas.Texture)
x := 0
y := 0
row := func () {
x = 0
y ++
}
col := func (id tomo.Icon) {
source[id] = atlasTexture.SubTexture(image.Rect (
x * width,
y * width,
(x + 1) * width,
(y + 1) * width))
x++
}
col(tomo.IconUnknown)
col(tomo.Icon("File"))
row()
// actions
col(tomo.IconAddressBookNew)
col(tomo.IconApplicationExit)
col(tomo.IconAppointmentNew)
col(tomo.IconCallStart)
col(tomo.IconCallStop)
col(tomo.IconContactNew)
// actions: dialog
col(tomo.IconDialogOkay)
col(tomo.IconDialogCancel)
// actions: edit
col(tomo.IconEditClear)
col(tomo.IconEditCopy)
col(tomo.IconEditCut)
col(tomo.IconEditDelete)
col(tomo.IconEditFind)
col(tomo.IconEditFindReplace)
col(tomo.IconEditPaste)
col(tomo.IconEditRedo)
col(tomo.IconEditSelectAll)
col(tomo.IconEditUndo)
// actions: file
col(tomo.IconFileNew)
col(tomo.IconDirectoryNew)
col(tomo.IconFileOpen)
col(tomo.IconFileOpenRecent)
col(tomo.IconFilePageSetup)
col(tomo.IconFilePrint)
col(tomo.IconFilePrintPreview)
col(tomo.IconFilePermissions)
col(tomo.IconFileProperties)
col(tomo.IconFileRename)
col(tomo.IconFileRevert)
col(tomo.IconFileSave)
col(tomo.IconFileSaveAs)
col(tomo.IconFileSend)
row()
// actions: format
col(tomo.IconFormatIndentLess)
col(tomo.IconFormatIndentMore)
col(tomo.IconFormatAlignCenter)
col(tomo.IconFormatAlignEven)
col(tomo.IconFormatAlignLeft)
col(tomo.IconFormatAlignRight)
col(tomo.IconFormatTextDirectionLtr)
col(tomo.IconFormatTextDirectionRtl)
col(tomo.IconFormatTextBold)
col(tomo.IconFormatTextItalic)
col(tomo.IconFormatTextUnderline)
col(tomo.IconFormatTextStrikethrough)
// actions: go
col(tomo.IconGoBottom)
col(tomo.IconGoDown)
col(tomo.IconGoFirst)
col(tomo.IconGoHome)
col(tomo.IconGoJump)
col(tomo.IconGoLast)
col(tomo.IconGoNext)
col(tomo.IconGoPrevious)
col(tomo.IconGoTop)
col(tomo.IconGoUp)
// actions: help
col(tomo.IconHelpAbout)
col(tomo.IconHelpContents)
col(tomo.IconHelpFaq)
// actions: insert
col(tomo.IconInsertImage)
col(tomo.IconInsertLink)
col(tomo.IconInsertObject)
col(tomo.IconInsertText)
// actions: list
col(tomo.IconListAdd)
col(tomo.IconListRemove)
row()
// actions: mail
col(tomo.IconMailForward)
col(tomo.IconMailMarkImportant)
col(tomo.IconMailMarkJunk)
col(tomo.IconMailMarkNotJunk)
col(tomo.IconMailMarkRead)
col(tomo.IconMailMarkUnread)
col(tomo.IconMailMessageNew)
col(tomo.IconMailReplyAll)
col(tomo.IconMailReplySender)
col(tomo.IconMailSend)
col(tomo.IconMailReceive)
// actions: media
col(tomo.IconMediaEject)
col(tomo.IconMediaPlaybackPause)
col(tomo.IconMediaPlaybackStart)
col(tomo.IconMediaPlaybackStop)
col(tomo.IconMediaRecord)
col(tomo.IconMediaSeekBackward)
col(tomo.IconMediaSeekForward)
col(tomo.IconMediaSkipBackward)
col(tomo.IconMediaSkipForward)
// actions: object
col(tomo.IconObjectFlipHorizontal)
col(tomo.IconObjectFlipVertical)
col(tomo.IconObjectRotateLeft)
col(tomo.IconObjectRotateRight)
// actions: process
col(tomo.IconProcessStop)
// actions: system
col(tomo.IconSystemLockScreen)
col(tomo.IconSystemLogOut)
col(tomo.IconSystemRun)
col(tomo.IconSystemSearch)
col(tomo.IconSystemReboot)
col(tomo.IconSystemShutdown)
row()
// actions: tools
col(tomo.IconToolsCheckSpelling)
// actions: value
col(tomo.IconValueIncrement)
col(tomo.IconValueDecrement)
col(tomo.IconValueReset)
// actions: view
col(tomo.IconViewFullscreen)
col(tomo.IconViewRefresh)
col(tomo.IconViewRestore)
col(tomo.IconViewSortAscending)
col(tomo.IconViewSortDescending)
// actions: window
col(tomo.IconWindowClose)
col(tomo.IconWindowNew)
// actions: zoom
col(tomo.IconZoomFitBest)
col(tomo.IconZoomIn)
col(tomo.IconZoomOriginal)
col(tomo.IconZoomOut)
row()
// applications
// Keep these in sync with nasin.ApplicationRole!
col(tomo.IconApplication)
col(tomo.IconApplicationWebBrowser)
col(tomo.IconApplicationMesssanger)
col(tomo.IconApplicationPhone)
col(tomo.IconApplicationMail)
col(tomo.IconApplicationTerminalEmulator)
col(tomo.IconApplicationFileBrowser)
col(tomo.IconApplicationTextEditor)
col(tomo.IconApplicationDocumentViewer)
col(tomo.IconApplicationWordProcessor)
col(tomo.IconApplicationSpreadsheet)
col(tomo.IconApplicationSlideshow)
col(tomo.IconApplicationCalculator)
col(tomo.IconApplicationPreferences)
col(tomo.IconApplicationProcessManager)
col(tomo.IconApplicationSystemInformation)
col(tomo.IconApplicationManual)
col(tomo.IconApplicationCamera)
col(tomo.IconApplicationImageViewer)
col(tomo.IconApplicationMediaPlayer)
col(tomo.IconApplicationImageEditor)
col(tomo.IconApplicationAudioEditor)
col(tomo.IconApplicationVideoEditor)
col(tomo.IconApplicationClock)
col(tomo.IconApplicationCalendar)
col(tomo.IconApplicationChecklist)
row()
// categories: applications
col(tomo.IconApplications)
col(tomo.IconApplicationsAccessories)
col(tomo.IconApplicationsDevelopment)
col(tomo.IconApplicationsEngineering)
col(tomo.IconApplicationsGames)
col(tomo.IconApplicationsGraphics)
col(tomo.IconApplicationsInternet)
col(tomo.IconApplicationsMultimedia)
col(tomo.IconApplicationsOffice)
col(tomo.IconApplicationsScience)
col(tomo.IconApplicationsSystem)
col(tomo.IconApplicationsUtilities)
// categories: preferences
col(tomo.IconPreferences)
col(tomo.IconPreferencesDesktop)
col(tomo.IconPreferencesPeripherals)
col(tomo.IconPreferencesPersonal)
col(tomo.IconPreferencesSystem)
col(tomo.IconPreferencesNetwork)
row()
// devices
col(tomo.IconDevice)
col(tomo.IconDeviceCamera)
col(tomo.IconDeviceWebCamera)
col(tomo.IconDeviceComputer)
col(tomo.IconDevicePda)
col(tomo.IconDevicePhone)
col(tomo.IconDevicePrinter)
col(tomo.IconDeviceScanner)
col(tomo.IconDeviceMultimediaPlayer)
col(tomo.IconDeviceVideoDisplay)
col(tomo.IconDeviceAudioInput)
col(tomo.IconDeviceAudioOutput)
// devices: hardware
col(tomo.IconHardware)
col(tomo.IconHardwareCPU)
col(tomo.IconHardwareGPU)
col(tomo.IconHardwareRAM)
col(tomo.IconHardwareSoundCard)
col(tomo.IconHardwareNetworkAdapter)
// devices: power
col(tomo.IconPowerBattery)
// devices: storage
col(tomo.IconStorageHardDisk)
col(tomo.IconStorageFloppyDisk)
col(tomo.IconStorageSolidState)
col(tomo.IconStorageOptical)
col(tomo.IconStorageFlashStick)
col(tomo.IconStorageFlashCard)
col(tomo.IconStorageMagneticTape)
// devices: input
col(tomo.IconInputGaming)
col(tomo.IconInputKeyboard)
col(tomo.IconInputMouse)
col(tomo.IconInputTablet)
row()
// devices: network
col(tomo.IconNetworkWired)
col(tomo.IconNetworkWireless)
col(tomo.IconNetworkCellular)
col(tomo.IconNetworkLocal)
col(tomo.IconNetworkInternet)
col(tomo.IconNetworkVPN)
col(tomo.IconNetworkServer)
col(tomo.IconNetworkWorkgroup)
row()
// emblems
col(tomo.IconEmblemDefault)
col(tomo.IconEmblemEncrypted)
col(tomo.IconEmblemFavorite)
col(tomo.IconEmblemImportant)
col(tomo.IconEmblemReadOnly)
col(tomo.IconEmblemShared)
col(tomo.IconEmblemSymbolicLink)
col(tomo.IconEmblemSynchronized)
col(tomo.IconEmblemSystem)
col(tomo.IconEmblemUnreadable)
row()
// places
col(tomo.IconPlaceDirectory)
col(tomo.IconPlaceRemote)
col(tomo.IconPlaceHome)
col(tomo.IconPlaceDownloads)
col(tomo.IconPlaceDesktop)
col(tomo.IconPlacePhotos)
col(tomo.IconPlaceBooks)
col(tomo.IconPlaceBookmarks)
col(tomo.IconPlaceTrash)
col(tomo.IconPlaceDocuments)
col(tomo.IconPlaceRepositories)
col(tomo.IconPlaceMusic)
col(tomo.IconPlaceArchives)
col(tomo.IconPlaceFonts)
col(tomo.IconPlaceBinaries)
col(tomo.IconPlaceVideos)
col(tomo.IconPlace3DObjects)
col(tomo.IconPlaceHistory)
col(tomo.IconPlacePreferences)
row()
// status: checkbox
col(tomo.IconCheckboxChecked)
col(tomo.IconCheckboxUnchecked)
// status: appointments
col(tomo.IconAppointmentMissed)
col(tomo.IconAppointmentSoon)
// status: dialogs
col(tomo.IconDialogError)
col(tomo.IconDialogInformation)
col(tomo.IconDialogPassword)
col(tomo.IconDialogQuestion)
col(tomo.IconDialogWarning)
// status: directories
col(tomo.IconDirectoryDragAccept)
col(tomo.IconDirectoryFull)
col(tomo.IconDirectoryOpen)
col(tomo.IconDirectoryVisiting)
// status: trash
col(tomo.IconTrashFull)
// status: resource
col(tomo.IconResourceLoading)
col(tomo.IconResourceMissing)
// status: mail
col(tomo.IconMailAttachment)
col(tomo.IconMailUnread)
col(tomo.IconMailReplied)
col(tomo.IconMailSigned)
col(tomo.IconMailSignedVerified)
row()
// status: network
col(tomo.IconCellularSignal0)
col(tomo.IconCellularSignal1)
col(tomo.IconCellularSignal2)
col(tomo.IconCellularSignal3)
col(tomo.IconWirelessSignal0)
col(tomo.IconWirelessSignal1)
col(tomo.IconWirelessSignal2)
col(tomo.IconWirelessSignal3)
col(tomo.IconNetworkError)
col(tomo.IconNetworkIdle)
col(tomo.IconNetworkOffline)
col(tomo.IconNetworkReceive)
col(tomo.IconNetworkTransmit)
col(tomo.IconNetworkTransmitReceive)
// status: print
col(tomo.IconPrintError)
col(tomo.IconPrintPrinting)
// status: security
col(tomo.IconSecurityHigh)
col(tomo.IconSecurityMedium)
col(tomo.IconSecurityLow)
// status: software
col(tomo.IconSoftwareUpdateAvailable)
col(tomo.IconSoftwareUpdateUrgent)
col(tomo.IconSoftwareInstalling)
// status: sync
col(tomo.IconSyncError)
col(tomo.IconSyncSynchronizing)
// status: tasks
col(tomo.IconTaskDue)
col(tomo.IconTaskPastDue)
// status: users
col(tomo.IconUserAvailable)
col(tomo.IconUserAway)
col(tomo.IconUserIdle)
col(tomo.IconUserOffline)
row()
// status: power
col(tomo.IconBattery0)
col(tomo.IconBattery1)
col(tomo.IconBattery2)
col(tomo.IconBattery3)
col(tomo.IconBrightness0)
col(tomo.IconBrightness1)
col(tomo.IconBrightness2)
col(tomo.IconBrightness3)
// status: media
col(tomo.IconVolume0)
col(tomo.IconVolume1)
col(tomo.IconVolume2)
col(tomo.IconVolume3)
col(tomo.IconPlaylistRepeat)
col(tomo.IconPlaylistShuffle)
// status: weather
col(tomo.IconWeatherClear)
col(tomo.IconWeatherClearNight)
col(tomo.IconWeatherFewClouds)
col(tomo.IconWeatherFewCloudsNight)
col(tomo.IconWeatherFog)
col(tomo.IconWeatherOvercast)
col(tomo.IconWeatherSevereAlert)
col(tomo.IconWeatherShowers)
col(tomo.IconWeatherShowersScattered)
col(tomo.IconWeatherSnow)
col(tomo.IconWeatherStorm)
return source
}
type iconTheme struct {
texturesSmall map[tomo.Icon] canvas.Texture
texturesLarge map[tomo.Icon] canvas.Texture
}
// New creates a new fallback icon theme.
func New () tomo.Icons {
return new(iconTheme)
}
func (this *iconTheme) ensure () {
if this.texturesSmall != nil { return }
this.texturesSmall = generateSource(atlasSmallBytes, 16)
this.texturesLarge = generateSource(atlasLargeBytes, 32)
}
func (this *iconTheme) selectSource (size tomo.IconSize) map[tomo.Icon] canvas.Texture {
if size == tomo.IconSizeSmall {
return this.texturesSmall
} else {
return this.texturesLarge
}
}
func (this *iconTheme) Icon (icon tomo.Icon, size tomo.IconSize) canvas.Texture {
this.ensure()
source := this.selectSource(size)
if texture, ok := source[icon]; ok {
return texture
}
return nil
}
func (this *iconTheme) MimeIcon (mime data.Mime, size tomo.IconSize) canvas.Texture {
this.ensure()
source := this.selectSource(size)
if mime == data.M("inode", "directory") {
return source[tomo.IconPlaceDirectory]
} else {
return source[tomo.Icon("File")]
}
}

182
internal/icons/xdg/icon.go Normal file
View File

@ -0,0 +1,182 @@
package xdgIcons
import "os"
import "fmt"
import "log"
import "image"
import "regexp"
import "strings"
import _ "image/png"
import "git.tebibyte.media/tomo/tomo"
import xdgIconTheme "git.tebibyte.media/tomo/xdg/icon-theme"
import "git.tebibyte.media/tomo/tomo/data"
import "git.tebibyte.media/tomo/tomo/canvas"
type iconTheme struct {
xdg xdgIconTheme.Theme
fallback tomo.Icons
texturesSmall map[tomo.Icon] canvas.Texture
texturesMedium map[tomo.Icon] canvas.Texture
texturesLarge map[tomo.Icon] canvas.Texture
}
func FindThemeWarn (name string, fallback tomo.Icons, path ...string) (tomo.Icons, error) {
this := &iconTheme {
fallback: fallback,
texturesLarge: make(map[tomo.Icon] canvas.Texture),
texturesMedium: make(map[tomo.Icon] canvas.Texture),
texturesSmall: make(map[tomo.Icon] canvas.Texture),
}
xdg, err := xdgIconTheme.FindThemeWarn(name, path...)
if err != nil { return nil, err }
this.xdg = xdg
return this, nil
}
func (this *iconTheme) selectSource (size tomo.IconSize) map[tomo.Icon] canvas.Texture {
switch size {
case tomo.IconSizeMedium: return this.texturesMedium
case tomo.IconSizeLarge: return this.texturesLarge
default: return this.texturesSmall
}
}
func (this *iconTheme) xdgIcon (name string, size tomo.IconSize) (canvas.Texture, bool) {
// TODO use scaling factor instead of 1
// find icon file
icon, err := this.xdg.FindIcon(name, iconSizePixels(size), 1, xdgIconTheme.PNG)
if err != nil { return nil, false }
// open icon file
iconFile, err := os.Open(icon.Path)
if err != nil {
// this failing indicates a broken icon theme
log.Printf("nasin: icon file '%s' is inaccessible: %v\n", icon.Path, err)
return nil, false
}
iconImage, _, err := image.Decode(iconFile)
if err != nil {
// this failing indicates a broken icon theme
log.Printf("nasin: icon file '%s' is broken: %v\n", icon.Path, err)
return nil, false
}
return tomo.NewTexture(iconImage), true
}
func (this *iconTheme) Icon (icon tomo.Icon, size tomo.IconSize) canvas.Texture {
source := this.selectSource(size)
texture, ok := source[icon]
if !ok {
texture = this.icon(icon, size)
source[icon] = texture
}
if texture == nil {
return this.fallback.Icon(icon, size)
} else {
return texture
}
}
func (this *iconTheme) MimeIcon (mime data.Mime, size tomo.IconSize) canvas.Texture {
icon := tomo.Icon(mime.String())
source := this.selectSource(size)
texture, ok := source[icon]
if !ok {
texture = this.mimeIcon(mime, size)
source[icon] = texture
}
if texture == nil {
return this.fallback.MimeIcon(mime, size)
} else {
return texture
}
}
func (this *iconTheme) icon (icon tomo.Icon, size tomo.IconSize) canvas.Texture {
if texture, ok := this.xdgIcon(XdgIconName(icon), size); ok {
return texture
}
if texture, ok := this.xdgIcon(XdgIconName(generalizeIcon(icon)), size); ok {
return texture
}
return nil
}
func (this *iconTheme) mimeIcon (mime data.Mime, size tomo.IconSize) canvas.Texture {
if texture, ok := this.xdgIcon(xdgFormatMime(mime), size); ok {
return texture
}
if texture, ok := this.xdgIcon(xdgFormatMime(generalizeMimeType(mime)), size); ok {
return texture
}
if texture, ok := this.xdgIcon(xdgFormatMime(data.M("text", "x-generic")), size); ok {
return texture
}
return nil
}
var kebabMatchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)")
var kebabMatchAllCaps = regexp.MustCompile("([a-z0-9])([A-Z])")
// XdgIconName returns the best XDG name for the given icon.
func XdgIconName (icon tomo.Icon) string {
if name, ok := xdgIconNames[icon]; ok {
return name
}
name := kebabMatchFirstCap.ReplaceAllString(string(icon), "${1}-${2}")
name = kebabMatchAllCaps.ReplaceAllString(string(name), "${1}-${2}")
return strings.ToLower(name)
}
func generalizeIcon (icon tomo.Icon) tomo.Icon {
name := string(icon)
switch {
case strings.HasPrefix(name, "Application"): return tomo.IconApplication
case strings.HasPrefix(name, "Preferences"): return tomo.IconPreferences
case strings.HasPrefix(name, "Device"): return tomo.IconDevice
case strings.HasPrefix(name, "Hardware"): return tomo.IconHardware
case strings.HasPrefix(name, "Storage"): return tomo.IconStorageHardDisk
case strings.HasPrefix(name, "Input"): return tomo.IconInputMouse
case strings.HasPrefix(name, "Network"): return tomo.IconNetworkWired
case strings.HasPrefix(name, "Place"): return tomo.IconPlaceDirectory
case strings.HasPrefix(name, "Directory"): return tomo.IconPlaceDirectory
case strings.HasPrefix(name, "Trash"): return tomo.IconPlaceTrash
case strings.HasPrefix(name, "Help"): return tomo.IconHelpContents
}
switch icon {
case tomo.IconCellularSignal0: return tomo.IconWirelessSignal0
case tomo.IconCellularSignal1: return tomo.IconWirelessSignal1
case tomo.IconCellularSignal2: return tomo.IconWirelessSignal2
case tomo.IconCellularSignal3: return tomo.IconWirelessSignal3
}
return icon
}
func xdgFormatMime (mime data.Mime) string {
return fmt.Sprintf("%s-%s", mime.Type, mime.Subtype)
}
func generalizeMimeType (mime data.Mime) data.Mime {
// FIXME make this more accurate
// https://specifications.freedesktop.org/icon-naming-spec/icon-naming-spec-latest.html
mime.Subtype = "x-generic"
return mime
}
func iconSizePixels (size tomo.IconSize) int {
// TODO: once Tomo has scaling support, take that into account here
switch size {
case tomo.IconSizeMedium: return 24
case tomo.IconSizeLarge: return 48
default: return 16
}
}

View File

@ -0,0 +1,136 @@
package xdgIcons
import "git.tebibyte.media/tomo/tomo"
// icons that can't be directly translated with regex
var xdgIconNames = map[tomo.Icon] string {
tomo.IconUnknown: "image-missing",
tomo.IconFileNew: "document-new",
tomo.IconDirectoryNew: "folder-new",
tomo.IconFileOpen: "document-open",
tomo.IconFileOpenRecent: "document-open-recent",
tomo.IconFilePageSetup: "document-page-setup",
tomo.IconFilePrint: "document-print",
tomo.IconFilePrintPreview: "document-print-preview",
tomo.IconFilePermissions: "document-permissions", // non-standard
tomo.IconFileProperties: "document-properties",
tomo.IconFileRename: "document-rename", // non-standard
tomo.IconFileRevert: "document-revert",
tomo.IconFileSave: "document-save",
tomo.IconFileSaveAs: "document-save-as",
tomo.IconFileSend: "document-send",
tomo.IconFormatAlignCenter: "format-justify-center",
tomo.IconFormatAlignEven: "format-justify-fill",
tomo.IconFormatAlignLeft: "format-justify-left",
tomo.IconFormatAlignRight: "format-justify-right",
tomo.IconMailReceive: "mail-send-receive",
tomo.IconValueIncrement: "list-add",
tomo.IconValueDecrement: "list-remove",
tomo.IconValueReset: "value-reset", // non-standard
tomo.IconApplication: "system-run",
tomo.IconApplicationWebBrowser: "web-browser",
tomo.IconApplicationMesssanger: "internet-messanger", // non-standard
tomo.IconApplicationPhone: "accessories-phone", // non-standard
tomo.IconApplicationMail: "internet-mail-client", // non-standard
tomo.IconApplicationTerminalEmulator: "utilities-terminal",
tomo.IconApplicationFileBrowser: "system-file-manager",
tomo.IconApplicationTextEditor: "accessories-text-editor",
tomo.IconApplicationDocumentViewer: "office-document-viewer", // non-standard
tomo.IconApplicationWordProcessor: "office-word-processor", // non-standard
tomo.IconApplicationSpreadsheet: "office-spreadsheet", // non-standard
tomo.IconApplicationSlideshow: "office-slideshow", // non-standard
tomo.IconApplicationCalculator: "accessories-calculator",
tomo.IconApplicationPreferences: "preferences-system",
tomo.IconApplicationProcessManager: "utilities-system-monitor",
tomo.IconApplicationSystemInformation: "distributor-logo", // non-standard
tomo.IconApplicationManual: "help-browser",
tomo.IconApplicationCamera: "accessories-camera", // non-standard
tomo.IconApplicationImageViewer: "graphics-image-viewer", // non-standard
tomo.IconApplicationMediaPlayer: "audio-video-media-player", // non-standard
tomo.IconApplicationImageEditor: "graphics-image-editor", // non-standard
tomo.IconApplicationAudioEditor: "audio-audio-editor", // non-standard
tomo.IconApplicationVideoEditor: "video-video-editor", // non-standard
tomo.IconApplicationClock: "accessories-clock", // non-standard
tomo.IconApplicationCalendar: "accessories-calendar", // non-standard
tomo.IconApplicationChecklist: "accessories-checklist", // non-standard
tomo.IconApplications: "applications-other",
tomo.IconPreferences: "preferences-other",
tomo.IconPreferencesNetwork: "preferences-system-network",
tomo.IconDevice: "device", // non-standard
tomo.IconDeviceCamera: "camera-photo",
tomo.IconDeviceWebCamera: "camera-web",
tomo.IconDeviceComputer: "computer",
tomo.IconDevicePda: "pda",
tomo.IconDevicePhone: "phone",
tomo.IconDevicePrinter: "printer",
tomo.IconDeviceScanner: "scanner",
tomo.IconDeviceMultimediaPlayer: "multimedia-player",
tomo.IconDeviceVideoDisplay: "video-display",
tomo.IconDeviceAudioInput: "audio-input-microphone",
tomo.IconDeviceAudioOutput: "audio-speakers",
tomo.IconHardware: "card", // non-standard
tomo.IconHardwareCPU: "cpu",
tomo.IconHardwareGPU: "video-card",
tomo.IconHardwareRAM: "ram",
tomo.IconHardwareSoundCard: "audio-card",
tomo.IconHardwareNetworkAdapter: "network-card",
tomo.IconPowerBattery: "battery",
tomo.IconStorageHardDisk: "drive-harddisk",
tomo.IconStorageFloppyDisk: "media-floppy",
tomo.IconStorageSolidState: "drive-solid",
tomo.IconStorageOptical: "media-optical",
tomo.IconStorageFlashStick: "media-removable",
tomo.IconStorageFlashCard: "media-flash",
tomo.IconStorageMagneticTape: "media-tape",
tomo.IconEmblemReadOnly: "emblem-readonly",
tomo.IconPlaceDirectory: "folder",
tomo.IconPlaceRemote: "folder-remote",
tomo.IconPlaceHome: "user-home",
tomo.IconPlaceDownloads: "folder-downloads", // common
tomo.IconPlaceDesktop: "user-desktop",
tomo.IconPlacePhotos: "folder-pictures", // common
tomo.IconPlaceBooks: "folder-books", // non-standard
tomo.IconPlaceBookmarks: "user-bookmarks",
tomo.IconPlaceTrash: "user-trash",
tomo.IconPlaceDocuments: "folder-documents", // common
tomo.IconPlaceRepositories: "folder-repositories", // non-standard
tomo.IconPlaceMusic: "folder-music", // common
tomo.IconPlaceArchives: "folder-archives", // non-standard
tomo.IconPlaceFonts: "folder-fonts", // non-standard
tomo.IconPlaceBinaries: "folder-executables", // non-standard
tomo.IconPlaceVideos: "folder-videos", // common
tomo.IconPlace3DObjects: "folder-3d-objects", // non-standard
tomo.IconPlaceHistory: "folder-history", // non-standard
tomo.IconPlacePreferences: "preferences-other",
tomo.IconDirectoryDragAccept: "folder-drag-accept",
tomo.IconDirectoryFull: "folder-full",
tomo.IconDirectoryOpen: "folder-open",
tomo.IconDirectoryVisiting: "folder-visiting",
tomo.IconTrashFull: "user-trash-full",
tomo.IconResourceLoading: "image-loading",
tomo.IconResourceMissing: "image-missing",
tomo.IconCellularSignal0: "nm-signal-00", // common
tomo.IconCellularSignal1: "nm-signal-25", // common
tomo.IconCellularSignal2: "nm-signal-75", // common
tomo.IconCellularSignal3: "nm-signal-100", // common
tomo.IconWirelessSignal0: "wifi-signal-00", // common
tomo.IconWirelessSignal1: "wifi-signal-25", // common
tomo.IconWirelessSignal2: "wifi-signal-75", // common
tomo.IconWirelessSignal3: "wifi-signal-100", // common
tomo.IconPrintError: "printer-error",
tomo.IconPrintPrinting: "printer-printing",
tomo.IconBattery0: "battery-caution",
tomo.IconBattery1: "battery-low",
tomo.IconBattery2: "battery-good", // common
tomo.IconBattery3: "battery-full", // common
tomo.IconBrightness0: "brightness-dim", // non-standard
tomo.IconBrightness1: "brightness-medium", // non-standard
tomo.IconBrightness2: "brightness-bright", // non-standard
tomo.IconBrightness3: "brightness-full", // non-standard
tomo.IconVolume0: "audio-volume-muted",
tomo.IconVolume1: "audio-volume-low",
tomo.IconVolume2: "audio-volume-medium",
tomo.IconVolume3: "audio-volume-high",
tomo.IconPlaylistRepeat: "media-playlist-repeat",
tomo.IconPlaylistShuffle: "media-playlist-shuffle",
}

View File

@ -0,0 +1,2 @@
// Package registrar provides platform-dependent components at compile time.
package registrar

View File

@ -0,0 +1,43 @@
//go:build unix && (!darwin)
package registrar
import "os"
import "log"
import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/backend/x"
import "git.tebibyte.media/tomo/nasin/internal/style"
import "git.tebibyte.media/tomo/nasin/internal/icons/xdg"
import "git.tebibyte.media/tomo/nasin/internal/icons/fallback"
import "git.tebibyte.media/tomo/nasin/internal/style/fallback"
import "git.tebibyte.media/tomo/nasin/internal/style/aluminum"
func RegisterBackend () error {
tomo.Register(1, x.New)
return nil
}
func SetTheme () error {
var styl *style.Style
// TODO eventually get rid of this when we make a file format for
// storing visual styles
if os.Getenv("TOMO_USE_ALUMINUM_STYLE") != "" {
styl = aluminumStyle.New()
} else {
styl = fallbackStyle.New()
}
icons := fallbackIcons.New()
iconThemeName := os.Getenv("TOMO_XDG_ICON_THEME")
if iconThemeName != "" {
xdgIconTheme, err := xdgIcons.FindThemeWarn(iconThemeName, icons)
if err == nil {
icons = xdgIconTheme
} else {
log.Printf("nasin: could not load icon theme '%s': %v", iconThemeName, err)
}
}
tomo.SetStyle(styl)
tomo.SetIcons(icons)
return nil
}

View File

@ -1,2 +0,0 @@
// Package registry provides platform-dependent components at compile time.
package registry

View File

@ -1,13 +0,0 @@
//go:build unix && (!darwin)
package registry
import "git.tebibyte.media/tomo/x"
import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/tomo/theme"
import "git.tebibyte.media/tomo/nasin/internal/theme/default"
func Init () error {
theme.SetTheme(defaultTheme.Theme())
tomo.Register(1, x.NewBackend)
return nil
}

View File

@ -0,0 +1,19 @@
package aluminumStyle
import "image/color"
import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/nasin/internal/style"
// New returns Aluminum, a futuristic, bluish-white style.
func New () *style.Style {
return &style.Style {
Colors: map[tomo.Color] color.Color {
tomo.ColorBackground: colorBackground,
tomo.ColorForeground: colorForeground,
tomo.ColorRaised: colorRaised,
tomo.ColorSunken: colorSunken,
tomo.ColorAccent: colorFocus,
},
Rules: rules,
}
}

View File

@ -0,0 +1,392 @@
package aluminumStyle
import "image/color"
import "git.tebibyte.media/tomo/tomo"
import "golang.org/x/image/font/basicfont"
import "git.tebibyte.media/tomo/nasin/internal/style"
func hex (color uint32) (c color.RGBA) {
c.A = uint8(color)
c.B = uint8(color >> 8)
c.G = uint8(color >> 16)
c.R = uint8(color >> 24)
return
}
func border (top, right, bottom, left uint32, width ...int) tomo.Border {
return tomo.Border {
Width: tomo.I(width...),
Color: [4]color.Color {
hex(top), hex(right),
hex(bottom), hex(left),
},
}
}
var colorDot = hex(0x7391c080)
var colorFocus = hex(0x5f8bc4FF)
var colorHighlight = hex(0x5f8bc4FF)
var colorBackground = hex(0xd4d4d4FF)
var colorForeground = color.Black
var colorOutline = color.Black
var colorGutter = hex(0xbfc6d1FF)
var colorGutterHovered = hex(0xc5cbd6FF)
var colorRaised = hex(0xe9eaeaFF)
var colorRaisedPressed = hex(0xccd4ddFF)
var colorRaisedFocused = hex(0xcfd6ddFF)
var colorRaisedHovered = hex(0xf1f3f5FF)
var colorSunken = hex(0xe9eaeaFF)
var colorSunkenFocused = hex(0xe0e6eeFF)
var colorSunkenPressed = hex(0xe0e6eeFF)
var colorCalendarWeekdayHeader = hex(0xd3cac2FF)
var colorCalendarWeekend = hex(0xc2d3c4FF)
var colorCalendarDay = hex(0xd6dae2FF)
var outline = tomo.Border {
Width: tomo.I(1),
Color: [4]color.Color {
colorOutline,
colorOutline,
colorOutline,
colorOutline,
},
}
var borderEngraved = border(0xc3c3c5FF, 0xe3e3e3FF, 0xe9e9e9ff, 0xc2c2c2ff, 1)
var borderGap = border(0x697c7cFF, 0x566767FF, 0x566767ff, 0x697c7cff, 1)
var borderLifted = border(0xf9fafcFF, 0xc2c8d3FF, 0xa4afc0ff, 0xf5f6f8ff, 1)
var borderLiftedFocused = border(0xf0f4f9FF, 0xb1bacaFF, 0x9aa6b7ff, 0xe4e9eeff, 1)
var borderFocused = border(0x5f8bc4FF, 0x5f8bc4FF, 0x5f8bc4ff, 0x5f8bc4ff, 1)
var borderTear = borderEngraved
var borderTearFocused = border(0x7f94b5FF, 0xced7e4FF, 0xced7e4FF, 0x7f94b5FF, 1)
var borderTearPad = border(0x00000000, 0x00000000, 0x00000000, 0x00000000, 3)
var borderTearPadFocused = border(0x7391c080, 0x7391c080, 0x7391c080, 0x7391c080, 3)
var borderInnerShadow = border(0xa4afc0FF, 0xa4afc0FF, 0xa4afc0ff, 0xa4afc0ff, 1, 0, 0, 1)
var borderOuterShadow = border(0xa4afc0FF, 0xa4afc0FF, 0xa4afc0ff, 0xa4afc0ff, 0, 1, 1, 0)
var rules = []style.Rule {
// *.*[*]
style.Rule {
Default: style.AS (
style.AttrFace { Face: basicfont.Face7x13 },
style.AttrTextColor { Color: tomo.ColorForeground },
style.AttrDotColor { Color: colorDot },
style.AttrGap { X: 8, Y: 8 },
),
},
// *.Button[*]
style.Rule {
Role: tomo.R("", "Button", ""),
Default: style.AS (
style.AttrBorder {
borderEngraved,
borderGap,
borderLifted,
},
style.AttrPadding(tomo.I(4, 8)),
style.AttrColor { Color: tomo.ColorRaised },
),
Pressed: style.AS (
style.AttrBorder {
borderEngraved,
borderGap,
borderInnerShadow,
},
style.AttrPadding(tomo.I(5, 8, 4, 9)),
style.AttrColor { Color: colorRaisedPressed },
),
Focused: style.AS (
style.AttrBorder {
borderEngraved,
borderGap,
borderLiftedFocused,
},
style.AttrPadding(tomo.I(4, 8)),
style.AttrColor { Color: colorRaisedFocused },
),
Hovered: style.AS (
style.AttrColor { Color: colorRaisedHovered },
),
},
// *.TextInput[*]
style.Rule {
Role: tomo.R("", "TextInput", ""),
Default: style.AS (
style.AttrBorder {
borderEngraved,
borderGap,
borderInnerShadow,
},
style.AttrColor { Color: tomo.ColorSunken },
style.AttrPadding(tomo.I(5, 4, 4, 5)),
),
Focused: style.AS (
style.AttrBorder {
borderEngraved,
borderFocused,
borderInnerShadow,
},
),
},
// *.TextView[*]
style.Rule {
Role: tomo.R("", "TextView", ""),
Default: style.AS (
style.AttrBorder {
borderEngraved,
borderGap,
borderInnerShadow,
},
style.AttrColor { Color: tomo.ColorSunken },
style.AttrPadding(tomo.I(8)),
),
},
// *.NumberInput[*]
style.Rule {
Role: tomo.R("", "NumberInput", ""),
Default: style.AS (
style.AttrGap { },
),
},
// *.Container[sunken]
style.Rule {
Role: tomo.R("", "Container", "sunken"),
Default: style.AS (
style.AttrBorder {
borderEngraved,
borderGap,
borderInnerShadow,
},
style.AttrColor { Color: tomo.ColorSunken },
style.AttrPadding(tomo.I(8)),
),
},
// *.Container[outer]
style.Rule {
Role: tomo.R("", "Container", "outer"),
Default: style.AS (
style.AttrColor { Color: tomo.ColorBackground },
style.AttrPadding(tomo.I(8)),
),
},
// *.Container[menu]
style.Rule {
Role: tomo.R("", "Container", "menu"),
Default: style.AS (
style.AttrBorder {
borderGap,
borderLifted,
},
style.AttrColor { Color: tomo.ColorBackground },
style.AttrGap { },
),
},
// *.Heading[*]
style.Rule {
Role: tomo.R("", "Heading", ""),
Default: style.AS (
style.AttrAlign { X: tomo.AlignMiddle, Y: tomo.AlignMiddle },
),
},
// *.Separator[*]
style.Rule {
Role: tomo.R("", "Separator", ""),
Default: style.AS (
style.AttrBorder {
borderEngraved,
},
),
},
// *.Slider[*]
style.Rule {
Role: tomo.R("", "Slider", ""),
Default: style.AS (
style.AttrBorder {
borderEngraved,
borderGap,
borderInnerShadow,
},
style.AttrColor { Color: colorGutter },
),
Focused: style.AS (
style.AttrBorder {
borderEngraved,
borderFocused,
borderInnerShadow,
},
),
Hovered: style.AS (
style.AttrColor { Color: colorGutterHovered },
),
},
// *.Slider[horizontal]
style.Rule {
Role: tomo.R("", "Slider", "horizontal"),
Default: style.AS(style.AttrMinimumSize { X: 48 }),
},
// *.Slider[vertical]
style.Rule {
Role: tomo.R("", "Slider", "vertical"),
Default: style.AS(style.AttrMinimumSize { Y: 48 }),
},
// *.SliderHandle[*]
style.Rule {
Role: tomo.R("", "SliderHandle", ""),
Default: style.AS (
style.AttrBorder {
borderOuterShadow,
borderGap,
borderLifted,
},
style.AttrColor { Color: tomo.ColorRaised },
style.AttrMinimumSize { X: 12, Y: 12, },
),
},
// *.Checkbox[*]
style.Rule {
Role: tomo.R("", "Checkbox", ""),
Default: style.AS (
style.AttrBorder {
borderEngraved,
borderGap,
borderInnerShadow,
},
style.AttrColor { Color: tomo.ColorSunken },
style.AttrPadding(tomo.I(0, 1, 1, 0)),
style.AttrMinimumSize { X: 19, Y: 19 },
),
Focused: style.AS (
style.AttrBorder {
borderEngraved,
borderFocused,
borderInnerShadow,
},
style.AttrPadding(tomo.I(0)),
style.AttrColor { Color: colorSunkenFocused },
),
},
// *.LabelCheckbox[*]
style.Rule {
Role: tomo.R("", "LabelCheckbox", ""),
Default: style.AS (
style.AttrGap { X: 8, Y: 8 },
),
},
// *.MenuItem[*]
style.Rule {
Role: tomo.R("", "MenuItem", ""),
Default: style.AS (
style.AttrPadding(tomo.I(4)),
style.AttrGap { X: 4, Y: 4 },
style.AttrColor { Color: color.Transparent },
),
Hovered: style.AS (
style.AttrColor { Color: colorDot },
),
Focused: style.AS (
style.AttrColor { Color: colorDot },
),
},
// *.File[*]
style.Rule {
Role: tomo.R("", "File", ""),
Default: style.AS (
style.AttrColor { Color: color.Transparent },
),
Focused: style.AS (
style.AttrColor { Color: colorDot },
),
},
// *.TearLine[*]
style.Rule {
Role: tomo.R("", "TearLine", ""),
Default: style.AS (
style.AttrBorder {
borderTearPad,
borderTear,
},
),
Hovered: style.AS (
style.AttrBorder {
borderTearPadFocused,
borderTearFocused,
},
),
Focused: style.AS (
style.AttrBorder {
borderTearPadFocused,
borderTearFocused,
},
),
},
// *.Calendar[*]
style.Rule {
Role: tomo.R("", "Calendar", ""),
Default: style.AS (
style.AttrBorder {
borderOuterShadow,
borderGap,
},
style.AttrColor { Color: tomo.ColorRaised },
style.AttrPadding(tomo.I(2)),
style.AttrGap { X: 2, Y: 2 },
),
},
// *.CalendarGrid[*]
style.Rule {
Role: tomo.R("", "CalendarGrid", ""),
Default: style.AS (
style.AttrGap { X: 2, Y: 2 },
),
},
// *.CalendarWeekdayHeader[*]
style.Rule {
Role: tomo.R("", "CalendarWeekdayHeader", ""),
Default: style.AS (
style.AttrPadding(tomo.I(2)),
style.AttrColor { Color: colorCalendarWeekdayHeader },
),
},
// *.CalendarDay[weekday]
style.Rule {
Role: tomo.R("", "CalendarDay", "weekday"),
Default: style.AS (
style.AttrPadding(tomo.I(2)),
style.AttrMinimumSize { X: 32, Y: 32 },
style.AttrColor { Color: colorCalendarDay },
),
},
// *.CalendarDay[weekend]
style.Rule {
Role: tomo.R("", "CalendarDay", "weekend"),
Default: style.AS (
style.AttrPadding(tomo.I(2)),
style.AttrMinimumSize { X: 32, Y: 32 },
style.AttrColor { Color: colorCalendarWeekend },
),
},
}

View File

@ -1,4 +1,4 @@
package theme package style
import "image" import "image"
import "image/color" import "image/color"

View File

@ -0,0 +1,558 @@
package fallbackStyle
import "image/color"
import "git.tebibyte.media/tomo/tomo"
import "golang.org/x/image/font/basicfont"
import "git.tebibyte.media/tomo/nasin/internal/style"
var colorFocus = color.RGBA { R: 61, G: 128, B: 143, A: 255 }
var colorInput = color.RGBA { R: 208, G: 203, B: 150, A: 255 }
var colorCarved = color.RGBA { R: 151, G: 160, B: 150, A: 255 }
var colorGutter = color.RGBA { R: 116, G: 132, B: 126, A: 255 }
var colorShadow = color.RGBA { R: 57, G: 59, B: 57, A: 255 }
var colorShade = color.RGBA { A: 128 }
var colorInputShadow = color.RGBA { R: 143, G: 146, B: 91, A: 255 }
var colorHighlight = color.RGBA { R: 207, G: 215, B: 210, A: 255 }
var colorBackground = color.RGBA { R: 169, G: 171, B: 168, A: 255 }
var colorCarvedPressed = color.RGBA { R: 129, G: 142, B: 137, A: 255 }
var colorForeground = color.Black
var colorOutline = color.Black
var colorCalendarWeekdayHeader = color.RGBA { R: 194, G: 162, B: 132, A: 255 }
var colorCalendarWeekend = color.RGBA { R: 165, G: 185, B: 120, A: 255 }
var colorCalendarDay = color.RGBA { R: 194, G: 189, B: 132, A: 255 }
var colorInactive = color.RGBA { R: 131, G: 147, B: 134, A: 255 }
var outline = tomo.Border {
Width: tomo.I(1),
Color: [4]color.Color {
colorOutline,
colorOutline,
colorOutline,
colorOutline,
},
}
var borderColorOutline = [4]color.Color { colorOutline, colorOutline, colorOutline, colorOutline }
var borderColorEngraved = [4]color.Color { colorShadow, colorHighlight, colorHighlight, colorShadow }
var borderColorLifted = [4]color.Color { colorHighlight, colorShadow, colorShadow, colorHighlight }
var borderColorInput = [4]color.Color { colorInputShadow, colorInput, colorInput, colorInputShadow }
var borderColorFocused = [4]color.Color { colorFocus, colorFocus, colorFocus, colorFocus }
var borderColorShade = [4]color.Color { colorShade, colorShade, colorShade, colorShade }
var rules = []style.Rule {
// *.*[*]
style.Rule {
Default: style.AS (
style.AttrFace { Face: basicfont.Face7x13 },
style.AttrTextColor { Color: tomo.ColorForeground },
style.AttrDotColor { Color: tomo.ColorAccent },
style.AttrGap { X: 8, Y: 8 },
),
},
// *.Button[*]
style.Rule {
Role: tomo.R("", "Button", ""),
Default: style.AS (
style.AttrBorder {
outline,
tomo.Border {
Width: tomo.I(1),
Color: borderColorLifted,
},
},
style.AttrPadding(tomo.I(4, 8)),
style.AttrColor { Color: tomo.ColorRaised },
),
Pressed: style.AS (
style.AttrBorder {
outline,
tomo.Border {
Width: tomo.I(1, 0, 0, 1),
Color: borderColorEngraved,
},
},
style.AttrPadding(tomo.I(5, 8, 4, 9)),
style.AttrColor { Color: colorCarvedPressed },
),
Focused: style.AS (
style.AttrBorder {
outline,
tomo.Border {
Width: tomo.I(1),
Color: borderColorFocused,
},
},
style.AttrPadding(tomo.I(4, 8)),
),
},
// *.TextInput[*]
style.Rule {
Role: tomo.R("", "TextInput", ""),
Default: style.AS (
style.AttrBorder {
outline,
tomo.Border {
Width: tomo.I(1),
Color: borderColorInput,
},
},
style.AttrColor { Color: colorInput },
style.AttrPadding(tomo.I(5, 4, 4, 5)),
),
Focused: style.AS (
style.AttrBorder {
outline,
tomo.Border {
Width: tomo.I(1),
Color: borderColorFocused,
},
},
),
},
// *.TextView[*]
style.Rule {
Role: tomo.R("", "TextView", ""),
Default: style.AS (
style.AttrBorder {
outline,
tomo.Border {
Width: tomo.I(1, 0, 0, 1),
Color: borderColorEngraved,
},
},
style.AttrColor { Color: tomo.ColorSunken },
style.AttrPadding(tomo.I(8)),
),
},
// *.NumberInput[*]
style.Rule {
Role: tomo.R("", "NumberInput", ""),
Default: style.AS (
style.AttrGap { },
),
},
// *.Container[sunken]
style.Rule {
Role: tomo.R("", "Container", "sunken"),
Default: style.AS (
style.AttrBorder {
outline,
tomo.Border {
Width: tomo.I(1, 0, 0, 1),
Color: borderColorEngraved,
},
},
style.AttrColor { Color: tomo.ColorSunken },
style.AttrPadding(tomo.I(8)),
),
},
// *.Container[outer]
style.Rule {
Role: tomo.R("", "Container", "outer"),
Default: style.AS (
style.AttrColor { Color: tomo.ColorBackground },
style.AttrPadding(tomo.I(8)),
),
},
// *.Container[menu]
style.Rule {
Role: tomo.R("", "Container", "menu"),
Default: style.AS (
style.AttrBorder {
outline,
tomo.Border {
Width: tomo.I(1),
Color: borderColorLifted,
},
},
style.AttrColor { Color: tomo.ColorBackground },
style.AttrGap { },
),
},
// *.Heading[*]
style.Rule {
Role: tomo.R("", "Heading", ""),
Default: style.AS (
style.AttrAlign { X: tomo.AlignMiddle, Y: tomo.AlignMiddle },
),
},
// *.Separator[*]
style.Rule {
Role: tomo.R("", "Separator", ""),
Default: style.AS (
style.AttrBorder {
tomo.Border {
Width: tomo.I(1),
Color: borderColorEngraved,
},
},
),
},
// *.Slider[*]
style.Rule {
Role: tomo.R("", "Slider", ""),
Default: style.AS (
style.AttrBorder {
outline,
tomo.Border {
Width: tomo.I(1, 0, 0, 1),
Color: borderColorEngraved,
},
},
style.AttrColor { Color: colorGutter },
style.AttrPadding(tomo.I(0, 1, 1, 0)),
),
Focused: style.AS (
style.AttrBorder {
outline,
tomo.Border {
Width: tomo.I(1),
Color: borderColorFocused,
},
},
style.AttrPadding(tomo.I(0)),
),
},
// *.Slider[horizontal]
style.Rule {
Role: tomo.R("", "Slider", "horizontal"),
Default: style.AS(style.AttrMinimumSize { X: 48 }),
},
// *.Slider[vertical]
style.Rule {
Role: tomo.R("", "Slider", "vertical"),
Default: style.AS(style.AttrMinimumSize { Y: 48 }),
},
// *.SliderHandle[*]
style.Rule {
Role: tomo.R("", "SliderHandle", ""),
Default: style.AS (
style.AttrBorder {
outline,
tomo.Border {
Width: tomo.I(1),
Color: borderColorLifted,
},
},
style.AttrColor { Color: tomo.ColorRaised },
style.AttrMinimumSize { X: 12, Y: 12, },
),
},
// *.Checkbox[*]
style.Rule {
Role: tomo.R("", "Checkbox", ""),
Default: style.AS (
style.AttrBorder {
outline,
tomo.Border {
Width: tomo.I(1, 0, 0, 1),
Color: borderColorEngraved,
},
},
style.AttrColor { Color: tomo.ColorSunken },
style.AttrPadding(tomo.I(0, 1, 1, 0)),
style.AttrMinimumSize { X: 19, Y: 19 },
),
Focused: style.AS (
style.AttrBorder {
outline,
tomo.Border {
Width: tomo.I(1),
Color: borderColorFocused,
},
},
style.AttrPadding(tomo.I(0)),
),
},
// *.MenuItem[*]
style.Rule {
Role: tomo.R("", "MenuItem", ""),
Default: style.AS (
style.AttrPadding(tomo.I(4)),
style.AttrGap { X: 4, Y: 4 },
style.AttrColor { Color: color.Transparent },
),
Hovered: style.AS (
style.AttrColor { Color: tomo.ColorAccent },
),
Focused: style.AS (
style.AttrColor { Color: tomo.ColorAccent },
),
},
// *.File[*]
style.Rule {
Role: tomo.R("", "File", ""),
Default: style.AS (
style.AttrColor { Color: color.Transparent },
),
Focused: style.AS (
style.AttrColor { Color: tomo.ColorAccent },
),
},
// *.TearLine[*]
style.Rule {
Role: tomo.R("", "TearLine", ""),
Default: style.AS (
style.AttrBorder {
tomo.Border {
Width: tomo.I(3),
Color: [4]color.Color {
color.Transparent,
color.Transparent,
color.Transparent,
color.Transparent,
},
},
},
style.AttrColor { Color: tomo.ColorForeground },
style.AttrPadding(tomo.I(1, 0, 0, 1)),
),
Hovered: style.AS (
style.AttrBorder {
tomo.Border {
Width: tomo.I(3),
Color: borderColorFocused,
},
},
),
Focused: style.AS (
style.AttrBorder {
tomo.Border {
Width: tomo.I(3),
Color: borderColorFocused,
},
},
),
},
// *.Calendar[*]
style.Rule {
Role: tomo.R("", "Calendar", ""),
Default: style.AS (
style.AttrBorder {
tomo.Border {
Width: tomo.I(0, 1, 1, 0),
Color: borderColorShade,
},
outline,
},
style.AttrColor { Color: colorInput },
style.AttrPadding(tomo.I(2)),
style.AttrGap { X: 2, Y: 2 },
),
},
// *.CalendarGrid[*]
style.Rule {
Role: tomo.R("", "CalendarGrid", ""),
Default: style.AS (
style.AttrGap { X: 2, Y: 2 },
),
},
// *.CalendarWeekdayHeader[*]
style.Rule {
Role: tomo.R("", "CalendarWeekdayHeader", ""),
Default: style.AS (
style.AttrPadding(tomo.I(2)),
style.AttrColor { Color: colorCalendarWeekdayHeader },
),
},
// *.CalendarDay[weekday]
style.Rule {
Role: tomo.R("", "CalendarDay", "weekday"),
Default: style.AS (
style.AttrPadding(tomo.I(2)),
style.AttrMinimumSize { X: 32, Y: 32 },
style.AttrColor { Color: colorCalendarDay },
),
},
// *.CalendarDay[weekend]
style.Rule {
Role: tomo.R("", "CalendarDay", "weekend"),
Default: style.AS (
style.AttrPadding(tomo.I(2)),
style.AttrMinimumSize { X: 32, Y: 32 },
style.AttrColor { Color: colorCalendarWeekend },
),
},
// *.TabbedContainer[*]
style.Rule {
Role: tomo.R("", "TabbedContainer", ""),
Default: style.AS (
style.AttrGap { },
),
},
// *.TabRow[*]
style.Rule {
Role: tomo.R("", "TabRow", ""),
Default: style.AS (
style.AttrBorder {
tomo.Border {
Width: tomo.I(1, 1, 0, 1),
Color: borderColorOutline,
},
tomo.Border {
Width: tomo.I(1, 0, 0, 1),
Color: borderColorEngraved,
},
},
style.AttrGap { X: 0, Y: 0 },
style.AttrColor { Color: colorGutter },
style.AttrPadding(tomo.I(1, 0, 0, 0)),
),
},
// *.TabSpacer[left]
style.Rule {
Role: tomo.R("", "TabSpacer", "left"),
Default: style.AS (
style.AttrBorder {
tomo.Border {
Width: tomo.I(0, 0, 1, 0),
Color: borderColorEngraved,
},
tomo.Border {
Width: tomo.I(0, 0, 1, 0),
Color: borderColorOutline,
},
},
style.AttrMinimumSize { X: 1 },
),
},
// *.TabSpacer[right]
style.Rule {
Role: tomo.R("", "TabSpacer", "right"),
Default: style.AS (
style.AttrBorder {
tomo.Border {
Width: tomo.I(1, 0, 0, 0),
Color: [4]color.Color {
colorGutter, colorGutter,
colorGutter, colorGutter,
},
},
tomo.Border {
Width: tomo.I(0, 0, 1, 0),
Color: borderColorEngraved,
},
tomo.Border {
Width: tomo.I(0, 0, 1, 1),
Color: borderColorOutline,
},
tomo.Border {
Width: tomo.I(0, 0, 0, 1),
Color: borderColorShade,
},
},
style.AttrMinimumSize { X: 3 },
),
},
// *.Tab[*]
style.Rule {
Role: tomo.R("", "Tab", ""),
Default: style.AS (
style.AttrBorder {
tomo.Border {
Width: tomo.I(1, 0, 0, 0),
Color: [4]color.Color {
colorGutter, colorGutter,
colorGutter, colorGutter,
},
},
tomo.Border {
Width: tomo.I(0, 0, 1, 0),
Color: borderColorEngraved,
},
tomo.Border {
Width: tomo.I(1, 0, 1, 1),
Color: borderColorOutline,
},
tomo.Border {
Width: tomo.I(1, 1, 0, 1),
Color: borderColorLifted,
},
},
style.AttrPadding(tomo.I(4, 8, 4, 8)),
style.AttrColor { Color: tomo.ColorRaised },
),
},
// *.Tab[active]
style.Rule {
Role: tomo.R("", "Tab", "active"),
Default: style.AS (
style.AttrBorder {
tomo.Border {
Width: tomo.I(1, 0, 0, 1),
Color: borderColorOutline,
},
tomo.Border {
Width: tomo.I(1, 1, 0, 1),
Color: borderColorLifted,
},
},
style.AttrPadding(tomo.I(4, 8, 4, 8)),
style.AttrColor { Color: tomo.ColorBackground },
),
},
// *.Swatch[*]
style.Rule {
Role: tomo.R("", "Swatch", ""),
Default: style.AS (
style.AttrBorder {
outline,
},
style.AttrMinimumSize { X: 19, Y: 19 },
),
Focused: style.AS (
style.AttrBorder {
outline,
tomo.Border {
Width: tomo.I(1),
Color: borderColorFocused,
},
},
),
},
// *.ColorPickerMap[*]
style.Rule {
Role: tomo.R("", "ColorPickerMap", ""),
Default: style.AS (
style.AttrBorder {
outline,
tomo.Border {
Width: tomo.I(1, 0, 0, 1),
Color: borderColorEngraved,
},
},
style.AttrColor { Color: tomo.ColorSunken },
style.AttrMinimumSize { X: 128, Y: 128 },
),
},
}

View File

@ -0,0 +1,20 @@
package fallbackStyle
import "image/color"
import "git.tebibyte.media/tomo/tomo"
import "git.tebibyte.media/tomo/nasin/internal/style"
// New returns Wintergreen, the default Tomo style. It is neutral-gray with
// green and turquoise accents.
func New () *style.Style {
return &style.Style {
Colors: map[tomo.Color] color.Color {
tomo.ColorBackground: colorBackground,
tomo.ColorForeground: colorForeground,
tomo.ColorRaised: colorCarved,
tomo.ColorSunken: colorCarved,
tomo.ColorAccent: colorFocus,
},
Rules: rules,
}
}

View File

@ -1,4 +1,4 @@
package theme package style
import "image" import "image"
import "image/color" import "image/color"

View File

@ -1,19 +1,17 @@
// Package theme provides a data-driven theme implementation. // Package style provides a data-driven style implementation.
package theme package style
import "image" import "image"
import "image/color" import "image/color"
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/event" import "git.tebibyte.media/tomo/tomo/event"
import "git.tebibyte.media/tomo/tomo/input" import "git.tebibyte.media/tomo/tomo/input"
import "git.tebibyte.media/tomo/tomo/theme"
import "git.tebibyte.media/tomo/tomo/canvas" import "git.tebibyte.media/tomo/tomo/canvas"
// this is CSS's bastard child // this is CSS's bastard child
// Theme allows the use of data to define a visual style. // Style allows the use of data to define a visual style.
type Theme struct { type Style struct {
// Textures maps texture names to image textures. // Textures maps texture names to image textures.
Textures map[string] image.Image Textures map[string] image.Image
textures map[string] canvas.TextureCloser // private texture cache textures map[string] canvas.TextureCloser // private texture cache
@ -22,26 +20,14 @@ type Theme struct {
// Rules determines which styles get applied to which Objects. // Rules determines which styles get applied to which Objects.
Rules []Rule Rules []Rule
// Colors maps theme.Color values to color.RGBA values. // Colors maps tomo.Color values to color.RGBA values.
Colors map[theme.Color] color.Color Colors map[tomo.Color] color.Color
// This type does not handle icons, and as such, a special icon theme
// must be separately specified.
IconTheme
}
// IconTheme implements the part of theme.Theme that handles icons.
type IconTheme interface {
// Icon returns a texture of the corresponding icon ID.
Icon (theme.Icon, theme.IconSize) canvas.Texture
// MimeIcon returns an icon corresponding to a MIME type.
MimeIcon (data.Mime, theme.IconSize) canvas.Texture
} }
// Rule describes under what circumstances should certain style attributes be // Rule describes under what circumstances should certain style attributes be
// active. // active.
type Rule struct { type Rule struct {
Role theme.Role Role tomo.Role
Default AttrSet Default AttrSet
Hovered AttrSet Hovered AttrSet
Pressed AttrSet Pressed AttrSet
@ -97,7 +83,7 @@ func (this *AttrSet) ensure () {
if this.set == nil { this.set = make(map[int] Attr) } if this.set == nil { this.set = make(map[int] Attr) }
} }
func (this *Theme) execute (object tomo.Object, set AttrSet) { func (this *Style) execute (object tomo.Object, set AttrSet) {
box := object.GetBox() box := object.GetBox()
for _, attr := range set.set { for _, attr := range set.set {
@ -105,7 +91,7 @@ func (this *Theme) execute (object tomo.Object, set AttrSet) {
case AttrColor: case AttrColor:
box.SetColor(attr.Color) box.SetColor(attr.Color)
case AttrTexture: case AttrTexture:
box.SetTexture(this.texture(string(attr))) box.SetTextureTile(this.texture(string(attr)))
case AttrBorder: case AttrBorder:
box.SetBorder([]tomo.Border(attr)...) box.SetBorder([]tomo.Border(attr)...)
case AttrMinimumSize: case AttrMinimumSize:
@ -133,12 +119,12 @@ func (this *Theme) execute (object tomo.Object, set AttrSet) {
box.SetAlign(attr.X, attr.Y) box.SetAlign(attr.X, attr.Y)
} }
default: default:
panic("bug: nasin/internal/theme.Theme: unexpected attribute") panic("bug: nasin/internal/tomo.Theme: unexpected attribute")
} }
} }
} }
func (this *Theme) texture (name string) canvas.Texture { func (this *Style) texture (name string) canvas.Texture {
this.ensureTextureCache() this.ensureTextureCache()
if texture, ok := this.textures[name]; ok { if texture, ok := this.textures[name]; ok {
return texture return texture
@ -153,19 +139,19 @@ func (this *Theme) texture (name string) canvas.Texture {
return this.missingTexture() return this.missingTexture()
} }
func (this *Theme) missingTexture () canvas.Texture { func (this *Style) missingTexture () canvas.Texture {
if this.missing == nil { if this.missing == nil {
this.missing = tomo.NewTexture(missingTexture(16)) this.missing = tomo.NewTexture(missingTexture(16))
} }
return this.missing return this.missing
} }
func (this *Theme) ensureTextureCache () { func (this *Style) ensureTextureCache () {
if this.textures == nil { this.textures = make(map[string] canvas.TextureCloser) } if this.textures == nil { this.textures = make(map[string] canvas.TextureCloser) }
} }
// setsFor builds flattened attr sets for a specific role based on the rules list // setsFor builds flattened attr sets for a specific role based on the rules list
func (this *Theme) setsFor (role theme.Role) (defaul, hovered, pressed, focused AttrSet) { func (this *Style) setsFor (role tomo.Role) (defaul, hovered, pressed, focused AttrSet) {
for _, current := range this.Rules { for _, current := range this.Rules {
// check for a match // check for a match
packageMatch := current.Role.Package == role.Package || current.Role.Package == "" packageMatch := current.Role.Package == role.Package || current.Role.Package == ""
@ -187,10 +173,11 @@ func (this *Theme) setsFor (role theme.Role) (defaul, hovered, pressed, focused
return defaul, hovered, pressed, focused return defaul, hovered, pressed, focused
} }
func (this *Theme) Apply (object tomo.Object, role theme.Role) event.Cookie { func (this *Style) Apply (object tomo.Object) event.Cookie {
pressed := false pressed := false
hovered := false hovered := false
box := object.GetBox() box := object.GetBox()
role := box.Role()
defaultSet, hoveredSet, pressedSet, focusedSet := this.setsFor(role) defaultSet, hoveredSet, pressedSet, focusedSet := this.setsFor(role)
@ -232,32 +219,16 @@ func (this *Theme) Apply (object tomo.Object, role theme.Role) event.Cookie {
} }
func (this *Theme) RGBA (c theme.Color) (r, g, b, a uint32) { func (this *Style) RGBA (c tomo.Color) (r, g, b, a uint32) {
if this.Colors == nil { return 0xFFFF, 0, 0xFFFF, 0xFFFF } if this.Colors == nil { return 0xFFFF, 0, 0xFFFF, 0xFFFF }
color, ok := this.Colors[c] color, ok := this.Colors[c]
if !ok { return 0xFFFF, 0, 0xFFFF, 0xFFFF } if !ok { return 0xFFFF, 0, 0xFFFF, 0xFFFF }
return color.RGBA() return color.RGBA()
} }
func (this *Theme) Icon (icon theme.Icon, size theme.IconSize) canvas.Texture { // Close closes all cached textures this style has open. Do not call this while
if this.IconTheme == nil { // the style is in use.
return this.missingTexture() func (this *Style) Close () error {
} else {
return this.IconTheme.Icon(icon, size)
}
}
func (this *Theme) MimeIcon (mime data.Mime, size theme.IconSize) canvas.Texture {
if this.IconTheme == nil {
return this.missingTexture()
} else {
return this.IconTheme.MimeIcon(mime, size)
}
}
// Close closes all cached textures this theme has open. Do not call this while
// the theme is in use.
func (this *Theme) Close () error {
this.missing.Close() this.missing.Close()
this.missing = nil this.missing = nil
for _, texture := range this.textures { for _, texture := range this.textures {

View File

@ -1,231 +0,0 @@
package defaultTheme
import "image/color"
import "git.tebibyte.media/tomo/tomo"
import "golang.org/x/image/font/basicfont"
import "git.tebibyte.media/tomo/tomo/theme"
import dataTheme "git.tebibyte.media/tomo/nasin/internal/theme"
var colorFocus = color.RGBA { R: 61, G: 128, B: 143, A: 255 }
var colorInput = color.RGBA { R: 208, G: 203, B: 150, A: 255 }
var colorCarved = color.RGBA { R: 151, G: 160, B: 150, A: 255 }
var colorShadow = color.RGBA { R: 57, G: 59, B: 57, A: 255 }
var colorInputShadow = color.RGBA { R: 143, G: 146, B: 91, A: 255 }
var colorHighlight = color.RGBA { R: 207, G: 215, B: 210, A: 255 }
var colorBackground = color.RGBA { R: 169, G: 171, B: 168, A: 255 }
var colorCarvedPressed = color.RGBA { R: 129, G: 142, B: 137, A: 255 }
var colorForeground = color.Black
var colorOutline = color.Black
var outline = tomo.Border {
Width: tomo.I(1),
Color: [4]color.Color {
colorOutline,
colorOutline,
colorOutline,
colorOutline,
},
}
var borderColorEngraved = [4]color.Color { colorShadow, colorHighlight, colorHighlight, colorShadow }
var borderColorLifted = [4]color.Color { colorHighlight, colorShadow, colorShadow, colorHighlight }
var borderColorInput = [4]color.Color { colorInputShadow, colorInput, colorInput, colorInputShadow }
var borderColorFocused = [4]color.Color { colorFocus, colorFocus, colorFocus, colorFocus }
var rules = []dataTheme.Rule {
// *.*[*]
dataTheme.Rule {
Default: dataTheme.AS (
dataTheme.AttrFace { Face: basicfont.Face7x13 },
dataTheme.AttrTextColor { Color: theme.ColorForeground },
dataTheme.AttrDotColor { Color: theme.ColorAccent },
dataTheme.AttrGap { X: 8, Y: 8 },
),
},
// *.Button[*]
dataTheme.Rule {
Role: theme.R("", "Button", ""),
Default: dataTheme.AS (
dataTheme.AttrBorder {
outline,
tomo.Border {
Width: tomo.I(1),
Color: borderColorLifted,
},
},
dataTheme.AttrPadding(tomo.I(4, 8)),
dataTheme.AttrColor { Color: theme.ColorRaised },
),
Pressed: dataTheme.AS (
dataTheme.AttrBorder {
outline,
tomo.Border {
Width: tomo.I(1, 0, 0, 1),
Color: borderColorEngraved,
},
},
dataTheme.AttrPadding(tomo.I(5, 8, 4, 9)),
dataTheme.AttrColor { Color: colorCarvedPressed },
),
Focused: dataTheme.AS (
dataTheme.AttrBorder {
outline,
tomo.Border {
Width: tomo.I(1),
Color: borderColorFocused,
},
},
dataTheme.AttrPadding(tomo.I(4, 8)),
),
},
// *.TextInput[*]
dataTheme.Rule {
Role: theme.R("", "TextInput", ""),
Default: dataTheme.AS (
dataTheme.AttrBorder {
outline,
tomo.Border {
Width: tomo.I(1),
Color: borderColorInput,
},
},
dataTheme.AttrColor { Color: colorInput },
dataTheme.AttrPadding(tomo.I(5, 4, 4, 5)),
),
Focused: dataTheme.AS (
dataTheme.AttrBorder {
outline,
tomo.Border {
Width: tomo.I(1),
Color: borderColorFocused,
},
},
),
},
// *.NumberInput[*]
dataTheme.Rule {
Role: theme.R("", "NumberInput", ""),
Default: dataTheme.AS (
dataTheme.AttrBorder {
outline,
tomo.Border {
Width: tomo.I(1),
Color: borderColorInput,
},
},
dataTheme.AttrColor { Color: colorInput },
dataTheme.AttrPadding(tomo.I(5, 4, 4, 5)),
),
Focused: dataTheme.AS (
dataTheme.AttrBorder {
outline,
tomo.Border {
Width: tomo.I(1),
Color: borderColorFocused,
},
},
),
},
// *.Container[outer]
dataTheme.Rule {
Role: theme.R("", "Container", "outer"),
Default: dataTheme.AS (
dataTheme.AttrColor { Color: theme.ColorBackground },
dataTheme.AttrPadding(tomo.I(8)),
),
},
// *.Heading[*]
dataTheme.Rule {
Role: theme.R("", "Heading", ""),
Default: dataTheme.AS (
dataTheme.AttrAlign { X: tomo.AlignMiddle, Y: tomo.AlignMiddle },
),
},
// *.Separator[*]
dataTheme.Rule {
Role: theme.R("", "Separator", ""),
Default: dataTheme.AS (
dataTheme.AttrBorder {
tomo.Border {
Width: tomo.I(1),
Color: borderColorEngraved,
},
},
),
},
// *.Slider[*]
dataTheme.Rule {
Role: theme.R("", "Slider", ""),
Default: dataTheme.AS (
dataTheme.AttrBorder {
outline,
tomo.Border {
Width: tomo.I(1, 0, 0, 1),
Color: borderColorEngraved,
},
},
dataTheme.AttrColor { Color: theme.ColorSunken },
dataTheme.AttrPadding(tomo.I(0, 1, 1, 0)),
),
Focused: dataTheme.AS (
dataTheme.AttrBorder {
outline,
tomo.Border {
Width: tomo.I(1),
Color: borderColorFocused,
},
},
dataTheme.AttrPadding(tomo.I(0)),
),
},
// *.Slider[horizontal]
dataTheme.Rule {
Role: theme.R("", "Slider", "horizontal"),
Default: dataTheme.AS(dataTheme.AttrMinimumSize { X: 48 }),
},
// *.Slider[vertical]
dataTheme.Rule {
Role: theme.R("", "Slider", "vertical"),
Default: dataTheme.AS(dataTheme.AttrMinimumSize { Y: 48 }),
},
// *.SliderHandle[*]
dataTheme.Rule {
Role: theme.R("", "SliderHandle", ""),
Default: dataTheme.AS (
dataTheme.AttrBorder {
outline,
tomo.Border {
Width: tomo.I(1),
Color: borderColorLifted,
},
},
dataTheme.AttrColor { Color: theme.ColorRaised },
dataTheme.AttrMinimumSize { X: 12, Y: 12, },
),
},
}
// Theme returns Wintergreen, the default Tomo theme. It is neutral-gray with
// green and turquoise accents.
func Theme () theme.Theme {
return &dataTheme.Theme {
Colors: map[theme.Color] color.Color {
theme.ColorBackground: colorBackground,
theme.ColorForeground: colorForeground,
theme.ColorRaised: colorCarved,
theme.ColorSunken: colorCarved,
theme.ColorAccent: colorFocus,
},
Rules: rules,
}
}

198
path.go
View File

@ -1,190 +1,32 @@
package nasin package nasin
import "io"
import "os" import "os"
import "io/fs"
import "strings"
import "path/filepath" import "path/filepath"
// FS is Tomo's implementation of fs.FS. It provides access to a specific part // ApplicationUserDataDir returns the directory path where an application can
// of the filesystem. // store its user data files. If the directory does not exist, it will be
type FS struct { // created.
path string func ApplicationUserDataDir (app ApplicationDescription) (string, error) {
return userMkdirAll(app.ID, userDataDir)
} }
// FileWriter is a writable version of fs.File. // ApplicationUserConfigDir returns the directory path where an application can
type FileWriter interface { // store its user configuration files.
fs.File func ApplicationUserConfigDir (app ApplicationDescription) (string, error) {
io.Writer return userMkdirAll(app.ID, userConfigDir)
} }
// ApplicationUserDataFS returns an FS that an application can use to store user // ApplicationUserCacheDir returns the directory path where an application can
// data files. // store its user cache files.
func ApplicationUserDataFS (app ApplicationDescription) (*FS, error) { func ApplicationUserCacheDir (app ApplicationDescription) (string, error) {
dataDir, err := userDataDir() return userMkdirAll(app.ID, userCacheDir)
if err != nil { return nil, err }
return appFs(dataDir, app)
} }
// ApplicationUserConfigFS returns an FS that an application can use to store func userMkdirAll (sub string, getter func () (string, error)) (string, error) {
// user configuration files. path, err := getter()
func ApplicationUserConfigFS (app ApplicationDescription) (*FS, error) { if err != nil { return "", err }
configDir, err := userConfigDir() path = filepath.Join(path, sub)
if err != nil { return nil, err } err = os.MkdirAll(path, 0700)
return appFs(configDir, app) if err != nil { return "", err }
} return path, nil
// ApplicationUserCacheFS returns an FS that an application can use to store
// user cache files.
func ApplicationUserCacheFS (app ApplicationDescription) (*FS, error) {
cacheDir, err := userCacheDir()
if err != nil { return nil, err }
return appFs(cacheDir, app)
}
func pathErr (op, path string, err error) error {
return &fs.PathError {
Op: op,
Path: path,
Err: err,
}
}
func appFs (root string, app ApplicationDescription) (*FS, error) {
// remove slashes
appid := app.ID
appid = strings.ReplaceAll(appid, "/", "-")
appid = strings.ReplaceAll(appid, "\\", "-")
path := filepath.Join(root, appid)
// ensure the directory actually exists
err := os.MkdirAll(path, 700)
if err != nil { return nil, err }
return &FS { path: path }, nil
}
func (this FS) subPath (name string) (string, error) {
if !fs.ValidPath(name) { return "", fs.ErrInvalid }
if strings.Contains(name, "/") { return "", fs.ErrInvalid }
return filepath.Join(this.path, name), nil
}
// Open opens the named file.
func (this FS) Open (name string) (fs.File, error) {
path, err := this.subPath(name)
if err != nil {
return nil, pathErr("open", name, err)
}
return os.Open(path)
}
// Create creates or truncates the named file.
func (this FS) Create (name string) (FileWriter, error) {
path, err := this.subPath(name)
if err != nil {
return nil, pathErr("create", name, err)
}
return os.Create(path)
}
// OpenFile is the generalized open call; most users will use Open or Create
// instead.
func (this FS) OpenFile (
name string,
flag int,
perm os.FileMode,
) (
FileWriter,
error,
) {
path, err := this.subPath(name)
if err != nil {
return nil, pathErr("open", name, err)
}
return os.OpenFile(path, flag, perm)
}
// ReadDir reads the named directory and returns a list of directory entries
// sorted by filename.
func (this FS) ReadDir (name string) ([]fs.DirEntry, error) {
path, err := this.subPath(name)
if err != nil {
return nil, pathErr("readdir", name, err)
}
return os.ReadDir(path)
}
// ReadFile reads the named file and returns its contents.
// A successful call returns a nil error, not io.EOF.
// (Because ReadFile reads the whole file, the expected EOF
// from the final Read is not treated as an error to be reported.)
//
// The caller is permitted to modify the returned byte slice.
func (this FS) ReadFile (name string) ([]byte, error) {
path, err := this.subPath(name)
if err != nil {
return nil, pathErr("readfile", name, err)
}
return os.ReadFile(path)
}
// WriteFile writes data to the named file, creating it if necessary.
func (this FS) WriteFile (name string, data []byte, perm os.FileMode) error {
path, err := this.subPath(name)
if err != nil {
return pathErr("writefile", name, err)
}
return os.WriteFile(path, data, perm)
}
// Stat returns a FileInfo describing the file.
func (this FS) Stat (name string) (fs.FileInfo, error) {
path, err := this.subPath(name)
if err != nil {
return nil, pathErr("stat", name, err)
}
return os.Stat(path)
}
// Remove removes the named file or (empty) directory.
func (this FS) Remove (name string) error {
path, err := this.subPath(name)
if err != nil {
return pathErr("remove", name, err)
}
return os.Remove(path)
}
// RemoveAll removes name and any children it contains.
func (this FS) RemoveAll (name string) error {
path, err := this.subPath(name)
if err != nil {
return pathErr("removeall", name, err)
}
return os.RemoveAll(path)
}
// Rename renames (moves) oldname to newname.
func (this FS) Rename (oldname, newname string) error {
oldpath, err := this.subPath(oldname)
if err != nil {
return pathErr("rename", oldname, err)
}
newpath, err := this.subPath(newname)
if err != nil {
return pathErr("rename", newname, err)
}
return os.Rename(oldpath, newpath)
} }