summaryrefslogtreecommitdiff
path: root/pim/cmd/cal9/event.go
diff options
context:
space:
mode:
Diffstat (limited to 'pim/cmd/cal9/event.go')
-rw-r--r--pim/cmd/cal9/event.go164
1 files changed, 164 insertions, 0 deletions
diff --git a/pim/cmd/cal9/event.go b/pim/cmd/cal9/event.go
new file mode 100644
index 0000000..6d23b11
--- /dev/null
+++ b/pim/cmd/cal9/event.go
@@ -0,0 +1,164 @@
+package main
+
+// The event view: one event, in a window of its own.
+//
+// Spawned by clicking an event in any grid, or from the command line:
+//
+// cal9 -v event /mnt/pim/calendars/work/events/date/2026/08/24/1000-API-WG
+//
+// It exists rather than a panel because a window is a thing you can
+// keep, put beside the calendar, and close when you are done -- and
+// because it can carry buttons the grid has no room for. Answering an
+// invitation is the whole reason: pim/showwin can print an event, but
+// it cannot offer you Accept.
+
+import (
+ "fmt"
+ "os"
+ "path"
+ "strings"
+ "time"
+
+ "9front/gui/draw"
+ "9front/gui/ui"
+)
+
+// drawEvent fills the window with one event.
+func (s *state) drawEvent(r draw.Rectangle) {
+ s.Fill(r, "bg")
+ hdrs, body := readEvent(s.eventPath)
+ det := uuidDir(s.eventPath, hdrs["event"])
+
+ y := r.Min.Y + pad
+ lh := int32(s.F.Height) + 2
+ line := func(c, str string) {
+ if y+lh > r.Max.Y {
+ return
+ }
+ lr := draw.Rect(r.Min.X, y, r.Max.X, y+lh)
+ i := len(s.selLines)
+ if s.selA >= 0 && i >= min(s.selA, s.selB) && i <= max(s.selA, s.selB) {
+ s.Fill(lr, "snarf")
+ }
+ s.selLines = append(s.selLines, selLine{lr, str})
+ s.Text(draw.Point{X: r.Min.X + pad, Y: y}, c, s.Fit(str, r.Dx()-pad*2))
+ y += lh
+ }
+ s.selLines = s.selLines[:0]
+
+ // what it is
+ line("ink", hdrs["summary"])
+ if d, ok := dayOf(hdrs["start"]); ok {
+ line("ink", d.Format("Monday 2 January 2006")+" "+
+ clock(hdrs["start"])+" - "+clock(hdrs["end"]))
+ }
+ if hdrs["rrule"] != "" {
+ line("ink", "repeats "+hdrs["rrule"])
+ }
+ if loc := strings.TrimSpace(field(det, "location")); loc != "" {
+ line("ink", "at "+loc)
+ }
+ if org := strings.TrimSpace(field(det, "organizer")); org != "" {
+ line("ink", "from "+org)
+ }
+ y += 4
+
+ // who, and where you stand
+ st := strings.TrimSpace(field(det, "partstat"))
+ if att := attendees(det); len(att) > 0 {
+ line("ink", fmt.Sprintf("%d attendees", len(att)))
+ for _, a := range att {
+ line("ink", " "+a)
+ }
+ y += 4
+ }
+ if st != "" {
+ line("ink", "you: "+st)
+ }
+
+ // and what you can do about it
+ y += 4
+ if s.canRSVP() {
+ x := r.Min.X + pad
+ for _, w := range []string{"ACCEPTED", "TENTATIVE", "DECLINED"} {
+ want := w
+ label := strings.Title(strings.ToLower(w))
+ bw := s.F.Width(label) + 20
+ br := draw.Rect(x, y, x+bw, y+lh+4)
+ s.Button(br, label, st == want, func() { s.rsvp(want) })
+ x = br.Max.X + 6
+ }
+ y += lh + 10
+ } else if st != "" {
+ line("rule", "this calendar cannot answer invitations")
+ }
+
+ if b := strings.TrimSpace(body); b != "" {
+ y += 4
+ for _, l := range s.Wrap(b, r.Dx()-pad*2) {
+ line("ink", l)
+ }
+ }
+ s.D.Flush()
+}
+
+// canRSVP asks the calendar, rather than guessing from the event.
+func (s *state) canRSVP() bool {
+ c := calRoot(s.eventPath)
+ if c == "" {
+ return false
+ }
+ b, err := os.ReadFile(path.Join(c, "ctl"))
+ if err != nil {
+ return false
+ }
+ for _, l := range strings.Split(string(b), "\n") {
+ if v, ok := strings.CutPrefix(l, "caps "); ok {
+ for _, f := range strings.Fields(v) {
+ if f == "rsvp" {
+ return true
+ }
+ }
+ }
+ }
+ return false
+}
+
+// rsvp answers, by writing the file. Whether that means a PUT, a piece
+// of mail or a refusal is the backend's business.
+func (s *state) rsvp(want string) {
+ hdrs, _ := readEvent(s.eventPath)
+ det := uuidDir(s.eventPath, hdrs["event"])
+ if det == "" {
+ return
+ }
+ f, err := os.OpenFile(path.Join(det, "partstat"), os.O_WRONLY, 0)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "cal: rsvp: %v\n", err)
+ return
+ }
+ _, err = f.WriteString(want)
+ f.Close()
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "cal: rsvp: %v\n", err)
+ }
+ s.redraw()
+}
+
+// calRoot walks back to /mnt/pim/calendars/<name> from a path inside it.
+func calRoot(p string) string {
+ for d := p; d != "/" && d != "."; d = path.Dir(d) {
+ if path.Base(path.Dir(d)) == "calendars" {
+ return d
+ }
+ }
+ return ""
+}
+
+// openEventWindow puts an event in a window of its own.
+func (s *state) openEventWindow(e *event) {
+ s.New(560, 420, "cal9", "-v", "event", e.file)
+}
+
+var _ = ui.Hit{}
+var _ = time.Time{}