Repository navigation
Expand file tree
/
Copy pathdeeplink.go
More file actions
189 lines (173 loc) · 6.22 KB
/
Copy pathdeeplink.go
File metadata and controls
189 lines (173 loc) · 6.22 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
package mygo
import (
"fmt"
"net/url"
"os"
"path/filepath"
"runtime"
"slices"
"strings"
"sync"
"github.com/egoist/mygo/internal/platform"
)
// Set by `mygo build` and `mygo dev` with -ldflags -X: what mygo.config.ts says
// about the app, for platforms whose executables carry no such metadata
// (Linux), and the URL schemes it declares.
var (
packageName string
packageVersion string
packageIdentifier string
packageURLSchemes string // comma separated
// packageFileExtensions are those of the file associations.
packageFileExtensions string // comma separated, lower case
)
// bundleInfo is what the backend reads of the app's bundle, which does not
// change while it runs. Public methods such as App.Name ask for it from any
// goroutine.
var bundleInfo = sync.OnceValues(func() (platform.PackageInfo, bool) { return backend().App().Package() })
// packageInfo describes the app as packaged by `mygo build` or `mygo dev`.
func packageInfo() (platform.PackageInfo, bool) {
if info, ok := bundleInfo(); ok {
return info, true
}
if packageIdentifier != "" {
return platform.PackageInfo{Name: packageName, Version: packageVersion, Identifier: packageIdentifier}, true
}
return platform.PackageInfo{}, false
}
// Deep links: URLs of the schemes the app handles reach OnOpenURL. macOS
// hands them to bundles that declare the scheme (urlSchemes in mygo.config.ts)
// with Apple Events; Windows and Linux start the app with the URL as an
// argument, so the arguments of the launch, and those that a second
// instance forwards (RequestSingleInstanceLock), are searched for them.
// registeredSchemes are the schemes passed to RegisterURLScheme.
var registeredSchemes struct {
sync.Mutex
set map[string]bool
}
// isURLScheme reports whether s is a URL scheme: a letter, then letters,
// digits, "+", "-" and ".". (Package regexp would make builds for browsers
// larger.)
func isURLScheme(s string) bool {
for i, c := range []byte(s) {
letter := c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z'
if !letter && (i == 0 || (c < '0' || c > '9') && c != '+' && c != '-' && c != '.') {
return false
}
}
return s != ""
}
// handlesScheme reports whether URLs of scheme reach OnOpenURL.
func handlesScheme(scheme string) bool {
scheme = strings.ToLower(scheme)
for _, s := range strings.Split(packageURLSchemes, ",") {
if s != "" && strings.EqualFold(s, scheme) {
return true
}
}
registeredSchemes.Lock()
defer registeredSchemes.Unlock()
return registeredSchemes.set[scheme]
}
// urlArgs returns the command line arguments that are URLs of schemes the
// app handles.
func urlArgs(args []string) []string {
var urls []string
for _, a := range args {
// A single letter is a Windows drive, as in C:\.
if scheme, _, ok := strings.Cut(a, ":"); ok && len(scheme) > 1 && isURLScheme(scheme) && handlesScheme(scheme) {
urls = append(urls, a)
}
}
return urls
}
// fileArgs returns the command line arguments that are files the app
// opens, of the extensions of its file associations, as absolute paths;
// relative ones are relative to wd. Linux launchers may pass file URLs.
func fileArgs(args []string, wd string) []string {
if packageFileExtensions == "" {
return nil
}
exts := strings.Split(packageFileExtensions, ",")
var files []string
for _, a := range args {
p := a
if u, err := url.Parse(a); err == nil && u.Scheme == "file" {
p = filepath.FromSlash(u.Path)
if runtime.GOOS == "windows" {
p = strings.TrimPrefix(p, `\`) // file:///C:/x
}
}
if !slices.Contains(exts, strings.TrimPrefix(strings.ToLower(filepath.Ext(p)), ".")) {
continue
}
if !filepath.IsAbs(p) {
p = filepath.Join(wd, p)
}
if info, err := os.Stat(p); err == nil && info.Mode().IsRegular() {
files = append(files, p)
}
}
return files
}
// deliverArgs passes the URLs among args to OnOpenURL and the files to
// OnOpenFile.
func deliverArgs(args []string, wd string) {
for _, u := range urlArgs(args) {
fire1(&App.onOpenURL, u)
}
for _, f := range fileArgs(args, wd) {
fire1(&App.onOpenFile, f)
}
}
// RegisterURLScheme makes the app the handler of the URLs of scheme for the
// current user, e.g. myapp://open?item=1 for "myapp": they reach OnOpenURL,
// including the one the app is started with. Call it before Run, typically
// every time the app starts: registering again is cheap.
//
// On Windows it registers the executable under HKEY_CURRENT_USER, on Linux
// a hidden desktop entry that becomes the default handler (as xdg-mime
// does). On macOS the scheme must be listed in urlSchemes in mygo.config.ts,
// which registers it with the system; this makes the app its default
// handler when others claim it too. Unregister it when the app is
// uninstalled, for example with UnregisterURLScheme.
func (a *Application) RegisterURLScheme(scheme string) error {
if !isURLScheme(scheme) {
return fmt.Errorf("mygo: invalid URL scheme %q", scheme)
}
scheme = strings.ToLower(scheme)
registeredSchemes.Lock()
if registeredSchemes.set == nil {
registeredSchemes.set = map[string]bool{}
}
registeredSchemes.set[scheme] = true
registeredSchemes.Unlock()
id, name := appID(), a.Name()
return onMainValue(func() error { return backend().App().RegisterURLScheme(scheme, id, name) })
}
// UnregisterURLScheme undoes RegisterURLScheme (Linux, Windows); URLs of
// the scheme no longer reach the app unless mygo.config.ts declares it.
func (a *Application) UnregisterURLScheme(scheme string) error {
if !isURLScheme(scheme) {
return fmt.Errorf("mygo: invalid URL scheme %q", scheme)
}
scheme = strings.ToLower(scheme)
registeredSchemes.Lock()
delete(registeredSchemes.set, scheme)
registeredSchemes.Unlock()
id, name := appID(), a.Name()
return onMainValue(func() error { return backend().App().UnregisterURLScheme(scheme, id, name) })
}
// IsURLSchemeRegistered reports whether the system opens URLs of scheme
// with this app.
func (a *Application) IsURLSchemeRegistered(scheme string) bool {
if !isURLScheme(scheme) {
return false
}
scheme = strings.ToLower(scheme)
id, name := appID(), a.Name()
return onMainValue(func() bool { return backend().App().IsURLSchemeRegistered(scheme, id, name) })
}
// launchArgs delivers the URLs and files the app was started with, once it
// is ready.
func launchArgs() { deliverArgs(os.Args[1:], startDir) }