diff options
| author | Calvin Morrison <calvin@pobox.com> | 2026-08-22 19:12:15 -0400 |
|---|---|---|
| committer | Calvin Morrison <calvin@pobox.com> | 2026-08-22 19:12:15 -0400 |
| commit | c16c778c60deaafa0c9e5bbc1db9a10f64fec9b2 (patch) | |
| tree | aced25a7611486d61f20094728b41fe322ad6feb /pim/cmd/cal9/menu.go | |
| parent | 77c7bc9b187450818ec068239a3ae836d01fdebc (diff) | |
cal9 kept an event in a panel and a day in another, with the state that
implies: a selection, a picked day, rectangles for the buttons on it,
and a hundred-odd lines to draw it. Opening a window instead deletes all
of that rather than tidying it away. Clicking an event opens cal9 -v
event, clicking a day opens cal9 -v day, and cal9 is three grids again.
The event window earns being cal9 rather than pim/showwin: it carries
Accept, Tentative and Declined, lit to show where you stand, and only
when the calendar's ctl says it can rsvp. cal9 -v invite is a form that
writes the calendar's new file in the tree's key: value form, so the
composer knows nothing about iCalendar either.
datepick is a program rather than a widget so anything can use it:
pim/agenda -d `{datepick}
-t asks for a time as well, in stages, because a grid of ninety-six
five-minute slots is not something you want to aim at. A window started
from wctl has nowhere useful to print, so -o and -s hand the answer back
by file or by snarf; the invite form spawns it that way and fills the
field when it returns.
An event you have not answered draws pale, so a glance at the week says
what is still waiting.
Move and Resize are gone from the menus: rio owns the border and does
both, better. Compact is one item showing whichever state you are not
in.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'pim/cmd/cal9/menu.go')
| -rw-r--r-- | pim/cmd/cal9/menu.go | 263 |
1 files changed, 38 insertions, 225 deletions
diff --git a/pim/cmd/cal9/menu.go b/pim/cmd/cal9/menu.go index 4443b00..f210ed0 100644 --- a/pim/cmd/cal9/menu.go +++ b/pim/cmd/cal9/menu.go @@ -1,116 +1,16 @@ package main import ( - "fmt" - "time" "os" "os/exec" "regexp" "strings" - "9front/gui/draw" + "9front/gui/ui" ) -// A button-3 menu, the way page and vdir do it: press 3, drag, release on an -// item. acme's plumb-on-3 is the outlier on this system, so plumbing lives -// in here as an item rather than owning the button. -type menuItem struct { - label string - do func(*state) -} - -func (s *state) menu(at draw.Point, mc *draw.Mousectl) { - items := s.menuItems() - if len(items) == 0 { - return - } - - lh := int32(s.f.Height) + 4 - var w int32 - for _, it := range items { - if x := s.f.Width(it.label) + 20; x > w { - w = x - } - } - h := lh*int32(len(items)) + 4 - win := s.win.Rect() - r := draw.Rect(at.X, at.Y, at.X+w, at.Y+h) - // Keep it on screen. - if r.Max.X > win.Max.X { - r = r.Add(draw.Point{X: win.Max.X - r.Max.X}) - } - if r.Max.Y > win.Max.Y { - r = r.Add(draw.Point{Y: win.Max.Y - r.Max.Y}) - } - - sel := -1 - paint := func() { - s.fill(r, "tag") - 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") - for i, it := range items { - ir := draw.Rect(r.Min.X+1, r.Min.Y+2+int32(i)*lh, r.Max.X-1, r.Min.Y+2+int32(i+1)*lh) - if i == sel { - s.fill(ir, "today") - } - s.text(draw.Point{X: ir.Min.X + 8, Y: ir.Min.Y + 2}, "ink", it.label) - } - s.d.Flush() - } - itemAt := func(p draw.Point) int { - if p.X < r.Min.X || p.X >= r.Max.X { - return -1 - } - i := int((p.Y - r.Min.Y - 2) / lh) - if i < 0 || i >= len(items) || p.Y < r.Min.Y+2 { - return -1 - } - return i - } - paint() - - // Track until button 3 comes back up. - for m := range mc.C { - if n := itemAt(m.Point); n != sel { - sel = n - paint() - } - if m.Buttons&4 == 0 { - if sel >= 0 { - items[sel].do(s) - } else { - s.redraw() - } - return - } - } -} - -func (s *state) menuItems() []menuItem { - items := []menuItem{} - if s.sel != nil { - items = append(items, menuItem{"Plumb", func(st *state) { - if err := st.plumb(st.sel); err != nil { - fmt.Fprintf(os.Stderr, "cal: plumb: %v\n", err) - } - st.redraw() - }}) - items = append(items, menuItem{"Open event", func(st *state) { - st.openEvent(st.sel) - }}) - items = append(items, menuItem{"Close event", func(st *state) { - st.sel = nil - st.redraw() - }}) - } - if s.sel == nil && !s.pick.IsZero() { - items = append(items, menuItem{"Close day", func(st *state) { - st.pick = time.Time{} - st.redraw() - }}) - } +func (s *state) menuItems() []ui.Item { + items := []ui.Item{} // one toggle per calendar, so a crowded work calendar can be put // aside without unmounting anything for _, c := range s.cals { @@ -119,61 +19,42 @@ func (s *state) menuItems() []menuItem { if s.on[c] { mark = "[x] " } - items = append(items, menuItem{mark + c, func(st *state) { - st.on[c] = !st.on[c] - st.load() - st.redraw() + items = append(items, ui.Item{Label: mark + c, Do: func() { + s.on[c] = !s.on[c] + s.load() + s.redraw() }}) } - // A second view of the calendar is a second cal, in its own window. - items = append(items, menuItem{"Open " + strings.ToLower(viewName[s.view]), - func(st *state) { st.openCal(st.view, st.at) }}) - if !s.pick.IsZero() && s.view != vDay { - d := s.pick - items = append(items, menuItem{"Open " + d.Format("2 Jan"), - func(st *state) { st.openCal(vDay, d) }}) + if len(writable()) > 0 { + items = append(items, ui.Item{Label: "New event", Do: func() { + s.New(520, 300, "cal9", "-v", "invite") + }}) } + + // A second view of the calendar is a second cal, in its own window. + items = append(items, ui.Item{Label: "Open " + strings.ToLower(viewName[s.view]), Do: func() { s.openCal(s.view, s.at) }}) items = append(items, - menuItem{"Today", func(st *state) { - st.at = now() - st.top = 8 * 60 - st.load() - st.redraw() - }}, - // rio has no iconify, so "compact" is a resize request: shrink the - // window and the small-window view takes over by itself. - menuItem{"Compact", func(st *state) { - r := st.win.Rect() - st.wctl(fmt.Sprintf("resize -r %d %d %d %d", - r.Min.X, r.Min.Y, r.Min.X+320, r.Min.Y+72)) - }}, - menuItem{"Restore", func(st *state) { - r := st.win.Rect() - st.wctl(fmt.Sprintf("resize -r %d %d %d %d", - r.Min.X, r.Min.Y, r.Min.X+820, r.Min.Y+620)) + ui.Item{Label: "Today", Do: func() { + s.at = now() + s.top = 8 * 60 + s.load() + s.redraw() }}, - menuItem{"Move", func(st *state) { st.track(st.mc, false) }}, - menuItem{"Resize", func(st *state) { st.track(st.mc, true) }}, - menuItem{"Hide", func(st *state) { st.wctl("hide") }}, - menuItem{"Exit", func(st *state) { st.quit = true }}, + // rio has no iconify, so "compact" is a resize request: shrink + // the window and the small-window view takes over by itself. + // One item, not two: the window is either compact or it is not, + // and offering the state you are already in is noise. + s.compactItem(), + // No move or resize here. rio owns the border -- it puts corner + // cursors there and takes the clicks before we see them -- so + // dragging it already moves and resizes the window, and doing + // it worse from in here helps nobody. + ui.Item{Label: "Hide", Do: func() { s.Wctl("hide") }}, + ui.Item{Label: "Exit", Do: func() { s.quit = true }}, ) return items } -// wctl asks rio to do something to our window. Errors are worth showing: -// outside rio there is no wctl and the menu items simply do nothing. -func (s *state) wctl(cmd string) { - f, err := os.OpenFile("/dev/wctl", os.O_WRONLY, 0) - if err != nil { - fmt.Fprintf(os.Stderr, "cal: wctl: %v\n", err) - return - } - defer f.Close() - if _, err := f.WriteString(cmd); err != nil { - fmt.Fprintf(os.Stderr, "cal: wctl %q: %v\n", cmd, err) - } -} - var urlRe = regexp.MustCompile(`https?://[^\s<>"]+`) // plumb sends the event's join link to the plumber, falling back to the @@ -188,80 +69,12 @@ func (s *state) plumb(e *event) error { return exec.Command("/bin/plumb", data).Run() } -// track implements Move and Resize ourselves. rio's wctl has no "let the -// user sweep" verb -- only move/resize with an explicit rectangle -- so we -// follow the pointer and write a new rect as it goes, and rio does the -// actual work. Any button press drops the window where it is. -// -// rio hands out a new image whenever the window's screen rect changes, so -// this loop has to service resize events as well as motion, or every write -// would be drawing into a stale image. -func (s *state) track(mc *draw.Mousectl, resize bool) { - r := s.win.Rect() - w, h := r.Dx(), r.Dy() - grab := draw.Point{} // pointer offset within the window, for Move - first := true - last := r - - for { - select { - case m, ok := <-mc.C: - if !ok { - return - } - if first { - grab = draw.Point{X: m.X - r.Min.X, Y: m.Y - r.Min.Y} - first = false - continue - } - if m.Buttons != 0 { // any press drops it - s.redraw() - return - } - var nr draw.Rectangle - if resize { - nr = draw.Rect(r.Min.X, r.Min.Y, m.X, m.Y) - if nr.Dx() < 120 { - nr.Max.X = nr.Min.X + 120 - } - if nr.Dy() < 60 { - nr.Max.Y = nr.Min.Y + 60 - } - } else { - min := draw.Point{X: m.X - grab.X, Y: m.Y - grab.Y} - nr = draw.Rect(min.X, min.Y, min.X+w, min.Y+h) - } - // Only bother rio when it would actually change something. - if abs(nr.Min.X-last.Min.X)+abs(nr.Min.Y-last.Min.Y)+ - abs(nr.Max.X-last.Max.X)+abs(nr.Max.Y-last.Max.Y) < 4 { - continue - } - last = nr - verb := "move" - if resize { - verb = "resize" - } - s.wctl(fmt.Sprintf("%s -r %d %d %d %d", verb, - nr.Min.X, nr.Min.Y, nr.Max.X, nr.Max.Y)) - - case <-mc.Resize: - win, err := s.d.Reattach("/dev", s.win) - if err != nil { - return - } - s.win = win - r = s.win.Rect() - if !resize { - w, h = r.Dx(), r.Dy() - } - s.redraw() - } - } -} - -func abs(n int32) int32 { - if n < 0 { - return -n - } - return n +// compactItem offers whichever of compact and restore you are not +// already in. compactH is the height below which the small view takes +// over, so it is also how we tell which state we are in. +func (s *state) compactItem() ui.Item { + if s.Win.Rect().Dy() < compactH { + return ui.Item{Label: "Restore", Do: func() { s.Resize(820, 620) }} + } + return ui.Item{Label: "Compact", Do: func() { s.Resize(320, 72) }} } |
