Compare commits

Author SHA1 Message Date
Illusion 3ddd238552 WIP: offloading to disk 2023-02-19 00:58:42 +02:00
20 changed files with 822 additions and 1482 deletions
+11
View File
@@ -0,0 +1,11 @@
# progress
it works nicely when there isn't spamming going on.
if spamming:
when pressing g while a message is being received the calculation messes up and the first shown message is actually the 2nd or 3rd
when pressing G while a message is being received a message or two will be missing, im suspecting this happens because of the ToInsert gets written to the db but we already fetched from the db so ToInsert is empty even tho we haven't appened it yet. (same thing happens when going down with d)
-- potential fixes
- save stuff to db message by message (this is slow as shit, but maybe goroutines could do something here ???)
- same way of throttling the message receiving when a key is pressed such as g, G, d or etc..
- maybe there's a way to optimize the readfromdb cmd, maybe the pointer stuff is slowing it down. idk
- last resort is making this whole feature an option for people that know they have slow chats and want to keep the program open for days (idk)
+146 -9
View File
@@ -17,10 +17,12 @@
package cmds
import (
"fmt"
"time"
tea "github.com/charmbracelet/bubbletea"
"github.com/illusionman1212/gorc/irc"
"github.com/illusionman1212/gorc/irc/commands"
)
type ConnectMsg struct{}
@@ -31,7 +33,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")
}
@@ -39,16 +41,28 @@ func Quit(client *irc.Client) tea.Cmd {
}
}
type TickMsg struct{}
func Tick(client *irc.Client) tea.Cmd {
return tea.Tick(time.Second/60, func(time.Time) tea.Msg {
client.SendCommand(commands.PRIVMSG, "#hell", fmt.Sprintf("%d", client.Ticks))
return TickMsg{}
})
}
type SendPrivMsgMsg struct {
Msg string
Datetime time.Time
Timestamp string
}
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,
Datetime: time.Now(),
Timestamp: timestamp,
}
}
}
@@ -65,14 +79,137 @@ func SwitchChannels() tea.Msg {
return SwitchChannelsMsg{}
}
type UpdateNicksMsg struct{}
func UpdateNicks() tea.Msg {
return UpdateNicksMsg{}
}
type UpdateTabBarMsg struct{}
func UpdateTabBar() tea.Msg {
return UpdateTabBarMsg{}
}
type ReadHistoryFromDBMsg struct {
LinesRead uint64
Direction string
}
func ReadHistoryFromDB(client *irc.Client, linesToRead int, direction string) tea.Cmd {
hash := client.Channels[client.ActiveChannelIndex].Hash
scrollBackBufFirstLine := client.Channels[client.ActiveChannelIndex].ScrollBackBufFirstLine
messages := make([]string, 0)
switch direction {
case "top":
client.DB.
Table("histories").
Select("message").
Where("hash = ?", hash).
Limit(linesToRead).
Scan(&messages).
Order("line ASC")
// set the new messages
copy(client.Channels[client.ActiveChannelIndex].ScrollBackBuf, messages)
// adjust the scrollback buf first line
client.Channels[client.ActiveChannelIndex].ScrollBackBufFirstLine = 1
return func() tea.Msg {
return ReadHistoryFromDBMsg{
LinesRead: 0,
Direction: "top",
}
}
case "up":
limit := linesToRead
linesToGet := 0
if linesToRead > int(scrollBackBufFirstLine) {
limit = int(scrollBackBufFirstLine) - 1
linesToGet = int(scrollBackBufFirstLine) - limit
} else {
linesToGet = int(scrollBackBufFirstLine) - linesToRead
}
client.DB.
Table("histories").
Select("message").
Where("hash = ? AND line >= ?", hash, linesToGet).
Limit(limit).
Scan(&messages).
Order("line ASC")
if len(messages) != 0 {
// prepend to the scroll back buf
client.Channels[client.ActiveChannelIndex].ScrollBackBuf = append(messages, client.Channels[client.ActiveChannelIndex].ScrollBackBuf...)
client.Channels[client.ActiveChannelIndex].ScrollBackBuf = client.Channels[client.ActiveChannelIndex].ScrollBackBuf[:irc.SCROLLBACK_MAX]
// adjust the scrollback buf first line
client.Channels[client.ActiveChannelIndex].ScrollBackBufFirstLine -= uint64(limit)
return func() tea.Msg {
return ReadHistoryFromDBMsg{
LinesRead: uint64(limit),
Direction: "up",
}
}
}
case "down":
client.DB.
Table("histories").
Select("message").
Where("hash = ? AND line >= ?", hash, client.Channels[client.ActiveChannelIndex].ScrollBackBufFirstLine+irc.SCROLLBACK_MAX).
Limit(linesToRead).
Scan(&messages).
Order("line ASC")
linesGot := len(messages)
if len(messages) != 0 {
extraMessagesLen := 0
// append to the scroll back buf
client.Channels[client.ActiveChannelIndex].ScrollBackBuf = append(client.Channels[client.ActiveChannelIndex].ScrollBackBuf, messages...)
// if we reached the end of the messages in the db. try appending the messages in the ToInsert buffer
if linesGot < linesToRead {
for _, history := range client.Channels[client.ActiveChannelIndex].ToInsert {
client.Channels[client.ActiveChannelIndex].ScrollBackBuf = append(client.Channels[client.ActiveChannelIndex].ScrollBackBuf, history.Message)
}
extraMessagesLen = len(client.Channels[client.ActiveChannelIndex].ToInsert)
}
client.Channels[client.ActiveChannelIndex].ScrollBackBuf = client.Channels[client.ActiveChannelIndex].ScrollBackBuf[len(client.Channels[client.ActiveChannelIndex].ScrollBackBuf)-irc.SCROLLBACK_MAX:]
// adjust the scrollback buf first line
client.Channels[client.ActiveChannelIndex].ScrollBackBufFirstLine += uint64(linesGot) + uint64(extraMessagesLen)
return func() tea.Msg {
return ReadHistoryFromDBMsg{
LinesRead: uint64(linesGot) + uint64(extraMessagesLen),
Direction: "down",
}
}
}
case "bottom":
client.DB.Raw("SELECT t.message FROM (SELECT line, message FROM histories WHERE hash = ? ORDER BY line DESC LIMIT ?) t ORDER BY t.line ASC;", hash, linesToRead).
Scan(&messages)
toInsert := make([]string, 0)
for _, history := range client.Channels[client.ActiveChannelIndex].ToInsert {
toInsert = append(toInsert, history.Message)
}
// set the new messages
copy(client.Channels[client.ActiveChannelIndex].ScrollBackBuf, messages)
client.Channels[client.ActiveChannelIndex].ScrollBackBuf = append(client.Channels[client.ActiveChannelIndex].ScrollBackBuf, toInsert...)
client.Channels[client.ActiveChannelIndex].ScrollBackBuf = client.Channels[client.ActiveChannelIndex].ScrollBackBuf[len(toInsert):]
// adjust the scrollback buf first line
client.Channels[client.ActiveChannelIndex].ScrollBackBufFirstLine = client.Channels[client.ActiveChannelIndex].TotalLines - irc.SCROLLBACK_MAX + uint64(len(toInsert))
return func() tea.Msg {
return ReadHistoryFromDBMsg{
LinesRead: 0,
Direction: "bottom",
}
}
}
return func() tea.Msg {
return ReadHistoryFromDBMsg{
LinesRead: 0,
Direction: "unknown",
}
}
}
+25
View File
@@ -0,0 +1,25 @@
package db
import (
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
type History struct {
Hash string `gorm:"index"`
Line uint64 `gorm:"index" gorm:"unique"`
Message string
}
func ConnectToDB() (*gorm.DB, error) {
db, err := gorm.Open(sqlite.Open("gorc.db"), &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
AllowGlobalUpdate: true,
})
db.AutoMigrate(&History{})
db.Delete(&History{})
return db, err
}
+13 -27
View File
@@ -1,33 +1,19 @@
module github.com/illusionman1212/gorc
go 1.23.0
go 1.16
toolchain go1.24.6
replace github.com/charmbracelet/bubbles => /home/illusion/Desktop/repos/bubbles
require (
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/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
github.com/charmbracelet/bubbles v0.10.2
github.com/charmbracelet/bubbletea v0.19.3
github.com/charmbracelet/lipgloss v0.4.0
github.com/containerd/console v1.0.3 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mattn/go-sqlite3 v1.14.13 // indirect
github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 // indirect
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 // indirect
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
gorm.io/driver/sqlite v1.3.4
gorm.io/gorm v1.23.6 // indirect
)
+61 -44
View File
@@ -1,49 +1,66 @@
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/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/charmbracelet/bubbletea v0.19.3 h1:OKeO/Y13rQQqt4snX+lePB0QrnW80UdrMNolnCcmoAw=
github.com/charmbracelet/bubbletea v0.19.3/go.mod h1:VuXF2pToRxDUHcBUcPmCRUHRvFATM4Ckb/ql1rBl3KA=
github.com/charmbracelet/harmonica v0.1.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
github.com/charmbracelet/lipgloss v0.4.0 h1:768h64EFkGUr8V5yAKV7/Ta0NiVceiPaV+PphaW1K9g=
github.com/charmbracelet/lipgloss v0.4.0/go.mod h1:vmdkHvce7UzX6xkyf4cca8WlwdQ5RQr8fzta+xl7BOM=
github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ=
github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-isatty v0.0.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.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/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.12 h1:TJ1bhYJPV44phC+IMu1u2K/i5RriLTPe+yc68XDJ1Z0=
github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.13 h1:1tj15ngiFfcZzii7yd82foL+ks+ouQcj8j/TPq3fk1I=
github.com/mattn/go-sqlite3 v1.14.13/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 h1:kMlmsLSbjkikxQJ1IPwaM+7LJ9ltFu/fi8CRzvSnQmA=
github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/termenv v0.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/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
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.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=
github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/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-20220114195835-da31bd327af9 h1:XfKQ4OlFl8okEOr5UvAqFRVj8pY/4yfcXrddB8qAbU0=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20210422114643-f5beecf764ed/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/sqlite v1.3.4 h1:NnFOPVfzi4CPsJPH4wXr6rMkPb4ElHEqKMvrsx9c9Fk=
gorm.io/driver/sqlite v1.3.4/go.mod h1:B+8GyC9K7VgzJAcrcXMRPdnMcck+8FgJynEehEPM16U=
gorm.io/gorm v1.23.4 h1:1BKWM67O6CflSLcwGQR7ccfmC4ebOxQrTfOQGRE9wjg=
gorm.io/gorm v1.23.4/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
gorm.io/gorm v1.23.6 h1:KFLdNgri4ExFFGTRGGFWON2P1ZN28+9SJRN8voOoYe0=
gorm.io/gorm v1.23.6/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
+105 -112
View File
@@ -26,31 +26,17 @@ import (
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/illusionman1212/gorc/db"
"github.com/illusionman1212/gorc/irc/commands"
"github.com/illusionman1212/gorc/ui"
"gorm.io/gorm"
)
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
@@ -58,11 +44,26 @@ type Channel struct {
// Channel topic
Topic string
// Channel messages history
History string
// Scrollback buffer for the channel message history
ScrollBackBuf []string
// Date of the last message we received in this channel
LastUpdatedDate time.Time
// Whethere the viewport is at the bottom or not
AtBottom bool
// Total amount of lines that have been received from the tcp conn
// and written to the history db
TotalLines uint64
// Buffer of messages that need to be inserted into the db
ToInsert []db.History
// Position/Index of first line in the scrollback buffer as mapped
// to TotalLines.
// Last line is ScrollBackBufFirstLine + SCROLLBACK_MAX - 1
ScrollBackBufFirstLine uint64
// Unique sha1 hash for the channel made from server hostname and channel name
Hash string
// Users in this channel
// The map key is the user's nickname
@@ -73,7 +74,7 @@ type Channel struct {
type Client struct {
// tcp connection
TCPConn net.Conn
TcpConn net.Conn
// Host that client is connected to
Host string
@@ -84,75 +85,58 @@ type Client struct {
// Nickname currently in use by the user
Nickname string
// The root of the doubly linked list of channel
RootChannel *Node[Channel]
// Joined channels
Channels []Channel
// Active channel
ActiveChannel *Node[Channel]
// Active channel name
ActiveChannel string
// The channel to join immediately after registration completes.
InitialChannel string
// Active channel index
ActiveChannelIndex int
// Reference to the bubbletea program
Tea *tea.Program
// TODO: remove this
Ticks int
// DB reference
DB *gorm.DB
// 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
// The acknowledged capabilities
EnabledCapabilities Capabilities
// The features currently enabled for this client
EnabledFeatures Features
LastTabIndexInTabBar int
}
type Capabilities map[string]string
type Features map[string]string
type MsgFmtOpts struct {
WithTimestamp bool
// TODO: make these a bitset? they should compliment eachother instead of conflicting or rendering twice.
AsServerMsg bool
AsErrorMsg bool
NotImpl bool
AsDate bool
}
var unimpl = lipgloss.NewStyle().Foreground(ui.ErrorColor).Render("[UNIMPL]")
var notImpl = lipgloss.NewStyle().Foreground(ui.ErrorColor).Render("[NOT IMPL]")
var serverMsgStyle = lipgloss.NewStyle().Foreground(ui.ServerMsgColor)
var errorMsgStyle = lipgloss.NewStyle().Foreground(ui.ErrorColor)
var timestampStyle = serverMsgStyle
var timestampStyle = serverMsgStyle.Copy()
var dateStyle = lipgloss.NewStyle().Foreground(ui.DateColor)
const CRLF = "\r\n"
const (
CRLF = "\r\n"
SCROLLBACK_MAX = 400
)
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) {
func (c *Channel) AppendMsg(host string, database *gorm.DB, timestamp string, fullMsg string, opts MsgFmtOpts) {
prefixes := ""
style := ui.DefaultStyle
if opts.WithTimestamp {
str := fmt.Sprintf("[%02d:%02d]", datetime.Hour(), datetime.Minute())
prefixes += timestampStyle.Render(str) + " "
prefixes += timestampStyle.Render(timestamp) + " "
}
if opts.NotImpl {
prefixes += unimpl + " "
prefixes += notImpl + " "
}
if opts.AsServerMsg {
@@ -160,28 +144,41 @@ func (c *Channel) AppendMsg(datetime time.Time, fullMsg string, opts MsgFmtOpts)
style = serverMsgStyle
}
if opts.AsErrorMsg {
prefixes += errorMsgStyle.Render("==") + " "
style = errorMsgStyle
if opts.AsDate {
style = dateStyle
}
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
finalMsg := prefixes + style.Render(fullMsg)
c.TotalLines++
history := db.History{Hash: c.Hash, Line: c.TotalLines, Message: finalMsg}
c.ToInsert = append(c.ToInsert, history)
if c.TotalLines%10 == 0 {
database.Create(c.ToInsert)
c.ToInsert = nil
}
c.History += prefixes + style.Render(fullMsg) + CRLF
if len(c.ScrollBackBuf) < SCROLLBACK_MAX {
c.ScrollBackBuf = append(c.ScrollBackBuf, prefixes+style.Render(fullMsg))
// We haven't reached the bottom yet so the first line is still 1
c.ScrollBackBufFirstLine = 1
} else if c.AtBottom && len(c.ScrollBackBuf) >= SCROLLBACK_MAX {
// TODO: when pressing 'g' in viewport, we need to set this to 1
// TODO: when pressing 'G' in viewport, we need to set this to TotalLines - 1000 + 1
// TODO: when going up in viewport, we need to decrement the amount of lines by how many lines we need to prepend to the scrollbackbuf
// TODO: when going down in viewport, we need to increment the amount of lines by how many lines we need to append to the scrollbackbuf
// If we're at bottom and we're appending a line, then we increment the first line
c.ScrollBackBufFirstLine++
c.ScrollBackBuf = append(c.ScrollBackBuf, prefixes+style.Render(fullMsg))
c.ScrollBackBuf = c.ScrollBackBuf[1:]
}
}
func (c *Client) Initialize(host string, port string, tlsEnabled bool) {
func (s *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)
@@ -190,7 +187,12 @@ func (c *Client) Initialize(host string, port string, tlsEnabled bool) {
log.Fatal(err)
}
c.TCPConn = conn
s.TcpConn = conn
s.Host = host
s.Port = port
s.ActiveChannel = host
s.Channels = make([]Channel, 0)
s.Ticks = 1
return
}
@@ -204,60 +206,51 @@ func (c *Client) Initialize(host string, port string, tlsEnabled bool) {
log.Fatal(err)
}
c.TCPConn = conn
s.TcpConn = conn
s.Host = host
s.Port = port
s.ActiveChannel = host
s.Channels = make([]Channel, 0)
s.Ticks = 1
}
func (c *Client) Register(nick string, password string, channel string) {
hostChannel := Channel{
c.Channels = append(c.Channels, 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")
c.SendCommand(commands.CAP, "LS")
if password != "" {
c.SendCommand(commands.PASS, password)
}
// TODO: check if nickname has spaces and remove them
c.SendCommand(commands.NICK, nick)
// set user-wanted nickname
c.Nickname = 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
if channel != "" {
c.SendCommand(commands.JOIN, channel)
}
}
func (c *Client) AppendChannel(channel Channel) *Node[Channel] {
first := c.RootChannel
last := c.RootChannel.Prev
newNode := &Node[Channel]{
Value: channel,
Next: first,
Prev: last,
func (c *Client) SetDay() {
msgOpts := MsgFmtOpts{
AsDate: true,
}
last.Next = newNode
first.Prev = newNode
now := time.Now()
msg := fmt.Sprintf("————— %s %d —————", now.Month().String(), now.Day())
c.Channels[0].AppendMsg(c.Host, c.DB, "", msg, msgOpts)
return newNode
// TODO: append new day to each channel whenever the day changes
}
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 {
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")
}
@@ -288,5 +281,5 @@ func (c *Client) SendCommand(cmd string, params ...string) {
}
}
c.TCPConn.Write([]byte(cmd + paramsString + CRLF))
c.TcpConn.Write([]byte(cmd + paramsString + CRLF))
}
+73 -112
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..
@@ -116,7 +116,6 @@ const (
RPL_MYINFO = "004" // RFC2812 - Implemented
RPL_ISUPPORT = "005" // ??????? - 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_TRACECONNECTING = "201" // RFC1459 - Not Implemented (TODO:)
RPL_TRACEHANDSHAKE = "202" // RFC1459 - Not Implemented (TODO:)
@@ -167,12 +166,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 - Implemented
RPL_AWAY = "301" // RFC1459 - Not Implemented (TODO:)
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 - Implemented
RPL_NOWAWAY = "306" // RFC1459 - Implemented
RPL_UNAWAY = "305" // RFC1459 - Not Implemented (TODO:)
RPL_NOWAWAY = "306" // RFC1459 - Not Implemented (TODO:)
RPL_WHOISUSER = "311" // RFC1459 - Implemented
RPL_WHOISSERVER = "312" // RFC1459 - Implemented
RPL_WHOISOPERATOR = "313" // RFC1459 - Not Implemented (TODO:)
@@ -187,8 +186,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 - Implemented
RPL_TOPIC = "332" // RFC1459 - Implemented
RPL_NOTOPIC = "331" // RFC1459 - Not Implemented (TODO:)
RPL_TOPIC = "332" // RFC1459 - Not Implemented (TODO:)
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 +196,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 - Implemented
RPL_VERSION = "351" // RFC1459 - Not Implemented (TODO:)
RPL_WHOREPLY = "352" // RFC1459 - Not Implemented (TODO:)
RPL_NAMREPLY = "353" // RFC1459 - Implemented
RPL_KILLDONE = "361" // RFC1459 - Not Implemented - Deprecated (TODO:)
@@ -209,10 +208,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 - Implemented
RPL_INFO = "371" // RFC1459 - Not Implemented (TODO:)
RPL_MOTD = "372" // RFC1459 - Implemented
RPL_INFOSTART = "373" // RFC1459 - Not Implemented - Deprecated (TODO:)
RPL_ENDOFINFO = "374" // RFC1459 - Implemented
RPL_ENDOFINFO = "374" // RFC1459 - Not Implemented (TODO:)
RPL_MOTDSTART = "375" // RFC1459 - Implemented
RPL_ENDOFMOTD = "376" // RFC1459 - Not Implemented (TODO:)
RPL_WHOISHOST = "378" // ??????? - Implemented - Has Conflicts
@@ -228,11 +227,11 @@ const (
RPL_NOUSERS = "395" // RFC1459 - Not Implemented (TODO:)
ERR_UNKNOWNERROR = "400" // ??????? - Not Implemented (TODO:)
ERR_NOSUCHNICK = "401" // RFC1459 - Implemented
ERR_NOSUCHNICK = "401" // RFC1459 - Not Implemented (TODO:)
ERR_NOSUCHSERVER = "402" // RFC1459 - Implemented
ERR_NOSUCHCHANNEL = "403" // RFC1459 - Implemented
ERR_CANNOTSENDTOCHAN = "404" // RFC1459 - Implemented
ERR_TOOMANYCHANNELS = "405" // RFC1459 - Implemented
ERR_NOSUCHCHANNEL = "403" // RFC1459 - Not Implemented (TODO:)
ERR_CANNOTSENDTOCHAN = "404" // RFC1459 - Not Implemented (TODO:)
ERR_TOOMANYCHANNELS = "405" // RFC1459 - Not Implemented (TODO:)
ERR_WASNOSUCHNICK = "406" // RFC1459 - Not Implemented (TODO:)
ERR_NOSUCHSERVICE = "408" // RFC2812 - Not Implemented - Has Conflicts (TODO:)
ERR_NOORIGIN = "409" // RFC1459 - Not Implemented (TODO:)
@@ -241,8 +240,8 @@ const (
ERR_NOTOPLEVEL = "413" // RFC1459 - Not Implemented (TODO:)
ERR_WILDTOPLEVEL = "414" // RFC1459 - Not Implemented (TODO:)
ERR_BADMASK = "415" // RFC2812 - Not Implemented (TODO:)
ERR_UNKNOWNCOMMAND = "421" // RFC1459 - Implemented
ERR_NOMOTD = "422" // RFC1459 - Implemented
ERR_UNKNOWNCOMMAND = "421" // RFC1459 - Not Implemented (TODO:)
ERR_NOMOTD = "422" // RFC1459 - Not Implemented (TODO:)
ERR_NOADMININFO = "423" // RFC1459 - Not Implemented (TODO:)
ERR_FILEERROR = "424" // RFC1459 - Not Implemented (TODO:)
ERR_NONICKNAMEGIVEN = "431" // RFC1459 - Implemented
@@ -269,10 +268,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 - Implemented
ERR_BADCHANMASK = "476" // RFC2812 - Not Implemented (TODO:)
ERR_BANLISTFULL = "478" // RFC2812 - Not Implemented (TODO:)
ERR_NOPRIVILEGES = "481" // RFC1459 - Not Implemented (TODO:)
ERR_CHANOPRIVSNEEDED = "482" // RFC1459 - Implemented
ERR_CHANOPRIVSNEEDED = "482" // RFC1459 - Not Implemented (TODO:)
ERR_CANTKILLSERVER = "483" // RFC1459 - Not Implemented (TODO:)
ERR_RESTRICTED = "484" // RFC2812 - Not Implemented - Has Conflicts (TODO:)
ERR_UNIQOPRIVSNEEDED = "485" // RFC2812 - Not Implemented (TODO:)
@@ -305,105 +304,67 @@ const (
/* --- RPL_ISUPPORT Parameters --- */
var Features = map[string]bool{
"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
"ACCEPT": true,
"AWAYLEN": 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,
"BOT": true,
"CALLERID": true,
"CASEMAPPING": true,
"CHANLIMIT": true,
"CHANMODES": true,
"CHANNELLEN": true,
"CHANTYPES": true,
"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,
"KICKLEN": true,
"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
"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
"NETWORK": true,
"NICKLEN": 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,
"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,
"TOPICLEN": 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,
"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,
}
var Capabilities = map[string]bool{
// Twitch-specific capabilities
"twitch.tv/membership": false,
"twitch.tv/commands": false,
"twitch.tv/tags": false,
// Messages MUST end in CRLF and SHOULD be encoded and decoded using UTF-8 (with fallbacks such as Latin-1).
// Names are casemapped, read the casemapping from the RPL_ISUPPORT the server sends when registration is completed.
// 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,
}
// When receiving a numeric reply, client MUST be able to handle any number of parameters on a numeric reply.
+119 -735
View File
File diff suppressed because it is too large Load Diff
+6 -80
View File
@@ -17,9 +17,8 @@
package handler
import (
"fmt"
"log"
"strings"
"time"
tea "github.com/charmbracelet/bubbletea"
"github.com/illusionman1212/gorc/cmds"
@@ -27,81 +26,6 @@ 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 = strings.ToLower(params[0])
}
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...)
return cmds.SwitchChannels
}
func HandleSlashCommand(msg string, client *irc.Client) tea.Cmd {
substrs := strings.Fields(msg[1:])
command := strings.ToUpper(substrs[0])
@@ -111,13 +35,15 @@ 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)
client.SendCommand(commands.JOIN, params...)
return cmds.SwitchChannels
case commands.QUIT:
client.SendCommand(command, params...)
return cmds.Quit(client)
case "TEST":
log.Println("running the spammer")
return cmds.Tick(client)
default:
client.SendCommand(command, params...)
return nil
+26 -54
View File
@@ -17,41 +17,30 @@
package parser
import (
"fmt"
"regexp"
"strings"
"github.com/illusionman1212/gorc/irc"
"time"
)
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)
}
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)
}
func parseTags(rawTags string) irc.MessageTags {
tags := make(irc.MessageTags, 0)
type Tags map[string]string
func (m *IRCMessage) setTimestamp() {
// TODO: get timestamp from time tag
now := time.Now()
m.Timestamp = fmt.Sprintf("[%02d:%02d:%02d.%03d]", now.Hour(), now.Minute(), now.Second(), now.Nanosecond()/1000000)
}
func parseTags(rawTags string) Tags {
tags := make(Tags, 0)
rawTagsSlice := strings.Split(rawTags, ";")
for _, tag := range rawTagsSlice {
@@ -62,24 +51,7 @@ func parseTags(rawTags string) irc.MessageTags {
// we need to make sure a "value" exists before accessing str[1]
if len(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++
}
value = str[1]
}
tags[key] = value
@@ -88,16 +60,16 @@ func parseTags(rawTags string) irc.MessageTags {
return tags
}
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]+$`)
func ParseIRCMessage(line string) (IRCMessage, bool) {
ircMessage := IRCMessage{}
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 irc.Message{}, false
return IRCMessage{}, false
}
if line[0] == '@' {
@@ -144,7 +116,7 @@ func ParseIRCMessage(line string) (irc.Message, bool) {
ircMessage.Parameters = finalParams
}
ircMessage.SetTimestamp()
ircMessage.setTimestamp()
return ircMessage, true
}
+6
View File
@@ -341,6 +341,7 @@ func TestParser(t *testing.T) {
}
})
// TODO: fails
t.Run("Test escaped tags", func(t *testing.T) {
testMessage := "@a=b\\\\and\\nk;c=72\\s45;d=gh\\:764 foo"
ircMessage, valid := ParseIRCMessage(testMessage)
@@ -400,6 +401,7 @@ func TestParser(t *testing.T) {
}
})
// TODO: FAIL
t.Run("Test single param with preceeding :", func(t *testing.T) {
testMessage := ":src JOIN :#chan"
ircMessage, valid := ParseIRCMessage(testMessage)
@@ -650,6 +652,7 @@ func TestParser(t *testing.T) {
}
})
// TODO: FAIL
t.Run("Test yaml encoding with slashes", func(t *testing.T) {
testMessage := "@foo=\\\\\\\\\\:\\\\s\\s\\r\\n COMMAND"
ircMessage, valid := ParseIRCMessage(testMessage)
@@ -746,6 +749,7 @@ func TestParser(t *testing.T) {
}
})
// TODO: FAIL
t.Run("Test tags. they should be parsed char-at-a-time", func(t *testing.T) {
testMessage := "@tag1=value\\\\ntest COMMAND"
ircMessage, valid := ParseIRCMessage(testMessage)
@@ -767,6 +771,7 @@ func TestParser(t *testing.T) {
}
})
// TODO: FAIl
t.Run("Test tag escape for char that doesn't need it", func(t *testing.T) {
testMessage := "@tag1=value\\1 COMMAND"
ircMessage, valid := ParseIRCMessage(testMessage)
@@ -784,6 +789,7 @@ func TestParser(t *testing.T) {
}
})
// TODO: FAIL
t.Run("Test slash at end of tag", func(t *testing.T) {
testMessage := "@tag1=value1\\ COMMAND"
ircMessage, valid := ParseIRCMessage(testMessage)
+12 -4
View File
@@ -20,6 +20,7 @@ import (
"log"
tea "github.com/charmbracelet/bubbletea"
"github.com/illusionman1212/gorc/db"
"github.com/illusionman1212/gorc/ui/app"
)
@@ -32,15 +33,22 @@ func main() {
tea.WithMouseCellMotion(),
)
gorc.Client.Tea = p
f, err := tea.LogToFile("gorc.log", "gorc")
db, err := db.ConnectToDB()
if err != nil {
log.Fatal(err)
}
gorc.Client.Tea = p
gorc.Client.DB = db
f, err := tea.LogToFile("gorc.log", "gorc")
defer f.Close()
_, err = p.Run()
if err != nil {
log.Fatal(err)
}
err = p.Start()
if err != nil {
log.Fatal(err)
+4 -15
View File
@@ -42,12 +42,10 @@ type UI struct {
type State struct {
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),
@@ -59,7 +57,7 @@ func InitialState() *State {
return &State{
Client: client,
UI: initialUIState(client),
UI: initialUiState(client),
}
}
@@ -80,15 +78,8 @@ func (s State) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
Width(msg.Width).
Height(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:
@@ -100,12 +91,10 @@ 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.UI.MainScreen.SetSize(s.TerminalWidth, s.TerminalHeight)
s.Client.SetDay()
go handler.ReadLoop(s.Client)
@@ -136,5 +125,5 @@ func (s State) View() string {
return s.UI.MainScreen.View()
}
panic("Invalid screen")
return "Error: this screen shouldn't ever show"
}
+23 -29
View File
@@ -17,6 +17,7 @@
package login
import (
"fmt"
"strings"
"github.com/charmbracelet/bubbles/textinput"
@@ -43,15 +44,15 @@ func NewLogin() State {
Inputs: make([]textinput.Model, 5),
ConnectButtonBlurredStyle: BlurredDisabledButton,
ConnectButtonFocusedStyle: FocusedDisabledButton,
DialogStyle: DialogStyle,
WelcomeMsgStyle: WelcomeMsgStyle,
Style: ui.MainStyle,
DialogStyle: DialogStyle.Copy(),
WelcomeMsgStyle: WelcomeMsgStyle.Copy(),
Style: ui.MainStyle.Copy(),
}
var t textinput.Model
for i := range state.Inputs {
t = textinput.New()
t.Cursor.Style = CursorStyle
t.CursorStyle = CursorStyle
t.Prompt = ""
switch i {
@@ -59,27 +60,19 @@ 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.CharLimit = 32
t.EchoMode = textinput.EchoPassword
t.EchoCharacter = '•'
}
@@ -148,7 +141,6 @@ 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() != "" {
@@ -165,43 +157,45 @@ func (s *State) updateInputs(msg tea.Msg) tea.Cmd {
}
func (s *State) SetSize(width, height int) {
s.DialogStyle = s.DialogStyle.Width(width - s.DialogStyle.GetHorizontalFrameSize())
s.DialogStyle = s.DialogStyle.Height(height*5/10 - s.DialogStyle.GetVerticalFrameSize())
s.DialogStyle = s.DialogStyle.Copy().Width(width - s.DialogStyle.GetHorizontalFrameSize())
s.DialogStyle = s.DialogStyle.Copy().Height(height*5/10 - s.DialogStyle.GetVerticalFrameSize())
s.WelcomeMsgStyle = s.WelcomeMsgStyle.Width(width)
s.WelcomeMsgStyle = s.WelcomeMsgStyle.Height(height * 5 / 10)
s.WelcomeMsgStyle = s.WelcomeMsgStyle.Copy().Width(width)
s.WelcomeMsgStyle = s.WelcomeMsgStyle.Copy().Height(height * 5 / 10)
}
func (s State) View() string {
var sb strings.Builder
for i, input := range s.Inputs {
sb.WriteString(input.View())
if i < len(s.Inputs) {
for i := range s.Inputs {
sb.WriteString(s.Inputs[i].View())
if i < len(s.Inputs)-1 {
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
}
button := s.ConnectButtonBlurredStyle
fmt.Fprintf(&sb, "\n%s\n", *checkbox)
button := &s.ConnectButtonBlurredStyle
if s.FocusIndex == len(s.Inputs)+1 {
button = s.ConnectButtonFocusedStyle
button = &s.ConnectButtonFocusedStyle
}
sb.WriteString(lipgloss.JoinVertical(lipgloss.Center, checkbox, button))
fmt.Fprintf(&sb, "\n%s\n", *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().
AlignHorizontal(lipgloss.Center)
Align(lipgloss.Center)
WelcomeMsgStyle = lipgloss.NewStyle().
AlignHorizontal(lipgloss.Center).
Align(lipgloss.Center).
Foreground(ui.PrimaryColor)
CursorStyle = lipgloss.NewStyle().
-44
View File
@@ -1,44 +0,0 @@
// 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{
Input: input,
Style: InputboxStyle,
Style: InputboxStyle.Copy(),
}
}
@@ -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.BorderForeground(ui.AccentColor)
s.Style = s.Style.Copy().BorderForeground(ui.AccentColor)
}
func (s *InputState) Blur() {
s.Input.Blur()
s.Style = s.Style.BorderForeground(ui.PrimaryColor)
s.Style = s.Style.Copy().BorderForeground(ui.PrimaryColor)
}
func (s *InputState) SetSize(width int) {
+139 -138
View File
@@ -18,7 +18,6 @@ package mainscreen
import (
"math"
"strconv"
"strings"
"github.com/charmbracelet/bubbles/textinput"
@@ -50,7 +49,7 @@ type State struct {
Client *irc.Client
Viewport *viewport.Model
FocusIndex Window
// TabRenderingDirection TabDirection
TabRenderingDirection TabDirection
InputBox InputState
SidePanel *SidePanelState
@@ -58,7 +57,9 @@ type State struct {
func NewMainScreen(client *irc.Client) State {
newViewport := viewport.New(0, 0)
newViewport.Style = MessagesStyle
newViewport.Wrap = viewport.Wrap
newViewport.Style = MessagesStyle.Copy()
newViewport.MouseWheelEnabled = false
return State{
Client: client,
@@ -66,7 +67,7 @@ func NewMainScreen(client *irc.Client) State {
FocusIndex: InputBox,
InputBox: NewInputBox(),
SidePanel: NewSidePanel(client),
// TabRenderingDirection: Right,
TabRenderingDirection: Right,
}
}
@@ -77,9 +78,25 @@ 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.ActiveChannel.Value.History)
s.Viewport.SetContent(strings.Join(s.Client.Channels[s.Client.ActiveChannelIndex].ScrollBackBuf, "\n"))
if wasAtBottom {
s.Client.Channels[s.Client.ActiveChannelIndex].AtBottom = true
s.Viewport.GotoBottom()
} else {
s.Client.Channels[s.Client.ActiveChannelIndex].AtBottom = false
}
return s, nil
case cmds.ReadHistoryFromDBMsg:
s.Viewport.SetContent(strings.Join(s.Client.Channels[s.Client.ActiveChannelIndex].ScrollBackBuf, "\n"))
switch msg.Direction {
case "top":
s.Viewport.GotoTop()
case "up":
s.Viewport.SetYOffset(s.Viewport.YOffset + int(msg.LinesRead))
case "down":
s.Viewport.SetYOffset(s.Viewport.YOffset - int(msg.LinesRead))
case "bottom":
s.Viewport.GotoBottom()
}
return s, nil
@@ -88,32 +105,34 @@ 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.Value.Name != s.Client.Host {
if s.Client.ActiveChannel != s.Client.Host {
fullMsg := s.Client.Nickname + ": " + msg.Msg
msgOpts := irc.MsgFmtOpts{
WithTimestamp: true,
}
s.Client.ActiveChannel.Value.AppendMsg(msg.Datetime, fullMsg, msgOpts)
s.Client.Channels[s.Client.ActiveChannelIndex].AppendMsg(s.Client.Host, s.Client.DB, msg.Timestamp, 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.Value.Name, msg.Msg)
s.Viewport.SetContent(s.Client.ActiveChannel.Value.History)
s.Client.SendCommand(commands.PRIVMSG, s.Client.ActiveChannel, msg.Msg)
s.Viewport.SetContent(strings.Join(s.Client.Channels[s.Client.ActiveChannelIndex].ScrollBackBuf, "\n"))
s.Viewport.GotoBottom()
}
}
return s, nil
case cmds.SwitchChannelsMsg:
s.Viewport.SetContent(s.Client.ActiveChannel.Value.History)
// log.Println(s)
s.Viewport.SetContent(strings.Join(s.Client.Channels[s.Client.ActiveChannelIndex].ScrollBackBuf, "\n"))
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 cmds.TickMsg:
s.Client.Ticks++
return s, cmds.Tick(s.Client)
case tea.KeyMsg:
key := msg.String()
switch key {
@@ -153,41 +172,65 @@ func (s State) Update(msg tea.Msg) (State, tea.Cmd) {
}
// we need this so that the viewport doesnt scroll to the bottom
if s.Client.RootChannel.Next == s.Client.RootChannel {
if len(s.Client.Channels) == 1 {
return s, nil
}
if key == "right" {
// s.Client.ActiveChannelIndex++
s.Client.ActiveChannel = s.Client.ActiveChannel.Next
s.Client.ActiveChannelIndex++
} else {
s.Client.ActiveChannel = s.Client.ActiveChannel.Prev
// s.Client.ActiveChannelIndex--
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 {
s.Viewport.GotoTop()
s.Client.Channels[s.Client.ActiveChannelIndex].AtBottom = false
if !s.Viewport.AtTop() {
cmdsToProcess = append(cmdsToProcess, cmds.ReadHistoryFromDB(s.Client, irc.SCROLLBACK_MAX, "top"))
}
}
case "G":
if s.FocusIndex == Viewport {
s.Viewport.GotoBottom()
if !s.Viewport.AtBottom() {
cmdsToProcess = append(cmdsToProcess, cmds.ReadHistoryFromDB(s.Client, irc.SCROLLBACK_MAX, "bottom"))
}
s.Client.Channels[s.Client.ActiveChannelIndex].AtBottom = true
}
case "j", "k", "d", "u", "f", "b", "up", "down":
// TODO: what???
// if s.Viewport.AtBottom() {
// break
// }
if key == "j" || key == "d" || key == "f" || key == "down" {
if s.Viewport.ScrollPercent() > 0.5 {
excess := s.Viewport.ScrollPercent() - 0.5 + 0.1
linesToRead := int(math.Floor(excess * float64(irc.SCROLLBACK_MAX)))
cmdsToProcess = append(cmdsToProcess, cmds.ReadHistoryFromDB(s.Client, linesToRead, "down"))
}
} else {
s.Client.Channels[s.Client.ActiveChannelIndex].AtBottom = false
if s.Viewport.ScrollPercent() < 0.5 && s.Viewport.ScrollPercent() > 0.0 {
excess := 0.5 - s.Viewport.ScrollPercent() + 0.1
linesToRead := int(math.Floor(excess * float64(irc.SCROLLBACK_MAX)))
cmdsToProcess = append(cmdsToProcess, cmds.ReadHistoryFromDB(s.Client, linesToRead, "up"))
}
}
}
}
@@ -198,36 +241,6 @@ 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)
}
@@ -238,35 +251,35 @@ func (s State) Update(msg tea.Msg) (State, tea.Cmd) {
}
func (s *State) Focus() {
s.Viewport.Style = s.Viewport.Style.BorderForeground(ui.AccentColor)
s.Viewport.Style = s.Viewport.Style.Copy().BorderForeground(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)
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)
}
func (s *State) Blur() {
s.Viewport.Style = s.Viewport.Style.BorderForeground(ui.PrimaryColor)
s.Viewport.Style = s.Viewport.Style.Copy().BorderForeground(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)
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)
}
func (s *State) SetSize(width, height int) {
s.InputBox.SetSize(width)
s.SidePanel.SetSize(width, height, s.InputBox.Style.GetVerticalPadding())
s.SidePanel.SetSize(width, height, s.InputBox.Style.GetVerticalFrameSize())
// We floor because width is an int and some fractions are lost when casting
// 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 - 1
newWidth := int(math.Floor(float64(width)*8/10) - float64(s.Viewport.Style.GetHorizontalFrameSize()))
newHeight := height - s.InputBox.Style.GetVerticalFrameSize() - s.Viewport.Style.GetVerticalFrameSize() - 3
s.Viewport.Width = newWidth
s.Viewport.Height = newHeight
@@ -275,8 +288,10 @@ func (s *State) SetSize(width, height int) {
s.Viewport.Style = s.Viewport.Style.Height(newHeight)
// we need this to render an empty viewport
// history := ""
history := s.Client.ActiveChannel.Value.History
history := ""
if len(s.Client.Channels) != 0 {
history = strings.Join(s.Client.Channels[s.Client.ActiveChannelIndex].ScrollBackBuf, "\n")
}
// we need to re-set the content because words wrap differently on different sizes
s.Viewport.SetContent(history)
@@ -287,64 +302,50 @@ func (s State) buildTabBar(rightArrow string, leftArrow string) string {
var renderedTabs []string
tabs := ""
current := s.Client.RootChannel
for {
if current == s.Client.ActiveChannel {
renderedTabs = append(renderedTabs, activeTab.Render(current.Value.Name))
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(renderedTabs, tab.Render(current.Value.Name))
renderedTabs = append([]string{tab.Render(s.Client.Channels[i].Name)}, renderedTabs...)
}
current = current.Next
if current == s.Client.RootChannel {
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 {
renderedTabs = append(renderedTabs, activeTab.Render(s.Client.Channels[i].Name))
} else {
renderedTabs = append(renderedTabs, tab.Render(s.Client.Channels[i].Name))
}
// 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...,
)
// 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
// }
// }
// }
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,
@@ -361,13 +362,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(
+12 -38
View File
@@ -19,10 +19,7 @@ package mainscreen
import (
"fmt"
"math"
"sort"
"strings"
"unicode"
"unicode/utf8"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
@@ -39,55 +36,32 @@ type SidePanelState struct {
func (s *SidePanelState) getHeader() string {
usersCount := 0
usersCount = len(s.Client.ActiveChannel.Value.Users)
separator := strings.Repeat("—", s.Viewport.Width-s.Viewport.Style.GetHorizontalFrameSize()) + "\n"
header := fmt.Sprintf("%d Users\n", usersCount) + separator
if len(s.Client.Channels) != 0 {
usersCount = len(s.Client.Channels[s.Client.ActiveChannelIndex].Users)
}
separator := strings.Repeat("—", s.Viewport.Width) + "\n"
header := fmt.Sprintf("Users in this channel (%d)\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)
for nick, user := range s.Client.ActiveChannel.Value.Users {
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)
}
}
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
newViewport.Style = SidePanelStyle.Copy()
newViewport.Wrap = viewport.Wrap
return &SidePanelState{
Client: client,
@@ -100,7 +74,7 @@ func (s SidePanelState) Update(msg tea.Msg) (SidePanelState, tea.Cmd) {
var cmdsToProcess []tea.Cmd
switch msg := msg.(type) {
case cmds.SwitchChannelsMsg, cmds.UpdateNicksMsg:
case cmds.SwitchChannelsMsg:
s.UpdateNicks()
return s, nil
case tea.KeyMsg:
@@ -145,7 +119,7 @@ func (s *SidePanelState) UpdateNicks() {
func (s *SidePanelState) SetSize(width, height, inputboxHeight int) {
// We ceil here because width is an int and some fractions are lost
// -1 is for some extra invisible margin or padding between the sidepanel and the inputbox
newWidth := int(math.Ceil((float64(width) * 2 / 10)))
newWidth := int(math.Ceil(math.Ceil((float64(width) * 2 / 10)) - float64(s.Viewport.Style.GetHorizontalFrameSize())))
newHeight := height - inputboxHeight - s.Viewport.Style.GetVerticalFrameSize() - 1
s.Viewport.Style = s.Viewport.Style.Width(newWidth)
+8 -8
View File
@@ -22,16 +22,16 @@ import (
)
var (
InputboxStyle = ui.DefaultStyle.
InputboxStyle = ui.DefaultStyle.Copy().
Border(lipgloss.NormalBorder(), true).
Padding(0, 1).
Width(ui.MainStyle.GetWidth()).
BorderForeground(ui.AccentColor)
MessagesStyle = ui.DefaultStyle.
MessagesStyle = ui.DefaultStyle.Copy().
Border(lipgloss.NormalBorder(), false, true, true, true).
Width(ui.MainStyle.GetWidth() * 8 / 10).
BorderForeground(ui.PrimaryColor)
SidePanelStyle = ui.DefaultStyle.
SidePanelStyle = ui.DefaultStyle.Copy().
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.
activeTab = tab.Copy().
Border(activeTabBorder, true).
BorderForeground(ui.AccentColor).
Foreground(ui.AccentColor).
Italic(true).
Bold(true)
leftArrowDim = tab.
leftArrowDim = tab.Copy().
Border(leftArrowBorder, true).
BorderForeground(ui.PrimaryColor).
Foreground(ui.DisabledColor)
rightArrowDim = tab.
rightArrowDim = tab.Copy().
Border(rightArrowBorder, true).
BorderForeground(ui.PrimaryColor).
Foreground(ui.DisabledColor)
leftArrowLit = tab.
leftArrowLit = tab.Copy().
Border(leftArrowBorder, true).
Foreground(ui.PrimaryColor)
rightArrowLit = tab.
rightArrowLit = tab.Copy().
Border(rightArrowBorder, true).
Foreground(ui.PrimaryColor)