restructure app state, basic server connection using inputted user info and basic mainscreen

This commit is contained in:
2021-11-14 06:19:46 +02:00
parent 4bdb4a0fb3
commit 9337b9c462
9 changed files with 210 additions and 97 deletions
+57 -21
View File
@@ -20,57 +20,93 @@ import (
"crypto/tls" "crypto/tls"
"fmt" "fmt"
"log" "log"
"strings"
"github.com/illusionman1212/gorc/parser"
) )
type Client struct { type Client struct {
// tcp connection // tcp connection
conn *tls.Conn Conn *tls.Conn
// channel to receive data that comes through from the connection // channel to receive data that comes through from the connection
receive chan string Receive chan string
} }
func NewClient(host string, port string) *Client { const CRLF = "\r\n"
// TODO: allow for insecure connections thru a checkbox (needed for irc servers that maybe don't have encryption on)
func NewClient(host string, port string, tlsEnabled bool) *Client {
// TODO: distinguish between secure and insecure connections // TODO: distinguish between secure and insecure connections
cfg := &tls.Config{ServerName: host}
addr := fmt.Sprintf("%s:%s", host, port) addr := fmt.Sprintf("%s:%s", host, port)
cfg := &tls.Config{ServerName: host}
conn, err := tls.Dial("tcp", addr, cfg) conn, err := tls.Dial("tcp", addr, cfg)
if err != nil { if err != nil {
// TODO: properly handle the error instead of Fatal-ing (failed to initiate a connection to server) // TODO: properly handle the error instead of Fatal-ing (failed to initiate a connection to server)
log.Fatal(err) 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{ return &Client{
conn: conn, Conn: conn,
receive: make(chan string), 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 { switch message.Command {
case "PING": case "PING":
c.conn.Write([]byte("PONG\r\n")) c.sendCommand("PONG")
break break
} }
// TODO: handle all other commands // 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() { func (c Client) Run() {
for { for {
select { select {
case line := <-c.receive: case line := <-c.Receive:
messages := parseIRCMessage(line) message := parser.ParseIRCMessage(line)
c.handleCommand(messages) c.handleCommand(message)
} }
} }
} }
-29
View File
@@ -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
}
-18
View File
@@ -27,22 +27,4 @@ func main() {
if err := tea.NewProgram(app.InitialState(), tea.WithAltScreen()).Start(); err != nil { if err := tea.NewProgram(app.InitialState(), tea.WithAltScreen()).Start(); err != nil {
log.Fatal(err) 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
}
*/
} }
+20 -4
View File
@@ -14,20 +14,36 @@
// You should have received a copy of the GNU General Public License along // You should have received a copy of the GNU General Public License along
// with this program; if not, see https://www.gnu.org/licenses. // with this program; if not, see https://www.gnu.org/licenses.
package client package parser
import ( import (
"strings" "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 { func parseTags(rawTags string) []Tag {
// TODO: // TODO:
// TOOD: some tags don't have an "=" and they're basically considered "true" // TOOD: some tags don't have an "=" and they're basically considered "true"
return nil return nil
} }
func parseIRCMessage(line string) IRCMessage { func ParseIRCMessage(line string) IRCMessage {
ircMessage := &IRCMessage{} ircMessage := IRCMessage{}
if len(line) <= 0 {
return IRCMessage{}
}
if line[0] == '@' { if line[0] == '@' {
substrs := strings.SplitN(line, " ", 2) substrs := strings.SplitN(line, " ", 2)
@@ -63,5 +79,5 @@ func parseIRCMessage(line string) IRCMessage {
ircMessage.Parameters = parameters ircMessage.Parameters = parameters
} }
return *ircMessage return ircMessage
} }
+61 -24
View File
@@ -17,12 +17,16 @@
package app package app
import ( import (
"bufio"
"log" "log"
"os" "os"
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea" tea "github.com/charmbracelet/bubbletea"
"github.com/illusionman1212/gorc/client"
"github.com/illusionman1212/gorc/ui" "github.com/illusionman1212/gorc/ui"
"github.com/illusionman1212/gorc/ui/login" "github.com/illusionman1212/gorc/ui/login"
"github.com/illusionman1212/gorc/ui/mainscreen"
"golang.org/x/term" "golang.org/x/term"
) )
@@ -33,39 +37,44 @@ const (
MainScreen MainScreen
) )
// TODO: have a global State that holds a separate client State and separate tui State type UI struct {
type State struct {
host string
port string
nick string
password string
windowWidth int windowWidth int
windowHeight int windowHeight int
currentScreen Screen 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())) width, height, err := term.GetSize(int(os.Stdout.Fd()))
if err != nil { if err != nil {
// TODO: properly handle this error
log.Fatal(err) log.Fatal(err)
} }
return UI{
login: login.NewLogin(),
windowWidth: width,
windowHeight: height,
}
}
func InitialState() State {
s := State{ s := State{
login: login.NewLogin(), client: &client.Client{},
windowWidth: width, ui: initialUiState(),
windowHeight: height,
currentScreen: Login,
} }
return s return s
} }
func (s State) Init() tea.Cmd { func (s State) Init() tea.Cmd {
return nil return textinput.Blink
} }
func (s State) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 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() { switch msg.String() {
case "ctrl+c": case "ctrl+c":
// TODO: clean up and gracefully quit by leaving the IRC channel/server and then quit bubble. // 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 return s, tea.Quit
} }
case tea.WindowSizeMsg: case tea.WindowSizeMsg:
s.windowWidth = msg.Width s.ui.windowWidth = msg.Width
s.windowHeight = msg.Height s.ui.windowHeight = msg.Height
ui.MainStyle = ui.MainStyle. ui.MainStyle = ui.MainStyle.
Width(msg.Width). Width(msg.Width).
Height(msg.Height) Height(msg.Height)
return s, nil 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 between which screen is currently active and update its state
switch s.currentScreen { switch s.ui.currentScreen {
case Login: case Login:
loginState, loginCmd := s.login.Update(msg) state, cmd := s.ui.login.Update(msg)
s.login = loginState s.ui.login = state
return s, loginCmd return s, cmd
case MainScreen:
state, cmd := s.ui.mainScreen.Update(msg)
s.ui.mainScreen = state
return s, cmd
} }
return s, nil return s, nil
@@ -101,9 +136,11 @@ func (s State) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
func (s State) View() string { func (s State) View() string {
// switch between which screen is currently active and render it. // switch between which screen is currently active and render it.
switch s.currentScreen { switch s.ui.currentScreen {
case Login: 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" return "Error: this screen shouldn't ever show"
+11
View File
@@ -0,0 +1,11 @@
package login
import (
tea "github.com/charmbracelet/bubbletea"
)
type ConnectingMsg struct{}
func connect() tea.Msg {
return ConnectingMsg{}
}
+1 -1
View File
@@ -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 // run the newClient cmd when pressing "enter" while focused on the connect button
if str == "enter" && s.FocusIndex == len(s.Inputs)+1 { 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 // toggle tls state when pressing "enter" while focused on the checkbox
+26
View File
@@ -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}
}
+34
View File
@@ -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
}