Files
settuings/internal/ui/audio/apps.go
T
Hadi 59b31e5ca3 init
Signed-off-by: Hadi <[email protected]>
2026-08-13 00:58:37 +02:00

174 lines
4.0 KiB
Go

package audio
import (
"strings"
"charm.land/bubbles/v2/key"
tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
ilovetui "github.com/anotherhadi/ilovetui"
"github.com/anotherhadi/settuings/internal/audio"
"github.com/anotherhadi/settuings/internal/keys"
)
type appsState struct {
loading bool
err error
streams []audio.Stream
cursor int
pending bool
feedback string
}
func newAppsState() appsState {
return appsState{loading: true}
}
func (m *Model) handleAppsList(msg appsListMsg) {
m.apps.loading = false
m.apps.err = msg.err
if msg.err != nil {
return
}
m.apps.streams = msg.streams
if m.apps.cursor >= len(m.apps.streams) {
m.apps.cursor = max(0, len(m.apps.streams)-1)
}
}
func (m *Model) handleAppsAction(msg appsActionMsg) tea.Cmd {
m.apps.pending = false
if msg.err != nil {
m.apps.feedback = msg.err.Error()
return nil
}
return fetchAppsCmd()
}
func (m Model) selectedStream() *audio.Stream {
if m.apps.cursor < 0 || m.apps.cursor >= len(m.apps.streams) {
return nil
}
return &m.apps.streams[m.apps.cursor]
}
func (m *Model) updateApps(msg tea.Msg) tea.Cmd {
keyMsg, ok := msg.(tea.KeyPressMsg)
if !ok {
return nil
}
g := keys.Keys.Global
a := keys.Keys.Audio
switch {
case key.Matches(keyMsg, g.Up):
if m.apps.cursor > 0 {
m.apps.cursor--
}
case key.Matches(keyMsg, g.Down):
if m.apps.cursor < len(m.apps.streams)-1 {
m.apps.cursor++
}
case key.Matches(keyMsg, g.Left):
return m.adjustStreamVolume(-volumeStep)
case key.Matches(keyMsg, g.Right):
return m.adjustStreamVolume(volumeStep)
case key.Matches(keyMsg, a.Refresh):
m.apps.pending = true
return fetchAppsCmd()
case key.Matches(keyMsg, a.Mute):
s := m.selectedStream()
if s == nil {
return nil
}
m.apps.feedback = ""
m.apps.pending = true
return appsToggleMuteCmd(s.ID)
}
return nil
}
func (m *Model) adjustStreamVolume(delta int) tea.Cmd {
s := m.selectedStream()
if s == nil {
return nil
}
pct := s.Volume + delta
if pct < 0 {
pct = 0
} else if pct > 100 {
pct = 100
}
s.Volume = pct
m.apps.feedback = ""
return appsSetVolumeCmd(s.ID, pct)
}
func sectionTitle(k audio.StreamKind) string {
if k == audio.StreamInput {
return "INPUT STREAMS (recording)"
}
return "OUTPUT STREAMS (playback)"
}
// windowAroundCursor picks a [start, end) slice of size items (or fewer, if
// total is smaller) that keeps cursor in view.
func windowAroundCursor(cursor, total, size int) (start, end int) {
if size >= total {
return 0, total
}
start = cursor - size/2
if start < 0 {
start = 0
}
if start+size > total {
start = total - size
}
return start, start + size
}
func (m Model) renderApps(width, height int) string {
if m.apps.loading {
return ilovetui.S.Faint.Render("Loading application streams…")
}
header := lipgloss.NewStyle().Bold(true).Foreground(ilovetui.S.Subtle).Render("APPLICATIONS")
if m.apps.pending {
header += mutedStyle().Render(" refreshing…")
}
if m.apps.err != nil {
return lipgloss.JoinVertical(lipgloss.Left, header, "", errorStyle().Render(m.apps.err.Error()))
}
if len(m.apps.streams) == 0 {
return lipgloss.JoinVertical(lipgloss.Left, header, "", ilovetui.S.Faint.Render("No application is currently playing or recording audio."))
}
maxItems := (height - 3) / streamRowHeight
if maxItems < 1 {
maxItems = 1
}
start, end := windowAroundCursor(m.apps.cursor, len(m.apps.streams), maxItems)
sectionStyle := lipgloss.NewStyle().Bold(true).Foreground(ilovetui.S.Subtle)
var b strings.Builder
lastKind := audio.StreamKind(-1)
for i := start; i < end; i++ {
s := m.apps.streams[i]
if s.Kind != lastKind {
b.WriteString(sectionStyle.Render(sectionTitle(s.Kind)) + "\n\n")
lastKind = s.Kind
}
b.WriteString(renderStreamRow(s, i == m.apps.cursor, width))
b.WriteString("\n\n")
}
list := strings.TrimRight(b.String(), "\n")
footer := ""
if m.apps.feedback != "" {
footer = subtleStyle().Render(m.apps.feedback)
}
return lipgloss.JoinVertical(lipgloss.Left, header, "", list, footer)
}