Compare commits

...

3 Commits

Author SHA1 Message Date
shinya
f69e6db582 completed tui sidebar scrolling and selecting chat 2025-08-17 22:58:19 +02:00
foglar
440ef38439 update tui and sidebar 2025-08-16 19:19:12 +02:00
foglar
71be36fc93 Testing messages loading 2025-08-12 13:45:36 +02:00
11 changed files with 424 additions and 186 deletions

View File

@ -1,9 +1,11 @@
package main
import (
"whspbrd/pkg/render_image"
"whspbrd/internal/tui"
//"whspbrd/pkg/render_image"
)
func main() {
render_image.RenderImage("kogami-pf-edit.jpg", 0, 3, 20, 0, 1)
//config.NewConfigLoadTemplate()
tui.Run()
}

View File

@ -0,0 +1,26 @@
{
"messages" :
[
{
"id": "1",
"sender": "Alice",
"receiver": "bob",
"content": "SGVsbG8gQm9iLCBob3cgYXJlIHlvdT8K",
"timestamp": "2023-10-01T10:00:00Z"
},
{
"id": "2",
"sender": "bob",
"receiver": "Alice",
"content": "SGkgQWxpY2UhIEknbSBkb2luZyB3ZWxsLCB0aGFua3MuIEhvdyBhYm91dCB5b3U/Cg==",
"timestamp": "2023-10-01T10:05:00Z"
},
{
"id": "3",
"sender": "Alice",
"receiver": "bob",
"content": "SSdtIGdyZWF0LCB0aGFua3MgZm9yIGFza2luZwo=",
"timestamp": "2023-10-01T10:06:00Z"
}
]
}

View File

@ -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"
}
]
}

View File

@ -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))
}

74
internal/tui/chat.go Normal file
View File

@ -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
}

39
internal/tui/colors.go Normal file
View File

@ -0,0 +1,39 @@
package tui
var Colors = struct {
base00 string
base01 string
base02 string
base03 string
base04 string
base05 string
base06 string
base07 string
base08 string
base09 string
base10 string
base11 string
base12 string
base13 string
base14 string
base15 string
base16 string
}{
base00: "\033[31;7m",
base01: "\033[32;7m",
base02: "\033[33;7m",
base03: "\033[34;7m",
base04: "\033[35;7m",
base05: "\033[36;7m",
base06: "\033[37;7m",
base07: "\033[38;7m",
base08: "\033[39;7m",
base09: "\033[310;7m",
base10: "\033[311;7m",
base11: "\033[312;7m",
base12: "\033[313;7m",
base13: "\033[314;7m",
base14: "\033[315;7m",
base15: "\033[316;7m",
base16: "\033[0m",
}

View File

@ -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
}

30
internal/tui/layout.go Normal file
View File

@ -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
}

73
internal/tui/messages.go Normal file
View File

@ -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))
}
}
}

65
internal/tui/sidebar.go Normal file
View File

@ -0,0 +1,65 @@
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()
updateUsersView(g)
}
return nil
}
func updateUsersView(g *gocui.Gui) error {
v, err := g.View("users")
if err != nil {
return err
}
v.Clear()
// TODO: Render profile image of users and change colors of each user maybe?
for i, u := range users {
// Change Selected User In The TUI Window
if i == selectedUserIdx {
fmt.Fprintf(v, "%s%s%s\n", Colors.base06, u, Colors.base16)
_, y := v.Size()
if i == 0 {
v.SetOrigin(0, 0)
} else {
v.SetOrigin(0, i-y+1)
}
} else {
fmt.Fprintln(v, u)
}
}
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)
}

View File

@ -1,199 +1,20 @@
package tui
import (
"fmt"
_ "image/jpeg"
_ "image/png"
"log"
"strings"
"github.com/jroimartin/gocui"
//"whspbrd/pkg/systray"
"whspbrd/pkg/render_image"
)
var messages []string
var users = []string{"Alice\n", "Bob\n", "Charlie\n"}
var users []string
var prevWidth, prevHeight int
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)
//imagePath := "kogami-rounded.png"
//file, err := os.Open(imagePath)
//if err != nil {
// log.Println("Error opening image:", err)
// continue
//}
//defer file.Close()
//img, _, err := image.Decode(file)
//if err != nil {
// log.Println("Error decoding image:", err)
// continue
//}
// Print image directly to terminal (stdout)
render_image.RenderImage("kogami-rounded.png", i*3+2, 23, 50, 50, 0)
//err = kittyimg.Fprintln(os.Stdout, img)
//if err != nil {
// log.Println("Error rendering image:", err)
//}
}
}
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)
}
}
v.Clear()
v.SetCursor(0, 0)
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 {
// Ctrl+C to quit
if err := g.SetKeybinding("", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil {
return err
}
// Enter to send message
// Enter adds a new line (multiline input)
//if err := g.SetKeybinding("input", gocui.KeyArrowDown, gocui.ModNone, insertNewline); 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
}
// Alt+Enter inserts newline
if err := g.SetKeybinding("input", gocui.KeyEnter, gocui.ModAlt, insertNewline); err != nil {
return err
}
// Enter (no modifiers) sends message
if err := g.SetKeybinding("input", gocui.KeyEnter, gocui.ModNone, sendMessage); err != nil {
return err
}
// Arrow up
if err := g.SetKeybinding("chat", gocui.KeyArrowUp, gocui.ModNone, cursorUp); err != nil {
return err
}
// Arrow down
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() {
//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 {
log.Panicln(err)