From 8ec1a3410c4258be6cc957a14102de780ac0d569 Mon Sep 17 00:00:00 2001 From: Davemane42 Date: Sun, 22 Mar 2026 23:53:58 -0400 Subject: [PATCH 1/4] add WiFi provider with Network Manager backend fix redundant code better frequency bands --- .../comm/handlers/subscriberequesthandler.go | 4 + internal/providers/wifi/README.md | 27 ++ internal/providers/wifi/backend.go | 38 ++ internal/providers/wifi/makefile | 39 ++ internal/providers/wifi/networkManager.go | 149 +++++++ internal/providers/wifi/setup.go | 362 ++++++++++++++++++ 6 files changed, 619 insertions(+) create mode 100644 internal/providers/wifi/README.md create mode 100644 internal/providers/wifi/backend.go create mode 100644 internal/providers/wifi/makefile create mode 100644 internal/providers/wifi/networkManager.go create mode 100644 internal/providers/wifi/setup.go diff --git a/internal/comm/handlers/subscriberequesthandler.go b/internal/comm/handlers/subscriberequesthandler.go index caceb0a4..3518dc67 100644 --- a/internal/comm/handlers/subscriberequesthandler.go +++ b/internal/comm/handlers/subscriberequesthandler.go @@ -83,6 +83,10 @@ func init() { p = "bluetooth" } + if strings.HasPrefix(p, "wifi:") { + p = "wifi" + } + toDelete := []uint32{} for k, v := range subs { diff --git a/internal/providers/wifi/README.md b/internal/providers/wifi/README.md new file mode 100644 index 00000000..887f874a --- /dev/null +++ b/internal/providers/wifi/README.md @@ -0,0 +1,27 @@ +### Elephant WiFi + +WiFi network management. Scan, connect, disconnect, and forget networks. + +To connect to a new secured network, type `#yourpassword` in the search bar before connecting. + +#### Requirements + +One of the following utils/backend: + +- `nmcli` - Network Manager + +#### Configuration + +- `backend` string - WiFi backend + - `auto` - Will try all backend until one works + - `nm` - Network Manager +- `message_time` int - Seconds to show status messages + - (e.g. Connecting, Disconnecting) +- `error_time` int - Seconds to show error messages + - (e.g. Connection failed, Password required) +- `subtext_format` string - Format string for the subtext displayed under each network + - `{lock}` - security icon: 🔓 (secured + saved), 🔒 (secured), 🌐 (open) + - `{status}` - connection status: `Connected`, `Saved`, or empty + - `{signal}` - signal strength percentage (e.g. `80%`) + - `{frequency}` - frequency band (e.g. `5 GHz`) + - `{security}` - security type (e.g. `WPA2`) diff --git a/internal/providers/wifi/backend.go b/internal/providers/wifi/backend.go new file mode 100644 index 00000000..220ff725 --- /dev/null +++ b/internal/providers/wifi/backend.go @@ -0,0 +1,38 @@ +package main + +import ( + "os/exec" +) + +type Backend interface { + CheckWifiState() bool + SetWifiEnabled(enabled bool) error + GetNetworks() []Network + Connect(ssid string, password string) error + Disconnect(ssid string) error + Forget(ssid string) error + Scan() +} + +var backends = map[string]func() Backend{ + "nm": func() Backend { return &NmcliBackend{} }, +} + +func detectBackend(preference string) Backend { + if preference != "auto" { + if fn, ok := backends[preference]; ok { + if p, err := exec.LookPath(preference); p != "" && err == nil { + return fn() + } + } + return nil + } + + for name, fn := range backends { + if p, err := exec.LookPath(name); p != "" && err == nil { + return fn() + } + } + + return nil +} diff --git a/internal/providers/wifi/makefile b/internal/providers/wifi/makefile new file mode 100644 index 00000000..02d3e42e --- /dev/null +++ b/internal/providers/wifi/makefile @@ -0,0 +1,39 @@ +DESTDIR ?= +CONFIGDIR = $(DESTDIR)/etc/xdg/elephant/providers + +GO_BUILD_FLAGS = -buildvcs=false -buildmode=plugin -trimpath +PLUGIN_NAME = wifi.so + +.PHONY: all build install uninstall clean + +all: build + +build: + go build $(GO_BUILD_FLAGS) + +install: build + # Install plugin + install -Dm 755 $(PLUGIN_NAME) $(CONFIGDIR)/$(PLUGIN_NAME) + +uninstall: + rm -f $(CONFIGDIR)/$(PLUGIN_NAME) + +clean: + go clean + rm -f $(PLUGIN_NAME) + +dev-install: install + +help: + @echo "Available targets:" + @echo " all - Build the plugin (default)" + @echo " build - Build the plugin" + @echo " install - Install the plugin" + @echo " uninstall - Remove installed plugin" + @echo " clean - Clean build artifacts" + @echo " help - Show this help" + @echo "" + @echo "Variables:" + @echo " DESTDIR - Destination directory for staged installs" + @echo "" + @echo "Note: This builds a Go plugin (.so file) for elephant" diff --git a/internal/providers/wifi/networkManager.go b/internal/providers/wifi/networkManager.go new file mode 100644 index 00000000..797edc16 --- /dev/null +++ b/internal/providers/wifi/networkManager.go @@ -0,0 +1,149 @@ +package main + +import ( + "log/slog" + "os/exec" + "strings" + "time" +) + +type NmcliBackend struct{} + +func (b *NmcliBackend) CheckWifiState() bool { + cmd := exec.Command("nmcli", "radio", "wifi") + out, err := cmd.CombinedOutput() + if err != nil { + return false + } + + return strings.TrimSpace(string(out)) == "enabled" +} + +func (b *NmcliBackend) SetWifiEnabled(enabled bool) error { + state := "off" + if enabled { + state = "on" + } + + cmd := exec.Command("nmcli", "radio", "wifi", state) + if out, err := cmd.CombinedOutput(); err != nil { + slog.Error(Name, "set wifi", string(out)) + return err + } + + return nil +} + +func (b *NmcliBackend) GetNetworks() []Network { + var result []Network + + known := make(map[string]string) + cmd := exec.Command("nmcli", "-t", "-f", "NAME,UUID,TYPE", "connection", "show") + out, err := cmd.CombinedOutput() + if err != nil { + slog.Error(Name, "get connections", err) + } + + for l := range strings.Lines(strings.TrimSpace(string(out))) { + l = strings.TrimSpace(l) + if l == "" { + continue + } + fields := strings.SplitN(l, ":", 3) + if len(fields) == 3 && fields[2] == "802-11-wireless" { + known[fields[0]] = fields[1] + } + } + + cmd = exec.Command("nmcli", "-t", "-f", "SSID,SIGNAL,SECURITY,IN-USE,FREQ", "device", "wifi", "list") + out, err = cmd.CombinedOutput() + if err != nil { + slog.Error(Name, "get wifi list", err) + return result + } + + seen := make(map[string]struct{}) + + for l := range strings.Lines(strings.TrimSpace(string(out))) { + l = strings.TrimSpace(l) + if l == "" { + continue + } + + fields := strings.SplitN(l, ":", 5) + if len(fields) < 5 { + continue + } + + ssid := fields[0] + if ssid == "" { + continue + } + + if _, ok := seen[ssid]; ok { + continue + } + seen[ssid] = struct{}{} + + n := Network{ + SSID: ssid, + Signal: fields[1], + Security: fields[2], + InUse: strings.TrimSpace(fields[3]) == "*", + Frequency: freqBand(fields[4]), + } + + if _, ok := known[ssid]; ok { + n.Known = true + n.UUID = known[ssid] + } + + result = append(result, n) + } + + return result +} + +func (b *NmcliBackend) Connect(ssid string, password string) error { + cmd := exec.Command("nmcli", "device", "wifi", "connect", ssid) + if password != "" { + cmd.Args = append(cmd.Args, "password", password) + } + + if out, err := cmd.CombinedOutput(); err != nil { + slog.Error(Name, "connect", string(out)) + return err + } + + return nil +} + +func (b *NmcliBackend) Disconnect(ssid string) error { + cmd := exec.Command("nmcli", "connection", "down", ssid) + if out, err := cmd.CombinedOutput(); err != nil { + slog.Error(Name, "disconnect", string(out)) + return err + } + + return nil +} + +func (b *NmcliBackend) Forget(ssid string) error { + cmd := exec.Command("nmcli", "connection", "delete", ssid) + if out, err := cmd.CombinedOutput(); err != nil { + slog.Error(Name, "forget", string(out)) + return err + } + + return nil +} + +func (b *NmcliBackend) Scan() { + for { + out, err := exec.Command("nmcli", "-t", "-f", "SSID", "device", "wifi", "list", "--rescan", "yes").CombinedOutput() + if err == nil && strings.TrimSpace(string(out)) != "" { + return + } + time.Sleep(500 * time.Millisecond) + } +} diff --git a/internal/providers/wifi/setup.go b/internal/providers/wifi/setup.go new file mode 100644 index 00000000..a0545dfb --- /dev/null +++ b/internal/providers/wifi/setup.go @@ -0,0 +1,362 @@ +package main + +import ( + "fmt" + "log/slog" + "net" + "strconv" + "strings" + "time" + + _ "embed" + + "github.com/abenz1267/elephant/v2/internal/comm/handlers" + "github.com/abenz1267/elephant/v2/internal/util" + "github.com/abenz1267/elephant/v2/pkg/common" + "github.com/abenz1267/elephant/v2/pkg/pb/pb" +) + +var ( + Name = "wifi" + NamePretty = "WiFi" + on = true +) + +//go:embed README.md +var readme string + +type Config struct { + common.Config `koanf:",squash"` + MessageTime int `koanf:"message_time" desc:"seconds to show status messages" default:"1"` + ErrorTime int `koanf:"error_time" desc:"seconds to show error messages" default:"3"` + Backend string `koanf:"backend" desc:"wifi backend: auto, nm" default:"auto"` + SubtextFormat string `koanf:"subtext_format" desc:"subtext format. placeholders: {lock}, {status}, {signal}, {frequency}, {security}" default:"{lock} {status} {signal} {frequency} {security}"` +} + +type Network struct { + SSID string + Signal string + Security string + Frequency string + InUse bool + Known bool + UUID string +} + +var ( + networks []Network + config *Config + backend Backend +) + +func Setup() { + LoadConfig() + + if config.NamePretty != "" { + NamePretty = config.NamePretty + } + + if config.Backend != "auto" { + if b := detectBackend(config.Backend); b != nil { + backend = b + } else { + slog.Warn(Name, "backend", fmt.Sprintf("configured backend %q not available, using auto-detected", config.Backend)) + } + } + + on = backend.CheckWifiState() +} + +func LoadConfig() { + config = &Config{ + Config: common.Config{ + Icon: "network-wireless-symbolic", + MinScore: 20, + }, + MessageTime: 1, + ErrorTime: 3, + Backend: "auto", + SubtextFormat: "{lock} {status} {signal} {frequency} {security}", + } + + common.LoadConfig(Name, config) +} + +func Available() bool { + backend = detectBackend("auto") + if backend == nil { + slog.Info(Name, "available", "no wifi backend found (nmcli/iwctl). disabling") + return false + } + + return true +} + +func PrintDoc(write bool) { + if !write { + fmt.Println(readme) + fmt.Println() + } + util.PrintConfig(config, Name, write) +} + +const ( + ActionWifiOff = "wifi_off" + ActionWifiOn = "wifi_on" + ActionConnect = "connect" + ActionDisconnect = "disconnect" + ActionForget = "forget" + ActionScan = "scan" +) + +func Activate(single bool, identifier, action string, query string, args string, format uint8, conn net.Conn) { + selNetwork := findNetwork(identifier) + if selNetwork == nil && (action == ActionConnect || action == ActionDisconnect || action == ActionForget) { + slog.Error(Name, "activate", fmt.Sprintf("network %q not found", identifier)) + return + } + + switch action { + case ActionWifiOn: + handlers.ProviderUpdated <- "wifi:wifion" + if err := backend.SetWifiEnabled(true); err != nil { + slog.Error(Name, "activate", err) + } + on = true + backend.Scan() + case ActionWifiOff: + handlers.ProviderUpdated <- "wifi:wifioff" + if err := backend.SetWifiEnabled(false); err != nil { + slog.Error(Name, "activate", err) + } + on = false + networks = nil + time.Sleep(time.Duration(config.MessageTime) * time.Second) + case ActionScan: + handlers.ProviderUpdated <- "wifi:scan" + backend.Scan() + case ActionConnect: + if args == "" && !selNetwork.Known && selNetwork.Security != "" { + handlers.ProviderUpdated <- "wifi:password_required" + time.Sleep(time.Duration(config.ErrorTime) * time.Second) + return + } + + handlers.ProviderUpdated <- "wifi:connect" + + password := args + if selNetwork.Known { + password = "" + } + + wasKnown := selNetwork.Known + + if err := backend.Connect(identifier, password); err != nil { + handlers.ProviderUpdated <- "wifi:connect_failed" + slog.Error(Name, "activate", err) + if !wasKnown { + backend.Forget(identifier) + } + time.Sleep(time.Duration(config.ErrorTime) * time.Second) + } else { + time.Sleep(time.Duration(config.MessageTime) * time.Second) + } + case ActionDisconnect: + handlers.ProviderUpdated <- "wifi:disconnect" + if err := backend.Disconnect(identifier); err != nil { + slog.Error(Name, "activate", err) + } + time.Sleep(time.Duration(config.MessageTime) * time.Second) + case ActionForget: + handlers.ProviderUpdated <- "wifi:forget" + if err := backend.Forget(identifier); err != nil { + slog.Error(Name, "activate", err) + } + time.Sleep(time.Duration(config.MessageTime) * time.Second) + default: + slog.Error(Name, "activate", fmt.Sprintf("unknown action: %s", action)) + return + } +} + +func Query(conn net.Conn, query string, _ bool, exact bool, _ uint8) []*pb.QueryResponse_Item { + start := time.Now() + entries := []*pb.QueryResponse_Item{} + + if !on { + return entries + } + + networks = backend.GetNetworks() + + for k, v := range networks { + s := []string{} + a := []string{} + + if v.InUse { + s = append(s, "connected") + a = append(a, ActionDisconnect) + } else { + a = append(a, ActionConnect) + } + + if v.Known { + s = append(s, "known") + a = append(a, ActionForget) + } + + subtext := formatSubtext(v) + + icon := signalIcon(v.Signal) + + score := 1000 - int32(k) + if v.InUse { + score = 10000 + } else if v.Known { + score = 5000 - int32(k) + } + + e := &pb.QueryResponse_Item{ + Identifier: v.SSID, + Score: score, + State: s, + Actions: a, + Icon: icon, + Text: v.SSID, + Subtext: subtext, + Provider: Name, + Type: pb.QueryResponse_REGULAR, + } + + if query != "" { + score, pos, start := common.FuzzyScore(query, v.SSID, exact) + + e.Score = score + e.Fuzzyinfo = &pb.QueryResponse_Item_FuzzyInfo{ + Field: "text", + Positions: pos, + Start: start, + } + } + + if e.Score > config.MinScore || query == "" { + entries = append(entries, e) + } + } + + slog.Debug(Name, "query", time.Since(start)) + return entries +} + +func Icon() string { + return config.Icon +} + +func HideFromProviderlist() bool { + return config.HideFromProviderlist +} + +func State(provider string) *pb.ProviderStateResponse { + actions := []string{} + + if on { + actions = append(actions, ActionWifiOff, ActionScan) + } else { + actions = append(actions, ActionWifiOn) + } + + return &pb.ProviderStateResponse{ + States: []string{}, + Actions: actions, + Provider: "", + } +} + +func signalIcon(signal string) string { + if signal == "" { + return "network-wireless-symbolic" + } + + var level int + fmt.Sscanf(signal, "%d", &level) + + switch { + case level >= 75: + return "network-wireless-signal-excellent-symbolic" + case level >= 50: + return "network-wireless-signal-good-symbolic" + case level >= 25: + return "network-wireless-signal-ok-symbolic" + default: + return "network-wireless-signal-weak-symbolic" + } +} + +// freqBand converts a frequency string like "5220 MHz" to a band label like "5 GHz". +func freqBand(freq string) string { + freq = strings.TrimSpace(freq) + freq = strings.TrimSuffix(freq, " MHz") + mhz, err := strconv.Atoi(freq) + if err != nil { + return "" + } + + switch { + case mhz < 5000: + return "2.4 GHz" + case mhz < 5925: + return "5 GHz" + default: + return "6 GHz" + } +} + +func formatSubtext(n Network) string { + lock := "🌐" + if n.Security != "" { + if n.Known { + lock = "🔓" + } else { + lock = "🔒" + } + } + + status := "" + if n.InUse { + status = "Connected" + } else if n.Known { + status = "Saved" + } + + signal := "" + if n.Signal != "" { + signal = fmt.Sprintf("%s%%", n.Signal) + } + + r := strings.NewReplacer( + "{lock}", lock, + "{status}", status, + "{signal}", signal, + "{frequency}", n.Frequency, + "{security}", n.Security, + ) + + result := r.Replace(config.SubtextFormat) + + // collapse multiple consecutive spaces from empty placeholders + for strings.Contains(result, " ") { + result = strings.ReplaceAll(result, " ", " ") + } + + return strings.TrimSpace(result) +} + +func findNetwork(ssid string) *Network { + for k := range networks { + if networks[k].SSID == ssid { + return &networks[k] + } + } + + return nil +} From 07f4c28b394b745110aa83417b67517cea2c81e4 Mon Sep 17 00:00:00 2001 From: Davemane42 Date: Mon, 23 Mar 2026 05:35:23 -0400 Subject: [PATCH 2/4] network password input with terminal --- internal/providers/wifi/README.md | 15 +- internal/providers/wifi/backend.go | 2 +- internal/providers/wifi/networkManager.go | 5 +- internal/providers/wifi/setup.go | 159 ++++++++++++++++++---- 4 files changed, 142 insertions(+), 39 deletions(-) diff --git a/internal/providers/wifi/README.md b/internal/providers/wifi/README.md index 887f874a..38e18cd5 100644 --- a/internal/providers/wifi/README.md +++ b/internal/providers/wifi/README.md @@ -2,7 +2,7 @@ WiFi network management. Scan, connect, disconnect, and forget networks. -To connect to a new secured network, type `#yourpassword` in the search bar before connecting. +Connecting to a new secured network will open a terminal to prompt for the password. #### Requirements @@ -19,9 +19,12 @@ One of the following utils/backend: - (e.g. Connecting, Disconnecting) - `error_time` int - Seconds to show error messages - (e.g. Connection failed, Password required) +- `reopen_after_fail` bool - Reopen wifi menu after connection failure (default: `true`) +- `reopen_after_connect` bool - Reopen wifi menu after successful connection (default: `false`) +- `show_password_dots` bool - Show dots while typing password in terminal (default: `true`) - `subtext_format` string - Format string for the subtext displayed under each network - - `{lock}` - security icon: 🔓 (secured + saved), 🔒 (secured), 🌐 (open) - - `{status}` - connection status: `Connected`, `Saved`, or empty - - `{signal}` - signal strength percentage (e.g. `80%`) - - `{frequency}` - frequency band (e.g. `5 GHz`) - - `{security}` - security type (e.g. `WPA2`) + - `%LOCK%` - security icon: 🔓 (secured + saved), 🔒 (secured), 🌐 (open) + - `%STATUS%` - connection status: `Connected`, `Saved`, or empty + - `%SIGNAL%` - signal strength percentage (e.g. `80%`) + - `%FREQUENCY%` - frequency band (e.g. `5 GHz`) + - `%SECURITY%` - security type (e.g. `WPA2`) diff --git a/internal/providers/wifi/backend.go b/internal/providers/wifi/backend.go index 220ff725..1f09b4f3 100644 --- a/internal/providers/wifi/backend.go +++ b/internal/providers/wifi/backend.go @@ -11,7 +11,7 @@ type Backend interface { Connect(ssid string, password string) error Disconnect(ssid string) error Forget(ssid string) error - Scan() + WaitForNetworks() } var backends = map[string]func() Backend{ diff --git a/internal/providers/wifi/networkManager.go b/internal/providers/wifi/networkManager.go index 797edc16..dcf6ba17 100644 --- a/internal/providers/wifi/networkManager.go +++ b/internal/providers/wifi/networkManager.go @@ -138,12 +138,13 @@ func (b *NmcliBackend) Forget(ssid string) error { return nil } -func (b *NmcliBackend) Scan() { - for { +func (b *NmcliBackend) WaitForNetworks() { + for range 10 { out, err := exec.Command("nmcli", "-t", "-f", "SSID", "device", "wifi", "list", "--rescan", "yes").CombinedOutput() if err == nil && strings.TrimSpace(string(out)) != "" { return } time.Sleep(500 * time.Millisecond) } + slog.Warn(Name, "scan", "max retries reached") } diff --git a/internal/providers/wifi/setup.go b/internal/providers/wifi/setup.go index a0545dfb..118c6882 100644 --- a/internal/providers/wifi/setup.go +++ b/internal/providers/wifi/setup.go @@ -2,8 +2,11 @@ package main import ( "fmt" + "io" "log/slog" "net" + "os" + "os/exec" "strconv" "strings" "time" @@ -26,11 +29,14 @@ var ( var readme string type Config struct { - common.Config `koanf:",squash"` - MessageTime int `koanf:"message_time" desc:"seconds to show status messages" default:"1"` - ErrorTime int `koanf:"error_time" desc:"seconds to show error messages" default:"3"` - Backend string `koanf:"backend" desc:"wifi backend: auto, nm" default:"auto"` - SubtextFormat string `koanf:"subtext_format" desc:"subtext format. placeholders: {lock}, {status}, {signal}, {frequency}, {security}" default:"{lock} {status} {signal} {frequency} {security}"` + common.Config `koanf:",squash"` + MessageTime int `koanf:"message_time" desc:"seconds to show status messages" default:"1"` + ErrorTime int `koanf:"error_time" desc:"seconds to show error messages" default:"3"` + Backend string `koanf:"backend" desc:"wifi backend: auto, nm" default:"auto"` + SubtextFormat string `koanf:"subtext_format" desc:"subtext format. placeholders: %LOCK%, %STATUS%, %SIGNAL%, %FREQUENCY%, %SECURITY%" default:"%LOCK% %STATUS% %SIGNAL% %FREQUENCY% %SECURITY%"` + ReopenAfterFail bool `koanf:"reopen_after_fail" desc:"reopen wifi menu after connection failure" default:"true"` + ReopenAfterConnect bool `koanf:"reopen_after_connect" desc:"reopen wifi menu after successful connection" default:"false"` + ShowPasswordDots bool `koanf:"show_password_dots" desc:"show dots while typing password in terminal" default:"true"` } type Network struct { @@ -73,10 +79,13 @@ func LoadConfig() { Icon: "network-wireless-symbolic", MinScore: 20, }, - MessageTime: 1, - ErrorTime: 3, - Backend: "auto", - SubtextFormat: "{lock} {status} {signal} {frequency} {security}", + MessageTime: 1, + ErrorTime: 3, + Backend: "auto", + SubtextFormat: "%LOCK% %STATUS% %SIGNAL% %FREQUENCY% %SECURITY%", + ReopenAfterFail: true, + ReopenAfterConnect: false, + ShowPasswordDots: true, } common.LoadConfig(Name, config) @@ -123,7 +132,7 @@ func Activate(single bool, identifier, action string, query string, args string, slog.Error(Name, "activate", err) } on = true - backend.Scan() + backend.WaitForNetworks() case ActionWifiOff: handlers.ProviderUpdated <- "wifi:wifioff" if err := backend.SetWifiEnabled(false); err != nil { @@ -134,29 +143,19 @@ func Activate(single bool, identifier, action string, query string, args string, time.Sleep(time.Duration(config.MessageTime) * time.Second) case ActionScan: handlers.ProviderUpdated <- "wifi:scan" - backend.Scan() + backend.WaitForNetworks() case ActionConnect: - if args == "" && !selNetwork.Known && selNetwork.Security != "" { - handlers.ProviderUpdated <- "wifi:password_required" - time.Sleep(time.Duration(config.ErrorTime) * time.Second) + if !selNetwork.Known && selNetwork.Security != "" { + handlers.ProviderUpdated <- "wifi:connect" + go connectWithTerminal(identifier) return } handlers.ProviderUpdated <- "wifi:connect" - password := args - if selNetwork.Known { - password = "" - } - - wasKnown := selNetwork.Known - - if err := backend.Connect(identifier, password); err != nil { + if err := backend.Connect(identifier, ""); err != nil { handlers.ProviderUpdated <- "wifi:connect_failed" slog.Error(Name, "activate", err) - if !wasKnown { - backend.Forget(identifier) - } time.Sleep(time.Duration(config.ErrorTime) * time.Second) } else { time.Sleep(time.Duration(config.MessageTime) * time.Second) @@ -334,11 +333,11 @@ func formatSubtext(n Network) string { } r := strings.NewReplacer( - "{lock}", lock, - "{status}", status, - "{signal}", signal, - "{frequency}", n.Frequency, - "{security}", n.Security, + "%LOCK%", lock, + "%STATUS%", status, + "%SIGNAL%", signal, + "%FREQUENCY%", n.Frequency, + "%SECURITY%", n.Security, ) result := r.Replace(config.SubtextFormat) @@ -360,3 +359,103 @@ func findNetwork(ssid string) *Network { return nil } + +func connectWithTerminal(ssid string) { + r, w, err := os.Pipe() + if err != nil { + slog.Error(Name, "activate", err) + return + } + defer r.Close() + + dot := "●" + backspace := `printf '\b \b'` + if !config.ShowPasswordDots { + dot = "" + backspace = "" + } + + repl := strings.NewReplacer( + "__DOT__", dot, + "__BACKSPACE__", backspace, + "__FD__", fmt.Sprintf("/proc/%d/fd/%d", os.Getpid(), w.Fd()), + ) + + script := repl.Replace(` + printf 'Password for %s: ' "$WIFI_SSID" + pw="" + bs=$(printf '\177') + while IFS= read -rsn1 c; do + [ -z "$c" ] && break + if [ "$c" = "$bs" ]; then + if [ -n "$pw" ]; then + pw="${pw%?}" + __BACKSPACE__ + fi + else + pw="$pw$c" + printf '__DOT__' + fi + done + echo + echo "$pw" > __FD__`, + ) + + terminal := common.GetTerminal() + if terminal == "" { + w.Close() + slog.Error(Name, "activate", "no terminal found") + return + } + + cmd := exec.Command(terminal, "-e", "bash", "-c", script) + cmd.Env = append(os.Environ(), "WIFI_SSID="+ssid) // Injected for sanitization + if err := cmd.Start(); err != nil { + w.Close() + slog.Error(Name, "activate", err) + return + } + + cmd.Wait() + w.Close() + + data, err := io.ReadAll(r) + if err != nil { + slog.Error(Name, "activate", err) + return + } + + password := strings.TrimSpace(string(data)) + if password == "" { + return + } + + if err := backend.Connect(ssid, password); err != nil { + slog.Error(Name, "activate", err) + backend.Forget(ssid) + + if config.ReopenAfterFail { + reopenWifiMenu() + for range int(config.ErrorTime * 4) { + handlers.ProviderUpdated <- "wifi:connect_failed" + time.Sleep(250 * time.Millisecond) + } + handlers.ProviderUpdated <- "wifi:reset" + } + } else { + if config.ReopenAfterConnect { + reopenWifiMenu() + } + time.Sleep(time.Duration(config.MessageTime) * time.Second) + } +} + +func reopenWifiMenu() { + // "elephant menu wifi" doesnt seem to work + cmd := exec.Command("walker", "-m", "wifi") + if err := cmd.Start(); err != nil { + slog.Error(Name, "reopen", err) + return + } + go cmd.Wait() +} From 286cb6a120d2ebb6dff6711208a54395ee900336 Mon Sep 17 00:00:00 2001 From: Davemane42 Date: Tue, 24 Mar 2026 04:55:57 -0400 Subject: [PATCH 3/4] feat: enhance WiFi provider with customizable password prompts and improved backend detection --- internal/providers/wifi/README.md | 31 ++- internal/providers/wifi/backend.go | 28 ++- internal/providers/wifi/customPrompt.go | 59 +++++ internal/providers/wifi/dmenuPrompt.go | 77 ++++++ internal/providers/wifi/networkManager.go | 69 +++++- internal/providers/wifi/passwordPrompt.go | 41 ++++ internal/providers/wifi/setup.go | 280 +++++++++------------- internal/providers/wifi/terminalPrompt.go | 88 +++++++ 8 files changed, 477 insertions(+), 196 deletions(-) create mode 100644 internal/providers/wifi/customPrompt.go create mode 100644 internal/providers/wifi/dmenuPrompt.go create mode 100644 internal/providers/wifi/passwordPrompt.go create mode 100644 internal/providers/wifi/terminalPrompt.go diff --git a/internal/providers/wifi/README.md b/internal/providers/wifi/README.md index 38e18cd5..585fcc0f 100644 --- a/internal/providers/wifi/README.md +++ b/internal/providers/wifi/README.md @@ -2,7 +2,7 @@ WiFi network management. Scan, connect, disconnect, and forget networks. -Connecting to a new secured network will open a terminal to prompt for the password. +Connecting to a new secured network will prompt for the password using the configured authentication agent. #### Requirements @@ -10,18 +10,35 @@ One of the following utils/backend: - `nmcli` - Network Manager +#### Password Prompts + +When connecting to a new password-protected network, a password prompt is used to request the password: + +- `terminal` - Opens a terminal window for password input +- `dmenu` - Uses a dmenu-compatible tool (walker, rofi, wofi) +- `custom` - Uses a custom command defined in `custom_prompt_command` + #### Configuration -- `backend` string - WiFi backend - - `auto` - Will try all backend until one works +- `backend` string - WiFi backend (default: `auto`) + - `auto` - Will try all backends until one works - `nm` - Network Manager -- `message_time` int - Seconds to show status messages - - (e.g. Connecting, Disconnecting) -- `error_time` int - Seconds to show error messages - - (e.g. Connection failed, Password required) +- `password_prompt` string - Password prompt method (default: `dmenu`) + - `auto` - Will try all prompts until one works + - `terminal` - Terminal-based password prompt + - `dmenu` - dmenu-compatible tool + - `custom` - Custom command +- `dmenu_command` string - dmenu-compatible tool to use (default: `walker`) + - `auto` - Will try walker, rofi, wofi in order + - `walker`, `rofi`, `wofi` - Use a specific tool +- `custom_prompt_command` string - Custom command for the `custom` password prompt. Use `%PROMPT%` as placeholder for the prompt text + - e.g. `rofi -dmenu -password -p %PROMPT%` +- `message_time` int - Seconds to show status messages (default: `1`) +- `error_time` int - Seconds to show error messages (default: `3`) - `reopen_after_fail` bool - Reopen wifi menu after connection failure (default: `true`) - `reopen_after_connect` bool - Reopen wifi menu after successful connection (default: `false`) - `show_password_dots` bool - Show dots while typing password in terminal (default: `true`) +- `notify` bool - Show desktop notifications (default: `true`) - `subtext_format` string - Format string for the subtext displayed under each network - `%LOCK%` - security icon: 🔓 (secured + saved), 🔒 (secured), 🌐 (open) - `%STATUS%` - connection status: `Connected`, `Saved`, or empty diff --git a/internal/providers/wifi/backend.go b/internal/providers/wifi/backend.go index 1f09b4f3..ea596ce9 100644 --- a/internal/providers/wifi/backend.go +++ b/internal/providers/wifi/backend.go @@ -1,10 +1,9 @@ package main -import ( - "os/exec" -) +import "log/slog" type Backend interface { + Available() bool CheckWifiState() bool SetWifiEnabled(enabled bool) error GetNetworks() []Network @@ -14,23 +13,26 @@ type Backend interface { WaitForNetworks() } -var backends = map[string]func() Backend{ - "nm": func() Backend { return &NmcliBackend{} }, -} +var backends = map[string]Backend{} func detectBackend(preference string) Backend { if preference != "auto" { - if fn, ok := backends[preference]; ok { - if p, err := exec.LookPath(preference); p != "" && err == nil { - return fn() + if b, ok := backends[preference]; ok { + if b.Available() { + slog.Info(Name, "detectBackend", preference) + return b } + slog.Warn(Name, "detectBackend", preference+" not available, trying others") } - return nil } - for name, fn := range backends { - if p, err := exec.LookPath(name); p != "" && err == nil { - return fn() + for name, b := range backends { + if name == preference { + continue + } + if b.Available() { + slog.Info(Name, "detectBackend", name) + return b } } diff --git a/internal/providers/wifi/customPrompt.go b/internal/providers/wifi/customPrompt.go new file mode 100644 index 00000000..1e491734 --- /dev/null +++ b/internal/providers/wifi/customPrompt.go @@ -0,0 +1,59 @@ +package main + +import ( + "fmt" + "log/slog" + "os/exec" + "strings" +) + +func init() { + passwordPrompts["custom"] = &CustomPrompt{} +} + +type CustomPrompt struct { + name string + args []string +} + +func (a *CustomPrompt) Available() bool { + if a.name != "" { + return true + } + + if config == nil || config.CustomPromptCommand == "" { + slog.Warn(Name, "custom_Available", "custom_prompt_command is empty") + return false + } + + parts := strings.Fields(config.CustomPromptCommand) + if p, err := exec.LookPath(parts[0]); p == "" || err != nil { + slog.Warn(Name, "custom_Available", parts[0]+" not found") + return false + } + + a.name = parts[0] + a.args = parts[1:] + return true +} + +func (a *CustomPrompt) RequestPassword(ssid string) (string, error) { + if a.name == "" { + return "", fmt.Errorf("no custom prompt command configured") + } + + prompt := fmt.Sprintf("Password for %s: ", ssid) + + args := make([]string, len(a.args)) + for i, arg := range a.args { + args[i] = strings.ReplaceAll(arg, "%PROMPT%", prompt) + } + + cmd := exec.Command(a.name, args...) + out, err := cmd.Output() + if err != nil { + return "", nil // user cancelled + } + + return strings.TrimRight(string(out), "\n\r"), nil +} diff --git a/internal/providers/wifi/dmenuPrompt.go b/internal/providers/wifi/dmenuPrompt.go new file mode 100644 index 00000000..b99721d5 --- /dev/null +++ b/internal/providers/wifi/dmenuPrompt.go @@ -0,0 +1,77 @@ +package main + +import ( + "fmt" + "log/slog" + "os/exec" + "strings" +) + +var dmenuTools = map[string][]string{ + "walker": {"-d", "-x", "--maxheight", "1", "-p", "%PROMPT%"}, + "rofi": {"-dmenu", "-password", "-p", "%PROMPT%"}, + "wofi": {"-d", "-P", "-L", "1", "-W", "300", "-b", "-p", "%PROMPT%"}, +} + +func init() { + passwordPrompts["dmenu"] = &DmenuPrompt{} +} + +type DmenuPrompt struct { + name string + args []string +} + +func (a *DmenuPrompt) Available() bool { + if a.name != "" { + return true + } + + if config == nil { + return false + } + + tool := config.DmenuCommand + + if tool != "auto" { + if args, ok := dmenuTools[tool]; ok { + if p, err := exec.LookPath(tool); p != "" && err == nil { + a.name = tool + a.args = args + return true + } + } + slog.Warn(Name, "dmenu_Available", tool+" not available, trying auto-detect") + } + + for name, args := range dmenuTools { + if p, err := exec.LookPath(name); p != "" && err == nil { + a.name = name + a.args = args + return true + } + } + + return false +} + +func (a *DmenuPrompt) RequestPassword(ssid string) (string, error) { + if a.name == "" { + return "", fmt.Errorf("no dmenu-compatible tool found") + } + + prompt := fmt.Sprintf("Password for %s: ", ssid) + + args := make([]string, len(a.args)) + for i, arg := range a.args { + args[i] = strings.ReplaceAll(arg, "%PROMPT%", prompt) + } + + cmd := exec.Command(a.name, args...) + out, err := cmd.Output() + if err != nil { + return "", nil // user cancelled + } + + return strings.TrimRight(string(out), "\n\r"), nil +} diff --git a/internal/providers/wifi/networkManager.go b/internal/providers/wifi/networkManager.go index dcf6ba17..96366b6e 100644 --- a/internal/providers/wifi/networkManager.go +++ b/internal/providers/wifi/networkManager.go @@ -3,12 +3,22 @@ package main import ( "log/slog" "os/exec" + "strconv" "strings" "time" ) +func init() { + backends["nm"] = &NmcliBackend{} +} + type NmcliBackend struct{} +func (b *NmcliBackend) Available() bool { + p, err := exec.LookPath("nmcli") + return p != "" && err == nil +} + func (b *NmcliBackend) CheckWifiState() bool { cmd := exec.Command("nmcli", "radio", "wifi") out, err := cmd.CombinedOutput() @@ -27,7 +37,7 @@ func (b *NmcliBackend) SetWifiEnabled(enabled bool) error { cmd := exec.Command("nmcli", "radio", "wifi", state) if out, err := cmd.CombinedOutput(); err != nil { - slog.Error(Name, "set wifi", string(out)) + slog.Error(Name, "nmcli_SetWifiEnabled", string(out)) return err } @@ -41,7 +51,7 @@ func (b *NmcliBackend) GetNetworks() []Network { cmd := exec.Command("nmcli", "-t", "-f", "NAME,UUID,TYPE", "connection", "show") out, err := cmd.CombinedOutput() if err != nil { - slog.Error(Name, "get connections", err) + slog.Error(Name, "nmcli_GetNetworks", err) } for l := range strings.Lines(strings.TrimSpace(string(out))) { @@ -49,7 +59,7 @@ func (b *NmcliBackend) GetNetworks() []Network { if l == "" { continue } - fields := strings.SplitN(l, ":", 3) + fields := nmcliSplitFields(l, 3) if len(fields) == 3 && fields[2] == "802-11-wireless" { known[fields[0]] = fields[1] } @@ -58,7 +68,7 @@ func (b *NmcliBackend) GetNetworks() []Network { cmd = exec.Command("nmcli", "-t", "-f", "SSID,SIGNAL,SECURITY,IN-USE,FREQ", "device", "wifi", "list") out, err = cmd.CombinedOutput() if err != nil { - slog.Error(Name, "get wifi list", err) + slog.Error(Name, "nmcli_GetNetworks", err) return result } @@ -70,7 +80,7 @@ func (b *NmcliBackend) GetNetworks() []Network { continue } - fields := strings.SplitN(l, ":", 5) + fields := nmcliSplitFields(l, 5) if len(fields) < 5 { continue } @@ -85,12 +95,15 @@ func (b *NmcliBackend) GetNetworks() []Network { } seen[ssid] = struct{}{} + signal, _ := strconv.Atoi(strings.TrimSpace(fields[1])) + freq, _ := strconv.Atoi(strings.TrimSpace(strings.TrimSuffix(strings.TrimSpace(fields[4]), " MHz"))) + n := Network{ SSID: ssid, - Signal: fields[1], + Signal: signal, Security: fields[2], InUse: strings.TrimSpace(fields[3]) == "*", - Frequency: freqBand(fields[4]), + Frequency: freq, } if _, ok := known[ssid]; ok { @@ -111,7 +124,7 @@ func (b *NmcliBackend) Connect(ssid string, password string) error { } if out, err := cmd.CombinedOutput(); err != nil { - slog.Error(Name, "connect", string(out)) + slog.Error(Name, "nmcli_Connect", string(out)) return err } @@ -121,7 +134,7 @@ func (b *NmcliBackend) Connect(ssid string, password string) error { func (b *NmcliBackend) Disconnect(ssid string) error { cmd := exec.Command("nmcli", "connection", "down", ssid) if out, err := cmd.CombinedOutput(); err != nil { - slog.Error(Name, "disconnect", string(out)) + slog.Error(Name, "nmcli_Disconnect", string(out)) return err } @@ -131,7 +144,7 @@ func (b *NmcliBackend) Disconnect(ssid string) error { func (b *NmcliBackend) Forget(ssid string) error { cmd := exec.Command("nmcli", "connection", "delete", ssid) if out, err := cmd.CombinedOutput(); err != nil { - slog.Error(Name, "forget", string(out)) + slog.Error(Name, "nmcli_Forget", string(out)) return err } @@ -139,12 +152,42 @@ func (b *NmcliBackend) Forget(ssid string) error { } func (b *NmcliBackend) WaitForNetworks() { - for range 10 { + maxTime := 5 //sec + delay := 500 //ms + for range int(maxTime * 1000 / delay) { out, err := exec.Command("nmcli", "-t", "-f", "SSID", "device", "wifi", "list", "--rescan", "yes").CombinedOutput() if err == nil && strings.TrimSpace(string(out)) != "" { return } - time.Sleep(500 * time.Millisecond) + time.Sleep(time.Duration(delay) * time.Millisecond) + } + slog.Warn(Name, "nmcli_WaitForNetworks", "max retries reached") +} + +// nmcliSplitFields splits an nmcli terse-mode line on unescaped colons. +func nmcliSplitFields(line string, n int) []string { + var fields []string + var buf strings.Builder + escaped := false + + for _, r := range line { + if escaped { + buf.WriteRune(r) + escaped = false + continue + } + if r == '\\' { + escaped = true + continue + } + if r == ':' && (n <= 0 || len(fields) < n-1) { + fields = append(fields, buf.String()) + buf.Reset() + continue + } + buf.WriteRune(r) } - slog.Warn(Name, "scan", "max retries reached") + + fields = append(fields, buf.String()) + return fields } diff --git a/internal/providers/wifi/passwordPrompt.go b/internal/providers/wifi/passwordPrompt.go new file mode 100644 index 00000000..9eafc0aa --- /dev/null +++ b/internal/providers/wifi/passwordPrompt.go @@ -0,0 +1,41 @@ +package main + +import "log/slog" + +type PasswordPrompt interface { + Available() bool + RequestPassword(ssid string) (string, error) +} + +var passwordPrompts = map[string]PasswordPrompt{} + +func detectPasswordPrompt(preference string) PasswordPrompt { + if preference != "auto" { + if p, ok := passwordPrompts[preference]; ok { + if p.Available() { + slog.Info(Name, "detectPasswordPrompt", preference) + return p + } + slog.Warn(Name, "detectPasswordPrompt", preference+" not available, trying others") + } + } + + for name, p := range passwordPrompts { + if name == preference || name == "terminal" { + continue + } + if p.Available() { + slog.Info(Name, "detectPasswordPrompt", name) + return p + } + } + + if preference != "terminal" { + if p, ok := passwordPrompts["terminal"]; ok && p.Available() { + slog.Warn(Name, "detectPasswordPrompt", "falling back to terminal") + return p + } + } + + return nil +} diff --git a/internal/providers/wifi/setup.go b/internal/providers/wifi/setup.go index 118c6882..6bc3a872 100644 --- a/internal/providers/wifi/setup.go +++ b/internal/providers/wifi/setup.go @@ -2,12 +2,9 @@ package main import ( "fmt" - "io" "log/slog" "net" - "os" "os/exec" - "strconv" "strings" "time" @@ -20,39 +17,44 @@ import ( ) var ( - Name = "wifi" - NamePretty = "WiFi" - on = true + Name = "wifi" + NamePretty = "WiFi" + wifiEnabled = true ) //go:embed README.md var readme string type Config struct { - common.Config `koanf:",squash"` - MessageTime int `koanf:"message_time" desc:"seconds to show status messages" default:"1"` - ErrorTime int `koanf:"error_time" desc:"seconds to show error messages" default:"3"` - Backend string `koanf:"backend" desc:"wifi backend: auto, nm" default:"auto"` - SubtextFormat string `koanf:"subtext_format" desc:"subtext format. placeholders: %LOCK%, %STATUS%, %SIGNAL%, %FREQUENCY%, %SECURITY%" default:"%LOCK% %STATUS% %SIGNAL% %FREQUENCY% %SECURITY%"` - ReopenAfterFail bool `koanf:"reopen_after_fail" desc:"reopen wifi menu after connection failure" default:"true"` - ReopenAfterConnect bool `koanf:"reopen_after_connect" desc:"reopen wifi menu after successful connection" default:"false"` - ShowPasswordDots bool `koanf:"show_password_dots" desc:"show dots while typing password in terminal" default:"true"` + common.Config `koanf:",squash"` + MessageTime int `koanf:"message_time" desc:"seconds to show status messages" default:"1"` + ErrorTime int `koanf:"error_time" desc:"seconds to show error messages" default:"3"` + Backend string `koanf:"backend" desc:"wifi backend: auto, nm" default:"auto"` + PasswordPrompt string `koanf:"password_prompt" desc:"password prompt method: auto, terminal, dmenu, custom" default:"dmenu"` + DmenuCommand string `koanf:"dmenu_command" desc:"dmenu-compatible tool: auto, walker, rofi, wofi" default:"walker"` + CustomPromptCommand string `koanf:"custom_prompt_command" desc:"custom command for 'custom' password prompt. use %PROMPT% as placeholder" default:""` + SubtextFormat string `koanf:"subtext_format" desc:"subtext format. placeholders: %LOCK%, %STATUS%, %SIGNAL%, %FREQUENCY%, %SECURITY%" default:"%LOCK% %STATUS% %SIGNAL% %FREQUENCY% %SECURITY%"` + ReopenAfterFail bool `koanf:"reopen_after_fail" desc:"reopen wifi menu after connection failure" default:"true"` + ReopenAfterConnect bool `koanf:"reopen_after_connect" desc:"reopen wifi menu after successful connection" default:"false"` + ShowPasswordDots bool `koanf:"show_password_dots" desc:"show dots while typing password in terminal" default:"true"` + Notify bool `koanf:"notify" desc:"show desktop notifications" default:"true"` } type Network struct { SSID string - Signal string + Signal int Security string - Frequency string + Frequency int InUse bool Known bool UUID string } var ( - networks []Network - config *Config - backend Backend + networks []Network + config *Config + backend Backend + passwordPrompt PasswordPrompt ) func Setup() { @@ -62,15 +64,18 @@ func Setup() { NamePretty = config.NamePretty } - if config.Backend != "auto" { - if b := detectBackend(config.Backend); b != nil { - backend = b - } else { - slog.Warn(Name, "backend", fmt.Sprintf("configured backend %q not available, using auto-detected", config.Backend)) - } + backend = detectBackend(config.Backend) + if backend == nil { + slog.Error(Name, "Setup", "no backend available") + return } - on = backend.CheckWifiState() + passwordPrompt = detectPasswordPrompt(config.PasswordPrompt) + if passwordPrompt == nil { + slog.Warn(Name, "Setup", "no password prompt found, password-protected networks will be skipped") + } + + wifiEnabled = backend.CheckWifiState() } func LoadConfig() { @@ -82,19 +87,25 @@ func LoadConfig() { MessageTime: 1, ErrorTime: 3, Backend: "auto", + PasswordPrompt: "dmenu", + DmenuCommand: "walker", SubtextFormat: "%LOCK% %STATUS% %SIGNAL% %FREQUENCY% %SECURITY%", ReopenAfterFail: true, ReopenAfterConnect: false, ShowPasswordDots: true, + Notify: true, } common.LoadConfig(Name, config) } func Available() bool { - backend = detectBackend("auto") if backend == nil { - slog.Info(Name, "available", "no wifi backend found (nmcli/iwctl). disabling") + backend = detectBackend("auto") + } + + if backend == nil { + slog.Info(Name, "Available", "no wifi backend found (nmcli). disabling") return false } @@ -121,7 +132,7 @@ const ( func Activate(single bool, identifier, action string, query string, args string, format uint8, conn net.Conn) { selNetwork := findNetwork(identifier) if selNetwork == nil && (action == ActionConnect || action == ActionDisconnect || action == ActionForget) { - slog.Error(Name, "activate", fmt.Sprintf("network %q not found", identifier)) + slog.Error(Name, "Activate", fmt.Sprintf("network %q not found", identifier)) return } @@ -129,51 +140,88 @@ func Activate(single bool, identifier, action string, query string, args string, case ActionWifiOn: handlers.ProviderUpdated <- "wifi:wifion" if err := backend.SetWifiEnabled(true); err != nil { - slog.Error(Name, "activate", err) + slog.Error(Name, "Activate_WifiOn", err) + return } - on = true + wifiEnabled = true backend.WaitForNetworks() case ActionWifiOff: handlers.ProviderUpdated <- "wifi:wifioff" if err := backend.SetWifiEnabled(false); err != nil { - slog.Error(Name, "activate", err) + slog.Error(Name, "Activate_WifiOff", err) + return } - on = false + wifiEnabled = false networks = nil time.Sleep(time.Duration(config.MessageTime) * time.Second) case ActionScan: handlers.ProviderUpdated <- "wifi:scan" backend.WaitForNetworks() case ActionConnect: + handlers.ProviderUpdated <- "wifi:connect" + + password := "" if !selNetwork.Known && selNetwork.Security != "" { - handlers.ProviderUpdated <- "wifi:connect" - go connectWithTerminal(identifier) - return + if passwordPrompt == nil { + slog.Error(Name, "Activate_Connect", "no password prompt available for password-protected network") + return + } + + var err error + password, err = passwordPrompt.RequestPassword(identifier) + if err != nil { + slog.Error(Name, "Activate_Connect", err) + return + } + + if password == "" { + slog.Info(Name, "Activate_Connect", "password prompt cancelled") + return + } } - handlers.ProviderUpdated <- "wifi:connect" + if err := backend.Connect(identifier, password); err != nil { + slog.Error(Name, "Activate_Connect", err) + if err := backend.Forget(identifier); err != nil { + slog.Warn(Name, "Activate_Connect", "forget after failed connect: "+err.Error()) + } + + if password != "" { + notify(fmt.Sprintf("Wrong password for %s", identifier)) + } else { + notify(fmt.Sprintf("Failed to connect to %s", identifier)) + } - if err := backend.Connect(identifier, ""); err != nil { - handlers.ProviderUpdated <- "wifi:connect_failed" - slog.Error(Name, "activate", err) - time.Sleep(time.Duration(config.ErrorTime) * time.Second) + if config.ReopenAfterFail { + reopenWifiMenu() + delay := 100 //in ms + for range int(config.ErrorTime * 1000 / delay) { + handlers.ProviderUpdated <- "wifi:connect_failed" + time.Sleep(time.Duration(delay) * time.Millisecond) + } + handlers.ProviderUpdated <- "wifi:reset" + } } else { + notify(fmt.Sprintf("Connected to %s", identifier)) + if config.ReopenAfterConnect { + reopenWifiMenu() + } time.Sleep(time.Duration(config.MessageTime) * time.Second) } case ActionDisconnect: handlers.ProviderUpdated <- "wifi:disconnect" - if err := backend.Disconnect(identifier); err != nil { - slog.Error(Name, "activate", err) + if err := backend.Disconnect(selNetwork.SSID); err != nil { + slog.Error(Name, "Activate_Disconnect", err) } time.Sleep(time.Duration(config.MessageTime) * time.Second) case ActionForget: handlers.ProviderUpdated <- "wifi:forget" - if err := backend.Forget(identifier); err != nil { - slog.Error(Name, "activate", err) + if err := backend.Forget(selNetwork.SSID); err != nil { + slog.Error(Name, "Activate_Forget", err) } time.Sleep(time.Duration(config.MessageTime) * time.Second) default: - slog.Error(Name, "activate", fmt.Sprintf("unknown action: %s", action)) + slog.Error(Name, "Activate", fmt.Sprintf("unknown action: %s", action)) return } } @@ -182,7 +230,7 @@ func Query(conn net.Conn, query string, _ bool, exact bool, _ uint8) []*pb.Query start := time.Now() entries := []*pb.QueryResponse_Item{} - if !on { + if !wifiEnabled { return entries } @@ -228,13 +276,13 @@ func Query(conn net.Conn, query string, _ bool, exact bool, _ uint8) []*pb.Query } if query != "" { - score, pos, start := common.FuzzyScore(query, v.SSID, exact) + score, pos, matchStart := common.FuzzyScore(query, v.SSID, exact) e.Score = score e.Fuzzyinfo = &pb.QueryResponse_Item_FuzzyInfo{ Field: "text", Positions: pos, - Start: start, + Start: matchStart, } } @@ -243,7 +291,7 @@ func Query(conn net.Conn, query string, _ bool, exact bool, _ uint8) []*pb.Query } } - slog.Debug(Name, "query", time.Since(start)) + slog.Debug(Name, "Query", time.Since(start)) return entries } @@ -258,7 +306,7 @@ func HideFromProviderlist() bool { func State(provider string) *pb.ProviderStateResponse { actions := []string{} - if on { + if wifiEnabled { actions = append(actions, ActionWifiOff, ActionScan) } else { actions = append(actions, ActionWifiOn) @@ -271,36 +319,23 @@ func State(provider string) *pb.ProviderStateResponse { } } -func signalIcon(signal string) string { - if signal == "" { - return "network-wireless-symbolic" - } - - var level int - fmt.Sscanf(signal, "%d", &level) - +func signalIcon(signal int) string { switch { - case level >= 75: + case signal >= 75: return "network-wireless-signal-excellent-symbolic" - case level >= 50: + case signal >= 50: return "network-wireless-signal-good-symbolic" - case level >= 25: + case signal >= 25: return "network-wireless-signal-ok-symbolic" default: return "network-wireless-signal-weak-symbolic" } } -// freqBand converts a frequency string like "5220 MHz" to a band label like "5 GHz". -func freqBand(freq string) string { - freq = strings.TrimSpace(freq) - freq = strings.TrimSuffix(freq, " MHz") - mhz, err := strconv.Atoi(freq) - if err != nil { - return "" - } - +func freqBand(mhz int) string { switch { + case mhz <= 0: + return "" case mhz < 5000: return "2.4 GHz" case mhz < 5925: @@ -328,15 +363,15 @@ func formatSubtext(n Network) string { } signal := "" - if n.Signal != "" { - signal = fmt.Sprintf("%s%%", n.Signal) + if n.Signal > 0 { + signal = fmt.Sprintf("%d%%", n.Signal) } r := strings.NewReplacer( "%LOCK%", lock, "%STATUS%", status, "%SIGNAL%", signal, - "%FREQUENCY%", n.Frequency, + "%FREQUENCY%", freqBand(n.Frequency), "%SECURITY%", n.Security, ) @@ -360,102 +395,21 @@ func findNetwork(ssid string) *Network { return nil } -func connectWithTerminal(ssid string) { - r, w, err := os.Pipe() - if err != nil { - slog.Error(Name, "activate", err) - return - } - defer r.Close() - - dot := "●" - backspace := `printf '\b \b'` - if !config.ShowPasswordDots { - dot = "" - backspace = "" - } - - repl := strings.NewReplacer( - "__DOT__", dot, - "__BACKSPACE__", backspace, - "__FD__", fmt.Sprintf("/proc/%d/fd/%d", os.Getpid(), w.Fd()), - ) - - script := repl.Replace(` - printf 'Password for %s: ' "$WIFI_SSID" - pw="" - bs=$(printf '\177') - while IFS= read -rsn1 c; do - [ -z "$c" ] && break - if [ "$c" = "$bs" ]; then - if [ -n "$pw" ]; then - pw="${pw%?}" - __BACKSPACE__ - fi - else - pw="$pw$c" - printf '__DOT__' - fi - done - echo - echo "$pw" > __FD__`, - ) - - terminal := common.GetTerminal() - if terminal == "" { - w.Close() - slog.Error(Name, "activate", "no terminal found") +func notify(msg string) { + if !config.Notify { return } - - cmd := exec.Command(terminal, "-e", "bash", "-c", script) - cmd.Env = append(os.Environ(), "WIFI_SSID="+ssid) // Injected for sanitization - if err := cmd.Start(); err != nil { - w.Close() - slog.Error(Name, "activate", err) - return - } - - cmd.Wait() - w.Close() - - data, err := io.ReadAll(r) + slog.Debug(Name, "notify", msg) + err := exec.Command("notify-send", "-a", "elephant", "-i", config.Icon, "-e", msg).Run() if err != nil { - slog.Error(Name, "activate", err) - return - } - - password := strings.TrimSpace(string(data)) - if password == "" { - return - } - - if err := backend.Connect(ssid, password); err != nil { - slog.Error(Name, "activate", err) - backend.Forget(ssid) - - if config.ReopenAfterFail { - reopenWifiMenu() - for range int(config.ErrorTime * 4) { - handlers.ProviderUpdated <- "wifi:connect_failed" - time.Sleep(250 * time.Millisecond) - } - handlers.ProviderUpdated <- "wifi:reset" - } - } else { - if config.ReopenAfterConnect { - reopenWifiMenu() - } - time.Sleep(time.Duration(config.MessageTime) * time.Second) + slog.Error(Name, "notify", err) } } func reopenWifiMenu() { // "elephant menu wifi" doesnt seem to work - cmd := exec.Command("walker", "-m", "wifi") - if err := cmd.Start(); err != nil { - slog.Error(Name, "reopen", err) - return + err := exec.Command("walker", "-m", "wifi").Run() + if err != nil { + slog.Error(Name, "reopenWifiMenu", err) } - go cmd.Wait() } diff --git a/internal/providers/wifi/terminalPrompt.go b/internal/providers/wifi/terminalPrompt.go new file mode 100644 index 00000000..d9b4fdee --- /dev/null +++ b/internal/providers/wifi/terminalPrompt.go @@ -0,0 +1,88 @@ +package main + +import ( + "fmt" + "io" + "log/slog" + "os" + "os/exec" + "strings" + + "github.com/abenz1267/elephant/v2/pkg/common" +) + +func init() { + passwordPrompts["terminal"] = &TerminalPrompt{} +} + +type TerminalPrompt struct{} + +func (a *TerminalPrompt) Available() bool { + return common.GetTerminal() != "" +} + +func (a *TerminalPrompt) RequestPassword(ssid string) (string, error) { + r, w, err := os.Pipe() + if err != nil { + return "", err + } + defer r.Close() + + dot := "●" + backspace := `printf '\b \b'` + if !config.ShowPasswordDots { + dot = "" + backspace = "" + } + + repl := strings.NewReplacer( + "__DOT__", dot, + "__BACKSPACE__", backspace, + "__FD__", fmt.Sprintf("/proc/%d/fd/%d", os.Getpid(), w.Fd()), + ) + + script := repl.Replace(` + printf 'Password for %s: ' "$WIFI_SSID" + pw="" + bs=$(printf '\177') + while IFS= read -rsn1 c; do + [ -z "$c" ] && break + if [ "$c" = "$bs" ]; then + if [ -n "$pw" ]; then + pw="${pw%?}" + __BACKSPACE__ + fi + else + pw="$pw$c" + printf '__DOT__' + fi + done + echo + echo "$pw" > __FD__`, + ) + + terminal := common.GetTerminal() + if terminal == "" { + w.Close() + return "", fmt.Errorf("no terminal found") + } + + cmd := exec.Command(terminal, "-e", "bash", "-c", script) + cmd.Env = append(os.Environ(), "WIFI_SSID="+ssid) + if err := cmd.Start(); err != nil { + w.Close() + return "", err + } + + if err := cmd.Wait(); err != nil { + slog.Debug(Name, "terminal_RequestPassword", err) + } + w.Close() + + data, err := io.ReadAll(r) + if err != nil { + return "", err + } + + return strings.TrimRight(string(data), "\n\r"), nil +} From aec1fb52a2b20fd2d4608a37d58d8ba67fe3d2e1 Mon Sep 17 00:00:00 2001 From: Davemane42 Date: Wed, 25 Mar 2026 00:14:58 -0400 Subject: [PATCH 4/4] consolidated the password prompts into 1 file --- internal/providers/wifi/README.md | 17 +-- internal/providers/wifi/customPrompt.go | 59 -------- internal/providers/wifi/dmenuPrompt.go | 77 ---------- internal/providers/wifi/password.go | 163 ++++++++++++++++++++++ internal/providers/wifi/passwordPrompt.go | 41 ------ internal/providers/wifi/setup.go | 6 +- internal/providers/wifi/terminalPrompt.go | 88 ------------ 7 files changed, 174 insertions(+), 277 deletions(-) delete mode 100644 internal/providers/wifi/customPrompt.go delete mode 100644 internal/providers/wifi/dmenuPrompt.go create mode 100644 internal/providers/wifi/password.go delete mode 100644 internal/providers/wifi/passwordPrompt.go delete mode 100644 internal/providers/wifi/terminalPrompt.go diff --git a/internal/providers/wifi/README.md b/internal/providers/wifi/README.md index 585fcc0f..4cf35c6a 100644 --- a/internal/providers/wifi/README.md +++ b/internal/providers/wifi/README.md @@ -12,10 +12,14 @@ One of the following utils/backend: #### Password Prompts -When connecting to a new password-protected network, a password prompt is used to request the password: +When connecting to a new password-protected network, a password prompt is used to request the password. +If the configured prompt is not available, all presets are tried, with `terminal` as the final fallback. + +- `walker` - Walker in dmenu mode +- `rofi` - Rofi in dmenu mode +- `wofi` - Wofi in dmenu mode - `terminal` - Opens a terminal window for password input -- `dmenu` - Uses a dmenu-compatible tool (walker, rofi, wofi) - `custom` - Uses a custom command defined in `custom_prompt_command` #### Configuration @@ -23,14 +27,11 @@ When connecting to a new password-protected network, a password prompt is used t - `backend` string - WiFi backend (default: `auto`) - `auto` - Will try all backends until one works - `nm` - Network Manager -- `password_prompt` string - Password prompt method (default: `dmenu`) - - `auto` - Will try all prompts until one works +- `password_prompt` string - Password prompt method (default: `walker`) + - `auto` - Will try all presets, then fall back to terminal + - `walker`, `rofi`, `wofi` - Use a specific tool - `terminal` - Terminal-based password prompt - - `dmenu` - dmenu-compatible tool - `custom` - Custom command -- `dmenu_command` string - dmenu-compatible tool to use (default: `walker`) - - `auto` - Will try walker, rofi, wofi in order - - `walker`, `rofi`, `wofi` - Use a specific tool - `custom_prompt_command` string - Custom command for the `custom` password prompt. Use `%PROMPT%` as placeholder for the prompt text - e.g. `rofi -dmenu -password -p %PROMPT%` - `message_time` int - Seconds to show status messages (default: `1`) diff --git a/internal/providers/wifi/customPrompt.go b/internal/providers/wifi/customPrompt.go deleted file mode 100644 index 1e491734..00000000 --- a/internal/providers/wifi/customPrompt.go +++ /dev/null @@ -1,59 +0,0 @@ -package main - -import ( - "fmt" - "log/slog" - "os/exec" - "strings" -) - -func init() { - passwordPrompts["custom"] = &CustomPrompt{} -} - -type CustomPrompt struct { - name string - args []string -} - -func (a *CustomPrompt) Available() bool { - if a.name != "" { - return true - } - - if config == nil || config.CustomPromptCommand == "" { - slog.Warn(Name, "custom_Available", "custom_prompt_command is empty") - return false - } - - parts := strings.Fields(config.CustomPromptCommand) - if p, err := exec.LookPath(parts[0]); p == "" || err != nil { - slog.Warn(Name, "custom_Available", parts[0]+" not found") - return false - } - - a.name = parts[0] - a.args = parts[1:] - return true -} - -func (a *CustomPrompt) RequestPassword(ssid string) (string, error) { - if a.name == "" { - return "", fmt.Errorf("no custom prompt command configured") - } - - prompt := fmt.Sprintf("Password for %s: ", ssid) - - args := make([]string, len(a.args)) - for i, arg := range a.args { - args[i] = strings.ReplaceAll(arg, "%PROMPT%", prompt) - } - - cmd := exec.Command(a.name, args...) - out, err := cmd.Output() - if err != nil { - return "", nil // user cancelled - } - - return strings.TrimRight(string(out), "\n\r"), nil -} diff --git a/internal/providers/wifi/dmenuPrompt.go b/internal/providers/wifi/dmenuPrompt.go deleted file mode 100644 index b99721d5..00000000 --- a/internal/providers/wifi/dmenuPrompt.go +++ /dev/null @@ -1,77 +0,0 @@ -package main - -import ( - "fmt" - "log/slog" - "os/exec" - "strings" -) - -var dmenuTools = map[string][]string{ - "walker": {"-d", "-x", "--maxheight", "1", "-p", "%PROMPT%"}, - "rofi": {"-dmenu", "-password", "-p", "%PROMPT%"}, - "wofi": {"-d", "-P", "-L", "1", "-W", "300", "-b", "-p", "%PROMPT%"}, -} - -func init() { - passwordPrompts["dmenu"] = &DmenuPrompt{} -} - -type DmenuPrompt struct { - name string - args []string -} - -func (a *DmenuPrompt) Available() bool { - if a.name != "" { - return true - } - - if config == nil { - return false - } - - tool := config.DmenuCommand - - if tool != "auto" { - if args, ok := dmenuTools[tool]; ok { - if p, err := exec.LookPath(tool); p != "" && err == nil { - a.name = tool - a.args = args - return true - } - } - slog.Warn(Name, "dmenu_Available", tool+" not available, trying auto-detect") - } - - for name, args := range dmenuTools { - if p, err := exec.LookPath(name); p != "" && err == nil { - a.name = name - a.args = args - return true - } - } - - return false -} - -func (a *DmenuPrompt) RequestPassword(ssid string) (string, error) { - if a.name == "" { - return "", fmt.Errorf("no dmenu-compatible tool found") - } - - prompt := fmt.Sprintf("Password for %s: ", ssid) - - args := make([]string, len(a.args)) - for i, arg := range a.args { - args[i] = strings.ReplaceAll(arg, "%PROMPT%", prompt) - } - - cmd := exec.Command(a.name, args...) - out, err := cmd.Output() - if err != nil { - return "", nil // user cancelled - } - - return strings.TrimRight(string(out), "\n\r"), nil -} diff --git a/internal/providers/wifi/password.go b/internal/providers/wifi/password.go new file mode 100644 index 00000000..a7505261 --- /dev/null +++ b/internal/providers/wifi/password.go @@ -0,0 +1,163 @@ +package main + +import ( + "fmt" + "io" + "log/slog" + "os" + "os/exec" + "strings" + + "github.com/abenz1267/elephant/v2/pkg/common" +) + +type PasswordPrompt interface { + RequestPassword(ssid string) (string, error) +} + +var presets = map[string][]string{ + "walker": {"walker", "-d", "-x", "--maxheight", "1", "-p", "%PROMPT%"}, + "rofi": {"rofi", "-dmenu", "-password", "-p", "%PROMPT%"}, + "wofi": {"wofi", "-d", "-P", "-L", "1", "-W", "300", "-b", "-p", "%PROMPT%"}, +} + +func detectPasswordPrompt(preference string) PasswordPrompt { + // try the preference first + if preference != "auto" { + switch preference { + case "terminal": + if common.GetTerminal() != "" { + slog.Info(Name, "detectPasswordPrompt", "terminal") + return &TerminalPrompt{} + } + case "custom": + if config.CustomPromptCommand == "" { + slog.Warn(Name, "detectPasswordPrompt", "custom_prompt_command is empty") + return nil + } + parts := strings.Fields(config.CustomPromptCommand) + if p, err := exec.LookPath(parts[0]); p != "" && err == nil { + slog.Info(Name, "detectPasswordPrompt", "custom: "+parts[0]) + return &CommandPrompt{command: parts} + } + default: + if args, ok := presets[preference]; ok { + if p, err := exec.LookPath(args[0]); p != "" && err == nil { + slog.Info(Name, "detectPasswordPrompt", preference) + return &CommandPrompt{command: args} + } + } + } + slog.Warn(Name, "detectPasswordPrompt", preference+" not available, trying others") + } + + // try all presets + for name, args := range presets { + if name == preference { + continue + } + if p, err := exec.LookPath(name); p != "" && err == nil { + slog.Info(Name, "detectPasswordPrompt", name) + return &CommandPrompt{command: args} + } + } + + // terminal as fallback + if preference != "terminal" { + if common.GetTerminal() != "" { + slog.Warn(Name, "detectPasswordPrompt", "falling back to terminal") + return &TerminalPrompt{} + } + } + + return nil +} + +type CommandPrompt struct { + command []string +} + +func (c *CommandPrompt) RequestPassword(ssid string) (string, error) { + prompt := fmt.Sprintf("Password for %s: ", ssid) + + args := make([]string, len(c.command)-1) + for i, arg := range c.command[1:] { + args[i] = strings.ReplaceAll(arg, "%PROMPT%", prompt) + } + + cmd := exec.Command(c.command[0], args...) + out, err := cmd.Output() + if err != nil { + return "", nil // user cancelled + } + + return strings.TrimRight(string(out), "\n\r"), nil +} + +type TerminalPrompt struct{} + +func (a *TerminalPrompt) RequestPassword(ssid string) (string, error) { + r, w, err := os.Pipe() + if err != nil { + return "", err + } + defer r.Close() + + dot := "●" + backspace := `printf '\b \b'` + if !config.ShowPasswordDots { + dot = "" + backspace = "" + } + + repl := strings.NewReplacer( + "__DOT__", dot, + "__BACKSPACE__", backspace, + "__FD__", fmt.Sprintf("/proc/%d/fd/%d", os.Getpid(), w.Fd()), + ) + + script := repl.Replace(` + printf 'Password for %s: ' "$WIFI_SSID" + pw="" + bs=$(printf '\177') + while IFS= read -rsn1 c; do + [ -z "$c" ] && break + if [ "$c" = "$bs" ]; then + if [ -n "$pw" ]; then + pw="${pw%?}" + __BACKSPACE__ + fi + else + pw="$pw$c" + printf '__DOT__' + fi + done + echo + echo "$pw" > __FD__`, + ) + + terminal := common.GetTerminal() + if terminal == "" { + w.Close() + return "", fmt.Errorf("no terminal found") + } + + cmd := exec.Command(terminal, "-e", "bash", "-c", script) + cmd.Env = append(os.Environ(), "WIFI_SSID="+ssid) + if err := cmd.Start(); err != nil { + w.Close() + return "", err + } + + if err := cmd.Wait(); err != nil { + slog.Debug(Name, "terminal_RequestPassword", err) + } + w.Close() + + data, err := io.ReadAll(r) + if err != nil { + return "", err + } + + return strings.TrimRight(string(data), "\n\r"), nil +} diff --git a/internal/providers/wifi/passwordPrompt.go b/internal/providers/wifi/passwordPrompt.go deleted file mode 100644 index 9eafc0aa..00000000 --- a/internal/providers/wifi/passwordPrompt.go +++ /dev/null @@ -1,41 +0,0 @@ -package main - -import "log/slog" - -type PasswordPrompt interface { - Available() bool - RequestPassword(ssid string) (string, error) -} - -var passwordPrompts = map[string]PasswordPrompt{} - -func detectPasswordPrompt(preference string) PasswordPrompt { - if preference != "auto" { - if p, ok := passwordPrompts[preference]; ok { - if p.Available() { - slog.Info(Name, "detectPasswordPrompt", preference) - return p - } - slog.Warn(Name, "detectPasswordPrompt", preference+" not available, trying others") - } - } - - for name, p := range passwordPrompts { - if name == preference || name == "terminal" { - continue - } - if p.Available() { - slog.Info(Name, "detectPasswordPrompt", name) - return p - } - } - - if preference != "terminal" { - if p, ok := passwordPrompts["terminal"]; ok && p.Available() { - slog.Warn(Name, "detectPasswordPrompt", "falling back to terminal") - return p - } - } - - return nil -} diff --git a/internal/providers/wifi/setup.go b/internal/providers/wifi/setup.go index 6bc3a872..a02d3f64 100644 --- a/internal/providers/wifi/setup.go +++ b/internal/providers/wifi/setup.go @@ -30,8 +30,7 @@ type Config struct { MessageTime int `koanf:"message_time" desc:"seconds to show status messages" default:"1"` ErrorTime int `koanf:"error_time" desc:"seconds to show error messages" default:"3"` Backend string `koanf:"backend" desc:"wifi backend: auto, nm" default:"auto"` - PasswordPrompt string `koanf:"password_prompt" desc:"password prompt method: auto, terminal, dmenu, custom" default:"dmenu"` - DmenuCommand string `koanf:"dmenu_command" desc:"dmenu-compatible tool: auto, walker, rofi, wofi" default:"walker"` + PasswordPrompt string `koanf:"password_prompt" desc:"password prompt method: auto, terminal, walker, rofi, wofi, custom" default:"walker"` CustomPromptCommand string `koanf:"custom_prompt_command" desc:"custom command for 'custom' password prompt. use %PROMPT% as placeholder" default:""` SubtextFormat string `koanf:"subtext_format" desc:"subtext format. placeholders: %LOCK%, %STATUS%, %SIGNAL%, %FREQUENCY%, %SECURITY%" default:"%LOCK% %STATUS% %SIGNAL% %FREQUENCY% %SECURITY%"` ReopenAfterFail bool `koanf:"reopen_after_fail" desc:"reopen wifi menu after connection failure" default:"true"` @@ -87,8 +86,7 @@ func LoadConfig() { MessageTime: 1, ErrorTime: 3, Backend: "auto", - PasswordPrompt: "dmenu", - DmenuCommand: "walker", + PasswordPrompt: "walker", SubtextFormat: "%LOCK% %STATUS% %SIGNAL% %FREQUENCY% %SECURITY%", ReopenAfterFail: true, ReopenAfterConnect: false, diff --git a/internal/providers/wifi/terminalPrompt.go b/internal/providers/wifi/terminalPrompt.go deleted file mode 100644 index d9b4fdee..00000000 --- a/internal/providers/wifi/terminalPrompt.go +++ /dev/null @@ -1,88 +0,0 @@ -package main - -import ( - "fmt" - "io" - "log/slog" - "os" - "os/exec" - "strings" - - "github.com/abenz1267/elephant/v2/pkg/common" -) - -func init() { - passwordPrompts["terminal"] = &TerminalPrompt{} -} - -type TerminalPrompt struct{} - -func (a *TerminalPrompt) Available() bool { - return common.GetTerminal() != "" -} - -func (a *TerminalPrompt) RequestPassword(ssid string) (string, error) { - r, w, err := os.Pipe() - if err != nil { - return "", err - } - defer r.Close() - - dot := "●" - backspace := `printf '\b \b'` - if !config.ShowPasswordDots { - dot = "" - backspace = "" - } - - repl := strings.NewReplacer( - "__DOT__", dot, - "__BACKSPACE__", backspace, - "__FD__", fmt.Sprintf("/proc/%d/fd/%d", os.Getpid(), w.Fd()), - ) - - script := repl.Replace(` - printf 'Password for %s: ' "$WIFI_SSID" - pw="" - bs=$(printf '\177') - while IFS= read -rsn1 c; do - [ -z "$c" ] && break - if [ "$c" = "$bs" ]; then - if [ -n "$pw" ]; then - pw="${pw%?}" - __BACKSPACE__ - fi - else - pw="$pw$c" - printf '__DOT__' - fi - done - echo - echo "$pw" > __FD__`, - ) - - terminal := common.GetTerminal() - if terminal == "" { - w.Close() - return "", fmt.Errorf("no terminal found") - } - - cmd := exec.Command(terminal, "-e", "bash", "-c", script) - cmd.Env = append(os.Environ(), "WIFI_SSID="+ssid) - if err := cmd.Start(); err != nil { - w.Close() - return "", err - } - - if err := cmd.Wait(); err != nil { - slog.Debug(Name, "terminal_RequestPassword", err) - } - w.Close() - - data, err := io.ReadAll(r) - if err != nil { - return "", err - } - - return strings.TrimRight(string(data), "\n\r"), nil -}