From f4dd0066c80f1d722b661d7dad21b23a35195e6d Mon Sep 17 00:00:00 2001 From: IllusionMan1212 Date: Sat, 19 Feb 2022 16:24:39 +0200 Subject: [PATCH] feat: message timestamps * made it easier to styles and prefix messages * added timestamps to messages * added prefix and styles to server messages * append current date to host channel * fixed some styles issues --- cmds/cmds.go | 14 +++- irc/client.go | 65 ++++++++++++++-- irc/handler/handler.go | 145 ++++++++++++++++++++++++++++-------- irc/parser/parser.go | 12 ++- main.go | 2 +- ui/app/app.go | 4 +- ui/login/login.go | 4 +- ui/login/styles.go | 2 - ui/mainscreen/mainscreen.go | 6 +- ui/mainscreen/styles.go | 6 +- ui/styles.go | 13 +++- 11 files changed, 222 insertions(+), 51 deletions(-) diff --git a/cmds/cmds.go b/cmds/cmds.go index 3972960..e3fe67c 100644 --- a/cmds/cmds.go +++ b/cmds/cmds.go @@ -17,6 +17,9 @@ package cmds import ( + "fmt" + "time" + tea "github.com/charmbracelet/bubbletea" "github.com/illusionman1212/gorc/irc" ) @@ -38,12 +41,19 @@ func Quit(client *irc.Client) tea.Cmd { } type SendPrivMsgMsg struct { - Msg string + Msg string + Timestamp string } func SendPrivMsg(msg string) tea.Cmd { return func() tea.Msg { - return SendPrivMsgMsg{Msg: msg} + now := time.Now() + timestamp := fmt.Sprintf("[%02d:%02d]", now.Hour(), now.Minute()) + + return SendPrivMsgMsg{ + Msg: msg, + Timestamp: timestamp, + } } } diff --git a/irc/client.go b/irc/client.go index a03db38..1e7b875 100644 --- a/irc/client.go +++ b/irc/client.go @@ -22,8 +22,12 @@ import ( "log" "net" "strings" + "time" tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/illusionman1212/gorc/irc/commands" + "github.com/illusionman1212/gorc/ui" ) type User struct { @@ -80,8 +84,44 @@ type Client struct { LastTabIndexInTabBar int } +type MsgFmtOpts struct { + WithTimestamp bool + AsServerMsg bool + NotImpl bool + AsDate bool +} + +var notImpl = lipgloss.NewStyle().Foreground(ui.ErrorColor).Render("[NOT IMPL]") +var serverMsgStyle = lipgloss.NewStyle().Foreground(ui.ServerMsgColor) +var timestampStyle = serverMsgStyle.Copy() +var dateStyle = lipgloss.NewStyle().Foreground(ui.DateColor) + const CRLF = "\r\n" +func (c *Channel) AppendMsg(timestamp string, fullMsg string, opts MsgFmtOpts) { + prefixes := "" + style := ui.DefaultStyle + + if opts.WithTimestamp { + prefixes += timestampStyle.Render(timestamp) + " " + } + + if opts.NotImpl { + prefixes += notImpl + " " + } + + if opts.AsServerMsg { + prefixes += serverMsgStyle.Render("==") + " " + style = serverMsgStyle + } + + if opts.AsDate { + style = dateStyle + } + + c.History += prefixes + style.Render(fullMsg) + CRLF +} + func (s *Client) Initialize(host string, port string, tlsEnabled bool) { addr := fmt.Sprintf("%s:%s", host, port) @@ -124,22 +164,35 @@ func (c *Client) Register(nick string, password string, channel string) { Users: make(map[string]User), }) - c.SendCommand("CAP", "LS") + c.SendCommand(commands.CAP, "LS") if password != "" { - c.SendCommand("PASS", password) + c.SendCommand(commands.PASS, password) } // TODO: check if nickname has spaces and remove them - c.SendCommand("NICK", nick) + c.SendCommand(commands.NICK, nick) + // set user-wanted nickname c.Nickname = nick - c.SendCommand("USER", nick, "0", "*", nick) + c.SendCommand(commands.USER, nick, "0", "*", nick) // TODO: CAP REQ :whatever capability the client recognizes and supports - c.SendCommand("CAP", "END") + c.SendCommand(commands.CAP, "END") // joining a channel when registering is optional if channel != "" { - c.SendCommand("JOIN", channel) + c.SendCommand(commands.JOIN, channel) } } +func (c *Client) SetDay() { + msgOpts := MsgFmtOpts{ + AsDate: true, + } + + now := time.Now() + msg := fmt.Sprintf("————— %s %d —————", now.Month().String(), now.Day()) + c.Channels[0].AppendMsg("", msg, msgOpts) + + // TODO: append new day to each channel whenever the day changes +} + func (c Client) SendCommand(cmd string, params ...string) { if c.TcpConn == nil { // TODO: properly handle the error instead of Fatal-ing diff --git a/irc/handler/handler.go b/irc/handler/handler.go index 1bcab4c..f8dedce 100644 --- a/irc/handler/handler.go +++ b/irc/handler/handler.go @@ -23,16 +23,12 @@ import ( "log" "strings" - "github.com/charmbracelet/lipgloss" "github.com/illusionman1212/gorc/cmds" "github.com/illusionman1212/gorc/irc" "github.com/illusionman1212/gorc/irc/commands" "github.com/illusionman1212/gorc/irc/parser" - "github.com/illusionman1212/gorc/ui" ) -var notImpl = lipgloss.NewStyle().Foreground(ui.Red).Render("[NOT IMPL]") - func ReadLoop(client *irc.Client) { // 512 bytes as a base + 8192 additional bytes for tags r := bufio.NewReaderSize(client.TcpConn, 8192+512) @@ -65,9 +61,13 @@ func handlePrivMsg(msg parser.IRCMessage, client *irc.Client) { msgContent := msg.Parameters[1] privMsg := fmt.Sprintf("%s: %s", nick, msgContent) + msgOpts := irc.MsgFmtOpts{ + WithTimestamp: true, + } + for i, c := range client.Channels { if c.Name == channel { - client.Channels[i].History += privMsg + irc.CRLF + client.Channels[i].AppendMsg(msg.Timestamp, privMsg, msgOpts) } } } @@ -76,7 +76,12 @@ func handleJoin(msg parser.IRCMessage, client *irc.Client) { nick := strings.SplitN(msg.Source, "!", 2)[0] channel := msg.Parameters[0] - joinMsg := fmt.Sprintf("== %s has joined", nick) + joinMsg := fmt.Sprintf("%s has joined", nick) + + msgOpts := irc.MsgFmtOpts{ + WithTimestamp: true, + AsServerMsg: true, + } if nick == client.Nickname { client.Channels = append(client.Channels, irc.Channel{ @@ -85,7 +90,7 @@ func handleJoin(msg parser.IRCMessage, client *irc.Client) { }) client.ActiveChannelIndex = len(client.Channels) - 1 client.ActiveChannel = channel - client.Channels[client.ActiveChannelIndex].History += joinMsg + irc.CRLF + client.Channels[client.ActiveChannelIndex].AppendMsg(msg.Timestamp, joinMsg, msgOpts) if _, exists := client.Channels[client.ActiveChannelIndex].Users[nick]; !exists { client.Channels[client.ActiveChannelIndex].Users[nick] = irc.User{} } @@ -93,7 +98,7 @@ func handleJoin(msg parser.IRCMessage, client *irc.Client) { } else { for i, c := range client.Channels { if c.Name == channel { - client.Channels[i].History += joinMsg + irc.CRLF + client.Channels[i].AppendMsg(msg.Timestamp, joinMsg, msgOpts) if _, exists := client.Channels[i].Users[nick]; !exists { client.Channels[i].Users[nick] = irc.User{} } @@ -109,14 +114,19 @@ func handleJoin(msg parser.IRCMessage, client *irc.Client) { func handleQuit(msg parser.IRCMessage, client *irc.Client) { nick := strings.SplitN(msg.Source, "!", 2)[0] reason := msg.Parameters[0] - quitMsg := fmt.Sprintf("== %s has quit (%s)", nick, reason) + quitMsg := fmt.Sprintf("%s has quit (%s)", nick, reason) + + msgOpts := irc.MsgFmtOpts{ + WithTimestamp: true, + AsServerMsg: true, + } for i := range client.Channels { // skip the server "channel" if i == 0 { continue } - client.Channels[i].History += quitMsg + irc.CRLF + client.Channels[i].AppendMsg(msg.Timestamp, quitMsg, msgOpts) delete(client.Channels[i].Users, nick) } @@ -131,7 +141,12 @@ func handlePart(msg parser.IRCMessage, client *irc.Client) { reason = msg.Parameters[1] } - partMsg := fmt.Sprintf("== %s has left %s (%s)", nick, channel, reason) + partMsg := fmt.Sprintf("%s has left %s (%s)", nick, channel, reason) + + msgOpts := irc.MsgFmtOpts{ + WithTimestamp: true, + AsServerMsg: true, + } for i, c := range client.Channels { if c.Name == channel { @@ -143,7 +158,7 @@ func handlePart(msg parser.IRCMessage, client *irc.Client) { } client.LastTabIndexInTabBar-- } else { - client.Channels[i].History += partMsg + irc.CRLF + client.Channels[i].AppendMsg(msg.Timestamp, partMsg, msgOpts) delete(client.Channels[i].Users, nick) } } @@ -153,69 +168,127 @@ func handlePart(msg parser.IRCMessage, client *irc.Client) { } func handleWELCOME(msg parser.IRCMessage, client *irc.Client) { + nick := msg.Parameters[0] welcomeMsg := msg.Parameters[1] - client.Channels[0].History += welcomeMsg + irc.CRLF + msgOpts := irc.MsgFmtOpts{ + WithTimestamp: true, + AsServerMsg: true, + } + + // set server-registered nickname + client.Nickname = nick + client.Channels[0].AppendMsg(msg.Timestamp, welcomeMsg, msgOpts) } func handleYOURHOST(msg parser.IRCMessage, client *irc.Client) { host := msg.Parameters[1] - client.Channels[0].History += host + irc.CRLF + msgOpts := irc.MsgFmtOpts{ + WithTimestamp: true, + AsServerMsg: true, + } + + client.Channels[0].AppendMsg(msg.Timestamp, host, msgOpts) } func handleCREATED(msg parser.IRCMessage, client *irc.Client) { created := msg.Parameters[1] - client.Channels[0].History += created + irc.CRLF + msgOpts := irc.MsgFmtOpts{ + WithTimestamp: true, + AsServerMsg: true, + } + + client.Channels[0].AppendMsg(msg.Timestamp, created, msgOpts) } func handleMYINFO(msg parser.IRCMessage, client *irc.Client) { info := strings.Join(msg.Parameters[1:], " ") - client.Channels[0].History += info + irc.CRLF + msgOpts := irc.MsgFmtOpts{ + WithTimestamp: true, + AsServerMsg: true, + } + + client.Channels[0].AppendMsg(msg.Timestamp, info, msgOpts) } func handleLUSERCLIENT(msg parser.IRCMessage, client *irc.Client) { message := msg.Parameters[1] - client.Channels[0].History += message + irc.CRLF + msgOpts := irc.MsgFmtOpts{ + WithTimestamp: true, + AsServerMsg: true, + } + + client.Channels[0].AppendMsg(msg.Timestamp, message, msgOpts) } func handleLUSEROP(msg parser.IRCMessage, client *irc.Client) { message := strings.Join(msg.Parameters[1:], " ") - client.Channels[0].History += message + irc.CRLF + msgOpts := irc.MsgFmtOpts{ + WithTimestamp: true, + AsServerMsg: true, + } + + client.Channels[0].AppendMsg(msg.Timestamp, message, msgOpts) } func handleLUSERUNKNOWN(msg parser.IRCMessage, client *irc.Client) { message := strings.Join(msg.Parameters[1:], " ") - client.Channels[0].History += message + irc.CRLF + msgOpts := irc.MsgFmtOpts{ + WithTimestamp: true, + AsServerMsg: true, + } + + client.Channels[0].AppendMsg(msg.Timestamp, message, msgOpts) } func handleLUSERCHANNELS(msg parser.IRCMessage, client *irc.Client) { message := strings.Join(msg.Parameters[1:], " ") - client.Channels[0].History += message + irc.CRLF + msgOpts := irc.MsgFmtOpts{ + WithTimestamp: true, + AsServerMsg: true, + } + + client.Channels[0].AppendMsg(msg.Timestamp, message, msgOpts) } func handleLUSERME(msg parser.IRCMessage, client *irc.Client) { message := msg.Parameters[1] - client.Channels[0].History += message + irc.CRLF + msgOpts := irc.MsgFmtOpts{ + WithTimestamp: true, + AsServerMsg: true, + } + + client.Channels[0].AppendMsg(msg.Timestamp, message, msgOpts) } func handleLOCALUSERS(msg parser.IRCMessage, client *irc.Client) { message := msg.Parameters[1] - client.Channels[0].History += message + irc.CRLF + msgOpts := irc.MsgFmtOpts{ + WithTimestamp: true, + AsServerMsg: true, + } + + client.Channels[0].AppendMsg(msg.Timestamp, message, msgOpts) } func handleGLOBALUSERS(msg parser.IRCMessage, client *irc.Client) { message := msg.Parameters[1] - client.Channels[0].History += message + irc.CRLF + msgOpts := irc.MsgFmtOpts{ + WithTimestamp: true, + AsServerMsg: true, + } + + client.Channels[0].AppendMsg(msg.Timestamp, message, msgOpts) } func handleNAMREPLY(msg parser.IRCMessage, client *irc.Client) { @@ -249,15 +322,25 @@ func handleNAMREPLY(msg parser.IRCMessage, client *irc.Client) { } func handleMOTDStart(msg parser.IRCMessage, client *irc.Client) { - params := strings.Join(msg.Parameters[1:], " ") + message := strings.Join(msg.Parameters[1:], " ") - client.Channels[0].History += params + irc.CRLF + msgOpts := irc.MsgFmtOpts{ + WithTimestamp: true, + AsServerMsg: true, + } + + client.Channels[0].AppendMsg(msg.Timestamp, message, msgOpts) } func handleMOTD(msg parser.IRCMessage, client *irc.Client) { messageLine := msg.Parameters[1] - client.Channels[0].History += messageLine + irc.CRLF + msgOpts := irc.MsgFmtOpts{ + WithTimestamp: true, + AsServerMsg: true, + } + + client.Channels[0].AppendMsg(msg.Timestamp, messageLine, msgOpts) } func HandleCommand(msg parser.IRCMessage, client *irc.Client) { @@ -315,15 +398,19 @@ func HandleCommand(msg parser.IRCMessage, client *irc.Client) { // and log an error if timeout ends without receiving this command. default: fullMsg := fmt.Sprintf( - "%s %s %s %s %s", - notImpl, + "%s %s %s %s", msg.Tags, msg.Source, msg.Command, strings.Join(msg.Parameters, " "), ) - client.Channels[0].History += fullMsg + irc.CRLF + msgOpts := irc.MsgFmtOpts{ + WithTimestamp: true, + NotImpl: true, + } + + client.Channels[0].AppendMsg(msg.Timestamp, fullMsg, msgOpts) } // send a receivedIRCmsg tea message so the ui can update diff --git a/irc/parser/parser.go b/irc/parser/parser.go index 642eb65..c2f8fa0 100644 --- a/irc/parser/parser.go +++ b/irc/parser/parser.go @@ -17,12 +17,14 @@ package parser import ( + "fmt" "regexp" "strings" + "time" ) type IRCMessage struct { - // TODO: prefix: that will hold the timestamp AND the server message prefix (==) + Timestamp string Tags Tags // starts with @ | Optional Source string // starts with : | Optional Command string // can either be a string or a numeric value | Required @@ -31,6 +33,12 @@ type IRCMessage struct { type Tags map[string]string +func (m *IRCMessage) setTimestamp() { + // TODO: get timestamp from time tag + now := time.Now() + m.Timestamp = fmt.Sprintf("[%02d:%02d]", now.Hour(), now.Minute()) +} + func parseTags(rawTags string) Tags { tags := make(Tags, 0) rawTagsSlice := strings.Split(rawTags, ";") @@ -108,5 +116,7 @@ func ParseIRCMessage(line string) (IRCMessage, bool) { ircMessage.Parameters = finalParams } + ircMessage.setTimestamp() + return ircMessage, true } diff --git a/main.go b/main.go index 291cc91..154981b 100644 --- a/main.go +++ b/main.go @@ -44,6 +44,6 @@ func main() { err = p.Start() if err != nil { - log.Println(err) + log.Fatal(err) } } diff --git a/ui/app/app.go b/ui/app/app.go index 6690140..19be1ea 100644 --- a/ui/app/app.go +++ b/ui/app/app.go @@ -92,11 +92,9 @@ func (s State) Update(msg tea.Msg) (tea.Model, tea.Cmd) { s.UI.CurrentScreen = MainScreen - // create new client with the provided host and port s.Client.Initialize(host, port, tlsEnabled) s.Client.Register(nickname, password, channel) - - s.Client.LastTabIndexInTabBar = len(s.Client.Channels) - 1 + s.Client.SetDay() go handler.ReadLoop(s.Client) diff --git a/ui/login/login.go b/ui/login/login.go index d85d767..925da19 100644 --- a/ui/login/login.go +++ b/ui/login/login.go @@ -123,8 +123,8 @@ func (s State) Update(msg tea.Msg) (State, tea.Cmd) { } s.Inputs[i].Blur() - s.Inputs[i].PromptStyle = NoStyle - s.Inputs[i].TextStyle = NoStyle + s.Inputs[i].PromptStyle = ui.DefaultStyle + s.Inputs[i].TextStyle = ui.DefaultStyle } return s, tea.Batch(cmds...) diff --git a/ui/login/styles.go b/ui/login/styles.go index 266a7dc..de886ba 100644 --- a/ui/login/styles.go +++ b/ui/login/styles.go @@ -69,7 +69,6 @@ var ( Padding(0, 2). Align(lipgloss.Center). Render("Connect") - FocusedCheckbox = lipgloss.NewStyle(). Foreground(ui.AccentColor). MarginTop(1). @@ -101,5 +100,4 @@ var ( Foreground(ui.AccentColor) FocusedStyle = lipgloss.NewStyle(). Foreground(ui.AccentColor) - NoStyle = lipgloss.NewStyle() ) diff --git a/ui/mainscreen/mainscreen.go b/ui/mainscreen/mainscreen.go index 4d73a57..3eab836 100644 --- a/ui/mainscreen/mainscreen.go +++ b/ui/mainscreen/mainscreen.go @@ -90,7 +90,11 @@ func (s State) Update(msg tea.Msg) (State, tea.Cmd) { } else { if s.Client.ActiveChannel != s.Client.Host { fullMsg := s.Client.Nickname + ": " + msg.Msg - s.Client.Channels[s.Client.ActiveChannelIndex].History += fullMsg + irc.CRLF + msgOpts := irc.MsgFmtOpts{ + WithTimestamp: true, + } + + s.Client.Channels[s.Client.ActiveChannelIndex].AppendMsg(msg.Timestamp, fullMsg, msgOpts) // TODO: make sure to only append the message to the history if server sends back no errors s.Client.SendCommand(commands.PRIVMSG, s.Client.ActiveChannel, msg.Msg) s.Viewport.SetContent(s.Client.Channels[s.Client.ActiveChannelIndex].History) diff --git a/ui/mainscreen/styles.go b/ui/mainscreen/styles.go index a55993c..e85504d 100644 --- a/ui/mainscreen/styles.go +++ b/ui/mainscreen/styles.go @@ -22,16 +22,16 @@ import ( ) var ( - InputboxStyle = lipgloss.NewStyle(). + InputboxStyle = ui.DefaultStyle.Copy(). Border(lipgloss.NormalBorder(), true). Padding(0, 1). Width(ui.MainStyle.GetWidth()). BorderForeground(ui.AccentColor) - MessagesStyle = lipgloss.NewStyle(). + MessagesStyle = ui.DefaultStyle.Copy(). Border(lipgloss.NormalBorder(), false, true, true, true). Width(ui.MainStyle.GetWidth() * 8 / 10). BorderForeground(ui.PrimaryColor) - SidePanelStyle = lipgloss.NewStyle(). + SidePanelStyle = ui.DefaultStyle.Copy(). Border(lipgloss.NormalBorder(), true). Width(ui.MainStyle.GetWidth() * 2 / 10). BorderForeground(ui.PrimaryColor) diff --git a/ui/styles.go b/ui/styles.go index 8588fc6..2934c02 100644 --- a/ui/styles.go +++ b/ui/styles.go @@ -23,6 +23,9 @@ var ( Width(0). Height(0) + DefaultStyle = lipgloss.NewStyle(). + Foreground(PrimaryColor) + AccentColor = lipgloss.AdaptiveColor{ Dark: "#1C6DD0", Light: "#1C6DD0", @@ -43,5 +46,13 @@ var ( Light: "#CCC", } - Red = lipgloss.Color("#F00") + ErrorColor = lipgloss.Color("#F00") + DateColor = lipgloss.AdaptiveColor{ + Dark: "#0F0", + Light: "#090", + } + ServerMsgColor = lipgloss.AdaptiveColor{ + Dark: "#999", + Light: "#777", + } )