Progress.

This commit is contained in:
Nuno Cruces 2021-04-30 19:19:14 +01:00
parent aeaa608758
commit 71541c249a
33 changed files with 464 additions and 177 deletions

View file

@ -13,8 +13,9 @@ 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.

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,7 +103,7 @@ 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 {
@ -135,12 +143,16 @@ func main() {
case colorSelectionDlg: case colorSelectionDlg:
colResult(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...))
}
default:
flag.Usage() flag.Usage()
} }
}
func setupFlags() { func setupFlags() {
// Application Options // Application Options
@ -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
} }

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

@ -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

@ -8,6 +8,7 @@ import (
"time" "time"
"github.com/ncruces/zenity" "github.com/ncruces/zenity"
"go.uber.org/goleak"
) )
func ExampleEntry() { func ExampleEntry() {
@ -16,18 +17,19 @@ 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()

View file

@ -29,6 +29,7 @@ func entry(text string, opts options) (out string, 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
@ -36,7 +37,6 @@ func entry(text string, opts options) (out string, 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

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()

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

@ -10,5 +10,4 @@ var (
Command bool Command bool
Timeout int Timeout int
Separator = "\x00" Separator = "\x00"
Canceled error
) )

View file

@ -13,5 +13,4 @@ var (
Command bool Command bool
Timeout int Timeout int
Separator = "\x1e" Separator = "\x1e"
Canceled error
) )

View file

@ -10,5 +10,4 @@ var (
Command bool Command bool
Timeout int Timeout int
Separator string Separator string
Canceled error
) )

View file

@ -48,16 +48,19 @@ 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}}`)) {{- end}}
{{define "progress" -}}
var progress = `
var app=Application.currentApplication() var app=Application.currentApplication()
app.includeStandardAdditions=true app.includeStandardAdditions=true
app.activate() app.activate()
ObjC.import('stdlib') ObjC.import('stdlib')
ObjC.import('readline') ObjC.import('readline')
try{Progress.totalUnitCount=$.getenv('total')}catch{} {{- if .Total}}
try{Progress.description=$.getenv('description')}catch{} Progress.totalUnitCount={{.Total}}
{{- end}}
{{- if .Description}}
Progress.description={{json .Description}}
{{- end}}
while(true){var s while(true){var s
try{s=$.readline('')}catch(e){if(e.errorNumber===-128)$.exit(1) try{s=$.readline('')}catch(e){if(e.errorNumber===-128)$.exit(1)
break} break}
@ -65,4 +68,5 @@ if(s.indexOf('#')===0){Progress.additionalDescription=s.slice(1)
continue} continue}
var i=parseInt(s) var i=parseInt(s)
if(i>=0&&Progress.totalUnitCount>0){Progress.completedUnitCount=i if(i>=0&&Progress.totalUnitCount>0){Progress.completedUnitCount=i
continue}}` continue}}
{{- end}}`))

View file

@ -21,11 +21,6 @@ func main() {
log.Fatal(err) log.Fatal(err)
} }
var args struct {
Templates string
Progress string
}
var str strings.Builder var str strings.Builder
for _, file := range files { for _, file := range files {
@ -39,25 +34,19 @@ func main() {
log.Fatal(err) log.Fatal(err)
} }
name = strings.TrimSuffix(name, filepath.Ext(name))
if name == "progress" {
args.Progress = string(data)
} else {
str.WriteString("\n" + `{{define "`) str.WriteString("\n" + `{{define "`)
str.WriteString(name) str.WriteString(strings.TrimSuffix(name, filepath.Ext(name)))
str.WriteString(`" -}}` + "\n") str.WriteString(`" -}}` + "\n")
str.Write(data) str.Write(data)
str.WriteString("\n{{- end}}") str.WriteString("\n{{- end}}")
} }
}
out, err := os.Create("osa_generated.go") out, err := os.Create("osa_generated.go")
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
args.Templates = str.String() err = generator.Execute(out, str.String())
err = generator.Execute(out, args)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
@ -116,6 +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(` + "`{{.Templates}}`" + `)) }}).Parse(` + "`{{.}}`))\n"))
var progress = ` + "`\n{{.Progress}}`\n"))

View file

@ -5,8 +5,12 @@ app.activate()
ObjC.import('stdlib') ObjC.import('stdlib')
ObjC.import('readline') ObjC.import('readline')
try { Progress.totalUnitCount = $.getenv('total') } catch { } {{- if .Total}}
try { Progress.description = $.getenv('description') } catch { } Progress.totalUnitCount = {{.Total}}
{{- end}}
{{- if .Description}}
Progress.description = {{json .Description}}
{{- end}}
while (true) { while (true) {
var s var s

View file

@ -3,6 +3,7 @@
package zenutil package zenutil
import ( import (
"bytes"
"context" "context"
"io" "io"
"os" "os"
@ -54,13 +55,9 @@ func (d *progressDialog) Done() <-chan struct{} {
} }
func (d *progressDialog) Complete() error { func (d *progressDialog) Complete() error {
err := d.Value(d.max)
close(d.lines) close(d.lines)
select { return err
case <-d.done:
return d.err
default:
return nil
}
} }
func (d *progressDialog) Close() error { func (d *progressDialog) Close() error {
@ -70,7 +67,7 @@ func (d *progressDialog) Close() error {
return d.err return d.err
} }
func (d *progressDialog) wait() { func (d *progressDialog) wait(extra *string, out *bytes.Buffer) {
err := d.cmd.Wait() err := d.cmd.Wait()
if cerr := d.ctx.Err(); cerr != nil { if cerr := d.ctx.Err(); cerr != nil {
err = cerr err = cerr
@ -80,7 +77,11 @@ func (d *progressDialog) wait() {
case eerr.ExitCode() == -1 && atomic.LoadInt32(&d.closed) != 0: case eerr.ExitCode() == -1 && atomic.LoadInt32(&d.closed) != 0:
err = nil err = nil
case eerr.ExitCode() == 1: case eerr.ExitCode() == 1:
err = Canceled if extra != nil && *extra+"\n" == string(out.Bytes()) {
err = ErrExtraButton
} else {
err = ErrCanceled
}
} }
} }
d.err = err d.err = err
@ -103,7 +104,10 @@ func (d *progressDialog) pipe(w io.WriteCloser) {
line = s line = s
case <-d.ctx.Done(): case <-d.ctx.Done():
return return
case <-d.done:
return
case <-time.After(timeout): case <-time.After(timeout):
// line = ""
} }
if _, err := w.Write([]byte(line + "\n")); err != nil { if _, err := w.Write([]byte(line + "\n")); err != nil {
return return

View file

@ -7,27 +7,23 @@ import (
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"strings"
"syscall" "syscall"
"time"
) )
// 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 {
@ -38,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()
@ -46,45 +42,57 @@ 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()
} }
// RunProgress is internal. // RunProgress is internal.
func RunProgress(ctx context.Context, max int, env []string) (*progressDialog, error) { 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
}
t, err := ioutil.TempDir("", "") t, err := ioutil.TempDir("", "")
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer func() { defer func() {
if err != nil { if err != nil {
if ctx != nil && ctx.Err() != nil {
err = ctx.Err()
}
os.RemoveAll(t) os.RemoveAll(t)
} }
}() }()
if ctx == nil {
ctx = context.Background()
}
var cmd *exec.Cmd var cmd *exec.Cmd
name := filepath.Join(t, "progress.app") name := filepath.Join(t, "progress.app")
cmd = exec.Command("osacompile", "-l", "JavaScript", "-o", name) cmd = exec.CommandContext(ctx, "osacompile", "-l", "JavaScript", "-o", name)
cmd.Stdin = strings.NewReader(progress) cmd.Stdin = &buf
if err := cmd.Run(); err != nil { if err := cmd.Run(); err != nil {
return nil, err return nil, err
} }
plist := filepath.Join(name, "Contents/Info.plist") plist := filepath.Join(name, "Contents/Info.plist")
cmd = exec.Command("defaults", "write", plist, "LSUIElement", "true") cmd = exec.CommandContext(ctx, "defaults", "write", plist, "LSUIElement", "true")
if err := cmd.Run(); err != nil { if err := cmd.Run(); err != nil {
return nil, err return nil, err
} }
cmd = exec.Command("defaults", "write", plist, "CFBundleName", "") cmd = exec.CommandContext(ctx, "defaults", "write", plist, "CFBundleName", "")
if err := cmd.Run(); err != nil { if err := cmd.Run(); err != nil {
return nil, err return nil, err
} }
var executable string var executable string
cmd = exec.Command("defaults", "read", plist, "CFBundleExecutable") cmd = exec.CommandContext(ctx, "defaults", "read", plist, "CFBundleExecutable")
if out, err := cmd.Output(); err != nil { if out, err := cmd.Output(); err != nil {
return nil, err return nil, err
} else { } else {
@ -92,8 +100,7 @@ func RunProgress(ctx context.Context, max int, env []string) (*progressDialog, e
executable = filepath.Join(name, "Contents/MacOS", string(out)) executable = filepath.Join(name, "Contents/MacOS", string(out))
} }
cmd = exec.Command(executable) cmd = exec.CommandContext(ctx, executable)
cmd.Env = env
pipe, err := cmd.StdinPipe() pipe, err := cmd.StdinPipe()
if err != nil { if err != nil {
return nil, err return nil, err
@ -101,11 +108,9 @@ func RunProgress(ctx context.Context, max int, env []string) (*progressDialog, e
if err := cmd.Start(); err != nil { if err := cmd.Start(); err != nil {
return nil, err return nil, err
} }
if ctx == nil {
ctx = context.Background()
}
dlg := &progressDialog{ dlg = &progressDialog{
ctx: ctx,
cmd: cmd, cmd: cmd,
max: max, max: max,
lines: make(chan string), lines: make(chan string),
@ -114,7 +119,7 @@ func RunProgress(ctx context.Context, max int, env []string) (*progressDialog, e
go dlg.pipe(pipe) go dlg.pipe(pipe)
go func() { go func() {
defer os.RemoveAll(t) defer os.RemoveAll(t)
dlg.wait() dlg.wait(nil, nil)
}() }()
return dlg, nil return dlg, nil
} }
@ -185,7 +190,6 @@ type File struct {
Options FileOptions Options FileOptions
} }
// FileOptions is internal.
type FileOptions struct { type FileOptions struct {
Prompt *string `json:"withPrompt,omitempty"` Prompt *string `json:"withPrompt,omitempty"`
Type []string `json:"ofType,omitempty"` Type []string `json:"ofType,omitempty"`
@ -206,3 +210,9 @@ type NotifyOptions struct {
Title *string `json:"withTitle,omitempty"` Title *string `json:"withTitle,omitempty"`
Subtitle string `json:"subtitle,omitempty"` Subtitle string `json:"subtitle,omitempty"`
} }
// Progress is internal.
type Progress struct {
Description *string
Total *int
}

View file

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

View file

@ -5,6 +5,10 @@ import (
) )
func list(text string, items []string, opts options) (string, 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, 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,18 +45,19 @@ 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()

View file

@ -42,6 +42,7 @@ func listDlg(text string, items []string, multiple bool, opts options) (out []st
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
@ -49,7 +50,6 @@ func listDlg(text string, items []string, multiple bool, opts options) (out []st
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

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() {
@ -45,7 +46,7 @@ var msgFuncs = []func(string, ...zenity.Option) error{
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)
@ -55,10 +56,12 @@ func TestMessageTimeout(t *testing.T) {
} }
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()

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()

View file

@ -29,7 +29,7 @@ type ProgressDialog interface {
Done() <-chan struct{} Done() <-chan struct{}
} }
// MaxValue returns an Option to set the maximum value (macOS only). // MaxValue returns an Option to set the maximum value (Windows and macOS only).
// The default maximum value is 100. // The default maximum value is 100.
func MaxValue(value int) Option { func MaxValue(value int) Option {
return funcOption(func(o *options) { o.maxValue = value }) return funcOption(func(o *options) { o.maxValue = value })
@ -40,7 +40,7 @@ func Pulsate() Option {
return funcOption(func(o *options) { o.maxValue = -1 }) return funcOption(func(o *options) { o.maxValue = -1 })
} }
// NoCancel returns an Option to hide the Cancel button (Unix only). // NoCancel returns an Option to hide the Cancel button (Windows and Unix only).
func NoCancel() Option { func NoCancel() Option {
return funcOption(func(o *options) { o.noCancel = true }) return funcOption(func(o *options) { o.noCancel = true })
} }

View file

@ -1,21 +1,22 @@
package zenity package zenity
import ( import (
"strconv"
"github.com/ncruces/zenity/internal/zenutil" "github.com/ncruces/zenity/internal/zenutil"
) )
func progress(opts options) (ProgressDialog, error) { func progress(opts options) (ProgressDialog, error) {
var env []string if opts.extraButton != nil {
if opts.title != nil { return nil, ErrUnsupported
env = append(env, "description="+*opts.title)
} }
var data zenutil.Progress
data.Description = opts.title
if opts.maxValue == 0 { if opts.maxValue == 0 {
opts.maxValue = 100 opts.maxValue = 100
} }
if opts.maxValue >= 0 { if opts.maxValue >= 0 {
env = append(env, "total="+strconv.Itoa(opts.maxValue)) data.Total = &opts.maxValue
} }
return zenutil.RunProgress(opts.ctx, opts.maxValue, env)
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)
}
}

View file

@ -24,5 +24,5 @@ func progress(opts options) (ProgressDialog, error) {
if opts.timeRemaining { if opts.timeRemaining {
args = append(args, "--time-remaining") args = append(args, "--time-remaining")
} }
return zenutil.RunProgress(opts.ctx, opts.maxValue, args) return zenutil.RunProgress(opts.ctx, opts.maxValue, opts.extraButton, args)
} }

View file

@ -48,25 +48,31 @@ func progressDlg(opts options, dlg *progressDialog) (err error) {
defer font.Delete() defer font.Delete()
defWindowProc := defWindowProc.Addr() defWindowProc := defWindowProc.Addr()
var wnd, textCtl, progCtl uintptr
var okBtn, cancelBtn, extraBtn uintptr
layout := func(dpi dpi) { layout := func(dpi dpi) {
hfont := font.ForDPI(dpi) hfont := font.ForDPI(dpi)
sendMessage.Call(textCtl, 0x0030 /* WM_SETFONT */, hfont, 1) sendMessage.Call(dlg.textCtl, 0x0030 /* WM_SETFONT */, hfont, 1)
sendMessage.Call(okBtn, 0x0030 /* WM_SETFONT */, hfont, 1) sendMessage.Call(dlg.okBtn, 0x0030 /* WM_SETFONT */, hfont, 1)
sendMessage.Call(cancelBtn, 0x0030 /* WM_SETFONT */, hfont, 1) sendMessage.Call(dlg.cancelBtn, 0x0030 /* WM_SETFONT */, hfont, 1)
setWindowPos.Call(wnd, 0, 0, 0, dpi.Scale(281), dpi.Scale(141), 0x6) // SWP_NOZORDER|SWP_NOMOVE sendMessage.Call(dlg.extraBtn, 0x0030 /* WM_SETFONT */, hfont, 1)
setWindowPos.Call(textCtl, 0, dpi.Scale(12), dpi.Scale(10), dpi.Scale(241), dpi.Scale(16), 0x4) // SWP_NOZORDER setWindowPos.Call(dlg.wnd, 0, 0, 0, dpi.Scale(281), dpi.Scale(141), 0x6) // SWP_NOZORDER|SWP_NOMOVE
setWindowPos.Call(progCtl, 0, dpi.Scale(12), dpi.Scale(30), dpi.Scale(241), dpi.Scale(24), 0x4) // SWP_NOZORDER setWindowPos.Call(dlg.textCtl, 0, dpi.Scale(12), dpi.Scale(10), dpi.Scale(241), dpi.Scale(16), 0x4) // SWP_NOZORDER
if extraBtn == 0 { setWindowPos.Call(dlg.progCtl, 0, dpi.Scale(12), dpi.Scale(30), dpi.Scale(241), 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 if dlg.extraBtn == 0 {
setWindowPos.Call(cancelBtn, 0, dpi.Scale(178), dpi.Scale(66), dpi.Scale(75), dpi.Scale(24), 0x4) // SWP_NOZORDER 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 { } else {
sendMessage.Call(extraBtn, 0x0030 /* WM_SETFONT */, hfont, 1) setWindowPos.Call(dlg.okBtn, 0, dpi.Scale(95), 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(dlg.cancelBtn, 0, dpi.Scale(178), 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 } 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
}
} }
} }
@ -118,54 +124,52 @@ func progressDlg(opts options, dlg *progressDialog) (err error) {
} }
defer unregisterClass.Call(cls, instance) defer unregisterClass.Call(cls, instance)
wnd, _, _ = createWindowEx.Call(0x10101, // WS_EX_CONTROLPARENT|WS_EX_WINDOWEDGE|WS_EX_DLGMODALFRAME dlg.wnd, _, _ = createWindowEx.Call(0x10101, // WS_EX_CONTROLPARENT|WS_EX_WINDOWEDGE|WS_EX_DLGMODALFRAME
cls, strptr(*opts.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, 0) 281, 141, 0, 0, instance, 0)
textCtl, _, _ = createWindowEx.Call(0, dlg.textCtl, _, _ = createWindowEx.Call(0,
strptr("STATIC"), 0, strptr("STATIC"), 0,
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, 0) 12, 10, 241, 16, dlg.wnd, 0, instance, 0)
var flags uintptr = 0x50000001 // WS_CHILD|WS_VISIBLE|PBS_SMOOTH var flags uintptr = 0x50000001 // WS_CHILD|WS_VISIBLE|PBS_SMOOTH
if opts.maxValue < 0 { if opts.maxValue < 0 {
flags |= 0x8 // PBS_MARQUEE flags |= 0x8 // PBS_MARQUEE
} }
progCtl, _, _ = createWindowEx.Call(0, dlg.progCtl, _, _ = createWindowEx.Call(0,
strptr("msctls_progress32"), // PROGRESS_CLASS strptr("msctls_progress32"), // PROGRESS_CLASS
0, flags, 0, flags,
12, 30, 241, 24, wnd, 0, instance, 0) 12, 30, 241, 24, dlg.wnd, 0, instance, 0)
okBtn, _, _ = createWindowEx.Call(0, dlg.okBtn, _, _ = createWindowEx.Call(0,
strptr("BUTTON"), strptr(*opts.okLabel), strptr("BUTTON"), strptr(*opts.okLabel),
0x58030001, // WS_CHILD|WS_VISIBLE|WS_DISABLED|WS_GROUP|WS_TABSTOP|BS_DEFPUSHBUTTON 0x58030001, // WS_CHILD|WS_VISIBLE|WS_DISABLED|WS_GROUP|WS_TABSTOP|BS_DEFPUSHBUTTON
12, 66, 75, 24, wnd, 1 /* IDOK */, instance, 0) 12, 66, 75, 24, dlg.wnd, 1 /* IDOK */, instance, 0)
cancelBtn, _, _ = createWindowEx.Call(0, if !opts.noCancel {
dlg.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, 0) 12, 66, 75, 24, dlg.wnd, 2 /* IDCANCEL */, instance, 0)
}
if opts.extraButton != nil { if opts.extraButton != nil {
extraBtn, _, _ = createWindowEx.Call(0, dlg.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, 0) 12, 66, 75, 24, dlg.wnd, 7 /* IDNO */, instance, 0)
} }
layout(getDPI(wnd)) layout(getDPI(dlg.wnd))
centerWindow(wnd) centerWindow(dlg.wnd)
showWindow.Call(wnd, 1 /* SW_SHOWNORMAL */, 0) showWindow.Call(dlg.wnd, 1 /* SW_SHOWNORMAL */, 0)
if opts.maxValue < 0 { if opts.maxValue < 0 {
sendMessage.Call(progCtl, 0x40a /* PBM_SETMARQUEE */, 1, 0) sendMessage.Call(dlg.progCtl, 0x40a /* PBM_SETMARQUEE */, 1, 0)
} else { } else {
sendMessage.Call(progCtl, 0x406 /* PBM_SETRANGE32 */, 0, uintptr(opts.maxValue)) sendMessage.Call(dlg.progCtl, 0x406 /* PBM_SETRANGE32 */, 0, uintptr(opts.maxValue))
} }
dlg.prog = progCtl
dlg.text = textCtl
dlg.ok = okBtn
dlg.wnd = wnd
dlg.init.Done() dlg.init.Done()
if opts.ctx != nil { if opts.ctx != nil {
@ -174,7 +178,7 @@ func progressDlg(opts options, dlg *progressDialog) (err error) {
go func() { go func() {
select { select {
case <-opts.ctx.Done(): case <-opts.ctx.Done():
sendMessage.Call(wnd, 0x0112 /* WM_SYSCOMMAND */, 0xf060 /* SC_CLOSE */, 0) sendMessage.Call(dlg.wnd, 0x0112 /* WM_SYSCOMMAND */, 0xf060 /* SC_CLOSE */, 0)
case <-wait: case <-wait:
} }
}() }()
@ -183,7 +187,7 @@ func progressDlg(opts options, dlg *progressDialog) (err error) {
// set default values // set default values
err = nil err = nil
if err := messageLoop(wnd); err != nil { if err := messageLoop(dlg.wnd); err != nil {
return err return err
} }
if opts.ctx != nil && opts.ctx.Err() != nil { if opts.ctx != nil && opts.ctx.Err() != nil {
@ -193,26 +197,22 @@ func progressDlg(opts options, dlg *progressDialog) (err error) {
} }
type progressDialog struct { type progressDialog struct {
err error max int
done chan struct{} done chan struct{}
init sync.WaitGroup init sync.WaitGroup
prog uintptr
text uintptr
wnd uintptr wnd uintptr
ok uintptr textCtl uintptr
max int progCtl uintptr
} okBtn uintptr
cancelBtn uintptr
func (d *progressDialog) Close() error { extraBtn uintptr
sendMessage.Call(d.wnd, 0x0112 /* WM_SYSCOMMAND */, 0xf060 /* SC_CLOSE */, 0) err error
<-d.done
return d.err
} }
func (d *progressDialog) Text(text string) error { func (d *progressDialog) Text(text string) error {
select { select {
default: default:
setWindowText.Call(d.text, strptr(text)) setWindowText.Call(d.textCtl, strptr(text))
return nil return nil
case <-d.done: case <-d.done:
return d.err return d.err
@ -222,9 +222,9 @@ func (d *progressDialog) Text(text string) error {
func (d *progressDialog) Value(value int) error { func (d *progressDialog) Value(value int) error {
select { select {
default: default:
sendMessage.Call(d.prog, 0x402 /* PBM_SETPOS */, uintptr(value), 0) sendMessage.Call(d.progCtl, 0x402 /* PBM_SETPOS */, uintptr(value), 0)
if value >= d.max { if value >= d.max {
enableWindow.Call(d.ok, 1) enableWindow.Call(d.okBtn, 1)
} }
return nil return nil
case <-d.done: case <-d.done:
@ -239,3 +239,23 @@ func (d *progressDialog) MaxValue() int {
func (d *progressDialog) Done() <-chan struct{} { func (d *progressDialog) Done() <-chan struct{} {
return d.done 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
}

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,18 +16,19 @@ 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()

View file

@ -3,7 +3,6 @@
package zenity package zenity
import ( import (
"bytes"
"os/exec" "os/exec"
"strconv" "strconv"
"strings" "strings"
@ -56,9 +55,8 @@ func appendIcon(args []string, opts options) []string {
} }
func strResult(opts options, out []byte, err error) (string, 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 "", ErrExtraButton return "", ErrExtraButton
} }
return "", ErrCanceled return "", ErrCanceled

View file

@ -66,6 +66,7 @@ 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")

View file

@ -17,25 +17,17 @@ import (
"github.com/ncruces/zenity/internal/zenutil" "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, // ErrCanceled is returned when the cancel button is pressed,
// or window functions are used to close the dialog. // or window functions are used to close the dialog.
const ErrCanceled = stringErr("dialog canceled") const ErrCanceled = zenutil.ErrCanceled
// ErrExtraButton is returned when the extra button is pressed. // ErrExtraButton is returned when the extra button is pressed.
const ErrExtraButton = stringErr("extra button pressed") const ErrExtraButton = zenutil.ErrExtraButton
// ErrUnsupported is returned when a combination of options is not supported. // ErrUnsupported is returned when a combination of options is not supported.
const ErrUnsupported = stringErr("unsupported option") const ErrUnsupported = zenutil.ErrUnsupported
func init() {
zenutil.Canceled = ErrCanceled
}
type options struct { type options struct {
// General options // General options