diff --git a/.gitignore b/.gitignore
index 6717be6..51df94b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
.DS_Store
TurtleSilicon.*
TurtleSilicon.app/Contents/MacOS/turtlesilicon
-*.dmg
\ No newline at end of file
+*.dmg
+turtlesilicon
\ No newline at end of file
diff --git a/FyneApp.toml b/FyneApp.toml
index 528f447..1a87ce7 100644
--- a/FyneApp.toml
+++ b/FyneApp.toml
@@ -3,4 +3,4 @@
Name = "TurtleSilicon"
ID = "com.tairasu.turtlesilicon"
Version = "1.2.1"
- Build = 37
+ Build = 47
diff --git a/main.go b/main.go
index 35075f2..7d4c114 100644
--- a/main.go
+++ b/main.go
@@ -10,9 +10,6 @@ import (
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
- "fyne.io/fyne/v2/container"
- "fyne.io/fyne/v2/dialog"
- "fyne.io/fyne/v2/widget"
)
const appVersion = "1.2.1"
@@ -26,29 +23,28 @@ func main() {
// Check for updates
go func() {
prefs, _ := utils.LoadPrefs()
- latest, notes, update, err := utils.CheckForUpdate(appVersion)
- debug.Printf("DEBUG RAW: latest=%q", latest)
- latestVersion := strings.TrimLeft(latest, "v.")
- debug.Printf("DEBUG: appVersion=%q, latest=%q, latestVersion=%q, suppressed=%q, update=%v, err=%v\n",
- appVersion, latest, latestVersion, prefs.SuppressedUpdateVersion, update, err)
- // Always skip popup if versions match
- if latestVersion == appVersion {
+ updateInfo, updateAvailable, err := utils.CheckForUpdateWithAssets(appVersion)
+ if err != nil {
+ debug.Printf("Failed to check for updates: %v", err)
return
}
- if err == nil && update && prefs.SuppressedUpdateVersion != latestVersion {
- checkbox := widget.NewCheck("Do not show this anymore", func(bool) {})
- content := container.NewVBox(
- widget.NewLabel("A new version ("+latestVersion+") is available!"),
- widget.NewLabel("Release notes:\n\n"+notes),
- checkbox,
- )
- dialog.ShowCustomConfirm("Update Available", "OK", "Cancel", content, func(ok bool) {
- if checkbox.Checked {
- prefs.SuppressedUpdateVersion = latestVersion
- utils.SavePrefs(prefs)
- }
- }, TSWindow)
+
+ if !updateAvailable {
+ debug.Printf("No updates available")
+ return
}
+
+ latestVersion := strings.TrimPrefix(updateInfo.TagName, "v")
+ debug.Printf("Update available: current=%s, latest=%s", appVersion, latestVersion)
+
+ // Skip if user has suppressed this version
+ if prefs.SuppressedUpdateVersion == latestVersion {
+ debug.Printf("Update suppressed by user: %s", latestVersion)
+ return
+ }
+
+ // Show enhanced update dialog
+ ui.ShowUpdateDialog(updateInfo, appVersion, TSWindow)
}()
content := ui.CreateUI(TSWindow)
diff --git a/pkg/ui/updater.go b/pkg/ui/updater.go
new file mode 100644
index 0000000..d7955f2
--- /dev/null
+++ b/pkg/ui/updater.go
@@ -0,0 +1,187 @@
+package ui
+
+import (
+ "fmt"
+ "strings"
+ "time"
+
+ "turtlesilicon/pkg/debug"
+ "turtlesilicon/pkg/utils"
+
+ "fyne.io/fyne/v2"
+ "fyne.io/fyne/v2/container"
+ "fyne.io/fyne/v2/dialog"
+ "fyne.io/fyne/v2/widget"
+)
+
+// ShowUpdateDialog displays an enhanced update dialog with download and install options
+func ShowUpdateDialog(updateInfo *utils.UpdateInfo, currentVersion string, myWindow fyne.Window) {
+ latestVersion := strings.TrimPrefix(updateInfo.TagName, "v")
+
+ // Find the DMG asset
+ var dmgAsset *utils.Asset
+ for _, asset := range updateInfo.Assets {
+ if strings.HasSuffix(asset.Name, ".dmg") {
+ dmgAsset = &asset
+ break
+ }
+ }
+
+ if dmgAsset == nil {
+ dialog.ShowError(fmt.Errorf("no DMG file found in the latest release"), myWindow)
+ return
+ }
+
+ // Create content for the update dialog
+ titleLabel := widget.NewLabel(fmt.Sprintf("Update Available: v%s", latestVersion))
+ titleLabel.TextStyle = fyne.TextStyle{Bold: true}
+
+ currentVersionLabel := widget.NewLabel(fmt.Sprintf("Current version: v%s", currentVersion))
+
+ // Format file size
+ fileSize := formatFileSize(dmgAsset.Size)
+ fileSizeLabel := widget.NewLabel(fmt.Sprintf("Download size: %s", fileSize))
+
+ // Release notes
+ notesLabel := widget.NewLabel("Release notes:")
+ notesLabel.TextStyle = fyne.TextStyle{Bold: true}
+ notesText := widget.NewRichTextFromMarkdown(updateInfo.Body)
+ notesScroll := container.NewScroll(notesText)
+ notesScroll.SetMinSize(fyne.NewSize(480, 120))
+
+ // Progress bar (initially hidden)
+ progressBar := widget.NewProgressBar()
+ progressBar.Hide()
+
+ progressLabel := widget.NewLabel("")
+ progressLabel.Hide()
+
+ // Checkbox for suppressing this version
+ suppressCheck := widget.NewCheck("Don't show this update again", nil)
+
+ content := container.NewVBox(
+ titleLabel,
+ currentVersionLabel,
+ fileSizeLabel,
+ widget.NewSeparator(),
+ notesLabel,
+ notesScroll,
+ widget.NewSeparator(),
+ progressBar,
+ progressLabel,
+ suppressCheck,
+ )
+
+ // Create custom dialog
+ d := dialog.NewCustom("New Update Available", "", content, myWindow)
+ d.Resize(fyne.NewSize(550, 400))
+
+ // Download and install function
+ downloadAndInstall := func() {
+ // Show progress elements
+ progressBar.Show()
+ progressLabel.Show()
+ progressLabel.SetText("Starting download...")
+
+ // Disable dialog closing during download
+ d.SetButtons([]fyne.CanvasObject{})
+
+ go func() {
+ // Download with progress
+ downloadPath, err := utils.DownloadUpdate(dmgAsset.BrowserDownloadURL, func(downloaded, total int64) {
+ // Update progress on UI thread
+ fyne.NewAnimation(
+ time.Millisecond*50,
+ func(float32) {
+ if total > 0 {
+ progress := float64(downloaded) / float64(total)
+ progressBar.SetValue(progress)
+ progressLabel.SetText(fmt.Sprintf("Downloaded: %s / %s (%.1f%%)",
+ formatFileSize(downloaded), formatFileSize(total), progress*100))
+ }
+ },
+ ).Curve = fyne.AnimationLinear
+ })
+
+ if err != nil {
+ progressLabel.SetText(fmt.Sprintf("Download failed: %v", err))
+ debug.Printf("Download failed: %v", err)
+
+ // Re-enable close button
+ d.SetButtons([]fyne.CanvasObject{
+ widget.NewButton("Close", func() { d.Hide() }),
+ })
+ return
+ }
+
+ progressLabel.SetText("Installing update...")
+ progressBar.SetValue(1.0)
+
+ // Install update
+ err = utils.InstallUpdate(downloadPath)
+ if err != nil {
+ progressLabel.SetText(fmt.Sprintf("Installation failed: %v", err))
+ debug.Printf("Installation failed: %v", err)
+
+ // Re-enable close button
+ d.SetButtons([]fyne.CanvasObject{
+ widget.NewButton("Close", func() { d.Hide() }),
+ })
+ return
+ }
+
+ // Success - show restart dialog
+ progressLabel.SetText("Update installed successfully!")
+
+ restartDialog := dialog.NewConfirm(
+ "Update Complete",
+ "The update has been installed successfully and will require a restart. Would you like to close the application now?",
+ func(restart bool) {
+ d.Hide()
+ if restart {
+ utils.RestartApp()
+ fyne.CurrentApp().Quit()
+ }
+ },
+ myWindow,
+ )
+ restartDialog.Show()
+ }()
+ }
+
+ // Set dialog buttons
+ d.SetButtons([]fyne.CanvasObject{
+ widget.NewButton("Download & Install", downloadAndInstall),
+ widget.NewButton("Later", func() {
+ if suppressCheck.Checked {
+ // Save suppressed version
+ prefs, _ := utils.LoadPrefs()
+ prefs.SuppressedUpdateVersion = latestVersion
+ utils.SavePrefs(prefs)
+ }
+ d.Hide()
+ }),
+ })
+
+ d.Show()
+}
+
+// formatFileSize formats a file size in bytes to a human-readable string
+func formatFileSize(bytes int64) string {
+ const (
+ KB = 1024
+ MB = KB * 1024
+ GB = MB * 1024
+ )
+
+ switch {
+ case bytes >= GB:
+ return fmt.Sprintf("%.1f GB", float64(bytes)/float64(GB))
+ case bytes >= MB:
+ return fmt.Sprintf("%.1f MB", float64(bytes)/float64(MB))
+ case bytes >= KB:
+ return fmt.Sprintf("%.1f KB", float64(bytes)/float64(KB))
+ default:
+ return fmt.Sprintf("%d bytes", bytes)
+ }
+}
diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go
index 2321287..c07b809 100644
--- a/pkg/utils/utils.go
+++ b/pkg/utils/utils.go
@@ -17,6 +17,14 @@ import (
"fyne.io/fyne/v2/dialog"
)
+// min returns the smaller of two integers
+func min(a, b int) int {
+ if a < b {
+ return a
+ }
+ return b
+}
+
// PathExists checks if a path exists.
func PathExists(path string) bool {
_, err := os.Stat(path)
@@ -140,36 +148,430 @@ func CheckForUpdate(currentVersion string) (latestVersion, releaseNotes string,
return latest, data.Body, latest != currentVersion, nil
}
-// GetBundledResourceSize returns the size of a bundled resource
-func GetBundledResourceSize(resourcePath string) (int64, error) {
- resource, err := fyne.LoadResourceFromPath(resourcePath)
- if err != nil {
- return 0, fmt.Errorf("failed to load bundled resource %s: %v", resourcePath, err)
- }
- return int64(len(resource.Content())), nil
+// UpdateInfo contains information about the latest release
+type UpdateInfo struct {
+ TagName string `json:"tag_name"`
+ Body string `json:"body"`
+ Assets []Asset `json:"assets"`
}
-// CompareFileWithBundledResource compares the size of a file with a bundled resource
-func CompareFileWithBundledResource(filePath, resourcePath string) bool {
+type Asset struct {
+ Name string `json:"name"`
+ BrowserDownloadURL string `json:"browser_download_url"`
+ Size int64 `json:"size"`
+}
+
+// CheckForUpdateWithAssets returns update information including download assets
+func CheckForUpdateWithAssets(currentVersion string) (*UpdateInfo, bool, error) {
+ resp, err := http.Get("https://api.github.com/repos/tairasu/TurtleSilicon/releases/latest")
+ if err != nil {
+ return nil, false, err
+ }
+ defer resp.Body.Close()
+
+ // Check for HTTP errors
+ if resp.StatusCode != 200 {
+ body, _ := io.ReadAll(resp.Body)
+ return nil, false, fmt.Errorf("GitHub API returned status %d: %s", resp.StatusCode, string(body))
+ }
+
+ // Read the response body first to check content
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return nil, false, fmt.Errorf("failed to read response body: %v", err)
+ }
+
+ // Check if response looks like HTML (rate limiting or other errors)
+ bodyStr := string(body)
+ if strings.Contains(bodyStr, "") || strings.Contains(bodyStr, " 0 {
+ _, writeErr := tempFile.Write(buffer[:n])
+ if writeErr != nil {
+ os.Remove(tempFile.Name())
+ return "", fmt.Errorf("failed to write update file: %v", writeErr)
+ }
+ downloaded += int64(n)
+ if progressCallback != nil {
+ progressCallback(downloaded, totalSize)
+ }
+ }
+ if err == io.EOF {
+ break
+ }
+ if err != nil {
+ os.Remove(tempFile.Name())
+ return "", fmt.Errorf("failed to read update data: %v", err)
+ }
+ }
+
+ return tempFile.Name(), nil
+}
+
+// InstallUpdate installs the downloaded update by mounting the DMG and replacing the current app
+func InstallUpdate(dmgPath string) error {
+ // Get current app path
+ execPath, err := os.Executable()
+ if err != nil {
+ return fmt.Errorf("failed to get executable path: %v", err)
+ }
+
+ // Navigate up to find the .app bundle
+ currentAppPath := execPath
+ for !strings.HasSuffix(currentAppPath, ".app") && currentAppPath != "/" {
+ currentAppPath = filepath.Dir(currentAppPath)
+ }
+
+ if !strings.HasSuffix(currentAppPath, ".app") {
+ return fmt.Errorf("could not find app bundle path")
+ }
+
+ // Mount the DMG and parse the mount point from plist output
+ debug.Printf("Mounting DMG: %s", dmgPath)
+ mountCmd := exec.Command("hdiutil", "attach", dmgPath, "-nobrowse", "-plist")
+ mountOutput, err := mountCmd.CombinedOutput()
+ if err != nil {
+ return fmt.Errorf("failed to mount DMG: %v, output: %s", err, string(mountOutput))
+ }
+
+ debug.Printf("Mount output: %s", string(mountOutput))
+
+ mountPoint := ""
+
+ // Parse the plist XML output to find mount points
+ outputStr := string(mountOutput)
+
+ // Look for mount-point entries in the XML
+ // The plist contains mount-point entries that show where volumes are mounted
+ lines := strings.Split(outputStr, "\n")
+ for i, line := range lines {
+ line = strings.TrimSpace(line)
+ if strings.Contains(line, "mount-point") && i+1 < len(lines) {
+ // The next line should contain the mount point path
+ nextLine := strings.TrimSpace(lines[i+1])
+ if strings.HasPrefix(nextLine, "/Volumes/") {
+ // Extract the path from /Volumes/...
+ start := strings.Index(nextLine, "") + 8
+ end := strings.Index(nextLine, "")
+ if start >= 8 && end > start {
+ mountPoint = nextLine[start:end]
+ debug.Printf("Found mount point in plist: %s", mountPoint)
+ break
+ }
+ }
+ }
+ }
+
+ // Fallback: try without -plist flag for simpler output
+ if mountPoint == "" {
+ debug.Printf("Plist parsing failed, trying simple mount")
+ // Unmount first if something was mounted
+ exec.Command("hdiutil", "detach", dmgPath, "-force").Run()
+
+ // Try mounting without plist
+ simpleMountCmd := exec.Command("hdiutil", "attach", dmgPath, "-nobrowse")
+ simpleOutput, simpleErr := simpleMountCmd.CombinedOutput()
+ if simpleErr != nil {
+ return fmt.Errorf("failed to mount DMG (simple): %v, output: %s", simpleErr, string(simpleOutput))
+ }
+
+ // Parse simple output
+ simpleLines := strings.Split(string(simpleOutput), "\n")
+ for _, line := range simpleLines {
+ line = strings.TrimSpace(line)
+ if strings.Contains(line, "/Volumes/") {
+ parts := strings.Fields(line)
+ for i := len(parts) - 1; i >= 0; i-- {
+ if strings.HasPrefix(parts[i], "/Volumes/") {
+ mountPoint = parts[i]
+ debug.Printf("Found mount point in simple output: %s", mountPoint)
+ break
+ }
+ }
+ if mountPoint != "" {
+ break
+ }
+ }
+ }
+ }
+
+ if mountPoint == "" {
+ return fmt.Errorf("could not find mount point. Mount output: %s", string(mountOutput))
+ }
+
+ debug.Printf("Using mount point: %s", mountPoint)
+
+ defer func() {
+ // Unmount the DMG
+ debug.Printf("Unmounting DMG from: %s", mountPoint)
+ unmountCmd := exec.Command("hdiutil", "detach", mountPoint, "-force")
+ unmountCmd.Run()
+ }()
+
+ // Find the app in the mounted DMG - search for any .app bundle
+ var newAppPath string
+
+ // First, try the exact name
+ exactPath := filepath.Join(mountPoint, "TurtleSilicon.app")
+ if PathExists(exactPath) {
+ newAppPath = exactPath
+ } else {
+ // Search for any .app bundle in the mount point
+ debug.Printf("TurtleSilicon.app not found at exact path, searching for .app bundles")
+ entries, err := os.ReadDir(mountPoint)
+ if err != nil {
+ return fmt.Errorf("failed to read DMG contents: %v", err)
+ }
+
+ for _, entry := range entries {
+ if entry.IsDir() && strings.HasSuffix(entry.Name(), ".app") {
+ candidatePath := filepath.Join(mountPoint, entry.Name())
+ debug.Printf("Found .app bundle: %s", candidatePath)
+ newAppPath = candidatePath
+ break
+ }
+ }
+ }
+
+ if newAppPath == "" {
+ return fmt.Errorf("no .app bundle found in DMG at %s", mountPoint)
+ }
+
+ debug.Printf("Found app to install: %s", newAppPath)
+
+ // Create backup of current app
+ backupPath := currentAppPath + ".backup"
+ debug.Printf("Creating backup: %s -> %s", currentAppPath, backupPath)
+
+ // Remove old backup if it exists
+ if PathExists(backupPath) {
+ os.RemoveAll(backupPath)
+ }
+
+ if err := CopyDir(currentAppPath, backupPath); err != nil {
+ return fmt.Errorf("failed to create backup: %v", err)
+ }
+
+ // Remove current app
+ debug.Printf("Removing current app: %s", currentAppPath)
+ if err := os.RemoveAll(currentAppPath); err != nil {
+ return fmt.Errorf("failed to remove current app: %v", err)
+ }
+
+ // Copy new app
+ debug.Printf("Installing new app: %s -> %s", newAppPath, currentAppPath)
+ if err := CopyDir(newAppPath, currentAppPath); err != nil {
+ // Try to restore backup on failure
+ debug.Printf("Installation failed, restoring backup")
+ os.RemoveAll(currentAppPath)
+ CopyDir(backupPath, currentAppPath)
+ return fmt.Errorf("failed to install new app: %v", err)
+ }
+
+ // Fix executable permissions for the main binary
+ executablePath := filepath.Join(currentAppPath, "Contents", "MacOS", "turtlesilicon")
+ if PathExists(executablePath) {
+ debug.Printf("Setting executable permissions for: %s", executablePath)
+ if err := os.Chmod(executablePath, 0755); err != nil {
+ debug.Printf("Warning: failed to set executable permissions: %v", err)
+ // Don't fail the entire update for this, but log it
+ }
+ } else {
+ debug.Printf("Warning: executable not found at expected path: %s", executablePath)
+ }
+
+ // Remove backup on success
+ os.RemoveAll(backupPath)
+
+ debug.Printf("Update installed successfully")
+ return nil
+}
+
+// TestDMGMount tests DMG mounting and returns mount point and app path for debugging
+func TestDMGMount(dmgPath string) (string, string, error) {
+ debug.Printf("Testing DMG mount: %s", dmgPath)
+
+ // Mount the DMG with verbose output to better parse mount point
+ mountCmd := exec.Command("hdiutil", "attach", dmgPath, "-nobrowse", "-plist")
+ mountOutput, err := mountCmd.CombinedOutput()
+ if err != nil {
+ return "", "", fmt.Errorf("failed to mount DMG: %v, output: %s", err, string(mountOutput))
+ }
+
+ debug.Printf("Mount output: %s", string(mountOutput))
+
+ // Parse mount output to get mount point
+ mountPoint := ""
+
+ // First try: look for /Volumes/ in the output lines
+ lines := strings.Split(string(mountOutput), "\n")
+ for _, line := range lines {
+ line = strings.TrimSpace(line)
+ if strings.Contains(line, "/Volumes/") {
+ parts := strings.Fields(line)
+ for i := len(parts) - 1; i >= 0; i-- {
+ if strings.HasPrefix(parts[i], "/Volumes/") {
+ mountPoint = parts[i]
+ break
+ }
+ }
+ if mountPoint != "" {
+ break
+ }
+ }
+ }
+
+ // Second try: use hdiutil info to get mount points if first method failed
+ if mountPoint == "" {
+ debug.Printf("First mount point detection failed, trying hdiutil info")
+ infoCmd := exec.Command("hdiutil", "info", "-plist")
+ infoOutput, infoErr := infoCmd.CombinedOutput()
+ if infoErr == nil {
+ infoLines := strings.Split(string(infoOutput), "\n")
+ for _, line := range infoLines {
+ if strings.Contains(line, "/Volumes/") && strings.Contains(line, "TurtleSilicon") {
+ line = strings.TrimSpace(line)
+ if strings.HasPrefix(line, "/Volumes/") {
+ mountPoint = line
+ break
+ }
+ }
+ }
+ }
+ }
+
+ if mountPoint == "" {
+ return "", "", fmt.Errorf("could not find mount point. Mount output: %s", string(mountOutput))
+ }
+
+ debug.Printf("Using mount point: %s", mountPoint)
+
+ // Find the app in the mounted DMG
+ var newAppPath string
+
+ // First, try the exact name
+ exactPath := filepath.Join(mountPoint, "TurtleSilicon.app")
+ if PathExists(exactPath) {
+ newAppPath = exactPath
+ } else {
+ // Search for any .app bundle in the mount point
+ debug.Printf("TurtleSilicon.app not found at exact path, searching for .app bundles")
+ entries, err := os.ReadDir(mountPoint)
+ if err != nil {
+ // Unmount before returning error
+ exec.Command("hdiutil", "detach", mountPoint, "-force").Run()
+ return "", "", fmt.Errorf("failed to read DMG contents: %v", err)
+ }
+
+ for _, entry := range entries {
+ if entry.IsDir() && strings.HasSuffix(entry.Name(), ".app") {
+ candidatePath := filepath.Join(mountPoint, entry.Name())
+ debug.Printf("Found .app bundle: %s", candidatePath)
+ newAppPath = candidatePath
+ break
+ }
+ }
+ }
+
+ // Unmount after testing
+ debug.Printf("Unmounting test DMG from: %s", mountPoint)
+ exec.Command("hdiutil", "detach", mountPoint, "-force").Run()
+
+ if newAppPath == "" {
+ return mountPoint, "", fmt.Errorf("no .app bundle found in DMG at %s", mountPoint)
+ }
+
+ return mountPoint, newAppPath, nil
+}
+
+// CompareFileWithBundledResource compares the size of a file on disk with a bundled resource
+func CompareFileWithBundledResource(filePath, resourceName string) bool {
+ // Check if the file exists first
if !PathExists(filePath) {
return false
}
- // Get file size
+ // Get file info for the existing file
fileInfo, err := os.Stat(filePath)
if err != nil {
- debug.Printf("Failed to get file info for %s: %v", filePath, err)
+ debug.Printf("Failed to stat file %s: %v", filePath, err)
return false
}
- fileSize := fileInfo.Size()
- // Get bundled resource size
- resourceSize, err := GetBundledResourceSize(resourcePath)
+ // Load the bundled resource
+ resource, err := fyne.LoadResourceFromPath(resourceName)
if err != nil {
- debug.Printf("Failed to get bundled resource size for %s: %v", resourcePath, err)
+ debug.Printf("Failed to load bundled resource %s: %v", resourceName, err)
return false
}
- debug.Printf("Comparing file sizes: %s (%d bytes) vs %s (%d bytes)", filePath, fileSize, resourcePath, resourceSize)
+ // Compare sizes
+ fileSize := fileInfo.Size()
+ resourceSize := int64(len(resource.Content()))
+
+ debug.Printf("Comparing file sizes - %s: %d bytes vs bundled %s: %d bytes",
+ filePath, fileSize, resourceName, resourceSize)
+
return fileSize == resourceSize
}
+
+// RestartApp restarts the application
+func RestartApp() error {
+ execPath, err := os.Executable()
+ if err != nil {
+ return fmt.Errorf("failed to get executable path: %v", err)
+ }
+
+ // Find the .app bundle
+ appPath := execPath
+ for !strings.HasSuffix(appPath, ".app") && appPath != "/" {
+ appPath = filepath.Dir(appPath)
+ }
+
+ if strings.HasSuffix(appPath, ".app") {
+ // Launch the app bundle
+ cmd := exec.Command("open", appPath)
+ return cmd.Start()
+ } else {
+ // Launch the executable directly
+ cmd := exec.Command(execPath)
+ return cmd.Start()
+ }
+}
diff --git a/turtlesilicon b/turtlesilicon
deleted file mode 100755
index cb42c00..0000000
Binary files a/turtlesilicon and /dev/null differ