summaryrefslogtreecommitdiff
path: root/pim/cmd/cal9/main.go
diff options
context:
space:
mode:
Diffstat (limited to 'pim/cmd/cal9/main.go')
-rw-r--r--pim/cmd/cal9/main.go531
1 files changed, 204 insertions, 327 deletions
diff --git a/pim/cmd/cal9/main.go b/pim/cmd/cal9/main.go
index aa9a370..26f7de0 100644
--- a/pim/cmd/cal9/main.go
+++ b/pim/cmd/cal9/main.go
@@ -16,6 +16,7 @@ import (
"time"
"9front/gui/draw"
+ "9front/gui/ui"
)
const (
@@ -34,27 +35,33 @@ const (
vDay view = iota
vWeek
vMonth
+ vEvent
+ vInvite
)
-var viewName = map[view]string{vDay: "Day", vWeek: "Week", vMonth: "Month"}
+var viewName = map[view]string{vDay: "Day", vWeek: "Week", vMonth: "Month", vEvent: "Event", vInvite: "Invite"}
// One colour per calendar. The fill is pale enough to take black text in
// the day and week views; the ink is the same hue darkened, for the month
// grid where events are text on the background rather than in a box.
-var calPalette = []struct{ fill, ink uint32 }{
- {0x8888CCFF, 0x333388FF}, // purpleblue, the original event colour
- {0x88CC88FF, 0x226622FF}, // green
- {0xE0B080FF, 0x805000FF}, // tan
- {0xCC8888FF, 0x883333FF}, // red
- {0x88CCCCFF, 0x226666FF}, // cyan
- {0xCCCC88FF, 0x666622FF}, // olive
+// One colour per calendar, plus a pale version of each. An event you
+// have not answered is drawn pale, so a glance tells you what is still
+// waiting without opening anything.
+var calPalette = []struct{ fill, pale, ink uint32 }{
+ {0x8888CCFF, 0xD8D8F0FF, 0x333388FF}, // purpleblue, the original
+ {0x88CC88FF, 0xD8F0D8FF, 0x226622FF}, // green
+ {0xE0B080FF, 0xF4E2CCFF, 0x805000FF}, // tan
+ {0xCC8888FF, 0xF0D8D8FF, 0x883333FF}, // red
+ {0x88CCCCFF, 0xD8F0F0FF, 0x226666FF}, // cyan
+ {0xCCCC88FF, 0xF0F0D8FF, 0x666622FF}, // olive
}
type event struct {
- min int // minutes past midnight
- title string
- file string
- cal string // which calendar it came from
+ min int // minutes past midnight
+ title string
+ file string
+ cal string // which calendar it came from
+ pending bool // you have not answered it yet
}
// selLine is one line of panel text, recorded so it can be selected.
@@ -63,38 +70,29 @@ type selLine struct {
text string
}
-// hit is a clickable region recorded during redraw.
-type hit struct {
- r draw.Rectangle
- do func(*state)
-}
-
type state struct {
- d *draw.Display
- win *draw.Image
- f *draw.Font
- col map[string]*draw.Image
+ ui.UI
view view
cals []string // every calendar mounted under mtpt
on map[string]bool // the ones being shown
- pick time.Time // the day whose list is in the panel; zero for none
// Text selection in the panel, by line. Character granularity would
// need the font metrics per glyph; whole lines are what you want to
// snarf out of a calendar anyway.
- panelR draw.Rectangle
- backAt draw.Rectangle
- openAt draw.Rectangle
- selLines []selLine
- selA int // first selected line, -1 for none
- selB int
- at time.Time // the anchor day
- top int // first visible minute, day/week views
- evs map[string][]event
- hits []hit
- sel *event // the opened event, if any
- mc *draw.Mousectl
- quit bool
+ panelR draw.Rectangle
+ backAt draw.Rectangle
+ openAt draw.Rectangle
+ selLines []selLine
+ selA int // first selected line, -1 for none
+ selB int
+ at time.Time // the anchor day
+ top int // first visible minute, day/week views
+ evs map[string][]event
+ eventPath string // -v event: the occurrence this window is showing
+ form *form // -v invite: the composer
+ picked chan string // a date coming back from datepick
+ mc *draw.Mousectl
+ quit bool
}
func now() time.Time { return time.Now() }
@@ -124,8 +122,12 @@ func run() error {
want = vWeek
case "month":
want = vMonth
+ case "event":
+ want = vEvent
+ case "invite":
+ want = vInvite
default:
- return fmt.Errorf("usage: cal [-n] [-v day|week|month] [YYYY-MM-DD]")
+ return fmt.Errorf("usage: cal [-n] [-v day|week|month|event] [YYYY-MM-DD|path]")
}
args = args[2:]
default:
@@ -135,15 +137,32 @@ func run() error {
done:
s := &state{view: want, at: time.Now(), top: 8 * 60, selA: -1, selB: -1}
+ s.picked = make(chan string, 1)
s.cals = calendars()
s.on = map[string]bool{}
for _, c := range s.cals {
s.on[c] = true
}
- if len(args) > 0 {
+ if want == vInvite {
+ w := writable()
+ if len(args) > 0 {
+ s.form = newForm(args[0])
+ } else if len(w) == 1 {
+ s.form = newForm(w[0])
+ } else if len(w) == 0 {
+ return fmt.Errorf("cal: no calendar here can create events")
+ } else {
+ return fmt.Errorf("cal: which calendar? one of: %s", strings.Join(w, " "))
+ }
+ } else if want == vEvent {
+ if len(args) == 0 {
+ return fmt.Errorf("usage: cal -v event <occurrence path>")
+ }
+ s.eventPath = args[0]
+ } else if len(args) > 0 {
t, err := time.Parse("2006-01-02", args[0])
if err != nil {
- return fmt.Errorf("usage: cal [-n] [-v day|week|month] [YYYY-MM-DD]")
+ return fmt.Errorf("usage: cal [-n] [-v day|week|month|event] [YYYY-MM-DD|path]")
}
s.at = t
}
@@ -165,14 +184,14 @@ done:
}
var err error
- if s.d, err = draw.Init("/dev"); err != nil {
+ if s.D, err = draw.Init("/dev"); err != nil {
return err
}
- defer s.d.Close()
- if s.f, err = s.d.OpenFont(fontpath); err != nil {
+ defer s.D.Close()
+ if s.F, err = s.D.OpenFont(fontpath); err != nil {
return err
}
- s.col = map[string]*draw.Image{}
+ s.Col = map[string]*draw.Image{}
for k, v := range map[string]uint32{
"bg": 0xFFFFEAFF, // acme body
"tag": 0xEAFFFFFF, // acme tag
@@ -185,7 +204,7 @@ done:
"now": 0xCC0000FF,
"border": 0x8888CCFF,
} {
- if s.col[k], err = s.d.Color(v); err != nil {
+ if s.Col[k], err = s.D.Color(v); err != nil {
return err
}
}
@@ -193,15 +212,18 @@ done:
// calendars than colours
for i := range s.cals {
p := calPalette[i%len(calPalette)]
- if s.col[fmt.Sprintf("fill%d", i)], err = s.d.Color(p.fill); err != nil {
+ if s.Col[fmt.Sprintf("fill%d", i)], err = s.D.Color(p.fill); err != nil {
return err
}
- if s.col[fmt.Sprintf("ink%d", i)], err = s.d.Color(p.ink); err != nil {
+ if s.Col[fmt.Sprintf("pale%d", i)], err = s.D.Color(p.pale); err != nil {
+ return err
+ }
+ if s.Col[fmt.Sprintf("ink%d", i)], err = s.D.Color(p.ink); err != nil {
return err
}
}
- if s.win, err = s.d.Window("/dev"); err != nil {
+ if s.Win, err = s.D.Window("/dev"); err != nil {
return err
}
@@ -239,7 +261,7 @@ done:
if m.Buttons&4 != 0 {
// Button 3 is the menu button on this system; Exit lives
// in there rather than owning the whole button.
- s.menu(m.Point, mc)
+ s.Menu(s.menuItems(), m.Point, mc, s.redraw)
if s.quit {
return nil
}
@@ -264,7 +286,7 @@ done:
s.redraw()
} else {
s.clearSel()
- s.click(m.Point)
+ s.Click(m.Point)
}
case down && dragging:
if i := s.lineAt(m.Point); i >= 0 && i != s.selB {
@@ -276,7 +298,7 @@ done:
if s.selA == s.selB {
// no drag: it was a click after all
s.clearSel()
- s.click(pressed)
+ s.Click(pressed)
} else {
s.snarfSel()
}
@@ -287,6 +309,23 @@ done:
keys = nil
continue
}
+ // In a form the keys are the point; the view shortcuts
+ // would eat every letter you tried to type.
+ if s.view == vInvite && s.form != nil {
+ f := s.form
+ switch r {
+ case '\t', '\n', '\r':
+ f.focus = (f.focus + 1) % len(f.fields)
+ s.redraw()
+ case 0x1B: // Esc gives up on the window
+ return nil
+ default:
+ if f.fields[f.focus].Key(r) {
+ s.redraw()
+ }
+ }
+ continue
+ }
switch r {
case 'q', 0x7F: // q or Del
return nil
@@ -312,13 +351,21 @@ done:
s.load()
s.redraw()
case 0x1B: // Esc clears whatever is showing
- s.sel = nil
- s.pick = time.Time{}
s.clearSel()
s.redraw()
}
+ case v := <-s.picked:
+ if s.form != nil && v != "" {
+ for _, fl := range s.form.fields {
+ if fl.Label == "when" {
+ fl.Value = v
+ }
+ }
+ s.redraw()
+ }
+
case <-mc.Resize:
- if s.win, err = s.d.Reattach("/dev", s.win); err != nil {
+ if s.Win, err = s.D.Reattach("/dev", s.Win); err != nil {
return err
}
s.redraw()
@@ -366,15 +413,6 @@ func sign(n int) int {
return 1
}
-func (s *state) click(p draw.Point) {
- for _, h := range s.hits {
- if p.X >= h.r.Min.X && p.X < h.r.Max.X && p.Y >= h.r.Min.Y && p.Y < h.r.Max.Y {
- h.do(s)
- return
- }
- }
-}
-
// ---------------------------------------------------------------- loading
// days returns the days the current view covers.
@@ -402,6 +440,9 @@ func (s *state) days() []time.Time {
func weekday(t time.Time) int { return int(t.Weekday()) }
func (s *state) load() {
+ if s.view == vEvent || s.view == vInvite {
+ return // these views read what they need themselves
+ }
s.evs = map[string][]event{}
for _, d := range s.days() {
var all []event
@@ -477,6 +518,7 @@ func readDay(cal string, t time.Time) ([]event, error) {
if t := summary(ev.file); t != "" {
ev.title = t
}
+ ev.pending = needsReply(ev.file)
evs = append(evs, ev)
}
sort.Slice(evs, func(i, j int) bool { return evs[i].min < evs[j].min })
@@ -512,7 +554,7 @@ func (s *state) calKey(what, cal string) string {
for i, c := range s.cals {
if c == cal {
k := fmt.Sprintf("%s%d", what, i)
- if _, ok := s.col[k]; ok {
+ if _, ok := s.Col[k]; ok {
return k
}
}
@@ -523,62 +565,28 @@ func (s *state) calKey(what, cal string) string {
return "ink"
}
-func (s *state) fit(str string, w int32) string {
- if s.f.Width(str) <= w {
- return str
- }
- r := []rune(str)
- for len(r) > 1 {
- r = r[:len(r)-1]
- if s.f.Width(string(r)+"..") <= w {
- return string(r) + ".."
- }
- }
- return ""
-}
-
-func (s *state) text(p draw.Point, c string, str string) {
- s.f.String(s.win, p, s.col[c], str)
-}
-
-func (s *state) fill(r draw.Rectangle, c string) {
- draw.Draw(s.win, r, s.col[c], nil, draw.ZP)
-}
-
-// button draws a labelled box and records its hit region.
-func (s *state) button(r draw.Rectangle, label string, on bool, do func(*state)) {
- bg := "tag"
- if on {
- bg = "today"
- }
- s.fill(r, bg)
- s.fill(draw.Rect(r.Min.X, r.Min.Y, r.Max.X, r.Min.Y+1), "border")
- s.fill(draw.Rect(r.Min.X, r.Max.Y-1, r.Max.X, r.Max.Y), "border")
- s.fill(draw.Rect(r.Min.X, r.Min.Y, r.Min.X+1, r.Max.Y), "border")
- s.fill(draw.Rect(r.Max.X-1, r.Min.Y, r.Max.X, r.Max.Y), "border")
- w := s.f.Width(label)
- s.text(draw.Point{X: r.Min.X + (r.Dx()-w)/2, Y: r.Min.Y + 3}, "ink", label)
- s.hits = append(s.hits, hit{r, do})
-}
-
func (s *state) redraw() {
- s.hits = s.hits[:0]
- r := s.win.Rect()
- s.fill(r, "bg")
+ s.Reset()
+ r := s.Body()
+ s.Fill(r, "bg")
if r.Dy() < compactH {
s.drawCompact(r)
- s.d.Flush()
+ s.D.Flush()
+ return
+ }
+
+ if s.view == vEvent {
+ s.drawEvent(r)
+ return
+ }
+ if s.view == vInvite {
+ s.drawInvite(r)
return
}
hdr := s.drawHeader(r)
body := draw.Rect(r.Min.X, hdr.Max.Y+pad, r.Max.X, r.Max.Y-pad)
- if (s.sel != nil || (!s.pick.IsZero() && s.view != vDay)) && body.Dy() > 160 {
- split := body.Max.Y - body.Dy()*2/5
- s.drawDetail(draw.Rect(body.Min.X, split, body.Max.X, body.Max.Y))
- body.Max.Y = split - pad
- }
switch s.view {
case vDay:
s.drawTimeGrid(body, []time.Time{s.at})
@@ -587,45 +595,44 @@ func (s *state) redraw() {
case vMonth:
s.drawMonth(body)
}
- s.d.Flush()
+ s.D.Flush()
}
func (s *state) drawHeader(r draw.Rectangle) draw.Rectangle {
- h := int32(s.f.Height) + 8
+ h := int32(s.F.Height) + 8
hdr := draw.Rect(r.Min.X, r.Min.Y, r.Max.X, r.Min.Y+h)
- s.fill(hdr, "tag")
+ s.Fill(hdr, "tag")
// Buttons, right to left: Month Week Day > <
x := r.Max.X - pad
- mk := func(label string, on bool, do func(*state)) {
- w := s.f.Width(label) + 16
+ mk := func(label string, on bool, do func()) {
+ w := s.F.Width(label) + 16
br := draw.Rect(x-w, hdr.Min.Y+2, x, hdr.Max.Y-2)
- s.button(br, label, on, do)
+ s.Button(br, label, on, do)
x = br.Min.X - 4
}
for _, v := range []view{vMonth, vWeek, vDay} {
vv := v
- mk(viewName[v], s.view == v, func(st *state) {
- st.view = vv
+ mk(viewName[v], s.view == v, func() {
+ s.view = vv
if vv == vDay {
- st.pick = time.Time{}
}
- st.load()
- st.redraw()
+ s.load()
+ s.redraw()
})
}
x -= 6
- mk(">", false, func(st *state) { st.step(1) })
- mk("<", false, func(st *state) { st.step(-1) })
+ mk(">", false, func() { s.step(1) })
+ mk("<", false, func() { s.step(-1) })
x -= 6
- mk("Today", false, func(st *state) {
- st.at = time.Now()
- st.top = 8 * 60
- st.load()
- st.redraw()
+ mk("Today", false, func() {
+ s.at = time.Now()
+ s.top = 8 * 60
+ s.load()
+ s.redraw()
})
- s.text(draw.Point{X: r.Min.X + pad, Y: hdr.Min.Y + 4}, "ink", s.fit(s.title(), x-r.Min.X-pad*2))
+ s.Text(draw.Point{X: r.Min.X + pad, Y: hdr.Min.Y + 4}, "ink", s.Fit(s.title(), x-r.Min.X-pad*2))
return hdr
}
@@ -642,7 +649,7 @@ func (s *state) title() string {
// drawTimeGrid renders one or more day columns against an hour axis.
func (s *state) drawTimeGrid(r draw.Rectangle, days []time.Time) {
- top, bot := r.Min.Y+int32(s.f.Height)+4, r.Max.Y
+ top, bot := r.Min.Y+int32(s.F.Height)+4, r.Max.Y
if bot <= top {
return
}
@@ -652,8 +659,8 @@ func (s *state) drawTimeGrid(r draw.Rectangle, days []time.Time) {
// Hour rules across the whole body.
for m := (s.top/60 + 1) * 60; m < s.top+spanMin; m += 60 {
y := yOf(m)
- s.fill(draw.Rect(r.Min.X+gutter, y, r.Max.X-pad, y+1), "rule")
- s.text(draw.Point{X: r.Min.X + pad, Y: y - int32(s.f.Height)/2}, "ink",
+ s.Fill(draw.Rect(r.Min.X+gutter, y, r.Max.X-pad, y+1), "rule")
+ s.Text(draw.Point{X: r.Min.X + pad, Y: y - int32(s.F.Height)/2}, "ink",
fmt.Sprintf("%02d:00", m/60))
}
@@ -669,14 +676,12 @@ func (s *state) drawTimeGrid(r draw.Rectangle, days []time.Time) {
c := "ink"
hdr := draw.Rect(cr.Min.X, r.Min.Y, cr.Max.X, top-2)
switch {
- case !s.pick.IsZero() && key == s.pick.Format("2006-01-02"):
- s.fill(hdr, "pick")
case key == time.Now().Format("2006-01-02"):
- s.fill(hdr, "today")
+ s.Fill(hdr, "today")
}
- s.text(draw.Point{X: cr.Min.X + 4, Y: r.Min.Y}, c, s.fit(lab, colw-8))
+ s.Text(draw.Point{X: cr.Min.X + 4, Y: r.Min.Y}, c, s.Fit(lab, colw-8))
dd := d
- s.hits = append(s.hits, hit{hdr, func(st *state) { st.pickDay(dd) }})
+ s.On(hdr, func() { s.pickDay(dd) })
}
for _, e := range s.evs[key] {
@@ -684,19 +689,20 @@ func (s *state) drawTimeGrid(r draw.Rectangle, days []time.Time) {
continue
}
y := yOf(e.min)
- box := draw.Rect(cr.Min.X+2, y+1, cr.Max.X, y+int32(s.f.Height)+6)
+ box := draw.Rect(cr.Min.X+2, y+1, cr.Max.X, y+int32(s.F.Height)+6)
if box.Max.Y > bot {
continue
}
- s.fill(box, s.calKey("fill", e.cal))
+ kind := "fill"
+ if e.pending {
+ kind = "pale" // unanswered: lighter than accepted
+ }
+ s.Fill(box, s.calKey(kind, e.cal))
lab := fmt.Sprintf("%02d:%02d %s", e.min/60, e.min%60, e.title)
- s.text(draw.Point{X: box.Min.X + 4, Y: box.Min.Y + 2}, "ink",
- s.fit(lab, box.Dx()-8))
+ s.Text(draw.Point{X: box.Min.X + 4, Y: box.Min.Y + 2}, "ink",
+ s.Fit(lab, box.Dx()-8))
ev := e
- s.hits = append(s.hits, hit{box, func(st *state) {
- st.sel = &ev
- st.redraw()
- }})
+ s.On(box, func() { s.openEventWindow(&ev) })
}
// Now line.
@@ -704,7 +710,7 @@ func (s *state) drawTimeGrid(r draw.Rectangle, days []time.Time) {
m := time.Now().Hour()*60 + time.Now().Minute()
if m >= s.top && m <= s.top+spanMin {
y := yOf(m)
- s.fill(draw.Rect(cr.Min.X, y, cr.Max.X, y+2), "now")
+ s.Fill(draw.Rect(cr.Min.X, y, cr.Max.X, y+2), "now")
}
}
}
@@ -719,14 +725,10 @@ func (s *state) drawMonth(r draw.Rectangle) {
return
}
today := time.Now().Format("2006-01-02")
- picked := ""
- if !s.pick.IsZero() {
- picked = s.pick.Format("2006-01-02")
- }
- lh := int32(s.f.Height) + 1
+ lh := int32(s.F.Height) + 1
// A day cell covers its events, and click takes the first hit, so
// these are appended only once every event hit is already down.
- var dayHits []hit
+ var dayHits []ui.Hit
for i, d := range days {
cx := r.Min.X + int32(i%7)*cw
cy := r.Min.Y + int32(i/7)*ch
@@ -734,18 +736,16 @@ func (s *state) drawMonth(r draw.Rectangle) {
key := d.Format("2006-01-02")
switch {
- case key == picked:
- s.fill(cell, "pick")
case key == today:
- s.fill(cell, "today")
+ s.Fill(cell, "today")
}
if d.Month() != s.at.Month() {
// Outside the anchor month: leave it on the background.
- s.fill(draw.Rect(cell.Min.X, cell.Min.Y, cell.Max.X, cell.Min.Y+1), "rule")
+ s.Fill(draw.Rect(cell.Min.X, cell.Min.Y, cell.Max.X, cell.Min.Y+1), "rule")
} else {
- s.fill(draw.Rect(cell.Min.X, cell.Min.Y, cell.Max.X, cell.Min.Y+1), "rule")
+ s.Fill(draw.Rect(cell.Min.X, cell.Min.Y, cell.Max.X, cell.Min.Y+1), "rule")
}
- s.text(draw.Point{X: cell.Min.X + 3, Y: cell.Min.Y + 2}, "ink", d.Format("2"))
+ s.Text(draw.Point{X: cell.Min.X + 3, Y: cell.Min.Y + 2}, "ink", d.Format("2"))
y := cell.Min.Y + 2 + lh
for _, e := range s.evs[key] {
@@ -753,37 +753,46 @@ func (s *state) drawMonth(r draw.Rectangle) {
break
}
lab := fmt.Sprintf("%02d:%02d %s", e.min/60, e.min%60, e.title)
- s.text(draw.Point{X: cell.Min.X + 3, Y: y}, s.calKey("ink", e.cal),
- s.fit(lab, cell.Dx()-6))
+ s.Text(draw.Point{X: cell.Min.X + 3, Y: y}, s.calKey("ink", e.cal),
+ s.Fit(lab, cell.Dx()-6))
ev := e
- s.hits = append(s.hits, hit{
- draw.Rect(cell.Min.X, y, cell.Max.X, y+lh),
- func(st *state) { st.sel = &ev; st.redraw() },
- })
+ s.On(draw.Rect(cell.Min.X, y, cell.Max.X, y+lh), func() { s.openEventWindow(&ev) })
y += lh
}
dd := d
- dayHits = append(dayHits, hit{cell, func(st *state) { st.pickDay(dd) }})
+ dayHits = append(dayHits, ui.Hit{R: cell, Do: func() { s.pickDay(dd) }})
+ }
+ for _, h := range dayHits {
+ s.On(h.R, h.Do)
}
- s.hits = append(s.hits, dayHits...)
}
-func min(a, b int) int { if a < b { return a }; return b }
-func max(a, b int) int { if a > b { return a }; return b }
+func min(a, b int) int {
+ if a < b {
+ return a
+ }
+ return b
+}
+func max(a, b int) int {
+ if a > b {
+ return a
+ }
+ return b
+}
func (s *state) clearSel() { s.selA, s.selB = -1, -1 }
// openCal starts another cal in its own window. A second view is a
// second process here, not a second window inside this one.
func (s *state) openCal(v view, d time.Time) {
- s.wctl(fmt.Sprintf("new -dx 820 -dy 620 cal9 -v %s %s",
+ s.Wctl(fmt.Sprintf("new -dx 820 -dy 620 cal9 -v %s %s",
strings.ToLower(viewName[v]), d.Format("2006-01-02")))
}
// openEvent pins one event in a window of its own.
func (s *state) openEvent(e *event) {
- s.wctl(fmt.Sprintf("new -dx 540 -dy 360 pim/showwin %s", e.file))
+ s.Wctl(fmt.Sprintf("new -dx 540 -dy 360 pim/showwin %s", e.file))
}
// inBack reports whether a point is on the panel's Back button, which
@@ -822,19 +831,17 @@ func (s *state) snarfSel() {
}
}
-// pickDay puts a whole day in the panel below. A month cell only has
-// room for the first few events, and this is how you see the rest without
-// losing the month.
+// pickDay opens a day in a window of its own. A month cell has room for
+// the first few events; this is how you see the rest, and the month
+// stays where it was.
func (s *state) pickDay(d time.Time) {
- s.pick = d
- s.sel = nil
- s.redraw()
+ s.New(820, 620, "cal9", "-v", "day", d.Format("2006-01-02"))
}
// drawCompact is what you get by making the window small: the next thing
// due. No minimise button, because rio has no iconify to hook one to.
func (s *state) drawCompact(r draw.Rectangle) {
- s.fill(r, "tag")
+ s.Fill(r, "tag")
now := time.Now()
key := now.Format("2006-01-02")
cur := now.Hour()*60 + now.Minute()
@@ -847,158 +854,15 @@ func (s *state) drawCompact(r draw.Rectangle) {
}
y := r.Min.Y + 2
if next == nil {
- s.text(draw.Point{X: r.Min.X + pad, Y: y}, "ink",
- s.fit("nothing else today", r.Dx()-pad*2))
+ s.Text(draw.Point{X: r.Min.X + pad, Y: y}, "ink",
+ s.Fit("nothing else today", r.Dx()-pad*2))
return
}
in := next.min - cur
- s.text(draw.Point{X: r.Min.X + pad, Y: y}, "ink",
- s.fit(fmt.Sprintf("%02d:%02d %s", next.min/60, next.min%60, next.title), r.Dx()-pad*2))
- s.text(draw.Point{X: r.Min.X + pad, Y: y + int32(s.f.Height) + 2}, "ink",
- s.fit(fmt.Sprintf("in %d min", in), r.Dx()-pad*2))
-}
-
-// drawDetail shows the opened event. The date tree carries only the basics;
-// everything else -- attendees, organizer, location, the full description --
-// lives in the per-event directory in the uuid tree, which the "event:"
-// header points at relative to the day directory.
-func (s *state) drawDetail(r draw.Rectangle) {
- s.fill(r, "tag")
- s.fill(draw.Rect(r.Min.X, r.Min.Y, r.Max.X, r.Min.Y+1), "border")
- s.panelR = r
- s.selLines = s.selLines[:0]
-
- y := r.Min.Y + 4
- 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
- }
-
- // A real button, like the ones in the header, rather than a line of
- // text that happens to be clickable.
- // Back, and Open beside it. Open means "put this in a window of its
- // own and leave it there".
- bar := func(back func(*state), open func(*state)) {
- x := r.Min.X + pad
- mk := func(label string, do func(*state)) draw.Rectangle {
- bw := s.f.Width(label) + 20
- br := draw.Rect(x, y, x+bw, y+lh+2)
- s.button(br, label, false, do)
- x = br.Max.X + 6
- return br
- }
- s.backAt = mk("Back", back)
- s.openAt = mk("Open", open)
- y = s.openAt.Max.Y + 4
- }
-
- // A day was clicked and no single event chosen: list the day.
- if s.sel == nil {
- key := s.pick.Format("2006-01-02")
- evs := s.evs[key]
- // Back from a day means no panel at all; the month is behind it.
- d := s.pick
- bar(func(st *state) { st.pick = time.Time{}; st.clearSel(); st.redraw() },
- func(st *state) { st.openCal(vDay, d) })
- line("ink", s.pick.Format("Monday 2 January 2006")+
- fmt.Sprintf(" (%d)", len(evs)))
- y += 3
- for _, e := range evs {
- ev := e
- top := y
- lab := fmt.Sprintf("%02d:%02d %s", e.min/60, e.min%60, e.title)
- if e.min == 0 && strings.HasPrefix(path.Base(e.file), "0000-allday-") {
- lab = "all day " + e.title
- }
- line(s.calKey("ink", e.cal), lab)
- if y > top {
- s.hits = append(s.hits, hit{
- draw.Rect(r.Min.X, top, r.Max.X, y),
- func(st *state) { st.sel = &ev; st.redraw() },
- })
- }
- }
- if len(evs) == 0 {
- line("ink", "nothing")
- }
- return
- }
-
- e := s.sel
- hdrs, body := readEvent(e.file)
-
- // A way back up. Coming from a month cell there is otherwise nothing
- // to click but the menu, and no sign of which day this was.
- day, haveDay := dayOf(hdrs["start"])
- if haveDay && s.view != vDay {
- d := day
- ev := e
- bar(func(st *state) { st.pick = d; st.sel = nil; st.clearSel(); st.redraw() },
- func(st *state) { st.openEvent(ev) })
- line("ink", d.Format("Monday 2 January 2006"))
- y += 3
- } else {
- // In day view the grid behind is already the whole day, so
- // there is nothing to go back to but the grid itself.
- ev := e
- bar(func(st *state) {
- st.sel = nil
- st.pick = time.Time{}
- st.clearSel()
- st.redraw()
- }, func(st *state) { st.openEvent(ev) })
- if haveDay {
- line("ink", day.Format("Monday 2 January 2006"))
- y += 3
- }
- }
-
- line("ink", hdrs["summary"])
- if st, en := hdrs["start"], hdrs["end"]; st != "" {
- when := clock(st) + " - " + clock(en)
- if !haveDay {
- when = st
- }
- if hdrs["rrule"] != "" {
- when += " (repeats)"
- }
- line("ink", when)
- }
-
- det := uuidDir(e.file, hdrs["event"])
- if loc := strings.TrimSpace(field(det, "location")); loc != "" {
- line("ink", "at "+loc)
- }
- if org := strings.TrimSpace(field(det, "organizer")); org != "" {
- line("ink", "organizer: "+org)
- }
- if att := attendees(det); len(att) > 0 {
- y += 3
- line("ink", fmt.Sprintf("%d attendees", len(att)))
- for _, a := range att {
- line("ink", " "+a)
- }
- }
-
- if strings.TrimSpace(body) == "" {
- body = field(det, "description")
- }
- if strings.TrimSpace(body) != "" {
- y += 3
- for _, w := range wrap(s, body, r.Dx()-pad*2) {
- line("ink", w)
- }
- }
+ s.Text(draw.Point{X: r.Min.X + pad, Y: y}, "ink",
+ s.Fit(fmt.Sprintf("%02d:%02d %s", next.min/60, next.min%60, next.title), r.Dx()-pad*2))
+ s.Text(draw.Point{X: r.Min.X + pad, Y: y + int32(s.F.Height) + 2}, "ink",
+ s.Fit(fmt.Sprintf("in %d min", in), r.Dx()-pad*2))
}
// uuidDir resolves the "event:" cross-link, which is relative to the day
@@ -1114,7 +978,7 @@ func wrap(s *state, text string, w int32) []string {
if cur != "" {
try = cur + " " + word
}
- if s.f.Width(try) <= w {
+ if s.F.Width(try) <= w {
cur = try
continue
}
@@ -1129,3 +993,16 @@ func wrap(s *state, text string, w int32) []string {
}
return out
}
+
+// needsReply says whether this event is still waiting on you. The
+// answer lives with the event, not the occurrence, so it costs one
+// more read per event -- a few dozen for a week, which is cheap enough
+// to be worth seeing at a glance.
+func needsReply(occ string) bool {
+ hdrs, _ := readEvent(occ)
+ det := uuidDir(occ, hdrs["event"])
+ if det == "" {
+ return false
+ }
+ return strings.TrimSpace(field(det, "partstat")) == "NEEDS-ACTION"
+}