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"
"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)
}
}
}
-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 {
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
// 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
}
+58 -21
View File
@@ -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
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)
}
s := State{
return UI{
login: login.NewLogin(),
windowWidth: width,
windowHeight: height,
currentScreen: Login,
}
}
func InitialState() State {
s := State{
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"
+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
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
+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
}