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
This commit is contained in:
1 parent
35e83dba09
commit
163dbb7608
7 files changed
+180
-18
No files matched your search
@@ -30,6 +30,10 @@ breathes while the app is open (the koala in `assets/logo.svg` is the mascot).*
|
|||||||
|
|
||||||
- **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.
|
||||||
|
|||||||
@@ -299,3 +299,49 @@ func TestScriptWrapperPrefersOriginalFile(t *testing.T) {
|
|||||||
t.Errorf("wrapper tidak menyiapkan PATH:\n%s", text)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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."},
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user