211 lines
6.2 KiB
Go
211 lines
6.2 KiB
Go
//go:build linux
|
|
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"os/user"
|
|
"path/filepath"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
)
|
|
|
|
// dialogTool is the zenity or kdialog binary used for native file dialogs, or
|
|
// "" when neither is installed (or there is no display) and the UI uses its
|
|
// built-in file browser.
|
|
var dialogTool = findDialogTool()
|
|
|
|
var nativeDialogs = dialogTool != ""
|
|
|
|
func findDialogTool() string {
|
|
if os.Getenv("DISPLAY") == "" && os.Getenv("WAYLAND_DISPLAY") == "" {
|
|
return ""
|
|
}
|
|
order := []string{"zenity", "kdialog"}
|
|
if strings.Contains(strings.ToUpper(os.Getenv("XDG_CURRENT_DESKTOP")), "KDE") {
|
|
order = []string{"kdialog", "zenity"}
|
|
}
|
|
for _, name := range order {
|
|
if p, err := exec.LookPath(name); err == nil {
|
|
return p
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// hideConsole puts child programs (ffmpeg, dialogs) in their own process group
|
|
// so Ctrl+C in the terminal reaches only VibEdit, which then cancels them and
|
|
// cleans up. They are killed if VibEdit itself dies.
|
|
func hideConsole(cmd *exec.Cmd) {
|
|
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true, Pdeathsig: syscall.SIGKILL}
|
|
}
|
|
|
|
// listRoots offers / plus mounted drives for the built-in file browser.
|
|
func listRoots() []string {
|
|
roots := []string{"/"}
|
|
var dirs []string
|
|
if u, err := user.Current(); err == nil {
|
|
dirs = append(dirs, "/run/media/"+u.Username, "/media/"+u.Username)
|
|
}
|
|
dirs = append(dirs, "/mnt")
|
|
for _, d := range dirs {
|
|
des, _ := os.ReadDir(d)
|
|
for _, de := range des {
|
|
if de.IsDir() && !strings.HasPrefix(de.Name(), ".") {
|
|
roots = append(roots, filepath.Join(d, de.Name()))
|
|
}
|
|
}
|
|
}
|
|
return roots
|
|
}
|
|
|
|
// openBrowser prefers a chromeless Chrome/Chromium/Edge/Brave "app" window,
|
|
// which feels like a normal desktop program, and falls back to the default
|
|
// browser.
|
|
func openBrowser(url string, preferDefault bool) error {
|
|
if !preferDefault {
|
|
for _, name := range []string{"google-chrome", "google-chrome-stable", "chromium", "chromium-browser",
|
|
"microsoft-edge", "microsoft-edge-stable", "brave-browser"} {
|
|
p, err := exec.LookPath(name)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if err := startDetached(p, "--app="+url, "--window-size=1440,920"); err == nil {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
return startDetached("xdg-open", url)
|
|
}
|
|
|
|
// startDetached starts a desktop program in its own session, so it neither
|
|
// gets the terminal's Ctrl+C nor is tied to VibEdit's lifetime.
|
|
func startDetached(name string, args ...string) error {
|
|
cmd := exec.Command(name, args...)
|
|
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
|
if err := cmd.Start(); err != nil {
|
|
return err
|
|
}
|
|
go cmd.Wait()
|
|
return nil
|
|
}
|
|
|
|
// revealInFileManager asks the file manager to show the file selected (the
|
|
// FileManager1 D-Bus interface of Nautilus, Dolphin, Nemo, Caja and Thunar)
|
|
// and falls back to opening its folder.
|
|
func revealInFileManager(path string) error {
|
|
if abs, err := filepath.Abs(path); err == nil {
|
|
path = abs
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
err := exec.CommandContext(ctx, "dbus-send", "--session", "--print-reply", "--reply-timeout=4000",
|
|
"--type=method_call", "--dest=org.freedesktop.FileManager1", "/org/freedesktop/FileManager1",
|
|
"org.freedesktop.FileManager1.ShowItems", "array:string:"+fileURI(path), "string:").Run()
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
return startDetached("xdg-open", filepath.Dir(path))
|
|
}
|
|
|
|
// fileURI percent-encodes everything except unreserved characters and slashes
|
|
// (dbus-send would otherwise split the array argument at commas).
|
|
func fileURI(path string) string {
|
|
var b strings.Builder
|
|
b.WriteString("file://")
|
|
for i := 0; i < len(path); i++ {
|
|
c := path[i]
|
|
if 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || '0' <= c && c <= '9' || strings.IndexByte("/-._~", c) >= 0 {
|
|
b.WriteByte(c)
|
|
} else {
|
|
fmt.Fprintf(&b, "%%%02X", c)
|
|
}
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
func waitBeforeExit() {}
|
|
|
|
func isKDialog() bool { return filepath.Base(dialogTool) == "kdialog" }
|
|
|
|
// runDialog runs zenity/kdialog and returns the chosen paths, one per output
|
|
// line. Both exit with status 1 when the user cancels.
|
|
func runDialog(args ...string) ([]string, error) {
|
|
if dialogTool == "" {
|
|
return nil, errors.New("no file dialog program (zenity or kdialog) found")
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Hour)
|
|
defer cancel()
|
|
out, err := command(ctx, dialogTool, args...).Output()
|
|
var exit *exec.ExitError
|
|
if errors.As(err, &exit) && exit.ExitCode() == 1 {
|
|
return nil, nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var lines []string
|
|
for _, l := range strings.Split(string(out), "\n") {
|
|
if l != "" {
|
|
lines = append(lines, l)
|
|
}
|
|
}
|
|
return lines, nil
|
|
}
|
|
|
|
// videoPatterns lists every media extension in lower and upper case, since
|
|
// GTK file filters are case-sensitive.
|
|
func videoPatterns() string {
|
|
var pats []string
|
|
for _, e := range mediaExts {
|
|
pats = append(pats, "*."+e, "*."+strings.ToUpper(e))
|
|
}
|
|
return strings.Join(pats, " ")
|
|
}
|
|
|
|
func nativeOpenDialog(title, dir string) ([]string, error) {
|
|
if !isDir(dir) {
|
|
dir, _ = os.UserHomeDir()
|
|
}
|
|
if isKDialog() {
|
|
return runDialog("--title", title, "--multiple", "--separate-output",
|
|
"--getopenfilename", dir, "Video files ("+videoPatterns()+")\nAll files (*)")
|
|
}
|
|
return runDialog("--file-selection", "--multiple", "--separator=\n", "--title="+title,
|
|
"--filename="+strings.TrimSuffix(dir, "/")+"/",
|
|
"--file-filter=Video files | "+videoPatterns(), "--file-filter=All files | *")
|
|
}
|
|
|
|
func nativeSaveDialog(title, path string) (string, error) {
|
|
ext := strings.TrimPrefix(filepath.Ext(path), ".")
|
|
var lines []string
|
|
var err error
|
|
if isKDialog() {
|
|
filter := "All files (*)"
|
|
if ext != "" {
|
|
filter = strings.ToUpper(ext) + " video (*." + ext + ")\n" + filter
|
|
}
|
|
lines, err = runDialog("--title", title, "--getsavefilename", path, filter)
|
|
} else {
|
|
args := []string{"--file-selection", "--save", "--title=" + title, "--filename=" + path}
|
|
if ext != "" {
|
|
args = append(args, "--file-filter="+strings.ToUpper(ext)+" video | *."+ext)
|
|
}
|
|
lines, err = runDialog(append(args, "--file-filter=All files | *")...)
|
|
}
|
|
if err != nil || len(lines) == 0 {
|
|
return "", err
|
|
}
|
|
// Like the Windows dialog, add the default extension when none was typed.
|
|
p := lines[0]
|
|
if ext != "" && filepath.Ext(p) == "" {
|
|
p += "." + ext
|
|
}
|
|
return p, nil
|
|
}
|