Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
20d15efc3b | ||
|
|
dfdab88b43 | ||
|
|
e771c00446 | ||
|
|
651fd0fc3d | ||
|
|
2bfed9bea3 | ||
|
|
63869e9158 | ||
|
|
35e83dba09 |
No files matched your search
@@ -32,35 +32,28 @@ 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:
|
env:
|
||||||
GH_TOKEN: ${{ github.token }}
|
TOKEN: ${{ github.token }}
|
||||||
run: |
|
run: |
|
||||||
cat > /tmp/notes.md <<'NOTES'
|
API="${GITHUB_API_URL:-${GITHUB_SERVER_URL}/api/v1}"
|
||||||
|
TAG="${GITHUB_REF_NAME}"
|
||||||
|
cat > /tmp/body.md <<'EOF'
|
||||||
Single-file AppImage for Linux x86_64. No root needed, everything stays in
|
Single-file AppImage for Linux x86_64. No root needed, everything stays in
|
||||||
`~/.local/` and `~/.config/lazyrun/`.
|
`~/.local/` and `~/.config/lazyrun/`.
|
||||||
|
|
||||||
```bash
|
Already using LazyRun? It upgrades itself from this release page.
|
||||||
chmod +x lazyrun-*-x86_64.AppImage
|
EOF
|
||||||
./lazyrun-*-x86_64.AppImage
|
python3 -c 'import json,sys;print(json.dumps({"tag_name":sys.argv[1],"name":"LazyRun "+sys.argv[1],"body":open(sys.argv[2]).read()}))' "$TAG" /tmp/body.md > /tmp/rel.json
|
||||||
```
|
RID=$(curl -sf -X POST -H "Authorization: token $TOKEN" -H "Content-Type: application/json" "$API/repos/$GITHUB_REPOSITORY/releases" --data @/tmp/rel.json | python3 -c 'import json,sys;print(json.load(sys.stdin)["id"])')
|
||||||
|
echo "release id=$RID"
|
||||||
Already using LazyRun? It checks this release page on startup and offers to
|
for f in dist/lazyrun-*.AppImage dist/lazyrun-*.AppImage.zsync; do
|
||||||
update itself (delta updates with `AppImageUpdate` when installed).
|
[ -f "$f" ] || continue
|
||||||
|
curl -sf -X POST -H "Authorization: token $TOKEN" -F "attachment=@$f" "$API/repos/$GITHUB_REPOSITORY/releases/$RID/assets?name=$(basename "$f")" -o /dev/null
|
||||||
---
|
done
|
||||||
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
|
|
||||||
@@ -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
|
||||||
|
|
||||||
[](https://github.com/oxpoe/lazyrun/actions/workflows/ci.yml)
|
[](https://gitea.oxpoe.qzz.io/oxpoe/lazyrun/actions/workflows/ci.yml)
|
||||||
[](https://github.com/oxpoe/lazyrun/releases/latest)
|
[](https://gitea.oxpoe.qzz.io/oxpoe/lazyrun/releases)
|
||||||
[](LICENSE)
|
[](LICENSE)
|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|||||||
+1
-1
@@ -12,5 +12,5 @@ var version = "dev"
|
|||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
cursor.Apply() // sebelum app dibuat: GLFW membaca env cursor saat init
|
cursor.Apply() // sebelum app dibuat: GLFW membaca env cursor saat init
|
||||||
ui.Run(app.NewWithID("io.github.lazyrun.LazyRun"), version)
|
ui.Run(app.NewWithID("io.oxpoe.lazyrun.LazyRun"), version)
|
||||||
}
|
}
|
||||||
@@ -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,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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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() }),
|
||||||
|
|||||||
@@ -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))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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")
|
||||||
|
}
|
||||||
@@ -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"},
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user