185 lines
5.3 KiB
Go
185 lines
5.3 KiB
Go
//go:build windows
|
|
|
|
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/binary"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
"unicode/utf16"
|
|
)
|
|
|
|
const nativeDialogs = true
|
|
|
|
const createNoWindow = 0x08000000
|
|
|
|
// hideConsole stops child console programs (ffmpeg, powershell) from opening
|
|
// a console window. HideWindow is deliberately not set: Windows would apply
|
|
// SW_HIDE to the first window the child shows, i.e. the file dialog.
|
|
func hideConsole(cmd *exec.Cmd) {
|
|
cmd.SysProcAttr = &syscall.SysProcAttr{CreationFlags: createNoWindow}
|
|
}
|
|
|
|
func listRoots() []string {
|
|
mask, _, _ := syscall.NewLazyDLL("kernel32.dll").NewProc("GetLogicalDrives").Call()
|
|
var roots []string
|
|
for i := 0; i < 26; i++ {
|
|
if mask&(1<<uint(i)) != 0 {
|
|
roots = append(roots, string(rune('A'+i))+`:\`)
|
|
}
|
|
}
|
|
return roots
|
|
}
|
|
|
|
// openBrowser prefers a chromeless Edge (or Chrome) "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 {
|
|
var candidates []string
|
|
for _, env := range []string{"ProgramFiles(x86)", "ProgramFiles", "LocalAppData"} {
|
|
base := os.Getenv(env)
|
|
if base == "" {
|
|
continue
|
|
}
|
|
candidates = append(candidates,
|
|
filepath.Join(base, `Microsoft\Edge\Application\msedge.exe`),
|
|
filepath.Join(base, `Google\Chrome\Application\chrome.exe`))
|
|
}
|
|
for _, p := range candidates {
|
|
if _, err := os.Stat(p); err != nil {
|
|
continue
|
|
}
|
|
cmd := exec.Command(p, "--app="+url, "--window-size=1440,920")
|
|
if err := cmd.Start(); err == nil {
|
|
go cmd.Wait()
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
return exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
|
|
}
|
|
|
|
func revealInFileManager(path string) error {
|
|
cmd := exec.Command("explorer.exe")
|
|
cmd.SysProcAttr = &syscall.SysProcAttr{CmdLine: `explorer.exe /select,"` + path + `"`}
|
|
if err := cmd.Start(); err != nil {
|
|
return err
|
|
}
|
|
go cmd.Wait() // explorer exits with status 1 even on success
|
|
return nil
|
|
}
|
|
|
|
func waitBeforeExit() {
|
|
fmt.Fprint(os.Stderr, "\nPress Enter to close...")
|
|
bufio.NewReader(os.Stdin).ReadString('\n')
|
|
}
|
|
|
|
// Native file dialogs are shown through PowerShell + Windows Forms so the
|
|
// program needs no cgo. Values are passed via environment variables, never
|
|
// spliced into the script.
|
|
const psDialogPrelude = `
|
|
$ErrorActionPreference = 'Stop'
|
|
[Console]::OutputEncoding = New-Object System.Text.UTF8Encoding $false
|
|
Add-Type -AssemblyName System.Windows.Forms
|
|
[System.Windows.Forms.Application]::EnableVisualStyles()
|
|
$owner = New-Object System.Windows.Forms.Form
|
|
$owner.TopMost = $true
|
|
$owner.ShowInTaskbar = $false
|
|
`
|
|
|
|
const psOpenDialog = psDialogPrelude + `
|
|
$d = New-Object System.Windows.Forms.OpenFileDialog
|
|
$d.Title = $env:VIBEDIT_TITLE
|
|
$d.Multiselect = $true
|
|
$d.Filter = $env:VIBEDIT_FILTER
|
|
if ($env:VIBEDIT_DIR -and (Test-Path -LiteralPath $env:VIBEDIT_DIR -PathType Container)) { $d.InitialDirectory = $env:VIBEDIT_DIR }
|
|
if ($d.ShowDialog($owner) -eq [System.Windows.Forms.DialogResult]::OK) {
|
|
foreach ($f in $d.FileNames) { [Console]::Out.WriteLine($f) }
|
|
}
|
|
$owner.Dispose()
|
|
`
|
|
|
|
const psSaveDialog = psDialogPrelude + `
|
|
$d = New-Object System.Windows.Forms.SaveFileDialog
|
|
$d.Title = $env:VIBEDIT_TITLE
|
|
$d.Filter = $env:VIBEDIT_FILTER
|
|
$d.OverwritePrompt = $false
|
|
$d.AddExtension = $true
|
|
$d.DefaultExt = $env:VIBEDIT_EXT
|
|
$d.FileName = $env:VIBEDIT_NAME
|
|
if ($env:VIBEDIT_DIR -and (Test-Path -LiteralPath $env:VIBEDIT_DIR -PathType Container)) { $d.InitialDirectory = $env:VIBEDIT_DIR }
|
|
if ($d.ShowDialog($owner) -eq [System.Windows.Forms.DialogResult]::OK) { [Console]::Out.WriteLine($d.FileName) }
|
|
$owner.Dispose()
|
|
`
|
|
|
|
func runPowerShell(script string, env map[string]string) ([]string, error) {
|
|
u := utf16.Encode([]rune(script))
|
|
b := make([]byte, len(u)*2)
|
|
for i, c := range u {
|
|
binary.LittleEndian.PutUint16(b[i*2:], c)
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Hour)
|
|
defer cancel()
|
|
cmd := exec.CommandContext(ctx, "powershell.exe", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-STA",
|
|
"-EncodedCommand", base64.StdEncoding.EncodeToString(b))
|
|
cmd.Env = os.Environ()
|
|
for k, v := range env {
|
|
cmd.Env = append(cmd.Env, k+"="+v)
|
|
}
|
|
hideConsole(cmd)
|
|
out, err := cmd.Output()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var lines []string
|
|
for _, l := range strings.Split(strings.TrimPrefix(string(out), "\uFEFF"), "\n") {
|
|
if l = strings.TrimSpace(l); l != "" {
|
|
lines = append(lines, l)
|
|
}
|
|
}
|
|
return lines, nil
|
|
}
|
|
|
|
func videoFilter() string {
|
|
var pats []string
|
|
for _, e := range mediaExts {
|
|
pats = append(pats, "*."+e)
|
|
}
|
|
return "Video files|" + strings.Join(pats, ";") + "|All files|*.*"
|
|
}
|
|
|
|
func nativeOpenDialog(title, dir string) ([]string, error) {
|
|
return runPowerShell(psOpenDialog, map[string]string{
|
|
"VIBEDIT_TITLE": title,
|
|
"VIBEDIT_FILTER": videoFilter(),
|
|
"VIBEDIT_DIR": dir,
|
|
})
|
|
}
|
|
|
|
func nativeSaveDialog(title, path string) (string, error) {
|
|
ext := strings.TrimPrefix(filepath.Ext(path), ".")
|
|
filter := "All files|*.*"
|
|
if ext != "" {
|
|
filter = strings.ToUpper(ext) + " video|*." + ext + "|" + filter
|
|
}
|
|
lines, err := runPowerShell(psSaveDialog, map[string]string{
|
|
"VIBEDIT_TITLE": title,
|
|
"VIBEDIT_FILTER": filter,
|
|
"VIBEDIT_EXT": ext,
|
|
"VIBEDIT_NAME": filepath.Base(path),
|
|
"VIBEDIT_DIR": filepath.Dir(path),
|
|
})
|
|
if err != nil || len(lines) == 0 {
|
|
return "", err
|
|
}
|
|
return lines[0], nil
|
|
}
|