Files
lazyrun/internal/shortcut/install.go
T
oxpoe 35e83dba09 Tombol Run menghormati Terminal=true (gio launch) + wrapper pakai script asli
- launch lewat 'gio launch <file .desktop>' 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
2026-10-13 09:36:02 +07:00

168 lines
4.6 KiB
Go

package shortcut
import (
"bytes"
"errors"
"io"
"os"
"path/filepath"
"strings"
"lazyrun/internal/xdg"
)
func validate(name string, menu, desktop bool) error {
if name == "" {
return errors.New("name is required")
}
if strings.ContainsAny(name, "\n\r") {
return errors.New("name must not contain line breaks")
}
if !menu && !desktop {
return errors.New("pick at least one location (menu or desktop)")
}
if slugify(name) == "" {
return errors.New("name must contain at least one letter or digit")
}
return nil
}
// checkRunnable memastikan file adalah ELF (binary/AppImage) atau script dengan shebang.
func checkRunnable(path string) error {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
st, err := f.Stat()
if err != nil {
return err
}
if st.IsDir() {
return errors.New("this is a directory, not a file")
}
head := make([]byte, 4)
n, _ := io.ReadFull(f, head)
head = head[:n]
if bytes.HasPrefix(head, []byte{0x7f, 'E', 'L', 'F'}) ||
bytes.HasPrefix(head, []byte("#!")) ||
bytes.HasPrefix(head, []byte("MZ")) { // .exe Windows (dijalankan lewat wine)
return nil
}
return errors.New("not an executable (ELF, AppImage, script, or Windows .exe)")
}
// copyFile menyalin src ke dst lewat file sementara supaya dst tidak pernah setengah tertulis.
func copyFile(src, dst string, mode os.FileMode) error {
in, err := os.Open(src)
if err != nil {
return err
}
defer in.Close()
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
return err
}
tmp, err := os.CreateTemp(filepath.Dir(dst), ".lazyrun-*")
if err != nil {
return err
}
defer os.Remove(tmp.Name())
if _, err := io.Copy(tmp, in); err != nil {
tmp.Close()
return err
}
if err := tmp.Chmod(mode); err != nil {
tmp.Close()
return err
}
if err := tmp.Close(); err != nil {
return err
}
return os.Rename(tmp.Name(), dst)
}
func samePath(a, b string) bool {
sa, err := os.Stat(a)
if err != nil {
return false
}
sb, err := os.Stat(b)
if err != nil {
return false
}
return os.SameFile(sa, sb)
}
// desktopCopy menyalin .desktop ke ~/Desktop agar bisa diklik ganda.
func desktopCopy(desktopPath, slug string) (string, error) {
dst := filepath.Join(xdg.DesktopDir(), slug+".desktop")
if _, err := os.Stat(dst); err == nil && !samePath(dst, desktopPath) {
return "", errors.New(dst + " already exists")
}
return dst, copyFile(desktopPath, dst, 0o755)
}
// installBinary menyalin berkas target ke binDst.
//
// Script dapat perlakuan khusus: yang disalin adalah script aslinya (dengan
// akhiran .real) plus wrapper kecil. Desktop environment menjalankan shortcut
// dengan cwd = home dan PATH sesi, sehingga script yang memakai path relatif
// atau tool di ~/.local/bin gagal; wrapper yang membereskan keduanya.
func installBinary(kind, src, binDst string) error {
if kind == KindScript {
if err := copyFile(src, binDst+scriptSuffix, 0o755); err != nil {
return err
}
return writeScriptWrapper(binDst, binDst+scriptSuffix, src)
}
if samePath(src, binDst) {
return nil
}
return copyFile(src, binDst, 0o755)
}
// writeFileMode menulis berkas dengan mode tertentu (dipakai saat restore).
func writeFileMode(path string, data []byte, mode os.FileMode) error {
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return err
}
return os.WriteFile(path, data, mode)
}
// scriptSuffix adalah akhiran berkas script asli di samping wrapper.
const scriptSuffix = ".real"
// writeScriptWrapper menulis wrapper yang menyiapkan PATH dan folder kerja
// 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\n" +
`PATH="$HOME/.local/bin:$PATH"` + "\n" +
"export PATH\n"
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)
}
// shellQuote mengutip nilai untuk dipakai di dalam script shell.
func shellQuote(value string) string {
return "'" + strings.ReplaceAll(value, "'", `'\''`) + "'"
}