From 6e6ca1d7a10a3a4a1decbdb78e2ea11ce24974e3 Mon Sep 17 00:00:00 2001 From: Kyren223 Date: Thu, 17 Oct 2024 17:12:57 +0300 Subject: feat: add ui for client --- internal/client/client.go | 98 ++++++++++++++++++++++-------------- internal/client/ui.go | 124 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 184 insertions(+), 38 deletions(-) create mode 100644 internal/client/ui.go (limited to 'internal/client') diff --git a/internal/client/client.go b/internal/client/client.go index 83b3d90..99c53b3 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -1,79 +1,94 @@ package client import ( - "bufio" "context" "crypto/tls" "crypto/x509" _ "embed" "fmt" "log" - "net" "os" - "strings" "time" "github.com/kyren223/eko/internal/packet" + "github.com/kyren223/eko/pkg/assert" ) //go:embed server.crt var certPEM []byte +var tlsConfig *tls.Config + func Run() { + logFile, err := os.OpenFile("client.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0666) + if err != nil { + log.Fatal(err) + } + defer logFile.Close() + log.SetOutput(logFile) + certPool := x509.NewCertPool() if !certPool.AppendCertsFromPEM(certPEM) { log.Fatalln("failed to append server certificate") } - tlsConfig := &tls.Config{ + tlsConfig = &tls.Config{ RootCAs: certPool, ServerName: "localhost", } log.Println("client started, waiting for user input...") - for { - fmt.Print("> ") - input, _ := bufio.NewReader(os.Stdin).ReadString('\n') - input = strings.TrimSpace(input) - if input == ":q" || input == "exit" || input == "quit" { - break - } - err := processRequest(input, tlsConfig) - if err != nil { - log.Println(err) - } + startUI() + + // for { + // fmt.Print("> ") + // input, _ := bufio.NewReader(os.Stdin).ReadString('\n') + // input = strings.TrimSpace(input) + // if input == ":q" || input == "exit" || input == "quit" { + // break + // } + // if input == "" { + // continue + // } + // err := processRequest(input) + // if err != nil { + // log.Println(err) + // fmt.Println(err) + // } + // } +} + +func sendMessage(message string) error { + request := packet.SendMessageMessage{Content: message} + var response packet.EkoMessage + if err := SendAndReceive(&request, &response); err != nil { + return err + } + assert.Assert(response.Message == "Eko OK", "server should return an OK status") + return nil +} + +func getMessages() ([]string, error) { + request := packet.GetMessagesMessage{} + var response packet.MessagesMessage + if err := SendAndReceive(&request, &response); err != nil { + return nil, err + } + var messages []string + for _, message := range response.Messages { + messages = append(messages, message.Contents) } + return messages, nil } -func processRequest(input string, tlsConfig *tls.Config) error { +func SendAndReceive(request packet.TypedMessage, response packet.TypedMessage) error { conn, err := tls.Dial("tcp4", ":7223", tlsConfig) if err != nil { return fmt.Errorf("error establishing connection with server: %v", err) } defer conn.Close() - log.Println("established connection with server:", conn.RemoteAddr().String()) - if input == "SHOW" { - request := packet.GetMessagesMessage{} - var response packet.MessagesMessage - if err := SendAndReceive(conn, &request, &response); err != nil { - return err - } - log.Println("server response:", response.Messages) - } else { - request := packet.SendMessageMessage{Content: input} - var response packet.EkoMessage - if err := SendAndReceive(conn, &request, &response); err != nil { - return err - } - log.Println("server response:", response.Message) - } - - return nil -} - -func SendAndReceive(conn net.Conn, request packet.TypedMessage, response packet.TypedMessage) error { encoder, err := packet.NewMsgPackEncoder(request) if err != nil { return fmt.Errorf("error encoding request: %v", err) @@ -92,7 +107,14 @@ func SendAndReceive(conn net.Conn, request packet.TypedMessage, response packet. select { case responsePacket := <-out: if err := responsePacket.DecodePayload(response); err != nil { - return fmt.Errorf("error decoding response: %v", err) + if responsePacket.Type() != packet.TypeError { + return fmt.Errorf("error decoding response: %v", err) + } + var errorResponse packet.ErrorMessage + if err := responsePacket.DecodePayload(&errorResponse); err != nil { + return fmt.Errorf("error decoding error packet: %w", err) + } + return fmt.Errorf("server error: %v", errorResponse.Error) } case err := <-outErr: diff --git a/internal/client/ui.go b/internal/client/ui.go new file mode 100644 index 0000000..839c0cd --- /dev/null +++ b/internal/client/ui.go @@ -0,0 +1,124 @@ +package client + +import ( + "fmt" + "log" + "strings" + + "github.com/charmbracelet/bubbles/cursor" + "github.com/charmbracelet/bubbles/textarea" + "github.com/charmbracelet/bubbles/viewport" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + + "github.com/kyren223/eko/pkg/assert" +) + +func startUI() { + p := tea.NewProgram(initialModel(), tea.WithAltScreen()) + if _, err := p.Run(); err != nil { + log.Println("charm ui error:", err) + } +} + +type model struct { + viewport viewport.Model + messages []string + textarea textarea.Model + senderStyle lipgloss.Style + err error +} + +func initialModel() model { + ta := textarea.New() + ta.Placeholder = "Send a message..." + ta.Focus() + + ta.Prompt = "┃ " + ta.CharLimit = 280 + + ta.SetWidth(30) + ta.SetHeight(3) + + // Remove cursor line styling + ta.FocusedStyle.CursorLine = lipgloss.NewStyle() + + ta.ShowLineNumbers = false + + vp := viewport.New(30, 20) + vp.SetContent("Welcome to Eko!\n Type a message and press Enter to send.") + + ta.KeyMap.InsertNewline.SetEnabled(false) + + messages, err := getMessages() + assert.NoError(err, "TODO HANDLE ERROR") + vp.SetContent(strings.Join(messages, "\n")) + + return model{ + textarea: ta, + messages: messages, + viewport: vp, + senderStyle: lipgloss.NewStyle().Foreground(lipgloss.Color("5")), + err: nil, + } +} + +func (m model) Init() tea.Cmd { + return textarea.Blink +} + +func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.WindowSizeMsg: + m.viewport.Width = msg.Width + m.textarea.SetWidth(msg.Width) + return m, tea.Printf("%dx%d", msg.Width, msg.Height) + case tea.KeyMsg: + switch msg.Type { + case tea.KeyEsc, tea.KeyCtrlC: + // Quit. + fmt.Println(m.textarea.Value()) + return m, tea.Quit + case tea.KeyEnter: + value := m.textarea.Value() + + content := strings.TrimSpace(value) + if content == "" { + // Don't send empty messages. + return m, nil + } + assert.NoError(sendMessage(content), "TODO HANDLE ERROR") + messages, err := getMessages() + assert.NoError(err, "TODO HANDLE ERROR") + + // m.messages = append(m.messages, m.senderStyle.Render("You: ")+content) + m.messages = messages + m.viewport.SetContent(strings.Join(m.messages, "\n")) + m.textarea.Reset() + m.viewport.GotoBottom() + return m, nil + default: + // Send all other keypresses to the textarea. + var cmd tea.Cmd + m.textarea, cmd = m.textarea.Update(msg) + return m, cmd + } + + case cursor.BlinkMsg: + // Textarea should also process cursor blinks. + var cmd tea.Cmd + m.textarea, cmd = m.textarea.Update(msg) + return m, cmd + + default: + return m, nil + } +} + +func (m model) View() string { + return fmt.Sprintf( + "%s\n\n%s", + m.viewport.View(), + m.textarea.View(), + ) + "\n\n" +} -- cgit v1.3.1