Files
mesh-catalog/modules/dmenu/cmd/dmenu-tools/dmenu.go
T
jochen 838a6e510b dmenu: the package, which makes the notifier's menu work, and a menu tool (hq to-be 42 phase 2.7)
Research 026/04 counted two plain dmenu calls failing with dmenu installed
nowhere. Measured, they are one line on each workstation: dunst's
`dmenu = /usr/bin/dmenu -p dunst:`. Installing the package fixes both by
existing; the line stays the dunst module's.

No claim: node-launcher is not in the controller's seat table yet, and the
module says so. Two Go tools: menu, shaped like that seat's verb (chosen
line, index, typed, cancelled, timed out within 25 s), and session.
2026-10-04 13:02:23 +02:00

113 lines
3.2 KiB
Go

package main
import (
"fmt"
"strconv"
"strings"
"time"
)
// MostWait is the longest a menu stays open: below the runtime's 30 s call limit, so an unanswered
// menu is answered as such rather than as a call the runtime gave up on.
const MostWait = 25
// MostChoices bounds the list.
const MostChoices = 1000
// Ask is one menu.
type Ask struct {
Choices []string
Prompt string
Lines int
CaseInsensitive bool
Timeout int
}
// MenuAnswer is what dmenu_menu answers.
type MenuAnswer struct {
Chosen string `json:"chosen,omitempty"`
Index int `json:"index"`
Typed bool `json:"typed"`
Cancelled bool `json:"cancelled"`
TimedOut bool `json:"timed_out"`
Session Session `json:"session"`
}
// Menu shows the choices and answers the one taken. dmenu prints the selected (or typed) line and
// exits 0; it exits 1 with nothing printed when dismissed.
func Menu(a Ask) (MenuAnswer, error) {
if len(a.Choices) == 0 {
return MenuAnswer{}, fmt.Errorf("choices is required: at least one line")
}
if len(a.Choices) > MostChoices {
return MenuAnswer{}, fmt.Errorf("%d choices; at most %d are shown", len(a.Choices), MostChoices)
}
for _, c := range a.Choices {
if strings.ContainsAny(c, "\n\r") {
return MenuAnswer{}, fmt.Errorf("a choice holds a line break: %q", c)
}
}
if strings.ContainsAny(a.Prompt, "\n\r") {
return MenuAnswer{}, fmt.Errorf("the prompt holds a line break")
}
s, err := findSession()
if err != nil {
return MenuAnswer{}, err
}
args := []string{}
if a.CaseInsensitive {
args = append(args, "-i")
}
if a.Lines > 0 {
args = append(args, "-l", strconv.Itoa(a.Lines))
}
if a.Prompt != "" {
args = append(args, "-p", a.Prompt)
}
c := Cmd{Name: "dmenu", Args: args, Stdin: strings.Join(a.Choices, "\n") + "\n", Env: s.Env(), Timeout: time.Duration(a.Timeout) * time.Second}
r := run(c)
out := MenuAnswer{Index: -1, Session: s}
switch {
case r.Error == "timeout":
out.TimedOut = true
return out, nil
case r.Error != "":
return MenuAnswer{}, failure(c, r)
case strings.Contains(r.Stderr, "cannot open display"):
return MenuAnswer{}, fmt.Errorf("the X session at %s (found by %s) refused the connection", s.Display, s.FoundBy)
case strings.Contains(r.Stderr, "cannot grab keyboard"):
return MenuAnswer{}, fmt.Errorf("dmenu could not take the keyboard: another program holds it (a locked screen, an open menu)")
case r.Status == 1 && strings.TrimSpace(r.Stdout) == "":
out.Cancelled = true
return out, nil
case r.Status != 0:
return MenuAnswer{}, failure(c, r)
}
out.Chosen = strings.TrimRight(r.Stdout, "\r\n")
for i, ch := range a.Choices {
if ch == out.Chosen {
out.Index = i
break
}
}
out.Typed = out.Index < 0
return out, nil
}
// SessionCheck answers the session the menu would appear in, and dmenu's version.
func SessionCheck() (map[string]any, error) {
out := map[string]any{}
if r := run(Cmd{Name: "dmenu", Args: []string{"-v"}}); r.Error == "" {
out["dmenu"] = strings.TrimSpace(r.Stdout + r.Stderr)
} else {
out["dmenu"] = failure(Cmd{Name: "dmenu"}, r).Error()
}
s, err := findSession()
if err != nil {
out["found"], out["why"] = false, err.Error()
return out, nil
}
out["found"], out["session"] = true, s
return out, nil
}