Files
Nat Fletcherandclaude[bot] 30dc1122c5 added launcher dmenu mode (#3639)
* added launcher dmenu mode

* fixed prechecker issues

* added unit tests for dms dmenu command

* added workaround for matugen printing error message to stdout

* correct help message: matching is case-*in*sensitive

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>

* no longer save dmenu selections to history; now properly close dmenu sockets

* increased parity with rofi -dmenu flags; added -i as an ignored flag to match dmenu

* go mod tidied

---------

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
2026-10-04 22:16:40 -04:00

226 lines
6.8 KiB
Go

package dmenuipc
import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net"
"os"
"path/filepath"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/utils"
)
type Range struct {
Start int `json:"start"`
End int `json:"end"`
}
type Keybind struct {
N int `json:"n"`
Key string `json:"key"`
}
type Header struct {
Prompt string `json:"prompt"`
Placeholder string `json:"placeholder,omitempty"`
Lines int `json:"lines,omitempty"`
PromptOnly bool `json:"promptOnly,omitempty"`
Select string `json:"select,omitempty"`
Mesg string `json:"mesg,omitempty"`
Password bool `json:"password,omitempty"`
MarkupRows bool `json:"markupRows,omitempty"`
OnlyMatch bool `json:"onlyMatch,omitempty"`
NoCustom bool `json:"noCustom,omitempty"`
MultiSelect bool `json:"multiSelect,omitempty"`
Active []Range `json:"active,omitempty"`
Urgent []Range `json:"urgent,omitempty"`
Keybinds []Keybind `json:"keybinds,omitempty"`
View string `json:"view,omitempty"`
Size string `json:"size,omitempty"`
Icon string `json:"icon,omitempty"`
DisableHistory bool `json:"disableHistory,omitempty"`
}
type ItemMeta struct {
Icon string `json:"icon,omitempty"`
Display string `json:"display,omitempty"`
Meta string `json:"meta,omitempty"`
Info string `json:"info,omitempty"`
NonSelectable bool `json:"nonSelectable,omitempty"`
Urgent bool `json:"urgent,omitempty"`
Active bool `json:"active,omitempty"`
Permanent bool `json:"permanent,omitempty"`
Header bool `json:"header,omitempty"`
}
type SelectedRow struct {
Index int `json:"index"`
Text string `json:"text"`
Info string `json:"info,omitempty"`
}
type envelope struct {
Type string `json:"type"`
Prompt string `json:"prompt,omitempty"`
Placeholder string `json:"placeholder,omitempty"`
Lines int `json:"lines,omitempty"`
PromptOnly bool `json:"promptOnly,omitempty"`
Select string `json:"select,omitempty"`
Mesg string `json:"mesg,omitempty"`
Password bool `json:"password,omitempty"`
MarkupRows bool `json:"markupRows,omitempty"`
OnlyMatch bool `json:"onlyMatch,omitempty"`
NoCustom bool `json:"noCustom,omitempty"`
MultiSelect bool `json:"multiSelect,omitempty"`
Active []Range `json:"active,omitempty"`
Urgent []Range `json:"urgent,omitempty"`
Keybinds []Keybind `json:"keybinds,omitempty"`
View string `json:"view,omitempty"`
Size string `json:"size,omitempty"`
Icon string `json:"icon,omitempty"`
DisableHistory bool `json:"disableHistory,omitempty"`
Text string `json:"text,omitempty"`
Row *ItemMeta `json:"row,omitempty"`
Kind string `json:"kind,omitempty"`
Index int `json:"index"`
Info string `json:"info,omitempty"`
FilterText string `json:"filterText,omitempty"`
Items []SelectedRow `json:"items,omitempty"`
KeybindN int `json:"keybindN,omitempty"`
}
type Session struct {
listener *net.UnixListener
path string
}
func Listen() (*Session, error) {
dir := filepath.Join(utils.RuntimeDir(), "dms")
if err := os.MkdirAll(dir, 0o700); err != nil {
return nil, fmt.Errorf("create dmenu socket dir: %w", err)
}
token := make([]byte, 8)
if _, err := rand.Read(token); err != nil {
return nil, fmt.Errorf("generate dmenu socket token: %w", err)
}
path := filepath.Join(dir, fmt.Sprintf("dmenu-%d-%s.sock", os.Getpid(), hex.EncodeToString(token)))
addr, err := net.ResolveUnixAddr("unix", path)
if err != nil {
return nil, fmt.Errorf("resolve dmenu socket address: %w", err)
}
listener, err := net.ListenUnix("unix", addr)
if err != nil {
return nil, fmt.Errorf("listen on dmenu socket: %w", err)
}
if err := os.Chmod(path, 0o600); err != nil {
listener.Close()
os.Remove(path)
return nil, fmt.Errorf("chmod dmenu socket: %w", err)
}
return &Session{listener: listener, path: path}, nil
}
func (s *Session) Path() string { return s.path }
func (s *Session) Close() {
s.listener.Close()
os.Remove(s.path)
}
func (s *Session) Accept(timeout time.Duration) (*Conn, error) {
if err := s.listener.SetDeadline(time.Now().Add(timeout)); err != nil {
return nil, err
}
conn, err := s.listener.AcceptUnix()
if err != nil {
return nil, err
}
if err := conn.SetDeadline(time.Time{}); err != nil {
conn.Close()
return nil, err
}
return &Conn{conn: conn, dec: json.NewDecoder(conn)}, nil
}
type Conn struct {
conn *net.UnixConn
dec *json.Decoder
}
func (c *Conn) Close() error { return c.conn.Close() }
func (c *Conn) SendHeader(h Header) error {
return c.send(envelope{
Type: "header",
Prompt: h.Prompt,
Placeholder: h.Placeholder,
Lines: h.Lines,
PromptOnly: h.PromptOnly,
Select: h.Select,
Mesg: h.Mesg,
Password: h.Password,
MarkupRows: h.MarkupRows,
OnlyMatch: h.OnlyMatch,
NoCustom: h.NoCustom,
MultiSelect: h.MultiSelect,
Active: h.Active,
Urgent: h.Urgent,
Keybinds: h.Keybinds,
View: h.View,
Size: h.Size,
Icon: h.Icon,
DisableHistory: h.DisableHistory,
})
}
func (c *Conn) SendItem(text string, meta *ItemMeta) error {
return c.send(envelope{Type: "item", Text: text, Row: meta})
}
func (c *Conn) SendEnd() error {
return c.send(envelope{Type: "end"})
}
func (c *Conn) send(e envelope) error {
return json.NewEncoder(c.conn).Encode(e)
}
type Selection struct {
Kind string // "row" | "freetext" | "multi"
Index int // valid when Kind == "row"
Text string // valid when Kind == "row" or "freetext"
Info string // valid when Kind == "row" and the row carried info
FilterText string // the search box's contents at accept time, always set
Items []SelectedRow
KeybindN int // nonzero when accepted via custom keybinding N
}
func (c *Conn) ReadSelection() (sel Selection, ok bool, err error) {
var e envelope
if decErr := c.dec.Decode(&e); decErr != nil {
if decErr == io.EOF {
return Selection{}, false, nil
}
return Selection{}, false, decErr
}
if e.Type != "select" {
return Selection{}, false, fmt.Errorf("unexpected dmenu response type %q", e.Type)
}
return Selection{
Kind: e.Kind,
Index: e.Index,
Text: e.Text,
Info: e.Info,
FilterText: e.FilterText,
Items: e.Items,
KeybindN: e.KeybindN,
}, true, nil
}