package main import ( "fmt" "strings" "time" "github.com/gdamore/tcell/v2" "github.com/rivo/tview" ) // Buffer holds messages for a channel/DM/status window. type Buffer struct { Name string Lines []string Unread int } // UI manages the terminal interface. type UI struct { app *tview.Application messages *tview.TextView statusBar *tview.TextView input *tview.InputField layout *tview.Flex buffers []*Buffer currentBuffer int onInput func(string) } // NewUI creates the tview-based IRC-like UI. func NewUI() *UI { ui := &UI{ //nolint:exhaustruct,varnamelen // fields set below; ui is idiomatic app: tview.NewApplication(), buffers: []*Buffer{ {Name: "(status)", Lines: nil, Unread: 0}, }, } ui.initMessages() ui.initStatusBar() ui.initInput() ui.initKeyCapture() ui.layout = tview.NewFlex(). SetDirection(tview.FlexRow). AddItem(ui.messages, 0, 1, false). AddItem(ui.statusBar, 1, 0, false). AddItem(ui.input, 1, 0, true) ui.app.SetRoot(ui.layout, true) ui.app.SetFocus(ui.input) return ui } // Run starts the UI event loop (blocks). func (ui *UI) Run() error { err := ui.app.Run() if err != nil { return fmt.Errorf("run ui: %w", err) } return nil } // Stop stops the UI. func (ui *UI) Stop() { ui.app.Stop() } // OnInput sets the callback for user input. func (ui *UI) OnInput(fn func(string)) { ui.onInput = fn } // AddLine adds a line to the specified buffer. func (ui *UI) AddLine(bufferName, line string) { ui.app.QueueUpdateDraw(func() { buf := ui.getOrCreateBuffer(bufferName) buf.Lines = append(buf.Lines, line) cur := ui.buffers[ui.currentBuffer] if cur != buf { buf.Unread++ ui.refreshStatusBar() } if cur == buf { _, _ = fmt.Fprintln(ui.messages, line) } }) } // AddStatus adds a line to the status buffer. func (ui *UI) AddStatus(line string) { ts := time.Now().Format("15:04") ui.AddLine( "(status)", "[gray]"+ts+"[white] "+line, ) } // SwitchBuffer switches to the buffer at index n. func (ui *UI) SwitchBuffer(bufIndex int) { ui.app.QueueUpdateDraw(func() { if bufIndex < 0 || bufIndex >= len(ui.buffers) { return } ui.currentBuffer = bufIndex buf := ui.buffers[bufIndex] buf.Unread = 0 ui.messages.Clear() for _, line := range buf.Lines { _, _ = fmt.Fprintln(ui.messages, line) } ui.messages.ScrollToEnd() ui.refreshStatusBar() }) } // SwitchToBuffer switches to named buffer, creating if // needed. func (ui *UI) SwitchToBuffer(name string) { ui.app.QueueUpdateDraw(func() { buf := ui.getOrCreateBuffer(name) for i, b := range ui.buffers { if b == buf { ui.currentBuffer = i break } } buf.Unread = 0 ui.messages.Clear() for _, line := range buf.Lines { _, _ = fmt.Fprintln(ui.messages, line) } ui.messages.ScrollToEnd() ui.refreshStatusBar() }) } // SetStatus updates the status bar text. func (ui *UI) SetStatus( nick, target, connStatus string, ) { ui.app.QueueUpdateDraw(func() { ui.renderStatusBar(nick, target, connStatus) }) } // BufferCount returns the number of buffers. func (ui *UI) BufferCount() int { return len(ui.buffers) } // BufferIndex returns the index of a named buffer. func (ui *UI) BufferIndex(name string) int { for i, buf := range ui.buffers { if buf.Name == name { return i } } return -1 } func (ui *UI) initMessages() { ui.messages = tview.NewTextView(). SetDynamicColors(true). SetScrollable(true). SetWordWrap(true). SetChangedFunc(func() { ui.app.Draw() }) ui.messages.SetBorder(false) } func (ui *UI) initStatusBar() { ui.statusBar = tview.NewTextView(). SetDynamicColors(true) ui.statusBar.SetBackgroundColor(tcell.ColorNavy) ui.statusBar.SetTextColor(tcell.ColorWhite) } func (ui *UI) initInput() { ui.input = tview.NewInputField(). SetFieldBackgroundColor(tcell.ColorBlack). SetFieldTextColor(tcell.ColorWhite) ui.input.SetDoneFunc(func(key tcell.Key) { if key != tcell.KeyEnter { return } text := ui.input.GetText() if text == "" { return } ui.input.SetText("") if ui.onInput != nil { ui.onInput(text) } }) } func (ui *UI) initKeyCapture() { ui.app.SetInputCapture( func(event *tcell.EventKey) *tcell.EventKey { if event.Modifiers()&tcell.ModAlt == 0 { return event } r := event.Rune() if r >= '0' && r <= '9' { idx := int(r - '0') ui.SwitchBuffer(idx) return nil } return event }, ) } func (ui *UI) refreshStatusBar() { // Placeholder; full refresh needs nick/target context. } func (ui *UI) renderStatusBar( nick, target, connStatus string, ) { var unreadParts []string for i, buf := range ui.buffers { if buf.Unread > 0 { unreadParts = append(unreadParts, fmt.Sprintf( "%d:%s(%d)", i, buf.Name, buf.Unread, ), ) } } unread := "" if len(unreadParts) > 0 { unread = " [Act: " + strings.Join(unreadParts, ",") + "]" } bufInfo := fmt.Sprintf( "[%d:%s]", ui.currentBuffer, ui.buffers[ui.currentBuffer].Name, ) ui.statusBar.Clear() _, _ = fmt.Fprintf(ui.statusBar, " [%s] %s %s %s%s", connStatus, nick, bufInfo, target, unread, ) } func (ui *UI) getOrCreateBuffer(name string) *Buffer { for _, buf := range ui.buffers { if buf.Name == name { return buf } } buf := &Buffer{Name: name, Lines: nil, Unread: 0} ui.buffers = append(ui.buffers, buf) return buf }