Ctrl K
ComunidadGuías…

internal/nativewin/folder_windows.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
//go:build windows

package nativewin

import (
	"errors"
	"fmt"
	"syscall"
	"unsafe"

	"golang.org/x/sys/windows"
)

// The native "pick a folder" dialog, driven through COM.
//
// This is IFileDialog with FOS_PICKFOLDERS, which is the modern Explorer-style
// browser -- the same one Windows' own installers show. The alternative,
// SHBrowseForFolder, is ten lines shorter and looks like Windows 2000: no
// address bar, no typing a path, no pinned places. Since the whole point of the
// window is that the wizard reads as a native app, the dialog it opens cannot
// be the one that gives that away.
//
// Shelling out to PowerShell's FolderBrowserDialog was the other option and is
// worse in every direction: a console flash, a second of startup, and a
// dependency on an execution policy that may be locked down.
const (
	coinitApartmentThreaded = 0x2
	rpcEChangedMode         = 0x80010106

	fosPickFolders     = 0x00000020
	fosForceFileSystem = 0x00000040
	fosPathMustExist   = 0x00000800
	fosNoChangeDir     = 0x00000008

	sigdnFileSysPath = 0x80058000

	// HRESULT_FROM_WIN32(ERROR_CANCELLED). Not an error: it is the user
	// answering "no folder", which the caller reports as an empty string.
	hresultCancelled = 0x800704C7
)

// Vtable slots. IFileOpenDialog inherits IFileDialog inherits IModalWindow
// inherits IUnknown, so the indices are fixed by that chain and are the only
// way to reach the methods without generating COM stubs.
const (
	vtRelease = 2

	vtShow       = 3
	vtSetOptions = 9
	vtSetFolder  = 12
	vtSetTitle   = 17
	vtGetResult  = 20

	vtGetDisplayName = 5 // IShellItem
)

var (
	ole32   = syscall.NewLazyDLL("ole32.dll")
	shell32 = syscall.NewLazyDLL("shell32.dll")

	coInitializeEx              = ole32.NewProc("CoInitializeEx")
	coUninitialize              = ole32.NewProc("CoUninitialize")
	coCreateInstance            = ole32.NewProc("CoCreateInstance")
	coTaskMemFree               = ole32.NewProc("CoTaskMemFree")
	shCreateItemFromParsingName = shell32.NewProc("SHCreateItemFromParsingName")
)

var (
	clsidFileOpenDialog = windows.GUID{Data1: 0xDC1C5A9C, Data2: 0xE88A, Data3: 0x4DDE,
		Data4: [8]byte{0xA5, 0xA1, 0x60, 0xF8, 0x2A, 0x20, 0xAE, 0xF7}}
	iidFileOpenDialog = windows.GUID{Data1: 0xD57C7288, Data2: 0xD4AD, Data3: 0x4768,
		Data4: [8]byte{0xBE, 0x02, 0x9D, 0x96, 0x95, 0x32, 0xD9, 0x60}}
	iidShellItem = windows.GUID{Data1: 0x43826D1E, Data2: 0xE718, Data3: 0x42EE,
		Data4: [8]byte{0xBC, 0x55, 0xA1, 0xE2, 0x61, 0xC3, 0x7B, 0xFE}}
)

// comCall invokes the idx'th method on a COM interface pointer.
//
// Every COM object begins with a pointer to its vtable, and the vtable is a
// flat array of function pointers in interface-declaration order, so a method
// call is two dereferences and a stdcall with the object as the first argument.
func comCall(obj uintptr, idx int, args ...uintptr) uintptr {
	vtbl := *(*uintptr)(unsafe.Pointer(obj))
	fn := *(*uintptr)(unsafe.Pointer(vtbl + uintptr(idx)*unsafe.Sizeof(uintptr(0))))
	ret, _, _ := syscall.SyscallN(fn, append([]uintptr{obj}, args...)...)
	return ret
}

func comRelease(obj uintptr) {
	if obj != 0 {
		comCall(obj, vtRelease)
	}
}

// PickFolder shows the folder browser and returns the chosen path, or "" if the
// user cancelled.
//
// owner makes the dialog modal to the calling window. Passing 0 would let the
// user click back to the window behind a dialog that is still open, which on
// Windows leaves the dialog stranded behind its own parent with no obvious way
// back.
func PickFolder(owner uintptr, title, start string) (string, error) {
	// The caller's thread is already an STA (WebView2 requires one), so this
	// almost always returns S_FALSE -- "already initialised", which still counts
	// as a reference and still has to be balanced. RPC_E_CHANGED_MODE means the
	// thread is an MTA and the call took no reference at all, so it must NOT be.
	hr, _, _ := coInitializeEx.Call(0, coinitApartmentThreaded)
	if uint32(hr) != rpcEChangedMode {
		defer coUninitialize.Call()
	}

	var dlg uintptr
	hr, _, _ = coCreateInstance.Call(
		uintptr(unsafe.Pointer(&clsidFileOpenDialog)),
		0,
		1, // CLSCTX_INPROC_SERVER
		uintptr(unsafe.Pointer(&iidFileOpenDialog)),
		uintptr(unsafe.Pointer(&dlg)),
	)
	if hr != 0 || dlg == 0 {
		return "", fmt.Errorf("could not open the folder browser (0x%08x)", uint32(hr))
	}
	defer comRelease(dlg)

	// FOS_FORCEFILESYSTEM alongside FOS_PICKFOLDERS: without it the dialog
	// happily returns a virtual place like "This PC" or a library, which has no
	// path to install into and would surface as a confusing error later.
	comCall(dlg, vtSetOptions, fosPickFolders|fosForceFileSystem|fosPathMustExist|fosNoChangeDir)

	if title != "" {
		if p, err := windows.UTF16PtrFromString(title); err == nil {
			comCall(dlg, vtSetTitle, uintptr(unsafe.Pointer(p)))
		}
	}
	// Best effort: a start folder that does not exist yet (the proposed install
	// dir usually does not) simply leaves the dialog wherever it would have
	// opened, which is better than refusing to open.
	if item := shellItem(start); item != 0 {
		comCall(dlg, vtSetFolder, item)
		comRelease(item)
	}

	if hr := comCall(dlg, vtShow, owner); hr != 0 {
		if uint32(hr) == hresultCancelled {
			return "", nil
		}
		return "", fmt.Errorf("folder browser failed (0x%08x)", uint32(hr))
	}

	var item uintptr
	if hr := comCall(dlg, vtGetResult, uintptr(unsafe.Pointer(&item))); hr != 0 || item == 0 {
		return "", errors.New("no folder was returned")
	}
	defer comRelease(item)

	var pathPtr *uint16
	if hr := comCall(item, vtGetDisplayName, sigdnFileSysPath,
		uintptr(unsafe.Pointer(&pathPtr))); hr != 0 || pathPtr == nil {
		return "", errors.New("that folder has no file-system path")
	}
	// The string is allocated by the shell, so it is freed with the shell's
	// allocator and not Go's.
	defer coTaskMemFree.Call(uintptr(unsafe.Pointer(pathPtr)))

	return windows.UTF16PtrToString(pathPtr), nil
}

// shellItem resolves a path to an IShellItem, or 0 if it cannot be resolved.
func shellItem(path string) uintptr {
	if path == "" {
		return 0
	}
	p, err := windows.UTF16PtrFromString(path)
	if err != nil {
		return 0
	}
	var item uintptr
	hr, _, _ := shCreateItemFromParsingName.Call(
		uintptr(unsafe.Pointer(p)),
		0,
		uintptr(unsafe.Pointer(&iidShellItem)),
		uintptr(unsafe.Pointer(&item)),
	)
	if hr != 0 {
		return 0
	}
	return item
}