mirror of
https://github.com/anotherhadi/settuings.git
synced 2026-10-05 16:18:24 +02:00
@@ -0,0 +1,173 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user