package main import ( "fmt" "strings" "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" "github.com/jixishi/SerialTerminalForWindowsTerminal/internal/event" ) func (m *uiModel) handleModalKey(msg tea.KeyMsg) bool { keyStr := strings.ToLower(msg.String()) if m.promptActive { return m.handlePromptKey(msg) } if keyStr == "esc" { m.closeModal() return true } if m.panelKind == event.UIPanelNone { if keyStr == "enter" { m.closeModal() } return true } switch m.panelKind { case event.UIPanelForward: return m.handleForwardPanelKey(keyStr) case event.UIPanelPlugin: return m.handlePluginPanelKey(keyStr) case event.UIPanelMode: return m.handleModePanelKey(keyStr) default: return true } } func (m *uiModel) closeModal() { m.showModal = false m.panelKind = event.UIPanelNone m.modalTitle = "" m.modalBody = "" m.promptActive = false m.promptSubmit = nil } func (m *uiModel) openPanel(kind event.UIPanelKind) { m.showModal = true m.panelKind = kind m.panelIndex = 0 m.promptActive = false m.promptSubmit = nil m.refreshPanel() } func (m *uiModel) refreshPanel() { switch m.panelKind { case event.UIPanelForward: m.forwardItems = m.app.forward.List() m.panelIndex = clampIndex(m.panelIndex, len(m.forwardItems)) case event.UIPanelPlugin: m.pluginItems = m.app.plugins.List() m.panelIndex = clampIndex(m.panelIndex, len(m.pluginItems)) case event.UIPanelMode: m.modeItems = m.buildModeItems() m.panelIndex = clampIndex(m.panelIndex, len(m.modeItems)) } } func (m *uiModel) buildModeItems() []modeItem { return []modeItem{{"in", "Input Charset", m.app.cfg.inputCode}, {"out", "Output Charset", m.app.cfg.outputCode}, {"end", "Line End", fmt.Sprintf("%q", m.app.cfg.endStr)}, {"frame", "Hex Frame Size", fmt.Sprintf("%d", m.app.cfg.frameSize)}, {"timestamp", "Timestamp", fmt.Sprintf("%v", m.app.cfg.timesTamp)}, {"timefmt", "Timestamp Format", m.app.cfg.timesFmt}} } func (m *uiModel) handleForwardPanelKey(key string) bool { switch key { case "up", "k": if m.panelIndex > 0 { m.panelIndex-- } return true case "down", "j": if m.panelIndex < len(m.forwardItems)-1 { m.panelIndex++ } return true case "r": m.refreshPanel() return true case "a": m.startPrompt("Add Forward", "tcp 127.0.0.1:12345", "", func(v string) { parts := strings.Fields(v) if len(parts) < 2 { m.app.Statusf("[forward] usage:
") return } m.app.handleLine(fmt.Sprintf(".forward add %s %s", parts[0], parts[1])) m.refreshPanel() }) return true } if len(m.forwardItems) == 0 { return true } sel := m.forwardItems[m.panelIndex] switch key { case "enter": if sel.Enabled { m.app.handleLine(fmt.Sprintf(".forward disable %d", sel.ID)) } else { m.app.handleLine(fmt.Sprintf(".forward enable %d", sel.ID)) } m.refreshPanel() return true case "d", "delete", "backspace": m.app.handleLine(fmt.Sprintf(".forward remove %d", sel.ID)) m.refreshPanel() return true case "u": m.startPrompt("Update Forward #"+fmt.Sprint(sel.ID), "tcp 127.0.0.1:12345", fmt.Sprintf("%s %s", sel.Mode, sel.Address), func(v string) { parts := strings.Fields(v) if len(parts) < 2 { m.app.Statusf("[forward] usage:
") return } m.app.handleLine(fmt.Sprintf(".forward update %d %s %s", sel.ID, parts[0], parts[1])) m.refreshPanel() }) return true default: return true } } func (m *uiModel) handlePluginPanelKey(key string) bool { switch key { case "up", "k": if m.panelIndex > 0 { m.panelIndex-- } return true case "down", "j": if m.panelIndex < len(m.pluginItems)-1 { m.panelIndex++ } return true case "r": m.refreshPanel() return true case "l": m.startPrompt("Load Plugin", "./plugins/demo.lua", "", func(v string) { path := strings.TrimSpace(v) if path == "" { m.app.Statusf("[plugin] load path is empty") return } m.app.handleLine(fmt.Sprintf(".plugin load %s", path)) m.refreshPanel() }) return true } if len(m.pluginItems) == 0 { return true } sel := m.pluginItems[m.panelIndex] switch key { case "enter": if sel.Enabled { m.app.handleLine(fmt.Sprintf(".plugin disable %s", sel.Name)) } else { m.app.handleLine(fmt.Sprintf(".plugin enable %s", sel.Name)) } m.refreshPanel() return true case "u": m.app.handleLine(fmt.Sprintf(".plugin reload %s", sel.Name)) m.refreshPanel() return true case "d", "delete", "backspace": m.app.handleLine(fmt.Sprintf(".plugin unload %s", sel.Name)) m.refreshPanel() return true default: return true } } func (m *uiModel) handleModePanelKey(key string) bool { switch key { case "up", "k": if m.panelIndex > 0 { m.panelIndex-- } return true case "down", "j": if m.panelIndex < len(m.modeItems)-1 { m.panelIndex++ } return true case "r": m.refreshPanel() return true } if len(m.modeItems) == 0 { return true } sel := m.modeItems[m.panelIndex] switch key { case " ": if sel.key == "timestamp" { if m.app.cfg.timesTamp { m.app.handleLine(".mode set timestamp off") } else { m.app.handleLine(".mode set timestamp on") } m.refreshPanel() } return true case "enter", "e": initial := strings.Trim(sel.value, "\"") m.startPrompt("Edit Mode: "+sel.label, "new value", initial, func(v string) { m.app.handleLine(fmt.Sprintf(".mode set %s %s", sel.key, v)) m.refreshPanel() }) return true default: return true } } func (m *uiModel) startPrompt(title, hint, initial string, submit func(string)) { in := textinput.New() in.Prompt = "> " in.Placeholder = hint in.SetValue(initial) in.Focus() in.CharLimit = 0 in.Width = 64 m.promptActive = true m.promptTitle = title m.promptHint = hint m.promptInput = in m.promptSubmit = submit } func (m *uiModel) handlePromptKey(msg tea.KeyMsg) bool { key := strings.ToLower(msg.String()) switch key { case "esc": m.promptActive = false m.promptSubmit = nil return true case "enter": value := strings.TrimSpace(m.promptInput.Value()) submit := m.promptSubmit m.promptActive = false m.promptSubmit = nil if submit != nil { submit(value) } return true default: var cmd tea.Cmd m.promptInput, cmd = m.promptInput.Update(msg) _ = cmd return true } } func (m *uiModel) renderPanel() string { switch m.panelKind { case event.UIPanelForward: return m.renderForwardPanel() case event.UIPanelPlugin: return m.renderPluginPanel() case event.UIPanelMode: return m.renderModePanel() default: return renderModal("Info", "No panel", m.availableModalWidth()) } } func (m *uiModel) renderForwardPanel() string { lines := make([]panelLine, 0, len(m.forwardItems)+2) if len(m.forwardItems) == 0 { lines = append(lines, panelLine{text: "No forwarding targets. Press 'a' to add one."}) } else { lines = append(lines, panelLine{text: "ID Mode Enabled Connected Address InBytes OutBytes"}) for i, it := range m.forwardItems { lines = append(lines, panelLine{text: fmt.Sprintf("%-3d %-5s %-7v %-9v %-22s %-7d %-8d", it.ID, it.Mode, it.Enabled, it.Connected, it.Address, it.ReadBytes, it.WriteByte), selected: i == m.panelIndex}) } } return renderPanelModal("Forward Panel", lines, "Up/Down select | Enter toggle enable | a add | u update | d remove | r refresh | Esc close", m.availableModalWidth()) } func (m *uiModel) renderPluginPanel() string { lines := make([]panelLine, 0, len(m.pluginItems)+2) if len(m.pluginItems) == 0 { lines = append(lines, panelLine{text: "No plugins loaded. Press 'l' to load one."}) } else { lines = append(lines, panelLine{text: "Name Enabled Path"}) for i, it := range m.pluginItems { lines = append(lines, panelLine{text: fmt.Sprintf("%-20s %-7v %s", it.Name, it.Enabled, it.Path), selected: i == m.panelIndex}) } } return renderPanelModal("Plugin Panel", lines, "Up/Down select | Enter toggle enable | l load | u reload | d unload | r refresh | Esc close", m.availableModalWidth()) } func (m *uiModel) renderModePanel() string { lines := make([]panelLine, 0, len(m.modeItems)+2) lines = append(lines, panelLine{text: "Field Value"}) for i, it := range m.modeItems { lines = append(lines, panelLine{text: fmt.Sprintf("%-16s %s", it.label, it.value), selected: i == m.panelIndex}) } return renderPanelModal("Mode Panel", lines, "Up/Down select | Enter edit value | Space toggle timestamp | r refresh | Esc close", m.availableModalWidth()) }