Files
vibedit/platform_windows.go
Sean MTS c6ac7d2222 Initial commit
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 02:28:47 +00:00

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
}