diff options
Diffstat (limited to 'pim/cmd/cal9/invite.go')
| -rw-r--r-- | pim/cmd/cal9/invite.go | 242 |
1 files changed, 242 insertions, 0 deletions
diff --git a/pim/cmd/cal9/invite.go b/pim/cmd/cal9/invite.go new file mode 100644 index 0000000..25dcb06 --- /dev/null +++ b/pim/cmd/cal9/invite.go @@ -0,0 +1,242 @@ +package main + +// The invite view: a window you fill in to make an event. +// +// cal9 -v invite [calendar] +// +// It writes the calendar's new file in the tree's own key: value form, +// so nothing here knows what iCalendar looks like -- calfs composes it, +// and a comma in a summary stays a comma. With attendees on a backend +// that schedules, writing the file is also sending the invitation. + +import ( + "fmt" + "os" + "os/exec" + "path" + "strings" + + "time" + + "9front/gui/draw" + "9front/gui/ui" +) + +type form struct { + cal string + fields []*ui.Field + focus int + note string // what happened, shown under the buttons +} + +func newForm(cal string) *form { + return &form{ + cal: cal, + fields: []*ui.Field{ + {Label: "summary"}, + {Label: "when"}, + {Label: "minutes", Value: "30"}, + {Label: "where"}, + {Label: "attendees"}, + }, + } +} + +func (s *state) drawInvite(r draw.Rectangle) { + s.Fill(r, "bg") + f := s.form + lh := int32(s.F.Height) + 8 + + y := r.Min.Y + pad + s.Text(draw.Point{X: r.Min.X + pad, Y: y}, "ink", "new event on "+f.cal) + y += lh + + for i, fl := range f.fields { + w := r.Max.X - pad + // the when field gets a button that opens the picker + if fl.Label == "when" { + bw := s.F.Width("pick") + 20 + w -= bw + 6 + s.Button(draw.Rect(w+6, y, w+6+bw, y+lh-4), "pick", false, s.pickWhen) + } + s.Draw(fl, draw.Rect(r.Min.X+pad, y, w, y+lh-4), i == f.focus) + n := i + s.On(fl.R, func() { f.focus = n; s.redraw() }) + y += lh + } + + y += 6 + x := r.Min.X + pad + for _, b := range []struct { + label string + do func() + }{ + {"Create", func() { s.create() }}, + {"Clear", func() { + for _, fl := range f.fields { + fl.Value = "" + } + f.fields[2].Value = "30" + f.note = "" + s.redraw() + }}, + } { + do := b.do + bw := s.F.Width(b.label) + 20 + br := draw.Rect(x, y, x+bw, y+lh) + s.Button(br, b.label, false, do) + x = br.Max.X + 6 + } + y += lh + 6 + + if f.note != "" { + s.Text(draw.Point{X: r.Min.X + pad, Y: y}, "ink", s.Fit(f.note, r.Dx()-pad*2)) + y += lh + } + s.Text(draw.Point{X: r.Min.X + pad, Y: r.Max.Y - int32(s.F.Height) - pad}, "rule", + "tab moves, ^U clears a field, when: 2026-08-28 15:00") + s.D.Flush() +} + +// pickWhen opens datepick in a window of its own and waits for it. +// +// A window started through wctl has nowhere useful to print -- nothing +// is reading its standard output -- so datepick is asked to leave the +// answer in a file, and a goroutine watches for it. That keeps the form +// answering the mouse while the picker is up. +func (s *state) pickWhen() { + tmp := fmt.Sprintf("/tmp/cal9.pick.%d", os.Getpid()) + os.Remove(tmp) + s.New(360, 320, "datepick", "-t", "-o", tmp) + go func() { + for i := 0; i < 600; i++ { // two minutes is long enough to choose + if b, err := os.ReadFile(tmp); err == nil && len(b) > 0 { + os.Remove(tmp) + s.picked <- strings.TrimSpace(string(b)) + return + } + time.Sleep(200 * time.Millisecond) + } + }() +} + +// create writes the form to the calendar's new file. The fs composes +// the icalendar; this only has to say what it means. +func (s *state) create() { + f := s.form + get := func(i int) string { return strings.TrimSpace(f.fields[i].Value) } + + if get(0) == "" || get(1) == "" { + f.note = "summary and when are needed" + s.redraw() + return + } + start, err := seconds(get(1)) + if err != nil { + f.note = "cannot read the time: " + get(1) + s.redraw() + return + } + mins := int64(30) + fmt.Sscan(get(2), &mins) + if mins <= 0 { + mins = 30 + } + + var b strings.Builder + fmt.Fprintf(&b, "summary: %s\n", get(0)) + fmt.Fprintf(&b, "start: %d\n", start) + fmt.Fprintf(&b, "end: %d\n", start+mins*60) + if v := get(3); v != "" { + fmt.Fprintf(&b, "location: %s\n", v) + } + if v := get(4); v != "" { + me := calMe(path.Join(mtpt, "calendars", f.cal)) + if me == "" { + f.note = f.cal + " has no me=; cannot say who is organising" + s.redraw() + return + } + fmt.Fprintf(&b, "organizer: %s\n", me) + for _, a := range strings.FieldsFunc(v, func(r rune) bool { + return r == ',' || r == ' ' + }) { + fmt.Fprintf(&b, "attendee: %s\n", a) + } + } + + nw := path.Join(mtpt, "calendars", f.cal, "new") + fd, err := os.OpenFile(nw, os.O_WRONLY, 0) + if err != nil { + f.note = err.Error() + s.redraw() + return + } + _, err = fd.WriteString(b.String()) + if cerr := fd.Close(); err == nil { + err = cerr + } + if err != nil { + f.note = err.Error() + } else { + f.note = "created: " + get(0) + for _, fl := range f.fields { + fl.Value = "" + } + f.fields[2].Value = "30" + } + s.redraw() +} + +// calMe reads whose calendar this is, the same line pim/invite reads. +func calMe(dir string) string { + b, err := os.ReadFile(path.Join(dir, "ctl")) + if err != nil { + return "" + } + for _, l := range strings.Split(string(b), "\n") { + if v, ok := strings.CutPrefix(l, "me "); ok { + if i := strings.IndexByte(v, ','); i >= 0 { + v = v[:i] + } + return strings.TrimSpace(v) + } + } + return "" +} + +// writable names the calendars that can take a new event. +func writable() []string { + var out []string + for _, c := range calendars() { + b, err := os.ReadFile(path.Join(mtpt, "calendars", c, "ctl")) + if err != nil { + continue + } + for _, l := range strings.Split(string(b), "\n") { + if v, ok := strings.CutPrefix(l, "caps "); ok { + for _, f := range strings.Fields(v) { + if f == "write" { + out = append(out, c) + } + } + } + } + } + return out +} + +// seconds parses a human date the way seconds(1) does, by asking it. +// Reimplementing tmparse here would be a second answer to a question +// the system already answers. +func seconds(when string) (int64, error) { + out, err := exec.Command("/bin/seconds", when).Output() + if err != nil { + return 0, err + } + var n int64 + if _, err := fmt.Sscan(strings.TrimSpace(string(out)), &n); err != nil { + return 0, err + } + return n, nil +} |
