Release 3.8.2.12: export/import full server list as JSON.
Save and replace profiles via system file dialog; buttons after Best; prefs unchanged on import. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
package filedialog
|
||||
|
||||
import "errors"
|
||||
|
||||
// ErrCanceled is returned when the user closes the dialog without choosing a file.
|
||||
var ErrCanceled = errors.New("filedialog: canceled")
|
||||
|
||||
// ErrUnsupported is returned on platforms without a native picker.
|
||||
var ErrUnsupported = errors.New("filedialog: not supported on this platform")
|
||||
@@ -0,0 +1,79 @@
|
||||
//go:build darwin
|
||||
|
||||
package filedialog
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func appleScriptQuote(s string) string {
|
||||
s = strings.ReplaceAll(s, `\`, `\\`)
|
||||
return strings.ReplaceAll(s, `"`, `\"`)
|
||||
}
|
||||
|
||||
// SaveJSON shows a Save As dialog for a .json file.
|
||||
func SaveJSON(title, defaultName string) (string, error) {
|
||||
if title == "" {
|
||||
title = "Сохранить список серверов"
|
||||
}
|
||||
if defaultName == "" {
|
||||
defaultName = "navis-servers.json"
|
||||
}
|
||||
script := `try
|
||||
set thePath to POSIX path of (choose file name with prompt "` + appleScriptQuote(title) + `" default name "` + appleScriptQuote(defaultName) + `")
|
||||
return thePath
|
||||
on error number -128
|
||||
error "canceled"
|
||||
end try`
|
||||
out, err := exec.Command("osascript", "-e", script).Output()
|
||||
if err != nil {
|
||||
if strings.Contains(string(out), "canceled") || strings.Contains(err.Error(), "canceled") {
|
||||
return "", ErrCanceled
|
||||
}
|
||||
// osascript writes AppleScript errors to stderr; treat cancel-ish exits as cancel.
|
||||
if exit, ok := err.(*exec.ExitError); ok && exit.ExitCode() != 0 {
|
||||
msg := strings.ToLower(string(exit.Stderr) + string(out) + err.Error())
|
||||
if strings.Contains(msg, "cancel") || strings.Contains(msg, "-128") {
|
||||
return "", ErrCanceled
|
||||
}
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
path := strings.TrimSpace(string(out))
|
||||
if path == "" {
|
||||
return "", ErrCanceled
|
||||
}
|
||||
if !strings.HasSuffix(strings.ToLower(path), ".json") {
|
||||
path += ".json"
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
|
||||
// OpenJSON shows an Open dialog for a .json file.
|
||||
func OpenJSON(title string) (string, error) {
|
||||
if title == "" {
|
||||
title = "Загрузить список серверов"
|
||||
}
|
||||
script := `try
|
||||
set theFile to choose file with prompt "` + appleScriptQuote(title) + `" of type {"public.json", "json", "public.plain-text"}
|
||||
return POSIX path of theFile
|
||||
on error number -128
|
||||
error "canceled"
|
||||
end try`
|
||||
out, err := exec.Command("osascript", "-e", script).Output()
|
||||
if err != nil {
|
||||
if exit, ok := err.(*exec.ExitError); ok {
|
||||
msg := strings.ToLower(string(exit.Stderr) + string(out) + err.Error())
|
||||
if strings.Contains(msg, "cancel") || strings.Contains(msg, "-128") {
|
||||
return "", ErrCanceled
|
||||
}
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
path := strings.TrimSpace(string(out))
|
||||
if path == "" {
|
||||
return "", ErrCanceled
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
//go:build !windows && !darwin
|
||||
|
||||
package filedialog
|
||||
|
||||
// SaveJSON is not available on this platform.
|
||||
func SaveJSON(title, defaultName string) (string, error) {
|
||||
return "", ErrUnsupported
|
||||
}
|
||||
|
||||
// OpenJSON is not available on this platform.
|
||||
func OpenJSON(title string) (string, error) {
|
||||
return "", ErrUnsupported
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
//go:build windows
|
||||
|
||||
package filedialog
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
var (
|
||||
comdlg32 = windows.NewLazySystemDLL("comdlg32.dll")
|
||||
procGetOpenFileName = comdlg32.NewProc("GetOpenFileNameW")
|
||||
procGetSaveFileName = comdlg32.NewProc("GetSaveFileNameW")
|
||||
)
|
||||
|
||||
const (
|
||||
ofnExplorer = 0x00080000
|
||||
ofnFileMustExist = 0x00001000
|
||||
ofnPathMustExist = 0x00000800
|
||||
ofnOverwritePrompt = 0x00000002
|
||||
ofnHideReadOnly = 0x00000004
|
||||
maxPath = 32768
|
||||
)
|
||||
|
||||
type openFileNameW struct {
|
||||
LStructSize uint32
|
||||
HwndOwner windows.HWND
|
||||
HInstance windows.Handle
|
||||
LpstrFilter *uint16
|
||||
LpstrCustomFilter *uint16
|
||||
NMaxCustFilter uint32
|
||||
NFilterIndex uint32
|
||||
LpstrFile *uint16
|
||||
NMaxFile uint32
|
||||
LpstrFileTitle *uint16
|
||||
NMaxFileTitle uint32
|
||||
LpstrInitialDir *uint16
|
||||
LpstrTitle *uint16
|
||||
Flags uint32
|
||||
NFileOffset uint16
|
||||
NFileExtension uint16
|
||||
LpstrDefExt *uint16
|
||||
LCustData uintptr
|
||||
LpfnHook uintptr
|
||||
LpTemplateName *uint16
|
||||
PvReserved unsafe.Pointer
|
||||
DwReserved uint32
|
||||
FlagsEx uint32
|
||||
}
|
||||
|
||||
func jsonFilter() *uint16 {
|
||||
// Double-null terminated pairs: label\0pattern\0label\0pattern\0\0
|
||||
s := "JSON (*.json)\x00*.json\x00All files\x00*.*\x00"
|
||||
return &utf16FromString(s)[0]
|
||||
}
|
||||
|
||||
func utf16FromString(s string) []uint16 {
|
||||
u, err := syscall.UTF16FromString(s)
|
||||
if err != nil {
|
||||
return []uint16{0}
|
||||
}
|
||||
return u
|
||||
}
|
||||
|
||||
func ensureJSONExt(path string) string {
|
||||
if strings.EqualFold(filepath.Ext(path), ".json") {
|
||||
return path
|
||||
}
|
||||
return path + ".json"
|
||||
}
|
||||
|
||||
// SaveJSON shows a Save As dialog for a .json file.
|
||||
func SaveJSON(title, defaultName string) (string, error) {
|
||||
if title == "" {
|
||||
title = "Сохранить список серверов"
|
||||
}
|
||||
if defaultName == "" {
|
||||
defaultName = "navis-servers.json"
|
||||
}
|
||||
buf := make([]uint16, maxPath)
|
||||
copy(buf, utf16FromString(defaultName))
|
||||
|
||||
filter := jsonFilter()
|
||||
titlePtr, _ := syscall.UTF16PtrFromString(title)
|
||||
defExt, _ := syscall.UTF16PtrFromString("json")
|
||||
|
||||
ofn := openFileNameW{
|
||||
LStructSize: uint32(unsafe.Sizeof(openFileNameW{})),
|
||||
LpstrFilter: filter,
|
||||
NFilterIndex: 1,
|
||||
LpstrFile: &buf[0],
|
||||
NMaxFile: uint32(len(buf)),
|
||||
LpstrTitle: titlePtr,
|
||||
Flags: ofnExplorer | ofnPathMustExist | ofnOverwritePrompt | ofnHideReadOnly,
|
||||
LpstrDefExt: defExt,
|
||||
}
|
||||
r, _, _ := procGetSaveFileName.Call(uintptr(unsafe.Pointer(&ofn)))
|
||||
if r == 0 {
|
||||
return "", ErrCanceled
|
||||
}
|
||||
path := windows.UTF16ToString(buf)
|
||||
if path == "" {
|
||||
return "", ErrCanceled
|
||||
}
|
||||
return ensureJSONExt(path), nil
|
||||
}
|
||||
|
||||
// OpenJSON shows an Open dialog for a .json file.
|
||||
func OpenJSON(title string) (string, error) {
|
||||
if title == "" {
|
||||
title = "Загрузить список серверов"
|
||||
}
|
||||
buf := make([]uint16, maxPath)
|
||||
filter := jsonFilter()
|
||||
titlePtr, _ := syscall.UTF16PtrFromString(title)
|
||||
|
||||
ofn := openFileNameW{
|
||||
LStructSize: uint32(unsafe.Sizeof(openFileNameW{})),
|
||||
LpstrFilter: filter,
|
||||
NFilterIndex: 1,
|
||||
LpstrFile: &buf[0],
|
||||
NMaxFile: uint32(len(buf)),
|
||||
LpstrTitle: titlePtr,
|
||||
Flags: ofnExplorer | ofnFileMustExist | ofnPathMustExist | ofnHideReadOnly,
|
||||
}
|
||||
r, _, _ := procGetOpenFileName.Call(uintptr(unsafe.Pointer(&ofn)))
|
||||
if r == 0 {
|
||||
return "", ErrCanceled
|
||||
}
|
||||
path := windows.UTF16ToString(buf)
|
||||
if path == "" {
|
||||
return "", ErrCanceled
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
Reference in New Issue
Block a user