diff options
| -rw-r--r-- | internal/client/api/api.go | 130 | ||||
| -rw-r--r-- | internal/client/ui/chat/chat.go | 130 | ||||
| -rw-r--r-- | internal/client/ui/core/chat/chat.go | 48 | ||||
| -rw-r--r-- | internal/client/ui/core/core.go | 20 | ||||
| -rw-r--r-- | internal/client/ui/core/network/network.go | 12 | ||||
| -rw-r--r-- | internal/client/ui/messagebox/messagebox.go | 131 | ||||
| -rw-r--r-- | internal/client/ui/viminput/linedecorations.go | 17 | ||||
| -rw-r--r-- | internal/client/ui/viminput/viminput.go | 89 |
8 files changed, 175 insertions, 402 deletions
diff --git a/internal/client/api/api.go b/internal/client/api/api.go deleted file mode 100644 index cfc4ce9..0000000 --- a/internal/client/api/api.go +++ /dev/null @@ -1,130 +0,0 @@ -package api - -import ( - "errors" - "fmt" - "log" - - tea "github.com/charmbracelet/bubbletea" - - "github.com/kyren223/eko/internal/client/gateway" - "github.com/kyren223/eko/internal/data" - "github.com/kyren223/eko/internal/packet" - "github.com/kyren223/eko/pkg/assert" - "github.com/kyren223/eko/pkg/snowflake" -) - -type ( - AppendMessage data.Message - UserProfileUpdate data.User -) - -func SendMessage(message string) tea.Cmd { - return func() tea.Msg { - log.Println("request SendMessage sent") - frequencyId := snowflake.ID(1852771536100921344) - request := packet.SendMessage{ - Content: message, - FrequencyID: &frequencyId, - } - response, ok := <-gateway.Send(&request) - if !ok { - return errors.New("request SendMessage timeout") - } - log.Println("request SendMessage received response") - - switch response := response.(type) { - case *packet.ErrorMessage: - return errors.New(response.Error) - case *packet.Messages: - assert.Assert(len(response.Messages) == 1, "server must return only the one message that was sent") - return AppendMessage(response.Messages[0]) - } - return fmt.Errorf("received invalid response from server: %v", response.Type()) - } -} - -func GetMessages() tea.Msg { - log.Println("request GetMessages sent") - frequencyId := snowflake.ID(1852771536100921344) - request := packet.GetMessagesRange{ - FrequencyID: &frequencyId, - ReceiverID: nil, - From: nil, - To: nil, - } - response, ok := <-gateway.Send(&request) - if !ok { - return errors.New("request GetMessages timeout") - } - log.Println("request GetMessages received response") - - switch response := response.(type) { - case *packet.ErrorMessage: - return errors.New(response.Error) - case *packet.Messages: - return response - } - return fmt.Errorf("received invalid response from server: %v", response.Type()) -} - -func GetUserById(id snowflake.ID) tea.Cmd { - return func() tea.Msg { - log.Println("request GetUserById for ID", id, "sent") - request := packet.GetUserByID{UserID: id} - response, ok := <-gateway.Send(&request) - if !ok { - return errors.New("request GetUserById timeout") - } - log.Println("request GetUserById received response") - - switch response := response.(type) { - case *packet.ErrorMessage: - return errors.New(response.Error) - case *packet.Users: - if len(response.Users) == 0 { - return fmt.Errorf("requested user id %v not found", id) - } - assert.Assert(len(response.Users) == 1, "server must return only one user with the matching id") - return UserProfileUpdate(response.Users[0]) - } - return fmt.Errorf("received invalid response from server: %v", response.Type()) - } -} - -func CreateServer( - name string, - icon string, - bgHexColor string, - fgHexColor string, - isPublic bool, -) tea.Cmd { - return func() tea.Msg { - requestName := "CreateServer" - log.Println("request", requestName, "sent") - request := packet.CreateNetwork{ - Name: name, - Icon: icon, - BgHexColor: bgHexColor, - FgHexColor: fgHexColor, - IsPublic: isPublic, - } - response, ok := <-gateway.Send(&request) - if !ok { - return fmt.Errorf("request %s timeout", requestName) - } - log.Println("request", requestName, "received response") - - switch response := response.(type) { - case *packet.ErrorMessage: - return errors.New(response.Error) - case *packet.Users: - if len(response.Users) == 0 { - return fmt.Errorf("requested user id %v not found", id) - } - assert.Assert(len(response.Users) == 1, "server must return only one user with the matching id") - return UserProfileUpdate(response.Users[0]) - } - return fmt.Errorf("received invalid response from server: %v", response.Type()) - } -} diff --git a/internal/client/ui/chat/chat.go b/internal/client/ui/chat/chat.go deleted file mode 100644 index c69d0fd..0000000 --- a/internal/client/ui/chat/chat.go +++ /dev/null @@ -1,130 +0,0 @@ -package chat - -import ( - "fmt" - "log" - "strings" - - "github.com/charmbracelet/bubbles/cursor" - "github.com/charmbracelet/bubbles/textarea" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" - - "github.com/kyren223/eko/internal/client/api" - "github.com/kyren223/eko/internal/client/ui/messagebox" - "github.com/kyren223/eko/internal/data" - "github.com/kyren223/eko/internal/packet" -) - -type Model struct { - messagebox messagebox.Model - textarea textarea.Model - err error -} - -func New() 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 - - ta.KeyMap.InsertNewline.SetEnabled(false) - - return Model{ - textarea: ta, - messagebox: messagebox.New(30, 20), - err: nil, - } -} - -func (m Model) Init() tea.Cmd { - return tea.Batch(textarea.Blink, api.GetMessages) -} - -func (m Model) View() string { - return fmt.Sprintf( - "%s\n%s", - m.messagebox.View(), - m.textarea.View(), - ) + "" -} - -func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { - switch msg := msg.(type) { - case tea.WindowSizeMsg: - m.messagebox.Viewport.Width = msg.Width - m.messagebox.Viewport.Height = msg.Height - m.textarea.Height() - m.messagebox.Viewport.GotoBottom() - - m.textarea.SetWidth(msg.Width) - log.Println("resized to:", msg.Width, "x", msg.Height) - - var mbCmd, taCmd tea.Cmd - m.messagebox, mbCmd = m.messagebox.Update(msg) - m.textarea, taCmd = m.textarea.Update(msg) - return m, tea.Batch(mbCmd, taCmd) - - case tea.KeyMsg: - switch msg.Type { - - case tea.KeyEnter: - value := m.textarea.Value() - - content := strings.TrimSpace(value) - if content == "" { - return m, nil - } - m.textarea.Reset() - - return m, api.SendMessage(content) - - default: - var cmd tea.Cmd - m.textarea, cmd = m.textarea.Update(msg) - return m, cmd - } - - case packet.Payload: - switch msg := msg.(type) { - case *packet.Messages: - m.messagebox.SetMessages(msg.Messages) - - var cmd tea.Cmd - m.messagebox, cmd = m.messagebox.Update(msg) - return m, cmd - - default: - return m, nil - } - - case api.AppendMessage: - m.messagebox.AppendMessage(data.Message(msg)) - - var cmd tea.Cmd - m.messagebox, cmd = m.messagebox.Update(msg) - return m, cmd - - case api.UserProfileUpdate: - var cmd tea.Cmd - m.messagebox, cmd = m.messagebox.Update(msg) - return m, cmd - - case cursor.BlinkMsg: - var cmd tea.Cmd - m.textarea, cmd = m.textarea.Update(msg) - return m, cmd - - default: - return m, nil - } -} diff --git a/internal/client/ui/core/chat/chat.go b/internal/client/ui/core/chat/chat.go new file mode 100644 index 0000000..b3597a8 --- /dev/null +++ b/internal/client/ui/core/chat/chat.go @@ -0,0 +1,48 @@ +package chat + +import ( + "log" + + "github.com/charmbracelet/bubbles/textarea" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + + "github.com/kyren223/eko/internal/client/ui/colors" + "github.com/kyren223/eko/internal/client/ui/viminput" +) + +type Model struct { + vi viminput.Model +} + +func New() Model { + vi := viminput.New(30, 3) + vi.Placeholder = "Send a message..." + vi.PlaceholderStyle = lipgloss.NewStyle().Foreground(colors.Gray) + vi.LineDecoration = func(lnum int, line string, cursorLnum int) string { + return "┃ " + } + // vi.ShowLineNumbers = false + + // vi.CharLimit = 280 + + // vi.Focus() + + log.Println("Placeholder:", vi.Placeholder) + + return Model{ + vi: vi, + } +} + +func (m Model) Init() tea.Cmd { + return tea.Batch(textarea.Blink, nil) +} + +func (m Model) View() string { + return m.vi.View() +} + +func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { + return m, nil +} diff --git a/internal/client/ui/core/core.go b/internal/client/ui/core/core.go index 5031dc0..b4390e0 100644 --- a/internal/client/ui/core/core.go +++ b/internal/client/ui/core/core.go @@ -58,14 +58,18 @@ type Model struct { func New(privKey ed25519.PrivateKey, name string) Model { m := Model{ - name: name, - privKey: privKey, - networkList: networklist.New(), - loading: loadscreen.New(connectingToServer), - timer: newTimer(initialTimeout), - timeout: initialTimeout, - connected: false, - focus: FocusNetworkList, + name: name, + privKey: privKey, + id: 0, + loading: loadscreen.New(connectingToServer), + timer: newTimer(initialTimeout), + timeout: initialTimeout, + connected: false, + networkCreationPopup: nil, + frequencyCreationPopup: nil, + networkList: networklist.New(), + network: network.New(), + focus: FocusNetworkList, } m.move(0) // Update focus diff --git a/internal/client/ui/core/network/network.go b/internal/client/ui/core/network/network.go index 75ea318..3c24350 100644 --- a/internal/client/ui/core/network/network.go +++ b/internal/client/ui/core/network/network.go @@ -8,6 +8,7 @@ import ( "github.com/kyren223/eko/internal/client/gateway" "github.com/kyren223/eko/internal/client/ui" "github.com/kyren223/eko/internal/client/ui/colors" + "github.com/kyren223/eko/internal/client/ui/core/chat" "github.com/kyren223/eko/internal/client/ui/core/state" "github.com/kyren223/eko/internal/packet" ) @@ -31,12 +32,16 @@ type Model struct { networkIndex int index int focus bool + chat chat.Model } func New() Model { + chat := chat.New() + return Model{ focus: false, index: 0, + chat: chat, } } @@ -69,13 +74,14 @@ func (m Model) View() string { builder.WriteString("\n") } - result := builder.String() - + sidebar := builder.String() sep := sepStyle.Height(ui.Height) if m.focus { sep = sep.BorderForeground(colors.Focus) } - result = lipgloss.JoinHorizontal(lipgloss.Top, result, sep.String()) + sidebar = lipgloss.JoinHorizontal(lipgloss.Top, sidebar, sep.String()) + + result := lipgloss.JoinHorizontal(lipgloss.Top, sidebar, m.chat.View()) return result } diff --git a/internal/client/ui/messagebox/messagebox.go b/internal/client/ui/messagebox/messagebox.go deleted file mode 100644 index 0728cb1..0000000 --- a/internal/client/ui/messagebox/messagebox.go +++ /dev/null @@ -1,131 +0,0 @@ -package messagebox - -import ( - "slices" - "strings" - "time" - - "github.com/charmbracelet/bubbles/viewport" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" - - "github.com/kyren223/eko/internal/client/api" - "github.com/kyren223/eko/internal/data" - "github.com/kyren223/eko/pkg/snowflake" -) - -var ( - senderStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("5")).Bold(true) - timestampStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#585c62")).Italic(true) -) - -type user struct { - name string - isFetching bool -} - -type Model struct { - Viewport viewport.Model - messages []data.Message - users map[snowflake.ID]user -} - -func New(width, height int) Model { - return Model{ - Viewport: viewport.New(width, height), - messages: nil, - users: make(map[snowflake.ID]user), - } -} - -func (m Model) Init() tea.Cmd { - return api.GetMessages -} - -func (m Model) View() string { - return m.Viewport.View() -} - -func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { - var cmds []tea.Cmd - - for id, user := range m.users { - if !user.isFetching && user.name == "" { - cmds = append(cmds, api.GetUserById(id)) - user.isFetching = true - m.users[id] = user - } - } - - var cmd tea.Cmd - m.Viewport, cmd = m.Viewport.Update(msg) - cmds = append(cmds, cmd) - - switch msg := msg.(type) { - case api.UserProfileUpdate: - m.users[msg.ID] = user{msg.Name, false} - m.UpdateMessages() - } - - return m, tea.Batch(cmds...) -} - -func (m *Model) AppendMessage(message data.Message) { - m.messages = append(m.messages, message) - m.UpdateMessages() -} - -func (m *Model) SetMessages(messages []data.Message) { - m.messages = messages - m.UpdateMessages() -} - -func (m *Model) UpdateMessages() { - for _, message := range m.messages { - if _, exists := m.users[message.SenderID]; !exists { - m.users[message.SenderID] = user{name: "", isFetching: false} - } - } - - m.sortMessages() - m.updateContent() - m.Viewport.GotoBottom() -} - -func (m *Model) sortMessages() { - slices.SortFunc(m.messages, func(a, b data.Message) int { - if a.ID-b.ID < 0 { - return -1 - } else { - return 1 - } - }) -} - -func (m *Model) updateContent() { - if len(m.messages) == 0 { - return - } - - var builder strings.Builder - - for _, message := range m.messages { - sender := m.users[message.SenderID].name - builder.WriteString(senderStyle.Render(sender)) - builder.WriteByte(' ') - - timestamp := timestampFromID(message.ID) - builder.WriteString(timestampStyle.Render(timestamp)) - builder.WriteByte('\n') - - builder.WriteString(message.Content) - builder.WriteByte('\n') - } - - m.Viewport.SetContent(builder.String()[:builder.Len()-1]) -} - -func timestampFromID(id snowflake.ID) string { - localTime := time.UnixMilli(id.Time()) - return localTime.Format("02/01/2006 3:04 PM") -} diff --git a/internal/client/ui/viminput/linedecorations.go b/internal/client/ui/viminput/linedecorations.go new file mode 100644 index 0000000..82e51f1 --- /dev/null +++ b/internal/client/ui/viminput/linedecorations.go @@ -0,0 +1,17 @@ +package viminput + +import ( + "strconv" + + "github.com/charmbracelet/lipgloss" +) + +func LineNumberDecoration(style lipgloss.Style) LineDecoration { + return func(lnum int, line string, cursorLnum int) string { + return style.Render(strconv.FormatInt(int64(lnum), 10)) + } +} + +func EmptyLineDecoration(lnum int, line string, cursorLnum int) string { + return "" +} diff --git a/internal/client/ui/viminput/viminput.go b/internal/client/ui/viminput/viminput.go new file mode 100644 index 0000000..e2b65a8 --- /dev/null +++ b/internal/client/ui/viminput/viminput.go @@ -0,0 +1,89 @@ +package viminput + +import ( + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +type ( + Keymap struct{} + LineDecoration = func(lnum int, line string, cursorLnum int) string +) + +type Line []byte + +type Model struct { + keymap Keymap + + PlaceholderStyle lipgloss.Style + PromptStyle lipgloss.Style + + Placeholder string + LineDecoration LineDecoration + + lines []Line + + width int + height int + focus bool +} + +func New(width, height int) Model { + return Model{ + keymap: Keymap{}, + PlaceholderStyle: lipgloss.NewStyle(), + PromptStyle: lipgloss.NewStyle(), + Placeholder: "", + LineDecoration: EmptyLineDecoration, + width: width, + height: height, + focus: false, + } +} + +func (m Model) Init() tea.Cmd { + return nil +} + +func (m Model) View() string { + placeholder := m.PlaceholderStyle.Render(m.Placeholder) + lineDecoration := m.LineDecoration(1, "", 1) + + result := lipgloss.JoinHorizontal(lipgloss.Top, lineDecoration, placeholder) + + result = lipgloss.NewStyle().Width(m.width).Height(m.height).Render(result) + + return result +} + +func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { + if !m.focus { + return m, nil + } + + return m, nil +} + +func (m *Model) SetWidth(width int) { + m.width = width +} + +func (m Model) Width() int { + return m.width +} + +func (m *Model) SetHeight(height int) { + m.height = height +} + +func (m Model) Height() int { + return m.height +} + +func (m *Model) Focus() { + m.focus = true +} + +func (m *Model) Blur() { + m.focus = false +} |
