mirror of
https://github.com/anotherhadi/settuings.git
synced 2026-10-05 16:18:24 +02:00
@@ -0,0 +1,273 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"charm.land/bubbles/v2/key"
|
||||
"charm.land/bubbles/v2/viewport"
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"charm.land/lipgloss/v2"
|
||||
ilovetui "github.com/anotherhadi/ilovetui"
|
||||
"github.com/anotherhadi/settuings/internal/keys"
|
||||
"github.com/anotherhadi/settuings/internal/network"
|
||||
"github.com/anotherhadi/settuings/internal/util"
|
||||
)
|
||||
|
||||
// deviceDetailState is the "network info" screen: full addressing/hardware
|
||||
// details for whichever connected device (wifi or ethernet) the user asked
|
||||
// about, plus a way to disconnect it. It overlays whichever tab opened it.
|
||||
// Content can exceed a short terminal, so it's shown in a viewport.
|
||||
type deviceDetailState struct {
|
||||
open bool
|
||||
kind tab // tabWifi or tabEthernet: decides which disconnect action to run
|
||||
device string
|
||||
title string
|
||||
security string // wifi only
|
||||
signal int // wifi only, <=0 means n/a
|
||||
connectionName string // wifi only: profile name, for secret lookup
|
||||
|
||||
loading bool
|
||||
err error
|
||||
data network.DeviceDetail
|
||||
|
||||
secretLoading bool
|
||||
secretValue string
|
||||
secretRevealed bool
|
||||
secretErr error
|
||||
|
||||
pending bool
|
||||
feedback string
|
||||
|
||||
viewport viewport.Model
|
||||
}
|
||||
|
||||
func (m *Model) openDeviceDetail(kind tab, device, title, security string, signal int) tea.Cmd {
|
||||
w, h := m.contentBudget()
|
||||
vp := viewport.New()
|
||||
vp.SetWidth(w)
|
||||
vp.SetHeight(h)
|
||||
|
||||
m.deviceDetail = deviceDetailState{
|
||||
open: true,
|
||||
kind: kind,
|
||||
device: device,
|
||||
title: title,
|
||||
security: security,
|
||||
signal: signal,
|
||||
connectionName: m.wifi.connectionName,
|
||||
loading: true,
|
||||
secretLoading: kind == tabWifi,
|
||||
viewport: vp,
|
||||
}
|
||||
m.refreshDeviceDetailContent()
|
||||
|
||||
cmds := []tea.Cmd{fetchDeviceDetailCmd(device)}
|
||||
if kind == tabWifi && m.wifi.connectionName != "" {
|
||||
cmds = append(cmds, fetchSecretCmd(m.wifi.connectionName))
|
||||
}
|
||||
return tea.Batch(cmds...)
|
||||
}
|
||||
|
||||
// contentBudget returns the exact space available inside the bordered tab
|
||||
// box, below the tab bar and above the status bar. View() renders the box
|
||||
// at this same size, so anything sized against contentBudget (e.g. a detail
|
||||
// viewport) fills the box exactly instead of leaving it short. Since the
|
||||
// status bar's height depends on live state (e.g. whether help is
|
||||
// expanded), callers must re-size whenever that state changes — see
|
||||
// resizeDeviceDetail/resizeKnownDetail.
|
||||
func (m Model) contentBudget() (w, h int) {
|
||||
frameW := windowStyle().GetHorizontalFrameSize()
|
||||
frameH := windowStyle().GetVerticalFrameSize()
|
||||
w = m.width - frameW
|
||||
|
||||
statusH := strings.Count(m.renderStatusBar(), "\n") + 1
|
||||
tabBarH := lipgloss.Height(m.renderTabBar(w))
|
||||
|
||||
h = m.height - tabBarH - frameH - statusH
|
||||
if h < 1 {
|
||||
h = 1
|
||||
}
|
||||
return w, h
|
||||
}
|
||||
|
||||
// resizeDetails re-sizes every open detail viewport to match the current
|
||||
// content budget. Call it whenever that budget could have changed: on
|
||||
// window resize, or when the status bar's height changes (e.g. toggling
|
||||
// help).
|
||||
func (m *Model) resizeDetails() {
|
||||
m.resizeDeviceDetail()
|
||||
m.resizeKnownDetail()
|
||||
}
|
||||
|
||||
func (m *Model) resizeDeviceDetail() {
|
||||
if !m.deviceDetail.open {
|
||||
return
|
||||
}
|
||||
w, h := m.contentBudget()
|
||||
m.deviceDetail.viewport.SetWidth(w)
|
||||
m.deviceDetail.viewport.SetHeight(h)
|
||||
m.refreshDeviceDetailContent()
|
||||
}
|
||||
|
||||
func (m *Model) handleDeviceDetail(msg deviceDetailMsg) {
|
||||
if !m.deviceDetail.open {
|
||||
return
|
||||
}
|
||||
m.deviceDetail.loading = false
|
||||
m.deviceDetail.data = msg.detail
|
||||
m.deviceDetail.err = msg.err
|
||||
m.refreshDeviceDetailContent()
|
||||
}
|
||||
|
||||
// handleDeviceDetailSecret feeds a knownSecretMsg to the info screen if it's
|
||||
// the one that asked for it. The known-networks tab checks the same message
|
||||
// for its own detail view; both can be true at once only in theory (they're
|
||||
// on different tabs), so this is safe to call unconditionally.
|
||||
func (m *Model) handleDeviceDetailSecret(msg knownSecretMsg) {
|
||||
if !m.deviceDetail.open || m.deviceDetail.kind != tabWifi || m.deviceDetail.connectionName != msg.name {
|
||||
return
|
||||
}
|
||||
m.deviceDetail.secretLoading = false
|
||||
m.deviceDetail.secretValue = msg.secret
|
||||
m.deviceDetail.secretErr = msg.err
|
||||
m.refreshDeviceDetailContent()
|
||||
}
|
||||
|
||||
// closeDeviceDetailIfPending closes the info screen once the disconnect it
|
||||
// triggered has completed, but only if it's still the one waiting on kind.
|
||||
func (m *Model) closeDeviceDetailIfPending(kind tab) {
|
||||
if m.deviceDetail.open && m.deviceDetail.kind == kind && m.deviceDetail.pending {
|
||||
m.deviceDetail = deviceDetailState{}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) updateDeviceDetail(msg tea.Msg) tea.Cmd {
|
||||
if wheel, ok := msg.(tea.MouseWheelMsg); ok {
|
||||
util.HandleMouseWheel(wheel, &m.deviceDetail.viewport)
|
||||
return nil
|
||||
}
|
||||
keyMsg, ok := msg.(tea.KeyPressMsg)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
g := keys.Keys.Global
|
||||
n := keys.Keys.Network
|
||||
|
||||
switch {
|
||||
case key.Matches(keyMsg, g.Escape), key.Matches(keyMsg, g.CycleFocus):
|
||||
m.deviceDetail = deviceDetailState{}
|
||||
case key.Matches(keyMsg, g.Help):
|
||||
m.help.ShowAll = !m.help.ShowAll
|
||||
m.resizeDetails()
|
||||
case key.Matches(keyMsg, n.Disconnect):
|
||||
m.deviceDetail.pending = true
|
||||
m.refreshDeviceDetailContent()
|
||||
switch m.deviceDetail.kind {
|
||||
case tabWifi:
|
||||
return disconnectWifiCmd()
|
||||
case tabEthernet:
|
||||
return ethernetDownCmd(m.deviceDetail.device)
|
||||
}
|
||||
case key.Matches(keyMsg, n.RevealSecret):
|
||||
if m.deviceDetail.kind == tabWifi {
|
||||
m.deviceDetail.secretRevealed = !m.deviceDetail.secretRevealed
|
||||
m.refreshDeviceDetailContent()
|
||||
}
|
||||
case key.Matches(keyMsg, g.Up):
|
||||
util.ScrollLines(&m.deviceDetail.viewport, -1)
|
||||
case key.Matches(keyMsg, g.Down):
|
||||
util.ScrollLines(&m.deviceDetail.viewport, 1)
|
||||
case key.Matches(keyMsg, g.ScrollUp):
|
||||
util.ScrollViewport(&m.deviceDetail.viewport, -1)
|
||||
case key.Matches(keyMsg, g.ScrollDown):
|
||||
util.ScrollViewport(&m.deviceDetail.viewport, 1)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m Model) renderDeviceDetail() string {
|
||||
return m.deviceDetail.viewport.View()
|
||||
}
|
||||
|
||||
func (m *Model) refreshDeviceDetailContent() {
|
||||
m.deviceDetail.viewport.SetContent(m.deviceDetailContent())
|
||||
util.RefreshScrollbar(&m.deviceDetail.viewport)
|
||||
}
|
||||
|
||||
func (m Model) deviceDetailContent() string {
|
||||
d := m.deviceDetail
|
||||
if d.loading {
|
||||
return ilovetui.S.Faint.Render("Loading network info…")
|
||||
}
|
||||
|
||||
title := lipgloss.NewStyle().Bold(true).Render(d.title)
|
||||
if d.pending {
|
||||
title += mutedStyle().Render(" disconnecting…")
|
||||
}
|
||||
rows := []string{title}
|
||||
if d.kind == tabWifi {
|
||||
rows = append(rows, subtleStyle().Render("Security: ")+securityLabel(d.security))
|
||||
if d.signal > 0 {
|
||||
rows = append(rows, subtleStyle().Render("Signal: ")+fmt.Sprintf("%d%%", d.signal))
|
||||
}
|
||||
rows = append(rows, subtleStyle().Render("Password: ")+m.renderDeviceSecret())
|
||||
}
|
||||
rows = append(rows, subtleStyle().Render("Interface: ")+d.device)
|
||||
|
||||
if d.err != nil {
|
||||
rows = append(rows, "", errorStyle().Render(d.err.Error()))
|
||||
return lipgloss.JoinVertical(lipgloss.Left, rows...)
|
||||
}
|
||||
|
||||
rows = append(rows,
|
||||
subtleStyle().Render("IPv4 Address: ")+valueOrDash(d.data.IPv4),
|
||||
subtleStyle().Render("Gateway: ")+valueOrDash(d.data.Gateway4),
|
||||
subtleStyle().Render("DNS: ")+valueOrDash(strings.Join(d.data.DNS4, ", ")),
|
||||
subtleStyle().Render("MAC Address: ")+valueOrDash(d.data.HWAddr),
|
||||
subtleStyle().Render("MTU: ")+valueOrDash(d.data.MTU),
|
||||
)
|
||||
|
||||
if len(d.data.IPv6) > 0 {
|
||||
rows = append(rows, "", subtleStyle().Render("IPv6 Address: ")+d.data.IPv6[0])
|
||||
for _, addr := range d.data.IPv6[1:] {
|
||||
rows = append(rows, " "+addr)
|
||||
}
|
||||
}
|
||||
if d.data.Gateway6 != "" {
|
||||
rows = append(rows, subtleStyle().Render("IPv6 Gateway: ")+d.data.Gateway6)
|
||||
}
|
||||
if len(d.data.DNS6) > 0 {
|
||||
rows = append(rows, subtleStyle().Render("IPv6 DNS: ")+strings.Join(d.data.DNS6, ", "))
|
||||
}
|
||||
|
||||
if d.feedback != "" {
|
||||
rows = append(rows, "", errorStyle().Render(d.feedback))
|
||||
}
|
||||
|
||||
return lipgloss.JoinVertical(lipgloss.Left, rows...)
|
||||
}
|
||||
|
||||
func (m Model) renderDeviceSecret() string {
|
||||
d := m.deviceDetail
|
||||
if d.secretLoading {
|
||||
return mutedStyle().Render("loading…")
|
||||
}
|
||||
if d.secretErr != nil {
|
||||
return errorStyle().Render(d.secretErr.Error())
|
||||
}
|
||||
if d.secretValue == "" {
|
||||
return subtleStyle().Render("(none)")
|
||||
}
|
||||
if !d.secretRevealed {
|
||||
return subtleStyle().Render(strings.Repeat("•", len(d.secretValue)))
|
||||
}
|
||||
return d.secretValue
|
||||
}
|
||||
|
||||
func valueOrDash(s string) string {
|
||||
if s == "" {
|
||||
return "—"
|
||||
}
|
||||
return s
|
||||
}
|
||||
Reference in New Issue
Block a user