Merge pull request #12 from ncruces/progress

Progress dialog.
This commit is contained in:
Nuno Cruces 2021-05-04 12:53:19 +01:00 committed by GitHub
commit 4d3f930ac7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
51 changed files with 1221 additions and 310 deletions

View File

@ -13,14 +13,14 @@ Implemented dialogs:
* [text entry](https://github.com/ncruces/zenity/wiki/Text-Entry-dialog)
* [list](https://github.com/ncruces/zenity/wiki/List-dialog) (simple)
* [password](https://github.com/ncruces/zenity/wiki/Password-dialog)
* [file selection](https://github.com/ncruces/zenity/wiki/File-Selection-dialog)
* [color selection](https://github.com/ncruces/zenity/wiki/Color-Selection-dialog)
* [file selection](https://github.com/ncruces/zenity/wiki/File-selection-dialog)
* [color selection](https://github.com/ncruces/zenity/wiki/Color-selection-dialog)
* [progress](https://github.com/ncruces/zenity/wiki/Progress-dialog)
* [notification](https://github.com/ncruces/zenity/wiki/Notification)
Behavior on Windows, macOS and other Unixes might differ slightly.
Some of that is intended (reflecting platform differences),
other bits are unfortunate limitations,
others still are open to be fixed.
other bits are unfortunate limitations.
## Why?
@ -37,7 +37,8 @@ Why reinvent this particular wheel?
* Explorer shell not required
* works in Server Core
* Unicode support
* High DPI support (no manifest required)
* High DPI (no manifest required)
* Visual Styles (no manifest required)
* WSL/Cygwin/MSYS2 [support](https://github.com/ncruces/zenity/wiki/Zenity-for-WSL,-Cygwin,-MSYS2)
* on macOS:
* only dependency is `osascript`

View File

@ -34,6 +34,7 @@ var (
passwordDlg bool
fileSelectionDlg bool
colorSelectionDlg bool
progressDlg bool
notification bool
// General options
@ -73,6 +74,13 @@ var (
defaultColor string
showPalette bool
// Progress options
percentage float64
pulsate bool
autoClose bool
autoKill bool
noCancel bool
// Windows specific options
cygpath bool
wslpath bool
@ -95,32 +103,32 @@ func main() {
if zenutil.Timeout > 0 {
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(zenutil.Timeout)*time.Second)
opts = append(opts, zenity.Context(ctx))
_ = cancel
defer cancel()
}
switch {
case errorDlg:
okResult(zenity.Error(text, opts...))
errResult(zenity.Error(text, opts...))
case infoDlg:
okResult(zenity.Info(text, opts...))
errResult(zenity.Info(text, opts...))
case warningDlg:
okResult(zenity.Warning(text, opts...))
errResult(zenity.Warning(text, opts...))
case questionDlg:
okResult(zenity.Question(text, opts...))
errResult(zenity.Question(text, opts...))
case entryDlg:
strOKResult(zenity.Entry(text, opts...))
strResult(zenity.Entry(text, opts...))
case listDlg:
if multiple {
listResult(zenity.ListMultiple(text, flag.Args(), opts...))
lstResult(zenity.ListMultiple(text, flag.Args(), opts...))
} else {
strOKResult(zenity.List(text, flag.Args(), opts...))
strResult(zenity.List(text, flag.Args(), opts...))
}
case passwordDlg:
_, pw, ok, err := zenity.Password(opts...)
strOKResult(pw, ok, err)
_, pw, err := zenity.Password(opts...)
strResult(pw, err)
case fileSelectionDlg:
switch {
@ -129,17 +137,21 @@ func main() {
case save:
strResult(egestPath(zenity.SelectFileSave(opts...)))
case multiple:
listResult(egestPaths(zenity.SelectFileMutiple(opts...)))
lstResult(egestPaths(zenity.SelectFileMutiple(opts...)))
}
case colorSelectionDlg:
colorResult(zenity.SelectColor(opts...))
colResult(zenity.SelectColor(opts...))
case progressDlg:
errResult(progress(opts...))
case notification:
errResult(zenity.Notify(text, opts...))
}
flag.Usage()
default:
flag.Usage()
}
}
func setupFlags() {
@ -153,6 +165,7 @@ func setupFlags() {
flag.BoolVar(&passwordDlg, "password", false, "Display password dialog")
flag.BoolVar(&fileSelectionDlg, "file-selection", false, "Display file selection dialog")
flag.BoolVar(&colorSelectionDlg, "color-selection", false, "Display color selection dialog")
flag.BoolVar(&progressDlg, "progress", false, "Display progress indication dialog")
flag.BoolVar(&notification, "notification", false, "Display notification")
// General options
@ -165,12 +178,12 @@ func setupFlags() {
flag.StringVar(&text, "text", "", "Set the dialog `text`")
flag.StringVar(&icon, "window-icon", "", "Set the window `icon` (error, info, question, warning)")
flag.BoolVar(&multiple, "multiple", false, "Allow multiple items to be selected")
flag.BoolVar(&defaultCancel, "default-cancel", false, "Give Cancel button focus by default")
// Message options
flag.StringVar(&icon, "icon-name", "", "Set the dialog `icon` (dialog-error, dialog-information, dialog-question, dialog-warning)")
flag.BoolVar(&noWrap, "no-wrap", false, "Do not enable text wrapping")
flag.BoolVar(&ellipsize, "ellipsize", false, "Enable ellipsizing in the dialog text")
flag.BoolVar(&defaultCancel, "default-cancel", false, "Give Cancel button focus by default")
// Entry options
flag.StringVar(&entryText, "entry-text", "", "Set the entry `text`")
@ -194,6 +207,15 @@ func setupFlags() {
flag.StringVar(&defaultColor, "color", "", "Set the `color`")
flag.BoolVar(&showPalette, "show-palette", false, "Show the palette")
// Progress options
flag.Float64Var(&percentage, "percentage", 0, "Set initial `percentage`")
flag.BoolVar(&pulsate, "pulsate", false, "Pulsate progress bar")
flag.BoolVar(&noCancel, "no-cancel", false, "Hide Cancel button (Windows and Unix only)")
flag.BoolVar(&autoClose, "auto-close", false, "Dismiss the dialog when 100% has been reached")
if runtime.GOOS != "windows" {
flag.BoolVar(&autoKill, "auto-kill", false, "Kill parent process if Cancel button is pressed (macOS and Unix only)")
}
// Windows specific options
if runtime.GOOS == "windows" {
flag.BoolVar(&cygpath, "cygpath", false, "Use cygpath for path translation (Windows only)")
@ -242,6 +264,9 @@ func validateFlags() {
if colorSelectionDlg {
n++
}
if progressDlg {
n++
}
if notification {
n++
}
@ -297,6 +322,11 @@ func loadFlags() []zenity.Option {
setDefault(&icon, "dialog-password")
setDefault(&okLabel, "OK")
setDefault(&cancelLabel, "Cancel")
case progressDlg:
setDefault(&title, "Progress")
setDefault(&text, "Running...")
setDefault(&okLabel, "OK")
setDefault(&cancelLabel, "Cancel")
default:
setDefault(&text, "")
}
@ -388,6 +418,15 @@ func loadFlags() []zenity.Option {
opts = append(opts, zenity.ShowPalette())
}
// Progress options
if pulsate {
opts = append(opts, zenity.Pulsate())
}
if noCancel {
opts = append(opts, zenity.NoCancel())
}
return opts
}
@ -395,6 +434,9 @@ func errResult(err error) {
if os.IsTimeout(err) {
os.Exit(5)
}
if err == zenity.ErrCanceled {
os.Exit(1)
}
if err == zenity.ErrExtraButton {
os.Stdout.WriteString(extraButton)
os.Stdout.WriteString(zenutil.LineBreak)
@ -408,64 +450,33 @@ func errResult(err error) {
os.Exit(0)
}
func okResult(ok bool, err error) {
if err != nil {
errResult(err)
}
if ok {
os.Exit(0)
}
os.Exit(1)
}
func strResult(s string, err error) {
if err != nil {
errResult(err)
}
if s == "" {
os.Exit(1)
}
os.Stdout.WriteString(s)
os.Stdout.WriteString(zenutil.LineBreak)
os.Exit(0)
}
func listResult(l []string, err error) {
func lstResult(l []string, err error) {
if err != nil {
errResult(err)
}
if l == nil {
os.Exit(1)
}
os.Stdout.WriteString(strings.Join(l, zenutil.Separator))
os.Stdout.WriteString(zenutil.LineBreak)
os.Exit(0)
}
func colorResult(c color.Color, err error) {
func colResult(c color.Color, err error) {
if err != nil {
errResult(err)
}
if c == nil {
os.Exit(1)
}
os.Stdout.WriteString(zenutil.UnparseColor(c))
os.Stdout.WriteString(zenutil.LineBreak)
os.Exit(0)
}
func strOKResult(s string, ok bool, err error) {
if err != nil {
errResult(err)
}
if !ok {
os.Exit(1)
}
os.Stdout.WriteString(s)
os.Stdout.WriteString(zenutil.LineBreak)
os.Exit(0)
}
func ingestPath(path string) string {
if runtime.GOOS == "windows" && path != "" {
var args []string

72
cmd/zenity/progress.go Normal file
View File

@ -0,0 +1,72 @@
package main
import (
"bufio"
"math"
"os"
"strconv"
"strings"
"github.com/ncruces/zenity"
)
func progress(opts ...zenity.Option) (err error) {
dlg, err := zenity.Progress(opts...)
if err != nil {
return err
}
if autoKill {
defer func() {
if err == zenity.ErrCanceled {
killParent()
}
}()
}
if err := dlg.Text(text); err != nil {
return err
}
if err := dlg.Value(int(math.Round(percentage))); err != nil {
return err
}
lines := make(chan string)
go func() {
defer close(lines)
for scanner := bufio.NewScanner(os.Stdin); scanner.Scan(); {
lines <- scanner.Text()
}
}()
for {
select {
case line, ok := <-lines:
if !ok {
break
}
if len(line) > 1 && line[0] == '#' {
if err := dlg.Text(strings.TrimSpace(line[1:])); err != nil {
return err
}
} else if v, err := strconv.ParseFloat(line, 64); err == nil {
if err := dlg.Value(int(math.Round(v))); err != nil {
return err
}
if v >= 100 && autoClose {
return dlg.Close()
}
}
continue
case <-dlg.Done():
}
break
}
if err := dlg.Complete(); err != nil {
return err
}
<-dlg.Done()
return dlg.Close()
}

View File

@ -0,0 +1,12 @@
// +build !windows,!js
package main
import (
"os"
"syscall"
)
func killParent() {
syscall.Kill(os.Getppid(), syscall.SIGHUP)
}

View File

@ -0,0 +1,3 @@
package main
func killParent() {}

View File

@ -4,8 +4,6 @@ import "image/color"
// SelectColor displays the color selection dialog.
//
// Returns nil on cancel.
//
// Valid options: Title, Color, ShowPalette.
func SelectColor(options ...Option) (color.Color, error) {
return selectColor(applyOptions(options))

View File

@ -20,9 +20,9 @@ func selectColor(opts options) (color.Color, error) {
float32(g) / 0xffff,
float32(b) / 0xffff,
})
str, ok, err := strResult(opts, out, err)
if ok {
return zenutil.ParseColor(str), nil
str, err := strResult(opts, out, err)
if err != nil {
return nil, err
}
return nil, err
return zenutil.ParseColor(str), nil
}

View File

@ -9,6 +9,7 @@ import (
"time"
"github.com/ncruces/zenity"
"go.uber.org/goleak"
)
func ExampleSelectColor() {
@ -24,18 +25,19 @@ func ExampleSelectColor_palette() {
// Output:
}
func TestSelectColorTimeout(t *testing.T) {
func TestSelectColor_timeout(t *testing.T) {
defer goleak.VerifyNone(t)
ctx, cancel := context.WithTimeout(context.Background(), time.Second/10)
defer cancel()
_, err := zenity.SelectColor(zenity.Context(ctx))
if !os.IsTimeout(err) {
t.Error("did not timeout:", err)
}
cancel()
}
func TestSelectColorCancel(t *testing.T) {
func TestSelectColor_cancel(t *testing.T) {
defer goleak.VerifyNone(t)
ctx, cancel := context.WithCancel(context.Background())
cancel()

View File

@ -20,9 +20,9 @@ func selectColor(opts options) (color.Color, error) {
}
out, err := zenutil.Run(opts.ctx, args)
str, ok, err := strResult(opts, out, err)
if ok {
return zenutil.ParseColor(str), nil
str, err := strResult(opts, out, err)
if err != nil {
return nil, err
}
return nil, err
return zenutil.ParseColor(str), nil
}

View File

@ -9,7 +9,7 @@ import (
var (
chooseColor = comdlg32.NewProc("ChooseColorW")
savedColors = [16]uint32{}
savedColors [16]uint32
colorsMutex sync.Mutex
)
@ -32,7 +32,7 @@ func selectColor(opts options) (color.Color, error) {
if opts.color != nil {
args.Flags |= 0x1 // CC_RGBINIT
n := color.NRGBAModel.Convert(opts.color).(color.NRGBA)
args.RgbResult = uint32(n.R) | (uint32(n.G) << 8) | (uint32(n.B) << 16)
args.RgbResult = uint32(n.R) | uint32(n.G)<<8 | uint32(n.B)<<16
}
if opts.showPalette {
args.Flags |= 0x4 // CC_PREVENTFULLOPEN

View File

@ -2,11 +2,9 @@ package zenity
// Entry displays the text entry dialog.
//
// Returns false on cancel, or ErrExtraButton.
//
// Valid options: Title, Width, Height, OKLabel, CancelLabel, ExtraButton,
// Icon, EntryText, HideText.
func Entry(text string, options ...Option) (string, bool, error) {
func Entry(text string, options ...Option) (string, error) {
return entry(text, applyOptions(options))
}

View File

@ -4,7 +4,7 @@ import (
"github.com/ncruces/zenity/internal/zenutil"
)
func entry(text string, opts options) (string, bool, error) {
func entry(text string, opts options) (string, error) {
var data zenutil.Dialog
data.Text = text
data.Operation = "displayDialog"

View File

@ -8,6 +8,7 @@ import (
"time"
"github.com/ncruces/zenity"
"go.uber.org/goleak"
)
func ExampleEntry() {
@ -16,22 +17,23 @@ func ExampleEntry() {
// Output:
}
func TestEntryTimeout(t *testing.T) {
func TestEntry_timeout(t *testing.T) {
defer goleak.VerifyNone(t)
ctx, cancel := context.WithTimeout(context.Background(), time.Second/10)
defer cancel()
_, _, err := zenity.Entry("", zenity.Context(ctx))
_, err := zenity.Entry("", zenity.Context(ctx))
if !os.IsTimeout(err) {
t.Error("did not timeout:", err)
}
cancel()
}
func TestEntryCancel(t *testing.T) {
func TestEntry_cancel(t *testing.T) {
defer goleak.VerifyNone(t)
ctx, cancel := context.WithCancel(context.Background())
cancel()
_, _, err := zenity.Entry("", zenity.Context(ctx))
_, err := zenity.Entry("", zenity.Context(ctx))
if !errors.Is(err, context.Canceled) {
t.Error("was not canceled:", err)
}

View File

@ -6,7 +6,7 @@ import (
"github.com/ncruces/zenity/internal/zenutil"
)
func entry(text string, opts options) (string, bool, error) {
func entry(text string, opts options) (string, error) {
args := []string{"--entry", "--text", text}
args = appendTitle(args, opts)
args = appendButtons(args, opts)

View File

@ -4,10 +4,9 @@ import (
"syscall"
)
func entry(text string, opts options) (out string, ok bool, err error) {
var title string
if opts.title != nil {
title = *opts.title
func entry(text string, opts options) (out string, err error) {
if opts.title == nil {
opts.title = stringPtr("")
}
if opts.okLabel == nil {
opts.okLabel = stringPtr("OK")
@ -15,10 +14,7 @@ func entry(text string, opts options) (out string, ok bool, err error) {
if opts.cancelLabel == nil {
opts.cancelLabel = stringPtr("Cancel")
}
return entryDlg(title, text, opts)
}
func entryDlg(title, text string, opts options) (out string, ok bool, err error) {
defer setup()()
font := getFont()
defer font.Delete()
@ -33,6 +29,7 @@ func entryDlg(title, text string, opts options) (out string, ok bool, err error)
sendMessage.Call(editCtl, 0x0030 /* WM_SETFONT */, hfont, 1)
sendMessage.Call(okBtn, 0x0030 /* WM_SETFONT */, hfont, 1)
sendMessage.Call(cancelBtn, 0x0030 /* WM_SETFONT */, hfont, 1)
sendMessage.Call(extraBtn, 0x0030 /* WM_SETFONT */, hfont, 1)
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(editCtl, 0, dpi.Scale(12), dpi.Scale(30), dpi.Scale(241), dpi.Scale(24), 0x4) // SWP_NOZORDER
@ -40,7 +37,6 @@ func entryDlg(title, text string, opts options) (out string, ok bool, err error)
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(66), 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(66), 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(66), dpi.Scale(75), dpi.Scale(24), 0x4) // SWP_NOZORDER
@ -53,6 +49,7 @@ func entryDlg(title, text string, opts options) (out string, ok bool, err error)
postQuitMessage.Call(0)
case 0x0010: // WM_CLOSE
err = ErrCanceled
destroyWindow.Call(wnd)
case 0x0111: // WM_COMMAND
@ -61,8 +58,8 @@ func entryDlg(title, text string, opts options) (out string, ok bool, err error)
return 1
case 1, 6: // IDOK, IDYES
out = getWindowString(editCtl)
ok = true
case 2: // IDCANCEL
err = ErrCanceled
case 7: // IDNO
err = ErrExtraButton
}
@ -72,39 +69,39 @@ func entryDlg(title, text string, opts options) (out string, ok bool, err error)
layout(dpi(uint32(wparam) >> 16))
default:
ret, _, _ := syscall.Syscall6(defWindowProc, 4, wnd, uintptr(msg), wparam, lparam, 0, 0)
return ret
res, _, _ := syscall.Syscall6(defWindowProc, 4, wnd, uintptr(msg), wparam, lparam, 0, 0)
return res
}
return 0
}
if opts.ctx != nil && opts.ctx.Err() != nil {
return "", false, opts.ctx.Err()
return "", opts.ctx.Err()
}
instance, _, err := getModuleHandle.Call(0)
if instance == 0 {
return "", false, err
return "", err
}
cls, err := registerClass(instance, syscall.NewCallback(proc))
if cls == 0 {
return "", false, err
return "", err
}
defer unregisterClass.Call(cls, instance)
wnd, _, _ = createWindowEx.Call(0x10101, // WS_EX_CONTROLPARENT|WS_EX_WINDOWEDGE|WS_EX_DLGMODALFRAME
cls, strptr(title),
cls, strptr(*opts.title),
0x84c80000, // WS_POPUPWINDOW|WS_CLIPSIBLINGS|WS_DLGFRAME
0x80000000, // CW_USEDEFAULT
0x80000000, // CW_USEDEFAULT
281, 141, 0, 0, instance)
281, 141, 0, 0, instance, 0)
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)
12, 10, 241, 16, wnd, 0, instance, 0)
var flags uintptr = 0x50030080 // WS_CHILD|WS_VISIBLE|WS_GROUP|WS_TABSTOP|ES_AUTOHSCROLL
if opts.hideText {
@ -113,21 +110,21 @@ func entryDlg(title, text string, opts options) (out string, ok bool, err error)
editCtl, _, _ = createWindowEx.Call(0x200, // WS_EX_CLIENTEDGE
strptr("EDIT"), strptr(opts.entryText),
flags,
12, 30, 241, 24, wnd, 0, instance)
12, 30, 241, 24, wnd, 0, instance, 0)
okBtn, _, _ = createWindowEx.Call(0,
strptr("BUTTON"), strptr(*opts.okLabel),
0x50030001, // WS_CHILD|WS_VISIBLE|WS_GROUP|WS_TABSTOP|BS_DEFPUSHBUTTON
12, 66, 75, 24, wnd, 1 /* IDOK */, instance)
12, 66, 75, 24, wnd, 1 /* IDOK */, instance, 0)
cancelBtn, _, _ = createWindowEx.Call(0,
strptr("BUTTON"), strptr(*opts.cancelLabel),
0x50010000, // WS_CHILD|WS_VISIBLE|WS_GROUP|WS_TABSTOP
12, 66, 75, 24, wnd, 2 /* IDCANCEL */, instance)
12, 66, 75, 24, wnd, 2 /* IDCANCEL */, instance, 0)
if opts.extraButton != nil {
extraBtn, _, _ = createWindowEx.Call(0,
strptr("BUTTON"), strptr(*opts.extraButton),
0x50010000, // WS_CHILD|WS_VISIBLE|WS_GROUP|WS_TABSTOP
12, 66, 75, 24, wnd, 7 /* IDNO */, instance)
12, 66, 75, 24, wnd, 7 /* IDNO */, instance, 0)
}
layout(getDPI(wnd))
@ -149,13 +146,13 @@ func entryDlg(title, text string, opts options) (out string, ok bool, err error)
}
// set default values
out, ok, err = "", false, nil
out, err = "", nil
if err := messageLoop(wnd); err != nil {
return "", false, err
return "", err
}
if opts.ctx != nil && opts.ctx.Err() != nil {
return "", false, opts.ctx.Err()
return "", opts.ctx.Err()
}
return out, ok, err
return out, err
}

View File

@ -8,8 +8,6 @@ import (
// SelectFile displays the file selection dialog.
//
// Returns an empty string on cancel.
//
// Valid options: Title, Directory, Filename, ShowHidden, FileFilter(s).
func SelectFile(options ...Option) (string, error) {
return selectFile(applyOptions(options))
@ -17,8 +15,6 @@ func SelectFile(options ...Option) (string, error) {
// SelectFileMutiple displays the multiple file selection dialog.
//
// Returns a nil slice on cancel.
//
// Valid options: Title, Directory, Filename, ShowHidden, FileFilter(s).
func SelectFileMutiple(options ...Option) ([]string, error) {
return selectFileMutiple(applyOptions(options))
@ -26,8 +22,6 @@ func SelectFileMutiple(options ...Option) ([]string, error) {
// SelectFileSave displays the save file selection dialog.
//
// Returns an empty string on cancel.
//
// Valid options: Title, Filename, ConfirmOverwrite, ConfirmCreate, ShowHidden,
// FileFilter(s).
func SelectFileSave(options ...Option) (string, error) {
@ -69,6 +63,9 @@ func Filename(filename string) Option {
//
// macOS hides filename filters from the user,
// and only supports filtering by extension (or "type").
//
// Patterns may use the GTK syntax on all platforms:
// https://developer.gnome.org/pygtk/stable/class-gtkfilefilter.html#method-gtkfilefilter--add-pattern
type FileFilter struct {
Name string // display string that describes the filter (optional)
Patterns []string // filter patterns for the display string

View File

@ -18,8 +18,7 @@ func selectFile(opts options) (string, error) {
}
out, err := zenutil.Run(opts.ctx, "file", data)
str, _, err := strResult(opts, out, err)
return str, err
return strResult(opts, out, err)
}
func selectFileMutiple(opts options) ([]string, error) {
@ -54,6 +53,5 @@ func selectFileSave(opts options) (string, error) {
}
out, err := zenutil.Run(opts.ctx, "file", data)
str, _, err := strResult(opts, out, err)
return str, err
return strResult(opts, out, err)
}

View File

@ -8,6 +8,7 @@ import (
"time"
"github.com/ncruces/zenity"
"go.uber.org/goleak"
)
const defaultPath = ``
@ -77,7 +78,7 @@ var fileFuncs = []func(...zenity.Option) (string, error){
},
}
func TestFileTimeout(t *testing.T) {
func TestFile_timeout(t *testing.T) {
for _, f := range fileFuncs {
ctx, cancel := context.WithTimeout(context.Background(), time.Second/10)
@ -87,10 +88,12 @@ func TestFileTimeout(t *testing.T) {
}
cancel()
goleak.VerifyNone(t)
}
}
func TestFileCancel(t *testing.T) {
func TestFile_cancel(t *testing.T) {
defer goleak.VerifyNone(t)
ctx, cancel := context.WithCancel(context.Background())
cancel()

View File

@ -14,8 +14,7 @@ func selectFile(opts options) (string, error) {
args = appendFileArgs(args, opts)
out, err := zenutil.Run(opts.ctx, args)
str, _, err := strResult(opts, out, err)
return str, err
return strResult(opts, out, err)
}
func selectFileMutiple(opts options) ([]string, error) {
@ -33,8 +32,7 @@ func selectFileSave(opts options) (string, error) {
args = appendFileArgs(args, opts)
out, err := zenutil.Run(opts.ctx, args)
str, _, err := strResult(opts, out, err)
return str, err
return strResult(opts, out, err)
}
func initFilters(filters []FileFilter) []string {

View File

@ -37,7 +37,7 @@ func selectFile(opts options) (string, error) {
args.Filter = &initFilters(opts.fileFilters)[0]
}
res := [32768]uint16{}
var res [32768]uint16
args.File = &res[0]
args.MaxFile = uint32(len(res))
args.InitialDir, args.DefExt = initDirNameExt(opts.filename, res[:])
@ -82,7 +82,7 @@ func selectFileMutiple(opts options) ([]string, error) {
args.Filter = &initFilters(opts.fileFilters)[0]
}
res := [32768 + 1024*256]uint16{}
var res [32768 + 1024*256]uint16
args.File = &res[0]
args.MaxFile = uint32(len(res))
args.InitialDir, args.DefExt = initDirNameExt(opts.filename, res[:])
@ -158,7 +158,7 @@ func selectFileSave(opts options) (string, error) {
args.Filter = &initFilters(opts.fileFilters)[0]
}
res := [32768]uint16{}
var res [32768]uint16
args.File = &res[0]
args.MaxFile = uint32(len(res))
args.InitialDir, args.DefExt = initDirNameExt(opts.filename, res[:])
@ -251,7 +251,7 @@ func pickFolders(opts options, multi bool) (str string, lst []string, err error)
return "", nil, opts.ctx.Err()
}
if hr == 0x800704c7 { // ERROR_CANCELLED
return "", nil, nil
return "", nil, ErrCanceled
}
if int32(hr) < 0 {
return "", nil, syscall.Errno(hr)
@ -335,11 +335,11 @@ func browseForFolder(opts options) (string, []string, error) {
return "", nil, opts.ctx.Err()
}
if ptr == 0 {
return "", nil, nil
return "", nil, ErrCanceled
}
defer coTaskMemFree.Call(ptr)
res := [32768]uint16{}
var res [32768]uint16
shGetPathFromIDListEx.Call(ptr, uintptr(unsafe.Pointer(&res[0])), uintptr(len(res)), 0)
str := syscall.UTF16ToString(res[:])

12
internal/zenutil/env.go Normal file
View File

@ -0,0 +1,12 @@
package zenutil
// These are internal.
const (
ErrCanceled = stringErr("dialog canceled")
ErrExtraButton = stringErr("extra button pressed")
ErrUnsupported = stringErr("unsupported option")
)
type stringErr string
func (e stringErr) Error() string { return string(e) }

View File

@ -48,4 +48,25 @@ res.join({{json .Separator}})
var app=Application.currentApplication()
app.includeStandardAdditions=true
void app.displayNotification({{json .Text}},{{json .Options}})
{{- end}}
{{define "progress" -}}
var app=Application.currentApplication()
app.includeStandardAdditions=true
app.activate()
ObjC.import('stdlib')
ObjC.import('readline')
{{- if .Total}}
Progress.totalUnitCount={{.Total}}
{{- end}}
{{- if .Description}}
Progress.description={{json .Description}}
{{- end}}
while(true){var s
try{s=$.readline('')}catch(e){if(e.errorNumber===-128)$.exit(1)
break}
if(s.indexOf('#')===0){Progress.additionalDescription=s.slice(1)
continue}
var i=parseInt(s)
if(i>=0&&Progress.totalUnitCount>0){Progress.completedUnitCount=i
continue}}
{{- end}}`))

View File

@ -4,7 +4,6 @@ package main
import (
"bytes"
"io/ioutil"
"log"
"os"
"path/filepath"
@ -17,7 +16,7 @@ import (
func main() {
dir := os.Args[1]
files, err := ioutil.ReadDir(dir)
files, err := os.ReadDir(dir)
if err != nil {
log.Fatal(err)
}
@ -26,12 +25,7 @@ func main() {
for _, file := range files {
name := file.Name()
str.WriteString("\n" + `{{define "`)
str.WriteString(strings.TrimSuffix(name, filepath.Ext(name)))
str.WriteString(`" -}}` + "\n")
data, err := ioutil.ReadFile(filepath.Join(dir, name))
data, err := os.ReadFile(filepath.Join(dir, name))
if err != nil {
log.Fatal(err)
}
@ -40,6 +34,9 @@ func main() {
log.Fatal(err)
}
str.WriteString("\n" + `{{define "`)
str.WriteString(strings.TrimSuffix(name, filepath.Ext(name)))
str.WriteString(`" -}}` + "\n")
str.Write(data)
str.WriteString("\n{{- end}}")
}
@ -108,5 +105,4 @@ import (
var scripts = template.Must(template.New("").Funcs(template.FuncMap{"json": func(v interface{}) (string, error) {
b, err := json.Marshal(v)
return string(b), err
}}).Parse(` + "`{{.}}`" + `))
`))
}}).Parse(` + "`{{.}}`))\n"))

View File

@ -0,0 +1,34 @@
var app = Application.currentApplication()
app.includeStandardAdditions = true
app.activate()
ObjC.import('stdlib')
ObjC.import('readline')
{{- if .Total}}
Progress.totalUnitCount = {{.Total}}
{{- end}}
{{- if .Description}}
Progress.description = {{json .Description}}
{{- end}}
while (true) {
var s
try {
s = $.readline('')
} catch (e) {
if (e.errorNumber === -128) $.exit(1)
break
}
if (s.indexOf('#') === 0) {
Progress.additionalDescription = s.slice(1)
continue
}
var i = parseInt(s)
if (i >= 0 && Progress.totalUnitCount > 0) {
Progress.completedUnitCount = i
continue
}
}

View File

@ -0,0 +1,116 @@
// +build !windows,!js
package zenutil
import (
"bytes"
"context"
"io"
"os"
"os/exec"
"runtime"
"strconv"
"sync/atomic"
"time"
)
type progressDialog struct {
ctx context.Context
cmd *exec.Cmd
max int
percent bool
closed int32
lines chan string
done chan struct{}
err error
}
func (d *progressDialog) send(line string) error {
select {
case d.lines <- line:
return nil
case <-d.done:
return d.err
}
}
func (d *progressDialog) Text(text string) error {
return d.send("#" + text)
}
func (d *progressDialog) Value(value int) error {
if d.percent {
return d.send(strconv.FormatFloat(100*float64(value)/float64(d.max), 'f', -1, 64))
} else {
return d.send(strconv.Itoa(value))
}
}
func (d *progressDialog) MaxValue() int {
return d.max
}
func (d *progressDialog) Done() <-chan struct{} {
return d.done
}
func (d *progressDialog) Complete() error {
err := d.Value(d.max)
close(d.lines)
return err
}
func (d *progressDialog) Close() error {
atomic.StoreInt32(&d.closed, 1)
d.cmd.Process.Signal(os.Interrupt)
<-d.done
return d.err
}
func (d *progressDialog) wait(extra *string, out *bytes.Buffer) {
err := d.cmd.Wait()
if cerr := d.ctx.Err(); cerr != nil {
err = cerr
}
if eerr, ok := err.(*exec.ExitError); ok {
switch {
case eerr.ExitCode() == -1 && atomic.LoadInt32(&d.closed) != 0:
err = nil
case eerr.ExitCode() == 1:
if extra != nil && *extra+"\n" == string(out.Bytes()) {
err = ErrExtraButton
} else {
err = ErrCanceled
}
}
}
d.err = err
close(d.done)
}
func (d *progressDialog) pipe(w io.WriteCloser) {
defer w.Close()
var timeout = time.Second
if runtime.GOOS == "darwin" {
timeout = 40 * time.Millisecond
}
for {
var line string
select {
case s, ok := <-d.lines:
if !ok {
return
}
line = s
case <-d.ctx.Done():
return
case <-d.done:
return
case <-time.After(timeout):
// line = ""
}
if _, err := w.Write([]byte(line + "\n")); err != nil {
return
}
}
}

View File

@ -1,30 +1,29 @@
package zenutil
import (
"bytes"
"context"
"io/ioutil"
"os"
"os/exec"
"strings"
"path/filepath"
"syscall"
)
// Run is internal.
func Run(ctx context.Context, script string, data interface{}) ([]byte, error) {
var buf strings.Builder
var buf bytes.Buffer
err := scripts.ExecuteTemplate(&buf, script, data)
if err != nil {
return nil, err
}
script = buf.String()
if Command {
// Try to use syscall.Exec, fallback to exec.Command.
if path, err := exec.LookPath("osascript"); 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.Write(buf.Bytes()); err != nil {
} else if _, err := t.Seek(0, 0); err != nil {
} else if err := syscall.Dup2(int(t.Fd()), syscall.Stdin); err != nil {
} else if err := os.Stderr.Close(); err != nil {
@ -35,7 +34,7 @@ func Run(ctx context.Context, script string, data interface{}) ([]byte, error) {
if ctx != nil {
cmd := exec.CommandContext(ctx, "osascript", "-l", "JavaScript")
cmd.Stdin = strings.NewReader(script)
cmd.Stdin = &buf
out, err := cmd.Output()
if ctx.Err() != nil {
err = ctx.Err()
@ -43,25 +42,86 @@ func Run(ctx context.Context, script string, data interface{}) ([]byte, error) {
return out, err
}
cmd := exec.Command("osascript", "-l", "JavaScript")
cmd.Stdin = strings.NewReader(script)
cmd.Stdin = &buf
return cmd.Output()
}
// File is internal.
type File struct {
Operation string
Separator string
Options FileOptions
}
// RunProgress is internal.
func RunProgress(ctx context.Context, max int, data Progress) (dlg *progressDialog, err error) {
var buf bytes.Buffer
err = scripts.ExecuteTemplate(&buf, "progress", data)
if err != nil {
return nil, err
}
// FileOptions is internal.
type FileOptions struct {
Prompt *string `json:"withPrompt,omitempty"`
Type []string `json:"ofType,omitempty"`
Name string `json:"defaultName,omitempty"`
Location string `json:"defaultLocation,omitempty"`
Multiple bool `json:"multipleSelectionsAllowed,omitempty"`
Invisibles bool `json:"invisibles,omitempty"`
t, err := ioutil.TempDir("", "")
if err != nil {
return nil, err
}
defer func() {
if err != nil {
if ctx != nil && ctx.Err() != nil {
err = ctx.Err()
}
os.RemoveAll(t)
}
}()
if ctx == nil {
ctx = context.Background()
}
var cmd *exec.Cmd
name := filepath.Join(t, "progress.app")
cmd = exec.CommandContext(ctx, "osacompile", "-l", "JavaScript", "-o", name)
cmd.Stdin = &buf
if err := cmd.Run(); err != nil {
return nil, err
}
plist := filepath.Join(name, "Contents/Info.plist")
cmd = exec.CommandContext(ctx, "defaults", "write", plist, "LSUIElement", "true")
if err := cmd.Run(); err != nil {
return nil, err
}
cmd = exec.CommandContext(ctx, "defaults", "write", plist, "CFBundleName", "")
if err := cmd.Run(); err != nil {
return nil, err
}
var executable string
cmd = exec.CommandContext(ctx, "defaults", "read", plist, "CFBundleExecutable")
if out, err := cmd.Output(); err != nil {
return nil, err
} else {
out = bytes.TrimSuffix(out, []byte{'\n'})
executable = filepath.Join(name, "Contents/MacOS", string(out))
}
cmd = exec.CommandContext(ctx, executable)
pipe, err := cmd.StdinPipe()
if err != nil {
return nil, err
}
if err := cmd.Start(); err != nil {
return nil, err
}
dlg = &progressDialog{
ctx: ctx,
cmd: cmd,
max: max,
lines: make(chan string),
done: make(chan struct{}),
}
go dlg.pipe(pipe)
go func() {
defer os.RemoveAll(t)
dlg.wait(nil, nil)
}()
return dlg, nil
}
// Dialog is internal.
@ -86,6 +146,25 @@ type DialogOptions struct {
Timeout int `json:"givingUpAfter,omitempty"`
}
// DialogButtons is internal.
type DialogButtons struct {
Buttons []string
Default int
Cancel int
Extra int
}
// SetButtons is internal.
func (d *Dialog) SetButtons(btns DialogButtons) {
d.Options.Buttons = btns.Buttons
d.Options.Default = btns.Default
d.Options.Cancel = btns.Cancel
if btns.Extra > 0 {
name := btns.Buttons[btns.Extra-1]
d.Extra = &name
}
}
// List is internal.
type List struct {
Items []string
@ -104,6 +183,22 @@ type ListOptions struct {
Empty bool `json:"emptySelectionAllowed,omitempty"`
}
// File is internal.
type File struct {
Operation string
Separator string
Options FileOptions
}
type FileOptions struct {
Prompt *string `json:"withPrompt,omitempty"`
Type []string `json:"ofType,omitempty"`
Name string `json:"defaultName,omitempty"`
Location string `json:"defaultLocation,omitempty"`
Multiple bool `json:"multipleSelectionsAllowed,omitempty"`
Invisibles bool `json:"invisibles,omitempty"`
}
// Notify is internal.
type Notify struct {
Text string
@ -116,19 +211,8 @@ type NotifyOptions struct {
Subtitle string `json:"subtitle,omitempty"`
}
type Buttons struct {
Buttons []string
Default int
Cancel int
Extra int
}
func (d *Dialog) SetButtons(btns Buttons) {
d.Options.Buttons = btns.Buttons
d.Options.Default = btns.Default
d.Options.Cancel = btns.Cancel
if btns.Extra > 0 {
name := btns.Buttons[btns.Extra-1]
d.Extra = &name
}
// Progress is internal.
type Progress struct {
Description *string
Total *int
}

View File

@ -3,6 +3,7 @@
package zenutil
import (
"bytes"
"context"
"os"
"os/exec"
@ -40,3 +41,42 @@ func Run(ctx context.Context, args []string) ([]byte, error) {
}
return exec.Command(tool, args...).Output()
}
// RunProgress is internal.
func RunProgress(ctx context.Context, max int, extra *string, args []string) (*progressDialog, error) {
if Command && path != "" {
if Timeout > 0 {
args = append(args, "--timeout", strconv.Itoa(Timeout))
}
syscall.Exec(path, append([]string{tool}, args...), os.Environ())
}
if ctx == nil {
ctx = context.Background()
}
cmd := exec.CommandContext(ctx, tool, args...)
pipe, err := cmd.StdinPipe()
if err != nil {
return nil, err
}
var out *bytes.Buffer
if extra != nil {
out = &bytes.Buffer{}
cmd.Stdout = out
}
if err := cmd.Start(); err != nil {
return nil, err
}
dlg := &progressDialog{
ctx: ctx,
cmd: cmd,
max: max,
percent: true,
lines: make(chan string),
done: make(chan struct{}),
}
go dlg.pipe(pipe)
go dlg.wait(extra, out)
return dlg, nil
}

12
list.go
View File

@ -2,25 +2,19 @@ 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) {
func List(text string, items []string, options ...Option) (string, 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) {
func ListItems(text string, items ...string) (string, 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) {
@ -28,8 +22,6 @@ func ListMultiple(text string, items []string, options ...Option) ([]string, err
}
// ListMultipleItems 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)
}

View File

@ -4,7 +4,11 @@ import (
"github.com/ncruces/zenity/internal/zenutil"
)
func list(text string, items []string, opts options) (string, bool, error) {
func list(text string, items []string, opts options) (string, error) {
if opts.extraButton != nil {
return "", ErrUnsupported
}
var data zenutil.List
data.Items = items
data.Options.Prompt = &text
@ -19,6 +23,10 @@ func list(text string, items []string, opts options) (string, bool, error) {
}
func listMultiple(text string, items []string, opts options) ([]string, error) {
if opts.extraButton != nil {
return nil, ErrUnsupported
}
var data zenutil.List
data.Items = items
data.Options.Prompt = &text

View File

@ -8,6 +8,7 @@ import (
"time"
"github.com/ncruces/zenity"
"go.uber.org/goleak"
)
func ExampleList() {
@ -44,22 +45,23 @@ func ExampleListMultipleItems() {
// Output:
}
func TestListTimeout(t *testing.T) {
func TestList_timeout(t *testing.T) {
defer goleak.VerifyNone(t)
ctx, cancel := context.WithTimeout(context.Background(), time.Second/10)
defer cancel()
_, _, err := zenity.List("", nil, zenity.Context(ctx))
_, err := zenity.List("", nil, zenity.Context(ctx))
if !os.IsTimeout(err) {
t.Error("did not timeout:", err)
}
cancel()
}
func TestListCancel(t *testing.T) {
func TestList_cancel(t *testing.T) {
defer goleak.VerifyNone(t)
ctx, cancel := context.WithCancel(context.Background())
cancel()
_, _, err := zenity.List("", nil, zenity.Context(ctx))
_, err := zenity.List("", nil, zenity.Context(ctx))
if !errors.Is(err, context.Canceled) {
t.Error("was not canceled:", err)
}

View File

@ -6,7 +6,7 @@ import (
"github.com/ncruces/zenity/internal/zenutil"
)
func list(text string, items []string, opts options) (string, bool, error) {
func list(text string, items []string, opts options) (string, error) {
args := []string{"--list", "--column=", "--hide-header", "--text", text}
args = appendTitle(args, opts)
args = appendButtons(args, opts)

View File

@ -5,28 +5,21 @@ import (
"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)
func list(text string, items []string, opts options) (string, error) {
items, err := listDlg(text, items, false, opts)
if len(items) == 1 {
return items[0], true, err
return items[0], err
}
return "", false, err
return "", err
}
func listMultiple(text string, items []string, opts options) ([]string, error) {
var title string
if opts.title != nil {
title = *opts.title
return listDlg(text, items, true, opts)
}
func listDlg(text string, items []string, multiple bool, opts options) (out []string, err error) {
if opts.title == nil {
opts.title = stringPtr("")
}
if opts.okLabel == nil {
opts.okLabel = stringPtr("OK")
@ -34,10 +27,7 @@ func listMultiple(text string, items []string, opts options) ([]string, error) {
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()
@ -52,6 +42,7 @@ func listDlg(title, text string, items []string, multiple bool, opts options) (o
sendMessage.Call(listCtl, 0x0030 /* WM_SETFONT */, hfont, 1)
sendMessage.Call(okBtn, 0x0030 /* WM_SETFONT */, hfont, 1)
sendMessage.Call(cancelBtn, 0x0030 /* WM_SETFONT */, hfont, 1)
sendMessage.Call(extraBtn, 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
@ -59,7 +50,6 @@ func listDlg(title, text string, items []string, multiple bool, opts options) (o
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
@ -72,6 +62,7 @@ func listDlg(title, text string, items []string, multiple bool, opts options) (o
postQuitMessage.Call(0)
case 0x0010: // WM_CLOSE
err = ErrCanceled
destroyWindow.Call(wnd)
case 0x0111: // WM_COMMAND
@ -98,6 +89,7 @@ func listDlg(title, text string, items []string, multiple bool, opts options) (o
}
}
case 2: // IDCANCEL
err = ErrCanceled
case 7: // IDNO
err = ErrExtraButton
}
@ -107,8 +99,8 @@ func listDlg(title, text string, items []string, multiple bool, opts options) (o
layout(dpi(uint32(wparam) >> 16))
default:
ret, _, _ := syscall.Syscall6(defWindowProc, 4, wnd, uintptr(msg), wparam, lparam, 0, 0)
return ret
res, _, _ := syscall.Syscall6(defWindowProc, 4, wnd, uintptr(msg), wparam, lparam, 0, 0)
return res
}
return 0
@ -130,16 +122,16 @@ func listDlg(title, text string, items []string, multiple bool, opts options) (o
defer unregisterClass.Call(cls, instance)
wnd, _, _ = createWindowEx.Call(0x10101, // WS_EX_CONTROLPARENT|WS_EX_WINDOWEDGE|WS_EX_DLGMODALFRAME
cls, strptr(title),
cls, strptr(*opts.title),
0x84c80000, // WS_POPUPWINDOW|WS_CLIPSIBLINGS|WS_DLGFRAME
0x80000000, // CW_USEDEFAULT
0x80000000, // CW_USEDEFAULT
281, 281, 0, 0, instance)
281, 281, 0, 0, instance, 0)
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)
12, 10, 241, 16, wnd, 0, instance, 0)
var flags uintptr = 0x50320000 // WS_CHILD|WS_VISIBLE|WS_VSCROLL|WS_GROUP|WS_TABSTOP
if multiple {
@ -148,21 +140,21 @@ func listDlg(title, text string, items []string, multiple bool, opts options) (o
listCtl, _, _ = createWindowEx.Call(0x200, // WS_EX_CLIENTEDGE
strptr("LISTBOX"), strptr(opts.entryText),
flags,
12, 30, 241, 164, wnd, 0, instance)
12, 30, 241, 164, wnd, 0, instance, 0)
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)
12, 206, 75, 24, wnd, 1 /* IDOK */, instance, 0)
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)
12, 206, 75, 24, wnd, 2 /* IDCANCEL */, instance, 0)
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)
12, 206, 75, 24, wnd, 7 /* IDNO */, instance, 0)
}
for _, item := range items {

20
msg.go
View File

@ -1,46 +1,34 @@
package zenity
// ErrExtraButton is returned by dialog functions when the extra button is
// pressed.
const ErrExtraButton = stringErr("extra button pressed")
// Question displays the question dialog.
//
// Returns true on OK, false on Cancel, or ErrExtraButton.
//
// Valid options: Title, Width, Height, OKLabel, CancelLabel, ExtraButton,
// Icon, NoWrap, Ellipsize, DefaultCancel.
func Question(text string, options ...Option) (bool, error) {
func Question(text string, options ...Option) error {
return message(questionKind, text, applyOptions(options))
}
// Info displays the info dialog.
//
// Returns true on OK, false on dismiss, or ErrExtraButton.
//
// Valid options: Title, Width, Height, OKLabel, ExtraButton, Icon,
// NoWrap, Ellipsize.
func Info(text string, options ...Option) (bool, error) {
func Info(text string, options ...Option) error {
return message(infoKind, text, applyOptions(options))
}
// Warning displays the warning dialog.
//
// Returns true on OK, false on dismiss, or ErrExtraButton.
//
// Valid options: Title, Width, Height, OKLabel, ExtraButton, Icon,
// NoWrap, Ellipsize.
func Warning(text string, options ...Option) (bool, error) {
func Warning(text string, options ...Option) error {
return message(warningKind, text, applyOptions(options))
}
// Error displays the error dialog.
//
// Returns true on OK, false on dismiss, or ErrExtraButton.
//
// Valid options: Title, Width, Height, OKLabel, ExtraButton, Icon,
// NoWrap, Ellipsize.
func Error(text string, options ...Option) (bool, error) {
func Error(text string, options ...Option) error {
return message(errorKind, text, applyOptions(options))
}

View File

@ -4,7 +4,7 @@ import (
"github.com/ncruces/zenity/internal/zenutil"
)
func message(kind messageKind, text string, opts options) (bool, error) {
func message(kind messageKind, text string, opts options) error {
var data zenutil.Dialog
data.Text = text
data.Options.Timeout = zenutil.Timeout
@ -33,6 +33,6 @@ func message(kind messageKind, text string, opts options) (bool, error) {
data.SetButtons(getButtons(dialog, kind == questionKind, opts))
out, err := zenutil.Run(opts.ctx, "dialog", data)
_, ok, err := strResult(opts, out, err)
return ok, err
_, err = strResult(opts, out, err)
return err
}

View File

@ -8,6 +8,7 @@ import (
"time"
"github.com/ncruces/zenity"
"go.uber.org/goleak"
)
func ExampleError() {
@ -38,32 +39,34 @@ func ExampleQuestion() {
// Output:
}
var msgFuncs = []func(string, ...zenity.Option) (bool, error){
var msgFuncs = []func(string, ...zenity.Option) error{
zenity.Error,
zenity.Info,
zenity.Warning,
zenity.Question,
}
func TestMessageTimeout(t *testing.T) {
func TestMessage_timeout(t *testing.T) {
for _, f := range msgFuncs {
ctx, cancel := context.WithTimeout(context.Background(), time.Second/10)
_, err := f("text", zenity.Context(ctx))
err := f("text", zenity.Context(ctx))
if !os.IsTimeout(err) {
t.Error("did not timeout:", err)
}
cancel()
goleak.VerifyNone(t)
}
}
func TestMessageCancel(t *testing.T) {
func TestMessage_cancel(t *testing.T) {
defer goleak.VerifyNone(t)
ctx, cancel := context.WithCancel(context.Background())
cancel()
for _, f := range msgFuncs {
_, err := f("text", zenity.Context(ctx))
err := f("text", zenity.Context(ctx))
if !errors.Is(err, context.Canceled) {
t.Error("was not canceled:", err)
}

View File

@ -6,7 +6,7 @@ import (
"github.com/ncruces/zenity/internal/zenutil"
)
func message(kind messageKind, text string, opts options) (bool, error) {
func message(kind messageKind, text string, opts options) error {
args := []string{"--text", text, "--no-markup"}
switch kind {
case questionKind:
@ -47,6 +47,6 @@ func message(kind messageKind, text string, opts options) (bool, error) {
}
out, err := zenutil.Run(opts.ctx, args)
_, ok, err := strResult(opts, out, err)
return ok, err
_, err = strResult(opts, out, err)
return err
}

View File

@ -12,7 +12,7 @@ var (
getDlgCtrlID = user32.NewProc("GetDlgCtrlID")
)
func message(kind messageKind, text string, opts options) (bool, error) {
func message(kind messageKind, text string, opts options) error {
var flags uintptr
switch {
@ -48,7 +48,7 @@ func message(kind messageKind, text string, opts options) (bool, error) {
if opts.ctx != nil || opts.okLabel != nil || opts.cancelLabel != nil || opts.extraButton != nil {
unhook, err := hookMessageLabels(kind, opts)
if err != nil {
return false, err
return err
}
defer unhook()
}
@ -62,17 +62,17 @@ func message(kind messageKind, text string, opts options) (bool, error) {
s, _, err := messageBox.Call(0, strptr(text), title, flags)
if opts.ctx != nil && opts.ctx.Err() != nil {
return false, opts.ctx.Err()
return opts.ctx.Err()
}
switch s {
case 1, 6: // IDOK, IDYES
return true, nil
return nil
case 2: // IDCANCEL
return false, nil
return ErrCanceled
case 7: // IDNO
return false, ErrExtraButton
return ErrExtraButton
default:
return false, err
return err
}
}
@ -80,7 +80,7 @@ func hookMessageLabels(kind messageKind, opts options) (unhook context.CancelFun
return hookDialog(opts.ctx, func(wnd uintptr) {
enumChildWindows.Call(wnd,
syscall.NewCallback(func(wnd, lparam uintptr) uintptr {
name := [8]uint16{}
var name [8]uint16
getClassName.Call(wnd, uintptr(unsafe.Pointer(&name)), uintptr(len(name)))
if syscall.UTF16ToString(name[:]) == "Button" {
ctl, _, _ := getDlgCtrlID.Call(wnd)
@ -89,11 +89,7 @@ func hookMessageLabels(kind messageKind, opts options) (unhook context.CancelFun
case 1, 6: // IDOK, IDYES
text = opts.okLabel
case 2: // IDCANCEL
if kind == questionKind {
text = opts.cancelLabel
} else {
text = opts.okLabel
}
text = opts.cancelLabel
case 7: // IDNO
text = opts.extraButton
}

View File

@ -6,6 +6,7 @@ import (
"testing"
"github.com/ncruces/zenity"
"go.uber.org/goleak"
)
func ExampleNotify() {
@ -15,7 +16,8 @@ func ExampleNotify() {
// Output:
}
func TestNotifyCancel(t *testing.T) {
func TestNotify_cancel(t *testing.T) {
defer goleak.VerifyNone(t)
ctx, cancel := context.WithCancel(context.Background())
cancel()

51
progress.go Normal file
View File

@ -0,0 +1,51 @@
package zenity
// Progress displays the progress indication dialog.
//
// Valid options: Title, Width, Height, OKLabel, CancelLabel, ExtraButton,
// Icon, MaxValue, Pulsate, NoCancel, TimeRemaining.
func Progress(options ...Option) (ProgressDialog, error) {
return progress(applyOptions(options))
}
// ProgressDialog allows you to interact with the progress indication dialog.
type ProgressDialog interface {
// Text sets the dialog text.
Text(string) error
// Value sets how much of the task has been completed.
Value(int) error
// MaxValue gets how much work the task requires in total.
MaxValue() int
// Complete marks the task completed.
Complete() error
// Close closes the dialog.
Close() error
// Done returns a channel that's closed when the dialog is closed.
Done() <-chan struct{}
}
// MaxValue returns an Option to set the maximum value (Windows and macOS only).
// The default maximum value is 100.
func MaxValue(value int) Option {
return funcOption(func(o *options) { o.maxValue = value })
}
// Pulsate returns an Option to pulsate the progress bar.
func Pulsate() Option {
return funcOption(func(o *options) { o.maxValue = -1 })
}
// NoCancel returns an Option to hide the Cancel button (Windows and Unix only).
func NoCancel() Option {
return funcOption(func(o *options) { o.noCancel = true })
}
// TimeRemaining returns an Option to estimate when progress will reach 100% (Unix only).
func TimeRemaining() Option {
return funcOption(func(o *options) { o.timeRemaining = true })
}

22
progress_darwin.go Normal file
View File

@ -0,0 +1,22 @@
package zenity
import (
"github.com/ncruces/zenity/internal/zenutil"
)
func progress(opts options) (ProgressDialog, error) {
if opts.extraButton != nil {
return nil, ErrUnsupported
}
var data zenutil.Progress
data.Description = opts.title
if opts.maxValue == 0 {
opts.maxValue = 100
}
if opts.maxValue >= 0 {
data.Total = &opts.maxValue
}
return zenutil.RunProgress(opts.ctx, opts.maxValue, data)
}

100
progress_test.go Normal file
View File

@ -0,0 +1,100 @@
package zenity_test
import (
"context"
"errors"
"log"
"testing"
"time"
"github.com/ncruces/zenity"
"go.uber.org/goleak"
)
func ExampleProgress() {
dlg, err := zenity.Progress(
zenity.Title("Update System Logs"))
if err != nil {
log.Fatal(err)
}
defer dlg.Close()
dlg.Text("Scanning mail logs...")
dlg.Value(0)
time.Sleep(time.Second)
dlg.Value(25)
time.Sleep(time.Second)
dlg.Text("Updating mail logs...")
dlg.Value(50)
time.Sleep(time.Second)
dlg.Text("Resetting cron jobs...")
dlg.Value(75)
time.Sleep(time.Second)
dlg.Text("Rebooting system...")
dlg.Value(100)
time.Sleep(time.Second)
dlg.Complete()
time.Sleep(time.Second)
// Output:
}
func ExampleProgress_pulsate() {
dlg, err := zenity.Progress(
zenity.Title("Update System Logs"),
zenity.Pulsate())
if err != nil {
log.Fatal(err)
}
defer dlg.Close()
dlg.Text("Scanning mail logs...")
time.Sleep(time.Second)
dlg.Text("Updating mail logs...")
time.Sleep(time.Second)
dlg.Text("Resetting cron jobs...")
time.Sleep(time.Second)
dlg.Text("Rebooting system...")
time.Sleep(time.Second)
dlg.Complete()
time.Sleep(time.Second)
// Output:
}
func TestProgress_cancel(t *testing.T) {
defer goleak.VerifyNone(t)
ctx, cancel := context.WithCancel(context.Background())
cancel()
_, err := zenity.Progress(zenity.Context(ctx))
if !errors.Is(err, context.Canceled) {
t.Error("was not canceled:", err)
}
}
func TestProgress_cancelAfter(t *testing.T) {
defer goleak.VerifyNone(t)
ctx, cancel := context.WithCancel(context.Background())
dlg, err := zenity.Progress(zenity.Context(ctx))
if err != nil {
t.Fatal(err)
}
go cancel()
<-dlg.Done()
err = dlg.Close()
if !errors.Is(err, context.Canceled) {
t.Error("was not canceled:", err)
}
}

28
progress_unix.go Normal file
View File

@ -0,0 +1,28 @@
// +build !windows,!darwin,!js
package zenity
import (
"github.com/ncruces/zenity/internal/zenutil"
)
func progress(opts options) (ProgressDialog, error) {
args := []string{"--progress"}
args = appendTitle(args, opts)
args = appendButtons(args, opts)
args = appendWidthHeight(args, opts)
args = appendIcon(args, opts)
if opts.maxValue == 0 {
opts.maxValue = 100
}
if opts.maxValue < 0 {
args = append(args, "--pulsate")
}
if opts.noCancel {
args = append(args, "--no-cancel")
}
if opts.timeRemaining {
args = append(args, "--time-remaining")
}
return zenutil.RunProgress(opts.ctx, opts.maxValue, opts.extraButton, args)
}

261
progress_windows.go Normal file
View File

@ -0,0 +1,261 @@
package zenity
import (
"context"
"sync"
"syscall"
)
func progress(opts options) (ProgressDialog, error) {
if opts.title == nil {
opts.title = stringPtr("")
}
if opts.okLabel == nil {
opts.okLabel = stringPtr("OK")
}
if opts.cancelLabel == nil {
opts.cancelLabel = stringPtr("Cancel")
}
if opts.maxValue == 0 {
opts.maxValue = 100
}
if opts.ctx == nil {
opts.ctx = context.Background()
}
dlg := &progressDialog{
done: make(chan struct{}),
max: opts.maxValue,
}
dlg.init.Add(1)
go func() {
err := progressDlg(opts, dlg)
if cerr := opts.ctx.Err(); cerr != nil {
err = cerr
}
dlg.err = err
close(dlg.done)
}()
dlg.init.Wait()
return dlg, nil
}
func progressDlg(opts options, dlg *progressDialog) (err error) {
defer setup()()
font := getFont()
defer font.Delete()
defWindowProc := defWindowProc.Addr()
layout := func(dpi dpi) {
hfont := font.ForDPI(dpi)
sendMessage.Call(dlg.textCtl, 0x0030 /* WM_SETFONT */, hfont, 1)
sendMessage.Call(dlg.okBtn, 0x0030 /* WM_SETFONT */, hfont, 1)
sendMessage.Call(dlg.cancelBtn, 0x0030 /* WM_SETFONT */, hfont, 1)
sendMessage.Call(dlg.extraBtn, 0x0030 /* WM_SETFONT */, hfont, 1)
setWindowPos.Call(dlg.wnd, 0, 0, 0, dpi.Scale(281), dpi.Scale(141), 0x6) // SWP_NOZORDER|SWP_NOMOVE
setWindowPos.Call(dlg.textCtl, 0, dpi.Scale(12), dpi.Scale(10), dpi.Scale(241), dpi.Scale(16), 0x4) // SWP_NOZORDER
setWindowPos.Call(dlg.progCtl, 0, dpi.Scale(12), dpi.Scale(30), dpi.Scale(241), dpi.Scale(24), 0x4) // SWP_NOZORDER
if dlg.extraBtn == 0 {
if dlg.cancelBtn == 0 {
setWindowPos.Call(dlg.okBtn, 0, dpi.Scale(178), dpi.Scale(66), dpi.Scale(75), dpi.Scale(24), 0x4) // SWP_NOZORDER
} else {
setWindowPos.Call(dlg.okBtn, 0, dpi.Scale(95), dpi.Scale(66), dpi.Scale(75), dpi.Scale(24), 0x4) // SWP_NOZORDER
setWindowPos.Call(dlg.cancelBtn, 0, dpi.Scale(178), dpi.Scale(66), dpi.Scale(75), dpi.Scale(24), 0x4) // SWP_NOZORDER
}
} else {
if dlg.cancelBtn == 0 {
setWindowPos.Call(dlg.okBtn, 0, dpi.Scale(95), dpi.Scale(66), dpi.Scale(75), dpi.Scale(24), 0x4) // SWP_NOZORDER
setWindowPos.Call(dlg.extraBtn, 0, dpi.Scale(178), dpi.Scale(66), dpi.Scale(75), dpi.Scale(24), 0x4) // SWP_NOZORDER
} else {
setWindowPos.Call(dlg.okBtn, 0, dpi.Scale(12), dpi.Scale(66), dpi.Scale(75), dpi.Scale(24), 0x4) // SWP_NOZORDER
setWindowPos.Call(dlg.extraBtn, 0, dpi.Scale(95), dpi.Scale(66), dpi.Scale(75), dpi.Scale(24), 0x4) // SWP_NOZORDER
setWindowPos.Call(dlg.cancelBtn, 0, dpi.Scale(178), dpi.Scale(66), 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
err = ErrCanceled
destroyWindow.Call(wnd)
case 0x0111: // WM_COMMAND
switch wparam {
default:
return 1
case 1, 6: // IDOK, IDYES
//
case 2: // IDCANCEL
err = ErrCanceled
case 7: // IDNO
err = ErrExtraButton
}
destroyWindow.Call(wnd)
case 0x02e0: // WM_DPICHANGED
layout(dpi(uint32(wparam) >> 16))
default:
res, _, _ := syscall.Syscall6(defWindowProc, 4, wnd, uintptr(msg), wparam, lparam, 0, 0)
return res
}
return 0
}
if opts.ctx != nil && opts.ctx.Err() != nil {
return opts.ctx.Err()
}
instance, _, err := getModuleHandle.Call(0)
if instance == 0 {
return err
}
cls, err := registerClass(instance, syscall.NewCallback(proc))
if cls == 0 {
return err
}
defer unregisterClass.Call(cls, instance)
dlg.wnd, _, _ = createWindowEx.Call(0x10101, // WS_EX_CONTROLPARENT|WS_EX_WINDOWEDGE|WS_EX_DLGMODALFRAME
cls, strptr(*opts.title),
0x84c80000, // WS_POPUPWINDOW|WS_CLIPSIBLINGS|WS_DLGFRAME
0x80000000, // CW_USEDEFAULT
0x80000000, // CW_USEDEFAULT
281, 141, 0, 0, instance, 0)
dlg.textCtl, _, _ = createWindowEx.Call(0,
strptr("STATIC"), 0,
0x5002e080, // WS_CHILD|WS_VISIBLE|WS_GROUP|SS_WORDELLIPSIS|SS_EDITCONTROL|SS_NOPREFIX
12, 10, 241, 16, dlg.wnd, 0, instance, 0)
var flags uintptr = 0x50000001 // WS_CHILD|WS_VISIBLE|PBS_SMOOTH
if opts.maxValue < 0 {
flags |= 0x8 // PBS_MARQUEE
}
dlg.progCtl, _, _ = createWindowEx.Call(0,
strptr("msctls_progress32"), // PROGRESS_CLASS
0, flags,
12, 30, 241, 24, dlg.wnd, 0, instance, 0)
dlg.okBtn, _, _ = createWindowEx.Call(0,
strptr("BUTTON"), strptr(*opts.okLabel),
0x58030001, // WS_CHILD|WS_VISIBLE|WS_DISABLED|WS_GROUP|WS_TABSTOP|BS_DEFPUSHBUTTON
12, 66, 75, 24, dlg.wnd, 1 /* IDOK */, instance, 0)
if !opts.noCancel {
dlg.cancelBtn, _, _ = createWindowEx.Call(0,
strptr("BUTTON"), strptr(*opts.cancelLabel),
0x50010000, // WS_CHILD|WS_VISIBLE|WS_GROUP|WS_TABSTOP
12, 66, 75, 24, dlg.wnd, 2 /* IDCANCEL */, instance, 0)
}
if opts.extraButton != nil {
dlg.extraBtn, _, _ = createWindowEx.Call(0,
strptr("BUTTON"), strptr(*opts.extraButton),
0x50010000, // WS_CHILD|WS_VISIBLE|WS_GROUP|WS_TABSTOP
12, 66, 75, 24, dlg.wnd, 7 /* IDNO */, instance, 0)
}
layout(getDPI(dlg.wnd))
centerWindow(dlg.wnd)
showWindow.Call(dlg.wnd, 1 /* SW_SHOWNORMAL */, 0)
if opts.maxValue < 0 {
sendMessage.Call(dlg.progCtl, 0x40a /* PBM_SETMARQUEE */, 1, 0)
} else {
sendMessage.Call(dlg.progCtl, 0x406 /* PBM_SETRANGE32 */, 0, uintptr(opts.maxValue))
}
dlg.init.Done()
if opts.ctx != nil {
wait := make(chan struct{})
defer close(wait)
go func() {
select {
case <-opts.ctx.Done():
sendMessage.Call(dlg.wnd, 0x0112 /* WM_SYSCOMMAND */, 0xf060 /* SC_CLOSE */, 0)
case <-wait:
}
}()
}
// set default values
err = nil
if err := messageLoop(dlg.wnd); err != nil {
return err
}
if opts.ctx != nil && opts.ctx.Err() != nil {
return opts.ctx.Err()
}
return err
}
type progressDialog struct {
max int
done chan struct{}
init sync.WaitGroup
wnd uintptr
textCtl uintptr
progCtl uintptr
okBtn uintptr
cancelBtn uintptr
extraBtn uintptr
err error
}
func (d *progressDialog) Text(text string) error {
select {
default:
setWindowText.Call(d.textCtl, strptr(text))
return nil
case <-d.done:
return d.err
}
}
func (d *progressDialog) Value(value int) error {
select {
default:
sendMessage.Call(d.progCtl, 0x402 /* PBM_SETPOS */, uintptr(value), 0)
if value >= d.max {
enableWindow.Call(d.okBtn, 1)
}
return nil
case <-d.done:
return d.err
}
}
func (d *progressDialog) MaxValue() int {
return d.max
}
func (d *progressDialog) Done() <-chan struct{} {
return d.done
}
func (d *progressDialog) Complete() error {
select {
default:
setWindowLong.Call(d.progCtl, intptr(-16) /* GWL_STYLE */, 0x50000001 /* WS_CHILD|WS_VISIBLE|PBS_SMOOTH */)
sendMessage.Call(d.progCtl, 0x406 /* PBM_SETRANGE32 */, 0, 1)
sendMessage.Call(d.progCtl, 0x402 /* PBM_SETPOS */, 1, 0)
enableWindow.Call(d.okBtn, 1)
enableWindow.Call(d.cancelBtn, 0)
return nil
case <-d.done:
return d.err
}
}
func (d *progressDialog) Close() error {
sendMessage.Call(d.wnd, 0x0112 /* WM_SYSCOMMAND */, 0xf060 /* SC_CLOSE */, 0)
<-d.done
return d.err
}

4
pwd.go
View File

@ -2,10 +2,8 @@ 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) {
func Password(options ...Option) (usr string, pw string, err error) {
return password(applyOptions(options))
}

View File

@ -2,8 +2,11 @@
package zenity
func password(opts options) (string, string, bool, error) {
func password(opts options) (string, string, error) {
if opts.username {
return "", "", ErrUnsupported
}
opts.hideText = true
str, ok, err := entry("Password:", opts)
return "", str, ok, err
str, err := entry("Password:", opts)
return "", str, err
}

View File

@ -8,6 +8,7 @@ import (
"time"
"github.com/ncruces/zenity"
"go.uber.org/goleak"
)
func ExamplePassword() {
@ -15,22 +16,23 @@ func ExamplePassword() {
// Output:
}
func TestPasswordTimeout(t *testing.T) {
func TestPassword_timeout(t *testing.T) {
defer goleak.VerifyNone(t)
ctx, cancel := context.WithTimeout(context.Background(), time.Second/10)
defer cancel()
_, _, _, err := zenity.Password(zenity.Context(ctx))
_, _, err := zenity.Password(zenity.Context(ctx))
if !os.IsTimeout(err) {
t.Error("did not timeout:", err)
}
cancel()
}
func TestPasswordCancel(t *testing.T) {
func TestPassword_cancel(t *testing.T) {
defer goleak.VerifyNone(t)
ctx, cancel := context.WithCancel(context.Background())
cancel()
_, _, _, err := zenity.Password(zenity.Context(ctx))
_, _, err := zenity.Password(zenity.Context(ctx))
if !errors.Is(err, context.Canceled) {
t.Error("was not canceled:", err)
}

View File

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

View File

@ -2,13 +2,13 @@ package zenity
import "github.com/ncruces/zenity/internal/zenutil"
func getButtons(dialog, okcancel bool, opts options) (btns zenutil.Buttons) {
func getButtons(dialog, okcancel bool, opts options) (btns zenutil.DialogButtons) {
if !okcancel {
opts.cancelLabel = nil
opts.defaultCancel = false
}
if opts.okLabel != nil || opts.cancelLabel != nil || opts.extraButton != nil || (dialog != okcancel) {
if opts.okLabel != nil || opts.cancelLabel != nil || opts.extraButton != nil || dialog != okcancel {
if opts.okLabel == nil {
opts.okLabel = stringPtr("OK")
}

View File

@ -3,7 +3,6 @@
package zenity
import (
"bytes"
"os/exec"
"strconv"
"strings"
@ -55,23 +54,22 @@ func appendIcon(args []string, opts options) []string {
return args
}
func strResult(opts options, out []byte, err error) (string, bool, error) {
out = bytes.TrimSuffix(out, []byte{'\n'})
func strResult(opts options, out []byte, err error) (string, error) {
if err, ok := err.(*exec.ExitError); ok && err.ExitCode() == 1 {
if opts.extraButton != nil && *opts.extraButton == string(out) {
return "", false, ErrExtraButton
if opts.extraButton != nil && *opts.extraButton+"\n" == string(out) {
return "", ErrExtraButton
}
return "", false, nil
return "", ErrCanceled
}
if err != nil {
return "", false, err
return "", err
}
return string(out), true, nil
return string(out), nil
}
func lstResult(opts options, out []byte, err error) ([]string, error) {
str, ok, err := strResult(opts, out, err)
if ok {
str, err := strResult(opts, out, err)
if err == nil {
return strings.Split(str, zenutil.Separator), nil
}
return nil, err

View File

@ -13,6 +13,7 @@ import (
)
var (
comctl32 = syscall.NewLazyDLL("comctl32.dll")
comdlg32 = syscall.NewLazyDLL("comdlg32.dll")
gdi32 = syscall.NewLazyDLL("gdi32.dll")
kernel32 = syscall.NewLazyDLL("kernel32.dll")
@ -21,6 +22,7 @@ var (
user32 = syscall.NewLazyDLL("user32.dll")
wtsapi32 = syscall.NewLazyDLL("wtsapi32.dll")
initCommonControlsEx = comctl32.NewProc("InitCommonControlsEx")
commDlgExtendedError = comdlg32.NewProc("CommDlgExtendedError")
deleteObject = gdi32.NewProc("DeleteObject")
@ -30,6 +32,10 @@ var (
getModuleHandle = kernel32.NewProc("GetModuleHandleW")
getCurrentThreadId = kernel32.NewProc("GetCurrentThreadId")
getConsoleWindow = kernel32.NewProc("GetConsoleWindow")
getSystemDirectory = kernel32.NewProc("GetSystemDirectoryW")
createActCtx = kernel32.NewProc("CreateActCtxW")
activateActCtx = kernel32.NewProc("ActivateActCtx")
deactivateActCtx = kernel32.NewProc("DeactivateActCtx")
coInitializeEx = ole32.NewProc("CoInitializeEx")
coUninitialize = ole32.NewProc("CoUninitialize")
@ -60,12 +66,14 @@ var (
systemParametersInfo = user32.NewProc("SystemParametersInfoW")
setWindowPos = user32.NewProc("SetWindowPos")
getWindowRect = user32.NewProc("GetWindowRect")
setWindowLong = user32.NewProc("SetWindowLongPtrW")
getSystemMetrics = user32.NewProc("GetSystemMetrics")
unregisterClass = user32.NewProc("UnregisterClassW")
registerClassEx = user32.NewProc("RegisterClassExW")
destroyWindow = user32.NewProc("DestroyWindow")
createWindowEx = user32.NewProc("CreateWindowExW")
showWindow = user32.NewProc("ShowWindow")
enableWindow = user32.NewProc("EnableWindow")
setFocus = user32.NewProc("SetFocus")
defWindowProc = user32.NewProc("DefWindowProcW")
)
@ -96,24 +104,33 @@ func setup() context.CancelFunc {
setForegroundWindow.Call(hwnd)
}
var old uintptr
runtime.LockOSThread()
var restore uintptr
cookie := enableVisualStyles()
if setThreadDpiAwarenessContext.Find() == nil {
// try:
// DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2
// DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE
// DPI_AWARENESS_CONTEXT_SYSTEM_AWARE
for i := -4; i <= -2; i++ {
restore, _, _ := setThreadDpiAwarenessContext.Call(uintptr(i))
restore, _, _ = setThreadDpiAwarenessContext.Call(uintptr(i))
if restore != 0 {
break
}
}
}
var icc _INITCOMMONCONTROLSEX
icc.Size = uint32(unsafe.Sizeof(icc))
icc.ICC = 0x00004020 // ICC_STANDARD_CLASSES|ICC_PROGRESS_CLASS
return func() {
if old != 0 {
setThreadDpiAwarenessContext.Call(old)
if restore != 0 {
setThreadDpiAwarenessContext.Call(restore)
}
if cookie != 0 {
deactivateActCtx.Call(cookie)
}
runtime.UnlockOSThread()
}
@ -122,7 +139,7 @@ func setup() context.CancelFunc {
func commDlgError() error {
s, _, _ := commDlgExtendedError.Call()
if s == 0 {
return nil
return ErrCanceled
} else {
return fmt.Errorf("Common Dialog error: %x", s)
}
@ -139,7 +156,7 @@ func hookDialog(ctx context.Context, initDialog func(wnd uintptr)) (unhook conte
hook, _, err = setWindowsHookEx.Call(12, // WH_CALLWNDPROCRET
syscall.NewCallback(func(code int32, wparam uintptr, lparam *_CWPRETSTRUCT) uintptr {
if lparam.Message == 0x0110 { // WM_INITDIALOG
name := [8]uint16{}
var name [8]uint16
getClassName.Call(lparam.Wnd, uintptr(unsafe.Pointer(&name)), uintptr(len(name)))
if syscall.UTF16ToString(name[:]) == "#32770" { // The class for a dialog box
var close bool
@ -252,8 +269,8 @@ func (f *font) Delete() {
func centerWindow(wnd uintptr) {
getMetric := func(i uintptr) int32 {
ret, _, _ := getSystemMetrics.Call(i)
return int32(ret)
n, _, _ := getSystemMetrics.Call(i)
return int32(n)
}
var rect _RECT
@ -280,8 +297,8 @@ func registerClass(instance, proc uintptr) (uintptr, error) {
wcx.Background = 5 // COLOR_WINDOW
wcx.ClassName = syscall.StringToUTF16Ptr(name)
ret, _, err := registerClassEx.Call(uintptr(unsafe.Pointer(&wcx)))
return ret, err
atom, _, err := registerClassEx.Call(uintptr(unsafe.Pointer(&wcx)))
return atom, err
}
// https://docs.microsoft.com/en-us/windows/win32/winmsg/using-messages-and-message-queues
@ -293,22 +310,62 @@ func messageLoop(wnd uintptr) error {
for {
var msg _MSG
ret, _, err := syscall.Syscall6(getMessage, 4, uintptr(unsafe.Pointer(&msg)), 0, 0, 0, 0, 0)
if int32(ret) == -1 {
s, _, err := syscall.Syscall6(getMessage, 4, uintptr(unsafe.Pointer(&msg)), 0, 0, 0, 0, 0)
if int32(s) == -1 {
return err
}
if ret == 0 {
if s == 0 {
return nil
}
ret, _, _ = syscall.Syscall(isDialogMessage, 2, wnd, uintptr(unsafe.Pointer(&msg)), 0)
if ret == 0 {
s, _, _ = syscall.Syscall(isDialogMessage, 2, wnd, uintptr(unsafe.Pointer(&msg)), 0)
if s == 0 {
syscall.Syscall(translateMessage, 1, uintptr(unsafe.Pointer(&msg)), 0, 0)
syscall.Syscall(dispatchMessage, 1, uintptr(unsafe.Pointer(&msg)), 0, 0)
}
}
}
// https://stackoverflow.com/questions/4308503/how-to-enable-visual-styles-without-a-manifest
func enableVisualStyles() (cookie uintptr) {
var dir [260]uint16
n, _, _ := getSystemDirectory.Call(uintptr(unsafe.Pointer(&dir[0])), uintptr(len(dir)))
if n == 0 || int(n) >= len(dir) {
return
}
var ctx _ACTCTX
ctx.Size = uint32(unsafe.Sizeof(ctx))
ctx.Flags = 0x01c // ACTCTX_FLAG_RESOURCE_NAME_VALID|ACTCTX_FLAG_SET_PROCESS_DEFAULT|ACTCTX_FLAG_ASSEMBLY_DIRECTORY_VALID
ctx.Source = syscall.StringToUTF16Ptr("shell32.dll")
ctx.AssemblyDirectory = &dir[0]
ctx.ResourceName = 124
if h, _, _ := createActCtx.Call(uintptr(unsafe.Pointer(&ctx))); h != 0 {
activateActCtx.Call(h, uintptr(unsafe.Pointer(&cookie)))
}
return
}
// https://docs.microsoft.com/en-us/windows/win32/api/winbase/ns-winbase-actctxw
type _ACTCTX struct {
Size uint32
Flags uint32
Source *uint16
ProcessorArchitecture uint16
LangId uint16
AssemblyDirectory *uint16
ResourceName uintptr
ApplicationName *uint16
Module uintptr
}
// https://docs.microsoft.com/en-us/windows/win32/api/commctrl/ns-commctrl-initcommoncontrolsex
type _INITCOMMONCONTROLSEX struct {
Size uint32
ICC uint32
}
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-cwpretstruct
type _CWPRETSTRUCT struct {
Result uintptr

View File

@ -13,14 +13,22 @@ package zenity
import (
"context"
"image/color"
"github.com/ncruces/zenity/internal/zenutil"
)
type stringErr string
func (e stringErr) Error() string { return string(e) }
func stringPtr(s string) *string { return &s }
// ErrCanceled is returned when the cancel button is pressed,
// or window functions are used to close the dialog.
const ErrCanceled = zenutil.ErrCanceled
// ErrExtraButton is returned when the extra button is pressed.
const ErrExtraButton = zenutil.ErrExtraButton
// ErrUnsupported is returned when a combination of options is not supported.
const ErrUnsupported = zenutil.ErrUnsupported
type options struct {
// General options
title *string
@ -57,6 +65,11 @@ type options struct {
color color.Color
showPalette bool
// Progress indication options
maxValue int
noCancel bool
timeRemaining bool
// Context for timeout
ctx context.Context
}