diff options
Diffstat (limited to 'pim/cmd/cal9/event.go')
| -rw-r--r-- | pim/cmd/cal9/event.go | 164 |
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{} |
