1package cli
2
3import (
4 _ "embed"
5 "fmt"
6 "os"
7 "os/exec"
8 "path/filepath"
9 "runtime"
10 "strings"
11
12 "github.com/floatpane/matcha/assets"
13)
14
15//go:embed macos_handler.swift
16var macosHandlerSwift string
17
18// SetupMailto registers matcha as the default handler for mailto: links.
19func SetupMailto() error {
20 exe, err := os.Executable()
21 if err != nil {
22 return fmt.Errorf("could not find executable: %w", err)
23 }
24 exe, err = filepath.Abs(exe)
25 if err != nil {
26 return fmt.Errorf("could not resolve absolute path: %w", err)
27 }
28
29 switch runtime.GOOS {
30 case "linux":
31 return setupMailtoLinux(exe)
32 case "darwin":
33 return setupMailtoDarwin(exe)
34 default:
35 return fmt.Errorf("unsupported operating system: %s", runtime.GOOS)
36 }
37}
38
39func setupMailtoLinux(exe string) error {
40 desktopContent := fmt.Sprintf(`[Desktop Entry]
41Name=Matcha Email
42Comment=Terminal-based email client
43Exec=%s %%u
44Terminal=true
45Type=Application
46Icon=matcha
47Categories=Network;Email;
48MimeType=x-scheme-handler/mailto;
49`, exe)
50
51 home, err := os.UserHomeDir()
52 if err != nil {
53 return err
54 }
55
56 iconsDir := filepath.Join(home, ".local", "share", "icons", "hicolor", "512x512", "apps")
57 if err := os.MkdirAll(iconsDir, 0750); err == nil {
58 iconFile := filepath.Join(iconsDir, "matcha.png")
59 _ = os.WriteFile(iconFile, assets.Logo, 0644)
60 _ = exec.Command("gtk-update-icon-cache", filepath.Join(home, ".local", "share", "icons", "hicolor")).Run() //nolint:noctx
61 }
62
63 appsDir := filepath.Join(home, ".local", "share", "applications")
64 if err := os.MkdirAll(appsDir, 0750); err != nil {
65 return err
66 }
67
68 desktopFile := filepath.Join(appsDir, "matcha.desktop")
69 if err := os.WriteFile(desktopFile, []byte(desktopContent), 0644); err != nil {
70 return err
71 }
72
73 // Update desktop database (ignore error if command doesn't exist)
74 _ = exec.Command("update-desktop-database", appsDir).Run() //nolint:noctx
75
76 // Try to set xdg-mime default
77 cmd := exec.Command("xdg-mime", "default", "matcha.desktop", "x-scheme-handler/mailto") //nolint:noctx
78 if err := cmd.Run(); err != nil {
79 return fmt.Errorf("failed to run xdg-mime: %w", err)
80 }
81
82 fmt.Printf("Successfully registered %s as default mail handler on Linux\n", exe)
83 return nil
84}
85
86func setupMailtoDarwin(exe string) error {
87 // For macOS, we need to create a tiny AppleScript/Swift app bundle to handle the URL event,
88 // because standard terminal programs can't easily register as URL handlers without an app bundle.
89
90 home, err := os.UserHomeDir()
91 if err != nil {
92 return err
93 }
94
95 appDir := filepath.Join(home, "Applications", "MatchaMail.app")
96 // Cleanup old version to avoid conflicts
97 os.RemoveAll(appDir) //nolint:errcheck,gosec
98
99 contentsDir := filepath.Join(appDir, "Contents")
100 macosDir := filepath.Join(contentsDir, "MacOS")
101 resourcesDir := filepath.Join(contentsDir, "Resources")
102
103 if err := os.MkdirAll(macosDir, 0750); err != nil {
104 return err
105 }
106 if err := os.MkdirAll(resourcesDir, 0750); err != nil {
107 return err
108 }
109
110 // Generate .icns from embedded logo
111 tmpLogo := filepath.Join(os.TempDir(), "matcha_logo.png")
112 if err := os.WriteFile(tmpLogo, assets.Logo, 0644); err == nil {
113 icnsPath := filepath.Join(resourcesDir, "MatchaMail.icns")
114 _ = exec.Command("sips", "-s", "format", "icns", tmpLogo, "--out", icnsPath).Run() //nolint:noctx
115 os.Remove(tmpLogo) //nolint:errcheck,gosec
116 }
117
118 infoPlist := `<?xml version="1.0" encoding="UTF-8"?>
119<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
120<plist version="1.0">
121<dict>
122 <key>CFBundleExecutable</key>
123 <string>MatchaMail</string>
124 <key>CFBundleIconFile</key>
125 <string>MatchaMail.icns</string>
126 <key>CFBundleIdentifier</key>
127 <string>com.floatpane.matcha.mailto-handler</string>
128 <key>CFBundleName</key>
129 <string>MatchaMail</string>
130 <key>CFBundlePackageType</key>
131 <string>APPL</string>
132 <key>CFBundleShortVersionString</key>
133 <string>1.1</string>
134 <key>CFBundleVersion</key>
135 <string>1</string>
136 <key>LSUIElement</key>
137 <true/>
138 <key>CFBundleURLTypes</key>
139 <array>
140 <dict>
141 <key>CFBundleURLName</key>
142 <string>Email Address</string>
143 <key>CFBundleURLSchemes</key>
144 <array>
145 <string>mailto</string>
146 </array>
147 <key>LSHandlerRank</key>
148 <string>Owner</string>
149 </dict>
150 </array>
151</dict>
152</plist>
153`
154 if err := os.WriteFile(filepath.Join(contentsDir, "Info.plist"), []byte(infoPlist), 0644); err != nil {
155 return err
156 }
157
158 // Swift source code to handle URL event and launch Terminal.app running matcha
159 swiftCode := strings.ReplaceAll(macosHandlerSwift, "{{MATCHA_PATH}}", exe)
160
161 tmpSwiftFile := filepath.Join(os.TempDir(), "matcha_handler.swift")
162 if err := os.WriteFile(tmpSwiftFile, []byte(swiftCode), 0644); err != nil {
163 return err
164 }
165 defer os.Remove(tmpSwiftFile) //nolint:errcheck
166
167 exeDest := filepath.Join(macosDir, "MatchaMail")
168
169 // Compile the Swift file
170 cmd := exec.Command("swiftc", "-O", tmpSwiftFile, "-o", exeDest) //nolint:noctx
171 if err := cmd.Run(); err != nil {
172 return fmt.Errorf("failed to compile Swift handler app: %w", err)
173 }
174
175 // Register the application
176 lsregister := "/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/lsregister"
177 _ = exec.Command(lsregister, "-f", appDir).Run() //nolint:noctx
178
179 fmt.Printf("Successfully created %s.\n", appDir)
180
181 // Set as default handler
182 // macOS does not provide a straightforward CLI to change default handler without 3rd party tools (like duti).
183 // We'll instruct the user on how to do it or try our best.
184 // Actually, starting from macOS 12, there's no native Apple command for it. But registering it usually makes it show up in Apple Mail -> Preferences -> Default email reader.
185
186 fmt.Printf("Successfully created %s.\n", appDir)
187 fmt.Println("To complete the setup on macOS:")
188 fmt.Println("1. Open Apple Mail.")
189 fmt.Println("2. Go to Mail -> Settings (or Preferences) -> General.")
190 fmt.Println("3. Select 'MatchaMail.app' from the 'Default email reader' dropdown.")
191
192 return nil
193}