mirror of
https://github.com/anotherhadi/settuings.git
synced 2026-10-05 16:18:24 +02:00
297 lines
7.7 KiB
Go
297 lines
7.7 KiB
Go
package network
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"charm.land/bubbles/v2/key"
|
|
"charm.land/bubbles/v2/textinput"
|
|
tea "charm.land/bubbletea/v2"
|
|
"charm.land/lipgloss/v2"
|
|
ilovetui "github.com/anotherhadi/ilovetui"
|
|
"github.com/anotherhadi/settuings/internal/icons"
|
|
"github.com/anotherhadi/settuings/internal/keys"
|
|
"github.com/anotherhadi/settuings/internal/network"
|
|
)
|
|
|
|
type wifiState struct {
|
|
loading bool
|
|
err error
|
|
ip string
|
|
device string // wifi interface name, e.g. "wlo1"
|
|
connectionName string // active connection profile name, e.g. "Djoon 1"
|
|
radioOn bool
|
|
current *network.WifiNetwork // the AP we're associated with, if any
|
|
others []network.WifiNetwork
|
|
cursor int
|
|
pending bool // a connect/disconnect/rescan/radio-toggle is in flight
|
|
feedback string
|
|
|
|
connecting bool
|
|
connectSSID string
|
|
password textinput.Model
|
|
}
|
|
|
|
func newWifiState() wifiState {
|
|
ti := textinput.New()
|
|
ti.Prompt = "Password: "
|
|
ti.EchoMode = textinput.EchoPassword
|
|
ti.EchoCharacter = '•'
|
|
s := ti.Styles()
|
|
s.Focused.Prompt = lipgloss.NewStyle().Foreground(ilovetui.S.Primary)
|
|
ti.SetStyles(s)
|
|
return wifiState{password: ti, loading: true}
|
|
}
|
|
|
|
func (m Model) isKnownSSID(ssid string) bool {
|
|
for _, k := range m.known.networks {
|
|
if k.Name == ssid {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (m *Model) handleWifiList(msg wifiListMsg) {
|
|
m.wifi.loading = false
|
|
m.wifi.err = msg.err
|
|
m.wifi.radioOn = msg.radioOn
|
|
if msg.err != nil {
|
|
return
|
|
}
|
|
m.wifi.ip = msg.ip
|
|
m.wifi.device = msg.device
|
|
m.wifi.connectionName = msg.connectionName
|
|
m.wifi.current = nil
|
|
m.wifi.others = m.wifi.others[:0]
|
|
for _, n := range msg.networks {
|
|
n := n
|
|
if n.Active {
|
|
m.wifi.current = &n
|
|
continue
|
|
}
|
|
m.wifi.others = append(m.wifi.others, n)
|
|
}
|
|
if m.wifi.cursor >= len(m.wifi.others) {
|
|
m.wifi.cursor = max(0, len(m.wifi.others)-1)
|
|
}
|
|
}
|
|
|
|
func (m *Model) handleWifiAction(msg wifiActionMsg) tea.Cmd {
|
|
m.wifi.pending = false
|
|
if msg.err != nil {
|
|
m.wifi.feedback = msg.err.Error()
|
|
m.deviceDetail.pending = false
|
|
m.deviceDetail.feedback = msg.err.Error()
|
|
m.refreshDeviceDetailContent()
|
|
return nil
|
|
}
|
|
m.wifi.feedback = ""
|
|
m.closeDeviceDetailIfPending(tabWifi)
|
|
return tea.Batch(fetchWifiCmd(), fetchKnownCmd())
|
|
}
|
|
|
|
func (m *Model) handleRadioAction(msg radioActionMsg) tea.Cmd {
|
|
m.wifi.pending = false
|
|
if msg.err != nil {
|
|
m.wifi.feedback = msg.err.Error()
|
|
return nil
|
|
}
|
|
m.wifi.feedback = ""
|
|
return fetchWifiCmd()
|
|
}
|
|
|
|
func (m *Model) updateWifi(msg tea.Msg) tea.Cmd {
|
|
if m.wifi.connecting {
|
|
return m.updateWifiPasswordPrompt(msg)
|
|
}
|
|
|
|
keyMsg, ok := msg.(tea.KeyPressMsg)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
g := keys.Keys.Global
|
|
n := keys.Keys.Network
|
|
|
|
switch {
|
|
case key.Matches(keyMsg, g.Up):
|
|
if m.wifi.cursor > 0 {
|
|
m.wifi.cursor--
|
|
}
|
|
case key.Matches(keyMsg, g.Down):
|
|
if m.wifi.cursor < len(m.wifi.others)-1 {
|
|
m.wifi.cursor++
|
|
}
|
|
case key.Matches(keyMsg, n.Refresh):
|
|
m.wifi.pending = true
|
|
return tea.Batch(rescanWifiCmd(), fetchWifiCmd())
|
|
case key.Matches(keyMsg, n.Select):
|
|
return m.selectWifi()
|
|
case key.Matches(keyMsg, n.ToggleRadio):
|
|
m.wifi.pending = true
|
|
return toggleWifiRadioCmd(!m.wifi.radioOn)
|
|
case key.Matches(keyMsg, n.Disconnect):
|
|
if m.wifi.current == nil {
|
|
m.wifi.feedback = "Not connected to a network."
|
|
return nil
|
|
}
|
|
m.wifi.pending = true
|
|
return disconnectWifiCmd()
|
|
case key.Matches(keyMsg, n.Info):
|
|
if m.wifi.current == nil {
|
|
m.wifi.feedback = "Not connected to a network."
|
|
return nil
|
|
}
|
|
cur := m.wifi.current
|
|
return m.openDeviceDetail(tabWifi, m.wifi.device, cur.SSID, cur.Security, cur.Signal)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *Model) selectWifi() tea.Cmd {
|
|
if len(m.wifi.others) == 0 {
|
|
return nil
|
|
}
|
|
target := m.wifi.others[m.wifi.cursor]
|
|
if target.Secured() && !m.isKnownSSID(target.SSID) {
|
|
m.wifi.connecting = true
|
|
m.wifi.connectSSID = target.SSID
|
|
m.wifi.password.SetValue("")
|
|
m.wifi.password.Focus()
|
|
return nil
|
|
}
|
|
m.wifi.pending = true
|
|
return connectWifiCmd(target.SSID, "")
|
|
}
|
|
|
|
func (m *Model) updateWifiPasswordPrompt(msg tea.Msg) tea.Cmd {
|
|
keyMsg, ok := msg.(tea.KeyPressMsg)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
g := keys.Keys.Global
|
|
switch {
|
|
case key.Matches(keyMsg, g.Escape):
|
|
m.wifi.connecting = false
|
|
m.wifi.password.Blur()
|
|
return nil
|
|
case keyMsg.String() == "enter":
|
|
ssid := m.wifi.connectSSID
|
|
password := m.wifi.password.Value()
|
|
m.wifi.connecting = false
|
|
m.wifi.password.Blur()
|
|
m.wifi.pending = true
|
|
return connectWifiCmd(ssid, password)
|
|
default:
|
|
var cmd tea.Cmd
|
|
m.wifi.password, cmd = m.wifi.password.Update(keyMsg)
|
|
return cmd
|
|
}
|
|
}
|
|
|
|
func (m Model) renderWifi(width, height int) string {
|
|
if m.wifi.connecting {
|
|
return m.renderWifiPasswordPrompt()
|
|
}
|
|
|
|
if m.wifi.loading {
|
|
return ilovetui.S.Faint.Render("Loading Wi-Fi networks…")
|
|
}
|
|
|
|
othersHeader := lipgloss.NewStyle().Bold(true).Foreground(ilovetui.S.Subtle).Render("OTHER NETWORKS")
|
|
if m.wifi.pending {
|
|
othersHeader += mutedStyle().Render(" refreshing…")
|
|
}
|
|
|
|
fixed := lipgloss.JoinVertical(lipgloss.Left,
|
|
m.renderWifiRadioStatus(),
|
|
"",
|
|
lipgloss.NewStyle().Bold(true).Foreground(ilovetui.S.Subtle).Render("CURRENT NETWORK"),
|
|
"",
|
|
m.renderCurrentWifiCard(width),
|
|
"",
|
|
othersHeader,
|
|
"",
|
|
)
|
|
fixedH := lipgloss.Height(fixed)
|
|
|
|
if m.wifi.err != nil {
|
|
return lipgloss.JoinVertical(lipgloss.Left, fixed, errorStyle().Render(m.wifi.err.Error()))
|
|
}
|
|
if len(m.wifi.others) == 0 {
|
|
return lipgloss.JoinVertical(lipgloss.Left, fixed, ilovetui.S.Faint.Render("No other networks in range."))
|
|
}
|
|
|
|
available := height - fixedH - 1 // reserve a trailing line for the paginator/feedback
|
|
if available < 1 {
|
|
available = 1
|
|
}
|
|
p, start, end := paginate(m.wifi.cursor, len(m.wifi.others), available)
|
|
|
|
var rows []string
|
|
for i := start; i < end; i++ {
|
|
rows = append(rows, m.renderWifiRow(m.wifi.others[i], i == m.wifi.cursor))
|
|
}
|
|
list := strings.Join(rows, "\n")
|
|
|
|
footer := renderPaginatorDots(p)
|
|
if m.wifi.feedback != "" {
|
|
footer = errorStyle().Render(m.wifi.feedback)
|
|
}
|
|
|
|
return lipgloss.JoinVertical(lipgloss.Left, fixed, list, footer)
|
|
}
|
|
|
|
func (m Model) renderWifiRadioStatus() string {
|
|
label := "Wi-Fi: "
|
|
if m.wifi.radioOn {
|
|
return label + successStyle().Bold(true).Render("On")
|
|
}
|
|
return label + errorStyle().Bold(true).Render("Off")
|
|
}
|
|
|
|
func (m Model) renderCurrentWifiCard(width int) string {
|
|
if m.wifi.current == nil {
|
|
return ilovetui.S.Faint.Render("Not connected")
|
|
}
|
|
cur := m.wifi.current
|
|
check := lipgloss.NewStyle().Foreground(ilovetui.S.Success).Render(icons.I.Check)
|
|
title := lipgloss.NewStyle().Bold(true).Render(cur.SSID)
|
|
line1 := check + title
|
|
line2 := lipgloss.NewStyle().Foreground(ilovetui.S.Subtle).Render(
|
|
fmt.Sprintf("%s · %d%%", securityLabel(cur.Security), cur.Signal))
|
|
if m.wifi.ip != "" {
|
|
line2 += lipgloss.NewStyle().Foreground(ilovetui.S.Subtle).Render(" · " + m.wifi.ip)
|
|
}
|
|
return lipgloss.NewStyle().Width(width).Render(lipgloss.JoinVertical(lipgloss.Left, line1, line2))
|
|
}
|
|
|
|
func (m Model) renderWifiRow(n network.WifiNetwork, selected bool) string {
|
|
textStyle := lipgloss.NewStyle().Foreground(ilovetui.S.Text)
|
|
subtleStyle := lipgloss.NewStyle().Foreground(ilovetui.S.Subtle)
|
|
if selected {
|
|
textStyle = textStyle.Bold(true).Foreground(ilovetui.S.Primary)
|
|
}
|
|
lock := ""
|
|
if n.Secured() {
|
|
lock = " " + icons.I.Lock
|
|
}
|
|
cursor := " "
|
|
if selected {
|
|
cursor = "> "
|
|
}
|
|
return fmt.Sprintf("%s%s%s %s", cursor, textStyle.Render(n.SSID), lock, subtleStyle.Render(fmt.Sprintf("%d%%", n.Signal)))
|
|
}
|
|
|
|
func (m Model) renderWifiPasswordPrompt() string {
|
|
title := lipgloss.NewStyle().Bold(true).Render("Connect to " + m.wifi.connectSSID)
|
|
return lipgloss.JoinVertical(lipgloss.Left, title, "", m.wifi.password.View())
|
|
}
|
|
|
|
func securityLabel(security string) string {
|
|
if security == "" || security == "--" {
|
|
return "Open"
|
|
}
|
|
return security
|
|
}
|