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, 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}