From 35e83dba09a55e9fc47c84fbba842b301a8c7c68 Mon Sep 17 00:00:00 2001 From: oxpoe Date: Tue, 13 Oct 2026 09:36:02 +0700 Subject: [PATCH] Tombol Run menghormati Terminal=true (gio launch) + wrapper pakai script asli - launch lewat 'gio launch ' kalau tersedia: Terminal=true membuka terminal, Path= dihormati, sama seperti menu desktop (script yang butuh sudo atau input sekarang bisa jalan dari tombol Run) - wrapper script memakai berkas script asli kalau masih ada, jadi dirname $0 menunjuk folder aslinya (laporan/hasil tidak nyasar ke ~/.local/bin); salinan .real tetap jadi cadangan - tes: pemilihan perintah launch, wrapper memakai script asli + PATH --- README.md | 5 ++- internal/shortcut/backup_e2e_test.go | 2 + internal/shortcut/install.go | 20 ++++++--- internal/shortcut/install_test.go | 32 ++++++++++++++ internal/shortcut/restore.go | 6 ++- internal/ui/launch_test.go | 62 ++++++++++++++++++++++++++++ internal/ui/links.go | 41 +++++++++++++----- 7 files changed, 150 insertions(+), 18 deletions(-) create mode 100644 internal/ui/launch_test.go diff --git a/README.md b/README.md index 71d8ddf..a6164d6 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,10 @@ breathes while the app is open (the koala in `assets/logo.svg` is the mascot).* - **Scripts run properly** — the desktop environment starts programs with your home as the working directory and the session `PATH`, which breaks scripts that use relative paths or tools from `~/.local/bin`. LazyRun therefore installs a tiny wrapper that - fixes both (and can open the script in a terminal window when you tick that option). + fixes both, running the original script file when it is still there (so `dirname $0` + keeps pointing at its own folder). *Run* in the list launches through + `gio launch `, so `Terminal=true` and `Path=` behave exactly like the + desktop menu — handy for scripts that ask for input or need `sudo`. - **Web app shortcut** — opens the URL as an app: `--app=` for Chromium-based browsers (Chrome, Chromium, Brave, Edge, Vivaldi, Helium, including Flatpak builds), `--new-window` for Firefox. diff --git a/internal/shortcut/backup_e2e_test.go b/internal/shortcut/backup_e2e_test.go index 96743af..76b8a8a 100644 --- a/internal/shortcut/backup_e2e_test.go +++ b/internal/shortcut/backup_e2e_test.go @@ -121,6 +121,8 @@ case "$PATH" in *".local/bin"*) echo "path ok";; *) echo "path hilang";; esac if !strings.Contains(string(out), "path ok") { t.Errorf("PATH tidak disiapkan wrapper: %s", out) } + // script asli di folder asalnya masih ada di mesin ini? bukan di sini, + // jadi wrapper memakai salinannya (sudah diuji lewat 'data ok' di atas) // binary biasa hasil restore juga harus executable tool := restored["Tool"] diff --git a/internal/shortcut/install.go b/internal/shortcut/install.go index 75988cb..1184d14 100644 --- a/internal/shortcut/install.go +++ b/internal/shortcut/install.go @@ -117,7 +117,7 @@ func installBinary(kind, src, binDst string) error { if err := copyFile(src, binDst+scriptSuffix, 0o755); err != nil { return err } - return writeScriptWrapper(binDst, binDst+scriptSuffix, filepath.Dir(src)) + return writeScriptWrapper(binDst, binDst+scriptSuffix, src) } if samePath(src, binDst) { return nil @@ -137,16 +137,26 @@ func writeFileMode(path string, data []byte, mode os.FileMode) error { const scriptSuffix = ".real" // writeScriptWrapper menulis wrapper yang menyiapkan PATH dan folder kerja -// sebelum menjalankan script aslinya. -func writeScriptWrapper(path, real, originDir string) error { +// sebelum menjalankan script. +// +// Script asli di folder asalnya dipakai lebih dulu kalau masih ada (supaya +// `dirname $0` dan berkas di sebelahnya tetap mengarah ke sana); salinan di +// ~/.local/bin jadi cadangan kalau script aslinya sudah pindah/dihapus. +func writeScriptWrapper(path, real, originFile string) error { body := "#!/bin/sh\n" + - "# dibuat oleh LazyRun: siapkan PATH & folder kerja lalu jalankan script aslinya\n" + + "# dibuat oleh LazyRun: siapkan PATH & folder kerja lalu jalankan script\n" + `PATH="$HOME/.local/bin:$PATH"` + "\n" + "export PATH\n" - if originDir != "" && originDir != "." { + + originDir := filepath.Dir(originFile) + if originDir != "" && originDir != "." && originDir != "/" { quoted := shellQuote(originDir) body += "if [ -d " + quoted + " ]; then cd " + quoted + " || exit 1; fi\n" } + + if originFile != "" && originFile != real { + body += "if [ -f " + shellQuote(originFile) + " ]; then exec " + shellQuote(originFile) + ` "$@"; fi` + "\n" + } body += "exec " + shellQuote(real) + ` "$@"` + "\n" return os.WriteFile(path, []byte(body), 0o755) } diff --git a/internal/shortcut/install_test.go b/internal/shortcut/install_test.go index 14ccd01..1929280 100644 --- a/internal/shortcut/install_test.go +++ b/internal/shortcut/install_test.go @@ -267,3 +267,35 @@ func TestUninstallKeepsBinaryByDefault(t *testing.T) { t.Errorf("binary harus tetap ada: %v", err) } } + +func TestScriptWrapperPrefersOriginalFile(t *testing.T) { + home := testEnv(t) + dir := filepath.Join(home, "proyek") + if err := os.MkdirAll(dir, 0o755); err != nil { + t.Fatal(err) + } + src := filepath.Join(dir, "bench.sh") + if err := os.WriteFile(src, []byte("#!/bin/sh\necho asli \"$(pwd)\"\n"), 0o755); err != nil { + t.Fatal(err) + } + + sc, err := InstallExecutable(ExecOptions{BinaryPath: src, Name: "Bench", Menu: true}) + if err != nil { + t.Fatal(err) + } + + wrapper, err := os.ReadFile(sc.BinaryPath) + if err != nil { + t.Fatal(err) + } + text := string(wrapper) + if !strings.Contains(text, shellQuote(src)) { + t.Errorf("wrapper tidak memakai script asli:\n%s", text) + } + if !strings.Contains(text, shellQuote(dir)) { + t.Errorf("wrapper tidak cd ke folder asli:\n%s", text) + } + if !strings.Contains(text, "PATH=\"$HOME/.local/bin:$PATH\"") { + t.Errorf("wrapper tidak menyiapkan PATH:\n%s", text) + } +} diff --git a/internal/shortcut/restore.go b/internal/shortcut/restore.go index ec448a7..02702c7 100644 --- a/internal/shortcut/restore.go +++ b/internal/shortcut/restore.go @@ -108,7 +108,11 @@ func restoreBinary(restored *store.Shortcut, slug string, binary []byte) error { if err := os.Rename(binDst, binDst+scriptSuffix); err != nil { return err } - if err := writeScriptWrapper(binDst, binDst+scriptSuffix, originDir); err != nil { + originFile := "" + if originDir != "" { + originFile = filepath.Join(originDir, filepath.Base(restored.BinaryPath)) + } + if err := writeScriptWrapper(binDst, binDst+scriptSuffix, originFile); err != nil { return err } } diff --git a/internal/ui/launch_test.go b/internal/ui/launch_test.go new file mode 100644 index 0000000..7ba18dc --- /dev/null +++ b/internal/ui/launch_test.go @@ -0,0 +1,62 @@ +package ui + +import ( + "os" + "os/exec" + "path/filepath" + "testing" + + "lazyrun/internal/store" +) + +func TestLaunchCommandPrefersDesktopFile(t *testing.T) { + dir := t.TempDir() + desk := filepath.Join(dir, "tool.desktop") + if err := os.WriteFile(desk, []byte("[Desktop Entry]\n"), 0o644); err != nil { + t.Fatal(err) + } + + sc := store.Shortcut{DesktopPath: desk, BinaryPath: "/bin/true", ExecArgs: []string{"/bin/true"}} + args, err := launchCommand(sc) + if err != nil { + t.Fatal(err) + } + + if _, err := exec.LookPath("gio"); err != nil { + // tanpa gio: langsung jalankan perintahnya + if len(args) != 1 || args[0] != "/bin/true" { + t.Errorf("tanpa gio harus jalankan binary, dapat %v", args) + } + return + } + if len(args) != 3 || args[1] != "launch" || args[2] != desk { + t.Errorf("harus lewat gio launch supaya Terminal=true dihormati, dapat %v", args) + } +} + +func TestLaunchCommandFallsBack(t *testing.T) { + // .desktop tidak ada -> pakai exec args + sc := store.Shortcut{DesktopPath: "/tidak/ada/x.desktop", ExecArgs: []string{"/usr/bin/wine", "/x/winbox"}} + args, err := launchCommand(sc) + if err != nil { + t.Fatal(err) + } + if len(args) != 2 || args[0] != "/usr/bin/wine" { + t.Errorf("fallback exec args salah: %v", args) + } + + // web app tanpa exec args + web := store.Shortcut{Type: store.TypeWebApp, URL: "https://example.com"} + args, err = launchCommand(web) + if err != nil { + t.Fatal(err) + } + if len(args) != 2 || args[0] != "xdg-open" { + t.Errorf("fallback web app salah: %v", args) + } + + // tanpa apa-apa + if _, err := launchCommand(store.Shortcut{}); err == nil { + t.Error("shortcut kosong harus error, bukan jalan tanpa perintah") + } +} diff --git a/internal/ui/links.go b/internal/ui/links.go index 9ebdfda..197b65a 100644 --- a/internal/ui/links.go +++ b/internal/ui/links.go @@ -1,7 +1,9 @@ package ui import ( + "errors" "net/url" + "os" "os/exec" "fyne.io/fyne/v2" @@ -27,18 +29,15 @@ func (ui *app) openURL(raw string) { } } -// launch menjalankan shortcut: pakai argv hasil install, fallback ke binary/URL. +// launch menjalankan shortcut. +// +// Kalau ada, `gio launch ` dipakai supaya aturannya sama dengan +// menu desktop: Terminal=true membuka jendela terminal (script yang butuh sudo +// atau input bisa jalan) dan Path= ikut dihormati. func (ui *app) launch(sc store.Shortcut) { - args := sc.ExecArgs - if len(args) == 0 { - switch { - case sc.Type == store.TypeWebApp && sc.URL != "": - args = []string{"xdg-open", sc.URL} - case sc.BinaryPath != "": - args = []string{sc.BinaryPath} - } - } - if len(args) == 0 { + args, err := launchCommand(sc) + if err != nil { + dialog.ShowError(err, ui.win) return } @@ -49,3 +48,23 @@ func (ui *app) launch(sc store.Shortcut) { } go cmd.Wait() // supaya proses anak tidak jadi zombie } + +// launchCommand menyusun perintah untuk menjalankan shortcut. +func launchCommand(sc store.Shortcut) ([]string, error) { + if sc.DesktopPath != "" { + if gio, err := exec.LookPath("gio"); err == nil { + if _, err := os.Stat(sc.DesktopPath); err == nil { + return []string{gio, "launch", sc.DesktopPath}, nil + } + } + } + switch { + case len(sc.ExecArgs) > 0: + return sc.ExecArgs, nil + case sc.Type == store.TypeWebApp && sc.URL != "": + return []string{"xdg-open", sc.URL}, nil + case sc.BinaryPath != "": + return []string{sc.BinaryPath}, nil + } + return nil, errors.New("shortcut ini tidak punya perintah yang bisa dijalankan") +}