mirror of
https://github.com/anotherhadi/settuings.git
synced 2026-10-05 16:18:24 +02:00
@@ -0,0 +1,286 @@
|
||||
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/icons"
|
||||
"github.com/anotherhadi/settuings/internal/keys"
|
||||
"github.com/anotherhadi/settuings/internal/network"
|
||||
"github.com/anotherhadi/settuings/internal/util"
|
||||
)
|
||||
|
||||
type knownState struct {
|
||||
loading bool
|
||||
err error
|
||||
networks []network.KnownNetwork
|
||||
cursor int
|
||||
pending bool
|
||||
feedback string
|
||||
|
||||
detail bool
|
||||
secretLoading bool
|
||||
secretRevealed bool
|
||||
secretValue string
|
||||
secretErr error
|
||||
|
||||
confirmForget bool
|
||||
|
||||
// Content can exceed a short terminal, so the detail screen is shown in
|
||||
// a viewport.
|
||||
viewport viewport.Model
|
||||
}
|
||||
|
||||
func newKnownState() knownState {
|
||||
return knownState{loading: true}
|
||||
}
|
||||
|
||||
func (m *Model) handleKnownList(msg knownListMsg) {
|
||||
m.known.loading = false
|
||||
m.known.err = msg.err
|
||||
if msg.err != nil {
|
||||
return
|
||||
}
|
||||
m.known.networks = msg.networks
|
||||
if m.known.cursor >= len(m.known.networks) {
|
||||
m.known.cursor = max(0, len(m.known.networks)-1)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) handleKnownSecret(msg knownSecretMsg) {
|
||||
if m.known.selected() == nil || m.known.selected().Name != msg.name {
|
||||
return // stale response for a network we've since navigated away from
|
||||
}
|
||||
m.known.secretLoading = false
|
||||
m.known.secretValue = msg.secret
|
||||
m.known.secretErr = msg.err
|
||||
m.refreshKnownDetailContent()
|
||||
}
|
||||
|
||||
func (m *Model) handleKnownAction(msg knownActionMsg) tea.Cmd {
|
||||
m.known.pending = false
|
||||
if msg.err != nil {
|
||||
m.known.feedback = msg.err.Error()
|
||||
m.refreshKnownDetailContent()
|
||||
return nil
|
||||
}
|
||||
m.known.feedback = ""
|
||||
m.known.detail = false
|
||||
return fetchKnownCmd()
|
||||
}
|
||||
|
||||
func (s knownState) selected() *network.KnownNetwork {
|
||||
if s.cursor < 0 || s.cursor >= len(s.networks) {
|
||||
return nil
|
||||
}
|
||||
return &s.networks[s.cursor]
|
||||
}
|
||||
|
||||
func (m *Model) updateKnown(msg tea.Msg) tea.Cmd {
|
||||
keyMsg, ok := msg.(tea.KeyPressMsg)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
if m.known.detail {
|
||||
return m.updateKnownDetail(keyMsg)
|
||||
}
|
||||
|
||||
g := keys.Keys.Global
|
||||
n := keys.Keys.Network
|
||||
switch {
|
||||
case key.Matches(keyMsg, g.Up):
|
||||
if m.known.cursor > 0 {
|
||||
m.known.cursor--
|
||||
}
|
||||
case key.Matches(keyMsg, g.Down):
|
||||
if m.known.cursor < len(m.known.networks)-1 {
|
||||
m.known.cursor++
|
||||
}
|
||||
case key.Matches(keyMsg, n.Refresh):
|
||||
return fetchKnownCmd()
|
||||
case key.Matches(keyMsg, n.Select):
|
||||
if sel := m.known.selected(); sel != nil {
|
||||
m.known.detail = true
|
||||
m.known.secretRevealed = false
|
||||
m.known.secretLoading = true
|
||||
m.known.secretValue = ""
|
||||
m.known.secretErr = nil
|
||||
m.openKnownDetail()
|
||||
return fetchSecretCmd(sel.Name)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Model) updateKnownDetail(keyMsg tea.KeyPressMsg) tea.Cmd {
|
||||
g := keys.Keys.Global
|
||||
n := keys.Keys.Network
|
||||
|
||||
if m.known.confirmForget {
|
||||
m.known.confirmForget = false
|
||||
defer m.refreshKnownDetailContent()
|
||||
if keyMsg.String() == "y" || keyMsg.String() == "enter" {
|
||||
if sel := m.known.selected(); sel != nil {
|
||||
m.known.pending = true
|
||||
return forgetCmd(sel.Name)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
switch {
|
||||
case key.Matches(keyMsg, g.Escape):
|
||||
m.known.detail = false
|
||||
case key.Matches(keyMsg, n.RevealSecret):
|
||||
m.known.secretRevealed = !m.known.secretRevealed
|
||||
m.refreshKnownDetailContent()
|
||||
case key.Matches(keyMsg, n.ToggleAuto):
|
||||
if sel := m.known.selected(); sel != nil {
|
||||
m.known.pending = true
|
||||
return toggleAutoConnectCmd(sel.Name, !sel.AutoConnect)
|
||||
}
|
||||
case key.Matches(keyMsg, n.Forget):
|
||||
m.known.confirmForget = true
|
||||
m.refreshKnownDetailContent()
|
||||
case key.Matches(keyMsg, g.Up):
|
||||
util.ScrollLines(&m.known.viewport, -1)
|
||||
case key.Matches(keyMsg, g.Down):
|
||||
util.ScrollLines(&m.known.viewport, 1)
|
||||
case key.Matches(keyMsg, g.ScrollUp):
|
||||
util.ScrollViewport(&m.known.viewport, -1)
|
||||
case key.Matches(keyMsg, g.ScrollDown):
|
||||
util.ScrollViewport(&m.known.viewport, 1)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// openKnownDetail sizes and populates the detail viewport for the currently
|
||||
// selected network. Call it whenever the detail screen is opened.
|
||||
func (m *Model) openKnownDetail() {
|
||||
w, h := m.contentBudget()
|
||||
vp := viewport.New()
|
||||
vp.SetWidth(w)
|
||||
vp.SetHeight(h)
|
||||
m.known.viewport = vp
|
||||
m.refreshKnownDetailContent()
|
||||
}
|
||||
|
||||
// resizeKnownDetail keeps the detail viewport sized to the available space
|
||||
// whenever the terminal is resized.
|
||||
func (m *Model) resizeKnownDetail() {
|
||||
if !m.known.detail {
|
||||
return
|
||||
}
|
||||
w, h := m.contentBudget()
|
||||
m.known.viewport.SetWidth(w)
|
||||
m.known.viewport.SetHeight(h)
|
||||
m.refreshKnownDetailContent()
|
||||
}
|
||||
|
||||
func (m *Model) refreshKnownDetailContent() {
|
||||
m.known.viewport.SetContent(m.knownDetailContent())
|
||||
util.RefreshScrollbar(&m.known.viewport)
|
||||
}
|
||||
|
||||
func (m Model) renderKnown(height int) string {
|
||||
if m.known.detail {
|
||||
return m.renderKnownDetail()
|
||||
}
|
||||
if m.known.loading {
|
||||
return ilovetui.S.Faint.Render("Loading known networks…")
|
||||
}
|
||||
|
||||
header := lipgloss.NewStyle().Bold(true).Foreground(ilovetui.S.Subtle).Render("KNOWN NETWORKS")
|
||||
|
||||
if m.known.err != nil {
|
||||
return lipgloss.JoinVertical(lipgloss.Left, header, "", errorStyle().Render(m.known.err.Error()))
|
||||
}
|
||||
if len(m.known.networks) == 0 {
|
||||
return lipgloss.JoinVertical(lipgloss.Left, header, "", ilovetui.S.Faint.Render("No saved Wi-Fi networks."))
|
||||
}
|
||||
|
||||
available := height - 3 // header + blank line + a trailing line for the paginator
|
||||
if available < 1 {
|
||||
available = 1
|
||||
}
|
||||
p, start, end := paginate(m.known.cursor, len(m.known.networks), available)
|
||||
|
||||
var rows []string
|
||||
for i := start; i < end; i++ {
|
||||
rows = append(rows, m.renderKnownRow(m.known.networks[i], i == m.known.cursor))
|
||||
}
|
||||
list := strings.Join(rows, "\n")
|
||||
|
||||
return lipgloss.JoinVertical(lipgloss.Left, header, "", list, renderPaginatorDots(p))
|
||||
}
|
||||
|
||||
func (m Model) renderKnownRow(k network.KnownNetwork, selected bool) string {
|
||||
textStyle := lipgloss.NewStyle().Foreground(ilovetui.S.Text)
|
||||
if selected {
|
||||
textStyle = textStyle.Bold(true).Foreground(ilovetui.S.Primary)
|
||||
}
|
||||
cursor := " "
|
||||
if selected {
|
||||
cursor = "> "
|
||||
}
|
||||
check := " "
|
||||
if k.Active {
|
||||
check = successStyle().Render(icons.I.Check)
|
||||
}
|
||||
auto := "manual"
|
||||
if k.AutoConnect {
|
||||
auto = "auto-join"
|
||||
}
|
||||
return fmt.Sprintf("%s%s%s %s", cursor, check, textStyle.Render(k.Name), subtleStyle().Render(auto))
|
||||
}
|
||||
|
||||
func (m Model) renderKnownDetail() string {
|
||||
return m.known.viewport.View()
|
||||
}
|
||||
|
||||
func (m Model) knownDetailContent() string {
|
||||
sel := m.known.selected()
|
||||
if sel == nil {
|
||||
return ilovetui.S.Faint.Render("No network selected.")
|
||||
}
|
||||
|
||||
rows := []string{
|
||||
lipgloss.NewStyle().Bold(true).Render(sel.Name),
|
||||
"",
|
||||
subtleStyle().Render("Security: ") + securityLabel(sel.Security),
|
||||
subtleStyle().Render("Auto-join: ") + fmt.Sprintf("%v", sel.AutoConnect),
|
||||
subtleStyle().Render("Active: ") + fmt.Sprintf("%v", sel.Active),
|
||||
subtleStyle().Render("Password: ") + m.renderKnownSecret(),
|
||||
}
|
||||
|
||||
if m.known.feedback != "" {
|
||||
rows = append(rows, "", errorStyle().Render(m.known.feedback))
|
||||
}
|
||||
|
||||
if m.known.confirmForget {
|
||||
rows = append(rows, "", errorStyle().Render(fmt.Sprintf("Forget %q? press y to confirm, any other key to cancel", sel.Name)))
|
||||
}
|
||||
|
||||
return lipgloss.JoinVertical(lipgloss.Left, rows...)
|
||||
}
|
||||
|
||||
func (m Model) renderKnownSecret() string {
|
||||
if m.known.secretLoading {
|
||||
return mutedStyle().Render("loading…")
|
||||
}
|
||||
if m.known.secretErr != nil {
|
||||
return errorStyle().Render(m.known.secretErr.Error())
|
||||
}
|
||||
if m.known.secretValue == "" {
|
||||
return subtleStyle().Render("(none)")
|
||||
}
|
||||
if !m.known.secretRevealed {
|
||||
return subtleStyle().Render(strings.Repeat("•", len(m.known.secretValue)))
|
||||
}
|
||||
return m.known.secretValue
|
||||
}
|
||||
Reference in New Issue
Block a user