Linux versino and Gif output
This commit is contained in:
210
platform_linux.go
Normal file
210
platform_linux.go
Normal file
@@ -0,0 +1,210 @@
|
||||
//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
|
||||
}
|
||||
Reference in New Issue
Block a user