commit
4d3f930ac7
51 changed files with 1221 additions and 310 deletions
11
README.md
11
README.md
|
@ -13,14 +13,14 @@ Implemented dialogs:
|
|||
* [text entry](https://github.com/ncruces/zenity/wiki/Text-Entry-dialog)
|
||||
* [list](https://github.com/ncruces/zenity/wiki/List-dialog) (simple)
|
||||
* [password](https://github.com/ncruces/zenity/wiki/Password-dialog)
|
||||
* [file selection](https://github.com/ncruces/zenity/wiki/File-Selection-dialog)
|
||||
* [color selection](https://github.com/ncruces/zenity/wiki/Color-Selection-dialog)
|
||||
* [file selection](https://github.com/ncruces/zenity/wiki/File-selection-dialog)
|
||||
* [color selection](https://github.com/ncruces/zenity/wiki/Color-selection-dialog)
|
||||
* [progress](https://github.com/ncruces/zenity/wiki/Progress-dialog)
|
||||
* [notification](https://github.com/ncruces/zenity/wiki/Notification)
|
||||
|
||||
Behavior on Windows, macOS and other Unixes might differ slightly.
|
||||
Some of that is intended (reflecting platform differences),
|
||||
other bits are unfortunate limitations,
|
||||
others still are open to be fixed.
|
||||
other bits are unfortunate limitations.
|
||||
|
||||
## Why?
|
||||
|
||||
|
@ -37,7 +37,8 @@ Why reinvent this particular wheel?
|
|||
* Explorer shell not required
|
||||
* works in Server Core
|
||||
* Unicode support
|
||||
* High DPI support (no manifest required)
|
||||
* High DPI (no manifest required)
|
||||
* Visual Styles (no manifest required)
|
||||
* WSL/Cygwin/MSYS2 [support](https://github.com/ncruces/zenity/wiki/Zenity-for-WSL,-Cygwin,-MSYS2)
|
||||
* on macOS:
|
||||
* only dependency is `osascript`
|
||||
|
|
|
@ -34,6 +34,7 @@ var (
|
|||
passwordDlg bool
|
||||
fileSelectionDlg bool
|
||||
colorSelectionDlg bool
|
||||
progressDlg bool
|
||||
notification bool
|
||||
|
||||
// General options
|
||||
|
@ -73,6 +74,13 @@ var (
|
|||
defaultColor string
|
||||
showPalette bool
|
||||
|
||||
// Progress options
|
||||
percentage float64
|
||||
pulsate bool
|
||||
autoClose bool
|
||||
autoKill bool
|
||||
noCancel bool
|
||||
|
||||
// Windows specific options
|
||||
cygpath bool
|
||||
wslpath bool
|
||||
|
@ -95,32 +103,32 @@ func main() {
|
|||
if zenutil.Timeout > 0 {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(zenutil.Timeout)*time.Second)
|
||||
opts = append(opts, zenity.Context(ctx))
|
||||
_ = cancel
|
||||
defer cancel()
|
||||
}
|
||||
|
||||
switch {
|
||||
case errorDlg:
|
||||
okResult(zenity.Error(text, opts...))
|
||||
errResult(zenity.Error(text, opts...))
|
||||
case infoDlg:
|
||||
okResult(zenity.Info(text, opts...))
|
||||
errResult(zenity.Info(text, opts...))
|
||||
case warningDlg:
|
||||
okResult(zenity.Warning(text, opts...))
|
||||
errResult(zenity.Warning(text, opts...))
|
||||
case questionDlg:
|
||||
okResult(zenity.Question(text, opts...))
|
||||
errResult(zenity.Question(text, opts...))
|
||||
|
||||
case entryDlg:
|
||||
strOKResult(zenity.Entry(text, opts...))
|
||||
strResult(zenity.Entry(text, opts...))
|
||||
|
||||
case listDlg:
|
||||
if multiple {
|
||||
listResult(zenity.ListMultiple(text, flag.Args(), opts...))
|
||||
lstResult(zenity.ListMultiple(text, flag.Args(), opts...))
|
||||
} else {
|
||||
strOKResult(zenity.List(text, flag.Args(), opts...))
|
||||
strResult(zenity.List(text, flag.Args(), opts...))
|
||||
}
|
||||
|
||||
case passwordDlg:
|
||||
_, pw, ok, err := zenity.Password(opts...)
|
||||
strOKResult(pw, ok, err)
|
||||
_, pw, err := zenity.Password(opts...)
|
||||
strResult(pw, err)
|
||||
|
||||
case fileSelectionDlg:
|
||||
switch {
|
||||
|
@ -129,17 +137,21 @@ func main() {
|
|||
case save:
|
||||
strResult(egestPath(zenity.SelectFileSave(opts...)))
|
||||
case multiple:
|
||||
listResult(egestPaths(zenity.SelectFileMutiple(opts...)))
|
||||
lstResult(egestPaths(zenity.SelectFileMutiple(opts...)))
|
||||
}
|
||||
|
||||
case colorSelectionDlg:
|
||||
colorResult(zenity.SelectColor(opts...))
|
||||
colResult(zenity.SelectColor(opts...))
|
||||
|
||||
case progressDlg:
|
||||
errResult(progress(opts...))
|
||||
|
||||
case notification:
|
||||
errResult(zenity.Notify(text, opts...))
|
||||
}
|
||||
|
||||
flag.Usage()
|
||||
default:
|
||||
flag.Usage()
|
||||
}
|
||||
}
|
||||
|
||||
func setupFlags() {
|
||||
|
@ -153,6 +165,7 @@ func setupFlags() {
|
|||
flag.BoolVar(&passwordDlg, "password", false, "Display password dialog")
|
||||
flag.BoolVar(&fileSelectionDlg, "file-selection", false, "Display file selection dialog")
|
||||
flag.BoolVar(&colorSelectionDlg, "color-selection", false, "Display color selection dialog")
|
||||
flag.BoolVar(&progressDlg, "progress", false, "Display progress indication dialog")
|
||||
flag.BoolVar(¬ification, "notification", false, "Display notification")
|
||||
|
||||
// General options
|
||||
|
@ -165,12 +178,12 @@ func setupFlags() {
|
|||
flag.StringVar(&text, "text", "", "Set the dialog `text`")
|
||||
flag.StringVar(&icon, "window-icon", "", "Set the window `icon` (error, info, question, warning)")
|
||||
flag.BoolVar(&multiple, "multiple", false, "Allow multiple items to be selected")
|
||||
flag.BoolVar(&defaultCancel, "default-cancel", false, "Give Cancel button focus by default")
|
||||
|
||||
// Message options
|
||||
flag.StringVar(&icon, "icon-name", "", "Set the dialog `icon` (dialog-error, dialog-information, dialog-question, dialog-warning)")
|
||||
flag.BoolVar(&noWrap, "no-wrap", false, "Do not enable text wrapping")
|
||||
flag.BoolVar(&ellipsize, "ellipsize", false, "Enable ellipsizing in the dialog text")
|
||||
flag.BoolVar(&defaultCancel, "default-cancel", false, "Give Cancel button focus by default")
|
||||
|
||||
// Entry options
|
||||
flag.StringVar(&entryText, "entry-text", "", "Set the entry `text`")
|
||||
|
@ -194,6 +207,15 @@ func setupFlags() {
|
|||
flag.StringVar(&defaultColor, "color", "", "Set the `color`")
|
||||
flag.BoolVar(&showPalette, "show-palette", false, "Show the palette")
|
||||
|
||||
// Progress options
|
||||
flag.Float64Var(&percentage, "percentage", 0, "Set initial `percentage`")
|
||||
flag.BoolVar(&pulsate, "pulsate", false, "Pulsate progress bar")
|
||||
flag.BoolVar(&noCancel, "no-cancel", false, "Hide Cancel button (Windows and Unix only)")
|
||||
flag.BoolVar(&autoClose, "auto-close", false, "Dismiss the dialog when 100% has been reached")
|
||||
if runtime.GOOS != "windows" {
|
||||
flag.BoolVar(&autoKill, "auto-kill", false, "Kill parent process if Cancel button is pressed (macOS and Unix only)")
|
||||
}
|
||||
|
||||
// Windows specific options
|
||||
if runtime.GOOS == "windows" {
|
||||
flag.BoolVar(&cygpath, "cygpath", false, "Use cygpath for path translation (Windows only)")
|
||||
|
@ -242,6 +264,9 @@ func validateFlags() {
|
|||
if colorSelectionDlg {
|
||||
n++
|
||||
}
|
||||
if progressDlg {
|
||||
n++
|
||||
}
|
||||
if notification {
|
||||
n++
|
||||
}
|
||||
|
@ -297,6 +322,11 @@ func loadFlags() []zenity.Option {
|
|||
setDefault(&icon, "dialog-password")
|
||||
setDefault(&okLabel, "OK")
|
||||
setDefault(&cancelLabel, "Cancel")
|
||||
case progressDlg:
|
||||
setDefault(&title, "Progress")
|
||||
setDefault(&text, "Running...")
|
||||
setDefault(&okLabel, "OK")
|
||||
setDefault(&cancelLabel, "Cancel")
|
||||
default:
|
||||
setDefault(&text, "")
|
||||
}
|
||||
|
@ -388,6 +418,15 @@ func loadFlags() []zenity.Option {
|
|||
opts = append(opts, zenity.ShowPalette())
|
||||
}
|
||||
|
||||
// Progress options
|
||||
|
||||
if pulsate {
|
||||
opts = append(opts, zenity.Pulsate())
|
||||
}
|
||||
if noCancel {
|
||||
opts = append(opts, zenity.NoCancel())
|
||||
}
|
||||
|
||||
return opts
|
||||
}
|
||||
|
||||
|
@ -395,6 +434,9 @@ func errResult(err error) {
|
|||
if os.IsTimeout(err) {
|
||||
os.Exit(5)
|
||||
}
|
||||
if err == zenity.ErrCanceled {
|
||||
os.Exit(1)
|
||||
}
|
||||
if err == zenity.ErrExtraButton {
|
||||
os.Stdout.WriteString(extraButton)
|
||||
os.Stdout.WriteString(zenutil.LineBreak)
|
||||
|
@ -408,64 +450,33 @@ func errResult(err error) {
|
|||
os.Exit(0)
|
||||
}
|
||||
|
||||
func okResult(ok bool, err error) {
|
||||
if err != nil {
|
||||
errResult(err)
|
||||
}
|
||||
if ok {
|
||||
os.Exit(0)
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func strResult(s string, err error) {
|
||||
if err != nil {
|
||||
errResult(err)
|
||||
}
|
||||
if s == "" {
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Stdout.WriteString(s)
|
||||
os.Stdout.WriteString(zenutil.LineBreak)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func listResult(l []string, err error) {
|
||||
func lstResult(l []string, err error) {
|
||||
if err != nil {
|
||||
errResult(err)
|
||||
}
|
||||
if l == nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Stdout.WriteString(strings.Join(l, zenutil.Separator))
|
||||
os.Stdout.WriteString(zenutil.LineBreak)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func colorResult(c color.Color, err error) {
|
||||
func colResult(c color.Color, err error) {
|
||||
if err != nil {
|
||||
errResult(err)
|
||||
}
|
||||
if c == nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Stdout.WriteString(zenutil.UnparseColor(c))
|
||||
os.Stdout.WriteString(zenutil.LineBreak)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func strOKResult(s string, ok bool, err error) {
|
||||
if err != nil {
|
||||
errResult(err)
|
||||
}
|
||||
if !ok {
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Stdout.WriteString(s)
|
||||
os.Stdout.WriteString(zenutil.LineBreak)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func ingestPath(path string) string {
|
||||
if runtime.GOOS == "windows" && path != "" {
|
||||
var args []string
|
||||
|
|
72
cmd/zenity/progress.go
Normal file
72
cmd/zenity/progress.go
Normal 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()
|
||||
}
|
12
cmd/zenity/progress_unix.go
Normal file
12
cmd/zenity/progress_unix.go
Normal file
|
@ -0,0 +1,12 @@
|
|||
// +build !windows,!js
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func killParent() {
|
||||
syscall.Kill(os.Getppid(), syscall.SIGHUP)
|
||||
}
|
3
cmd/zenity/progress_windows.go
Normal file
3
cmd/zenity/progress_windows.go
Normal file
|
@ -0,0 +1,3 @@
|
|||
package main
|
||||
|
||||
func killParent() {}
|
2
color.go
2
color.go
|
@ -4,8 +4,6 @@ import "image/color"
|
|||
|
||||
// SelectColor displays the color selection dialog.
|
||||
//
|
||||
// Returns nil on cancel.
|
||||
//
|
||||
// Valid options: Title, Color, ShowPalette.
|
||||
func SelectColor(options ...Option) (color.Color, error) {
|
||||
return selectColor(applyOptions(options))
|
||||
|
|
|
@ -20,9 +20,9 @@ func selectColor(opts options) (color.Color, error) {
|
|||
float32(g) / 0xffff,
|
||||
float32(b) / 0xffff,
|
||||
})
|
||||
str, ok, err := strResult(opts, out, err)
|
||||
if ok {
|
||||
return zenutil.ParseColor(str), nil
|
||||
str, err := strResult(opts, out, err)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, err
|
||||
return zenutil.ParseColor(str), nil
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/ncruces/zenity"
|
||||
"go.uber.org/goleak"
|
||||
)
|
||||
|
||||
func ExampleSelectColor() {
|
||||
|
@ -24,18 +25,19 @@ func ExampleSelectColor_palette() {
|
|||
// Output:
|
||||
}
|
||||
|
||||
func TestSelectColorTimeout(t *testing.T) {
|
||||
func TestSelectColor_timeout(t *testing.T) {
|
||||
defer goleak.VerifyNone(t)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second/10)
|
||||
defer cancel()
|
||||
|
||||
_, err := zenity.SelectColor(zenity.Context(ctx))
|
||||
if !os.IsTimeout(err) {
|
||||
t.Error("did not timeout:", err)
|
||||
}
|
||||
|
||||
cancel()
|
||||
}
|
||||
|
||||
func TestSelectColorCancel(t *testing.T) {
|
||||
func TestSelectColor_cancel(t *testing.T) {
|
||||
defer goleak.VerifyNone(t)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
|
|
|
@ -20,9 +20,9 @@ func selectColor(opts options) (color.Color, error) {
|
|||
}
|
||||
|
||||
out, err := zenutil.Run(opts.ctx, args)
|
||||
str, ok, err := strResult(opts, out, err)
|
||||
if ok {
|
||||
return zenutil.ParseColor(str), nil
|
||||
str, err := strResult(opts, out, err)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, err
|
||||
return zenutil.ParseColor(str), nil
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import (
|
|||
var (
|
||||
chooseColor = comdlg32.NewProc("ChooseColorW")
|
||||
|
||||
savedColors = [16]uint32{}
|
||||
savedColors [16]uint32
|
||||
colorsMutex sync.Mutex
|
||||
)
|
||||
|
||||
|
@ -32,7 +32,7 @@ func selectColor(opts options) (color.Color, error) {
|
|||
if opts.color != nil {
|
||||
args.Flags |= 0x1 // CC_RGBINIT
|
||||
n := color.NRGBAModel.Convert(opts.color).(color.NRGBA)
|
||||
args.RgbResult = uint32(n.R) | (uint32(n.G) << 8) | (uint32(n.B) << 16)
|
||||
args.RgbResult = uint32(n.R) | uint32(n.G)<<8 | uint32(n.B)<<16
|
||||
}
|
||||
if opts.showPalette {
|
||||
args.Flags |= 0x4 // CC_PREVENTFULLOPEN
|
||||
|
|
4
entry.go
4
entry.go
|
@ -2,11 +2,9 @@ package zenity
|
|||
|
||||
// Entry displays the text entry dialog.
|
||||
//
|
||||
// Returns false on cancel, or ErrExtraButton.
|
||||
//
|
||||
// Valid options: Title, Width, Height, OKLabel, CancelLabel, ExtraButton,
|
||||
// Icon, EntryText, HideText.
|
||||
func Entry(text string, options ...Option) (string, bool, error) {
|
||||
func Entry(text string, options ...Option) (string, error) {
|
||||
return entry(text, applyOptions(options))
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import (
|
|||
"github.com/ncruces/zenity/internal/zenutil"
|
||||
)
|
||||
|
||||
func entry(text string, opts options) (string, bool, error) {
|
||||
func entry(text string, opts options) (string, error) {
|
||||
var data zenutil.Dialog
|
||||
data.Text = text
|
||||
data.Operation = "displayDialog"
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/ncruces/zenity"
|
||||
"go.uber.org/goleak"
|
||||
)
|
||||
|
||||
func ExampleEntry() {
|
||||
|
@ -16,22 +17,23 @@ func ExampleEntry() {
|
|||
// Output:
|
||||
}
|
||||
|
||||
func TestEntryTimeout(t *testing.T) {
|
||||
func TestEntry_timeout(t *testing.T) {
|
||||
defer goleak.VerifyNone(t)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second/10)
|
||||
defer cancel()
|
||||
|
||||
_, _, err := zenity.Entry("", zenity.Context(ctx))
|
||||
_, err := zenity.Entry("", zenity.Context(ctx))
|
||||
if !os.IsTimeout(err) {
|
||||
t.Error("did not timeout:", err)
|
||||
}
|
||||
|
||||
cancel()
|
||||
}
|
||||
|
||||
func TestEntryCancel(t *testing.T) {
|
||||
func TestEntry_cancel(t *testing.T) {
|
||||
defer goleak.VerifyNone(t)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
_, _, err := zenity.Entry("", zenity.Context(ctx))
|
||||
_, err := zenity.Entry("", zenity.Context(ctx))
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
t.Error("was not canceled:", err)
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ import (
|
|||
"github.com/ncruces/zenity/internal/zenutil"
|
||||
)
|
||||
|
||||
func entry(text string, opts options) (string, bool, error) {
|
||||
func entry(text string, opts options) (string, error) {
|
||||
args := []string{"--entry", "--text", text}
|
||||
args = appendTitle(args, opts)
|
||||
args = appendButtons(args, opts)
|
||||
|
|
|
@ -4,10 +4,9 @@ import (
|
|||
"syscall"
|
||||
)
|
||||
|
||||
func entry(text string, opts options) (out string, ok bool, err error) {
|
||||
var title string
|
||||
if opts.title != nil {
|
||||
title = *opts.title
|
||||
func entry(text string, opts options) (out string, err error) {
|
||||
if opts.title == nil {
|
||||
opts.title = stringPtr("")
|
||||
}
|
||||
if opts.okLabel == nil {
|
||||
opts.okLabel = stringPtr("OK")
|
||||
|
@ -15,10 +14,7 @@ func entry(text string, opts options) (out string, ok bool, err error) {
|
|||
if opts.cancelLabel == nil {
|
||||
opts.cancelLabel = stringPtr("Cancel")
|
||||
}
|
||||
return entryDlg(title, text, opts)
|
||||
}
|
||||
|
||||
func entryDlg(title, text string, opts options) (out string, ok bool, err error) {
|
||||
defer setup()()
|
||||
font := getFont()
|
||||
defer font.Delete()
|
||||
|
@ -33,6 +29,7 @@ func entryDlg(title, text string, opts options) (out string, ok bool, err error)
|
|||
sendMessage.Call(editCtl, 0x0030 /* WM_SETFONT */, hfont, 1)
|
||||
sendMessage.Call(okBtn, 0x0030 /* WM_SETFONT */, hfont, 1)
|
||||
sendMessage.Call(cancelBtn, 0x0030 /* WM_SETFONT */, hfont, 1)
|
||||
sendMessage.Call(extraBtn, 0x0030 /* WM_SETFONT */, hfont, 1)
|
||||
setWindowPos.Call(wnd, 0, 0, 0, dpi.Scale(281), dpi.Scale(141), 0x6) // SWP_NOZORDER|SWP_NOMOVE
|
||||
setWindowPos.Call(textCtl, 0, dpi.Scale(12), dpi.Scale(10), dpi.Scale(241), dpi.Scale(16), 0x4) // SWP_NOZORDER
|
||||
setWindowPos.Call(editCtl, 0, dpi.Scale(12), dpi.Scale(30), dpi.Scale(241), dpi.Scale(24), 0x4) // SWP_NOZORDER
|
||||
|
@ -40,7 +37,6 @@ func entryDlg(title, text string, opts options) (out string, ok bool, err error)
|
|||
setWindowPos.Call(okBtn, 0, dpi.Scale(95), dpi.Scale(66), dpi.Scale(75), dpi.Scale(24), 0x4) // SWP_NOZORDER
|
||||
setWindowPos.Call(cancelBtn, 0, dpi.Scale(178), dpi.Scale(66), dpi.Scale(75), dpi.Scale(24), 0x4) // SWP_NOZORDER
|
||||
} else {
|
||||
sendMessage.Call(extraBtn, 0x0030 /* WM_SETFONT */, hfont, 1)
|
||||
setWindowPos.Call(okBtn, 0, dpi.Scale(12), dpi.Scale(66), dpi.Scale(75), dpi.Scale(24), 0x4) // SWP_NOZORDER
|
||||
setWindowPos.Call(extraBtn, 0, dpi.Scale(95), dpi.Scale(66), dpi.Scale(75), dpi.Scale(24), 0x4) // SWP_NOZORDER
|
||||
setWindowPos.Call(cancelBtn, 0, dpi.Scale(178), dpi.Scale(66), dpi.Scale(75), dpi.Scale(24), 0x4) // SWP_NOZORDER
|
||||
|
@ -53,6 +49,7 @@ func entryDlg(title, text string, opts options) (out string, ok bool, err error)
|
|||
postQuitMessage.Call(0)
|
||||
|
||||
case 0x0010: // WM_CLOSE
|
||||
err = ErrCanceled
|
||||
destroyWindow.Call(wnd)
|
||||
|
||||
case 0x0111: // WM_COMMAND
|
||||
|
@ -61,8 +58,8 @@ func entryDlg(title, text string, opts options) (out string, ok bool, err error)
|
|||
return 1
|
||||
case 1, 6: // IDOK, IDYES
|
||||
out = getWindowString(editCtl)
|
||||
ok = true
|
||||
case 2: // IDCANCEL
|
||||
err = ErrCanceled
|
||||
case 7: // IDNO
|
||||
err = ErrExtraButton
|
||||
}
|
||||
|
@ -72,39 +69,39 @@ func entryDlg(title, text string, opts options) (out string, ok bool, err error)
|
|||
layout(dpi(uint32(wparam) >> 16))
|
||||
|
||||
default:
|
||||
ret, _, _ := syscall.Syscall6(defWindowProc, 4, wnd, uintptr(msg), wparam, lparam, 0, 0)
|
||||
return ret
|
||||
res, _, _ := syscall.Syscall6(defWindowProc, 4, wnd, uintptr(msg), wparam, lparam, 0, 0)
|
||||
return res
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
if opts.ctx != nil && opts.ctx.Err() != nil {
|
||||
return "", false, opts.ctx.Err()
|
||||
return "", opts.ctx.Err()
|
||||
}
|
||||
|
||||
instance, _, err := getModuleHandle.Call(0)
|
||||
if instance == 0 {
|
||||
return "", false, err
|
||||
return "", err
|
||||
}
|
||||
|
||||
cls, err := registerClass(instance, syscall.NewCallback(proc))
|
||||
if cls == 0 {
|
||||
return "", false, err
|
||||
return "", err
|
||||
}
|
||||
defer unregisterClass.Call(cls, instance)
|
||||
|
||||
wnd, _, _ = createWindowEx.Call(0x10101, // WS_EX_CONTROLPARENT|WS_EX_WINDOWEDGE|WS_EX_DLGMODALFRAME
|
||||
cls, strptr(title),
|
||||
cls, strptr(*opts.title),
|
||||
0x84c80000, // WS_POPUPWINDOW|WS_CLIPSIBLINGS|WS_DLGFRAME
|
||||
0x80000000, // CW_USEDEFAULT
|
||||
0x80000000, // CW_USEDEFAULT
|
||||
281, 141, 0, 0, instance)
|
||||
281, 141, 0, 0, instance, 0)
|
||||
|
||||
textCtl, _, _ = createWindowEx.Call(0,
|
||||
strptr("STATIC"), strptr(text),
|
||||
0x5002e080, // WS_CHILD|WS_VISIBLE|WS_GROUP|SS_WORDELLIPSIS|SS_EDITCONTROL|SS_NOPREFIX
|
||||
12, 10, 241, 16, wnd, 0, instance)
|
||||
12, 10, 241, 16, wnd, 0, instance, 0)
|
||||
|
||||
var flags uintptr = 0x50030080 // WS_CHILD|WS_VISIBLE|WS_GROUP|WS_TABSTOP|ES_AUTOHSCROLL
|
||||
if opts.hideText {
|
||||
|
@ -113,21 +110,21 @@ func entryDlg(title, text string, opts options) (out string, ok bool, err error)
|
|||
editCtl, _, _ = createWindowEx.Call(0x200, // WS_EX_CLIENTEDGE
|
||||
strptr("EDIT"), strptr(opts.entryText),
|
||||
flags,
|
||||
12, 30, 241, 24, wnd, 0, instance)
|
||||
12, 30, 241, 24, wnd, 0, instance, 0)
|
||||
|
||||
okBtn, _, _ = createWindowEx.Call(0,
|
||||
strptr("BUTTON"), strptr(*opts.okLabel),
|
||||
0x50030001, // WS_CHILD|WS_VISIBLE|WS_GROUP|WS_TABSTOP|BS_DEFPUSHBUTTON
|
||||
12, 66, 75, 24, wnd, 1 /* IDOK */, instance)
|
||||
12, 66, 75, 24, wnd, 1 /* IDOK */, instance, 0)
|
||||
cancelBtn, _, _ = createWindowEx.Call(0,
|
||||
strptr("BUTTON"), strptr(*opts.cancelLabel),
|
||||
0x50010000, // WS_CHILD|WS_VISIBLE|WS_GROUP|WS_TABSTOP
|
||||
12, 66, 75, 24, wnd, 2 /* IDCANCEL */, instance)
|
||||
12, 66, 75, 24, wnd, 2 /* IDCANCEL */, instance, 0)
|
||||
if opts.extraButton != nil {
|
||||
extraBtn, _, _ = createWindowEx.Call(0,
|
||||
strptr("BUTTON"), strptr(*opts.extraButton),
|
||||
0x50010000, // WS_CHILD|WS_VISIBLE|WS_GROUP|WS_TABSTOP
|
||||
12, 66, 75, 24, wnd, 7 /* IDNO */, instance)
|
||||
12, 66, 75, 24, wnd, 7 /* IDNO */, instance, 0)
|
||||
}
|
||||
|
||||
layout(getDPI(wnd))
|
||||
|
@ -149,13 +146,13 @@ func entryDlg(title, text string, opts options) (out string, ok bool, err error)
|
|||
}
|
||||
|
||||
// set default values
|
||||
out, ok, err = "", false, nil
|
||||
out, err = "", nil
|
||||
|
||||
if err := messageLoop(wnd); err != nil {
|
||||
return "", false, err
|
||||
return "", err
|
||||
}
|
||||
if opts.ctx != nil && opts.ctx.Err() != nil {
|
||||
return "", false, opts.ctx.Err()
|
||||
return "", opts.ctx.Err()
|
||||
}
|
||||
return out, ok, err
|
||||
return out, err
|
||||
}
|
||||
|
|
9
file.go
9
file.go
|
@ -8,8 +8,6 @@ import (
|
|||
|
||||
// SelectFile displays the file selection dialog.
|
||||
//
|
||||
// Returns an empty string on cancel.
|
||||
//
|
||||
// Valid options: Title, Directory, Filename, ShowHidden, FileFilter(s).
|
||||
func SelectFile(options ...Option) (string, error) {
|
||||
return selectFile(applyOptions(options))
|
||||
|
@ -17,8 +15,6 @@ func SelectFile(options ...Option) (string, error) {
|
|||
|
||||
// SelectFileMutiple displays the multiple file selection dialog.
|
||||
//
|
||||
// Returns a nil slice on cancel.
|
||||
//
|
||||
// Valid options: Title, Directory, Filename, ShowHidden, FileFilter(s).
|
||||
func SelectFileMutiple(options ...Option) ([]string, error) {
|
||||
return selectFileMutiple(applyOptions(options))
|
||||
|
@ -26,8 +22,6 @@ func SelectFileMutiple(options ...Option) ([]string, error) {
|
|||
|
||||
// SelectFileSave displays the save file selection dialog.
|
||||
//
|
||||
// Returns an empty string on cancel.
|
||||
//
|
||||
// Valid options: Title, Filename, ConfirmOverwrite, ConfirmCreate, ShowHidden,
|
||||
// FileFilter(s).
|
||||
func SelectFileSave(options ...Option) (string, error) {
|
||||
|
@ -69,6 +63,9 @@ func Filename(filename string) Option {
|
|||
//
|
||||
// macOS hides filename filters from the user,
|
||||
// and only supports filtering by extension (or "type").
|
||||
//
|
||||
// Patterns may use the GTK syntax on all platforms:
|
||||
// https://developer.gnome.org/pygtk/stable/class-gtkfilefilter.html#method-gtkfilefilter--add-pattern
|
||||
type FileFilter struct {
|
||||
Name string // display string that describes the filter (optional)
|
||||
Patterns []string // filter patterns for the display string
|
||||
|
|
|
@ -18,8 +18,7 @@ func selectFile(opts options) (string, error) {
|
|||
}
|
||||
|
||||
out, err := zenutil.Run(opts.ctx, "file", data)
|
||||
str, _, err := strResult(opts, out, err)
|
||||
return str, err
|
||||
return strResult(opts, out, err)
|
||||
}
|
||||
|
||||
func selectFileMutiple(opts options) ([]string, error) {
|
||||
|
@ -54,6 +53,5 @@ func selectFileSave(opts options) (string, error) {
|
|||
}
|
||||
|
||||
out, err := zenutil.Run(opts.ctx, "file", data)
|
||||
str, _, err := strResult(opts, out, err)
|
||||
return str, err
|
||||
return strResult(opts, out, err)
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/ncruces/zenity"
|
||||
"go.uber.org/goleak"
|
||||
)
|
||||
|
||||
const defaultPath = ``
|
||||
|
@ -77,7 +78,7 @@ var fileFuncs = []func(...zenity.Option) (string, error){
|
|||
},
|
||||
}
|
||||
|
||||
func TestFileTimeout(t *testing.T) {
|
||||
func TestFile_timeout(t *testing.T) {
|
||||
for _, f := range fileFuncs {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second/10)
|
||||
|
||||
|
@ -87,10 +88,12 @@ func TestFileTimeout(t *testing.T) {
|
|||
}
|
||||
|
||||
cancel()
|
||||
goleak.VerifyNone(t)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileCancel(t *testing.T) {
|
||||
func TestFile_cancel(t *testing.T) {
|
||||
defer goleak.VerifyNone(t)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
|
|
|
@ -14,8 +14,7 @@ func selectFile(opts options) (string, error) {
|
|||
args = appendFileArgs(args, opts)
|
||||
|
||||
out, err := zenutil.Run(opts.ctx, args)
|
||||
str, _, err := strResult(opts, out, err)
|
||||
return str, err
|
||||
return strResult(opts, out, err)
|
||||
}
|
||||
|
||||
func selectFileMutiple(opts options) ([]string, error) {
|
||||
|
@ -33,8 +32,7 @@ func selectFileSave(opts options) (string, error) {
|
|||
args = appendFileArgs(args, opts)
|
||||
|
||||
out, err := zenutil.Run(opts.ctx, args)
|
||||
str, _, err := strResult(opts, out, err)
|
||||
return str, err
|
||||
return strResult(opts, out, err)
|
||||
}
|
||||
|
||||
func initFilters(filters []FileFilter) []string {
|
||||
|
|
|
@ -37,7 +37,7 @@ func selectFile(opts options) (string, error) {
|
|||
args.Filter = &initFilters(opts.fileFilters)[0]
|
||||
}
|
||||
|
||||
res := [32768]uint16{}
|
||||
var res [32768]uint16
|
||||
args.File = &res[0]
|
||||
args.MaxFile = uint32(len(res))
|
||||
args.InitialDir, args.DefExt = initDirNameExt(opts.filename, res[:])
|
||||
|
@ -82,7 +82,7 @@ func selectFileMutiple(opts options) ([]string, error) {
|
|||
args.Filter = &initFilters(opts.fileFilters)[0]
|
||||
}
|
||||
|
||||
res := [32768 + 1024*256]uint16{}
|
||||
var res [32768 + 1024*256]uint16
|
||||
args.File = &res[0]
|
||||
args.MaxFile = uint32(len(res))
|
||||
args.InitialDir, args.DefExt = initDirNameExt(opts.filename, res[:])
|
||||
|
@ -158,7 +158,7 @@ func selectFileSave(opts options) (string, error) {
|
|||
args.Filter = &initFilters(opts.fileFilters)[0]
|
||||
}
|
||||
|
||||
res := [32768]uint16{}
|
||||
var res [32768]uint16
|
||||
args.File = &res[0]
|
||||
args.MaxFile = uint32(len(res))
|
||||
args.InitialDir, args.DefExt = initDirNameExt(opts.filename, res[:])
|
||||
|
@ -251,7 +251,7 @@ func pickFolders(opts options, multi bool) (str string, lst []string, err error)
|
|||
return "", nil, opts.ctx.Err()
|
||||
}
|
||||
if hr == 0x800704c7 { // ERROR_CANCELLED
|
||||
return "", nil, nil
|
||||
return "", nil, ErrCanceled
|
||||
}
|
||||
if int32(hr) < 0 {
|
||||
return "", nil, syscall.Errno(hr)
|
||||
|
@ -335,11 +335,11 @@ func browseForFolder(opts options) (string, []string, error) {
|
|||
return "", nil, opts.ctx.Err()
|
||||
}
|
||||
if ptr == 0 {
|
||||
return "", nil, nil
|
||||
return "", nil, ErrCanceled
|
||||
}
|
||||
defer coTaskMemFree.Call(ptr)
|
||||
|
||||
res := [32768]uint16{}
|
||||
var res [32768]uint16
|
||||
shGetPathFromIDListEx.Call(ptr, uintptr(unsafe.Pointer(&res[0])), uintptr(len(res)), 0)
|
||||
|
||||
str := syscall.UTF16ToString(res[:])
|
||||
|
|
12
internal/zenutil/env.go
Normal file
12
internal/zenutil/env.go
Normal 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) }
|
|
@ -48,4 +48,25 @@ res.join({{json .Separator}})
|
|||
var app=Application.currentApplication()
|
||||
app.includeStandardAdditions=true
|
||||
void app.displayNotification({{json .Text}},{{json .Options}})
|
||||
{{- end}}
|
||||
{{define "progress" -}}
|
||||
var app=Application.currentApplication()
|
||||
app.includeStandardAdditions=true
|
||||
app.activate()
|
||||
ObjC.import('stdlib')
|
||||
ObjC.import('readline')
|
||||
{{- if .Total}}
|
||||
Progress.totalUnitCount={{.Total}}
|
||||
{{- end}}
|
||||
{{- if .Description}}
|
||||
Progress.description={{json .Description}}
|
||||
{{- end}}
|
||||
while(true){var s
|
||||
try{s=$.readline('')}catch(e){if(e.errorNumber===-128)$.exit(1)
|
||||
break}
|
||||
if(s.indexOf('#')===0){Progress.additionalDescription=s.slice(1)
|
||||
continue}
|
||||
var i=parseInt(s)
|
||||
if(i>=0&&Progress.totalUnitCount>0){Progress.completedUnitCount=i
|
||||
continue}}
|
||||
{{- end}}`))
|
||||
|
|
|
@ -4,7 +4,6 @@ package main
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
@ -17,7 +16,7 @@ import (
|
|||
func main() {
|
||||
dir := os.Args[1]
|
||||
|
||||
files, err := ioutil.ReadDir(dir)
|
||||
files, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
@ -26,12 +25,7 @@ func main() {
|
|||
|
||||
for _, file := range files {
|
||||
name := file.Name()
|
||||
|
||||
str.WriteString("\n" + `{{define "`)
|
||||
str.WriteString(strings.TrimSuffix(name, filepath.Ext(name)))
|
||||
str.WriteString(`" -}}` + "\n")
|
||||
|
||||
data, err := ioutil.ReadFile(filepath.Join(dir, name))
|
||||
data, err := os.ReadFile(filepath.Join(dir, name))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
@ -40,6 +34,9 @@ func main() {
|
|||
log.Fatal(err)
|
||||
}
|
||||
|
||||
str.WriteString("\n" + `{{define "`)
|
||||
str.WriteString(strings.TrimSuffix(name, filepath.Ext(name)))
|
||||
str.WriteString(`" -}}` + "\n")
|
||||
str.Write(data)
|
||||
str.WriteString("\n{{- end}}")
|
||||
}
|
||||
|
@ -108,5 +105,4 @@ import (
|
|||
var scripts = template.Must(template.New("").Funcs(template.FuncMap{"json": func(v interface{}) (string, error) {
|
||||
b, err := json.Marshal(v)
|
||||
return string(b), err
|
||||
}}).Parse(` + "`{{.}}`" + `))
|
||||
`))
|
||||
}}).Parse(` + "`{{.}}`))\n"))
|
||||
|
|
34
internal/zenutil/osascripts/progress.gojs
Normal file
34
internal/zenutil/osascripts/progress.gojs
Normal 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
|
||||
}
|
||||
}
|
116
internal/zenutil/progress_unix.go
Normal file
116
internal/zenutil/progress_unix.go
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,30 +1,29 @@
|
|||
package zenutil
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// Run is internal.
|
||||
func Run(ctx context.Context, script string, data interface{}) ([]byte, error) {
|
||||
var buf strings.Builder
|
||||
|
||||
var buf bytes.Buffer
|
||||
err := scripts.ExecuteTemplate(&buf, script, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
script = buf.String()
|
||||
if Command {
|
||||
// Try to use syscall.Exec, fallback to exec.Command.
|
||||
if path, err := exec.LookPath("osascript"); err != nil {
|
||||
} else if t, err := ioutil.TempFile("", ""); err != nil {
|
||||
} else if err := os.Remove(t.Name()); err != nil {
|
||||
} else if _, err := t.WriteString(script); err != nil {
|
||||
} else if _, err := t.Write(buf.Bytes()); err != nil {
|
||||
} else if _, err := t.Seek(0, 0); err != nil {
|
||||
} else if err := syscall.Dup2(int(t.Fd()), syscall.Stdin); err != nil {
|
||||
} else if err := os.Stderr.Close(); err != nil {
|
||||
|
@ -35,7 +34,7 @@ func Run(ctx context.Context, script string, data interface{}) ([]byte, error) {
|
|||
|
||||
if ctx != nil {
|
||||
cmd := exec.CommandContext(ctx, "osascript", "-l", "JavaScript")
|
||||
cmd.Stdin = strings.NewReader(script)
|
||||
cmd.Stdin = &buf
|
||||
out, err := cmd.Output()
|
||||
if ctx.Err() != nil {
|
||||
err = ctx.Err()
|
||||
|
@ -43,25 +42,86 @@ func Run(ctx context.Context, script string, data interface{}) ([]byte, error) {
|
|||
return out, err
|
||||
}
|
||||
cmd := exec.Command("osascript", "-l", "JavaScript")
|
||||
cmd.Stdin = strings.NewReader(script)
|
||||
cmd.Stdin = &buf
|
||||
return cmd.Output()
|
||||
}
|
||||
|
||||
// File is internal.
|
||||
type File struct {
|
||||
Operation string
|
||||
Separator string
|
||||
Options FileOptions
|
||||
}
|
||||
// RunProgress is internal.
|
||||
func RunProgress(ctx context.Context, max int, data Progress) (dlg *progressDialog, err error) {
|
||||
var buf bytes.Buffer
|
||||
err = scripts.ExecuteTemplate(&buf, "progress", data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// FileOptions is internal.
|
||||
type FileOptions struct {
|
||||
Prompt *string `json:"withPrompt,omitempty"`
|
||||
Type []string `json:"ofType,omitempty"`
|
||||
Name string `json:"defaultName,omitempty"`
|
||||
Location string `json:"defaultLocation,omitempty"`
|
||||
Multiple bool `json:"multipleSelectionsAllowed,omitempty"`
|
||||
Invisibles bool `json:"invisibles,omitempty"`
|
||||
t, err := ioutil.TempDir("", "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
if ctx != nil && ctx.Err() != nil {
|
||||
err = ctx.Err()
|
||||
}
|
||||
os.RemoveAll(t)
|
||||
}
|
||||
}()
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
|
||||
var cmd *exec.Cmd
|
||||
name := filepath.Join(t, "progress.app")
|
||||
|
||||
cmd = exec.CommandContext(ctx, "osacompile", "-l", "JavaScript", "-o", name)
|
||||
cmd.Stdin = &buf
|
||||
if err := cmd.Run(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
plist := filepath.Join(name, "Contents/Info.plist")
|
||||
|
||||
cmd = exec.CommandContext(ctx, "defaults", "write", plist, "LSUIElement", "true")
|
||||
if err := cmd.Run(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cmd = exec.CommandContext(ctx, "defaults", "write", plist, "CFBundleName", "")
|
||||
if err := cmd.Run(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var executable string
|
||||
cmd = exec.CommandContext(ctx, "defaults", "read", plist, "CFBundleExecutable")
|
||||
if out, err := cmd.Output(); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
out = bytes.TrimSuffix(out, []byte{'\n'})
|
||||
executable = filepath.Join(name, "Contents/MacOS", string(out))
|
||||
}
|
||||
|
||||
cmd = exec.CommandContext(ctx, executable)
|
||||
pipe, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := cmd.Start(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dlg = &progressDialog{
|
||||
ctx: ctx,
|
||||
cmd: cmd,
|
||||
max: max,
|
||||
lines: make(chan string),
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
go dlg.pipe(pipe)
|
||||
go func() {
|
||||
defer os.RemoveAll(t)
|
||||
dlg.wait(nil, nil)
|
||||
}()
|
||||
return dlg, nil
|
||||
}
|
||||
|
||||
// Dialog is internal.
|
||||
|
@ -86,6 +146,25 @@ type DialogOptions struct {
|
|||
Timeout int `json:"givingUpAfter,omitempty"`
|
||||
}
|
||||
|
||||
// DialogButtons is internal.
|
||||
type DialogButtons struct {
|
||||
Buttons []string
|
||||
Default int
|
||||
Cancel int
|
||||
Extra int
|
||||
}
|
||||
|
||||
// SetButtons is internal.
|
||||
func (d *Dialog) SetButtons(btns DialogButtons) {
|
||||
d.Options.Buttons = btns.Buttons
|
||||
d.Options.Default = btns.Default
|
||||
d.Options.Cancel = btns.Cancel
|
||||
if btns.Extra > 0 {
|
||||
name := btns.Buttons[btns.Extra-1]
|
||||
d.Extra = &name
|
||||
}
|
||||
}
|
||||
|
||||
// List is internal.
|
||||
type List struct {
|
||||
Items []string
|
||||
|
@ -104,6 +183,22 @@ type ListOptions struct {
|
|||
Empty bool `json:"emptySelectionAllowed,omitempty"`
|
||||
}
|
||||
|
||||
// File is internal.
|
||||
type File struct {
|
||||
Operation string
|
||||
Separator string
|
||||
Options FileOptions
|
||||
}
|
||||
|
||||
type FileOptions struct {
|
||||
Prompt *string `json:"withPrompt,omitempty"`
|
||||
Type []string `json:"ofType,omitempty"`
|
||||
Name string `json:"defaultName,omitempty"`
|
||||
Location string `json:"defaultLocation,omitempty"`
|
||||
Multiple bool `json:"multipleSelectionsAllowed,omitempty"`
|
||||
Invisibles bool `json:"invisibles,omitempty"`
|
||||
}
|
||||
|
||||
// Notify is internal.
|
||||
type Notify struct {
|
||||
Text string
|
||||
|
@ -116,19 +211,8 @@ type NotifyOptions struct {
|
|||
Subtitle string `json:"subtitle,omitempty"`
|
||||
}
|
||||
|
||||
type Buttons struct {
|
||||
Buttons []string
|
||||
Default int
|
||||
Cancel int
|
||||
Extra int
|
||||
}
|
||||
|
||||
func (d *Dialog) SetButtons(btns Buttons) {
|
||||
d.Options.Buttons = btns.Buttons
|
||||
d.Options.Default = btns.Default
|
||||
d.Options.Cancel = btns.Cancel
|
||||
if btns.Extra > 0 {
|
||||
name := btns.Buttons[btns.Extra-1]
|
||||
d.Extra = &name
|
||||
}
|
||||
// Progress is internal.
|
||||
type Progress struct {
|
||||
Description *string
|
||||
Total *int
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
package zenutil
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
@ -40,3 +41,42 @@ func Run(ctx context.Context, args []string) ([]byte, error) {
|
|||
}
|
||||
return exec.Command(tool, args...).Output()
|
||||
}
|
||||
|
||||
// RunProgress is internal.
|
||||
func RunProgress(ctx context.Context, max int, extra *string, args []string) (*progressDialog, error) {
|
||||
if Command && path != "" {
|
||||
if Timeout > 0 {
|
||||
args = append(args, "--timeout", strconv.Itoa(Timeout))
|
||||
}
|
||||
syscall.Exec(path, append([]string{tool}, args...), os.Environ())
|
||||
}
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, tool, args...)
|
||||
pipe, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var out *bytes.Buffer
|
||||
if extra != nil {
|
||||
out = &bytes.Buffer{}
|
||||
cmd.Stdout = out
|
||||
}
|
||||
if err := cmd.Start(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dlg := &progressDialog{
|
||||
ctx: ctx,
|
||||
cmd: cmd,
|
||||
max: max,
|
||||
percent: true,
|
||||
lines: make(chan string),
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
go dlg.pipe(pipe)
|
||||
go dlg.wait(extra, out)
|
||||
return dlg, nil
|
||||
}
|
||||
|
|
12
list.go
12
list.go
|
@ -2,25 +2,19 @@ package zenity
|
|||
|
||||
// List displays the list dialog.
|
||||
//
|
||||
// Returns false on cancel, or ErrExtraButton.
|
||||
//
|
||||
// Valid options: Title, Width, Height, OKLabel, CancelLabel, ExtraButton,
|
||||
// Icon, DefaultItems, DisallowEmpty.
|
||||
func List(text string, items []string, options ...Option) (string, bool, error) {
|
||||
func List(text string, items []string, options ...Option) (string, error) {
|
||||
return list(text, items, applyOptions(options))
|
||||
}
|
||||
|
||||
// ListItems displays the list dialog.
|
||||
//
|
||||
// Returns false on cancel, or ErrExtraButton.
|
||||
func ListItems(text string, items ...string) (string, bool, error) {
|
||||
func ListItems(text string, items ...string) (string, error) {
|
||||
return List(text, items)
|
||||
}
|
||||
|
||||
// ListMultiple displays the list dialog, allowing multiple items to be selected.
|
||||
//
|
||||
// Returns a nil slice on cancel, or ErrExtraButton.
|
||||
//
|
||||
// Valid options: Title, Width, Height, OKLabel, CancelLabel, ExtraButton,
|
||||
// Icon, DefaultItems, DisallowEmpty.
|
||||
func ListMultiple(text string, items []string, options ...Option) ([]string, error) {
|
||||
|
@ -28,8 +22,6 @@ func ListMultiple(text string, items []string, options ...Option) ([]string, err
|
|||
}
|
||||
|
||||
// ListMultipleItems displays the list dialog, allowing multiple items to be selected.
|
||||
//
|
||||
// Returns a nil slice on cancel, or ErrExtraButton.
|
||||
func ListMultipleItems(text string, items ...string) ([]string, error) {
|
||||
return ListMultiple(text, items)
|
||||
}
|
||||
|
|
|
@ -4,7 +4,11 @@ import (
|
|||
"github.com/ncruces/zenity/internal/zenutil"
|
||||
)
|
||||
|
||||
func list(text string, items []string, opts options) (string, bool, error) {
|
||||
func list(text string, items []string, opts options) (string, error) {
|
||||
if opts.extraButton != nil {
|
||||
return "", ErrUnsupported
|
||||
}
|
||||
|
||||
var data zenutil.List
|
||||
data.Items = items
|
||||
data.Options.Prompt = &text
|
||||
|
@ -19,6 +23,10 @@ func list(text string, items []string, opts options) (string, bool, error) {
|
|||
}
|
||||
|
||||
func listMultiple(text string, items []string, opts options) ([]string, error) {
|
||||
if opts.extraButton != nil {
|
||||
return nil, ErrUnsupported
|
||||
}
|
||||
|
||||
var data zenutil.List
|
||||
data.Items = items
|
||||
data.Options.Prompt = &text
|
||||
|
|
14
list_test.go
14
list_test.go
|
@ -8,6 +8,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/ncruces/zenity"
|
||||
"go.uber.org/goleak"
|
||||
)
|
||||
|
||||
func ExampleList() {
|
||||
|
@ -44,22 +45,23 @@ func ExampleListMultipleItems() {
|
|||
// Output:
|
||||
}
|
||||
|
||||
func TestListTimeout(t *testing.T) {
|
||||
func TestList_timeout(t *testing.T) {
|
||||
defer goleak.VerifyNone(t)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second/10)
|
||||
defer cancel()
|
||||
|
||||
_, _, err := zenity.List("", nil, zenity.Context(ctx))
|
||||
_, err := zenity.List("", nil, zenity.Context(ctx))
|
||||
if !os.IsTimeout(err) {
|
||||
t.Error("did not timeout:", err)
|
||||
}
|
||||
|
||||
cancel()
|
||||
}
|
||||
|
||||
func TestListCancel(t *testing.T) {
|
||||
func TestList_cancel(t *testing.T) {
|
||||
defer goleak.VerifyNone(t)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
_, _, err := zenity.List("", nil, zenity.Context(ctx))
|
||||
_, err := zenity.List("", nil, zenity.Context(ctx))
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
t.Error("was not canceled:", err)
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ import (
|
|||
"github.com/ncruces/zenity/internal/zenutil"
|
||||
)
|
||||
|
||||
func list(text string, items []string, opts options) (string, bool, error) {
|
||||
func list(text string, items []string, opts options) (string, error) {
|
||||
args := []string{"--list", "--column=", "--hide-header", "--text", text}
|
||||
args = appendTitle(args, opts)
|
||||
args = appendButtons(args, opts)
|
||||
|
|
|
@ -5,28 +5,21 @@ import (
|
|||
"unsafe"
|
||||
)
|
||||
|
||||
func list(text string, items []string, opts options) (string, bool, error) {
|
||||
var title string
|
||||
if opts.title != nil {
|
||||
title = *opts.title
|
||||
}
|
||||
if opts.okLabel == nil {
|
||||
opts.okLabel = stringPtr("OK")
|
||||
}
|
||||
if opts.cancelLabel == nil {
|
||||
opts.cancelLabel = stringPtr("Cancel")
|
||||
}
|
||||
items, err := listDlg(title, text, items, false, opts)
|
||||
func list(text string, items []string, opts options) (string, error) {
|
||||
items, err := listDlg(text, items, false, opts)
|
||||
if len(items) == 1 {
|
||||
return items[0], true, err
|
||||
return items[0], err
|
||||
}
|
||||
return "", false, err
|
||||
return "", err
|
||||
}
|
||||
|
||||
func listMultiple(text string, items []string, opts options) ([]string, error) {
|
||||
var title string
|
||||
if opts.title != nil {
|
||||
title = *opts.title
|
||||
return listDlg(text, items, true, opts)
|
||||
}
|
||||
|
||||
func listDlg(text string, items []string, multiple bool, opts options) (out []string, err error) {
|
||||
if opts.title == nil {
|
||||
opts.title = stringPtr("")
|
||||
}
|
||||
if opts.okLabel == nil {
|
||||
opts.okLabel = stringPtr("OK")
|
||||
|
@ -34,10 +27,7 @@ func listMultiple(text string, items []string, opts options) ([]string, error) {
|
|||
if opts.cancelLabel == nil {
|
||||
opts.cancelLabel = stringPtr("Cancel")
|
||||
}
|
||||
return listDlg(title, text, items, true, opts)
|
||||
}
|
||||
|
||||
func listDlg(title, text string, items []string, multiple bool, opts options) (out []string, err error) {
|
||||
defer setup()()
|
||||
font := getFont()
|
||||
defer font.Delete()
|
||||
|
@ -52,6 +42,7 @@ func listDlg(title, text string, items []string, multiple bool, opts options) (o
|
|||
sendMessage.Call(listCtl, 0x0030 /* WM_SETFONT */, hfont, 1)
|
||||
sendMessage.Call(okBtn, 0x0030 /* WM_SETFONT */, hfont, 1)
|
||||
sendMessage.Call(cancelBtn, 0x0030 /* WM_SETFONT */, hfont, 1)
|
||||
sendMessage.Call(extraBtn, 0x0030 /* WM_SETFONT */, hfont, 1)
|
||||
setWindowPos.Call(wnd, 0, 0, 0, dpi.Scale(281), dpi.Scale(281), 0x6) // SWP_NOZORDER|SWP_NOMOVE
|
||||
setWindowPos.Call(textCtl, 0, dpi.Scale(12), dpi.Scale(10), dpi.Scale(241), dpi.Scale(16), 0x4) // SWP_NOZORDER
|
||||
setWindowPos.Call(listCtl, 0, dpi.Scale(12), dpi.Scale(30), dpi.Scale(241), dpi.Scale(164), 0x4) // SWP_NOZORDER
|
||||
|
@ -59,7 +50,6 @@ func listDlg(title, text string, items []string, multiple bool, opts options) (o
|
|||
setWindowPos.Call(okBtn, 0, dpi.Scale(95), dpi.Scale(206), dpi.Scale(75), dpi.Scale(24), 0x4) // SWP_NOZORDER
|
||||
setWindowPos.Call(cancelBtn, 0, dpi.Scale(178), dpi.Scale(206), dpi.Scale(75), dpi.Scale(24), 0x4) // SWP_NOZORDER
|
||||
} else {
|
||||
sendMessage.Call(extraBtn, 0x0030 /* WM_SETFONT */, hfont, 1)
|
||||
setWindowPos.Call(okBtn, 0, dpi.Scale(12), dpi.Scale(206), dpi.Scale(75), dpi.Scale(24), 0x4) // SWP_NOZORDER
|
||||
setWindowPos.Call(extraBtn, 0, dpi.Scale(95), dpi.Scale(206), dpi.Scale(75), dpi.Scale(24), 0x4) // SWP_NOZORDER
|
||||
setWindowPos.Call(cancelBtn, 0, dpi.Scale(178), dpi.Scale(206), dpi.Scale(75), dpi.Scale(24), 0x4) // SWP_NOZORDER
|
||||
|
@ -72,6 +62,7 @@ func listDlg(title, text string, items []string, multiple bool, opts options) (o
|
|||
postQuitMessage.Call(0)
|
||||
|
||||
case 0x0010: // WM_CLOSE
|
||||
err = ErrCanceled
|
||||
destroyWindow.Call(wnd)
|
||||
|
||||
case 0x0111: // WM_COMMAND
|
||||
|
@ -98,6 +89,7 @@ func listDlg(title, text string, items []string, multiple bool, opts options) (o
|
|||
}
|
||||
}
|
||||
case 2: // IDCANCEL
|
||||
err = ErrCanceled
|
||||
case 7: // IDNO
|
||||
err = ErrExtraButton
|
||||
}
|
||||
|
@ -107,8 +99,8 @@ func listDlg(title, text string, items []string, multiple bool, opts options) (o
|
|||
layout(dpi(uint32(wparam) >> 16))
|
||||
|
||||
default:
|
||||
ret, _, _ := syscall.Syscall6(defWindowProc, 4, wnd, uintptr(msg), wparam, lparam, 0, 0)
|
||||
return ret
|
||||
res, _, _ := syscall.Syscall6(defWindowProc, 4, wnd, uintptr(msg), wparam, lparam, 0, 0)
|
||||
return res
|
||||
}
|
||||
|
||||
return 0
|
||||
|
@ -130,16 +122,16 @@ func listDlg(title, text string, items []string, multiple bool, opts options) (o
|
|||
defer unregisterClass.Call(cls, instance)
|
||||
|
||||
wnd, _, _ = createWindowEx.Call(0x10101, // WS_EX_CONTROLPARENT|WS_EX_WINDOWEDGE|WS_EX_DLGMODALFRAME
|
||||
cls, strptr(title),
|
||||
cls, strptr(*opts.title),
|
||||
0x84c80000, // WS_POPUPWINDOW|WS_CLIPSIBLINGS|WS_DLGFRAME
|
||||
0x80000000, // CW_USEDEFAULT
|
||||
0x80000000, // CW_USEDEFAULT
|
||||
281, 281, 0, 0, instance)
|
||||
281, 281, 0, 0, instance, 0)
|
||||
|
||||
textCtl, _, _ = createWindowEx.Call(0,
|
||||
strptr("STATIC"), strptr(text),
|
||||
0x5002e080, // WS_CHILD|WS_VISIBLE|WS_GROUP|SS_WORDELLIPSIS|SS_EDITCONTROL|SS_NOPREFIX
|
||||
12, 10, 241, 16, wnd, 0, instance)
|
||||
12, 10, 241, 16, wnd, 0, instance, 0)
|
||||
|
||||
var flags uintptr = 0x50320000 // WS_CHILD|WS_VISIBLE|WS_VSCROLL|WS_GROUP|WS_TABSTOP
|
||||
if multiple {
|
||||
|
@ -148,21 +140,21 @@ func listDlg(title, text string, items []string, multiple bool, opts options) (o
|
|||
listCtl, _, _ = createWindowEx.Call(0x200, // WS_EX_CLIENTEDGE
|
||||
strptr("LISTBOX"), strptr(opts.entryText),
|
||||
flags,
|
||||
12, 30, 241, 164, wnd, 0, instance)
|
||||
12, 30, 241, 164, wnd, 0, instance, 0)
|
||||
|
||||
okBtn, _, _ = createWindowEx.Call(0,
|
||||
strptr("BUTTON"), strptr(*opts.okLabel),
|
||||
0x50030001, // WS_CHILD|WS_VISIBLE|WS_GROUP|WS_TABSTOP|BS_DEFPUSHBUTTON
|
||||
12, 206, 75, 24, wnd, 1 /* IDOK */, instance)
|
||||
12, 206, 75, 24, wnd, 1 /* IDOK */, instance, 0)
|
||||
cancelBtn, _, _ = createWindowEx.Call(0,
|
||||
strptr("BUTTON"), strptr(*opts.cancelLabel),
|
||||
0x50010000, // WS_CHILD|WS_VISIBLE|WS_GROUP|WS_TABSTOP
|
||||
12, 206, 75, 24, wnd, 2 /* IDCANCEL */, instance)
|
||||
12, 206, 75, 24, wnd, 2 /* IDCANCEL */, instance, 0)
|
||||
if opts.extraButton != nil {
|
||||
extraBtn, _, _ = createWindowEx.Call(0,
|
||||
strptr("BUTTON"), strptr(*opts.extraButton),
|
||||
0x50010000, // WS_CHILD|WS_VISIBLE|WS_GROUP|WS_TABSTOP
|
||||
12, 206, 75, 24, wnd, 7 /* IDNO */, instance)
|
||||
12, 206, 75, 24, wnd, 7 /* IDNO */, instance, 0)
|
||||
}
|
||||
|
||||
for _, item := range items {
|
||||
|
|
20
msg.go
20
msg.go
|
@ -1,46 +1,34 @@
|
|||
package zenity
|
||||
|
||||
// ErrExtraButton is returned by dialog functions when the extra button is
|
||||
// pressed.
|
||||
const ErrExtraButton = stringErr("extra button pressed")
|
||||
|
||||
// Question displays the question dialog.
|
||||
//
|
||||
// Returns true on OK, false on Cancel, or ErrExtraButton.
|
||||
//
|
||||
// Valid options: Title, Width, Height, OKLabel, CancelLabel, ExtraButton,
|
||||
// Icon, NoWrap, Ellipsize, DefaultCancel.
|
||||
func Question(text string, options ...Option) (bool, error) {
|
||||
func Question(text string, options ...Option) error {
|
||||
return message(questionKind, text, applyOptions(options))
|
||||
}
|
||||
|
||||
// Info displays the info dialog.
|
||||
//
|
||||
// Returns true on OK, false on dismiss, or ErrExtraButton.
|
||||
//
|
||||
// Valid options: Title, Width, Height, OKLabel, ExtraButton, Icon,
|
||||
// NoWrap, Ellipsize.
|
||||
func Info(text string, options ...Option) (bool, error) {
|
||||
func Info(text string, options ...Option) error {
|
||||
return message(infoKind, text, applyOptions(options))
|
||||
}
|
||||
|
||||
// Warning displays the warning dialog.
|
||||
//
|
||||
// Returns true on OK, false on dismiss, or ErrExtraButton.
|
||||
//
|
||||
// Valid options: Title, Width, Height, OKLabel, ExtraButton, Icon,
|
||||
// NoWrap, Ellipsize.
|
||||
func Warning(text string, options ...Option) (bool, error) {
|
||||
func Warning(text string, options ...Option) error {
|
||||
return message(warningKind, text, applyOptions(options))
|
||||
}
|
||||
|
||||
// Error displays the error dialog.
|
||||
//
|
||||
// Returns true on OK, false on dismiss, or ErrExtraButton.
|
||||
//
|
||||
// Valid options: Title, Width, Height, OKLabel, ExtraButton, Icon,
|
||||
// NoWrap, Ellipsize.
|
||||
func Error(text string, options ...Option) (bool, error) {
|
||||
func Error(text string, options ...Option) error {
|
||||
return message(errorKind, text, applyOptions(options))
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import (
|
|||
"github.com/ncruces/zenity/internal/zenutil"
|
||||
)
|
||||
|
||||
func message(kind messageKind, text string, opts options) (bool, error) {
|
||||
func message(kind messageKind, text string, opts options) error {
|
||||
var data zenutil.Dialog
|
||||
data.Text = text
|
||||
data.Options.Timeout = zenutil.Timeout
|
||||
|
@ -33,6 +33,6 @@ func message(kind messageKind, text string, opts options) (bool, error) {
|
|||
data.SetButtons(getButtons(dialog, kind == questionKind, opts))
|
||||
|
||||
out, err := zenutil.Run(opts.ctx, "dialog", data)
|
||||
_, ok, err := strResult(opts, out, err)
|
||||
return ok, err
|
||||
_, err = strResult(opts, out, err)
|
||||
return err
|
||||
}
|
||||
|
|
13
msg_test.go
13
msg_test.go
|
@ -8,6 +8,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/ncruces/zenity"
|
||||
"go.uber.org/goleak"
|
||||
)
|
||||
|
||||
func ExampleError() {
|
||||
|
@ -38,32 +39,34 @@ func ExampleQuestion() {
|
|||
// Output:
|
||||
}
|
||||
|
||||
var msgFuncs = []func(string, ...zenity.Option) (bool, error){
|
||||
var msgFuncs = []func(string, ...zenity.Option) error{
|
||||
zenity.Error,
|
||||
zenity.Info,
|
||||
zenity.Warning,
|
||||
zenity.Question,
|
||||
}
|
||||
|
||||
func TestMessageTimeout(t *testing.T) {
|
||||
func TestMessage_timeout(t *testing.T) {
|
||||
for _, f := range msgFuncs {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second/10)
|
||||
|
||||
_, err := f("text", zenity.Context(ctx))
|
||||
err := f("text", zenity.Context(ctx))
|
||||
if !os.IsTimeout(err) {
|
||||
t.Error("did not timeout:", err)
|
||||
}
|
||||
|
||||
cancel()
|
||||
goleak.VerifyNone(t)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageCancel(t *testing.T) {
|
||||
func TestMessage_cancel(t *testing.T) {
|
||||
defer goleak.VerifyNone(t)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
for _, f := range msgFuncs {
|
||||
_, err := f("text", zenity.Context(ctx))
|
||||
err := f("text", zenity.Context(ctx))
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
t.Error("was not canceled:", err)
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ import (
|
|||
"github.com/ncruces/zenity/internal/zenutil"
|
||||
)
|
||||
|
||||
func message(kind messageKind, text string, opts options) (bool, error) {
|
||||
func message(kind messageKind, text string, opts options) error {
|
||||
args := []string{"--text", text, "--no-markup"}
|
||||
switch kind {
|
||||
case questionKind:
|
||||
|
@ -47,6 +47,6 @@ func message(kind messageKind, text string, opts options) (bool, error) {
|
|||
}
|
||||
|
||||
out, err := zenutil.Run(opts.ctx, args)
|
||||
_, ok, err := strResult(opts, out, err)
|
||||
return ok, err
|
||||
_, err = strResult(opts, out, err)
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ var (
|
|||
getDlgCtrlID = user32.NewProc("GetDlgCtrlID")
|
||||
)
|
||||
|
||||
func message(kind messageKind, text string, opts options) (bool, error) {
|
||||
func message(kind messageKind, text string, opts options) error {
|
||||
var flags uintptr
|
||||
|
||||
switch {
|
||||
|
@ -48,7 +48,7 @@ func message(kind messageKind, text string, opts options) (bool, error) {
|
|||
if opts.ctx != nil || opts.okLabel != nil || opts.cancelLabel != nil || opts.extraButton != nil {
|
||||
unhook, err := hookMessageLabels(kind, opts)
|
||||
if err != nil {
|
||||
return false, err
|
||||
return err
|
||||
}
|
||||
defer unhook()
|
||||
}
|
||||
|
@ -62,17 +62,17 @@ func message(kind messageKind, text string, opts options) (bool, error) {
|
|||
s, _, err := messageBox.Call(0, strptr(text), title, flags)
|
||||
|
||||
if opts.ctx != nil && opts.ctx.Err() != nil {
|
||||
return false, opts.ctx.Err()
|
||||
return opts.ctx.Err()
|
||||
}
|
||||
switch s {
|
||||
case 1, 6: // IDOK, IDYES
|
||||
return true, nil
|
||||
return nil
|
||||
case 2: // IDCANCEL
|
||||
return false, nil
|
||||
return ErrCanceled
|
||||
case 7: // IDNO
|
||||
return false, ErrExtraButton
|
||||
return ErrExtraButton
|
||||
default:
|
||||
return false, err
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -80,7 +80,7 @@ func hookMessageLabels(kind messageKind, opts options) (unhook context.CancelFun
|
|||
return hookDialog(opts.ctx, func(wnd uintptr) {
|
||||
enumChildWindows.Call(wnd,
|
||||
syscall.NewCallback(func(wnd, lparam uintptr) uintptr {
|
||||
name := [8]uint16{}
|
||||
var name [8]uint16
|
||||
getClassName.Call(wnd, uintptr(unsafe.Pointer(&name)), uintptr(len(name)))
|
||||
if syscall.UTF16ToString(name[:]) == "Button" {
|
||||
ctl, _, _ := getDlgCtrlID.Call(wnd)
|
||||
|
@ -89,11 +89,7 @@ func hookMessageLabels(kind messageKind, opts options) (unhook context.CancelFun
|
|||
case 1, 6: // IDOK, IDYES
|
||||
text = opts.okLabel
|
||||
case 2: // IDCANCEL
|
||||
if kind == questionKind {
|
||||
text = opts.cancelLabel
|
||||
} else {
|
||||
text = opts.okLabel
|
||||
}
|
||||
text = opts.cancelLabel
|
||||
case 7: // IDNO
|
||||
text = opts.extraButton
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/ncruces/zenity"
|
||||
"go.uber.org/goleak"
|
||||
)
|
||||
|
||||
func ExampleNotify() {
|
||||
|
@ -15,7 +16,8 @@ func ExampleNotify() {
|
|||
// Output:
|
||||
}
|
||||
|
||||
func TestNotifyCancel(t *testing.T) {
|
||||
func TestNotify_cancel(t *testing.T) {
|
||||
defer goleak.VerifyNone(t)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
|
|
51
progress.go
Normal file
51
progress.go
Normal 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
22
progress_darwin.go
Normal 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
100
progress_test.go
Normal 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
28
progress_unix.go
Normal 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
261
progress_windows.go
Normal 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
4
pwd.go
|
@ -2,10 +2,8 @@ package zenity
|
|||
|
||||
// Password displays the password dialog.
|
||||
//
|
||||
// Returns false on cancel, or ErrExtraButton.
|
||||
//
|
||||
// Valid options: Title, OKLabel, CancelLabel, ExtraButton, Icon, Username.
|
||||
func Password(options ...Option) (usr string, pw string, ok bool, err error) {
|
||||
func Password(options ...Option) (usr string, pw string, err error) {
|
||||
return password(applyOptions(options))
|
||||
}
|
||||
|
||||
|
|
|
@ -2,8 +2,11 @@
|
|||
|
||||
package zenity
|
||||
|
||||
func password(opts options) (string, string, bool, error) {
|
||||
func password(opts options) (string, string, error) {
|
||||
if opts.username {
|
||||
return "", "", ErrUnsupported
|
||||
}
|
||||
opts.hideText = true
|
||||
str, ok, err := entry("Password:", opts)
|
||||
return "", str, ok, err
|
||||
str, err := entry("Password:", opts)
|
||||
return "", str, err
|
||||
}
|
||||
|
|
14
pwd_test.go
14
pwd_test.go
|
@ -8,6 +8,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/ncruces/zenity"
|
||||
"go.uber.org/goleak"
|
||||
)
|
||||
|
||||
func ExamplePassword() {
|
||||
|
@ -15,22 +16,23 @@ func ExamplePassword() {
|
|||
// Output:
|
||||
}
|
||||
|
||||
func TestPasswordTimeout(t *testing.T) {
|
||||
func TestPassword_timeout(t *testing.T) {
|
||||
defer goleak.VerifyNone(t)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second/10)
|
||||
defer cancel()
|
||||
|
||||
_, _, _, err := zenity.Password(zenity.Context(ctx))
|
||||
_, _, err := zenity.Password(zenity.Context(ctx))
|
||||
if !os.IsTimeout(err) {
|
||||
t.Error("did not timeout:", err)
|
||||
}
|
||||
|
||||
cancel()
|
||||
}
|
||||
|
||||
func TestPasswordCancel(t *testing.T) {
|
||||
func TestPassword_cancel(t *testing.T) {
|
||||
defer goleak.VerifyNone(t)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
_, _, _, err := zenity.Password(zenity.Context(ctx))
|
||||
_, _, err := zenity.Password(zenity.Context(ctx))
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
t.Error("was not canceled:", err)
|
||||
}
|
||||
|
|
10
pwd_unix.go
10
pwd_unix.go
|
@ -8,7 +8,7 @@ import (
|
|||
"github.com/ncruces/zenity/internal/zenutil"
|
||||
)
|
||||
|
||||
func password(opts options) (string, string, bool, error) {
|
||||
func password(opts options) (string, string, error) {
|
||||
args := []string{"--password"}
|
||||
args = appendTitle(args, opts)
|
||||
args = appendButtons(args, opts)
|
||||
|
@ -17,11 +17,11 @@ func password(opts options) (string, string, bool, error) {
|
|||
}
|
||||
|
||||
out, err := zenutil.Run(opts.ctx, args)
|
||||
str, ok, err := strResult(opts, out, err)
|
||||
if ok && opts.username {
|
||||
str, err := strResult(opts, out, err)
|
||||
if err == nil && opts.username {
|
||||
if split := strings.SplitN(string(out), "|", 2); len(split) == 2 {
|
||||
return split[0], split[1], true, nil
|
||||
return split[0], split[1], nil
|
||||
}
|
||||
}
|
||||
return "", str, ok, err
|
||||
return "", str, err
|
||||
}
|
||||
|
|
|
@ -2,13 +2,13 @@ package zenity
|
|||
|
||||
import "github.com/ncruces/zenity/internal/zenutil"
|
||||
|
||||
func getButtons(dialog, okcancel bool, opts options) (btns zenutil.Buttons) {
|
||||
func getButtons(dialog, okcancel bool, opts options) (btns zenutil.DialogButtons) {
|
||||
if !okcancel {
|
||||
opts.cancelLabel = nil
|
||||
opts.defaultCancel = false
|
||||
}
|
||||
|
||||
if opts.okLabel != nil || opts.cancelLabel != nil || opts.extraButton != nil || (dialog != okcancel) {
|
||||
if opts.okLabel != nil || opts.cancelLabel != nil || opts.extraButton != nil || dialog != okcancel {
|
||||
if opts.okLabel == nil {
|
||||
opts.okLabel = stringPtr("OK")
|
||||
}
|
||||
|
|
18
util_unix.go
18
util_unix.go
|
@ -3,7 +3,6 @@
|
|||
package zenity
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
@ -55,23 +54,22 @@ func appendIcon(args []string, opts options) []string {
|
|||
return args
|
||||
}
|
||||
|
||||
func strResult(opts options, out []byte, err error) (string, bool, error) {
|
||||
out = bytes.TrimSuffix(out, []byte{'\n'})
|
||||
func strResult(opts options, out []byte, err error) (string, error) {
|
||||
if err, ok := err.(*exec.ExitError); ok && err.ExitCode() == 1 {
|
||||
if opts.extraButton != nil && *opts.extraButton == string(out) {
|
||||
return "", false, ErrExtraButton
|
||||
if opts.extraButton != nil && *opts.extraButton+"\n" == string(out) {
|
||||
return "", ErrExtraButton
|
||||
}
|
||||
return "", false, nil
|
||||
return "", ErrCanceled
|
||||
}
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
return "", err
|
||||
}
|
||||
return string(out), true, nil
|
||||
return string(out), nil
|
||||
}
|
||||
|
||||
func lstResult(opts options, out []byte, err error) ([]string, error) {
|
||||
str, ok, err := strResult(opts, out, err)
|
||||
if ok {
|
||||
str, err := strResult(opts, out, err)
|
||||
if err == nil {
|
||||
return strings.Split(str, zenutil.Separator), nil
|
||||
}
|
||||
return nil, err
|
||||
|
|
|
@ -13,6 +13,7 @@ import (
|
|||
)
|
||||
|
||||
var (
|
||||
comctl32 = syscall.NewLazyDLL("comctl32.dll")
|
||||
comdlg32 = syscall.NewLazyDLL("comdlg32.dll")
|
||||
gdi32 = syscall.NewLazyDLL("gdi32.dll")
|
||||
kernel32 = syscall.NewLazyDLL("kernel32.dll")
|
||||
|
@ -21,6 +22,7 @@ var (
|
|||
user32 = syscall.NewLazyDLL("user32.dll")
|
||||
wtsapi32 = syscall.NewLazyDLL("wtsapi32.dll")
|
||||
|
||||
initCommonControlsEx = comctl32.NewProc("InitCommonControlsEx")
|
||||
commDlgExtendedError = comdlg32.NewProc("CommDlgExtendedError")
|
||||
|
||||
deleteObject = gdi32.NewProc("DeleteObject")
|
||||
|
@ -30,6 +32,10 @@ var (
|
|||
getModuleHandle = kernel32.NewProc("GetModuleHandleW")
|
||||
getCurrentThreadId = kernel32.NewProc("GetCurrentThreadId")
|
||||
getConsoleWindow = kernel32.NewProc("GetConsoleWindow")
|
||||
getSystemDirectory = kernel32.NewProc("GetSystemDirectoryW")
|
||||
createActCtx = kernel32.NewProc("CreateActCtxW")
|
||||
activateActCtx = kernel32.NewProc("ActivateActCtx")
|
||||
deactivateActCtx = kernel32.NewProc("DeactivateActCtx")
|
||||
|
||||
coInitializeEx = ole32.NewProc("CoInitializeEx")
|
||||
coUninitialize = ole32.NewProc("CoUninitialize")
|
||||
|
@ -60,12 +66,14 @@ var (
|
|||
systemParametersInfo = user32.NewProc("SystemParametersInfoW")
|
||||
setWindowPos = user32.NewProc("SetWindowPos")
|
||||
getWindowRect = user32.NewProc("GetWindowRect")
|
||||
setWindowLong = user32.NewProc("SetWindowLongPtrW")
|
||||
getSystemMetrics = user32.NewProc("GetSystemMetrics")
|
||||
unregisterClass = user32.NewProc("UnregisterClassW")
|
||||
registerClassEx = user32.NewProc("RegisterClassExW")
|
||||
destroyWindow = user32.NewProc("DestroyWindow")
|
||||
createWindowEx = user32.NewProc("CreateWindowExW")
|
||||
showWindow = user32.NewProc("ShowWindow")
|
||||
enableWindow = user32.NewProc("EnableWindow")
|
||||
setFocus = user32.NewProc("SetFocus")
|
||||
defWindowProc = user32.NewProc("DefWindowProcW")
|
||||
)
|
||||
|
@ -96,24 +104,33 @@ func setup() context.CancelFunc {
|
|||
setForegroundWindow.Call(hwnd)
|
||||
}
|
||||
|
||||
var old uintptr
|
||||
runtime.LockOSThread()
|
||||
|
||||
var restore uintptr
|
||||
cookie := enableVisualStyles()
|
||||
if setThreadDpiAwarenessContext.Find() == nil {
|
||||
// try:
|
||||
// DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2
|
||||
// DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE
|
||||
// DPI_AWARENESS_CONTEXT_SYSTEM_AWARE
|
||||
for i := -4; i <= -2; i++ {
|
||||
restore, _, _ := setThreadDpiAwarenessContext.Call(uintptr(i))
|
||||
restore, _, _ = setThreadDpiAwarenessContext.Call(uintptr(i))
|
||||
if restore != 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var icc _INITCOMMONCONTROLSEX
|
||||
icc.Size = uint32(unsafe.Sizeof(icc))
|
||||
icc.ICC = 0x00004020 // ICC_STANDARD_CLASSES|ICC_PROGRESS_CLASS
|
||||
|
||||
return func() {
|
||||
if old != 0 {
|
||||
setThreadDpiAwarenessContext.Call(old)
|
||||
if restore != 0 {
|
||||
setThreadDpiAwarenessContext.Call(restore)
|
||||
}
|
||||
if cookie != 0 {
|
||||
deactivateActCtx.Call(cookie)
|
||||
}
|
||||
runtime.UnlockOSThread()
|
||||
}
|
||||
|
@ -122,7 +139,7 @@ func setup() context.CancelFunc {
|
|||
func commDlgError() error {
|
||||
s, _, _ := commDlgExtendedError.Call()
|
||||
if s == 0 {
|
||||
return nil
|
||||
return ErrCanceled
|
||||
} else {
|
||||
return fmt.Errorf("Common Dialog error: %x", s)
|
||||
}
|
||||
|
@ -139,7 +156,7 @@ func hookDialog(ctx context.Context, initDialog func(wnd uintptr)) (unhook conte
|
|||
hook, _, err = setWindowsHookEx.Call(12, // WH_CALLWNDPROCRET
|
||||
syscall.NewCallback(func(code int32, wparam uintptr, lparam *_CWPRETSTRUCT) uintptr {
|
||||
if lparam.Message == 0x0110 { // WM_INITDIALOG
|
||||
name := [8]uint16{}
|
||||
var name [8]uint16
|
||||
getClassName.Call(lparam.Wnd, uintptr(unsafe.Pointer(&name)), uintptr(len(name)))
|
||||
if syscall.UTF16ToString(name[:]) == "#32770" { // The class for a dialog box
|
||||
var close bool
|
||||
|
@ -252,8 +269,8 @@ func (f *font) Delete() {
|
|||
|
||||
func centerWindow(wnd uintptr) {
|
||||
getMetric := func(i uintptr) int32 {
|
||||
ret, _, _ := getSystemMetrics.Call(i)
|
||||
return int32(ret)
|
||||
n, _, _ := getSystemMetrics.Call(i)
|
||||
return int32(n)
|
||||
}
|
||||
|
||||
var rect _RECT
|
||||
|
@ -280,8 +297,8 @@ func registerClass(instance, proc uintptr) (uintptr, error) {
|
|||
wcx.Background = 5 // COLOR_WINDOW
|
||||
wcx.ClassName = syscall.StringToUTF16Ptr(name)
|
||||
|
||||
ret, _, err := registerClassEx.Call(uintptr(unsafe.Pointer(&wcx)))
|
||||
return ret, err
|
||||
atom, _, err := registerClassEx.Call(uintptr(unsafe.Pointer(&wcx)))
|
||||
return atom, err
|
||||
}
|
||||
|
||||
// https://docs.microsoft.com/en-us/windows/win32/winmsg/using-messages-and-message-queues
|
||||
|
@ -293,22 +310,62 @@ func messageLoop(wnd uintptr) error {
|
|||
|
||||
for {
|
||||
var msg _MSG
|
||||
ret, _, err := syscall.Syscall6(getMessage, 4, uintptr(unsafe.Pointer(&msg)), 0, 0, 0, 0, 0)
|
||||
if int32(ret) == -1 {
|
||||
s, _, err := syscall.Syscall6(getMessage, 4, uintptr(unsafe.Pointer(&msg)), 0, 0, 0, 0, 0)
|
||||
if int32(s) == -1 {
|
||||
return err
|
||||
}
|
||||
if ret == 0 {
|
||||
if s == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
ret, _, _ = syscall.Syscall(isDialogMessage, 2, wnd, uintptr(unsafe.Pointer(&msg)), 0)
|
||||
if ret == 0 {
|
||||
s, _, _ = syscall.Syscall(isDialogMessage, 2, wnd, uintptr(unsafe.Pointer(&msg)), 0)
|
||||
if s == 0 {
|
||||
syscall.Syscall(translateMessage, 1, uintptr(unsafe.Pointer(&msg)), 0, 0)
|
||||
syscall.Syscall(dispatchMessage, 1, uintptr(unsafe.Pointer(&msg)), 0, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/questions/4308503/how-to-enable-visual-styles-without-a-manifest
|
||||
func enableVisualStyles() (cookie uintptr) {
|
||||
var dir [260]uint16
|
||||
n, _, _ := getSystemDirectory.Call(uintptr(unsafe.Pointer(&dir[0])), uintptr(len(dir)))
|
||||
if n == 0 || int(n) >= len(dir) {
|
||||
return
|
||||
}
|
||||
|
||||
var ctx _ACTCTX
|
||||
ctx.Size = uint32(unsafe.Sizeof(ctx))
|
||||
ctx.Flags = 0x01c // ACTCTX_FLAG_RESOURCE_NAME_VALID|ACTCTX_FLAG_SET_PROCESS_DEFAULT|ACTCTX_FLAG_ASSEMBLY_DIRECTORY_VALID
|
||||
ctx.Source = syscall.StringToUTF16Ptr("shell32.dll")
|
||||
ctx.AssemblyDirectory = &dir[0]
|
||||
ctx.ResourceName = 124
|
||||
|
||||
if h, _, _ := createActCtx.Call(uintptr(unsafe.Pointer(&ctx))); h != 0 {
|
||||
activateActCtx.Call(h, uintptr(unsafe.Pointer(&cookie)))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/winbase/ns-winbase-actctxw
|
||||
type _ACTCTX struct {
|
||||
Size uint32
|
||||
Flags uint32
|
||||
Source *uint16
|
||||
ProcessorArchitecture uint16
|
||||
LangId uint16
|
||||
AssemblyDirectory *uint16
|
||||
ResourceName uintptr
|
||||
ApplicationName *uint16
|
||||
Module uintptr
|
||||
}
|
||||
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/commctrl/ns-commctrl-initcommoncontrolsex
|
||||
type _INITCOMMONCONTROLSEX struct {
|
||||
Size uint32
|
||||
ICC uint32
|
||||
}
|
||||
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-cwpretstruct
|
||||
type _CWPRETSTRUCT struct {
|
||||
Result uintptr
|
||||
|
|
21
zenity.go
21
zenity.go
|
@ -13,14 +13,22 @@ package zenity
|
|||
import (
|
||||
"context"
|
||||
"image/color"
|
||||
|
||||
"github.com/ncruces/zenity/internal/zenutil"
|
||||
)
|
||||
|
||||
type stringErr string
|
||||
|
||||
func (e stringErr) Error() string { return string(e) }
|
||||
|
||||
func stringPtr(s string) *string { return &s }
|
||||
|
||||
// ErrCanceled is returned when the cancel button is pressed,
|
||||
// or window functions are used to close the dialog.
|
||||
const ErrCanceled = zenutil.ErrCanceled
|
||||
|
||||
// ErrExtraButton is returned when the extra button is pressed.
|
||||
const ErrExtraButton = zenutil.ErrExtraButton
|
||||
|
||||
// ErrUnsupported is returned when a combination of options is not supported.
|
||||
const ErrUnsupported = zenutil.ErrUnsupported
|
||||
|
||||
type options struct {
|
||||
// General options
|
||||
title *string
|
||||
|
@ -57,6 +65,11 @@ type options struct {
|
|||
color color.Color
|
||||
showPalette bool
|
||||
|
||||
// Progress indication options
|
||||
maxValue int
|
||||
noCancel bool
|
||||
timeRemaining bool
|
||||
|
||||
// Context for timeout
|
||||
ctx context.Context
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue