Switch remote deploy to vendored source builds

Move remote deployment to a vendored source bundle built on the target host via Docker so redeploys no longer require local cross-compilation or host Go installation.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
GitHub Actions
2026-05-08 12:19:18 +08:00
parent bb27566e38
commit c1a0fe2949
1320 changed files with 497125 additions and 11 deletions

View File

@@ -0,0 +1,72 @@
// Cross-platform.
// Common File Dialogs
package cfd
type Dialog interface {
// Show the dialog to the user.
// Blocks until the user has closed the dialog.
Show() error
// Sets the dialog's parent window. Use 0 to set the dialog to have no parent window.
SetParentWindowHandle(hwnd uintptr)
// Show the dialog to the user.
// Blocks until the user has closed the dialog and returns their selection.
// Returns an error if the user cancelled the dialog.
// Do not use for the Open Multiple Files dialog. Use ShowAndGetResults instead.
ShowAndGetResult() (string, error)
// Sets the title of the dialog window.
SetTitle(title string) error
// Sets the "role" of the dialog. This is used to derive the dialog's GUID, which the
// OS will use to differentiate it from dialogs that are intended for other purposes.
// This means that, for example, a dialog with role "Import" will have a different
// previous location that it will open to than a dialog with role "Open". Can be any string.
SetRole(role string) error
// Sets the folder used as a default if there is not a recently used folder value available
SetDefaultFolder(defaultFolder string) error
// Sets the folder that the dialog always opens to.
// If this is set, it will override the "default folder" behaviour and the dialog will always open to this folder.
SetFolder(folder string) error
// Gets the selected file or folder path, as an absolute path eg. "C:\Folder\file.txt"
// Do not use for the Open Multiple Files dialog. Use GetResults instead.
GetResult() (string, error)
// Sets the file name, I.E. the contents of the file name text box.
// For Select Folder Dialog, sets folder name.
SetFileName(fileName string) error
// Release the resources allocated to this Dialog.
// Should be called when the dialog is finished with.
Release() error
}
type FileDialog interface {
Dialog
// Set the list of file filters that the user can select.
SetFileFilters(fileFilter []FileFilter) error
// Set the selected item from the list of file filters (set using SetFileFilters) by its index. Defaults to 0 (the first item in the list) if not called.
SetSelectedFileFilterIndex(index uint) error
// Sets the default extension applied when a user does not provide one as part of the file name.
// If the user selects a different file filter, the default extension will be automatically updated to match the new file filter.
// For Open / Open Multiple File Dialog, this only has an effect when the user specifies a file name with no extension and a file with the default extension exists.
// For Save File Dialog, this extension will be used whenever a user does not specify an extension.
SetDefaultExtension(defaultExtension string) error
}
type OpenFileDialog interface {
FileDialog
}
type OpenMultipleFilesDialog interface {
FileDialog
// Show the dialog to the user.
// Blocks until the user has closed the dialog and returns the selected files.
ShowAndGetResults() ([]string, error)
// Gets the selected file paths, as absolute paths eg. "C:\Folder\file.txt"
GetResults() ([]string, error)
}
type SelectFolderDialog interface {
Dialog
}
type SaveFileDialog interface { // TODO Properties
FileDialog
}

View File

@@ -0,0 +1,28 @@
//go:build !windows
// +build !windows
package cfd
import "fmt"
var unsupportedError = fmt.Errorf("common file dialogs are only available on windows")
// TODO doc
func NewOpenFileDialog(config DialogConfig) (OpenFileDialog, error) {
return nil, unsupportedError
}
// TODO doc
func NewOpenMultipleFilesDialog(config DialogConfig) (OpenMultipleFilesDialog, error) {
return nil, unsupportedError
}
// TODO doc
func NewSelectFolderDialog(config DialogConfig) (SelectFolderDialog, error) {
return nil, unsupportedError
}
// TODO doc
func NewSaveFileDialog(config DialogConfig) (SaveFileDialog, error) {
return nil, unsupportedError
}

View File

@@ -0,0 +1,79 @@
//go:build windows
// +build windows
package cfd
import "github.com/go-ole/go-ole"
func initialize() {
// Swallow error
_ = ole.CoInitializeEx(0, ole.COINIT_APARTMENTTHREADED|ole.COINIT_DISABLE_OLE1DDE)
}
// TODO doc
func NewOpenFileDialog(config DialogConfig) (OpenFileDialog, error) {
initialize()
openDialog, err := newIFileOpenDialog()
if err != nil {
return nil, err
}
err = config.apply(openDialog)
if err != nil {
return nil, err
}
return openDialog, nil
}
// TODO doc
func NewOpenMultipleFilesDialog(config DialogConfig) (OpenMultipleFilesDialog, error) {
initialize()
openDialog, err := newIFileOpenDialog()
if err != nil {
return nil, err
}
err = config.apply(openDialog)
if err != nil {
return nil, err
}
err = openDialog.setIsMultiselect(true)
if err != nil {
return nil, err
}
return openDialog, nil
}
// TODO doc
func NewSelectFolderDialog(config DialogConfig) (SelectFolderDialog, error) {
initialize()
openDialog, err := newIFileOpenDialog()
if err != nil {
return nil, err
}
err = config.apply(openDialog)
if err != nil {
return nil, err
}
err = openDialog.setPickFolders(true)
if err != nil {
return nil, err
}
return openDialog, nil
}
// TODO doc
func NewSaveFileDialog(config DialogConfig) (SaveFileDialog, error) {
initialize()
saveDialog, err := newIFileSaveDialog()
if err != nil {
return nil, err
}
err = config.apply(saveDialog)
if err != nil {
return nil, err
}
return saveDialog, nil
}

View File

@@ -0,0 +1,141 @@
// Cross-platform.
package cfd
import (
"fmt"
"os"
"reflect"
)
type FileFilter struct {
// The display name of the filter (That is shown to the user)
DisplayName string
// The filter pattern. Eg. "*.txt;*.png" to select all txt and png files, "*.*" to select any files, etc.
Pattern string
}
// Never obfuscate the FileFilter type.
var _ = reflect.TypeOf(FileFilter{})
type DialogConfig struct {
// The title of the dialog
Title string
// The role of the dialog. This is used to derive the dialog's GUID, which the
// OS will use to differentiate it from dialogs that are intended for other purposes.
// This means that, for example, a dialog with role "Import" will have a different
// previous location that it will open to than a dialog with role "Open". Can be any string.
Role string
// The default folder - the folder that is used the first time the user opens it
// (after the first time their last used location is used).
DefaultFolder string
// The initial folder - the folder that the dialog always opens to if not empty.
// If this is not empty, it will override the "default folder" behaviour and
// the dialog will always open to this folder.
Folder string
// The file filters that restrict which types of files the dialog is able to choose.
// Ignored by Select Folder Dialog.
FileFilters []FileFilter
// Sets the initially selected file filter. This is an index of FileFilters.
// Ignored by Select Folder Dialog.
SelectedFileFilterIndex uint
// The initial name of the file (I.E. the text in the file name text box) when the user opens the dialog.
// For the Select Folder Dialog, this sets the initial folder name.
FileName string
// The default extension applied when a user does not provide one as part of the file name.
// If the user selects a different file filter, the default extension will be automatically updated to match the new file filter.
// For Open / Open Multiple File Dialog, this only has an effect when the user specifies a file name with no extension and a file with the default extension exists.
// For Save File Dialog, this extension will be used whenever a user does not specify an extension.
// Ignored by Select Folder Dialog.
DefaultExtension string
// ParentWindowHandle is the handle (HWND) to the parent window of the dialog.
// If left as 0 / nil, the dialog will have no parent window.
ParentWindowHandle uintptr
}
var defaultFilters = []FileFilter{
{
DisplayName: "All Files (*.*)",
Pattern: "*.*",
},
}
func (config *DialogConfig) apply(dialog Dialog) (err error) {
if config.Title != "" {
err = dialog.SetTitle(config.Title)
if err != nil {
return
}
}
if config.Role != "" {
err = dialog.SetRole(config.Role)
if err != nil {
return
}
}
if config.Folder != "" {
_, err = os.Stat(config.Folder)
if err != nil {
return
}
err = dialog.SetFolder(config.Folder)
if err != nil {
return
}
}
if config.DefaultFolder != "" {
_, err = os.Stat(config.DefaultFolder)
if err != nil {
return
}
err = dialog.SetDefaultFolder(config.DefaultFolder)
if err != nil {
return
}
}
if config.FileName != "" {
err = dialog.SetFileName(config.FileName)
if err != nil {
return
}
}
dialog.SetParentWindowHandle(config.ParentWindowHandle)
if dialog, ok := dialog.(FileDialog); ok {
var fileFilters []FileFilter
if config.FileFilters != nil && len(config.FileFilters) > 0 {
fileFilters = config.FileFilters
} else {
fileFilters = defaultFilters
}
err = dialog.SetFileFilters(fileFilters)
if err != nil {
return
}
if config.SelectedFileFilterIndex != 0 {
if config.SelectedFileFilterIndex > uint(len(fileFilters)) {
err = fmt.Errorf("selected file filter index out of range")
return
}
err = dialog.SetSelectedFileFilterIndex(config.SelectedFileFilterIndex)
if err != nil {
return
}
}
if config.DefaultExtension != "" {
err = dialog.SetDefaultExtension(config.DefaultExtension)
if err != nil {
return
}
}
}
return
}

View File

@@ -0,0 +1,9 @@
package cfd
import "errors"
var (
ErrCancelled = errors.New("cancelled by user")
ErrInvalidGUID = errors.New("guid cannot be nil")
ErrEmptyFilters = errors.New("must specify at least one filter")
)

View File

@@ -0,0 +1,200 @@
//go:build windows
// +build windows
package cfd
import (
"github.com/go-ole/go-ole"
"github.com/google/uuid"
"syscall"
"unsafe"
)
var (
fileOpenDialogCLSID = ole.NewGUID("{DC1C5A9C-E88A-4dde-A5A1-60F82A20AEF7}")
fileOpenDialogIID = ole.NewGUID("{d57c7288-d4ad-4768-be02-9d969532d960}")
)
type iFileOpenDialog struct {
vtbl *iFileOpenDialogVtbl
parentWindowHandle uintptr
}
type iFileOpenDialogVtbl struct {
iFileDialogVtbl
GetResults uintptr // func (ppenum **IShellItemArray) HRESULT
GetSelectedItems uintptr
}
func newIFileOpenDialog() (*iFileOpenDialog, error) {
if unknown, err := ole.CreateInstance(fileOpenDialogCLSID, fileOpenDialogIID); err == nil {
return (*iFileOpenDialog)(unsafe.Pointer(unknown)), nil
} else {
return nil, err
}
}
func (fileOpenDialog *iFileOpenDialog) Show() error {
return fileOpenDialog.vtbl.show(unsafe.Pointer(fileOpenDialog), fileOpenDialog.parentWindowHandle)
}
func (fileOpenDialog *iFileOpenDialog) SetParentWindowHandle(hwnd uintptr) {
fileOpenDialog.parentWindowHandle = hwnd
}
func (fileOpenDialog *iFileOpenDialog) ShowAndGetResult() (string, error) {
isMultiselect, err := fileOpenDialog.isMultiselect()
if err != nil {
return "", err
}
if isMultiselect {
// We should panic as this error is caused by the developer using the library
panic("use ShowAndGetResults for open multiple files dialog")
}
if err := fileOpenDialog.Show(); err != nil {
return "", err
}
return fileOpenDialog.GetResult()
}
func (fileOpenDialog *iFileOpenDialog) ShowAndGetResults() ([]string, error) {
isMultiselect, err := fileOpenDialog.isMultiselect()
if err != nil {
return nil, err
}
if !isMultiselect {
// We should panic as this error is caused by the developer using the library
panic("use ShowAndGetResult for open single file dialog")
}
if err := fileOpenDialog.Show(); err != nil {
return nil, err
}
return fileOpenDialog.GetResults()
}
func (fileOpenDialog *iFileOpenDialog) SetTitle(title string) error {
return fileOpenDialog.vtbl.setTitle(unsafe.Pointer(fileOpenDialog), title)
}
func (fileOpenDialog *iFileOpenDialog) GetResult() (string, error) {
isMultiselect, err := fileOpenDialog.isMultiselect()
if err != nil {
return "", err
}
if isMultiselect {
// We should panic as this error is caused by the developer using the library
panic("use GetResults for open multiple files dialog")
}
return fileOpenDialog.vtbl.getResultString(unsafe.Pointer(fileOpenDialog))
}
func (fileOpenDialog *iFileOpenDialog) Release() error {
return fileOpenDialog.vtbl.release(unsafe.Pointer(fileOpenDialog))
}
func (fileOpenDialog *iFileOpenDialog) SetDefaultFolder(defaultFolderPath string) error {
return fileOpenDialog.vtbl.setDefaultFolder(unsafe.Pointer(fileOpenDialog), defaultFolderPath)
}
func (fileOpenDialog *iFileOpenDialog) SetFolder(defaultFolderPath string) error {
return fileOpenDialog.vtbl.setFolder(unsafe.Pointer(fileOpenDialog), defaultFolderPath)
}
func (fileOpenDialog *iFileOpenDialog) SetFileFilters(filter []FileFilter) error {
return fileOpenDialog.vtbl.setFileTypes(unsafe.Pointer(fileOpenDialog), filter)
}
func (fileOpenDialog *iFileOpenDialog) SetRole(role string) error {
return fileOpenDialog.vtbl.setClientGuid(unsafe.Pointer(fileOpenDialog), StringToUUID(role))
}
// This should only be callable when the user asks for a multi select because
// otherwise they will be given the Dialog interface which does not expose this function.
func (fileOpenDialog *iFileOpenDialog) GetResults() ([]string, error) {
isMultiselect, err := fileOpenDialog.isMultiselect()
if err != nil {
return nil, err
}
if !isMultiselect {
// We should panic as this error is caused by the developer using the library
panic("use GetResult for open single file dialog")
}
return fileOpenDialog.vtbl.getResultsStrings(unsafe.Pointer(fileOpenDialog))
}
func (fileOpenDialog *iFileOpenDialog) SetDefaultExtension(defaultExtension string) error {
return fileOpenDialog.vtbl.setDefaultExtension(unsafe.Pointer(fileOpenDialog), defaultExtension)
}
func (fileOpenDialog *iFileOpenDialog) SetFileName(initialFileName string) error {
return fileOpenDialog.vtbl.setFileName(unsafe.Pointer(fileOpenDialog), initialFileName)
}
func (fileOpenDialog *iFileOpenDialog) SetSelectedFileFilterIndex(index uint) error {
return fileOpenDialog.vtbl.setSelectedFileFilterIndex(unsafe.Pointer(fileOpenDialog), index)
}
func (fileOpenDialog *iFileOpenDialog) setPickFolders(pickFolders bool) error {
const FosPickfolders = 0x20
if pickFolders {
return fileOpenDialog.vtbl.addOption(unsafe.Pointer(fileOpenDialog), FosPickfolders)
} else {
return fileOpenDialog.vtbl.removeOption(unsafe.Pointer(fileOpenDialog), FosPickfolders)
}
}
const FosAllowMultiselect = 0x200
func (fileOpenDialog *iFileOpenDialog) isMultiselect() (bool, error) {
options, err := fileOpenDialog.vtbl.getOptions(unsafe.Pointer(fileOpenDialog))
if err != nil {
return false, err
}
return options&FosAllowMultiselect != 0, nil
}
func (fileOpenDialog *iFileOpenDialog) setIsMultiselect(isMultiselect bool) error {
if isMultiselect {
return fileOpenDialog.vtbl.addOption(unsafe.Pointer(fileOpenDialog), FosAllowMultiselect)
} else {
return fileOpenDialog.vtbl.removeOption(unsafe.Pointer(fileOpenDialog), FosAllowMultiselect)
}
}
func (vtbl *iFileOpenDialogVtbl) getResults(objPtr unsafe.Pointer) (*iShellItemArray, error) {
var shellItemArray *iShellItemArray
ret, _, _ := syscall.SyscallN(vtbl.GetResults,
uintptr(objPtr),
uintptr(unsafe.Pointer(&shellItemArray)),
0)
return shellItemArray, hresultToError(ret)
}
func (vtbl *iFileOpenDialogVtbl) getResultsStrings(objPtr unsafe.Pointer) ([]string, error) {
shellItemArray, err := vtbl.getResults(objPtr)
if err != nil {
return nil, err
}
if shellItemArray == nil {
return nil, ErrCancelled
}
defer shellItemArray.vtbl.release(unsafe.Pointer(shellItemArray))
count, err := shellItemArray.vtbl.getCount(unsafe.Pointer(shellItemArray))
if err != nil {
return nil, err
}
var results []string
for i := uintptr(0); i < count; i++ {
newItem, err := shellItemArray.vtbl.getItemAt(unsafe.Pointer(shellItemArray), i)
if err != nil {
return nil, err
}
results = append(results, newItem)
}
return results, nil
}
func StringToUUID(str string) *ole.GUID {
return ole.NewGUID(uuid.NewSHA1(uuid.Nil, []byte(str)).String())
}

View File

@@ -0,0 +1,92 @@
//go:build windows
// +build windows
package cfd
import (
"github.com/go-ole/go-ole"
"unsafe"
)
var (
saveFileDialogCLSID = ole.NewGUID("{C0B4E2F3-BA21-4773-8DBA-335EC946EB8B}")
saveFileDialogIID = ole.NewGUID("{84bccd23-5fde-4cdb-aea4-af64b83d78ab}")
)
type iFileSaveDialog struct {
vtbl *iFileSaveDialogVtbl
parentWindowHandle uintptr
}
type iFileSaveDialogVtbl struct {
iFileDialogVtbl
SetSaveAsItem uintptr
SetProperties uintptr
SetCollectedProperties uintptr
GetProperties uintptr
ApplyProperties uintptr
}
func newIFileSaveDialog() (*iFileSaveDialog, error) {
if unknown, err := ole.CreateInstance(saveFileDialogCLSID, saveFileDialogIID); err == nil {
return (*iFileSaveDialog)(unsafe.Pointer(unknown)), nil
} else {
return nil, err
}
}
func (fileSaveDialog *iFileSaveDialog) Show() error {
return fileSaveDialog.vtbl.show(unsafe.Pointer(fileSaveDialog), fileSaveDialog.parentWindowHandle)
}
func (fileSaveDialog *iFileSaveDialog) SetParentWindowHandle(hwnd uintptr) {
fileSaveDialog.parentWindowHandle = hwnd
}
func (fileSaveDialog *iFileSaveDialog) ShowAndGetResult() (string, error) {
if err := fileSaveDialog.Show(); err != nil {
return "", err
}
return fileSaveDialog.GetResult()
}
func (fileSaveDialog *iFileSaveDialog) SetTitle(title string) error {
return fileSaveDialog.vtbl.setTitle(unsafe.Pointer(fileSaveDialog), title)
}
func (fileSaveDialog *iFileSaveDialog) GetResult() (string, error) {
return fileSaveDialog.vtbl.getResultString(unsafe.Pointer(fileSaveDialog))
}
func (fileSaveDialog *iFileSaveDialog) Release() error {
return fileSaveDialog.vtbl.release(unsafe.Pointer(fileSaveDialog))
}
func (fileSaveDialog *iFileSaveDialog) SetDefaultFolder(defaultFolderPath string) error {
return fileSaveDialog.vtbl.setDefaultFolder(unsafe.Pointer(fileSaveDialog), defaultFolderPath)
}
func (fileSaveDialog *iFileSaveDialog) SetFolder(defaultFolderPath string) error {
return fileSaveDialog.vtbl.setFolder(unsafe.Pointer(fileSaveDialog), defaultFolderPath)
}
func (fileSaveDialog *iFileSaveDialog) SetFileFilters(filter []FileFilter) error {
return fileSaveDialog.vtbl.setFileTypes(unsafe.Pointer(fileSaveDialog), filter)
}
func (fileSaveDialog *iFileSaveDialog) SetRole(role string) error {
return fileSaveDialog.vtbl.setClientGuid(unsafe.Pointer(fileSaveDialog), StringToUUID(role))
}
func (fileSaveDialog *iFileSaveDialog) SetDefaultExtension(defaultExtension string) error {
return fileSaveDialog.vtbl.setDefaultExtension(unsafe.Pointer(fileSaveDialog), defaultExtension)
}
func (fileSaveDialog *iFileSaveDialog) SetFileName(initialFileName string) error {
return fileSaveDialog.vtbl.setFileName(unsafe.Pointer(fileSaveDialog), initialFileName)
}
func (fileSaveDialog *iFileSaveDialog) SetSelectedFileFilterIndex(index uint) error {
return fileSaveDialog.vtbl.setSelectedFileFilterIndex(unsafe.Pointer(fileSaveDialog), index)
}

View File

@@ -0,0 +1,56 @@
//go:build windows
// +build windows
package cfd
import (
"github.com/go-ole/go-ole"
"syscall"
"unsafe"
)
var (
procSHCreateItemFromParsingName = syscall.NewLazyDLL("Shell32.dll").NewProc("SHCreateItemFromParsingName")
iidShellItem = ole.NewGUID("43826d1e-e718-42ee-bc55-a1e261c37bfe")
)
type iShellItem struct {
vtbl *iShellItemVtbl
}
type iShellItemVtbl struct {
iUnknownVtbl
BindToHandler uintptr
GetParent uintptr
GetDisplayName uintptr // func (sigdnName SIGDN, ppszName *LPWSTR) HRESULT
GetAttributes uintptr
Compare uintptr
}
func newIShellItem(path string) (*iShellItem, error) {
var shellItem *iShellItem
pathPtr := ole.SysAllocString(path)
defer func(v *int16) {
_ = ole.SysFreeString(v)
}(pathPtr)
ret, _, _ := procSHCreateItemFromParsingName.Call(
uintptr(unsafe.Pointer(pathPtr)),
0,
uintptr(unsafe.Pointer(iidShellItem)),
uintptr(unsafe.Pointer(&shellItem)))
return shellItem, hresultToError(ret)
}
func (vtbl *iShellItemVtbl) getDisplayName(objPtr unsafe.Pointer) (string, error) {
var ptr *uint16
ret, _, _ := syscall.SyscallN(vtbl.GetDisplayName,
uintptr(objPtr),
0x80058000, // SIGDN_FILESYSPATH,
uintptr(unsafe.Pointer(&ptr)))
if err := hresultToError(ret); err != nil {
return "", err
}
defer ole.CoTaskMemFree(uintptr(unsafe.Pointer(ptr)))
return ole.LpOleStrToString(ptr), nil
}

View File

@@ -0,0 +1,64 @@
//go:build windows
// +build windows
package cfd
import (
"github.com/go-ole/go-ole"
"syscall"
"unsafe"
)
const (
iidShellItemArrayGUID = "{b63ea76d-1f85-456f-a19c-48159efa858b}"
)
var (
iidShellItemArray *ole.GUID
)
func init() {
iidShellItemArray, _ = ole.IIDFromString(iidShellItemArrayGUID)
}
type iShellItemArray struct {
vtbl *iShellItemArrayVtbl
}
type iShellItemArrayVtbl struct {
iUnknownVtbl
BindToHandler uintptr
GetPropertyStore uintptr
GetPropertyDescriptionList uintptr
GetAttributes uintptr
GetCount uintptr // func (pdwNumItems *DWORD) HRESULT
GetItemAt uintptr // func (dwIndex DWORD, ppsi **IShellItem) HRESULT
EnumItems uintptr
}
func (vtbl *iShellItemArrayVtbl) getCount(objPtr unsafe.Pointer) (uintptr, error) {
var count uintptr
ret, _, _ := syscall.SyscallN(vtbl.GetCount,
uintptr(objPtr),
uintptr(unsafe.Pointer(&count)))
if err := hresultToError(ret); err != nil {
return 0, err
}
return count, nil
}
func (vtbl *iShellItemArrayVtbl) getItemAt(objPtr unsafe.Pointer, index uintptr) (string, error) {
var shellItem *iShellItem
ret, _, _ := syscall.SyscallN(vtbl.GetItemAt,
uintptr(objPtr),
index,
uintptr(unsafe.Pointer(&shellItem)))
if err := hresultToError(ret); err != nil {
return "", err
}
if shellItem == nil {
return "", ErrCancelled
}
defer shellItem.vtbl.release(unsafe.Pointer(shellItem))
return shellItem.vtbl.getDisplayName(unsafe.Pointer(shellItem))
}

View File

@@ -0,0 +1,48 @@
//go:build windows
// +build windows
package cfd
type comDlgFilterSpec struct {
pszName *int16
pszSpec *int16
}
type iUnknownVtbl struct {
QueryInterface uintptr
AddRef uintptr
Release uintptr
}
type iModalWindowVtbl struct {
iUnknownVtbl
Show uintptr // func (hwndOwner HWND) HRESULT
}
type iFileDialogVtbl struct {
iModalWindowVtbl
SetFileTypes uintptr // func (cFileTypes UINT, rgFilterSpec *COMDLG_FILTERSPEC) HRESULT
SetFileTypeIndex uintptr // func(iFileType UINT) HRESULT
GetFileTypeIndex uintptr
Advise uintptr
Unadvise uintptr
SetOptions uintptr // func (fos FILEOPENDIALOGOPTIONS) HRESULT
GetOptions uintptr // func (pfos *FILEOPENDIALOGOPTIONS) HRESULT
SetDefaultFolder uintptr // func (psi *IShellItem) HRESULT
SetFolder uintptr // func (psi *IShellItem) HRESULT
GetFolder uintptr
GetCurrentSelection uintptr
SetFileName uintptr // func (pszName LPCWSTR) HRESULT
GetFileName uintptr
SetTitle uintptr // func(pszTitle LPCWSTR) HRESULT
SetOkButtonLabel uintptr
SetFileNameLabel uintptr
GetResult uintptr // func (ppsi **IShellItem) HRESULT
AddPlace uintptr
SetDefaultExtension uintptr // func (pszDefaultExtension LPCWSTR) HRESULT
// This can only be used from a callback.
Close uintptr
SetClientGuid uintptr // func (guid REFGUID) HRESULT
ClearClientData uintptr
SetFilter uintptr
}

View File

@@ -0,0 +1,224 @@
//go:build windows
package cfd
import (
"github.com/go-ole/go-ole"
"strings"
"syscall"
"unsafe"
)
func hresultToError(hr uintptr) error {
if hr < 0 {
return ole.NewError(hr)
}
return nil
}
func (vtbl *iUnknownVtbl) release(objPtr unsafe.Pointer) error {
ret, _, _ := syscall.SyscallN(vtbl.Release,
uintptr(objPtr),
0)
return hresultToError(ret)
}
func (vtbl *iModalWindowVtbl) show(objPtr unsafe.Pointer, hwnd uintptr) error {
ret, _, _ := syscall.SyscallN(vtbl.Show,
uintptr(objPtr),
hwnd)
return hresultToError(ret)
}
func (vtbl *iFileDialogVtbl) setFileTypes(objPtr unsafe.Pointer, filters []FileFilter) error {
cFileTypes := len(filters)
if cFileTypes < 0 {
return ErrEmptyFilters
}
comDlgFilterSpecs := make([]comDlgFilterSpec, cFileTypes)
for i := 0; i < cFileTypes; i++ {
filter := &filters[i]
comDlgFilterSpecs[i] = comDlgFilterSpec{
pszName: ole.SysAllocString(filter.DisplayName),
pszSpec: ole.SysAllocString(filter.Pattern),
}
}
// Ensure memory is freed after use
defer func() {
for _, spec := range comDlgFilterSpecs {
ole.SysFreeString(spec.pszName)
ole.SysFreeString(spec.pszSpec)
}
}()
ret, _, _ := syscall.SyscallN(vtbl.SetFileTypes,
uintptr(objPtr),
uintptr(cFileTypes),
uintptr(unsafe.Pointer(&comDlgFilterSpecs[0])))
return hresultToError(ret)
}
// Options are:
// FOS_OVERWRITEPROMPT = 0x2,
// FOS_STRICTFILETYPES = 0x4,
// FOS_NOCHANGEDIR = 0x8,
// FOS_PICKFOLDERS = 0x20,
// FOS_FORCEFILESYSTEM = 0x40,
// FOS_ALLNONSTORAGEITEMS = 0x80,
// FOS_NOVALIDATE = 0x100,
// FOS_ALLOWMULTISELECT = 0x200,
// FOS_PATHMUSTEXIST = 0x800,
// FOS_FILEMUSTEXIST = 0x1000,
// FOS_CREATEPROMPT = 0x2000,
// FOS_SHAREAWARE = 0x4000,
// FOS_NOREADONLYRETURN = 0x8000,
// FOS_NOTESTFILECREATE = 0x10000,
// FOS_HIDEMRUPLACES = 0x20000,
// FOS_HIDEPINNEDPLACES = 0x40000,
// FOS_NODEREFERENCELINKS = 0x100000,
// FOS_OKBUTTONNEEDSINTERACTION = 0x200000,
// FOS_DONTADDTORECENT = 0x2000000,
// FOS_FORCESHOWHIDDEN = 0x10000000,
// FOS_DEFAULTNOMINIMODE = 0x20000000,
// FOS_FORCEPREVIEWPANEON = 0x40000000,
// FOS_SUPPORTSTREAMABLEITEMS = 0x80000000
func (vtbl *iFileDialogVtbl) setOptions(objPtr unsafe.Pointer, options uint32) error {
ret, _, _ := syscall.SyscallN(vtbl.SetOptions,
uintptr(objPtr),
uintptr(options))
return hresultToError(ret)
}
func (vtbl *iFileDialogVtbl) getOptions(objPtr unsafe.Pointer) (uint32, error) {
var options uint32
ret, _, _ := syscall.SyscallN(vtbl.GetOptions,
uintptr(objPtr),
uintptr(unsafe.Pointer(&options)))
return options, hresultToError(ret)
}
func (vtbl *iFileDialogVtbl) addOption(objPtr unsafe.Pointer, option uint32) error {
if options, err := vtbl.getOptions(objPtr); err == nil {
return vtbl.setOptions(objPtr, options|option)
} else {
return err
}
}
func (vtbl *iFileDialogVtbl) removeOption(objPtr unsafe.Pointer, option uint32) error {
if options, err := vtbl.getOptions(objPtr); err == nil {
return vtbl.setOptions(objPtr, options&^option)
} else {
return err
}
}
func (vtbl *iFileDialogVtbl) setDefaultFolder(objPtr unsafe.Pointer, path string) error {
shellItem, err := newIShellItem(path)
if err != nil {
return err
}
defer shellItem.vtbl.release(unsafe.Pointer(shellItem))
ret, _, _ := syscall.SyscallN(vtbl.SetDefaultFolder,
uintptr(objPtr),
uintptr(unsafe.Pointer(shellItem)))
return hresultToError(ret)
}
func (vtbl *iFileDialogVtbl) setFolder(objPtr unsafe.Pointer, path string) error {
shellItem, err := newIShellItem(path)
if err != nil {
return err
}
defer shellItem.vtbl.release(unsafe.Pointer(shellItem))
ret, _, _ := syscall.SyscallN(vtbl.SetFolder,
uintptr(objPtr),
uintptr(unsafe.Pointer(shellItem)))
return hresultToError(ret)
}
func (vtbl *iFileDialogVtbl) setTitle(objPtr unsafe.Pointer, title string) error {
titlePtr := ole.SysAllocString(title)
defer ole.SysFreeString(titlePtr) // Ensure the string is freed
ret, _, _ := syscall.SyscallN(vtbl.SetTitle,
uintptr(objPtr),
uintptr(unsafe.Pointer(titlePtr)))
return hresultToError(ret)
}
func (vtbl *iFileDialogVtbl) close(objPtr unsafe.Pointer) error {
ret, _, _ := syscall.SyscallN(vtbl.Close,
uintptr(objPtr))
return hresultToError(ret)
}
func (vtbl *iFileDialogVtbl) getResult(objPtr unsafe.Pointer) (*iShellItem, error) {
var shellItem *iShellItem
ret, _, _ := syscall.SyscallN(vtbl.GetResult,
uintptr(objPtr),
uintptr(unsafe.Pointer(&shellItem)))
return shellItem, hresultToError(ret)
}
func (vtbl *iFileDialogVtbl) getResultString(objPtr unsafe.Pointer) (string, error) {
shellItem, err := vtbl.getResult(objPtr)
if err != nil {
return "", err
}
if shellItem == nil {
return "", ErrCancelled
}
defer shellItem.vtbl.release(unsafe.Pointer(shellItem))
return shellItem.vtbl.getDisplayName(unsafe.Pointer(shellItem))
}
func (vtbl *iFileDialogVtbl) setClientGuid(objPtr unsafe.Pointer, guid *ole.GUID) error {
// Ensure the GUID is not nil
if guid == nil {
return ErrInvalidGUID
}
// Call the SetClientGuid method
ret, _, _ := syscall.SyscallN(vtbl.SetClientGuid,
uintptr(objPtr),
uintptr(unsafe.Pointer(guid)))
// Convert the HRESULT to a Go error
return hresultToError(ret)
}
func (vtbl *iFileDialogVtbl) setDefaultExtension(objPtr unsafe.Pointer, defaultExtension string) error {
// Ensure the string is not empty before accessing the first character
if len(defaultExtension) > 0 && defaultExtension[0] == '.' {
defaultExtension = strings.TrimPrefix(defaultExtension, ".")
}
// Allocate memory for the default extension string
defaultExtensionPtr := ole.SysAllocString(defaultExtension)
defer ole.SysFreeString(defaultExtensionPtr) // Ensure the string is freed
// Call the SetDefaultExtension method
ret, _, _ := syscall.SyscallN(vtbl.SetDefaultExtension,
uintptr(objPtr),
uintptr(unsafe.Pointer(defaultExtensionPtr)))
// Convert the HRESULT to a Go error
return hresultToError(ret)
}
func (vtbl *iFileDialogVtbl) setFileName(objPtr unsafe.Pointer, fileName string) error {
fileNamePtr := ole.SysAllocString(fileName)
defer ole.SysFreeString(fileNamePtr) // Ensure the string is freed
ret, _, _ := syscall.SyscallN(vtbl.SetFileName,
uintptr(objPtr),
uintptr(unsafe.Pointer(fileNamePtr)))
return hresultToError(ret)
}
func (vtbl *iFileDialogVtbl) setSelectedFileFilterIndex(objPtr unsafe.Pointer, index uint) error {
ret, _, _ := syscall.SyscallN(vtbl.SetFileTypeIndex,
uintptr(objPtr),
uintptr(index+1)) // SetFileTypeIndex counts from 1
return hresultToError(ret)
}