Skip to content

Commit e0e2496

Browse files
committed
feat: custom url scheme
1 parent c8639ce commit e0e2496

10 files changed

Lines changed: 248 additions & 38 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ frontend/public/release
1010
.eslintcache
1111
sdk/node/README.md
1212
**/.vitepress/cache
13+
.fuse_hidden*

service/discovery/file.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import (
1212
func getFilePath() string {
1313
if runtime.GOOS == "windows" {
1414
localAppData := os.Getenv("LOCALAPPDATA")
15-
return filepath.Join(localAppData, "UnitedToken", "service.json")
15+
return filepath.Join(localAppData, "UniToken", "service.json")
1616
}
1717
home := os.Getenv("HOME")
1818
return filepath.Join(home, ".local", "share", "uni-token", "service.json")

service/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ require (
1212
github.com/kardianos/service v1.2.4
1313
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
1414
go.etcd.io/bbolt v1.4.2
15+
golang.org/x/sys v0.35.0
1516
)
1617

1718
require (
@@ -37,7 +38,6 @@ require (
3738
golang.org/x/arch v0.18.0 // indirect
3839
golang.org/x/crypto v0.39.0 // indirect
3940
golang.org/x/net v0.41.0 // indirect
40-
golang.org/x/sys v0.34.0 // indirect
4141
golang.org/x/text v0.26.0 // indirect
4242
google.golang.org/protobuf v1.36.6 // indirect
4343
gopkg.in/yaml.v3 v3.0.1 // indirect

service/go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,8 @@ golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
9090
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
9191
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
9292
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
93-
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
94-
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
93+
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
94+
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
9595
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
9696
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
9797
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=

service/logic/url_scheme/linux.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
//go:build linux
2+
3+
package urlScheme
4+
5+
import (
6+
"fmt"
7+
"os"
8+
"os/exec"
9+
"path/filepath"
10+
)
11+
12+
// Stub for Windows function when building on Linux
13+
func registerURLSchemeWindows(options UrlSchemeRegisterOption) error {
14+
return fmt.Errorf("windows URL scheme registration not available on linux")
15+
}
16+
17+
// registerURLSchemeForLinux registers URL scheme on Linux
18+
func registerURLSchemeLinux(options UrlSchemeRegisterOption) error {
19+
homeDir, err := os.UserHomeDir()
20+
if err != nil {
21+
return fmt.Errorf("failed to get user home directory: %w", err)
22+
}
23+
24+
// .desktop files are usually placed in ~/.local/share/applications/
25+
desktopDir := filepath.Join(homeDir, ".local", "share", "applications")
26+
if _, err := os.Stat(desktopDir); os.IsNotExist(err) {
27+
if err := os.MkdirAll(desktopDir, 0755); err != nil {
28+
return fmt.Errorf("failed to create directory %s: %w", desktopDir, err)
29+
}
30+
}
31+
32+
// Define .desktop file name and path
33+
desktopFileName := fmt.Sprintf("%s.desktop", options.AppName)
34+
desktopFilePath := filepath.Join(desktopDir, desktopFileName)
35+
36+
// Get current executable path
37+
exePath, err := os.Executable()
38+
if err != nil {
39+
return fmt.Errorf("failed to get executable path: %w", err)
40+
}
41+
42+
// Write .desktop file content
43+
content := fmt.Sprintf(`[Desktop Entry]
44+
Name=%s
45+
Comment=Opens %s:// links
46+
Exec=%s url %%u
47+
Terminal=false
48+
Type=Application
49+
MimeType=x-scheme-handler/%s;
50+
`, options.AppName, options.Scheme, exePath, options.Scheme)
51+
52+
if err := os.WriteFile(desktopFilePath, []byte(content), 0644); err != nil {
53+
return fmt.Errorf("failed to write .desktop file: %w", err)
54+
}
55+
// Use xdg-mime command to register MIME type
56+
// This tells the system which application handles 'x-scheme-handler/my-app'
57+
cmd := exec.Command("xdg-mime", "default", desktopFileName, fmt.Sprintf("x-scheme-handler/%s", options.Scheme))
58+
if output, err := cmd.CombinedOutput(); err != nil {
59+
return fmt.Errorf("xdg-mime execution failed: %w\nOutput: %s", err, output)
60+
}
61+
62+
// Some desktop environments may need to update desktop database, but xdg-mime default is usually sufficient
63+
// cmd = exec.Command("update-desktop-database", desktopDir)
64+
// if output, err := cmd.CombinedOutput(); err != nil {
65+
// fmt.Printf("⚠️ Warning: update-desktop-database may fail or be unnecessary: %w\nOutput: %s\n", err, output)
66+
// }
67+
68+
return nil
69+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package urlScheme
2+
3+
import (
4+
"fmt"
5+
"runtime"
6+
)
7+
8+
type UrlSchemeRegisterOption struct {
9+
Scheme string
10+
AppName string
11+
}
12+
13+
func RegisterURLScheme(options UrlSchemeRegisterOption) error {
14+
switch runtime.GOOS {
15+
case "windows":
16+
return registerURLSchemeWindows(options)
17+
case "linux":
18+
return registerURLSchemeLinux(options)
19+
// case "darwin":
20+
// return registerURLSchemeMacOS(options)
21+
default:
22+
return fmt.Errorf("unsupported OS: %s", runtime.GOOS)
23+
}
24+
}

service/logic/url_scheme/stub.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
//go:build !linux && !windows
2+
3+
package urlScheme
4+
5+
import "fmt"
6+
7+
func registerURLSchemeWindows(options UrlSchemeRegisterOption) error {
8+
return fmt.Errorf("Windows URL scheme registration not implemented on this platform")
9+
}
10+
11+
func registerURLSchemeLinux(options UrlSchemeRegisterOption) error {
12+
return fmt.Errorf("Linux URL scheme registration not implemented on this platform")
13+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
//go:build windows
2+
3+
package urlScheme
4+
5+
import (
6+
"fmt"
7+
"os"
8+
"path/filepath"
9+
"strings"
10+
11+
"golang.org/x/sys/windows/registry"
12+
)
13+
14+
// Stub for Linux function when building on Windows
15+
func registerURLSchemeLinux(options UrlSchemeRegisterOption) error {
16+
return fmt.Errorf("Linux URL scheme registration not available on Windows")
17+
}
18+
19+
// registerURLSchemeForWindows registers URL scheme on Windows
20+
func registerURLSchemeWindows(options UrlSchemeRegisterOption) error {
21+
// Get current executable path
22+
exePath, err := os.Executable()
23+
if err != nil {
24+
return fmt.Errorf("failed to get executable path: %w", err)
25+
}
26+
// Convert path to Windows-style slashes
27+
exePath = filepath.ToSlash(exePath)
28+
29+
// Build registry path
30+
// HKEY_CURRENT_USER is user-specific, can be modified without admin privileges
31+
baseKey := "Software\\Classes\\" + options.Scheme
32+
commandKey := baseKey + "\\shell\\open\\command"
33+
34+
// 1. Create or open main key (e.g.: HKEY_CURRENT_USER\Software\Classes\my-app)
35+
k, _, err := registry.CreateKey(registry.CURRENT_USER, baseKey, registry.SET_VALUE)
36+
if err != nil {
37+
return fmt.Errorf("failed to create registry key %s: %w", baseKey, err)
38+
}
39+
defer k.Close()
40+
41+
// Set default value and "URL Protocol" flag
42+
// @="URL: My App Protocol"
43+
if err := k.SetStringValue("", "URL: "+strings.ToUpper(options.Scheme)+" Protocol"); err != nil {
44+
return fmt.Errorf("failed to set default value for key %s: %w", baseKey, err)
45+
}
46+
// "URL Protocol"="" (empty string value, indicates this is a URL Protocol Handler)
47+
if err := k.SetStringValue("URL Protocol", ""); err != nil {
48+
return fmt.Errorf("failed to set URL Protocol value for key %s: %w", baseKey, err)
49+
}
50+
51+
// 2. Create or open command key (e.g.: ...\my-app\shell\open\command)
52+
cmdK, _, err := registry.CreateKey(registry.CURRENT_USER, commandKey, registry.SET_VALUE)
53+
if err != nil {
54+
return fmt.Errorf("failed to create registry key %s: %w", commandKey, err)
55+
}
56+
defer cmdK.Close()
57+
58+
// Set command key value to executable path and %1 (full URL placeholder)
59+
// @="\"C:\\Path\\To\\Your\\MyApp.exe\" \"%1\""
60+
commandValue := fmt.Sprintf(`"%s" "%%1"`, exePath) // Note: %% before %1 is for escaping
61+
if err := cmdK.SetStringValue("", commandValue); err != nil {
62+
return fmt.Errorf("failed to set command value for key %s: %w", commandKey, err)
63+
}
64+
65+
return nil
66+
}

service/main.go

Lines changed: 69 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@ package main
33
import (
44
"fmt"
55
"os"
6-
"path/filepath"
76
"strings"
87
"time"
98

109
"github.com/kardianos/service"
1110

1211
"uni-token-service/discovery"
1312
"uni-token-service/logic"
13+
urlScheme "uni-token-service/logic/url_scheme"
1414
"uni-token-service/server"
1515
"uni-token-service/store"
1616
)
@@ -94,44 +94,23 @@ func main() {
9494
}
9595
command := os.Args[1]
9696

97-
if command == "sudo" {
98-
command, err := filepath.Abs(os.Args[0])
99-
if err != nil {
100-
panic(err)
101-
}
102-
for _, arg := range os.Args[2:] {
103-
command += " " + arg
104-
}
105-
106-
res, err := logic.SudoExec(command, &logic.SudoOptions{
107-
Name: serviceDisplayName,
108-
})
109-
if err != nil {
110-
panic(err)
111-
}
112-
if res.Stdout != "" {
113-
fmt.Println(res.Stdout)
114-
}
115-
if res.Stderr != "" {
116-
fmt.Println(res.Stderr)
117-
}
97+
if command == "version" {
98+
fmt.Printf("Service Version: %d\n", logic.GetVersion())
11899
return
119100
}
120101

121-
if command == "version" {
122-
fmt.Printf("Service Version: %d\n", logic.GetVersion())
102+
if command == "url" {
103+
handleUrlScheme(os.Args[2])
123104
return
124105
}
125106

126107
if command == "setup" {
127-
err = service.Control(s, "install")
128-
if err != nil && !strings.Contains(err.Error(), "already exists") {
129-
fmt.Println("Failed to install service:", err)
130-
}
131-
err = service.Control(s, "start")
132-
if err != nil {
133-
panic(err)
134-
}
108+
handleSetup()
109+
return
110+
}
111+
112+
if command == "setup-in-sudo" {
113+
handleSetupInSudo(&s)
135114
return
136115
}
137116

@@ -142,3 +121,61 @@ func main() {
142121
panic(err)
143122
}
144123
}
124+
125+
func handleSetup() {
126+
// Register URL scheme for the application
127+
err := urlScheme.RegisterURLScheme(urlScheme.UrlSchemeRegisterOption{
128+
Scheme: "uni-token",
129+
AppName: "UniToken",
130+
})
131+
if err != nil {
132+
panic(err)
133+
}
134+
135+
// Install and start the service
136+
execPath, err := os.Executable()
137+
if err != nil {
138+
panic(err)
139+
}
140+
res, err := logic.SudoExec(
141+
execPath+" setup-in-sudo",
142+
&logic.SudoOptions{
143+
Name: serviceDisplayName,
144+
},
145+
)
146+
if err != nil {
147+
panic(err)
148+
}
149+
if res.Stdout != "" {
150+
fmt.Println(res.Stdout)
151+
}
152+
if res.Stderr != "" {
153+
fmt.Println(res.Stderr)
154+
}
155+
}
156+
157+
func handleUrlScheme(url string) {
158+
if !strings.HasPrefix(url, "uni-token://") {
159+
fmt.Println("Invalid URL scheme. Expected 'uni-token://'.")
160+
return
161+
}
162+
url = strings.TrimPrefix(url, "uni-token://")
163+
164+
switch url {
165+
case "start":
166+
handleSetup()
167+
default:
168+
fmt.Printf("Unknown URL action: %s\n", url)
169+
}
170+
}
171+
172+
func handleSetupInSudo(s *service.Service) {
173+
err := service.Control(*s, "install")
174+
if err != nil && !strings.Contains(err.Error(), "already exists") {
175+
fmt.Println("Failed to install service:", err)
176+
}
177+
err = service.Control(*s, "start")
178+
if err != nil {
179+
panic(err)
180+
}
181+
}

service/server/action.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ func SetupActionAPI(router *gin.Engine) {
1616

1717
func handleCheck(c *gin.Context) {
1818
c.JSON(http.StatusOK, gin.H{
19-
"__united_token": true,
20-
"version": logic.GetVersion(),
19+
"__uni_token": true,
20+
"version": logic.GetVersion(),
2121
})
2222
}
2323

0 commit comments

Comments
 (0)