5 Commits
Author SHA1 Message Date
oxpoe e771c00446 rebrand: ganti sisa teks 'GitHub' -> 'Gitea' (UI, komentar, dokumen)
CI / test (push) Successful in 1m0s
2026-10-14 21:09:03 +07:00
oxpoe 651fd0fc3d rebrand: arahkan tautan, badge, label UI & cek-update ke Gitea (bukan GitHub)
CI / test (push) Successful in 1m34s
2026-10-14 21:08:03 +07:00
oxpoe 2bfed9bea3 docs: banner mirror (repo asli di Gitea)
CI / test (push) Successful in 5m0s
2026-10-14 12:30:38 +07:00
oxpoe 63869e9158 Deteksi aplikasi terminal (CLI/TUI) + perbaikan shortcut lazydocker
- internal/shortcut/terminal.go: RunsInTerminal() menebak dari library dinamis
  (tanpa lib GUI = aplikasi terminal); script selalu true, .exe wine false
- checkbox 'Run in a terminal window' kini muncul untuk semua executable, bukan
  hanya script, dan terisi otomatis saat berkas dipilih
- dialog edit: kalau entri lama belum menyalakan Terminal padahal aplikasinya
  CLI/TUI, muncul catatan kecil sebagai petunjuk
- tes: script/.exe/binary CLI (test binary)/binary GUI (helium)/lazydocker nyata
- kasus nyata: lazydocker & docker-dash terpasang Terminal=false -> TUI gagal
  ('open /dev/tty: no such device or address'); dengan Terminal=true TUI jalan
2026-10-13 10:34:05 +07:00
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
18 changed files with 373 additions and 93 deletions

No files matched your search

+14 -26
View File
@@ -32,35 +32,23 @@ jobs:
- name: Build AppImage - name: Build AppImage
env: env:
# GH_USER/GH_REPO dipakai build.sh untuk menyisipkan info update zsync # build.sh menyisipkan info update zsync yang mengarah ke rilis di Gitea.
# (gh-releases-zsync|<user>|<repo>|latest|lazyrun-*.AppImage.zsync) GITEA_BASE: ${{ github.server_url }}/${{ github.repository }}
GH_USER: ${{ github.repository_owner }}
GH_REPO: ${{ github.event.repository.name }}
run: | run: |
VERSION="${GITHUB_REF_NAME#v}" ./build.sh VERSION="${GITHUB_REF_NAME#v}" ./build.sh
ls -lh dist/lazyrun-*.AppImage* ls -lh dist/lazyrun-*.AppImage*
- name: Create release - name: Create release
env: uses: softprops/action-gh-release@v2
GH_TOKEN: ${{ github.token }} with:
run: | name: LazyRun ${{ github.ref_name }}
cat > /tmp/notes.md <<'NOTES' generate_release_notes: true
Single-file AppImage for Linux x86_64. No root needed, everything stays in body: |
`~/.local/` and `~/.config/lazyrun/`. Single-file AppImage for Linux x86_64. No root needed, everything stays in
`~/.local/` and `~/.config/lazyrun/`.
```bash Already using LazyRun? It checks this release page on startup and offers to
chmod +x lazyrun-*-x86_64.AppImage update itself (delta updates with `AppImageUpdate` when installed).
./lazyrun-*-x86_64.AppImage files: |
``` dist/lazyrun-*.AppImage
dist/lazyrun-*.AppImage.zsync
Already using LazyRun? It checks this release page on startup and offers to
update itself (delta updates with `AppImageUpdate` when installed).
---
NOTES
gh release create "$GITHUB_REF_NAME" \
dist/lazyrun-*.AppImage \
dist/lazyrun-*.AppImage.zsync \
--title "LazyRun ${GITHUB_REF_NAME}" \
--notes-file /tmp/notes.md \
--generate-notes
+17 -8
View File
@@ -1,8 +1,10 @@
> 🪞 **Mirror.** Repo asli & pengembangan ada di **Gitea**: https://gitea.oxpoe.qzz.io/oxpoe/lazyrun
# LazyRun — lazy shortcut build # LazyRun — lazy shortcut build
[![CI](https://github.com/oxpoe/lazyrun/actions/workflows/ci.yml/badge.svg)](https://github.com/oxpoe/lazyrun/actions/workflows/ci.yml) [![CI](https://gitea.oxpoe.qzz.io/oxpoe/lazyrun/actions/workflows/ci.yml/badge.svg)](https://gitea.oxpoe.qzz.io/oxpoe/lazyrun/actions/workflows/ci.yml)
[![Release](https://img.shields.io/github/v/release/oxpoe/lazyrun?label=release)](https://github.com/oxpoe/lazyrun/releases/latest) [![Release](https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fgitea.oxpoe.qzz.io%2Fapi%2Fv1%2Frepos%2Foxpoe%2Flazyrun%2Freleases%2Flatest&query=%24.tag_name&label=release&color=green&logo=gitea)](https://gitea.oxpoe.qzz.io/oxpoe/lazyrun/releases)
[![License](https://img.shields.io/github/license/oxpoe/lazyrun)](LICENSE) [![License](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
![Platform](https://img.shields.io/badge/platform-Linux-blue) ![Platform](https://img.shields.io/badge/platform-Linux-blue)
![UI](https://img.shields.io/badge/UI-Fyne%20%2F%20Go-00ADD8) ![UI](https://img.shields.io/badge/UI-Fyne%20%2F%20Go-00ADD8)
@@ -25,18 +27,25 @@ breathes while the app is open (the koala in `assets/logo.svg` is the mascot).*
<img src="docs/main-light.png" width="49%" alt="LazyRun in light theme"> <img src="docs/main-light.png" width="49%" alt="LazyRun in light theme">
</p> </p>
- **Repo:** <https://github.com/oxpoe/lazyrun> - **Repo:** <https://gitea.oxpoe.qzz.io/oxpoe/lazyrun>
- **Donate:** <https://donasi.oxpoe.qzz.io/> - **Donate:** <https://donasi.oxpoe.qzz.io/>
- **Executable shortcut** — picks an ELF/AppImage/script/Windows `.exe`, copies it to - **Executable shortcut** — picks an ELF/AppImage/script/Windows `.exe`, copies it to
`~/.local/bin/`, installs the icon, writes the `.desktop` entry. `~/.local/bin/`, installs the icon, writes the `.desktop` entry.
- **Terminal apps just work** — CLI/TUI programs (lazydocker, htop, a script) need
`Terminal=true`, while GUI apps must not open a terminal. LazyRun guesses this from
the libraries the binary links (no GUI library → terminal app), pre-fills the
*Run in a terminal window* checkbox, and keeps it editable.
- **Automatic type label** — every shortcut is tagged with what it actually is, - **Automatic type label** — every shortcut is tagged with what it actually is,
detected from the file: `Go`, `AppImage`, `Electron`, `Script`, `Windows`, `Binary` detected from the file: `Go`, `AppImage`, `Electron`, `Script`, `Windows`, `Binary`
(`.exe` files are launched through `wine`), or `PWA` for web apps. (`.exe` files are launched through `wine`), or `PWA` for web apps.
- **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.
@@ -70,7 +79,7 @@ breathes while the app is open (the koala in `assets/logo.svg` is the mascot).*
Needs a desktop Linux with the usual GL/X11 or Wayland libraries (any KDE Plasma or Needs a desktop Linux with the usual GL/X11 or Wayland libraries (any KDE Plasma or
GNOME install has them). No root, nothing outside your home directory. GNOME install has them). No root, nothing outside your home directory.
Download the AppImage from [Releases](https://github.com/oxpoe/lazyrun/releases), then: Download the AppImage from [Releases](https://gitea.oxpoe.qzz.io/oxpoe/lazyrun/releases), then:
```bash ```bash
chmod +x lazyrun-*-x86_64.AppImage chmod +x lazyrun-*-x86_64.AppImage
@@ -119,7 +128,7 @@ start (only when running as an AppImage) and offers to install it:
*Check for Updates* in the footer runs the same flow manually. *Check for Updates* in the footer runs the same flow manually.
**Publishing a release** is just a tag — GitHub Actions builds and uploads **Publishing a release** is just a tag — Gitea Actions builds and uploads
everything (`.github/workflows/release.yml`): everything (`.github/workflows/release.yml`):
```bash ```bash
@@ -158,7 +167,7 @@ GH_USER=oxpoe GH_REPO=lazyrun ./build.sh
Desktop copy, import/export, dark mode. Desktop copy, import/export, dark mode.
- Firefox PWA via `--kiosk`, Flatpak/Snap targets, icon preview. - Firefox PWA via `--kiosk`, Flatpak/Snap targets, icon preview.
Known limitations are tracked as [issues](https://github.com/oxpoe/lazyrun/issues); Known limitations are tracked as [issues](https://gitea.oxpoe.qzz.io/oxpoe/lazyrun/issues);
the main ones: the main ones:
- the type label (`Go`, `Electron`, …) is detected when the shortcut is created, so a - the type label (`Go`, `Electron`, …) is detected when the shortcut is created, so a
+2 -6
View File
@@ -39,12 +39,8 @@ export ARCH
export LINUXDEPLOY_OUTPUT_APP_NAME=lazyrun export LINUXDEPLOY_OUTPUT_APP_NAME=lazyrun
export LINUXDEPLOY_OUTPUT_VERSION="$VERSION" export LINUXDEPLOY_OUTPUT_VERSION="$VERSION"
export LDAI_OUTPUT="$PWD/$OUT/lazyrun-$VERSION-$ARCH.AppImage" export LDAI_OUTPUT="$PWD/$OUT/lazyrun-$VERSION-$ARCH.AppImage"
if [[ -n "${GH_USER:-}" && -n "${GH_REPO:-}" ]]; then GITEA_BASE="${GITEA_BASE:-https://gitea.oxpoe.qzz.io/oxpoe/lazyrun}"
export LDAI_UPDATE_INFORMATION="gh-releases-zsync|$GH_USER|$GH_REPO|latest|lazyrun-*.AppImage.zsync" export LDAI_UPDATE_INFORMATION="zsync|$GITEA_BASE/releases/download/v$VERSION/lazyrun-$VERSION-$ARCH.AppImage.zsync"
else
echo "==> catatan: GH_USER/GH_REPO kosong, AppImage tanpa update info zsync"
export LDAI_NO_APPSTREAM=1
fi
# Library X11/Wayland/xkb/cursor tidak boleh ikut dibundel: kalau versinya beda # Library X11/Wayland/xkb/cursor tidak boleh ikut dibundel: kalau versinya beda
# dengan host, cursor & rendering jadi tidak native (mis. cursor theme desktop tidak terpakai). # dengan host, cursor & rendering jadi tidak native (mis. cursor theme desktop tidak terpakai).
+2
View File
@@ -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"]
+15 -5
View File
@@ -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)
} }
+78
View File
@@ -267,3 +267,81 @@ 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)
}
}
func TestInstallCLIAppGetsTerminalEntry(t *testing.T) {
testEnv(t)
// pakai binary tes ini sendiri: ELF asli tanpa library GUI
self, err := os.Executable()
if err != nil {
t.Fatal(err)
}
src := self
// binary tanpa library GUI -> dianggap aplikasi terminal
terminal := RunsInTerminal(src)
if !terminal {
t.Fatal("binary CLI harus terdeteksi butuh terminal")
}
sc, err := InstallExecutable(ExecOptions{BinaryPath: src, Name: "TUI Tool", Menu: true, Terminal: terminal})
if err != nil {
t.Fatal(err)
}
content, err := os.ReadFile(sc.DesktopPath)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(content), "Terminal=true\n") {
t.Errorf("aplikasi CLI harus Terminal=true:\n%s", content)
}
}
func TestInstallGUIAppKeepsTerminalFalse(t *testing.T) {
home := testEnv(t)
src := fakeBinary(t, home, "gui-tool")
// simulasi aplikasi GUI: tidak butuh terminal
sc, err := InstallExecutable(ExecOptions{BinaryPath: src, Name: "GUI Tool", Menu: true, Terminal: false})
if err != nil {
t.Fatal(err)
}
content, err := os.ReadFile(sc.DesktopPath)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(content), "Terminal=false\n") {
t.Errorf("aplikasi GUI tidak boleh membuka terminal:\n%s", content)
}
}
+5 -1
View File
@@ -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
} }
} }
+48
View File
@@ -0,0 +1,48 @@
package shortcut
import (
"debug/elf"
"strings"
)
// guiLibs adalah library yang menandakan aplikasi berjendela sendiri.
// Kalau binary tidak memakai satu pun dari ini, biasanya ia program terminal.
var guiLibs = []string{
"libX11", "libxcb", "libwayland-client", "libgtk", "libgdk",
"libQt5", "libQt6", "libSDL", "libGL", "libEGL", "libglfw", "libglib",
}
// RunsInTerminal menebak apakah program perlu jendela terminal.
//
// Script dan aplikasi baris perintah butuh Terminal=true di .desktop,
// sedangkan aplikasi GUI tidak (kalau dipaksa, jendela terminal ikut terbuka).
// Tebakannya dari daftar library dinamis: tanpa library GUI dianggap CLI.
func RunsInTerminal(path string) bool {
kind := DetectKind(path)
if kind == KindScript {
return true
}
if kind == KindWindows {
return false // .exe Windows (lewat wine) umumnya GUI
}
f, err := elf.Open(path)
if err != nil {
return false
}
defer f.Close()
libs, err := f.DynString(elf.DT_NEEDED)
if err != nil {
// binary statis (Go murni) tanpa dependency dinamis: kemungkinan CLI
return true
}
for _, lib := range libs {
for _, gui := range guiLibs {
if strings.Contains(lib, gui) {
return false
}
}
}
return true
}
+69
View File
@@ -0,0 +1,69 @@
package shortcut
import (
"os"
"os/exec"
"path/filepath"
"testing"
)
func TestRunsInTerminal(t *testing.T) {
dir := t.TempDir()
script := filepath.Join(dir, "tool.sh")
if err := os.WriteFile(script, []byte("#!/bin/sh\necho hai\n"), 0o755); err != nil {
t.Fatal(err)
}
if !RunsInTerminal(script) {
t.Error("script harus dianggap butuh terminal")
}
exe := filepath.Join(dir, "win.exe")
if err := os.WriteFile(exe, append([]byte("MZ\x90\x00"), make([]byte, 32)...), 0o755); err != nil {
t.Fatal(err)
}
if RunsInTerminal(exe) {
t.Error("aplikasi Windows (wine) tidak perlu terminal")
}
// binary CLI tanpa library GUI: pakai binary tes ini sendiri
self, err := os.Executable()
if err != nil {
t.Fatal(err)
}
if !RunsInTerminal(self) {
t.Error("binary CLI (tanpa lib GUI) harus dianggap butuh terminal")
}
// berkas tidak ada / bukan ELF
if RunsInTerminal(filepath.Join(dir, "tidak-ada")) {
t.Error("berkas tidak ada tidak boleh dianggap butuh terminal")
}
}
func TestRunsInTerminalOnGUIBinary(t *testing.T) {
// pakai aplikasi GUI nyata kalau ada di mesin ini
candidates := []string{"/opt/helium-browser-bin/helium", "/usr/bin/vivaldi"}
for _, path := range candidates {
if _, err := os.Stat(path); err != nil {
continue
}
if RunsInTerminal(path) {
t.Errorf("%s memakai lib GUI, tidak boleh dianggap butuh terminal", path)
}
return
}
t.Skip("tidak ada binary GUI untuk diuji")
}
func TestRunsInTerminalOnRealCLITool(t *testing.T) {
// lazydocker (TUI Go) adalah kasus nyata yang dilaporkan user
if _, err := os.Stat("/home/oxpoe/.local/bin/lazydocker"); err != nil {
t.Skip("lazydocker tidak ada di mesin ini")
}
if !RunsInTerminal("/home/oxpoe/.local/bin/lazydocker") {
t.Error("lazydocker adalah TUI, harus butuh terminal")
}
}
var _ = exec.LookPath
+8 -9
View File
@@ -100,9 +100,6 @@ func (ui *app) showEditDialog(sc store.Shortcut) {
desktopChk.SetChecked(sc.Targets.Desktop) desktopChk.SetChecked(sc.Targets.Desktop)
terminal := widget.NewCheck(T("check_terminal"), nil) terminal := widget.NewCheck(T("check_terminal"), nil)
terminal.SetChecked(sc.Terminal) terminal.SetChecked(sc.Terminal)
if sc.Kind != shortcut.KindScript {
terminal.Hide()
}
browseIcon := newButton(T("browse"), theme.FolderOpenIcon(), T("tip_browse"), widget.MediumImportance, func() { browseIcon := newButton(T("browse"), theme.FolderOpenIcon(), T("tip_browse"), widget.MediumImportance, func() {
pickFile(ui.win, imageFilter, func(path string) { pickFile(ui.win, imageFilter, func(path string) {
@@ -123,12 +120,7 @@ func (ui *app) showEditDialog(sc store.Shortcut) {
pickFile(ui.win, nil, func(path string) { pickFile(ui.win, nil, func(path string) {
binPath = path binPath = path
binLabel.SetText(filepath.Base(path) + " · " + detectedKind(path)) binLabel.SetText(filepath.Base(path) + " · " + detectedKind(path))
if shortcut.DetectKind(path) == shortcut.KindScript { terminal.SetChecked(shortcut.RunsInTerminal(path))
terminal.Show()
} else {
terminal.SetChecked(false)
terminal.Hide()
}
}) })
}) })
category := widget.NewSelect(categories, nil) category := widget.NewSelect(categories, nil)
@@ -220,6 +212,12 @@ func (ui *app) showEditDialog(sc store.Shortcut) {
}) })
hint := textLine(T("edit_hint"), 12, false, colorNameMuted) hint := textLine(T("edit_hint"), 12, false, colorNameMuted)
// beberapa entri lama belum menyalakan Terminal walau aplikasinya CLI/TUI
terminalSuggest := container.NewVBox()
if !sc.Terminal && shortcut.RunsInTerminal(sc.BinaryPath) {
terminalSuggest.Add(textLine(T("terminal_suggest"), 12, false, colorNameChipDesk))
}
content := container.NewVScroll(container.NewVBox( content := container.NewVScroll(container.NewVBox(
closeRow(func() { d.Hide() }), closeRow(func() { d.Hide() }),
widget.NewForm(items...), widget.NewForm(items...),
@@ -228,6 +226,7 @@ func (ui *app) showEditDialog(sc store.Shortcut) {
menu, menu,
desktopChk, desktopChk,
terminal, terminal,
terminalSuggest,
hint, hint,
container.NewHBox(logoBox(newLogo(), 20), spacer(), container.NewHBox(logoBox(newLogo(), 20), spacer(),
newButton(T("cancel"), nil, T("tip_cancel"), widget.MediumImportance, func() { d.Hide() }), newButton(T("cancel"), nil, T("tip_cancel"), widget.MediumImportance, func() { d.Hide() }),
+3 -8
View File
@@ -41,19 +41,14 @@ func (ui *app) showExecutableDialog() {
desktop := widget.NewCheck(T("check_desktop"), nil) desktop := widget.NewCheck(T("check_desktop"), nil)
desktop.SetChecked(true) desktop.SetChecked(true)
terminal := widget.NewCheck(T("check_terminal"), nil) 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() { browseBin := newButton(T("browse"), theme.FolderOpenIcon(), T("tip_browse"), widget.MediumImportance, func() {
pickFile(ui.win, nil, func(path string) { pickFile(ui.win, nil, func(path string) {
binPath = path binPath = path
binLabel.SetText(filepath.Base(path) + " · " + detectedKind(path)) binLabel.SetText(filepath.Base(path) + " · " + detectedKind(path))
// terminal hanya perlu untuk script; tampilkan kalau terdeteksi // aplikasi terminal (TUI/CLI/script) perlu Terminal=true;
if shortcut.DetectKind(path) == shortcut.KindScript { // tebakannya dari library yang dipakai binary
terminal.Show() terminal.SetChecked(shortcut.RunsInTerminal(path))
} else {
terminal.SetChecked(false)
terminal.Hide()
}
if strings.TrimSpace(nameEntry.Text) == "" { if strings.TrimSpace(nameEntry.Text) == "" {
nameEntry.SetText(appNameFromFile(path)) nameEntry.SetText(appNameFromFile(path))
} }
+62
View File
@@ -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")
}
}
+32 -13
View File
@@ -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"
@@ -10,9 +12,9 @@ import (
"lazyrun/internal/store" "lazyrun/internal/store"
) )
// Link aplikasi: repo GitHub LazyRun dan halaman donasi. // Link aplikasi: repo Gitea LazyRun dan halaman donasi.
const ( const (
repoURL = "https://github.com/oxpoe/lazyrun" repoURL = "https://gitea.oxpoe.qzz.io/oxpoe/lazyrun"
donateURL = "https://donasi.oxpoe.qzz.io/" donateURL = "https://donasi.oxpoe.qzz.io/"
) )
@@ -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")
}
+3 -2
View File
@@ -49,7 +49,8 @@ var tr = map[string][2]string{
"section_location": {"Shortcut location", "Lokasi shortcut"}, "section_location": {"Shortcut location", "Lokasi shortcut"},
"check_menu": {"Application menu", "Menu aplikasi"}, "check_menu": {"Application menu", "Menu aplikasi"},
"check_desktop": {"Desktop", "Desktop"}, "check_desktop": {"Desktop", "Desktop"},
"check_terminal": {"Run in a terminal window (scripts)", "Jalankan di jendela terminal (script)"}, "check_terminal": {"Run in a terminal window (CLI/TUI apps)", "Jalankan di jendela terminal (aplikasi CLI/TUI)"},
"terminal_suggest": {"This looks like a terminal app - tick the box above if it closes right away.", "Ini kelihatan aplikasi terminal - centang kotak di atas kalau menutup begitu saja."},
"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."}, "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."}, "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."},
@@ -67,7 +68,7 @@ var tr = map[string][2]string{
"need_target": {"Pick at least one location: menu or desktop.", "Pilih minimal satu lokasi: menu atau desktop."}, "need_target": {"Pick at least one location: menu or desktop.", "Pilih minimal satu lokasi: menu atau desktop."},
"no_browser": {"No supported browser found. Install Chrome, Chromium, Brave, Edge, Vivaldi, or Firefox first.", "Browser yang didukung tidak ditemukan. Install Chrome, Chromium, Brave, Edge, Vivaldi, atau Firefox dulu."}, "no_browser": {"No supported browser found. Install Chrome, Chromium, Brave, Edge, Vivaldi, or Firefox first.", "Browser yang didukung tidak ditemukan. Install Chrome, Chromium, Brave, Edge, Vivaldi, atau Firefox dulu."},
"github": {"GitHub", "GitHub"}, "github": {"Gitea", "Gitea"},
"donate": {"Donate", "Donasi"}, "donate": {"Donate", "Donasi"},
"check_updates": {"Check for Updates", "Cek Update"}, "check_updates": {"Check for Updates", "Cek Update"},
+2 -2
View File
@@ -57,7 +57,7 @@ func (ui *app) scheduleUpdateCheck() {
}() }()
} }
// checkForUpdate mengambil rilis terbaru dari GitHub. manual=false hanya // checkForUpdate mengambil rilis terbaru dari Gitea. manual=false hanya
// memberitahu kalau memang ada versi baru. // memberitahu kalau memang ada versi baru.
func (ui *app) checkForUpdate(manual bool) { func (ui *app) checkForUpdate(manual bool) {
if ui.version == "" || ui.version == "dev" { if ui.version == "" || ui.version == "dev" {
@@ -97,7 +97,7 @@ func (ui *app) markUpdatePending(rel update.Release) {
// showUpdateDialog menawarkan update: unduh sekarang, buka halaman rilis, atau nanti. // showUpdateDialog menawarkan update: unduh sekarang, buka halaman rilis, atau nanti.
func (ui *app) showUpdateDialog(rel update.Release) { func (ui *app) showUpdateDialog(rel update.Release) {
// catatan rilis GitHub berformat markdown; render supaya blok kode/daftar // catatan rilis Gitea berformat markdown; render supaya blok kode/daftar
// tidak tampil sebagai teks mentah // tidak tampil sebagai teks mentah
notes := widget.NewRichTextFromMarkdown(rel.Notes) notes := widget.NewRichTextFromMarkdown(rel.Notes)
+8 -8
View File
@@ -1,4 +1,4 @@
// Package update memeriksa rilis terbaru LazyRun di GitHub dan memasangnya. // Package update memeriksa rilis terbaru LazyRun di Gitea dan memasangnya.
package update package update
import ( import (
@@ -14,8 +14,8 @@ import (
) )
var ( var (
apiBase = "https://api.github.com/repos/oxpoe/lazyrun/releases" apiBase = "https://gitea.oxpoe.qzz.io/api/v1/repos/oxpoe/lazyrun/releases"
repoURL = "https://github.com/oxpoe/lazyrun" repoURL = "https://gitea.oxpoe.qzz.io/oxpoe/lazyrun"
) )
// LAZYRUN_UPDATE_API bisa dipakai untuk mengarahkan cek update ke server lain // LAZYRUN_UPDATE_API bisa dipakai untuk mengarahkan cek update ke server lain
@@ -38,22 +38,22 @@ func RepoURL() string { return repoURL }
type Release struct { type Release struct {
Tag string // tag asli, mis. "v1.1.0" Tag string // tag asli, mis. "v1.1.0"
Version string // versi tanpa awalan v, mis. "1.1.0" Version string // versi tanpa awalan v, mis. "1.1.0"
Notes string // catatan rilis (markdown GitHub) Notes string // catatan rilis (markdown Gitea)
PageURL string // halaman rilis di GitHub PageURL string // halaman rilis di Gitea
AppImage string // URL unduhan AppImage (kosong kalau tidak ada) AppImage string // URL unduhan AppImage (kosong kalau tidak ada)
Zsync string // URL berkas .zsync (opsional) Zsync string // URL berkas .zsync (opsional)
} }
var client = &http.Client{Timeout: 20 * time.Second} var client = &http.Client{Timeout: 20 * time.Second}
// Latest mengambil rilis terbaru dari GitHub. // Latest mengambil rilis terbaru dari Gitea.
func Latest() (Release, error) { func Latest() (Release, error) {
var rel Release var rel Release
req, err := http.NewRequest(http.MethodGet, apiBase+"/latest", nil) req, err := http.NewRequest(http.MethodGet, apiBase+"/latest", nil)
if err != nil { if err != nil {
return rel, err return rel, err
} }
req.Header.Set("Accept", "application/vnd.github+json") req.Header.Set("Accept", "application/json")
req.Header.Set("User-Agent", "LazyRun") req.Header.Set("User-Agent", "LazyRun")
resp, err := client.Do(req) resp, err := client.Do(req)
@@ -62,7 +62,7 @@ func Latest() (Release, error) {
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
return rel, fmt.Errorf("GitHub menjawab %s", resp.Status) return rel, fmt.Errorf("Gitea menjawab %s", resp.Status)
} }
var payload struct { var payload struct {
+3 -3
View File
@@ -46,12 +46,12 @@ func TestNewerIgnoresSameAndOlder(t *testing.T) {
func TestLatestParsesRelease(t *testing.T) { func TestLatestParsesRelease(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("User-Agent") == "" { if r.Header.Get("User-Agent") == "" {
t.Error("GitHub butuh header User-Agent") t.Error("Gitea butuh header User-Agent")
} }
_, _ = w.Write([]byte(`{ _, _ = w.Write([]byte(`{
"tag_name": "v1.2.0", "tag_name": "v1.2.0",
"body": " - ikon online\n - animasi ", "body": " - ikon online\n - animasi ",
"html_url": "https://github.com/oxpoe/lazyrun/releases/tag/v1.2.0", "html_url": "https://gitea.oxpoe.qzz.io/oxpoe/lazyrun/releases/tag/v1.2.0",
"assets": [ "assets": [
{"name": "lazyrun-1.2.0-x86_64.AppImage.zsync", "browser_download_url": "https://contoh/zsync"}, {"name": "lazyrun-1.2.0-x86_64.AppImage.zsync", "browser_download_url": "https://contoh/zsync"},
{"name": "lazyrun-1.2.0-x86_64.AppImage", "browser_download_url": "https://contoh/appimage"} {"name": "lazyrun-1.2.0-x86_64.AppImage", "browser_download_url": "https://contoh/appimage"}
@@ -61,7 +61,7 @@ func TestLatestParsesRelease(t *testing.T) {
defer srv.Close() defer srv.Close()
SetEndpoint(srv.URL) SetEndpoint(srv.URL)
t.Cleanup(func() { SetEndpoint("https://api.github.com/repos/oxpoe/lazyrun/releases") }) t.Cleanup(func() { SetEndpoint("https://gitea.oxpoe.qzz.io/api/v1/repos/oxpoe/lazyrun/releases") })
rel, err := Latest() rel, err := Latest()
if err != nil { if err != nil {
+2 -2
View File
@@ -127,7 +127,7 @@ Setiap kali membuat shortcut, pengguna memilih lokasi via **checkbox** yang flek
- Bundle AppImage dengan dukungan **zsync**. - Bundle AppImage dengan dukungan **zsync**.
- Menu **"Check for Updates"** memanggil `AppImageUpdate`. - Menu **"Check for Updates"** memanggil `AppImageUpdate`.
- Update otomatis dari GitHub Releases. - Update otomatis dari Gitea Releases.
--- ---
@@ -336,7 +336,7 @@ ln -s usr/bin/lazyrun lazyrun.AppDir/AppRun
# 3. Build AppImage dengan dukungan update # 3. Build AppImage dengan dukungan update
./appimagetool-x86_64.AppImage \ ./appimagetool-x86_64.AppImage \
-u "gh-releases-zsync|user|lazyrun|latest|lazyrun-*.AppImage.zsync" \ -u "zsync|https://gitea.oxpoe.qzz.io/oxpoe/lazyrun/releases/download/<tag>/lazyrun-<ver>-x86_64.AppImage.zsync" \
lazyrun.AppDir lazyrun.AppDir
``` ```