tui simulation
This commit is contained in:
290
internal/tui/view.go
Normal file
290
internal/tui/view.go
Normal file
@@ -0,0 +1,290 @@
|
||||
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()
|
||||
}
|
||||
Reference in New Issue
Block a user