integration.go

  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}