Merge pull request #10 from ncruces/dev

List dialog, fix #3.
This commit is contained in:
Nuno Cruces 2021-04-09 15:25:16 +01:00 committed by GitHub
commit 18e4c20ad5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 983 additions and 630 deletions

View file

@ -8,15 +8,13 @@ This repo includes both a cross-platform Go package providing
(simple dialogs that interact graphically with the user), (simple dialogs that interact graphically with the user),
as well as a *“port”* of the `zenity` command to both Windows and macOS based on that library. as well as a *“port”* of the `zenity` command to both Windows and macOS based on that library.
**This is a work in progress.** Implemented dialogs:
Lots of things are missing.
For now, these are the only implemented dialogs:
* [message](https://github.com/ncruces/zenity/wiki/Message-dialog) (error, info, question, warning) * [message](https://github.com/ncruces/zenity/wiki/Message-dialog) (error, info, question, warning)
* [text entry](https://github.com/ncruces/zenity/wiki/Text-Entry-dialog)
* [list](https://github.com/ncruces/zenity/wiki/List-dialog)
* [password](https://github.com/ncruces/zenity/wiki/Password-dialog)
* [file selection](https://github.com/ncruces/zenity/wiki/File-Selection-dialog) * [file selection](https://github.com/ncruces/zenity/wiki/File-Selection-dialog)
* [color selection](https://github.com/ncruces/zenity/wiki/Color-Selection-dialog) * [color selection](https://github.com/ncruces/zenity/wiki/Color-Selection-dialog)
* [text entry](https://github.com/ncruces/zenity/wiki/Text-Entry-dialog)
* [password](https://github.com/ncruces/zenity/wiki/Password-dialog)
* [notification](https://github.com/ncruces/zenity/wiki/Notification) * [notification](https://github.com/ncruces/zenity/wiki/Notification)
Behavior on Windows, macOS and other Unixes might differ slightly. Behavior on Windows, macOS and other Unixes might differ slightly.

View file

@ -3,6 +3,7 @@ package main
import ( import (
"bytes" "bytes"
"context" "context"
"errors"
"flag" "flag"
"image/color" "image/color"
"os" "os"
@ -24,15 +25,16 @@ const (
var ( var (
// Application Options // Application Options
notification bool
entryDlg bool
errorDlg bool errorDlg bool
infoDlg bool infoDlg bool
warningDlg bool warningDlg bool
questionDlg bool questionDlg bool
entryDlg bool
listDlg bool
passwordDlg bool passwordDlg bool
fileSelectionDlg bool fileSelectionDlg bool
colorSelectionDlg bool colorSelectionDlg bool
notification bool
// General options // General options
title string title string
@ -43,25 +45,29 @@ var (
extraButton string extraButton string
text string text string
icon string icon string
multiple bool
// Entry options
entryText string
hideText bool
// Message options // Message options
noWrap bool noWrap bool
ellipsize bool ellipsize bool
defaultCancel bool defaultCancel bool
// Entry options
entryText string
hideText bool
// List options
columns int
allowEmpty bool
// File selection options // File selection options
save bool save bool
multiple bool
directory bool directory bool
confirmOverwrite bool confirmOverwrite bool
confirmCreate bool confirmCreate bool
showHidden bool showHidden bool
filename string filename string
fileFilters FileFilters fileFilters zenity.FileFilters
// Color selection options // Color selection options
defaultColor string defaultColor string
@ -93,12 +99,6 @@ func main() {
} }
switch { switch {
case notification:
errResult(zenity.Notify(text, opts...))
case entryDlg:
strOKResult(zenity.Entry(text, opts...))
case errorDlg: case errorDlg:
okResult(zenity.Error(text, opts...)) okResult(zenity.Error(text, opts...))
case infoDlg: case infoDlg:
@ -108,6 +108,16 @@ func main() {
case questionDlg: case questionDlg:
okResult(zenity.Question(text, opts...)) okResult(zenity.Question(text, opts...))
case entryDlg:
strOKResult(zenity.Entry(text, opts...))
case listDlg:
if multiple {
listResult(zenity.ListMultiple(text, flag.Args(), opts...))
} else {
strOKResult(zenity.List(text, flag.Args(), opts...))
}
case passwordDlg: case passwordDlg:
_, pw, ok, err := zenity.Password(opts...) _, pw, ok, err := zenity.Password(opts...)
strOKResult(pw, ok, err) strOKResult(pw, ok, err)
@ -124,6 +134,9 @@ func main() {
case colorSelectionDlg: case colorSelectionDlg:
colorResult(zenity.SelectColor(opts...)) colorResult(zenity.SelectColor(opts...))
case notification:
errResult(zenity.Notify(text, opts...))
} }
flag.Usage() flag.Usage()
@ -131,15 +144,16 @@ func main() {
func setupFlags() { func setupFlags() {
// Application Options // Application Options
flag.BoolVar(&notification, "notification", false, "Display notification")
flag.BoolVar(&entryDlg, "entry", false, "Display text entry dialog")
flag.BoolVar(&errorDlg, "error", false, "Display error dialog") flag.BoolVar(&errorDlg, "error", false, "Display error dialog")
flag.BoolVar(&infoDlg, "info", false, "Display info dialog") flag.BoolVar(&infoDlg, "info", false, "Display info dialog")
flag.BoolVar(&warningDlg, "warning", false, "Display warning dialog") flag.BoolVar(&warningDlg, "warning", false, "Display warning dialog")
flag.BoolVar(&questionDlg, "question", false, "Display question dialog") flag.BoolVar(&questionDlg, "question", false, "Display question dialog")
flag.BoolVar(&entryDlg, "entry", false, "Display text entry dialog")
flag.BoolVar(&listDlg, "list", false, "Display list dialog")
flag.BoolVar(&passwordDlg, "password", false, "Display password dialog") flag.BoolVar(&passwordDlg, "password", false, "Display password dialog")
flag.BoolVar(&fileSelectionDlg, "file-selection", false, "Display file selection dialog") flag.BoolVar(&fileSelectionDlg, "file-selection", false, "Display file selection dialog")
flag.BoolVar(&colorSelectionDlg, "color-selection", false, "Display color selection dialog") flag.BoolVar(&colorSelectionDlg, "color-selection", false, "Display color selection dialog")
flag.BoolVar(&notification, "notification", false, "Display notification")
// General options // General options
flag.StringVar(&title, "title", "", "Set the dialog `title`") flag.StringVar(&title, "title", "", "Set the dialog `title`")
@ -150,10 +164,7 @@ func setupFlags() {
flag.StringVar(&extraButton, "extra-button", "", "Add an extra button") flag.StringVar(&extraButton, "extra-button", "", "Add an extra button")
flag.StringVar(&text, "text", "", "Set the dialog `text`") flag.StringVar(&text, "text", "", "Set the dialog `text`")
flag.StringVar(&icon, "window-icon", "", "Set the window `icon` (error, info, question, warning)") flag.StringVar(&icon, "window-icon", "", "Set the window `icon` (error, info, question, warning)")
flag.BoolVar(&multiple, "multiple", false, "Allow multiple items to be selected")
// Entry options
flag.StringVar(&entryText, "entry-text", "", "Set the entry `text`")
flag.BoolVar(&hideText, "hide-text", false, "Hide the entry text")
// Message options // Message options
flag.StringVar(&icon, "icon-name", "", "Set the dialog `icon` (dialog-error, dialog-information, dialog-question, dialog-warning)") flag.StringVar(&icon, "icon-name", "", "Set the dialog `icon` (dialog-error, dialog-information, dialog-question, dialog-warning)")
@ -161,15 +172,23 @@ func setupFlags() {
flag.BoolVar(&ellipsize, "ellipsize", false, "Enable ellipsizing in the dialog text") flag.BoolVar(&ellipsize, "ellipsize", false, "Enable ellipsizing in the dialog text")
flag.BoolVar(&defaultCancel, "default-cancel", false, "Give Cancel button focus by default") flag.BoolVar(&defaultCancel, "default-cancel", false, "Give Cancel button focus by default")
// Entry options
flag.StringVar(&entryText, "entry-text", "", "Set the entry `text`")
flag.BoolVar(&hideText, "hide-text", false, "Hide the entry text")
// List options
flag.Var(funcValue(addColumn), "column", "Set the column header")
flag.Bool("hide-header", true, "Hide the column headers")
flag.BoolVar(&allowEmpty, "allow-empty", true, "Allow empty selection (macOS only)")
// File selection options // File selection options
flag.BoolVar(&save, "save", false, "Activate save mode") flag.BoolVar(&save, "save", false, "Activate save mode")
flag.BoolVar(&multiple, "multiple", false, "Allow multiple files to be selected")
flag.BoolVar(&directory, "directory", false, "Activate directory-only selection") flag.BoolVar(&directory, "directory", false, "Activate directory-only selection")
flag.BoolVar(&confirmOverwrite, "confirm-overwrite", false, "Confirm file selection if filename already exists") flag.BoolVar(&confirmOverwrite, "confirm-overwrite", false, "Confirm file selection if filename already exists")
flag.BoolVar(&confirmCreate, "confirm-create", false, "Confirm file selection if filename does not yet exist (Windows only)") flag.BoolVar(&confirmCreate, "confirm-create", false, "Confirm file selection if filename does not yet exist (Windows only)")
flag.BoolVar(&showHidden, "show-hidden", false, "Show hidden files (Windows and macOS only)") flag.BoolVar(&showHidden, "show-hidden", false, "Show hidden files (Windows and macOS only)")
flag.StringVar(&filename, "filename", "", "Set the `filename`") flag.StringVar(&filename, "filename", "", "Set the `filename`")
flag.Var(&fileFilters, "file-filter", "Set a filename filter (NAME | PATTERN1 PATTERN2 ...)") flag.Var(funcValue(addFileFilter), "file-filter", "Set a filename filter (NAME | PATTERN1 PATTERN2 ...)")
// Color selection options // Color selection options
flag.StringVar(&defaultColor, "color", "", "Set the `color`") flag.StringVar(&defaultColor, "color", "", "Set the `color`")
@ -196,12 +215,6 @@ func setupFlags() {
func validateFlags() { func validateFlags() {
var n int var n int
if notification {
n++
}
if entryDlg {
n++
}
if errorDlg { if errorDlg {
n++ n++
} }
@ -214,6 +227,12 @@ func validateFlags() {
if questionDlg { if questionDlg {
n++ n++
} }
if entryDlg {
n++
}
if listDlg {
n++
}
if passwordDlg { if passwordDlg {
n++ n++
} }
@ -223,6 +242,9 @@ func validateFlags() {
if colorSelectionDlg { if colorSelectionDlg {
n++ n++
} }
if notification {
n++
}
if n != 1 { if n != 1 {
flag.Usage() flag.Usage()
} }
@ -239,11 +261,6 @@ func loadFlags() []zenity.Option {
} }
} }
switch { switch {
case entryDlg:
setDefault(&title, "Add a new entry")
setDefault(&text, "Enter new text:")
setDefault(&okLabel, "OK")
setDefault(&cancelLabel, "Cancel")
case errorDlg: case errorDlg:
setDefault(&title, "Error") setDefault(&title, "Error")
setDefault(&icon, "dialog-error") setDefault(&icon, "dialog-error")
@ -265,6 +282,16 @@ func loadFlags() []zenity.Option {
setDefault(&text, "Are you sure you want to proceed?") setDefault(&text, "Are you sure you want to proceed?")
setDefault(&okLabel, "Yes") setDefault(&okLabel, "Yes")
setDefault(&cancelLabel, "No") setDefault(&cancelLabel, "No")
case entryDlg:
setDefault(&title, "Add a new entry")
setDefault(&text, "Enter new text:")
setDefault(&okLabel, "OK")
setDefault(&cancelLabel, "Cancel")
case listDlg:
setDefault(&title, "Select items from the list")
setDefault(&text, "Select items from the list below:")
setDefault(&okLabel, "OK")
setDefault(&cancelLabel, "Cancel")
case passwordDlg: case passwordDlg:
setDefault(&title, "Type your password") setDefault(&title, "Type your password")
setDefault(&icon, "dialog-password") setDefault(&icon, "dialog-password")
@ -308,13 +335,6 @@ func loadFlags() []zenity.Option {
} }
opts = append(opts, zenity.Icon(ico)) opts = append(opts, zenity.Icon(ico))
// Entry options
opts = append(opts, zenity.EntryText(entryText))
if hideText {
opts = append(opts, zenity.HideText())
}
// Message options // Message options
if noWrap { if noWrap {
@ -327,6 +347,19 @@ func loadFlags() []zenity.Option {
opts = append(opts, zenity.DefaultCancel()) opts = append(opts, zenity.DefaultCancel())
} }
// Entry options
opts = append(opts, zenity.EntryText(entryText))
if hideText {
opts = append(opts, zenity.HideText())
}
// List options
if !allowEmpty {
opts = append(opts, zenity.DisallowEmpty())
}
// File selection options // File selection options
if directory { if directory {
@ -487,18 +520,20 @@ func egestPaths(paths []string, err error) ([]string, error) {
return paths, err return paths, err
} }
// FileFilters is internal. type funcValue func(string) error
type FileFilters struct {
zenity.FileFilters func (f funcValue) String() string { return "" }
func (f funcValue) Set(s string) error { return f(s) }
func addColumn(s string) error {
columns++
if columns <= 1 {
return nil
}
return errors.New("multiple columns not supported")
} }
// String is internal. func addFileFilter(s string) error {
func (f *FileFilters) String() string {
return "zenity.FileFilters"
}
// Set is internal.
func (f *FileFilters) Set(s string) error {
var filter zenity.FileFilter var filter zenity.FileFilter
if split := strings.SplitN(s, "|", 2); len(split) > 1 { if split := strings.SplitN(s, "|", 2); len(split) > 1 {
@ -507,7 +542,7 @@ func (f *FileFilters) Set(s string) error {
} }
filter.Patterns = strings.Split(strings.TrimSpace(s), " ") filter.Patterns = strings.Split(strings.TrimSpace(s), " ")
f.FileFilters = append(f.FileFilters, filter) fileFilters = append(fileFilters, filter)
return nil return nil
} }

View file

@ -2,7 +2,6 @@ package zenity
import ( import (
"image/color" "image/color"
"os/exec"
"github.com/ncruces/zenity/internal/zenutil" "github.com/ncruces/zenity/internal/zenutil"
) )
@ -21,11 +20,9 @@ func selectColor(opts options) (color.Color, error) {
float32(g) / 0xffff, float32(g) / 0xffff,
float32(b) / 0xffff, float32(b) / 0xffff,
}) })
if err, ok := err.(*exec.ExitError); ok && err.ExitCode() == 1 { str, ok, err := strResult(opts, out, err)
return nil, nil if ok {
return zenutil.ParseColor(str), nil
} }
if err != nil {
return nil, err return nil, err
} }
return zenutil.ParseColor(string(out)), nil
}

View file

@ -4,7 +4,6 @@ package zenity
import ( import (
"image/color" "image/color"
"os/exec"
"github.com/ncruces/zenity/internal/zenutil" "github.com/ncruces/zenity/internal/zenutil"
) )
@ -12,9 +11,7 @@ import (
func selectColor(opts options) (color.Color, error) { func selectColor(opts options) (color.Color, error) {
args := []string{"--color-selection"} args := []string{"--color-selection"}
if opts.title != nil { args = appendTitle(args, opts)
args = append(args, "--title", *opts.title)
}
if opts.color != nil { if opts.color != nil {
args = append(args, "--color", zenutil.UnparseColor(opts.color)) args = append(args, "--color", zenutil.UnparseColor(opts.color))
} }
@ -23,11 +20,9 @@ func selectColor(opts options) (color.Color, error) {
} }
out, err := zenutil.Run(opts.ctx, args) out, err := zenutil.Run(opts.ctx, args)
if err, ok := err.(*exec.ExitError); ok && err.ExitCode() == 1 { str, ok, err := strResult(opts, out, err)
return nil, nil if ok {
return zenutil.ParseColor(str), nil
} }
if err != nil {
return nil, err return nil, err
} }
return zenutil.ParseColor(string(out)), nil
}

View file

@ -10,15 +10,6 @@ func Entry(text string, options ...Option) (string, bool, error) {
return entry(text, applyOptions(options)) return entry(text, applyOptions(options))
} }
// Password displays the password dialog.
//
// Returns false on cancel, or ErrExtraButton.
//
// Valid options: Title, OKLabel, CancelLabel, ExtraButton, Icon, Username.
func Password(options ...Option) (usr string, pw string, ok bool, err error) {
return password(applyOptions(options))
}
// EntryText returns an Option to set the entry text. // EntryText returns an Option to set the entry text.
func EntryText(text string) Option { func EntryText(text string) Option {
return funcOption(func(o *options) { o.entryText = text }) return funcOption(func(o *options) { o.entryText = text })
@ -28,8 +19,3 @@ func EntryText(text string) Option {
func HideText() Option { func HideText() Option {
return funcOption(func(o *options) { o.hideText = true }) return funcOption(func(o *options) { o.hideText = true })
} }
// Username returns an Option to display the username (Unix only).
func Username() Option {
return funcOption(func(o *options) { o.username = true })
}

View file

@ -1,9 +1,6 @@
package zenity package zenity
import ( import (
"bytes"
"os/exec"
"github.com/ncruces/zenity/internal/zenutil" "github.com/ncruces/zenity/internal/zenutil"
) )
@ -19,22 +16,5 @@ func entry(text string, opts options) (string, bool, error) {
data.SetButtons(getButtons(true, true, opts)) data.SetButtons(getButtons(true, true, opts))
out, err := zenutil.Run(opts.ctx, "dialog", data) out, err := zenutil.Run(opts.ctx, "dialog", data)
out = bytes.TrimSuffix(out, []byte{'\n'}) return strResult(opts, out, err)
if err, ok := err.(*exec.ExitError); ok && err.ExitCode() == 1 {
if opts.extraButton != nil &&
*opts.extraButton == string(out) {
return "", false, ErrExtraButton
}
return "", false, nil
}
if err != nil {
return "", false, err
}
return string(out), true, nil
}
func password(opts options) (string, string, bool, error) {
opts.hideText = true
pass, ok, err := entry("Password:", opts)
return "", pass, ok, err
} }

View file

@ -16,11 +16,6 @@ func ExampleEntry() {
// Output: // Output:
} }
func ExamplePassword() {
zenity.Password(zenity.Title("Type your password"))
// Output:
}
func TestEntryTimeout(t *testing.T) { func TestEntryTimeout(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second/10) ctx, cancel := context.WithTimeout(context.Background(), time.Second/10)
@ -41,24 +36,3 @@ func TestEntryCancel(t *testing.T) {
t.Error("was not canceled:", err) t.Error("was not canceled:", err)
} }
} }
func TestPasswordTimeout(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second/10)
_, _, _, err := zenity.Password(zenity.Context(ctx))
if !os.IsTimeout(err) {
t.Error("did not timeout:", err)
}
cancel()
}
func TestPasswordCancel(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
_, _, _, err := zenity.Password(zenity.Context(ctx))
if !errors.Is(err, context.Canceled) {
t.Error("was not canceled:", err)
}
}

View file

@ -3,97 +3,22 @@
package zenity package zenity
import ( import (
"bytes"
"os/exec"
"strconv"
"strings"
"github.com/ncruces/zenity/internal/zenutil" "github.com/ncruces/zenity/internal/zenutil"
) )
func entry(text string, opts options) (string, bool, error) { func entry(text string, opts options) (string, bool, error) {
args := []string{"--entry", "--text", text, "--entry-text", opts.entryText} args := []string{"--entry", "--text", text}
if opts.title != nil { args = appendTitle(args, opts)
args = append(args, "--title", *opts.title) args = appendButtons(args, opts)
} args = appendWidthHeight(args, opts)
if opts.width > 0 { args = appendIcon(args, opts)
args = append(args, "--width", strconv.FormatUint(uint64(opts.width), 10)) if opts.entryText != "" {
} args = append(args, "--entry-text", opts.entryText)
if opts.height > 0 {
args = append(args, "--height", strconv.FormatUint(uint64(opts.height), 10))
}
if opts.okLabel != nil {
args = append(args, "--ok-label", *opts.okLabel)
}
if opts.cancelLabel != nil {
args = append(args, "--cancel-label", *opts.cancelLabel)
}
if opts.extraButton != nil {
args = append(args, "--extra-button", *opts.extraButton)
} }
if opts.hideText { if opts.hideText {
args = append(args, "--hide-text") args = append(args, "--hide-text")
} }
switch opts.icon {
case ErrorIcon:
args = append(args, "--window-icon=error")
case WarningIcon:
args = append(args, "--window-icon=warning")
case InfoIcon:
args = append(args, "--window-icon=info")
case QuestionIcon:
args = append(args, "--window-icon=question")
}
out, err := zenutil.Run(opts.ctx, args) out, err := zenutil.Run(opts.ctx, args)
out = bytes.TrimSuffix(out, []byte{'\n'}) return strResult(opts, out, err)
if err, ok := err.(*exec.ExitError); ok && err.ExitCode() == 1 {
if opts.extraButton != nil &&
*opts.extraButton == string(out) {
return "", false, ErrExtraButton
}
return "", false, nil
}
if err != nil {
return "", false, err
}
return string(out), true, nil
}
func password(opts options) (string, string, bool, error) {
args := []string{"--password"}
if opts.title != nil {
args = append(args, "--title", *opts.title)
}
if opts.okLabel != nil {
args = append(args, "--ok-label", *opts.okLabel)
}
if opts.cancelLabel != nil {
args = append(args, "--cancel-label", *opts.cancelLabel)
}
if opts.extraButton != nil {
args = append(args, "--extra-button", *opts.extraButton)
}
if opts.username {
args = append(args, "--username")
}
out, err := zenutil.Run(opts.ctx, args)
out = bytes.TrimSuffix(out, []byte{'\n'})
if err, ok := err.(*exec.ExitError); ok && err.ExitCode() == 1 {
if opts.extraButton != nil &&
*opts.extraButton == string(out) {
return "", "", false, ErrExtraButton
}
return "", "", false, nil
}
if err != nil {
return "", "", false, err
}
if opts.username {
if split := strings.SplitN(string(out), "|", 2); len(split) == 2 {
return split[0], split[1], true, nil
}
}
return "", string(out), true, nil
} }

View file

@ -1,12 +1,10 @@
package zenity package zenity
import ( import (
"strconv"
"syscall" "syscall"
"unsafe"
) )
func entry(text string, opts options) (string, bool, error) { func entry(text string, opts options) (out string, ok bool, err error) {
var title string var title string
if opts.title != nil { if opts.title != nil {
title = *opts.title title = *opts.title
@ -17,238 +15,17 @@ func entry(text string, opts options) (string, bool, error) {
if opts.cancelLabel == nil { if opts.cancelLabel == nil {
opts.cancelLabel = stringPtr("Cancel") opts.cancelLabel = stringPtr("Cancel")
} }
return editBox(title, text, opts) return entryDlg(title, text, opts)
} }
func password(opts options) (string, string, bool, error) { func entryDlg(title, text string, opts options) (out string, ok bool, err error) {
opts.hideText = true
pass, ok, err := entry("Password:", opts)
return "", pass, ok, err
}
var (
registerClassEx = user32.NewProc("RegisterClassExW")
unregisterClass = user32.NewProc("UnregisterClassW")
createWindowEx = user32.NewProc("CreateWindowExW")
destroyWindow = user32.NewProc("DestroyWindow")
isDialogMessage = user32.NewProc("IsDialogMessageW")
translateMessage = user32.NewProc("TranslateMessage")
dispatchMessage = user32.NewProc("DispatchMessageW")
postQuitMessage = user32.NewProc("PostQuitMessage")
defWindowProc = user32.NewProc("DefWindowProcW")
getWindowRect = user32.NewProc("GetWindowRect")
setWindowPos = user32.NewProc("SetWindowPos")
setFocus = user32.NewProc("SetFocus")
showWindow = user32.NewProc("ShowWindow")
systemParametersInfo = user32.NewProc("SystemParametersInfoW")
getSystemMetrics = user32.NewProc("GetSystemMetrics")
getWindowDC = user32.NewProc("GetWindowDC")
releaseDC = user32.NewProc("ReleaseDC")
getDpiForWindow = user32.NewProc("GetDpiForWindow")
deleteObject = gdi32.NewProc("DeleteObject")
getDeviceCaps = gdi32.NewProc("GetDeviceCaps")
createFontIndirect = gdi32.NewProc("CreateFontIndirectW")
)
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-wndclassexw
type _WNDCLASSEX struct {
Size uint32
Style uint32
WndProc uintptr
ClsExtra int32
WndExtra int32
Instance uintptr
Icon uintptr
Cursor uintptr
Background uintptr
MenuName *uint16
ClassName *uint16
IconSm uintptr
}
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-msg
type _MSG struct {
Owner syscall.Handle
Message uint32
WParam uintptr
LParam uintptr
Time uint32
Pt _POINT
}
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-nonclientmetricsw
type _NONCLIENTMETRICS struct {
Size uint32
BorderWidth int32
ScrollWidth int32
ScrollHeight int32
CaptionWidth int32
CaptionHeight int32
CaptionFont _LOGFONT
SmCaptionWidth int32
SmCaptionHeight int32
SmCaptionFont _LOGFONT
MenuWidth int32
MenuHeight int32
MenuFont _LOGFONT
StatusFont _LOGFONT
MessageFont _LOGFONT
}
// https://docs.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-logfontw
type _LOGFONT struct {
Height int32
Width int32
Escapement int32
Orientation int32
Weight int32
Italic byte
Underline byte
StrikeOut byte
CharSet byte
OutPrecision byte
ClipPrecision byte
Quality byte
PitchAndFamily byte
FaceName [32]uint16
}
// https://docs.microsoft.com/en-us/windows/win32/api/windef/ns-windef-point
type _POINT struct {
x, y int32
}
// https://docs.microsoft.com/en-us/windows/win32/api/windef/ns-windef-rect
type _RECT struct {
left int32
top int32
right int32
bottom int32
}
type dpi uintptr
func getDPI(wnd uintptr) dpi {
var res uintptr
if wnd != 0 && getDpiForWindow.Find() == nil {
res, _, _ = getDpiForWindow.Call(wnd)
} else if dc, _, _ := getWindowDC.Call(wnd); dc != 0 {
res, _, _ = getDeviceCaps.Call(dc, 90) // LOGPIXELSY
releaseDC.Call(0, dc)
}
if res == 0 {
return 96 // USER_DEFAULT_SCREEN_DPI
}
return dpi(res)
}
func (d dpi) Scale(dim uintptr) uintptr {
if d == 0 {
return dim
}
return dim * uintptr(d) / 96 // USER_DEFAULT_SCREEN_DPI
}
type font struct {
handle uintptr
face _LOGFONT
}
func getFont() font {
var metrics _NONCLIENTMETRICS
metrics.Size = uint32(unsafe.Sizeof(metrics))
systemParametersInfo.Call(0x29, // SPI_GETNONCLIENTMETRICS
unsafe.Sizeof(metrics), uintptr(unsafe.Pointer(&metrics)), 0)
return font{face: metrics.MessageFont}
}
func (f *font) ForDPI(dpi dpi) uintptr {
if h := -int32(dpi.Scale(12)); f.handle == 0 || f.face.Height != h {
f.Delete()
f.face.Height = h
f.handle, _, _ = createFontIndirect.Call(uintptr(unsafe.Pointer(&f.face)))
}
return f.handle
}
func (f *font) Delete() {
if f.handle != 0 {
deleteObject.Call(f.handle)
f.handle = 0
}
}
func getWindowTextString(wnd uintptr) string {
len, _, _ := getWindowTextLength.Call(wnd)
buf := make([]uint16, len+1)
getWindowText.Call(wnd, uintptr(unsafe.Pointer(&buf[0])), len+1)
return syscall.UTF16ToString(buf)
}
func centerWindow(wnd uintptr) {
getMetric := func(i uintptr) int32 {
ret, _, _ := getSystemMetrics.Call(i)
return int32(ret)
}
var rect _RECT
getWindowRect.Call(wnd, uintptr(unsafe.Pointer(&rect)))
x := (getMetric(0 /* SM_CXSCREEN */) - (rect.right - rect.left)) / 2
y := (getMetric(1 /* SM_CYSCREEN */) - (rect.bottom - rect.top)) / 2
setWindowPos.Call(wnd, 0, uintptr(x), uintptr(y), 0, 0, 0x5) // SWP_NOZORDER|SWP_NOSIZE
}
func registerClass(instance, proc uintptr) (uintptr, error) {
name := "WC_" + strconv.FormatUint(uint64(proc), 16)
var wcx _WNDCLASSEX
wcx.Size = uint32(unsafe.Sizeof(wcx))
wcx.WndProc = proc
wcx.Instance = instance
wcx.Background = 5 // COLOR_WINDOW
wcx.ClassName = syscall.StringToUTF16Ptr(name)
ret, _, err := registerClassEx.Call(uintptr(unsafe.Pointer(&wcx)))
return ret, err
}
// https://docs.microsoft.com/en-us/windows/win32/winmsg/using-messages-and-message-queues
func messageLoop(wnd uintptr) error {
getMessage := getMessage.Addr()
isDialogMessage := isDialogMessage.Addr()
translateMessage := translateMessage.Addr()
dispatchMessage := dispatchMessage.Addr()
for {
var msg _MSG
ret, _, err := syscall.Syscall6(getMessage, 4, uintptr(unsafe.Pointer(&msg)), 0, 0, 0, 0, 0)
if int32(ret) == -1 {
return err
}
if ret == 0 {
return nil
}
ret, _, _ = syscall.Syscall(isDialogMessage, 2, wnd, uintptr(unsafe.Pointer(&msg)), 0)
if ret == 0 {
syscall.Syscall(translateMessage, 1, uintptr(unsafe.Pointer(&msg)), 0, 0)
syscall.Syscall(dispatchMessage, 1, uintptr(unsafe.Pointer(&msg)), 0, 0)
}
}
}
func editBox(title, text string, opts options) (out string, ok bool, err error) {
var wnd, textCtl, editCtl uintptr
var okBtn, cancelBtn, extraBtn uintptr
defWindowProc := defWindowProc.Addr()
defer setup()() defer setup()()
font := getFont() font := getFont()
defer font.Delete() defer font.Delete()
defWindowProc := defWindowProc.Addr()
var wnd, textCtl, editCtl uintptr
var okBtn, cancelBtn, extraBtn uintptr
layout := func(dpi dpi) { layout := func(dpi dpi) {
hfont := font.ForDPI(dpi) hfont := font.ForDPI(dpi)
@ -256,17 +33,17 @@ func editBox(title, text string, opts options) (out string, ok bool, err error)
sendMessage.Call(editCtl, 0x0030 /* WM_SETFONT */, hfont, 1) sendMessage.Call(editCtl, 0x0030 /* WM_SETFONT */, hfont, 1)
sendMessage.Call(okBtn, 0x0030 /* WM_SETFONT */, hfont, 1) sendMessage.Call(okBtn, 0x0030 /* WM_SETFONT */, hfont, 1)
sendMessage.Call(cancelBtn, 0x0030 /* WM_SETFONT */, hfont, 1) sendMessage.Call(cancelBtn, 0x0030 /* WM_SETFONT */, hfont, 1)
setWindowPos.Call(wnd, 0, 0, 0, dpi.Scale(281), dpi.Scale(140), 0x6) // SWP_NOZORDER|SWP_NOMOVE setWindowPos.Call(wnd, 0, 0, 0, dpi.Scale(281), dpi.Scale(141), 0x6) // SWP_NOZORDER|SWP_NOMOVE
setWindowPos.Call(textCtl, 0, dpi.Scale(12), dpi.Scale(10), dpi.Scale(241), dpi.Scale(16), 0x4) // SWP_NOZORDER setWindowPos.Call(textCtl, 0, dpi.Scale(12), dpi.Scale(10), dpi.Scale(241), dpi.Scale(16), 0x4) // SWP_NOZORDER
setWindowPos.Call(editCtl, 0, dpi.Scale(12), dpi.Scale(30), dpi.Scale(241), dpi.Scale(24), 0x4) // SWP_NOZORDER setWindowPos.Call(editCtl, 0, dpi.Scale(12), dpi.Scale(30), dpi.Scale(241), dpi.Scale(24), 0x4) // SWP_NOZORDER
if extraBtn == 0 { if extraBtn == 0 {
setWindowPos.Call(okBtn, 0, dpi.Scale(95), dpi.Scale(65), dpi.Scale(75), dpi.Scale(24), 0x4) // SWP_NOZORDER setWindowPos.Call(okBtn, 0, dpi.Scale(95), dpi.Scale(66), dpi.Scale(75), dpi.Scale(24), 0x4) // SWP_NOZORDER
setWindowPos.Call(cancelBtn, 0, dpi.Scale(178), dpi.Scale(65), dpi.Scale(75), dpi.Scale(24), 0x4) // SWP_NOZORDER setWindowPos.Call(cancelBtn, 0, dpi.Scale(178), dpi.Scale(66), dpi.Scale(75), dpi.Scale(24), 0x4) // SWP_NOZORDER
} else { } else {
sendMessage.Call(extraBtn, 0x0030 /* WM_SETFONT */, hfont, 1) sendMessage.Call(extraBtn, 0x0030 /* WM_SETFONT */, hfont, 1)
setWindowPos.Call(okBtn, 0, dpi.Scale(12), dpi.Scale(65), dpi.Scale(75), dpi.Scale(24), 0x4) // SWP_NOZORDER setWindowPos.Call(okBtn, 0, dpi.Scale(12), dpi.Scale(66), dpi.Scale(75), dpi.Scale(24), 0x4) // SWP_NOZORDER
setWindowPos.Call(extraBtn, 0, dpi.Scale(95), dpi.Scale(65), dpi.Scale(75), dpi.Scale(24), 0x4) // SWP_NOZORDER setWindowPos.Call(extraBtn, 0, dpi.Scale(95), dpi.Scale(66), dpi.Scale(75), dpi.Scale(24), 0x4) // SWP_NOZORDER
setWindowPos.Call(cancelBtn, 0, dpi.Scale(178), dpi.Scale(65), dpi.Scale(75), dpi.Scale(24), 0x4) // SWP_NOZORDER setWindowPos.Call(cancelBtn, 0, dpi.Scale(178), dpi.Scale(66), dpi.Scale(75), dpi.Scale(24), 0x4) // SWP_NOZORDER
} }
} }
@ -283,7 +60,7 @@ func editBox(title, text string, opts options) (out string, ok bool, err error)
default: default:
return 1 return 1
case 1, 6: // IDOK, IDYES case 1, 6: // IDOK, IDYES
out = getWindowTextString(editCtl) out = getWindowString(editCtl)
ok = true ok = true
case 2: // IDCANCEL case 2: // IDCANCEL
case 7: // IDNO case 7: // IDNO
@ -322,7 +99,7 @@ func editBox(title, text string, opts options) (out string, ok bool, err error)
0x84c80000, // WS_POPUPWINDOW|WS_CLIPSIBLINGS|WS_DLGFRAME 0x84c80000, // WS_POPUPWINDOW|WS_CLIPSIBLINGS|WS_DLGFRAME
0x80000000, // CW_USEDEFAULT 0x80000000, // CW_USEDEFAULT
0x80000000, // CW_USEDEFAULT 0x80000000, // CW_USEDEFAULT
281, 140, 0, 0, instance) 281, 141, 0, 0, instance)
textCtl, _, _ = createWindowEx.Call(0, textCtl, _, _ = createWindowEx.Call(0,
strptr("STATIC"), strptr(text), strptr("STATIC"), strptr(text),
@ -341,16 +118,16 @@ func editBox(title, text string, opts options) (out string, ok bool, err error)
okBtn, _, _ = createWindowEx.Call(0, okBtn, _, _ = createWindowEx.Call(0,
strptr("BUTTON"), strptr(*opts.okLabel), strptr("BUTTON"), strptr(*opts.okLabel),
0x50030001, // WS_CHILD|WS_VISIBLE|WS_GROUP|WS_TABSTOP|BS_DEFPUSHBUTTON 0x50030001, // WS_CHILD|WS_VISIBLE|WS_GROUP|WS_TABSTOP|BS_DEFPUSHBUTTON
12, 65, 75, 24, wnd, 1 /* IDOK */, instance) 12, 66, 75, 24, wnd, 1 /* IDOK */, instance)
cancelBtn, _, _ = createWindowEx.Call(0, cancelBtn, _, _ = createWindowEx.Call(0,
strptr("BUTTON"), strptr(*opts.cancelLabel), strptr("BUTTON"), strptr(*opts.cancelLabel),
0x50010000, // WS_CHILD|WS_VISIBLE|WS_GROUP|WS_TABSTOP 0x50010000, // WS_CHILD|WS_VISIBLE|WS_GROUP|WS_TABSTOP
12, 65, 75, 24, wnd, 2 /* IDCANCEL */, instance) 12, 66, 75, 24, wnd, 2 /* IDCANCEL */, instance)
if opts.extraButton != nil { if opts.extraButton != nil {
extraBtn, _, _ = createWindowEx.Call(0, extraBtn, _, _ = createWindowEx.Call(0,
strptr("BUTTON"), strptr(*opts.extraButton), strptr("BUTTON"), strptr(*opts.extraButton),
0x50010000, // WS_CHILD|WS_VISIBLE|WS_GROUP|WS_TABSTOP 0x50010000, // WS_CHILD|WS_VISIBLE|WS_GROUP|WS_TABSTOP
12, 65, 75, 24, wnd, 7 /* IDNO */, instance) 12, 66, 75, 24, wnd, 7 /* IDNO */, instance)
} }
layout(getDPI(wnd)) layout(getDPI(wnd))

View file

@ -1,8 +1,6 @@
package zenity package zenity
import ( import (
"bytes"
"os/exec"
"strings" "strings"
"github.com/ncruces/zenity/internal/zenutil" "github.com/ncruces/zenity/internal/zenutil"
@ -22,13 +20,8 @@ func selectFile(opts options) (string, error) {
} }
out, err := zenutil.Run(opts.ctx, "file", data) out, err := zenutil.Run(opts.ctx, "file", data)
if err, ok := err.(*exec.ExitError); ok && err.ExitCode() == 1 { str, _, err := strResult(opts, out, err)
return "", nil return str, err
}
if err != nil {
return "", err
}
return string(bytes.TrimSuffix(out, []byte{'\n'})), nil
} }
func selectFileMutiple(opts options) ([]string, error) { func selectFileMutiple(opts options) ([]string, error) {
@ -47,17 +40,7 @@ func selectFileMutiple(opts options) ([]string, error) {
} }
out, err := zenutil.Run(opts.ctx, "file", data) out, err := zenutil.Run(opts.ctx, "file", data)
if err, ok := err.(*exec.ExitError); ok && err.ExitCode() == 1 { return lstResult(opts, out, err)
return nil, nil
}
if err != nil {
return nil, err
}
out = bytes.TrimSuffix(out, []byte{'\n'})
if len(out) == 0 {
return nil, nil
}
return strings.Split(string(out), zenutil.Separator), nil
} }
func selectFileSave(opts options) (string, error) { func selectFileSave(opts options) (string, error) {
@ -73,13 +56,8 @@ func selectFileSave(opts options) (string, error) {
} }
out, err := zenutil.Run(opts.ctx, "file", data) out, err := zenutil.Run(opts.ctx, "file", data)
if err, ok := err.(*exec.ExitError); ok && err.ExitCode() == 1 { str, _, err := strResult(opts, out, err)
return "", nil return str, err
}
if err != nil {
return "", err
}
return string(bytes.TrimSuffix(out, []byte{'\n'})), nil
} }
func initFilters(filters []FileFilter) []string { func initFilters(filters []FileFilter) []string {

View file

@ -3,8 +3,6 @@
package zenity package zenity
import ( import (
"bytes"
"os/exec"
"strings" "strings"
"github.com/ncruces/zenity/internal/zenutil" "github.com/ncruces/zenity/internal/zenutil"
@ -12,78 +10,31 @@ import (
func selectFile(opts options) (string, error) { func selectFile(opts options) (string, error) {
args := []string{"--file-selection"} args := []string{"--file-selection"}
if opts.title != nil { args = appendTitle(args, opts)
args = append(args, "--title", *opts.title) args = appendFileArgs(args, opts)
}
if opts.directory {
args = append(args, "--directory")
}
if opts.filename != "" {
args = append(args, "--filename", opts.filename)
}
args = append(args, initFilters(opts.fileFilters)...)
out, err := zenutil.Run(opts.ctx, args) out, err := zenutil.Run(opts.ctx, args)
if err, ok := err.(*exec.ExitError); ok && err.ExitCode() == 1 { str, _, err := strResult(opts, out, err)
return "", nil return str, err
}
if err != nil {
return "", err
}
return string(bytes.TrimSuffix(out, []byte{'\n'})), nil
} }
func selectFileMutiple(opts options) ([]string, error) { func selectFileMutiple(opts options) ([]string, error) {
args := []string{"--file-selection", "--multiple", "--separator", zenutil.Separator} args := []string{"--file-selection", "--multiple", "--separator", zenutil.Separator}
if opts.title != nil { args = appendTitle(args, opts)
args = append(args, "--title", *opts.title) args = appendFileArgs(args, opts)
}
if opts.directory {
args = append(args, "--directory")
}
if opts.filename != "" {
args = append(args, "--filename", opts.filename)
}
args = append(args, initFilters(opts.fileFilters)...)
out, err := zenutil.Run(opts.ctx, args) out, err := zenutil.Run(opts.ctx, args)
if err, ok := err.(*exec.ExitError); ok && err.ExitCode() == 1 { return lstResult(opts, out, err)
return nil, nil
}
if err != nil {
return nil, err
}
out = bytes.TrimSuffix(out, []byte{'\n'})
if len(out) == 0 {
return nil, nil
}
return strings.Split(string(out), zenutil.Separator), nil
} }
func selectFileSave(opts options) (string, error) { func selectFileSave(opts options) (string, error) {
args := []string{"--file-selection", "--save"} args := []string{"--file-selection", "--save"}
if opts.title != nil { args = appendTitle(args, opts)
args = append(args, "--title", *opts.title) args = appendFileArgs(args, opts)
}
if opts.directory {
args = append(args, "--directory")
}
if opts.confirmOverwrite {
args = append(args, "--confirm-overwrite")
}
if opts.filename != "" {
args = append(args, "--filename", opts.filename)
}
args = append(args, initFilters(opts.fileFilters)...)
out, err := zenutil.Run(opts.ctx, args) out, err := zenutil.Run(opts.ctx, args)
if err, ok := err.(*exec.ExitError); ok && err.ExitCode() == 1 { str, _, err := strResult(opts, out, err)
return "", nil return str, err
}
if err != nil {
return "", err
}
return string(bytes.TrimSuffix(out, []byte{'\n'})), nil
} }
func initFilters(filters []FileFilter) []string { func initFilters(filters []FileFilter) []string {
@ -103,3 +54,18 @@ func initFilters(filters []FileFilter) []string {
} }
return res return res
} }
func appendFileArgs(args []string, opts options) []string {
if opts.directory {
args = append(args, "--directory")
}
if opts.filename != "" {
args = append(args, "--filename", opts.filename)
}
if opts.confirmOverwrite {
args = append(args, "--confirm-overwrite")
}
args = append(args, initFilters(opts.fileFilters)...)
return args
}

View file

@ -47,12 +47,9 @@ func ParseColor(s string) color.Color {
} }
} }
c, ok := colornames.Map[strings.ToLower(s)] c, _ := colornames.Map[strings.ToLower(s)]
if ok {
return c return c
} }
return nil
}
// UnparseColor is internal. // UnparseColor is internal.
func UnparseColor(c color.Color) string { func UnparseColor(c color.Color) string {

View file

@ -3,8 +3,10 @@
package zenutil package zenutil
import "encoding/json" import (
import "text/template" "encoding/json"
"text/template"
)
var scripts = template.Must(template.New("").Funcs(template.FuncMap{"json": func(v interface{}) (string, error) { var scripts = template.Must(template.New("").Funcs(template.FuncMap{"json": func(v interface{}) (string, error) {
b, err := json.Marshal(v) b, err := json.Marshal(v)
@ -36,6 +38,12 @@ app.activate()
var res=app.{{.Operation}}({{json .Options}}) var res=app.{{.Operation}}({{json .Options}})
if(Array.isArray(res)){res.join({{json .Separator}})}else{res.toString()} if(Array.isArray(res)){res.join({{json .Separator}})}else{res.toString()}
{{- end}} {{- end}}
{{define "list" -}}
var app=Application.currentApplication()
app.includeStandardAdditions=true
var res=app.chooseFromList({{json .Items}},{{json .Options}})
res.join({{json .Separator}})
{{- end}}
{{define "notify" -}} {{define "notify" -}}
var app=Application.currentApplication() var app=Application.currentApplication()
app.includeStandardAdditions=true app.includeStandardAdditions=true

View file

@ -100,8 +100,10 @@ var generator = template.Must(template.New("").Parse(`// Code generated by zenit
package zenutil package zenutil
import "encoding/json" import (
import "text/template" "encoding/json"
"text/template"
)
var scripts = template.Must(template.New("").Funcs(template.FuncMap{"json": func(v interface{}) (string, error) { var scripts = template.Must(template.New("").Funcs(template.FuncMap{"json": func(v interface{}) (string, error) {
b, err := json.Marshal(v) b, err := json.Marshal(v)

View file

@ -0,0 +1,5 @@
var app = Application.currentApplication()
app.includeStandardAdditions = true
var res = app.chooseFromList({{json .Items}}, {{json .Options}})
res.join({{json .Separator}})

View file

@ -23,9 +23,9 @@ func Run(ctx context.Context, script string, data interface{}) ([]byte, error) {
// Try to use syscall.Exec, fallback to exec.Command. // Try to use syscall.Exec, fallback to exec.Command.
if path, err := exec.LookPath("osascript"); err != nil { if path, err := exec.LookPath("osascript"); err != nil {
} else if t, err := ioutil.TempFile("", ""); err != nil { } else if t, err := ioutil.TempFile("", ""); err != nil {
} else if err := os.Remove(t.Name()); err != nil {
} else if _, err := t.WriteString(script); err != nil { } else if _, err := t.WriteString(script); err != nil {
} else if _, err := t.Seek(0, 0); err != nil { } else if _, err := t.Seek(0, 0); err != nil {
} else if err := os.Remove(t.Name()); err != nil {
} else if err := syscall.Dup2(int(t.Fd()), syscall.Stdin); err != nil { } else if err := syscall.Dup2(int(t.Fd()), syscall.Stdin); err != nil {
} else if err := os.Stderr.Close(); err != nil { } else if err := os.Stderr.Close(); err != nil {
} else { } else {
@ -86,6 +86,24 @@ type DialogOptions struct {
Timeout int `json:"givingUpAfter,omitempty"` Timeout int `json:"givingUpAfter,omitempty"`
} }
// List is internal.
type List struct {
Items []string
Separator string
Options ListOptions
}
// ListOptions is internal.
type ListOptions struct {
Title *string `json:"withTitle,omitempty"`
Prompt *string `json:"withPrompt,omitempty"`
OK *string `json:"okButtonName,omitempty"`
Cancel *string `json:"cancelButtonName,omitempty"`
Default []string `json:"defaultItems,omitempty"`
Multiple bool `json:"multipleSelectionsAllowed,omitempty"`
Empty bool `json:"emptySelectionAllowed,omitempty"`
}
// Notify is internal. // Notify is internal.
type Notify struct { type Notify struct {
Text string Text string

45
list.go Normal file
View file

@ -0,0 +1,45 @@
package zenity
// List displays the list dialog.
//
// Returns false on cancel, or ErrExtraButton.
//
// Valid options: Title, Width, Height, OKLabel, CancelLabel, ExtraButton,
// Icon, DefaultItems, DisallowEmpty.
func List(text string, items []string, options ...Option) (string, bool, error) {
return list(text, items, applyOptions(options))
}
// ListItems displays the list dialog.
//
// Returns false on cancel, or ErrExtraButton.
func ListItems(text string, items ...string) (string, bool, error) {
return List(text, items)
}
// ListMultiple displays the list dialog, allowing multiple items to be selected.
//
// Returns a nil slice on cancel, or ErrExtraButton.
//
// Valid options: Title, Width, Height, OKLabel, CancelLabel, ExtraButton,
// Icon, DefaultItems, DisallowEmpty.
func ListMultiple(text string, items []string, options ...Option) ([]string, error) {
return listMultiple(text, items, applyOptions(options))
}
// ListMultiple displays the list dialog, allowing multiple items to be selected.
//
// Returns a nil slice on cancel, or ErrExtraButton.
func ListMultipleItems(text string, items ...string) ([]string, error) {
return ListMultiple(text, items)
}
// DefaultItems returns an Option to set the items to initally select (macOS only).
func DefaultItems(items ...string) Option {
return funcOption(func(o *options) { o.defaultItems = items })
}
// DisallowEmpty returns an Option to not allow zero items to be selected (macOS only).
func DisallowEmpty() Option {
return funcOption(func(o *options) { o.disallowEmpty = true })
}

35
list_darwin.go Normal file
View file

@ -0,0 +1,35 @@
package zenity
import (
"github.com/ncruces/zenity/internal/zenutil"
)
func list(text string, items []string, opts options) (string, bool, error) {
var data zenutil.List
data.Items = items
data.Options.Prompt = &text
data.Options.Title = opts.title
data.Options.OK = opts.okLabel
data.Options.Cancel = opts.cancelLabel
data.Options.Default = opts.defaultItems
data.Options.Empty = !opts.disallowEmpty
out, err := zenutil.Run(opts.ctx, "list", data)
return strResult(opts, out, err)
}
func listMultiple(text string, items []string, opts options) ([]string, error) {
var data zenutil.List
data.Items = items
data.Options.Prompt = &text
data.Options.Title = opts.title
data.Options.OK = opts.okLabel
data.Options.Cancel = opts.cancelLabel
data.Options.Default = opts.defaultItems
data.Options.Empty = !opts.disallowEmpty
data.Options.Multiple = true
data.Separator = zenutil.Separator
out, err := zenutil.Run(opts.ctx, "list", data)
return lstResult(opts, out, err)
}

66
list_test.go Normal file
View file

@ -0,0 +1,66 @@
package zenity_test
import (
"context"
"errors"
"os"
"testing"
"time"
"github.com/ncruces/zenity"
)
func ExampleList() {
zenity.List(
"Select items from the list below:",
[]string{"apples", "oranges", "bananas", "strawberries"},
zenity.Title("Select items from the list"),
zenity.DisallowEmpty(),
)
// Output:
}
func ExampleListItems() {
zenity.ListItems(
"Select items from the list below:",
"apples", "oranges", "bananas", "strawberries")
// Output:
}
func ExampleListMultiple() {
zenity.ListMultiple(
"Select items from the list below:",
[]string{"apples", "oranges", "bananas", "strawberries"},
zenity.Title("Select items from the list"),
zenity.DefaultItems("apples", "bananas"),
)
// Output:
}
func ExampleListMultipleItems() {
zenity.ListMultipleItems(
"Select items from the list below:",
"apples", "oranges", "bananas", "strawberries")
// Output:
}
func TestListTimeout(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second/10)
_, _, err := zenity.List("", nil, zenity.Context(ctx))
if !os.IsTimeout(err) {
t.Error("did not timeout:", err)
}
cancel()
}
func TestListCancel(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
_, _, err := zenity.List("", nil, zenity.Context(ctx))
if !errors.Is(err, context.Canceled) {
t.Error("was not canceled:", err)
}
}

31
list_unix.go Normal file
View file

@ -0,0 +1,31 @@
// +build !windows,!darwin
package zenity
import (
"github.com/ncruces/zenity/internal/zenutil"
)
func list(text string, items []string, opts options) (string, bool, error) {
args := []string{"--list", "--column=", "--hide-header", "--text", text}
args = appendTitle(args, opts)
args = appendButtons(args, opts)
args = appendWidthHeight(args, opts)
args = appendIcon(args, opts)
args = append(args, items...)
out, err := zenutil.Run(opts.ctx, args)
return strResult(opts, out, err)
}
func listMultiple(text string, items []string, opts options) ([]string, error) {
args := []string{"--list", "--column=", "--hide-header", "--text", text, "--multiple", "--separator", zenutil.Separator}
args = appendTitle(args, opts)
args = appendButtons(args, opts)
args = appendWidthHeight(args, opts)
args = appendIcon(args, opts)
args = append(args, items...)
out, err := zenutil.Run(opts.ctx, args)
return lstResult(opts, out, err)
}

199
list_windows.go Normal file
View file

@ -0,0 +1,199 @@
package zenity
import (
"syscall"
"unsafe"
)
func list(text string, items []string, opts options) (string, bool, error) {
var title string
if opts.title != nil {
title = *opts.title
}
if opts.okLabel == nil {
opts.okLabel = stringPtr("OK")
}
if opts.cancelLabel == nil {
opts.cancelLabel = stringPtr("Cancel")
}
items, err := listDlg(title, text, items, false, opts)
if len(items) == 1 {
return items[0], true, err
}
return "", false, err
}
func listMultiple(text string, items []string, opts options) ([]string, error) {
var title string
if opts.title != nil {
title = *opts.title
}
if opts.okLabel == nil {
opts.okLabel = stringPtr("OK")
}
if opts.cancelLabel == nil {
opts.cancelLabel = stringPtr("Cancel")
}
return listDlg(title, text, items, true, opts)
}
func listDlg(title, text string, items []string, multiple bool, opts options) (out []string, err error) {
defer setup()()
font := getFont()
defer font.Delete()
defWindowProc := defWindowProc.Addr()
var wnd, textCtl, listCtl uintptr
var okBtn, cancelBtn, extraBtn uintptr
layout := func(dpi dpi) {
hfont := font.ForDPI(dpi)
sendMessage.Call(textCtl, 0x0030 /* WM_SETFONT */, hfont, 1)
sendMessage.Call(listCtl, 0x0030 /* WM_SETFONT */, hfont, 1)
sendMessage.Call(okBtn, 0x0030 /* WM_SETFONT */, hfont, 1)
sendMessage.Call(cancelBtn, 0x0030 /* WM_SETFONT */, hfont, 1)
setWindowPos.Call(wnd, 0, 0, 0, dpi.Scale(281), dpi.Scale(281), 0x6) // SWP_NOZORDER|SWP_NOMOVE
setWindowPos.Call(textCtl, 0, dpi.Scale(12), dpi.Scale(10), dpi.Scale(241), dpi.Scale(16), 0x4) // SWP_NOZORDER
setWindowPos.Call(listCtl, 0, dpi.Scale(12), dpi.Scale(30), dpi.Scale(241), dpi.Scale(164), 0x4) // SWP_NOZORDER
if extraBtn == 0 {
setWindowPos.Call(okBtn, 0, dpi.Scale(95), dpi.Scale(206), dpi.Scale(75), dpi.Scale(24), 0x4) // SWP_NOZORDER
setWindowPos.Call(cancelBtn, 0, dpi.Scale(178), dpi.Scale(206), dpi.Scale(75), dpi.Scale(24), 0x4) // SWP_NOZORDER
} else {
sendMessage.Call(extraBtn, 0x0030 /* WM_SETFONT */, hfont, 1)
setWindowPos.Call(okBtn, 0, dpi.Scale(12), dpi.Scale(206), dpi.Scale(75), dpi.Scale(24), 0x4) // SWP_NOZORDER
setWindowPos.Call(extraBtn, 0, dpi.Scale(95), dpi.Scale(206), dpi.Scale(75), dpi.Scale(24), 0x4) // SWP_NOZORDER
setWindowPos.Call(cancelBtn, 0, dpi.Scale(178), dpi.Scale(206), dpi.Scale(75), dpi.Scale(24), 0x4) // SWP_NOZORDER
}
}
proc := func(wnd uintptr, msg uint32, wparam, lparam uintptr) uintptr {
switch msg {
case 0x0002: // WM_DESTROY
postQuitMessage.Call(0)
case 0x0010: // WM_CLOSE
destroyWindow.Call(wnd)
case 0x0111: // WM_COMMAND
switch wparam {
default:
return 1
case 1, 6: // IDOK, IDYES
if multiple {
if len, _, _ := sendMessage.Call(listCtl, 0x190 /* LB_GETSELCOUNT */, 0, 0); int32(len) >= 0 {
out = make([]string, len)
if len > 0 {
indices := make([]int32, len)
sendMessage.Call(listCtl, 0x191 /* LB_GETSELITEMS */, len, uintptr(unsafe.Pointer(&indices[0])))
for i, idx := range indices {
out[i] = items[idx]
}
}
}
} else {
if idx, _, _ := sendMessage.Call(listCtl, 0x188 /* LB_GETCURSEL */, 0, 0); int32(idx) >= 0 {
out = []string{items[idx]}
} else {
out = []string{}
}
}
case 2: // IDCANCEL
case 7: // IDNO
err = ErrExtraButton
}
destroyWindow.Call(wnd)
case 0x02e0: // WM_DPICHANGED
layout(dpi(uint32(wparam) >> 16))
default:
ret, _, _ := syscall.Syscall6(defWindowProc, 4, wnd, uintptr(msg), wparam, lparam, 0, 0)
return ret
}
return 0
}
if opts.ctx != nil && opts.ctx.Err() != nil {
return nil, opts.ctx.Err()
}
instance, _, err := getModuleHandle.Call(0)
if instance == 0 {
return nil, err
}
cls, err := registerClass(instance, syscall.NewCallback(proc))
if cls == 0 {
return nil, err
}
defer unregisterClass.Call(cls, instance)
wnd, _, _ = createWindowEx.Call(0x10101, // WS_EX_CONTROLPARENT|WS_EX_WINDOWEDGE|WS_EX_DLGMODALFRAME
cls, strptr(title),
0x84c80000, // WS_POPUPWINDOW|WS_CLIPSIBLINGS|WS_DLGFRAME
0x80000000, // CW_USEDEFAULT
0x80000000, // CW_USEDEFAULT
281, 281, 0, 0, instance)
textCtl, _, _ = createWindowEx.Call(0,
strptr("STATIC"), strptr(text),
0x5002e080, // WS_CHILD|WS_VISIBLE|WS_GROUP|SS_WORDELLIPSIS|SS_EDITCONTROL|SS_NOPREFIX
12, 10, 241, 16, wnd, 0, instance)
var flags uintptr = 0x50320000 // WS_CHILD|WS_VISIBLE|WS_VSCROLL|WS_GROUP|WS_TABSTOP
if multiple {
flags |= 0x0800 // LBS_EXTENDEDSEL
}
listCtl, _, _ = createWindowEx.Call(0x200, // WS_EX_CLIENTEDGE
strptr("LISTBOX"), strptr(opts.entryText),
flags,
12, 30, 241, 164, wnd, 0, instance)
okBtn, _, _ = createWindowEx.Call(0,
strptr("BUTTON"), strptr(*opts.okLabel),
0x50030001, // WS_CHILD|WS_VISIBLE|WS_GROUP|WS_TABSTOP|BS_DEFPUSHBUTTON
12, 206, 75, 24, wnd, 1 /* IDOK */, instance)
cancelBtn, _, _ = createWindowEx.Call(0,
strptr("BUTTON"), strptr(*opts.cancelLabel),
0x50010000, // WS_CHILD|WS_VISIBLE|WS_GROUP|WS_TABSTOP
12, 206, 75, 24, wnd, 2 /* IDCANCEL */, instance)
if opts.extraButton != nil {
extraBtn, _, _ = createWindowEx.Call(0,
strptr("BUTTON"), strptr(*opts.extraButton),
0x50010000, // WS_CHILD|WS_VISIBLE|WS_GROUP|WS_TABSTOP
12, 206, 75, 24, wnd, 7 /* IDNO */, instance)
}
for _, item := range items {
sendMessage.Call(listCtl, 0x180 /* LB_ADDSTRING */, 0, strptr(item))
}
layout(getDPI(wnd))
centerWindow(wnd)
setFocus.Call(listCtl)
showWindow.Call(wnd, 1 /* SW_SHOWNORMAL */, 0)
if opts.ctx != nil {
wait := make(chan struct{})
defer close(wait)
go func() {
select {
case <-opts.ctx.Done():
sendMessage.Call(wnd, 0x0112 /* WM_SYSCOMMAND */, 0xf060 /* SC_CLOSE */, 0)
case <-wait:
}
}()
}
// set default values
out, err = nil, nil
if err := messageLoop(wnd); err != nil {
return nil, err
}
if opts.ctx != nil && opts.ctx.Err() != nil {
return nil, opts.ctx.Err()
}
return out, err
}

View file

@ -1,9 +1,6 @@
package zenity package zenity
import ( import (
"bytes"
"os/exec"
"github.com/ncruces/zenity/internal/zenutil" "github.com/ncruces/zenity/internal/zenutil"
) )
@ -36,15 +33,6 @@ func message(kind messageKind, text string, opts options) (bool, error) {
data.SetButtons(getButtons(dialog, kind == questionKind, opts)) data.SetButtons(getButtons(dialog, kind == questionKind, opts))
out, err := zenutil.Run(opts.ctx, "dialog", data) out, err := zenutil.Run(opts.ctx, "dialog", data)
if err, ok := err.(*exec.ExitError); ok && err.ExitCode() == 1 { _, ok, err := strResult(opts, out, err)
if opts.extraButton != nil && return ok, err
*opts.extraButton == string(bytes.TrimSuffix(out, []byte{'\n'})) {
return false, ErrExtraButton
}
return false, nil
}
if err != nil {
return false, err
}
return true, err
} }

View file

@ -3,10 +3,6 @@
package zenity package zenity
import ( import (
"bytes"
"os/exec"
"strconv"
"github.com/ncruces/zenity/internal/zenutil" "github.com/ncruces/zenity/internal/zenutil"
) )
@ -22,24 +18,10 @@ func message(kind messageKind, text string, opts options) (bool, error) {
case errorKind: case errorKind:
args = append(args, "--error") args = append(args, "--error")
} }
if opts.title != nil { args = appendTitle(args, opts)
args = append(args, "--title", *opts.title) args = appendButtons(args, opts)
} args = appendWidthHeight(args, opts)
if opts.width > 0 { args = appendIcon(args, opts)
args = append(args, "--width", strconv.FormatUint(uint64(opts.width), 10))
}
if opts.height > 0 {
args = append(args, "--height", strconv.FormatUint(uint64(opts.height), 10))
}
if opts.okLabel != nil {
args = append(args, "--ok-label", *opts.okLabel)
}
if opts.cancelLabel != nil {
args = append(args, "--cancel-label", *opts.cancelLabel)
}
if opts.extraButton != nil {
args = append(args, "--extra-button", *opts.extraButton)
}
if opts.noWrap { if opts.noWrap {
args = append(args, "--no-wrap") args = append(args, "--no-wrap")
} }
@ -51,13 +33,13 @@ func message(kind messageKind, text string, opts options) (bool, error) {
} }
switch opts.icon { switch opts.icon {
case ErrorIcon: case ErrorIcon:
args = append(args, "--window-icon=error", "--icon-name=dialog-error") args = append(args, "--icon-name=dialog-error")
case WarningIcon: case WarningIcon:
args = append(args, "--window-icon=warning", "--icon-name=dialog-warning") args = append(args, "--icon-name=dialog-warning")
case InfoIcon: case InfoIcon:
args = append(args, "--window-icon=info", "--icon-name=dialog-information") args = append(args, "--icon-name=dialog-information")
case QuestionIcon: case QuestionIcon:
args = append(args, "--window-icon=question", "--icon-name=dialog-question") args = append(args, "--icon-name=dialog-question")
case PasswordIcon: case PasswordIcon:
args = append(args, "--icon-name=dialog-password") args = append(args, "--icon-name=dialog-password")
case NoIcon: case NoIcon:
@ -65,15 +47,6 @@ func message(kind messageKind, text string, opts options) (bool, error) {
} }
out, err := zenutil.Run(opts.ctx, args) out, err := zenutil.Run(opts.ctx, args)
if err, ok := err.(*exec.ExitError); ok && err.ExitCode() == 1 { _, ok, err := strResult(opts, out, err)
if opts.extraButton != nil && return ok, err
*opts.extraButton == string(bytes.TrimSuffix(out, []byte{'\n'})) {
return false, ErrExtraButton
}
return false, nil
}
if err != nil {
return false, err
}
return true, err
} }

View file

@ -8,9 +8,7 @@ import (
func notify(text string, opts options) error { func notify(text string, opts options) error {
args := []string{"--notification", "--text", text} args := []string{"--notification", "--text", text}
if opts.title != nil { args = appendTitle(args, opts)
args = append(args, "--title", *opts.title)
}
switch opts.icon { switch opts.icon {
case ErrorIcon: case ErrorIcon:
args = append(args, "--window-icon=dialog-error") args = append(args, "--window-icon=dialog-error")

15
pwd.go Normal file
View file

@ -0,0 +1,15 @@
package zenity
// Password displays the password dialog.
//
// Returns false on cancel, or ErrExtraButton.
//
// Valid options: Title, OKLabel, CancelLabel, ExtraButton, Icon, Username.
func Password(options ...Option) (usr string, pw string, ok bool, err error) {
return password(applyOptions(options))
}
// Username returns an Option to display the username (Unix only).
func Username() Option {
return funcOption(func(o *options) { o.username = true })
}

9
pwd_stub.go Normal file
View file

@ -0,0 +1,9 @@
// +build windows darwin
package zenity
func password(opts options) (string, string, bool, error) {
opts.hideText = true
str, ok, err := entry("Password:", opts)
return "", str, ok, err
}

37
pwd_test.go Normal file
View file

@ -0,0 +1,37 @@
package zenity_test
import (
"context"
"errors"
"os"
"testing"
"time"
"github.com/ncruces/zenity"
)
func ExamplePassword() {
zenity.Password(zenity.Title("Type your password"))
// Output:
}
func TestPasswordTimeout(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second/10)
_, _, _, err := zenity.Password(zenity.Context(ctx))
if !os.IsTimeout(err) {
t.Error("did not timeout:", err)
}
cancel()
}
func TestPasswordCancel(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
_, _, _, err := zenity.Password(zenity.Context(ctx))
if !errors.Is(err, context.Canceled) {
t.Error("was not canceled:", err)
}
}

27
pwd_unix.go Normal file
View file

@ -0,0 +1,27 @@
// +build !windows,!darwin
package zenity
import (
"strings"
"github.com/ncruces/zenity/internal/zenutil"
)
func password(opts options) (string, string, bool, error) {
args := []string{"--password"}
args = appendTitle(args, opts)
args = appendButtons(args, opts)
if opts.username {
args = append(args, "--username")
}
out, err := zenutil.Run(opts.ctx, args)
str, ok, err := strResult(opts, out, err)
if ok && opts.username {
if split := strings.SplitN(string(out), "|", 2); len(split) == 2 {
return split[0], split[1], true, nil
}
}
return "", str, ok, err
}

78
util_unix.go Normal file
View file

@ -0,0 +1,78 @@
// +build !windows
package zenity
import (
"bytes"
"os/exec"
"strconv"
"strings"
"github.com/ncruces/zenity/internal/zenutil"
)
func appendTitle(args []string, opts options) []string {
if opts.title != nil {
args = append(args, "--title", *opts.title)
}
return args
}
func appendButtons(args []string, opts options) []string {
if opts.okLabel != nil {
args = append(args, "--ok-label", *opts.okLabel)
}
if opts.cancelLabel != nil {
args = append(args, "--cancel-label", *opts.cancelLabel)
}
if opts.extraButton != nil {
args = append(args, "--extra-button", *opts.extraButton)
}
return args
}
func appendWidthHeight(args []string, opts options) []string {
if opts.width > 0 {
args = append(args, "--width", strconv.FormatUint(uint64(opts.width), 10))
}
if opts.height > 0 {
args = append(args, "--height", strconv.FormatUint(uint64(opts.height), 10))
}
return args
}
func appendIcon(args []string, opts options) []string {
switch opts.icon {
case ErrorIcon:
args = append(args, "--window-icon=error")
case WarningIcon:
args = append(args, "--window-icon=warning")
case InfoIcon:
args = append(args, "--window-icon=info")
case QuestionIcon:
args = append(args, "--window-icon=question")
}
return args
}
func strResult(opts options, out []byte, err error) (string, bool, error) {
out = bytes.TrimSuffix(out, []byte{'\n'})
if err, ok := err.(*exec.ExitError); ok && err.ExitCode() == 1 {
if opts.extraButton != nil && *opts.extraButton == string(out) {
return "", false, ErrExtraButton
}
return "", false, nil
}
if err != nil {
return "", false, err
}
return string(out), true, nil
}
func lstResult(opts options, out []byte, err error) ([]string, error) {
str, ok, err := strResult(opts, out, err)
if ok {
return strings.Split(str, zenutil.Separator), nil
}
return nil, err
}

View file

@ -6,6 +6,7 @@ import (
"os" "os"
"reflect" "reflect"
"runtime" "runtime"
"strconv"
"sync/atomic" "sync/atomic"
"syscall" "syscall"
"unsafe" "unsafe"
@ -22,6 +23,10 @@ var (
commDlgExtendedError = comdlg32.NewProc("CommDlgExtendedError") commDlgExtendedError = comdlg32.NewProc("CommDlgExtendedError")
deleteObject = gdi32.NewProc("DeleteObject")
getDeviceCaps = gdi32.NewProc("GetDeviceCaps")
createFontIndirect = gdi32.NewProc("CreateFontIndirectW")
getModuleHandle = kernel32.NewProc("GetModuleHandleW") getModuleHandle = kernel32.NewProc("GetModuleHandleW")
getCurrentThreadId = kernel32.NewProc("GetCurrentThreadId") getCurrentThreadId = kernel32.NewProc("GetCurrentThreadId")
getConsoleWindow = kernel32.NewProc("GetConsoleWindow") getConsoleWindow = kernel32.NewProc("GetConsoleWindow")
@ -33,9 +38,13 @@ var (
getMessage = user32.NewProc("GetMessageW") getMessage = user32.NewProc("GetMessageW")
sendMessage = user32.NewProc("SendMessageW") sendMessage = user32.NewProc("SendMessageW")
postQuitMessage = user32.NewProc("PostQuitMessage")
isDialogMessage = user32.NewProc("IsDialogMessageW")
dispatchMessage = user32.NewProc("DispatchMessageW")
translateMessage = user32.NewProc("TranslateMessage")
getClassName = user32.NewProc("GetClassNameW") getClassName = user32.NewProc("GetClassNameW")
setWindowsHookEx = user32.NewProc("SetWindowsHookExW")
unhookWindowsHookEx = user32.NewProc("UnhookWindowsHookEx") unhookWindowsHookEx = user32.NewProc("UnhookWindowsHookEx")
setWindowsHookEx = user32.NewProc("SetWindowsHookExW")
callNextHookEx = user32.NewProc("CallNextHookEx") callNextHookEx = user32.NewProc("CallNextHookEx")
enumWindows = user32.NewProc("EnumWindows") enumWindows = user32.NewProc("EnumWindows")
enumChildWindows = user32.NewProc("EnumChildWindows") enumChildWindows = user32.NewProc("EnumChildWindows")
@ -45,8 +54,30 @@ var (
setForegroundWindow = user32.NewProc("SetForegroundWindow") setForegroundWindow = user32.NewProc("SetForegroundWindow")
getWindowThreadProcessId = user32.NewProc("GetWindowThreadProcessId") getWindowThreadProcessId = user32.NewProc("GetWindowThreadProcessId")
setThreadDpiAwarenessContext = user32.NewProc("SetThreadDpiAwarenessContext") setThreadDpiAwarenessContext = user32.NewProc("SetThreadDpiAwarenessContext")
getDpiForWindow = user32.NewProc("GetDpiForWindow")
releaseDC = user32.NewProc("ReleaseDC")
getWindowDC = user32.NewProc("GetWindowDC")
systemParametersInfo = user32.NewProc("SystemParametersInfoW")
setWindowPos = user32.NewProc("SetWindowPos")
getWindowRect = user32.NewProc("GetWindowRect")
getSystemMetrics = user32.NewProc("GetSystemMetrics")
unregisterClass = user32.NewProc("UnregisterClassW")
registerClassEx = user32.NewProc("RegisterClassExW")
destroyWindow = user32.NewProc("DestroyWindow")
createWindowEx = user32.NewProc("CreateWindowExW")
showWindow = user32.NewProc("ShowWindow")
setFocus = user32.NewProc("SetFocus")
defWindowProc = user32.NewProc("DefWindowProcW")
) )
func intptr(i int64) uintptr {
return uintptr(i)
}
func strptr(s string) uintptr {
return uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(s)))
}
func setup() context.CancelFunc { func setup() context.CancelFunc {
var hwnd uintptr var hwnd uintptr
enumWindows.Call(syscall.NewCallback(func(wnd, lparam uintptr) uintptr { enumWindows.Call(syscall.NewCallback(func(wnd, lparam uintptr) uintptr {
@ -83,8 +114,8 @@ func setup() context.CancelFunc {
return func() { return func() {
if old != 0 { if old != 0 {
setThreadDpiAwarenessContext.Call(old) setThreadDpiAwarenessContext.Call(old)
runtime.UnlockOSThread()
} }
runtime.UnlockOSThread()
} }
} }
@ -97,15 +128,6 @@ func commDlgError() error {
} }
} }
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-cwpretstruct
type _CWPRETSTRUCT struct {
Result uintptr
LParam uintptr
WParam uintptr
Message uint32
Wnd uintptr
}
func hookDialog(ctx context.Context, initDialog func(wnd uintptr)) (unhook context.CancelFunc, err error) { func hookDialog(ctx context.Context, initDialog func(wnd uintptr)) (unhook context.CancelFunc, err error) {
if ctx != nil && ctx.Err() != nil { if ctx != nil && ctx.Err() != nil {
return nil, ctx.Err() return nil, ctx.Err()
@ -174,12 +196,202 @@ func hookDialogTitle(ctx context.Context, title *string) (unhook context.CancelF
return hookDialog(ctx, init) return hookDialog(ctx, init)
} }
func strptr(s string) uintptr { type dpi uintptr
return uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(s)))
func getDPI(wnd uintptr) dpi {
var res uintptr
if wnd != 0 && getDpiForWindow.Find() == nil {
res, _, _ = getDpiForWindow.Call(wnd)
} else if dc, _, _ := getWindowDC.Call(wnd); dc != 0 {
res, _, _ = getDeviceCaps.Call(dc, 90) // LOGPIXELSY
releaseDC.Call(0, dc)
} }
func intptr(i int64) uintptr { if res == 0 {
return uintptr(i) return 96 // USER_DEFAULT_SCREEN_DPI
}
return dpi(res)
}
func (d dpi) Scale(dim uintptr) uintptr {
if d == 0 {
return dim
}
return dim * uintptr(d) / 96 // USER_DEFAULT_SCREEN_DPI
}
type font struct {
handle uintptr
logical _LOGFONT
}
func getFont() font {
var metrics _NONCLIENTMETRICS
metrics.Size = uint32(unsafe.Sizeof(metrics))
systemParametersInfo.Call(0x29, // SPI_GETNONCLIENTMETRICS
unsafe.Sizeof(metrics), uintptr(unsafe.Pointer(&metrics)), 0)
return font{logical: metrics.MessageFont}
}
func (f *font) ForDPI(dpi dpi) uintptr {
if h := -int32(dpi.Scale(12)); f.handle == 0 || f.logical.Height != h {
f.Delete()
f.logical.Height = h
f.handle, _, _ = createFontIndirect.Call(uintptr(unsafe.Pointer(&f.logical)))
}
return f.handle
}
func (f *font) Delete() {
if f.handle != 0 {
deleteObject.Call(f.handle)
f.handle = 0
}
}
func centerWindow(wnd uintptr) {
getMetric := func(i uintptr) int32 {
ret, _, _ := getSystemMetrics.Call(i)
return int32(ret)
}
var rect _RECT
getWindowRect.Call(wnd, uintptr(unsafe.Pointer(&rect)))
x := (getMetric(0 /* SM_CXSCREEN */) - (rect.right - rect.left)) / 2
y := (getMetric(1 /* SM_CYSCREEN */) - (rect.bottom - rect.top)) / 2
setWindowPos.Call(wnd, 0, uintptr(x), uintptr(y), 0, 0, 0x5) // SWP_NOZORDER|SWP_NOSIZE
}
func getWindowString(wnd uintptr) string {
len, _, _ := getWindowTextLength.Call(wnd)
buf := make([]uint16, len+1)
getWindowText.Call(wnd, uintptr(unsafe.Pointer(&buf[0])), len+1)
return syscall.UTF16ToString(buf)
}
func registerClass(instance, proc uintptr) (uintptr, error) {
name := "WC_" + strconv.FormatUint(uint64(proc), 16)
var wcx _WNDCLASSEX
wcx.Size = uint32(unsafe.Sizeof(wcx))
wcx.WndProc = proc
wcx.Instance = instance
wcx.Background = 5 // COLOR_WINDOW
wcx.ClassName = syscall.StringToUTF16Ptr(name)
ret, _, err := registerClassEx.Call(uintptr(unsafe.Pointer(&wcx)))
return ret, err
}
// https://docs.microsoft.com/en-us/windows/win32/winmsg/using-messages-and-message-queues
func messageLoop(wnd uintptr) error {
getMessage := getMessage.Addr()
isDialogMessage := isDialogMessage.Addr()
translateMessage := translateMessage.Addr()
dispatchMessage := dispatchMessage.Addr()
for {
var msg _MSG
ret, _, err := syscall.Syscall6(getMessage, 4, uintptr(unsafe.Pointer(&msg)), 0, 0, 0, 0, 0)
if int32(ret) == -1 {
return err
}
if ret == 0 {
return nil
}
ret, _, _ = syscall.Syscall(isDialogMessage, 2, wnd, uintptr(unsafe.Pointer(&msg)), 0)
if ret == 0 {
syscall.Syscall(translateMessage, 1, uintptr(unsafe.Pointer(&msg)), 0, 0)
syscall.Syscall(dispatchMessage, 1, uintptr(unsafe.Pointer(&msg)), 0, 0)
}
}
}
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-cwpretstruct
type _CWPRETSTRUCT struct {
Result uintptr
LParam uintptr
WParam uintptr
Message uint32
Wnd uintptr
}
// https://docs.microsoft.com/en-us/windows/win32/api/wingdi/ns-wingdi-logfontw
type _LOGFONT struct {
Height int32
Width int32
Escapement int32
Orientation int32
Weight int32
Italic byte
Underline byte
StrikeOut byte
CharSet byte
OutPrecision byte
ClipPrecision byte
Quality byte
PitchAndFamily byte
FaceName [32]uint16
}
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-nonclientmetricsw
type _NONCLIENTMETRICS struct {
Size uint32
BorderWidth int32
ScrollWidth int32
ScrollHeight int32
CaptionWidth int32
CaptionHeight int32
CaptionFont _LOGFONT
SmCaptionWidth int32
SmCaptionHeight int32
SmCaptionFont _LOGFONT
MenuWidth int32
MenuHeight int32
MenuFont _LOGFONT
StatusFont _LOGFONT
MessageFont _LOGFONT
}
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-msg
type _MSG struct {
Owner syscall.Handle
Message uint32
WParam uintptr
LParam uintptr
Time uint32
Pt _POINT
}
// https://docs.microsoft.com/en-us/windows/win32/api/windef/ns-windef-point
type _POINT struct {
x, y int32
}
// https://docs.microsoft.com/en-us/windows/win32/api/windef/ns-windef-rect
type _RECT struct {
left int32
top int32
right int32
bottom int32
}
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-wndclassexw
type _WNDCLASSEX struct {
Size uint32
Style uint32
WndProc uintptr
ClsExtra int32
WndExtra int32
Instance uintptr
Icon uintptr
Cursor uintptr
Background uintptr
MenuName *uint16
ClassName *uint16
IconSm uintptr
} }
// https://github.com/wine-mirror/wine/blob/master/include/unknwn.idl // https://github.com/wine-mirror/wine/blob/master/include/unknwn.idl

View file

@ -1,5 +0,0 @@
package zenity
func init() {
user32.NewProc("SetProcessDPIAware").Call()
}

View file

@ -41,6 +41,10 @@ type options struct {
ellipsize bool ellipsize bool
defaultCancel bool defaultCancel bool
// List options
disallowEmpty bool
defaultItems []string
// File selection options // File selection options
directory bool directory bool
confirmOverwrite bool confirmOverwrite bool