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, 0755); 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()
61 }
62
63 appsDir := filepath.Join(home, ".local", "share", "applications")
64 if err := os.MkdirAll(appsDir, 0755); 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
74 if err := exec.Command("update-desktop-database", appsDir).Run(); err != nil {
75 // Ignore error if command doesn't exist
76 }
77
78 // Try to set xdg-mime default
79 cmd := exec.Command("xdg-mime", "default", "matcha.desktop", "x-scheme-handler/mailto")
80 if err := cmd.Run(); err != nil {
81 return fmt.Errorf("failed to run xdg-mime: %w", err)
82 }
83
84 fmt.Printf("Successfully registered %s as default mail handler on Linux\n", exe)
85 return nil
86}
87
88func setupMailtoDarwin(exe string) error {
89 // For macOS, we need to create a tiny AppleScript/Swift app bundle to handle the URL event,
90 // because standard terminal programs can't easily register as URL handlers without an app bundle.
91
92 home, err := os.UserHomeDir()
93 if err != nil {
94 return err
95 }
96
97 appDir := filepath.Join(home, "Applications", "MatchaMail.app")
98 // Cleanup old version to avoid conflicts
99 os.RemoveAll(appDir)
100
101 contentsDir := filepath.Join(appDir, "Contents")
102 macosDir := filepath.Join(contentsDir, "MacOS")
103 resourcesDir := filepath.Join(contentsDir, "Resources")
104
105 if err := os.MkdirAll(macosDir, 0755); err != nil {
106 return err
107 }
108 if err := os.MkdirAll(resourcesDir, 0755); err != nil {
109 return err
110 }
111
112 // Generate .icns from embedded logo
113 tmpLogo := filepath.Join(os.TempDir(), "matcha_logo.png")
114 if err := os.WriteFile(tmpLogo, assets.Logo, 0644); err == nil {
115 icnsPath := filepath.Join(resourcesDir, "MatchaMail.icns")
116 _ = exec.Command("sips", "-s", "format", "icns", tmpLogo, "--out", icnsPath).Run()
117 os.Remove(tmpLogo)
118 }
119
120 infoPlist := `<?xml version="1.0" encoding="UTF-8"?>
121<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
122<plist version="1.0">
123<dict>
124 <key>CFBundleExecutable</key>
125 <string>MatchaMail</string>
126 <key>CFBundleIconFile</key>
127 <string>MatchaMail.icns</string>
128 <key>CFBundleIdentifier</key>
129 <string>com.floatpane.matcha.mailto-handler</string>
130 <key>CFBundleName</key>
131 <string>MatchaMail</string>
132 <key>CFBundlePackageType</key>
133 <string>APPL</string>
134 <key>CFBundleShortVersionString</key>
135 <string>1.1</string>
136 <key>CFBundleVersion</key>
137 <string>1</string>
138 <key>LSUIElement</key>
139 <true/>
140 <key>CFBundleURLTypes</key>
141 <array>
142 <dict>
143 <key>CFBundleURLName</key>
144 <string>Email Address</string>
145 <key>CFBundleURLSchemes</key>
146 <array>
147 <string>mailto</string>
148 </array>
149 <key>LSHandlerRank</key>
150 <string>Owner</string>
151 </dict>
152 </array>
153</dict>
154</plist>
155`
156 if err := os.WriteFile(filepath.Join(contentsDir, "Info.plist"), []byte(infoPlist), 0644); err != nil {
157 return err
158 }
159
160 // Swift source code to handle URL event and launch Terminal.app running matcha
161 swiftCode := strings.ReplaceAll(macosHandlerSwift, "{{MATCHA_PATH}}", exe)
162
163 tmpSwiftFile := filepath.Join(os.TempDir(), "matcha_handler.swift")
164 if err := os.WriteFile(tmpSwiftFile, []byte(swiftCode), 0644); err != nil {
165 return err
166 }
167 defer os.Remove(tmpSwiftFile)
168
169 exeDest := filepath.Join(macosDir, "MatchaMail")
170
171 // Compile the Swift file
172 cmd := exec.Command("swiftc", "-O", tmpSwiftFile, "-o", exeDest)
173 if err := cmd.Run(); err != nil {
174 return fmt.Errorf("failed to compile Swift handler app: %w", err)
175 }
176
177 // Register the application
178 lsregister := "/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/lsregister"
179 _ = exec.Command(lsregister, "-f", appDir).Run()
180
181 fmt.Printf("Successfully created %s.\n", appDir)
182
183 // Set as default handler
184 // macOS does not provide a straightforward CLI to change default handler without 3rd party tools (like duti).
185 // We'll instruct the user on how to do it or try our best.
186 // 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.
187
188 fmt.Printf("Successfully created %s.\n", appDir)
189 fmt.Println("To complete the setup on macOS:")
190 fmt.Println("1. Open Apple Mail.")
191 fmt.Println("2. Go to Mail -> Settings (or Preferences) -> General.")
192 fmt.Println("3. Select 'MatchaMail.app' from the 'Default email reader' dropdown.")
193
194 return nil
195}