mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-14 23:34:01 -04:00
* 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>
226 lines
6.8 KiB
Go
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
|
|
}
|