diff --git a/icons.go b/icons.go index fda7481f..0e44d08b 100644 --- a/icons.go +++ b/icons.go @@ -10,6 +10,13 @@ type AppData interface { Categories() []string // Categories is a list of categories that the app fits in (platform specific) Hidden() bool // Hidden specifies whether instances of this app should be hidden Icon(theme string, size int) fyne.Resource // Icon returns an icon for the app in the requested theme and size + + Source() *AppSource // Source will return the location of the app source code from metadata, if known +} + +// AppSource represents the source code informtion of an application +type AppSource struct { + Repo, Dir string } // ApplicationProvider describes a type that can locate icons and applications for the current system diff --git a/internal/icon/fdo.go b/internal/icon/fdo.go index 66edeb49..4c4cfe15 100644 --- a/internal/icon/fdo.go +++ b/internal/icon/fdo.go @@ -34,6 +34,8 @@ type fdoApplicationData struct { categories []string hide bool iconCache fyne.Resource + + source *fynedesk.AppSource } // Name returns the name associated with an fdo app @@ -55,7 +57,7 @@ func (data *fdoApplicationData) IconName() string { return data.iconName } -// IconPath returns the path of the icon that an fdo app wishes to use +// Icon returns the path of the icon that an fdo app wishes to use func (data *fdoApplicationData) Icon(theme string, size int) fyne.Resource { if data.iconCache != nil { return data.iconCache @@ -73,6 +75,10 @@ func (data *fdoApplicationData) Icon(theme string, size int) fyne.Resource { return data.iconCache } +func (data *fdoApplicationData) Source() *fynedesk.AppSource { + return data.source +} + // extractArgs sanitises argument parameters from an Exec configuration func extractArgs(args []string) []string { var ret []string @@ -106,7 +112,7 @@ func (data *fdoApplicationData) Run(env []string) error { return cmd.Start() } -func (data fdoApplicationData) mainCategory() string { +func (data *fdoApplicationData) mainCategory() string { if len(data.Categories()) == 0 { return fallbackCategory } @@ -457,7 +463,21 @@ func newFdoIconData(desktopPath string) fynedesk.AppData { if strings.HasPrefix(line, "[") { currentSection = line } - if currentSection != "[Desktop Entry]" { + switch currentSection { + case "[X-Fyne Source]": + if fdoApp.source == nil { + fdoApp.source = &fynedesk.AppSource{} + } + if strings.HasPrefix(line, "Repo=") { + name := strings.SplitAfter(line, "=") + fdoApp.source.Repo = name[1] + } + if strings.HasPrefix(line, "Dir=") { + name := strings.SplitAfter(line, "=") + fdoApp.source.Dir = name[1] + } + case "[Desktop Entry]": // fall through to code below + default: continue } if strings.HasPrefix(line, "Name=") { diff --git a/internal/icon/fdo_test.go b/internal/icon/fdo_test.go index f785bd3f..163e3bda 100644 --- a/internal/icon/fdo_test.go +++ b/internal/icon/fdo_test.go @@ -102,6 +102,16 @@ func TestFdoIconHicolorFallbackScalable(t *testing.T) { assert.Equal(t, true, exists(data)) } +// applications/com.fyne.app.desktop +func TestFdoIconSource(t *testing.T) { + setTestEnv(t) + data := NewFDOIconProvider().(*fdoIconProvider).lookupApplication("app2") + + assert.NotNil(t, data.Source()) + assert.Equal(t, "https://example.com/repo", data.Source().Repo) + assert.Equal(t, "cmd/dir", data.Source().Dir) +} + // applications/app7.desktop and icons/default_theme/apps/16x16/app7.png func TestFdoLookupDefaultThemeDifferentSize(t *testing.T) { setTestEnv(t) diff --git a/internal/icon/macos.go b/internal/icon/macos.go index 9202910a..be946037 100644 --- a/internal/icon/macos.go +++ b/internal/icon/macos.go @@ -76,6 +76,10 @@ func (m *macOSAppBundle) Run([]string) error { return exec.Command("open", "-a", m.runPath).Start() } +func (m *macOSAppBundle) Source() *fynedesk.AppSource { + return nil +} + func loadAppBundle(name, path, category string) fynedesk.AppData { buf, err := os.Open(filepath.Join(path, "Contents", "Info.plist")) if err != nil { diff --git a/internal/icon/testdata/applications/com.fyne.app.desktop b/internal/icon/testdata/applications/com.fyne.app.desktop index 19aaef8f..28cb79f8 100644 --- a/internal/icon/testdata/applications/com.fyne.app.desktop +++ b/internal/icon/testdata/applications/com.fyne.app.desktop @@ -2,3 +2,7 @@ Name=App2 Exec=app2 Icon=app2 + +[X-Fyne Source] +Repo=https://example.com/repo +Dir=cmd/dir diff --git a/internal/ui/bar_test.go b/internal/ui/bar_test.go index 5dfad73b..a334710c 100644 --- a/internal/ui/bar_test.go +++ b/internal/ui/bar_test.go @@ -4,6 +4,8 @@ import ( "image/color" "testing" + "fyshos.com/fynedesk" + "fyne.io/fyne/v2" "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/test" @@ -40,6 +42,11 @@ func (d *dummyIcon) Run([]string) error { return nil } +func (d *dummyIcon) Source() *fynedesk.AppSource { + // no-op + return nil +} + func testBar(icons []string) *bar { testBar := newBar(wmTest.NewDesktopWithWM(&embededWM{})) testBar.children = []fyne.CanvasObject{} // remove divider, then we add it again later diff --git a/internal/ui/baricon.go b/internal/ui/baricon.go index 94f8c852..6d03b042 100644 --- a/internal/ui/baricon.go +++ b/internal/ui/baricon.go @@ -2,9 +2,15 @@ package ui import ( "image/color" + "os" + "os/exec" + "os/user" + "path/filepath" "fyne.io/fyne/v2" "fyne.io/fyne/v2/canvas" + "fyne.io/fyne/v2/container" + deskDriver "fyne.io/fyne/v2/driver/desktop" "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" @@ -143,6 +149,13 @@ func (bi *barIcon) TappedSecondary(ev *fyne.PointEvent) { } items := []*fyne.MenuItem{addRemove} + editor := editorPath() + if app.Source() != nil && editor != "" { + items = append(items, fyne.NewMenuItem("Edit", func() { + editApp(app, editor) + })) + } + fynedesk.Instance().ShowMenuAt(fyne.NewMenu("", items...), ev.AbsolutePosition) } @@ -154,9 +167,81 @@ func (bi *barIcon) CreateRenderer() fyne.WidgetRenderer { return render } +func cloneRepo(src *fynedesk.AppSource, path string) error { + spin := widget.NewActivity() + prop := canvas.NewRectangle(color.Transparent) + prop.SetMinSize(fyne.NewSquareSize(56)) + + w := fyne.CurrentApp().Driver().(deskDriver.Driver).CreateSplashWindow() + w.SetContent( + container.NewBorder(nil, widget.NewLabel("Downloading..."), nil, nil, + container.NewStack(prop, spin))) + spin.Start() + w.Show() + + defer func() { + w.Hide() + spin.Stop() + }() + + cmd := exec.Command("git", "clone", src.Repo, path) + return cmd.Run() +} + +func editApp(app fynedesk.AppData, editor string) { + root := sourceRoot() + srcDir := filepath.Join(root, app.Name()) + + if !exists(srcDir) { + if !exists(root) { + err := os.MkdirAll(root, 0755) + if err != nil { + fyne.LogError("Failed to make source root", err) + return + } + } + + err := cloneRepo(app.Source(), srcDir) + if err != nil { + fyne.LogError("Error cloning the app source", err) + return + } + } + + cmd := exec.Command(editor, srcDir) + err := cmd.Start() + + if err != nil { + fyne.LogError("Failed to start app editor: "+editor, err) + } +} + func newBarIcon(res fyne.Resource, appData fynedesk.AppData, winData *appWindow) *barIcon { barIcon := &barIcon{resource: res, appData: appData, windowData: winData} barIcon.ExtendBaseWidget(barIcon) return barIcon } + +func editorPath() string { + fysion, err := exec.LookPath("fysion") + if err == nil && fysion != "" { + return fysion + } + + return "" +} + +func exists(path string) bool { + _, err := os.Stat(path) + return err == nil +} + +func sourceRoot() string { + u, err := user.Current() + if err != nil { + return "" + } + + return filepath.Join(u.HomeDir, "FysionApps") +} diff --git a/modules/status/network.go b/modules/status/network.go index 7019fc06..51876a7b 100644 --- a/modules/status/network.go +++ b/modules/status/network.go @@ -184,3 +184,7 @@ func (n *networkApp) Hidden() bool { func (n *networkApp) Icon(theme string, size int) fyne.Resource { return wmtheme.WifiIcon } + +func (n *networkApp) Source() *fynedesk.AppSource { + return nil +} diff --git a/test/icons.go b/test/icons.go index 4f8deee7..9c59c8c6 100644 --- a/test/icons.go +++ b/test/icons.go @@ -44,6 +44,10 @@ func (tad *testAppData) Icon(theme string, size int) fyne.Resource { return wmTheme.IconifyIcon } +func (tad *testAppData) Source() *fynedesk.AppSource { + return nil +} + type testAppProvider struct { apps []fynedesk.AppData }