Compare commits

...
17 changed files with 413 additions and 159 deletions
+1 -1
View File
@@ -32,7 +32,7 @@ func Connect() tea.Msg {
func Quit(client *irc.Client) tea.Cmd { func Quit(client *irc.Client) tea.Cmd {
return func() tea.Msg { return func() tea.Msg {
if client.TcpConn != nil { if client.TCPConn != nil {
client.SendCommand("QUIT") client.SendCommand("QUIT")
} }
+27 -10
View File
@@ -1,16 +1,33 @@
module github.com/illusionman1212/gorc module github.com/illusionman1212/gorc
go 1.16 go 1.23.0
replace github.com/charmbracelet/bubbles => /home/illusion/Desktop/repos/bubbles toolchain go1.24.6
require ( require (
github.com/charmbracelet/bubbles v0.10.2 github.com/charmbracelet/bubbles v0.21.0
github.com/charmbracelet/bubbletea v0.19.3 github.com/charmbracelet/bubbletea v1.3.6
github.com/charmbracelet/lipgloss v0.4.0 github.com/charmbracelet/lipgloss v1.1.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 require (
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 // indirect github.com/atotto/clipboard v0.1.4 // indirect
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/colorprofile v0.3.1 // indirect
github.com/charmbracelet/x/ansi v0.10.1 // indirect
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.35.0 // indirect
golang.org/x/text v0.28.0 // indirect
) )
+44 -39
View File
@@ -1,44 +1,49 @@
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/charmbracelet/bubbletea v0.19.3 h1:OKeO/Y13rQQqt4snX+lePB0QrnW80UdrMNolnCcmoAw= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/charmbracelet/bubbletea v0.19.3/go.mod h1:VuXF2pToRxDUHcBUcPmCRUHRvFATM4Ckb/ql1rBl3KA= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/charmbracelet/harmonica v0.1.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
github.com/charmbracelet/lipgloss v0.4.0 h1:768h64EFkGUr8V5yAKV7/Ta0NiVceiPaV+PphaW1K9g= github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
github.com/charmbracelet/lipgloss v0.4.0/go.mod h1:vmdkHvce7UzX6xkyf4cca8WlwdQ5RQr8fzta+xl7BOM= github.com/charmbracelet/bubbletea v1.3.6 h1:VkHIxPJQeDt0aFJIsVxw8BQdh/F/L2KKZGsK6et5taU=
github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ= github.com/charmbracelet/bubbletea v1.3.6/go.mod h1:oQD9VCRQFF8KplacJLo28/jofOI2ToOfGYeFgBBxHOc=
github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL3m+AxCzDz40=
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= github.com/charmbracelet/colorprofile v0.3.1/go.mod h1:/GkGusxNs8VB/RSOh3fu0TJmQ4ICMMPApIIVn0KszZ0=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ=
github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 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/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 h1:kMlmsLSbjkikxQJ1IPwaM+7LJ9ltFu/fi8CRzvSnQmA= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
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/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/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 h1:XfKQ4OlFl8okEOr5UvAqFRVj8pY/4yfcXrddB8qAbU0= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20210422114643-f5beecf764ed/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
+23 -19
View File
@@ -54,7 +54,7 @@ type Channel struct {
type Client struct { type Client struct {
// tcp connection // tcp connection
TcpConn net.Conn TCPConn net.Conn
// Host that client is connected to // Host that client is connected to
Host string Host string
@@ -82,8 +82,13 @@ type Client struct {
// Index of the last visible tab in the tab bar // Index of the last visible tab in the tab bar
LastTabIndexInTabBar int LastTabIndexInTabBar int
// The acknowledged capabilities
EnabledCapabilities Capabilities
} }
type Capabilities map[string]string
type MsgFmtOpts struct { type MsgFmtOpts struct {
WithTimestamp bool WithTimestamp bool
AsServerMsg bool AsServerMsg bool
@@ -93,7 +98,7 @@ type MsgFmtOpts struct {
var notImpl = lipgloss.NewStyle().Foreground(ui.ErrorColor).Render("[NOT IMPL]") var notImpl = lipgloss.NewStyle().Foreground(ui.ErrorColor).Render("[NOT IMPL]")
var serverMsgStyle = lipgloss.NewStyle().Foreground(ui.ServerMsgColor) var serverMsgStyle = lipgloss.NewStyle().Foreground(ui.ServerMsgColor)
var timestampStyle = serverMsgStyle.Copy() var timestampStyle = serverMsgStyle
var dateStyle = lipgloss.NewStyle().Foreground(ui.DateColor) var dateStyle = lipgloss.NewStyle().Foreground(ui.DateColor)
const CRLF = "\r\n" const CRLF = "\r\n"
@@ -122,7 +127,7 @@ func (c *Channel) AppendMsg(timestamp string, fullMsg string, opts MsgFmtOpts) {
c.History += prefixes + style.Render(fullMsg) + CRLF c.History += prefixes + style.Render(fullMsg) + CRLF
} }
func (s *Client) Initialize(host string, port string, tlsEnabled bool) { func (c *Client) Initialize(host string, port string, tlsEnabled bool) {
addr := fmt.Sprintf("%s:%s", host, port) addr := fmt.Sprintf("%s:%s", host, port)
if tlsEnabled { if tlsEnabled {
@@ -133,11 +138,12 @@ func (s *Client) Initialize(host string, port string, tlsEnabled bool) {
log.Fatal(err) log.Fatal(err)
} }
s.TcpConn = conn c.TCPConn = conn
s.Host = host c.Host = host
s.Port = port c.Port = port
s.ActiveChannel = host c.ActiveChannel = host
s.Channels = make([]Channel, 0) c.Channels = make([]Channel, 0)
c.EnabledCapabilities = make(Capabilities, 0)
return return
} }
@@ -151,11 +157,12 @@ func (s *Client) Initialize(host string, port string, tlsEnabled bool) {
log.Fatal(err) log.Fatal(err)
} }
s.TcpConn = conn c.TCPConn = conn
s.Host = host c.Host = host
s.Port = port c.Port = port
s.ActiveChannel = host c.ActiveChannel = host
s.Channels = make([]Channel, 0) c.Channels = make([]Channel, 0)
c.EnabledCapabilities = make(Capabilities, 0)
} }
func (c *Client) Register(nick string, password string, channel string) { func (c *Client) Register(nick string, password string, channel string) {
@@ -164,17 +171,14 @@ func (c *Client) Register(nick string, password string, channel string) {
Users: make(map[string]User), Users: make(map[string]User),
}) })
c.SendCommand(commands.CAP, "LS") c.SendCommand(commands.CAP, "LS", "302")
if password != "" { if password != "" {
c.SendCommand(commands.PASS, password) c.SendCommand(commands.PASS, password)
} }
// TODO: check if nickname has spaces and remove them
c.SendCommand(commands.NICK, nick) c.SendCommand(commands.NICK, nick)
// set user-wanted nickname // set user-wanted nickname
c.Nickname = nick c.Nickname = nick
c.SendCommand(commands.USER, nick, "0", "*", nick) c.SendCommand(commands.USER, nick, "0", "*", nick)
// TODO: CAP REQ :whatever capability the client recognizes and supports
c.SendCommand(commands.CAP, "END")
// joining a channel when registering is optional // joining a channel when registering is optional
if channel != "" { if channel != "" {
c.SendCommand(commands.JOIN, channel) c.SendCommand(commands.JOIN, channel)
@@ -194,7 +198,7 @@ func (c *Client) SetDay() {
} }
func (c Client) SendCommand(cmd string, params ...string) { func (c Client) SendCommand(cmd string, params ...string) {
if c.TcpConn == nil { if c.TCPConn == nil {
// TODO: properly handle the error instead of Fatal-ing // TODO: properly handle the error instead of Fatal-ing
log.Fatal("Attempted to write data to nil connection") log.Fatal("Attempted to write data to nil connection")
} }
@@ -225,5 +229,5 @@ func (c Client) SendCommand(cmd string, params ...string) {
} }
} }
c.TcpConn.Write([]byte(cmd + paramsString + CRLF)) c.TCPConn.Write([]byte(cmd + paramsString + CRLF))
} }
+41 -5
View File
@@ -116,6 +116,7 @@ const (
RPL_MYINFO = "004" // RFC2812 - Implemented RPL_MYINFO = "004" // RFC2812 - Implemented
RPL_ISUPPORT = "005" // ??????? - Not Implemented (TODO:) RPL_ISUPPORT = "005" // ??????? - Not Implemented (TODO:)
RPL_BOUNCE = "010" // ??????? - Not Implemented (TODO:) RPL_BOUNCE = "010" // ??????? - Not Implemented (TODO:)
RPL_REMOTEISUPPORT = "105" // ??????? - Not Implemented (TODO:) // Exact same as RPL_ISUPPORT but for remote servers
RPL_TRACELINK = "200" // RFC1459 - Not Implemented (TODO:) RPL_TRACELINK = "200" // RFC1459 - Not Implemented (TODO:)
RPL_TRACECONNECTING = "201" // RFC1459 - Not Implemented (TODO:) RPL_TRACECONNECTING = "201" // RFC1459 - Not Implemented (TODO:)
RPL_TRACEHANDSHAKE = "202" // RFC1459 - Not Implemented (TODO:) RPL_TRACEHANDSHAKE = "202" // RFC1459 - Not Implemented (TODO:)
@@ -240,7 +241,7 @@ const (
ERR_NOTOPLEVEL = "413" // RFC1459 - Not Implemented (TODO:) ERR_NOTOPLEVEL = "413" // RFC1459 - Not Implemented (TODO:)
ERR_WILDTOPLEVEL = "414" // RFC1459 - Not Implemented (TODO:) ERR_WILDTOPLEVEL = "414" // RFC1459 - Not Implemented (TODO:)
ERR_BADMASK = "415" // RFC2812 - Not Implemented (TODO:) ERR_BADMASK = "415" // RFC2812 - Not Implemented (TODO:)
ERR_UNKNOWNCOMMAND = "421" // RFC1459 - Not Implemented (TODO:) ERR_UNKNOWNCOMMAND = "421" // RFC1459 - Implemented
ERR_NOMOTD = "422" // RFC1459 - Not Implemented (TODO:) ERR_NOMOTD = "422" // RFC1459 - Not Implemented (TODO:)
ERR_NOADMININFO = "423" // RFC1459 - Not Implemented (TODO:) ERR_NOADMININFO = "423" // RFC1459 - Not Implemented (TODO:)
ERR_FILEERROR = "424" // RFC1459 - Not Implemented (TODO:) ERR_FILEERROR = "424" // RFC1459 - Not Implemented (TODO:)
@@ -312,7 +313,7 @@ var Features = map[string]bool{
"CHANLIMIT": true, "CHANLIMIT": true,
"CHANMODES": true, "CHANMODES": true,
"CHANNELLEN": true, "CHANNELLEN": true,
"CHANTYPES": true, "CHANTYPES": true, // Channel Types. Default is #. Available are #&
"CHARSET": true, // Deprecated but might still be used "CHARSET": true, // Deprecated but might still be used
"CLIENTVER": true, // Deprecated but might still be used "CLIENTVER": true, // Deprecated but might still be used
"CNOTICE": true, "CNOTICE": true,
@@ -364,7 +365,42 @@ var Features = map[string]bool{
"WHOX": true, "WHOX": true,
} }
// Messages MUST end in CRLF and SHOULD be encoded and decoded using UTF-8 (with fallbacks such as Latin-1). var Capabilities = map[string]bool{
// Names are casemapped, read the casemapping from the RPL_ISUPPORT the server sends when registration is completed. // Twitch-specific capabilities
"twitch.tv/membership": false,
"twitch.tv/commands": false,
"twitch.tv/tags": false,
// When receiving a numeric reply, client MUST be able to handle any number of parameters on a numeric reply. // Solanum-specific capabilities
"solanum.chat/identify-msg": false,
"solanum.chat/oper": false,
"solanum.chat/realhost": false,
// IRCv3 capabilities
"account-notify": false,
"account-registration": false, // Draft
"account-tag": false,
"away-notify": false,
"batch": false,
"cap-notify": true,
"channel-rename": false, // Draft
"chathistory": false, // Draft
"chghost": false,
"echo-message": false,
"event-playback": false, // Draft
"extended-join": false,
"extended-monitor": false,
"invite-notify": false,
"labeled-response": false,
"message-tags": false,
"metadata": false,
"monitor": false,
"multi-prefix": false,
"multiline": false, // Draft
"read-marker": false, // Draft
"sasl": false,
"server-time": true,
"setname": false,
"tls": false, // Deprecated
"userhost-in-names": false,
}
+101 -3
View File
@@ -25,6 +25,8 @@ import (
"strings" "strings"
"time" "time"
"slices"
"github.com/illusionman1212/gorc/cmds" "github.com/illusionman1212/gorc/cmds"
"github.com/illusionman1212/gorc/irc" "github.com/illusionman1212/gorc/irc"
"github.com/illusionman1212/gorc/irc/commands" "github.com/illusionman1212/gorc/irc/commands"
@@ -33,7 +35,7 @@ import (
func ReadLoop(client *irc.Client) { func ReadLoop(client *irc.Client) {
// 512 bytes as a base + 8192 additional bytes for tags // 512 bytes as a base + 8192 additional bytes for tags
r := bufio.NewReaderSize(client.TcpConn, 8192+512) r := bufio.NewReaderSize(client.TCPConn, 8192+512)
for { for {
msg, err := r.ReadString('\n') msg, err := r.ReadString('\n')
@@ -41,7 +43,7 @@ func ReadLoop(client *irc.Client) {
if err != io.EOF { if err != io.EOF {
log.Println(err) log.Println(err)
} }
client.TcpConn.Close() client.TCPConn.Close()
return return
} }
@@ -153,7 +155,7 @@ func handlePart(msg parser.IRCMessage, client *irc.Client) {
for i, c := range client.Channels { for i, c := range client.Channels {
if c.Name == channel { if c.Name == channel {
if nick == client.Nickname { if nick == client.Nickname {
client.Channels = append(client.Channels[:i], client.Channels[i+1:]...) client.Channels = slices.Delete(client.Channels, i, i+1)
if client.ActiveChannelIndex >= i { if client.ActiveChannelIndex >= i {
client.ActiveChannelIndex-- client.ActiveChannelIndex--
client.ActiveChannel = client.Channels[client.ActiveChannelIndex].Name client.ActiveChannel = client.Channels[client.ActiveChannelIndex].Name
@@ -169,6 +171,87 @@ func handlePart(msg parser.IRCMessage, client *irc.Client) {
client.Tea.Send(cmds.SwitchChannels()) client.Tea.Send(cmds.SwitchChannels())
} }
func handleCAP(msg parser.IRCMessage, client *irc.Client) {
switch msg.Parameters[1] {
case "LIST":
fallthrough
case "NEW":
fallthrough
case "LS":
caps := strings.Split(msg.Parameters[2], " ")
recognizedCaps := make([]irc.Capabilities, 0)
capabilities := make(irc.Capabilities, 0)
length := 0
for _, capability := range caps {
// NOTE: not sure if I should store the cap value the server advertises or use my own
parts := strings.Split(capability, "=")
key := parts[0]
value := ""
if len(parts) > 1 {
value = parts[1]
}
if commands.Capabilities[key] {
// When the final parameter approaches 510 bytes,
// we send multiple REQ commands
// (https://ircv3.net/specs/extensions/capability-negotiation.html#the-cap-req-subcommand)
if length+len(capability) >= 500 {
recognizedCaps = append(recognizedCaps, capabilities)
capabilities = make(irc.Capabilities, 0)
length = 0
}
capabilities[key] = value
length += len(capability)
} else {
log.Println("Unknown capability:", capability)
}
}
recognizedCaps = append(recognizedCaps, capabilities)
for _, capsList := range recognizedCaps {
list := make([]string, 0)
for key, value := range capsList {
if len(value) > 0 {
list = append(list, key+"="+value)
} else {
list = append(list, key)
}
}
client.SendCommand(commands.CAP, "REQ", strings.Join(list, " "))
}
client.SendCommand(commands.CAP, "END")
case "ACK":
caps := strings.Split(msg.Parameters[2], " ")
for _, capability := range caps {
if capability[0] == '-' {
delete(client.EnabledCapabilities, capability[1:])
} else {
parts := strings.Split(capability, "=")
key := parts[0]
value := ""
if len(parts) > 1 {
value = parts[1]
}
client.EnabledCapabilities[key] = value
}
}
case "NAK":
caps := strings.Split(msg.Parameters[2], " ")
msgOpts := irc.MsgFmtOpts{
WithTimestamp: true,
AsServerMsg: true,
}
client.Channels[0].AppendMsg(msg.Timestamp, "Unrecognized capabilities: "+strings.Join(caps, " "), msgOpts)
case "DEL":
caps := strings.Split(msg.Parameters[2], " ")
for _, capability := range caps {
delete(client.EnabledCapabilities, capability)
}
}
}
func handleWELCOME(msg parser.IRCMessage, client *irc.Client) { func handleWELCOME(msg parser.IRCMessage, client *irc.Client) {
nick := msg.Parameters[0] nick := msg.Parameters[0]
welcomeMsg := msg.Parameters[1] welcomeMsg := msg.Parameters[1]
@@ -467,6 +550,17 @@ func handleNOSUCHSERVER(msg parser.IRCMessage, client *irc.Client) {
client.Channels[0].AppendMsg(msg.Timestamp, message, msgOpts) client.Channels[0].AppendMsg(msg.Timestamp, message, msgOpts)
} }
func handleUNKNOWNCOMMAND(msg parser.IRCMessage, client *irc.Client) {
message := msg.Parameters[1] + " " + msg.Parameters[2]
msgOpts := irc.MsgFmtOpts{
WithTimestamp: true,
AsServerMsg: true,
}
client.Channels[0].AppendMsg(msg.Timestamp, message, msgOpts)
}
func handleNONICKNAMEGIVEN(msg parser.IRCMessage, client *irc.Client) { func handleNONICKNAMEGIVEN(msg parser.IRCMessage, client *irc.Client) {
message := msg.Parameters[1] message := msg.Parameters[1]
@@ -513,6 +607,8 @@ func HandleCommand(msg parser.IRCMessage, client *irc.Client) {
handleQuit(msg, client) handleQuit(msg, client)
case commands.PART: case commands.PART:
handlePart(msg, client) handlePart(msg, client)
case commands.CAP:
handleCAP(msg, client)
case commands.RPL_WELCOME: case commands.RPL_WELCOME:
handleWELCOME(msg, client) handleWELCOME(msg, client)
case commands.RPL_YOURHOST: case commands.RPL_YOURHOST:
@@ -569,6 +665,8 @@ func HandleCommand(msg parser.IRCMessage, client *irc.Client) {
handleWHOISMODES(msg, client) handleWHOISMODES(msg, client)
case commands.ERR_NOSUCHSERVER: case commands.ERR_NOSUCHSERVER:
handleNOSUCHSERVER(msg, client) handleNOSUCHSERVER(msg, client)
case commands.ERR_UNKNOWNCOMMAND:
handleUNKNOWNCOMMAND(msg, client)
case commands.ERR_NONICKNAMEGIVEN: case commands.ERR_NONICKNAMEGIVEN:
handleNONICKNAMEGIVEN(msg, client) handleNONICKNAMEGIVEN(msg, client)
case commands.ERR_NEEDMOREPARAMS: case commands.ERR_NEEDMOREPARAMS:
+58 -5
View File
@@ -18,6 +18,7 @@ package parser
import ( import (
"fmt" "fmt"
"log"
"regexp" "regexp"
"strings" "strings"
"time" "time"
@@ -34,9 +35,44 @@ type IRCMessage struct {
type Tags map[string]string type Tags map[string]string
func (m *IRCMessage) setTimestamp() { func (m *IRCMessage) setTimestamp() {
// TODO: get timestamp from time tag if serverTime, ok := m.Tags["time"]; ok {
t, err := time.Parse("2006-01-02T15:04:05.000Z", serverTime)
if err != nil {
// TODO: properly handle error
log.Fatalln(err)
}
m.Timestamp = fmt.Sprintf("[%02d:%02d]", t.Local().Hour(), t.Local().Minute())
} else {
now := time.Now() now := time.Now()
m.Timestamp = fmt.Sprintf("[%02d:%02d]", now.Hour(), now.Minute()) m.Timestamp = fmt.Sprintf("[%02d:%02d]", now.Hour(), now.Minute())
}
}
func escapeCharacters(char rune) string {
switch char {
case 's':
return " "
case 'n':
return "\n"
case 'r':
return "\r"
case 'b':
return "\b"
case 't':
return "\t"
case 'f':
return "\f"
case ':':
return ";"
case '\\':
fallthrough
case '\'':
fallthrough
case '"':
fallthrough
default:
return string(char)
}
} }
func parseTags(rawTags string) Tags { func parseTags(rawTags string) Tags {
@@ -51,7 +87,24 @@ func parseTags(rawTags string) Tags {
// we need to make sure a "value" exists before accessing str[1] // we need to make sure a "value" exists before accessing str[1]
if len(str) > 1 { if len(str) > 1 {
value = str[1] rawValue := str[1]
for idx := 0; idx < len(rawValue); {
// if we have a backslash at the end of the string
// we need to ignore it
if idx == len(rawValue)-1 && rawValue[idx] == '\\' {
idx += 2
continue
}
if rawValue[idx] == '\\' {
value += escapeCharacters(rune(rawValue[idx+1]))
idx += 2
continue
}
value += string(rawValue[idx])
idx++
}
} }
tags[key] = value tags[key] = value
@@ -62,9 +115,9 @@ func parseTags(rawTags string) Tags {
func ParseIRCMessage(line string) (IRCMessage, bool) { func ParseIRCMessage(line string) (IRCMessage, bool) {
ircMessage := IRCMessage{} ircMessage := IRCMessage{}
multipleSpacesRegex := regexp.MustCompile("[^\\S\\t]+") multipleSpacesRegex := regexp.MustCompile(`[^\S\t]+`)
trailingParamRegex := regexp.MustCompile("[^\\S\\t]+:") trailingParamRegex := regexp.MustCompile(`[^\S\t]+:`)
whitespaceRegex := regexp.MustCompile("^[^\\S\\t]+$") whitespaceRegex := regexp.MustCompile(`^[^\S\t]+$`)
if len(line) <= 0 { if len(line) <= 0 {
// TODO: do something here ??? // TODO: do something here ???
-6
View File
@@ -341,7 +341,6 @@ func TestParser(t *testing.T) {
} }
}) })
// TODO: fails
t.Run("Test escaped tags", func(t *testing.T) { t.Run("Test escaped tags", func(t *testing.T) {
testMessage := "@a=b\\\\and\\nk;c=72\\s45;d=gh\\:764 foo" testMessage := "@a=b\\\\and\\nk;c=72\\s45;d=gh\\:764 foo"
ircMessage, valid := ParseIRCMessage(testMessage) ircMessage, valid := ParseIRCMessage(testMessage)
@@ -401,7 +400,6 @@ func TestParser(t *testing.T) {
} }
}) })
// TODO: FAIL
t.Run("Test single param with preceeding :", func(t *testing.T) { t.Run("Test single param with preceeding :", func(t *testing.T) {
testMessage := ":src JOIN :#chan" testMessage := ":src JOIN :#chan"
ircMessage, valid := ParseIRCMessage(testMessage) ircMessage, valid := ParseIRCMessage(testMessage)
@@ -652,7 +650,6 @@ func TestParser(t *testing.T) {
} }
}) })
// TODO: FAIL
t.Run("Test yaml encoding with slashes", func(t *testing.T) { t.Run("Test yaml encoding with slashes", func(t *testing.T) {
testMessage := "@foo=\\\\\\\\\\:\\\\s\\s\\r\\n COMMAND" testMessage := "@foo=\\\\\\\\\\:\\\\s\\s\\r\\n COMMAND"
ircMessage, valid := ParseIRCMessage(testMessage) ircMessage, valid := ParseIRCMessage(testMessage)
@@ -749,7 +746,6 @@ func TestParser(t *testing.T) {
} }
}) })
// TODO: FAIL
t.Run("Test tags. they should be parsed char-at-a-time", func(t *testing.T) { t.Run("Test tags. they should be parsed char-at-a-time", func(t *testing.T) {
testMessage := "@tag1=value\\\\ntest COMMAND" testMessage := "@tag1=value\\\\ntest COMMAND"
ircMessage, valid := ParseIRCMessage(testMessage) ircMessage, valid := ParseIRCMessage(testMessage)
@@ -771,7 +767,6 @@ func TestParser(t *testing.T) {
} }
}) })
// TODO: FAIl
t.Run("Test tag escape for char that doesn't need it", func(t *testing.T) { t.Run("Test tag escape for char that doesn't need it", func(t *testing.T) {
testMessage := "@tag1=value\\1 COMMAND" testMessage := "@tag1=value\\1 COMMAND"
ircMessage, valid := ParseIRCMessage(testMessage) ircMessage, valid := ParseIRCMessage(testMessage)
@@ -789,7 +784,6 @@ func TestParser(t *testing.T) {
} }
}) })
// TODO: FAIL
t.Run("Test slash at end of tag", func(t *testing.T) { t.Run("Test slash at end of tag", func(t *testing.T) {
testMessage := "@tag1=value1\\ COMMAND" testMessage := "@tag1=value1\\ COMMAND"
ircMessage, valid := ParseIRCMessage(testMessage) ircMessage, valid := ParseIRCMessage(testMessage)
+2 -3
View File
@@ -35,13 +35,12 @@ func main() {
gorc.Client.Tea = p gorc.Client.Tea = p
f, err := tea.LogToFile("gorc.log", "gorc") f, err := tea.LogToFile("gorc.log", "gorc")
defer f.Close()
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
defer f.Close()
err = p.Start() _, err = p.Run()
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
+3 -3
View File
@@ -45,7 +45,7 @@ type State struct {
Client *irc.Client Client *irc.Client
} }
func initialUiState(client *irc.Client) UI { func initialUIState(client *irc.Client) UI {
return UI{ return UI{
Login: login.NewLogin(), Login: login.NewLogin(),
MainScreen: mainscreen.NewMainScreen(client), MainScreen: mainscreen.NewMainScreen(client),
@@ -57,7 +57,7 @@ func InitialState() *State {
return &State{ return &State{
Client: client, Client: client,
UI: initialUiState(client), UI: initialUIState(client),
} }
} }
@@ -125,5 +125,5 @@ func (s State) View() string {
return s.UI.MainScreen.View() return s.UI.MainScreen.View()
} }
return "Error: this screen shouldn't ever show" panic("Invalid screen")
} }
+29 -23
View File
@@ -17,7 +17,6 @@
package login package login
import ( import (
"fmt"
"strings" "strings"
"github.com/charmbracelet/bubbles/textinput" "github.com/charmbracelet/bubbles/textinput"
@@ -44,15 +43,15 @@ func NewLogin() State {
Inputs: make([]textinput.Model, 5), Inputs: make([]textinput.Model, 5),
ConnectButtonBlurredStyle: BlurredDisabledButton, ConnectButtonBlurredStyle: BlurredDisabledButton,
ConnectButtonFocusedStyle: FocusedDisabledButton, ConnectButtonFocusedStyle: FocusedDisabledButton,
DialogStyle: DialogStyle.Copy(), DialogStyle: DialogStyle,
WelcomeMsgStyle: WelcomeMsgStyle.Copy(), WelcomeMsgStyle: WelcomeMsgStyle,
Style: ui.MainStyle.Copy(), Style: ui.MainStyle,
} }
var t textinput.Model var t textinput.Model
for i := range state.Inputs { for i := range state.Inputs {
t = textinput.New() t = textinput.New()
t.CursorStyle = CursorStyle t.Cursor.Style = CursorStyle
t.Prompt = "" t.Prompt = ""
switch i { switch i {
@@ -60,19 +59,27 @@ func NewLogin() State {
t.Placeholder = "Host" t.Placeholder = "Host"
t.Focus() t.Focus()
t.CharLimit = 40 t.CharLimit = 40
t.Width = len(t.Placeholder)
t.TextStyle = FocusedStyle t.TextStyle = FocusedStyle
t.Validate = NoSpacesValidation
case 1: case 1:
t.Placeholder = "Port" t.Placeholder = "Port"
t.CharLimit = 5 t.CharLimit = 5
t.Width = len(t.Placeholder)
t.Validate = ValidatePort
case 2: case 2:
t.Placeholder = "Channel" t.Placeholder = "Channel"
t.CharLimit = 32 t.CharLimit = 32
t.Width = len(t.Placeholder)
case 3: case 3:
t.Placeholder = "Nickname" t.Placeholder = "Nickname"
t.CharLimit = 32 t.CharLimit = 32
t.Width = len(t.Placeholder)
t.Validate = NoSpacesValidation
case 4: case 4:
t.Placeholder = "Password" t.Placeholder = "Password"
t.CharLimit = 32 t.CharLimit = 64
t.Width = len(t.Placeholder)
t.EchoMode = textinput.EchoPassword t.EchoMode = textinput.EchoPassword
t.EchoCharacter = '•' t.EchoCharacter = '•'
} }
@@ -141,6 +148,7 @@ func (s *State) updateInputs(msg tea.Msg) tea.Cmd {
for i := range s.Inputs { for i := range s.Inputs {
s.Inputs[i], cmds[i] = s.Inputs[i].Update(msg) s.Inputs[i], cmds[i] = s.Inputs[i].Update(msg)
s.Inputs[i].Width = max(len(s.Inputs[i].Placeholder), len(s.Inputs[i].Value()))
} }
if s.Inputs[0].Value() != "" && s.Inputs[1].Value() != "" && s.Inputs[3].Value() != "" { if s.Inputs[0].Value() != "" && s.Inputs[1].Value() != "" && s.Inputs[3].Value() != "" {
@@ -157,45 +165,43 @@ func (s *State) updateInputs(msg tea.Msg) tea.Cmd {
} }
func (s *State) SetSize(width, height int) { func (s *State) SetSize(width, height int) {
s.DialogStyle = s.DialogStyle.Copy().Width(width - s.DialogStyle.GetHorizontalFrameSize()) s.DialogStyle = s.DialogStyle.Width(width - s.DialogStyle.GetHorizontalFrameSize())
s.DialogStyle = s.DialogStyle.Copy().Height(height*5/10 - s.DialogStyle.GetVerticalFrameSize()) s.DialogStyle = s.DialogStyle.Height(height*5/10 - s.DialogStyle.GetVerticalFrameSize())
s.WelcomeMsgStyle = s.WelcomeMsgStyle.Copy().Width(width) s.WelcomeMsgStyle = s.WelcomeMsgStyle.Width(width)
s.WelcomeMsgStyle = s.WelcomeMsgStyle.Copy().Height(height * 5 / 10) s.WelcomeMsgStyle = s.WelcomeMsgStyle.Height(height * 5 / 10)
} }
func (s State) View() string { func (s State) View() string {
var sb strings.Builder var sb strings.Builder
for i, input := range s.Inputs {
for i := range s.Inputs { sb.WriteString(input.View())
sb.WriteString(s.Inputs[i].View()) if i < len(s.Inputs) {
if i < len(s.Inputs)-1 {
sb.WriteRune('\n') sb.WriteRune('\n')
} }
} }
checkbox := &BlurredCheckbox checkbox := BlurredCheckbox
// if the checkbox is focused // if the checkbox is focused
if s.FocusIndex == len(s.Inputs) { if s.FocusIndex == len(s.Inputs) {
checkbox = &FocusedCheckbox checkbox = FocusedCheckbox
// if tls is enabled // if tls is enabled
if s.TLS { if s.TLS {
checkbox = &FocusedCheckboxChecked checkbox = FocusedCheckboxChecked
} }
} }
// if the checkbox is not focused and tls is enabled // if the checkbox is not focused and tls is enabled
if s.FocusIndex != len(s.Inputs) && s.TLS { if s.FocusIndex != len(s.Inputs) && s.TLS {
checkbox = &BlurredCheckboxChecked checkbox = BlurredCheckboxChecked
} }
fmt.Fprintf(&sb, "\n%s\n", *checkbox) button := s.ConnectButtonBlurredStyle
button := &s.ConnectButtonBlurredStyle
if s.FocusIndex == len(s.Inputs)+1 { if s.FocusIndex == len(s.Inputs)+1 {
button = &s.ConnectButtonFocusedStyle button = s.ConnectButtonFocusedStyle
} }
fmt.Fprintf(&sb, "\n%s\n", *button)
sb.WriteString(lipgloss.JoinVertical(lipgloss.Center, checkbox, button))
screen := lipgloss.JoinVertical(lipgloss.Center, s.WelcomeMsgStyle.Render(WelcomeMsg), s.DialogStyle.Render(sb.String())) screen := lipgloss.JoinVertical(lipgloss.Center, s.WelcomeMsgStyle.Render(WelcomeMsg), s.DialogStyle.Render(sb.String()))
+6 -6
View File
@@ -72,28 +72,28 @@ var (
FocusedCheckbox = lipgloss.NewStyle(). FocusedCheckbox = lipgloss.NewStyle().
Foreground(ui.AccentColor). Foreground(ui.AccentColor).
MarginTop(1). MarginTop(1).
Render("TLS Enabled [✕]") Render("TLS Enabled [☓]")
BlurredCheckbox = lipgloss.NewStyle(). BlurredCheckbox = lipgloss.NewStyle().
Foreground(ui.PrimaryColor). Foreground(ui.PrimaryColor).
MarginTop(1). MarginTop(1).
Render("TLS Enabled [✕]") Render("TLS Enabled [☓]")
FocusedCheckboxChecked = lipgloss.NewStyle(). FocusedCheckboxChecked = lipgloss.NewStyle().
Foreground(ui.AccentColor). Foreground(ui.AccentColor).
MarginTop(1). MarginTop(1).
Render("TLS Enabled [🗸]") Render("TLS Enabled [✔]")
BlurredCheckboxChecked = lipgloss.NewStyle(). BlurredCheckboxChecked = lipgloss.NewStyle().
Foreground(ui.PrimaryColor). Foreground(ui.PrimaryColor).
MarginTop(1). MarginTop(1).
Render("TLS Enabled [🗸]") Render("TLS Enabled [✔]")
DialogStyle = lipgloss.NewStyle(). DialogStyle = lipgloss.NewStyle().
Align(lipgloss.Center) AlignHorizontal(lipgloss.Center)
WelcomeMsgStyle = lipgloss.NewStyle(). WelcomeMsgStyle = lipgloss.NewStyle().
Align(lipgloss.Center). AlignHorizontal(lipgloss.Center).
Foreground(ui.PrimaryColor) Foreground(ui.PrimaryColor)
CursorStyle = lipgloss.NewStyle(). CursorStyle = lipgloss.NewStyle().
+44
View File
@@ -0,0 +1,44 @@
// gorc project
// Copyright (C) 2023 IllusionMan1212
//
// 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 login
import (
"errors"
"strconv"
"strings"
)
func NoSpacesValidation(input string) error {
if strings.Contains(input, " ") {
return errors.New("field cannot contain spaces")
}
return nil
}
func ValidatePort(input string) error {
pNum, err := strconv.ParseUint(input, 10, 16)
if err != nil {
return err
}
if pNum < 1 || pNum > 65535 {
return errors.New("invalid port number")
}
return nil
}
+3 -3
View File
@@ -37,7 +37,7 @@ func NewInputBox() InputState {
return InputState{ return InputState{
Input: input, Input: input,
Style: InputboxStyle.Copy(), Style: InputboxStyle,
} }
} }
@@ -65,12 +65,12 @@ func (s InputState) Update(msg tea.Msg) (InputState, tea.Cmd) {
func (s *InputState) Focus() { func (s *InputState) Focus() {
s.Input.Focus() s.Input.Focus()
s.Style = s.Style.Copy().BorderForeground(ui.AccentColor) s.Style = s.Style.BorderForeground(ui.AccentColor)
} }
func (s *InputState) Blur() { func (s *InputState) Blur() {
s.Input.Blur() s.Input.Blur()
s.Style = s.Style.Copy().BorderForeground(ui.PrimaryColor) s.Style = s.Style.BorderForeground(ui.PrimaryColor)
} }
func (s *InputState) SetSize(width int) { func (s *InputState) SetSize(width int) {
+18 -19
View File
@@ -57,8 +57,7 @@ type State struct {
func NewMainScreen(client *irc.Client) State { func NewMainScreen(client *irc.Client) State {
newViewport := viewport.New(0, 0) newViewport := viewport.New(0, 0)
newViewport.Wrap = viewport.Wrap newViewport.Style = MessagesStyle
newViewport.Style = MessagesStyle.Copy()
return State{ return State{
Client: client, Client: client,
@@ -205,35 +204,35 @@ func (s State) Update(msg tea.Msg) (State, tea.Cmd) {
} }
func (s *State) Focus() { func (s *State) Focus() {
s.Viewport.Style = s.Viewport.Style.Copy().BorderForeground(ui.AccentColor) s.Viewport.Style = s.Viewport.Style.BorderForeground(ui.AccentColor)
tab = tab.Copy().BorderForeground(ui.AccentColor) tab = tab.BorderForeground(ui.AccentColor)
leftArrowDim = leftArrowDim.Copy().BorderForeground(ui.AccentColor) leftArrowDim = leftArrowDim.BorderForeground(ui.AccentColor)
rightArrowDim = rightArrowDim.Copy().BorderForeground(ui.AccentColor) rightArrowDim = rightArrowDim.BorderForeground(ui.AccentColor)
leftArrowLit = leftArrowLit.Copy().BorderForeground(ui.AccentColor) leftArrowLit = leftArrowLit.BorderForeground(ui.AccentColor)
rightArrowLit = rightArrowLit.Copy().BorderForeground(ui.AccentColor) rightArrowLit = rightArrowLit.BorderForeground(ui.AccentColor)
tabLine = tabLine.Copy().Foreground(ui.AccentColor) tabLine = tabLine.Foreground(ui.AccentColor)
} }
func (s *State) Blur() { func (s *State) Blur() {
s.Viewport.Style = s.Viewport.Style.Copy().BorderForeground(ui.PrimaryColor) s.Viewport.Style = s.Viewport.Style.BorderForeground(ui.PrimaryColor)
tab = tab.Copy().BorderForeground(ui.PrimaryColor) tab = tab.BorderForeground(ui.PrimaryColor)
leftArrowDim = leftArrowDim.Copy().BorderForeground(ui.PrimaryColor) leftArrowDim = leftArrowDim.BorderForeground(ui.PrimaryColor)
rightArrowDim = rightArrowDim.Copy().BorderForeground(ui.PrimaryColor) rightArrowDim = rightArrowDim.BorderForeground(ui.PrimaryColor)
leftArrowLit = leftArrowLit.Copy().BorderForeground(ui.PrimaryColor) leftArrowLit = leftArrowLit.BorderForeground(ui.PrimaryColor)
rightArrowLit = rightArrowLit.Copy().BorderForeground(ui.PrimaryColor) rightArrowLit = rightArrowLit.BorderForeground(ui.PrimaryColor)
tabLine = tabLine.Copy().Foreground(ui.PrimaryColor) tabLine = tabLine.Foreground(ui.PrimaryColor)
} }
func (s *State) SetSize(width, height int) { func (s *State) SetSize(width, height int) {
s.InputBox.SetSize(width) s.InputBox.SetSize(width)
s.SidePanel.SetSize(width, height, s.InputBox.Style.GetVerticalFrameSize()) s.SidePanel.SetSize(width, height, s.InputBox.Style.GetVerticalPadding())
// We floor because width is an int and some fractions are lost when casting // We floor because width is an int and some fractions are lost when casting
// and also because we ceil the sidepanel's width // and also because we ceil the sidepanel's width
// -3 for the tab bar height // -3 for the tab bar height
newWidth := int(math.Floor(float64(width)*8/10) - float64(s.Viewport.Style.GetHorizontalFrameSize())) newWidth := int(math.Floor(float64(width) * 8 / 10))
newHeight := height - s.InputBox.Style.GetVerticalFrameSize() - s.Viewport.Style.GetVerticalFrameSize() - 3 newHeight := height - s.InputBox.Style.GetVerticalFrameSize() - 3 - 1
s.Viewport.Width = newWidth s.Viewport.Width = newWidth
s.Viewport.Height = newHeight s.Viewport.Height = newHeight
+3 -4
View File
@@ -39,7 +39,7 @@ func (s *SidePanelState) getHeader() string {
if len(s.Client.Channels) != 0 { if len(s.Client.Channels) != 0 {
usersCount = len(s.Client.Channels[s.Client.ActiveChannelIndex].Users) usersCount = len(s.Client.Channels[s.Client.ActiveChannelIndex].Users)
} }
separator := strings.Repeat("—", s.Viewport.Width) + "\n" separator := strings.Repeat("—", s.Viewport.Width-s.Viewport.Style.GetHorizontalFrameSize()) + "\n"
header := fmt.Sprintf("Users in this channel (%d)\n", usersCount) + separator header := fmt.Sprintf("Users in this channel (%d)\n", usersCount) + separator
return header return header
@@ -60,8 +60,7 @@ func (s *SidePanelState) getLatestNicks() []string {
func NewSidePanel(client *irc.Client) *SidePanelState { func NewSidePanel(client *irc.Client) *SidePanelState {
newViewport := viewport.New(0, 0) newViewport := viewport.New(0, 0)
newViewport.Style = SidePanelStyle.Copy() newViewport.Style = SidePanelStyle
newViewport.Wrap = viewport.Wrap
return &SidePanelState{ return &SidePanelState{
Client: client, Client: client,
@@ -119,7 +118,7 @@ func (s *SidePanelState) UpdateNicks() {
func (s *SidePanelState) SetSize(width, height, inputboxHeight int) { func (s *SidePanelState) SetSize(width, height, inputboxHeight int) {
// We ceil here because width is an int and some fractions are lost // 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 // -1 is for some extra invisible margin or padding between the sidepanel and the inputbox
newWidth := int(math.Ceil(math.Ceil((float64(width) * 2 / 10)) - float64(s.Viewport.Style.GetHorizontalFrameSize()))) newWidth := int(math.Ceil((float64(width) * 2 / 10)))
newHeight := height - inputboxHeight - s.Viewport.Style.GetVerticalFrameSize() - 1 newHeight := height - inputboxHeight - s.Viewport.Style.GetVerticalFrameSize() - 1
s.Viewport.Style = s.Viewport.Style.Width(newWidth) s.Viewport.Style = s.Viewport.Style.Width(newWidth)
+8 -8
View File
@@ -22,16 +22,16 @@ import (
) )
var ( var (
InputboxStyle = ui.DefaultStyle.Copy(). InputboxStyle = ui.DefaultStyle.
Border(lipgloss.NormalBorder(), true). Border(lipgloss.NormalBorder(), true).
Padding(0, 1). Padding(0, 1).
Width(ui.MainStyle.GetWidth()). Width(ui.MainStyle.GetWidth()).
BorderForeground(ui.AccentColor) BorderForeground(ui.AccentColor)
MessagesStyle = ui.DefaultStyle.Copy(). MessagesStyle = ui.DefaultStyle.
Border(lipgloss.NormalBorder(), false, true, true, true). Border(lipgloss.NormalBorder(), false, true, true, true).
Width(ui.MainStyle.GetWidth() * 8 / 10). Width(ui.MainStyle.GetWidth() * 8 / 10).
BorderForeground(ui.PrimaryColor) BorderForeground(ui.PrimaryColor)
SidePanelStyle = ui.DefaultStyle.Copy(). SidePanelStyle = ui.DefaultStyle.
Border(lipgloss.NormalBorder(), true). Border(lipgloss.NormalBorder(), true).
Width(ui.MainStyle.GetWidth() * 2 / 10). Width(ui.MainStyle.GetWidth() * 2 / 10).
BorderForeground(ui.PrimaryColor) BorderForeground(ui.PrimaryColor)
@@ -85,26 +85,26 @@ var (
Foreground(ui.PrimaryColor). Foreground(ui.PrimaryColor).
Padding(0, 1) Padding(0, 1)
activeTab = tab.Copy(). activeTab = tab.
Border(activeTabBorder, true). Border(activeTabBorder, true).
BorderForeground(ui.AccentColor). BorderForeground(ui.AccentColor).
Foreground(ui.AccentColor). Foreground(ui.AccentColor).
Italic(true). Italic(true).
Bold(true) Bold(true)
leftArrowDim = tab.Copy(). leftArrowDim = tab.
Border(leftArrowBorder, true). Border(leftArrowBorder, true).
BorderForeground(ui.PrimaryColor). BorderForeground(ui.PrimaryColor).
Foreground(ui.DisabledColor) Foreground(ui.DisabledColor)
rightArrowDim = tab.Copy(). rightArrowDim = tab.
Border(rightArrowBorder, true). Border(rightArrowBorder, true).
BorderForeground(ui.PrimaryColor). BorderForeground(ui.PrimaryColor).
Foreground(ui.DisabledColor) Foreground(ui.DisabledColor)
leftArrowLit = tab.Copy(). leftArrowLit = tab.
Border(leftArrowBorder, true). Border(leftArrowBorder, true).
Foreground(ui.PrimaryColor) Foreground(ui.PrimaryColor)
rightArrowLit = tab.Copy(). rightArrowLit = tab.
Border(rightArrowBorder, true). Border(rightArrowBorder, true).
Foreground(ui.PrimaryColor) Foreground(ui.PrimaryColor)