diff --git a/cmd/main.go b/cmd/main.go index dd1dc65..9d8a7a9 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -7,5 +7,5 @@ import ( func main() { //config.NewConfigLoadTemplate() - tui.Run("alice") + tui.Run() } diff --git a/configs/servers/default/users/alice/messages.json b/configs/servers/default/users/alice/messages.json index e6bce51..a1a4f5d 100644 --- a/configs/servers/default/users/alice/messages.json +++ b/configs/servers/default/users/alice/messages.json @@ -3,7 +3,7 @@ [ { "id": "1", - "sender": "alice", + "sender": "Alice", "receiver": "bob", "content": "SGVsbG8gQm9iLCBob3cgYXJlIHlvdT8K", "timestamp": "2023-10-01T10:00:00Z" @@ -11,13 +11,13 @@ { "id": "2", "sender": "bob", - "receiver": "alice", + "receiver": "Alice", "content": "SGkgQWxpY2UhIEknbSBkb2luZyB3ZWxsLCB0aGFua3MuIEhvdyBhYm91dCB5b3U/Cg==", "timestamp": "2023-10-01T10:05:00Z" }, { "id": "3", - "sender": "alice", + "sender": "Alice", "receiver": "bob", "content": "SSdtIGdyZWF0LCB0aGFua3MgZm9yIGFza2luZwo=", "timestamp": "2023-10-01T10:06:00Z" diff --git a/configs/servers/default/users/bob/messages.json b/configs/servers/default/users/bob/messages.json index e69de29..11fcbd6 100644 --- a/configs/servers/default/users/bob/messages.json +++ b/configs/servers/default/users/bob/messages.json @@ -0,0 +1,26 @@ +{ + "messages" : + [ + { + "id": "1", + "sender": "Bob", + "receiver": "Filip", + "content": "SGVsbG8gQm9iLCBob3cgYXJlIHlvdT8K", + "timestamp": "2023-10-01T10:00:00Z" + }, + { + "id": "2", + "sender": "Filip", + "receiver": "Bob", + "content": "SGkgQWxpY2UhIEknbSBkb2luZyB3ZWxsLCB0aGFua3MuIEhvdyBhYm91dCB5b3U/Cg==", + "timestamp": "2023-10-01T10:05:00Z" + }, + { + "id": "3", + "sender": "Bob", + "receiver": "FIlip", + "content": "SSdtIGdyZWF0LCB0aGFua3MgZm9yIGFza2luZwo=", + "timestamp": "2023-10-01T10:06:00Z" + } + ] +} \ No newline at end of file diff --git a/internal/config/configuration.go b/internal/config/configuration.go new file mode 100644 index 0000000..8791e14 --- /dev/null +++ b/internal/config/configuration.go @@ -0,0 +1,18 @@ +package config + +import ( + "embed" + "fmt" +) + +//go:embed config.json +var configuration embed.FS + +func NewConfigLoadTemplate() { + config, err := configuration.ReadFile("config.json") + if err != nil { + panic(err) + } + + fmt.Println(string(config)) +} diff --git a/internal/tui/chat.go b/internal/tui/chat.go new file mode 100644 index 0000000..05bc5dc --- /dev/null +++ b/internal/tui/chat.go @@ -0,0 +1,74 @@ +package tui + +import ( + "fmt" + "log" + + "whspbrd/pkg/cell_size" + "whspbrd/pkg/render_image" + + "github.com/jroimartin/gocui" +) + +func layoutChat(g *gocui.Gui, maxX, maxY int) error { + if v, err := g.SetView("chat", 21, 0, maxX-1, maxY-5); err != nil { + if err != gocui.ErrUnknownView { + return err + } + v.Title = "Chat" + v.Wrap = true + v.Autoscroll = true + updateChatView(v) + } + return nil +} + +func layoutInput(g *gocui.Gui, maxX, maxY int) error { + if v, err := g.SetView("input", 21, maxY-4, maxX-1, maxY-1); err != nil { + if err != gocui.ErrUnknownView { + return err + } + v.Title = "Type your message" + v.Editable = true + v.Wrap = true + if _, err := g.SetCurrentView("input"); err != nil { + return err + } + } + return nil +} + +func updateChatView(v *gocui.View) { + v.Clear() + for i, msg := range messages { + fmt.Fprintf(v, "%s\n\n", msg) + w, h, err := cell_size.GetTerminalCellSizePixels() + if err != nil { + log.Println("Error getting terminal cell size:", err) + continue + } + if h > w { + h = h * 2 + w = 0 + } else { + w = w*3 - (w / 10) + h = 0 + } + render_image.RenderImage("kogami-rounded.png", i*3+2, 23, w, h, 0) + } +} + +func sendMessage(g *gocui.Gui, v *gocui.View) error { + input := v.Buffer() + v.Clear() + v.SetCursor(0, 0) + v.SetOrigin(0, 0) + + if input != "" { + messages = append(messages, "\t\t\t\tYou:\n\t\t\t\t"+input) + if chatView, err := g.View("chat"); err == nil { + updateChatView(chatView) + } + } + return nil +} diff --git a/internal/tui/colors.go b/internal/tui/colors.go new file mode 100644 index 0000000..8a39626 --- /dev/null +++ b/internal/tui/colors.go @@ -0,0 +1 @@ +package tui \ No newline at end of file diff --git a/internal/tui/keybindings.go b/internal/tui/keybindings.go new file mode 100644 index 0000000..a829730 --- /dev/null +++ b/internal/tui/keybindings.go @@ -0,0 +1,64 @@ +package tui + +import ( + "github.com/jroimartin/gocui" +) + +var selectedUserIdx int + +func keybindings(g *gocui.Gui) error { + if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil { + return err + } + if err := g.SetKeybinding("input", gocui.KeyEnter, gocui.ModNone, sendMessage); err != nil { + return err + } + if err := g.SetKeybinding("chat", gocui.KeyArrowUp, gocui.ModNone, cursorUp); err != nil { + return err + } + if err := g.SetKeybinding("chat", gocui.KeyArrowDown, gocui.ModNone, cursorDown); err != nil { + return err + } + if err := g.SetKeybinding("", gocui.KeyTab, gocui.ModNone, nextContact); err != nil { + return err + } + if err := g.SetKeybinding("", gocui.KeySpace, gocui.ModNone, prevContact); err != nil { + return err + } + return nil +} + +func insertNewline(g *gocui.Gui, v *gocui.View) error { + v.EditNewLine() + return nil +} + +func quit(g *gocui.Gui, v *gocui.View) error { + return gocui.ErrQuit +} + +func cursorDown(g *gocui.Gui, v *gocui.View) error { + if v != nil { + cx, cy := v.Cursor() + if err := v.SetCursor(cx, cy+1); err != nil { + ox, oy := v.Origin() + if err := v.SetOrigin(ox, oy+1); err != nil { + return err + } + } + } + return nil +} + +func cursorUp(g *gocui.Gui, v *gocui.View) error { + if v != nil { + ox, oy := v.Origin() + cx, cy := v.Cursor() + if err := v.SetCursor(cx, cy-1); err != nil && oy > 0 { + if err := v.SetOrigin(ox, oy-1); err != nil { + return err + } + } + } + return nil +} diff --git a/internal/tui/layout.go b/internal/tui/layout.go new file mode 100644 index 0000000..4f8db40 --- /dev/null +++ b/internal/tui/layout.go @@ -0,0 +1,30 @@ +package tui + +import ( + "github.com/jroimartin/gocui" +) + +func layout(g *gocui.Gui) error { + maxX, maxY := g.Size() + + if maxX != prevWidth || maxY != prevHeight { + prevWidth, prevHeight = maxX, maxY + if chatView, err := g.View("chat"); err == nil { + updateChatView(chatView) + } + } + + if err := layoutSidebar(g, maxY); err != nil { + return err + } + if err := layoutChat(g, maxX, maxY); err != nil { + return err + } + if err := layoutInput(g, maxX, maxY); err != nil { + return err + } + + return nil +} + + diff --git a/internal/tui/messages.go b/internal/tui/messages.go new file mode 100644 index 0000000..d112e61 --- /dev/null +++ b/internal/tui/messages.go @@ -0,0 +1,73 @@ +package tui + +import ( + "encoding/base64" + "encoding/json" + "log" + "os" + "path/filepath" + "strings" + "time" +) + +type ChatMessage struct { + ID string `json:"id"` + Sender string `json:"sender"` + Receiver string `json:"receiver"` + Content string `json:"content"` + Timestamp string `json:"timestamp"` +} + +type ChatData struct { + Messages []ChatMessage `json:"messages"` +} + +func LoadContacts(path string) { + contactsPath := filepath.Join(path, "users") + folders, err := os.ReadDir(contactsPath) + if err != nil { + log.Printf("Error reading contacts directory: %v", err) + return + } + + for _, folder := range folders { + if folder.IsDir() { + users = append(users, folder.Name()) + } + } +} + +func LoadMessages(username string) { + chatFile := filepath.Join("configs", "servers", "default", "users", strings.ToLower(username), "messages.json") + data, err := os.ReadFile(chatFile) + if err != nil { + log.Printf("Error reading chat file: %v", err) + return + } + + var chatData ChatData + if err := json.Unmarshal(data, &chatData); err != nil { + log.Printf("Error parsing JSON: %v", err) + return + } + + for _, msg := range chatData.Messages { + decoded, err := base64.StdEncoding.DecodeString(msg.Content) + if err != nil { + log.Printf("Error decoding message: %v", err) + continue + } + if strings.HasSuffix(string(decoded), "\n") { + decoded = []byte(strings.TrimSuffix(string(decoded), "\n")) + } + + t, _ := time.Parse(time.RFC3339, msg.Timestamp) + formattedTime := t.Format("2006-01-02 15:04") + + if !strings.EqualFold(msg.Sender, username) { + messages = append(messages, "\t\t\t\tYou ("+formattedTime+"):\n\t\t\t\t"+string(decoded)) + } else { + messages = append(messages, "\t\t\t\t"+msg.Sender+" ("+formattedTime+"):\n\t\t\t\t"+string(decoded)) + } + } +} diff --git a/internal/tui/sidebar.go b/internal/tui/sidebar.go new file mode 100644 index 0000000..b4bd253 --- /dev/null +++ b/internal/tui/sidebar.go @@ -0,0 +1,57 @@ +package tui + +import ( + "fmt" + + "github.com/jroimartin/gocui" +) + +// LAYOUT +func layoutSidebar(g *gocui.Gui, maxY int) error { + if v, err := g.SetView("users", 0, 0, 20, maxY-1); err != nil { + if err != gocui.ErrUnknownView { + return err + } + v.Title = "Users" + v.Clear() + for _, u := range users { + fmt.Fprintln(v, u+"\n") + } + } + return nil +} + +func updateUsersView(g *gocui.Gui) error { + v, err := g.View("users") + if err != nil { + return err + } + + v.Clear() + for i, u := range users { + if i == selectedUserIdx { + fmt.Fprintln(v, fmt.Sprintf(">%s\n", u)) + } else { + fmt.Fprintln(v, u+"\n") + } + } + + return nil +} + +// KEYBINDINGS +func nextContact(g *gocui.Gui, v *gocui.View) error { + if len(users) == 0 { + return nil + } + selectedUserIdx = (selectedUserIdx + 1) % len(users) + return updateUsersView(g) +} + +func prevContact(g *gocui.Gui, v *gocui.View) error { + if len(users) == 0 { + return nil + } + selectedUserIdx = (selectedUserIdx - 1 + len(users)) % len(users) + return updateUsersView(g) +} diff --git a/internal/tui/tui.go b/internal/tui/tui.go index 6a13562..517ebe7 100644 --- a/internal/tui/tui.go +++ b/internal/tui/tui.go @@ -1,230 +1,19 @@ package tui import ( - "encoding/base64" - "encoding/json" - "fmt" "log" - "os" - "path/filepath" - "strings" - "time" - - "whspbrd/pkg/cell_size" - "whspbrd/pkg/render_image" "github.com/jroimartin/gocui" ) -type ChatMessage struct { - ID string `json:"id"` - Sender string `json:"sender"` - Receiver string `json:"receiver"` - Content string `json:"content"` - Timestamp string `json:"timestamp"` -} - -type ChatData struct { - Messages []ChatMessage `json:"messages"` -} - var messages []string -var users = []string{"Alice\n", "Bob\n", "Charlie\n"} +var users []string var prevWidth, prevHeight int -// Load messages from configs/servers/default/users//messages.json -func loadMessages(username string) { - chatFile := filepath.Join("configs", "servers", "default", "users", strings.ToLower(username), "messages.json") - data, err := os.ReadFile(chatFile) - if err != nil { - log.Printf("Error reading chat file: %v", err) - return - } - - var chatData ChatData - if err := json.Unmarshal(data, &chatData); err != nil { - log.Printf("Error parsing JSON: %v", err) - return - } - - for _, msg := range chatData.Messages { - decoded, err := base64.StdEncoding.DecodeString(msg.Content) - if err != nil { - log.Printf("Error decoding message: %v", err) - continue - } - // Check if message is ending with a newline and remove it - if strings.HasSuffix(string(decoded), "\n") { - decoded = []byte(strings.TrimSuffix(string(decoded), "\n")) - } - - // Show timestamp in readable format - t, _ := time.Parse(time.RFC3339, msg.Timestamp) - formattedTime := t.Format("2006-01-02 15:04") - - if strings.EqualFold(msg.Sender, username) { - // Sent by "you" - messages = append(messages, fmt.Sprintf("\t\t\t\tYou (%s):\n\t\t\t\t%s", formattedTime, decoded)) - } else { - // Sent by other user - messages = append(messages, fmt.Sprintf("\t\t\t\t%s (%s):\n\t\t\t\t%s", msg.Sender, formattedTime, decoded)) - } - } -} - -func layout(g *gocui.Gui) error { - maxX, maxY := g.Size() - - if maxX != prevWidth || maxY != prevHeight { - prevWidth, prevHeight = maxX, maxY - if chatView, err := g.View("chat"); err == nil { - updateChatView(chatView) - } - } - - if v, err := g.SetView("users", 0, 0, 20, maxY-1); err != nil { - if err != gocui.ErrUnknownView { - return err - } - v.Title = "Users" - v.Clear() - for _, u := range users { - fmt.Fprintln(v, u) - } - } - - if v, err := g.SetView("chat", 21, 0, maxX-1, maxY-5); err != nil { - if err != gocui.ErrUnknownView { - return err - } - v.Title = "Chat" - v.Wrap = true - v.Autoscroll = true - updateChatView(v) - } - - if v, err := g.SetView("input", 21, maxY-4, maxX-1, maxY-1); err != nil { - if err != gocui.ErrUnknownView { - return err - } - v.Title = "Type your message" - v.Editable = true - v.Wrap = true - if _, err := g.SetCurrentView("input"); err != nil { - return err - } - } - - return nil -} - -func updateChatView(v *gocui.View) { - v.Clear() - for i, msg := range messages { - fmt.Fprintf(v, "%s\n\n", msg) - w, h, err := cell_size.GetTerminalCellSizePixels() - if err != nil { - log.Println("Error getting terminal cell size:", err) - continue - } - if h > w { - h = h * 2 - w = 0 - } else { - w = w*3 - (w / 10) - h = 0 - } - render_image.RenderImage("kogami-rounded.png", i*3+2, 23, w, h, 0) - } -} - -func sendMessage(g *gocui.Gui, v *gocui.View) error { - input := strings.TrimSpace(v.Buffer()) - v.Clear() - v.SetCursor(0, 0) - v.SetOrigin(0, 0) - - if input != "" { - messages = append(messages, "\t\t\t\tYou:\n\t\t\t\t"+input) - if chatView, err := g.View("chat"); err == nil { - updateChatView(chatView) - } - } - return nil -} - -func cursorDown(g *gocui.Gui, v *gocui.View) error { - if v != nil { - cx, cy := v.Cursor() - if err := v.SetCursor(cx, cy+1); err != nil { - ox, oy := v.Origin() - if err := v.SetOrigin(ox, oy+1); err != nil { - return err - } - } - } - return nil -} - -func cursorUp(g *gocui.Gui, v *gocui.View) error { - if v != nil { - ox, oy := v.Origin() - cx, cy := v.Cursor() - if err := v.SetCursor(cx, cy-1); err != nil && oy > 0 { - if err := v.SetOrigin(ox, oy-1); err != nil { - return err - } - } - } - return nil -} - -func nextView(g *gocui.Gui, v *gocui.View) error { - if v == nil || v.Name() == "chat" { - _, err := g.SetCurrentView("input") - return err - } - _, err := g.SetCurrentView("chat") - return err -} - -func keybindings(g *gocui.Gui) error { - if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil { - return err - } - if err := g.SetKeybinding("chat", gocui.KeyCtrlSpace, gocui.ModNone, nextView); err != nil { - return err - } - if err := g.SetKeybinding("input", gocui.KeyCtrlSpace, gocui.ModNone, nextView); err != nil { - return err - } - if err := g.SetKeybinding("input", gocui.KeyEnter, gocui.ModAlt, insertNewline); err != nil { - return err - } - if err := g.SetKeybinding("input", gocui.KeyEnter, gocui.ModNone, sendMessage); err != nil { - return err - } - if err := g.SetKeybinding("chat", gocui.KeyArrowUp, gocui.ModNone, cursorUp); err != nil { - return err - } - if err := g.SetKeybinding("chat", gocui.KeyArrowDown, gocui.ModNone, cursorDown); err != nil { - return err - } - return nil -} - -func insertNewline(g *gocui.Gui, v *gocui.View) error { - v.EditNewLine() - return nil -} - -func quit(g *gocui.Gui, v *gocui.View) error { - return gocui.ErrQuit -} - -func Run(username string) { - // Load chat history for the given user - loadMessages(username) +func Run() { + //LoadContacts("configs/servers/default") + users = []string{"Alice", "Bob", "Charlie", "David", "Eve", "Frank", "Grace", "Heidi", "Ivan", "Judy", "Karl", "Leo", "Mallory", "Nina", "Oscar", "Peggy", "Quentin", "Rupert", "Sybil", "Trent", "Uma", "Victor", "Walter", "Xena", "Yara", "Zane"} + LoadMessages(users[0]) g, err := gocui.NewGui(gocui.OutputNormal) if err != nil {