diff --git a/cmd/zenity/main.go b/cmd/zenity/main.go index 2c3537c..4eb63fd 100644 --- a/cmd/zenity/main.go +++ b/cmd/zenity/main.go @@ -36,6 +36,7 @@ var ( questionDlg bool entryDlg bool listDlg bool + calendarDlg bool passwordDlg bool fileSelectionDlg bool colorSelectionDlg bool @@ -66,6 +67,11 @@ var ( columns int allowEmpty bool + // Calendar options + year uint + month uint + day uint + // File selection options save bool directory bool @@ -145,6 +151,9 @@ func main() { strResult(zenity.List(text, flag.Args(), opts...)) } + case calendarDlg: + calResult(zenity.Calendar(text, opts...)) + case passwordDlg: _, pw, err := zenity.Password(opts...) strResult(pw, err) @@ -181,6 +190,7 @@ func setupFlags() { flag.BoolVar(&questionDlg, "question", false, "Display question dialog") flag.BoolVar(&entryDlg, "entry", false, "Display text entry dialog") flag.BoolVar(&listDlg, "list", false, "Display list dialog") + flag.BoolVar(&calendarDlg, "calendar", false, "Display calendar dialog") 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") @@ -214,6 +224,12 @@ func setupFlags() { flag.Bool("hide-header", true, "Hide the column headers") flag.BoolVar(&allowEmpty, "allow-empty", true, "Allow empty selection (macOS only)") + // Calendar options + flag.UintVar(&year, "year", 0, "Set the calendar `year`") + flag.UintVar(&month, "month", 0, "Set the calendar `month`") + flag.UintVar(&day, "day", 0, "Set the calendar `day`") + flag.StringVar(&zenutil.DateFormat, "date-format", "%m/%d/%Y", "Set the `format` for the returned date") + // File selection options flag.BoolVar(&save, "save", false, "Activate save mode") flag.BoolVar(&directory, "directory", false, "Activate directory-only selection") @@ -283,6 +299,9 @@ func validateFlags() { if listDlg { n++ } + if calendarDlg { + n++ + } if passwordDlg { n++ } @@ -341,13 +360,18 @@ func loadFlags() []zenity.Option { setDefault(&text, "Select items from the list below:") setDefault(&okLabel, "OK") setDefault(&cancelLabel, "Cancel") + case calendarDlg: + setDefault(&title, "Calendar selection") + setDefault(&text, "Select a date from below:") + setDefault(&okLabel, "OK") + setDefault(&cancelLabel, "Cancel") case passwordDlg: setDefault(&title, "Type your password") setDefault(&okLabel, "OK") setDefault(&cancelLabel, "Cancel") case progressDlg: setDefault(&title, "Progress") - setDefault(&text, "Running...") + setDefault(&text, "Running…") setDefault(&okLabel, "OK") setDefault(&cancelLabel, "Cancel") } @@ -413,6 +437,18 @@ func loadFlags() []zenity.Option { opts = append(opts, zenity.DisallowEmpty()) } + y, m, d := time.Now().Date() + if month != 0 { + m = time.Month(month) + } + if day != 0 { + d = int(day) + } + if year != 0 { + y = int(year) + } + opts = append(opts, zenity.DefaultDate(y, m, d)) + // File selection options if directory { @@ -484,6 +520,12 @@ func lstResult(l []string, err error) { os.Stdout.WriteString(zenutil.LineBreak) } +func calResult(d time.Time, err error) { + errResult(err) + os.Stdout.WriteString(d.Format(zenutil.Strftime(zenutil.DateFormat))) + os.Stdout.WriteString(zenutil.LineBreak) +} + func colResult(c color.Color, err error) { errResult(err) os.Stdout.WriteString(zenutil.UnparseColor(c)) diff --git a/date_darwin.go b/date_darwin.go index 082b14d..2de3344 100644 --- a/date_darwin.go +++ b/date_darwin.go @@ -10,7 +10,7 @@ func calendar(text string, opts options) (time.Time, error) { var date zenutil.Date date.OK, date.Cancel, date.Extra = getAlertButtons(opts) - date.Format = "yyyy-MM-dd" + date.Format = zenutil.StrftimeUTS35(zenutil.DateFormat) if opts.time != nil { date.Date = opts.time.Unix() } @@ -27,5 +27,5 @@ func calendar(text string, opts options) (time.Time, error) { if err != nil { return time.Time{}, err } - return time.Parse("2006-01-02", str) + return time.Parse(zenutil.Strftime(zenutil.DateFormat), str) } diff --git a/date_unix.go b/date_unix.go index 51db210..366beb1 100644 --- a/date_unix.go +++ b/date_unix.go @@ -10,7 +10,7 @@ import ( ) func calendar(text string, opts options) (time.Time, error) { - args := []string{"--calendar", "--text", text, "--date-format=%F"} + args := []string{"--calendar", "--text", text, "--date-format", zenutil.DateFormat} args = appendTitle(args, opts) args = appendButtons(args, opts) args = appendWidthHeight(args, opts) @@ -27,5 +27,5 @@ func calendar(text string, opts options) (time.Time, error) { if err != nil { return time.Time{}, err } - return time.Parse("2006-01-02", str) + return time.Parse(zenutil.Strftime(zenutil.DateFormat), str) } diff --git a/internal/zenutil/date.go b/internal/zenutil/date.go new file mode 100644 index 0000000..375603d --- /dev/null +++ b/internal/zenutil/date.go @@ -0,0 +1,151 @@ +package zenutil + +import "strings" + +// https://strftime.org/ +var strftimeTable = map[byte]string{ + 'B': "January", + 'b': "Jan", + 'h': "Jan", + 'm': "01", + 'A': "Monday", + 'a': "Mon", + 'e': "_2", + 'd': "02", + 'j': "002", + 'H': "15", + 'I': "03", + 'M': "04", + 'S': "05", + 'Y': "2006", + 'y': "06", + 'p': "PM", + 'Z': "MST", + 'z': "-0700", + 'L': "000", + 'f': "000000", + + '+': "Mon Jan _2 03:04:05 PM MST 2006", + 'c': "Mon Jan _2 15:04:05 2006", + 'F': "2006-01-02", + 'D': "01/02/06", + 'x': "01/02/06", + 'r': "03:04:05 PM", + 'T': "15:04:05", + 'X': "15:04:05", + 'R': "15:04", + + '%': "%", + 't': "\t", + 'n': LineBreak, +} + +// Strftime is internal. +func Strftime(fmt string) string { + var res strings.Builder + res.Grow(len(fmt)) + + const ( + initial = iota + special + ) + + state := initial + for _, b := range []byte(fmt) { + switch state { + case initial: + if b == '%' { + state = special + } else { + res.WriteByte(b) + state = initial + } + + case special: + s, ok := strftimeTable[b] + if ok { + res.WriteString(s) + } else { + res.WriteByte(b) + } + state = initial + } + } + + return res.String() +} + +// https://nsdateformatter.com/ +var strftimeUTS35Table = map[byte]string{ + 'B': "MMMM", + 'b': "MMM", + 'h': "MMM", + 'm': "MM", + 'A': "EEEE", + 'a': "E", + 'd': "dd", + 'j': "DDD", + 'H': "HH", + 'I': "hh", + 'M': "mm", + 'S': "ss", + 'Y': "yyyy", + 'y': "yy", + 'G': "YYYY", + 'g': "YY", + 'V': "ww", + 'p': "a", + 'Z': "zzz", + 'z': "Z", + 'L': "SSS", + 'f': "SSSSSS", + + '+': "E MMM d hh:mm:ss a zzz yyyy", + 'c': "E MMM d HH:mm:ss yyyy", + 'F': "yyyy-MM-dd", + 'D': "MM/dd/yy", + 'x': "MM/dd/yy", + 'r': "hh:mm:ss a", + 'T': "HH:mm:ss", + 'X': "HH:mm:ss", + 'R': "HH:mm", + + '%': "%", + 't': "\t", + 'n': LineBreak, +} + +// StrftimeUTS35 is internal. +func StrftimeUTS35(fmt string) string { + var res strings.Builder + res.Grow(len(fmt)) + + const ( + initial = iota + special + ) + + state := initial + for _, b := range []byte(fmt) { + switch state { + case initial: + if b == '%' { + state = special + } else { + res.WriteByte(b) + state = initial + } + + case special: + s, ok := strftimeUTS35Table[b] + if ok { + res.WriteString(s) + } else { + res.WriteByte(b) + } + state = initial + } + } + + return res.String() +} diff --git a/internal/zenutil/env_darwin.go b/internal/zenutil/env_darwin.go index 5d11472..c4fd4a5 100644 --- a/internal/zenutil/env_darwin.go +++ b/internal/zenutil/env_darwin.go @@ -2,8 +2,9 @@ package zenutil // These are internal. var ( - Command bool - Timeout int - LineBreak = "\n" - Separator = "\x00" + Command bool + Timeout int + LineBreak = "\n" + Separator = "\x00" + DateFormat = "%F" ) diff --git a/internal/zenutil/env_unix.go b/internal/zenutil/env_unix.go index 663d4e4..eaec7df 100644 --- a/internal/zenutil/env_unix.go +++ b/internal/zenutil/env_unix.go @@ -5,8 +5,9 @@ package zenutil // These are internal. var ( - Command bool - Timeout int - LineBreak = "\n" - Separator = "\x1e" + Command bool + Timeout int + LineBreak = "\n" + Separator = "\x1e" + DateFormat = "%F" ) diff --git a/internal/zenutil/env_windows.go b/internal/zenutil/env_windows.go index fdeed2b..993ea6c 100644 --- a/internal/zenutil/env_windows.go +++ b/internal/zenutil/env_windows.go @@ -2,8 +2,9 @@ package zenutil // These are internal. var ( - Command bool - Timeout int - Separator string - LineBreak = "\r\n" + Command bool + Timeout int + Separator string + LineBreak = "\r\n" + DateFormat = "%F" ) diff --git a/internal/zenutil/unescape.go b/internal/zenutil/unescape.go index e4a703e..e542960 100644 --- a/internal/zenutil/unescape.go +++ b/internal/zenutil/unescape.go @@ -1,5 +1,7 @@ package zenutil +import "strings" + // Unescape is internal. func Unescape(s string) string { // Apply rules described in: @@ -12,7 +14,7 @@ func Unescape(s string) string { escape3 ) var oct byte - var res []byte + var res strings.Builder state := initial for _, b := range []byte(s) { switch state { @@ -21,7 +23,7 @@ func Unescape(s string) string { case '\\': state = escape1 default: - res = append(res, b) + res.WriteByte(b) state = initial } @@ -31,25 +33,25 @@ func Unescape(s string) string { oct = b - '0' state = escape2 case 'b': - res = append(res, '\b') + res.WriteByte('\b') state = initial case 'f': - res = append(res, '\f') + res.WriteByte('\f') state = initial case 'n': - res = append(res, '\n') + res.WriteByte('\n') state = initial case 'r': - res = append(res, '\r') + res.WriteByte('\r') state = initial case 't': - res = append(res, '\t') + res.WriteByte('\t') state = initial case 'v': - res = append(res, '\v') + res.WriteByte('\v') state = initial default: - res = append(res, b) + res.WriteByte(b) state = initial } @@ -59,10 +61,11 @@ func Unescape(s string) string { oct = oct<<3 | (b - '0') state = escape3 case '\\': - res = append(res, oct) + res.WriteByte(oct) state = escape1 default: - res = append(res, oct, b) + res.WriteByte(oct) + res.WriteByte(b) state = initial } @@ -70,20 +73,21 @@ func Unescape(s string) string { switch b { case '0', '1', '2', '3', '4', '5', '6', '7': oct = oct<<3 | (b - '0') - res = append(res, oct) + res.WriteByte(oct) state = initial case '\\': - res = append(res, oct) + res.WriteByte(oct) state = escape1 default: - res = append(res, oct, b) + res.WriteByte(oct) + res.WriteByte(b) state = initial } } } if state == escape2 || state == escape3 { - res = append(res, oct) + res.WriteByte(oct) } - return string(res) + return res.String() }