mirror of
https://github.com/anotherhadi/settuings.git
synced 2026-10-05 08:18:23 +02:00
207 lines
5.4 KiB
Go
207 lines
5.4 KiB
Go
package bluetooth
|
|
|
|
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/bluetooth"
|
|
"github.com/anotherhadi/settuings/internal/keys"
|
|
"github.com/anotherhadi/settuings/internal/util"
|
|
)
|
|
|
|
// deviceDetailState is the "device info" screen for whichever device (paired
|
|
// or not) the user asked about. It overlays the main list. Content can
|
|
// exceed a short terminal, so it's shown in a viewport.
|
|
type deviceDetailState struct {
|
|
open bool
|
|
address string
|
|
title string
|
|
|
|
loading bool
|
|
err error
|
|
data bluetooth.DeviceDetail
|
|
|
|
pending bool
|
|
feedback string
|
|
|
|
viewport viewport.Model
|
|
}
|
|
|
|
func (m *Model) openDetail(address, title string) tea.Cmd {
|
|
w, h := m.contentBudget()
|
|
vp := viewport.New()
|
|
vp.SetWidth(w)
|
|
vp.SetHeight(h)
|
|
|
|
m.detail = deviceDetailState{
|
|
open: true,
|
|
address: address,
|
|
title: title,
|
|
loading: true,
|
|
viewport: vp,
|
|
}
|
|
m.refreshDetailContent()
|
|
return fetchDetailCmd(address)
|
|
}
|
|
|
|
// contentBudget returns the exact space available inside the bordered page
|
|
// box, below the tab bar and above the status bar. View() renders the box
|
|
// at this same size, so the detail viewport fills it exactly instead of
|
|
// leaving it short. Since the status bar's height depends on live state
|
|
// (e.g. whether help is expanded), callers must call resizeDetail whenever
|
|
// that state changes.
|
|
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(renderTabBar(w))
|
|
|
|
h = m.height - tabBarH - frameH - statusH
|
|
if h < 1 {
|
|
h = 1
|
|
}
|
|
return w, h
|
|
}
|
|
|
|
func (m *Model) resizeDetail() {
|
|
if !m.detail.open {
|
|
return
|
|
}
|
|
w, h := m.contentBudget()
|
|
m.detail.viewport.SetWidth(w)
|
|
m.detail.viewport.SetHeight(h)
|
|
m.refreshDetailContent()
|
|
}
|
|
|
|
func (m *Model) handleDetail(msg detailMsg) {
|
|
if !m.detail.open {
|
|
return
|
|
}
|
|
m.detail.loading = false
|
|
m.detail.data = msg.detail
|
|
m.detail.err = msg.err
|
|
m.refreshDetailContent()
|
|
}
|
|
|
|
// closeDetailIfPending closes the info screen once the connect/disconnect
|
|
// it triggered has completed, but only if it's still the one waiting.
|
|
func (m *Model) closeDetailIfPending() {
|
|
if m.detail.open && m.detail.pending {
|
|
m.detail = deviceDetailState{}
|
|
}
|
|
}
|
|
|
|
func (m *Model) updateDetail(msg tea.Msg) tea.Cmd {
|
|
if wheel, ok := msg.(tea.MouseWheelMsg); ok {
|
|
util.HandleMouseWheel(wheel, &m.detail.viewport)
|
|
return nil
|
|
}
|
|
keyMsg, ok := msg.(tea.KeyPressMsg)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
g := keys.Keys.Global
|
|
b := keys.Keys.Bluetooth
|
|
|
|
switch {
|
|
case key.Matches(keyMsg, g.Escape), key.Matches(keyMsg, g.CycleFocus):
|
|
m.detail = deviceDetailState{}
|
|
case key.Matches(keyMsg, g.Help):
|
|
m.help.ShowAll = !m.help.ShowAll
|
|
m.resizeDetail()
|
|
case key.Matches(keyMsg, b.Select):
|
|
m.detail.pending = true
|
|
m.refreshDetailContent()
|
|
switch {
|
|
case m.detail.data.Connected:
|
|
return disconnectCmd(m.detail.address)
|
|
case m.detail.data.Paired:
|
|
return connectCmd(m.detail.address)
|
|
default:
|
|
return pairCmd(m.detail.address)
|
|
}
|
|
case key.Matches(keyMsg, b.ToggleTrust):
|
|
m.detail.pending = true
|
|
m.refreshDetailContent()
|
|
return trustCmd(m.detail.address, !m.detail.data.Trusted)
|
|
case key.Matches(keyMsg, g.Up):
|
|
util.ScrollLines(&m.detail.viewport, -1)
|
|
case key.Matches(keyMsg, g.Down):
|
|
util.ScrollLines(&m.detail.viewport, 1)
|
|
case key.Matches(keyMsg, g.ScrollUp):
|
|
util.ScrollViewport(&m.detail.viewport, -1)
|
|
case key.Matches(keyMsg, g.ScrollDown):
|
|
util.ScrollViewport(&m.detail.viewport, 1)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m Model) renderDetail() string {
|
|
return m.detail.viewport.View()
|
|
}
|
|
|
|
func (m *Model) refreshDetailContent() {
|
|
m.detail.viewport.SetContent(m.detailContent())
|
|
util.RefreshScrollbar(&m.detail.viewport)
|
|
}
|
|
|
|
func (m Model) detailContent() string {
|
|
d := m.detail
|
|
if d.loading {
|
|
return ilovetui.S.Faint.Render("Loading device info…")
|
|
}
|
|
|
|
title := lipgloss.NewStyle().Bold(true).Render(d.title)
|
|
if d.pending {
|
|
title += mutedStyle().Render(" working…")
|
|
}
|
|
rows := []string{title, subtleStyle().Render("Address: ") + d.address}
|
|
|
|
if d.err != nil {
|
|
rows = append(rows, "", errorStyle().Render(d.err.Error()))
|
|
return lipgloss.JoinVertical(lipgloss.Left, rows...)
|
|
}
|
|
|
|
rows = append(rows,
|
|
subtleStyle().Render("Paired: ")+yesNo(d.data.Paired),
|
|
subtleStyle().Render("Trusted: ")+yesNo(d.data.Trusted),
|
|
subtleStyle().Render("Connected: ")+yesNo(d.data.Connected),
|
|
subtleStyle().Render("Blocked: ")+yesNo(d.data.Blocked),
|
|
)
|
|
if d.data.Icon != "" {
|
|
rows = append(rows, subtleStyle().Render("Type: ")+d.data.Icon)
|
|
}
|
|
if d.data.Battery >= 0 {
|
|
rows = append(rows, subtleStyle().Render("Battery: ")+fmt.Sprintf("%d%%", d.data.Battery))
|
|
}
|
|
if d.data.Modalias != "" {
|
|
rows = append(rows, subtleStyle().Render("Modalias: ")+d.data.Modalias)
|
|
}
|
|
if len(d.data.UUIDs) > 0 {
|
|
rows = append(rows, "", subtleStyle().Render("Services:"))
|
|
for _, u := range d.data.UUIDs {
|
|
rows = append(rows, " "+u)
|
|
}
|
|
}
|
|
|
|
if d.feedback != "" {
|
|
rows = append(rows, "", errorStyle().Render(d.feedback))
|
|
}
|
|
|
|
return lipgloss.JoinVertical(lipgloss.Left, rows...)
|
|
}
|
|
|
|
func yesNo(b bool) string {
|
|
if b {
|
|
return successStyle().Render("yes")
|
|
}
|
|
return subtleStyle().Render("no")
|
|
}
|