package main import ( "fmt" "time" "os" "os/exec" "regexp" "strings" "9front/gui/draw" ) // 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() }}) } // one toggle per calendar, so a crowded work calendar can be put // aside without unmounting anything for _, c := range s.cals { c := c mark := "[ ] " 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() }}) } // 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) }}) } 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)) }}, 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 }}, ) 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 // event file itself, which lands in acme. func (s *state) plumb(e *event) error { data := e.file if body, err := os.ReadFile(e.file); err == nil { if m := urlRe.Find(body); m != nil { data = strings.TrimRight(string(m), ".,)") } } 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 }