Backup/restore: - berkas .lazy (format versi 2) kini menyimpan ISI berkas: binary/script dan ikon (maks 512 MB per berkas); format versi 1 tetap bisa diimpor - impor memakai shortcut.Restore: binary ditulis ke ~/.local/bin lokal, ikon dari cadangan (atau diunduh ulang dari icon_source), .desktop ditulis dengan path lokal, salinan Desktop dibuat sesuai target - path dari komputer lama tidak dipakai mentah -> tidak ada lagi 'banyak file missing'; entri yang tidak bisa dipulihkan ditandai dan bisa dibetulkan via edit - tes end-to-end: pasang 3 shortcut -> ekspor -> pulihkan di HOME lain -> semua berkas lokal, script hasil restore benar-benar jalan Script: - script dipasang sebagai wrapper + <slug>.real: wrapper menambahkan ~/.local/bin ke PATH dan cd ke folder asli script (desktop environment menjalankan dengan cwd = home, PATH sesi) - .desktop dapat Path= sesuai folder asli script - opsi 'Run in a terminal window (scripts)' di dialog install & edit - uninstall membuang wrapper + script aslinya
213 lines
5.6 KiB
Go
213 lines
5.6 KiB
Go
package shortcut
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"lazyrun/internal/store"
|
|
"lazyrun/internal/xdg"
|
|
)
|
|
|
|
// EditExecutable memperbarui shortcut executable yang sudah ada:
|
|
// binary bisa diganti (disalin ulang), begitu juga ikon, nama, dan lokasinya.
|
|
func EditExecutable(old store.Shortcut, o ExecOptions) (store.Shortcut, error) {
|
|
name := strings.TrimSpace(o.Name)
|
|
if err := validate(name, o.Menu, o.Desktop); err != nil {
|
|
return old, err
|
|
}
|
|
|
|
src := o.BinaryPath
|
|
if src == "" {
|
|
src = old.BinaryPath
|
|
}
|
|
if err := checkRunnable(src); err != nil {
|
|
return old, fmt.Errorf("%s: %w", filepath.Base(src), err)
|
|
}
|
|
|
|
sl := slugify(name)
|
|
binDst := filepath.Join(xdg.BinDir(), sl)
|
|
if sl != old.Slug {
|
|
if _, err := os.Stat(filepath.Join(xdg.ApplicationsDir(), sl+".desktop")); err == nil {
|
|
return old, fmt.Errorf("shortcut %q already exists", name)
|
|
}
|
|
}
|
|
|
|
kind := DetectKind(src)
|
|
if err := installBinary(kind, src, binDst); err != nil {
|
|
return old, err
|
|
}
|
|
|
|
execArgs := []string{binDst}
|
|
if kind == KindWindows {
|
|
wine, err := exec.LookPath("wine")
|
|
if err != nil {
|
|
return old, errors.New("this is a Windows .exe; install wine first so the shortcut can run it")
|
|
}
|
|
execArgs = []string{wine, binDst}
|
|
}
|
|
|
|
iconPath, iconSource := old.IconPath, old.IconSource
|
|
if o.IconPath != "" || o.IconURL != "" || len(o.IconData) > 0 {
|
|
path, source, err := installIconFor(iconOptions{
|
|
IconPath: o.IconPath, IconURL: o.IconURL, IconData: o.IconData,
|
|
Query: name, Slug: sl,
|
|
})
|
|
if err != nil {
|
|
return old, err
|
|
}
|
|
iconPath, iconSource = path, source
|
|
}
|
|
|
|
category := category(o.Category, "Utility;")
|
|
if o.Category == "" {
|
|
category = old.Categories
|
|
if category == "" {
|
|
category = "Utility;"
|
|
}
|
|
}
|
|
|
|
entry := Entry{
|
|
Name: name,
|
|
ExecArgs: execArgs,
|
|
Icon: iconPath,
|
|
Categories: category,
|
|
Terminal: o.Terminal && kind != KindWindows,
|
|
NoDisplay: !o.Menu,
|
|
}
|
|
if kind == KindScript {
|
|
if _, err := os.Stat(filepath.Dir(src)); err == nil {
|
|
entry.WorkingDir = filepath.Dir(src)
|
|
}
|
|
}
|
|
|
|
updated := old
|
|
updated.Slug = sl
|
|
updated.Name = name
|
|
updated.Kind = kind
|
|
updated.ExecArgs = execArgs
|
|
updated.Categories = category
|
|
updated.Terminal = entry.Terminal
|
|
updated.BinaryPath = binDst
|
|
updated.IconPath = iconPath
|
|
updated.IconSource = iconSource
|
|
updated.Targets = store.Targets{Menu: o.Menu, Desktop: o.Desktop}
|
|
|
|
if err := rewriteShortcutFiles(old, &updated, entry); err != nil {
|
|
return old, err
|
|
}
|
|
return updated, nil
|
|
}
|
|
|
|
// EditWebApp memperbarui shortcut web app (URL, browser, ikon, nama, lokasi).
|
|
func EditWebApp(old store.Shortcut, o WebOptions) (store.Shortcut, error) {
|
|
name := strings.TrimSpace(o.Name)
|
|
if err := validate(name, o.Menu, o.Desktop); err != nil {
|
|
return old, err
|
|
}
|
|
if len(o.Browser.Exec) == 0 {
|
|
return old, errors.New("no browser selected")
|
|
}
|
|
pageURL, err := normalizeURL(o.URL)
|
|
if err != nil {
|
|
return old, err
|
|
}
|
|
|
|
sl := slugify(name)
|
|
if sl != old.Slug {
|
|
if _, err := os.Stat(filepath.Join(xdg.ApplicationsDir(), sl+".desktop")); err == nil {
|
|
return old, fmt.Errorf("shortcut %q already exists", name)
|
|
}
|
|
}
|
|
|
|
iconPath, iconSource := old.IconPath, old.IconSource
|
|
if o.IconPath != "" || o.IconURL != "" || len(o.IconData) > 0 {
|
|
path, source, err := installIconFor(iconOptions{
|
|
IconPath: o.IconPath, IconURL: o.IconURL, IconData: o.IconData,
|
|
PageURL: pageURL, Query: name, Slug: sl,
|
|
})
|
|
if err != nil {
|
|
return old, err
|
|
}
|
|
iconPath, iconSource = path, source
|
|
}
|
|
|
|
appArgs := o.Browser.Exec[1:]
|
|
if o.Browser.Firefox {
|
|
appArgs = append(appArgs, "--new-window", pageURL)
|
|
} else {
|
|
appArgs = append(appArgs, "--app="+pageURL)
|
|
}
|
|
execArgs := append([]string{o.Browser.Exec[0]}, appArgs...)
|
|
|
|
updated := old
|
|
updated.Slug = sl
|
|
updated.Name = name
|
|
updated.Kind = KindPWA
|
|
updated.URL = pageURL
|
|
updated.Browser = strings.Join(o.Browser.Exec, " ")
|
|
updated.ExecArgs = execArgs
|
|
updated.IconPath = iconPath
|
|
updated.IconSource = iconSource
|
|
updated.Targets = store.Targets{Menu: o.Menu, Desktop: o.Desktop}
|
|
|
|
if err := rewriteShortcutFiles(old, &updated, Entry{
|
|
Name: name,
|
|
ExecArgs: execArgs,
|
|
Icon: iconPath,
|
|
Categories: old.Categories,
|
|
NoDisplay: !o.Menu,
|
|
}); err != nil {
|
|
return old, err
|
|
}
|
|
return updated, nil
|
|
}
|
|
|
|
// rewriteShortcutFiles menulis ulang .desktop (dan salinannya) untuk shortcut
|
|
// yang diedit, termasuk membereskan berkas lama kalau nama/slug berubah.
|
|
// updated diisi lewat pointer karena path-nya berubah di sini.
|
|
func rewriteShortcutFiles(old store.Shortcut, updated *store.Shortcut, entry Entry) error {
|
|
if entry.Categories == "" {
|
|
entry.Categories = "Utility;"
|
|
}
|
|
|
|
newDesk := filepath.Join(xdg.ApplicationsDir(), updated.Slug+".desktop")
|
|
if err := WriteDesktop(newDesk, entry); err != nil {
|
|
return err
|
|
}
|
|
updated.DesktopPath = newDesk
|
|
|
|
// salinan Desktop: dibuat kalau diminta, dihapus kalau tidak
|
|
newCopy := ""
|
|
if updated.Targets.Desktop {
|
|
newCopy = filepath.Join(xdg.DesktopDir(), updated.Slug+".desktop")
|
|
if err := copyFile(newDesk, newCopy, 0o755); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
updated.DesktopCopyPath = newCopy
|
|
|
|
// bersihkan berkas lama kalau lokasinya berubah
|
|
if old.DesktopPath != "" && old.DesktopPath != newDesk {
|
|
_ = os.Remove(old.DesktopPath)
|
|
}
|
|
if old.DesktopCopyPath != "" && old.DesktopCopyPath != newCopy {
|
|
_ = os.Remove(old.DesktopCopyPath)
|
|
}
|
|
if old.IconPath != "" && old.IconPath != updated.IconPath {
|
|
_ = os.Remove(old.IconPath)
|
|
}
|
|
// binary lama dibuang kalau nama berkasnya berubah
|
|
if old.Type == store.TypeExecutable && old.BinaryPath != "" && old.BinaryPath != updated.BinaryPath {
|
|
if !samePath(old.BinaryPath, updated.BinaryPath) {
|
|
_ = os.Remove(old.BinaryPath)
|
|
}
|
|
}
|
|
|
|
go RefreshCache()
|
|
return nil
|
|
}
|