Compare commits

...
17 Commits
Author SHA1 Message Date
Illusion d8804add60 fix: .Local() message time 2025-09-02 06:53:29 +02:00
Illusion 096743adca fix: also append the day's date if we append a message with a different date than the last channel date 2025-08-29 15:59:46 +02:00
Illusion f88c2af404 WIP: refactor 2025-08-18 21:10:04 +02:00
Illusion 573a0da672 feat: handle KICK command/reply 2025-08-15 22:21:34 +02:00
Illusion 026021c118 fix: only join the initial channel after registration is complete
During registration, only a subset of commands are allowed.
Servers MUST send a RPL_WELCOME (001) after registration is complete so
we join the channel in response to that numeric reply.

https://modern.ircdocs.horse/#connection-registration
2025-08-15 22:18:55 +02:00
Illusion 1d10198d37 feat: handle ERR_NOSUCHNICK 2025-08-15 22:17:13 +02:00
Illusion eab5f8747a feat: support and handle changing nicknames 2025-08-15 21:35:27 +02:00
Illusion ff0e201e02 feat: support sending and receiving direct messages from other users 2025-08-15 21:34:04 +02:00
Illusion 0e213c691a feat: implement some miscellaneous numeric replies and errors 2025-08-15 21:21:21 +02:00
Illusion d5a99ada51 fix: better formatting for ERR_NEEDMOREPARAMS; display better information for LOCALUSERS and GLOBALUSERS 2025-08-15 21:18:40 +02:00
Illusion 41645fb679 feat: implement ISUPPORT and add support for a few easy features
fix: CAP ACK commands with no acknowledged capabilities causing an OOB.

Also log unsupported and unknown capabilities and features.
2025-08-15 21:10:52 +02:00
Illusion 4be731c813 irc: NOT IMPL -> UNIMPL; red color for error messages; deduplicate some code 2025-08-15 20:18:24 +02:00
Illusion 71471a0f69 sidepanel: sort user nicks alphabetically and ascendingly 2025-08-15 19:53:09 +02:00
Illusion 7e2f446671 refactor/feat: move IRCMessage to irc package; handle NOTICE commands 2025-08-13 13:05:15 +02:00
Illusion 6c2e54275c feat: implement TOPIC-related commands and replies 2025-08-13 11:31:16 +02:00
Illusion 772365ef13 fix(login): manually set the width for the textinput fields 2025-08-11 23:39:55 +02:00
Illusion e24201c204 chore: upgrade dependencies. remove deprecated code. conform to linter style 2025-08-11 22:51:50 +02:00
17 changed files with 1240 additions and 532 deletions
+11 -9
View File
@@ -17,7 +17,6 @@
package cmds
import (
"fmt"
"time"
tea "github.com/charmbracelet/bubbletea"
@@ -32,7 +31,7 @@ func Connect() tea.Msg {
func Quit(client *irc.Client) tea.Cmd {
return func() tea.Msg {
if client.TcpConn != nil {
if client.TCPConn != nil {
client.SendCommand("QUIT")
}
@@ -41,18 +40,15 @@ func Quit(client *irc.Client) tea.Cmd {
}
type SendPrivMsgMsg struct {
Msg string
Timestamp string
Msg string
Datetime time.Time
}
func SendPrivMsg(msg string) tea.Cmd {
return func() tea.Msg {
now := time.Now()
timestamp := fmt.Sprintf("[%02d:%02d]", now.Hour(), now.Minute())
return SendPrivMsgMsg{
Msg: msg,
Timestamp: timestamp,
Msg: msg,
Datetime: time.Now(),
}
}
}
@@ -69,6 +65,12 @@ func SwitchChannels() tea.Msg {
return SwitchChannelsMsg{}
}
type UpdateNicksMsg struct{}
func UpdateNicks() tea.Msg {
return UpdateNicksMsg{}
}
type UpdateTabBarMsg struct{}
func UpdateTabBar() tea.Msg {
+20 -17
View File
@@ -1,30 +1,33 @@
module github.com/illusionman1212/gorc
go 1.19
go 1.23.0
replace github.com/charmbracelet/bubbles => /home/illusion/Desktop/repos/bubbles
toolchain go1.24.6
require (
github.com/charmbracelet/bubbles v0.15.0
github.com/charmbracelet/bubbletea v0.23.2
github.com/charmbracelet/lipgloss v0.6.0
github.com/charmbracelet/bubbles v0.21.0
github.com/charmbracelet/bubbletea v1.3.6
github.com/charmbracelet/lipgloss v1.1.0
)
require (
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52 v1.2.1 // indirect
github.com/containerd/console v1.0.3 // 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.17 // 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.14 // indirect
github.com/muesli/ansi v0.0.0-20221106050444-61f0cd9a192a // 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/reflow v0.3.0 // indirect
github.com/muesli/termenv v0.14.0 // indirect
github.com/rivo/uniseg v0.4.3 // indirect
golang.org/x/sync v0.1.0 // indirect
golang.org/x/sys v0.5.0 // indirect
golang.org/x/term v0.5.0 // indirect
golang.org/x/text v0.7.0 // 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
)
+40 -53
View File
@@ -1,62 +1,49 @@
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
github.com/aymanbagabas/go-osc52 v1.0.3/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4=
github.com/aymanbagabas/go-osc52 v1.2.1 h1:q2sWUyDcozPLcLabEMd+a+7Ea2DitxZVN9hTxab9L4E=
github.com/aymanbagabas/go-osc52 v1.2.1/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4=
github.com/charmbracelet/bubbles v0.15.0 h1:c5vZ3woHV5W2b8YZI1q7v4ZNQaPetfHuoHzx+56Z6TI=
github.com/charmbracelet/bubbles v0.15.0/go.mod h1:Y7gSFbBzlMpUDR/XM9MhZI374Q+1p1kluf1uLl8iK74=
github.com/charmbracelet/bubbletea v0.23.1/go.mod h1:JAfGK/3/pPKHTnAS8JIE2u9f61BjWTQY57RbT25aMXU=
github.com/charmbracelet/bubbletea v0.23.2 h1:vuUJ9HJ7b/COy4I30e8xDVQ+VRDUEFykIjryPfgsdps=
github.com/charmbracelet/bubbletea v0.23.2/go.mod h1:FaP3WUivcTM0xOKNmhciz60M6I+weYLF76mr1JyI7sM=
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
github.com/charmbracelet/lipgloss v0.6.0 h1:1StyZB9vBSOyuZxQUcUwGr17JmojPNm87inij9N3wJY=
github.com/charmbracelet/lipgloss v0.6.0/go.mod h1:tHh2wr34xcHjC2HCXIlGSG1jaDF0S0atAUvBMP6Ppuk=
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/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/charmbracelet/bubbles v0.21.0 h1:9TdC97SdRVg/1aaXNVWfFH3nnLAwOXr8Fn6u6mfQdFs=
github.com/charmbracelet/bubbles v0.21.0/go.mod h1:HF+v6QUR4HkEpz62dx7ym2xc71/KBHg+zKwJtMw+qtg=
github.com/charmbracelet/bubbletea v1.3.6 h1:VkHIxPJQeDt0aFJIsVxw8BQdh/F/L2KKZGsK6et5taU=
github.com/charmbracelet/bubbletea v1.3.6/go.mod h1:oQD9VCRQFF8KplacJLo28/jofOI2ToOfGYeFgBBxHOc=
github.com/charmbracelet/colorprofile v0.3.1 h1:k8dTHMd7fgw4bnFd7jXTLZrSU/CQrKnL3m+AxCzDz40=
github.com/charmbracelet/colorprofile v0.3.1/go.mod h1:/GkGusxNs8VB/RSOh3fu0TJmQ4ICMMPApIIVn0KszZ0=
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/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
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/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU=
github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
github.com/muesli/ansi v0.0.0-20221106050444-61f0cd9a192a h1:jlDOeO5TU0pYlbc/y6PFguab5IjANI0Knrpg3u/ton4=
github.com/muesli/ansi v0.0.0-20221106050444-61f0cd9a192a/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
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/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.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs=
github.com/muesli/termenv v0.13.0/go.mod h1:sP1+uffeLaEYpyOTb8pLCUctGcGLnoFjSn4YJK5e2bc=
github.com/muesli/termenv v0.14.0 h1:8x9NFfOe8lmIWK4pgy3IfVEy47f+ppe3tUqdPZG2Uy0=
github.com/muesli/termenv v0.14.0/go.mod h1:kG/pF1E7fh949Xhe156crRUrHNyK221IuGO7Ez60Uc8=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.3 h1:utMvzDsuh3suAEnhH0RdHmoPbU648o6CvXxTx4SBMOw=
github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
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-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
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=
+107 -48
View File
@@ -30,11 +30,27 @@ import (
"github.com/illusionman1212/gorc/ui"
)
type Message struct {
DateTime time.Time
Tags MessageTags // 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 MessageTags map[string]string
type User struct {
// User prefix in channel
Prefix string
}
type Node[T any] struct {
Next *Node[T]
Prev *Node[T]
Value T
}
type Channel struct {
// Channel name
Name string
@@ -45,6 +61,9 @@ type Channel struct {
// Channel messages history
History string
// Date of the last message we received in this channel
LastUpdatedDate time.Time
// Users in this channel
// The map key is the user's nickname
// and the user struct holds data about that user
@@ -54,7 +73,7 @@ type Channel struct {
type Client struct {
// tcp connection
TcpConn net.Conn
TCPConn net.Conn
// Host that client is connected to
Host string
@@ -65,54 +84,75 @@ type Client struct {
// Nickname currently in use by the user
Nickname string
// Joined channels
Channels []Channel
// The root of the doubly linked list of channel
RootChannel *Node[Channel]
// Active channel name
ActiveChannel string
// Active channel
ActiveChannel *Node[Channel]
// Active channel index
ActiveChannelIndex int
// The channel to join immediately after registration completes.
InitialChannel string
// Reference to the bubbletea program
Tea *tea.Program
// Index of the first visible tab in the tab bar
FirstTabIndexInTabBar int
// FirstTabIndexInTabBar int
// Index of the last visible tab in the tab bar
LastTabIndexInTabBar int
// LastTabIndexInTabBar int
// The acknowledged capabilities
EnabledCapabilities Capabilities
// The features currently enabled for this client
EnabledFeatures Features
}
type Capabilities map[string]string
type Features map[string]string
type MsgFmtOpts struct {
WithTimestamp bool
AsServerMsg bool
NotImpl bool
AsDate bool
// TODO: make these a bitset? they should compliment eachother instead of conflicting or rendering twice.
AsServerMsg bool
AsErrorMsg bool
NotImpl bool
}
var notImpl = lipgloss.NewStyle().Foreground(ui.ErrorColor).Render("[NOT IMPL]")
var unimpl = lipgloss.NewStyle().Foreground(ui.ErrorColor).Render("[UNIMPL]")
var serverMsgStyle = lipgloss.NewStyle().Foreground(ui.ServerMsgColor)
var timestampStyle = serverMsgStyle.Copy()
var errorMsgStyle = lipgloss.NewStyle().Foreground(ui.ErrorColor)
var timestampStyle = serverMsgStyle
var dateStyle = lipgloss.NewStyle().Foreground(ui.DateColor)
const CRLF = "\r\n"
func (c *Channel) AppendMsg(timestamp string, fullMsg string, opts MsgFmtOpts) {
func (m *Message) SetTimestamp() {
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.DateTime = t.Local()
} else {
m.DateTime = time.Now()
}
}
func (c *Channel) AppendMsg(datetime time.Time, fullMsg string, opts MsgFmtOpts) {
prefixes := ""
style := ui.DefaultStyle
if opts.WithTimestamp {
prefixes += timestampStyle.Render(timestamp) + " "
str := fmt.Sprintf("[%02d:%02d]", datetime.Hour(), datetime.Minute())
prefixes += timestampStyle.Render(str) + " "
}
if opts.NotImpl {
prefixes += notImpl + " "
prefixes += unimpl + " "
}
if opts.AsServerMsg {
@@ -120,16 +160,28 @@ func (c *Channel) AppendMsg(timestamp string, fullMsg string, opts MsgFmtOpts) {
style = serverMsgStyle
}
if opts.AsDate {
style = dateStyle
if opts.AsErrorMsg {
prefixes += errorMsgStyle.Render("==") + " "
style = errorMsgStyle
}
if datetime.Year() > c.LastUpdatedDate.Year() || datetime.YearDay() > c.LastUpdatedDate.YearDay() {
c.LastUpdatedDate = datetime
dateMsg := fmt.Sprintf("————— %s %d —————", c.LastUpdatedDate.Month().String(), c.LastUpdatedDate.Day())
c.History += dateStyle.Render(dateMsg) + 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)
c.Host = host
c.Port = port
c.EnabledCapabilities = make(Capabilities, 0)
c.EnabledFeatures = make(Features, 0)
if tlsEnabled {
cfg := &tls.Config{ServerName: host}
conn, err := tls.Dial("tcp", addr, cfg)
@@ -138,12 +190,7 @@ func (s *Client) Initialize(host string, port string, tlsEnabled bool) {
log.Fatal(err)
}
s.TcpConn = conn
s.Host = host
s.Port = port
s.ActiveChannel = host
s.Channels = make([]Channel, 0)
s.EnabledCapabilities = make(Capabilities, 0)
c.TCPConn = conn
return
}
@@ -157,19 +204,20 @@ func (s *Client) Initialize(host string, port string, tlsEnabled bool) {
log.Fatal(err)
}
s.TcpConn = conn
s.Host = host
s.Port = port
s.ActiveChannel = host
s.Channels = make([]Channel, 0)
s.EnabledCapabilities = make(Capabilities, 0)
c.TCPConn = conn
}
func (c *Client) Register(nick string, password string, channel string) {
c.Channels = append(c.Channels, Channel{
hostChannel := Channel{
Name: c.Host,
Users: make(map[string]User),
})
}
root := &Node[Channel]{Value: hostChannel}
root.Next = root
root.Prev = root
c.ActiveChannel = root
c.RootChannel = root
c.SendCommand(commands.CAP, "LS", "302")
if password != "" {
@@ -179,26 +227,37 @@ func (c *Client) Register(nick string, password string, channel string) {
// set user-wanted nickname
c.Nickname = nick
c.SendCommand(commands.USER, nick, "0", "*", nick)
// joining a channel when registering is optional
if channel != "" {
c.SendCommand(commands.JOIN, channel)
}
}
func (c *Client) SetDay() {
msgOpts := MsgFmtOpts{
AsDate: true,
func (c *Client) AppendChannel(channel Channel) *Node[Channel] {
first := c.RootChannel
last := c.RootChannel.Prev
newNode := &Node[Channel]{
Value: channel,
Next: first,
Prev: last,
}
now := time.Now()
msg := fmt.Sprintf("————— %s %d —————", now.Month().String(), now.Day())
c.Channels[0].AppendMsg("", msg, msgOpts)
last.Next = newNode
first.Prev = newNode
// TODO: append new day to each channel whenever the day changes
return newNode
}
func (c Client) SendCommand(cmd string, params ...string) {
if c.TcpConn == nil {
func (c *Client) RemoveChannel(channel *Node[Channel]) {
prev := channel.Prev
next := channel.Next
prev.Next = next
next.Prev = prev
channel.Prev = nil
channel.Next = nil
}
func (c *Client) SendCommand(cmd string, params ...string) {
if c.TCPConn == nil {
// TODO: properly handle the error instead of Fatal-ing
log.Fatal("Attempted to write data to nil connection")
}
@@ -229,5 +288,5 @@ func (c Client) SendCommand(cmd string, params ...string) {
}
}
c.TcpConn.Write([]byte(cmd + paramsString + CRLF))
c.TCPConn.Write([]byte(cmd + paramsString + CRLF))
}
+72 -69
View File
@@ -58,7 +58,7 @@ const (
NOTICE = "NOTICE" // Send notices between users and to channels.
// Operator Messages
KILL = "KILL" // CLose connection between a client and the server. IRC Operators only (do we need this ??)
KILL = "KILL" // Close connection between a client and the server. IRC Operators only (do we need this ??)
// Optional Messages
AWAY = "AWAY" // Indicate that the client (user) is away/afk/etc..
@@ -167,12 +167,12 @@ const (
RPL_GLOBALUSERS = "266" // aircd, Hybird, Bahamut - Implemented
RPL_WHOISCERTFP = "276" // oftc-hybrid - Not Implemented - Has Conflicts (TODO:)
RPL_NONE = "300" // RFC1459 - Not Implemented (TODO:)
RPL_AWAY = "301" // RFC1459 - Not Implemented (TODO:)
RPL_AWAY = "301" // RFC1459 - Implemented
RPL_USERHOST = "302" // RFC1459 - Not Implemented (TODO:)
RPL_ISON = "303" // RFC1459 - Not Implemented (TODO:)
RPL_TEXT = "304" // irc2 - Not Implemented (TODO:)
RPL_UNAWAY = "305" // RFC1459 - Not Implemented (TODO:)
RPL_NOWAWAY = "306" // RFC1459 - Not Implemented (TODO:)
RPL_UNAWAY = "305" // RFC1459 - Implemented
RPL_NOWAWAY = "306" // RFC1459 - Implemented
RPL_WHOISUSER = "311" // RFC1459 - Implemented
RPL_WHOISSERVER = "312" // RFC1459 - Implemented
RPL_WHOISOPERATOR = "313" // RFC1459 - Not Implemented (TODO:)
@@ -187,8 +187,8 @@ const (
RPL_CHANNELMODEIS = "324" // RFC1459 - Not Implemented (TODO:)
RPL_UNIQOPIS = "325" // RFC2812 - Not Implemented - Has Conflicts (TODO:)
RPL_CREATIONTIME = "329" // Bahamut,InspIRCd - Not Implemented (TODO:)
RPL_NOTOPIC = "331" // RFC1459 - Not Implemented (TODO:)
RPL_TOPIC = "332" // RFC1459 - Not Implemented (TODO:)
RPL_NOTOPIC = "331" // RFC1459 - Implemented
RPL_TOPIC = "332" // RFC1459 - Implemented
RPL_TOPICWHOTIME = "333" // ircu,InspIRCd - Not Implemented (TODO:)
RPL_INVITING = "341" // RFC1459 - Not Implemented (TODO:)
RPL_SUMMONING = "342" // RFC1459 - Not Implemented - Deprecated (TODO:)
@@ -197,7 +197,7 @@ const (
RPL_EXCEPTLIST = "348" // RFC2812 - Not Implemented (TODO:)
RPL_ENDOFEXCEPTLIST = "349" // RFC2812 - Not Implemented (TODO:)
RPL_WHOISGATEWAY = "350" // InspIRCd - Not Implemented (TODO:)
RPL_VERSION = "351" // RFC1459 - Not Implemented (TODO:)
RPL_VERSION = "351" // RFC1459 - Implemented
RPL_WHOREPLY = "352" // RFC1459 - Not Implemented (TODO:)
RPL_NAMREPLY = "353" // RFC1459 - Implemented
RPL_KILLDONE = "361" // RFC1459 - Not Implemented - Deprecated (TODO:)
@@ -209,10 +209,10 @@ const (
RPL_BANLIST = "367" // RFC1459 - Not Implemented (TODO:)
RPL_ENDOFBANLIST = "368" // RFC1459 - Not Implemented (TODO:)
RPL_ENDOFWHOWAS = "369" // RFC1459 - Not Implemented (TODO:)
RPL_INFO = "371" // RFC1459 - Not Implemented (TODO:)
RPL_INFO = "371" // RFC1459 - Implemented
RPL_MOTD = "372" // RFC1459 - Implemented
RPL_INFOSTART = "373" // RFC1459 - Not Implemented - Deprecated (TODO:)
RPL_ENDOFINFO = "374" // RFC1459 - Not Implemented (TODO:)
RPL_ENDOFINFO = "374" // RFC1459 - Implemented
RPL_MOTDSTART = "375" // RFC1459 - Implemented
RPL_ENDOFMOTD = "376" // RFC1459 - Not Implemented (TODO:)
RPL_WHOISHOST = "378" // ??????? - Implemented - Has Conflicts
@@ -228,11 +228,11 @@ const (
RPL_NOUSERS = "395" // RFC1459 - Not Implemented (TODO:)
ERR_UNKNOWNERROR = "400" // ??????? - Not Implemented (TODO:)
ERR_NOSUCHNICK = "401" // RFC1459 - Not Implemented (TODO:)
ERR_NOSUCHNICK = "401" // RFC1459 - Implemented
ERR_NOSUCHSERVER = "402" // RFC1459 - Implemented
ERR_NOSUCHCHANNEL = "403" // RFC1459 - Not Implemented (TODO:)
ERR_CANNOTSENDTOCHAN = "404" // RFC1459 - Not Implemented (TODO:)
ERR_TOOMANYCHANNELS = "405" // RFC1459 - Not Implemented (TODO:)
ERR_NOSUCHCHANNEL = "403" // RFC1459 - Implemented
ERR_CANNOTSENDTOCHAN = "404" // RFC1459 - Implemented
ERR_TOOMANYCHANNELS = "405" // RFC1459 - Implemented
ERR_WASNOSUCHNICK = "406" // RFC1459 - Not Implemented (TODO:)
ERR_NOSUCHSERVICE = "408" // RFC2812 - Not Implemented - Has Conflicts (TODO:)
ERR_NOORIGIN = "409" // RFC1459 - Not Implemented (TODO:)
@@ -242,7 +242,7 @@ const (
ERR_WILDTOPLEVEL = "414" // RFC1459 - Not Implemented (TODO:)
ERR_BADMASK = "415" // RFC2812 - Not Implemented (TODO:)
ERR_UNKNOWNCOMMAND = "421" // RFC1459 - Implemented
ERR_NOMOTD = "422" // RFC1459 - Not Implemented (TODO:)
ERR_NOMOTD = "422" // RFC1459 - Implemented
ERR_NOADMININFO = "423" // RFC1459 - Not Implemented (TODO:)
ERR_FILEERROR = "424" // RFC1459 - Not Implemented (TODO:)
ERR_NONICKNAMEGIVEN = "431" // RFC1459 - Implemented
@@ -269,10 +269,10 @@ const (
ERR_INVITEONLYCHAN = "473" // RFC1459 - Not Implemented (TODO:)
ERR_BANNEDFROMCHAN = "474" // RFC1459 - Not Implemented (TODO:)
ERR_BADCHANNELKEY = "475" // RFC1459 - Not Implemented (TODO:)
ERR_BADCHANMASK = "476" // RFC2812 - Not Implemented (TODO:)
ERR_BADCHANMASK = "476" // RFC2812 - Implemented
ERR_BANLISTFULL = "478" // RFC2812 - Not Implemented (TODO:)
ERR_NOPRIVILEGES = "481" // RFC1459 - Not Implemented (TODO:)
ERR_CHANOPRIVSNEEDED = "482" // RFC1459 - Not Implemented (TODO:)
ERR_CHANOPRIVSNEEDED = "482" // RFC1459 - Implemented
ERR_CANTKILLSERVER = "483" // RFC1459 - Not Implemented (TODO:)
ERR_RESTRICTED = "484" // RFC2812 - Not Implemented - Has Conflicts (TODO:)
ERR_UNIQOPRIVSNEEDED = "485" // RFC2812 - Not Implemented (TODO:)
@@ -305,64 +305,67 @@ const (
/* --- RPL_ISUPPORT Parameters --- */
var Features = map[string]bool{
"ACCEPT": true,
"ACCEPT": false,
// The AWAYLEN parameter indicates the maximum length for the <reason> of an AWAY command.
// If an AWAY <reason> has more characters than this parameter, it may be silently truncated by the server before being passed on to other clients.
// Clients MAY receive an AWAY <reason> that has more characters than this parameter
"AWAYLEN": true,
"BOT": true,
"CALLERID": true,
"CASEMAPPING": true,
"CHANLIMIT": true,
"CHANMODES": true,
"CHANNELLEN": true,
"CHANTYPES": true, // Channel Types. Default is #. Available are #&
"CHARSET": true, // Deprecated but might still be used
"CLIENTVER": true, // Deprecated but might still be used
"CNOTICE": true,
"CPRIVMSG": true,
"DEAF": true,
"ELIST": true,
"ESILENCE": true,
"ETRACE": true,
"EXCEPTS": true,
"EXTBAN": true,
"FNC": true, // Deprecated but might still be used
"INVEX": true,
"KEYLEN": true,
"BOT": false,
"CALLERID": false,
"CASEMAPPING": false,
"CHANLIMIT": false,
"CHANMODES": false,
"CHANNELLEN": false,
"CHANTYPES": false, // Channel Types. Default is #. Available are #&
"CHARSET": false, // Deprecated but might still be used
"CLIENTVER": false, // Deprecated but might still be used
"CNOTICE": false,
"CPRIVMSG": false,
"DEAF": false,
"ELIST": false,
"ESILENCE": false,
"ETRACE": false,
"EXCEPTS": false,
"EXTBAN": false,
"FNC": false, // Deprecated but might still be used
"INVEX": false,
"KEYLEN": false,
"KICKLEN": true,
"KNOCK": true,
"LINELEN": true, // Proposed
"MAP": true, // Deprecated but might still be used
"MAXBANS": true, // Deprecated but might still be used
"MAXCHANNELS": true, // Deprecated but might still be used
"MAXLIST": true,
"MAXNICKLEN": true,
"MAXPARA": true, // Deprecated but might still be used
"MAXTARGETS": true,
"METADATA": true,
"MODES": true,
"MONITOR": true,
"NAMESX": true, // Deprecated but might still be used
"KNOCK": false,
"LINELEN": false, // Proposed
"MAP": false, // Deprecated but might still be used
"MAXBANS": false, // Deprecated but might still be used
"MAXCHANNELS": false, // Deprecated but might still be used
"MAXLIST": false,
"MAXNICKLEN": false,
"MAXPARA": false, // Deprecated but might still be used
"MAXTARGETS": false,
"METADATA": false,
"MODES": false,
"MONITOR": false,
"NAMESX": false, // Deprecated but might still be used
"NETWORK": true,
"NICKLEN": true,
"OVERRIDE": true,
"PREFIX": true,
"SAFELIST": true,
"SECURELIST": true,
"SILENCE": true,
"SSL": true, // Deprecated but might still be used
"STARTTLS": true, // Deprecated but might still be used
"STATUSMSG": true,
"STD": true, // Deprecated but might still be used
"TARGMAX": true,
"OVERRIDE": false,
"PREFIX": false,
"SAFELIST": false,
"SECURELIST": false,
"SILENCE": false,
"SSL": false, // Deprecated but might still be used
"STARTTLS": false, // Deprecated but might still be used
"STATUSMSG": false,
"STD": false, // Deprecated but might still be used
"TARGMAX": false,
"TOPICLEN": true,
"UHNAMES": true, // Deprecated but might still be used
"USERIP": true,
"USERLEN": true, // Proposed
"VBANLIST": true, // Deprecared but might still be used
"VLIST": true,
"WALLCHOPS": true, // Deprecated but might still be used
"WALLVOICES": true, // Deprecated but might still be used
"WATCH": true,
"WHOX": true,
"UHNAMES": false, // Deprecated but might still be used
"USERIP": false,
"USERLEN": false, // Proposed
"VBANLIST": false, // Deprecared but might still be used
"VLIST": false,
"WALLCHOPS": false, // Deprecated but might still be used
"WALLVOICES": false, // Deprecated but might still be used
"WATCH": false,
"WHOX": false,
}
var Capabilities = map[string]bool{
+664 -132
View File
File diff suppressed because it is too large Load Diff
+66 -5
View File
@@ -17,7 +17,9 @@
package handler
import (
"fmt"
"strings"
"time"
tea "github.com/charmbracelet/bubbletea"
"github.com/illusionman1212/gorc/cmds"
@@ -25,18 +27,75 @@ import (
"github.com/illusionman1212/gorc/irc/commands"
)
func handleSlashPrivMsg(params []string, client *irc.Client) tea.Cmd {
var batchedCmds []tea.Cmd
if len(params) < 2 {
client.SendCommand(commands.PRIVMSG, params...)
return nil
}
target := strings.ToLower(params[0])
msgOpts := irc.MsgFmtOpts{
WithTimestamp: true,
}
now := time.Now()
msg := fmt.Sprintf("%s: %s", client.Nickname, strings.Join(params[1:], " "))
c := client.RootChannel
for {
if c.Value.Name == target {
client.ActiveChannel = c
c.Value.AppendMsg(now, msg, msgOpts)
client.SendCommand(commands.PRIVMSG, params...)
return cmds.SwitchChannels
}
c = c.Next
if c == client.RootChannel {
break
}
}
// If we're messaging a user and their "channel" wasn't found in the previous loop, then create it and append it
if target[0] != '#' && target[0] != '&' {
newChannel := irc.Channel{
Name: target,
Users: map[string]irc.User{target: {}},
}
newChannel.AppendMsg(now, msg, msgOpts)
client.ActiveChannel = client.AppendChannel(newChannel)
batchedCmds = append(batchedCmds, cmds.UpdateTabBar)
}
client.SendCommand(commands.PRIVMSG, params...)
batchedCmds = append(batchedCmds, cmds.SwitchChannels)
return tea.Batch(batchedCmds...)
}
func handleSlashJoin(params []string, client *irc.Client) tea.Cmd {
channel := ""
if len(params) > 0 {
channel = params[0]
channel = strings.ToLower(params[0])
}
for i, c := range client.Channels {
if c.Name == channel {
client.ActiveChannelIndex = i
client.ActiveChannel = channel
c := client.RootChannel
for {
if c.Value.Name == channel {
client.ActiveChannel = c
return cmds.SwitchChannels
}
c = c.Next
if c == client.RootChannel {
break
}
}
client.SendCommand(commands.JOIN, params...)
@@ -52,6 +111,8 @@ func HandleSlashCommand(msg string, client *irc.Client) tea.Cmd {
}
switch command {
case commands.PRIVMSG:
return handleSlashPrivMsg(params, client)
case commands.JOIN:
return handleSlashJoin(params, client)
case commands.QUIT:
+11 -36
View File
@@ -17,37 +17,12 @@
package parser
import (
"fmt"
"log"
"regexp"
"strings"
"time"
"github.com/illusionman1212/gorc/irc"
)
type IRCMessage struct {
Timestamp string
Tags Tags // 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 Tags map[string]string
func (m *IRCMessage) setTimestamp() {
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()
m.Timestamp = fmt.Sprintf("[%02d:%02d]", now.Hour(), now.Minute())
}
}
func escapeCharacters(char rune) string {
switch char {
case 's':
@@ -75,8 +50,8 @@ func escapeCharacters(char rune) string {
}
}
func parseTags(rawTags string) Tags {
tags := make(Tags, 0)
func parseTags(rawTags string) irc.MessageTags {
tags := make(irc.MessageTags, 0)
rawTagsSlice := strings.Split(rawTags, ";")
for _, tag := range rawTagsSlice {
@@ -113,16 +88,16 @@ func parseTags(rawTags string) Tags {
return tags
}
func ParseIRCMessage(line string) (IRCMessage, bool) {
ircMessage := IRCMessage{}
multipleSpacesRegex := regexp.MustCompile("[^\\S\\t]+")
trailingParamRegex := regexp.MustCompile("[^\\S\\t]+:")
whitespaceRegex := regexp.MustCompile("^[^\\S\\t]+$")
func ParseIRCMessage(line string) (irc.Message, bool) {
ircMessage := irc.Message{}
multipleSpacesRegex := regexp.MustCompile(`[^\S\t]+`)
trailingParamRegex := regexp.MustCompile(`[^\S\t]+:`)
whitespaceRegex := regexp.MustCompile(`^[^\S\t]+$`)
if len(line) <= 0 {
// TODO: do something here ???
// log.Println("empty message")
return IRCMessage{}, false
return irc.Message{}, false
}
if line[0] == '@' {
@@ -169,7 +144,7 @@ func ParseIRCMessage(line string) (IRCMessage, bool) {
ircMessage.Parameters = finalParams
}
ircMessage.setTimestamp()
ircMessage.SetTimestamp()
return ircMessage, true
}
+2 -3
View File
@@ -35,13 +35,12 @@ func main() {
gorc.Client.Tea = p
f, err := tea.LogToFile("gorc.log", "gorc")
defer f.Close()
if err != nil {
log.Fatal(err)
}
defer f.Close()
err = p.Start()
_, err = p.Run()
if err != nil {
log.Fatal(err)
+19 -8
View File
@@ -41,11 +41,13 @@ type UI struct {
}
type State struct {
UI UI
Client *irc.Client
UI UI
TerminalWidth int
TerminalHeight int
Client *irc.Client
}
func initialUiState(client *irc.Client) UI {
func initialUIState(client *irc.Client) UI {
return UI{
Login: login.NewLogin(),
MainScreen: mainscreen.NewMainScreen(client),
@@ -57,7 +59,7 @@ func InitialState() *State {
return &State{
Client: client,
UI: initialUiState(client),
UI: initialUIState(client),
}
}
@@ -78,8 +80,15 @@ func (s State) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
Width(msg.Width).
Height(msg.Height)
s.UI.Login.SetSize(msg.Width, msg.Height)
s.UI.MainScreen.SetSize(msg.Width, msg.Height)
s.TerminalWidth = msg.Width
s.TerminalHeight = msg.Height
switch s.UI.CurrentScreen {
case Login:
s.UI.Login.SetSize(msg.Width, msg.Height)
case MainScreen:
s.UI.MainScreen.SetSize(msg.Width, msg.Height)
}
return s, nil
case cmds.ConnectMsg:
@@ -91,10 +100,12 @@ func (s State) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
tlsEnabled := s.UI.Login.TLS
s.UI.CurrentScreen = MainScreen
s.Client.InitialChannel = channel
s.Client.Initialize(host, port, tlsEnabled)
s.Client.Register(nickname, password, channel)
s.Client.SetDay()
s.UI.MainScreen.SetSize(s.TerminalWidth, s.TerminalHeight)
go handler.ReadLoop(s.Client)
@@ -125,5 +136,5 @@ func (s State) View() string {
return s.UI.MainScreen.View()
}
return "Error: this screen shouldn't ever show"
panic("Invalid screen")
}
+25 -22
View File
@@ -17,7 +17,6 @@
package login
import (
"fmt"
"strings"
"github.com/charmbracelet/bubbles/textinput"
@@ -44,15 +43,15 @@ func NewLogin() State {
Inputs: make([]textinput.Model, 5),
ConnectButtonBlurredStyle: BlurredDisabledButton,
ConnectButtonFocusedStyle: FocusedDisabledButton,
DialogStyle: DialogStyle.Copy(),
WelcomeMsgStyle: WelcomeMsgStyle.Copy(),
Style: ui.MainStyle.Copy(),
DialogStyle: DialogStyle,
WelcomeMsgStyle: WelcomeMsgStyle,
Style: ui.MainStyle,
}
var t textinput.Model
for i := range state.Inputs {
t = textinput.New()
t.CursorStyle = CursorStyle
t.Cursor.Style = CursorStyle
t.Prompt = ""
switch i {
@@ -60,22 +59,27 @@ func NewLogin() State {
t.Placeholder = "Host"
t.Focus()
t.CharLimit = 40
t.Width = len(t.Placeholder)
t.TextStyle = FocusedStyle
t.Validate = NoSpacesValidation
case 1:
t.Placeholder = "Port"
t.CharLimit = 5
t.Width = len(t.Placeholder)
t.Validate = ValidatePort
case 2:
t.Placeholder = "Channel"
t.CharLimit = 32
t.Width = len(t.Placeholder)
case 3:
t.Placeholder = "Nickname"
t.CharLimit = 32
t.Width = len(t.Placeholder)
t.Validate = NoSpacesValidation
case 4:
t.Placeholder = "Password"
t.CharLimit = 64
t.Width = len(t.Placeholder)
t.EchoMode = textinput.EchoPassword
t.EchoCharacter = '•'
}
@@ -144,6 +148,7 @@ func (s *State) updateInputs(msg tea.Msg) tea.Cmd {
for i := range s.Inputs {
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() != "" {
@@ -160,45 +165,43 @@ func (s *State) updateInputs(msg tea.Msg) tea.Cmd {
}
func (s *State) SetSize(width, height int) {
s.DialogStyle = s.DialogStyle.Copy().Width(width - s.DialogStyle.GetHorizontalFrameSize())
s.DialogStyle = s.DialogStyle.Copy().Height(height*5/10 - s.DialogStyle.GetVerticalFrameSize())
s.DialogStyle = s.DialogStyle.Width(width - s.DialogStyle.GetHorizontalFrameSize())
s.DialogStyle = s.DialogStyle.Height(height*5/10 - s.DialogStyle.GetVerticalFrameSize())
s.WelcomeMsgStyle = s.WelcomeMsgStyle.Copy().Width(width)
s.WelcomeMsgStyle = s.WelcomeMsgStyle.Copy().Height(height * 5 / 10)
s.WelcomeMsgStyle = s.WelcomeMsgStyle.Width(width)
s.WelcomeMsgStyle = s.WelcomeMsgStyle.Height(height * 5 / 10)
}
func (s State) View() string {
var sb strings.Builder
for i := range s.Inputs {
sb.WriteString(s.Inputs[i].View())
if i < len(s.Inputs)-1 {
for i, input := range s.Inputs {
sb.WriteString(input.View())
if i < len(s.Inputs) {
sb.WriteRune('\n')
}
}
checkbox := &BlurredCheckbox
checkbox := BlurredCheckbox
// if the checkbox is focused
if s.FocusIndex == len(s.Inputs) {
checkbox = &FocusedCheckbox
checkbox = FocusedCheckbox
// if tls is enabled
if s.TLS {
checkbox = &FocusedCheckboxChecked
checkbox = FocusedCheckboxChecked
}
}
// if the checkbox is not focused and tls is enabled
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 {
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()))
+6 -6
View File
@@ -72,28 +72,28 @@ var (
FocusedCheckbox = lipgloss.NewStyle().
Foreground(ui.AccentColor).
MarginTop(1).
Render("TLS Enabled [✕]")
Render("TLS Enabled [☓]")
BlurredCheckbox = lipgloss.NewStyle().
Foreground(ui.PrimaryColor).
MarginTop(1).
Render("TLS Enabled [✕]")
Render("TLS Enabled [☓]")
FocusedCheckboxChecked = lipgloss.NewStyle().
Foreground(ui.AccentColor).
MarginTop(1).
Render("TLS Enabled [🗸]")
Render("TLS Enabled [✔]")
BlurredCheckboxChecked = lipgloss.NewStyle().
Foreground(ui.PrimaryColor).
MarginTop(1).
Render("TLS Enabled [🗸]")
Render("TLS Enabled [✔]")
DialogStyle = lipgloss.NewStyle().
Align(lipgloss.Center)
AlignHorizontal(lipgloss.Center)
WelcomeMsgStyle = lipgloss.NewStyle().
Align(lipgloss.Center).
AlignHorizontal(lipgloss.Center).
Foreground(ui.PrimaryColor)
CursorStyle = lipgloss.NewStyle().
+2 -2
View File
@@ -24,7 +24,7 @@ import (
func NoSpacesValidation(input string) error {
if strings.Contains(input, " ") {
return errors.New("Field cannot contain spaces")
return errors.New("field cannot contain spaces")
}
return nil
@@ -37,7 +37,7 @@ func ValidatePort(input string) error {
}
if pNum < 1 || pNum > 65535 {
return errors.New("Invalid port number")
return errors.New("invalid port number")
}
return nil
+3 -3
View File
@@ -37,7 +37,7 @@ func NewInputBox() InputState {
return InputState{
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() {
s.Input.Focus()
s.Style = s.Style.Copy().BorderForeground(ui.AccentColor)
s.Style = s.Style.BorderForeground(ui.AccentColor)
}
func (s *InputState) 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) {
+146 -100
View File
@@ -18,6 +18,7 @@ package mainscreen
import (
"math"
"strconv"
"strings"
"github.com/charmbracelet/bubbles/textinput"
@@ -46,10 +47,10 @@ const (
)
type State struct {
Client *irc.Client
Viewport *viewport.Model
FocusIndex Window
TabRenderingDirection TabDirection
Client *irc.Client
Viewport *viewport.Model
FocusIndex Window
// TabRenderingDirection TabDirection
InputBox InputState
SidePanel *SidePanelState
@@ -57,15 +58,15 @@ type State struct {
func NewMainScreen(client *irc.Client) State {
newViewport := viewport.New(0, 0)
newViewport.Style = MessagesStyle.Copy()
newViewport.Style = MessagesStyle
return State{
Client: client,
Viewport: &newViewport,
FocusIndex: InputBox,
InputBox: NewInputBox(),
SidePanel: NewSidePanel(client),
TabRenderingDirection: Right,
Client: client,
Viewport: &newViewport,
FocusIndex: InputBox,
InputBox: NewInputBox(),
SidePanel: NewSidePanel(client),
// TabRenderingDirection: Right,
}
}
@@ -76,7 +77,7 @@ func (s State) Update(msg tea.Msg) (State, tea.Cmd) {
switch msg := msg.(type) {
case cmds.ReceivedIRCMsgMsg:
wasAtBottom := s.Viewport.AtBottom()
s.Viewport.SetContent(s.Client.Channels[s.Client.ActiveChannelIndex].History)
s.Viewport.SetContent(s.Client.ActiveChannel.Value.History)
if wasAtBottom {
s.Viewport.GotoBottom()
@@ -87,30 +88,31 @@ func (s State) Update(msg tea.Msg) (State, tea.Cmd) {
cmd = handler.HandleSlashCommand(msg.Msg, s.Client)
return s, cmd
} else {
if s.Client.ActiveChannel != s.Client.Host {
if s.Client.ActiveChannel.Value.Name != s.Client.Host {
fullMsg := s.Client.Nickname + ": " + msg.Msg
msgOpts := irc.MsgFmtOpts{
WithTimestamp: true,
}
s.Client.Channels[s.Client.ActiveChannelIndex].AppendMsg(msg.Timestamp, fullMsg, msgOpts)
s.Client.ActiveChannel.Value.AppendMsg(msg.Datetime, fullMsg, msgOpts)
// 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.ActiveChannelIndex].History)
s.Client.SendCommand(commands.PRIVMSG, s.Client.ActiveChannel.Value.Name, msg.Msg)
s.Viewport.SetContent(s.Client.ActiveChannel.Value.History)
s.Viewport.GotoBottom()
}
}
return s, nil
case cmds.SwitchChannelsMsg:
s.Viewport.SetContent(s.Client.Channels[s.Client.ActiveChannelIndex].History)
s.Viewport.SetContent(s.Client.ActiveChannel.Value.History)
// log.Println(s)
s.Viewport.GotoBottom()
*s.SidePanel, cmd = s.SidePanel.Update(msg)
return s, cmd
case cmds.UpdateTabBarMsg:
s.Client.LastTabIndexInTabBar = len(s.Client.Channels) - 1
s.TabRenderingDirection = Left
// s.Client.LastTabIndexInTabBar = len(s.Client.Channels) - 1
// s.TabRenderingDirection = Left
return s, nil
case tea.KeyMsg:
key := msg.String()
@@ -151,31 +153,33 @@ func (s State) Update(msg tea.Msg) (State, tea.Cmd) {
}
// we need this so that the viewport doesnt scroll to the bottom
if len(s.Client.Channels) == 1 {
if s.Client.RootChannel.Next == s.Client.RootChannel {
return s, nil
}
if key == "right" {
s.Client.ActiveChannelIndex++
// s.Client.ActiveChannelIndex++
s.Client.ActiveChannel = s.Client.ActiveChannel.Next
} else {
s.Client.ActiveChannelIndex--
s.Client.ActiveChannel = s.Client.ActiveChannel.Prev
// s.Client.ActiveChannelIndex--
}
if s.Client.ActiveChannelIndex >= len(s.Client.Channels) {
s.Client.ActiveChannelIndex = 0
} else if s.Client.ActiveChannelIndex < 0 {
s.Client.ActiveChannelIndex = len(s.Client.Channels) - 1
}
// if s.Client.ActiveChannelIndex >= len(s.Client.Channels) {
// s.Client.ActiveChannelIndex = 0
// } else if s.Client.ActiveChannelIndex < 0 {
// s.Client.ActiveChannelIndex = len(s.Client.Channels) - 1
// }
if s.Client.ActiveChannelIndex > s.Client.LastTabIndexInTabBar {
s.TabRenderingDirection = Left
s.Client.LastTabIndexInTabBar = s.Client.ActiveChannelIndex
} else if s.Client.ActiveChannelIndex < s.Client.FirstTabIndexInTabBar {
s.Client.FirstTabIndexInTabBar = s.Client.ActiveChannelIndex
s.TabRenderingDirection = Right
}
// if s.Client.ActiveChannelIndex > s.Client.LastTabIndexInTabBar {
// s.TabRenderingDirection = Left
// s.Client.LastTabIndexInTabBar = s.Client.ActiveChannelIndex
// } else if s.Client.ActiveChannelIndex < s.Client.FirstTabIndexInTabBar {
// s.Client.FirstTabIndexInTabBar = s.Client.ActiveChannelIndex
// s.TabRenderingDirection = Right
// }
s.Client.ActiveChannel = s.Client.Channels[s.Client.ActiveChannelIndex].Name
// s.Client.ActiveChannel = s.Client.Channels[s.Client.ActiveChannelIndex].Name
return s, cmds.SwitchChannels
case "g":
if s.FocusIndex == Viewport {
@@ -194,6 +198,36 @@ func (s State) Update(msg tea.Msg) (State, tea.Cmd) {
cmdsToProcess = append(cmdsToProcess, cmd)
case InputBox:
s.InputBox, cmd = s.InputBox.Update(msg)
width := 0
command := strings.Split(s.InputBox.Input.Value(), " ")[0]
if len(command) > 0 && command[0] == '/' {
switch strings.ToUpper(command)[1:] {
case commands.AWAY:
w, err := strconv.ParseInt(s.Client.EnabledFeatures["AWAYLEN"], 10, 32)
if err == nil {
width = int(w)
}
case commands.TOPIC:
w, err := strconv.ParseInt(s.Client.EnabledFeatures["TOPICLEN"], 10, 32)
if err == nil {
width = int(w)
}
case commands.NICK:
w, err := strconv.ParseInt(s.Client.EnabledFeatures["NICKLEN"], 10, 32)
if err == nil {
width = int(w)
}
case commands.KICK:
w, err := strconv.ParseInt(s.Client.EnabledFeatures["KICKLEN"], 10, 32)
if err == nil {
width = int(w)
}
}
}
s.InputBox.Input.CharLimit = width
cmdsToProcess = append(cmdsToProcess, cmd)
}
@@ -204,24 +238,24 @@ func (s State) Update(msg tea.Msg) (State, tea.Cmd) {
}
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)
leftArrowDim = leftArrowDim.Copy().BorderForeground(ui.AccentColor)
rightArrowDim = rightArrowDim.Copy().BorderForeground(ui.AccentColor)
leftArrowLit = leftArrowLit.Copy().BorderForeground(ui.AccentColor)
rightArrowLit = rightArrowLit.Copy().BorderForeground(ui.AccentColor)
tabLine = tabLine.Copy().Foreground(ui.AccentColor)
tab = tab.BorderForeground(ui.AccentColor)
leftArrowDim = leftArrowDim.BorderForeground(ui.AccentColor)
rightArrowDim = rightArrowDim.BorderForeground(ui.AccentColor)
leftArrowLit = leftArrowLit.BorderForeground(ui.AccentColor)
rightArrowLit = rightArrowLit.BorderForeground(ui.AccentColor)
tabLine = tabLine.Foreground(ui.AccentColor)
}
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)
leftArrowDim = leftArrowDim.Copy().BorderForeground(ui.PrimaryColor)
rightArrowDim = rightArrowDim.Copy().BorderForeground(ui.PrimaryColor)
leftArrowLit = leftArrowLit.Copy().BorderForeground(ui.PrimaryColor)
rightArrowLit = rightArrowLit.Copy().BorderForeground(ui.PrimaryColor)
tabLine = tabLine.Copy().Foreground(ui.PrimaryColor)
tab = tab.BorderForeground(ui.PrimaryColor)
leftArrowDim = leftArrowDim.BorderForeground(ui.PrimaryColor)
rightArrowDim = rightArrowDim.BorderForeground(ui.PrimaryColor)
leftArrowLit = leftArrowLit.BorderForeground(ui.PrimaryColor)
rightArrowLit = rightArrowLit.BorderForeground(ui.PrimaryColor)
tabLine = tabLine.Foreground(ui.PrimaryColor)
}
func (s *State) SetSize(width, height int) {
@@ -232,7 +266,7 @@ func (s *State) SetSize(width, height int) {
// and also because we ceil the sidepanel's width
// -3 for the tab bar height
newWidth := int(math.Floor(float64(width) * 8 / 10))
newHeight := height - s.InputBox.Style.GetVerticalFrameSize() - 3
newHeight := height - s.InputBox.Style.GetVerticalFrameSize() - 3 - 1
s.Viewport.Width = newWidth
s.Viewport.Height = newHeight
@@ -241,10 +275,8 @@ func (s *State) SetSize(width, height int) {
s.Viewport.Style = s.Viewport.Style.Height(newHeight)
// we need this to render an empty viewport
history := ""
if len(s.Client.Channels) != 0 {
history = s.Client.Channels[s.Client.ActiveChannelIndex].History
}
// history := ""
history := s.Client.ActiveChannel.Value.History
// we need to re-set the content because words wrap differently on different sizes
s.Viewport.SetContent(history)
@@ -255,51 +287,65 @@ func (s State) buildTabBar(rightArrow string, leftArrow string) string {
var renderedTabs []string
tabs := ""
switch s.TabRenderingDirection {
case Left:
for i := s.Client.LastTabIndexInTabBar; i >= 0; i-- {
if s.Client.Channels[i].Name == s.Client.ActiveChannel {
renderedTabs = append([]string{activeTab.Render(s.Client.Channels[i].Name)}, renderedTabs...)
} else {
renderedTabs = append([]string{tab.Render(s.Client.Channels[i].Name)}, renderedTabs...)
}
tabs = lipgloss.JoinHorizontal(
lipgloss.Top,
renderedTabs...,
)
if lipgloss.Width(tabs) > lipgloss.Width(s.Viewport.View())-lipgloss.Width(leftArrow)-lipgloss.Width(rightArrow) {
// set the first tab to be displayed to the index of the previous tab in the loop
s.Client.FirstTabIndexInTabBar = i + 1
// dont render the newly added tab
renderedTabs = renderedTabs[1:]
break
}
current := s.Client.RootChannel
for {
if current == s.Client.ActiveChannel {
renderedTabs = append(renderedTabs, activeTab.Render(current.Value.Name))
} else {
renderedTabs = append(renderedTabs, tab.Render(current.Value.Name))
}
case Right:
for i := s.Client.FirstTabIndexInTabBar; i < len(s.Client.Channels); i++ {
if s.Client.Channels[i].Name == s.Client.ActiveChannel {
renderedTabs = append(renderedTabs, activeTab.Render(s.Client.Channels[i].Name))
} else {
renderedTabs = append(renderedTabs, tab.Render(s.Client.Channels[i].Name))
}
tabs = lipgloss.JoinHorizontal(
lipgloss.Top,
renderedTabs...,
)
if lipgloss.Width(tabs) > lipgloss.Width(s.Viewport.View())-lipgloss.Width(leftArrow)-lipgloss.Width(rightArrow) {
// set the last tab to be displayed to the index of the previous tab in the loop
s.Client.LastTabIndexInTabBar = i - 1
// dont render the newly added tab
renderedTabs = renderedTabs[:len(renderedTabs)-1]
break
}
current = current.Next
if current == s.Client.RootChannel {
break
}
}
// switch s.TabRenderingDirection {
// case Left:
// for i := s.Client.LastTabIndexInTabBar; i >= 0; i-- {
// if s.Client.Channels[i].Name == s.Client.ActiveChannel.Name {
// renderedTabs = append([]string{activeTab.Render(s.Client.Channels[i].Name)}, renderedTabs...)
// } else {
// renderedTabs = append([]string{tab.Render(s.Client.Channels[i].Name)}, renderedTabs...)
// }
// tabs = lipgloss.JoinHorizontal(
// lipgloss.Top,
// renderedTabs...,
// )
// if lipgloss.Width(tabs) > lipgloss.Width(s.Viewport.View())-lipgloss.Width(leftArrow)-lipgloss.Width(rightArrow) {
// // set the first tab to be displayed to the index of the previous tab in the loop
// s.Client.FirstTabIndexInTabBar = i + 1
// // dont render the newly added tab
// renderedTabs = renderedTabs[1:]
// break
// }
// }
// case Right:
// for i := s.Client.FirstTabIndexInTabBar; i < len(s.Client.Channels); i++ {
// if s.Client.Channels[i].Name == s.Client.ActiveChannel.Name {
// renderedTabs = append(renderedTabs, activeTab.Render(s.Client.Channels[i].Name))
// } else {
// renderedTabs = append(renderedTabs, tab.Render(s.Client.Channels[i].Name))
// }
// tabs = lipgloss.JoinHorizontal(
// lipgloss.Top,
// renderedTabs...,
// )
// if lipgloss.Width(tabs) > lipgloss.Width(s.Viewport.View())-lipgloss.Width(leftArrow)-lipgloss.Width(rightArrow) {
// // set the last tab to be displayed to the index of the previous tab in the loop
// s.Client.LastTabIndexInTabBar = i - 1
// // dont render the newly added tab
// renderedTabs = renderedTabs[:len(renderedTabs)-1]
// break
// }
// }
// }
tabs = lipgloss.JoinHorizontal(
lipgloss.Top,
renderedTabs...,
@@ -315,13 +361,13 @@ func (s State) View() string {
tabs := s.buildTabBar(leftArrow, rightArrow)
tabBar := strings.Builder{}
if s.Client.FirstTabIndexInTabBar != 0 {
leftArrow = leftArrowLit.Render("❰")
}
// if s.Client.FirstTabIndexInTabBar != 0 {
// leftArrow = leftArrowLit.Render("❰")
// }
if s.Client.LastTabIndexInTabBar != len(s.Client.Channels)-1 {
rightArrow = rightArrowLit.Render("❱")
}
// if s.Client.LastTabIndexInTabBar != len(s.Client.Channels)-1 {
// rightArrow = rightArrowLit.Render("❱")
// }
tabBarLine := tabLine.Render(
strings.Repeat(
+38 -11
View File
@@ -19,7 +19,10 @@ package mainscreen
import (
"fmt"
"math"
"sort"
"strings"
"unicode"
"unicode/utf8"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
@@ -36,31 +39,55 @@ type SidePanelState struct {
func (s *SidePanelState) getHeader() string {
usersCount := 0
if len(s.Client.Channels) != 0 {
usersCount = len(s.Client.Channels[s.Client.ActiveChannelIndex].Users)
}
usersCount = len(s.Client.ActiveChannel.Value.Users)
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("%d Users\n", usersCount) + separator
return header
}
// lessCaseInsensitive compares s, t without allocating
func lessCaseInsensitive(s, t string) bool {
for {
if len(t) == 0 {
return false
}
if len(s) == 0 {
return true
}
c, sizec := utf8.DecodeRuneInString(s)
d, sized := utf8.DecodeRuneInString(t)
lowerc := unicode.ToLower(c)
lowerd := unicode.ToLower(d)
if lowerc < lowerd {
return true
}
if lowerc > lowerd {
return false
}
s = s[sizec:]
t = t[sized:]
}
}
func (s *SidePanelState) getLatestNicks() []string {
nicks := make([]string, 0)
if len(s.Client.Channels) != 0 {
for nick, user := range s.Client.Channels[s.Client.ActiveChannelIndex].Users {
_nick := user.Prefix + nick
nicks = append(nicks, _nick)
}
for nick, user := range s.Client.ActiveChannel.Value.Users {
_nick := user.Prefix + nick
nicks = append(nicks, _nick)
}
sort.Slice(nicks, func(i, j int) bool { return lessCaseInsensitive(nicks[i], nicks[j]) })
return nicks
}
func NewSidePanel(client *irc.Client) *SidePanelState {
newViewport := viewport.New(0, 0)
newViewport.Style = SidePanelStyle.Copy()
newViewport.Style = SidePanelStyle
return &SidePanelState{
Client: client,
@@ -73,7 +100,7 @@ func (s SidePanelState) Update(msg tea.Msg) (SidePanelState, tea.Cmd) {
var cmdsToProcess []tea.Cmd
switch msg := msg.(type) {
case cmds.SwitchChannelsMsg:
case cmds.SwitchChannelsMsg, cmds.UpdateNicksMsg:
s.UpdateNicks()
return s, nil
case tea.KeyMsg:
+8 -8
View File
@@ -22,16 +22,16 @@ import (
)
var (
InputboxStyle = ui.DefaultStyle.Copy().
InputboxStyle = ui.DefaultStyle.
Border(lipgloss.NormalBorder(), true).
Padding(0, 1).
Width(ui.MainStyle.GetWidth()).
BorderForeground(ui.AccentColor)
MessagesStyle = ui.DefaultStyle.Copy().
MessagesStyle = ui.DefaultStyle.
Border(lipgloss.NormalBorder(), false, true, true, true).
Width(ui.MainStyle.GetWidth() * 8 / 10).
BorderForeground(ui.PrimaryColor)
SidePanelStyle = ui.DefaultStyle.Copy().
SidePanelStyle = ui.DefaultStyle.
Border(lipgloss.NormalBorder(), true).
Width(ui.MainStyle.GetWidth() * 2 / 10).
BorderForeground(ui.PrimaryColor)
@@ -85,26 +85,26 @@ var (
Foreground(ui.PrimaryColor).
Padding(0, 1)
activeTab = tab.Copy().
activeTab = tab.
Border(activeTabBorder, true).
BorderForeground(ui.AccentColor).
Foreground(ui.AccentColor).
Italic(true).
Bold(true)
leftArrowDim = tab.Copy().
leftArrowDim = tab.
Border(leftArrowBorder, true).
BorderForeground(ui.PrimaryColor).
Foreground(ui.DisabledColor)
rightArrowDim = tab.Copy().
rightArrowDim = tab.
Border(rightArrowBorder, true).
BorderForeground(ui.PrimaryColor).
Foreground(ui.DisabledColor)
leftArrowLit = tab.Copy().
leftArrowLit = tab.
Border(leftArrowBorder, true).
Foreground(ui.PrimaryColor)
rightArrowLit = tab.Copy().
rightArrowLit = tab.
Border(rightArrowBorder, true).
Foreground(ui.PrimaryColor)