Perbaiki backup/restore dan script yang tidak jalan dari shortcut

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
This commit is contained in:
oxpoe committed 2026-10-13 09:25:49 +07:00
1 parent fdb503e152
commit 581cd07dba
16 files changed
+779 -59

No files matched your search

+29 -7
View File
@@ -72,7 +72,7 @@ func (ui *app) importShortcuts() {
return
}
added, skipped := mergeImported(ui.store, items)
added, skipped := ui.restoreImported(items)
if err := ui.store.Save(); err != nil {
dialog.ShowError(err, ui.win)
return
@@ -98,6 +98,11 @@ func (ui *app) showEditDialog(sc store.Shortcut) {
menu.SetChecked(sc.Targets.Menu)
desktopChk := widget.NewCheck(T("check_desktop"), nil)
desktopChk.SetChecked(sc.Targets.Desktop)
terminal := widget.NewCheck(T("check_terminal"), nil)
terminal.SetChecked(sc.Terminal)
if sc.Kind != shortcut.KindScript {
terminal.Hide()
}
browseIcon := newButton(T("browse"), theme.FolderOpenIcon(), T("tip_browse"), widget.MediumImportance, func() {
pickFile(ui.win, imageFilter, func(path string) {
@@ -118,6 +123,12 @@ func (ui *app) showEditDialog(sc store.Shortcut) {
pickFile(ui.win, nil, func(path string) {
binPath = path
binLabel.SetText(filepath.Base(path) + " · " + detectedKind(path))
if shortcut.DetectKind(path) == shortcut.KindScript {
terminal.Show()
} else {
terminal.SetChecked(false)
terminal.Hide()
}
})
})
category := widget.NewSelect(categories, nil)
@@ -185,6 +196,7 @@ func (ui *app) showEditDialog(sc store.Shortcut) {
IconData: iconData,
Name: nameEntry.Text,
Category: category.Selected,
Terminal: terminal.Checked,
Menu: menu.Checked,
Desktop: desktopChk.Checked,
})
@@ -215,6 +227,7 @@ func (ui *app) showEditDialog(sc store.Shortcut) {
sectionTitle(T("section_location")),
menu,
desktopChk,
terminal,
hint,
container.NewHBox(logoBox(newLogo(), 20), spacer(),
newButton(T("cancel"), nil, T("tip_cancel"), widget.MediumImportance, func() { d.Hide() }),
@@ -226,19 +239,28 @@ func (ui *app) showEditDialog(sc store.Shortcut) {
showDialog(d, ui.win, 560, 520)
}
// mergeImported menambahkan shortcut hasil impor tanpa menimpa yang sudah ada.
func mergeImported(target *store.Store, items []store.Shortcut) (added, skipped int) {
for _, item := range items {
// restoreImported memasang ulang setiap entri cadangan ke mesin ini
// (binary, ikon, .desktop) lalu menambahkannya ke daftar, tanpa menimpa
// shortcut yang sudah ada.
func (ui *app) restoreImported(items []db.Backup) (added, skipped int) {
for _, backup := range items {
item := backup.Item
item.Name = strings.TrimSpace(item.Name)
if item.Name == "" || item.Slug == "" {
if item.Name == "" {
skipped++
continue
}
if _, exists := target.Get(item.Slug); exists {
if _, exists := ui.store.Get(shortcut.Slug(item.Name)); exists {
skipped++
continue
}
target.Add(item)
restored, err := shortcut.Restore(item, backup.Binary, backup.Icon)
if err != nil {
skipped++
continue
}
ui.store.Add(restored)
added++
}
return added, skipped
+11
View File
@@ -40,11 +40,20 @@ func (ui *app) showExecutableDialog() {
menu.SetChecked(true)
desktop := widget.NewCheck(T("check_desktop"), nil)
desktop.SetChecked(true)
terminal := widget.NewCheck(T("check_terminal"), nil)
terminal.Hide() // hanya relevan untuk script
browseBin := newButton(T("browse"), theme.FolderOpenIcon(), T("tip_browse"), widget.MediumImportance, func() {
pickFile(ui.win, nil, func(path string) {
binPath = path
binLabel.SetText(filepath.Base(path) + " · " + detectedKind(path))
// terminal hanya perlu untuk script; tampilkan kalau terdeteksi
if shortcut.DetectKind(path) == shortcut.KindScript {
terminal.Show()
} else {
terminal.SetChecked(false)
terminal.Hide()
}
if strings.TrimSpace(nameEntry.Text) == "" {
nameEntry.SetText(appNameFromFile(path))
}
@@ -83,6 +92,7 @@ func (ui *app) showExecutableDialog() {
IconData: iconData,
Name: nameEntry.Text,
Category: category.Selected,
Terminal: terminal.Checked,
Menu: menu.Checked,
Desktop: desktop.Checked,
}
@@ -106,6 +116,7 @@ func (ui *app) showExecutableDialog() {
sectionTitle(T("section_location")),
menu,
desktop,
terminal,
hint,
container.NewHBox(logoBox(newLogo(), 20), spacer(),
newButton(T("cancel"), nil, T("tip_cancel"), widget.MediumImportance, func() { d.Hide() }),
+47 -17
View File
@@ -10,6 +10,7 @@ import (
"fyne.io/fyne/v2/test"
"lazyrun/internal/db"
"lazyrun/internal/icon"
"lazyrun/internal/store"
)
@@ -121,26 +122,55 @@ func TestStringsCoverUsedKeys(t *testing.T) {
}
}
func TestMergeImported(t *testing.T) {
target := &store.Store{}
target.Add(store.Shortcut{Slug: "winbox", Name: "Winbox"})
// TestRestoreImported memasang ulang entri cadangan: berkas dibuat di HOME saat ini
// dan entri yang sudah ada tidak ditimpa.
func TestRestoreImported(t *testing.T) {
home := t.TempDir()
t.Setenv("HOME", home)
t.Setenv("XDG_DATA_HOME", "")
t.Setenv("PATH", "")
added, skipped := mergeImported(target, []store.Shortcut{
{Slug: "winbox", Name: "Winbox lama"}, // sudah ada -> dilewati
{Slug: "figma", Name: "Figma"}, // baru
{Slug: "", Name: "Tanpa slug"}, // rusak -> dilewati
{Slug: "kosong", Name: " "}, // tanpa nama -> dilewati
{Slug: "deploy", Name: " deploy.sh "}, // nama dirapikan
})
script := []byte("#!/bin/sh\necho halo\n")
items := []db.Backup{
{Item: store.Shortcut{Slug: "lama", Name: "Deploy", Type: store.TypeExecutable, Kind: "script",
BinaryPath: "/komputer-lama/.local/bin/deploy", Targets: store.Targets{Menu: true}}, Binary: script},
{Item: store.Shortcut{Name: "Figma", Type: store.TypeWebApp, Kind: "pwa",
URL: "https://figma.com", Browser: "/usr/bin/vivaldi", Targets: store.Targets{Menu: true}}},
}
if added != 2 || skipped != 3 {
t.Errorf("added=%d skipped=%d, mau 2 dan 3", added, skipped)
a := test.NewApp()
defer a.Quit()
ui, err := build(a, "test")
if err != nil {
t.Fatal(err)
}
if len(target.Items) != 3 {
t.Errorf("jumlah item = %d", len(target.Items))
ui.store.Items = nil
ui.store.Add(store.Shortcut{Slug: "figma", Name: "Figma"}) // sudah ada -> dilewati
added, skipped := ui.restoreImported(items)
if added != 1 || skipped != 1 {
t.Fatalf("added=%d skipped=%d, mau 1 dan 1", added, skipped)
}
dep, ok := target.Get("deploy")
if !ok || target.Items[dep].Name != "deploy.sh" {
t.Errorf("nama item tidak dirapikan: %+v", target.Items)
deploy, ok := ui.store.Get("deploy")
if !ok {
t.Fatal("shortcut hasil restore tidak masuk daftar")
}
sc := ui.store.Items[deploy]
want := filepath.Join(home, ".local", "bin", "deploy")
if sc.BinaryPath != want {
t.Errorf("binary = %s, mau %s", sc.BinaryPath, want)
}
if _, err := os.Stat(want + ".real"); err != nil {
t.Errorf("script asli (.real) tidak ditulis: %v", err)
}
if _, err := os.Stat(sc.DesktopPath); err != nil {
t.Errorf(".desktop tidak dibuat: %v", err)
}
if sc.IconPath == "" {
t.Error("ikon tidak dipasang")
}
if sc.Kind != "script" {
t.Errorf("kind hasil restore = %q", sc.Kind)
}
}
+1
View File
@@ -49,6 +49,7 @@ var tr = map[string][2]string{
"section_location": {"Shortcut location", "Lokasi shortcut"},
"check_menu": {"Application menu", "Menu aplikasi"},
"check_desktop": {"Desktop", "Desktop"},
"check_terminal": {"Run in a terminal window (scripts)", "Jalankan di jendela terminal (script)"},
"exec_hint": {"The binary is copied to ~/.local/bin and can be uninstalled from the list.", "Binary disalin ke ~/.local/bin dan bisa dihapus lagi dari daftar."},
"web_hint": {"The icon comes from the site's favicon, or is looked up online if there is none.", "Ikon diambil dari favicon situs, atau dicari otomatis di internet kalau tidak ada."},