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/ 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{}