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) * [text entry](https://github.com/ncruces/zenity/wiki/Text-Entry-dialog)
* [list](https://github.com/ncruces/zenity/wiki/List-dialog) (simple) * [list](https://github.com/ncruces/zenity/wiki/List-dialog) (simple)
* [password](https://github.com/ncruces/zenity/wiki/Password-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)
* [progress](https://github.com/ncruces/zenity/wiki/Progress-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.
Some of that is intended (reflecting platform differences), Some of that is intended (reflecting platform differences),
other bits are unfortunate limitations, other bits are unfortunate limitations.
others still are open to be fixed.
## Why? ## Why?
@ -37,7 +37,8 @@ Why reinvent this particular wheel?
* Explorer shell not required * Explorer shell not required
* works in Server Core * works in Server Core
* Unicode support * 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) * WSL/Cygwin/MSYS2 [support](https://github.com/ncruces/zenity/wiki/Zenity-for-WSL,-Cygwin,-MSYS2)
* on macOS: * on macOS:
* only dependency is `osascript` * only dependency is `osascript`

View File

@ -34,6 +34,7 @@ var (
passwordDlg bool passwordDlg bool
fileSelectionDlg bool fileSelectionDlg bool
colorSelectionDlg bool colorSelectionDlg bool
progressDlg bool
notification bool notification bool
// General options // General options
@ -73,6 +74,13 @@ var (
defaultColor string defaultColor string
showPalette bool showPalette bool
// Progress options
percentage float64
pulsate bool
autoClose bool
autoKill bool
noCancel bool
// Windows specific options // Windows specific options
cygpath bool cygpath bool
wslpath bool wslpath bool
@ -95,32 +103,32 @@ func main() {
if zenutil.Timeout > 0 { if zenutil.Timeout > 0 {
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(zenutil.Timeout)*time.Second) ctx, cancel := context.WithTimeout(context.Background(), time.Duration(zenutil.Timeout)*time.Second)
opts = append(opts, zenity.Context(ctx)) opts = append(opts, zenity.Context(ctx))
_ = cancel defer cancel()
} }
switch { switch {
case errorDlg: case errorDlg:
okResult(zenity.Error(text, opts...)) errResult(zenity.Error(text, opts...))
case infoDlg: case infoDlg:
okResult(zenity.Info(text, opts...)) errResult(zenity.Info(text, opts...))
case warningDlg: case warningDlg:
okResult(zenity.Warning(text, opts...)) errResult(zenity.Warning(text, opts...))
case questionDlg: case questionDlg:
okResult(zenity.Question(text, opts...)) errResult(zenity.Question(text, opts...))
case entryDlg: case entryDlg:
strOKResult(zenity.Entry(text, opts...)) strResult(zenity.Entry(text, opts...))
case listDlg: case listDlg:
if multiple { if multiple {
listResult(zenity.ListMultiple(text, flag.Args(), opts...)) lstResult(zenity.ListMultiple(text, flag.Args(), opts...))
} else { } else {
strOKResult(zenity.List(text, flag.Args(), opts...)) strResult(zenity.List(text, flag.Args(), opts...))
} }
case passwordDlg: case passwordDlg:
_, pw, ok, err := zenity.Password(opts...) _, pw, err := zenity.Password(opts...)
strOKResult(pw, ok, err) strResult(pw, err)
case fileSelectionDlg: case fileSelectionDlg:
switch { switch {
@ -129,17 +137,21 @@ func main() {
case save: case save:
strResult(egestPath(zenity.SelectFileSave(opts...))) strResult(egestPath(zenity.SelectFileSave(opts...)))
case multiple: case multiple:
listResult(egestPaths(zenity.SelectFileMutiple(opts...))) lstResult(egestPaths(zenity.SelectFileMutiple(opts...)))
} }
case colorSelectionDlg: case colorSelectionDlg:
colorResult(zenity.SelectColor(opts...)) colResult(zenity.SelectColor(opts...))
case progressDlg:
errResult(progress(opts...))
case notification: case notification:
errResult(zenity.Notify(text, opts...)) errResult(zenity.Notify(text, opts...))
}
flag.Usage() default:
flag.Usage()
}
} }
func setupFlags() { func setupFlags() {
@ -153,6 +165,7 @@ func setupFlags() {
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(&progressDlg, "progress", false, "Display progress indication dialog")
flag.BoolVar(&notification, "notification", false, "Display notification") flag.BoolVar(&notification, "notification", false, "Display notification")
// General options // General options
@ -165,12 +178,12 @@ func setupFlags() {
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") 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 // 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)")
flag.BoolVar(&noWrap, "no-wrap", false, "Do not enable text wrapping") flag.BoolVar(&noWrap, "no-wrap", false, "Do not enable text wrapping")
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")
// Entry options // Entry options
flag.StringVar(&entryText, "entry-text", "", "Set the entry `text`") flag.StringVar(&entryText, "entry-text", "", "Set the entry `text`")
@ -194,6 +207,15 @@ func setupFlags() {
flag.StringVar(&defaultColor, "color", "", "Set the `color`") flag.StringVar(&defaultColor, "color", "", "Set the `color`")
flag.BoolVar(&showPalette, "show-palette", false, "Show the palette") 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 // Windows specific options
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
flag.BoolVar(&cygpath, "cygpath", false, "Use cygpath for path translation (Windows only)") flag.BoolVar(&cygpath, "cygpath", false, "Use cygpath for path translation (Windows only)")
@ -242,6 +264,9 @@ func validateFlags() {
if colorSelectionDlg { if colorSelectionDlg {
n++ n++
} }
if progressDlg {
n++
}
if notification { if notification {
n++ n++
} }
@ -297,6 +322,11 @@ func loadFlags() []zenity.Option {
setDefault(&icon, "dialog-password") setDefault(&icon, "dialog-password")
setDefault(&okLabel, "OK") setDefault(&okLabel, "OK")
setDefault(&cancelLabel, "Cancel") setDefault(&cancelLabel, "Cancel")
case progressDlg:
setDefault(&title, "Progress")
setDefault(&text, "Running...")
setDefault(&okLabel, "OK")
setDefault(&cancelLabel, "Cancel")
default: default:
setDefault(&text, "") setDefault(&text, "")
} }
@ -388,6 +418,15 @@ func loadFlags() []zenity.Option {
opts = append(opts, zenity.ShowPalette()) opts = append(opts, zenity.ShowPalette())
} }
// Progress options
if pulsate {
opts = append(opts, zenity.Pulsate())
}
if noCancel {
opts = append(opts, zenity.NoCancel())
}
return opts return opts
} }
@ -395,6 +434,9 @@ func errResult(err error) {
if os.IsTimeout(err) { if os.IsTimeout(err) {
os.Exit(5) os.Exit(5)
} }
if err == zenity.ErrCanceled {
os.Exit(1)
}
if err == zenity.ErrExtraButton { if err == zenity.ErrExtraButton {
os.Stdout.WriteString(extraButton) os.Stdout.WriteString(extraButton)
os.Stdout.WriteString(zenutil.LineBreak) os.Stdout.WriteString(zenutil.LineBreak)
@ -408,64 +450,33 @@ func errResult(err error) {
os.Exit(0) 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) { func strResult(s string, err error) {
if err != nil { if err != nil {
errResult(err) errResult(err)
} }
if s == "" {
os.Exit(1)
}
os.Stdout.WriteString(s) os.Stdout.WriteString(s)
os.Stdout.WriteString(zenutil.LineBreak) os.Stdout.WriteString(zenutil.LineBreak)
os.Exit(0) os.Exit(0)
} }
func listResult(l []string, err error) { func lstResult(l []string, err error) {
if err != nil { if err != nil {
errResult(err) errResult(err)
} }
if l == nil {
os.Exit(1)
}
os.Stdout.WriteString(strings.Join(l, zenutil.Separator)) os.Stdout.WriteString(strings.Join(l, zenutil.Separator))
os.Stdout.WriteString(zenutil.LineBreak) os.Stdout.WriteString(zenutil.LineBreak)
os.Exit(0) os.Exit(0)
} }
func colorResult(c color.Color, err error) { func colResult(c color.Color, err error) {
if err != nil { if err != nil {
errResult(err) errResult(err)
} }
if c == nil {
os.Exit(1)
}
os.Stdout.WriteString(zenutil.UnparseColor(c)) os.Stdout.WriteString(zenutil.UnparseColor(c))
os.Stdout.WriteString(zenutil.LineBreak) os.Stdout.WriteString(zenutil.LineBreak)
os.Exit(0) 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 { func ingestPath(path string) string {
if runtime.GOOS == "windows" && path != "" { if runtime.GOOS == "windows" && path != "" {
var args []string 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. // SelectColor displays the color selection dialog.
// //
// Returns nil on cancel.
//
// Valid options: Title, Color, ShowPalette. // Valid options: Title, Color, ShowPalette.
func SelectColor(options ...Option) (color.Color, error) { func SelectColor(options ...Option) (color.Color, error) {
return selectColor(applyOptions(options)) return selectColor(applyOptions(options))

View File

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

View File

@ -9,6 +9,7 @@ import (
"time" "time"
"github.com/ncruces/zenity" "github.com/ncruces/zenity"
"go.uber.org/goleak"
) )
func ExampleSelectColor() { func ExampleSelectColor() {
@ -24,18 +25,19 @@ func ExampleSelectColor_palette() {
// Output: // 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) ctx, cancel := context.WithTimeout(context.Background(), time.Second/10)
defer cancel()
_, err := zenity.SelectColor(zenity.Context(ctx)) _, err := zenity.SelectColor(zenity.Context(ctx))
if !os.IsTimeout(err) { if !os.IsTimeout(err) {
t.Error("did not timeout:", 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()) ctx, cancel := context.WithCancel(context.Background())
cancel() cancel()

View File

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

View File

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

View File

@ -2,11 +2,9 @@ package zenity
// Entry displays the text entry dialog. // Entry displays the text entry dialog.
// //
// Returns false on cancel, or ErrExtraButton.
//
// Valid options: Title, Width, Height, OKLabel, CancelLabel, ExtraButton, // Valid options: Title, Width, Height, OKLabel, CancelLabel, ExtraButton,
// Icon, EntryText, HideText. // 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)) return entry(text, applyOptions(options))
} }

View File

@ -4,7 +4,7 @@ import (
"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, error) {
var data zenutil.Dialog var data zenutil.Dialog
data.Text = text data.Text = text
data.Operation = "displayDialog" data.Operation = "displayDialog"

View File

@ -8,6 +8,7 @@ import (
"time" "time"
"github.com/ncruces/zenity" "github.com/ncruces/zenity"
"go.uber.org/goleak"
) )
func ExampleEntry() { func ExampleEntry() {
@ -16,22 +17,23 @@ func ExampleEntry() {
// Output: // 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) 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) { if !os.IsTimeout(err) {
t.Error("did not timeout:", 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()) ctx, cancel := context.WithCancel(context.Background())
cancel() cancel()
_, _, err := zenity.Entry("", zenity.Context(ctx)) _, err := zenity.Entry("", zenity.Context(ctx))
if !errors.Is(err, context.Canceled) { if !errors.Is(err, context.Canceled) {
t.Error("was not canceled:", err) t.Error("was not canceled:", err)
} }

View File

@ -6,7 +6,7 @@ import (
"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, error) {
args := []string{"--entry", "--text", text} args := []string{"--entry", "--text", text}
args = appendTitle(args, opts) args = appendTitle(args, opts)
args = appendButtons(args, opts) args = appendButtons(args, opts)

View File

@ -4,10 +4,9 @@ import (
"syscall" "syscall"
) )
func entry(text string, opts options) (out string, ok bool, err error) { func entry(text string, opts options) (out string, err error) {
var title string if opts.title == nil {
if opts.title != nil { opts.title = stringPtr("")
title = *opts.title
} }
if opts.okLabel == nil { if opts.okLabel == nil {
opts.okLabel = stringPtr("OK") opts.okLabel = stringPtr("OK")
@ -15,10 +14,7 @@ func entry(text string, opts options) (out string, ok bool, err error) {
if opts.cancelLabel == nil { if opts.cancelLabel == nil {
opts.cancelLabel = stringPtr("Cancel") opts.cancelLabel = stringPtr("Cancel")
} }
return entryDlg(title, text, opts)
}
func entryDlg(title, text string, opts options) (out string, ok bool, err error) {
defer setup()() defer setup()()
font := getFont() font := getFont()
defer font.Delete() 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(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)
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(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
@ -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(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 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)
setWindowPos.Call(okBtn, 0, dpi.Scale(12), dpi.Scale(66), 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(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 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) postQuitMessage.Call(0)
case 0x0010: // WM_CLOSE case 0x0010: // WM_CLOSE
err = ErrCanceled
destroyWindow.Call(wnd) destroyWindow.Call(wnd)
case 0x0111: // WM_COMMAND case 0x0111: // WM_COMMAND
@ -61,8 +58,8 @@ func entryDlg(title, text string, opts options) (out string, ok bool, err error)
return 1 return 1
case 1, 6: // IDOK, IDYES case 1, 6: // IDOK, IDYES
out = getWindowString(editCtl) out = getWindowString(editCtl)
ok = true
case 2: // IDCANCEL case 2: // IDCANCEL
err = ErrCanceled
case 7: // IDNO case 7: // IDNO
err = ErrExtraButton err = ErrExtraButton
} }
@ -72,39 +69,39 @@ func entryDlg(title, text string, opts options) (out string, ok bool, err error)
layout(dpi(uint32(wparam) >> 16)) layout(dpi(uint32(wparam) >> 16))
default: default:
ret, _, _ := syscall.Syscall6(defWindowProc, 4, wnd, uintptr(msg), wparam, lparam, 0, 0) res, _, _ := syscall.Syscall6(defWindowProc, 4, wnd, uintptr(msg), wparam, lparam, 0, 0)
return ret return res
} }
return 0 return 0
} }
if opts.ctx != nil && opts.ctx.Err() != nil { if opts.ctx != nil && opts.ctx.Err() != nil {
return "", false, opts.ctx.Err() return "", opts.ctx.Err()
} }
instance, _, err := getModuleHandle.Call(0) instance, _, err := getModuleHandle.Call(0)
if instance == 0 { if instance == 0 {
return "", false, err return "", err
} }
cls, err := registerClass(instance, syscall.NewCallback(proc)) cls, err := registerClass(instance, syscall.NewCallback(proc))
if cls == 0 { if cls == 0 {
return "", false, err return "", err
} }
defer unregisterClass.Call(cls, instance) defer unregisterClass.Call(cls, instance)
wnd, _, _ = createWindowEx.Call(0x10101, // WS_EX_CONTROLPARENT|WS_EX_WINDOWEDGE|WS_EX_DLGMODALFRAME 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 0x84c80000, // WS_POPUPWINDOW|WS_CLIPSIBLINGS|WS_DLGFRAME
0x80000000, // CW_USEDEFAULT 0x80000000, // CW_USEDEFAULT
0x80000000, // CW_USEDEFAULT 0x80000000, // CW_USEDEFAULT
281, 141, 0, 0, instance) 281, 141, 0, 0, instance, 0)
textCtl, _, _ = createWindowEx.Call(0, textCtl, _, _ = createWindowEx.Call(0,
strptr("STATIC"), strptr(text), strptr("STATIC"), strptr(text),
0x5002e080, // WS_CHILD|WS_VISIBLE|WS_GROUP|SS_WORDELLIPSIS|SS_EDITCONTROL|SS_NOPREFIX 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 var flags uintptr = 0x50030080 // WS_CHILD|WS_VISIBLE|WS_GROUP|WS_TABSTOP|ES_AUTOHSCROLL
if opts.hideText { 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 editCtl, _, _ = createWindowEx.Call(0x200, // WS_EX_CLIENTEDGE
strptr("EDIT"), strptr(opts.entryText), strptr("EDIT"), strptr(opts.entryText),
flags, flags,
12, 30, 241, 24, wnd, 0, instance) 12, 30, 241, 24, wnd, 0, instance, 0)
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, 66, 75, 24, wnd, 1 /* IDOK */, instance) 12, 66, 75, 24, wnd, 1 /* IDOK */, instance, 0)
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, 66, 75, 24, wnd, 2 /* IDCANCEL */, instance) 12, 66, 75, 24, wnd, 2 /* IDCANCEL */, instance, 0)
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, 66, 75, 24, wnd, 7 /* IDNO */, instance) 12, 66, 75, 24, wnd, 7 /* IDNO */, instance, 0)
} }
layout(getDPI(wnd)) layout(getDPI(wnd))
@ -149,13 +146,13 @@ func entryDlg(title, text string, opts options) (out string, ok bool, err error)
} }
// set default values // set default values
out, ok, err = "", false, nil out, err = "", nil
if err := messageLoop(wnd); err != nil { if err := messageLoop(wnd); err != nil {
return "", false, err return "", err
} }
if opts.ctx != nil && opts.ctx.Err() != nil { 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. // SelectFile displays the file selection dialog.
// //
// Returns an empty string on cancel.
//
// Valid options: Title, Directory, Filename, ShowHidden, FileFilter(s). // Valid options: Title, Directory, Filename, ShowHidden, FileFilter(s).
func SelectFile(options ...Option) (string, error) { func SelectFile(options ...Option) (string, error) {
return selectFile(applyOptions(options)) return selectFile(applyOptions(options))
@ -17,8 +15,6 @@ func SelectFile(options ...Option) (string, error) {
// SelectFileMutiple displays the multiple file selection dialog. // SelectFileMutiple displays the multiple file selection dialog.
// //
// Returns a nil slice on cancel.
//
// Valid options: Title, Directory, Filename, ShowHidden, FileFilter(s). // Valid options: Title, Directory, Filename, ShowHidden, FileFilter(s).
func SelectFileMutiple(options ...Option) ([]string, error) { func SelectFileMutiple(options ...Option) ([]string, error) {
return selectFileMutiple(applyOptions(options)) return selectFileMutiple(applyOptions(options))
@ -26,8 +22,6 @@ func SelectFileMutiple(options ...Option) ([]string, error) {
// SelectFileSave displays the save file selection dialog. // SelectFileSave displays the save file selection dialog.
// //
// Returns an empty string on cancel.
//
// Valid options: Title, Filename, ConfirmOverwrite, ConfirmCreate, ShowHidden, // Valid options: Title, Filename, ConfirmOverwrite, ConfirmCreate, ShowHidden,
// FileFilter(s). // FileFilter(s).
func SelectFileSave(options ...Option) (string, error) { func SelectFileSave(options ...Option) (string, error) {
@ -69,6 +63,9 @@ func Filename(filename string) Option {
// //
// macOS hides filename filters from the user, // macOS hides filename filters from the user,
// and only supports filtering by extension (or "type"). // 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 { type FileFilter struct {
Name string // display string that describes the filter (optional) Name string // display string that describes the filter (optional)
Patterns []string // filter patterns for the display string 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) out, err := zenutil.Run(opts.ctx, "file", data)
str, _, err := strResult(opts, out, err) return strResult(opts, out, err)
return str, err
} }
func selectFileMutiple(opts options) ([]string, error) { func selectFileMutiple(opts options) ([]string, error) {
@ -54,6 +53,5 @@ func selectFileSave(opts options) (string, error) {
} }
out, err := zenutil.Run(opts.ctx, "file", data) out, err := zenutil.Run(opts.ctx, "file", data)
str, _, err := strResult(opts, out, err) return strResult(opts, out, err)
return str, err
} }

View File

@ -8,6 +8,7 @@ import (
"time" "time"
"github.com/ncruces/zenity" "github.com/ncruces/zenity"
"go.uber.org/goleak"
) )
const defaultPath = `` 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 { for _, f := range fileFuncs {
ctx, cancel := context.WithTimeout(context.Background(), time.Second/10) ctx, cancel := context.WithTimeout(context.Background(), time.Second/10)
@ -87,10 +88,12 @@ func TestFileTimeout(t *testing.T) {
} }
cancel() 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()) ctx, cancel := context.WithCancel(context.Background())
cancel() cancel()

View File

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

View File

@ -37,7 +37,7 @@ func selectFile(opts options) (string, error) {
args.Filter = &initFilters(opts.fileFilters)[0] args.Filter = &initFilters(opts.fileFilters)[0]
} }
res := [32768]uint16{} var res [32768]uint16
args.File = &res[0] args.File = &res[0]
args.MaxFile = uint32(len(res)) args.MaxFile = uint32(len(res))
args.InitialDir, args.DefExt = initDirNameExt(opts.filename, 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] args.Filter = &initFilters(opts.fileFilters)[0]
} }
res := [32768 + 1024*256]uint16{} var res [32768 + 1024*256]uint16
args.File = &res[0] args.File = &res[0]
args.MaxFile = uint32(len(res)) args.MaxFile = uint32(len(res))
args.InitialDir, args.DefExt = initDirNameExt(opts.filename, 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] args.Filter = &initFilters(opts.fileFilters)[0]
} }
res := [32768]uint16{} var res [32768]uint16
args.File = &res[0] args.File = &res[0]
args.MaxFile = uint32(len(res)) args.MaxFile = uint32(len(res))
args.InitialDir, args.DefExt = initDirNameExt(opts.filename, 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() return "", nil, opts.ctx.Err()
} }
if hr == 0x800704c7 { // ERROR_CANCELLED if hr == 0x800704c7 { // ERROR_CANCELLED
return "", nil, nil return "", nil, ErrCanceled
} }
if int32(hr) < 0 { if int32(hr) < 0 {
return "", nil, syscall.Errno(hr) return "", nil, syscall.Errno(hr)
@ -335,11 +335,11 @@ func browseForFolder(opts options) (string, []string, error) {
return "", nil, opts.ctx.Err() return "", nil, opts.ctx.Err()
} }
if ptr == 0 { if ptr == 0 {
return "", nil, nil return "", nil, ErrCanceled
} }
defer coTaskMemFree.Call(ptr) defer coTaskMemFree.Call(ptr)
res := [32768]uint16{} var res [32768]uint16
shGetPathFromIDListEx.Call(ptr, uintptr(unsafe.Pointer(&res[0])), uintptr(len(res)), 0) shGetPathFromIDListEx.Call(ptr, uintptr(unsafe.Pointer(&res[0])), uintptr(len(res)), 0)
str := syscall.UTF16ToString(res[:]) 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() var app=Application.currentApplication()
app.includeStandardAdditions=true app.includeStandardAdditions=true
void app.displayNotification({{json .Text}},{{json .Options}}) 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}}`)) {{- end}}`))

View File

@ -4,7 +4,6 @@ package main
import ( import (
"bytes" "bytes"
"io/ioutil"
"log" "log"
"os" "os"
"path/filepath" "path/filepath"
@ -17,7 +16,7 @@ import (
func main() { func main() {
dir := os.Args[1] dir := os.Args[1]
files, err := ioutil.ReadDir(dir) files, err := os.ReadDir(dir)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -26,12 +25,7 @@ func main() {
for _, file := range files { for _, file := range files {
name := file.Name() name := file.Name()
data, err := os.ReadFile(filepath.Join(dir, name))
str.WriteString("\n" + `{{define "`)
str.WriteString(strings.TrimSuffix(name, filepath.Ext(name)))
str.WriteString(`" -}}` + "\n")
data, err := ioutil.ReadFile(filepath.Join(dir, name))
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -40,6 +34,9 @@ func main() {
log.Fatal(err) log.Fatal(err)
} }
str.WriteString("\n" + `{{define "`)
str.WriteString(strings.TrimSuffix(name, filepath.Ext(name)))
str.WriteString(`" -}}` + "\n")
str.Write(data) str.Write(data)
str.WriteString("\n{{- end}}") str.WriteString("\n{{- end}}")
} }
@ -108,5 +105,4 @@ import (
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)
return string(b), err 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 package zenutil
import ( import (
"bytes"
"context" "context"
"io/ioutil" "io/ioutil"
"os" "os"
"os/exec" "os/exec"
"strings" "path/filepath"
"syscall" "syscall"
) )
// Run is internal. // Run is internal.
func Run(ctx context.Context, script string, data interface{}) ([]byte, error) { 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) err := scripts.ExecuteTemplate(&buf, script, data)
if err != nil { if err != nil {
return nil, err return nil, err
} }
script = buf.String()
if Command { if Command {
// 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 := 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 := t.Seek(0, 0); 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 {
@ -35,7 +34,7 @@ func Run(ctx context.Context, script string, data interface{}) ([]byte, error) {
if ctx != nil { if ctx != nil {
cmd := exec.CommandContext(ctx, "osascript", "-l", "JavaScript") cmd := exec.CommandContext(ctx, "osascript", "-l", "JavaScript")
cmd.Stdin = strings.NewReader(script) cmd.Stdin = &buf
out, err := cmd.Output() out, err := cmd.Output()
if ctx.Err() != nil { if ctx.Err() != nil {
err = ctx.Err() err = ctx.Err()
@ -43,25 +42,86 @@ func Run(ctx context.Context, script string, data interface{}) ([]byte, error) {
return out, err return out, err
} }
cmd := exec.Command("osascript", "-l", "JavaScript") cmd := exec.Command("osascript", "-l", "JavaScript")
cmd.Stdin = strings.NewReader(script) cmd.Stdin = &buf
return cmd.Output() return cmd.Output()
} }
// File is internal. // RunProgress is internal.
type File struct { func RunProgress(ctx context.Context, max int, data Progress) (dlg *progressDialog, err error) {
Operation string var buf bytes.Buffer
Separator string err = scripts.ExecuteTemplate(&buf, "progress", data)
Options FileOptions if err != nil {
} return nil, err
}
// FileOptions is internal. t, err := ioutil.TempDir("", "")
type FileOptions struct { if err != nil {
Prompt *string `json:"withPrompt,omitempty"` return nil, err
Type []string `json:"ofType,omitempty"` }
Name string `json:"defaultName,omitempty"` defer func() {
Location string `json:"defaultLocation,omitempty"` if err != nil {
Multiple bool `json:"multipleSelectionsAllowed,omitempty"` if ctx != nil && ctx.Err() != nil {
Invisibles bool `json:"invisibles,omitempty"` 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. // Dialog is internal.
@ -86,6 +146,25 @@ type DialogOptions struct {
Timeout int `json:"givingUpAfter,omitempty"` 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. // List is internal.
type List struct { type List struct {
Items []string Items []string
@ -104,6 +183,22 @@ type ListOptions struct {
Empty bool `json:"emptySelectionAllowed,omitempty"` 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. // Notify is internal.
type Notify struct { type Notify struct {
Text string Text string
@ -116,19 +211,8 @@ type NotifyOptions struct {
Subtitle string `json:"subtitle,omitempty"` Subtitle string `json:"subtitle,omitempty"`
} }
type Buttons struct { // Progress is internal.
Buttons []string type Progress struct {
Default int Description *string
Cancel int Total *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
}
} }

View File

@ -3,6 +3,7 @@
package zenutil package zenutil
import ( import (
"bytes"
"context" "context"
"os" "os"
"os/exec" "os/exec"
@ -40,3 +41,42 @@ func Run(ctx context.Context, args []string) ([]byte, error) {
} }
return exec.Command(tool, args...).Output() 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. // List displays the list dialog.
// //
// Returns false on cancel, or ErrExtraButton.
//
// Valid options: Title, Width, Height, OKLabel, CancelLabel, ExtraButton, // Valid options: Title, Width, Height, OKLabel, CancelLabel, ExtraButton,
// Icon, DefaultItems, DisallowEmpty. // 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)) return list(text, items, applyOptions(options))
} }
// ListItems displays the list dialog. // ListItems displays the list dialog.
// func ListItems(text string, items ...string) (string, error) {
// Returns false on cancel, or ErrExtraButton.
func ListItems(text string, items ...string) (string, bool, error) {
return List(text, items) return List(text, items)
} }
// ListMultiple displays the list dialog, allowing multiple items to be selected. // 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, // Valid options: Title, Width, Height, OKLabel, CancelLabel, ExtraButton,
// Icon, DefaultItems, DisallowEmpty. // Icon, DefaultItems, DisallowEmpty.
func ListMultiple(text string, items []string, options ...Option) ([]string, error) { 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. // 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) { func ListMultipleItems(text string, items ...string) ([]string, error) {
return ListMultiple(text, items) return ListMultiple(text, items)
} }

View File

@ -4,7 +4,11 @@ import (
"github.com/ncruces/zenity/internal/zenutil" "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 var data zenutil.List
data.Items = items data.Items = items
data.Options.Prompt = &text 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) { func listMultiple(text string, items []string, opts options) ([]string, error) {
if opts.extraButton != nil {
return nil, ErrUnsupported
}
var data zenutil.List var data zenutil.List
data.Items = items data.Items = items
data.Options.Prompt = &text data.Options.Prompt = &text

View File

@ -8,6 +8,7 @@ import (
"time" "time"
"github.com/ncruces/zenity" "github.com/ncruces/zenity"
"go.uber.org/goleak"
) )
func ExampleList() { func ExampleList() {
@ -44,22 +45,23 @@ func ExampleListMultipleItems() {
// Output: // 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) 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) { if !os.IsTimeout(err) {
t.Error("did not timeout:", 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()) ctx, cancel := context.WithCancel(context.Background())
cancel() cancel()
_, _, err := zenity.List("", nil, zenity.Context(ctx)) _, err := zenity.List("", nil, zenity.Context(ctx))
if !errors.Is(err, context.Canceled) { if !errors.Is(err, context.Canceled) {
t.Error("was not canceled:", err) t.Error("was not canceled:", err)
} }

View File

@ -6,7 +6,7 @@ import (
"github.com/ncruces/zenity/internal/zenutil" "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 := []string{"--list", "--column=", "--hide-header", "--text", text}
args = appendTitle(args, opts) args = appendTitle(args, opts)
args = appendButtons(args, opts) args = appendButtons(args, opts)

View File

@ -5,28 +5,21 @@ import (
"unsafe" "unsafe"
) )
func list(text string, items []string, opts options) (string, bool, error) { func list(text string, items []string, opts options) (string, error) {
var title string items, err := listDlg(text, items, false, opts)
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 { 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) { func listMultiple(text string, items []string, opts options) ([]string, error) {
var title string return listDlg(text, items, true, opts)
if opts.title != nil { }
title = *opts.title
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 { if opts.okLabel == nil {
opts.okLabel = stringPtr("OK") opts.okLabel = stringPtr("OK")
@ -34,10 +27,7 @@ func listMultiple(text string, items []string, opts options) ([]string, error) {
if opts.cancelLabel == nil { if opts.cancelLabel == nil {
opts.cancelLabel = stringPtr("Cancel") 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()() defer setup()()
font := getFont() font := getFont()
defer font.Delete() 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(listCtl, 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)
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(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(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 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(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 setWindowPos.Call(cancelBtn, 0, dpi.Scale(178), dpi.Scale(206), dpi.Scale(75), dpi.Scale(24), 0x4) // SWP_NOZORDER
} else { } 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(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(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 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) postQuitMessage.Call(0)
case 0x0010: // WM_CLOSE case 0x0010: // WM_CLOSE
err = ErrCanceled
destroyWindow.Call(wnd) destroyWindow.Call(wnd)
case 0x0111: // WM_COMMAND case 0x0111: // WM_COMMAND
@ -98,6 +89,7 @@ func listDlg(title, text string, items []string, multiple bool, opts options) (o
} }
} }
case 2: // IDCANCEL case 2: // IDCANCEL
err = ErrCanceled
case 7: // IDNO case 7: // IDNO
err = ErrExtraButton err = ErrExtraButton
} }
@ -107,8 +99,8 @@ func listDlg(title, text string, items []string, multiple bool, opts options) (o
layout(dpi(uint32(wparam) >> 16)) layout(dpi(uint32(wparam) >> 16))
default: default:
ret, _, _ := syscall.Syscall6(defWindowProc, 4, wnd, uintptr(msg), wparam, lparam, 0, 0) res, _, _ := syscall.Syscall6(defWindowProc, 4, wnd, uintptr(msg), wparam, lparam, 0, 0)
return ret return res
} }
return 0 return 0
@ -130,16 +122,16 @@ func listDlg(title, text string, items []string, multiple bool, opts options) (o
defer unregisterClass.Call(cls, instance) defer unregisterClass.Call(cls, instance)
wnd, _, _ = createWindowEx.Call(0x10101, // WS_EX_CONTROLPARENT|WS_EX_WINDOWEDGE|WS_EX_DLGMODALFRAME 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 0x84c80000, // WS_POPUPWINDOW|WS_CLIPSIBLINGS|WS_DLGFRAME
0x80000000, // CW_USEDEFAULT 0x80000000, // CW_USEDEFAULT
0x80000000, // CW_USEDEFAULT 0x80000000, // CW_USEDEFAULT
281, 281, 0, 0, instance) 281, 281, 0, 0, instance, 0)
textCtl, _, _ = createWindowEx.Call(0, textCtl, _, _ = createWindowEx.Call(0,
strptr("STATIC"), strptr(text), strptr("STATIC"), strptr(text),
0x5002e080, // WS_CHILD|WS_VISIBLE|WS_GROUP|SS_WORDELLIPSIS|SS_EDITCONTROL|SS_NOPREFIX 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 var flags uintptr = 0x50320000 // WS_CHILD|WS_VISIBLE|WS_VSCROLL|WS_GROUP|WS_TABSTOP
if multiple { 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 listCtl, _, _ = createWindowEx.Call(0x200, // WS_EX_CLIENTEDGE
strptr("LISTBOX"), strptr(opts.entryText), strptr("LISTBOX"), strptr(opts.entryText),
flags, flags,
12, 30, 241, 164, wnd, 0, instance) 12, 30, 241, 164, wnd, 0, instance, 0)
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, 206, 75, 24, wnd, 1 /* IDOK */, instance) 12, 206, 75, 24, wnd, 1 /* IDOK */, instance, 0)
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, 206, 75, 24, wnd, 2 /* IDCANCEL */, instance) 12, 206, 75, 24, wnd, 2 /* IDCANCEL */, instance, 0)
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, 206, 75, 24, wnd, 7 /* IDNO */, instance) 12, 206, 75, 24, wnd, 7 /* IDNO */, instance, 0)
} }
for _, item := range items { for _, item := range items {

20
msg.go
View File

@ -1,46 +1,34 @@
package zenity 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. // Question displays the question dialog.
// //
// Returns true on OK, false on Cancel, or ErrExtraButton.
//
// Valid options: Title, Width, Height, OKLabel, CancelLabel, ExtraButton, // Valid options: Title, Width, Height, OKLabel, CancelLabel, ExtraButton,
// Icon, NoWrap, Ellipsize, DefaultCancel. // 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)) return message(questionKind, text, applyOptions(options))
} }
// Info displays the info dialog. // Info displays the info dialog.
// //
// Returns true on OK, false on dismiss, or ErrExtraButton.
//
// Valid options: Title, Width, Height, OKLabel, ExtraButton, Icon, // Valid options: Title, Width, Height, OKLabel, ExtraButton, Icon,
// NoWrap, Ellipsize. // NoWrap, Ellipsize.
func Info(text string, options ...Option) (bool, error) { func Info(text string, options ...Option) error {
return message(infoKind, text, applyOptions(options)) return message(infoKind, text, applyOptions(options))
} }
// Warning displays the warning dialog. // Warning displays the warning dialog.
// //
// Returns true on OK, false on dismiss, or ErrExtraButton.
//
// Valid options: Title, Width, Height, OKLabel, ExtraButton, Icon, // Valid options: Title, Width, Height, OKLabel, ExtraButton, Icon,
// NoWrap, Ellipsize. // NoWrap, Ellipsize.
func Warning(text string, options ...Option) (bool, error) { func Warning(text string, options ...Option) error {
return message(warningKind, text, applyOptions(options)) return message(warningKind, text, applyOptions(options))
} }
// Error displays the error dialog. // Error displays the error dialog.
// //
// Returns true on OK, false on dismiss, or ErrExtraButton.
//
// Valid options: Title, Width, Height, OKLabel, ExtraButton, Icon, // Valid options: Title, Width, Height, OKLabel, ExtraButton, Icon,
// NoWrap, Ellipsize. // NoWrap, Ellipsize.
func Error(text string, options ...Option) (bool, error) { func Error(text string, options ...Option) error {
return message(errorKind, text, applyOptions(options)) return message(errorKind, text, applyOptions(options))
} }

View File

@ -4,7 +4,7 @@ import (
"github.com/ncruces/zenity/internal/zenutil" "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 var data zenutil.Dialog
data.Text = text data.Text = text
data.Options.Timeout = zenutil.Timeout 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)) data.SetButtons(getButtons(dialog, kind == questionKind, opts))
out, err := zenutil.Run(opts.ctx, "dialog", data) out, err := zenutil.Run(opts.ctx, "dialog", data)
_, ok, err := strResult(opts, out, err) _, err = strResult(opts, out, err)
return ok, err return err
} }

View File

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

View File

@ -6,7 +6,7 @@ import (
"github.com/ncruces/zenity/internal/zenutil" "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"} args := []string{"--text", text, "--no-markup"}
switch kind { switch kind {
case questionKind: case questionKind:
@ -47,6 +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)
_, ok, err := strResult(opts, out, err) _, err = strResult(opts, out, err)
return ok, err return err
} }

View File

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

View File

@ -6,6 +6,7 @@ import (
"testing" "testing"
"github.com/ncruces/zenity" "github.com/ncruces/zenity"
"go.uber.org/goleak"
) )
func ExampleNotify() { func ExampleNotify() {
@ -15,7 +16,8 @@ func ExampleNotify() {
// Output: // Output:
} }
func TestNotifyCancel(t *testing.T) { func TestNotify_cancel(t *testing.T) {
defer goleak.VerifyNone(t)
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
cancel() 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. // Password displays the password dialog.
// //
// Returns false on cancel, or ErrExtraButton.
//
// Valid options: Title, OKLabel, CancelLabel, ExtraButton, Icon, Username. // 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)) return password(applyOptions(options))
} }

View File

@ -2,8 +2,11 @@
package zenity 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 opts.hideText = true
str, ok, err := entry("Password:", opts) str, err := entry("Password:", opts)
return "", str, ok, err return "", str, err
} }

View File

@ -8,6 +8,7 @@ import (
"time" "time"
"github.com/ncruces/zenity" "github.com/ncruces/zenity"
"go.uber.org/goleak"
) )
func ExamplePassword() { func ExamplePassword() {
@ -15,22 +16,23 @@ func ExamplePassword() {
// Output: // 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) 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) { if !os.IsTimeout(err) {
t.Error("did not timeout:", 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()) ctx, cancel := context.WithCancel(context.Background())
cancel() cancel()
_, _, _, err := zenity.Password(zenity.Context(ctx)) _, _, err := zenity.Password(zenity.Context(ctx))
if !errors.Is(err, context.Canceled) { if !errors.Is(err, context.Canceled) {
t.Error("was not canceled:", err) t.Error("was not canceled:", err)
} }

View File

@ -8,7 +8,7 @@ import (
"github.com/ncruces/zenity/internal/zenutil" "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 := []string{"--password"}
args = appendTitle(args, opts) args = appendTitle(args, opts)
args = appendButtons(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) out, err := zenutil.Run(opts.ctx, args)
str, ok, err := strResult(opts, out, err) str, err := strResult(opts, out, err)
if ok && opts.username { if err == nil && opts.username {
if split := strings.SplitN(string(out), "|", 2); len(split) == 2 { 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" 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 { if !okcancel {
opts.cancelLabel = nil opts.cancelLabel = nil
opts.defaultCancel = false 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 { if opts.okLabel == nil {
opts.okLabel = stringPtr("OK") opts.okLabel = stringPtr("OK")
} }

View File

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

View File

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

View File

@ -13,14 +13,22 @@ package zenity
import ( import (
"context" "context"
"image/color" "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 } 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 { type options struct {
// General options // General options
title *string title *string
@ -57,6 +65,11 @@ type options struct {
color color.Color color color.Color
showPalette bool showPalette bool
// Progress indication options
maxValue int
noCancel bool
timeRemaining bool
// Context for timeout // Context for timeout
ctx context.Context ctx context.Context
} }