diff --git a/cmd/zenity/build.sh b/cmd/zenity/build.sh index 4d3f7c5..cf568d6 100755 --- a/cmd/zenity/build.sh +++ b/cmd/zenity/build.sh @@ -18,7 +18,7 @@ go run github.com/randall77/makefat zenity zenity_macos_x64 zenity_macos_arm && zip -9 zenity_macos.zip zenity zip -9 zenity_brew.zip zenity zenity.exe - rm zenity zenity_macos_* zenity.exe +GOOS=linux go build -tags dev go build -tags dev diff --git a/cmd/zenity/main.go b/cmd/zenity/main.go index 9257913..0e14bfe 100644 --- a/cmd/zenity/main.go +++ b/cmd/zenity/main.go @@ -19,6 +19,7 @@ import ( "time" "github.com/ncruces/zenity" + "github.com/ncruces/zenity/internal/strftime" "github.com/ncruces/zenity/internal/zenutil" ) @@ -522,7 +523,7 @@ func lstResult(l []string, err error) { func calResult(d time.Time, err error) { errResult(err) - os.Stdout.WriteString(zenutil.Strftime(zenutil.DateFormat, d)) + os.Stdout.WriteString(strftime.Format(zenutil.DateFormat, d)) os.Stdout.WriteString(zenutil.LineBreak) } diff --git a/date_darwin.go b/date_darwin.go index 11f6999..55edf18 100644 --- a/date_darwin.go +++ b/date_darwin.go @@ -3,16 +3,21 @@ package zenity import ( "time" + "github.com/ncruces/zenity/internal/strftime" "github.com/ncruces/zenity/internal/zenutil" ) -func calendar(text string, opts options) (time.Time, error) { +func calendar(text string, opts options) (t time.Time, err error) { var date zenutil.Date date.OK, date.Cancel, date.Extra = getAlertButtons(opts) - date.Format = zenutil.StrftimeUTS35(zenutil.DateFormat) + date.Format, err = strftime.UTS35(zenutil.DateFormat) + if err != nil { + return + } if opts.time != nil { - date.Date = opts.time.Unix() + unix := opts.time.Unix() + date.Date = &unix } if opts.title != nil { @@ -25,8 +30,7 @@ func calendar(text string, opts options) (time.Time, error) { out, err := zenutil.Run(opts.ctx, "date", date) str, err := strResult(opts, out, err) if err != nil { - return time.Time{}, err + return } - layout := zenutil.StrftimeLayout(zenutil.DateFormat) - return time.Parse(layout, str) + return strftime.Parse(zenutil.DateFormat, str) } diff --git a/date_unix.go b/date_unix.go index 80c8117..5e0ae42 100644 --- a/date_unix.go +++ b/date_unix.go @@ -6,6 +6,7 @@ import ( "strconv" "time" + "github.com/ncruces/zenity/internal/strftime" "github.com/ncruces/zenity/internal/zenutil" ) @@ -27,6 +28,5 @@ func calendar(text string, opts options) (time.Time, error) { if err != nil { return time.Time{}, err } - layout := zenutil.StrftimeLayout(zenutil.DateFormat) - return time.Parse(layout, str) + return strftime.Parse(zenutil.DateFormat, str) } diff --git a/internal/strftime/parser.go b/internal/strftime/parser.go new file mode 100644 index 0000000..141f233 --- /dev/null +++ b/internal/strftime/parser.go @@ -0,0 +1,65 @@ +package strftime + +type parser struct { + fmt string + specs map[byte]string + unpadded map[byte]string + writeLit func(byte) error + writeFmt func(string) error + fallback func(spec byte, pad bool) error +} + +func (p *parser) parse() error { + const ( + initial = iota + specifier + nopadding + ) + + var err error + state := initial + for _, b := range []byte(p.fmt) { + switch state { + default: + if b == '%' { + state = specifier + } else { + err = p.writeLit(b) + } + + case specifier: + if b == '-' { + state = nopadding + continue + } + if s, ok := p.specs[b]; ok { + err = p.writeFmt(s) + } else { + err = p.fallback(b, true) + } + state = initial + + case nopadding: + if s, ok := p.unpadded[b]; ok { + err = p.writeFmt(s) + } else if s, ok := p.specs[b]; ok { + err = p.writeFmt(s) + } else { + err = p.fallback(b, false) + } + state = initial + } + + if err != nil { + return err + } + } + + switch state { + case specifier: + return p.writeLit('%') + case nopadding: + return p.writeLit('-') + } + return nil +} diff --git a/internal/strftime/specifiers.go b/internal/strftime/specifiers.go new file mode 100644 index 0000000..1caa9de --- /dev/null +++ b/internal/strftime/specifiers.go @@ -0,0 +1,102 @@ +package strftime + +func (p *parser) goSpecifiers() { + // https://strftime.org/ + p.specs = 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", + 'N': "000000000", + + '+': "Mon Jan _2 15:04:05 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': "\n", + } + + p.unpadded = map[byte]string{ + 'm': "1", + 'd': "2", + 'I': "3", + 'M': "4", + 'S': "5", + } +} + +func (p *parser) uts35Specifiers() { // https://nsdateformatter.com/ + p.specs = 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", + 'N': "SSSSSSSSS", + + '+': "E MMM d HH:mm:ss 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': "\n", + } + + p.unpadded = map[byte]string{ + 'm': "M", + 'd': "d", + 'j': "D", + 'H': "H", + 'I': "h", + 'M': "m", + 'S': "s", + } +} diff --git a/internal/strftime/strftime.go b/internal/strftime/strftime.go new file mode 100644 index 0000000..88ea218 --- /dev/null +++ b/internal/strftime/strftime.go @@ -0,0 +1,148 @@ +package strftime + +import ( + "bytes" + "errors" + "strconv" + "strings" + "time" +) + +func Format(fmt string, t time.Time) string { + var res strings.Builder + + var parser parser + parser.fmt = fmt + parser.goSpecifiers() + + parser.writeLit = res.WriteByte + + parser.writeFmt = func(fmt string) error { + switch fmt { + default: + res.WriteString(t.Format(fmt)) + case "000", "000000", "000000000": + res.WriteString(t.Format("." + fmt)[1:]) + } + return nil + } + + parser.fallback = func(spec byte, pad bool) error { + switch spec { + default: + return errors.New("strftime: unsupported specifier: %" + string(spec)) + case 'C': + s := t.Format("2006") + res.WriteString(s[:len(s)-2]) + case 'g': + y, _ := t.ISOWeek() + res.WriteString(time.Date(y, 1, 1, 0, 0, 0, 0, time.UTC).Format("06")) + case 'G': + y, _ := t.ISOWeek() + res.WriteString(time.Date(y, 1, 1, 0, 0, 0, 0, time.UTC).Format("2006")) + case 'V': + _, w := t.ISOWeek() + if w < 10 && pad { + res.WriteByte('0') + } + res.WriteString(strconv.Itoa(w)) + case 'w': + w := int(t.Weekday()) + res.WriteString(strconv.Itoa(w)) + case 'u': + if w := int(t.Weekday()); w == 0 { + res.WriteByte('7') + } else { + res.WriteString(strconv.Itoa(w)) + } + } + return nil + } + + parser.parse() + return res.String() +} + +func Parse(fmt, value string) (time.Time, error) { + layout, err := Layout(fmt) + if err != nil { + return time.Time{}, err + } + return time.Parse(layout, value) +} + +func Layout(fmt string) (string, error) { + var res strings.Builder + + var parser parser + parser.fmt = fmt + parser.goSpecifiers() + + parser.writeLit = func(b byte) error { + if bytes.IndexByte([]byte("MonJan_0123456789"), b) >= 0 { + return errors.New("strftime: unsupported literal: " + string(b)) + } + res.WriteByte(b) + return nil + } + + parser.writeFmt = func(s string) error { + res.WriteString(s) + return nil + } + + parser.fallback = func(spec byte, pad bool) error { + return errors.New("strftime: unsupported specifier: %" + string(spec)) + } + + if err := parser.parse(); err != nil { + return "", err + } + + parser.writeFmt("") + return res.String(), nil +} + +func UTS35(fmt string) (string, error) { + var parser parser + parser.fmt = fmt + parser.uts35Specifiers() + + const quote = '\'' + var literal bool + var res strings.Builder + + parser.writeLit = func(b byte) error { + if b == quote { + res.WriteByte(quote) + res.WriteByte(quote) + return nil + } + if !literal && ('a' <= b && b <= 'z' || 'A' <= b && b <= 'Z') { + literal = true + res.WriteByte(quote) + } + res.WriteByte(b) + return nil + } + + parser.writeFmt = func(s string) error { + if literal { + literal = false + res.WriteByte(quote) + } + res.WriteString(s) + return nil + } + + parser.fallback = func(spec byte, pad bool) error { + return errors.New("strftime: unsupported specifier: %" + string(spec)) + } + + if err := parser.parse(); err != nil { + return "", err + } + + parser.writeFmt("") + return res.String(), nil +} diff --git a/internal/zenutil/date.go b/internal/zenutil/date.go deleted file mode 100644 index e540abe..0000000 --- a/internal/zenutil/date.go +++ /dev/null @@ -1,215 +0,0 @@ -package zenutil - -import ( - "strings" - "time" -) - -// Strftime is internal. -func Strftime(fmt string, t time.Time) string { - var res strings.Builder - writeLit := res.WriteByte - writeFmt := func(fmt string) (int, error) { - return res.WriteString(t.Format(fmt)) - } - strftimeGo(fmt, writeLit, writeFmt) - return res.String() -} - -// StrftimeLayout is internal. -func StrftimeLayout(fmt string) string { - var res strings.Builder - strftimeGo(fmt, res.WriteByte, res.WriteString) - return res.String() -} - -func strftimeGo(fmt string, writeLit func(byte) error, writeFmt func(string) (int, error)) { - // https://strftime.org/ - fmts := 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", - 'N': "000000000", - - '+': "Mon Jan _2 15:04:05 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, - } - - unpaded := map[byte]string{ - 'm': "1", - 'd': "2", - 'I': "3", - 'M': "4", - 'S': "5", - } - - parser(fmt, fmts, unpaded, writeLit, writeFmt) -} - -// StrftimeUTS35 is internal. -func StrftimeUTS35(fmt string) string { - // https://nsdateformatter.com/ - fmts := 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", - 'N': "SSSSSSSSS", - - '+': "E MMM d HH:mm:ss 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, - } - - unpaded := map[byte]string{ - 'm': "M", - 'd': "d", - 'j': "D", - 'H': "H", - 'I': "h", - 'M': "m", - 'S': "s", - } - - const quote = '\'' - var literal bool - var res strings.Builder - - writeLit := func(b byte) error { - if b == quote { - res.WriteByte(quote) - return res.WriteByte(quote) - } - if !literal && ('a' <= b && b <= 'z' || 'A' <= b && b <= 'Z') { - literal = true - res.WriteByte(quote) - } - return res.WriteByte(b) - } - - writeFmt := func(s string) (int, error) { - if literal { - literal = false - res.WriteByte(quote) - } - return res.WriteString(s) - } - - parser(fmt, fmts, unpaded, writeLit, writeFmt) - writeFmt("") - - return res.String() -} - -func parser( - fmt string, - formats, unpadded map[byte]string, - writeLit func(byte) error, writeFmt func(string) (int, error)) { - - const ( - initial = iota - special - padding - ) - - state := initial - for _, b := range []byte(fmt) { - switch state { - case initial: - if b == '%' { - state = special - } else { - writeLit(b) - } - - case special: - if b == '-' { - state = padding - continue - } - if s, ok := formats[b]; ok { - writeFmt(s) - } else { - writeLit('%') - writeLit(b) - } - state = initial - - case padding: - if s, ok := unpadded[b]; ok { - writeFmt(s) - } else if s, ok := formats[b]; ok { - writeFmt(s) - } else { - writeLit('%') - writeLit('-') - writeLit(b) - } - state = initial - } - } - - switch state { - case padding: - writeLit('%') - fallthrough - case special: - writeLit('-') - } -} diff --git a/internal/zenutil/osa_generated.go b/internal/zenutil/osa_generated.go index a36961d..91dbdf6 100644 --- a/internal/zenutil/osa_generated.go +++ b/internal/zenutil/osa_generated.go @@ -29,8 +29,10 @@ ObjC.import('stdlib') var date=$.NSDatePicker.alloc.init date.setDatePickerStyle($.NSDatePickerStyleClockAndCalendar) date.setDatePickerElements($.NSDatePickerElementFlagYearMonthDay) -date.setDateValue($.NSDate.dateWithTimeIntervalSince1970({{.Date}})) date.setFrameSize(date.fittingSize) +{{- if .Date}} +date.setDateValue($.NSDate.dateWithTimeIntervalSince1970({{.Date}})) +{{- end}} var alert=$.NSAlert.alloc.init alert.setAccessoryView(date) alert.setMessageText({{json .Text}}) diff --git a/internal/zenutil/osascripts/date.gojs b/internal/zenutil/osascripts/date.gojs index 76daed4..b52843f 100644 --- a/internal/zenutil/osascripts/date.gojs +++ b/internal/zenutil/osascripts/date.gojs @@ -9,8 +9,10 @@ ObjC.import('stdlib') var date = $.NSDatePicker.alloc.init date.setDatePickerStyle($.NSDatePickerStyleClockAndCalendar) date.setDatePickerElements($.NSDatePickerElementFlagYearMonthDay) -date.setDateValue($.NSDate.dateWithTimeIntervalSince1970({{.Date}})) date.setFrameSize(date.fittingSize) +{{- if .Date}} + date.setDateValue($.NSDate.dateWithTimeIntervalSince1970({{.Date}})) +{{- end}} var alert = $.NSAlert.alloc.init alert.setAccessoryView(date) diff --git a/internal/zenutil/run_darwin.go b/internal/zenutil/run_darwin.go index c375cdd..d2bbfa0 100644 --- a/internal/zenutil/run_darwin.go +++ b/internal/zenutil/run_darwin.go @@ -220,7 +220,7 @@ type Progress struct { // Date is internal. type Date struct { - Date int64 + Date *int64 Text string Info string Format string