mirror of
https://github.com/anotherhadi/settuings.git
synced 2026-10-05 16:18:24 +02:00
@@ -0,0 +1,220 @@
|
||||
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 inputState struct {
|
||||
loading bool
|
||||
err error
|
||||
devices []audio.Device
|
||||
cursor int
|
||||
pending bool
|
||||
feedback string
|
||||
|
||||
testing bool
|
||||
monitor *audio.LevelMonitor
|
||||
level float64 // last peak reading in [0, 1], only meaningful while testing
|
||||
}
|
||||
|
||||
func newInputState() inputState {
|
||||
return inputState{loading: true}
|
||||
}
|
||||
|
||||
// stopLevelMonitor tears down a running live input-level test, if any. Safe
|
||||
// to call unconditionally.
|
||||
func (s *inputState) stopLevelMonitor() {
|
||||
if s.monitor != nil {
|
||||
s.monitor.Stop()
|
||||
s.monitor = nil
|
||||
}
|
||||
s.testing = false
|
||||
s.level = 0
|
||||
}
|
||||
|
||||
func (m *Model) handleInputList(msg inputListMsg) {
|
||||
m.input.loading = false
|
||||
m.input.err = msg.err
|
||||
if msg.err != nil {
|
||||
return
|
||||
}
|
||||
m.input.devices = msg.devices
|
||||
if m.input.cursor >= len(m.input.devices) {
|
||||
m.input.cursor = max(0, len(m.input.devices)-1)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) handleInputAction(msg inputActionMsg) tea.Cmd {
|
||||
m.input.pending = false
|
||||
if msg.err != nil {
|
||||
m.input.feedback = msg.err.Error()
|
||||
return nil
|
||||
}
|
||||
return fetchInputCmd()
|
||||
}
|
||||
|
||||
// handleLevelStarted reacts to a StartLevelMonitor result. If the user
|
||||
// toggled testing off again before this arrived, the freshly-started monitor
|
||||
// is stopped immediately instead of being adopted.
|
||||
func (m *Model) handleLevelStarted(msg levelStartedMsg) tea.Cmd {
|
||||
if msg.err != nil {
|
||||
m.input.testing = false
|
||||
m.input.feedback = msg.err.Error()
|
||||
return nil
|
||||
}
|
||||
if !m.input.testing {
|
||||
msg.monitor.Stop()
|
||||
return nil
|
||||
}
|
||||
m.input.monitor = msg.monitor
|
||||
return waitLevelCmd(msg.monitor)
|
||||
}
|
||||
|
||||
// handleLevel reacts to one peak reading, re-arming the wait for the next
|
||||
// one as long as the test is still active.
|
||||
func (m *Model) handleLevel(msg levelMsg) tea.Cmd {
|
||||
if !m.input.testing || m.input.monitor == nil {
|
||||
return nil
|
||||
}
|
||||
if msg.err != nil {
|
||||
m.input.feedback = "Input test stopped: " + msg.err.Error()
|
||||
m.input.stopLevelMonitor()
|
||||
return nil
|
||||
}
|
||||
m.input.level = msg.peak
|
||||
return waitLevelCmd(m.input.monitor)
|
||||
}
|
||||
|
||||
func (m Model) selectedInput() *audio.Device {
|
||||
if m.input.cursor < 0 || m.input.cursor >= len(m.input.devices) {
|
||||
return nil
|
||||
}
|
||||
return &m.input.devices[m.input.cursor]
|
||||
}
|
||||
|
||||
func (m *Model) updateInput(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.input.cursor > 0 {
|
||||
m.input.stopLevelMonitor()
|
||||
m.input.cursor--
|
||||
}
|
||||
case key.Matches(keyMsg, g.Down):
|
||||
if m.input.cursor < len(m.input.devices)-1 {
|
||||
m.input.stopLevelMonitor()
|
||||
m.input.cursor++
|
||||
}
|
||||
case key.Matches(keyMsg, g.Left):
|
||||
return m.adjustInputVolume(-volumeStep)
|
||||
case key.Matches(keyMsg, g.Right):
|
||||
return m.adjustInputVolume(volumeStep)
|
||||
case key.Matches(keyMsg, a.Refresh):
|
||||
m.input.pending = true
|
||||
return fetchInputCmd()
|
||||
case key.Matches(keyMsg, a.Mute):
|
||||
dev := m.selectedInput()
|
||||
if dev == nil {
|
||||
return nil
|
||||
}
|
||||
m.input.feedback = ""
|
||||
m.input.pending = true
|
||||
return inputToggleMuteCmd(dev.ID)
|
||||
case key.Matches(keyMsg, a.Select):
|
||||
dev := m.selectedInput()
|
||||
if dev == nil {
|
||||
return nil
|
||||
}
|
||||
if dev.Default {
|
||||
m.input.feedback = "Already the default input."
|
||||
return nil
|
||||
}
|
||||
m.input.feedback = ""
|
||||
m.input.pending = true
|
||||
return inputSetDefaultCmd(dev.ID)
|
||||
case key.Matches(keyMsg, a.Test):
|
||||
dev := m.selectedInput()
|
||||
if dev == nil {
|
||||
return nil
|
||||
}
|
||||
if m.input.testing {
|
||||
m.input.stopLevelMonitor()
|
||||
return nil
|
||||
}
|
||||
m.input.testing = true
|
||||
m.input.feedback = ""
|
||||
return startLevelMonitorCmd(dev.ID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Model) adjustInputVolume(delta int) tea.Cmd {
|
||||
dev := m.selectedInput()
|
||||
if dev == nil {
|
||||
return nil
|
||||
}
|
||||
pct := dev.Volume + delta
|
||||
if pct < 0 {
|
||||
pct = 0
|
||||
} else if pct > 100 {
|
||||
pct = 100
|
||||
}
|
||||
dev.Volume = pct
|
||||
m.input.feedback = ""
|
||||
return inputSetVolumeCmd(dev.ID, pct)
|
||||
}
|
||||
|
||||
func (m Model) renderInput(width, height int) string {
|
||||
if m.input.loading {
|
||||
return ilovetui.S.Faint.Render("Loading input devices…")
|
||||
}
|
||||
|
||||
header := lipgloss.NewStyle().Bold(true).Foreground(ilovetui.S.Subtle).Render("INPUT DEVICES")
|
||||
if m.input.pending {
|
||||
header += mutedStyle().Render(" refreshing…")
|
||||
}
|
||||
|
||||
if m.input.err != nil {
|
||||
return lipgloss.JoinVertical(lipgloss.Left, header, "", errorStyle().Render(m.input.err.Error()))
|
||||
}
|
||||
if len(m.input.devices) == 0 {
|
||||
return lipgloss.JoinVertical(lipgloss.Left, header, "", ilovetui.S.Faint.Render("No input devices found."))
|
||||
}
|
||||
|
||||
available := (height - 3) / deviceRowHeight
|
||||
if available < 1 {
|
||||
available = 1
|
||||
}
|
||||
p, start, end := paginate(m.input.cursor, len(m.input.devices), available)
|
||||
|
||||
var rows []string
|
||||
for i := start; i < end; i++ {
|
||||
selected := i == m.input.cursor
|
||||
row := renderDeviceRow(m.input.devices[i], selected, width)
|
||||
if selected && m.input.testing {
|
||||
row += "\n " + renderLevelMeter(m.input.level, barWidth(width))
|
||||
}
|
||||
rows = append(rows, row)
|
||||
}
|
||||
list := strings.Join(rows, "\n\n")
|
||||
|
||||
footer := renderPaginatorDots(p)
|
||||
if m.input.feedback != "" {
|
||||
footer = subtleStyle().Render(m.input.feedback)
|
||||
}
|
||||
|
||||
return lipgloss.JoinVertical(lipgloss.Left, header, "", list, footer)
|
||||
}
|
||||
Reference in New Issue
Block a user