From 9337b9c4627353d3cf7791699b52ffb0e6c7cf94 Mon Sep 17 00:00:00 2001 From: IllusionMan1212 Date: Sun, 14 Nov 2021 06:19:46 +0200 Subject: [PATCH] restructure app state, basic server connection using inputted user info and basic mainscreen --- client/client.go | 78 ++++++++++++++++++++++++--------- client/structs.go | 29 ------------ main.go | 18 -------- {client => parser}/parser.go | 24 ++++++++-- ui/app/app.go | 85 ++++++++++++++++++++++++++---------- ui/login/cmds.go | 11 +++++ ui/login/login.go | 2 +- ui/mainscreen/cmds.go | 26 +++++++++++ ui/mainscreen/mainscreen.go | 34 +++++++++++++++ 9 files changed, 210 insertions(+), 97 deletions(-) delete mode 100644 client/structs.go rename {client => parser}/parser.go (77%) create mode 100644 ui/login/cmds.go create mode 100644 ui/mainscreen/cmds.go create mode 100644 ui/mainscreen/mainscreen.go diff --git a/client/client.go b/client/client.go index 411d566..454a1bc 100644 --- a/client/client.go +++ b/client/client.go @@ -20,57 +20,93 @@ import ( "crypto/tls" "fmt" "log" + "strings" + + "github.com/illusionman1212/gorc/parser" ) type Client struct { // tcp connection - conn *tls.Conn + Conn *tls.Conn // channel to receive data that comes through from the connection - receive chan string + Receive chan string } -func NewClient(host string, port string) *Client { - // TODO: allow for insecure connections thru a checkbox (needed for irc servers that maybe don't have encryption on) +const CRLF = "\r\n" + +func NewClient(host string, port string, tlsEnabled bool) *Client { // TODO: distinguish between secure and insecure connections - cfg := &tls.Config{ServerName: host} addr := fmt.Sprintf("%s:%s", host, port) + + cfg := &tls.Config{ServerName: host} conn, err := tls.Dial("tcp", addr, cfg) if err != nil { // TODO: properly handle the error instead of Fatal-ing (failed to initiate a connection to server) log.Fatal(err) } - // TODO: read nickname and channel from user input (stdin ?? or TUI) - conn.Write([]byte("CAP LS\r\n")) - // conn.Write([]byte("PASS password")) - conn.Write([]byte("NICK illusion434\r\n")) - conn.Write([]byte("USER illusion434 0 * :illusion434\r\n")) - // TODO: CAP REQ :whatever capability the client recognizes and supports - conn.Write([]byte("CAP END\r\n")) - conn.Write([]byte("JOIN ##programming\r\n")) - return &Client{ - conn: conn, - receive: make(chan string), + Conn: conn, + Receive: make(chan string), } } -func (c Client) handleCommand(message IRCMessage) { +func (c Client) Register(nick string, password string, channel string) { + if c.Conn == nil { + // TODO: properly handle the error instead of Fatal-ing + log.Fatal("Attempted to write data to nil connection") + } + + c.sendCommand("CAP", "LS") + if password != "" { + c.sendCommand("PASS", password) + } + // TODO: check if nickname has spaces and remove them + c.sendCommand("NICK", nick) + c.sendCommand("USER", nick, "0", "*", nick) + // TODO: CAP REQ :whatever capability the client recognizes and supports + c.sendCommand("CAP", "END") + c.sendCommand("JOIN", channel) +} + +func (c Client) handleCommand(message parser.IRCMessage) { + fmt.Printf("Tags: %s\n", message.Tags) + fmt.Printf("Source: %s\n", message.Source) + fmt.Printf("Command: %s\n", message.Command) + fmt.Printf("Params: %s\n", message.Parameters) + switch message.Command { case "PING": - c.conn.Write([]byte("PONG\r\n")) + c.sendCommand("PONG") break } // TODO: handle all other commands } +func (c Client) sendCommand(cmd string, params ...string) { + paramsString := "" + if len(params) > 0 { + paramsString = " " + strings.Join(params, " ") + } + + // if we have more than 1 param then replace the last param's space with a " :" + // NOTE: not sure if we should only check if we have more than 1 param + // cuz one param could have spaces and would require a colon + if len(params) > 1 { + i := strings.LastIndex(paramsString, " ") + paramsString = paramsString[:i] + strings.Replace(paramsString[i:], " ", " :", 1) + } + + c.Conn.Write([]byte(cmd + paramsString + CRLF)) +} + func (c Client) Run() { for { select { - case line := <-c.receive: - messages := parseIRCMessage(line) - c.handleCommand(messages) + case line := <-c.Receive: + message := parser.ParseIRCMessage(line) + c.handleCommand(message) } } } diff --git a/client/structs.go b/client/structs.go deleted file mode 100644 index c93fcc0..0000000 --- a/client/structs.go +++ /dev/null @@ -1,29 +0,0 @@ -// gorc project -// Copyright (C) 2021 IllusionMan1212 and contributors -// -// This program is free software; you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation; either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License along -// with this program; if not, see https://www.gnu.org/licenses. - -package client - -type IRCMessage struct { - Tags []Tag // starts with @ | Optional - Source string // starts with : | Optional - Command string // can either be a string or a numeric value | Required - Parameters []string // Optional (Dependant on command) -} - -type Tag struct { - key string - value string -} diff --git a/main.go b/main.go index bfe26fe..e8fd363 100644 --- a/main.go +++ b/main.go @@ -27,22 +27,4 @@ func main() { if err := tea.NewProgram(app.InitialState(), tea.WithAltScreen()).Start(); err != nil { log.Fatal(err) } - - /* - // create new client with the provided host and port - client := NewClient("irc.libera.chat", "6697") - defer client.conn.Close() - go client.Run() - - // main loop - r := bufio.NewReaderSize(client.conn, 512) - for { - msg, err := r.ReadString('\n') - if err != nil { - log.Print(err) - } - - client.receive <- msg - } - */ } diff --git a/client/parser.go b/parser/parser.go similarity index 77% rename from client/parser.go rename to parser/parser.go index 5683672..bb93930 100644 --- a/client/parser.go +++ b/parser/parser.go @@ -14,20 +14,36 @@ // You should have received a copy of the GNU General Public License along // with this program; if not, see https://www.gnu.org/licenses. -package client +package parser import ( "strings" ) +type IRCMessage struct { + Tags []Tag // starts with @ | Optional + Source string // starts with : | Optional + Command string // can either be a string or a numeric value | Required + Parameters []string // Optional (Dependant on command) +} + +type Tag struct { + key string + value string +} + func parseTags(rawTags string) []Tag { // TODO: // TOOD: some tags don't have an "=" and they're basically considered "true" return nil } -func parseIRCMessage(line string) IRCMessage { - ircMessage := &IRCMessage{} +func ParseIRCMessage(line string) IRCMessage { + ircMessage := IRCMessage{} + + if len(line) <= 0 { + return IRCMessage{} + } if line[0] == '@' { substrs := strings.SplitN(line, " ", 2) @@ -63,5 +79,5 @@ func parseIRCMessage(line string) IRCMessage { ircMessage.Parameters = parameters } - return *ircMessage + return ircMessage } diff --git a/ui/app/app.go b/ui/app/app.go index 0d436c5..727d9db 100644 --- a/ui/app/app.go +++ b/ui/app/app.go @@ -17,12 +17,16 @@ package app import ( + "bufio" "log" "os" + "github.com/charmbracelet/bubbles/textinput" tea "github.com/charmbracelet/bubbletea" + "github.com/illusionman1212/gorc/client" "github.com/illusionman1212/gorc/ui" "github.com/illusionman1212/gorc/ui/login" + "github.com/illusionman1212/gorc/ui/mainscreen" "golang.org/x/term" ) @@ -33,39 +37,44 @@ const ( MainScreen ) -// TODO: have a global State that holds a separate client State and separate tui State -type State struct { - host string - port string - - nick string - password string - +type UI struct { windowWidth int windowHeight int currentScreen Screen - - login login.State + login login.State + mainScreen mainscreen.State } -func InitialState() State { +type State struct { + ui UI + client *client.Client +} + +func initialUiState() UI { width, height, err := term.GetSize(int(os.Stdout.Fd())) if err != nil { + // TODO: properly handle this error log.Fatal(err) } + return UI{ + login: login.NewLogin(), + windowWidth: width, + windowHeight: height, + } +} + +func InitialState() State { s := State{ - login: login.NewLogin(), - windowWidth: width, - windowHeight: height, - currentScreen: Login, + client: &client.Client{}, + ui: initialUiState(), } return s } func (s State) Init() tea.Cmd { - return nil + return textinput.Blink } func (s State) Update(msg tea.Msg) (tea.Model, tea.Cmd) { @@ -74,26 +83,52 @@ func (s State) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg.String() { case "ctrl+c": // TODO: clean up and gracefully quit by leaving the IRC channel/server and then quit bubble. + if s.client.Conn != nil { + s.client.Conn.Close() + } return s, tea.Quit } case tea.WindowSizeMsg: - s.windowWidth = msg.Width - s.windowHeight = msg.Height + s.ui.windowWidth = msg.Width + s.ui.windowHeight = msg.Height ui.MainStyle = ui.MainStyle. Width(msg.Width). Height(msg.Height) return s, nil + case login.ConnectingMsg: + host := s.ui.login.Inputs[0].Value() + port := s.ui.login.Inputs[1].Value() + channel := s.ui.login.Inputs[2].Value() + nickname := s.ui.login.Inputs[3].Value() + password := s.ui.login.Inputs[4].Value() + tlsEnabled := s.ui.login.TLS + + s.ui.currentScreen = MainScreen + + // create new client with the provided host and port + client := client.NewClient(host, port, tlsEnabled) + client.Register(nickname, password, channel) + s.client = client + + r := bufio.NewReaderSize(client.Conn, 512) + s.ui.mainScreen.Reader = r + + return s, mainscreen.InitialRead } // switch between which screen is currently active and update its state - switch s.currentScreen { + switch s.ui.currentScreen { case Login: - loginState, loginCmd := s.login.Update(msg) - s.login = loginState - return s, loginCmd + state, cmd := s.ui.login.Update(msg) + s.ui.login = state + return s, cmd + case MainScreen: + state, cmd := s.ui.mainScreen.Update(msg) + s.ui.mainScreen = state + return s, cmd } return s, nil @@ -101,9 +136,11 @@ func (s State) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (s State) View() string { // switch between which screen is currently active and render it. - switch s.currentScreen { + switch s.ui.currentScreen { case Login: - return s.login.View() + return s.ui.login.View() + case MainScreen: + return s.ui.mainScreen.View() } return "Error: this screen shouldn't ever show" diff --git a/ui/login/cmds.go b/ui/login/cmds.go new file mode 100644 index 0000000..7ed5d3f --- /dev/null +++ b/ui/login/cmds.go @@ -0,0 +1,11 @@ +package login + +import ( + tea "github.com/charmbracelet/bubbletea" +) + +type ConnectingMsg struct{} + +func connect() tea.Msg { + return ConnectingMsg{} +} diff --git a/ui/login/login.go b/ui/login/login.go index 53c36c6..d30882e 100644 --- a/ui/login/login.go +++ b/ui/login/login.go @@ -79,7 +79,7 @@ func (s State) Update(msg tea.Msg) (State, tea.Cmd) { // run the newClient cmd when pressing "enter" while focused on the connect button if str == "enter" && s.FocusIndex == len(s.Inputs)+1 { - return s, tea.Quit // TODO: change this to create a newClient and connect using a tea.Cmd + return s, connect } // toggle tls state when pressing "enter" while focused on the checkbox diff --git a/ui/mainscreen/cmds.go b/ui/mainscreen/cmds.go new file mode 100644 index 0000000..7910d63 --- /dev/null +++ b/ui/mainscreen/cmds.go @@ -0,0 +1,26 @@ +package mainscreen + +import ( + "log" + + tea "github.com/charmbracelet/bubbletea" +) + +type ReceivedIRCCommandMsg struct { + Msg string +} + +type InitialReadMsg struct{} + +func InitialRead() tea.Msg { + return InitialReadMsg{} +} + +func (s State) readFromServer() tea.Msg { + msg, err := s.Reader.ReadString('\n') + if err != nil { + log.Print(err) + } + + return ReceivedIRCCommandMsg{Msg: msg} +} diff --git a/ui/mainscreen/mainscreen.go b/ui/mainscreen/mainscreen.go new file mode 100644 index 0000000..609249c --- /dev/null +++ b/ui/mainscreen/mainscreen.go @@ -0,0 +1,34 @@ +package mainscreen + +import ( + "bufio" + "fmt" + "strings" + + tea "github.com/charmbracelet/bubbletea" + "github.com/illusionman1212/gorc/parser" +) + +type State struct { + content string + Reader *bufio.Reader +} + +func (s State) Update(msg tea.Msg) (State, tea.Cmd) { + switch msg := msg.(type) { + case InitialReadMsg: + return s, s.readFromServer + case ReceivedIRCCommandMsg: + message := parser.ParseIRCMessage(msg.Msg) + fullMsg := fmt.Sprintf("%s %s %s", message.Source, message.Command, strings.Join(message.Parameters, " ")) + s.content += fullMsg + + return s, s.readFromServer + } + + return s, nil +} + +func (s State) View() string { + return s.content +}