feat: handle more commands and properly implement older ones

* fix parser tests
* write logs to file
* gracefully terminate program
* make viewport follow output
* properly handle channel nicks and display them in the sidepanel
* general refactors and fixes
This commit is contained in:
2022-01-15 21:56:06 +02:00
parent 8a7e1568ae
commit 22d5e39899
15 changed files with 487 additions and 181 deletions
+3
View File
@@ -3,3 +3,6 @@ gorc
# vscode stuff
.vscode
# log file
gorc.log
+26 -27
View File
@@ -22,11 +22,13 @@ import (
"log"
"net"
"strings"
tea "github.com/charmbracelet/bubbletea"
)
type User struct {
// Nickname of user
Nick string
// User prefix in channel
Prefix string
}
type Channel struct {
@@ -37,27 +39,28 @@ type Channel struct {
History string
// Users in this channel
Users []User
// The map key is the user's nickname
// and the user struct holds data about that user
// such as, prefixes in this channel and etc...
Users map[string]User
}
type Client struct {
// tcp connection
TcpConn net.Conn
// Channel to receive quit msg
Quit chan struct{}
Host string
Port string
Nickname string
MOTD string // TODO:
Channels map[string]Channel // TODO:
ActiveChannel string
Tea *tea.Program
}
const CRLF = "\r\n"
func NewClient(host string, port string, tlsEnabled bool) Client {
func (s *Client) Initialize(host string, port string, tlsEnabled bool) {
addr := fmt.Sprintf("%s:%s", host, port)
if tlsEnabled {
@@ -68,14 +71,12 @@ func NewClient(host string, port string, tlsEnabled bool) Client {
log.Fatal(err)
}
return Client{
TcpConn: conn,
Host: host,
Port: port,
Quit: make(chan struct{}),
ActiveChannel: host,
Channels: make(map[string]Channel),
}
s.TcpConn = conn
s.Host = host
s.Port = port
s.ActiveChannel = host
s.Channels = make(map[string]Channel)
return
}
addrTCP, err := net.ResolveTCPAddr("tcp", addr)
@@ -88,14 +89,11 @@ func NewClient(host string, port string, tlsEnabled bool) Client {
log.Fatal(err)
}
return Client{
TcpConn: conn,
Host: host,
Port: port,
Quit: make(chan struct{}),
ActiveChannel: host,
Channels: make(map[string]Channel),
}
s.TcpConn = conn
s.Host = host
s.Port = port
s.ActiveChannel = host
s.Channels = make(map[string]Channel)
}
func (c *Client) Register(nick string, password string, channel string) {
@@ -112,11 +110,12 @@ func (c *Client) Register(nick string, password string, channel string) {
c.SendCommand("CAP", "END")
// joining a channel when registering is optional
if channel != "" {
c.SendCommand("JOIN", channel)
c.ActiveChannel = channel
c.Channels[c.ActiveChannel] = Channel{}
} else {
c.Channels[c.ActiveChannel] = Channel{}
c.SendCommand("JOIN", channel)
}
c.Channels[c.ActiveChannel] = Channel{
Users: make(map[string]User),
}
}
+7 -7
View File
@@ -106,13 +106,13 @@ const (
)
/* --- Channel membership prefixes --- */
const (
Founder = "+q"
Protected = "+a"
Operator = "@"
HalfOp = "%"
Voice = "+v"
)
var UserPrefixes = map[string]bool{
"~": true,
"&": true,
"@": true,
"%": true,
"+": true,
}
/* --- Numeric Replies --- */
const (
+6 -1
View File
@@ -5,8 +5,13 @@ go 1.16
replace github.com/charmbracelet/bubbles => /home/illusion/Desktop/repos/bubbles
require (
github.com/atotto/clipboard v0.1.4 // indirect
github.com/charmbracelet/bubbles v0.9.0
github.com/charmbracelet/bubbletea v0.19.1
github.com/charmbracelet/bubbletea v0.19.3
github.com/charmbracelet/lipgloss v0.4.0
github.com/containerd/console v1.0.3 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 // indirect
golang.org/x/sys v0.0.0-20220111092808-5a964db01320 // indirect
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
)
+13 -11
View File
@@ -1,46 +1,48 @@
github.com/atotto/clipboard v0.1.2 h1:YZCtFu5Ie8qX2VmVTBnrqLSiU9XOWwqNRmdT3gIQzbY=
github.com/atotto/clipboard v0.1.2/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/charmbracelet/bubbletea v0.14.1/go.mod h1:b5lOf5mLjMg1tRn1HVla54guZB+jvsyV0yYAQja95zE=
github.com/charmbracelet/bubbletea v0.19.1 h1:VHuzkJbnTAkxhOfi9+Lb5PYfNM9+Oh+qhP8uDX5ReOU=
github.com/charmbracelet/bubbletea v0.19.1/go.mod h1:VuXF2pToRxDUHcBUcPmCRUHRvFATM4Ckb/ql1rBl3KA=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/charmbracelet/bubbletea v0.19.3 h1:OKeO/Y13rQQqt4snX+lePB0QrnW80UdrMNolnCcmoAw=
github.com/charmbracelet/bubbletea v0.19.3/go.mod h1:VuXF2pToRxDUHcBUcPmCRUHRvFATM4Ckb/ql1rBl3KA=
github.com/charmbracelet/harmonica v0.1.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
github.com/charmbracelet/lipgloss v0.3.0/go.mod h1:VkhdBS2eNAmRkTwRKLJCFhCOVkjntMusBDxv7TXahuk=
github.com/charmbracelet/lipgloss v0.4.0 h1:768h64EFkGUr8V5yAKV7/Ta0NiVceiPaV+PphaW1K9g=
github.com/charmbracelet/lipgloss v0.4.0/go.mod h1:vmdkHvce7UzX6xkyf4cca8WlwdQ5RQr8fzta+xl7BOM=
github.com/containerd/console v1.0.1/go.mod h1:XUsP6YE/mKtz6bxc+I8UiKKTP04qjQL4qcS3XoQ5xkw=
github.com/containerd/console v1.0.2 h1:Pi6D+aZXM+oUw1czuKgH5IJ+y0jhYcwBJfx5/Ghn9dE=
github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ=
github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.13 h1:qdl+GuBjcsKKDco5BsxPJlId98mSWNKqYA+Co0SC1yA=
github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34=
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 h1:kMlmsLSbjkikxQJ1IPwaM+7LJ9ltFu/fi8CRzvSnQmA=
github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/termenv v0.8.1/go.mod h1:kzt/D/4a88RoheZmwfqorY3A+tnsSMA9HJC/fQSFKo0=
github.com/muesli/termenv v0.9.0 h1:wnbOaGz+LUR3jNT0zOzinPnyDaCZUQRZj9GxK8eRVl8=
github.com/muesli/termenv v0.9.0/go.mod h1:R/LzAKf+suGs4IsO95y7+7DpFHO0KABgnZqtlyx2mBw=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200916030750-2334cc1a136f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220111092808-5a964db01320 h1:0jf+tOCoZ3LyutmCOWpVni1chK4VfFLhRsDK7MhqGRY=
golang.org/x/sys v0.0.0-20220111092808-5a964db01320/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20210422114643-f5beecf764ed/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+18 -2
View File
@@ -24,10 +24,26 @@ import (
)
func main() {
if err := tea.NewProgram(app.InitialState(),
_app := app.InitialState()
p := tea.NewProgram(
_app,
tea.WithAltScreen(),
tea.WithMouseCellMotion(),
).Start(); err != nil {
)
_app.Client.Tea = p
f, err := tea.LogToFile("gorc.log", "gorc")
defer f.Close()
if err != nil {
log.Fatal(err)
}
err = p.Start()
if err != nil {
log.Println(err)
}
}
+175 -35
View File
@@ -23,7 +23,11 @@ import "testing"
func TestParser(t *testing.T) {
t.Run("Test simple command", func(t *testing.T) {
testMessage := "foo bar baz asdf"
ircMessage := ParseIRCMessage(testMessage)
ircMessage, value := ParseIRCMessage(testMessage)
if !value {
t.Fatal("Invalid irc message")
}
if ircMessage.Command != "foo" {
t.Fatal("Parsing message with only command and params")
@@ -44,7 +48,11 @@ func TestParser(t *testing.T) {
t.Run("Test source", func(t *testing.T) {
testMessage := ":coolguy foo bar baz asdf"
ircMessage := ParseIRCMessage(testMessage)
ircMessage, value := ParseIRCMessage(testMessage)
if !value {
t.Fatal("Invalid irc message")
}
if ircMessage.Source != "coolguy" {
t.Fatal("Parsing regular source")
@@ -69,7 +77,11 @@ func TestParser(t *testing.T) {
t.Run("Test trailing param", func(t *testing.T) {
testMessage := "foo bar baz :asdf quux"
ircMessage := ParseIRCMessage(testMessage)
ircMessage, value := ParseIRCMessage(testMessage)
if !value {
t.Fatal("Invalid irc message")
}
if ircMessage.Parameters[0] != "bar" {
t.Fatal("Parsing 1st param in message with trailing param")
@@ -86,7 +98,11 @@ func TestParser(t *testing.T) {
t.Run("Test empty trailing param", func(t *testing.T) {
testMessage := "foo bar baz :"
ircMessage := ParseIRCMessage(testMessage)
ircMessage, value := ParseIRCMessage(testMessage)
if !value {
t.Fatal("Invalid irc message")
}
if ircMessage.Parameters[0] != "bar" {
t.Fatal("Parsing 1st param in message with trailing param")
@@ -103,7 +119,11 @@ func TestParser(t *testing.T) {
t.Run("Test multiple colons at trailing param", func(t *testing.T) {
testMessage := "foo bar baz ::asdf"
ircMessage := ParseIRCMessage(testMessage)
ircMessage, value := ParseIRCMessage(testMessage)
if !value {
t.Fatal("Invalid irc message")
}
if ircMessage.Parameters[0] != "bar" {
t.Fatal("Parsing 1st param in message with trailing param")
@@ -120,7 +140,11 @@ func TestParser(t *testing.T) {
t.Run("Test source and trailing param", func(t *testing.T) {
testMessage := ":coolguy foo bar baz :asdf quux"
ircMessage := ParseIRCMessage(testMessage)
ircMessage, value := ParseIRCMessage(testMessage)
if !value {
t.Fatal("Invalid irc message")
}
if ircMessage.Source != "coolguy" {
t.Fatal("Parsing source")
@@ -145,7 +169,11 @@ func TestParser(t *testing.T) {
t.Run("Test source and trailing param with spaces", func(t *testing.T) {
testMessage := ":coolguy foo bar baz : asdf quux "
ircMessage := ParseIRCMessage(testMessage)
ircMessage, value := ParseIRCMessage(testMessage)
if !value {
t.Fatal("Invalid irc message")
}
if ircMessage.Source != "coolguy" {
t.Fatal("Parsing source")
@@ -170,7 +198,11 @@ func TestParser(t *testing.T) {
t.Run("Test source and trailing param 2", func(t *testing.T) {
testMessage := ":coolguy PRIVMSG bar :lol :) "
ircMessage := ParseIRCMessage(testMessage)
ircMessage, value := ParseIRCMessage(testMessage)
if !value {
t.Fatal("Invalid irc message")
}
if ircMessage.Source != "coolguy" {
t.Fatal("Parsing source")
@@ -191,7 +223,11 @@ func TestParser(t *testing.T) {
t.Run("Test source and empty trailing param", func(t *testing.T) {
testMessage := ":coolguy foo bar baz :"
ircMessage := ParseIRCMessage(testMessage)
ircMessage, value := ParseIRCMessage(testMessage)
if !value {
t.Fatal("Invalid irc message")
}
if ircMessage.Source != "coolguy" {
t.Fatal("Parsing source")
@@ -216,7 +252,11 @@ func TestParser(t *testing.T) {
t.Run("Test source and trailing param with spaces 2", func(t *testing.T) {
testMessage := ":coolguy foo bar baz : "
ircMessage := ParseIRCMessage(testMessage)
ircMessage, value := ParseIRCMessage(testMessage)
if !value {
t.Fatal("Invalid irc message")
}
if ircMessage.Source != "coolguy" {
t.Fatal("Parsing source")
@@ -241,7 +281,11 @@ func TestParser(t *testing.T) {
t.Run("Test tags", func(t *testing.T) {
testMessage := "@id=234AB;rose :dan!d@localhost PRIVMSG #chan :Hey what's up!"
ircMessage := ParseIRCMessage(testMessage)
ircMessage, value := ParseIRCMessage(testMessage)
if !value {
t.Fatal("Invalid irc message")
}
if ircMessage.Tags["id"] != "234AB" {
t.Fatal("Parsing regular tag")
@@ -270,7 +314,11 @@ func TestParser(t *testing.T) {
t.Run("Test tags 2", func(t *testing.T) {
testMessage := "@a=b;c=32;k;rt=q17 foo"
ircMessage := ParseIRCMessage(testMessage)
ircMessage, value := ParseIRCMessage(testMessage)
if !value {
t.Fatal("Invalid irc message")
}
if ircMessage.Tags["a"] != "b" {
t.Fatal("Parsing regular tag")
@@ -296,7 +344,11 @@ func TestParser(t *testing.T) {
// TODO: fails
t.Run("Test escaped tags", func(t *testing.T) {
testMessage := "@a=b\\\\and\\nk;c=72\\s45;d=gh\\:764 foo"
ircMessage := ParseIRCMessage(testMessage)
ircMessage, value := ParseIRCMessage(testMessage)
if !value {
t.Fatal("Invalid irc message")
}
if ircMessage.Tags["a"] != "b\\and\nk" {
t.Fatal("Parsing escaped tag")
@@ -317,7 +369,11 @@ func TestParser(t *testing.T) {
t.Run("Test tags with source", func(t *testing.T) {
testMessage := "@c;h=;a=b :quux ab cd"
ircMessage := ParseIRCMessage(testMessage)
ircMessage, value := ParseIRCMessage(testMessage)
if !value {
t.Fatal("Invalid irc message")
}
if ircMessage.Tags["c"] != "" {
t.Log(ircMessage.Tags)
@@ -348,7 +404,11 @@ func TestParser(t *testing.T) {
// TODO: FAIL
t.Run("Test single param with preceeding :", func(t *testing.T) {
testMessage := ":src JOIN :#chan"
ircMessage := ParseIRCMessage(testMessage)
ircMessage, value := ParseIRCMessage(testMessage)
if !value {
t.Fatal("Invalid irc message")
}
if ircMessage.Parameters[0] != "#chan" {
t.Fatal("Parsing a single param with a preceeding \":\"")
@@ -357,7 +417,11 @@ func TestParser(t *testing.T) {
t.Run("Test with no space as last param", func(t *testing.T) {
testMessage := ":src AWAY"
ircMessage := ParseIRCMessage(testMessage)
ircMessage, value := ParseIRCMessage(testMessage)
if !value {
t.Fatal("Invalid irc message")
}
if ircMessage.Source != "src" {
t.Fatal("Parsing source")
@@ -374,7 +438,11 @@ func TestParser(t *testing.T) {
t.Run("Test with space as last param", func(t *testing.T) {
testMessage := ":src AWAY "
ircMessage := ParseIRCMessage(testMessage)
ircMessage, value := ParseIRCMessage(testMessage)
if !value {
t.Fatal("Invalid irc message")
}
if ircMessage.Source != "src" {
t.Fatal("Parsing source")
@@ -392,7 +460,11 @@ func TestParser(t *testing.T) {
t.Run("Test tab as space", func(t *testing.T) {
testMessage := ":cool\tguy foo bar baz"
ircMessage := ParseIRCMessage(testMessage)
ircMessage, value := ParseIRCMessage(testMessage)
if !value {
t.Fatal("Invalid irc message")
}
if ircMessage.Source != "cool\tguy" {
t.Fatal("Parsing source")
@@ -413,7 +485,11 @@ func TestParser(t *testing.T) {
t.Run("Test weird control codes in source", func(t *testing.T) {
testMessage := ":coolguy!ag@net\x035w\x03ork.admin PRIVMSG foo :bar baz"
ircMessage := ParseIRCMessage(testMessage)
ircMessage, value := ParseIRCMessage(testMessage)
if !value {
t.Fatal("Invalid irc message")
}
if ircMessage.Source != "coolguy!ag@net\x035w\x03ork.admin" {
t.Fatal("Parsing source")
@@ -434,7 +510,11 @@ func TestParser(t *testing.T) {
t.Run("Test more weird control codes", func(t *testing.T) {
testMessage := ":coolguy!~ag@n\x02et\x0305w\x0fork.admin PRIVMSG foo :bar baz"
ircMessage := ParseIRCMessage(testMessage)
ircMessage, value := ParseIRCMessage(testMessage)
if !value {
t.Fatal("Invalid irc message")
}
if ircMessage.Source != "coolguy!~ag@n\x02et\x0305w\x0fork.admin" {
t.Fatal("Parsing source")
@@ -455,7 +535,11 @@ func TestParser(t *testing.T) {
t.Run("Test full message", func(t *testing.T) {
testMessage := "@tag1=value1;tag2;vendor1/tag3=value2;vendor2/tag4= :irc.example.com COMMAND param1 param2 :param3 param3"
ircMessage := ParseIRCMessage(testMessage)
ircMessage, value := ParseIRCMessage(testMessage)
if !value {
t.Fatal("Invalid irc message")
}
if ircMessage.Tags["tag1"] != "value1" {
t.Fatal("Parsing regular tag")
@@ -496,7 +580,11 @@ func TestParser(t *testing.T) {
t.Run("Test message without tags", func(t *testing.T) {
testMessage := ":irc.example.com COMMAND param1 param2 :param3 param3"
ircMessage := ParseIRCMessage(testMessage)
ircMessage, value := ParseIRCMessage(testMessage)
if !value {
t.Fatal("Invalid irc message")
}
if ircMessage.Source != "irc.example.com" {
t.Fatal("Parsing source")
@@ -521,7 +609,11 @@ func TestParser(t *testing.T) {
t.Run("Test message without source", func(t *testing.T) {
testMessage := "@tag1=value1;tag2;vendor1/tag3=value2;vendor2/tag4= COMMAND param1 param2 :param3 param3"
ircMessage := ParseIRCMessage(testMessage)
ircMessage, value := ParseIRCMessage(testMessage)
if !value {
t.Fatal("Invalid irc message")
}
if ircMessage.Tags["tag1"] != "value1" {
t.Fatal("Parsing regular tag")
@@ -563,7 +655,11 @@ func TestParser(t *testing.T) {
// TODO: FAIL
t.Run("Test yaml encoding with slashes", func(t *testing.T) {
testMessage := "@foo=\\\\\\\\\\:\\\\s\\s\\r\\n COMMAND"
ircMessage := ParseIRCMessage(testMessage)
ircMessage, value := ParseIRCMessage(testMessage)
if !value {
t.Fatal("Invalid irc message")
}
if ircMessage.Tags["foo"] != "\\\\;\\s \r\n" {
t.Fatal("Parsing escaped tag")
@@ -576,7 +672,11 @@ func TestParser(t *testing.T) {
t.Run("Test broken messages 1", func(t *testing.T) {
testMessage := ":gravel.mozilla.org 432 #momo :Erroneous Nickname: Illegal characters"
ircMessage := ParseIRCMessage(testMessage)
ircMessage, value := ParseIRCMessage(testMessage)
if !value {
t.Fatal("Invalid irc message")
}
if ircMessage.Source != "gravel.mozilla.org" {
t.Fatal("Parsing source")
@@ -597,7 +697,11 @@ func TestParser(t *testing.T) {
t.Run("Test broken messages 2", func(t *testing.T) {
testMessage := ":gravel.mozilla.org MODE #tckk +n "
ircMessage := ParseIRCMessage(testMessage)
ircMessage, value := ParseIRCMessage(testMessage)
if !value {
t.Fatal("Invalid irc message")
}
if ircMessage.Source != "gravel.mozilla.org" {
t.Fatal("Parsing source")
@@ -618,7 +722,11 @@ func TestParser(t *testing.T) {
t.Run("Test broken messages 3", func(t *testing.T) {
testMessage := ":services.esper.net MODE #foo-bar +o foobar "
ircMessage := ParseIRCMessage(testMessage)
ircMessage, value := ParseIRCMessage(testMessage)
if !value {
t.Fatal("Invalid irc message")
}
if ircMessage.Source != "services.esper.net" {
t.Fatal("Parsing source")
@@ -644,7 +752,11 @@ func TestParser(t *testing.T) {
// TODO: FAIL
t.Run("Test tags. they should be parsed char-at-a-time", func(t *testing.T) {
testMessage := "@tag1=value\\\\ntest COMMAND"
ircMessage := ParseIRCMessage(testMessage)
ircMessage, value := ParseIRCMessage(testMessage)
if !value {
t.Fatal("Invalid irc message")
}
if ircMessage.Tags["tag1"] != "value\\ntest" {
t.Fatal("Parsing escaped tag")
@@ -662,7 +774,11 @@ func TestParser(t *testing.T) {
// TODO: FAIl
t.Run("Test tag escape for char that doesn't need it", func(t *testing.T) {
testMessage := "@tag1=value\\1 COMMAND"
ircMessage := ParseIRCMessage(testMessage)
ircMessage, value := ParseIRCMessage(testMessage)
if !value {
t.Fatal("Invalid irc message")
}
if ircMessage.Tags["tag1"] != "value1" {
t.Fatal("Parsing escaped tag")
@@ -676,7 +792,11 @@ func TestParser(t *testing.T) {
// TODO: FAIL
t.Run("Test slash at end of tag", func(t *testing.T) {
testMessage := "@tag1=value1\\ COMMAND"
ircMessage := ParseIRCMessage(testMessage)
ircMessage, value := ParseIRCMessage(testMessage)
if !value {
t.Fatal("Invalid irc message")
}
if ircMessage.Tags["tag1"] != "value1" {
t.Fatal("Parsing escaped tag")
@@ -689,7 +809,11 @@ func TestParser(t *testing.T) {
t.Run("Test duplicate tags", func(t *testing.T) {
testMessage := "@tag1=1;tag2=3;tag3=4;tag1=5 COMMAND"
ircMessage := ParseIRCMessage(testMessage)
ircMessage, value := ParseIRCMessage(testMessage)
if !value {
t.Fatal("Invalid irc message")
}
if ircMessage.Tags["tag1"] != "5" {
t.Fatal("Parsing duplicate tag")
@@ -710,7 +834,11 @@ func TestParser(t *testing.T) {
t.Run("Test vendored tags can have the same name as unvendored tag", func(t *testing.T) {
testMessage := "@tag1=1;tag2=3;tag3=4;tag1=5;vendor/tag2=8 COMMAND"
ircMessage := ParseIRCMessage(testMessage)
ircMessage, value := ParseIRCMessage(testMessage)
if !value {
t.Fatal("Invalid irc message")
}
if ircMessage.Tags["tag1"] != "5" {
t.Fatal("Parsing duplicate tag")
@@ -735,7 +863,11 @@ func TestParser(t *testing.T) {
t.Run("Test: some parsers handle /MODE in a special way", func(t *testing.T) {
testMessage := ":SomeOp MODE #channel :+i"
ircMessage := ParseIRCMessage(testMessage)
ircMessage, value := ParseIRCMessage(testMessage)
if !value {
t.Fatal("Invalid irc message")
}
if ircMessage.Source != "SomeOp" {
t.Fatal("Parsing source")
@@ -756,7 +888,11 @@ func TestParser(t *testing.T) {
t.Run("Test: some parsers handle /MODE in a special way 2", func(t *testing.T) {
testMessage := ":SomeOp MODE #channel +oo SomeUser :AnotherUser"
ircMessage := ParseIRCMessage(testMessage)
ircMessage, value := ParseIRCMessage(testMessage)
if !value {
t.Fatal("Invalid irc message")
}
if ircMessage.Source != "SomeOp" {
t.Fatal("Parsing source")
@@ -785,7 +921,11 @@ func TestParser(t *testing.T) {
t.Run("Test multiple spaces", func(t *testing.T) {
testMessage := ":src JOIN #chan :thing lol haha"
ircMessage := ParseIRCMessage(testMessage)
ircMessage, value := ParseIRCMessage(testMessage)
if !value {
t.Fatal("Invalid irc message")
}
if ircMessage.Source != "src" {
t.Fatal("Parsing source with multiple spaces in between")
+4 -6
View File
@@ -30,7 +30,7 @@ import (
type Screen int
const (
Login = iota
Login Screen = iota
MainScreen
)
@@ -52,15 +52,13 @@ func initialUiState(client *client.Client) UI {
}
}
func InitialState() State {
func InitialState() *State {
client := &client.Client{}
s := State{
return &State{
Client: client,
UI: initialUiState(client),
}
return s
}
func (s State) Init() tea.Cmd {
@@ -94,7 +92,7 @@ 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) = client.NewClient(host, port, tlsEnabled)
s.Client.Initialize(host, port, tlsEnabled)
s.Client.Register(nickname, password, channel)
// 512 bytes as a base + 8192 additional bytes for tags
-5
View File
@@ -19,13 +19,8 @@ package app
import tea "github.com/charmbracelet/bubbletea"
func (s *State) Quit() tea.Msg {
if s.Client.Quit != nil {
close(s.Client.Quit)
}
if s.Client.TcpConn != nil {
s.Client.SendCommand("QUIT")
s.Client.TcpConn.Close()
}
return tea.Quit()
+18 -6
View File
@@ -17,6 +17,7 @@
package mainscreen
import (
"io"
"log"
"strings"
@@ -26,13 +27,13 @@ import (
func (s *State) ReadLoop() {
for {
select {
case <-s.Client.Quit:
return
default:
msg, err := s.ConnReader.ReadString('\n')
if err != nil {
log.Fatal(err)
if err != io.EOF {
log.Println(err)
}
s.Client.TcpConn.Close()
return
}
msg = strings.Replace(msg, "\r\n", "", 1)
@@ -41,7 +42,6 @@ func (s *State) ReadLoop() {
}
}
}
}
type SendPrivMsg struct {
Msg string
@@ -52,3 +52,15 @@ func (s InputState) SendingPrivMsg(msg string) tea.Cmd {
return SendPrivMsg{Msg: msg}
}
}
type ReceivedIRCMsgMsg struct{}
func ReceivedIRCMsg() tea.Msg {
return ReceivedIRCMsgMsg{}
}
type SwitchChannelsMsg struct{}
func SwitchChannels() tea.Msg {
return SwitchChannelsMsg{}
}
+13
View File
@@ -62,7 +62,20 @@ func (s InputState) Update(msg tea.Msg) (InputState, tea.Cmd) {
return s, cmd
}
func (s *InputState) Focus() {
s.Input.Focus()
s.Style = s.Style.Copy().BorderForeground(lipgloss.Color("105"))
}
func (s *InputState) Blur() {
s.Input.Blur()
s.Style = s.Style.Copy().BorderForeground(lipgloss.Color("#EEE"))
}
func (s *InputState) SetSize(width int) {
// set a max width for the input field so it scrolls horizontally instead of wrapping to a newline
// -4 for input prompt char, cursor, and some extra magical padding
s.Input.Width = width - s.Style.GetHorizontalFrameSize() - 4
s.Style = s.Style.Width(width - s.Style.GetHorizontalBorderSize())
}
+118 -51
View File
@@ -19,6 +19,7 @@ package mainscreen
import (
"bufio"
"fmt"
"math"
"strings"
"github.com/charmbracelet/bubbles/textinput"
@@ -34,7 +35,8 @@ import (
type Window int
const (
Viewport = iota
Viewport Window = iota
SidePanel
InputBox
)
@@ -42,7 +44,6 @@ type State struct {
ConnReader *bufio.Reader
Client *client.Client
Viewport *viewport.Model
Style lipgloss.Style
FocusIndex Window
InputBox InputState
@@ -52,14 +53,14 @@ type State struct {
func NewMainScreen(client *client.Client) State {
newViewport := &viewport.Model{
HighPerformanceRendering: false,
Wrap: true,
Wrap: viewport.Wrap,
Style: MessagesStyle.Copy(),
}
return State{
ConnReader: nil,
Client: client,
Viewport: newViewport,
Style: MessagesStyle.Copy(),
FocusIndex: InputBox,
InputBox: NewInputBox(),
SidePanel: NewSidePanel(client),
@@ -71,6 +72,9 @@ func (s State) Update(msg tea.Msg) (State, tea.Cmd) {
var cmds []tea.Cmd
switch msg := msg.(type) {
case ReceivedIRCMsgMsg:
s.Viewport.GotoBottom()
return s, nil
case SendPrivMsg:
// for sending slash commands
// TODO: make this better
@@ -83,21 +87,28 @@ func (s State) Update(msg tea.Msg) (State, tea.Cmd) {
}
if strings.ToUpper(command) == commands.JOIN {
s.Client.ActiveChannel = params[0]
s.Client.Channels[params[0]] = client.Channel{}
s.Client.SendCommand(command, params...)
return s, nil
if _, ok := s.Client.Channels[params[0]]; !ok {
s.Client.Channels[params[0]] = client.Channel{
Users: make(map[string]client.User),
}
}
s.Client.SendCommand(commands.JOIN, params...)
return s, SwitchChannels
}
s.Client.SendCommand(command, params...)
} else {
// TODO: dont send command when activechannel == c.host
if s.Client.ActiveChannel != s.Client.Host {
fullMsg := s.Client.Nickname + ": " + msg.Msg
if c, ok := s.Client.Channels[s.Client.ActiveChannel]; ok {
c.History += fullMsg + client.CRLF
s.Client.Channels[s.Client.ActiveChannel] = c
}
// 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.ActiveChannel].History)
s.Viewport.GotoBottom()
}
}
return s, nil
@@ -111,35 +122,32 @@ func (s State) Update(msg tea.Msg) (State, tea.Cmd) {
s.FocusIndex--
}
if s.FocusIndex > 1 {
if s.FocusIndex > 2 {
s.FocusIndex = 0
} else if s.FocusIndex < 0 {
s.FocusIndex = 1
s.FocusIndex = 2
}
switch s.FocusIndex {
case Viewport:
s.Style = s.Style.Copy().BorderForeground(lipgloss.Color("105"))
s.InputBox.Style = s.InputBox.Style.Copy().BorderForeground(lipgloss.Color("#EEE"))
s.InputBox.Input.Blur()
s.Focus()
s.InputBox.Blur()
s.SidePanel.Blur()
case InputBox:
s.Style = s.Style.Copy().BorderForeground(lipgloss.Color("#EEE"))
s.InputBox.Style = s.InputBox.Style.Copy().BorderForeground(lipgloss.Color("105"))
s.InputBox, cmd = s.InputBox.Update(msg)
cmds = append(cmds, cmd)
cmds = append(cmds, textinput.Blink)
s.InputBox.Input.Focus()
s.Blur()
s.InputBox.Focus()
s.SidePanel.Blur()
case SidePanel:
s.Blur()
s.InputBox.Blur()
s.SidePanel.Focus()
}
return s, tea.Batch(cmds...)
}
}
// TODO: maybe we still need this, not sure
// s.Viewport.SetContent(s.Client.ActiveChannel.History)
switch s.FocusIndex {
case Viewport:
*s.Viewport, cmd = s.Viewport.Update(msg)
@@ -149,22 +157,35 @@ func (s State) Update(msg tea.Msg) (State, tea.Cmd) {
cmds = append(cmds, cmd)
}
*s.SidePanel, cmd = s.SidePanel.Update(msg)
cmds = append(cmds, cmd)
return s, tea.Batch(cmds...)
}
func (s *State) Focus() {
s.Viewport.Style = s.Viewport.Style.Copy().BorderForeground(lipgloss.Color("105"))
}
func (s *State) Blur() {
s.Viewport.Style = s.Viewport.Style.Copy().BorderForeground(lipgloss.Color("#EEE"))
}
func (s *State) SetSize(width, height int) {
s.InputBox.SetSize(width)
s.SidePanel.SetSize(width, height, s.InputBox.Style.Copy().GetVerticalFrameSize())
s.SidePanel.SetSize(width, height, s.InputBox.Style.GetVerticalFrameSize())
// -1 is for some extra invisible margin or padding or whatever (idk what it is tbh)
newViewportWidth := (width * 8 / 10) - s.Style.GetHorizontalBorderSize() - 1
newViewportHeight := height - s.InputBox.Style.GetVerticalFrameSize() - s.Style.GetVerticalBorderSize() - 1
// We floor here because width is an int and some fractions are lost
// and also because we ceil the sidepanel's width
// -1 is for some extra invisible margin or padding between the main viewport and the inputbox
newWidth := int(math.Floor(float64(width)*8.0/10.0) - float64(s.Viewport.Style.GetHorizontalFrameSize()))
newHeight := height - s.InputBox.Style.GetVerticalFrameSize() - s.Viewport.Style.GetVerticalFrameSize() - 1
s.Viewport.Width = newWidth
s.Viewport.Height = newHeight
s.Viewport.Style = s.Viewport.Style.Width(newWidth)
s.Viewport.Style = s.Viewport.Style.Height(newHeight)
// copying the existing style is important here otherwise we'll end up with artifacting
s.Viewport.Width = newViewportWidth
s.Style = s.Style.Width(newViewportWidth)
s.Viewport.Height = newViewportHeight
s.Style = s.Style.Height(newViewportHeight)
// we need to re-set the content because words wrap differently on different sizes
s.Viewport.SetContent(s.Client.Channels[s.Client.ActiveChannel].History)
}
@@ -173,15 +194,16 @@ func (s *State) HandleCommand(msg parser.IRCMessage) {
// TODO: handle different commands
switch msg.Command {
case commands.PING:
s.Client.SendCommand(commands.PONG, msg.Parameters[0])
token := msg.Parameters[0]
s.Client.SendCommand(commands.PONG, token)
case commands.PRIVMSG:
nick := strings.SplitN(msg.Source, "!", 2)[0]
channel := msg.Parameters[0]
msgContent := msg.Parameters[1]
fullMsg := fmt.Sprintf("%s: %s", nick, msgContent)
privMsg := fmt.Sprintf("%s: %s", nick, msgContent)
if c, ok := s.Client.Channels[channel]; ok {
c.History += fullMsg + client.CRLF
c.History += privMsg + client.CRLF
s.Client.Channels[channel] = c
}
@@ -190,40 +212,81 @@ func (s *State) HandleCommand(msg parser.IRCMessage) {
nick := strings.SplitN(msg.Source, "!", 2)[0]
channel := msg.Parameters[0]
fullMsg := fmt.Sprintf("== %s has joined", nick)
joinMsg := fmt.Sprintf("== %s has joined", nick)
if c, ok := s.Client.Channels[channel]; ok {
c.History += fullMsg + client.CRLF
c.History += joinMsg + client.CRLF
if _, exists := c.Users[nick]; !exists {
c.Users[nick] = client.User{}
}
s.Client.Channels[channel] = c
}
if channel == s.Client.ActiveChannel {
s.SidePanel.UpdateNicks()
}
s.Viewport.SetContent(s.Client.Channels[s.Client.ActiveChannel].History)
case commands.QUIT:
nick := strings.SplitN(msg.Source, "!", 2)[0]
reason := msg.Parameters[0]
fullMsg := fmt.Sprintf("== %s has quit (%s)", nick, reason)
if c, ok := s.Client.Channels[s.Client.ActiveChannel]; ok {
c.History += fullMsg + client.CRLF
quitMsg := fmt.Sprintf("== %s has quit (%s)", nick, reason)
s.Client.Channels[s.Client.ActiveChannel] = c
for chanName, channel := range s.Client.Channels {
channel.History += quitMsg + client.CRLF
delete(channel.Users, nick)
s.Client.Channels[chanName] = channel
}
s.SidePanel.UpdateNicks()
s.Viewport.SetContent(s.Client.Channels[s.Client.ActiveChannel].History)
case commands.PART:
nick := strings.SplitN(msg.Source, "!", 2)[0]
channel := msg.Parameters[0]
reason := ""
if len(msg.Parameters) > 1 {
reason = msg.Parameters[1]
}
partMsg := fmt.Sprintf("== %s has left %s (%s)", nick, channel, reason)
if c, ok := s.Client.Channels[channel]; ok {
c.History += partMsg + client.CRLF
delete(c.Users, nick)
s.Client.Channels[channel] = c
}
s.SidePanel.UpdateNicks()
s.Viewport.SetContent(s.Client.Channels[s.Client.ActiveChannel].History)
case commands.RPL_NAMREPLY:
// TODO: put the users into their respective channels
// TODO: do i need these
// client := msg.Parameters[0]
// chanSymbol := msg.Parameters[1]
// channel := msg.Parameters[2]
channel := msg.Parameters[2]
nicks := strings.Split(msg.Parameters[3], " ")
s.SidePanel.Nicks = append(s.SidePanel.Nicks, nicks...)
case commands.RPL_ENDOFNAMES:
fullMsg := fmt.Sprintf("%s %s %s %s", msg.Tags, msg.Source, msg.Command, strings.Join(msg.Parameters, " "))
if c, ok := s.Client.Channels[s.Client.ActiveChannel]; ok {
c.History += fullMsg + client.CRLF
if c, ok := s.Client.Channels[channel]; ok {
for _, nick := range nicks {
prefix := ""
_nick := nick
s.Client.Channels[s.Client.ActiveChannel] = c
if commands.UserPrefixes[string(nick[0])] {
prefix = string(nick[0])
_nick = nick[1:]
}
s.Viewport.SetContent(s.Client.Channels[s.Client.ActiveChannel].History)
c.Users[_nick] = client.User{
Prefix: prefix,
}
}
}
if channel == s.Client.ActiveChannel {
s.SidePanel.UpdateNicks()
}
case commands.RPL_ENDOFNAMES:
// TODO: what do i do here lol
default:
fullMsg := fmt.Sprintf("%s %s %s %s", msg.Tags, msg.Source, msg.Command, strings.Join(msg.Parameters, " "))
if c, ok := s.Client.Channels[s.Client.ActiveChannel]; ok {
@@ -234,10 +297,14 @@ func (s *State) HandleCommand(msg parser.IRCMessage) {
s.Viewport.SetContent(s.Client.Channels[s.Client.ActiveChannel].History)
}
// send a receivedIRCmsg tea message so the ui can update
// we also use this tea message to scroll the viewport down
s.Client.Tea.Send(ReceivedIRCMsg())
}
func (s State) View() string {
top := lipgloss.JoinHorizontal(lipgloss.Right, s.Style.Render(s.Viewport.View()), s.SidePanel.View())
top := lipgloss.JoinHorizontal(lipgloss.Right, s.Viewport.View(), s.SidePanel.View())
screen := lipgloss.JoinVertical(0, top, s.InputBox.View())
return ui.MainStyle.Render(screen)
+71 -13
View File
@@ -18,6 +18,7 @@ package mainscreen
import (
"fmt"
"math"
"strings"
"github.com/charmbracelet/bubbles/viewport"
@@ -27,34 +28,91 @@ import (
)
type SidePanelState struct {
// TODO: make a viewport and use that to display the names
// OR: we could use a list
Style lipgloss.Style
Nicks []string
Client *client.Client
Viewport viewport.Model
Content string
Focused bool
}
func (s *SidePanelState) getHeader() string {
separator := strings.Repeat("—", s.Viewport.Width) + "\n"
header := fmt.Sprintf("Users in this channel (%d)\n", len(s.Client.Channels[s.Client.ActiveChannel].Users)) + separator
return header
}
func (s *SidePanelState) getLatestNicks() []string {
nicks := make([]string, 0)
for nick, user := range s.Client.Channels[s.Client.ActiveChannel].Users {
_nick := user.Prefix + nick
nicks = append(nicks, _nick)
}
return nicks
}
func NewSidePanel(client *client.Client) *SidePanelState {
return &SidePanelState{
Client: client,
Viewport: viewport.Model{
HighPerformanceRendering: false,
Wrap: viewport.Wrap,
Style: SidePanelStyle.Copy(),
},
}
}
func (s SidePanelState) Update(msg tea.Msg) (SidePanelState, tea.Cmd) {
var cmd tea.Cmd
var cmds []tea.Cmd
switch msg.(type) {
case SwitchChannelsMsg:
s.UpdateNicks()
return s, nil
}
if s.Focused {
s.Viewport, cmd = s.Viewport.Update(msg)
cmds = append(cmds, cmd)
}
return s, tea.Batch(cmds...)
}
func (s *SidePanelState) Focus() {
s.Focused = true
s.Viewport.Style = s.Viewport.Style.BorderForeground(lipgloss.Color("105"))
}
func (s *SidePanelState) Blur() {
s.Focused = false
s.Viewport.Style = s.Viewport.Style.BorderForeground(lipgloss.Color("#EEE"))
}
func (s *SidePanelState) UpdateNicks() {
header := s.getHeader()
nicks := s.getLatestNicks()
s.Viewport.SetContent(header + strings.Join(nicks, "\n"))
}
func (s *SidePanelState) SetSize(width, height, inputboxHeight int) {
s.Style = s.Style.Copy().Width(width * 2 / 10)
// -1 is for some extra invisible margin or padding or whatever (idk what it is tbh)
s.Style = s.Style.Copy().Height(height - inputboxHeight - s.Style.GetVerticalBorderSize() - 1)
// We ceil here because width is an int and some fractions are lost
// -1 is for some extra invisible margin or padding between the sidepanel and the inputbox
newWidth := int(math.Ceil(math.Ceil((float64(width) * 2.0 / 10.0)) - float64(s.Viewport.Style.GetHorizontalFrameSize())))
newHeight := height - inputboxHeight - s.Viewport.Style.GetVerticalFrameSize() - 1
s.Viewport.Style = s.Viewport.Style.Width(newWidth)
s.Viewport.Style = s.Viewport.Style.Height(newHeight)
s.Viewport.Width = newWidth
s.Viewport.Height = newHeight
// on resize recalculate the width and re-set the viewport contents
s.UpdateNicks()
}
func (s SidePanelState) View() string {
separator := strings.Repeat("-", s.Style.GetWidth()) + "\n"
header := fmt.Sprintf("Users in this channel (%d)\n", len(s.Nicks)) + separator
nicks := strings.Join(s.Nicks, "\n")
return s.Style.Render(header + nicks)
return s.Viewport.View()
}
-2
View File
@@ -35,6 +35,4 @@ var (
Border(lipgloss.NormalBorder(), true).
Width(ui.MainStyle.GetWidth() * 2 / 10).
BorderForeground(lipgloss.Color("#EEE"))
InputBoxHeight = 1
)
+2 -2
View File
@@ -20,8 +20,8 @@ import "github.com/charmbracelet/lipgloss"
var (
MainStyle = lipgloss.NewStyle().
Width(172).
Height(40)
Width(0).
Height(0)
DialogStyle = lipgloss.NewStyle().
Border(lipgloss.RoundedBorder(), true).