LazyRun v1.0.0: shortcut maker untuk executable dan PWA
- Fungsi A: shortcut dari file executable (ELF/AppImage/script) - Fungsi B: shortcut PWA/URL dengan deteksi browser (native, /opt, Flatpak) - Checkbox Menu Aplikasi / Desktop, desktop-only pakai NoDisplay=true - List + uninstall + deteksi shortcut yang file .desktop-nya hilang - Metadata ~/.config/lazyrun/shortcuts.json, refresh cache XDG/KDE - AppImage + zsync update info via build.sh
This commit is contained in:
1 parent
9cd5a1f82d
commit
da31f7d7d7
31 files changed
+2916
No files matched your search
@@ -0,0 +1,213 @@
|
||||
package shortcut
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"lazyrun/internal/icon"
|
||||
"lazyrun/internal/store"
|
||||
"lazyrun/internal/xdg"
|
||||
)
|
||||
|
||||
// ExecOptions adalah input Fungsi A: shortcut dari file executable.
|
||||
type ExecOptions struct {
|
||||
BinaryPath string
|
||||
IconPath string // opsional
|
||||
Name string
|
||||
Category string
|
||||
Menu bool
|
||||
Desktop bool
|
||||
}
|
||||
|
||||
// InstallExecutable menyalin binary + ikon, menulis .desktop, lalu refresh cache.
|
||||
func InstallExecutable(o ExecOptions) (store.Shortcut, error) {
|
||||
var sc store.Shortcut
|
||||
|
||||
name := strings.TrimSpace(o.Name)
|
||||
if err := validate(name, o.Menu, o.Desktop); err != nil {
|
||||
return sc, err
|
||||
}
|
||||
if err := checkRunnable(o.BinaryPath); err != nil {
|
||||
return sc, fmt.Errorf("%s: %w", filepath.Base(o.BinaryPath), err)
|
||||
}
|
||||
|
||||
sl := slugify(name)
|
||||
binDst := filepath.Join(xdg.BinDir(), sl)
|
||||
deskDst := filepath.Join(xdg.ApplicationsDir(), sl+".desktop")
|
||||
if _, err := os.Stat(deskDst); err == nil {
|
||||
return sc, fmt.Errorf("shortcut %q already exists, uninstall it first", name)
|
||||
}
|
||||
if st, err := os.Stat(binDst); err == nil && !st.IsDir() && !samePath(o.BinaryPath, binDst) {
|
||||
return sc, fmt.Errorf("%s already exists, rename the app or remove that file first", binDst)
|
||||
}
|
||||
|
||||
if !samePath(o.BinaryPath, binDst) {
|
||||
if err := copyFile(o.BinaryPath, binDst, 0o755); err != nil {
|
||||
return sc, err
|
||||
}
|
||||
}
|
||||
iconPath, err := icon.Save(o.IconPath, filepath.Dir(o.BinaryPath), strings.TrimSuffix(filepath.Base(o.BinaryPath), filepath.Ext(o.BinaryPath)), sl)
|
||||
if err != nil {
|
||||
return sc, err
|
||||
}
|
||||
|
||||
entry := Entry{
|
||||
Name: name,
|
||||
ExecArgs: []string{binDst},
|
||||
Icon: iconPath,
|
||||
Categories: category(o.Category, "Utility;"),
|
||||
NoDisplay: !o.Menu,
|
||||
}
|
||||
if err := WriteDesktop(deskDst, entry); err != nil {
|
||||
return sc, err
|
||||
}
|
||||
|
||||
sc = store.Shortcut{
|
||||
Slug: sl,
|
||||
Name: name,
|
||||
Type: store.TypeExecutable,
|
||||
BinaryPath: binDst,
|
||||
IconPath: iconPath,
|
||||
DesktopPath: deskDst,
|
||||
Targets: store.Targets{Menu: o.Menu, Desktop: o.Desktop},
|
||||
InstalledAt: time.Now().UTC(),
|
||||
}
|
||||
if o.Desktop {
|
||||
cp, err := desktopCopy(deskDst, sl)
|
||||
if err != nil {
|
||||
return sc, err
|
||||
}
|
||||
sc.DesktopCopyPath = cp
|
||||
}
|
||||
go RefreshCache()
|
||||
return sc, nil
|
||||
}
|
||||
|
||||
// WebOptions adalah input Fungsi B: shortcut dari URL (PWA).
|
||||
type WebOptions struct {
|
||||
URL string
|
||||
Name string
|
||||
Browser BrowserArg
|
||||
IconPath string // opsional; kosong = auto-fetch favicon
|
||||
Menu bool
|
||||
Desktop bool
|
||||
}
|
||||
|
||||
// BrowserArg adalah browser target yang dipilih di UI.
|
||||
type BrowserArg struct {
|
||||
Name string
|
||||
Exec []string
|
||||
// Firefox membuka dengan --new-window, sisanya (Chromium-based) dengan --app=.
|
||||
Firefox bool
|
||||
}
|
||||
|
||||
// InstallWebApp menulis .desktop yang membuka URL sebagai aplikasi.
|
||||
func InstallWebApp(o WebOptions) (store.Shortcut, error) {
|
||||
var sc store.Shortcut
|
||||
|
||||
name := strings.TrimSpace(o.Name)
|
||||
if err := validate(name, o.Menu, o.Desktop); err != nil {
|
||||
return sc, err
|
||||
}
|
||||
if len(o.Browser.Exec) == 0 {
|
||||
return sc, errors.New("no browser selected")
|
||||
}
|
||||
pageURL, err := normalizeURL(o.URL)
|
||||
if err != nil {
|
||||
return sc, err
|
||||
}
|
||||
|
||||
sl := slugify(name)
|
||||
deskDst := filepath.Join(xdg.ApplicationsDir(), sl+".desktop")
|
||||
if _, err := os.Stat(deskDst); err == nil {
|
||||
return sc, fmt.Errorf("shortcut %q already exists, uninstall it first", name)
|
||||
}
|
||||
|
||||
iconPath, err := installWebIcon(o.IconPath, pageURL, sl)
|
||||
if err != nil {
|
||||
return sc, err
|
||||
}
|
||||
|
||||
appArgs := o.Browser.Exec[1:]
|
||||
if o.Browser.Firefox {
|
||||
appArgs = append(appArgs, "--new-window", pageURL)
|
||||
} else {
|
||||
appArgs = append(appArgs, "--app="+pageURL)
|
||||
}
|
||||
entry := Entry{
|
||||
Name: name,
|
||||
ExecArgs: append([]string{o.Browser.Exec[0]}, appArgs...),
|
||||
Icon: iconPath,
|
||||
Categories: "Network;WebBrowser;",
|
||||
NoDisplay: !o.Menu,
|
||||
}
|
||||
if err := WriteDesktop(deskDst, entry); err != nil {
|
||||
return sc, err
|
||||
}
|
||||
|
||||
sc = store.Shortcut{
|
||||
Slug: sl,
|
||||
Name: name,
|
||||
Type: store.TypeWebApp,
|
||||
URL: pageURL,
|
||||
Browser: strings.Join(o.Browser.Exec, " "),
|
||||
IconPath: iconPath,
|
||||
DesktopPath: deskDst,
|
||||
Targets: store.Targets{Menu: o.Menu, Desktop: o.Desktop},
|
||||
InstalledAt: time.Now().UTC(),
|
||||
}
|
||||
if o.Desktop {
|
||||
cp, err := desktopCopy(deskDst, sl)
|
||||
if err != nil {
|
||||
return sc, err
|
||||
}
|
||||
sc.DesktopCopyPath = cp
|
||||
}
|
||||
go RefreshCache()
|
||||
return sc, nil
|
||||
}
|
||||
|
||||
func installWebIcon(userIcon, pageURL, slug string) (string, error) {
|
||||
if userIcon != "" {
|
||||
return icon.Save(userIcon, "", "", slug)
|
||||
}
|
||||
if data, ext, err := icon.Fetch(pageURL); err == nil {
|
||||
return icon.SaveBytes(data, ext, slug)
|
||||
}
|
||||
return icon.Default(slug)
|
||||
}
|
||||
|
||||
func category(c, fallback string) string {
|
||||
c = strings.TrimSpace(c)
|
||||
if c == "" {
|
||||
return fallback
|
||||
}
|
||||
return strings.TrimSuffix(c, ";") + ";"
|
||||
}
|
||||
|
||||
// normalizeURL menambah skema https kalau user menulis domain saja.
|
||||
func normalizeURL(raw string) (string, error) {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return "", errors.New("URL is required")
|
||||
}
|
||||
if !strings.Contains(raw, "://") {
|
||||
raw = "https://" + raw
|
||||
}
|
||||
u, err := url.Parse(raw)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if u.Scheme != "http" && u.Scheme != "https" {
|
||||
return "", errors.New("only http/https URLs are supported")
|
||||
}
|
||||
if u.Host == "" {
|
||||
return "", errors.New("URL has no host")
|
||||
}
|
||||
return u.String(), nil
|
||||
}
|
||||
Reference in new issue
Block a user