Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
35e83dba09 |
No files matched your search
@@ -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
|
- **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
|
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
|
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 <desktop file>`, 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=<url>` for
|
- **Web app shortcut** — opens the URL as an app: `--app=<url>` for
|
||||||
Chromium-based browsers (Chrome, Chromium, Brave, Edge, Vivaldi, Helium,
|
Chromium-based browsers (Chrome, Chromium, Brave, Edge, Vivaldi, Helium,
|
||||||
including Flatpak builds), `--new-window` for Firefox.
|
including Flatpak builds), `--new-window` for Firefox.
|
||||||
|
|||||||
@@ -121,6 +121,8 @@ case "$PATH" in *".local/bin"*) echo "path ok";; *) echo "path hilang";; esac
|
|||||||
if !strings.Contains(string(out), "path ok") {
|
if !strings.Contains(string(out), "path ok") {
|
||||||
t.Errorf("PATH tidak disiapkan wrapper: %s", out)
|
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
|
// binary biasa hasil restore juga harus executable
|
||||||
tool := restored["Tool"]
|
tool := restored["Tool"]
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ func installBinary(kind, src, binDst string) error {
|
|||||||
if err := copyFile(src, binDst+scriptSuffix, 0o755); err != nil {
|
if err := copyFile(src, binDst+scriptSuffix, 0o755); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return writeScriptWrapper(binDst, binDst+scriptSuffix, filepath.Dir(src))
|
return writeScriptWrapper(binDst, binDst+scriptSuffix, src)
|
||||||
}
|
}
|
||||||
if samePath(src, binDst) {
|
if samePath(src, binDst) {
|
||||||
return nil
|
return nil
|
||||||
@@ -137,16 +137,26 @@ func writeFileMode(path string, data []byte, mode os.FileMode) error {
|
|||||||
const scriptSuffix = ".real"
|
const scriptSuffix = ".real"
|
||||||
|
|
||||||
// writeScriptWrapper menulis wrapper yang menyiapkan PATH dan folder kerja
|
// writeScriptWrapper menulis wrapper yang menyiapkan PATH dan folder kerja
|
||||||
// sebelum menjalankan script aslinya.
|
// sebelum menjalankan script.
|
||||||
func writeScriptWrapper(path, real, originDir string) error {
|
//
|
||||||
|
// 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" +
|
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" +
|
`PATH="$HOME/.local/bin:$PATH"` + "\n" +
|
||||||
"export PATH\n"
|
"export PATH\n"
|
||||||
if originDir != "" && originDir != "." {
|
|
||||||
|
originDir := filepath.Dir(originFile)
|
||||||
|
if originDir != "" && originDir != "." && originDir != "/" {
|
||||||
quoted := shellQuote(originDir)
|
quoted := shellQuote(originDir)
|
||||||
body += "if [ -d " + quoted + " ]; then cd " + quoted + " || exit 1; fi\n"
|
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"
|
body += "exec " + shellQuote(real) + ` "$@"` + "\n"
|
||||||
return os.WriteFile(path, []byte(body), 0o755)
|
return os.WriteFile(path, []byte(body), 0o755)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -267,3 +267,35 @@ func TestUninstallKeepsBinaryByDefault(t *testing.T) {
|
|||||||
t.Errorf("binary harus tetap ada: %v", err)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -108,7 +108,11 @@ func restoreBinary(restored *store.Shortcut, slug string, binary []byte) error {
|
|||||||
if err := os.Rename(binDst, binDst+scriptSuffix); err != nil {
|
if err := os.Rename(binDst, binDst+scriptSuffix); err != nil {
|
||||||
return err
|
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
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
+30
-11
@@ -1,7 +1,9 @@
|
|||||||
package ui
|
package ui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
|
||||||
"fyne.io/fyne/v2"
|
"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 <file .desktop>` 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) {
|
func (ui *app) launch(sc store.Shortcut) {
|
||||||
args := sc.ExecArgs
|
args, err := launchCommand(sc)
|
||||||
if len(args) == 0 {
|
if err != nil {
|
||||||
switch {
|
dialog.ShowError(err, ui.win)
|
||||||
case sc.Type == store.TypeWebApp && sc.URL != "":
|
|
||||||
args = []string{"xdg-open", sc.URL}
|
|
||||||
case sc.BinaryPath != "":
|
|
||||||
args = []string{sc.BinaryPath}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(args) == 0 {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,3 +48,23 @@ func (ui *app) launch(sc store.Shortcut) {
|
|||||||
}
|
}
|
||||||
go cmd.Wait() // supaya proses anak tidak jadi zombie
|
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")
|
||||||
|
}
|
||||||
Reference in new issue
Block a user