291 lines
7.4 KiB
Go
291 lines
7.4 KiB
Go
package tui
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/charmbracelet/lipgloss"
|
|
)
|
|
|
|
// Styles for the TUI
|
|
var (
|
|
// Colors
|
|
primaryColor = lipgloss.Color("39") // Light blue
|
|
secondaryColor = lipgloss.Color("245") // Gray
|
|
accentColor = lipgloss.Color("205") // Pink
|
|
successColor = lipgloss.Color("46") // Green
|
|
warningColor = lipgloss.Color("226") // Yellow
|
|
errorColor = lipgloss.Color("196") // Red
|
|
|
|
// Panel styles
|
|
panelStyle = lipgloss.NewStyle().
|
|
Border(lipgloss.RoundedBorder()).
|
|
BorderForeground(secondaryColor).
|
|
Padding(0, 1)
|
|
|
|
activePanelStyle = lipgloss.NewStyle().
|
|
Border(lipgloss.RoundedBorder()).
|
|
BorderForeground(primaryColor).
|
|
Padding(0, 1)
|
|
|
|
// Title styles
|
|
titleStyle = lipgloss.NewStyle().
|
|
Bold(true).
|
|
Foreground(primaryColor).
|
|
MarginBottom(1)
|
|
|
|
// Button styles
|
|
buttonStyle = lipgloss.NewStyle().
|
|
Padding(0, 2).
|
|
Background(secondaryColor).
|
|
Foreground(lipgloss.Color("0"))
|
|
|
|
activeButtonStyle = lipgloss.NewStyle().
|
|
Padding(0, 2).
|
|
Background(primaryColor).
|
|
Foreground(lipgloss.Color("0")).
|
|
Bold(true)
|
|
|
|
// Slider styles
|
|
sliderLabelStyle = lipgloss.NewStyle().
|
|
Width(14)
|
|
|
|
sliderValueStyle = lipgloss.NewStyle().
|
|
Width(6).
|
|
Align(lipgloss.Right)
|
|
|
|
// Log level styles
|
|
logLevelStyles = map[string]lipgloss.Style{
|
|
"INFO": lipgloss.NewStyle().Foreground(successColor),
|
|
"CONN": lipgloss.NewStyle().Foreground(primaryColor),
|
|
"TX": lipgloss.NewStyle().Foreground(accentColor),
|
|
"RX": lipgloss.NewStyle().Foreground(warningColor),
|
|
"ERR": lipgloss.NewStyle().Foreground(errorColor),
|
|
}
|
|
|
|
// Status styles
|
|
statusOnStyle = lipgloss.NewStyle().
|
|
Foreground(successColor).
|
|
Bold(true)
|
|
|
|
statusOffStyle = lipgloss.NewStyle().
|
|
Foreground(secondaryColor)
|
|
|
|
// Help style
|
|
helpStyle = lipgloss.NewStyle().
|
|
Foreground(secondaryColor).
|
|
Italic(true)
|
|
)
|
|
|
|
// View renders the TUI
|
|
func (m Model) View() string {
|
|
if m.quitting {
|
|
return "Goodbye!\n"
|
|
}
|
|
|
|
// Calculate panel widths
|
|
leftWidth := m.width/2 - 2
|
|
rightWidth := m.width - leftWidth - 6
|
|
|
|
// Render panels
|
|
leftPanel := m.renderControlsPanel(leftWidth)
|
|
rightPanel := m.renderLogsPanel(rightWidth)
|
|
|
|
// Apply panel styles based on focus
|
|
var leftStyled, rightStyled string
|
|
if m.focusedPanel == PanelControls {
|
|
leftStyled = activePanelStyle.Width(leftWidth).Render(leftPanel)
|
|
rightStyled = panelStyle.Width(rightWidth).Render(rightPanel)
|
|
} else {
|
|
leftStyled = panelStyle.Width(leftWidth).Render(leftPanel)
|
|
rightStyled = activePanelStyle.Width(rightWidth).Render(rightPanel)
|
|
}
|
|
|
|
// Join panels horizontally
|
|
content := lipgloss.JoinHorizontal(lipgloss.Top, leftStyled, rightStyled)
|
|
|
|
// Add title and help
|
|
title := titleStyle.Render("BLE Simulator TUI")
|
|
help := helpStyle.Render("↑↓: Navigate ←→: Adjust Enter: Activate Tab: Switch Panel q: Quit")
|
|
|
|
return lipgloss.JoinVertical(lipgloss.Left, title, content, help)
|
|
}
|
|
|
|
// renderControlsPanel renders the left controls panel
|
|
func (m Model) renderControlsPanel(width int) string {
|
|
var b strings.Builder
|
|
|
|
b.WriteString(titleStyle.Render("Controls"))
|
|
b.WriteString("\n\n")
|
|
|
|
// Render interactive controls
|
|
for i, ctrl := range m.controls {
|
|
isActive := m.focusedPanel == PanelControls && i == m.focusedCtrl
|
|
|
|
switch ctrl.Type {
|
|
case ControlButton:
|
|
if isActive {
|
|
b.WriteString(activeButtonStyle.Render(ctrl.Name))
|
|
} else {
|
|
b.WriteString(buttonStyle.Render(ctrl.Name))
|
|
}
|
|
b.WriteString("\n")
|
|
|
|
case ControlSlider:
|
|
b.WriteString(m.renderSlider(ctrl, isActive, width-4))
|
|
b.WriteString("\n")
|
|
}
|
|
b.WriteString("\n")
|
|
}
|
|
|
|
// Parameter display section
|
|
b.WriteString("\n")
|
|
b.WriteString(titleStyle.Render("Parameters (R/O)"))
|
|
b.WriteString("\n\n")
|
|
|
|
params := m.state.GetAllParams()
|
|
paramOrder := []int{1, 2, 3, 4, 11, 12, 20}
|
|
paramNames := map[int]string{
|
|
1: "W1 (Min)",
|
|
2: "W2 (Max)",
|
|
3: "W3 (Pulses)",
|
|
4: "W4 (Time)",
|
|
11: "W11 (MinPD)",
|
|
12: "W12 (MaxPD)",
|
|
20: "W20 (Gain)",
|
|
}
|
|
paramRanges := map[int][2]int{
|
|
1: {0, 2000},
|
|
2: {0, 4095},
|
|
3: {1, 50},
|
|
4: {10, 1000},
|
|
11: {0, 1000},
|
|
12: {0, 1000},
|
|
20: {0, 255},
|
|
}
|
|
|
|
for _, id := range paramOrder {
|
|
val := params[id]
|
|
rng := paramRanges[id]
|
|
name := paramNames[id]
|
|
bar := m.renderProgressBar(val, rng[0], rng[1], 10)
|
|
b.WriteString(fmt.Sprintf("%s %s %d\n", sliderLabelStyle.Render(name), bar, val))
|
|
}
|
|
|
|
// Status section
|
|
b.WriteString("\n")
|
|
b.WriteString(titleStyle.Render("Status"))
|
|
b.WriteString("\n\n")
|
|
|
|
// Connection status
|
|
connCount := m.state.GetConnectedClients()
|
|
if connCount > 0 {
|
|
b.WriteString(fmt.Sprintf("Connected: %s\n", statusOnStyle.Render(fmt.Sprintf("Yes (%d)", connCount))))
|
|
} else {
|
|
b.WriteString(fmt.Sprintf("Connected: %s\n", statusOffStyle.Render("No")))
|
|
}
|
|
|
|
// Alarm status
|
|
if alarmActive, alarmType := m.state.GetAlarmState(); alarmActive {
|
|
b.WriteString(fmt.Sprintf("Alarm: %s\n", statusOnStyle.Render(alarmType)))
|
|
} else {
|
|
b.WriteString(fmt.Sprintf("Alarm: %s\n", statusOffStyle.Render("OFF")))
|
|
}
|
|
|
|
// Prog mode status
|
|
if m.state.GetProgMode() {
|
|
b.WriteString(fmt.Sprintf("Prog Mode: %s\n", statusOnStyle.Render("ON")))
|
|
} else {
|
|
b.WriteString(fmt.Sprintf("Prog Mode: %s\n", statusOffStyle.Render("OFF")))
|
|
}
|
|
|
|
return b.String()
|
|
}
|
|
|
|
// renderSlider renders a slider control
|
|
func (m Model) renderSlider(ctrl Control, isActive bool, _ int) string {
|
|
value := ctrl.GetValue()
|
|
bar := m.renderProgressBar(value, ctrl.Min, ctrl.Max, 15)
|
|
|
|
label := sliderLabelStyle.Render(ctrl.Name)
|
|
valueStr := sliderValueStyle.Render(fmt.Sprintf("%d", value))
|
|
|
|
var prefix string
|
|
if isActive {
|
|
prefix = lipgloss.NewStyle().Foreground(primaryColor).Bold(true).Render("> ")
|
|
} else {
|
|
prefix = " "
|
|
}
|
|
|
|
return fmt.Sprintf("%s%s %s %s", prefix, label, bar, valueStr)
|
|
}
|
|
|
|
// renderProgressBar renders a progress bar
|
|
func (m Model) renderProgressBar(value, min, max, barWidth int) string {
|
|
if max <= min {
|
|
return strings.Repeat("░", barWidth)
|
|
}
|
|
|
|
ratio := float64(value-min) / float64(max-min)
|
|
filled := int(ratio * float64(barWidth))
|
|
if filled > barWidth {
|
|
filled = barWidth
|
|
}
|
|
if filled < 0 {
|
|
filled = 0
|
|
}
|
|
|
|
filledStyle := lipgloss.NewStyle().Foreground(primaryColor)
|
|
emptyStyle := lipgloss.NewStyle().Foreground(secondaryColor)
|
|
|
|
return filledStyle.Render(strings.Repeat("█", filled)) +
|
|
emptyStyle.Render(strings.Repeat("░", barWidth-filled))
|
|
}
|
|
|
|
// renderLogsPanel renders the right logs panel
|
|
func (m Model) renderLogsPanel(width int) string {
|
|
var b strings.Builder
|
|
|
|
b.WriteString(titleStyle.Render("Logs"))
|
|
b.WriteString("\n\n")
|
|
|
|
entries := m.logBuffer.Entries()
|
|
maxVisible := m.height - 10 // Account for borders, title, help
|
|
|
|
maxVisible = max(maxVisible, 1)
|
|
|
|
// Calculate which entries to show
|
|
start := max(len(entries)-maxVisible-m.logOffset, 0)
|
|
end := min(start+maxVisible, len(entries))
|
|
|
|
for i := start; i < end; i++ {
|
|
entry := entries[i]
|
|
|
|
// Format log entry
|
|
levelStyle, ok := logLevelStyles[entry.Level]
|
|
if !ok {
|
|
levelStyle = lipgloss.NewStyle()
|
|
}
|
|
|
|
timeStr := entry.Time.Format("15:04:05")
|
|
levelStr := levelStyle.Render(fmt.Sprintf("[%s]", entry.Level))
|
|
|
|
// Truncate message if too long
|
|
msg := entry.Message
|
|
maxMsgLen := max(width-16, 10)
|
|
if len(msg) > maxMsgLen {
|
|
msg = msg[:maxMsgLen-3] + "..."
|
|
}
|
|
|
|
b.WriteString(fmt.Sprintf("%s %s %s\n", timeStr, levelStr, msg))
|
|
}
|
|
|
|
// Show scroll indicator if needed
|
|
if len(entries) > maxVisible {
|
|
indicator := fmt.Sprintf("\n[%d/%d entries]", end, len(entries))
|
|
b.WriteString(helpStyle.Render(indicator))
|
|
}
|
|
|
|
return b.String()
|
|
}
|